]> scripts.mit.edu Git - wizard.git/commitdiff
Implement 'wizard backup'. Other minor refactorings:
authorEdward Z. Yang <ezyang@mit.edu>
Sat, 3 Oct 2009 00:04:52 +0000 (20:04 -0400)
committerEdward Z. Yang <ezyang@mit.edu>
Sat, 3 Oct 2009 00:04:52 +0000 (20:04 -0400)
* Moved SQL credential detection to new wizard.scripts module
* Made error messages for wizard install a little better
* Make help message not as long for environment variables
* Move safe_unlink to wizard.util

Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
bin/wizard
wizard/app/__init__.py
wizard/app/mediawiki.py
wizard/command/__init__.py
wizard/command/backup.py [new file with mode: 0644]
wizard/command/migrate.py
wizard/deploy.py
wizard/install.py
wizard/scripts.py [new file with mode: 0644]
wizard/util.py

index 5fe8b7ca10b1cf9f24535481ff415c3db3f3737d..7889fb47635f1558b97953373bd128ccb19ab041 100755 (executable)
@@ -15,6 +15,7 @@ def main():
 Wizard is a Git-based autoinstall management system for scripts.
 
 Its commands are:
+    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
index c729245504c430283d6ae2c4e2c38aa00770c474..4193fd1a113ddf1c2c28cc876905b19649a31378 100644 (file)
@@ -98,3 +98,16 @@ class UpgradeFailure(Error):
 ERROR: Upgrade script failed, details:
 
 %s""" % self.details
+
+class BackupFailure(Error):
+    """Backup script failed."""
+    #: String details of failure
+    details = None
+    def __init__(self, details):
+        self.details = details
+    def __str__(self):
+        return """
+
+ERROR: Backup script failed, details:
+
+%s""" % self.details
index 827c30fbe87d6b2db0c77109dde6bde49dbcd7e6..d77b0c38dad11d50221dd8c87a77d8e0736cb98e 100644 (file)
@@ -1,8 +1,11 @@
 import re
 import distutils.version
 import os
+import datetime
+import logging
+import shlex
 
-from wizard import app, deploy, install, shell, util
+from wizard import app, deploy, install, scripts, shell, util
 from wizard.app import php
 
 def make_filename_regex(var):
@@ -84,4 +87,31 @@ class Application(deploy.Application):
         result = sh.eval("php", "maintenance/update.php", "--quick", log=True)
         if not result.rstrip().split()[-1] == "Done.":
             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())
+        if not os.path.exists(backupdir):
+            os.mkdir(backupdir)
+        if os.path.exists(outdir):
+            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("gzip", "--best", outfile)
+        except shell.CallError as e:
+            raise app.BackupFailure(e.stderr)
 
index 78da5b21a8d1195cd9a21205be2fc9e42a8451f9..b483fe0adae0ffa43109bcf197e90efcee275815 100644 (file)
@@ -142,11 +142,11 @@ class WizardOptionParser(optparse.OptionParser):
         self.add_option("-h", "--help", action="help", help=optparse.SUPPRESS_HELP)
         group = optparse.OptionGroup(self, "Common Options")
         group.add_option("-v", "--verbose", dest="verbose", action="store_true",
-                default=boolish(os.getenv("WIZARD_VERBOSE")), help="Turns on verbose output.  Environment variable is WIZARD_VERBOSE")
+                default=boolish(os.getenv("WIZARD_VERBOSE")), help="Turns on verbose output.  Envvar is WIZARD_VERBOSE")
         group.add_option("--debug", dest="debug", action="store_true",
-                default=boolish(os.getenv("WIZARD_DEBUG")), help="Turns on debugging output.  Environment variable is WIZARD_DEBUG")
+                default=boolish(os.getenv("WIZARD_DEBUG")), help="Turns on debugging output.  Envvar is WIZARD_DEBUG")
         group.add_option("-q", "--quiet", dest="quiet", action="store_true",
-                default=boolish(os.getenv("WIZARD_QUIET")), help="Turns off output to stdout. Environment variable is WIZARD_QUIET")
+                default=boolish(os.getenv("WIZARD_QUIET")), help="Turns off output to stdout. Envvar is WIZARD_QUIET")
         group.add_option("--log-file", dest="log_file", metavar="FILE",
                 default=None, help="Logs verbose output to file")
         self.add_option_group(group)
diff --git a/wizard/command/backup.py b/wizard/command/backup.py
new file mode 100644 (file)
index 0000000..77e81a9
--- /dev/null
@@ -0,0 +1,32 @@
+import logging
+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)
+    if not args:
+        dir = "."
+    else:
+        dir = args[0]
+    shell.drop_priviledges(dir, options.log_file)
+    util.chdir(dir)
+    d = deploy.Deployment(".")
+    d.verify()
+    d.verifyConfigured()
+    d.application.backup(d, options)
+
+def parse_args(argv, baton):
+    usage = """usage: %prog backup [ARGS] [DIR]
+
+Takes a configured autoinstall and performs a backup of
+its data.  This data is stored by default in
+.scripts/backups/x.y.z-yyyy-mm-dd"""
+    parser = command.WizardOptionParser(usage)
+    options, args = parser.parse_all(argv)
+    if len(args) > 1:
+        parser.error("too many arguments")
+    return options, args
+
index 8182285edfb634cf64bcfc8f5f4831fe7d524199..420564ebf2303f82917d0ee6beaee6b4499048d2 100644 (file)
@@ -98,24 +98,13 @@ def perform_force(options):
 
     if has_git:
         logging.warning("Force removing .git directory")
-        if not options.dry_run: backup = safe_unlink(".git")
+        if not options.dry_run: backup = util.safe_unlink(".git")
         logging.info(".git backed up to %s" % backup)
     if has_scripts:
         logging.warning("Force removing .scripts directory")
-        if not options.dry_run: backup = safe_unlink(".scripts")
+        if not options.dry_run: backup = util.safe_unlink(".scripts")
         logging.info(".scripts backed up to %s" % backup)
 
-def safe_unlink(file):
-    """Moves a file to a backup location."""
-    prefix = "%s.bak" % file
-    name = None
-    for i in itertools.count():
-        name = "%s.%d" % (prefix, i)
-        if not os.path.exists(name):
-            break
-    os.rename(file, name)
-    return name
-
 def make_repository(sh, options, repo, tag):
     sh.call("git", "init") # create repository
     # configure our alternates (to save space and make this quick)
index 70cac22a62efb78d8419f61143d19425bc17fb48..1403d2cde37a60c0bf81e516ad30b378cb8543f5 100644 (file)
@@ -347,13 +347,22 @@ class Application(object):
     def install(self, version, options):
         """
         Run for 'wizard configure' (and, by proxy, 'wizard install')
-        to configure an application.
+        to configure an application.  This assumes that the current
+        working directory is a deployment.
         """
         raise NotImplemented
     def upgrade(self, version, options):
         """
         Run for 'wizard upgrade' to upgrade database schemas and other
-        non-versioned data in an application.
+        non-versioned data in an application.  This assumes that
+        the current working directory is the deployment.
+        """
+        raise NotImplemented
+    def backup(self, deployment, options):
+        """
+        Run for 'wizard backup' and upgrades to backup database schemas
+        and other non-versioned data in an application.  This assumes
+        that the current working directory is the deployment.
         """
         raise NotImplemented
     def detectVersion(self, deployment):
index b6d5271e00a713feffa3c4356820d43d0a0f782c..500680d0b806e530b78280f19c6ea7ec69934597 100644 (file)
@@ -65,7 +65,7 @@ import subprocess
 import getpass
 
 import wizard
-from wizard import shell, util
+from wizard import scripts, shell, util
 
 def fetch(options, path, post=None):
     """
@@ -142,9 +142,8 @@ class ScriptsMysqlStrategy(Strategy):
     def execute(self, options):
         """Attempts to create a database using Scripts utilities."""
         sh = shell.Shell()
-        try:
-            triplet = sh.eval("/mit/scripts/sql/bin/get-password").split()
-        except:
+        triplet = scripts.get_sql_credentials()
+        if not triplet:
             raise StrategyFailed
         name = os.path.basename(os.getcwd())
         username = os.getenv('USER')
@@ -329,7 +328,10 @@ class ArgHandler(object):
         for sets in (argsets_strategy, argsets_strategy_with_side_effects):
             for argset in sets:
                 if all_set(argset): continue
-                argset.strategy.execute(options)
+                try:
+                    argset.strategy.execute(options)
+                except StrategyFailed:
+                    pass
                 for arg in argset.args:
                     if getattr(options, arg.name) is None:
                         # XXX: arg.prompt(options)
diff --git a/wizard/scripts.py b/wizard/scripts.py
new file mode 100644 (file)
index 0000000..415be1a
--- /dev/null
@@ -0,0 +1,13 @@
+from wizard import shell
+
+def get_sql_credentials():
+    """
+    Attempts to determine a user's MySQL credentials.  They are
+    returned as a three-tuple (host, user, password).
+    """
+    sh = shell.Shell()
+    try:
+        return sh.eval("/mit/scripts/sql/bin/get-password").split()
+    except CallError:
+        return None
+
index 2c28e61baadc70aab2254c38ed811f4f38c7c11b..93f2a34a49047a88e08e68d4740e5722a4d32ed0 100644 (file)
@@ -13,6 +13,7 @@ import pwd
 import sys
 import socket
 import errno
+import itertools
 
 import wizard
 
@@ -246,6 +247,17 @@ def get_git_footer():
         ,"Wizard-args: %s" % " ".join(sys.argv)
         ])
 
+def safe_unlink(file):
+    """Moves a file/dir to a backup location."""
+    prefix = "%s.bak" % file
+    name = None
+    for i in itertools.count():
+        name = "%s.%d" % (prefix, i)
+        if not os.path.exists(name):
+            break
+    os.rename(file, name)
+    return name
+
 class NoOperatorInfo(wizard.Error):
     """No information could be found about the operator from Kerberos."""
     pass