source: traclegosscript/anyrelease/traclegos/legos.py

Last change on this file was 8058, checked in by ejucovy, 13 years ago

apply slinkp's patch for preventing overwriting of existing projects (#7164)

File size: 16.5 KB
Line 
1#!/usr/bin/env python
2"""
3driver to create trac projects --
4the head of the octopus
5"""
6
7import inspect
8import os
9import sys
10
11from martini.config import ConfigMunger
12from martini.utils import getlist
13from optparse import OptionParser
14from paste.script.templates import var
15from paste.script.templates import Template
16from trac.admin.console import TracAdmin
17from trac.core import TracError
18from trac.env import Environment
19from traclegos.admin import TracLegosAdmin
20from traclegos.db import available_databases
21from traclegos.db import SQLite
22from traclegos.pastescript.command import create_distro_command
23from traclegos.pastescript.string import PasteScriptStringTemplate
24from traclegos.pastescript.var import vars2dict, dict2vars
25from traclegos.project import project_dict
26from traclegos.project import TracProject
27from traclegos.repository import available_repositories
28from traclegos.templates import ProjectTemplates
29from StringIO import StringIO
30
31# TODO: warn about duplicate variables or options (optionally)
32
33class TracLegos(object):
34    """tool for assembling trac projects from building blocks"""
35
36    # global options -- these should apply to any trac instance
37    # XXX maybe these should move to TracProject
38    # (like so many other things)
39    options = [ var('project', 'name of project'),
40                var('directory', 'directory for trac projects'),
41                var('description', 'description of the trac project'),
42                var('url', 'alternate url for project'),
43                var('favicon', 'favicon for the project')
44                ]
45
46    interactive = True
47
48    def __init__(self, directory, master=None, inherit=None, 
49                 site_templates=None, vars=None, options=(),
50                 permissions=None, wiki=None):
51        """
52        * directory: directory for project creation
53        * master: path to master trac instance
54        * inherit: inherited configuration to update
55        * site_templates: global site configuration used for all projects
56        * vars: values of variables for interpolation
57        * options: descriptions and annotations on variables
58        """
59
60        self.directory = directory
61        assert self.directory # must have a directory to work in!
62        self.site_templates = site_templates or []
63        self.vars = { 'url': '', 'favicon': ''}
64        self.vars.update(vars or {})
65        self.master = master
66        self.inherit = inherit or master or None
67        self.options.extend(options)
68        self.permissions = permissions or {}
69        self.wiki = wiki or []
70
71        if not os.path.exists(self.directory):
72            os.mkdir(self.directory)
73
74        self.vars['directory'] = self.directory
75
76    ### utility functions
77
78    @classmethod
79    def arguments(cls):
80        """
81        returns a dictionary arguments to the constructor, __init__,
82        and their defaults or None for required arguments
83        """
84        argspec = inspect.getargspec(cls.__init__)
85        args = dict([(i, None) for i in argspec[0][1:]])
86        args.update(dict(zip(argspec[0][len(argspec[0])- len(argspec[-1]):], 
87                             argspec[-1])))
88        return args
89
90    def project_templates(self, templates):
91        # apply site configuration last (override)
92        return ProjectTemplates(*(templates + self.site_templates))
93   
94    def repository_fields(self, project):
95        """
96        arguments to RepositorySetup.setup that may be interpolated
97        from variables
98        """
99        return { 'SVNSync': {'repository_dir': os.path.join(self.directory, project, 'mirror') },
100                 'NewSVN': {'repository_dir': os.path.join(self.directory, project, 'svn') },
101                 'NoRepository': { 'repository_dir': '' },
102                 }
103
104    def create_project(self, project, templates, vars=None,
105                       database=None, repository=None,
106                       return_missing=False):
107        """
108        * project: path name of project
109        * templates: templates to be applied on project creation
110        * vars: variables for interpolation
111        * database:  type of database to use
112        * repository: type of repository to use
113        """
114       
115        ### set variables
116
117        dirname = os.path.join(self.directory, project)
118       
119        if os.path.isdir(dirname) and os.listdir(dirname):
120            raise ValueError("Project directory %r already exists, "
121                             "cowardly refusing to create anything" % dirname)
122
123        _vars = vars or {}
124        vars = self.vars.copy()
125        vars.update(_vars)
126        vars['project'] = project       
127        permissions = dict([(key, value[:]) 
128                            for key, value in self.permissions.items()])
129        wiki = self.wiki[:]
130
131        ### munge configuration
132
133        # get templates
134
135        # XXX hack to get the specified DB out of pastescript templates
136        if not database:
137            if isinstance(templates, ProjectTemplates):
138                pastescript_templates = templates.pastescript_templates
139            else:
140                pastescript_templates = ProjectTemplates(*(templates + self.site_templates)).pastescript_templates
141            databases = set([ template.database() for template in pastescript_templates
142                              if template.db is not None])
143            if databases:
144                assert len(databases) == 1
145                database = databases.pop()
146        if not database:
147            database = SQLite()
148
149        _templates = []
150        _templates.append(database.config())
151        if repository:
152            _templates.append(repository.config())
153
154        if isinstance(templates, ProjectTemplates):
155            if _templates:
156                templates.append(*_templates)
157        else:
158            templates += _templates
159            templates = self.project_templates(templates)
160
161        # determine the vars/options
162        optdict = templates.vars(self.options)
163        repo_fields = {}
164        if database:
165            vars2dict(optdict, *database.options)
166        if repository:
167            vars2dict(optdict, *repository.options)
168            repo_fields = self.repository_fields(project).get(repository.name, {})
169
170        ### interpolate configuration
171
172        command = create_distro_command(interactive=self.interactive)
173       
174        # check for missing variables
175        missing = templates.missing(vars)
176        missing.update(set(optdict.keys()).difference(vars.keys()))
177        if return_missing:
178            return missing
179        if missing:
180
181            # use default repo fields if they are missing
182            for field in repo_fields:
183                if field in missing:
184                    vars[field] = repo_fields[field]
185                    missing.remove(field)
186
187            # add missing variables to the optdict
188            for missed in missing:
189                if missed not in optdict:
190                    optdict[missed] = var(missed, '')
191
192            if missing:
193                paste_template = Template(project)
194                paste_template._read_vars = dict2vars(optdict) # XXX bad touch
195                paste_template.check_vars(vars, command)
196
197        # run the pre method of the pastescript templates
198        # XXX should this be done here?
199        command.interactive = False
200        for paste_template in templates.pastescript_templates:
201            paste_template.pre(command, dirname, vars)
202
203        ### create the database
204        if database:
205            database.setup(**vars)
206       
207        ### create the trac environment
208        options = templates.options_tuples(vars)
209        options.append(('project', 'name', project)) # XXX needed?
210        if self.inherit:
211            options.append(('inherit', 'file', self.inherit))
212        env = Environment(dirname, create=True, options=options)
213
214        ### repository setup
215        if repository:
216            repository.setup(**vars)
217            try:
218                repos = env.get_repository()
219                repos.sync()
220            except TracError:
221                pass
222
223        ### read the generated configuration
224        _conf_file = os.path.join(dirname, 'conf', 'trac.ini')
225        fp = file(_conf_file)
226        _conf = fp.read()
227        fp.close()
228
229        ### run pastescript templates
230        for paste_template in templates.pastescript_templates:
231            paste_template.write_files(command, dirname, vars)
232            paste_template.post(command, dirname, vars)
233
234            # read permissions
235            for agent, perm in paste_template.permissions.items():
236                permissions.setdefault(agent, []).extend(perm)
237
238            # read wiki directories
239            wiki_dir = paste_template.wiki_dir()
240            if wiki_dir is not None:
241                wiki.append(wiki_dir)
242
243        # write back munged configuration
244        munger = ConfigMunger(_conf, options)
245        fp = file(_conf_file, 'w')
246        munger.write(fp)
247        fp.close()
248
249        # TODO: update the inherited file:
250        # * intertrac
251
252        # trac-admin upgrade the project
253        env = Environment(dirname)
254        if env.needs_upgrade():
255            env.upgrade()
256
257        ### trac-admin operations
258        admin = TracLegosAdmin(dirname)
259
260        # remove the default items
261        admin.delete_all()
262
263        # load wiki pages
264        admin.load_pages() # default wiki pages
265        for page_dir in wiki:
266            admin.load_pages(page_dir)
267
268        # add permissions
269        if permissions:
270            admin.add_permissions(permissions)
271
272        # TODO:  addition of groups, milestones, versions, etc
273        # via trac-admin
274
275
276### site configuration
277
278sections = set(('site-configuration', 'variables', 'permissions'))
279
280def site_configuration(*ini_files):
281    """returns a dictionary of configuration from .ini files"""
282    conf = ConfigMunger(*ini_files).dict()
283    for section in sections:
284        if section not in conf:
285            conf[section] = {}
286    return conf
287
288def traclegos_argspec(*dictionaries):
289    """
290    returns an argspec from a list of dictionaries appropriate to
291    constructing a TracLegos instance;
292    later dictionaries take precedence over earlier dictionaries
293    """
294    argspec = TracLegos.arguments()
295    for key in argspec:
296        args = [ dictionary.get(key) for dictionary in dictionaries ]
297        argspec[key] = reduce(lambda x, y: y or x, args, argspec[key])
298    return argspec
299
300def traclegos_factory(ini_files, configuration, variables):
301    """
302    returns configuration needed to drive a TracLegos constructor
303    * ini_files: site configuration .ini files   
304    * configuration: dictionary of overrides to TracLegos arguments
305    * variables: used for template substitution
306    """
307    # XXX could instead return a constructed TracLegos instance
308    conf = site_configuration(*ini_files)
309    site_wiki = conf['site-configuration'].pop('wiki', None)
310    if site_wiki:
311        configuration['wiki'] = [ site_wiki ] + configuration['wiki']
312       
313    argspec = traclegos_argspec(conf['site-configuration'],
314                                configuration)
315    argspec['vars'] = argspec['vars'] or {}
316    argspec['vars'].update(conf['variables'])
317    argspec['vars'].update(variables)
318
319    # permissions:
320    if conf['permissions']:
321        conf['permissions'] = dict([(key, getlist(value))
322                                    for key, value in conf['permissions'].items()])
323    argspec['permissions'] = conf['permissions'] or None
324       
325    return argspec
326
327### functions for the command line front-end to TracLegos
328
329def get_parser():
330    """return an OptionParser object for TracLegos"""
331
332    parser = OptionParser()
333
334    # command line parser options
335    parser.add_option("-c", "--conf",
336                      dest="conf", action="append", default=[],
337                      help="site configuration files")
338    parser.add_option("-d", "--directory",
339                      dest="directory", default=".",
340                      help="trac projects directory")
341    parser.add_option("-i", "--inherit", dest="inherit", default=None,
342                      help=".ini file to inherit from")
343    parser.add_option("-s", "--repository",  dest="repository",
344                      help="repository type to use")
345    parser.add_option("-t", dest="templates", action="append", default=[],
346                      help="trac.ini templates to be applied in order")
347    parser.add_option("--db", "--database",
348                      dest="database", default=None,
349                      help="database type to use")
350    parser.add_option("-w", "--wiki", dest="wiki",
351                      action="append", default=[],
352                      help="directories containing Trac wiki pages to import")
353
354    # options to yield information
355    parser.add_option("--list-templates", dest="listprojects",
356                      action="store_true", default=False,
357                      help="list available TracProject PasteScript templates")
358    parser.add_option("--list-repositories", dest="listrepositories",
359                      action="store_true", default=False,
360                      help="list repository types available for setup by TracLegos")
361    parser.add_option("--list-databases", dest="listdatabases",
362                      action="store_true", default=False,
363                      help="list available database types available for setup by TracLegos")
364    parser.add_option("--list-variables", dest="printmissing",
365                      action="store_true", default=False,
366                      help="print variable names missing for a given configuration")
367
368    parser.set_usage("%prog [options] project <project2> <...> var1=1 var2=2 ...")
369    parser.set_description("assemble a trac project from components")
370    return parser
371
372def parse(parser, args=None):
373    """return (legos, projects) or None"""
374
375    options, args = parser.parse_args(args)
376
377    # parse command line variables and determine list of projects
378    projects = [] # projects to create
379    vars = {} # defined variables
380    for arg in args:
381        if '=' in arg:
382            variable, value = arg.split('=', 1)
383            vars[variable] = value
384        else:
385            projects.append(arg)
386
387    # list the packaged pastescript TracProject templates
388    if options.listprojects:
389        print 'Available project types:'
390        for name, template in project_dict().items():
391            print '%s: %s' % (name, template.summary)
392        return
393
394    # get the repository
395    repository = None
396    if options.repository: 
397        repository = available_repositories().get(options.repository)
398        if not repository:
399            print 'Error: repository type "%s" not available\n' % options.repository
400            options.listrepositories = True
401   
402    # list the available SCM repository setup agents
403    if options.listrepositories:
404        print 'Available repositories:'
405        for name, repository in available_repositories().items():
406            if name is not 'NoRepository': # no need to print this one
407                print '%s: %s' % (name, repository.description)
408        return
409
410    # get the database
411    if options.database:
412        database = available_databases().get(options.database)
413        if not database:
414            print 'Error: database type "%s" not available\n' % options.database
415            options.listdatabases = True
416    else:
417        database = None
418
419    # list the available database setup agents
420    if options.listdatabases:
421        print 'Available databases:'
422        for name, database in available_databases().items():
423            print '%s: %s' % (name, database.description)
424        return       
425
426    # print help if no projects given
427    if not projects and not options.printmissing: 
428        parser.print_help()
429        return
430
431    # parse and apply site-configuration (including variables)
432    argspec = traclegos_factory(options.conf, options.__dict__, vars)
433    # project creator
434    legos = TracLegos(**argspec)
435
436    return legos, projects, { 'templates': options.templates, 
437                              'repository': repository,
438                              'database': database,
439                              'return_missing': options.printmissing}
440
441def main(args=None):
442    """main command line entry point"""
443
444    # command line parser
445    parser = get_parser()   
446   
447    # get some legos
448    parsed = parse(parser)
449    if parsed == None:
450        return # exit condition
451    legos, projects, arguments = parsed
452
453    # print missing options if told to do so
454    if arguments['return_missing']:
455        missing = legos.create_project('foo', **arguments)
456        print '\n'.join(sorted(list(missing)))
457        return
458
459    # create the projects
460    for project in projects:
461        legos.create_project(project, **arguments)
462
463if __name__ == '__main__':
464    main(sys.argv[1:])
Note: See TracBrowser for help on using the repository browser.