Wizard is a Git-based autoinstall management system for scripts.
-Its commands are:
+User commands:
backup Backup data not on filesystem (database, etc)
- configure Configures an autoinstall (database, etc) to work
- errors Lists all broken autoinstall metadata
install Installs an application
+ migrate Migrate autoinstalls from old format to Git-based format
+ restore Restores files and database to previous version
+ upgrade Upgrades an autoinstall to the latest version
+
+Administrative commands:
+ errors Lists all broken autoinstall metadata
list Lists autoinstalls, with optional filtering
mass-migrate Performs mass migration of autoinstalls of an application
mass-upgrade Performs mass upgrade of autoinstalls of an application
- migrate Migrate autoinstalls from old format to Git-based format
- prepare-config Prepares configuration files for versioning
research Print statistics about a possible upgrade
summary Generate statistics (see help for subcommands)
- upgrade Upgrades an autoinstall to the latest version
+
+Utility commands:
+ configure Configures an autoinstall (database, etc) to work
+ prepare-config Prepares configuration files for versioning
See '%prog help COMMAND' for more information on a specific command."""
.. autofunction:: set_author_env
.. autofunction:: set_git_env
.. autofunction:: get_git_footer
+.. autofunction:: safe_unlink
Exceptions
----------
ERROR: Backup script failed, details:
%s""" % self.details
+
+class RestoreFailure(Error):
+ """Restore script failed."""
+ #: String details of failure
+ details = None
+ def __init__(self, details):
+ self.details = details
+ def __str__(self):
+ return """
+
+ERROR: Restore script failed, details:
+
+%s""" % self.details
import datetime
import logging
import shlex
+import shutil
from wizard import app, deploy, install, scripts, shell, util
from wizard.app import php
raise app.UpgradeFailure(result)
def backup(self, deployment, options):
sh = shell.Shell()
- vars = deployment.extract()
- if any(vars[i] is None for i in ['WIZARD_DBSERVER', 'WIZARD_DBNAME', 'WIZARD_DBUSER', 'WIZARD_DBPASSWORD']):
- raise app.BackupFailure("Missing WIZARD variables from configuration files")
# XXX: duplicate code, refactor, also, race condition
backupdir = os.path.join(".scripts", "backups")
outdir = os.path.join(backupdir, str(deployment.version) + "-" + datetime.date.today().isoformat())
util.safe_unlink(outdir)
os.mkdir(outdir)
outfile = os.path.join(outdir, "db.sql")
- # XXX: add support for getting these out of options
- triplet = scripts.get_sql_credentials()
- args = ["mysqldump", "--compress", "-r", outfile];
- if triplet is not None:
- server, user, password = triplet
- args += ["-h", server, "-u", user, "-p" + password]
- name = shlex.split(vars['WIZARD_DBNAME'])[0]
- args.append(name)
try:
- sh.call(*args)
+ sh.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment))
sh.call("gzip", "--best", outfile)
except shell.CallError as e:
raise app.BackupFailure(e.stderr)
+ def restore(self, deployment, backup, options):
+ sh = shell.Shell()
+ backup_dir = os.path.join(".scripts", "backups", backup)
+ if not os.path.exists(backup_dir):
+ raise app.RestoreFailure("Backup %s doesn't exist", backup)
+ sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
+ sh.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
+ sql.seek(0)
+ sh.call("mysql", *get_mysql_args(deployment), stdin=sql)
+ sql.close()
+def get_mysql_args(d):
+ # XXX: add support for getting these out of options
+ vars = d.extract()
+ if 'WIZARD_DBNAME' not in vars:
+ raise app.BackupFailure("Could not determine database name")
+ triplet = scripts.get_sql_credentials()
+ args = []
+ if triplet is not None:
+ server, user, password = triplet
+ args += ["-h", server, "-u", user, "-p" + password]
+ name = shlex.split(vars['WIZARD_DBNAME'])[0]
+ args.append(name)
+ return args
--- /dev/null
+import logging
+import os
+import optparse
+import sys
+import distutils.version
+
+from wizard import command, deploy, git, shell, util
+
+def main(argv, baton):
+ options, args = parse_args(argv, baton)
+ backups = ".scripts/backups"
+ if not args:
+ if not os.path.exists(backups):
+ print "No restore points available"
+ return
+ sys.stderr.write("Available backups:\n")
+ count = 0
+ for d in reversed(sorted(os.listdir(backups))):
+ if ".bak" in d:
+ continue
+ if os.listdir(os.path.join(backups, d)):
+ print d
+ else:
+ count += 1
+ if count:
+ logging.warning("Pruned %d empty backups" % count)
+ return
+ backup = args[0]
+ bits = backup.split('-')
+ date = '-'.join(bits[-3:])
+ version = '-'.join(bits[0:-3])
+ shell.drop_priviledges(dir, options.log_file)
+ d = deploy.Deployment(".")
+ d.verify()
+ d.verifyConfigured()
+ tag = "%s-%s" % (d.application.name, version)
+ sh = shell.Shell()
+ try:
+ sh.call("git", "rev-parse", tag)
+ except shell.CallError:
+ raise Exception("Tag %s doesn't exist in repository" % tag)
+ sh.call("git", "reset", "--hard", tag)
+ d.application.restore(d, backup, options)
+
+def parse_args(argv, baton):
+ usage = """usage: %prog restore [ARGS] ID
+
+Takes a backup from the backups/ directory and does
+a full restore back to it. CURRENT DATA IS DESTROYED,
+so you may want to do a backup before you do a restore."""
+ parser = command.WizardOptionParser(usage)
+ options, args = parser.parse_all(argv)
+ if len(args) > 1:
+ parser.error("too many arguments")
+ return options, args
+
that the current working directory is the deployment.
"""
raise NotImplemented
+ def restore(self, deployment, backup, options):
+ """
+ Run for 'wizard restore' and failed upgrades to restore database
+ and other non-versioned data to a backed up version. This assumes
+ that the current working directory is the deployment.
+ """
+ raise NotImplemented
def detectVersion(self, deployment):
"""
Checks source files to determine the version manually.
specify this if you are using another wrapper around this function).
:param log: if True, we log the call as INFO, if False, we log the call
as DEBUG, otherwise, we detect based on ``strip``.
+ :param stdout:
+ :param stderr:
+ :param stdin: a file-type object that will be written to or read from as a pipe.
:returns: a tuple of strings ``(stdout, stderr)``, or a string ``stdout``
if ``strip`` is specified.
kwargs.setdefault("strip", False)
kwargs.setdefault("python", None)
kwargs.setdefault("log", None)
+ kwargs.setdefault("stdout", subprocess.PIPE)
+ kwargs.setdefault("stdin", subprocess.PIPE)
+ kwargs.setdefault("stderr", subprocess.PIPE)
msg = "Running `" + ' '.join(args) + "`"
if kwargs["strip"] and not kwargs["log"] is True or kwargs["log"] is False:
logging.debug(msg)
stdin=sys.stdin
stderr=sys.stderr
else:
- stdout=subprocess.PIPE
- stdin=subprocess.PIPE
- stderr=subprocess.PIPE
+ stdout=kwargs["stdout"]
+ stdin=kwargs["stdin"]
+ stderr=kwargs["stderr"]
# XXX: There is a possible problem here where we can fill up
# the kernel buffer if we have 64KB of data. This shouldn't
# be a problem, and the fix for such case would be to write to