New OGo Help Desk Feature

Enhancement Bug#2027 "Allow help desk users to create tasks on behalf of users" has been resolved as of r2274. This update requires a database schema change -


UPDATE job SET owner_id = creator_id;

The database scripts pg-build-schema.psql and pg-update-1.x-to-5.5.psql have been updated. NOTE: Be careful not to run this part of pg-update-1.x-to-5.5.psql more than once or you risk modifying actual data in your database if you use the OGoHelpDeskRoleName default.

This feature allows a member of the team whose named is defined in the OGoHelpDeskRoleName default to create tasks with an owner other than themselves. Delegated and archived task lists now display tasks based on owner rather than creator. By default the owner is the creator so this has no effect on normal task behaviour. Currently the help desk feature, setting of the owner to another user, is only available via the zOGI API. The modifications to the zOGI API are documented on the Task entity.

If your help desk team name contains spaces be sure to use proper quoting - Defaults write NSGlobalDomain OGoHelpDeskRoleName '"all intranet"' - or you may not get the results you expect. Obviously if you set OGoHelpDeskRoleName to "all intranet" all users will be able to create tasks on behalf of other users.


Consonance 0.0.9p

I've uploaded Consonance 0.0.9pre to the downloads site on Google Code. And announced it on Freshmeat. This version uses the shiny new LINQ backend. Contact and Enterprise operations, including the Contact CRUD window, should be stable. That was the focus of 0.0.9. Hopefully I can get 0.0.10 out the door by this weekend: the goal for 0.0.10 is to re-enable the main task lists and stabilize the Task window. I've already got the task lists loading and as an extra re-enabled the busy-pulser on the main window. Patches welcome!


Consonance Progress, at last.

While on vacation I had to time to address some unresolved issues left over from the last revamp of the OpenGroupware .NET client assembly and Gtk# application [Consonance]. For the client searching now works again - that was a stupid bug where the callback was never set. Also fixed up numerous bugs in the Gtk# client so Contacts can be updated including company values, addresses, and telephone numbers. After five years of off-and-on hacking I think Whitemice.ZOGI.Backend is now stable and seems to perform very well.
  • Operations from the work queue now time out, so if a callback is lost the work queue doesn't hang.
  • Responses to background priority requests are delayed until there is no higher priority work; previously responses to RPC calls where all processed at the same priority regardless of the priority of the original request.
  • ContactDetailForm is now an IStorableEntityForm.


Service BASIC HTTP Authentication with Python

While building the core parts of OpenGroupware COILS I noticed there don't appear to be any example of providing HTTP's BASIC authentication scheme anywhere on the interweb. Or any other authentication schemes for that matter. To remedy that here is a rough outline, including the example code, of how it is implemented in COILS. First spin up an HTTP server:

import BaseHTTPServer

class HTTPServer(BaseHTTPServer.HTTPServer):

from coils.net.handler import HTTPRequestHandler
from coils.net.server import HTTPServer
HTTP_HOST = 'localhost'
HTTP_PORT = 8080
httpd = HTTPServer((HTTP_HOST, HTTP_PORT), HTTPRequestHandler)

Your app will almost certainly provide a custom HTTPRequestHandler to delegate the requests to whatever logic your application provides. In the BaseHTTPRequestHandler a GET request is handed to do_GET, a POST request to do_POST, etc... In our case the object that handles the request will decide what to do based on the request type so we channel all requests to our generic process_request method. process_request looks up the object targeted by the request via marshall_handler() [not shown] and then calls that objects do_request method. The important part for authentication is to catch the exception raised by the object handling the request if that object thinks the request is not authenticated and requires authentication. In this case that is the COILS' AuthenticationException; your application has to provide something equivalent. In order to make the client try again with authentication you need to send a "WWW-Authenticate" header telling the client to use basic authentication and what realm to use. See RFC2617 for details on Basic authentication in general

class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):

def process_request(self):
"""Respond to a request"""
""" find the object mapped to the specified request
The marshall_handler() is a COILS specific things so it isn't
shown in this example. """
handler = self.marshall_handler()
except AuthenticationException, err:
""" An AuthenticationException has an error code of 401
We need to add an authentication header so the client will know
to respond the the failure with the appropriate credentials
TODO: Provide a digest realm once digest authentication is supported
self.send_header('WWW-Authenticate', 'Basic realm="OpenGroupware COILS"')
self.wfile.write('Authentication failure')
except CoilsException, err:
# An Coils Exception has an error code of 500
self.send_response(err.error_code(), err.error_text())
except Exception, err:
# Yikes, something generic web very wrong
self.send_response(500, err)

def do_GET(self):
"""Respond to a GET request."""

def do_POST(self):
"""Respond to a POST request"""

In order to actually process the authentication request COILS uses an Authenticator object. Our DBAuthenticator object provides basic authentication against accounts with passwords stored directly in the database [verses accounts from LDAP or trusting an external authorization mechanism such as Kerberos]. Explanation for the steps to process a Basic authentication operation have been added to the code example:

from base64 import b64decode
from crypt import crypt
class DBAuthenticator(Authenticator):

def _authenticate(self, context, request):
Authenticator._authenticate(self, context, request)
authorization = request.headers.get('authorization')
if (authorization == None):
raise AuthenticationException('Authentication Required')
(kind, data) = authorization.split(' ')
if (kind == 'Basic'):
# Authentication method is "Basic"
(username, _, password) = b64decode(data).partition(':')
""" This method provided by the parent class goes to the ORM and
retrieves the account object for the specified username. It
will throw an authentication exception if no such username is
found (user entered it wrong?) or a generic Coils Exception if
multiple objects match the username (that doesn't make sense) -
either will stop the authentication process, but the authentication
exception should reprompt the client to try again. """
account = Authenticator._getLogin(self, username)
secret = account.password
if (secret == crypt(password, secret[:2])):
# Password matches, user is authenticated
self.loginId = account.objectId
self.login = account.login
# Password does not match, authentication failes
raise AuthenticationException('Incorrect username or password')
# Authorization header indicated an authentication type other than BASIC
CoilsException('Unsupported HTTP Authenticated Mech')

See, that easy easy.


GRLUG OpenGroupware Presentation

Last night I presented on OpenGroupware to the Grand Rapids LINUX User's Group.

Presentation File

Multiple Interfaces On Windows

A great post over on Ivan Zlatev's BLOG on how to get a working configuration of multiple network interfaces on a Microsoft Windows workstation. There probably isn't anyone who has been forced to use that runt of an OS and not been frustrated by how it handles network connections.

Database Changes to OpenGroupware v5.5

For those building from trunk, or currently pulling packages from OBS, there is a new table required as of r2256.

INSERT INTO ctags (entity) VALUES ('Person');
INSERT INTO ctags (entity) VALUES ('Enterprise');
INSERT INTO ctags (entity) VALUES ('Date');
INSERT INTO ctags (entity) VALUES ('Job');
INSERT INTO ctags (entity) VALUES ('Team');

This is part of adding ctag support to OGo (specifically ZideStore). ctags allow a client to *very* quickly detect if the contents of a collection have changed; such as the /public/Contacts folder. Thus avoiding doing a PROPFIND on the entire folder to determine if a re-sync is needing. ctag support is not complete or working yet, but without this table a server post-r2256 will fails some operations with a database error.

The subversion repository contains an update script for migrating a v5.4 database to v5.5.


Announcing OpenGroupware COILS

I've create a new project on the shiny new SourceForge: OpenGroupware COILS. This is "a re-implementation of the groupware functionality provided by OpenGroupware.org. COILS is parallel installable and is entirely compatible with the Objective-C version 5.5 of OpenGroupware.org". COILS is developed in Python for reasons explained on the Wiki. If you'd like to participate in the development of COILS please join the COILS mail list. I also hang out on the OGo IRC channel on FreeNode. And no, this does not mean I am going to stop working on the Objective-C OGo. But I believe there is a need for a refreshing of the OGo platform (BTW, there is also OGo/J) and there are some ideas I want to explore that really can't be done [easily] in the existing OGo/ObjC codebase.


Python XML-RPC Presentation

Presented on using XML-RPC with Python to the Grand Rapids Python Users Group (GRPUG).

Presentation File

While it is admittedly a runt of an RPC solution I've found XML-RPC to be very useful and have used it extensively in both my work on OpenGroupware and other projects.


Logging *USEFUL* Web Access To PostgreSQL

It is pretty easy to configure syslog to log system messages to a PostgreSQL database; and that sure beats flat files. With some GRANT/REVOKE goodness this setup can be made quite secure - only allowing the message import process to insert messages and denying the ability to everyone else to modify the contents of the database. But all that data is still pretty ugly to deal with as the messages are just long strings; so some method is needed to break down that "data" into "information". I had the specific need to do this with SQUID weblogs. I already had syslog logging to a PostgreSQL database so the first step was just to have SQUID use syslog to log access. That is easily accomplished like:

cache_access_log syslog:LOG_LOCAL4

After a restart SQUID messages show up in the LOCAL4 facility (table: facility_local4) with the priority of "info". Then I created a table to hold the parsed messages:

CREATE TABLE proxy_audit (
timestamp TIMESTAMP,
elapsed INT,
source VARCHAR(40),
status VARCHAR(15),
size INT,
method VARCHAR(15),
url VARCHAR(128),
domain VARCHAR(60),
identity VARCHAR(25),
mimetype VARCHAR(45));

Last a trigger on the LOCAL4 log parses incoming messages into this table:

IF (new.hostname = ''hod-a'' AND new.priority = ''info'') THEN
INSERT INTO proxy_audit (timestamp, elapsed, source, status, size,
method, url, domain, identity, mimetype)
VALUES(''epoch''::TIMESTAMP + (split_part(split_part(new.message, '' '', 1), ''.'', 1)::INT) * ''1 second''::INTERVAL,
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 1)::INT,
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 2),
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 3)::VARCHAR(15),
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 4)::INT,
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 5)::VARCHAR(15),
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 6)::VARCHAR(128),
split_part(split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 6), ''/'', 3)::VARCHAR(60),
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 7)::VARCHAR(25),
split_part(TRIM(substring(new.message, strpos(new.message, '' ''))), '' '', 9));
' LANGUAGE plpgsql;
ON facility_local4 FOR EACH ROW
EXECUTE PROCEDURE p_proxy_audit()

Once you get allot of data in your table, which happens quickly, you'll probably discover you need an index.

CREATE INDEX proxy_audit_i1 ON proxy_audit(identity, timestamp);

Now looking at a user's web usage is straight-forward. Finally.


OBS Packages Updated

OpenGroupware packages on the OBS have been updated to r2207. This revision provides improved GroupDAV version 1 compatibility and adds GroupDAV version 2 support. The upside of those improvements is that the new ZideOne Outlook plugin should work well with this revision (or later) of OpenGroupware. Numerous enhancements and bug fixes in Logic and zOGI are also included.

NOTE: The GroupDAV version 2 specification has not been published yet.



OpenGroupware revision 2181 closes Bug#394. It is now possible to set a default to determine the default storage backend for new projects. (Wow, with GNUstep based stuff one ends up using the term "default" so often in different ways....)

To select database as your default project storage backend:
Defaults write NSGlobalDomain OGoDefaultProjectStorageBackend Database

To select the filesystem as your default project storage backend:
Defaults write NSGlobalDomain OGoDefaultProjectStorageBackend FileSystem

Most people I assume will want "Database". Remember that default values are CASE SENSITIVE!

If you (at least on r2181) attempt to create a project without selecting FileSystem/Database you'll get a popup warning: "Please specify your project storage!" If the default is set then the preferred storage backend is preselected (and can be changed).

Aside: If you use FileSystem projects make sure you have the SkyFSPath default set and that the referred to directory has the correct permissions.


Packages Complete

OpenGroupware packages have been completely built on the build service for CentOS5, Fedora 9, openSUSE 10.3, RHEL 5, and SLES10. The Fedora 10 and openSUSE 11.x packages needs some additional work.

Repositories at
Initial testing on CentOS 5 seems pretty positive, after a:
$ yum install ogo-meta ogo-database-setup

Only glaring bug is that ogo-database-setup doesn't actually populate the database schema so that still needs to be done by hand:

$ cat pg-build-schema.psql | psql -h localhost -U OGo OGo

Then restarting the services gives you [as far as I can tell so far] a fully working OGo install.

P.S. Make sure your getting the most current packages from the repository. mod_ngobjweb in particular should be dated March 4th; with the previous version the web resources (icons, etc...) for the WebUI won't be found at the configured path.


Setting the TimeZone in VMware's VIMA Appliance

After importing the VIMA appliance the time zone is Pacific time. When going to change the time zone you'll discover that they include neither redhat-config-date or timeconfig. So one has to go about it old-school:

[vi-admin@vima ~]$ date
Tue Feb 24 12:37:01 PST 2009
[vi-admin@vima ~]$ sudo rm /etc/localtime
[vi-admin@vima ~]$ sudo ln -s /usr/share/zoneinfo/America/Detroit /etc/localtime
[vi-admin@vima ~]$ date
Tue Feb 24 15:40:59 EST 2009


"What Carriers Aren’t Eager to Tell You About Texting"

Excellent New York times article about cell carriers and the true cost of text messenging. As usual with telecom: the customer is getting robbed.

Almost there

The ogo-meta package now almost installs on CentOS5 except that we don't have the ogo-environment or the ngobjweb packages. OpenSUSE 11.0 and 11.1 have some problems building anything beyond ogo-gnustep_make; OpenSUSE 10.3 builds up to the same point as CentOS5.

$ yum install ogo-meta
---> Package ogo-meta.i386 0:1.1-6.7 set to be updated
--> Processing Dependency: mod_ngobjweb for package: ogo-meta
--> Processing Dependency: ogo-environment for package: ogo-meta
---> Package postgresql.i386 0:8.1.11-1.el5_1.1 set to be updated
---> Package postgresql-libs.i386 0:8.1.11-1.el5_1.1 set to be updated
--> Finished Dependency Resolution
Error: Missing Dependency: ogo-environment is needed by package ogo-meta
Error: Missing Dependency: mod_ngobjweb is needed by package ogo-meta


From 72 to 56

A default minimal install of CentOS5 leaves you with a fairly busy box. If you just login on the console you'll find about 72 running processes. This includes what I consider to be some pretty oddball stuff like "rpcgssd" and "rpcidmapd". How many sites actually make use of these services? A simple run through of:

chkconfig irqbalance off
chkconfig isdn off
chkconfig portmap off
chkconfig rpcgssd off
chkconfig rpcidmapd off
chkconfig sendmail off
chkconfig yum-updatesd off
chkconfig cups off
chkconfig gpm off
chkconfig mdmonitor off
chkconfig nfslock off
chkconfig smartd off
chkconfig autofs off
chkconfig bluetooth off
chkconfig acpid off
chkconfig pcscd off

- leaves one with 56 processes after boot and allot less open ports. Some of these make sense (like acpid) but I'm running in a VM so I don't want those either. Would be nice if the default state of CentOS5 was a little cleaner.


OpenGroupware Package Repositories

Currently working on getting OpenGroupware (and related GNUStep-make, SOPE, & ngobjweb) packages built of the Novell Build Service. Repositories at:

Not everything working yet, but getting there.