Moved files into their new locations prior to commit of 0.2.
authorMichael Spang <mspang@uwaterloo.ca>
Sat, 27 Jan 2007 23:48:26 +0000 (18:48 -0500)
committerMichael Spang <mspang@uwaterloo.ca>
Sat, 27 Jan 2007 23:48:26 +0000 (18:48 -0500)
The changes in most files between 0.1 and 0.2 are so large that git is unable
to automatically detect the renames that took place. This commit moves files
into their new locations so that the history is accurate.

36 files changed:
debian/rules
etc/accounts.cf [new file with mode: 0644]
etc/csc/accounts.cf [deleted file]
etc/csc/members.cf [deleted file]
etc/members.cf [new file with mode: 0644]
py/csc/__init__.py [deleted file]
py/csc/admin/__init__.py [deleted file]
py/csc/admin/accounts.py [deleted file]
py/csc/admin/members.py [deleted file]
py/csc/admin/terms.py [deleted file]
py/csc/apps/__init__.py [deleted file]
py/csc/apps/legacy/__init__.py [deleted file]
py/csc/apps/legacy/helpers.py [deleted file]
py/csc/apps/legacy/main.py [deleted file]
py/csc/backend/__init__.py [deleted file]
py/csc/backend/db.py [deleted file]
py/csc/backend/ipc.py [deleted file]
py/csc/backend/krb.py [deleted file]
py/csc/backend/ldapi.py [deleted file]
py/csc/lib/__init__.py [deleted file]
pylib/csc/__init__.py [new file with mode: 0644]
pylib/csc/adm/__init__.py [new file with mode: 0644]
pylib/csc/adm/accounts.py [new file with mode: 0644]
pylib/csc/adm/members.py [new file with mode: 0644]
pylib/csc/adm/terms.py [new file with mode: 0644]
pylib/csc/apps/__init__.py [new file with mode: 0644]
pylib/csc/apps/legacy/__init__.py [new file with mode: 0644]
pylib/csc/apps/legacy/helpers.py [new file with mode: 0644]
pylib/csc/apps/legacy/main.py [new file with mode: 0644]
pylib/csc/backends/__init__.py [new file with mode: 0644]
pylib/csc/backends/db.py [new file with mode: 0644]
pylib/csc/backends/ipc.py [new file with mode: 0644]
pylib/csc/backends/krb.py [new file with mode: 0644]
pylib/csc/backends/ldapi.py [new file with mode: 0644]
pylib/csc/common/__init__.py [new file with mode: 0644]
pylib/csc/common/conf.py [new file with mode: 0644]

index b69af8d..2009af7 100755 (executable)
@@ -17,7 +17,7 @@ clean:
        dh_clean
        rm -f build-stamp
        rm -rf build/
-       find py/ -name '*.pyc' -print0 | xargs -0 rm -f
+       find pylib/ -name '*.pyc' -print0 | xargs -0 rm -f
 
 install: build
        dh_testdir
@@ -25,12 +25,12 @@ install: build
        dh_clean -k 
 
        # configuration files will contain sensitive information
-       chmod 600 etc/csc/*
+       chmod 600 etc/*
        
-       dh_installdirs etc usr/lib/$(PYTHON)/site-packages usr/share/csc \
+       dh_installdirs etc/csc usr/lib/$(PYTHON)/site-packages usr/share/csc \
                       usr/lib/csc usr/bin
-       dh_install -X.svn -X.pyc py/csc usr/lib/$(PYTHON)/site-packages/
-       dh_install -X.svn -X.pyc etc/csc etc/
+       dh_install -X.svn -X.pyc pylib/csc usr/lib/$(PYTHON)/site-packages/
+       dh_install -X.svn -X.pyc etc/* etc/csc/
        dh_install -X.svn -X.pyc sql/* usr/share/csc/
        
        dh_install -X.svn -X.pyc bin/ceo usr/lib/csc/
diff --git a/etc/accounts.cf b/etc/accounts.cf
new file mode 100644 (file)
index 0000000..cfe64ed
--- /dev/null
@@ -0,0 +1,35 @@
+# $Id: accounts.cf 45 2007-01-02 01:39:10Z mspang $
+# CSC Accounts Configuration
+
+### Account Options ###
+
+minimum_id = 20000
+maximum_id = 40000
+
+shell = "/bin/bash"
+home = "/users"
+gid = 100
+
+
+### LDAP Configuration ###
+
+server_url = "ldap:///"
+
+users_base  = "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
+groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca"
+
+bind_dn       = "cn=ceo,dc=csclub,dc=uwaterloo,dc=ca"
+bind_password = "secret"
+
+
+### Kerberos Configuration ###
+
+realm = "CSCLUB.UWATERLOO.CA"
+principal = "ceo/admin@CSCLUB.UWATERLOO.CA"
+keytab = "/etc/csc/ceo.keytab"
+
+
+### Validation Tuning ###
+
+username_regex = "^[a-z][-a-z0-9]*$"
+realname_regex = "^[^,:=]*$"
diff --git a/etc/csc/accounts.cf b/etc/csc/accounts.cf
deleted file mode 100644 (file)
index cfe64ed..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-# $Id: accounts.cf 45 2007-01-02 01:39:10Z mspang $
-# CSC Accounts Configuration
-
-### Account Options ###
-
-minimum_id = 20000
-maximum_id = 40000
-
-shell = "/bin/bash"
-home = "/users"
-gid = 100
-
-
-### LDAP Configuration ###
-
-server_url = "ldap:///"
-
-users_base  = "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
-groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca"
-
-bind_dn       = "cn=ceo,dc=csclub,dc=uwaterloo,dc=ca"
-bind_password = "secret"
-
-
-### Kerberos Configuration ###
-
-realm = "CSCLUB.UWATERLOO.CA"
-principal = "ceo/admin@CSCLUB.UWATERLOO.CA"
-keytab = "/etc/csc/ceo.keytab"
-
-
-### Validation Tuning ###
-
-username_regex = "^[a-z][-a-z0-9]*$"
-realname_regex = "^[^,:=]*$"
diff --git a/etc/csc/members.cf b/etc/csc/members.cf
deleted file mode 100644 (file)
index e984294..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-# $Id: members.cf 45 2007-01-02 01:39:10Z mspang $
-# CSC Members Configuration
-
-### Database Configuration ###
-
-server = "localhost"
-database = "ceo"
-
-user = "ceo"
-password = "secret"
-
-### Validation Tuning ###
-
-studentid_regex = "^[0-9]{8}$"
-realname_regex = "^[^,:=]*$"
diff --git a/etc/members.cf b/etc/members.cf
new file mode 100644 (file)
index 0000000..e984294
--- /dev/null
@@ -0,0 +1,15 @@
+# $Id: members.cf 45 2007-01-02 01:39:10Z mspang $
+# CSC Members Configuration
+
+### Database Configuration ###
+
+server = "localhost"
+database = "ceo"
+
+user = "ceo"
+password = "secret"
+
+### Validation Tuning ###
+
+studentid_regex = "^[0-9]{8}$"
+realname_regex = "^[^,:=]*$"
diff --git a/py/csc/__init__.py b/py/csc/__init__.py
deleted file mode 100644 (file)
index 0583735..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-# $Id: __init__.py 24 2006-12-18 20:23:12Z mspang $
-"""
-PyCSC - CSC Administrative Utilities
-
-Member Management:
-
-    ceo - legacy ceo interface
-
-Account Management:
-
-    ceo - legacy ceo interface
-
-Modules:
-
-    admin - administrative code (member and account management)
-    backend - backend interface code
-    ui - user interface code
-
-"""
diff --git a/py/csc/admin/__init__.py b/py/csc/admin/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/py/csc/admin/accounts.py b/py/csc/admin/accounts.py
deleted file mode 100644 (file)
index d9975f3..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-# $Id: accounts.py 44 2006-12-31 07:09:27Z mspang $
-# UNIX Accounts Module
-import re
-from csc.backend import ldapi, krb
-from csc.lib import read_config
-
-CONFIG_FILE = '/etc/csc/accounts.cf'
-
-cfg = {}
-
-# error constants
-SUCCESS = 0
-LDAP_EXISTS = 1
-LDAP_NO_IDS = 2
-LDAP_NO_USER = 3
-KRB_EXISTS = 5
-KRB_NO_USER = 6
-BAD_USERNAME = 8
-BAD_REALNAME = 9
-
-# error messages
-errors = [ "Success", "LDAP: entry exists",
-    "LDAP: no user ids available", "LDAP: no such entry",
-    "KRB: principal exists", "KRB: no such principal",
-    "Invalid username", "Invalid real name"]
-
-
-class AccountException(Exception):
-    """Exception class for account-related errors."""
-
-
-def load_configuration():
-    """Load Accounts Configuration."""
-    
-    # configuration already loaded?
-    if len(cfg) > 0:
-        return
-    
-    # read in the file
-    cfg_tmp = read_config(CONFIG_FILE)
-
-    if not cfg_tmp:
-        raise AccountException("unable to read configuration file: %s" % CONFIG_FILE)
-
-    # check that essential fields are completed
-    mandatory_fields = [ 'minimum_id', 'maximum_id', 'shell', 'home',
-        'gid', 'server_url', 'users_base', 'groups_base', 'bind_dn',
-        'bind_password', 'realm', 'principal', 'keytab', 'username_regex',
-        'realname_regex'
-    ]
-
-    for field in mandatory_fields:
-        if not field in cfg_tmp:
-            raise AccountException("missing configuration option: %s" % field)
-        if not cfg_tmp[field]:
-            raise AccountException("null configuration option: %s" % field)
-    
-    # check that numeric fields are ints
-    numeric_fields = [ 'minimum_id', 'maximum_id', 'gid' ]
-
-    for field in numeric_fields:
-        if not type(cfg_tmp[field]) in (int, long):
-            raise AccountException("non-numeric value for configuration option: %s" % field)
-
-    # update the current configuration with the loaded values
-    cfg.update(cfg_tmp)
-        
-
-def create_account(username, password, realname='', gecos_other=''):
-    """
-    Creates a UNIX account for a member. This involves
-    first creating a directory entry, then creating
-    a Kerberos principal.
-
-    Parameters:
-        username - UNIX username for the member
-        realname - real name of the member
-        password - password for the account
-
-    Exceptions:
-        LDAPException - on LDAP failure
-        KrbException  - on Kerberos failure
-        
-    Returns:
-        SUCCESS      - on success
-        BAD_REALNAME - on badly formed real name
-        BAD_USERNAME - on badly formed user name
-        LDAP_EXISTS  - when the user exists in LDAP
-        LDAP_NO_IDS  - when no user ids are free
-        KRB_EXISTS   - when the user exists in Kerberos
-    """
-
-    # Load Configuration
-    load_configuration()
-
-    ### Connect to the Backends ###
-
-    ldap_connection = ldapi.LDAPConnection()
-    krb_connection = krb.KrbConnection()
-
-    try:
-
-        # connect to the LDAP server
-        ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base'])
-
-        # connect to the Kerberos master server
-        krb_connection.connect(cfg['principal'], cfg['keytab'])
-
-        ### Sanity-checks ###
-   
-        # check the username and realame for validity
-        if not re.match(cfg['username_regex'], username):
-            return BAD_USERNAME
-        if not re.match(cfg['realname_regex'], realname):
-            return BAD_REALNAME
-
-        # see if user exists in LDAP
-        if ldap_connection.user_lookup(username):
-            return LDAP_EXISTS
-
-        # determine the first available userid
-        userid = ldap_connection.first_id(cfg['minimum_id'], cfg['maximum_id'])
-        if not userid: return LDAP_NO_IDS
-
-        # build principal name from username
-        principal = username + '@' + cfg['realm']
-    
-        # see if user exists in Kerberos
-        if krb_connection.get_principal(principal):
-            return KRB_EXISTS
-    
-        ### User creation ###
-
-        # process gecos_other (used to store memberid)
-        if gecos_other:
-            gecos_other = ',' + str(gecos_other)
-    
-        # account information defaults
-        shell = cfg['shell']
-        home = cfg['home'] + '/' + username
-        gecos = realname + ',,,' + gecos_other
-        gid = cfg['gid']
-    
-        # create the LDAP entry
-        ldap_connection.user_add(username, realname, shell, userid, gid, home, gecos)
-    
-        # create the Kerberos principal
-        krb_connection.add_principal(principal, password)
-
-    finally:
-        ldap_connection.disconnect()
-        krb_connection.disconnect()
-    
-    return SUCCESS
-    
-
-def delete_account(username):
-    """
-    Deletes the UNIX account of a member.
-    
-    Parameters:
-        username - UNIX username for the member
-
-    Exceptions:
-        LDAPException - on LDAP failure
-        KrbException  - on Kerberos failure
-        
-    Returns:
-        SUCCESS      - on success
-        LDAP_NO_USER - when the user does not exist in LDAP
-        KRB_NO_USER  - when the user does not exist in Kerberos
-    """
-
-    # Load Configuration
-    load_configuration()
-
-    ### Connect to the Backends ###
-
-    ldap_connection = ldapi.LDAPConnection()
-    krb_connection = krb.KrbConnection()
-
-    try:
-    
-        # connect to the LDAP server
-        ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base'])
-
-        # connect to the Kerberos master server
-        krb_connection.connect(cfg['principal'], cfg['keytab'])
-
-        ### Sanity-checks ###
-    
-        # ensure user exists in LDAP
-        if not ldap_connection.user_lookup(username):
-            return LDAP_NO_USER
-    
-        # build principal name from username
-        principal = username + '@' + cfg['realm']
-
-        # see if user exists in Kerberos
-        if not krb_connection.get_principal(principal):
-            return KRB_NO_USER
-
-        ### User deletion ###
-    
-        # delete the LDAP entry
-        ldap_connection.user_delete(username)
-    
-        # delete the Kerberos principal
-        krb_connection.delete_principal(principal)
-
-    finally:
-        ldap_connection.disconnect()
-        krb_connection.disconnect()
-    
-    return SUCCESS
-
-
-
-### Tests ###
-
-if __name__ == '__main__':
-
-    # A word of notice: this test creates a _working_ account (and then deletes it).
-    # If deletion fails it must be cleaned up manually.
-    
-    # a bit of salt so the test account is reasonably tough to crack
-    import random
-    pw = str(random.randint(100000000000000000, 999999999999999999))
-    
-    print "running create_account('testuser', ..., 'Test User', ...)", "->", errors[create_account('testuser', pw, 'Test User')]
-    print "running delete_account('testuser')", "->", errors[delete_account('testuser')]
-
diff --git a/py/csc/admin/members.py b/py/csc/admin/members.py
deleted file mode 100644 (file)
index 326a7de..0000000
+++ /dev/null
@@ -1,426 +0,0 @@
-# $Id: members.py 44 2006-12-31 07:09:27Z mspang $
-"""
-CSC Member Management
-
-This module contains functions for registering new members, registering
-members for terms, searching for members, and other member-related
-functions.
-
-Transactions are used in each method that modifies the database. 
-Future changes to the members database that need to be atomic
-must also be moved into this module.
-"""
-
-import re
-from csc.admin import terms
-from csc.backend import db
-from csc.lib import read_config
-
-
-
-
-### Configuration
-
-CONFIG_FILE = '/etc/csc/members.cf'
-
-cfg = {}
-
-
-def load_configuration():
-    """Load Members Configuration"""
-
-    # configuration already loaded?
-    if len(cfg) > 0:
-        return
-
-    # read in the file
-    cfg_tmp = read_config(CONFIG_FILE)
-
-    if not cfg_tmp:
-        raise MemberException("unable to read configuration file: %s"
-                % CONFIG_FILE)
-
-    # check that essential fields are completed
-    mandatory_fields = [ 'server', 'database', 'user', 'password' ]
-
-    for field in mandatory_fields:
-        if not field in cfg_tmp:
-            raise MemberException("missing configuratino option: %s" % field)
-        if not cfg_tmp[field]:
-            raise MemberException("null configuration option: %s" %field)
-
-    # update the current configuration with the loaded values
-    cfg.update(cfg_tmp)
-
-
-
-### Exceptions ###
-
-class MemberException(Exception):
-    """Exception class for member-related errors."""
-
-class DuplicateStudentID(MemberException):
-    """Exception class for student ID conflicts."""
-    pass
-
-class InvalidStudentID(MemberException):
-    """Exception class for malformed student IDs."""
-    pass
-
-class InvalidTerm(MemberException):
-    """Exception class for malformed terms."""
-    pass
-
-class NoSuchMember(MemberException):
-    """Exception class for nonexistent members."""
-    pass
-
-
-
-### Connection Management ###
-
-# global database connection
-connection = db.DBConnection()
-
-
-def connect():
-    """Connect to PostgreSQL."""
-    
-    load_configuration()
-    
-    connection.connect(cfg['server'], cfg['database'])
-       
-
-def disconnect():
-    """Disconnect from PostgreSQL."""
-    
-    connection.disconnect()
-
-
-def connected():
-    """Determine whether the connection has been established."""
-
-    return connection.connected()
-
-
-### Member Table ###
-
-def new(realname, studentid=None, program=None):
-    """
-    Registers a new CSC member. The member is added
-    to the members table and registered for the current
-    term.
-
-    Parameters:
-        realname  - the full real name of the member
-        studentid - the student id number of the member
-        program   - the program of study of the member
-
-    Returns: the memberid of the new member
-
-    Exceptions:
-        DuplicateStudentID - if the student id already exists in the database
-        InvalidStudentID   - if the student id is malformed
-
-    Example: new("Michael Spang", program="CS") -> 3349
-    """
-
-    # blank attributes should be NULL
-    if studentid == '': studentid = None
-    if program == '': program = None
-
-    # check the student id format
-    regex = '^[0-9]{8}$'
-    if studentid != None and not re.match(regex, str(studentid)):
-        raise InvalidStudentID("student id is invalid: %s" % studentid)
-
-    # check for duplicate student id
-    member = connection.select_member_by_studentid(studentid)
-    if member:
-        raise DuplicateStudentID("student id exists in database: %s" % studentid)
-
-    # add the member
-    memberid = connection.insert_member(realname, studentid, program)
-
-    # register them for this term
-    connection.insert_term(memberid, terms.current())
-
-    # commit the transaction
-    connection.commit()
-
-    return memberid
-
-
-def get(memberid):
-    """
-    Look up attributes of a member by memberid.
-
-    Parameters:
-        memberid - the member id number
-
-    Returns: a dictionary of attributes
-
-    Example: get(3349) -> {
-                 'memberid': 3349,
-                 'name': 'Michael Spang',
-                 'program': 'Computer Science',
-                 ...
-             }
-    """
-
-    return connection.select_member_by_id(memberid)
-
-
-def get_userid(userid):
-    """
-    Look up attributes of a member by userid.
-
-    Parameters:
-        userid - the UNIX user id
-
-    Returns: a dictionary of attributes
-
-    Example: get('mspang') -> {
-                 'memberid': 3349,
-                 'name': 'Michael Spang',
-                 'program': 'Computer Science',
-                 ...
-             }
-    """
-
-    return connection.select_member_by_account(userid)
-
-
-def get_studentid(studentid):
-    """
-    Look up attributes of a member by studnetid.
-
-    Parameters:
-        studentid - the student ID number
-
-    Returns: a dictionary of attributes
-    
-    Example: get(...) -> {
-                 'memberid': 3349,
-                 'name': 'Michael Spang',
-                 'program': 'Computer Science',
-                 ...
-             }
-    """
-
-    return connection.select_member_by_studentid(studentid)
-
-
-def list_term(term):
-    """
-    Build a list of members in a term.
-
-    Parameters:
-        term - the term to match members against
-
-    Returns: a list of member dictionaries
-
-    Example: list_term('f2006'): -> [
-                 { 'memberid': 3349, ... },
-                 { 'memberid': ... }.
-                 ...
-             ]
-    """
-
-    # retrieve a list of memberids in term
-    memberlist = connection.select_members_by_term(term)
-
-    # convert the list of memberids to a list of dictionaries
-    memberlist = map(connection.select_member_by_id, memberlist)
-
-    return memberlist
-        
-
-def list_name(name):
-    """
-    Build a list of members with matching names.
-
-    Parameters:
-        name - the name to match members against
-
-    Returns: a list of member dictionaries
-
-    Example: list_name('Spang'): -> [
-                 { 'memberid': 3349, ... },
-                 { 'memberid': ... },
-                 ...
-             ]
-    """
-
-    # retrieve a list of memberids matching name
-    memberlist = connection.select_members_by_name(name)
-
-    # convert the list of memberids to a list of dictionaries
-    memberlist = map(connection.select_member_by_id, memberlist)
-
-    return memberlist
-
-
-def delete(memberid):
-    """
-    Erase all records of a member.
-
-    Note: real members are never removed
-          from the database
-
-    Parameters:
-        memberid - the member id number
-
-    Returns: attributes and terms of the
-             member in a tuple
-
-    Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
-    """
-
-    # save member data
-    member = connection.select_member_by_id(memberid)
-    term_list = connection.select_terms(memberid)
-
-    # remove data from the db
-    connection.delete_term_all(memberid)
-    connection.delete_member(memberid)
-    connection.commit()
-
-    return (member, term_list)
-
-
-def update(member):
-    """
-    Update CSC member attributes. None is NULL.
-
-    Parameters:
-        member - a dictionary with member attributes as
-                 returned by get, possibly omitting some
-                 attributes. member['memberid'] must exist
-                 and be valid.
-
-    Exceptions:
-        NoSuchMember       - if the member id does not exist
-        InvalidStudentID   - if the student id number is malformed
-        DuplicateStudentID - if the student id number exists 
-
-    Example: update( {'memberid': 3349, userid: 'mspang'} )
-    """
-
-    if member.has_key('studentid') and member['studentid'] != None:
-
-        studentid = member['studentid']
-        
-        # check the student id format
-        regex = '^[0-9]{8}$'
-        if studentid != None and not re.match(regex, str(studentid)):
-            raise InvalidStudentID("student id is invalid: %s" % studentid)
-
-        # check for duplicate student id
-        member = connection.select_member_by_studentid(studentid)
-        if member:
-            raise DuplicateStudentID("student id exists in database: %s" %
-                    studentid)
-
-    # not specifying memberid is a bug
-    if not member.has_key('memberid'):
-        raise Exception("no member specified in call to update")
-    memberid = member['memberid']
-
-    # see if member exists
-    old_member = connection.select_member_by_id(memberid)
-    if not old_member:
-        raise NoSuchMember("memberid does not exist in database: %d" %
-                memberid)
-    
-    # do the update
-    connection.update_member(member)
-
-    # commit the transaction
-    connection.commit()
-
-
-
-### Term Table ###
-
-def register(memberid, term_list):
-    """
-    Registers a member for one or more terms.
-
-    Parameters:
-        memberid  - the member id number
-        term_list - the term to register for, or a list of terms
-
-    Exceptions:
-        InvalidTerm - if a term is malformed
-
-    Example: register(3349, "w2007")
-
-    Example: register(3349, ["w2007", "s2007"])
-    """
-
-    if not type(term_list) in (list, tuple):
-        term_list = [ term_list ]
-
-    for term in term_list:
-        
-        # check term syntax
-        if not re.match('^[wsf][0-9]{4}$', term):
-            raise InvalidTerm("term is invalid: %s" % term)
-    
-        # add term to database
-        connection.insert_term(memberid, term)
-
-    connection.commit()
-
-
-def registered(memberid, term):
-    """
-    Determines whether a member is registered
-    for a term.
-
-    Parameters:
-        memberid - the member id number
-        term     - the term to check
-
-    Returns: whether the member is registered
-
-    Example: registered(3349, "f2006") -> True
-    """
-
-    return connection.select_term(memberid, term) != None
-
-
-def terms_list(memberid):
-    """
-    Retrieves a list of terms a member is
-    registered for.
-
-    Parameters:
-        memberid - the member id number
-
-    Returns: list of term strings
-
-    Example: registered(0) -> 's1993'
-    """
-
-    return connection.select_terms(memberid)
-
-
-
-### Tests ###
-
-if __name__ == '__main__':
-
-    connect()
-    
-    
-    sid = new("Test User", "99999999", "CS")
-
-    assert registered(id, terms.current())
-    print get(sid)
-    register(sid, terms.next(terms.current()))
-    assert registered(sid, terms.next(terms.current()))
-    print terms_list(sid)
-    print get(sid)
-    print delete(sid)
diff --git a/py/csc/admin/terms.py b/py/csc/admin/terms.py
deleted file mode 100644 (file)
index 3f9cae2..0000000
+++ /dev/null
@@ -1,252 +0,0 @@
-# $Id: terms.py 44 2006-12-31 07:09:27Z mspang $
-"""
-Terms Routines
-
-This module contains functions for manipulating
-terms, such as determining the current term,
-finding the next or previous term, converting
-dates to terms, and more.
-"""
-import time, datetime, re
-
-# year to count terms from
-EPOCH = 1970
-
-# seasons list
-SEASONS = [ 'w', 's', 'f' ]
-
-
-def valid(term):
-    """
-    Determines whether a term is well-formed:
-
-    Parameters:
-        term - the term string
-
-    Returns: whether the term is valid (boolean)
-
-    Example: valid("f2006") -> True
-    """
-
-    regex = '^[wsf][0-9]{4}$'
-    return re.match(regex, term) != None
-
-
-def parse(term):
-    """Helper function to convert a term string to the number of terms
-       since the epoch. Such numbers are intended for internal use only."""
-
-    if not valid(term):
-        raise Exception("malformed term: %s" % term)
-
-    year = int( term[1:] )
-    season = SEASONS.index( term[0] )
-
-    return (year - EPOCH) * len(SEASONS) + season
-
-
-def generate(term):
-    """Helper function to convert a year and season to a term string."""
-    
-    year = int(term / len(SEASONS)) + EPOCH
-    season = term % len(SEASONS)
-    
-    return "%s%04d" % ( SEASONS[season], year )
-
-
-def next(term):
-    """
-    Returns the next term. (convenience function)
-
-    Parameters:
-        term - the term string
-
-    Retuns: the term string of the following term
-
-    Example: next("f2006") -> "w2007"
-    """
-    
-    return add(term, 1)
-
-
-def previous(term):
-    """
-    Returns the previous term. (convenience function)
-
-    Parameters:
-        term - the term string
-
-    Returns: the term string of the preceding term
-
-    Example: previous("f2006") -> "s2006"
-    """
-
-    return add(term, -1)
-
-
-def add(term, offset):
-    """
-    Calculates a term relative to some base term.
-    
-    Parameters:
-        term   - the base term
-        offset - the number of terms since term (may be negative)
-
-    Returns: the term that comes offset terms after term
-    """
-
-    return generate(parse(term) + offset)
-
-
-def delta(initial, final):
-    """
-    Calculates the distance between two terms.
-    It should be true that add(a, delta(a, b)) == b.
-
-    Parameters:
-        initial - the base term
-        final   - the term at some offset from the base term
-
-    Returns: the offset of final relative to initial
-    """
-
-    return parse(final) - parse(initial)
-
-
-def compare(first, second):
-    """
-    Compares two terms. This function is suitable
-    for use with list.sort().
-
-    Parameters:
-        first  - base term for comparison
-        second - term to compare to
-
-    Returns: > 0 (if first >  second)
-             = 0 (if first == second)
-             < 0 (if first <  second)
-    """
-    return delta(second, first)
-             
-
-def interval(base, count):
-    """
-    Returns a list of adjacent terms.
-
-    Parameters:
-        base    - the first term in the interval
-        count   - the number of terms to include
-
-    Returns: a list of count terms starting with initial
-
-    Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ]
-    """
-    
-    terms = []
-
-    for num in xrange(count):
-        terms.append( add(base, num) )
-    
-    return terms
-        
-
-def tstamp(timestamp):
-    """Helper to convert seconds since the epoch
-    to terms since the epoch."""
-
-    # let python determine the month and year
-    date = datetime.date.fromtimestamp(timestamp)
-
-    # determine season
-    if date.month <= 4:
-        season = SEASONS.index('w')
-    elif date.month <= 8:
-        season = SEASONS.index('s')
-    else:
-        season = SEASONS.index('f')
-
-    return (date.year - EPOCH) * len(SEASONS) + season
-
-
-def from_timestamp(timestamp):
-    """
-    Converts a number of seconds since
-    the epoch to a number of terms since
-    the epoch.
-
-    This function notes that:
-        WINTER = JANUARY to APRIL
-        SPRING = MAY TO AUGUST
-        FALL   = SEPTEMBER TO DECEMBER
-    
-    Parameters:
-        timestamp - number of seconds since the epoch
-
-    Returns: the number of terms since the epoch
-
-    Example: from_timestamp(1166135779) -> 'f2006'
-    """
-
-    return generate( tstamp(timestamp) )
-    
-
-def curr():
-    """Helper to determine the current term."""
-
-    return tstamp( time.time() )
-
-
-def current():
-    """
-    Determines the current term.
-
-    Returns: current term
-
-    Example: current() -> 'f2006'
-    """
-
-    return generate( curr() )
-    
-
-def next_unregistered(registered):
-    """
-    Find the first future or current unregistered term.
-    Intended as the 'default' for registrations.
-
-    Parameters:
-        registered - a list of terms a member is registered for
-
-    Returns: the next unregistered term
-    """
-    
-    # get current term number
-    now = curr()
-
-    # never registered -> current term is next
-    if len( registered) < 1:
-        return generate( now )
-
-    # return the first unregistered, or the current term (whichever is greater)
-    return generate(max([max(map(parse, registered))+1, now]))
-
-
-
-### Tests ###
-
-if __name__ == '__main__':
-
-    assert parse('f2006') == 110
-    assert generate(110) == 'f2006'
-    assert next('f2006') == 'w2007'
-    assert previous('f2006') == 's2006'
-    assert delta('f2006', 'w2007') == 1
-    assert add('f2006', delta('f2006', 'w2010')) == 'w2010'
-    assert interval('f2006', 3) == ['f2006', 'w2007', 's2007']
-    assert from_timestamp(1166135779) == 'f2006'
-    assert parse( current() ) >= 110
-    assert next_unregistered( [current()] ) == next( current() )
-    assert next_unregistered( [] ) == current()
-    assert next_unregistered( [previous(current())] ) == current()
-    assert next_unregistered( [add(current(), -2)] ) == current()
-
-    print "All tests passed." "\n"
diff --git a/py/csc/apps/__init__.py b/py/csc/apps/__init__.py
deleted file mode 100644 (file)
index 09ddf85..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
-"""
-User Interfaces
-
-This module contains frontends and related modules.
-CEO's primary frontends are:
-
-    legacy - aims to reproduce the curses UI of the previous CEO
-"""
diff --git a/py/csc/apps/legacy/__init__.py b/py/csc/apps/legacy/__init__.py
deleted file mode 100644 (file)
index 18352d2..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
-"""
-Legacy User Interface
-
-This module contains the legacy CEO user interface and related modules.
-Important modules are:
-
-    main.py    - all of the main UI logic
-    helpers.py - user interface library routines
-"""
diff --git a/py/csc/apps/legacy/helpers.py b/py/csc/apps/legacy/helpers.py
deleted file mode 100644 (file)
index 9d7cbaa..0000000
+++ /dev/null
@@ -1,412 +0,0 @@
-# $Id: helpers.py 35 2006-12-28 05:14:05Z mspang $
-"""
-Helpers for legacy User Interface
-
-This module contains numerous functions that are designed to immitate
-the look and behavior of the previous CEO. Included is code for various
-curses-based UI widgets that were provided by Perl 5's Curses and
-Curses::Widgets libraries.
-
-Though attempts have been made to keep the UI bug-compatible with
-the previous system, some compromises have been made. For example,
-the input and textboxes draw 'OK' and 'Cancel' buttons where the old
-CEO had them, but they are fake. That is, the buttons in the old
-CEO were selectable but non-operational, but in the new CEO they are
-not even selectable.
-"""
-import curses.ascii
-
-# key constants not defined in CURSES
-KEY_RETURN = ord('\n')
-KEY_ESCAPE = 27
-KEY_EOT = 4
-
-
-def center(parent_dim, child_dim):
-    """Helper for centering a length in a larget length."""
-    return (parent_dim-child_dim)/2
-
-
-def read_input(wnd, offy, offx, width, maxlen, echo=True):
-    """
-    Read user input within a confined region of a window.
-
-    Basic line-editing is supported:
-        LEFT, RIGHT, HOME, and END move around.
-        BACKSPACE and DEL remove characters.
-        INSERT switches between insert and overwrite mode.
-        ESC and C-d abort input.
-        RETURN completes input.
-    
-    Parameters:
-        wnd    - parent window for region
-        offy   - the vertical offset for the beginning of the input region
-        offx   - the horizontal offset for the beginning of the input region
-        width  - the width of the region
-        maxlen - greatest number of characters to read (0 for no limit)
-        echo   - boolean: whether to display typed characters
-    
-    Returns: the string, or None when the user aborts.
-    """
-
-    # turn on cursor
-    try:
-        curses.curs_set(1)
-    except:
-        pass
-
-    # set keypad mode to allow UP, DOWN, etc
-    wnd.keypad(1)
-
-    # the input string
-    input = ""
-
-    # offset of cursor in input
-    # i.e. the next operation is applied at input[inputoff]
-    inputoff = 0
-
-    # display offset (for scrolling)
-    # i.e. the first character in the region is input[displayoff]
-    displayoff = 0
-
-    # insert mode (True) or overwrite mode (False)
-    insert = True
-
-    while True:
-
-        # echo mode, display the string
-        if echo:
-            # discard characters before displayoff, 
-            # as the window may be scrolled to the right
-            substring = input[displayoff:]
-    
-            # pad the string with zeroes to overwrite stale characters
-            substring = substring + " " * (width - len(substring))
-    
-            # display the substring
-            wnd.addnstr(offy, offx, substring, width)
-    
-            # await input
-            key = wnd.getch(offy, offx + inputoff - displayoff)
-
-        # not echo mode, don't display the string
-        else:
-            # await input at arbitrary location
-            key = wnd.getch(offy, offx)
-
-        # enter returns input
-        if key == KEY_RETURN:
-            return input
-
-        # escape aborts input
-        elif key == KEY_ESCAPE:
-            return None
-
-        # EOT (C-d) aborts if there is no input
-        elif key == KEY_EOT:
-            if len(input) == 0:
-                return None
-
-        # backspace removes the previous character
-        elif key == curses.KEY_BACKSPACE:
-            if inputoff > 0:
-
-                # remove the character immediately before the input offset
-                input = input[0:inputoff-1] + input[inputoff:]
-                inputoff -= 1
-
-                # move either the cursor or entire line of text left
-                if displayoff > 0:
-                    displayoff -= 1
-
-        # delete removes the current character
-        elif key == curses.KEY_DC:
-            if inputoff < len(input):
-                
-                # remove the character at the input offset
-                input = input[0:inputoff] + input[inputoff+1:]
-
-        # left moves the cursor one character left
-        elif key == curses.KEY_LEFT:
-            if inputoff > 0:
-
-                # move the cursor to the left
-                inputoff -= 1
-
-                # scroll left if necessary
-                if inputoff < displayoff:
-                    displayoff -= 1
-
-        # right moves the cursor one character right
-        elif key == curses.KEY_RIGHT:
-            if inputoff < len(input):
-                
-                # move the cursor to the right
-                inputoff += 1
-
-                # scroll right if necessary
-                if displayoff - inputoff == width:
-                    displayoff += 1
-
-        # home moves the cursor to the first character
-        elif key == curses.KEY_HOME:
-            inputoff = 0
-            displayoff = 0
-
-        # end moves the cursor past the last character
-        elif key == curses.KEY_END:
-            inputoff = len(input)
-            displayoff = len(input) - width + 1
-
-        # insert toggles insert/overwrite mode
-        elif key == curses.KEY_IC:
-            insert = not insert
-
-        # other (printable) characters are added to the input string
-        elif curses.ascii.isprint(key):
-            if len(input) < maxlen or maxlen == 0:
-
-                # insert mode: insert before current offset
-                if insert:
-                    input = input[0:inputoff] + chr(key) + input[inputoff:]
-    
-                # overwrite mode: replace current offset
-                else:
-                    input = input[0:inputoff] + chr(key) + input[inputoff+1:]
-    
-                # increment the input offset
-                inputoff += 1
-    
-                # scroll right if necessary
-                if inputoff - displayoff == width:
-                    displayoff += 1
-
-
-def inputbox(wnd, prompt, field_width, echo=True):
-    """Display a window for user input."""
-
-    wnd_height, wnd_width = wnd.getmaxyx()
-    height, width = 12, field_width + 7
-
-    # draw a window for the dialog
-    childy, childx = center(wnd_height-1, height)+1, center(wnd_width, width)
-    child_wnd = wnd.subwin(height, width, childy, childx)
-    child_wnd.clear()
-    child_wnd.border()
-
-    # draw another window for the text box
-    texty, textx = center(height-1, 3)+1, center(width-1, width-5)+1
-    textheight, textwidth = 3, width-5
-    text_wnd = child_wnd.derwin(textheight, textwidth, texty, textx)
-    text_wnd.clear()
-    text_wnd.border()
-    
-    # draw the prompt
-    prompty, promptx = 2, 3
-    child_wnd.addnstr(prompty, promptx, prompt, width-2)
-
-    # draw the fake buttons
-    fakey, fakex = 9, width - 19
-    child_wnd.addstr(fakey, fakex, "< OK > < Cancel >")
-    child_wnd.addch(fakey, fakex+2, "O", curses.A_UNDERLINE)
-    child_wnd.addch(fakey, fakex+9, "C", curses.A_UNDERLINE)
-
-    # update the screen
-    child_wnd.noutrefresh()
-    text_wnd.noutrefresh()
-    curses.doupdate()
-
-    # read an input string within the field region of text_wnd
-    inputy, inputx, inputwidth = 1, 1, textwidth - 2
-    input = read_input(text_wnd, inputy, inputx, inputwidth, 0, echo)
-    
-    # erase the window
-    child_wnd.erase()
-    child_wnd.refresh()
-
-    return input
-
-
-def line_wrap(line, width):
-    """Wrap a string to a certain width (returns a list of strings)."""
-
-    wrapped_lines = []
-    tokens = line.split(" ")
-    tokens.reverse()
-    tmp = tokens.pop()
-    if len(tmp) > width:
-        wrapped_lines.append(tmp[0:width])
-        tmp = tmp[width:]
-    while len(tokens) > 0:
-        token = tokens.pop()
-        if len(tmp) + len(token) + 1 <= width:
-            tmp += " " + token
-        elif len(token) > width:
-            tmp += " " + token[0:width-len(tmp)-1]
-            tokens.push(token[width-len(tmp)-1:])
-        else:
-            wrapped_lines.append(tmp)
-            tmp = token
-    wrapped_lines.append(tmp)
-    return wrapped_lines
-
-
-def msgbox(wnd, msg, title="Message"):
-    """Display a message in a window."""
-
-    # split message into a list of lines
-    lines = msg.split("\n")
-    
-    # determine the dimensions of the method
-    message_height = len(lines)
-    message_width = 0
-    for line in lines:
-        if len(line) > message_width:
-            message_width = len(line)
-
-    # ensure the window fits the title
-    if len(title) > message_width:
-        message_width = len(title)
-
-    # maximum message width
-    parent_height, parent_width = wnd.getmaxyx()
-    max_message_width = parent_width - 8
-
-    # line-wrap if necessary
-    if message_width > max_message_width:
-        newlines = []
-        for line in lines:
-            for newline in line_wrap(line, max_message_width):
-                newlines.append(newline)
-        lines = newlines
-        message_width = max_message_width
-        message_height = len(lines)
-
-    # random padding that perl's curses adds
-    pad_width = 2
-
-    # create the outer window
-    outer_height, outer_width = message_height + 8, message_width + pad_width + 6
-    outer_y, outer_x = center(parent_height+1, outer_height)-1, center(parent_width, outer_width)
-    outer_wnd = wnd.derwin(outer_height, outer_width, outer_y, outer_x)
-    outer_wnd.erase()
-    outer_wnd.border()
-
-    # create the inner window
-    inner_height, inner_width = message_height + 2, message_width + pad_width + 2
-    inner_y, inner_x = 2, center(outer_width, inner_width)
-    inner_wnd = outer_wnd.derwin(inner_height, inner_width, inner_y, inner_x)
-    inner_wnd.border()
-
-    # display the title
-    outer_wnd.addstr(0, 1, " " + title + " ", curses.A_REVERSE | curses.A_BOLD)
-    
-    # display the message
-    for i in xrange(len(lines)):
-        inner_wnd.addnstr(i+1, 1, lines[i], message_width)
-
-        # draw a solid block at the end of the first few lines
-        if i < len(lines) - 1:
-            inner_wnd.addch(i+1, inner_width-1, ' ', curses.A_REVERSE)
-
-    # display the fake OK button
-    fakey, fakex = outer_height - 3, outer_width - 8
-    outer_wnd.addstr(fakey, fakex, "< OK >", curses.A_REVERSE)
-    outer_wnd.addch(fakey, fakex+2, "O", curses.A_UNDERLINE | curses.A_REVERSE)
-
-    # update display
-    outer_wnd.noutrefresh()
-    inner_wnd.noutrefresh()
-    curses.doupdate()
-
-    # read a RETURN or ESC before returning
-    curses.curs_set(0)
-    outer_wnd.keypad(1)
-    while True:
-        key = outer_wnd.getch(0,0)
-        if key == KEY_RETURN or key == KEY_ESCAPE:
-            break
-
-    # clear the window
-    outer_wnd.erase()
-    outer_wnd.refresh()
-    
-
-def menu(wnd, offy, offx, width, options, _acquire_wnd=None):
-    """
-    Draw a menu and wait for a selection.
-
-    Parameters:
-        wnd          - parent window
-        offy         - vertical offset for menu region
-        offx         - horizontal offset for menu region
-        width        - width of menu region
-        options      - a list of selections
-        _acquire_wnd - hack to support resize: must be a function callback
-                       that returns new values for wnd, offy, offx, height,
-                       width. Unused if None.
-
-    Returns: index into options that was selected
-    """
-
-    # the currently selected option
-    selected = 0
-
-    while True:
-        # disable cursor
-        curses.curs_set(0)
-
-        # hack to support resize: recreate the
-        # parent window every iteration
-        if _acquire_wnd:
-            wnd, offy, offx, height, width = _acquire_wnd()
-
-        # keypad mode so getch() works with up, down
-        wnd.keypad(1)
-
-        # display the menu
-        for i in xrange(len(options)):
-            text, callback = options[i]
-            text = text + " " * (width - len(text))
-
-            # the selected option is displayed in reverse video
-            if i == selected:
-                wnd.addstr(i+offy, offx, text, curses.A_REVERSE)
-            else:
-                wnd.addstr(i+offy, offx, text)
-                    # display the member
-
-        wnd.refresh()
-        
-        # read one keypress
-        input = wnd.getch()
-
-        # UP moves to the previous option
-        if input == curses.KEY_UP and selected > 0:
-            selected = (selected - 1)
-
-        # DOWN moves to the next option
-        elif input == curses.KEY_DOWN and selected < len(options) - 1:
-            selected = (selected + 1)
-
-        # RETURN runs the callback for the selected option
-        elif input == KEY_RETURN:
-            text, callback = options[selected]
-
-            # highlight the selected option
-            text = text + " " * (width - len(text))
-            wnd.addstr(selected+offy, offx, text, curses.A_REVERSE | curses.A_BOLD)
-            wnd.refresh()
-
-            # execute the selected option
-            if callback(wnd): # success
-                break
-
-
-def reset():
-    """Reset the terminal and clear the screen."""
-
-    reset = curses.tigetstr('rs1')
-    if not reset: reset = '\x1bc'
-    curses.putp(reset)
-
diff --git a/py/csc/apps/legacy/main.py b/py/csc/apps/legacy/main.py
deleted file mode 100644 (file)
index a55e46c..0000000
+++ /dev/null
@@ -1,555 +0,0 @@
-# $Id: main.py 44 2006-12-31 07:09:27Z mspang $
-"""
-CEO-like Frontend
-
-This frontend aims to be compatible in both look and function with the
-curses UI of CEO.
-
-Some small improvements have been made, such as not echoing passwords and
-aborting when nothing is typed into the first input box during an operation.
-
-This frontend is poorly documented, deprecated, and undoubtedly full of bugs.
-"""
-import curses.ascii, re, os
-from helpers import menu, inputbox, msgbox, reset
-from csc.admin import accounts, members, terms
-
-# color of the ceo border
-BORDER_COLOR = curses.COLOR_RED
-
-
-def action_new_member(wnd):
-    """Interactively add a new member."""
-
-    username, studentid, program = '', None, ''
-
-    # read the name
-    prompt = "      Name: "
-    realname = inputbox(wnd, prompt, 18)
-
-    # abort if no username is entered
-    if not realname or realname.lower() == 'exit':
-        return False
-
-    # read the student id
-    prompt = "Student id:"
-    while studentid == None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
-        studentid = inputbox(wnd, prompt, 18)
-
-    # abort if exit is entered
-    if studentid.lower() == 'exit':
-        return False
-
-    if studentid == '':
-        studentid = None
-
-    # read the program of study
-    prompt = "   Program:"
-    program = inputbox(wnd, prompt, 18)
-
-    # abort if exit is entered
-    if program == None or program.lower() == 'exit':
-        return False
-
-    # connect the members module to its backend if necessary
-    if not members.connected(): members.connect()
-
-    # attempt to create the member
-    try:
-        memberid = members.new(realname, studentid, program)
-
-        msgbox(wnd, "Success! Your memberid is %s.  You are now registered\n"
-                    % memberid + "for the " + terms.current() + " term.");
-
-    except members.InvalidStudentID:
-        msgbox(wnd, "Invalid student ID.")
-        return False
-    except members.DuplicateStudentID:
-        msgbox(wnd, "A member with this student ID exists.")
-        return False
-
-
-def action_term_register(wnd):
-    """Interactively register a member for a term."""
-
-    memberid, term = '', ''
-
-    # read the member id
-    prompt = 'Enter memberid ("exit" to cancel):'
-    memberuserid = inputbox(wnd, prompt, 36)
-
-    if not memberuserid or memberuserid.lower() == 'exit':
-        return False
-
-    member = get_member_memberid_userid(wnd, memberuserid)
-    if not member: return False
-
-    memberid = member['memberid']
-    term_list = members.terms_list(memberid)
-    
-    # display user
-    display_member_details(wnd, member, term_list)
-
-    # read the term
-    prompt = "Which term to register for ([fws]20nn):"
-    while not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit':
-        term = inputbox(wnd, prompt, 41) 
-
-    # abort when exit is entered
-    if term.lower() == 'exit':
-        return False
-
-    # already registered?
-    if members.registered(memberid, term):
-        msgbox(wnd, "You are already registered for term " + term)
-        return False
-
-    try:
-
-        # attempt to register
-        members.register(memberid, term)
-        
-        # display success message [sic]
-        msgbox(wnd, "Your are now registered for term " + term)
-
-    except members.InvalidTerm:
-        msgbox(wnd, "Term is not valid: %s" % term)
-
-    return False
-
-
-def action_term_register_multiple(wnd):
-    """Interactively register a member for multiple terms."""
-
-    memberid, base, num = '', '', None
-
-    # read the member id
-    prompt = 'Enter memberid ("exit" to cancel):'
-    memberuserid = inputbox(wnd, prompt, 36)
-
-    if not memberuserid or memberuserid.lower() == 'exit':
-        return False
-
-    member = get_member_memberid_userid(wnd, memberuserid)
-    if not member: return False
-
-    memberid = member['memberid']
-    term_list = members.terms_list(memberid)
-    
-    # display user
-    display_member_details(wnd, member, term_list)
-
-    # read the base
-    prompt = "Which term to start registering ([fws]20nn):"
-    while not re.match('^[wsf][0-9]{4}$', base) and not base == 'exit':
-        base = inputbox(wnd, prompt, 41) 
-
-    # abort when exit is entered
-    if base.lower() == 'exit':
-        return False
-
-    # read number of terms
-    prompt = 'How many terms?'
-    while not num or not re.match('^[0-9]*$', num):
-        num = inputbox(wnd, prompt, 36)
-    num = int(num)
-
-    # any terms in the range?
-    if num < 1:
-        msgbox(wnd, "No terms to register.")
-        return False
-
-    # compile a list to register
-    term_list = terms.interval(base, num)
-
-    # already registered?
-    for term in term_list:
-        if members.registered(memberid, term):
-            msgbox(wnd, "You are already registered for term " + term)
-            return False
-
-    try:
-
-        # attempt to register all terms
-        members.register(memberid, term_list)
-        
-        # display success message [sic]
-        msgbox(wnd, "Your are now registered for terms: " + ", ".join(term_list))
-
-    except members.InvalidTerm:
-        msgbox(wnd, "Term is not valid: %s" % term)
-
-    return False
-
-
-def action_create_account(wnd):
-    """Interactively create an account for a member."""
-    
-    memberid, userid = '', ''
-
-    # read the member id
-    prompt = 'Enter member ID (exit to cancel):'
-    memberid = inputbox(wnd, prompt, 35)
-
-    if not memberid or memberid.lower() == 'exit':
-        return False
-
-    member = get_member_memberid_userid(wnd, memberid)
-    if not member: return False
-
-    memberid = member['memberid']
-    term_list = members.terms_list(memberid)
-    
-    # display the member
-    display_member_details(wnd, member, term_list)
-    
-    # verify member
-    prompt = "Is this the correct member?"
-    answer = None
-    while answer != "yes" and answer != "y" and answer != "n" and answer != "no" and answer != "exit":
-        answer = inputbox(wnd, prompt, 28) 
-
-    # user abort
-    if answer == "exit":
-        return False
-
-    # incorrect member; abort
-    if answer == "no" or answer == "n":
-        msgbox(wnd, "I suggest searching for the member by userid or name from the main menu.")
-        return False
-
-    # read user id
-    prompt = "Userid:"
-    while userid == '':
-        userid = inputbox(wnd, prompt, 18) 
-
-    # user abort
-    if userid == None or userid.lower() == 'exit':
-        return False
-
-    # member already has an account?
-    #if member['userid'] != None:
-    #    msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
-    #                "Contact the sysadmin if there are still problems." )
-    #    return False
-
-    # password input loop
-    password = "password"
-    check = "check"
-    while password != check:
-    
-        # read password
-        prompt = "User password:"
-        password = None
-        while not password:
-            password = inputbox(wnd, prompt, 18, False) 
-
-        # read another password
-        prompt = "Enter the password again:"
-        check = None
-        while not check:
-            check = inputbox(wnd, prompt, 27, False) 
-
-
-    # create the UNIX account
-    result = accounts.create_account(userid, password, member['name'], memberid)
-
-    if result == accounts.LDAP_EXISTS:
-        msgbox(wnd, "Error: Could not do stuff , Already exists.")
-        return False
-    elif result == accounts.KRB_EXISTS:
-        msgbox(wnd, "This account already exists in Kerberos, but not in LDAP. Please contact the Systems Administrator.")
-        return False
-    elif result == accounts.LDAP_NO_IDS:
-        msgbox(wnd, "There are no available UNIX user ids. This is a fatal error. Contact the Systems Administrator.")
-        return False
-    elif result == accounts.BAD_REALNAME:
-        msgbox(wnd, "Invalid real name: %s. Contact the Systems Administrator." % member['name'])
-        return False
-    elif result == accounts.BAD_USERNAME:
-        msgbox(wnd, "Invalid username: %s. Enter a valid username." % userid)
-        return False
-    elif result != accounts.SUCCESS:
-        raise Exception("Unexpected return status of accounts.create_account(): %s" % result)
-        
-    # now update the CEO database with the username
-    members.update( {'memberid':memberid, 'userid': userid} )
-
-    # success
-    msgbox(wnd, "Please run 'addhomedir " + userid + "'.")
-    msgbox(wnd, "Success! Your account has been added")
-
-
-def display_member_details(wnd, member, term_list):
-    """Display member attributes in a message box."""
-
-    # clone and sort term_list
-    term_list = list(term_list)
-    term_list.sort( terms.compare )
-
-    # labels for data
-    id_label, studentid_label, name_label = "ID:", "StudentID:", "Name:"
-    program_label, userid_label, terms_label = "Program:", "User ID:", "Terms:"
-
-    # format it all into a massive string
-    message =  "%8s %-20s %10s %-10s (user)\n" % (name_label, member['name'], id_label, member['memberid']) + \
-               "%8s %-20s %10s %-10s\n" % (program_label, member['program'], studentid_label, member['studentid'])
-
-    if member['userid']:
-        message += "%8s %s\n" % (userid_label, member['userid'])
-    else:
-        message += 'No user ID.\n'
-
-    message += "%s %s" % (terms_label, " ".join(term_list))
-
-    # display the string in a message box
-    msgbox(wnd, message)
-
-
-def get_member_memberid_userid(wnd, memberuserid):
-    """Retrieve member attributes by member of user id."""
-
-    # connect the members module to its backends
-    if not members.connected(): members.connect()
-
-    # retrieve member data
-
-    if re.match('^[0-9]*$', memberuserid):
-
-        # numeric memberid, look it up
-        memberid = int(memberuserid)
-        member = members.get( memberid )
-        if not member:
-            msgbox(wnd, '%s is an invalid memberid' % memberuserid)
-
-    else:
-
-        # non-numeric memberid: try userids
-        member = members.get_userid( memberuserid )
-        if not member:
-            msgbox(wnd, "%s is an invalid account userid" % memberuserid)
-            
-    return member
-    
-
-def action_display_member(wnd):
-    """Interactively display a member."""
-    
-    # read the member id
-    prompt = 'Memberid: '
-    memberid = inputbox(wnd, prompt, 36)
-
-    if not memberid or memberid.lower() == 'exit':
-        return False
-
-    member = get_member_memberid_userid(wnd, memberid)
-    if not member: return
-    term_list = members.terms_list( member['memberid'] )
-
-    # display the details in a window
-    display_member_details(wnd, member, term_list)
-
-
-def page(text):
-    
-    try:
-        pipe = os.popen('/usr/bin/less', 'w')
-        pipe.write(text)
-        pipe.close() 
-    except IOError:
-        # broken pipe (user didn't read the whole list)
-        pass
-    
-
-def format_members(member_list):
-    """Format a member list into a string."""
-
-    # clone and sort member_list
-    member_list = list(member_list)
-    member_list.sort( lambda x, y: x['memberid']-y['memberid'] )
-
-    buf = ''
-    
-    for member in member_list:
-        attrs = ( member['memberid'], member['name'], member['studentid'],
-                member['type'], member['program'], member['userid'] )
-        buf += "%4d %50s %10s %10s \n%55s %10s\n\n" % attrs
-
-    return buf
-
-
-def action_list_term(wnd):
-    """Interactively list members registered in a term."""
-
-    term = None
-
-    # read the term
-    prompt = "Which term to list members for ([fws]20nn): "
-    while term == None or (not term == '' and not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit'):
-        term = inputbox(wnd, prompt, 41) 
-
-    # abort when exit is entered
-    if not term or term.lower() == 'exit':
-        return False
-
-    # connect the members module to its backends if necessary
-    if not members.connected(): members.connect()
-    
-    # retrieve a list of members for term
-    member_list = members.list_term(term)
-
-    # format the data into a mess of text
-    buf = format_members(member_list)
-
-    # display the mass of text with a pager
-    page( buf )
-
-
-def action_list_name(wnd):
-    
-    name = None
-
-    # read the name
-    prompt = "Enter the member's name: "
-    name = inputbox(wnd, prompt, 41) 
-
-    # abort when exit is entered
-    if not name or name.lower() == 'exit':
-        return False
-
-    # connect the members module to its backends if necessary
-    if not members.connected(): members.connect()
-    
-    # retrieve a list of members for term
-    member_list = members.list_name(name)
-
-    # format the data into a mess of text
-    buf = format_members(member_list)
-
-    # display the mass of text with a pager
-    page( buf )
-
-
-def action_list_studentid(wnd):
-
-    studentid = None
-
-    # read the studentid
-    prompt = "Enter the member's student id: "
-    studentid = inputbox(wnd, prompt, 41) 
-
-    # abort when exit is entered
-    if not studentid or studentid.lower() == 'exit':
-        return False
-
-    # connect the members module to its backends if necessary
-    if not members.connected(): members.connect()
-    
-    # retrieve a list of members for term
-    member = members.get_studentid(studentid)
-    if member != None:
-        member_list = [ members.get_studentid(studentid) ]
-    else:
-        member_list = []
-
-    # format the data into a mess of text
-    buf = format_members(member_list)
-
-    # display the mass of text with a pager
-    page( buf )
-
-
-def null_callback(wnd):
-    """Callback for unimplemented menu options."""
-    return False
-
-
-def exit_callback(wnd):
-    """Callback for the exit option."""
-    return True
-
-
-# the top level ceo menu
-top_menu = [
-    ( "New member", action_new_member ),
-    ( "Register for a term", action_term_register ),
-    ( "Register for multiple terms", action_term_register_multiple ),
-    ( "Display a member", action_display_member ),
-    ( "List members registered in a term", action_list_term ),
-    ( "Search for a member by name", action_list_name ),
-    ( "Search for a member by student id", action_list_studentid ),
-    ( "Create an account", action_create_account ),
-    ( "Re Create an account", null_callback ),
-    ( "Library functions", null_callback ),
-    ( "Exit", exit_callback ),
-]
-
-
-def acquire_ceo_wnd(screen=None):
-    """Create the top level ceo window."""
-    
-    # hack to get a reference to the entire screen
-    # even when the caller doesn't (shouldn't) have one
-    global _screen
-    if screen == None:
-        screen = _screen
-    else:
-        _screen = screen
-
-    # if the screen changes size, a mess may be left
-    screen.erase()
-
-    # for some reason, the legacy ceo system
-    # excluded the top line from its window
-    height, width = screen.getmaxyx()
-    ceo_wnd = screen.subwin(height-1, width, 1, 0)
-
-    # draw the border around the ceo window
-    curses.init_pair(1, BORDER_COLOR, -1)
-    color_attr = curses.color_pair(1) | curses.A_BOLD
-    ceo_wnd.attron(color_attr)
-    ceo_wnd.border()
-    ceo_wnd.attroff(color_attr)
-
-    # return window and dimensions of inner area
-    return ceo_wnd, 1, 1, height-2, width-2
-
-
-def ceo_main_curses(screen):
-    """Wrapped main for curses."""
-    
-    curses.use_default_colors()
-
-    # workaround for SSH sessions on virtual consoles (reset terminal)
-    reset()
-
-    # create ceo window
-    ceo_wnd, menu_y, menu_x, menu_height, menu_width = acquire_ceo_wnd(screen)
-
-    # display the top level menu
-    menu(ceo_wnd, menu_y, menu_x, menu_width, top_menu, acquire_ceo_wnd)
-
-
-def run():
-    """Main function for legacy UI."""
-
-    # wrap the entire program using curses.wrapper
-    # so that the terminal is restored to a sane state
-    # when the program exits
-    try:
-        curses.wrapper(ceo_main_curses)
-    except KeyboardInterrupt:
-        pass
-    except curses.error:
-        print "Your screen is too small!"
-        raise
-    except:
-        reset()
-        raise
-
-    # clean up screen before exit
-    reset()
-
-if __name__ == '__main__':
-    run()
-
diff --git a/py/csc/backend/__init__.py b/py/csc/backend/__init__.py
deleted file mode 100644 (file)
index 531a029..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-# $Id$
-"""
-Backends
-
-This module contains backend interfaces and related modules.
-CEO's primary backends are:
-
-    db.py    - CEO's database for member and term registrations
-    ldapi.py - LDAP, for UNIX account metadata administration
-    krb.py   - Kerberos, for UNIX account password administration
-    
-"""
diff --git a/py/csc/backend/db.py b/py/csc/backend/db.py
deleted file mode 100644 (file)
index d01e1a0..0000000
+++ /dev/null
@@ -1,548 +0,0 @@
-# $Id: db.py 37 2006-12-28 10:00:50Z mspang $
-"""
-Database Backend Interface
-
-This module is intended to be a thin wrapper around CEO database operations.
-Methods on the connection class correspond in a straightforward way to SQL
-queries. These methods may restructure and clean up query output but may make
-no other assumptions about its content or purpose.
-
-This module makes use of the PygreSQL Python bindings to libpq,
-PostgreSQL's native C client library.
-"""
-import pgdb
-
-
-class DBException(Exception):
-    """Exception class for database-related errors."""
-    pass
-    
-    
-class DBConnection(object):
-    """
-    Connection to CEO's backend database. All database queries
-    and updates are made via this class.
-    
-    Exceptions: (all methods)
-        DBException - on database query failure
-
-    Note: Updates will never take place until commit() is called.
-
-    Note: In the event that any method of this class raises a
-          DBException and that exception is caught, rollback()
-          must be called before further queries will be successful.
-    
-    Example:
-        connection = DBConnection()
-        connection.connect("localhost", "ceo")
-        
-        # make queries and updates, i.e.
-        connection.insert_member("Calum T. Dalek")
-        
-        connection.commit()
-        connection.disconnect()
-    """
-
-    def __init__(self):
-        self.cnx = None
-        self.cursor = None
-
-          
-    def connect(self, hostname=None, database=None, username=None, password=None):
-        """
-        Establishes the connection to CEO's PostgreSQL database.
-        
-        Parameters:
-            hostname - hostname:port to connect to
-            database - name of database
-            username - user to authenticate as
-            password - password of username
-        """
-
-        if self.cnx: raise DBException("unable to connect: already connected")
-        
-        try:
-            self.cnx = pgdb.connect(host=hostname, database=database,
-                    user=username, password=password)
-            self.cursor = self.cnx.cursor()
-        except pgdb.Error, e:
-            raise DBException("unable to connect: %s" % e)
-
-
-    def disconnect(self):
-        """Closes the connection to CEO's PostgreSQL database."""
-
-        if self.cursor:
-            self.cursor.close()
-            self.cursor = None
-
-        if self.cnx:
-            self.cnx.close()
-            self.cnx = None
-
-    
-    def connected(self):
-        """Determine whether the connection has been established."""
-
-        return self.cnx != None
-
-
-    def commit(self):
-        """Commits the current transaction and starts a new one."""
-
-        self.cnx.commit()
-
-
-    def rollback(self):
-        """Aborts the current transaction."""
-
-        self.cnx.rollback()
-
-
-
-    ### Member-related methods ###
-    
-    def select_members(self, sql, params=None):
-        """
-        Retrieves a list CSC members selected by given SQL statement.
-        
-        This is a helper function that should generally not be called directly.
-        
-        Parameters:
-            sql    - the SELECT sql statement
-            params - parameters for the SQL statement
-
-        The sql statement must select the six columns
-        (memberid, name, studentid, program, type, userid)
-        from the members table in that order.
-        
-        Returns: a memberid-keyed dictionary whose values are
-                 column-keyed dictionaries with member attributes
-        """
-        
-        # retrieve a list of all members
-        try:
-            self.cursor.execute(sql, params)
-            members_list = self.cursor.fetchall()
-        except pgdb.Error, e:
-            raise DBException("SELECT statement failed: %s" % e)
-        
-        # build a dictionary of dictionaries from the result (a list of lists)
-        members_dict = {}
-        for member in members_list:
-            memberid, name, studentid, program, type, userid = member
-            members_dict[memberid] = {
-                'memberid': member[0],
-                'name': member[1],
-                'studentid': member[2],
-                'program': member[3],
-                'type': member[4],
-                'userid': member[5],
-            }
-
-        return members_dict
-
-
-    def select_single_member(self, sql, params=None):
-        """
-        Retrieves a single member by memberid.
-
-        This is a helper function that should generally not be called directly.
-        
-        See: self.select_members()
-
-        Returns: a column-keyed dictionary with member attributes, or
-                 None if no member matching member exists
-        """
-
-        # retrieve the member
-        results = self.select_members(sql, params)
-
-        # too many members returned
-        if len(results) > 1:
-            raise DBException("multiple members selected: sql='%s' params=%s" % (sql, repr(params)))
-
-        # no such member
-        elif len(results) < 1:
-            return None
-
-        # return the single match
-        memberid = results.keys()[0]
-        return results[memberid]
-
-   
-    def select_all_members(self):
-        """
-        Retrieves a list of all CSC members (past and present).
-
-        See: self.select_members()
-        
-        Example: connection.select_all_members() -> {
-                     0:    { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
-                     3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
-                     ...
-                 }
-        """
-        sql = "SELECT memberid, name, studentid, program, type, userid FROM members"
-        return self.select_members(sql)
-        
-    
-    def select_members_by_name(self, name_re):
-        """
-        Retrieves a list of all CSC members whose name matches name_re.
-        
-        See: self.select_members()
-        
-        Example: connection.select_members_by_name('Michael') -> {
-                     3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
-                     ...
-                 }
-        """
-        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE name ~* %s"
-        params = [ str(name_re) ]
-     
-        return self.select_members(sql, params)
-
-    
-    def select_members_by_term(self, term):
-        """
-        Retrieves a list of all CSC members who were members in the specified term.
-        
-        See: self.select_members()
-        
-        Example: connection.select_members_by_term('f2006') -> {
-                     3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
-                     ...
-                 }
-        """
-        sql = "SELECT members.memberid, name, studentid, program, type, userid FROM members JOIN terms ON members.memberid=terms.memberid WHERE term=%s"
-        params = [ str(term) ]
-        
-        return self.select_members(sql, params)
-
-    
-    def select_member_by_id(self, memberid):
-        """
-        Retrieves a single member by memberid.
-
-        See: self.select_single_member()
-
-        Example: connection.select_member_by_id(0) ->
-                 { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
-        """
-        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE memberid=%d"
-        params = [ int(memberid) ]
-
-        return self.select_single_member(sql, params)
-
-    
-    def select_member_by_account(self, username):
-        """
-        Retrieves a single member by UNIX account username.
-
-        See: self.select_single_member()
-
-        Example: connection.select_member_by_account('ctdalek') ->
-                 { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
-        """
-        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE userid=%s"
-        params = [ username ]
-
-        return self.select_single_member(sql, params)
-
-
-    def select_member_by_studentid(self, studentid):
-        """
-        Retrieves a single member by student id number.
-
-        See: self.select_single_member()
-
-        Example: connection.select_member_by_studentid('nnnnnnnn') ->
-                 { 'memberid': 3349, 'name': 'Michael Spang' ...}
-        """
-        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE studentid=%s"
-        params = [ studentid ]
-
-        return self.select_single_member(sql, params)
-
-    
-    def insert_member(self, name, studentid=None, program=None):
-        """
-        Creates a member with the specified attributes.
-
-        Parameters:
-            name      - full name of member
-            studentid - student id number
-            program   - program of study
-
-        Example: connection.insert_member('Michael Spang', '99999999', 'Math/CS') -> 3349
-
-        Returns: a memberid of created user
-        """
-        try:
-            # retrieve the next memberid
-            sql = "SELECT nextval('memberid_seq')"
-            self.cursor.execute(sql)
-            result = self.cursor.fetchone()
-            memberid = result[0]
-        
-            # insert the member
-            sql = "INSERT INTO members (memberid, name, studentid, program, type) VALUES (%d, %s, %s, %s, %s)"
-            params = [ memberid, name, studentid, program, 'user' ]
-            self.cursor.execute(sql, params)
-            
-            return memberid
-        except pgdb.Error, e:
-            raise DBException("failed to create member: %s" % e)
-
-    
-    def delete_member(self, memberid):
-        """
-        Deletes a member. Note that a member cannot
-        be deleted until it has been unregistered from
-        all terms.
-
-        Parameters:
-            memberid - the member id number to delete
-
-        Example: connection.delete_member(3349)
-        """
-        sql = "DELETE FROM members WHERE memberid=%d"
-        params = [ memberid ]
-
-        try:
-            self.cursor.execute(sql, params)
-        except pgdb.Error, e:
-            raise DBException("DELETE statement failed: %s" %e)
-
-    
-    def update_member(self, member):
-        """
-        Modifies member attributes.
-
-        Parameters:
-            member - a column-keyed dictionary with the new state of the member.
-                     member['memberid'] must be present. ommitted columns
-                     will not be changed. None is NULL.
-
-        Returns: the full new state of the member as a column-keyed dictionary
-
-        Example: connection.update_member({
-                     'memberid': 3349,
-                     'name': 'Michael C. Spang',
-                     'program': 'CS!'
-                 }) -> {
-                     'memberid': 3349,
-                     'name': 'Michael C. Spang',
-                     'program': CS!',
-                     'studentid': '99999999' # unchanged
-                 }
-
-        Equivalent Example:
-                 member = connection.select_member_by_id(3349)
-                 member['name'] = 'Michael C. Spang'
-                 member['program'] = 'CS!'
-                 connection.update_member(member) -> { see above }
-        """
-        try:
-            
-            # memberid to update
-            memberid = member['memberid']
-            
-            # retrieve current state of member
-            member_state = self.select_member_by_id(memberid)
-
-            # build a list of changes to make
-            changes = []
-            for column in member.keys():
-                if member[column] != member_state[column]:
-
-                    # column's value must be updated
-                    changes.append( (column, member[column]) )
-                    member_state[column] = member[column]
-            
-            # no changes?
-            if len(changes) < 1:
-                return member_state
-            
-            # make the necessary changes in an update statement
-            changes = zip(*changes)
-            sql = "UPDATE members SET " + ", ".join(["%s=%%s"] * len(changes[0])) % changes[0] + " WHERE memberid=%d"
-            params = changes[1] + ( memberid, )
-            self.cursor.execute(sql, params)
-
-            return member_state
-        except pgdb.Error, e:
-            raise DBException("member update failed: %s" % e)
-        
-
-
-    ### Term-related methods ###
-
-    def select_term(self, memberid, term):
-        """
-        Determines whether a member is registered for a term.
-        
-        Parameters:
-            memberid - the member id number
-            term     - the term to check
-
-        Returns: a matching term, or None
-
-        Example: connection.select_term(3349, 'f2006') -> 'f2006'
-        """
-        sql = "SELECT term FROM terms WHERE memberid=%d AND term=%s"
-        params = [ memberid, term ]
-
-        # retrieve matches
-        try:
-            self.cursor.execute(sql, params)
-            result = self.cursor.fetchall()
-        except pgdb.Error, e:
-            raise DBException("SELECT statement failed: %s" % e)
-
-        if len(result) > 1:
-            raise DBException("multiple rows in terms with memberid=%d term=%s" % (memberid, term))
-        elif len(result) == 0:
-            return None
-        else:
-            return result[0][0]
-
-
-    def select_terms(self, memberid):
-        """
-        Retrieves a list of terms a member is registered for.
-
-        Parameters:
-            memberid - the member id number
-
-        Returns: a sorted list of terms
-        
-        Example: connection.select_terms(3349) -> ['f2006']
-        """
-        sql = "SELECT term FROM terms WHERE memberid=%d"
-        params = [ memberid ]
-
-        # retrieve the list of terms
-        try:
-            self.cursor.execute(sql, params)
-            result = self.cursor.fetchall()
-        except pgdb.Error, e:
-            raise DBException("SELECT statement failed: %s" % e)
-        
-        result = [ row[0] for row in result ]
-
-        return result
-
-
-    def insert_term(self, memberid, term):
-        """
-        Registers a member for a term.
-
-        Parameters:
-            memberid - the member id number to register
-            term     - string representation of the term
-
-        Example: connection.insert_term(3349, 'f2006')
-        """
-        sql = "INSERT INTO terms (memberid, term) VALUES (%d, %s)"
-        params = [ memberid, term ]
-        
-        try:
-            self.cursor.execute(sql, params)
-        except pgdb.Error, e:
-            raise DBException("INSERT statement failed: %s" % e)
-
-
-    def delete_term(self, memberid, term):
-        """
-        Unregisters a member for a term.
-
-        Parameters:
-            memberid - the member id number to register
-            term     - string representation of the term
-        
-        Example: connection.delete_term(3349, 'f2006')
-        """
-        sql = "DELETE FROM terms WHERE memberid=%d and term=%s"
-        params = [ memberid, term ]
-
-        try:
-            self.cursor.execute(sql, params)
-        except pgdb.Error, e:
-            raise DBException("DELETE statement failed: %s" % e)
-
-    
-    def delete_term_all(self, memberid):
-        """
-        Unregisters a member for all registered terms.
-
-        Parameters:
-            memberid - the member id number to unregister
-        
-        Example: connection.delete_term_all(3349)
-        """
-        sql = "DELETE FROM terms WHERE memberid=%d"
-        params = [ memberid ]
-        
-        # retrieve a list of terms
-        try:
-            self.cursor.execute(sql, params)
-        except pgdb.Error, e:
-            raise DBException("DELETE statement failed: %s" % e)
-
-
-    ### Miscellaneous methods ###
-
-    def trim_memberid_sequence(self):
-        """
-        Sets the value of the member id sequence to the id of the newest
-        member. For use after extensive testing to prevent large
-        intervals of unused memberids.
-
-        Note: this does nothing unless the most recently added member(s) have been deleted
-        """
-        self.cursor.execute("SELECT setval('memberid_seq', (SELECT max(memberid) FROM members))")
-
-
-
-### Tests ###
-
-if __name__ == '__main__':
-    HOST = "localhost"
-    DATABASE = "ceo"
-
-    connection = DBConnection()
-
-    print "Running disconnect()"
-    connection.disconnect()
-
-    print "Running connect('%s', '%s')" % (HOST, DATABASE)
-    connection.connect(HOST, DATABASE)
-
-    print "Running select_all_members()", "->", len(connection.select_all_members()), "members"
-    print "Running select_member_by_id(0)", "->", connection.select_member_by_id(0)['userid']
-    print "Running select_members_by_name('Spang')", "->", connection.select_members_by_name('Spang').keys()
-    print "Running select_members_by_term('f2006')", "->", "[" + ", ".join(map(str, connection.select_members_by_term('f2006').keys()[0:10])) + " ...]"
-    
-    print "Running insert_member('test_member', '99999999', 'program')",
-    memberid = connection.insert_member('test_member', '99999999', 'program')
-    print "->", memberid
-
-    print "Running select_member_by_id(%d)" % memberid, "->", connection.select_member_by_id(memberid)
-    print "Running insert_term(%d, 'f2006')" % memberid
-    connection.insert_term(memberid, 'f2006')
-
-    print "Running select_terms(%d)" % memberid, "->", connection.select_terms(memberid)
-    print "Running update_member({'memberid':%d,'name':'test_updated','studentid':-1})" % memberid
-    connection.update_member({'memberid':memberid,'name':'test_updated','studentid':99999999})
-    print "Running select_member_by_id(%d)" % memberid, "->", connection.select_member_by_id(memberid)
-   
-    print "Running rollback()"
-    connection.rollback()
-
-    print "Resetting memberid sequence"
-    connection.trim_memberid_sequence()
-    
-    print "Running disconnect()"
-    connection.disconnect() 
diff --git a/py/csc/backend/ipc.py b/py/csc/backend/ipc.py
deleted file mode 100644 (file)
index 8348a57..0000000
+++ /dev/null
@@ -1,222 +0,0 @@
-# $Id: ipc.py 26 2006-12-20 21:25:08Z mspang $
-"""
-IPC Library Functions
-
-This module contains very UNIX-specific code to allow interactive
-communication with another program. For CEO they are required to
-talk to kadmin because there is no Kerberos administration Python
-module. Real bindings to libkadm5 are doable and thus a TODO.
-"""
-import os, pty, select
-
-
-class _pty_file(object):
-    """
-    A 'file'-like wrapper class for pseudoterminal file descriptors.
-    
-    This wrapper is necessary because Python has a nasty
-    habit of throwing OSError at pty EOF.
-      
-    This class also implements timeouts for read operations
-    which are handy for avoiding deadlock when both
-    processes are blocked in a read().
-      
-    See the Python documentation of the file class
-    for explanation of the methods.
-    """
-    def __init__(self, fd):
-        self.fd = fd
-        self.buffer = ''
-        self.closed = False
-    def __repr__(self):
-        status='open'
-        if self.closed:
-            status = 'closed'
-        return "<" + status + " pty '" + os.ttyname(self.fd) + "'>"
-    def read(self, size=-1, block=True, timeout=0.1):
-        if self.closed: raise ValueError
-        if size < 0:
-            data = None
-
-            # read data, catching OSError as EOF
-            try:
-                while data != '':
-                
-                    # wait timeout for the pty to become ready, otherwise stop reading
-                    if not block and len(select.select([self.fd],[],[], timeout)[0]) == 0:
-                       break
-                       
-                    data = os.read(self.fd, 65536)
-                    self.buffer += data
-            except OSError:
-                pass
-            
-            data = self.buffer
-            self.buffer = ''
-            return data
-        else:
-            if len(self.buffer) < size:
-
-                # read data, catching OSError as EOF
-                try:
-                    
-                    # wait timeout for the pty to become ready, then read
-                    if block or len(select.select([self.fd],[],[], timeout)[0]) != 0:
-                        self.buffer += os.read(self.fd, size - len(self.buffer) )
-                    
-                except OSError:
-                    pass
-
-            data = self.buffer[:size]
-            self.buffer = self.buffer[size:]
-            return data
-    def readline(self, size=-1, block=True, timeout=0.1):
-        data = None
-
-        # read data, catching OSError as EOF
-        try:
-            while data != '' and self.buffer.find("\n") == -1 and (size < 0 or len(self.buffer) < size):
-
-                # wait timeout for the pty to become ready, otherwise stop reading
-                if not block and len(select.select([self.fd],[],[], timeout)[0]) == 0:
-                   break
-                 
-                data = os.read(self.fd, 128)
-                self.buffer += data
-        except OSError:
-            pass
-            
-        split_index = self.buffer.find("\n") + 1
-        if split_index < 0:
-            split_index = len(self.buffer)
-        if size >= 0 and split_index > size:
-            split_index = size
-        line = self.buffer[:split_index]
-        self.buffer = self.buffer[split_index:]
-        return line
-    def readlines(self, sizehint=None, block=True, timeout=0.1):
-        lines = []
-        line = None
-        while True:
-            line = self.readline(-1, False, timeout)
-            if line == '': break
-            lines.append(line)
-        return lines
-    def write(self, data):
-        if self.closed: raise ValueError
-        os.write(self.fd, data)
-    def writelines(self, lines):
-        for line in lines:
-            self.write(line)
-    def __iter__(self):
-        return self
-    def next(self):
-        line = self.readline()
-        if line == '':
-            raise StopIteration
-        return line
-    def isatty(self):
-        if self.closed: raise ValueError
-        return os.isatty(self.fd)
-    def fileno(self):
-        if self.closed: raise ValueError
-        return self.fd
-    def flush(self):
-        if self.closed: raise ValueError
-        os.fsync(self.fd)
-    def close(self):
-        if not self.closed: os.close(self.fd)
-        self.closed = True
-            
-
-def popeni(command, args, env=None):
-    """
-    Open an interactive session with another command.
-
-    Parameters:
-        command - the command to run (full path)
-        args    - a list of arguments to pass to command
-        env     - optional environment for command
-
-    Returns: (pid, stdout, stdIn)
-    """
-    
-    # use a pipe to send data to the child
-    child_stdin, parent_stdin = os.pipe()
-
-    # a pipe for receiving data would cause buffering and
-    # is therefore not suitable for interactive communication
-    # i.e. parent_stdout, child_stdout = os.pipe()
-
-    # therefore a pty must be used instead
-    master, slave = pty.openpty()
-    # collect both stdout and stderr on the pty
-    parent_stdout, child_stdout = master, slave
-    parent_stderr, child_stderr = master, slave
-
-    # fork the child to communicate with
-    pid = os.fork()
-
-    # child process
-    if pid == 0:
-     
-        # close all of the parent's fds
-        os.close(parent_stdin)
-        if parent_stdout != parent_stdin:
-            os.close(parent_stdout)
-        if parent_stderr != parent_stdin and parent_stderr != parent_stdout:
-            os.close(parent_stderr)
-    
-        # if stdout is a terminal, set it to the controlling terminal
-        if os.isatty(child_stdout):
-
-            # determine the filename of the tty
-            tty = os.ttyname(child_stdout)
-        
-            # create a new session to disconnect
-            # from the parent's controlling terminal
-            os.setsid()
-    
-            # set the controlling terminal to the pty
-            # by opening it (and closing it again since
-            # it's already open as child_stdout)
-            fd = os.open(tty, os.O_RDWR);
-            os.close(fd)
-
-        # init stdin/out/err
-        os.dup2(child_stdin,  0)
-        os.dup2(child_stdout, 1)
-        if child_stderr >= 0:
-            os.dup2(child_stderr, 2)
-    
-        # finally, execute the child
-        if env:
-            os.execv(command, args, env)
-        else:
-            os.execv(command, args)
-
-    # parent process
-    else:
-
-        # close all of the child's fds
-        os.close(child_stdin)
-        if child_stdout != child_stdin:
-            os.close(child_stdout)
-        if child_stderr >= 0 and child_stderr != child_stdin and child_stderr != child_stdout:
-            os.close(child_stderr)
-
-        return pid, _pty_file(parent_stdout), os.fdopen(parent_stdin, 'w')
-
-
-### Tests ###
-
-if __name__ == '__main__':
-
-    import sys
-    pid, recv, send = popeni('/usr/sbin/kadmin.local', ['kadmin'])
-
-    send.write("listprincs\n")
-    send.flush()
-
-    print recv.readlines()
diff --git a/py/csc/backend/krb.py b/py/csc/backend/krb.py
deleted file mode 100644 (file)
index 23d021b..0000000
+++ /dev/null
@@ -1,448 +0,0 @@
-# $Id: krb.py 40 2006-12-29 00:40:31Z mspang $
-"""
-Kerberos Backend Interface
-
-This module is intended to be a thin wrapper around Kerberos operations.
-Methods on the connection object correspond in a straightforward way to
-calls to the Kerberos Master server.
-
-A Kerberos principal is the second half of a CSC UNIX account. The principal
-stores the user's password and and is used for all authentication on CSC
-systems. Accounts that do not authenticate (e.g. club accounts) do not need
-a Kerberos principal.
-
-Unfortunately, there are no Python bindings to libkadm at this time. As a
-temporary workaround, This module communicates with the kadmin CLI interface
-via a pseudoterminal and pipe.
-"""
-import os
-import ipc
-
-
-class KrbException(Exception):
-    """Exception class for all Kerberos-related errors."""
-    pass
-
-
-class KrbConnection(object):
-    """
-    Connection to the Kerberos master server (kadmind). All Kerberos
-    principal updates are made via this class.
-
-    Exceptions: (all methods)
-        KrbException - on query/update failure
-
-    Example:
-        connection = KrbConnection()
-        connection.connect(...)
-
-        # make queries and updates, e.g.
-        connection.delete_principal("mspang")
-
-        connection.disconnect()
-    """
-
-    def __init__(self):
-        self.pid = None
-    
-
-    def connect(self, principal, keytab):
-        """
-        Establishes the connection to the Kerberos master server.
-
-        Parameters:
-            principal - the Kerberos princiapl to authenticate as
-            keytab    - keytab filename for authentication
-
-        Example: connection.connect('ceo/admin@CSCLUB.UWATERLOO.CA', '/etc/ceo.keytab')
-        """
-
-        # check keytab
-        if not os.access(keytab, os.R_OK):
-            raise KrbException("cannot access Kerberos keytab: %s" % keytab)
-        
-        # command to run
-        kadmin = '/usr/sbin/kadmin'
-        kadmin_args = ['kadmin', '-p', principal, '-kt', keytab]
-        
-        # fork the kadmin command
-        self.pid, self.kadm_out, self.kadm_in = ipc.popeni(kadmin, kadmin_args)
-        
-        # read welcome messages
-        welcome = self.read_result()
-        
-        # sanity checks on welcome messages
-        for line in welcome:
-            
-            # ignore auth message
-            if line.find("Authenticating") == 0:
-                continue
-
-            # ignore log file message
-            elif line.find("kadmin.log") != -1:
-                continue
-
-            # error message?
-            else:
-                raise KrbException("unexpected kadmin output: " + welcome[0])
-    
-    
-    def disconnect(self):
-        """Close the connection to the master server."""
-        
-        if self.pid:
-            
-            # close the pipe connected to kadmin's standard input
-            self.kadm_in.close()
-            
-            # close the master pty connected to kadmin's stdout
-            try:
-                self.kadm_out.close()
-            except OSError:
-                pass
-
-            # wait for kadmin to terminate
-            os.waitpid(self.pid, 0)
-            self.pid = None
-
-
-    def connected(self):
-        """Determine whether the connection has been established."""
-
-        return self.pid != None
-
-
-
-    ### Helper Methods ###
-    
-    def read_result(self):
-        """
-        Helper function to read output of kadmin until it
-        prompts for input.
-
-        Returns: a list of lines returned by kadmin
-        """
-
-        # list of lines output by kadmin
-        result = []
-
-        # the kadmin prompt that signals the end output
-        # note: KADMIN_ARGS[0] must be "kadmin" or the actual prompt will differ
-        prompt = "kadmin:"
-
-        # timeout variables. the timeout will start at timeout and
-        # increase up to max_timeout when read() returns nothing (i.e., times out)
-        timeout = 0.01
-        timeout_increment = 0.10
-        timeout_maximum = 1.00
-        
-        # input loop: read from kadmin until the kadmin prompt
-        buffer = ''
-        while True:
-            
-            # attempt to read any available data
-            data = self.kadm_out.read(block=False, timeout=timeout)
-            buffer += data
-
-            # nothing was read
-            if data == '':
-                
-                # so wait longer for data next time
-                if timeout < timeout_maximum:
-                    timeout += timeout_increment
-                    continue
-
-                # give up after too much waiting
-                else:
-
-                    # check kadmin status
-                    status = os.waitpid(self.pid, os.WNOHANG)
-                    if status[0] == 0:
-
-                        # kadmin still alive
-                        raise KrbException("timeout while reading response from kadmin")
-
-                    else:
-
-                        # kadmin died!
-                        raise KrbException("kadmin died while reading response")
-
-            # break into lines and save all but the final
-            # line (which is incomplete) into result
-            lines = buffer.split("\n")
-            buffer = lines[-1]
-            lines = lines[:-1]
-            for line in lines:
-                line = line.strip()
-                result.append(line)
-           
-            # if the incomplete lines in the buffer is the kadmin prompt,
-            # then the result is complete and may be returned
-            if buffer.strip() == prompt:
-                break
-
-        return result
-    
-    
-    def execute(self, command):
-        """
-        Helper function to execute a kadmin command.
-
-        Parameters:
-            command - the command to execute
-        
-        Returns: a list of lines output by the command
-        """
-        
-        # there should be no remaining output from the previous
-        # command. if there is then something is broken.
-        stale_output = self.kadm_out.read(block=False, timeout=0)
-        if stale_output != '':
-            raise KrbException("unexpected kadmin output: " + stale_output)
-        
-        # send the command to kadmin
-        self.kadm_in.write(command + "\n")
-        self.kadm_in.flush()
-        
-        # read the command output and return it
-        result = self.read_result()
-        return result
-    
-
-    
-    ### Commands ###
-    
-    def list_principals(self):
-        """
-        Retrieve a list of Kerberos principals.
-
-        Returns: a list of principals
-
-        Example: connection.list_principals() -> [
-                     "ceo/admin@CSCLUB.UWATERLOO.CA",
-                     "sysadmin/admin@CSCLUB.UWATERLOO.CA",
-                     "mspang@CSCLUB.UWATERLOO.CA",
-                 ]
-        
-        """
-        
-        principals = self.execute("list_principals")
-
-        # assuming that there at least some host principals
-        if len(principals) < 1:
-            raise KrbException("no kerberos principals")
-
-        # detect error message
-        if principals[0].find("kadmin:") == 0:
-            raise KrbException("list_principals returned error: " + principals[0])
-
-        # verify principals are well-formed
-        for principal in principals:
-            if principal.find("@") == -1:
-                raise KrbException('malformed pricipal: "' + principal + '"')
-
-        return principals
-    
-    
-    def get_principal(self, principal):
-        """
-        Retrieve principal details.
-
-        Returns: a dictionary of principal attributes
-
-        Example: connection.get_principal("ceo/admin@CSCLUB.UWATERLOO.CA") -> {
-                     "Principal": "ceo/admin@CSCLUB.UWATERLOO.CA",
-                     "Policy": "[none]",
-                     ...
-                 }
-        """
-        
-        output = self.execute('get_principal "' + principal + '"')
-        
-        # detect error message
-        if output[0].find("kadmin:") == 0:
-            raise KrbException("get_principal returned error: " + output[0])
-
-        # detect more errors
-        if output[0].find("get_principal: ") == 0:
-            
-            message = output[0][15:]
-            
-            # principal does not exist => None
-            if message.find("Principal does not exist") == 0:
-                return None
-
-        # dictionary to store attributes
-        principal_attributes = {}
-
-        # attributes that will not be returned
-        ignore_attributes = ['Key']
-
-        # split output into a dictionary of attributes
-        for line in output:
-            key, value = line.split(":", 1)
-            value = value.strip()
-            if not key in ignore_attributes:
-                principal_attributes[key] = value
-                
-        return principal_attributes
-    
-    
-    def get_privs(self):
-        """
-        Retrieve privileges of the current principal.
-        
-        Returns: a list of privileges
-
-        Example: connection.get_privs() ->
-                     [ "GET", "ADD", "MODIFY", "DELETE" ]
-        """
-        
-        output = self.execute("get_privs")
-
-        # one line of output is expected
-        if len(output) > 1:
-            raise KrbException("unexpected output of get_privs: " + output[1])
-
-        # detect error message
-        if output[0].find("kadmin:") == 0:
-            raise KrbException("get_privs returned error: " + output[0])
-
-        # parse output by removing the prefix and splitting it around spaces
-        if output[0][:20] != "current privileges: ":
-            raise KrbException("malformed get_privs output: " + output[0])
-        privs = output[0][20:].split(" ")
-
-        return privs
-    
-    
-    def add_principal(self, principal, password):
-        """
-        Create a new principal.
-
-        Parameters:
-            principal - the name of the principal
-            password  - the principal's initial password
-        
-        Example: connection.add_principal("mspang@CSCLUB.UWATERLOO.CA", "opensesame")
-        """
-
-        # exec the add_principal command
-        if password.find('"') == -1:
-            self.kadm_in.write('add_principal -pw "' + password + '" "' + principal + '"\n')
-            
-        # fools at MIT didn't bother implementing escaping, so passwords
-        # that contain double quotes must be treated specially
-        else:
-            self.kadm_in.write('add_principal "' + principal + '"\n')
-            self.kadm_in.write(password + "\n" + password + "\n")
-
-        # send request and read response
-        self.kadm_in.flush()
-        output = self.read_result()
-
-        # verify output
-        created = False
-        for line in output:
-
-            # ignore NOTICE lines
-            if line.find("NOTICE:") == 0:
-                continue
-
-            # ignore prompts
-            elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0:
-                continue
-
-            # record whether success message was encountered
-            elif line.find("Principal") == 0 and line.find("created.") != 0:
-                created = True
-
-            # error messages
-            elif line.find("add_principal:") == 0 or line.find("kadmin:") == 0:
-                
-                # principal exists
-                if line.find("already exists") != -1:
-                    raise KrbException("principal already exists")
-
-                # misc errors
-                else:
-                    raise KrbException(line)
-
-            # unknown output
-            else:
-                raise KrbException("unexpected add_principal output: " + line)
-           
-        # ensure success message was received
-        if not created:
-            raise KrbException("did not receive principal created in response")
-    
-    
-    def delete_principal(self, principal):
-        """
-        Delete a principal.
-
-        Parameters:
-            principal - the principal name
-
-        Example: connection.delete_principal("mspang@CSCLUB.UWATERLOO.CA")
-        """
-        
-        # exec the delete_principal command and read response
-        self.kadm_in.write('delete_principal -force "' + principal + '"\n')
-        self.kadm_in.flush()
-        output = self.read_result()
-
-        # verify output
-        deleted = False
-        for line in output:
-
-            # ignore reminder
-            if line.find("Make sure that") == 0:
-                continue
-
-            # record whether success message was encountered
-            elif line.find("Principal") == 0 and line.find("deleted.") != -1:
-                deleted = True
-
-            # error messages
-            elif line.find("delete_principal:") == 0 or line.find("kadmin:") == 0:
-                
-                # principal exists
-                if line.find("does not exist") != -1:
-                    raise KrbException("principal does not exist")
-
-                # misc errors
-                else:
-                    raise KrbException(line)
-
-            # unknown output
-            else:
-                raise KrbException("unexpected delete_principal output: " + line)
-           
-        # ensure success message was received
-        if not deleted:
-            raise KrbException("did not receive principal deleted")
-        
-
-
-### Tests ###
-
-if __name__ == '__main__':
-    PRINCIPAL = 'ceo/admin@CSCLUB.UWATERLOO.CA'
-    KEYTAB = 'ceo.keytab'
-    
-    connection = KrbConnection()
-    print "running disconnect()"
-    connection.disconnect()
-    print "running connect('%s', '%s')" % (PRINCIPAL, KEYTAB)
-    connection.connect(PRINCIPAL, KEYTAB)
-    print "running list_principals()", "->", "[" + ", ".join(map(repr,connection.list_principals()[0:3])) + " ...]"
-    print "running get_privs()", "->", str(connection.get_privs())
-    print "running add_principal('testtest', 'BLAH')"
-    connection.add_principal("testtest", "FJDSLDLFKJSF")
-    print "running get_principal('testtest')", "->", '(' + connection.get_principal("testtest")['Principal'] + ')'
-    print "running delete_principal('testtest')"
-    connection.delete_principal("testtest")
-    print "running disconnect()"
-    connection.disconnect()
-
diff --git a/py/csc/backend/ldapi.py b/py/csc/backend/ldapi.py
deleted file mode 100644 (file)
index cf32e8f..0000000
+++ /dev/null
@@ -1,577 +0,0 @@
-# $Id: ldapi.py 41 2006-12-29 04:22:31Z mspang $
-"""
-LDAP Backend Interface
-
-This module is intended to be a thin wrapper around LDAP operations.
-Methods on the connection object correspond in a straightforward way
-to LDAP queries and updates.
-
-A LDAP entry is the most important component of a CSC UNIX account.
-The entry contains the username, user id number, real name, shell,
-and other important information. All non-local UNIX accounts must
-have an LDAP entry, even if the account does not log in directly.
-
-This module makes use of python-ldap, a Python module with bindings
-to libldap, OpenLDAP's native C client library.
-"""
-import ldap.modlist
-
-
-class LDAPException(Exception):
-    """Exception class for LDAP-related errors."""
-
-
-class LDAPConnection(object):
-    """
-    Connection to the LDAP directory. All directory
-    queries and updates are made via this class.
-
-    Exceptions: (all methods)
-        LDAPException - on directory query failure
-
-    Example:
-         connection = LDAPConnection()
-         connection.connect(...)
-
-         # make queries and updates, e.g.
-         connection.user_delete('mspang')
-
-         connection.disconnect()
-    """
-
-    def __init__(self):
-        self.ldap = None
-
-    
-    def connect(self, server, bind_dn, bind_pw, user_base, group_base):
-        """
-        Establish a connection to the LDAP Server.
-
-        Parameters:
-            server     - connection string (e.g. ldap://foo.com, ldaps://bar.com)
-            bind_dn    - distinguished name to bind to
-            bind_pw    - password of bind_dn
-            user_base  - base of the users subtree
-            group_base - baes of the group subtree
-
-        Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
-                     'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
-                     'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
-        
-        """
-
-        if bind_pw == None: bind_pw = ''
-
-        try:
-
-            # open the connection
-            self.ldap = ldap.initialize(server)
-
-            # authenticate as ceo
-            self.ldap.simple_bind_s(bind_dn, bind_pw)
-
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to connect: %s" % e)
-
-        self.user_base = user_base
-        self.group_base = group_base
-
-
-    def disconnect(self):
-        """Close the connection to the LDAP server."""
-        
-        if self.ldap:
-
-            # close connection
-            try:
-                self.ldap.unbind_s()
-                self.ldap = None
-            except ldap.LDAPError, e:
-                raise LDAPException("unable to disconnect: %s" % e)
-
-
-    def connected(self):
-        """Determine whether the connection has been established."""
-
-        return self.ldap != None
-
-
-
-    ### Helper Methods ###
-
-    def lookup(self, dn):
-        """
-        Helper method to retrieve the attributes of an entry.
-
-        Parameters:
-            dn - the distinguished name of the directory entry
-
-        Returns: a dictionary of attributes of the matched dn, or
-                 None of the dn does not exist in the directory
-        """
-
-        # search for the specified dn
-        try:
-            matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
-        except ldap.NO_SUCH_OBJECT:
-            return None
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
-            
-        # this should never happen due to the nature of DNs
-        if len(matches) > 1:
-            raise LDAPException("duplicate dn in ldap: " + dn)
-        
-        # return the attributes of the single successful match
-        else:
-            match = matches[0]
-            match_dn, match_attributes = match
-            return match_attributes
-
-
-    
-    ### User-related Methods ###
-
-    def user_lookup(self, uid):
-        """
-        Retrieve the attributes of a user.
-
-        Parameters:
-            uid - the UNIX user accound name of the user
-
-        Returns: attributes of user with uid
-
-        Example: connection.user_lookup('mspang') ->
-                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
-        """
-        
-        dn = 'uid=' + uid + ',' + self.user_base
-        return self.lookup(dn)
-        
-
-    def user_search(self, filter):
-        """
-        Helper for user searches.
-
-        Parameters:
-            filter - LDAP filter string to match users against
-
-        Returns: the list of uids matched
-        """
-
-        # search for entries that match the filter
-        try:
-            matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, filter)
-        except ldap.LDAPError, e:
-            raise LDAPException("user search failed: %s" % e)
-        
-        # list for uids found
-        uids = []
-        
-        for match in matches:
-            dn, attributes = match
-            
-            # uid is a required attribute of posixAccount
-            if not attributes.has_key('uid'):
-                raise LDAPException(dn + ' (posixAccount) has no uid')
-            
-            # do not handle the case of multiple usernames in one entry (yet)
-            elif len(attributes['uid']) > 1:
-                raise LDAPException(dn + ' (posixAccount) has multiple uids')
-            
-            # append the sole uid of this match to the list
-            uids.append( attributes['uid'][0] )
-
-        return uids
-
-
-    def user_search_id(self, uidNumber):
-        """
-        Retrieves a list of users with a certain UNIX uid number.
-
-        LDAP (or passwd for that matter) does not enforce any
-        restriction on the number of accounts that can have
-        a certain UID. Therefore this method returns a list of matches.
-
-        Parameters:
-            uidNumber - the user id of the accounts desired
-
-        Returns: the list of uids matched
-
-        Example: connection.user_search_id(21292) -> ['mspang']
-        """
-
-        # search for posixAccount entries with the specified uidNumber
-        filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
-        return self.user_search(filter)
-
-
-    def user_search_gid(self, gidNumber):
-        """
-        Retrieves a list of users with a certain UNIX gid number.
-
-        Parameters:
-            gidNumber - the group id of the accounts desired
-
-        Returns: the list of uids matched
-        """
-
-        # search for posixAccount entries with the specified gidNumber
-        filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
-        return self.user_search(filter)
-
-
-    def user_add(self, uid, cn, loginShell, uidNumber, gidNumber, homeDirectory, gecos):
-        """
-        Adds a user to the directory.
-
-        Parameters:
-            uid           - the UNIX username for the account
-            cn            - the full name of the member
-            userPassword  - password of the account (our setup does not use this)
-            loginShell    - login shell for the user
-            uidNumber     - the UNIX user id number
-            gidNumber     - the UNIX group id number
-            homeDirectory - home directory for the user
-            gecos         - comment field (usually stores miscellania)
-
-        Example: connection.user_add('mspang', 'Michael Spang',
-                     '/bin/bash', 21292, 100, '/users/mspang',
-                     'Michael Spang,,,')
-        """
-        
-        dn = 'uid=' + uid + ',' + self.user_base
-        attrs = {
-            'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
-            'uid': [ uid ],
-            'cn': [ cn ],
-            'loginShell': [ loginShell ],
-            'uidNumber': [ str(uidNumber) ],
-            'gidNumber': [ str(gidNumber) ],
-            'homeDirectory': [ homeDirectory ],
-            'gecos': [ gecos ],
-        }
-
-        try:
-            modlist = ldap.modlist.addModlist(attrs)
-            self.ldap.add_s(dn, modlist)
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to add: %s" % e)
-
-
-    def user_modify(self, uid, attrs):
-        """
-        Update user attributes in the directory.
-
-        Parameters:
-            uid   - username of the user to modify
-            entry - dictionary as returned by user_lookup() with changes to make.
-                    omitted attributes are DELETED.
-
-        Example: user = user_lookup('mspang')
-                 user['uidNumber'] = [ '0' ]
-                 connection.user_modify('mspang', user)
-        """
-
-        # distinguished name of the entry to modify
-        dn = 'uid=' + uid + ',' + self.user_base
-
-        # retrieve current state of user
-        old_user = self.user_lookup(uid)
-
-        try:
-            
-            # build list of modifications to make
-            changes = ldap.modlist.modifyModlist(old_user, attrs)
-
-            # apply changes
-            self.ldap.modify_s(dn, changes)
-
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to modify: %s" % e)
-
-
-    def user_delete(self, uid):
-        """
-        Removes a user from the directory.
-
-        Parameters:
-            uid - the UNIX username of the account
-        
-        Example: connection.user_delete('mspang')
-        """
-        
-        try:
-            dn = 'uid=' + uid + ',' + self.user_base
-            self.ldap.delete_s(dn)
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to delete: %s" % e)
-
-
-
-    ### Group-related Methods ###
-
-    def group_lookup(self, cn):
-        """
-        Retrieves the attributes of a group.
-
-        Parameters:
-            cn - the UNIX group name to lookup
-
-        Returns: attributes of group with cn
-
-        Example: connection.group_lookup('office') -> {
-                     'cn': 'office',
-                     'gidNumber', '1001',
-                     ...
-                 }
-        """
-        
-        dn = 'cn=' + cn + ',' + self.group_base
-        return self.lookup(dn)
-                                                                                    
-
-    def group_search_id(self, gidNumber):
-        """
-        Retrieves a list of groups with the specified UNIX group number.
-        
-        Parameters:
-            gidNumber - the group id of the groups desired
-
-        Returns: a list of groups with gid gidNumber
-
-        Example: connection.group_search_id(1001) -> ['office']
-        """
-
-        # search for posixAccount entries with the specified uidNumber
-        try:
-            filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
-            matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, filter)
-        except ldap.LDAPError,e :
-            raise LDAPException("group search failed: %s" % e)
-
-        # list for groups found
-        group_cns = []
-
-        for match in matches:
-            dn, attributes = match
-
-            # cn is a required attribute of posixGroup
-            if not attributes.has_key('cn'):
-                raise LDAPException(dn + ' (posixGroup) has no cn')
-
-            # do not handle the case of multiple cns for one group (yet)
-            elif len(attributes['cn']) > 1:
-                raise LDAPException(dn + ' (posixGroup) has multiple cns')
-
-            # append the sole uid of this match to the list
-            group_cns.append( attributes['cn'][0] )
-
-        return group_cns
-
-
-    def group_add(self, cn, gidNumber):
-        """
-        Adds a group to the directory.
-
-        Parameters:
-            cn        - the name of the group
-            gidNumber - the number of the group
-
-        Example: connection.group_add('office', 1001)
-        """
-        
-        dn = 'cn=' + cn + ',' + self.group_base
-        attrs = {
-            'objectClass': [ 'top', 'posixGroup' ],
-            'cn': [ cn ],
-            'gidNumber': [ str(gidNumber) ],
-        }
-
-        try:
-            modlist = ldap.modlist.addModlist(attrs)
-            self.ldap.add_s(dn, modlist)
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to add group: %s" % e)
-
-
-    def group_modify(self, cn, attrs):
-        """
-        Update group attributes in the directory.
-        
-        The only available updates are fairly destructive
-        (rename or renumber) but this method is provided
-        for completeness.
-
-        Parameters:
-            cn    - name of the group to modify
-            entry - dictionary as returned by group_lookup() with changes to make.
-                    omitted attributes are DELETED.
-
-        Example: group = group_lookup('office')
-                 group['gidNumber'] = [ str(connection.first_id(20000, 40000)) ]
-                 del group['memberUid']
-                 connection.group_modify('office', group)
-        """
-
-        # distinguished name of the entry to modify
-        dn = 'cn=' + cn + ',' + self.group_base
-
-        # retrieve current state of group
-        old_group = self.group_lookup(cn)
-
-        try:
-            
-            # build list of modifications to make
-            changes = ldap.modlist.modifyModlist(old_group, attrs)
-
-            # apply changes
-            self.ldap.modify_s(dn, changes)
-
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to modify: %s" % e)
-
-
-    def group_delete(self, cn):
-        """
-        Removes a group from the directory."
-
-        Parameters:
-            cn - the name of the group
-
-        Example: connection.group_delete('office')
-        """
-        
-        try:
-            dn = 'cn=' + cn + ',' + self.group_base
-            self.ldap.delete_s(dn)
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to delete group: %s" % e)
-
-
-    def group_members(self, cn):
-        """
-        Retrieves a group's members.
-
-        Parameters:
-            cn - the name of the group
-
-        Example: connection.group_members('office') ->
-                 ['sfflaw', 'jeperry', 'cschopf' ...]
-        """
-
-        group = self.group_lookup(cn)
-        return group.get('memberUid', None)
-
-
-    ### Miscellaneous Methods ###
-    
-    def first_id(self, minimum, maximum):
-        """
-        Determines the first available id within a range.
-
-        To be "available", there must be neither a user
-        with the id nor a group with the id.
-
-        Parameters:
-            minimum - smallest uid that may be returned
-            maximum - largest uid that may be returned
-
-        Returns: the id, or None if there are none available
-
-        Example: connection.first_id(20000, 40000) -> 20018
-        """
-
-        # compile a list of used uids
-        try:
-            users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['uidNumber'])
-        except ldap.LDAPError, e:
-            raise LDAPException("search for uids failed: %s" % e)
-        uids = []
-        for user in users:
-            dn, attrs = user
-            uid = int(attrs['uidNumber'][0])
-            if minimum <= uid <= maximum:
-                uids.append(uid)
-
-        # compile a list of used gids
-        try:
-            groups = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)', ['gidNumber'])
-        except ldap.LDAPError, e:
-            raise LDAPException("search for gids failed: %s" % e)
-        gids = []
-        for group in groups:
-            dn, attrs = group
-            gid = int(attrs['gidNumber'][0])
-            if minimum <= gid <= maximum:
-                gids.append(gid)
-
-        # iterate through ids and return the first available
-        for id in xrange(minimum, maximum+1):
-            if not id in uids and not id in gids:
-                return id
-
-        # no suitable id was found
-        return None
-
-
-### Tests ###
-
-if __name__ == '__main__':
-    
-    password_file = 'ldap.ceo'
-    server   = 'ldaps:///'
-    base_dn  = 'dc=csclub,dc=uwaterloo,dc=ca'
-    bind_dn  = 'cn=ceo,' + base_dn
-    user_dn  = 'ou=People,' + base_dn
-    group_dn = 'ou=Group,' + base_dn
-    bind_pw = open(password_file).readline().strip()
-
-    connection = LDAPConnection()
-    print "running disconnect()"
-    connection.disconnect()
-    print "running connect('%s', '%s', '%s', '%s', '%s')" % (server, bind_dn, '***', user_dn, group_dn)
-    connection.connect(server, bind_dn, bind_pw, user_dn, group_dn)
-    print "running user_lookup('mspang')", "->", "(%s)" % connection.user_lookup('mspang')['uidNumber'][0]
-    print "running user_search_id(21292)", "->", connection.user_search_id(21292)
-    print "running first_id(20000, 40000)", "->",
-    first_id = connection.first_id(20000, 40000)
-    print first_id
-    print "running group_add('testgroup', %d)" % first_id
-    try:
-        connection.group_add('testgroup', first_id)
-    except Exception, e:
-        print "FAILED: %s (continuing)" % e
-    print "running user_add('testuser', 'Test User', '/bin/false', %d, %d, '/home/null', 'Test User,,,')" % (first_id, first_id)
-    try:
-        connection.user_add('testuser', 'Test User', '/bin/false', first_id, first_id, '/home/null', 'Test User,,,')
-    except Exception, e:
-        print "FAILED: %s (continuing)" % e
-    print "running user_lookup('testuser')", "->",
-    user = connection.user_lookup('testuser')
-    print repr(connection.user_lookup('testuser')['cn'][0])
-    user['homeDirectory'] = ['/home/changed']
-    user['loginShell'] = ['/bin/true']
-    print "running user_modify(...)"
-    connection.user_modify('testuser', user)
-    print "running user_lookup('testuser')", "->",
-    user = connection.user_lookup('testuser')
-    print '(%s, %s)' % (user['homeDirectory'], user['loginShell'])
-    print "running group_lookup('testgroup')", "->",
-    group = connection.group_lookup('testgroup')
-    print group
-    print "running group_modify(...)"
-    group['gidNumber'] = [str(connection.first_id(20000, 40000))]
-    group['memberUid'] = [ str(first_id) ]
-    connection.group_modify('testgroup', group)
-    print "running group_lookup('testgroup')", "->",
-    group = connection.group_lookup('testgroup')
-    print group
-    print "running user_delete('testuser')"
-    connection.user_delete('testuser')
-    print "running group_delete('testgroup')"
-    connection.group_delete('testgroup')
-    print "running user_search_gid(100)", "->", "[" + ", ".join(map(repr,connection.user_search_gid(100)[:10])) + " ...]"
-    print "running group_members('office')", "->", "[" + ", ".join(map(repr,connection.group_members('office')[:10])) + " ...]"
-    print "running disconnect()"
-    connection.disconnect()
diff --git a/py/csc/lib/__init__.py b/py/csc/lib/__init__.py
deleted file mode 100644 (file)
index 2902f1b..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Library Routines"""
-
-def read_config(config_file):
-
-    try:
-        conffile = open(config_file)
-    except IOError:
-        return None
-    
-    options = {}
-
-    while True:
-
-        line = conffile.readline()
-        if line == '':
-            break
-
-        if '#' in line:
-            line = line[:line.find('#')]
-
-        while len(line) > 1 and line[-2] == '\\':
-            line = line[:-2] + line[-1]
-            next = conffile.readline()
-            line += next
-            if next == '':
-                break
-
-        pair = map(str.strip, line.split('=', 1))
-        
-        if len(pair) == 2:
-            key, val = pair
-
-            if val[0] == val[-1] == '"':
-                val = val[1:-1]
-            else:
-                try:
-                    val = int(val)
-                except:
-                    pass
-            
-            options[key] = val
-        elif len(pair[0]) > 1:
-            key, = pair
-            options[key] = None
-
-    return options
diff --git a/pylib/csc/__init__.py b/pylib/csc/__init__.py
new file mode 100644 (file)
index 0000000..0583735
--- /dev/null
@@ -0,0 +1,19 @@
+# $Id: __init__.py 24 2006-12-18 20:23:12Z mspang $
+"""
+PyCSC - CSC Administrative Utilities
+
+Member Management:
+
+    ceo - legacy ceo interface
+
+Account Management:
+
+    ceo - legacy ceo interface
+
+Modules:
+
+    admin - administrative code (member and account management)
+    backend - backend interface code
+    ui - user interface code
+
+"""
diff --git a/pylib/csc/adm/__init__.py b/pylib/csc/adm/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pylib/csc/adm/accounts.py b/pylib/csc/adm/accounts.py
new file mode 100644 (file)
index 0000000..3586461
--- /dev/null
@@ -0,0 +1,232 @@
+# $Id: accounts.py 44 2006-12-31 07:09:27Z mspang $
+# UNIX Accounts Module
+import re
+from csc.backends import ldapi, krb
+from csc.common.conf import read_config
+
+CONFIG_FILE = '/etc/csc/accounts.cf'
+
+cfg = {}
+
+# error constants
+SUCCESS = 0
+LDAP_EXISTS = 1
+LDAP_NO_IDS = 2
+LDAP_NO_USER = 3
+KRB_EXISTS = 5
+KRB_NO_USER = 6
+BAD_USERNAME = 8
+BAD_REALNAME = 9
+
+# error messages
+errors = [ "Success", "LDAP: entry exists",
+    "LDAP: no user ids available", "LDAP: no such entry",
+    "KRB: principal exists", "KRB: no such principal",
+    "Invalid username", "Invalid real name"]
+
+
+class AccountException(Exception):
+    """Exception class for account-related errors."""
+
+
+def load_configuration():
+    """Load Accounts Configuration."""
+    
+    # configuration already loaded?
+    if len(cfg) > 0:
+        return
+    
+    # read in the file
+    cfg_tmp = read_config(CONFIG_FILE)
+
+    if not cfg_tmp:
+        raise AccountException("unable to read configuration file: %s" % CONFIG_FILE)
+
+    # check that essential fields are completed
+    mandatory_fields = [ 'minimum_id', 'maximum_id', 'shell', 'home',
+        'gid', 'server_url', 'users_base', 'groups_base', 'bind_dn',
+        'bind_password', 'realm', 'principal', 'keytab', 'username_regex',
+        'realname_regex'
+    ]
+
+    for field in mandatory_fields:
+        if not field in cfg_tmp:
+            raise AccountException("missing configuration option: %s" % field)
+        if not cfg_tmp[field]:
+            raise AccountException("null configuration option: %s" % field)
+    
+    # check that numeric fields are ints
+    numeric_fields = [ 'minimum_id', 'maximum_id', 'gid' ]
+
+    for field in numeric_fields:
+        if not type(cfg_tmp[field]) in (int, long):
+            raise AccountException("non-numeric value for configuration option: %s" % field)
+
+    # update the current configuration with the loaded values
+    cfg.update(cfg_tmp)
+        
+
+def create_account(username, password, realname='', gecos_other=''):
+    """
+    Creates a UNIX account for a member. This involves
+    first creating a directory entry, then creating
+    a Kerberos principal.
+
+    Parameters:
+        username - UNIX username for the member
+        realname - real name of the member
+        password - password for the account
+
+    Exceptions:
+        LDAPException - on LDAP failure
+        KrbException  - on Kerberos failure
+        
+    Returns:
+        SUCCESS      - on success
+        BAD_REALNAME - on badly formed real name
+        BAD_USERNAME - on badly formed user name
+        LDAP_EXISTS  - when the user exists in LDAP
+        LDAP_NO_IDS  - when no user ids are free
+        KRB_EXISTS   - when the user exists in Kerberos
+    """
+
+    # Load Configuration
+    load_configuration()
+
+    ### Connect to the Backends ###
+
+    ldap_connection = ldapi.LDAPConnection()
+    krb_connection = krb.KrbConnection()
+
+    try:
+
+        # connect to the LDAP server
+        ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base'])
+
+        # connect to the Kerberos master server
+        krb_connection.connect(cfg['principal'], cfg['keytab'])
+
+        ### Sanity-checks ###
+   
+        # check the username and realame for validity
+        if not re.match(cfg['username_regex'], username):
+            return BAD_USERNAME
+        if not re.match(cfg['realname_regex'], realname):
+            return BAD_REALNAME
+
+        # see if user exists in LDAP
+        if ldap_connection.user_lookup(username):
+            return LDAP_EXISTS
+
+        # determine the first available userid
+        userid = ldap_connection.first_id(cfg['minimum_id'], cfg['maximum_id'])
+        if not userid: return LDAP_NO_IDS
+
+        # build principal name from username
+        principal = username + '@' + cfg['realm']
+    
+        # see if user exists in Kerberos
+        if krb_connection.get_principal(principal):
+            return KRB_EXISTS
+    
+        ### User creation ###
+
+        # process gecos_other (used to store memberid)
+        if gecos_other:
+            gecos_other = ',' + str(gecos_other)
+    
+        # account information defaults
+        shell = cfg['shell']
+        home = cfg['home'] + '/' + username
+        gecos = realname + ',,,' + gecos_other
+        gid = cfg['gid']
+    
+        # create the LDAP entry
+        ldap_connection.user_add(username, realname, shell, userid, gid, home, gecos)
+    
+        # create the Kerberos principal
+        krb_connection.add_principal(principal, password)
+
+    finally:
+        ldap_connection.disconnect()
+        krb_connection.disconnect()
+    
+    return SUCCESS
+    
+
+def delete_account(username):
+    """
+    Deletes the UNIX account of a member.
+    
+    Parameters:
+        username - UNIX username for the member
+
+    Exceptions:
+        LDAPException - on LDAP failure
+        KrbException  - on Kerberos failure
+        
+    Returns:
+        SUCCESS      - on success
+        LDAP_NO_USER - when the user does not exist in LDAP
+        KRB_NO_USER  - when the user does not exist in Kerberos
+    """
+
+    # Load Configuration
+    load_configuration()
+
+    ### Connect to the Backends ###
+
+    ldap_connection = ldapi.LDAPConnection()
+    krb_connection = krb.KrbConnection()
+
+    try:
+    
+        # connect to the LDAP server
+        ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base'])
+
+        # connect to the Kerberos master server
+        krb_connection.connect(cfg['principal'], cfg['keytab'])
+
+        ### Sanity-checks ###
+    
+        # ensure user exists in LDAP
+        if not ldap_connection.user_lookup(username):
+            return LDAP_NO_USER
+    
+        # build principal name from username
+        principal = username + '@' + cfg['realm']
+
+        # see if user exists in Kerberos
+        if not krb_connection.get_principal(principal):
+            return KRB_NO_USER
+
+        ### User deletion ###
+    
+        # delete the LDAP entry
+        ldap_connection.user_delete(username)
+    
+        # delete the Kerberos principal
+        krb_connection.delete_principal(principal)
+
+    finally:
+        ldap_connection.disconnect()
+        krb_connection.disconnect()
+    
+    return SUCCESS
+
+
+
+### Tests ###
+
+if __name__ == '__main__':
+
+    # A word of notice: this test creates a _working_ account (and then deletes it).
+    # If deletion fails it must be cleaned up manually.
+    
+    # a bit of salt so the test account is reasonably tough to crack
+    import random
+    pw = str(random.randint(100000000000000000, 999999999999999999))
+    
+    print "running create_account('testuser', ..., 'Test User', ...)", "->", errors[create_account('testuser', pw, 'Test User')]
+    print "running delete_account('testuser')", "->", errors[delete_account('testuser')]
+
diff --git a/pylib/csc/adm/members.py b/pylib/csc/adm/members.py
new file mode 100644 (file)
index 0000000..405ec29
--- /dev/null
@@ -0,0 +1,426 @@
+# $Id: members.py 44 2006-12-31 07:09:27Z mspang $
+"""
+CSC Member Management
+
+This module contains functions for registering new members, registering
+members for terms, searching for members, and other member-related
+functions.
+
+Transactions are used in each method that modifies the database. 
+Future changes to the members database that need to be atomic
+must also be moved into this module.
+"""
+
+import re
+from csc.adm import terms
+from csc.backends import db
+from csc.common.conf import read_config
+
+
+
+
+### Configuration
+
+CONFIG_FILE = '/etc/csc/members.cf'
+
+cfg = {}
+
+
+def load_configuration():
+    """Load Members Configuration"""
+
+    # configuration already loaded?
+    if len(cfg) > 0:
+        return
+
+    # read in the file
+    cfg_tmp = read_config(CONFIG_FILE)
+
+    if not cfg_tmp:
+        raise MemberException("unable to read configuration file: %s"
+                % CONFIG_FILE)
+
+    # check that essential fields are completed
+    mandatory_fields = [ 'server', 'database', 'user', 'password' ]
+
+    for field in mandatory_fields:
+        if not field in cfg_tmp:
+            raise MemberException("missing configuratino option: %s" % field)
+        if not cfg_tmp[field]:
+            raise MemberException("null configuration option: %s" %field)
+
+    # update the current configuration with the loaded values
+    cfg.update(cfg_tmp)
+
+
+
+### Exceptions ###
+
+class MemberException(Exception):
+    """Exception class for member-related errors."""
+
+class DuplicateStudentID(MemberException):
+    """Exception class for student ID conflicts."""
+    pass
+
+class InvalidStudentID(MemberException):
+    """Exception class for malformed student IDs."""
+    pass
+
+class InvalidTerm(MemberException):
+    """Exception class for malformed terms."""
+    pass
+
+class NoSuchMember(MemberException):
+    """Exception class for nonexistent members."""
+    pass
+
+
+
+### Connection Management ###
+
+# global database connection
+connection = db.DBConnection()
+
+
+def connect():
+    """Connect to PostgreSQL."""
+    
+    load_configuration()
+    
+    connection.connect(cfg['server'], cfg['database'])
+       
+
+def disconnect():
+    """Disconnect from PostgreSQL."""
+    
+    connection.disconnect()
+
+
+def connected():
+    """Determine whether the connection has been established."""
+
+    return connection.connected()
+
+
+### Member Table ###
+
+def new(realname, studentid=None, program=None):
+    """
+    Registers a new CSC member. The member is added
+    to the members table and registered for the current
+    term.
+
+    Parameters:
+        realname  - the full real name of the member
+        studentid - the student id number of the member
+        program   - the program of study of the member
+
+    Returns: the memberid of the new member
+
+    Exceptions:
+        DuplicateStudentID - if the student id already exists in the database
+        InvalidStudentID   - if the student id is malformed
+
+    Example: new("Michael Spang", program="CS") -> 3349
+    """
+
+    # blank attributes should be NULL
+    if studentid == '': studentid = None
+    if program == '': program = None
+
+    # check the student id format
+    regex = '^[0-9]{8}$'
+    if studentid != None and not re.match(regex, str(studentid)):
+        raise InvalidStudentID("student id is invalid: %s" % studentid)
+
+    # check for duplicate student id
+    member = connection.select_member_by_studentid(studentid)
+    if member:
+        raise DuplicateStudentID("student id exists in database: %s" % studentid)
+
+    # add the member
+    memberid = connection.insert_member(realname, studentid, program)
+
+    # register them for this term
+    connection.insert_term(memberid, terms.current())
+
+    # commit the transaction
+    connection.commit()
+
+    return memberid
+
+
+def get(memberid):
+    """
+    Look up attributes of a member by memberid.
+
+    Parameters:
+        memberid - the member id number
+
+    Returns: a dictionary of attributes
+
+    Example: get(3349) -> {
+                 'memberid': 3349,
+                 'name': 'Michael Spang',
+                 'program': 'Computer Science',
+                 ...
+             }
+    """
+
+    return connection.select_member_by_id(memberid)
+
+
+def get_userid(userid):
+    """
+    Look up attributes of a member by userid.
+
+    Parameters:
+        userid - the UNIX user id
+
+    Returns: a dictionary of attributes
+
+    Example: get('mspang') -> {
+                 'memberid': 3349,
+                 'name': 'Michael Spang',
+                 'program': 'Computer Science',
+                 ...
+             }
+    """
+
+    return connection.select_member_by_account(userid)
+
+
+def get_studentid(studentid):
+    """
+    Look up attributes of a member by studnetid.
+
+    Parameters:
+        studentid - the student ID number
+
+    Returns: a dictionary of attributes
+    
+    Example: get(...) -> {
+                 'memberid': 3349,
+                 'name': 'Michael Spang',
+                 'program': 'Computer Science',
+                 ...
+             }
+    """
+
+    return connection.select_member_by_studentid(studentid)
+
+
+def list_term(term):
+    """
+    Build a list of members in a term.
+
+    Parameters:
+        term - the term to match members against
+
+    Returns: a list of member dictionaries
+
+    Example: list_term('f2006'): -> [
+                 { 'memberid': 3349, ... },
+                 { 'memberid': ... }.
+                 ...
+             ]
+    """
+
+    # retrieve a list of memberids in term
+    memberlist = connection.select_members_by_term(term)
+
+    # convert the list of memberids to a list of dictionaries
+    memberlist = map(connection.select_member_by_id, memberlist)
+
+    return memberlist
+        
+
+def list_name(name):
+    """
+    Build a list of members with matching names.
+
+    Parameters:
+        name - the name to match members against
+
+    Returns: a list of member dictionaries
+
+    Example: list_name('Spang'): -> [
+                 { 'memberid': 3349, ... },
+                 { 'memberid': ... },
+                 ...
+             ]
+    """
+
+    # retrieve a list of memberids matching name
+    memberlist = connection.select_members_by_name(name)
+
+    # convert the list of memberids to a list of dictionaries
+    memberlist = map(connection.select_member_by_id, memberlist)
+
+    return memberlist
+
+
+def delete(memberid):
+    """
+    Erase all records of a member.
+
+    Note: real members are never removed
+          from the database
+
+    Parameters:
+        memberid - the member id number
+
+    Returns: attributes and terms of the
+             member in a tuple
+
+    Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
+    """
+
+    # save member data
+    member = connection.select_member_by_id(memberid)
+    term_list = connection.select_terms(memberid)
+
+    # remove data from the db
+    connection.delete_term_all(memberid)
+    connection.delete_member(memberid)
+    connection.commit()
+
+    return (member, term_list)
+
+
+def update(member):
+    """
+    Update CSC member attributes. None is NULL.
+
+    Parameters:
+        member - a dictionary with member attributes as
+                 returned by get, possibly omitting some
+                 attributes. member['memberid'] must exist
+                 and be valid.
+
+    Exceptions:
+        NoSuchMember       - if the member id does not exist
+        InvalidStudentID   - if the student id number is malformed
+        DuplicateStudentID - if the student id number exists 
+
+    Example: update( {'memberid': 3349, userid: 'mspang'} )
+    """
+
+    if member.has_key('studentid') and member['studentid'] != None:
+
+        studentid = member['studentid']
+        
+        # check the student id format
+        regex = '^[0-9]{8}$'
+        if studentid != None and not re.match(regex, str(studentid)):
+            raise InvalidStudentID("student id is invalid: %s" % studentid)
+
+        # check for duplicate student id
+        member = connection.select_member_by_studentid(studentid)
+        if member:
+            raise DuplicateStudentID("student id exists in database: %s" %
+                    studentid)
+
+    # not specifying memberid is a bug
+    if not member.has_key('memberid'):
+        raise Exception("no member specified in call to update")
+    memberid = member['memberid']
+
+    # see if member exists
+    old_member = connection.select_member_by_id(memberid)
+    if not old_member:
+        raise NoSuchMember("memberid does not exist in database: %d" %
+                memberid)
+    
+    # do the update
+    connection.update_member(member)
+
+    # commit the transaction
+    connection.commit()
+
+
+
+### Term Table ###
+
+def register(memberid, term_list):
+    """
+    Registers a member for one or more terms.
+
+    Parameters:
+        memberid  - the member id number
+        term_list - the term to register for, or a list of terms
+
+    Exceptions:
+        InvalidTerm - if a term is malformed
+
+    Example: register(3349, "w2007")
+
+    Example: register(3349, ["w2007", "s2007"])
+    """
+
+    if not type(term_list) in (list, tuple):
+        term_list = [ term_list ]
+
+    for term in term_list:
+        
+        # check term syntax
+        if not re.match('^[wsf][0-9]{4}$', term):
+            raise InvalidTerm("term is invalid: %s" % term)
+    
+        # add term to database
+        connection.insert_term(memberid, term)
+
+    connection.commit()
+
+
+def registered(memberid, term):
+    """
+    Determines whether a member is registered
+    for a term.
+
+    Parameters:
+        memberid - the member id number
+        term     - the term to check
+
+    Returns: whether the member is registered
+
+    Example: registered(3349, "f2006") -> True
+    """
+
+    return connection.select_term(memberid, term) != None
+
+
+def terms_list(memberid):
+    """
+    Retrieves a list of terms a member is
+    registered for.
+
+    Parameters:
+        memberid - the member id number
+
+    Returns: list of term strings
+
+    Example: registered(0) -> 's1993'
+    """
+
+    return connection.select_terms(memberid)
+
+
+
+### Tests ###
+
+if __name__ == '__main__':
+
+    connect()
+    
+    
+    sid = new("Test User", "99999999", "CS")
+
+    assert registered(id, terms.current())
+    print get(sid)
+    register(sid, terms.next(terms.current()))
+    assert registered(sid, terms.next(terms.current()))
+    print terms_list(sid)
+    print get(sid)
+    print delete(sid)
diff --git a/pylib/csc/adm/terms.py b/pylib/csc/adm/terms.py
new file mode 100644 (file)
index 0000000..3f9cae2
--- /dev/null
@@ -0,0 +1,252 @@
+# $Id: terms.py 44 2006-12-31 07:09:27Z mspang $
+"""
+Terms Routines
+
+This module contains functions for manipulating
+terms, such as determining the current term,
+finding the next or previous term, converting
+dates to terms, and more.
+"""
+import time, datetime, re
+
+# year to count terms from
+EPOCH = 1970
+
+# seasons list
+SEASONS = [ 'w', 's', 'f' ]
+
+
+def valid(term):
+    """
+    Determines whether a term is well-formed:
+
+    Parameters:
+        term - the term string
+
+    Returns: whether the term is valid (boolean)
+
+    Example: valid("f2006") -> True
+    """
+
+    regex = '^[wsf][0-9]{4}$'
+    return re.match(regex, term) != None
+
+
+def parse(term):
+    """Helper function to convert a term string to the number of terms
+       since the epoch. Such numbers are intended for internal use only."""
+
+    if not valid(term):
+        raise Exception("malformed term: %s" % term)
+
+    year = int( term[1:] )
+    season = SEASONS.index( term[0] )
+
+    return (year - EPOCH) * len(SEASONS) + season
+
+
+def generate(term):
+    """Helper function to convert a year and season to a term string."""
+    
+    year = int(term / len(SEASONS)) + EPOCH
+    season = term % len(SEASONS)
+    
+    return "%s%04d" % ( SEASONS[season], year )
+
+
+def next(term):
+    """
+    Returns the next term. (convenience function)
+
+    Parameters:
+        term - the term string
+
+    Retuns: the term string of the following term
+
+    Example: next("f2006") -> "w2007"
+    """
+    
+    return add(term, 1)
+
+
+def previous(term):
+    """
+    Returns the previous term. (convenience function)
+
+    Parameters:
+        term - the term string
+
+    Returns: the term string of the preceding term
+
+    Example: previous("f2006") -> "s2006"
+    """
+
+    return add(term, -1)
+
+
+def add(term, offset):
+    """
+    Calculates a term relative to some base term.
+    
+    Parameters:
+        term   - the base term
+        offset - the number of terms since term (may be negative)
+
+    Returns: the term that comes offset terms after term
+    """
+
+    return generate(parse(term) + offset)
+
+
+def delta(initial, final):
+    """
+    Calculates the distance between two terms.
+    It should be true that add(a, delta(a, b)) == b.
+
+    Parameters:
+        initial - the base term
+        final   - the term at some offset from the base term
+
+    Returns: the offset of final relative to initial
+    """
+
+    return parse(final) - parse(initial)
+
+
+def compare(first, second):
+    """
+    Compares two terms. This function is suitable
+    for use with list.sort().
+
+    Parameters:
+        first  - base term for comparison
+        second - term to compare to
+
+    Returns: > 0 (if first >  second)
+             = 0 (if first == second)
+             < 0 (if first <  second)
+    """
+    return delta(second, first)
+             
+
+def interval(base, count):
+    """
+    Returns a list of adjacent terms.
+
+    Parameters:
+        base    - the first term in the interval
+        count   - the number of terms to include
+
+    Returns: a list of count terms starting with initial
+
+    Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ]
+    """
+    
+    terms = []
+
+    for num in xrange(count):
+        terms.append( add(base, num) )
+    
+    return terms
+        
+
+def tstamp(timestamp):
+    """Helper to convert seconds since the epoch
+    to terms since the epoch."""
+
+    # let python determine the month and year
+    date = datetime.date.fromtimestamp(timestamp)
+
+    # determine season
+    if date.month <= 4:
+        season = SEASONS.index('w')
+    elif date.month <= 8:
+        season = SEASONS.index('s')
+    else:
+        season = SEASONS.index('f')
+
+    return (date.year - EPOCH) * len(SEASONS) + season
+
+
+def from_timestamp(timestamp):
+    """
+    Converts a number of seconds since
+    the epoch to a number of terms since
+    the epoch.
+
+    This function notes that:
+        WINTER = JANUARY to APRIL
+        SPRING = MAY TO AUGUST
+        FALL   = SEPTEMBER TO DECEMBER
+    
+    Parameters:
+        timestamp - number of seconds since the epoch
+
+    Returns: the number of terms since the epoch
+
+    Example: from_timestamp(1166135779) -> 'f2006'
+    """
+
+    return generate( tstamp(timestamp) )
+    
+
+def curr():
+    """Helper to determine the current term."""
+
+    return tstamp( time.time() )
+
+
+def current():
+    """
+    Determines the current term.
+
+    Returns: current term
+
+    Example: current() -> 'f2006'
+    """
+
+    return generate( curr() )
+    
+
+def next_unregistered(registered):
+    """
+    Find the first future or current unregistered term.
+    Intended as the 'default' for registrations.
+
+    Parameters:
+        registered - a list of terms a member is registered for
+
+    Returns: the next unregistered term
+    """
+    
+    # get current term number
+    now = curr()
+
+    # never registered -> current term is next
+    if len( registered) < 1:
+        return generate( now )
+
+    # return the first unregistered, or the current term (whichever is greater)
+    return generate(max([max(map(parse, registered))+1, now]))
+
+
+
+### Tests ###
+
+if __name__ == '__main__':
+
+    assert parse('f2006') == 110
+    assert generate(110) == 'f2006'
+    assert next('f2006') == 'w2007'
+    assert previous('f2006') == 's2006'
+    assert delta('f2006', 'w2007') == 1
+    assert add('f2006', delta('f2006', 'w2010')) == 'w2010'
+    assert interval('f2006', 3) == ['f2006', 'w2007', 's2007']
+    assert from_timestamp(1166135779) == 'f2006'
+    assert parse( current() ) >= 110
+    assert next_unregistered( [current()] ) == next( current() )
+    assert next_unregistered( [] ) == current()
+    assert next_unregistered( [previous(current())] ) == current()
+    assert next_unregistered( [add(current(), -2)] ) == current()
+
+    print "All tests passed." "\n"
diff --git a/pylib/csc/apps/__init__.py b/pylib/csc/apps/__init__.py
new file mode 100644 (file)
index 0000000..09ddf85
--- /dev/null
@@ -0,0 +1,9 @@
+# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
+"""
+User Interfaces
+
+This module contains frontends and related modules.
+CEO's primary frontends are:
+
+    legacy - aims to reproduce the curses UI of the previous CEO
+"""
diff --git a/pylib/csc/apps/legacy/__init__.py b/pylib/csc/apps/legacy/__init__.py
new file mode 100644 (file)
index 0000000..18352d2
--- /dev/null
@@ -0,0 +1,10 @@
+# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
+"""
+Legacy User Interface
+
+This module contains the legacy CEO user interface and related modules.
+Important modules are:
+
+    main.py    - all of the main UI logic
+    helpers.py - user interface library routines
+"""
diff --git a/pylib/csc/apps/legacy/helpers.py b/pylib/csc/apps/legacy/helpers.py
new file mode 100644 (file)
index 0000000..9d7cbaa
--- /dev/null
@@ -0,0 +1,412 @@
+# $Id: helpers.py 35 2006-12-28 05:14:05Z mspang $
+"""
+Helpers for legacy User Interface
+
+This module contains numerous functions that are designed to immitate
+the look and behavior of the previous CEO. Included is code for various
+curses-based UI widgets that were provided by Perl 5's Curses and
+Curses::Widgets libraries.
+
+Though attempts have been made to keep the UI bug-compatible with
+the previous system, some compromises have been made. For example,
+the input and textboxes draw 'OK' and 'Cancel' buttons where the old
+CEO had them, but they are fake. That is, the buttons in the old
+CEO were selectable but non-operational, but in the new CEO they are
+not even selectable.
+"""
+import curses.ascii
+
+# key constants not defined in CURSES
+KEY_RETURN = ord('\n')
+KEY_ESCAPE = 27
+KEY_EOT = 4
+
+
+def center(parent_dim, child_dim):
+    """Helper for centering a length in a larget length."""
+    return (parent_dim-child_dim)/2
+
+
+def read_input(wnd, offy, offx, width, maxlen, echo=True):
+    """
+    Read user input within a confined region of a window.
+
+    Basic line-editing is supported:
+        LEFT, RIGHT, HOME, and END move around.
+        BACKSPACE and DEL remove characters.
+        INSERT switches between insert and overwrite mode.
+        ESC and C-d abort input.
+        RETURN completes input.
+    
+    Parameters:
+        wnd    - parent window for region
+        offy   - the vertical offset for the beginning of the input region
+        offx   - the horizontal offset for the beginning of the input region
+        width  - the width of the region
+        maxlen - greatest number of characters to read (0 for no limit)
+        echo   - boolean: whether to display typed characters
+    
+    Returns: the string, or None when the user aborts.
+    """
+
+    # turn on cursor
+    try:
+        curses.curs_set(1)
+    except:
+        pass
+
+    # set keypad mode to allow UP, DOWN, etc
+    wnd.keypad(1)
+
+    # the input string
+    input = ""
+
+    # offset of cursor in input
+    # i.e. the next operation is applied at input[inputoff]
+    inputoff = 0
+
+    # display offset (for scrolling)
+    # i.e. the first character in the region is input[displayoff]
+    displayoff = 0
+
+    # insert mode (True) or overwrite mode (False)
+    insert = True
+
+    while True:
+
+        # echo mode, display the string
+        if echo:
+            # discard characters before displayoff, 
+            # as the window may be scrolled to the right
+            substring = input[displayoff:]
+    
+            # pad the string with zeroes to overwrite stale characters
+            substring = substring + " " * (width - len(substring))
+    
+            # display the substring
+            wnd.addnstr(offy, offx, substring, width)
+    
+            # await input
+            key = wnd.getch(offy, offx + inputoff - displayoff)
+
+        # not echo mode, don't display the string
+        else:
+            # await input at arbitrary location
+            key = wnd.getch(offy, offx)
+
+        # enter returns input
+        if key == KEY_RETURN:
+            return input
+
+        # escape aborts input
+        elif key == KEY_ESCAPE:
+            return None
+
+        # EOT (C-d) aborts if there is no input
+        elif key == KEY_EOT:
+            if len(input) == 0:
+                return None
+
+        # backspace removes the previous character
+        elif key == curses.KEY_BACKSPACE:
+            if inputoff > 0:
+
+                # remove the character immediately before the input offset
+                input = input[0:inputoff-1] + input[inputoff:]
+                inputoff -= 1
+
+                # move either the cursor or entire line of text left
+                if displayoff > 0:
+                    displayoff -= 1
+
+        # delete removes the current character
+        elif key == curses.KEY_DC:
+            if inputoff < len(input):
+                
+                # remove the character at the input offset
+                input = input[0:inputoff] + input[inputoff+1:]
+
+        # left moves the cursor one character left
+        elif key == curses.KEY_LEFT:
+            if inputoff > 0:
+
+                # move the cursor to the left
+                inputoff -= 1
+
+                # scroll left if necessary
+                if inputoff < displayoff:
+                    displayoff -= 1
+
+        # right moves the cursor one character right
+        elif key == curses.KEY_RIGHT:
+            if inputoff < len(input):
+                
+                # move the cursor to the right
+                inputoff += 1
+
+                # scroll right if necessary
+                if displayoff - inputoff == width:
+                    displayoff += 1
+
+        # home moves the cursor to the first character
+        elif key == curses.KEY_HOME:
+            inputoff = 0
+            displayoff = 0
+
+        # end moves the cursor past the last character
+        elif key == curses.KEY_END:
+            inputoff = len(input)
+            displayoff = len(input) - width + 1
+
+        # insert toggles insert/overwrite mode
+        elif key == curses.KEY_IC:
+            insert = not insert
+
+        # other (printable) characters are added to the input string
+        elif curses.ascii.isprint(key):
+            if len(input) < maxlen or maxlen == 0:
+
+                # insert mode: insert before current offset
+                if insert:
+                    input = input[0:inputoff] + chr(key) + input[inputoff:]
+    
+                # overwrite mode: replace current offset
+                else:
+                    input = input[0:inputoff] + chr(key) + input[inputoff+1:]
+    
+                # increment the input offset
+                inputoff += 1
+    
+                # scroll right if necessary
+                if inputoff - displayoff == width:
+                    displayoff += 1
+
+
+def inputbox(wnd, prompt, field_width, echo=True):
+    """Display a window for user input."""
+
+    wnd_height, wnd_width = wnd.getmaxyx()
+    height, width = 12, field_width + 7
+
+    # draw a window for the dialog
+    childy, childx = center(wnd_height-1, height)+1, center(wnd_width, width)
+    child_wnd = wnd.subwin(height, width, childy, childx)
+    child_wnd.clear()
+    child_wnd.border()
+
+    # draw another window for the text box
+    texty, textx = center(height-1, 3)+1, center(width-1, width-5)+1
+    textheight, textwidth = 3, width-5
+    text_wnd = child_wnd.derwin(textheight, textwidth, texty, textx)
+    text_wnd.clear()
+    text_wnd.border()
+    
+    # draw the prompt
+    prompty, promptx = 2, 3
+    child_wnd.addnstr(prompty, promptx, prompt, width-2)
+
+    # draw the fake buttons
+    fakey, fakex = 9, width - 19
+    child_wnd.addstr(fakey, fakex, "< OK > < Cancel >")
+    child_wnd.addch(fakey, fakex+2, "O", curses.A_UNDERLINE)
+    child_wnd.addch(fakey, fakex+9, "C", curses.A_UNDERLINE)
+
+    # update the screen
+    child_wnd.noutrefresh()
+    text_wnd.noutrefresh()
+    curses.doupdate()
+
+    # read an input string within the field region of text_wnd
+    inputy, inputx, inputwidth = 1, 1, textwidth - 2
+    input = read_input(text_wnd, inputy, inputx, inputwidth, 0, echo)
+    
+    # erase the window
+    child_wnd.erase()
+    child_wnd.refresh()
+
+    return input
+
+
+def line_wrap(line, width):
+    """Wrap a string to a certain width (returns a list of strings)."""
+
+    wrapped_lines = []
+    tokens = line.split(" ")
+    tokens.reverse()
+    tmp = tokens.pop()
+    if len(tmp) > width:
+        wrapped_lines.append(tmp[0:width])
+        tmp = tmp[width:]
+    while len(tokens) > 0:
+        token = tokens.pop()
+        if len(tmp) + len(token) + 1 <= width:
+            tmp += " " + token
+        elif len(token) > width:
+            tmp += " " + token[0:width-len(tmp)-1]
+            tokens.push(token[width-len(tmp)-1:])
+        else:
+            wrapped_lines.append(tmp)
+            tmp = token
+    wrapped_lines.append(tmp)
+    return wrapped_lines
+
+
+def msgbox(wnd, msg, title="Message"):
+    """Display a message in a window."""
+
+    # split message into a list of lines
+    lines = msg.split("\n")
+    
+    # determine the dimensions of the method
+    message_height = len(lines)
+    message_width = 0
+    for line in lines:
+        if len(line) > message_width:
+            message_width = len(line)
+
+    # ensure the window fits the title
+    if len(title) > message_width:
+        message_width = len(title)
+
+    # maximum message width
+    parent_height, parent_width = wnd.getmaxyx()
+    max_message_width = parent_width - 8
+
+    # line-wrap if necessary
+    if message_width > max_message_width:
+        newlines = []
+        for line in lines:
+            for newline in line_wrap(line, max_message_width):
+                newlines.append(newline)
+        lines = newlines
+        message_width = max_message_width
+        message_height = len(lines)
+
+    # random padding that perl's curses adds
+    pad_width = 2
+
+    # create the outer window
+    outer_height, outer_width = message_height + 8, message_width + pad_width + 6
+    outer_y, outer_x = center(parent_height+1, outer_height)-1, center(parent_width, outer_width)
+    outer_wnd = wnd.derwin(outer_height, outer_width, outer_y, outer_x)
+    outer_wnd.erase()
+    outer_wnd.border()
+
+    # create the inner window
+    inner_height, inner_width = message_height + 2, message_width + pad_width + 2
+    inner_y, inner_x = 2, center(outer_width, inner_width)
+    inner_wnd = outer_wnd.derwin(inner_height, inner_width, inner_y, inner_x)
+    inner_wnd.border()
+
+    # display the title
+    outer_wnd.addstr(0, 1, " " + title + " ", curses.A_REVERSE | curses.A_BOLD)
+    
+    # display the message
+    for i in xrange(len(lines)):
+        inner_wnd.addnstr(i+1, 1, lines[i], message_width)
+
+        # draw a solid block at the end of the first few lines
+        if i < len(lines) - 1:
+            inner_wnd.addch(i+1, inner_width-1, ' ', curses.A_REVERSE)
+
+    # display the fake OK button
+    fakey, fakex = outer_height - 3, outer_width - 8
+    outer_wnd.addstr(fakey, fakex, "< OK >", curses.A_REVERSE)
+    outer_wnd.addch(fakey, fakex+2, "O", curses.A_UNDERLINE | curses.A_REVERSE)
+
+    # update display
+    outer_wnd.noutrefresh()
+    inner_wnd.noutrefresh()
+    curses.doupdate()
+
+    # read a RETURN or ESC before returning
+    curses.curs_set(0)
+    outer_wnd.keypad(1)
+    while True:
+        key = outer_wnd.getch(0,0)
+        if key == KEY_RETURN or key == KEY_ESCAPE:
+            break
+
+    # clear the window
+    outer_wnd.erase()
+    outer_wnd.refresh()
+    
+
+def menu(wnd, offy, offx, width, options, _acquire_wnd=None):
+    """
+    Draw a menu and wait for a selection.
+
+    Parameters:
+        wnd          - parent window
+        offy         - vertical offset for menu region
+        offx         - horizontal offset for menu region
+        width        - width of menu region
+        options      - a list of selections
+        _acquire_wnd - hack to support resize: must be a function callback
+                       that returns new values for wnd, offy, offx, height,
+                       width. Unused if None.
+
+    Returns: index into options that was selected
+    """
+
+    # the currently selected option
+    selected = 0
+
+    while True:
+        # disable cursor
+        curses.curs_set(0)
+
+        # hack to support resize: recreate the
+        # parent window every iteration
+        if _acquire_wnd:
+            wnd, offy, offx, height, width = _acquire_wnd()
+
+        # keypad mode so getch() works with up, down
+        wnd.keypad(1)
+
+        # display the menu
+        for i in xrange(len(options)):
+            text, callback = options[i]
+            text = text + " " * (width - len(text))
+
+            # the selected option is displayed in reverse video
+            if i == selected:
+                wnd.addstr(i+offy, offx, text, curses.A_REVERSE)
+            else:
+                wnd.addstr(i+offy, offx, text)
+                    # display the member
+
+        wnd.refresh()
+        
+        # read one keypress
+        input = wnd.getch()
+
+        # UP moves to the previous option
+        if input == curses.KEY_UP and selected > 0:
+            selected = (selected - 1)
+
+        # DOWN moves to the next option
+        elif input == curses.KEY_DOWN and selected < len(options) - 1:
+            selected = (selected + 1)
+
+        # RETURN runs the callback for the selected option
+        elif input == KEY_RETURN:
+            text, callback = options[selected]
+
+            # highlight the selected option
+            text = text + " " * (width - len(text))
+            wnd.addstr(selected+offy, offx, text, curses.A_REVERSE | curses.A_BOLD)
+            wnd.refresh()
+
+            # execute the selected option
+            if callback(wnd): # success
+                break
+
+
+def reset():
+    """Reset the terminal and clear the screen."""
+
+    reset = curses.tigetstr('rs1')
+    if not reset: reset = '\x1bc'
+    curses.putp(reset)
+
diff --git a/pylib/csc/apps/legacy/main.py b/pylib/csc/apps/legacy/main.py
new file mode 100644 (file)
index 0000000..736d55b
--- /dev/null
@@ -0,0 +1,555 @@
+# $Id: main.py 44 2006-12-31 07:09:27Z mspang $
+"""
+CEO-like Frontend
+
+This frontend aims to be compatible in both look and function with the
+curses UI of CEO.
+
+Some small improvements have been made, such as not echoing passwords and
+aborting when nothing is typed into the first input box during an operation.
+
+This frontend is poorly documented, deprecated, and undoubtedly full of bugs.
+"""
+import curses.ascii, re, os
+from helpers import menu, inputbox, msgbox, reset
+from csc.adm import accounts, members, terms
+
+# color of the ceo border
+BORDER_COLOR = curses.COLOR_RED
+
+
+def action_new_member(wnd):
+    """Interactively add a new member."""
+
+    username, studentid, program = '', None, ''
+
+    # read the name
+    prompt = "      Name: "
+    realname = inputbox(wnd, prompt, 18)
+
+    # abort if no username is entered
+    if not realname or realname.lower() == 'exit':
+        return False
+
+    # read the student id
+    prompt = "Student id:"
+    while studentid == None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
+        studentid = inputbox(wnd, prompt, 18)
+
+    # abort if exit is entered
+    if studentid.lower() == 'exit':
+        return False
+
+    if studentid == '':
+        studentid = None
+
+    # read the program of study
+    prompt = "   Program:"
+    program = inputbox(wnd, prompt, 18)
+
+    # abort if exit is entered
+    if program == None or program.lower() == 'exit':
+        return False
+
+    # connect the members module to its backend if necessary
+    if not members.connected(): members.connect()
+
+    # attempt to create the member
+    try:
+        memberid = members.new(realname, studentid, program)
+
+        msgbox(wnd, "Success! Your memberid is %s.  You are now registered\n"
+                    % memberid + "for the " + terms.current() + " term.");
+
+    except members.InvalidStudentID:
+        msgbox(wnd, "Invalid student ID.")
+        return False
+    except members.DuplicateStudentID:
+        msgbox(wnd, "A member with this student ID exists.")
+        return False
+
+
+def action_term_register(wnd):
+    """Interactively register a member for a term."""
+
+    memberid, term = '', ''
+
+    # read the member id
+    prompt = 'Enter memberid ("exit" to cancel):'
+    memberuserid = inputbox(wnd, prompt, 36)
+
+    if not memberuserid or memberuserid.lower() == 'exit':
+        return False
+
+    member = get_member_memberid_userid(wnd, memberuserid)
+    if not member: return False
+
+    memberid = member['memberid']
+    term_list = members.terms_list(memberid)
+    
+    # display user
+    display_member_details(wnd, member, term_list)
+
+    # read the term
+    prompt = "Which term to register for ([fws]20nn):"
+    while not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit':
+        term = inputbox(wnd, prompt, 41) 
+
+    # abort when exit is entered
+    if term.lower() == 'exit':
+        return False
+
+    # already registered?
+    if members.registered(memberid, term):
+        msgbox(wnd, "You are already registered for term " + term)
+        return False
+
+    try:
+
+        # attempt to register
+        members.register(memberid, term)
+        
+        # display success message [sic]
+        msgbox(wnd, "Your are now registered for term " + term)
+
+    except members.InvalidTerm:
+        msgbox(wnd, "Term is not valid: %s" % term)
+
+    return False
+
+
+def action_term_register_multiple(wnd):
+    """Interactively register a member for multiple terms."""
+
+    memberid, base, num = '', '', None
+
+    # read the member id
+    prompt = 'Enter memberid ("exit" to cancel):'
+    memberuserid = inputbox(wnd, prompt, 36)
+
+    if not memberuserid or memberuserid.lower() == 'exit':
+        return False
+
+    member = get_member_memberid_userid(wnd, memberuserid)
+    if not member: return False
+
+    memberid = member['memberid']
+    term_list = members.terms_list(memberid)
+    
+    # display user
+    display_member_details(wnd, member, term_list)
+
+    # read the base
+    prompt = "Which term to start registering ([fws]20nn):"
+    while not re.match('^[wsf][0-9]{4}$', base) and not base == 'exit':
+        base = inputbox(wnd, prompt, 41) 
+
+    # abort when exit is entered
+    if base.lower() == 'exit':
+        return False
+
+    # read number of terms
+    prompt = 'How many terms?'
+    while not num or not re.match('^[0-9]*$', num):
+        num = inputbox(wnd, prompt, 36)
+    num = int(num)
+
+    # any terms in the range?
+    if num < 1:
+        msgbox(wnd, "No terms to register.")
+        return False
+
+    # compile a list to register
+    term_list = terms.interval(base, num)
+
+    # already registered?
+    for term in term_list:
+        if members.registered(memberid, term):
+            msgbox(wnd, "You are already registered for term " + term)
+            return False
+
+    try:
+
+        # attempt to register all terms
+        members.register(memberid, term_list)
+        
+        # display success message [sic]
+        msgbox(wnd, "Your are now registered for terms: " + ", ".join(term_list))
+
+    except members.InvalidTerm:
+        msgbox(wnd, "Term is not valid: %s" % term)
+
+    return False
+
+
+def action_create_account(wnd):
+    """Interactively create an account for a member."""
+    
+    memberid, userid = '', ''
+
+    # read the member id
+    prompt = 'Enter member ID (exit to cancel):'
+    memberid = inputbox(wnd, prompt, 35)
+
+    if not memberid or memberid.lower() == 'exit':
+        return False
+
+    member = get_member_memberid_userid(wnd, memberid)
+    if not member: return False
+
+    memberid = member['memberid']
+    term_list = members.terms_list(memberid)
+    
+    # display the member
+    display_member_details(wnd, member, term_list)
+    
+    # verify member
+    prompt = "Is this the correct member?"
+    answer = None
+    while answer != "yes" and answer != "y" and answer != "n" and answer != "no" and answer != "exit":
+        answer = inputbox(wnd, prompt, 28) 
+
+    # user abort
+    if answer == "exit":
+        return False
+
+    # incorrect member; abort
+    if answer == "no" or answer == "n":
+        msgbox(wnd, "I suggest searching for the member by userid or name from the main menu.")
+        return False
+
+    # read user id
+    prompt = "Userid:"
+    while userid == '':
+        userid = inputbox(wnd, prompt, 18) 
+
+    # user abort
+    if userid == None or userid.lower() == 'exit':
+        return False
+
+    # member already has an account?
+    #if member['userid'] != None:
+    #    msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
+    #                "Contact the sysadmin if there are still problems." )
+    #    return False
+
+    # password input loop
+    password = "password"
+    check = "check"
+    while password != check:
+    
+        # read password
+        prompt = "User password:"
+        password = None
+        while not password:
+            password = inputbox(wnd, prompt, 18, False) 
+
+        # read another password
+        prompt = "Enter the password again:"
+        check = None
+        while not check:
+            check = inputbox(wnd, prompt, 27, False) 
+
+
+    # create the UNIX account
+    result = accounts.create_account(userid, password, member['name'], memberid)
+
+    if result == accounts.LDAP_EXISTS:
+        msgbox(wnd, "Error: Could not do stuff , Already exists.")
+        return False
+    elif result == accounts.KRB_EXISTS:
+        msgbox(wnd, "This account already exists in Kerberos, but not in LDAP. Please contact the Systems Administrator.")
+        return False
+    elif result == accounts.LDAP_NO_IDS:
+        msgbox(wnd, "There are no available UNIX user ids. This is a fatal error. Contact the Systems Administrator.")
+        return False
+    elif result == accounts.BAD_REALNAME:
+        msgbox(wnd, "Invalid real name: %s. Contact the Systems Administrator." % member['name'])
+        return False
+    elif result == accounts.BAD_USERNAME:
+        msgbox(wnd, "Invalid username: %s. Enter a valid username." % userid)
+        return False
+    elif result != accounts.SUCCESS:
+        raise Exception("Unexpected return status of accounts.create_account(): %s" % result)
+        
+    # now update the CEO database with the username
+    members.update( {'memberid':memberid, 'userid': userid} )
+
+    # success
+    msgbox(wnd, "Please run 'addhomedir " + userid + "'.")
+    msgbox(wnd, "Success! Your account has been added")
+
+
+def display_member_details(wnd, member, term_list):
+    """Display member attributes in a message box."""
+
+    # clone and sort term_list
+    term_list = list(term_list)
+    term_list.sort( terms.compare )
+
+    # labels for data
+    id_label, studentid_label, name_label = "ID:", "StudentID:", "Name:"
+    program_label, userid_label, terms_label = "Program:", "User ID:", "Terms:"
+
+    # format it all into a massive string
+    message =  "%8s %-20s %10s %-10s (user)\n" % (name_label, member['name'], id_label, member['memberid']) + \
+               "%8s %-20s %10s %-10s\n" % (program_label, member['program'], studentid_label, member['studentid'])
+
+    if member['userid']:
+        message += "%8s %s\n" % (userid_label, member['userid'])
+    else:
+        message += 'No user ID.\n'
+
+    message += "%s %s" % (terms_label, " ".join(term_list))
+
+    # display the string in a message box
+    msgbox(wnd, message)
+
+
+def get_member_memberid_userid(wnd, memberuserid):
+    """Retrieve member attributes by member of user id."""
+
+    # connect the members module to its backends
+    if not members.connected(): members.connect()
+
+    # retrieve member data
+
+    if re.match('^[0-9]*$', memberuserid):
+
+        # numeric memberid, look it up
+        memberid = int(memberuserid)
+        member = members.get( memberid )
+        if not member:
+            msgbox(wnd, '%s is an invalid memberid' % memberuserid)
+
+    else:
+
+        # non-numeric memberid: try userids
+        member = members.get_userid( memberuserid )
+        if not member:
+            msgbox(wnd, "%s is an invalid account userid" % memberuserid)
+            
+    return member
+    
+
+def action_display_member(wnd):
+    """Interactively display a member."""
+    
+    # read the member id
+    prompt = 'Memberid: '
+    memberid = inputbox(wnd, prompt, 36)
+
+    if not memberid or memberid.lower() == 'exit':
+        return False
+
+    member = get_member_memberid_userid(wnd, memberid)
+    if not member: return
+    term_list = members.terms_list( member['memberid'] )
+
+    # display the details in a window
+    display_member_details(wnd, member, term_list)
+
+
+def page(text):
+    
+    try:
+        pipe = os.popen('/usr/bin/less', 'w')
+        pipe.write(text)
+        pipe.close() 
+    except IOError:
+        # broken pipe (user didn't read the whole list)
+        pass
+    
+
+def format_members(member_list):
+    """Format a member list into a string."""
+
+    # clone and sort member_list
+    member_list = list(member_list)
+    member_list.sort( lambda x, y: x['memberid']-y['memberid'] )
+
+    buf = ''
+    
+    for member in member_list:
+        attrs = ( member['memberid'], member['name'], member['studentid'],
+                member['type'], member['program'], member['userid'] )
+        buf += "%4d %50s %10s %10s \n%55s %10s\n\n" % attrs
+
+    return buf
+
+
+def action_list_term(wnd):
+    """Interactively list members registered in a term."""
+
+    term = None
+
+    # read the term
+    prompt = "Which term to list members for ([fws]20nn): "
+    while term == None or (not term == '' and not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit'):
+        term = inputbox(wnd, prompt, 41) 
+
+    # abort when exit is entered
+    if not term or term.lower() == 'exit':
+        return False
+
+    # connect the members module to its backends if necessary
+    if not members.connected(): members.connect()
+    
+    # retrieve a list of members for term
+    member_list = members.list_term(term)
+
+    # format the data into a mess of text
+    buf = format_members(member_list)
+
+    # display the mass of text with a pager
+    page( buf )
+
+
+def action_list_name(wnd):
+    
+    name = None
+
+    # read the name
+    prompt = "Enter the member's name: "
+    name = inputbox(wnd, prompt, 41) 
+
+    # abort when exit is entered
+    if not name or name.lower() == 'exit':
+        return False
+
+    # connect the members module to its backends if necessary
+    if not members.connected(): members.connect()
+    
+    # retrieve a list of members for term
+    member_list = members.list_name(name)
+
+    # format the data into a mess of text
+    buf = format_members(member_list)
+
+    # display the mass of text with a pager
+    page( buf )
+
+
+def action_list_studentid(wnd):
+
+    studentid = None
+
+    # read the studentid
+    prompt = "Enter the member's student id: "
+    studentid = inputbox(wnd, prompt, 41) 
+
+    # abort when exit is entered
+    if not studentid or studentid.lower() == 'exit':
+        return False
+
+    # connect the members module to its backends if necessary
+    if not members.connected(): members.connect()
+    
+    # retrieve a list of members for term
+    member = members.get_studentid(studentid)
+    if member != None:
+        member_list = [ members.get_studentid(studentid) ]
+    else:
+        member_list = []
+
+    # format the data into a mess of text
+    buf = format_members(member_list)
+
+    # display the mass of text with a pager
+    page( buf )
+
+
+def null_callback(wnd):
+    """Callback for unimplemented menu options."""
+    return False
+
+
+def exit_callback(wnd):
+    """Callback for the exit option."""
+    return True
+
+
+# the top level ceo menu
+top_menu = [
+    ( "New member", action_new_member ),
+    ( "Register for a term", action_term_register ),
+    ( "Register for multiple terms", action_term_register_multiple ),
+    ( "Display a member", action_display_member ),
+    ( "List members registered in a term", action_list_term ),
+    ( "Search for a member by name", action_list_name ),
+    ( "Search for a member by student id", action_list_studentid ),
+    ( "Create an account", action_create_account ),
+    ( "Re Create an account", null_callback ),
+    ( "Library functions", null_callback ),
+    ( "Exit", exit_callback ),
+]
+
+
+def acquire_ceo_wnd(screen=None):
+    """Create the top level ceo window."""
+    
+    # hack to get a reference to the entire screen
+    # even when the caller doesn't (shouldn't) have one
+    global _screen
+    if screen == None:
+        screen = _screen
+    else:
+        _screen = screen
+
+    # if the screen changes size, a mess may be left
+    screen.erase()
+
+    # for some reason, the legacy ceo system
+    # excluded the top line from its window
+    height, width = screen.getmaxyx()
+    ceo_wnd = screen.subwin(height-1, width, 1, 0)
+
+    # draw the border around the ceo window
+    curses.init_pair(1, BORDER_COLOR, -1)
+    color_attr = curses.color_pair(1) | curses.A_BOLD
+    ceo_wnd.attron(color_attr)
+    ceo_wnd.border()
+    ceo_wnd.attroff(color_attr)
+
+    # return window and dimensions of inner area
+    return ceo_wnd, 1, 1, height-2, width-2
+
+
+def ceo_main_curses(screen):
+    """Wrapped main for curses."""
+    
+    curses.use_default_colors()
+
+    # workaround for SSH sessions on virtual consoles (reset terminal)
+    reset()
+
+    # create ceo window
+    ceo_wnd, menu_y, menu_x, menu_height, menu_width = acquire_ceo_wnd(screen)
+
+    # display the top level menu
+    menu(ceo_wnd, menu_y, menu_x, menu_width, top_menu, acquire_ceo_wnd)
+
+
+def run():
+    """Main function for legacy UI."""
+
+    # wrap the entire program using curses.wrapper
+    # so that the terminal is restored to a sane state
+    # when the program exits
+    try:
+        curses.wrapper(ceo_main_curses)
+    except KeyboardInterrupt:
+        pass
+    except curses.error:
+        print "Your screen is too small!"
+        raise
+    except:
+        reset()
+        raise
+
+    # clean up screen before exit
+    reset()
+
+if __name__ == '__main__':
+    run()
+
diff --git a/pylib/csc/backends/__init__.py b/pylib/csc/backends/__init__.py
new file mode 100644 (file)
index 0000000..531a029
--- /dev/null
@@ -0,0 +1,12 @@
+# $Id$
+"""
+Backends
+
+This module contains backend interfaces and related modules.
+CEO's primary backends are:
+
+    db.py    - CEO's database for member and term registrations
+    ldapi.py - LDAP, for UNIX account metadata administration
+    krb.py   - Kerberos, for UNIX account password administration
+    
+"""
diff --git a/pylib/csc/backends/db.py b/pylib/csc/backends/db.py
new file mode 100644 (file)
index 0000000..d01e1a0
--- /dev/null
@@ -0,0 +1,548 @@
+# $Id: db.py 37 2006-12-28 10:00:50Z mspang $
+"""
+Database Backend Interface
+
+This module is intended to be a thin wrapper around CEO database operations.
+Methods on the connection class correspond in a straightforward way to SQL
+queries. These methods may restructure and clean up query output but may make
+no other assumptions about its content or purpose.
+
+This module makes use of the PygreSQL Python bindings to libpq,
+PostgreSQL's native C client library.
+"""
+import pgdb
+
+
+class DBException(Exception):
+    """Exception class for database-related errors."""
+    pass
+    
+    
+class DBConnection(object):
+    """
+    Connection to CEO's backend database. All database queries
+    and updates are made via this class.
+    
+    Exceptions: (all methods)
+        DBException - on database query failure
+
+    Note: Updates will never take place until commit() is called.
+
+    Note: In the event that any method of this class raises a
+          DBException and that exception is caught, rollback()
+          must be called before further queries will be successful.
+    
+    Example:
+        connection = DBConnection()
+        connection.connect("localhost", "ceo")
+        
+        # make queries and updates, i.e.
+        connection.insert_member("Calum T. Dalek")
+        
+        connection.commit()
+        connection.disconnect()
+    """
+
+    def __init__(self):
+        self.cnx = None
+        self.cursor = None
+
+          
+    def connect(self, hostname=None, database=None, username=None, password=None):
+        """
+        Establishes the connection to CEO's PostgreSQL database.
+        
+        Parameters:
+            hostname - hostname:port to connect to
+            database - name of database
+            username - user to authenticate as
+            password - password of username
+        """
+
+        if self.cnx: raise DBException("unable to connect: already connected")
+        
+        try:
+            self.cnx = pgdb.connect(host=hostname, database=database,
+                    user=username, password=password)
+            self.cursor = self.cnx.cursor()
+        except pgdb.Error, e:
+            raise DBException("unable to connect: %s" % e)
+
+
+    def disconnect(self):
+        """Closes the connection to CEO's PostgreSQL database."""
+
+        if self.cursor:
+            self.cursor.close()
+            self.cursor = None
+
+        if self.cnx:
+            self.cnx.close()
+            self.cnx = None
+
+    
+    def connected(self):
+        """Determine whether the connection has been established."""
+
+        return self.cnx != None
+
+
+    def commit(self):
+        """Commits the current transaction and starts a new one."""
+
+        self.cnx.commit()
+
+
+    def rollback(self):
+        """Aborts the current transaction."""
+
+        self.cnx.rollback()
+
+
+
+    ### Member-related methods ###
+    
+    def select_members(self, sql, params=None):
+        """
+        Retrieves a list CSC members selected by given SQL statement.
+        
+        This is a helper function that should generally not be called directly.
+        
+        Parameters:
+            sql    - the SELECT sql statement
+            params - parameters for the SQL statement
+
+        The sql statement must select the six columns
+        (memberid, name, studentid, program, type, userid)
+        from the members table in that order.
+        
+        Returns: a memberid-keyed dictionary whose values are
+                 column-keyed dictionaries with member attributes
+        """
+        
+        # retrieve a list of all members
+        try:
+            self.cursor.execute(sql, params)
+            members_list = self.cursor.fetchall()
+        except pgdb.Error, e:
+            raise DBException("SELECT statement failed: %s" % e)
+        
+        # build a dictionary of dictionaries from the result (a list of lists)
+        members_dict = {}
+        for member in members_list:
+            memberid, name, studentid, program, type, userid = member
+            members_dict[memberid] = {
+                'memberid': member[0],
+                'name': member[1],
+                'studentid': member[2],
+                'program': member[3],
+                'type': member[4],
+                'userid': member[5],
+            }
+
+        return members_dict
+
+
+    def select_single_member(self, sql, params=None):
+        """
+        Retrieves a single member by memberid.
+
+        This is a helper function that should generally not be called directly.
+        
+        See: self.select_members()
+
+        Returns: a column-keyed dictionary with member attributes, or
+                 None if no member matching member exists
+        """
+
+        # retrieve the member
+        results = self.select_members(sql, params)
+
+        # too many members returned
+        if len(results) > 1:
+            raise DBException("multiple members selected: sql='%s' params=%s" % (sql, repr(params)))
+
+        # no such member
+        elif len(results) < 1:
+            return None
+
+        # return the single match
+        memberid = results.keys()[0]
+        return results[memberid]
+
+   
+    def select_all_members(self):
+        """
+        Retrieves a list of all CSC members (past and present).
+
+        See: self.select_members()
+        
+        Example: connection.select_all_members() -> {
+                     0:    { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
+                     3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
+                     ...
+                 }
+        """
+        sql = "SELECT memberid, name, studentid, program, type, userid FROM members"
+        return self.select_members(sql)
+        
+    
+    def select_members_by_name(self, name_re):
+        """
+        Retrieves a list of all CSC members whose name matches name_re.
+        
+        See: self.select_members()
+        
+        Example: connection.select_members_by_name('Michael') -> {
+                     3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
+                     ...
+                 }
+        """
+        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE name ~* %s"
+        params = [ str(name_re) ]
+     
+        return self.select_members(sql, params)
+
+    
+    def select_members_by_term(self, term):
+        """
+        Retrieves a list of all CSC members who were members in the specified term.
+        
+        See: self.select_members()
+        
+        Example: connection.select_members_by_term('f2006') -> {
+                     3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
+                     ...
+                 }
+        """
+        sql = "SELECT members.memberid, name, studentid, program, type, userid FROM members JOIN terms ON members.memberid=terms.memberid WHERE term=%s"
+        params = [ str(term) ]
+        
+        return self.select_members(sql, params)
+
+    
+    def select_member_by_id(self, memberid):
+        """
+        Retrieves a single member by memberid.
+
+        See: self.select_single_member()
+
+        Example: connection.select_member_by_id(0) ->
+                 { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
+        """
+        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE memberid=%d"
+        params = [ int(memberid) ]
+
+        return self.select_single_member(sql, params)
+
+    
+    def select_member_by_account(self, username):
+        """
+        Retrieves a single member by UNIX account username.
+
+        See: self.select_single_member()
+
+        Example: connection.select_member_by_account('ctdalek') ->
+                 { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
+        """
+        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE userid=%s"
+        params = [ username ]
+
+        return self.select_single_member(sql, params)
+
+
+    def select_member_by_studentid(self, studentid):
+        """
+        Retrieves a single member by student id number.
+
+        See: self.select_single_member()
+
+        Example: connection.select_member_by_studentid('nnnnnnnn') ->
+                 { 'memberid': 3349, 'name': 'Michael Spang' ...}
+        """
+        sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE studentid=%s"
+        params = [ studentid ]
+
+        return self.select_single_member(sql, params)
+
+    
+    def insert_member(self, name, studentid=None, program=None):
+        """
+        Creates a member with the specified attributes.
+
+        Parameters:
+            name      - full name of member
+            studentid - student id number
+            program   - program of study
+
+        Example: connection.insert_member('Michael Spang', '99999999', 'Math/CS') -> 3349
+
+        Returns: a memberid of created user
+        """
+        try:
+            # retrieve the next memberid
+            sql = "SELECT nextval('memberid_seq')"
+            self.cursor.execute(sql)
+            result = self.cursor.fetchone()
+            memberid = result[0]
+        
+            # insert the member
+            sql = "INSERT INTO members (memberid, name, studentid, program, type) VALUES (%d, %s, %s, %s, %s)"
+            params = [ memberid, name, studentid, program, 'user' ]
+            self.cursor.execute(sql, params)
+            
+            return memberid
+        except pgdb.Error, e:
+            raise DBException("failed to create member: %s" % e)
+
+    
+    def delete_member(self, memberid):
+        """
+        Deletes a member. Note that a member cannot
+        be deleted until it has been unregistered from
+        all terms.
+
+        Parameters:
+            memberid - the member id number to delete
+
+        Example: connection.delete_member(3349)
+        """
+        sql = "DELETE FROM members WHERE memberid=%d"
+        params = [ memberid ]
+
+        try:
+            self.cursor.execute(sql, params)
+        except pgdb.Error, e:
+            raise DBException("DELETE statement failed: %s" %e)
+
+    
+    def update_member(self, member):
+        """
+        Modifies member attributes.
+
+        Parameters:
+            member - a column-keyed dictionary with the new state of the member.
+                     member['memberid'] must be present. ommitted columns
+                     will not be changed. None is NULL.
+
+        Returns: the full new state of the member as a column-keyed dictionary
+
+        Example: connection.update_member({
+                     'memberid': 3349,
+                     'name': 'Michael C. Spang',
+                     'program': 'CS!'
+                 }) -> {
+                     'memberid': 3349,
+                     'name': 'Michael C. Spang',
+                     'program': CS!',
+                     'studentid': '99999999' # unchanged
+                 }
+
+        Equivalent Example:
+                 member = connection.select_member_by_id(3349)
+                 member['name'] = 'Michael C. Spang'
+                 member['program'] = 'CS!'
+                 connection.update_member(member) -> { see above }
+        """
+        try:
+            
+            # memberid to update
+            memberid = member['memberid']
+            
+            # retrieve current state of member
+            member_state = self.select_member_by_id(memberid)
+
+            # build a list of changes to make
+            changes = []
+            for column in member.keys():
+                if member[column] != member_state[column]:
+
+                    # column's value must be updated
+                    changes.append( (column, member[column]) )
+                    member_state[column] = member[column]
+            
+            # no changes?
+            if len(changes) < 1:
+                return member_state
+            
+            # make the necessary changes in an update statement
+            changes = zip(*changes)
+            sql = "UPDATE members SET " + ", ".join(["%s=%%s"] * len(changes[0])) % changes[0] + " WHERE memberid=%d"
+            params = changes[1] + ( memberid, )
+            self.cursor.execute(sql, params)
+
+            return member_state
+        except pgdb.Error, e:
+            raise DBException("member update failed: %s" % e)
+        
+
+
+    ### Term-related methods ###
+
+    def select_term(self, memberid, term):
+        """
+        Determines whether a member is registered for a term.
+        
+        Parameters:
+            memberid - the member id number
+            term     - the term to check
+
+        Returns: a matching term, or None
+
+        Example: connection.select_term(3349, 'f2006') -> 'f2006'
+        """
+        sql = "SELECT term FROM terms WHERE memberid=%d AND term=%s"
+        params = [ memberid, term ]
+
+        # retrieve matches
+        try:
+            self.cursor.execute(sql, params)
+            result = self.cursor.fetchall()
+        except pgdb.Error, e:
+            raise DBException("SELECT statement failed: %s" % e)
+
+        if len(result) > 1:
+            raise DBException("multiple rows in terms with memberid=%d term=%s" % (memberid, term))
+        elif len(result) == 0:
+            return None
+        else:
+            return result[0][0]
+
+
+    def select_terms(self, memberid):
+        """
+        Retrieves a list of terms a member is registered for.
+
+        Parameters:
+            memberid - the member id number
+
+        Returns: a sorted list of terms
+        
+        Example: connection.select_terms(3349) -> ['f2006']
+        """
+        sql = "SELECT term FROM terms WHERE memberid=%d"
+        params = [ memberid ]
+
+        # retrieve the list of terms
+        try:
+            self.cursor.execute(sql, params)
+            result = self.cursor.fetchall()
+        except pgdb.Error, e:
+            raise DBException("SELECT statement failed: %s" % e)
+        
+        result = [ row[0] for row in result ]
+
+        return result
+
+
+    def insert_term(self, memberid, term):
+        """
+        Registers a member for a term.
+
+        Parameters:
+            memberid - the member id number to register
+            term     - string representation of the term
+
+        Example: connection.insert_term(3349, 'f2006')
+        """
+        sql = "INSERT INTO terms (memberid, term) VALUES (%d, %s)"
+        params = [ memberid, term ]
+        
+        try:
+            self.cursor.execute(sql, params)
+        except pgdb.Error, e:
+            raise DBException("INSERT statement failed: %s" % e)
+
+
+    def delete_term(self, memberid, term):
+        """
+        Unregisters a member for a term.
+
+        Parameters:
+            memberid - the member id number to register
+            term     - string representation of the term
+        
+        Example: connection.delete_term(3349, 'f2006')
+        """
+        sql = "DELETE FROM terms WHERE memberid=%d and term=%s"
+        params = [ memberid, term ]
+
+        try:
+            self.cursor.execute(sql, params)
+        except pgdb.Error, e:
+            raise DBException("DELETE statement failed: %s" % e)
+
+    
+    def delete_term_all(self, memberid):
+        """
+        Unregisters a member for all registered terms.
+
+        Parameters:
+            memberid - the member id number to unregister
+        
+        Example: connection.delete_term_all(3349)
+        """
+        sql = "DELETE FROM terms WHERE memberid=%d"
+        params = [ memberid ]
+        
+        # retrieve a list of terms
+        try:
+            self.cursor.execute(sql, params)
+        except pgdb.Error, e:
+            raise DBException("DELETE statement failed: %s" % e)
+
+
+    ### Miscellaneous methods ###
+
+    def trim_memberid_sequence(self):
+        """
+        Sets the value of the member id sequence to the id of the newest
+        member. For use after extensive testing to prevent large
+        intervals of unused memberids.
+
+        Note: this does nothing unless the most recently added member(s) have been deleted
+        """
+        self.cursor.execute("SELECT setval('memberid_seq', (SELECT max(memberid) FROM members))")
+
+
+
+### Tests ###
+
+if __name__ == '__main__':
+    HOST = "localhost"
+    DATABASE = "ceo"
+
+    connection = DBConnection()
+
+    print "Running disconnect()"
+    connection.disconnect()
+
+    print "Running connect('%s', '%s')" % (HOST, DATABASE)
+    connection.connect(HOST, DATABASE)
+
+    print "Running select_all_members()", "->", len(connection.select_all_members()), "members"
+    print "Running select_member_by_id(0)", "->", connection.select_member_by_id(0)['userid']
+    print "Running select_members_by_name('Spang')", "->", connection.select_members_by_name('Spang').keys()
+    print "Running select_members_by_term('f2006')", "->", "[" + ", ".join(map(str, connection.select_members_by_term('f2006').keys()[0:10])) + " ...]"
+    
+    print "Running insert_member('test_member', '99999999', 'program')",
+    memberid = connection.insert_member('test_member', '99999999', 'program')
+    print "->", memberid
+
+    print "Running select_member_by_id(%d)" % memberid, "->", connection.select_member_by_id(memberid)
+    print "Running insert_term(%d, 'f2006')" % memberid
+    connection.insert_term(memberid, 'f2006')
+
+    print "Running select_terms(%d)" % memberid, "->", connection.select_terms(memberid)
+    print "Running update_member({'memberid':%d,'name':'test_updated','studentid':-1})" % memberid
+    connection.update_member({'memberid':memberid,'name':'test_updated','studentid':99999999})
+    print "Running select_member_by_id(%d)" % memberid, "->", connection.select_member_by_id(memberid)
+   
+    print "Running rollback()"
+    connection.rollback()
+
+    print "Resetting memberid sequence"
+    connection.trim_memberid_sequence()
+    
+    print "Running disconnect()"
+    connection.disconnect() 
diff --git a/pylib/csc/backends/ipc.py b/pylib/csc/backends/ipc.py
new file mode 100644 (file)
index 0000000..8348a57
--- /dev/null
@@ -0,0 +1,222 @@
+# $Id: ipc.py 26 2006-12-20 21:25:08Z mspang $
+"""
+IPC Library Functions
+
+This module contains very UNIX-specific code to allow interactive
+communication with another program. For CEO they are required to
+talk to kadmin because there is no Kerberos administration Python
+module. Real bindings to libkadm5 are doable and thus a TODO.
+"""
+import os, pty, select
+
+
+class _pty_file(object):
+    """
+    A 'file'-like wrapper class for pseudoterminal file descriptors.
+    
+    This wrapper is necessary because Python has a nasty
+    habit of throwing OSError at pty EOF.
+      
+    This class also implements timeouts for read operations
+    which are handy for avoiding deadlock when both
+    processes are blocked in a read().
+      
+    See the Python documentation of the file class
+    for explanation of the methods.
+    """
+    def __init__(self, fd):
+        self.fd = fd
+        self.buffer = ''
+        self.closed = False
+    def __repr__(self):
+        status='open'
+        if self.closed:
+            status = 'closed'
+        return "<" + status + " pty '" + os.ttyname(self.fd) + "'>"
+    def read(self, size=-1, block=True, timeout=0.1):
+        if self.closed: raise ValueError
+        if size < 0:
+            data = None
+
+            # read data, catching OSError as EOF
+            try:
+                while data != '':
+                
+                    # wait timeout for the pty to become ready, otherwise stop reading
+                    if not block and len(select.select([self.fd],[],[], timeout)[0]) == 0:
+                       break
+                       
+                    data = os.read(self.fd, 65536)
+                    self.buffer += data
+            except OSError:
+                pass
+            
+            data = self.buffer
+            self.buffer = ''
+            return data
+        else:
+            if len(self.buffer) < size:
+
+                # read data, catching OSError as EOF
+                try:
+                    
+                    # wait timeout for the pty to become ready, then read
+                    if block or len(select.select([self.fd],[],[], timeout)[0]) != 0:
+                        self.buffer += os.read(self.fd, size - len(self.buffer) )
+                    
+                except OSError:
+                    pass
+
+            data = self.buffer[:size]
+            self.buffer = self.buffer[size:]
+            return data
+    def readline(self, size=-1, block=True, timeout=0.1):
+        data = None
+
+        # read data, catching OSError as EOF
+        try:
+            while data != '' and self.buffer.find("\n") == -1 and (size < 0 or len(self.buffer) < size):
+
+                # wait timeout for the pty to become ready, otherwise stop reading
+                if not block and len(select.select([self.fd],[],[], timeout)[0]) == 0:
+                   break
+                 
+                data = os.read(self.fd, 128)
+                self.buffer += data
+        except OSError:
+            pass
+            
+        split_index = self.buffer.find("\n") + 1
+        if split_index < 0:
+            split_index = len(self.buffer)
+        if size >= 0 and split_index > size:
+            split_index = size
+        line = self.buffer[:split_index]
+        self.buffer = self.buffer[split_index:]
+        return line
+    def readlines(self, sizehint=None, block=True, timeout=0.1):
+        lines = []
+        line = None
+        while True:
+            line = self.readline(-1, False, timeout)
+            if line == '': break
+            lines.append(line)
+        return lines
+    def write(self, data):
+        if self.closed: raise ValueError
+        os.write(self.fd, data)
+    def writelines(self, lines):
+        for line in lines:
+            self.write(line)
+    def __iter__(self):
+        return self
+    def next(self):
+        line = self.readline()
+        if line == '':
+            raise StopIteration
+        return line
+    def isatty(self):
+        if self.closed: raise ValueError
+        return os.isatty(self.fd)
+    def fileno(self):
+        if self.closed: raise ValueError
+        return self.fd
+    def flush(self):
+        if self.closed: raise ValueError
+        os.fsync(self.fd)
+    def close(self):
+        if not self.closed: os.close(self.fd)
+        self.closed = True
+            
+
+def popeni(command, args, env=None):
+    """
+    Open an interactive session with another command.
+
+    Parameters:
+        command - the command to run (full path)
+        args    - a list of arguments to pass to command
+        env     - optional environment for command
+
+    Returns: (pid, stdout, stdIn)
+    """
+    
+    # use a pipe to send data to the child
+    child_stdin, parent_stdin = os.pipe()
+
+    # a pipe for receiving data would cause buffering and
+    # is therefore not suitable for interactive communication
+    # i.e. parent_stdout, child_stdout = os.pipe()
+
+    # therefore a pty must be used instead
+    master, slave = pty.openpty()
+    # collect both stdout and stderr on the pty
+    parent_stdout, child_stdout = master, slave
+    parent_stderr, child_stderr = master, slave
+
+    # fork the child to communicate with
+    pid = os.fork()
+
+    # child process
+    if pid == 0:
+     
+        # close all of the parent's fds
+        os.close(parent_stdin)
+        if parent_stdout != parent_stdin:
+            os.close(parent_stdout)
+        if parent_stderr != parent_stdin and parent_stderr != parent_stdout:
+            os.close(parent_stderr)
+    
+        # if stdout is a terminal, set it to the controlling terminal
+        if os.isatty(child_stdout):
+
+            # determine the filename of the tty
+            tty = os.ttyname(child_stdout)
+        
+            # create a new session to disconnect
+            # from the parent's controlling terminal
+            os.setsid()
+    
+            # set the controlling terminal to the pty
+            # by opening it (and closing it again since
+            # it's already open as child_stdout)
+            fd = os.open(tty, os.O_RDWR);
+            os.close(fd)
+
+        # init stdin/out/err
+        os.dup2(child_stdin,  0)
+        os.dup2(child_stdout, 1)
+        if child_stderr >= 0:
+            os.dup2(child_stderr, 2)
+    
+        # finally, execute the child
+        if env:
+            os.execv(command, args, env)
+        else:
+            os.execv(command, args)
+
+    # parent process
+    else:
+
+        # close all of the child's fds
+        os.close(child_stdin)
+        if child_stdout != child_stdin:
+            os.close(child_stdout)
+        if child_stderr >= 0 and child_stderr != child_stdin and child_stderr != child_stdout:
+            os.close(child_stderr)
+
+        return pid, _pty_file(parent_stdout), os.fdopen(parent_stdin, 'w')
+
+
+### Tests ###
+
+if __name__ == '__main__':
+
+    import sys
+    pid, recv, send = popeni('/usr/sbin/kadmin.local', ['kadmin'])
+
+    send.write("listprincs\n")
+    send.flush()
+
+    print recv.readlines()
diff --git a/pylib/csc/backends/krb.py b/pylib/csc/backends/krb.py
new file mode 100644 (file)
index 0000000..23d021b
--- /dev/null
@@ -0,0 +1,448 @@
+# $Id: krb.py 40 2006-12-29 00:40:31Z mspang $
+"""
+Kerberos Backend Interface
+
+This module is intended to be a thin wrapper around Kerberos operations.
+Methods on the connection object correspond in a straightforward way to
+calls to the Kerberos Master server.
+
+A Kerberos principal is the second half of a CSC UNIX account. The principal
+stores the user's password and and is used for all authentication on CSC
+systems. Accounts that do not authenticate (e.g. club accounts) do not need
+a Kerberos principal.
+
+Unfortunately, there are no Python bindings to libkadm at this time. As a
+temporary workaround, This module communicates with the kadmin CLI interface
+via a pseudoterminal and pipe.
+"""
+import os
+import ipc
+
+
+class KrbException(Exception):
+    """Exception class for all Kerberos-related errors."""
+    pass
+
+
+class KrbConnection(object):
+    """
+    Connection to the Kerberos master server (kadmind). All Kerberos
+    principal updates are made via this class.
+
+    Exceptions: (all methods)
+        KrbException - on query/update failure
+
+    Example:
+        connection = KrbConnection()
+        connection.connect(...)
+
+        # make queries and updates, e.g.
+        connection.delete_principal("mspang")
+
+        connection.disconnect()
+    """
+
+    def __init__(self):
+        self.pid = None
+    
+
+    def connect(self, principal, keytab):
+        """
+        Establishes the connection to the Kerberos master server.
+
+        Parameters:
+            principal - the Kerberos princiapl to authenticate as
+            keytab    - keytab filename for authentication
+
+        Example: connection.connect('ceo/admin@CSCLUB.UWATERLOO.CA', '/etc/ceo.keytab')
+        """
+
+        # check keytab
+        if not os.access(keytab, os.R_OK):
+            raise KrbException("cannot access Kerberos keytab: %s" % keytab)
+        
+        # command to run
+        kadmin = '/usr/sbin/kadmin'
+        kadmin_args = ['kadmin', '-p', principal, '-kt', keytab]
+        
+        # fork the kadmin command
+        self.pid, self.kadm_out, self.kadm_in = ipc.popeni(kadmin, kadmin_args)
+        
+        # read welcome messages
+        welcome = self.read_result()
+        
+        # sanity checks on welcome messages
+        for line in welcome:
+            
+            # ignore auth message
+            if line.find("Authenticating") == 0:
+                continue
+
+            # ignore log file message
+            elif line.find("kadmin.log") != -1:
+                continue
+
+            # error message?
+            else:
+                raise KrbException("unexpected kadmin output: " + welcome[0])
+    
+    
+    def disconnect(self):
+        """Close the connection to the master server."""
+        
+        if self.pid:
+            
+            # close the pipe connected to kadmin's standard input
+            self.kadm_in.close()
+            
+            # close the master pty connected to kadmin's stdout
+            try:
+                self.kadm_out.close()
+            except OSError:
+                pass
+
+            # wait for kadmin to terminate
+            os.waitpid(self.pid, 0)
+            self.pid = None
+
+
+    def connected(self):
+        """Determine whether the connection has been established."""
+
+        return self.pid != None
+
+
+
+    ### Helper Methods ###
+    
+    def read_result(self):
+        """
+        Helper function to read output of kadmin until it
+        prompts for input.
+
+        Returns: a list of lines returned by kadmin
+        """
+
+        # list of lines output by kadmin
+        result = []
+
+        # the kadmin prompt that signals the end output
+        # note: KADMIN_ARGS[0] must be "kadmin" or the actual prompt will differ
+        prompt = "kadmin:"
+
+        # timeout variables. the timeout will start at timeout and
+        # increase up to max_timeout when read() returns nothing (i.e., times out)
+        timeout = 0.01
+        timeout_increment = 0.10
+        timeout_maximum = 1.00
+        
+        # input loop: read from kadmin until the kadmin prompt
+        buffer = ''
+        while True:
+            
+            # attempt to read any available data
+            data = self.kadm_out.read(block=False, timeout=timeout)
+            buffer += data
+
+            # nothing was read
+            if data == '':
+                
+                # so wait longer for data next time
+                if timeout < timeout_maximum:
+                    timeout += timeout_increment
+                    continue
+
+                # give up after too much waiting
+                else:
+
+                    # check kadmin status
+                    status = os.waitpid(self.pid, os.WNOHANG)
+                    if status[0] == 0:
+
+                        # kadmin still alive
+                        raise KrbException("timeout while reading response from kadmin")
+
+                    else:
+
+                        # kadmin died!
+                        raise KrbException("kadmin died while reading response")
+
+            # break into lines and save all but the final
+            # line (which is incomplete) into result
+            lines = buffer.split("\n")
+            buffer = lines[-1]
+            lines = lines[:-1]
+            for line in lines:
+                line = line.strip()
+                result.append(line)
+           
+            # if the incomplete lines in the buffer is the kadmin prompt,
+            # then the result is complete and may be returned
+            if buffer.strip() == prompt:
+                break
+
+        return result
+    
+    
+    def execute(self, command):
+        """
+        Helper function to execute a kadmin command.
+
+        Parameters:
+            command - the command to execute
+        
+        Returns: a list of lines output by the command
+        """
+        
+        # there should be no remaining output from the previous
+        # command. if there is then something is broken.
+        stale_output = self.kadm_out.read(block=False, timeout=0)
+        if stale_output != '':
+            raise KrbException("unexpected kadmin output: " + stale_output)
+        
+        # send the command to kadmin
+        self.kadm_in.write(command + "\n")
+        self.kadm_in.flush()
+        
+        # read the command output and return it
+        result = self.read_result()
+        return result
+    
+
+    
+    ### Commands ###
+    
+    def list_principals(self):
+        """
+        Retrieve a list of Kerberos principals.
+
+        Returns: a list of principals
+
+        Example: connection.list_principals() -> [
+                     "ceo/admin@CSCLUB.UWATERLOO.CA",
+                     "sysadmin/admin@CSCLUB.UWATERLOO.CA",
+                     "mspang@CSCLUB.UWATERLOO.CA",
+                 ]
+        
+        """
+        
+        principals = self.execute("list_principals")
+
+        # assuming that there at least some host principals
+        if len(principals) < 1:
+            raise KrbException("no kerberos principals")
+
+        # detect error message
+        if principals[0].find("kadmin:") == 0:
+            raise KrbException("list_principals returned error: " + principals[0])
+
+        # verify principals are well-formed
+        for principal in principals:
+            if principal.find("@") == -1:
+                raise KrbException('malformed pricipal: "' + principal + '"')
+
+        return principals
+    
+    
+    def get_principal(self, principal):
+        """
+        Retrieve principal details.
+
+        Returns: a dictionary of principal attributes
+
+        Example: connection.get_principal("ceo/admin@CSCLUB.UWATERLOO.CA") -> {
+                     "Principal": "ceo/admin@CSCLUB.UWATERLOO.CA",
+                     "Policy": "[none]",
+                     ...
+                 }
+        """
+        
+        output = self.execute('get_principal "' + principal + '"')
+        
+        # detect error message
+        if output[0].find("kadmin:") == 0:
+            raise KrbException("get_principal returned error: " + output[0])
+
+        # detect more errors
+        if output[0].find("get_principal: ") == 0:
+            
+            message = output[0][15:]
+            
+            # principal does not exist => None
+            if message.find("Principal does not exist") == 0:
+                return None
+
+        # dictionary to store attributes
+        principal_attributes = {}
+
+        # attributes that will not be returned
+        ignore_attributes = ['Key']
+
+        # split output into a dictionary of attributes
+        for line in output:
+            key, value = line.split(":", 1)
+            value = value.strip()
+            if not key in ignore_attributes:
+                principal_attributes[key] = value
+                
+        return principal_attributes
+    
+    
+    def get_privs(self):
+        """
+        Retrieve privileges of the current principal.
+        
+        Returns: a list of privileges
+
+        Example: connection.get_privs() ->
+                     [ "GET", "ADD", "MODIFY", "DELETE" ]
+        """
+        
+        output = self.execute("get_privs")
+
+        # one line of output is expected
+        if len(output) > 1:
+            raise KrbException("unexpected output of get_privs: " + output[1])
+
+        # detect error message
+        if output[0].find("kadmin:") == 0:
+            raise KrbException("get_privs returned error: " + output[0])
+
+        # parse output by removing the prefix and splitting it around spaces
+        if output[0][:20] != "current privileges: ":
+            raise KrbException("malformed get_privs output: " + output[0])
+        privs = output[0][20:].split(" ")
+
+        return privs
+    
+    
+    def add_principal(self, principal, password):
+        """
+        Create a new principal.
+
+        Parameters:
+            principal - the name of the principal
+            password  - the principal's initial password
+        
+        Example: connection.add_principal("mspang@CSCLUB.UWATERLOO.CA", "opensesame")
+        """
+
+        # exec the add_principal command
+        if password.find('"') == -1:
+            self.kadm_in.write('add_principal -pw "' + password + '" "' + principal + '"\n')
+            
+        # fools at MIT didn't bother implementing escaping, so passwords
+        # that contain double quotes must be treated specially
+        else:
+            self.kadm_in.write('add_principal "' + principal + '"\n')
+            self.kadm_in.write(password + "\n" + password + "\n")
+
+        # send request and read response
+        self.kadm_in.flush()
+        output = self.read_result()
+
+        # verify output
+        created = False
+        for line in output:
+
+            # ignore NOTICE lines
+            if line.find("NOTICE:") == 0:
+                continue
+
+            # ignore prompts
+            elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0:
+                continue
+
+            # record whether success message was encountered
+            elif line.find("Principal") == 0 and line.find("created.") != 0:
+                created = True
+
+            # error messages
+            elif line.find("add_principal:") == 0 or line.find("kadmin:") == 0:
+                
+                # principal exists
+                if line.find("already exists") != -1:
+                    raise KrbException("principal already exists")
+
+                # misc errors
+                else:
+                    raise KrbException(line)
+
+            # unknown output
+            else:
+                raise KrbException("unexpected add_principal output: " + line)
+           
+        # ensure success message was received
+        if not created:
+            raise KrbException("did not receive principal created in response")
+    
+    
+    def delete_principal(self, principal):
+        """
+        Delete a principal.
+
+        Parameters:
+            principal - the principal name
+
+        Example: connection.delete_principal("mspang@CSCLUB.UWATERLOO.CA")
+        """
+        
+        # exec the delete_principal command and read response
+        self.kadm_in.write('delete_principal -force "' + principal + '"\n')
+        self.kadm_in.flush()
+        output = self.read_result()
+
+        # verify output
+        deleted = False
+        for line in output:
+
+            # ignore reminder
+            if line.find("Make sure that") == 0:
+                continue
+
+            # record whether success message was encountered
+            elif line.find("Principal") == 0 and line.find("deleted.") != -1:
+                deleted = True
+
+            # error messages
+            elif line.find("delete_principal:") == 0 or line.find("kadmin:") == 0:
+                
+                # principal exists
+                if line.find("does not exist") != -1:
+                    raise KrbException("principal does not exist")
+
+                # misc errors
+                else:
+                    raise KrbException(line)
+
+            # unknown output
+            else:
+                raise KrbException("unexpected delete_principal output: " + line)
+           
+        # ensure success message was received
+        if not deleted:
+            raise KrbException("did not receive principal deleted")
+        
+
+
+### Tests ###
+
+if __name__ == '__main__':
+    PRINCIPAL = 'ceo/admin@CSCLUB.UWATERLOO.CA'
+    KEYTAB = 'ceo.keytab'
+    
+    connection = KrbConnection()
+    print "running disconnect()"
+    connection.disconnect()
+    print "running connect('%s', '%s')" % (PRINCIPAL, KEYTAB)
+    connection.connect(PRINCIPAL, KEYTAB)
+    print "running list_principals()", "->", "[" + ", ".join(map(repr,connection.list_principals()[0:3])) + " ...]"
+    print "running get_privs()", "->", str(connection.get_privs())
+    print "running add_principal('testtest', 'BLAH')"
+    connection.add_principal("testtest", "FJDSLDLFKJSF")
+    print "running get_principal('testtest')", "->", '(' + connection.get_principal("testtest")['Principal'] + ')'
+    print "running delete_principal('testtest')"
+    connection.delete_principal("testtest")
+    print "running disconnect()"
+    connection.disconnect()
+
diff --git a/pylib/csc/backends/ldapi.py b/pylib/csc/backends/ldapi.py
new file mode 100644 (file)
index 0000000..cf32e8f
--- /dev/null
@@ -0,0 +1,577 @@
+# $Id: ldapi.py 41 2006-12-29 04:22:31Z mspang $
+"""
+LDAP Backend Interface
+
+This module is intended to be a thin wrapper around LDAP operations.
+Methods on the connection object correspond in a straightforward way
+to LDAP queries and updates.
+
+A LDAP entry is the most important component of a CSC UNIX account.
+The entry contains the username, user id number, real name, shell,
+and other important information. All non-local UNIX accounts must
+have an LDAP entry, even if the account does not log in directly.
+
+This module makes use of python-ldap, a Python module with bindings
+to libldap, OpenLDAP's native C client library.
+"""
+import ldap.modlist
+
+
+class LDAPException(Exception):
+    """Exception class for LDAP-related errors."""
+
+
+class LDAPConnection(object):
+    """
+    Connection to the LDAP directory. All directory
+    queries and updates are made via this class.
+
+    Exceptions: (all methods)
+        LDAPException - on directory query failure
+
+    Example:
+         connection = LDAPConnection()
+         connection.connect(...)
+
+         # make queries and updates, e.g.
+         connection.user_delete('mspang')
+
+         connection.disconnect()
+    """
+
+    def __init__(self):
+        self.ldap = None
+
+    
+    def connect(self, server, bind_dn, bind_pw, user_base, group_base):
+        """
+        Establish a connection to the LDAP Server.
+
+        Parameters:
+            server     - connection string (e.g. ldap://foo.com, ldaps://bar.com)
+            bind_dn    - distinguished name to bind to
+            bind_pw    - password of bind_dn
+            user_base  - base of the users subtree
+            group_base - baes of the group subtree
+
+        Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
+                     'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
+                     'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
+        
+        """
+
+        if bind_pw == None: bind_pw = ''
+
+        try:
+
+            # open the connection
+            self.ldap = ldap.initialize(server)
+
+            # authenticate as ceo
+            self.ldap.simple_bind_s(bind_dn, bind_pw)
+
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to connect: %s" % e)
+
+        self.user_base = user_base
+        self.group_base = group_base
+
+
+    def disconnect(self):
+        """Close the connection to the LDAP server."""
+        
+        if self.ldap:
+
+            # close connection
+            try:
+                self.ldap.unbind_s()
+                self.ldap = None
+            except ldap.LDAPError, e:
+                raise LDAPException("unable to disconnect: %s" % e)
+
+
+    def connected(self):
+        """Determine whether the connection has been established."""
+
+        return self.ldap != None
+
+
+
+    ### Helper Methods ###
+
+    def lookup(self, dn):
+        """
+        Helper method to retrieve the attributes of an entry.
+
+        Parameters:
+            dn - the distinguished name of the directory entry
+
+        Returns: a dictionary of attributes of the matched dn, or
+                 None of the dn does not exist in the directory
+        """
+
+        # search for the specified dn
+        try:
+            matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
+        except ldap.NO_SUCH_OBJECT:
+            return None
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
+            
+        # this should never happen due to the nature of DNs
+        if len(matches) > 1:
+            raise LDAPException("duplicate dn in ldap: " + dn)
+        
+        # return the attributes of the single successful match
+        else:
+            match = matches[0]
+            match_dn, match_attributes = match
+            return match_attributes
+
+
+    
+    ### User-related Methods ###
+
+    def user_lookup(self, uid):
+        """
+        Retrieve the attributes of a user.
+
+        Parameters:
+            uid - the UNIX user accound name of the user
+
+        Returns: attributes of user with uid
+
+        Example: connection.user_lookup('mspang') ->
+                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
+        """
+        
+        dn = 'uid=' + uid + ',' + self.user_base
+        return self.lookup(dn)
+        
+
+    def user_search(self, filter):
+        """
+        Helper for user searches.
+
+        Parameters:
+            filter - LDAP filter string to match users against
+
+        Returns: the list of uids matched
+        """
+
+        # search for entries that match the filter
+        try:
+            matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, filter)
+        except ldap.LDAPError, e:
+            raise LDAPException("user search failed: %s" % e)
+        
+        # list for uids found
+        uids = []
+        
+        for match in matches:
+            dn, attributes = match
+            
+            # uid is a required attribute of posixAccount
+            if not attributes.has_key('uid'):
+                raise LDAPException(dn + ' (posixAccount) has no uid')
+            
+            # do not handle the case of multiple usernames in one entry (yet)
+            elif len(attributes['uid']) > 1:
+                raise LDAPException(dn + ' (posixAccount) has multiple uids')
+            
+            # append the sole uid of this match to the list
+            uids.append( attributes['uid'][0] )
+
+        return uids
+
+
+    def user_search_id(self, uidNumber):
+        """
+        Retrieves a list of users with a certain UNIX uid number.
+
+        LDAP (or passwd for that matter) does not enforce any
+        restriction on the number of accounts that can have
+        a certain UID. Therefore this method returns a list of matches.
+
+        Parameters:
+            uidNumber - the user id of the accounts desired
+
+        Returns: the list of uids matched
+
+        Example: connection.user_search_id(21292) -> ['mspang']
+        """
+
+        # search for posixAccount entries with the specified uidNumber
+        filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
+        return self.user_search(filter)
+
+
+    def user_search_gid(self, gidNumber):
+        """
+        Retrieves a list of users with a certain UNIX gid number.
+
+        Parameters:
+            gidNumber - the group id of the accounts desired
+
+        Returns: the list of uids matched
+        """
+
+        # search for posixAccount entries with the specified gidNumber
+        filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
+        return self.user_search(filter)
+
+
+    def user_add(self, uid, cn, loginShell, uidNumber, gidNumber, homeDirectory, gecos):
+        """
+        Adds a user to the directory.
+
+        Parameters:
+            uid           - the UNIX username for the account
+            cn            - the full name of the member
+            userPassword  - password of the account (our setup does not use this)
+            loginShell    - login shell for the user
+            uidNumber     - the UNIX user id number
+            gidNumber     - the UNIX group id number
+            homeDirectory - home directory for the user
+            gecos         - comment field (usually stores miscellania)
+
+        Example: connection.user_add('mspang', 'Michael Spang',
+                     '/bin/bash', 21292, 100, '/users/mspang',
+                     'Michael Spang,,,')
+        """
+        
+        dn = 'uid=' + uid + ',' + self.user_base
+        attrs = {
+            'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
+            'uid': [ uid ],
+            'cn': [ cn ],
+            'loginShell': [ loginShell ],
+            'uidNumber': [ str(uidNumber) ],
+            'gidNumber': [ str(gidNumber) ],
+            'homeDirectory': [ homeDirectory ],
+            'gecos': [ gecos ],
+        }
+
+        try:
+            modlist = ldap.modlist.addModlist(attrs)
+            self.ldap.add_s(dn, modlist)
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to add: %s" % e)
+
+
+    def user_modify(self, uid, attrs):
+        """
+        Update user attributes in the directory.
+
+        Parameters:
+            uid   - username of the user to modify
+            entry - dictionary as returned by user_lookup() with changes to make.
+                    omitted attributes are DELETED.
+
+        Example: user = user_lookup('mspang')
+                 user['uidNumber'] = [ '0' ]
+                 connection.user_modify('mspang', user)
+        """
+
+        # distinguished name of the entry to modify
+        dn = 'uid=' + uid + ',' + self.user_base
+
+        # retrieve current state of user
+        old_user = self.user_lookup(uid)
+
+        try:
+            
+            # build list of modifications to make
+            changes = ldap.modlist.modifyModlist(old_user, attrs)
+
+            # apply changes
+            self.ldap.modify_s(dn, changes)
+
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to modify: %s" % e)
+
+
+    def user_delete(self, uid):
+        """
+        Removes a user from the directory.
+
+        Parameters:
+            uid - the UNIX username of the account
+        
+        Example: connection.user_delete('mspang')
+        """
+        
+        try:
+            dn = 'uid=' + uid + ',' + self.user_base
+            self.ldap.delete_s(dn)
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to delete: %s" % e)
+
+
+
+    ### Group-related Methods ###
+
+    def group_lookup(self, cn):
+        """
+        Retrieves the attributes of a group.
+
+        Parameters:
+            cn - the UNIX group name to lookup
+
+        Returns: attributes of group with cn
+
+        Example: connection.group_lookup('office') -> {
+                     'cn': 'office',
+                     'gidNumber', '1001',
+                     ...
+                 }
+        """
+        
+        dn = 'cn=' + cn + ',' + self.group_base
+        return self.lookup(dn)
+                                                                                    
+
+    def group_search_id(self, gidNumber):
+        """
+        Retrieves a list of groups with the specified UNIX group number.
+        
+        Parameters:
+            gidNumber - the group id of the groups desired
+
+        Returns: a list of groups with gid gidNumber
+
+        Example: connection.group_search_id(1001) -> ['office']
+        """
+
+        # search for posixAccount entries with the specified uidNumber
+        try:
+            filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
+            matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, filter)
+        except ldap.LDAPError,e :
+            raise LDAPException("group search failed: %s" % e)
+
+        # list for groups found
+        group_cns = []
+
+        for match in matches:
+            dn, attributes = match
+
+            # cn is a required attribute of posixGroup
+            if not attributes.has_key('cn'):
+                raise LDAPException(dn + ' (posixGroup) has no cn')
+
+            # do not handle the case of multiple cns for one group (yet)
+            elif len(attributes['cn']) > 1:
+                raise LDAPException(dn + ' (posixGroup) has multiple cns')
+
+            # append the sole uid of this match to the list
+            group_cns.append( attributes['cn'][0] )
+
+        return group_cns
+
+
+    def group_add(self, cn, gidNumber):
+        """
+        Adds a group to the directory.
+
+        Parameters:
+            cn        - the name of the group
+            gidNumber - the number of the group
+
+        Example: connection.group_add('office', 1001)
+        """
+        
+        dn = 'cn=' + cn + ',' + self.group_base
+        attrs = {
+            'objectClass': [ 'top', 'posixGroup' ],
+            'cn': [ cn ],
+            'gidNumber': [ str(gidNumber) ],
+        }
+
+        try:
+            modlist = ldap.modlist.addModlist(attrs)
+            self.ldap.add_s(dn, modlist)
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to add group: %s" % e)
+
+
+    def group_modify(self, cn, attrs):
+        """
+        Update group attributes in the directory.
+        
+        The only available updates are fairly destructive
+        (rename or renumber) but this method is provided
+        for completeness.
+
+        Parameters:
+            cn    - name of the group to modify
+            entry - dictionary as returned by group_lookup() with changes to make.
+                    omitted attributes are DELETED.
+
+        Example: group = group_lookup('office')
+                 group['gidNumber'] = [ str(connection.first_id(20000, 40000)) ]
+                 del group['memberUid']
+                 connection.group_modify('office', group)
+        """
+
+        # distinguished name of the entry to modify
+        dn = 'cn=' + cn + ',' + self.group_base
+
+        # retrieve current state of group
+        old_group = self.group_lookup(cn)
+
+        try:
+            
+            # build list of modifications to make
+            changes = ldap.modlist.modifyModlist(old_group, attrs)
+
+            # apply changes
+            self.ldap.modify_s(dn, changes)
+
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to modify: %s" % e)
+
+
+    def group_delete(self, cn):
+        """
+        Removes a group from the directory."
+
+        Parameters:
+            cn - the name of the group
+
+        Example: connection.group_delete('office')
+        """
+        
+        try:
+            dn = 'cn=' + cn + ',' + self.group_base
+            self.ldap.delete_s(dn)
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to delete group: %s" % e)
+
+
+    def group_members(self, cn):
+        """
+        Retrieves a group's members.
+
+        Parameters:
+            cn - the name of the group
+
+        Example: connection.group_members('office') ->
+                 ['sfflaw', 'jeperry', 'cschopf' ...]
+        """
+
+        group = self.group_lookup(cn)
+        return group.get('memberUid', None)
+
+
+    ### Miscellaneous Methods ###
+    
+    def first_id(self, minimum, maximum):
+        """
+        Determines the first available id within a range.
+
+        To be "available", there must be neither a user
+        with the id nor a group with the id.
+
+        Parameters:
+            minimum - smallest uid that may be returned
+            maximum - largest uid that may be returned
+
+        Returns: the id, or None if there are none available
+
+        Example: connection.first_id(20000, 40000) -> 20018
+        """
+
+        # compile a list of used uids
+        try:
+            users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['uidNumber'])
+        except ldap.LDAPError, e:
+            raise LDAPException("search for uids failed: %s" % e)
+        uids = []
+        for user in users:
+            dn, attrs = user
+            uid = int(attrs['uidNumber'][0])
+            if minimum <= uid <= maximum:
+                uids.append(uid)
+
+        # compile a list of used gids
+        try:
+            groups = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)', ['gidNumber'])
+        except ldap.LDAPError, e:
+            raise LDAPException("search for gids failed: %s" % e)
+        gids = []
+        for group in groups:
+            dn, attrs = group
+            gid = int(attrs['gidNumber'][0])
+            if minimum <= gid <= maximum:
+                gids.append(gid)
+
+        # iterate through ids and return the first available
+        for id in xrange(minimum, maximum+1):
+            if not id in uids and not id in gids:
+                return id
+
+        # no suitable id was found
+        return None
+
+
+### Tests ###
+
+if __name__ == '__main__':
+    
+    password_file = 'ldap.ceo'
+    server   = 'ldaps:///'
+    base_dn  = 'dc=csclub,dc=uwaterloo,dc=ca'
+    bind_dn  = 'cn=ceo,' + base_dn
+    user_dn  = 'ou=People,' + base_dn
+    group_dn = 'ou=Group,' + base_dn
+    bind_pw = open(password_file).readline().strip()
+
+    connection = LDAPConnection()
+    print "running disconnect()"
+    connection.disconnect()
+    print "running connect('%s', '%s', '%s', '%s', '%s')" % (server, bind_dn, '***', user_dn, group_dn)
+    connection.connect(server, bind_dn, bind_pw, user_dn, group_dn)
+    print "running user_lookup('mspang')", "->", "(%s)" % connection.user_lookup('mspang')['uidNumber'][0]
+    print "running user_search_id(21292)", "->", connection.user_search_id(21292)
+    print "running first_id(20000, 40000)", "->",
+    first_id = connection.first_id(20000, 40000)
+    print first_id
+    print "running group_add('testgroup', %d)" % first_id
+    try:
+        connection.group_add('testgroup', first_id)
+    except Exception, e:
+        print "FAILED: %s (continuing)" % e
+    print "running user_add('testuser', 'Test User', '/bin/false', %d, %d, '/home/null', 'Test User,,,')" % (first_id, first_id)
+    try:
+        connection.user_add('testuser', 'Test User', '/bin/false', first_id, first_id, '/home/null', 'Test User,,,')
+    except Exception, e:
+        print "FAILED: %s (continuing)" % e
+    print "running user_lookup('testuser')", "->",
+    user = connection.user_lookup('testuser')
+    print repr(connection.user_lookup('testuser')['cn'][0])
+    user['homeDirectory'] = ['/home/changed']
+    user['loginShell'] = ['/bin/true']
+    print "running user_modify(...)"
+    connection.user_modify('testuser', user)
+    print "running user_lookup('testuser')", "->",
+    user = connection.user_lookup('testuser')
+    print '(%s, %s)' % (user['homeDirectory'], user['loginShell'])
+    print "running group_lookup('testgroup')", "->",
+    group = connection.group_lookup('testgroup')
+    print group
+    print "running group_modify(...)"
+    group['gidNumber'] = [str(connection.first_id(20000, 40000))]
+    group['memberUid'] = [ str(first_id) ]
+    connection.group_modify('testgroup', group)
+    print "running group_lookup('testgroup')", "->",
+    group = connection.group_lookup('testgroup')
+    print group
+    print "running user_delete('testuser')"
+    connection.user_delete('testuser')
+    print "running group_delete('testgroup')"
+    connection.group_delete('testgroup')
+    print "running user_search_gid(100)", "->", "[" + ", ".join(map(repr,connection.user_search_gid(100)[:10])) + " ...]"
+    print "running group_members('office')", "->", "[" + ", ".join(map(repr,connection.group_members('office')[:10])) + " ...]"
+    print "running disconnect()"
+    connection.disconnect()
diff --git a/pylib/csc/common/__init__.py b/pylib/csc/common/__init__.py
new file mode 100644 (file)
index 0000000..7369902
--- /dev/null
@@ -0,0 +1,3 @@
+"""
+Generally Useful Common Modules
+"""
diff --git a/pylib/csc/common/conf.py b/pylib/csc/common/conf.py
new file mode 100644 (file)
index 0000000..2902f1b
--- /dev/null
@@ -0,0 +1,46 @@
+"""Library Routines"""
+
+def read_config(config_file):
+
+    try:
+        conffile = open(config_file)
+    except IOError:
+        return None
+    
+    options = {}
+
+    while True:
+
+        line = conffile.readline()
+        if line == '':
+            break
+
+        if '#' in line:
+            line = line[:line.find('#')]
+
+        while len(line) > 1 and line[-2] == '\\':
+            line = line[:-2] + line[-1]
+            next = conffile.readline()
+            line += next
+            if next == '':
+                break
+
+        pair = map(str.strip, line.split('=', 1))
+        
+        if len(pair) == 2:
+            key, val = pair
+
+            if val[0] == val[-1] == '"':
+                val = val[1:-1]
+            else:
+                try:
+                    val = int(val)
+                except:
+                    pass
+            
+            options[key] = val
+        elif len(pair[0]) > 1:
+            key, = pair
+            options[key] = None
+
+    return options