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 def connect(url): """Convenience method for connecting to a MySQL database.""" engine = sqlalchemy.create_engine(url) meta = sqlalchemy.MetaData() meta.bind = engine meta.reflect() return meta 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 not url.database: # it's hopeless return url # omitted port and query if any((url.host, url.username, url.password)): # don't try for defaults if a few of these were set return url 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