import urlparse
import tempfile
import pkg_resources
+import traceback
import wizard
-from wizard import resolve, shell, sql, util
-
-# SCRIPTS SPECIFIC
-_scripts_application_list = [
- "mediawiki", "wordpress", "joomla", "e107", "gallery2",
- "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
- "rails",
- # these are technically deprecated
- "advancedpoll", "gallery",
-]
-def _scripts_make(name):
- """Makes an application, but uses the correct subtype if available."""
- try:
- __import__("wizard.app." + name)
- return getattr(wizard.app, name).Application(name)
- except ImportError as error:
- # XXX ugly hack to check if the import error is from the top level
- # module we care about or a submodule. should be an archetectural change.
- if error.args[0].split()[-1]==name:
- return Application(name)
- else:
- raise
+from wizard import plugin, resolve, shell, sql, util
_applications = None
def applications():
"""Hash table for looking up string application name to instance"""
global _applications
if not _applications:
- # SCRIPTS SPECIFIC
- _applications = dict([(n,_scripts_make(n)) for n in _scripts_application_list ])
- # setup plugins
+ _applications = dict()
for dist in pkg_resources.working_set:
for appname, entry in dist.get_entry_map("wizard.app").items():
if appname in _applications:
appname = newname
appclass = entry.load()
_applications[appname] = appclass(appname)
+ # setup dummy apps
+ for entry in pkg_resources.iter_entry_points("wizard.dummy_apps"):
+ appfun = entry.load()
+ dummy_apps = appfun()
+ for appname in dummy_apps:
+ # a dummy app that already exists is not a fatal error
+ if appname in _applications:
+ continue
+ _applications[appname] = Application(appname)
return _applications
def getApplication(appname):
"""Retrieves application instance given a name"""
- return applications()[appname]
+ try:
+ return applications()[appname]
+ except KeyError:
+ raise NoSuchApplication(appname)
class Application(object):
"""
"""
#: String name of the application
name = None
+ #: Human-readable name of the application
+ fullname = None
#: Dictionary of version strings to :class:`ApplicationVersion`.
#: See also :meth:`makeVersion`.
versions = None
Makes/retrieves a singleton :class:`ApplicationVersion` from
a``app`` and ``version`` string.
"""
- try:
- # defer to the application for version creation to enforce
- # singletons
- return applications()[app].makeVersion(version)
- except KeyError:
- raise NoSuchApplication(app)
+ # defer to the application for version creation to enforce
+ # singletons
+ return getApplication(app).makeVersion(version)
def expand_re(val):
"""
return subs
return h
-def backup_database(outdir, deployment):
+@decorator.decorator
+def throws_database_errors(f, self, *args, **kwargs):
"""
- Generic database backup function for MySQL.
+ Decorator that takes database errors from :mod:`wizard.sql` and
+ converts them into application script failures from
+ :mod:`wizard.app`. We can't throw application errors directly from
+ :mod:`wizard.sql` because that would result in a cyclic import;
+ also, it's cleaner to distinguish between a database error and an
+ application script failure.
"""
- # XXX: Change this once deployments support multiple dbs
- if deployment.application.database == "mysql":
- return backup_mysql_database(outdir, deployment)
- else:
- raise NotImplementedError
-
-def backup_mysql_database(outdir, deployment):
- """
- Database backups for MySQL using the :command:`mysqldump` utility.
- """
- outfile = os.path.join(outdir, "db.sql")
try:
- shell.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
- shell.call("gzip", "--best", outfile)
- except shell.CallError as e:
- raise BackupFailure(e.stderr)
-
-def restore_database(backup_dir, deployment):
- """
- Generic database restoration function for MySQL.
- """
- # XXX: see backup_database
- if deployment.application.database == "mysql":
- return restore_mysql_database(backup_dir, deployment)
- else:
- raise NotImplementedError
-
-def restore_mysql_database(backup_dir, deployment):
- """
- Database restoration for MySQL by piping SQL commands into :command:`mysql`.
- """
- if not os.path.exists(backup_dir):
- raise RestoreFailure("Backup %s doesn't exist", backup_dir.rpartition("/")[2])
- sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
- shell.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
- sql.seek(0)
- shell.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
- sql.close()
-
-# XXX: SCRIPTS
-def remove_database(deployment):
- """
- Generic database removal function. Actually, not so generic because we
- go and check if we're on scripts and if we are run a different command.
- """
- if deployment.dsn.host == "sql.mit.edu":
- try:
- shell.call("/mit/scripts/sql/bin/drop-database", deployment.dsn.database)
- return
- except shell.CallError:
- pass
- engine = sqlalchemy.create_engine(deployment.dsn)
- engine.execute("DROP DATABASE `%s`" % deployment.dsn.database)
-
-def get_mysql_args(dsn):
- """
- Extracts arguments that would be passed to the command line mysql utility
- from a deployment.
- """
- args = []
- if dsn.host:
- args += ["-h", dsn.host]
- if dsn.username:
- args += ["-u", dsn.username]
- if dsn.password:
- args += ["-p" + dsn.password]
- args += [dsn.database]
- return args
+ return f(self, *args, **kwargs)
+ except sql.BackupDatabaseError:
+ raise BackupFailure(traceback.format_exc())
+ except sql.RestoreDatabaseError:
+ raise RestoreFailure(traceback.format_exc())
+ except sql.RemoveDatabaseError:
+ raise RemoveFailure(traceback.format_exc())
class Error(wizard.Error):
"""Generic error class for this module."""
location = None
def __init__(self, value):
self.value = value
+ def __str__(self):
+ return "Could not parse '%s' from versions store in '%s'" % (self.value, self.location)
class NoSuchApplication(Error):
"""
location = None
def __init__(self, app):
self.app = app
+ def __str__(self):
+ return "Wizard doesn't know about an application named '%s'." % self.app
class Failure(Error):
"""