| 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 |
|---|