Edgewall Software

source: trunk/trac/env.py

Last change on this file was 10812, checked in by rblank, 8 months ago

0.13dev: Merged [10807:10811] from 0.12-stable.

  • Property svn:eol-style set to native
File size: 37.5 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2011 Edgewall Software
4# Copyright (C) 2003-2007 Jonas Borgström <jonas@edgewall.com>
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at http://trac.edgewall.org/wiki/TracLicense.
10#
11# This software consists of voluntary contributions made by many
12# individuals. For the exact contribution history, see the revision
13# history and logs, available at http://trac.edgewall.org/log/.
14#
15# Author: Jonas Borgström <jonas@edgewall.com>
16
17"""Trac Environment model and related APIs."""
18
19from __future__ import with_statement
20
21import os.path
22import setuptools
23import sys
24from urlparse import urlsplit
25
26from trac import db_default
27from trac.admin import AdminCommandError, IAdminCommandProvider
28from trac.cache import CacheManager
29from trac.config import *
30from trac.core import Component, ComponentManager, implements, Interface, \
31                      ExtensionPoint, TracError
32from trac.db.api import (DatabaseManager, QueryContextManager, 
33                         TransactionContextManager, with_transaction)
34from trac.util import copytree, create_file, get_pkginfo, lazy, makedirs
35from trac.util.concurrency import threading
36from trac.util.text import exception_to_unicode, path_to_unicode, printerr, \
37                           printout
38from trac.util.translation import _, N_
39from trac.versioncontrol import RepositoryManager
40from trac.web.href import Href
41
42__all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment']
43
44
45class ISystemInfoProvider(Interface):
46    """Provider of system information, displayed in the "About Trac"
47    page and in internal error reports.
48    """
49    def get_system_info():
50        """Yield a sequence of `(name, version)` tuples describing the
51        name and version information of external packages used by a
52        component.
53        """
54
55
56class IEnvironmentSetupParticipant(Interface):
57    """Extension point interface for components that need to
58    participate in the creation and upgrading of Trac environments,
59    for example to create additional database tables."""
60
61    def environment_created():
62        """Called when a new Trac environment is created."""
63
64    def environment_needs_upgrade(db):
65        """Called when Trac checks whether the environment needs to be
66        upgraded.
67       
68        Should return `True` if this participant needs an upgrade to
69        be performed, `False` otherwise.
70        """
71
72    def upgrade_environment(db):
73        """Actually perform an environment upgrade.
74       
75        Implementations of this method don't need to commit any
76        database transactions. This is done implicitly for each
77        participant if the upgrade succeeds without an error being
78        raised.
79
80        However, if the `upgrade_environment` consists of small,
81        restartable, steps of upgrade, it can decide to commit on its
82        own after each successful step.
83        """
84
85
86class Environment(Component, ComponentManager):
87    """Trac environment manager.
88
89    Trac stores project information in a Trac environment. It consists
90    of a directory structure containing among other things:
91        * a configuration file,
92        * project-specific templates and plugins,
93        * the wiki and ticket attachments files,
94        * the SQLite database file (stores tickets, wiki pages...)
95          in case the database backend is sqlite
96    """
97
98    implements(ISystemInfoProvider)
99
100    required = True
101   
102    system_info_providers = ExtensionPoint(ISystemInfoProvider)
103    setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)
104
105    components_section = ConfigSection('components',
106        """This section is used to enable or disable components
107        provided by plugins, as well as by Trac itself. The component
108        to enable/disable is specified via the name of the
109        option. Whether its enabled is determined by the option value;
110        setting the value to `enabled` or `on` will enable the
111        component, any other value (typically `disabled` or `off`)
112        will disable the component.
113
114        The option name is either the fully qualified name of the
115        components or the module/package prefix of the component. The
116        former enables/disables a specific component, while the latter
117        enables/disables any component in the specified
118        package/module.
119
120        Consider the following configuration snippet:
121        {{{
122        [components]
123        trac.ticket.report.ReportModule = disabled
124        webadmin.* = enabled
125        }}}
126       
127        The first option tells Trac to disable the
128        [wiki:TracReports report module].
129        The second option instructs Trac to enable all components in
130        the `webadmin` package. Note that the trailing wildcard is
131        required for module/package matching.
132       
133        To view the list of active components, go to the ''Plugins''
134        page on ''About Trac'' (requires `CONFIG_VIEW`
135        [wiki:TracPermissions permissions]).
136       
137        See also: TracPlugins
138        """)
139
140    shared_plugins_dir = PathOption('inherit', 'plugins_dir', '',
141        """Path to the //shared plugins directory//.
142       
143        Plugins in that directory are loaded in addition to those in
144        the directory of the environment `plugins`, with this one
145        taking precedence.
146       
147        (''since 0.11'')""")
148
149    base_url = Option('trac', 'base_url', '',
150        """Reference URL for the Trac deployment.
151       
152        This is the base URL that will be used when producing
153        documents that will be used outside of the web browsing
154        context, like for example when inserting URLs pointing to Trac
155        resources in notification e-mails.""")
156
157    base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
158            False, 
159        """Optionally use `[trac] base_url` for redirects.
160       
161        In some configurations, usually involving running Trac behind
162        a HTTP proxy, Trac can't automatically reconstruct the URL
163        that is used to access it. You may need to use this option to
164        force Trac to use the `base_url` setting also for
165        redirects. This introduces the obvious limitation that this
166        environment will only be usable when accessible from that URL,
167        as redirects are frequently used. ''(since 0.10.5)''""")
168
169    secure_cookies = BoolOption('trac', 'secure_cookies', False,
170        """Restrict cookies to HTTPS connections.
171       
172        When true, set the `secure` flag on all cookies so that they
173        are only sent to the server on HTTPS connections. Use this if
174        your Trac instance is only accessible through HTTPS. (''since
175        0.11.2'')""")
176
177    project_name = Option('project', 'name', 'My Project',
178        """Name of the project.""")
179
180    project_description = Option('project', 'descr', 'My example project',
181        """Short description of the project.""")
182
183    project_url = Option('project', 'url', '',
184        """URL of the main project web site, usually the website in
185        which the `base_url` resides. This is used in notification
186        e-mails.""")
187
188    project_admin = Option('project', 'admin', '',
189        """E-Mail address of the project's administrator.""")
190
191    project_admin_trac_url = Option('project', 'admin_trac_url', '.',
192        """Base URL of a Trac instance where errors in this Trac
193        should be reported.
194       
195        This can be an absolute or relative URL, or '.' to reference
196        this Trac instance. An empty value will disable the reporting
197        buttons.  (''since 0.11.3'')""")
198
199    project_footer = Option('project', 'footer',
200                            N_('Visit the Trac open source project at<br />'
201                               '<a href="http://trac.edgewall.org/">'
202                               'http://trac.edgewall.org/</a>'),
203        """Page footer text (right-aligned).""")
204
205    project_icon = Option('project', 'icon', 'common/trac.ico',
206        """URL of the icon of the project.""")
207
208    log_type = Option('logging', 'log_type', 'none',
209        """Logging facility to use.
210       
211        Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")
212
213    log_file = Option('logging', 'log_file', 'trac.log',
214        """If `log_type` is `file`, this should be a path to the
215        log-file.  Relative paths are resolved relative to the `log`
216        directory of the environment.""")
217
218    log_level = Option('logging', 'log_level', 'DEBUG',
219        """Level of verbosity in log.
220       
221        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
222
223    log_format = Option('logging', 'log_format', None,
224        """Custom logging format.
225
226        If nothing is set, the following will be used:
227       
228        Trac[$(module)s] $(levelname)s: $(message)s
229
230        In addition to regular key names supported by the Python
231        logger library (see
232        http://docs.python.org/library/logging.html), one could use:
233         - $(path)s     the path for the current environment
234         - $(basename)s the last path component of the current environment
235         - $(project)s  the project name
236
237        Note the usage of `$(...)s` instead of `%(...)s` as the latter form
238        would be interpreted by the ConfigParser itself.
239
240        Example:
241        `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s`
242
243        ''(since 0.10.5)''""")
244
245    def __init__(self, path, create=False, options=[]):
246        """Initialize the Trac environment.
247       
248        :param path:   the absolute path to the Trac environment
249        :param create: if `True`, the environment is created and
250                       populated with default data; otherwise, the
251                       environment is expected to already exist.
252        :param options: A list of `(section, name, value)` tuples that
253                        define configuration options
254        """
255        ComponentManager.__init__(self)
256
257        self.path = path
258        self.systeminfo = []
259        self._href = self._abs_href = None
260
261        if create:
262            self.create(options)
263        else:
264            self.verify()
265            self.setup_config()
266
267        if create:
268            for setup_participant in self.setup_participants:
269                setup_participant.environment_created()
270
271    def get_systeminfo(self):
272        """Return a list of `(name, version)` tuples describing the
273        name and version information of external packages used by Trac
274        and plugins.
275        """
276        info = self.systeminfo[:]
277        for provider in self.system_info_providers:
278            info.extend(provider.get_system_info() or [])
279        info.sort(key=lambda (name, version): (name != 'Trac', name.lower()))
280        return info
281
282    # ISystemInfoProvider methods
283
284    def get_system_info(self):
285        from trac import core, __version__ as VERSION
286        yield 'Trac', get_pkginfo(core).get('version', VERSION)
287        yield 'Python', sys.version
288        yield 'setuptools', setuptools.__version__
289        from trac.util.datefmt import pytz
290        if pytz is not None:
291            yield 'pytz', pytz.__version__
292   
293    def component_activated(self, component):
294        """Initialize additional member variables for components.
295       
296        Every component activated through the `Environment` object
297        gets three member variables: `env` (the environment object),
298        `config` (the environment configuration) and `log` (a logger
299        object)."""
300        component.env = self
301        component.config = self.config
302        component.log = self.log
303
304    def _component_name(self, name_or_class):
305        name = name_or_class
306        if not isinstance(name_or_class, basestring):
307            name = name_or_class.__module__ + '.' + name_or_class.__name__
308        return name.lower()
309
310    @property
311    def _component_rules(self):
312        try:
313            return self._rules
314        except AttributeError:
315            self._rules = {}
316            for name, value in self.components_section.options():
317                if name.endswith('.*'):
318                    name = name[:-2]
319                self._rules[name.lower()] = value.lower() in ('enabled', 'on')
320            return self._rules
321       
322    def is_component_enabled(self, cls):
323        """Implemented to only allow activation of components that are
324        not disabled in the configuration.
325       
326        This is called by the `ComponentManager` base class when a
327        component is about to be activated. If this method returns
328        `False`, the component does not get activated. If it returns
329        `None`, the component only gets activated if it is located in
330        the `plugins` directory of the environment.
331        """
332        component_name = self._component_name(cls)
333
334        # Disable the pre-0.11 WebAdmin plugin
335        # Please note that there's no recommendation to uninstall the
336        # plugin because doing so would obviously break the backwards
337        # compatibility that the new integration administration
338        # interface tries to provide for old WebAdmin extensions
339        if component_name.startswith('webadmin.'):
340            self.log.info("The legacy TracWebAdmin plugin has been "
341                          "automatically disabled, and the integrated "
342                          "administration interface will be used "
343                          "instead.")
344            return False
345       
346        rules = self._component_rules
347        cname = component_name
348        while cname:
349            enabled = rules.get(cname)
350            if enabled is not None:
351                return enabled
352            idx = cname.rfind('.')
353            if idx < 0:
354                break
355            cname = cname[:idx]
356
357        # By default, all components in the trac package are enabled
358        return component_name.startswith('trac.') or None
359
360    def enable_component(self, cls):
361        """Enable a component or module."""
362        self._component_rules[self._component_name(cls)] = True
363
364    def verify(self):
365        """Verify that the provided path points to a valid Trac environment
366        directory."""
367        with open(os.path.join(self.path, 'VERSION'), 'r') as fd:
368            assert fd.read(26) == 'Trac Environment Version 1'
369
370    def get_db_cnx(self):
371        """Return a database connection from the connection pool
372
373        :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead
374
375        `db_transaction` for obtaining the `db` database connection
376        which can be used for performing any query
377        (SELECT/INSERT/UPDATE/DELETE)::
378       
379           with env.db_transaction as db:
380               ...
381
382           
383        `db_query` for obtaining a `db` database connection which can
384        be used for performing SELECT queries only::
385
386           with env.db_query as db:
387               ...
388        """
389        return DatabaseManager(self).get_connection()
390
391    @lazy
392    def db_exc(self):
393        """Return an object (typically a module) containing all the
394        backend-specific exception types as attributes, named
395        according to the Python Database API
396        (http://www.python.org/dev/peps/pep-0249/).
397       
398        To catch a database exception, use the following pattern::
399       
400            try:
401                with env.db_transaction as db:
402                    ...
403            except env.db_exc.IntegrityError, e:
404                ...
405        """
406        return DatabaseManager(self).get_exceptions()
407
408    def with_transaction(self, db=None):
409        """Decorator for transaction functions :deprecated:"""
410        return with_transaction(self, db)
411
412    def get_read_db(self):
413        """Return a database connection for read purposes :deprecated:
414
415        See `trac.db.api.get_read_db` for detailed documentation."""
416        return DatabaseManager(self).get_connection(readonly=True)
417
418    @property
419    def db_query(self):
420        """Return a context manager which can be used to obtain a
421        read-only database connection.
422
423        Example::
424
425            with env.db_query as db:
426                cursor = db.cursor()
427                cursor.execute("SELECT ...")
428                for row in cursor.fetchall():
429                    ...
430
431        Note that a connection retrieved this way can be "called"
432        directly in order to execute a query::
433
434            with env.db_query as db:
435                for row in db("SELECT ..."):
436                    ...
437       
438        If you don't need to manipulate the connection itself, this
439        can even be simplified to::
440
441            for row in env.db_query("SELECT ..."):
442                ...
443
444        :warning: after a `with env.db_query as db` block, though the
445          `db` variable is still available, you shouldn't use it as it
446          might have been closed when exiting the context, if this
447          context was the outermost context (`db_query` or
448          `db_transaction`).
449        """
450        return QueryContextManager(self)
451
452    @property
453    def db_transaction(self):
454        """Return a context manager which can be used to obtain a
455        writable database connection.
456       
457        Example::
458
459            with env.db_transaction as db:
460                cursor = db.cursor()
461                cursor.execute("UPDATE ...")
462
463        Upon successful exit of the context, the context manager will
464        commit the transaction. In case of nested contexts, only the
465        outermost context performs a commit. However, should an
466        exception happen, any context manager will perform a rollback.
467
468        Like for its read-only counterpart, you can directly execute a
469        DML query on the `db`::
470
471            with env.db_transaction as db:
472                db("UPDATE ...")
473
474        If you don't need to manipulate the connection itself, this
475        can also be simplified to::
476
477            env.db_transaction("UPDATE ...")
478
479        :warning: after a `with env.db_transaction` as db` block,
480          though the `db` variable is still available, you shouldn't
481          use it as it might have been closed when exiting the
482          context, if this context was the outermost context
483          (`db_query` or `db_transaction`).
484        """
485        return TransactionContextManager(self)
486
487    def shutdown(self, tid=None):
488        """Close the environment."""
489        RepositoryManager(self).shutdown(tid)
490        DatabaseManager(self).shutdown(tid)
491        if tid is None:
492            self.log.removeHandler(self._log_handler)
493            self._log_handler.flush()
494            self._log_handler.close()
495            del self._log_handler
496
497    def get_repository(self, reponame=None, authname=None):
498        """Return the version control repository with the given name,
499        or the default repository if `None`.
500       
501        The standard way of retrieving repositories is to use the
502        methods of `RepositoryManager`. This method is retained here
503        for backward compatibility.
504       
505        :param reponame: the name of the repository
506        :param authname: the user name for authorization (not used
507                         anymore, left here for compatibility with
508                         0.11)
509        """
510        return RepositoryManager(self).get_repository(reponame)
511
512    def create(self, options=[]):
513        """Create the basic directory structure of the environment,
514        initialize the database and populate the configuration file
515        with default values.
516
517        If options contains ('inherit', 'file'), default values will
518        not be loaded; they are expected to be provided by that file
519        or other options.
520        """
521        # Create the directory structure
522        if not os.path.exists(self.path):
523            os.mkdir(self.path)
524        os.mkdir(self.get_log_dir())
525        os.mkdir(self.get_htdocs_dir())
526        os.mkdir(os.path.join(self.path, 'plugins'))
527
528        # Create a few files
529        create_file(os.path.join(self.path, 'VERSION'),
530                    'Trac Environment Version 1\n')
531        create_file(os.path.join(self.path, 'README'),
532                    'This directory contains a Trac environment.\n'
533                    'Visit http://trac.edgewall.org/ for more information.\n')
534
535        # Setup the default configuration
536        os.mkdir(os.path.join(self.path, 'conf'))
537        create_file(os.path.join(self.path, 'conf', 'trac.ini.sample'))
538        config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'))
539        for section, name, value in options:
540            config.set(section, name, value)
541        config.save()
542        self.setup_config()
543        if not any((section, option) == ('inherit', 'file')
544                   for section, option, value in options):
545            self.config.set_defaults(self)
546            self.config.save()
547
548        # Create the database
549        DatabaseManager(self).init_db()
550
551    def get_version(self, db=None, initial=False):
552        """Return the current version of the database.  If the
553        optional argument `initial` is set to `True`, the version of
554        the database used at the time of creation will be returned.
555
556        In practice, for database created before 0.11, this will
557        return `False` which is "older" than any db version number.
558
559        :since: 0.11
560
561        :since 0.13: deprecation warning: the `db` parameter is no
562                     longer used and will be removed in version 0.14
563        """
564        rows = self.db_query("""
565                SELECT value FROM system WHERE name='%sdatabase_version'
566                """ % ('initial_' if initial else ''))
567        return rows and int(rows[0][0])
568
569    def setup_config(self):
570        """Load the configuration file."""
571        self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'),
572                                    {'envname': os.path.basename(self.path)})
573        self.setup_log()
574        from trac.loader import load_components
575        plugins_dir = self.shared_plugins_dir
576        load_components(self, plugins_dir and (plugins_dir,))
577
578    def get_templates_dir(self):
579        """Return absolute path to the templates directory."""
580        return os.path.join(self.path, 'templates')
581
582    def get_htdocs_dir(self):
583        """Return absolute path to the htdocs directory."""
584        return os.path.join(self.path, 'htdocs')
585
586    def get_log_dir(self):
587        """Return absolute path to the log directory."""
588        return os.path.join(self.path, 'log')
589
590    def setup_log(self):
591        """Initialize the logging sub-system."""
592        from trac.log import logger_handler_factory
593        logtype = self.log_type
594        logfile = self.log_file
595        if logtype == 'file' and not os.path.isabs(logfile):
596            logfile = os.path.join(self.get_log_dir(), logfile)
597        format = self.log_format
598        if format:
599            format = format.replace('$(', '%(') \
600                     .replace('%(path)s', self.path) \
601                     .replace('%(basename)s', os.path.basename(self.path)) \
602                     .replace('%(project)s', self.project_name)
603        self.log, self._log_handler = logger_handler_factory(
604            logtype, logfile, self.log_level, self.path, format=format)
605        from trac import core, __version__ as VERSION
606        self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
607                      get_pkginfo(core).get('version', VERSION))
608
609    def get_known_users(self, cnx=None):
610        """Generator that yields information about all known users,
611        i.e. users that have logged in to this Trac environment and
612        possibly set their name and email.
613
614        This function generates one tuple for every user, of the form
615        (username, name, email) ordered alpha-numerically by username.
616
617        :param cnx: the database connection; if ommitted, a new
618                    connection is retrieved
619
620        :since 0.13: deprecation warning: the `cnx` parameter is no
621                     longer used and will be removed in version 0.14
622        """
623        for username, name, email in self.db_query("""
624                SELECT DISTINCT s.sid, n.value, e.value
625                FROM session AS s
626                 LEFT JOIN session_attribute AS n ON (n.sid=s.sid
627                  and n.authenticated=1 AND n.name = 'name')
628                 LEFT JOIN session_attribute AS e ON (e.sid=s.sid
629                  AND e.authenticated=1 AND e.name = 'email')
630                WHERE s.authenticated=1 ORDER BY s.sid
631                """):
632            yield username, name, email
633
634    def backup(self, dest=None):
635        """Create a backup of the database.
636
637        :param dest: Destination file; if not specified, the backup is
638                     stored in a file called db_name.trac_version.bak
639        """
640        return DatabaseManager(self).backup(dest)
641
642    def needs_upgrade(self):
643        """Return whether the environment needs to be upgraded."""
644        with self.db_query as db:
645            for participant in self.setup_participants:
646                if participant.environment_needs_upgrade(db):
647                    self.log.warn("Component %s requires environment upgrade",
648                                  participant)
649                    return True
650            return False
651
652    def upgrade(self, backup=False, backup_dest=None):
653        """Upgrade database.
654       
655        :param backup: whether or not to backup before upgrading
656        :param backup_dest: name of the backup file
657        :return: whether the upgrade was performed
658        """
659        upgraders = []
660        with self.db_query as db:
661            for participant in self.setup_participants:
662                if participant.environment_needs_upgrade(db):
663                    upgraders.append(participant)
664        if not upgraders:
665            return
666
667        if backup:
668            self.backup(backup_dest)
669
670        for participant in upgraders:
671            self.log.info("%s.%s upgrading...", participant.__module__,
672                          participant.__class__.__name__)
673            with self.db_transaction as db:
674                participant.upgrade_environment(db)
675            # Database schema may have changed, so close all connections
676            DatabaseManager(self).shutdown()
677        return True
678
679    @property
680    def href(self):
681        """The application root path"""
682        if not self._href:
683            self._href = Href(urlsplit(self.abs_href.base)[2])
684        return self._href
685
686    @property
687    def abs_href(self):
688        """The application URL"""
689        if not self._abs_href:
690            if not self.base_url:
691                self.log.warn("base_url option not set in configuration, "
692                              "generated links may be incorrect")
693                self._abs_href = Href('')
694            else:
695                self._abs_href = Href(self.base_url)
696        return self._abs_href
697
698
699class EnvironmentSetup(Component):
700    """Manage automatic environment upgrades."""
701   
702    required = True
703
704    implements(IEnvironmentSetupParticipant)
705
706    # IEnvironmentSetupParticipant methods
707
708    def environment_created(self):
709        """Insert default data into the database."""
710        with self.env.db_transaction as db:
711            for table, cols, vals in db_default.get_data(db):
712                db.executemany("INSERT INTO %s (%s) VALUES (%s)"
713                   % (table, ','.join(cols), ','.join(['%s' for c in cols])),
714                   vals)
715        self._update_sample_config()
716
717    def environment_needs_upgrade(self, db):
718        dbver = self.env.get_version(db)
719        if dbver == db_default.db_version:
720            return False
721        elif dbver > db_default.db_version:
722            raise TracError(_('Database newer than Trac version'))
723        self.log.info("Trac database schema version is %d, should be %d",
724                      dbver, db_default.db_version)
725        return True
726
727    def upgrade_environment(self, db):
728        """Each db version should have its own upgrade module, named
729        upgrades/dbN.py, where 'N' is the version number (int).
730        """
731        cursor = db.cursor()
732        dbver = self.env.get_version()
733        for i in range(dbver + 1, db_default.db_version + 1):
734            name  = 'db%i' % i
735            try:
736                upgrades = __import__('upgrades', globals(), locals(), [name])
737                script = getattr(upgrades, name)
738            except AttributeError:
739                raise TracError(_("No upgrade module for version %(num)i "
740                                  "(%(version)s.py)", num=i, version=name))
741            script.do_upgrade(self.env, i, cursor)
742            cursor.execute("""
743                UPDATE system SET value=%s WHERE name='database_version'
744                """, (i,))
745            self.log.info("Upgraded database version from %d to %d", i - 1, i)
746            db.commit()
747        self._update_sample_config()
748
749    # Internal methods
750
751    def _update_sample_config(self):
752        filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
753        if not os.path.isfile(filename):
754            return
755        config = Configuration(filename)
756        for section, default_options in config.defaults().iteritems():
757            for name, value in default_options.iteritems():
758                config.set(section, name, value)
759        try:
760            config.save()
761            self.log.info("Wrote sample configuration file with the new "
762                          "settings and their default values: %s",
763                          filename)
764        except IOError, e:
765            self.log.warn("Couldn't write sample configuration file (%s)", e,
766                          exc_info=True)
767
768
769env_cache = {}
770env_cache_lock = threading.Lock()
771
772def open_environment(env_path=None, use_cache=False):
773    """Open an existing environment object, and verify that the database is up
774    to date.
775
776    :param env_path: absolute path to the environment directory; if
777                     ommitted, the value of the `TRAC_ENV` environment
778                     variable is used
779    :param use_cache: whether the environment should be cached for
780                      subsequent invocations of this function
781    :return: the `Environment` object
782    """
783    if not env_path:
784        env_path = os.getenv('TRAC_ENV')
785    if not env_path:
786        raise TracError(_('Missing environment variable "TRAC_ENV". '
787                          'Trac requires this variable to point to a valid '
788                          'Trac environment.'))
789
790    env_path = os.path.normcase(os.path.normpath(env_path))
791    if use_cache:
792        with env_cache_lock:
793            env = env_cache.get(env_path)
794            if env and env.config.parse_if_needed():
795                # The environment configuration has changed, so shut it down
796                # and remove it from the cache so that it gets reinitialized
797                env.log.info('Reloading environment due to configuration '
798                             'change')
799                env.shutdown()
800                del env_cache[env_path]
801                env = None
802            if env is None:
803                env = env_cache.setdefault(env_path, open_environment(env_path))
804            else:
805                CacheManager(env).reset_metadata()
806    else:
807        env = Environment(env_path)
808        needs_upgrade = False
809        try:
810            needs_upgrade = env.needs_upgrade()
811        except Exception, e: # e.g. no database connection
812            env.log.error("Exception caught while checking for upgrade: %s",
813                          exception_to_unicode(e, traceback=True))
814        if needs_upgrade:
815            raise TracError(_('The Trac Environment needs to be upgraded.\n\n'
816                              'Run "trac-admin %(path)s upgrade"',
817                              path=env_path))
818
819    return env
820
821
822class EnvironmentAdmin(Component):
823    """trac-admin command provider for environment administration."""
824
825    implements(IAdminCommandProvider)
826
827    # IAdminCommandProvider methods
828
829    def get_admin_commands(self):
830        yield ('deploy', '<directory>',
831               'Extract static resources from Trac and all plugins',
832               None, self._do_deploy)
833        yield ('hotcopy', '<backupdir> [--no-database]',
834               """Make a hot backup copy of an environment
835               
836               The database is backed up to the 'db' directory of the
837               destination, unless the --no-database option is
838               specified.
839               """,
840               None, self._do_hotcopy)
841        yield ('upgrade', '',
842               'Upgrade database to current version',
843               None, self._do_upgrade)
844
845    def _do_deploy(self, dest):
846        target = os.path.normpath(dest)
847        chrome_target = os.path.join(target, 'htdocs')
848        script_target = os.path.join(target, 'cgi-bin')
849
850        # Copy static content
851        makedirs(target, overwrite=True)
852        makedirs(chrome_target, overwrite=True)
853        from trac.web.chrome import Chrome
854        printout(_("Copying resources from:"))
855        for provider in Chrome(self.env).template_providers:
856            paths = list(provider.get_htdocs_dirs() or [])
857            if not len(paths):
858                continue
859            printout(%s.%s' % (provider.__module__, 
860                                  provider.__class__.__name__))
861            for key, root in paths:
862                if not root:
863                    continue
864                source = os.path.normpath(root)
865                printout('   ', source)
866                if os.path.exists(source):
867                    dest = os.path.join(chrome_target, key)
868                    copytree(source, dest, overwrite=True)
869
870        # Create and copy scripts
871        makedirs(script_target, overwrite=True)
872        printout(_("Creating scripts."))
873        data = {'env': self.env, 'executable': sys.executable}
874        for script in ('cgi', 'fcgi', 'wsgi'):
875            dest = os.path.join(script_target, 'trac.' + script)
876            template = Chrome(self.env).load_template('deploy_trac.' + script,
877                                                      'text')
878            stream = template.generate(**data)
879            with open(dest, 'w') as out:
880                stream.render('text', out=out, encoding='utf-8')
881
882    def _do_hotcopy(self, dest, no_db=None):
883        if no_db not in (None, '--no-database'):
884            raise AdminCommandError(_("Invalid argument '%(arg)s'", arg=no_db),
885                                    show_usage=True)
886
887        if os.path.exists(dest):
888            raise TracError(_("hotcopy can't overwrite existing '%(dest)s'",
889                              dest=path_to_unicode(dest)))
890        import shutil
891
892        # Bogus statement to lock the database while copying files
893        with self.env.db_transaction as db:
894            db("UPDATE system SET name=NULL WHERE name IS NULL")
895
896            printout(_("Hotcopying %(src)s to %(dst)s ...", 
897                       src=path_to_unicode(self.env.path),
898                       dst=path_to_unicode(dest)))
899            db_str = self.env.config.get('trac', 'database')
900            prefix, db_path = db_str.split(':', 1)
901            skip = []
902
903            if prefix == 'sqlite':
904                db_path = os.path.join(self.env.path, os.path.normpath(db_path))
905                # don't copy the journal (also, this would fail on Windows)
906                skip = [db_path + '-journal', db_path + '-stmtjrnl']
907                if no_db:
908                    skip.append(db_path)
909
910            try:
911                copytree(self.env.path, dest, symlinks=1, skip=skip)
912                retval = 0
913            except shutil.Error, e:
914                retval = 1
915                printerr(_("The following errors happened while copying "
916                           "the environment:"))
917                for (src, dst, err) in e.args[0]:
918                    if src in err:
919                        printerr(%s' % err)
920                    else:
921                        printerr(%s: '%s'" % (err, path_to_unicode(src)))
922
923
924            # db backup for non-sqlite
925            if prefix != 'sqlite' and not no_db:
926                printout(_("Backing up database ..."))
927                sql_backup = os.path.join(dest, 'db',
928                                          '%s-db-backup.sql' % prefix)
929                self.env.backup(sql_backup)
930
931        printout(_("Hotcopy done."))
932        return retval
933
934    def _do_upgrade(self, no_backup=None):
935        if no_backup not in (None, '-b', '--no-backup'):
936            raise AdminCommandError(_("Invalid arguments"), show_usage=True)
937
938        if not self.env.needs_upgrade():
939            printout(_("Database is up to date, no upgrade necessary."))
940            return
941
942        try:
943            self.env.upgrade(backup=no_backup is None)
944        except TracError, e:
945            raise TracError(_("Backup failed: %(msg)s.\nUse '--no-backup' to "
946                              "upgrade without doing a backup.",
947                              msg=unicode(e)))
948
949        # Remove wiki-macros if it is empty and warn if it isn't
950        wiki_macros = os.path.join(self.env.path, 'wiki-macros')
951        try:
952            entries = os.listdir(wiki_macros)
953        except OSError:
954            pass
955        else:
956            if entries:
957                printerr(_("Warning: the wiki-macros directory in the "
958                           "environment is non-empty, but Trac\n"
959                           "doesn't load plugins from there anymore. "
960                           "Please remove it by hand."))
961            else:
962                try:
963                    os.rmdir(wiki_macros)
964                except OSError, e:
965                    printerr(_("Error while removing wiki-macros: %(err)s\n"
966                               "Trac doesn't load plugins from wiki-macros "
967                               "anymore. Please remove it by hand.",
968                               err=exception_to_unicode(e)))
969       
970        printout(_("Upgrade done.\n\n"
971                   "You may want to upgrade the Trac documentation now by "
972                   "running:\n\n  trac-admin %(path)s wiki upgrade",
973                   path=path_to_unicode(self.env.path)))
Note: See TracBrowser for help on using the repository browser.