import sqlalchemy
+import os
+import pkg_resources
+import copy
+import decorator
+
+import wizard
+from wizard import plugin, shell
# We're going to use sqlalchemy.engine.url.URL as our database
# info intermediate object
meta.reflect()
return meta
-def fill_url(url):
+def auth(url):
"""
If the URL has a database name but no other values, it will
use the global configuration, and then try the database name.
+
+ This function implements a plugin interface named
+ :ref:`wizard.sql.auth`.
"""
if not url:
return None
if any((url.host, url.username, url.password)):
# don't try for defaults if a few of these were set
return url
- # this is hook stuff
- if url.driver == "mysql":
- try:
- url.host, url.username, url.password = sh.eval("/mit/scripts/sql/bin/get-password").split()
- return url
- except shell.CallError:
- pass
- dsn = os.getenv("WIZARD_DSN")
- old_url = url
- url = sqlalchemy.engine.url.make_url(dsn)
- url.database = old_url.database
+ for entry in pkg_resources.iter_entry_points("wizard.sql.auth"):
+ func = entry.load()
+ r = func(copy.copy(url))
+ if r is not None:
+ return r
+ env_dsn = os.getenv("WIZARD_DSN")
+ if env_dsn:
+ old_url = url
+ url = sqlalchemy.engine.url.make_url(env_dsn)
+ url.database = old_url.database
return url
+
+def backup(outdir, deployment):
+ """
+ Generic database backup function.
+ """
+ # XXX: Change this once deployments support multiple dbs
+ if deployment.application.database == "mysql":
+ return backup_mysql(outdir, deployment)
+ else:
+ raise NotImplementedError
+
+def backup_mysql(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 BackupDatabaseError(e.stderr)
+
+def restore(backup_dir, deployment):
+ """
+ Generic database restoration function.
+ """
+ # XXX: see backup
+ if deployment.application.database == "mysql":
+ return restore_mysql(backup_dir, deployment)
+ else:
+ raise NotImplementedError
+
+def restore_mysql(backup_dir, deployment):
+ """
+ Database restoration for MySQL by piping SQL commands into :command:`mysql`.
+ """
+ if not os.path.exists(backup_dir):
+ raise RestoreDatabaseError("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()
+
+def drop(url):
+ """
+ Generic drop database function. Attempts to run ``DROP
+ DATABASE`` on the database if no plugins succeed.
+
+ This function implements the plugin interface named
+ :ref:`wizard.sql.drop`.
+ """
+ r = plugin.hook("wizard.sql.drop", [url])
+ if r is not None:
+ return
+ engine = sqlalchemy.create_engine(url)
+ engine.execute("DROP DATABASE `%s`" % url.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
+
+class Error(wizard.Error):
+ """Generic error class for this module."""
+ pass
+
+class BackupDatabaseError(Error):
+ """Backup script failed."""
+ #: String details of failure
+ details = None
+ def __init__(self, details):
+ self.details = details
+ def __str__(self):
+ return """
+
+ERROR: Backing up the database failed, details:
+
+%s""" % self.details
+
+class RestoreDatabaseError(Error):
+ """Restore script failed."""
+ #: String details of failure
+ details = None
+ def __init__(self, details):
+ self.details = details
+ def __str__(self):
+ return """
+
+ERROR: Restoring the database failed, details:
+
+%s""" % self.details
+
+class RemoveDatabaseError(Error):
+ """Removing the database failed."""
+ #: String details of failure
+ details = None
+ def __init__(self, details):
+ self.details = details
+ def __str__(self):
+ return """
+
+ERROR: Removing the database failed, details:
+
+%s""" % self.details