]> scripts.mit.edu Git - wizard.git/commitdiff
Implement 'wizard restore'.
authorEdward Z. Yang <ezyang@mit.edu>
Sat, 3 Oct 2009 01:03:56 +0000 (21:03 -0400)
committerEdward Z. Yang <ezyang@mit.edu>
Sat, 3 Oct 2009 01:03:56 +0000 (21:03 -0400)
* Added stdin, stderr and stdout kwargs to shell

Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
bin/wizard
doc/module/wizard.util.rst
wizard/app/__init__.py
wizard/app/mediawiki.py
wizard/command/restore.py [new file with mode: 0644]
wizard/deploy.py
wizard/shell.py

index 7889fb47635f1558b97953373bd128ccb19ab041..ac092c33e3c908703b3f5e9913c228429058093b 100755 (executable)
@@ -14,19 +14,24 @@ def main():
 
 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."""
 
index 2ea07a6d4487388152dece8a0261078609115dfa..762c0b2c3d0923779921eebd4422a9810cc6d782 100644 (file)
@@ -26,6 +26,7 @@ Functions
 .. autofunction:: set_author_env
 .. autofunction:: set_git_env
 .. autofunction:: get_git_footer
+.. autofunction:: safe_unlink
 
 Exceptions
 ----------
index 4193fd1a113ddf1c2c28cc876905b19649a31378..cc6bacf6ef85505540af2214d37cae75ec060207 100644 (file)
@@ -111,3 +111,16 @@ class BackupFailure(Error):
 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
index d77b0c38dad11d50221dd8c87a77d8e0736cb98e..65a8704cb40afc474ed5dc4dcec1b321da303ef8 100644 (file)
@@ -4,6 +4,7 @@ import os
 import datetime
 import logging
 import shlex
+import shutil
 
 from wizard import app, deploy, install, scripts, shell, util
 from wizard.app import php
@@ -89,9 +90,6 @@ class Application(deploy.Application):
             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())
@@ -101,17 +99,32 @@ class Application(deploy.Application):
             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
diff --git a/wizard/command/restore.py b/wizard/command/restore.py
new file mode 100644 (file)
index 0000000..152ce19
--- /dev/null
@@ -0,0 +1,56 @@
+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
+
index 1403d2cde37a60c0bf81e516ad30b378cb8543f5..0c6fa94f7a2881c322116363a2fba1ad7a59fd6f 100644 (file)
@@ -365,6 +365,13 @@ class Application(object):
         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.
index 9f2d43c7eecdb72b3a726decb6dae02e42055f02..8660d1ade198840e5c15d79ee1abb8fcdf30ff14 100644 (file)
@@ -75,6 +75,9 @@ class Shell(object):
             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.
 
@@ -90,6 +93,9 @@ class Shell(object):
         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)
@@ -110,9 +116,9 @@ class Shell(object):
             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