root/cleverbox/branches/0.4/cleverbox/scripts/admin.py @ 339

Revision 339, 44.1 KB (checked in by trivoallan, 2 years ago)

cleverbox : preparing release of cleverbox-0.4.4

Line 
1# -*- coding: utf-8 -*-
2
3# This file is part of the "Cleverbox" program.
4#
5# Cleverbox is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Cleverbox is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Cleverbox.  If not, see <http://www.gnu.org/licenses/>.
17#
18# Copyright 2008, Tristan Rivoallan
19
20import shutil
21import cmd
22import os
23import shlex
24import sys
25import traceback
26import re
27from trac import util
28from trac.scripts.admin import TracAdmin
29import ConfigParser
30from pkg_resources import parse_version
31
32_defaults = {
33    'env_dir_layout'    : ('clients-available', 'clients-enabled',
34                           'projects-available', 'projects-enabled',
35                           'profiles/default'),
36    'client_dir_layout' : ('htdocs', 'logs', 'tmp', 'uploads',
37                           'var/svn', 'var/trac'),
38    'profile_files'     : ('project.apache.conf', 'trac-defaults.ini', 'permissions.ini')
39}
40
41_version = '0.4.4'
42
43class CleverboxAdmin(cmd.Cmd):
44    intro = ''
45    license = ''
46    doc_header = 'Cleverbox Admin Console \n' \
47                 'Available Commands:\n'
48    ruler = ''
49    prompt = "Cleverbox > "
50    envname = None
51    __env = None
52    _date_format = '%Y-%m-%d'
53    _datetime_format = '%Y-%m-%d %H:%M:%S'
54    _date_format_hint = 'YYYY-MM-DD'
55
56    _config = None
57
58    def __init__(self, envdir = None):
59        cmd.Cmd.__init__(self)
60        if envdir:
61            self.env_set(os.path.abspath(envdir))
62        self.interactive = False
63
64    ##
65    ## Environment methods
66    ##
67
68    def env_set(self, envname, env=None):
69        self.envname = envname
70        self.prompt = "Cleverbox [%s] > " % self.envname
71
72        # Check if environment needs an upgrade
73        #  - open VERSION
74        try:
75            import trac
76            if trac.__version__ == '0.10.3':
77                print
78                print "A bug in trac-0.10.3 prevents the cleverbox from working correctly."
79                print "Please upgrade or downgrade your trac installation."
80                print
81
82                sys.exit(1)
83
84            env_version = open(os.path.join(self.envname, 'VERSION')).read().strip()
85
86            #  - compare with self._version
87            #  - if VERSION < self._version :
88            if parse_version(env_version) < parse_version(_version):
89                print "\nCleverbox environment needs to be upgraded. Please run :"
90                print "  cleverbox-admin %s upgrade\n" % self.envname
91                sys.exit(1)
92        except IOError, e:
93            # no VERSION file means user wants to create an environment
94            pass
95
96        if env is not None:
97            self.__env = env
98
99        # Read configuration
100        self._config = ConfigParser.SafeConfigParser()
101        self._config.read(os.path.join(self.envname, 'cleverbox.ini'))
102
103
104    def env_check(self):
105        try:
106            pass
107            #self.__env = Environment(self.envname)
108        except:
109            return 0
110        return 1
111
112    def env_open(self):
113        try:
114            if not self.__env:
115                #self.__env = Environment(self.envname)
116                return self.__env
117        except Exception, e:
118            print 'Failed to open environment.', e
119            traceback.print_exc()
120            sys.exit(1)
121
122    def emptyline(self):
123        pass
124
125    def onecmd(self, line):
126        try:
127            rv = cmd.Cmd.onecmd(self, line) or 0
128        except SystemExit:
129            raise
130        except Exception, e:
131            print >> sys.stderr, 'Command failed: %s' % e
132            rv = 2
133        if not self.interactive:
134            return rv
135
136    def run(self):
137        self.interactive = True
138        print 'Welcome to cleverbox-admin v%s\n'                \
139              'Interactive Cleverbox administration console.\n'       \
140              "Type:  '?' or 'help' for help on commands.\n" % _version
141        self.cmdloop()
142
143    #
144    # Configuration related methods
145    #
146    def getConfig(self, directive, section):
147        try:
148            val = self._config.get(section, directive)
149        except ConfigParser.NoOptionError, exception:
150            val = ''
151        return val
152
153    #
154    # Commands
155    #
156
157    ## Environment
158    _help_initenv = [('initenv', 'Environment initialisation')]
159    def do_initenv(self, line=None):
160        print "Environment initialisation in %(env_dir)s" % {'env_dir' : self.envname}
161
162        # Collect local configuration info
163        collected_infos = {}
164
165        # Path to client dir
166        default_root = '/var/cleverbox'
167        collected_infos['clients_root'] = raw_input('Projects directory [%s]> ' % default_root).strip() or default_root
168
169        # Path to cleverbox assets
170        default_assets_dir = '/usr/share/cleverbox'
171        collected_infos['assets_dir'] = raw_input('Cleverbox assets directory [%s]> ' % default_assets_dir).strip() or default_assets_dir
172
173        try :
174            os.makedirs(collected_infos['clients_root'], 0775)
175        except IOError, ioexception :
176            print ioexception
177            print "** Projects storage dir could not be created."
178
179        # Apache user & group
180        d_uid = 33
181        d_gid = 33
182        collected_infos['apache_user'] = raw_input('Webserver user id [%d]> ' % d_uid).strip() or d_uid
183        collected_infos['apache_group'] = raw_input('Webserver group id [%d]> ' % d_gid).strip() or d_gid
184
185        # root user & group
186        # we keep the ssh_user notion for backward compatibility.
187        # this will have to disappear in a future release
188        collected_infos['ssh_user'] = 0
189        collected_infos['ssh_group'] = 0
190
191        # Host server domain name
192        collected_infos['domain'] = raw_input('Domain name > ').strip()
193
194        # Authentication backend password (if any)
195        collected_infos['authbackend_pass'] = raw_input('Authentication backend password (if any) []> ').strip() or ''
196
197        # Default configuration profile
198        dcp = 'default'
199        collected_infos['default_profile'] = raw_input('Default configuration profile [%s]> ' % dcp).strip() or dcp
200
201        # Write ini file
202        self._config.add_section('general')
203        for directive, value in collected_infos.items() :
204            self._config.set('general', directive, str(value))
205
206        trac_infos = {}
207        d_lib_dir = '/usr/share/python-support/trac'
208        trac_infos['lib_dir'] = raw_input('Trac libs directory [%s]> ' % d_lib_dir).strip() or d_lib_dir
209
210        d_assets_dir = '/usr/share/trac'
211        trac_infos['assets_dir'] = raw_input('Trac assets directory [%s]> ' % d_assets_dir).strip() or d_assets_dir
212
213        self._config.add_section('trac')
214        for directive, value in trac_infos.items() :
215            self._config.set('trac', directive, str(value))
216
217        fh_config = open(os.path.join(self.envname, 'cleverbox.ini'), 'w+')
218        self._config.write(fh_config)
219
220        # Create directory structure
221        try:
222            print "\n\tCreating directory layout\n"
223            env_dirs = []
224            for dirname in _defaults['env_dir_layout']:
225                env_dirs.append( os.path.join(self.envname, dirname) )
226            map( os.makedirs, env_dirs )
227        except IOError, ioexception :
228            print ioexception
229            print "** Environment couldn't be initialized in %(env_dir)s\n" % {'env_dir' : self.envname}
230
231        # Create VERSION file
232        try:
233            print "\n\tCreating VERSION file\n"
234            fd = open( os.path.join(self.envname, 'VERSION'), 'w' )
235            fd.write(_version)
236        finally:
237            fd.close()
238
239        # Default configuration profile
240        print "\n\tCreating default configuration profile\n"
241        for filename in _defaults['profile_files']:
242            shutil.copy(collected_infos['assets_dir'] + '/' + filename, os.path.join(self.envname, 'profiles', 'default'))
243
244
245        print
246        print "Environment successfully initialized\n"
247        print "You need to add this statement to your apache configuration : \n" \
248            "\t'Include %(env_dir)s/clients-enabled/*'\n" % {'env_dir' : self.envname}
249
250    _help_upgrade = [('upgrade', 'Executes necessary operation to make environment up to date')]
251    def do_upgrade(self, line=None):
252        env_version = open(os.path.join(self.envname, 'VERSION')).read()
253        if parse_version(env_version) < parse_version(_version):
254            from cleverbox.upgrades import upgrades
255            i = 1
256            while i:
257                try:
258                    upgrade_func = getattr(upgrades, 'do_upgrade_' + str(i))
259                    upgrade_func.__call__(self.envname, env_version)
260                except AttributeError, e:
261                    break
262                i = i + 1
263
264
265    ## help
266    def do_help(self, line=None):
267        arg = self._arg_tokenize(line)
268
269        if arg[0]:
270            try:
271                doc = getattr(self, "_help_" + arg[0])
272                TracAdmin.print_doc(doc)
273            except AttributeError:
274                print "No documentation found for %s" % arg[0]
275        else:
276            docs = (self._help_client + self._help_project + self._help_initenv + self._help_upgrade)
277            print 'cleverbox-admin - The Cleverbox Administration Console v%s' % _version
278            if not self.interactive:
279                print
280                print "Usage: cleverbox-admin env_dir [command [subcommand] [option ...]]\n"
281                print "Invoking cleverbox-admin without command starts "\
282                      "interactive mode."
283            TracAdmin.print_doc(docs)
284
285
286    #
287    # "Client" command set
288    #
289    _help_client = [('client list', 'Lists available clients'),
290                    ('client add <name>', 'Adds a client'),
291                    ('client remove <name>', 'Deletes a client'),
292                    ('client enable <name>', 'Enables a client'),
293                    ('client disable <name>', 'Disables a client')]
294    def do_client(self, line):
295        args = self._arg_tokenize(line)
296        if args[0] == 'list':
297            self._do_client_list()
298        elif args[0] == 'add' and len(args) == 2:
299            self._do_client_add(args[1])
300        elif args[0] == 'remove' and len(args) == 2:
301            self._do_client_remove(args[1])
302        elif args[0] == 'enable' and len(args) == 2:
303            self._do_client_enable(args[1])
304        elif args[0] == 'disable' and len(args) == 2:
305            self._do_client_disable(args[1])
306        else:
307            self.do_help('client')
308
309
310    def complete_client(self, text, line, begidx, endidx):
311        '''
312        Completions for the client command.
313        Each client subcommand has its own completion command, named _complete_client_<subcommand>
314        '''
315
316        comp = ['list', 'add', 'remove', 'enable', 'disable']
317        parts = self._arg_tokenize(line)
318
319        if len(parts) > 1 and parts[1] in comp:
320            complete_func = getattr(self, '_complete_client_' + parts[1])
321            if (float(sys.version[:3]) < 2.4):
322                comp = complete_func.__call__(self)
323            else:
324                comp = complete_func.__call__()
325
326        return self.word_complete( text, comp )
327
328
329    def _do_client_list(self):
330        '''
331        Displays enabled and disabled clients.
332        '''
333
334        print "Enabled :\n" \
335              "%s\n\n" \
336              "Disabled : \n" \
337              "%s\n" % (self._get_clients('enabled'), self._get_clients('disabled'))
338
339    def _do_client_add(self, client_name):
340        """
341        Creates a new client.
342          * Checks if client not already exists.
343          * Collects informations about client
344          * Checks informations are correct
345          * Create clients environment directory layout
346          * Creates apache conf for apache
347          * Inserts client into LDAP
348        """
349
350        if not self._client_exists(client_name):
351
352            # Client name cannot contain strokes
353            if client_name.count('-') != 0:
354                print "** Client's identifier can not contain the '-' character."
355                return
356
357            # Information collection
358            collected_infos = { 'full_name'    : None,
359                                'home_dir'     : None,
360                                'ftp_password' : None }
361
362            collected_infos['home_dir'] = os.path.join(self.getConfig('clients_root', 'general'), client_name)
363
364            # A few checks before effectively creating client
365            # -- homedir
366            parent_dir = os.path.normpath(os.path.join(collected_infos['home_dir'], os.pardir))
367            dir_is_ok = os.access(parent_dir, os.W_OK) and not os.access(collected_infos['home_dir'], os.R_OK)
368            if not dir_is_ok:
369                print "Directory %s is not writable or already exists, aborting." % collected_infos['home_dir']
370                raise os.error
371
372
373            print
374            print "Creating a new client."
375            print "Let's collect some informations about him : "
376            print
377            print "  Please enter client's full name."
378            print "  This will be used in several interface screens and in emails."
379            print
380
381            dfn = client_name.capitalize()
382            collected_infos['full_name'] = raw_input('Full Name [%s]> ' % dfn).strip() or dfn
383
384            print
385            print "Supplied informations verified."
386            print "Let's proceed with client creation :"
387            print
388
389            # Client creation
390            # -- Homedir layout
391            homedir_layout = []
392            for d in _defaults['client_dir_layout']:
393                homedir_layout.append( os.path.join(collected_infos['home_dir'], d) )
394            map( os.makedirs, homedir_layout )
395
396            print "  Directory layout created in %s\n" % collected_infos['home_dir']
397
398            # -- Apache configuration
399            apache_conf = """
400# -- Include enabled projects configuration files
401Include %(env_dir)s/projects-enabled/%(client_name)s-*
402""" % { 'client_name' : client_name,
403        'home_dir'    : collected_infos['home_dir'],
404        'env_dir'     : self.envname }
405
406            # -- Write conf to filesystem
407            apache_conf_filepath = os.path.join( self.envname, 'clients-available', client_name )
408            f = file(apache_conf_filepath, 'w+')
409            f.write(apache_conf)
410            f.close()
411
412            print "  Apache configuration written to %s\n" % apache_conf_filepath
413
414            # -- Fix permissions
415            """
416            chown -R dev:dev /$CLIENTSROOT/$CLIENTNAME
417            chown dev:www-data /$CLIENTSROOT/$CLIENTNAME/logs
418            chmod g+w /$CLIENTSROOT/$CLIENTNAME/logs
419            chown dev:www-data /$CLIENTSROOT/$CLIENTNAME/uploads
420            chmod g+w /$CLIENTSROOT/$CLIENTNAME/uploads
421            """
422            self._rchown( collected_infos['home_dir'],
423                         int(self.getConfig('ssh_user', 'general')),
424                         int(self.getConfig('ssh_group', 'general')) )
425            os.chown( os.path.join(collected_infos['home_dir'], 'logs'),
426                     int(self.getConfig('ssh_user', 'general')), int(self.getConfig('apache_group', 'general')) )
427            os.chmod( os.path.join(collected_infos['home_dir'], 'logs'), 0775 )
428            os.chown( os.path.join(collected_infos['home_dir'], 'uploads'),
429                     int(self.getConfig('ssh_user', 'general')), int(self.getConfig('apache_group', 'general')) )
430            os.chmod( os.path.join(collected_infos['home_dir'], 'uploads'), 0775 )
431            os.chown( os.path.join(collected_infos['home_dir'], 'tmp'),
432                     int(self.getConfig('ssh_user', 'general')), int(self.getConfig('apache_group', 'general')) )
433            os.chmod( os.path.join(collected_infos['home_dir'], 'tmp'), 0775 )
434
435            print "  Fixed permissions in %s\n" % collected_infos['home_dir']
436
437            #  Final steps
438            # -- Enable client ?
439            print "Client has been created. Do you want to enable it ?"
440            print
441            enable_client = raw_input('Enable client ? [y/N]> ').strip() or False
442
443            if enable_client == 'y':
444                self._do_client_enable(client_name)
445
446            print
447            print "Client %s was successfully created." % collected_infos['full_name']
448            print "Apache configuration need to be reloaded for this to be effective."
449            print
450            print "You may want create some projects now. Try 'help project' for some documentation."
451            print
452
453        else:
454            print "This client already exists !"
455
456    def _do_client_remove(self, client_name):
457        """
458        Suppression d'un client.
459        Un client actif ne peut être supprimé.
460        TODO : suppression du répertoire du client. (bof)
461        """
462
463        if self._client_exists(client_name):
464            continue_removal = False
465            print "You are about to remove client '%s'." % client_name
466            confirm_removal = raw_input('Remove client ? [y/N]> ').strip() or False
467            if confirm_removal == 'y':
468                continue_removal = True
469
470            if not continue_removal:
471                print "Client '%s' was *not* removed" % client_name
472                return
473
474            # Client is enabled, propose disabling it
475            client_disabled = True
476            if client_name in self._get_clients('enabled'):
477                client_disabled = False
478                print "You can not remove an enabled client. Disabling it."
479                try:
480                    self._do_client_disable(client_name)
481                except Exception, e:
482                    print e
483
484            # Disable client's projects
485            for project_name in self._get_projects(client_name, 'enabled'):
486                self._do_project_disable((client_name, project_name))
487
488            # Disable client
489            client_apacheconf = os.path.join( self.envname, 'clients-available', client_name )
490            os.unlink(client_apacheconf)
491            print "Client '%s' has been removed." % client_name
492            print "Apache configuration needs to be reloaded for this to be effective."
493        else:
494            print "This client does not exist !"
495
496    def _do_client_enable(self, client_name):
497        enable_client = True
498        if not self._client_exists(client_name):
499            print "This client does not exist !"
500            enable_client = False
501
502        if client_name in self._get_clients('enabled'):
503            print "This client is already enabled !"
504            enable_client = False
505
506        if enable_client:
507            target = os.path.join( self.envname, 'clients-available', client_name )
508            linkname = os.path.join( self.envname, 'clients-enabled', client_name )
509            os.symlink( target, linkname )
510            print "Client '%s' has been enabled" % client_name
511            print "Apache configuration needs to be reloaded for this to be effective."
512        else:
513            print "Client '%s' was *not* enabled" % client_name
514
515    def _do_client_disable(self, client_name):
516        """
517        Disables a client.
518        TODO : disable client's projects.
519        """
520        disable_client = True
521        if not self._client_exists(client_name):
522            print "This client does not exist !"
523            disable_client = False
524
525        if client_name in self._get_clients('disabled'):
526            print "This client is already disabled !"
527            disable_client = False
528
529        if disable_client:
530            linkname = os.path.join( self.envname, 'clients-enabled', client_name )
531            os.unlink( linkname )
532            print "Client '%s' has been disabled" % client_name
533            print "Apache configuration needs to be reloaded for this to be effective."
534        else:
535            print "Client '%s' was *not* disabled" % client_name
536
537
538    def _complete_client_remove(self):
539        return self._get_clients()
540
541    def _complete_client_enable(self):
542        return self._get_clients('disabled')
543
544    def _complete_client_disable(self):
545        return self._get_clients('enabled')
546
547
548    def _client_exists(self, name):
549        """
550        Tells whether or not a client exists.
551        return bool
552        """
553        return name in self._get_clients()
554
555    def _get_clients(self, only = False):
556        '''
557        Returns a list of clients.
558        The "only" parameter is used to restrict returned clients list. Recognized values are "enabled" and "disabled".
559        '''
560
561        list_clients = []
562
563        # Each client
564        if not only:
565            list_clients = os.listdir(os.path.join(self.envname, 'clients-available'))
566
567        # Only enabled clients
568        elif only == 'enabled':
569            list_clients = os.listdir(os.path.join(self.envname, 'clients-enabled'))
570
571        # Only disabled clients (ie. not enabled)
572        elif only == 'disabled':
573
574            disabled_clients = []
575            available_clients = os.listdir(os.path.join(self.envname, 'clients-available'))
576            enabled_clients = os.listdir(os.path.join(self.envname, 'clients-enabled'))
577
578            # Expect poor performances for this intersection algorithm
579            # see http://python.project.cwi.nl/search/hypermail/python-recent/0159.html
580            for e in available_clients:
581                if e not in enabled_clients:
582                    disabled_clients.append(e)
583
584            list_clients = disabled_clients
585
586        return list_clients
587
588
589    #
590    # Project command set
591    #
592
593    _help_project = [('project list', 'Lists available projects'),
594                    ('project add <client> <name>', 'Adds a project'),
595                    ('project remove <client> <name>', 'Deletes a project'),
596                    ('project enable <client> <name>', 'Enables a project'),
597                    ('project disable <client> <name>', 'Disables a project')]
598    def do_project(self, line):
599        parts = self._arg_tokenize(line)
600        try:
601            do_func = getattr(self, '_do_project_' + parts[0])
602            # Python __call__() signature changed in Python2.4
603            if ( sys.version[:3] == '2.4'):
604                do_func.__call__(parts[1:])
605            else:
606                do_func.__call__(self, parts[1:])
607        except AttributeError, e:
608            print e
609            self.do_help('project')
610
611    def _do_project_list(self, line=None):
612        """
613        TODO : Nicer formatting
614        """
615
616        print
617        print "Available projects :"
618        print "--------------------"
619
620        client_projects = {}
621
622        clients = self._get_clients()
623        for client_name in clients:
624            client_projects[client_name] = self._get_projects(client_name)
625
626        print client_projects
627
628
629    def _project_exists(self, client_name, project_name, project_status = None):
630        return project_name in self._get_projects(client_name, project_status)
631
632    def _get_projects(self, client_name = None, status = None):
633        """
634        TODO : Extend command options in order to provide better filtering (client, enabled / disabled)
635        """
636        list_projects = []
637
638        # -- List of requested projects
639        enabled_projects_dir = 'projects-enabled'
640        all_projects_dir     = 'projects-available'
641
642        list_enabled_projects = os.listdir(os.path.join(self.envname, enabled_projects_dir))
643        list_all_projects = os.listdir(os.path.join(self.envname, all_projects_dir))
644
645        list_projects = []
646        if (status == None):
647            list_projects = list_all_projects
648        if (status == 'enabled'):
649            list_projects = list_enabled_projects
650        if (status == 'disabled'):
651            for p in list_all_projects:
652                if p not in list_enabled_projects:
653                    list_projects.append(p)
654
655        # Restrict to requested client
656        pattern = re.compile('(\w+)-(.*)')
657        if (client_name):
658            client_projects = []
659            for project_name in list_projects:
660                matches = pattern.findall(project_name)
661                if (client_name == matches[0][0]):
662                    client_projects.append(matches[0][1])
663            list_projects = client_projects
664
665        return list_projects
666
667    def _do_project_disable(self, args):
668        if len(args) == 2:
669            (client_name, project_name) = args
670            if (self._project_exists(client_name, project_name)):
671                try:
672                    os.unlink( os.path.join(self.envname, 'projects-enabled', '%s-%s' % (client_name, project_name)) )
673                    print "Project '%s' has been disabled. Apache needs to be reloaded." % project_name
674                except (IOError, os.error), exception:
675                        print "** Project '%s' is already disabled." % project_name
676
677            else:
678                print "** Client '%s' has no project '%s'" % (client_name, project_name)
679        else:
680            print "** Invalid syntax"
681            self.do_help('project')
682
683    def _do_project_enable(self, args):
684        if len(args) == 2:
685            (client_name, project_name) = args
686            if self._project_exists(client_name, project_name):
687                try:
688                    target = os.path.join( self.envname,
689                                          'projects-available',
690                                          '%s-%s' % (client_name, project_name) )
691                    linkname = os.path.join( self.envname,
692                                          'projects-enabled',
693                                          '%s-%s' % (client_name, project_name) )
694                    os.symlink( target, linkname )
695                    print "Project '%s' has been enabled. Apache needs to be reloaded." % project_name
696                except Exception, e:
697                    print e
698
699            else:
700                print "** Client '%s' has no project '%s'" % (client_name, project_name)
701        else:
702            print "** Invalid syntax"
703            self.do_help('project')
704
705    def _do_project_remove(self, args):
706        print "** TODO : This command has yet to be implemented."
707
708        (client_name, project_name) = args
709
710        # Confirm deletion
711        continue_removal = False
712        print "You are about to remove project '%s'." % project_name
713        confirm_removal = raw_input('Remove project ? [y/N]> ').strip() or False
714        if confirm_removal == 'y':
715            continue_removal = True
716
717        if not continue_removal:
718            print "Project '%s' was *not* removed\n" % project_name
719            return
720
721        # Disable project
722        if self._project_exists(client_name, project_name, 'enabled'):
723            print "You cannot remove an enabled project. Disabling it."
724            self._do_project_disable(args)
725
726        # Delete apache config file
727        try:
728            linkname = os.path.join( self.envname, 'projects-available', '%s-%s'% (client_name, project_name) )
729            os.unlink( linkname )
730            print "Project '%s' has been removed" % project_name
731            print "Apache configuration needs to be reloaded for this to be effective."
732        except (IOError, os.error), exception:
733            print "** An error occured. Project was NOT removed. Report the following message to the sysadmin."
734            print exception
735
736    def _do_project_add(self, args):
737        if len(args) == 2:
738            client_name = args[0]
739            project_name = args[1]
740
741            # Make sure client exists
742            if not self._client_exists(client_name):
743                print "Client '%s' does not exist !" % client_name
744                return
745            # Make sure project does not already exists
746            if self._project_exists(client_name, project_name):
747                print "Client '%s' already has a project named '%s'" % (client_name, project_name)
748                return
749
750            # Information collection
751            collected_infos = {
752                'client'     : client_name,
753                'short_name' : project_name,
754                'full_name'  : None
755                }
756
757            print
758            print "Creating a new project for client '%s'" % client_name
759            print "Let's collect some informations about the project :"
760            print
761            print "  Please enter project's full name."
762            print "  This will be displayed in various places and emails."
763            print
764
765            dfn = project_name.capitalize()
766            collected_infos['full_name'] = raw_input('Full Name [%s]> ' % dfn).strip() or dfn
767
768            print
769            print "  What configuration profile should be used ?"
770            print "  Those reside in %s/profiles/" % self.envname
771            print
772
773            dp = self.getConfig('default_profile', 'general')
774            collected_infos['profile'] = raw_input('Configuration profile [%s]> ' % dp).strip() or dp
775
776            # Project creation
777            print
778            print "Supplied informations verified."
779            print "Let's proceed with project creation :"
780            print
781
782            try:
783                # -- Apache configuration file
784                self._project_write_apache_conf( collected_infos )
785
786                # -- Create required dirs
787                self._project_create_dirs( collected_infos )
788
789                # -- SVN repository creation
790                self._project_create_svn_repos( collected_infos )
791
792                # -- Trac environment initialisation
793                self._project_trac_initenv( collected_infos )
794
795                # -- Revoke all permissions in trac
796                self._project_trac_revoke_all_perms( collected_infos )
797
798                # -- Trac initial permissions
799                self._project_trac_setperms( collected_infos )
800
801                # -- Modifies Trac default conf
802                self._project_trac_defaultconf( collected_infos )
803
804                # -- Fix perms
805                self._project_fix_perms( collected_infos )
806
807            except Exception, e:
808                print "An error occured :"
809                print e
810                traceback.print_exc()
811
812            # -- Enable client ?
813            print "Project has been created. Do you want to enable it ?"
814            print
815            enable_project = raw_input('Enable project ? [y/N]> ').strip() or False
816
817            if enable_project.find('y') == 0:
818                self._do_project_enable((client_name, project_name))
819
820        else:
821            self.do_help('project')
822
823    def _project_create_dirs(self, infos):
824        os.makedirs(os.path.join(self.getConfig('clients_root', 'general'),
825                                 infos['client'],
826                                 'htdocs',
827                                 infos['short_name']), 0775)
828
829        print "  Creating project's directory layout\n"
830
831    def _project_write_apache_conf(self, infos):
832        """
833        TODO : clients may not be stored in 'clients_root', be careful with that.
834        """
835        conf_template = open(os.path.join(self.envname, 'profiles', infos['profile'], 'project.apache.conf'))
836        conf_data = conf_template.read() % {'client_name'      : infos['client'],
837                                            'project_name'     : infos['short_name'],
838                                            'clients_root'     : self.getConfig('clients_root', 'general'),
839                                            'authbackend_pass' : self.getConfig('authbackend_pass', 'general'),
840                                            'trac_install_dir' : self.getConfig('lib_dir', 'trac'),
841                                            'domain_name'      : self.getConfig('domain', 'general')}
842
843        apache_conf_filepath = os.path.join( self.envname, 'projects-available', '%s-%s' % (infos['client'], infos['short_name']) )
844        f = file(apache_conf_filepath, 'w+')
845        f.write(conf_data)
846        f.close()
847
848        print "  Apache configuration written to %s\n" % apache_conf_filepath
849
850    def _project_create_svn_repos(self, infos):
851        repos_path = os.path.join( self.getConfig('clients_root', 'general'), infos['client'], 'var/svn', infos['short_name'] )
852        create_cmd = 'svnadmin create %s' % repos_path
853        (stdin, stdout, stderr) = os.popen3( create_cmd )
854
855        err = stderr.read()
856        if err:
857            raise Exception( err )
858
859        print "  Subversion repository created in %s\n" % repos_path
860
861    def _project_trac_initenv(self, infos):
862
863        trac_env_path = os.path.join( self.getConfig('clients_root', 'general'),
864                                     infos['client'],
865                                     'var/trac',
866                                     infos['short_name'] )
867
868        svn_path = os.path.join( self.getConfig('clients_root', 'general'),
869                                infos['client'],
870                                'var/svn',
871                                infos['short_name'] )
872
873        cmd_data = { 'env_path'         : trac_env_path,
874                     'title'            : '"%s - %s - Trac"' % (infos['client'], infos['short_name']),
875                     'db_dsn'           : 'sqlite:db/trac.db',
876                     'svn_path'         : svn_path,
877                     'templates_path'   : '%s/templates' % self.getConfig('assets_dir', 'trac'),
878                     'trac_install_dir' : self.getConfig('lib_dir', 'trac')}
879
880        trac_cmd = 'trac-admin %(env_path)s initenv %(title)s %(db_dsn)s svn %(svn_path)s %(templates_path)s' % cmd_data
881
882        (stdin, stdout, stderr) = os.popen3( trac_cmd )
883
884        err = stderr.read()
885        if err:
886            err = stdout.read()
887            raise Exception( err )
888
889        print "  Trac project initialized in %s\n" % trac_env_path
890
891    def _project_trac_setperms(self, infos):
892        trac_env_path = os.path.join( self.getConfig('clients_root', 'general'),
893                                     infos['client'],
894                                     'var/trac',
895                                     infos['short_name'] )
896
897        trac_perms_cmd = 'trac-admin %(env_path)s permission %(subcommand)s %(subject)s %(perms)s'
898
899        # Grant default permissions
900        perms_config = ConfigParser.SafeConfigParser()
901        perms_config.read(os.path.join(self.envname, 'profiles', infos['profile'], 'permissions.ini'))
902        for profile in perms_config.options('trac'):
903            os.system( trac_perms_cmd % {'env_path'   : trac_env_path,
904                                         'subcommand' : 'add',
905                                         'subject'    : profile,
906                                         'perms'      : perms_config.get('trac', profile)} )
907
908        # Let user choose who gets admin rights
909        print "  Please enter trac administrator username."
910        print "  This user will be granted full privileges on project's Trac instance."
911        print
912
913        dan = infos['short_name'] + '-admin'
914        admin_login = raw_input('Admin Login [%s]> ' % dan).strip() or dan
915
916        os.system( trac_perms_cmd % {'env_path'   : trac_env_path,
917                                     'subcommand' : 'add',
918                                     'subject'    : admin_login,
919                                     'perms'      : 'TRAC_ADMIN'} )
920        print
921        print "  Trac initial permissions set (admin rights given to '%s')\n" % admin_login
922
923    def _project_trac_revoke_all_perms(self, infos):
924        import trac.env
925
926        trac_env_path = os.path.join( self.getConfig('clients_root', 'general'),
927                                     infos['client'],
928                                     'var/trac',
929                                     infos['short_name'] )
930
931        # Revoke all permissions
932        trac_env = trac.env.open_environment(trac_env_path)
933        trac_db = trac_env.get_db_cnx()
934        db_cursor = trac_db.cursor()
935        db_cursor.execute('DELETE FROM permission')
936        trac_db.commit()
937
938        print "  Revoked Trac default permissions\n"
939
940    def _project_fix_perms(self, infos):
941        """
942        chown -R dev:www-data /$CLIENTSROOT/$CLIENTNAME/var/svn/$PROJECTNAME
943        chmod g+w /$CLIENTSROOT/$CLIENTNAME/var/svn/$PROJECTNAME
944        chown -R www-data:dev /$CLIENTSROOT/$CLIENTNAME/var/trac/$PROJECTNAME/db
945            chown dev:www-data /$CLIENTSROOT/$CLIENTNAME/var/trac/conf/trac.ini
946            chmod g+w /$CLIENTSROOT/$CLIENTNAME/var/trac/conf/trac.ini
947        """
948
949        # Trac DB
950        trac_db_path   = os.path.join( self.getConfig('clients_root', 'general'),
951                                      infos['client'],
952                                      'var/trac',
953                                      infos['short_name'],
954                                      'db' )
955        self._rchown( trac_db_path , int(self.getConfig('apache_user', 'general')), int(self.getConfig('ssh_group', 'general')) )
956
957        # Trac attachments
958        trac_attachments_path = os.path.join( self.getConfig('clients_root', 'general'),
959                                             infos['client'],
960                                             'var/trac',
961                                             infos['short_name'],
962                                             'attachments' )
963
964        self._rchown( trac_attachments_path , int(self.getConfig('apache_user', 'general')), int(self.getConfig('ssh_group', 'general')) )
965
966        # Trac conf
967        trac_ini_path   = os.path.join( self.getConfig('clients_root', 'general'),
968                                       infos['client'],
969                                       'var','trac',
970                                       infos['short_name'],
971                                       'conf','trac.ini' )
972        os.chmod( trac_ini_path, 0777 )
973
974        # Subversion repository
975        svn_repos_path = os.path.join( self.getConfig('clients_root', 'general'),
976                                      infos['client'],
977                                      'var/svn',
978                                      infos['short_name'] )
979
980        self._rchown( svn_repos_path, int(self.getConfig('apache_user', 'general')), int(self.getConfig('ssh_group', 'general')) )
981        os.chmod( svn_repos_path, 0775 )
982
983        # Htdocs directory
984        htdocs_path = os.path.join( self.getConfig('clients_root', 'general'),
985                                    infos['client'],
986                                    'htdocs',
987                                    infos['short_name'] )
988
989        self._rchown( htdocs_path, int(self.getConfig('apache_user', 'general')), int(self.getConfig('ssh_group', 'general')) )
990        os.chmod( htdocs_path, 0775 )
991
992        print "  Perms fixed\n"
993
994    def _project_trac_defaultconf(self, infos):
995        """
996        Overrides project's default trac.ini with values provided in configuration profile.
997        """
998
999        # New defaults
1000        tracdefaults_config = ConfigParser.SafeConfigParser()
1001        tracdefaults_config.read(os.path.join(self.envname, 'profiles', infos['profile'], 'trac-defaults.ini'))
1002
1003        # Trac base config file
1004        project_config_path = os.path.join(self.getConfig('clients_root', 'general'), infos['client'], 'var', 'trac', infos['short_name'], 'conf', 'trac.ini')
1005        tracproject_config = ConfigParser.SafeConfigParser()
1006        tracproject_config.read(project_config_path)
1007
1008        # Overriding
1009        for section in tracdefaults_config.sections():
1010            if not tracproject_config.has_section(section):
1011                tracproject_config.add_section(section)
1012            for option in tracdefaults_config.options(section):
1013                # Perform variable substituion in each default option
1014                # It is probably not very good in term of performances, but we'll wait until someone complains to fix it
1015                default_option = tracdefaults_config.get(section, option) % {'client_name'      : infos['client'],
1016                                                                             'project_name'     : infos['short_name'],
1017                                                                             'clients_root'     : self.getConfig('clients_root', 'general'),
1018                                                                             'authbackend_pass' : self.getConfig('authbackend_pass', 'general'),
1019                                                                             'trac_install_dir' : self.getConfig('lib_dir', 'trac'),
1020                                                                             'domain_name'      : self.getConfig('domain', 'general')}
1021                tracproject_config.set(section, option, default_option)
1022
1023        fp = open(project_config_path, 'w+')
1024        tracproject_config.write(fp)
1025
1026    def complete_project(self, text, line, begidx, endidx):
1027        comp = ['list', 'add', 'remove', 'enable', 'disable']
1028        parts = self._arg_tokenize(line)
1029
1030        if len(parts) > 1 and parts[1] in comp:
1031            complete_func = getattr(self, '_complete_project_' + parts[1])
1032            if (float(sys.version[:3]) < 2.4):
1033                comp = complete_func.__call__(self, parts[2:])
1034            else:
1035                comp = complete_func.__call__(parts[2:])
1036
1037        return self.word_complete(text, comp)
1038
1039    def _complete_project_add(self, args):
1040        comp = []
1041
1042        # "add" subcommand's first argument is client's name
1043        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
1044            comp = self._get_clients()
1045
1046        return comp
1047
1048    def _complete_project_enable(self, args):
1049        comp = []
1050
1051        # "enable" subcommand's first argument is client's name
1052        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
1053            comp = self._get_clients()
1054        # "enable" subcommand second argument is project's name
1055        else :
1056            comp = self._get_projects(args[0], 'disabled')
1057
1058        return comp
1059
1060    def _complete_project_disable(self, args):
1061        comp = []
1062
1063        # "disable" subcommand's first argument is client's name
1064        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
1065            comp = self._get_clients()
1066        # "disable" subcommand second argument is project's name
1067        else :
1068            comp = self._get_projects(args[0], 'enabled')
1069
1070        return comp
1071
1072    def _complete_project_remove(self, args):
1073        comp = []
1074
1075        # "remove" subcommand's first argument is client's name
1076        if not args or (len(args) <= 1 and args[0] not in self._get_clients()):
1077            comp = self._get_clients()
1078        # "remove" subcommand second argument is project's name
1079        else :
1080            comp = self._get_projects(args[0])
1081
1082        return comp
1083
1084    ## Quit / EOF
1085    def do_quit(self, line):
1086        print
1087        sys.exit()
1088
1089    do_exit = do_quit # Alias
1090    do_EOF = do_quit # Alias
1091
1092
1093
1094    #
1095    # Helpers
1096    #
1097
1098    def word_complete (self, text, words):
1099        """
1100        Word completion helper.
1101        """
1102        return [a for a in words if a.startswith (text)]
1103
1104    def _rchown(self, directory, uid, gid):
1105        """
1106        Recursive chown.
1107        """
1108        try:
1109            if (os.path.isdir(directory)):
1110                os.chown(directory, uid, gid)
1111                direntries = os.listdir(directory)
1112                for direntry in direntries:
1113                    direntry = os.path.join(directory, direntry)
1114                    if (os.path.isdir(direntry)):
1115                        os.chown(direntry, uid, gid)
1116                        os.chdir(direntry)
1117                        self._rchown(direntry, uid, gid)
1118                    else:
1119                        os.chown(direntry, uid, gid)
1120            else:
1121                os.chown(directory, uid, gid)
1122        except (IOError, os.error), why:
1123            print "rchown %s: %s" % (str(directory), str(why))
1124
1125    def _arg_tokenize (self, argstr):
1126        """
1127        Command line parameters tokenizer
1128        """
1129        argstr = util.to_utf8(argstr, sys.stdin.encoding)
1130        return shlex.split(argstr) or ['']
1131
1132def run(args):
1133    """Main entry point."""
1134    admin = CleverboxAdmin()
1135
1136    if len(args) > 0:
1137        if len(args) > 1:
1138            if args[1] in ('-h', '--help', 'help'):
1139                return admin.onecmd("help")
1140            elif args[1] in ('-v','--version','about'):
1141                return admin.onecmd("about")
1142            elif args[1] in ('-u', '--upgrade', 'upgrade'):
1143                admin.envname = args[0]
1144                return admin.onecmd("upgrade")
1145        else:
1146            admin.env_set(os.path.abspath(args[0]))
1147            if len(args) > 1:
1148                s_args = ' '.join(["'%s'" % command_parts for command_parts in args[2:]])
1149                command = args[1] + ' ' +s_args
1150                return admin.onecmd(command)
1151            else:
1152                while True:
1153                    admin.run()
1154    else:
1155        return admin.onecmd("help")
1156
Note: See TracBrowser for help on using the browser.