Index: cleverbox/tags/0.1/scripts/cleverbox-admin
===================================================================
--- cleverbox/tags/0.1/scripts/cleverbox-admin (revision 91)
+++ cleverbox/tags/0.1/scripts/cleverbox-admin (revision 91)
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+import sys
+
+from cleverbox.scripts.admin import run
+sys.exit(run(sys.argv[1:]))
Index: cleverbox/tags/0.1/cleverbox/__init__.py
===================================================================
--- cleverbox/tags/0.1/cleverbox/__init__.py (revision 44)
+++ cleverbox/tags/0.1/cleverbox/__init__.py (revision 44)
@@ -0,0 +1,1 @@
+# -*- coding: utf-8 -*-
Index: cleverbox/tags/0.1/cleverbox/CHANGELOG
===================================================================
--- cleverbox/tags/0.1/cleverbox/CHANGELOG (revision 85)
+++ cleverbox/tags/0.1/cleverbox/CHANGELOG (revision 85)
@@ -0,0 +1,7 @@
+Trunk
+-----
+
+Version 0.1
+------------
+
+* Initial public release
Index: cleverbox/tags/0.1/cleverbox/scripts/admin.py
===================================================================
--- cleverbox/tags/0.1/cleverbox/scripts/admin.py (revision 93)
+++ cleverbox/tags/0.1/cleverbox/scripts/admin.py (revision 93)
@@ -0,0 +1,1049 @@
+# -*- coding: utf-8 -*-
+
+import cmd
+import os
+import shlex
+import sys
+import traceback
+import re
+from trac import util
+from trac.scripts.admin import TracAdmin
+import ConfigParser
+
+_defaults = {
+    'env_dir_layout'       : ('clients-available', 'clients-enabled',
+                              'projects-available', 'projects-enabled'),
+    'client_dir_layout' : ('htdocs', 'logs', 'tmp', 'uploads',
+                           'var/svn', 'var/trac'),
+}
+
+_version = '0.1'
+
+class CleverboxAdmin(cmd.Cmd):
+    intro = ''
+    license = ''
+    doc_header = 'Cleverbox Admin Console \n' \
+                 'Available Commands:\n'
+    ruler = ''
+    prompt = "Cleverbox > "
+    envname = None
+    __env = None
+    _date_format = '%Y-%m-%d'
+    _datetime_format = '%Y-%m-%d %H:%M:%S'
+    _date_format_hint = 'YYYY-MM-DD'
+
+    _config = None
+
+    def __init__(self, envdir = None):
+        cmd.Cmd.__init__(self)
+        if envdir:
+            self.env_set(os.path.abspath(envdir))
+        self.interactive = False
+
+    ##
+    ## Environment methods
+    ##
+
+    def env_set(self, envname, env=None):
+        self.envname = envname
+        self.prompt = "Cleverbox [%s] > " % self.envname
+        if env is not None:
+            self.__env = env
+
+        # Read configuration
+        self._config = ConfigParser.SafeConfigParser()
+        self._config.read(os.path.join(self.envname, 'cleverbox.ini'))
+
+
+    def env_check(self):
+        try:
+            pass
+            #self.__env = Environment(self.envname)
+        except:
+            return 0
+        return 1
+
+    def env_open(self):
+        try:
+            if not self.__env:
+                #self.__env = Environment(self.envname)
+                return self.__env
+        except Exception, e:
+            print 'Failed to open environment.', e
+            traceback.print_exc()
+            sys.exit(1)
+
+    def emptyline(self):
+        pass
+
+    def onecmd(self, line):
+        try:
+            rv = cmd.Cmd.onecmd(self, line) or 0
+        except SystemExit:
+            raise
+        except Exception, e:
+            print >> sys.stderr, 'Command failed: %s' % e
+            rv = 2
+        if not self.interactive:
+            return rv
+
+    def run(self):
+        self.interactive = True
+        print 'Welcome to cleverbox-admin\n'                \
+              'Interactive Cleverbox administration console.\n'       \
+              "Type:  '?' or 'help' for help on commands.\n"
+        self.cmdloop()
+
+    #
+    # Configuration related methods
+    #
+    def getConfig(self, directive, section):
+        return self._config.get(section, directive)
+
+    #
+    # Commands
+    #
+
+    ## Environment
+    _help_initenv = [('initenv', 'Environment initialisation')]
+    def do_initenv(self, line=None):
+        print "Environment initialisation in %(env_dir)s" % {'env_dir' : self.envname}
+
+        # Collect local configuration info
+        collected_infos = {}
+
+        # Path to client dir
+        default_root = '/var/www/cleverbox'
+        collected_infos['clients_root'] = raw_input('Projects directory [%s]> ' % default_root).strip() or default_root
+
+        try :
+            os.makedirs(collected_infos['clients_root'], 0775)
+        except IOError, ioexception :
+            print ioexception
+            print "** Projects storage dir could not be created."
+
+        # Apache user & group
+        d_uid = 33
+        d_gid = 33
+        collected_infos['apache_user'] = raw_input('Webserver user id [%d]> ' % d_uid).strip() or d_uid
+        collected_infos['apache_group'] = raw_input('Webserver group id [%d]> ' % d_gid).strip() or d_gid
+
+        # SSH user & group
+        d_uid = '1001'
+        d_gid = '1001'
+        collected_infos['ssh_user'] = raw_input('SSH user id [%s]> ' % d_uid).strip() or d_uid
+        collected_infos['ssh_group'] = raw_input('SSH group id [%s]> ' % d_gid).strip() or d_gid
+
+        # Write ini file
+        self._config.add_section('general')
+        for directive, value in collected_infos.items() :
+            self._config.set('general', directive, str(value))
+
+        fh_config = open(os.path.join(self.envname, 'cleverbox.ini'), 'w+')
+        self._config.write(fh_config)
+
+        # Create directory structure
+        try:
+            print "\n\tCreating directory structure\n"
+            env_dirs = []
+            for dirname in _defaults['env_dir_layout']:
+                env_dirs.append( os.path.join(self.envname, dirname) )
+            map( os.makedirs, env_dirs )
+        except IOError, ioexception :
+            print ioexception
+            print "** Environment couldn't be initialized in %(env_dir)s\n" % {'env_dir' : self.envname}
+
+        # Create VERSION file
+        try:
+            fd = open( os.path.join(self.envname, 'VERSION'), 'w+' )
+            fd.write(_version)
+        finally:
+            fd.close()
+
+        print
+        print "Environment successfully initialized\n"
+        print "You need to add this statement to your apache configuration : \n" \
+              "\t'Include %(env_dir)s/clients-enabled/*'\n" % {'env_dir' : self.envname}
+
+    ## help
+    def do_help(self, line=None):
+        arg = self._arg_tokenize(line)
+
+        if arg[0]:
+            try:
+                doc = getattr(self, "_help_" + arg[0])
+                TracAdmin.print_doc(TracAdmin(), doc)
+            except AttributeError:
+                print "No documentation found for %s" % arg[0]
+        else:
+            docs = (self._help_client + self._help_project + self._help_initenv)
+            print 'cleverbox-admin - The Cleverbox Administration Console'
+            if not self.interactive:
+                print
+                print "Usage: cleverbox-admin env_dir [command [subcommand] [option ...]]\n"
+                print "Invoking cleverbox-admin without command starts "\
+                      "interactive mode."
+            TracAdmin.print_doc(TracAdmin(), docs)
+
+
+    #
+    # "Client" command set
+    #
+    _help_client = [('client list', 'Lists available clients'),
+                    ('client add <name>', 'Adds a client'),
+                    ('client remove <name>', 'Deletes a client'),
+                    ('client enable <name>', 'Enables a client'),
+                    ('client disable <name>', 'Disables a client')]
+    def do_client(self, line):
+        args = self._arg_tokenize(line)
+        if args[0] == 'list':
+            self._do_client_list()
+        elif args[0] == 'add' and len(args) == 2:
+            self._do_client_add(args[1])
+        elif args[0] == 'remove' and len(args) == 2:
+            self._do_client_remove(args[1])
+        elif args[0] == 'enable' and len(args) == 2:
+            self._do_client_enable(args[1])
+        elif args[0] == 'disable' and len(args) == 2:
+            self._do_client_disable(args[1])
+        else:
+            self.do_help('client')
+
+
+    def complete_client(self, text, line, begidx, endidx):
+        '''
+        Completions for the client command.
+        Each client subcommand has its own completion command, named _complete_client_<subcommand>
+        '''
+
+        comp = ['list', 'add', 'remove', 'enable', 'disable']
+        parts = self._arg_tokenize(line)
+
+        if len(parts) > 1 and parts[1] in comp:
+            complete_func = getattr(self, '_complete_client_' + parts[1])
+            if (float(sys.version[:3]) < 2.4):
+                comp = complete_func.__call__(self)
+            else:
+                comp = complete_func.__call__()
+
+        return self.word_complete( text, comp )
+
+
+    def _do_client_list(self):
+        '''
+        Displays enabled and disabled clients.
+        '''
+
+        print "Enabled :\n" \
+              "%s\n\n" \
+              "Disabled : \n" \
+              "%s\n" % (self._get_clients('enabled'), self._get_clients('disabled'))
+
+    def _do_client_add(self, client_name):
+        """
+        Creates a new client.
+          * Checks if client not already exists.
+          * Collects informations about client
+          * Checks informations are correct
+          * Create clients environment directory layout
+          * Creates apache conf for apache
+          * Inserts client into LDAP
+        """
+
+        if not self._client_exists(client_name):
+
+            # Client name cannot contain strokes
+            if client_name.count('-') != 0:
+                print "** Client's identifier can not contain the '-' character."
+                return
+
+            # Information collection
+            collected_infos = { 'full_name'    : None,
+                                'home_dir'     : None,
+                                'ftp_password' : None }
+
+            collected_infos['home_dir'] = os.path.join(self.getConfig('clients_root', 'general'), client_name)
+
+            # A few checks before effectively creating client
+            # -- homedir
+            parent_dir = os.path.normpath(os.path.join(collected_infos['home_dir'], os.pardir))
+            dir_is_ok = os.access(parent_dir, os.W_OK) and not os.access(collected_infos['home_dir'], os.R_OK)
+            if not dir_is_ok:
+                print "Directory %s is not writable or already exists, aborting." % collected_infos['home_dir']
+                raise os.error
+
+
+            print
+            print "Creating a new client."
+            print "Let's collect some informations about him : "
+            print
+            print "  Please enter client's full name."
+            print "  This will be used in several interface screens and in emails."
+            print
+
+            dfn = client_name.capitalize()
+            collected_infos['full_name'] = raw_input('Full Name [%s]> ' % dfn).strip() or dfn
+
+            print
+            print "Supplied informations verified."
+            print "Let's proceed with client creation :"
+            print
+
+            # Client creation
+            # -- Homedir layout
+            homedir_layout = []
+            for d in _defaults['client_dir_layout']:
+                homedir_layout.append( os.path.join(collected_infos['home_dir'], d) )
+            map( os.makedirs, homedir_layout )
+
+            print "  Directory layout created in %s\n" % collected_infos['home_dir']
+
+            # -- Apache configuration
+            apache_conf = """
+# -- Apache logs
+ErrorLog %(home_dir)s/logs/error_log
+CustomLog %(home_dir)s/logs/access_log common
+
+# -- Include enabled projects configuration files
+Include %(env_dir)s/projects-enabled/%(client_name)s-*
+""" % { 'client_name' : client_name,
+        'home_dir'    : collected_infos['home_dir'],
+        'env_dir'     : self.envname }
+
+            # -- Write conf to filesystem
+            apache_conf_filepath = os.path.join( self.envname, 'clients-available', client_name )
+            f = file(apache_conf_filepath, 'w+')
+            f.write(apache_conf)
+            f.close()
+
+            print "  Apache configuration written to %s\n" % apache_conf_filepath
+
+            # -- Fix permissions
+            """
+            chown -R dev:dev /home/$CLIENTNAME
+            chown dev:www-data /home/$CLIENTNAME/logs
+            chmod g+w /home/$CLIENTNAME/logs
+            chown dev:www-data /home/$CLIENTNAME/uploads
+            chmod g+w /home/$CLIENTNAME/uploads
+            """
+            self._rchown( collected_infos['home_dir'],
+                         int(self.getConfig('ssh_user', 'general')),
+                         int(self.getConfig('ssh_group', 'general')) )
+            os.chown( os.path.join(collected_infos['home_dir'], 'logs'),
+                     int(self.getConfig('ssh_user', 'general')), int(self.getConfig('apache_group', 'general')) )
+            os.chmod( os.path.join(collected_infos['home_dir'], 'logs'), 0775 )
+            os.chown( os.path.join(collected_infos['home_dir'], 'uploads'),
+                     int(self.getConfig('ssh_user', 'general')), int(self.getConfig('apache_group', 'general')) )
+            os.chmod( os.path.join(collected_infos['home_dir'], 'uploads'), 0775 )
+
+            print "  Fixed permissions in %s\n" % collected_infos['home_dir']
+
+            #  Final steps
+            # -- Enable client ?
+            print "Client has been created. Do you want to enable it ?"
+            print
+            enable_client = raw_input('Enable client ? [y/N]> ').strip() or False
+
+            if enable_client == 'y':
+                self._do_client_enable(client_name)
+
+            print
+            print "Client %s was successfully created." % collected_infos['full_name']
+            print "Apache configuration need to be reloaded for this to be effective."
+            print
+            print "You may want create some projects now. Try 'help project' for some documentation."
+            print
+
+        else:
+            print "This client already exists !"
+
+    def _do_client_remove(self, client_name):
+        """
+        Suppression d'un client.
+        Un client actif ne peut être supprimé.
+        TODO : suppression du répertoire du client. (bof)
+        """
+
+        if self._client_exists(client_name):
+            continue_removal = False
+            print "You are about to remove client '%s'." % client_name
+            confirm_removal = raw_input('Remove client ? [y/N]> ').strip() or False
+            if confirm_removal == 'y':
+                continue_removal = True
+
+            if not continue_removal:
+                print "Client '%s' was *not* removed" % client_name
+                return
+
+            # Client is enabled, propose disabling it
+            client_disabled = True
+            if client_name in self._get_clients('enabled'):
+                client_disabled = False
+                print "You can not remove an enabled client. Disabling it."
+                try:
+                    self._do_client_disable(client_name)
+                except Exception, e:
+                    print e
+
+            # Disable client's projects
+            for project_name in self._get_projects(client_name, 'enabled'):
+                self._do_project_disable((client_name, project_name))
+
+            # Disable client
+            client_apacheconf = os.path.join( self.envname, 'clients-available', client_name )
+            os.unlink(client_apacheconf)
+            print "Client '%s' has been removed." % client_name
+            print "Apache configuration needs to be reloaded for this to be effective."
+        else:
+            print "This client does not exist !"
+
+    def _do_client_enable(self, client_name):
+        enable_client = True
+        if not self._client_exists(client_name):
+            print "This client does not exist !"
+            enable_client = False
+
+        if client_name in self._get_clients('enabled'):
+            print "This client is already enabled !"
+            enable_client = False
+
+        if enable_client:
+            target = os.path.join( self.envname, 'clients-available', client_name )
+            linkname = os.path.join( self.envname, 'clients-enabled', client_name )
+            os.symlink( target, linkname )
+            print "Client '%s' has been enabled" % client_name
+            print "Apache configuration needs to be reloaded for this to be effective."
+        else:
+            print "Client '%s' was *not* enabled" % client_name
+
+    def _do_client_disable(self, client_name):
+        """
+        Disables a client.
+        TODO : disable client's projects.
+        """
+        disable_client = True
+        if not self._client_exists(client_name):
+            print "This client does not exist !"
+            disable_client = False
+
+        if client_name in self._get_clients('disabled'):
+            print "This client is already disabled !"
+            disable_client = False
+
+        if disable_client:
+            linkname = os.path.join( self.envname, 'clients-enabled', client_name )
+            os.unlink( linkname )
+            print "Client '%s' has been disabled" % client_name
+            print "Apache configuration needs to be reloaded for this to be effective."
+        else:
+            print "Client '%s' was *not* disabled" % client_name
+
+
+    def _complete_client_remove(self):
+        return self._get_clients()
+
+    def _complete_client_enable(self):
+        return self._get_clients('disabled')
+
+    def _complete_client_disable(self):
+        return self._get_clients('enabled')
+
+
+    def _client_exists(self, name):
+        """
+        Tells whether or not a client exists.
+        return bool
+        """
+        return name in self._get_clients()
+
+    def _get_clients(self, only = False):
+        '''
+        Returns a list of clients.
+        The "only" parameter is used to restrict returned clients list. Recognized values are "enabled" and "disabled".
+        '''
+
+        list_clients = []
+
+        # Each client
+        if not only:
+            list_clients = os.listdir(os.path.join(self.envname, 'clients-available'))
+
+        # Only enabled clients
+        elif only == 'enabled':
+            list_clients = os.listdir(os.path.join(self.envname, 'clients-enabled'))
+
+        # Only disabled clients (ie. not enabled)
+        elif only == 'disabled':
+
+            disabled_clients = []
+            available_clients = os.listdir(os.path.join(self.envname, 'clients-available'))
+            enabled_clients = os.listdir(os.path.join(self.envname, 'clients-enabled'))
+
+            # Expect poor performances for this intersection algorithm
+            # see http://python.project.cwi.nl/search/hypermail/python-recent/0159.html
+            for e in available_clients:
+                if e not in enabled_clients:
+                    disabled_clients.append(e)
+
+            list_clients = disabled_clients
+
+        return list_clients
+
+
+    #
+    # Project command set
+    #
+
+    _help_project = [('project list', 'Lists available projects'),
+                    ('project add <client> <name>', 'Adds a project'),
+                    ('project remove <client> <name>', 'Deletes a project'),
+                    ('project enable <client> <name>', 'Enables a project'),
+                    ('project disable <client> <name>', 'Disables a project')]
+    def do_project(self, line):
+        parts = self._arg_tokenize(line)
+        try:
+            do_func = getattr(self, '_do_project_' + parts[0])
+            # Python __call__() signature changed in Python2.4
+            if ( sys.version[:3] == '2.4'):
+                do_func.__call__(parts[1:])
+            else:
+                do_func.__call__(self, parts[1:])
+        except AttributeError, e:
+            print e
+            self.do_help('project')
+
+    def _do_project_list(self, line=None):
+        """
+        TODO : Nicer formatting
+        """
+
+        print
+        print "Available projects :"
+        print "--------------------"
+
+        client_projects = {}
+
+        clients = self._get_clients()
+        for client_name in clients:
+            client_projects[client_name] = self._get_projects(client_name)
+
+        print client_projects
+
+
+    def _project_exists(self, client_name, project_name, project_status = None):
+        return project_name in self._get_projects(client_name, project_status)
+
+    def _get_projects(self, client_name = None, status = None):
+        """
+        TODO : Extend command options in order to provide better filtering (client, enabled / disabled)
+        """
+        list_projects = []
+
+        # -- List of requested projects
+        enabled_projects_dir = 'projects-enabled'
+        all_projects_dir     = 'projects-available'
+
+        list_enabled_projects = os.listdir(os.path.join(self.envname, enabled_projects_dir))
+        list_all_projects = os.listdir(os.path.join(self.envname, all_projects_dir))
+
+        list_projects = []
+        if (status == None):
+            list_projects = list_all_projects
+        if (status == 'enabled'):
+            list_projects = list_enabled_projects
+        if (status == 'disabled'):
+            for p in list_all_projects:
+                if p not in list_enabled_projects:
+                    list_projects.append(p)
+
+        # Restrict to requested client
+        pattern = re.compile('(\w+)-(.*)')
+        if (client_name):
+            client_projects = []
+            for project_name in list_projects:
+                matches = pattern.findall(project_name)
+                if (client_name == matches[0][0]):
+                    client_projects.append(matches[0][1])
+            list_projects = client_projects
+
+        return list_projects
+
+    def _do_project_disable(self, args):
+        if len(args) == 2:
+            (client_name, project_name) = args
+            if (self._project_exists(client_name, project_name)):
+                try:
+                    os.unlink( os.path.join(self.envname, 'projects-enabled', '%s-%s' % (client_name, project_name)) )
+                    print "Project '%s' has been disabled. Apache needs to be reloaded." % project_name
+                except (IOError, os.error), exception:
+                        print "** Project '%s' is already disabled." % project_name
+
+            else:
+                print "** Client '%s' has no project '%s'" % (client_name, project_name)
+        else:
+            print "** Invalid syntax"
+            self.do_help('project')
+
+    def _do_project_enable(self, args):
+        if len(args) == 2:
+            (client_name, project_name) = args
+            if self._project_exists(client_name, project_name):
+                try:
+                    target = os.path.join( self.envname,
+                                          'projects-available',
+                                          '%s-%s' % (client_name, project_name) )
+                    linkname = os.path.join( self.envname,
+                                          'projects-enabled',
+                                          '%s-%s' % (client_name, project_name) )
+                    os.symlink( target, linkname )
+                    print "Project '%s' has been enabled. Apache needs to be reloaded." % project_name
+                except Exception, e:
+                    print e
+
+            else:
+                print "** Client '%s' has no project '%s'" % (client_name, project_name)
+        else:
+            print "** Invalid syntax"
+            self.do_help('project')
+
+    def _do_project_remove(self, args):
+        print "** TODO : This command has yet to be implemented."
+
+        (client_name, project_name) = args
+
+        # Confirm deletion
+        continue_removal = False
+        print "You are about to remove project '%s'." % project_name
+        confirm_removal = raw_input('Remove project ? [y/N]> ').strip() or False
+        if confirm_removal == 'y':
+            continue_removal = True
+
+        if not continue_removal:
+            print "Project '%s' was *not* removed\n" % project_name
+            return
+
+        # Disable project
+        if self._project_exists(client_name, project_name, 'enabled'):
+            print "You cannot remove an enabled project. Disabling it."
+            self._do_project_disable(args)
+
+        # Delete apache config file
+        try:
+            linkname = os.path.join( self.envname, 'projects-available', '%s-%s'% (client_name, project_name) )
+            os.unlink( linkname )
+            print "Project '%s' has been removed" % project_name
+            print "Apache configuration needs to be reloaded for this to be effective."
+        except (IOError, os.error), exception:
+            print "** An error occured. Project was NOT removed. Report the following message to the sysadmin."
+            print exception
+
+    def _do_project_add(self, args):
+
+        if len(args) == 2:
+            client_name = args[0]
+            project_name = args[1]
+
+            # Make sure client exists
+            if not self._client_exists(client_name):
+                print "Client '%s' does not exist !" % client_name
+                return
+            # Make sure project does not already exists
+            if self._project_exists(client_name, project_name):
+                print "Client '%s' already has a project named '%s'" % (client_name, project_name)
+                return
+
+            # Information collection
+            collected_infos = {
+                'client'     : client_name,
+                'short_name' : project_name,
+                'full_name'  : None
+                }
+
+            print
+            print "Creating a new project for client '%s'" % client_name
+            print "Let's collect some informations about the project :"
+            print
+            print "  Please enter project's full name."
+            print "  This will be displayed in various places and emails."
+            print
+
+            dfn = project_name.capitalize()
+            collected_infos['full_name'] = raw_input('Full Name [%s]> ' % dfn).strip() or dfn
+
+            # Project creation
+            print
+            print "Supplied informations verified."
+            print "Let's proceed with project creation :"
+            print
+
+            try:
+                # -- Apache configuration file
+                self._project_write_apache_conf( collected_infos )
+
+                # -- Create required dirs
+                self._project_create_dirs( collected_infos )
+
+                # -- SVN repository creation
+                self._project_create_svn_repos( collected_infos )
+
+                # -- Trac environment initialisation
+                self._project_trac_initenv( collected_infos )
+
+                # -- Trac initial permissions
+                self._project_trac_setperms( collected_infos )
+
+                # -- Fix perms
+                self._project_fix_perms( collected_infos )
+
+            except Exception, e:
+                print "An error occured :"
+                print e
+                traceback.print_exc()
+
+            # -- Enable client ?
+            print "Project has been created. Do you want to enable it ?"
+            print
+            enable_project = raw_input('Enable project ? [y/N]> ').strip() or False
+
+            if enable_project == 'y':
+                self._do_project_enable((client_name, project_name))
+
+        else:
+            self.do_help('project')
+
+    def _project_create_dirs(self, infos):
+        os.makedirs(os.path.join(self.getConfig('clients_root', 'general'),
+                                 infos['client'],
+                                 'htdocs',
+                                 infos['short_name']), 0775)
+
+        print "  Creating project's directory layout\n"
+
+    def _project_write_apache_conf(self, infos):
+        """
+        TODO : clients may not be stored in 'clients_root', be careful with that.
+        """
+
+        conf_data = """
+#
+# Client  : %(client_name)s
+# Project : %(project_name)s
+#
+#  Users are authenticated against Clever Age's Active Directory LDAP server
+#
+
+# -- Subversion
+<Location /%(client_name)s/%(project_name)s/svn>
+  DAV svn
+  SVNPath %(clients_root)s/%(client_name)s/var/svn/%(project_name)s
+  AuthType Basic
+  AuthName \"%(client_name)s :: %(project_name)s :: SVN\"
+
+  # -- LDAP based authentification
+  AuthLDAPBindDN administrateur@ad-ca-paris
+  AuthLDAPBindPassword %(ldap_password)s
+  AuthLDAPURL ldap://213.215.10.66:389/ou=collaborateurs,dc=ad-ca-paris,dc=clever-age,dc=com?samaccountname?sub?(objectClass=*)
+
+  # -- File based authentification
+  #AuthUserFile %(clients_root)s/%(client_name)s/var/svn/%(project_name)s/.htusers
+
+  require valid-user
+</Location>
+
+# -- Trac
+<Location /%(client_name)s/%(project_name)s/trac>
+  SetHandler mod_python
+  PythonHandler trac.web.modpython_frontend
+  PythonOption TracEnv %(clients_root)s/%(client_name)s/var/trac/%(project_name)s
+  PythonOption TracUriRoot /%(client_name)s/%(project_name)s/trac
+  SetEnv PYTHON_EGG_CACHE %(clients_root)s/%(client_name)s/tmp
+</Location>
+
+# Note : the match against "timeline" is needed to provide authenticated RSS feeds (see http://lab.clever-age.net/ticket/35)
+<LocationMatch /%(client_name)s/%(project_name)s/trac/(login|timeline)>
+  AuthType Basic
+  AuthName \"%(client_name)s :: %(project_name)s :: Trac\"
+
+  # -- LDAP based authentification
+  AuthLDAPBindDN administrateur@ad-ca-paris
+  AuthLDAPBindPassword %(ldap_password)s
+  AuthLDAPURL ldap://213.215.10.66:389/ou=collaborateurs,dc=ad-ca-paris,dc=clever-age,dc=com?samaccountname?sub?(objectClass=*)
+
+  # -- File based authentification
+  # -- Users can be used using :
+  # --  htpasswd2 %(clients_root)s/%(client_name)s/var/trac/%(project_name)s/.htusers <username>
+  #AuthUserFile %(clients_root)s/%(client_name)s/var/trac/%(project_name)s/.htusers
+
+  Require valid-user
+</LocationMatch>
+
+# -- Files under src/ will be available on the web
+AliasMatch /%(client_name)s/%(project_name)s/src(.*) /home/%(client_name)s/htdocs/%(project_name)s$1
+""" % {'client_name'   : infos['client'],
+       'project_name'  : infos['short_name'],
+       'clients_root'  : self.getConfig('clients_root', 'general'),
+       'ldap_password' : self.getConfig('ldap_password', 'general') }
+
+        apache_conf_filepath = os.path.join( self.envname, 'projects-available', '%s-%s' % (infos['client'], infos['short_name']) )
+        f = file(apache_conf_filepath, 'w+')
+        f.write(conf_data)
+        f.close()
+
+        print "  Apache configuration written to %s\n" % apache_conf_filepath
+
+
+    def _project_create_svn_repos(self, infos):
+        repos_path = os.path.join( self.getConfig('clients_root', 'general'), infos['client'], 'var/svn', infos['short_name'] )
+        create_cmd = 'svnadmin create %s' % repos_path
+        (stdin, stdout, stderr) = os.popen3( create_cmd )
+
+        err = stderr.read()
+        if err:
+            raise Exception( err )
+
+        print "  Subversion repository created in %s\n" % repos_path
+
+    def _project_trac_initenv(self, infos):
+
+        trac_env_path = os.path.join( self.getConfig('clients_root', 'general'),
+                                     infos['client'],
+                                     'var/trac',
+                                     infos['short_name'] )
+
+        svn_path = os.path.join( self.getConfig('clients_root', 'general'),
+                                infos['client'],
+                                'var/svn',
+                                infos['short_name'] )
+
+        cmd_data = { 'env_path'       : trac_env_path,
+                     'title'          : '"%s :: %s :: Trac"' % (infos['client'], infos['short_name']),
+                     'db_dsn'         : 'sqlite:db/trac.db',
+                     'svn_path'       : svn_path,
+                     'templates_path' : '/usr/share/trac/templates' }
+
+        trac_cmd = 'trac-admin %(env_path)s initenv %(title)s %(db_dsn)s %(svn_path)s %(templates_path)s' % cmd_data
+
+        (stdin, stdout, stderr) = os.popen3( trac_cmd )
+
+        err = stderr.read()
+        if err:
+            err = stdout.read()
+            raise Exception( err )
+
+        print "  Trac project initialized in %s\n" % trac_env_path
+
+    def _project_trac_setperms(self, infos):
+        trac_env_path = os.path.join( self.getConfig('clients_root', 'general'),
+                                     infos['client'],
+                                     'var/trac',
+                                     infos['short_name'] )
+
+        # Perms for authenticated users
+        default_perms = ( 'BROWSER_VIEW',
+                          'CHANGESET_VIEW',
+                          'FILE_VIEW',
+                          'LOG_VIEW',
+                          'MILESTONE_VIEW',
+                          'REPORT_SQL_VIEW',
+                          'REPORT_VIEW',
+                          'ROADMAP_VIEW',
+                          'SEARCH_VIEW',
+                          'TICKET_CREATE',
+                          'TICKET_MODIFY',
+                          'TICKET_VIEW',
+                          'TIMELINE_VIEW',
+                          'WIKI_CREATE',
+                          'WIKI_MODIFY',
+                          'WIKI_VIEW' )
+
+        trac_perms_cmd = 'trac-admin %(env_path)s permission %(subcommand)s %(subject)s %(perms)s'
+
+        # Remove all anonymous perms
+        os.system( trac_perms_cmd % {'env_path'   : trac_env_path,
+                                     'subcommand' : 'remove',
+                                     'subject'    : 'anonymous',
+                                     'perms'      : ' '.join(default_perms)} )
+
+        # Grant default perms to authenticated users
+        os.system( trac_perms_cmd % {'env_path'   : trac_env_path,
+                                     'subcommand' : 'add',
+                                     'subject'    : 'authenticated',
+                                     'perms'      : ' '.join(default_perms)} )
+
+        # Let user choose who gets admin rights
+        print "  Please enter trac administrator username."
+        print "  This user will be granted full privileges on project's Trac instance."
+        print
+
+        dan = infos['short_name'] + '-admin'
+        admin_login = raw_input('Admin Login [%s]> ' % dan).strip() or dan
+
+        os.system( trac_perms_cmd % {'env_path'   : trac_env_path,
+                                     'subcommand' : 'add',
+                                     'subject'    : admin_login,
+                                     'perms'      : 'TRAC_ADMIN'} )
+        print
+        print "  Trac initial permissions set (admin rights given to '%s')\n" % admin_login
+
+    def _project_fix_perms(self, infos):
+        """
+        chown -R dev:www-data /home/$CLIENTNAME/var/svn/$PROJECTNAME
+        chmod g+w /home/$CLIENTNAME/var/svn/$PROJECTNAME
+        chown -R www-data:dev /home/$CLIENTNAME/var/trac/$PROJECTNAME/db
+        """
+
+        # Trac DB
+        trac_db_path   = os.path.join( self.getConfig('clients_root', 'general'),
+                                      infos['client'],
+                                      'var/trac',
+                                      infos['short_name'],
+                                      'db' )
+        self._rchown( trac_db_path , int(self.getConfig('apache_user', 'general')), int(self.getConfig('ssh_group', 'general')) )
+
+        # Trac attachments
+        trac_attachments_path = os.path.join( self.getConfig('clients_root', 'general'),
+                                             infos['client'],
+                                             'var/trac',
+                                             infos['short_name'],
+                                             'attachments' )
+
+        self._rchown( trac_attachments_path , int(self.getConfig('apache_user', 'general')), int(self.getConfig('ssh_group', 'general')) )
+
+        # Subversion repository
+        svn_repos_path = os.path.join( self.getConfig('clients_root', 'general'),
+                                      infos['client'],
+                                      'var/svn',
+                                      infos['short_name'] )
+
+        self._rchown( svn_repos_path, int(self.getConfig('apache_user', 'general')), int(self.getConfig('ssh_group', 'general')) )
+        os.chmod( svn_repos_path, 0775 )
+
+        print "  Perms fixed\n"
+
+    def complete_project(self, text, line, begidx, endidx):
+        comp = ['list', 'add', 'remove', 'enable', 'disable']
+        parts = self._arg_tokenize(line)
+
+        if len(parts) > 1 and parts[1] in comp:
+            complete_func = getattr(self, '_complete_project_' + parts[1])
+            if (float(sys.version[:3]) < 2.4):
+                comp = complete_func.__call__(self, parts[2:])
+            else:
+                comp = complete_func.__call__(parts[2:])
+
+        return self.word_complete(text, comp)
+
+    def _complete_project_add(self, args):
+        comp = []
+
+        # "add" subcommand's first argument is client's name
+        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
+            comp = self._get_clients()
+
+        return comp
+
+    def _complete_project_enable(self, args):
+        comp = []
+
+        # "enable" subcommand's first argument is client's name
+        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
+            comp = self._get_clients()
+        # "enable" subcommand second argument is project's name
+        else :
+            comp = self._get_projects(args[0], 'disabled')
+
+        return comp
+
+    def _complete_project_disable(self, args):
+        comp = []
+
+        # "disable" subcommand's first argument is client's name
+        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
+            comp = self._get_clients()
+        # "disable" subcommand second argument is project's name
+        else :
+            comp = self._get_projects(args[0], 'enabled')
+
+        return comp
+
+    def _complete_project_remove(self, args):
+        comp = []
+
+        # "remove" subcommand's first argument is client's name
+        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
+            comp = self._get_clients()
+        # "remove" subcommand second argument is project's name
+        else :
+            comp = self._get_projects(args[0])
+
+        return comp
+
+    ## Quit / EOF
+    def do_quit(self, line):
+        print
+        sys.exit()
+
+    do_exit = do_quit # Alias
+    do_EOF = do_quit # Alias
+
+
+
+    #
+    # Helpers
+    #
+
+    def word_complete (self, text, words):
+        """
+        Word completion helper.
+        """
+        return [a for a in words if a.startswith (text)]
+
+    def _rchown(self, directory, uid, gid):
+        """
+        Recursive chown.
+        """
+        try:
+            if (os.path.isdir(directory)):
+                os.chown(directory, uid, gid)
+                direntries = os.listdir(directory)
+                for direntry in direntries:
+                    direntry = os.path.join(directory, direntry)
+                    if (os.path.isdir(direntry)):
+                        os.chown(direntry, uid, gid)
+                        os.chdir(direntry)
+                        self._rchown(direntry, uid, gid)
+                    else:
+                        os.chown(direntry, uid, gid)
+            else:
+                os.chown(directory, uid, gid)
+        except (IOError, os.error), why:
+            print "rchown %s: %s" % (str(directory), str(why))
+
+    def _arg_tokenize (self, argstr):
+        """
+        Command line parameters tokenizer
+        """
+        argstr = util.to_utf8(argstr, sys.stdin.encoding)
+        return shlex.split(argstr) or ['']
+
+def run(args):
+    """Main entry point."""
+    admin = CleverboxAdmin()
+
+    if len(args) > 0:
+        if args[0] in ('-h', '--help', 'help'):
+            return admin.onecmd("help")
+        elif args[0] in ('-v','--version','about'):
+            return admin.onecmd("about")
+        else:
+            admin.env_set(os.path.abspath(args[0]))
+            if len(args) > 1:
+                s_args = ' '.join(["'%s'" % command_parts for command_parts in args[2:]])
+                command = args[1] + ' ' +s_args
+                return admin.onecmd(command)
+            else:
+                while True:
+                    admin.run()
+    else:
+        return admin.onecmd("help")
+
