Software Freedom Law Center

root/trunk/trac/trac/env.py

Revision 103, 22.3 kB (checked in by bkuhn, 8 months ago)

r129@hughes: bkuhn | 2008-05-01 21:46:34 -0400

  • Merged upstream trac via: svk smerge /loblaw/local/branches/trac.upstream-r6969 .
Line 
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2003-2008 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 import os
18 try:
19     import threading
20 except ImportError:
21     import dummy_threading as threading
22 import setuptools
23 import sys
24 from urlparse import urlsplit
25
26 from trac import db_default
27 from trac.config import *
28 from trac.core import Component, ComponentManager, implements, Interface, \
29                       ExtensionPoint, TracError
30 from trac.db import DatabaseManager
31 from trac.util import get_pkginfo
32 from trac.util.translation import _
33 from trac.versioncontrol import RepositoryManager
34 from trac.web.href import Href
35
36 __all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment']
37
38
39 class IEnvironmentSetupParticipant(Interface):
40     """Extension point interface for components that need to participate in the
41     creation and upgrading of Trac environments, for example to create
42     additional database tables."""
43
44     def environment_created():
45         """Called when a new Trac environment is created."""
46
47     def environment_needs_upgrade(db):
48         """Called when Trac checks whether the environment needs to be upgraded.
49        
50         Should return `True` if this participant needs an upgrade to be
51         performed, `False` otherwise.
52         """
53
54     def upgrade_environment(db):
55         """Actually perform an environment upgrade.
56        
57         Implementations of this method should not commit any database
58         transactions. This is done implicitly after all participants have
59         performed the upgrades they need without an error being raised.
60         """
61
62
63 class Environment(Component, ComponentManager):
64     """Trac stores project information in a Trac environment.
65
66     A Trac environment consists of a directory structure containing among other
67     things:
68      * a configuration file.
69      * an SQLite database (stores tickets, wiki pages...)
70      * Project specific templates and plugins.
71      * wiki and ticket attachments.
72     """   
73     setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)
74
75     shared_plugins_dir = PathOption('inherit', 'plugins_dir', '',
76         """Path of the directory containing additional plugins.
77        
78         Plugins in that directory are loaded in addition to those in the
79         directory of the environment `plugins`, with this one taking
80         precedence.
81        
82         (''since 0.11'')""")
83
84     base_url = Option('trac', 'base_url', '',
85         """Reference URL for the Trac deployment.
86        
87         This is the base URL that will be used when producing documents that
88         will be used outside of the web browsing context, like for example
89         when inserting URLs pointing to Trac resources in notification
90         e-mails.""")
91
92     base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
93             False,
94         """Optionally use `[trac] base_url` for redirects.
95        
96         In some configurations, usually involving running Trac behind a HTTP
97         proxy, Trac can't automatically reconstruct the URL that is used to
98         access it. You may need to use this option to force Trac to use the
99         `base_url` setting also for redirects. This introduces the obvious
100         limitation that this environment will only be usable when accessible
101         from that URL, as redirects are frequently used.""")
102
103     project_name = Option('project', 'name', 'My Project',
104         """Name of the project.""")
105
106     project_description = Option('project', 'descr', 'My example project',
107         """Short description of the project.""")
108
109     project_url = Option('project', 'url', '',
110         """URL of the main project web site, usually the website in which
111         the `base_url` resides.""")
112
113     project_admin = Option('project', 'admin', '',
114         """E-Mail address of the project's administrator.""")
115
116     project_footer = Option('project', 'footer',
117                             'Visit the Trac open source project at<br />'
118                             '<a href="http://trac.edgewall.org/">'
119                             'http://trac.edgewall.org/</a>',
120         """Page footer text (right-aligned).""")
121
122     project_icon = Option('project', 'icon', 'common/trac.ico',
123         """URL of the icon of the project.""")
124
125     log_type = Option('logging', 'log_type', 'none',
126         """Logging facility to use.
127        
128         Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")
129
130     log_file = Option('logging', 'log_file', 'trac.log',
131         """If `log_type` is `file`, this should be a path to the log-file.""")
132
133     log_level = Option('logging', 'log_level', 'DEBUG',
134         """Level of verbosity in log.
135        
136         Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
137
138     log_format = Option('logging', 'log_format', None,
139         """Custom logging format.
140
141         If nothing is set, the following will be used:
142        
143         Trac[$(module)s] $(levelname)s: $(message)s
144
145         In addition to regular key names supported by the Python logger library
146         library (see http://docs.python.org/lib/node422.html), one could use:
147          - $(path)s     the path for the current environment
148          - $(basename)s the last path component of the current environment
149          - $(project)s  the project name
150
151          Note the usage of `$(...)s` instead of `%(...)s` as the latter form
152          would be interpreted by the ConfigParser itself.
153
154          Example:
155          ($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s
156
157          (since 0.11)""")
158
159     def __init__(self, path, create=False, options=[]):
160         """Initialize the Trac environment.
161        
162         @param path:   the absolute path to the Trac environment
163         @param create: if `True`, the environment is created and populated with
164                        default data; otherwise, the environment is expected to
165                        already exist.
166         @param options: A list of `(section, name, value)` tuples that define
167                         configuration options
168         """
169         ComponentManager.__init__(self)
170
171         self.path = path
172         self.setup_config(load_defaults=create)
173         self.setup_log()
174
175         from trac import core, __version__ as VERSION
176         self.systeminfo = [
177             ('Trac', get_pkginfo(core).get('version', VERSION)),
178             ('Python', sys.version),
179             ('setuptools', setuptools.__version__),
180             ]
181         self._href = self._abs_href = None
182
183         from trac.loader import load_components
184         plugins_dir = self.config.get('inherit', 'plugins_dir')
185         load_components(self, plugins_dir and (plugins_dir,))
186
187         if create:
188             self.create(options)
189         else:
190             self.verify()
191
192         if create:
193             for setup_participant in self.setup_participants:
194                 setup_participant.environment_created()
195
196     def component_activated(self, component):
197         """Initialize additional member variables for components.
198        
199         Every component activated through the `Environment` object gets three
200         member variables: `env` (the environment object), `config` (the
201         environment configuration) and `log` (a logger object)."""
202         component.env = self
203         component.config = self.config
204         component.log = self.log
205
206     def is_component_enabled(self, cls):
207         """Implemented to only allow activation of components that are not
208         disabled in the configuration.
209        
210         This is called by the `ComponentManager` base class when a component is
211         about to be activated. If this method returns false, the component does
212         not get activated."""
213         if not isinstance(cls, basestring):
214             component_name = (cls.__module__ + '.' + cls.__name__).lower()
215         else:
216             component_name = cls.lower()
217
218         rules = [(name.lower(), value.lower() in ('enabled', 'on'))
219                  for name, value in self.config.options('components')]
220         rules.sort(lambda a, b: -cmp(len(a[0]), len(b[0])))
221
222         for pattern, enabled in rules:
223             if component_name == pattern or pattern.endswith('*') \
224                     and component_name.startswith(pattern[:-1]):
225                 # force disabling of the pre-0.11 WebAdmin plugin
226                 if component_name.startswith('webadmin.'):
227                     self.log.warning('The obsolete 0.10 TracWebAdmin plugin '
228                                      'had to be disabled. Please uninstall it.')
229                     return False
230                 return enabled
231
232         # versioncontrol components are enabled if the repository is configured
233         # FIXME: this shouldn't be hardcoded like this
234         if component_name.startswith('trac.versioncontrol.'):
235             return self.config.get('trac', 'repository_dir') != ''
236
237         # By default, all components in the trac package are enabled
238         return component_name.startswith('trac.')
239
240     def verify(self):
241         """Verify that the provided path points to a valid Trac environment
242         directory."""
243         fd = open(os.path.join(self.path, 'VERSION'), 'r')
244         try:
245             assert fd.read(26) == 'Trac Environment Version 1'
246         finally:
247             fd.close()
248
249     def get_db_cnx(self):
250         """Return a database connection from the connection pool."""
251         return DatabaseManager(self).get_connection()
252
253     def shutdown(self, tid=None):
254         """Close the environment."""
255         RepositoryManager(self).shutdown(tid)
256         DatabaseManager(self).shutdown(tid)
257
258     def get_repository(self, authname=None):
259         """Return the version control repository configured for this
260         environment.
261        
262         @param authname: user name for authorization
263         """
264         return RepositoryManager(self).get_repository(authname)
265
266     def create(self, options=[]):
267         """Create the basic directory structure of the environment, initialize
268         the database and populate the configuration file with default values.
269
270         If options contains ('inherit', 'file'), default values will not be
271         loaded; they are expected to be provided by that file or other options.
272         """
273         def _create_file(fname, data=None):
274             fd = open(fname, 'w')
275             if data:
276                 fd.write(data)
277             fd.close()
278
279         # Create the directory structure
280         if not os.path.exists(self.path):
281             os.mkdir(self.path)
282         os.mkdir(self.get_log_dir())
283         os.mkdir(self.get_htdocs_dir())
284         os.mkdir(os.path.join(self.path, 'plugins'))
285
286         # Create a few files
287         _create_file(os.path.join(self.path, 'VERSION'),
288                      'Trac Environment Version 1\n')
289         _create_file(os.path.join(self.path, 'README'),
290                      'This directory contains a Trac environment.\n'
291                      'Visit http://trac.edgewall.org/ for more information.\n')
292
293         # Setup the default configuration
294         os.mkdir(os.path.join(self.path, 'conf'))
295         _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
296         skip_defaults = options and ('inherit', 'file') in [(section, option) \
297                 for (section, option, value) in options]
298         self.setup_config(load_defaults=not skip_defaults)
299         for section, name, value in options:
300             self.config.set(section, name, value)
301         self.config.save()
302         self.config.parse_if_needed() # Full reload to get 'inherit' working
303
304         # Create the database
305         DatabaseManager(self).init_db()
306
307     def get_version(self, db=None, initial=False):
308         """Return the current version of the database.
309         If the optional argument `initial` is set to `True`, the version
310         of the database used at the time of creation will be returned.
311
312         In practice, for database created before 0.11, this will return `False`
313         which is "older" than any db version number.
314
315         :since 0.11:
316         """
317         if not db:
318             db = self.get_db_cnx()
319         cursor = db.cursor()
320         cursor.execute("SELECT value FROM system "
321                        "WHERE name='%sdatabase_version'" %
322                        (initial and 'initial_' or ''))
323         row = cursor.fetchone()
324         return row and int(row[0])
325
326     def setup_config(self, load_defaults=False):
327         """Load the configuration file."""
328         self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'))
329         if load_defaults:
330             for section, default_options in self.config.defaults().items():
331                 for name, value in default_options.items():
332                     if self.config.parent and name in self.config.parent[section]:
333                         value = None
334                     self.config.set(section, name, value)
335
336     def get_templates_dir(self):
337         """Return absolute path to the templates directory."""
338         return os.path.join(self.path, 'templates')
339
340     def get_htdocs_dir(self):
341         """Return absolute path to the htdocs directory."""
342         return os.path.join(self.path, 'htdocs')
343
344     def get_log_dir(self):
345         """Return absolute path to the log directory."""
346         return os.path.join(self.path, 'log')
347
348     def setup_log(self):
349         """Initialize the logging sub-system."""
350         from trac.log import logger_factory
351         logtype = self.log_type
352         logfile = self.log_file
353         if logtype == 'file' and not os.path.isabs(logfile):
354             logfile = os.path.join(self.get_log_dir(), logfile)
355         format = self.log_format
356         if format:
357             format = format.replace('$(', '%(') \
358                      .replace('%(path)s', self.path) \
359                      .replace('%(basename)s', os.path.basename(self.path)) \
360                      .replace('%(project)s', self.project_name)
361         self.log = logger_factory(logtype, logfile, self.log_level, self.path,
362                                   format=format)
363
364     def get_known_users(self, cnx=None):
365         """Generator that yields information about all known users, i.e. users
366         that have logged in to this Trac environment and possibly set their name
367         and email.
368
369         This function generates one tuple for every user, of the form
370         (username, name, email) ordered alpha-numerically by username.
371
372         @param cnx: the database connection; if ommitted, a new connection is
373                     retrieved
374         """
375         if not cnx:
376             cnx = self.get_db_cnx()
377         cursor = cnx.cursor()
378         cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
379                        "FROM session AS s "
380                        " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
381                        "  and n.authenticated=1 AND n.name = 'name') "
382                        " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
383                        "  AND e.authenticated=1 AND e.name = 'email') "
384                        "WHERE s.authenticated=1 ORDER BY s.sid")
385         for username,name,email in cursor:
386             yield username, name, email
387
388     def backup(self, dest=None):
389         """Simple SQLite-specific backup of the database.
390
391         @param dest: Destination file; if not specified, the backup is stored in
392                      a file called db_name.trac_version.bak
393         """
394         import shutil
395
396         db_str = self.config.get('trac', 'database')
397         if not db_str.startswith('sqlite:'):
398             raise TracError(_('Can only backup sqlite databases'))
399         db_name = os.path.join(self.path, db_str[7:])
400         if not dest:
401             dest = '%s.%i.bak' % (db_name, self.get_version())
402         shutil.copy (db_name, dest)
403
404     def needs_upgrade(self):
405         """Return whether the environment needs to be upgraded."""
406         db = self.get_db_cnx()
407         for participant in self.setup_participants:
408             if participant.environment_needs_upgrade(db):
409                 self.log.warning('Component %s requires environment upgrade',
410                                  participant)
411                 return True
412         return False
413
414     def upgrade(self, backup=False, backup_dest=None):
415         """Upgrade database.
416        
417         Each db version should have its own upgrade module, names
418         upgrades/dbN.py, where 'N' is the version number (int).
419
420         @param backup: whether or not to backup before upgrading
421         @param backup_dest: name of the backup file
422         @return: whether the upgrade was performed
423         """
424         db = self.get_db_cnx()
425
426         upgraders = []
427         for participant in self.setup_participants:
428             if participant.environment_needs_upgrade(db):
429                 upgraders.append(participant)
430         if not upgraders:
431             return False
432
433         if backup:
434             self.backup(backup_dest)
435         for participant in upgraders:
436             participant.upgrade_environment(db)
437         db.commit()
438
439         # Database schema may have changed, so close all connections
440         self.shutdown()
441
442         return True
443
444     def _get_href(self):
445         if not self._href:
446             self._href = Href(urlsplit(self.abs_href.base)[2])
447         return self._href
448     href = property(_get_href, 'The application root path')
449
450     def _get_abs_href(self):
451         if not self._abs_href:
452             if not self.base_url:
453                 self.log.warn('base_url option not set in configuration, '
454                               'generated links may be incorrect')
455                 self._abs_href = Href('')
456             else:
457                 self._abs_href = Href(self.base_url)
458         return self._abs_href
459     abs_href = property(_get_abs_href, 'The application URL')
460
461
462 class EnvironmentSetup(Component):
463     implements(IEnvironmentSetupParticipant)
464
465     # IEnvironmentSetupParticipant methods
466
467     def environment_created(self):
468         """Insert default data into the database."""
469         db = self.env.get_db_cnx()
470         cursor = db.cursor()
471         for table, cols, vals in db_default.get_data(db):
472             cursor.executemany("INSERT INTO %s (%s) VALUES (%s)" % (table,
473                                ','.join(cols), ','.join(['%s' for c in cols])),
474                                vals)
475         db.commit()
476         self._update_sample_config()
477
478     def environment_needs_upgrade(self, db):
479         dbver = self.env.get_version(db)
480         if dbver == db_default.db_version:
481             return False
482         elif dbver > db_default.db_version:
483             raise TracError(_('Database newer than Trac version'))
484         return True
485
486     def upgrade_environment(self, db):
487         cursor = db.cursor()
488         dbver = self.env.get_version()
489         for i in range(dbver + 1, db_default.db_version + 1):
490             name  = 'db%i' % i
491             try:
492                 upgrades = __import__('upgrades', globals(), locals(), [name])
493                 script = getattr(upgrades, name)
494             except AttributeError:
495                 raise TracError(_('No upgrade module for version %(num)i '
496                                   '(%(version)s.py)', num=i, version=name))
497             script.do_upgrade(self.env, i, cursor)
498         cursor.execute("UPDATE system SET value=%s WHERE "
499                        "name='database_version'", (db_default.db_version,))
500         self.log.info('Upgraded database version from %d to %d',
501                       dbver, db_default.db_version)
502         self._update_sample_config()
503
504     # Internal methods
505
506     def _update_sample_config(self):
507         filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
508         config = Configuration(filename)
509         for section, default_options in config.defaults().iteritems():
510             for name, value in default_options.iteritems():
511                 config.set(section, name, value)
512         try:
513             config.save()
514             self.log.info('Wrote sample configuration file with the new '
515                           'settings and their default values: %s',
516                           filename)
517         except IOError, e:
518             self.log.warn('Couldn\'t write sample configuration file (%s)', e,
519                           exc_info=True)
520
521
522 env_cache = {}
523 env_cache_lock = threading.Lock()
524
525 def open_environment(env_path=None, use_cache=False):
526     """Open an existing environment object, and verify that the database is up
527     to date.
528
529     @param env_path: absolute path to the environment directory; if ommitted,
530                      the value of the `TRAC_ENV` environment variable is used
531     @param use_cache: whether the environment should be cached for subsequent
532                       invocations of this function
533     @return: the `Environment` object
534     """
535     global env_cache, env_cache_lock
536
537     if not env_path:
538         env_path = os.getenv('TRAC_ENV')
539     if not env_path:
540         raise TracError(_('Missing environment variable "TRAC_ENV". '
541                           'Trac requires this variable to point to a valid '
542                           'Trac environment.'))
543
544     if use_cache:
545         env_cache_lock.acquire()
546         try:
547             env = env_cache.get(env_path)
548             if env and env.config.parse_if_needed():
549                 # The environment configuration has changed, so shut it down
550                 # and remove it from the cache so that it gets reinitialized
551                 env.log.info('Reloading environment due to configuration '
552                              'change')
553                 env.shutdown()
554                 if hasattr(env.log, '_trac_handler'):
555                     hdlr = env.log._trac_handler
556                     env.log.removeHandler(hdlr)
557                     hdlr.close()
558                 del env_cache[env_path]
559                 env = None
560             if env is None:
561                 env = env_cache.setdefault(env_path, open_environment(env_path))
562         finally:
563             env_cache_lock.release()
564     else:
565         env = Environment(env_path)
566         needs_upgrade = False
567         try:
568             needs_upgrade = env.needs_upgrade()
569         except Exception, e: # e.g. no database connection
570             env.log.exception(e)
571         if needs_upgrade:
572             raise TracError(_('The Trac Environment needs to be upgraded.\n\n'
573                               'Run "trac-admin %(path)s upgrade"',
574                               path=env_path))
575
576     return env
Note: See TracBrowser for help on using the browser.

SFLC Main Page

[frdm] Support SFLC