39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2003-2005 Edgewall Software
|
|
4 # Copyright (C) 2003-2005 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.com/license.html.
|
|
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://projects.edgewall.com/trac/.
|
|
14 #
|
|
15 # Author: Jonas Borgström <jonas@edgewall.com>
|
|
16
|
|
17 import os
|
|
18
|
|
19 from trac import db_default, util
|
|
20 from trac.config import *
|
|
21 from trac.core import Component, ComponentManager, implements, Interface, \
|
|
22 ExtensionPoint, TracError
|
|
23 from trac.db import DatabaseManager
|
|
24 from trac.versioncontrol import RepositoryManager
|
|
25
|
|
26 __all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment']
|
|
27
|
|
28
|
|
29 class IEnvironmentSetupParticipant(Interface):
|
|
30 """Extension point interface for components that need to participate in the
|
|
31 creation and upgrading of Trac environments, for example to create
|
|
32 additional database tables."""
|
|
33
|
|
34 def environment_created():
|
|
35 """Called when a new Trac environment is created."""
|
|
36
|
|
37 def environment_needs_upgrade(db):
|
|
38 """Called when Trac checks whether the environment needs to be upgraded.
|
|
39
|
|
40 Should return `True` if this participant needs an upgrade to be
|
|
41 performed, `False` otherwise.
|
|
42 """
|
|
43
|
|
44 def upgrade_environment(db):
|
|
45 """Actually perform an environment upgrade.
|
|
46
|
|
47 Implementations of this method should not commit any database
|
|
48 transactions. This is done implicitly after all participants have
|
|
49 performed the upgrades they need without an error being raised.
|
|
50 """
|
|
51
|
|
52
|
|
53 class Environment(Component, ComponentManager):
|
|
54 """Trac stores project information in a Trac environment.
|
|
55
|
|
56 A Trac environment consists of a directory structure containing among other
|
|
57 things:
|
|
58 * a configuration file.
|
|
59 * an SQLite database (stores tickets, wiki pages...)
|
|
60 * Project specific templates and wiki macros.
|
|
61 * wiki and ticket attachments.
|
|
62 """
|
|
63 setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)
|
|
64
|
|
65 base_url = Option('trac', 'base_url', '',
|
|
66 """Base URL of the Trac deployment.
|
|
67
|
|
68 In most configurations, Trac will automatically reconstruct the URL
|
|
69 that is used to access it automatically. However, in more complex
|
|
70 setups, usually involving running Trac behind a HTTP proxy, you may
|
|
71 need to use this option to force Trac to use the correct URL.""")
|
|
72
|
|
73 project_name = Option('project', 'name', 'My Project',
|
|
74 """Name of the project.""")
|
|
75
|
|
76 project_description = Option('project', 'descr', 'My example project',
|
|
77 """Short description of the project.""")
|
|
78
|
|
79 project_url = Option('project', 'url', 'http://example.org/',
|
|
80 """URL of the main project web site.""")
|
|
81
|
|
82 project_footer = Option('project', 'footer',
|
|
83 'Visit the Trac open source project at<br />'
|
|
84 '<a href="http://trac.edgewall.com/">'
|
|
85 'http://trac.edgewall.com/</a>',
|
|
86 """Page footer text (right-aligned).""")
|
|
87
|
|
88 project_icon = Option('project', 'icon', 'common/trac.ico',
|
|
89 """URL of the icon of the project.""")
|
|
90
|
|
91 log_type = Option('logging', 'log_type', 'none',
|
|
92 """Logging facility to use.
|
|
93
|
|
94 Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")
|
|
95
|
|
96 log_file = Option('logging', 'log_file', 'trac.log',
|
|
97 """If `log_type` is `file`, this should be a path to the log-file.""")
|
|
98
|
|
99 log_level = Option('logging', 'log_level', 'DEBUG',
|
|
100 """Level of verbosity in log.
|
|
101
|
|
102 Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
|
|
103
|
|
104 def __init__(self, path, create=False, options=[]):
|
|
105 """Initialize the Trac environment.
|
|
106
|
|
107 @param path: the absolute path to the Trac environment
|
|
108 @param create: if `True`, the environment is created and populated with
|
|
109 default data; otherwise, the environment is expected to
|
|
110 already exist.
|
|
111 @param options: A list of `(section, name, value)` tuples that define
|
|
112 configuration options
|
|
113 """
|
|
114 ComponentManager.__init__(self)
|
|
115
|
|
116 self.path = path
|
|
117 self.setup_config(load_defaults=create)
|
|
118 self.setup_log()
|
|
119
|
|
120 from trac.loader import load_components
|
|
121 load_components(self)
|
|
122
|
|
123 if create:
|
|
124 self.create(options)
|
|
125 else:
|
|
126 self.verify()
|
|
127
|
|
128 if create:
|
|
129 for setup_participant in self.setup_participants:
|
|
130 setup_participant.environment_created()
|
|
131
|
|
132 def component_activated(self, component):
|
|
133 """Initialize additional member variables for components.
|
|
134
|
|
135 Every component activated through the `Environment` object gets three
|
|
136 member variables: `env` (the environment object), `config` (the
|
|
137 environment configuration) and `log` (a logger object)."""
|
|
138 component.env = self
|
|
139 component.config = self.config
|
|
140 component.log = self.log
|
|
141
|
|
142 def is_component_enabled(self, cls):
|
|
143 """Implemented to only allow activation of components that are not
|
|
144 disabled in the configuration.
|
|
145
|
|
146 This is called by the `ComponentManager` base class when a component is
|
|
147 about to be activated. If this method returns false, the component does
|
|
148 not get activated."""
|
|
149 if not isinstance(cls, basestring):
|
|
150 component_name = (cls.__module__ + '.' + cls.__name__).lower()
|
|
151 else:
|
|
152 component_name = cls.lower()
|
|
153
|
|
154 rules = [(name.lower(), value.lower() in ('enabled', 'on'))
|
|
155 for name, value in self.config.options('components')]
|
|
156 rules.sort(lambda a, b: -cmp(len(a[0]), len(b[0])))
|
|
157
|
|
158 for pattern, enabled in rules:
|
|
159 if component_name == pattern or pattern.endswith('*') \
|
|
160 and component_name.startswith(pattern[:-1]):
|
|
161 return enabled
|
|
162
|
|
163 # versioncontrol components are enabled if the repository is configured
|
|
164 # FIXME: this shouldn't be hardcoded like this
|
|
165 if component_name.startswith('trac.versioncontrol.'):
|
|
166 return self.config.get('trac', 'repository_dir') != ''
|
|
167
|
|
168 # By default, all components in the trac package are enabled
|
|
169 return component_name.startswith('trac.')
|
|
170
|
|
171 def verify(self):
|
|
172 """Verify that the provided path points to a valid Trac environment
|
|
173 directory."""
|
|
174 fd = open(os.path.join(self.path, 'VERSION'), 'r')
|
|
175 try:
|
|
176 assert fd.read(26) == 'Trac Environment Version 1'
|
|
177 finally:
|
|
178 fd.close()
|
|
179
|
|
180 def get_db_cnx(self):
|
|
181 """Return a database connection from the connection pool."""
|
|
182 return DatabaseManager(self).get_connection()
|
|
183
|
|
184 def shutdown(self):
|
|
185 """Close the environment."""
|
|
186 DatabaseManager(self).shutdown()
|
|
187
|
|
188 def get_repository(self, authname=None):
|
|
189 """Return the version control repository configured for this
|
|
190 environment.
|
|
191
|
|
192 @param authname: user name for authorization
|
|
193 """
|
|
194 return RepositoryManager(self).get_repository(authname)
|
|
195
|
|
196 def create(self, options=[]):
|
|
197 """Create the basic directory structure of the environment, initialize
|
|
198 the database and populate the configuration file with default values."""
|
|
199 def _create_file(fname, data=None):
|
|
200 fd = open(fname, 'w')
|
|
201 if data: fd.write(data)
|
|
202 fd.close()
|
|
203
|
|
204 # Create the directory structure
|
|
205 if not os.path.exists(self.path):
|
|
206 os.mkdir(self.path)
|
|
207 os.mkdir(self.get_log_dir())
|
|
208 os.mkdir(self.get_htdocs_dir())
|
|
209 os.mkdir(os.path.join(self.path, 'plugins'))
|
|
210 os.mkdir(os.path.join(self.path, 'wiki-macros'))
|
|
211
|
|
212 # Create a few files
|
|
213 _create_file(os.path.join(self.path, 'VERSION'),
|
|
214 'Trac Environment Version 1\n')
|
|
215 _create_file(os.path.join(self.path, 'README'),
|
|
216 'This directory contains a Trac environment.\n'
|
|
217 'Visit http://trac.edgewall.com/ for more information.\n')
|
|
218
|
|
219 # Setup the default configuration
|
|
220 os.mkdir(os.path.join(self.path, 'conf'))
|
|
221 _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
|
|
222 self.setup_config(load_defaults=True)
|
|
223 for section, name, value in options:
|
|
224 self.config.set(section, name, value)
|
|
225 self.config.save()
|
|
226
|
|
227 # Create the database
|
|
228 DatabaseManager(self).init_db()
|
|
229
|
|
230 def get_version(self, db=None):
|
|
231 """Return the current version of the database."""
|
|
232 if not db:
|
|
233 db = self.get_db_cnx()
|
|
234 cursor = db.cursor()
|
|
235 cursor.execute("SELECT value FROM system WHERE name='database_version'")
|
|
236 row = cursor.fetchone()
|
|
237 return row and int(row[0])
|
|
238
|
|
239 def setup_config(self, load_defaults=False):
|
|
240 """Load the configuration file."""
|
|
241 self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'))
|
|
242 if load_defaults:
|
|
243 for section, default_options in self.config.defaults().iteritems():
|
|
244 for name, value in default_options.iteritems():
|
|
245 self.config.set(section, name, value)
|
|
246
|
|
247 def get_templates_dir(self):
|
|
248 """Return absolute path to the templates directory."""
|
|
249 return os.path.join(self.path, 'templates')
|
|
250
|
|
251 def get_htdocs_dir(self):
|
|
252 """Return absolute path to the htdocs directory."""
|
|
253 return os.path.join(self.path, 'htdocs')
|
|
254
|
|
255 def get_log_dir(self):
|
|
256 """Return absolute path to the log directory."""
|
|
257 return os.path.join(self.path, 'log')
|
|
258
|
|
259 def setup_log(self):
|
|
260 """Initialize the logging sub-system."""
|
|
261 from trac.log import logger_factory
|
|
262 logtype = self.log_type
|
|
263 logfile = self.log_file
|
|
264 if logtype == 'file' and not os.path.isabs(logfile):
|
|
265 logfile = os.path.join(self.get_log_dir(), logfile)
|
|
266 self.log = logger_factory(logtype, logfile, self.log_level, self.path)
|
|
267
|
|
268 def get_known_users(self, cnx=None):
|
|
269 """Generator that yields information about all known users, i.e. users
|
|
270 that have logged in to this Trac environment and possibly set their name
|
|
271 and email.
|
|
272
|
|
273 This function generates one tuple for every user, of the form
|
|
274 (username, name, email) ordered alpha-numerically by username.
|
|
275
|
|
276 @param cnx: the database connection; if ommitted, a new connection is
|
|
277 retrieved
|
|
278 """
|
|
279 if not cnx:
|
|
280 cnx = self.get_db_cnx()
|
|
281 cursor = cnx.cursor()
|
|
282 cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
|
|
283 "FROM session AS s "
|
|
284 " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
|
|
285 " and n.authenticated=1 AND n.name = 'name') "
|
|
286 " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
|
|
287 " AND e.authenticated=1 AND e.name = 'email') "
|
|
288 "WHERE s.authenticated=1 ORDER BY s.sid")
|
|
289 for username,name,email in cursor:
|
|
290 yield username, name, email
|
|
291
|
|
292 def backup(self, dest=None):
|
|
293 """Simple SQLite-specific backup of the database.
|
|
294
|
|
295 @param dest: Destination file; if not specified, the backup is stored in
|
|
296 a file called db_name.trac_version.bak
|
|
297 """
|
|
298 import shutil
|
|
299
|
|
300 db_str = self.config.get('trac', 'database')
|
|
301 if not db_str.startswith('sqlite:'):
|
|
302 raise EnvironmentError, 'Can only backup sqlite databases'
|
|
303 db_name = os.path.join(self.path, db_str[7:])
|
|
304 if not dest:
|
|
305 dest = '%s.%i.bak' % (db_name, self.get_version())
|
|
306 shutil.copy (db_name, dest)
|
|
307
|
|
308 def needs_upgrade(self):
|
|
309 """Return whether the environment needs to be upgraded."""
|
|
310 db = self.get_db_cnx()
|
|
311 for participant in self.setup_participants:
|
|
312 if participant.environment_needs_upgrade(db):
|
|
313 self.log.warning('Component %s requires environment upgrade',
|
|
314 participant)
|
|
315 return True
|
|
316 return False
|
|
317
|
|
318 def upgrade(self, backup=False, backup_dest=None):
|
|
319 """Upgrade database.
|
|
320
|
|
321 Each db version should have its own upgrade module, names
|
|
322 upgrades/dbN.py, where 'N' is the version number (int).
|
|
323
|
|
324 @param backup: whether or not to backup before upgrading
|
|
325 @param backup_dest: name of the backup file
|
|
326 @return: whether the upgrade was performed
|
|
327 """
|
|
328 db = self.get_db_cnx()
|
|
329
|
|
330 upgraders = []
|
|
331 for participant in self.setup_participants:
|
|
332 if participant.environment_needs_upgrade(db):
|
|
333 upgraders.append(participant)
|
|
334 if not upgraders:
|
|
335 return False
|
|
336
|
|
337 if backup:
|
|
338 self.backup(backup_dest)
|
|
339 for participant in upgraders:
|
|
340 participant.upgrade_environment(db)
|
|
341 db.commit()
|
|
342
|
|
343 # Database schema may have changed, so close all connections
|
|
344 self.shutdown()
|
|
345
|
|
346 return True
|
|
347
|
|
348
|
|
349 class EnvironmentSetup(Component):
|
|
350 implements(IEnvironmentSetupParticipant)
|
|
351
|
|
352 # IEnvironmentSetupParticipant methods
|
|
353
|
|
354 def environment_created(self):
|
|
355 """Insert default data into the database."""
|
|
356 db = self.env.get_db_cnx()
|
|
357 cursor = db.cursor()
|
|
358 for table, cols, vals in db_default.data:
|
|
359 cursor.executemany("INSERT INTO %s (%s) VALUES (%s)" % (table,
|
|
360 ','.join(cols), ','.join(['%s' for c in cols])),
|
|
361 vals)
|
|
362 db.commit()
|
|
363 self._update_sample_config()
|
|
364
|
|
365 def environment_needs_upgrade(self, db):
|
|
366 dbver = self.env.get_version(db)
|
|
367 if dbver == db_default.db_version:
|
|
368 return False
|
|
369 elif dbver > db_default.db_version:
|
|
370 raise TracError, 'Database newer than Trac version'
|
|
371 return True
|
|
372
|
|
373 def upgrade_environment(self, db):
|
|
374 cursor = db.cursor()
|
|
375 dbver = self.env.get_version()
|
|
376 for i in range(dbver + 1, db_default.db_version + 1):
|
|
377 name = 'db%i' % i
|
|
378 try:
|
|
379 upgrades = __import__('upgrades', globals(), locals(), [name])
|
|
380 script = getattr(upgrades, name)
|
|
381 except AttributeError:
|
|
382 err = 'No upgrade module for version %i (%s.py)' % (i, name)
|
|
383 raise TracError, err
|
|
384 script.do_upgrade(self.env, i, cursor)
|
|
385 cursor.execute("UPDATE system SET value=%s WHERE "
|
|
386 "name='database_version'", (db_default.db_version,))
|
|
387 self.log.info('Upgraded database version from %d to %d',
|
|
388 dbver, db_default.db_version)
|
|
389 self._update_sample_config()
|
|
390
|
|
391 # Internal methods
|
|
392
|
|
393 def _update_sample_config(self):
|
|
394 from ConfigParser import ConfigParser
|
|
395 config = ConfigParser()
|
|
396 for section, options in self.config.defaults().items():
|
|
397 config.add_section(section)
|
|
398 for name, value in options.items():
|
|
399 config.set(section, name, value)
|
|
400 filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
|
|
401 try:
|
|
402 fileobj = file(filename, 'w')
|
|
403 try:
|
|
404 config.write(fileobj)
|
|
405 fileobj.close()
|
|
406 finally:
|
|
407 fileobj.close()
|
|
408 self.log.info('Wrote sample configuration file with the new '
|
|
409 'settings and their default values: %s',
|
|
410 filename)
|
|
411 except IOError, e:
|
|
412 self.log.warn('Couldn\'t write sample configuration file (%s)', e,
|
|
413 exc_info=True)
|
|
414
|
|
415
|
|
416 def open_environment(env_path=None):
|
|
417 """Open an existing environment object, and verify that the database is up
|
|
418 to date.
|
|
419
|
|
420 @param: env_path absolute path to the environment directory; if ommitted,
|
|
421 the value of the `TRAC_ENV` environment variable is used
|
|
422 @return: the `Environment` object
|
|
423 """
|
|
424 if not env_path:
|
|
425 env_path = os.getenv('TRAC_ENV')
|
|
426 if not env_path:
|
|
427 raise TracError, 'Missing environment variable "TRAC_ENV". Trac ' \
|
|
428 'requires this variable to point to a valid Trac ' \
|
|
429 'environment.'
|
|
430
|
|
431 env = Environment(env_path)
|
|
432 if env.needs_upgrade():
|
|
433 raise TracError, 'The Trac Environment needs to be upgraded. Run ' \
|
|
434 'trac-admin %s upgrade"' % env_path
|
|
435 return env
|