]> scripts.mit.edu Git - wizard.git/commitdiff
Implement test suite for Wizard; includes numerous API changes.
authorEdward Z. Yang <ezyang@mit.edu>
Wed, 12 Aug 2009 22:45:20 +0000 (18:45 -0400)
committerEdward Z. Yang <ezyang@mit.edu>
Thu, 13 Aug 2009 00:16:29 +0000 (20:16 -0400)
* Added environment variables for all installation arguments.
* Added --srv-path option, for specifying local repositories.
* Added environment variables for some common command options,
  namely WIZARD_SRV_PATH, WIZARD_VERBOSE and WIZARD_DEBUG.
* API change: 'wizard install --app APP DIR' to 'wizard
  install APP DIR'
* Changed Application.repository to be a method that takes
  srv_path as an argument (since this can now vary).  Also
  look for "$APP/.git" if "$APP.git" doesn't exist.
* Added AppVersion.pristine_tag
* Change Deployment.app_version algorithm to use
  'git describe' over .scripts/version
* Miscellaneous typo fixes

Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
bin/wizard
tests/.gitignore [new file with mode: 0644]
tests/setup [new file with mode: 0644]
tests/test-upgrade-mediawiki-1.15.0.sh [new file with mode: 0755]
wizard/command/__init__.py
wizard/command/install.py
wizard/command/massmigrate.py
wizard/command/migrate.py
wizard/command/upgrade.py
wizard/deploy.py
wizard/install.py

index 7169496aebf63059858f98463e0052e32420c209..3f2771d7e1d65e7a18a0843888f9f660eff7365c 100755 (executable)
@@ -33,8 +33,11 @@ See '%prog help COMMAND' for more information on a specific command."""
     rest_argv = args[1:]
     baton = command.OptionBaton()
     baton.add("--versions-path", dest="versions_path",
-        default="/afs/athena.mit.edu/contrib/scripts/sec-tools/store/versions",
-        help="Location of parallel-find output directory, or a file containing a newline separated list of 'all autoinstalls' (for testing).")
+        default=os.getenv("WIZARD_VERSIONS_PATH") or "/afs/athena.mit.edu/contrib/scripts/sec-tools/store/versions",
+        help="Location of parallel-find output directory, or a file containing a newline separated list of 'all autoinstalls' (for development work).  Environment variable is WIZARD_VERSIONS_PATH.")
+    baton.add("--srv-path", dest="srv_path",
+        default=os.getenv("WIZARD_SRV_PATH") or "/afs/athena.mit.edu/contrib/scripts/git/autoinstalls",
+        help="Location of autoinstall Git repositories, such that $REPO_PATH/$APP.git is a repository (for development work).  Environment variable is WIZARD_SRV_PATH.")
     try:
         command_name = args[0]
     except IndexError:
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644 (file)
index 0000000..8f53440
--- /dev/null
@@ -0,0 +1,2 @@
+config
+testdir*
diff --git a/tests/setup b/tests/setup
new file mode 100644 (file)
index 0000000..e5a39c6
--- /dev/null
@@ -0,0 +1,8 @@
+# this file is meant to be source'd by any test script
+
+export WIZARD_ADMIN_NAME="admin"
+export WIZARD_ADMIN_PASSWORD="wizard"
+
+if [ -e "config" ]; then
+    source config
+fi
diff --git a/tests/test-upgrade-mediawiki-1.15.0.sh b/tests/test-upgrade-mediawiki-1.15.0.sh
new file mode 100755 (executable)
index 0000000..9c8ce7a
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/bash -e
+
+TMPNAME="testdir-upgrade-mediawiki-1.15.0"
+APPNAME="mediawiki"
+source setup
+
+if [ -e "$TMPNAME" ]; then
+    echo "Removing previous $TMPNAME folder..."
+    rm -Rf "$TMPNAME"
+fi
+
+wizard install mediawiki-1.15.0-scripts "$TMPNAME" -- --title="TestApp"
+wizard upgrade "$TMPNAME"
index e5cb0b4354f06b3d3c85b2ab9d43c92aa8e16ab8..db4ff2731219690f8cbcbf2ef21f1abbd6f56953 100644 (file)
@@ -35,6 +35,25 @@ class NoSuchDirectoryError(Error):
 ERROR: No such directory... check your typing
 """
 
+def boolish(val):
+    """
+    Parse the contents of an environment variable as a boolean.
+    This recognizes more values as ``False`` than :func:`bool` would.
+
+        >>> boolish("0")
+        False
+        >>> boolish("no")
+        False
+        >>> boolish("1")
+        True
+    """
+    try:
+        return bool(int(val))
+    except (ValueError, TypeError):
+        if val == "No" or val == "no" or val == "false" or val == "False":
+            return False
+        return bool(val)
+
 def chdir(dir):
     try:
         os.chdir(dir)
@@ -112,16 +131,16 @@ 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=False, help="Turns on verbose output")
+                default=boolish(os.getenv("WIZARD_VERBOSE")), help="Turns on verbose output.  Environment variable is WIZARD_VERBOSE")
         group.add_option("--debug", dest="debug", action="store_true",
-                default=False, help="Turns on debugging output")
+                default=boolish("WIZARD_DEBUG"), help="Turns on debugging output.  Environment variable is WIZARD_DEBUG")
         group.add_option("-q", "--quiet", dest="quiet", action="store_true",
                 default=False, help="Turns off output to stdout")
-        group.add_option("--log-file", dest="log_file",
+        group.add_option("--log-file", dest="log_file", metavar="FILE",
                 default=None, help="Logs verbose output to file")
-        group.add_option("--log-file-chmod", dest="log_file_chmod",
+        group.add_option("--log-file-chmod", dest="log_file_chmod", metavar="CHMOD",
                 default=None, help="Chmod the log file after opening.  Number is octal. You must chmod the log file 666 and place the file in /tmp if subprocesses are running as different users.")
-        group.add_option("--indent", dest="indent",
+        group.add_option("--indent", dest="indent", metavar="WIDTH",
                 default=0, help="Indents stdout, useful for nested calls")
         group.add_option("--context", dest="context", action="store_true",
                 default=False, help="Adds context to logs, useful for parallel processing")
index a331e2e42c8c1391cc46d9cf2c185448b28bc853..746463169e40ded3deb0d8ac2bf0b50a53861d3d 100644 (file)
@@ -8,25 +8,27 @@ import wizard
 from wizard import command, deploy, shell, util
 
 def main(argv, baton):
-    options, args = parse_args(argv)
+    options, args = parse_args(argv, baton)
     # XXX: do something smart if -scripts is not at the end
-    dir = args[0]
+    app = args[0]
+    dir = args[1]
     if os.path.exists(dir):
         raise DirectoryExistsError
-    appname, _, version = options.app.partition('-')
-    app = deploy.applications()[appname]
+    appname, _, version = app.partition('-')
+    application = deploy.applications()[appname]
     sh = shell.Shell()
-    sh.call("git", "clone", "--shared", app.repository, dir)
+    sh.call("git", "clone", "--shared", application.repository(options.srv_path), dir)
     with util.ChangeDirectory(dir):
         if version:
-            sh.call("git", "checkout", options.app)
+            sh.call("git", "checkout", app)
         # this command's stdin should be hooked up to ours
         try:
-            sh.call("wizard", "configure", *args[1:], interactive=True)
+            configure_args = args[2:] + command.makeBaseArgs(options)
+            sh.call("wizard", "configure", *configure_args, interactive=True)
         except shell.PythonCallError:
             sys.exit(1)
 
-def parse_args(argv):
+def parse_args(argv, baton):
     usage = """usage: %prog install [APP [DIR -- [SETUPARGS]]]
 
 Autoinstalls the application APP in the directory
@@ -36,13 +38,14 @@ for possible arguments.
 
 WARNING: This command's API may change."""
     parser = command.WizardOptionParser(usage)
-    parser.add_option("--app", dest="app",
-            help="Application to install, optionally specifying a version as APP-VERSION")
+    baton.push(parser, "srv_path")
     options, args = parser.parse_all(argv)
+    # XXX: in the future, not specifying stuff is supported, since
+    # we'll prompt for it interactively
     if not args:
-        # XXX: in the future, not specifying stuff is supported, since
-        # we'll prompt for it interactively
-        parser.error("must specify application")
+        parser.error("must specify application and directory")
+    elif len(args) == 1:
+        parser.error("must specify directory")
     return options, args
 
 class DirectoryExistsError(wizard.Error):
index 6af91cdfe1a345ea2375221d837b377f42cbec89..01a178dd8980e06f1c19f39d4eb61c800659b823 100644 (file)
@@ -83,6 +83,7 @@ untrusted repositories."""
     parser.add_option("--seen", dest="seen",
             default=None, help="File to read/write paths of already processed installs. These will be skipped.")
     baton.push(parser, "versions_path")
+    baton.push(parser, "srv_path")
     options, args, = parser.parse_all(argv)
     if len(args) > 1:
         parser.error("too many arguments")
@@ -93,7 +94,7 @@ untrusted repositories."""
     return options, args
 
 def calculate_base_args(options):
-    base_args = command.makeBaseArgs(options, dry_run="--dry-run")
+    base_args = command.makeBaseArgs(options, dry_run="--dry-run", srv_path="--srv-path")
     if not options.debug:
         base_args.append("--quiet")
     return base_args
index f33289114f868c1abc5e05039c3092bd0e9a5240..7979a6c831fbb2ed866e9a04f7ea4c09e354bbfd 100644 (file)
@@ -7,7 +7,7 @@ import sys
 from wizard import command, deploy, shell, util
 
 def main(argv, baton):
-    options, args = parse_args(argv)
+    options, args = parse_args(argv, baton)
     dir = args[0]
 
     logging.debug("uid is %d" % os.getuid())
@@ -17,7 +17,7 @@ def main(argv, baton):
 
     deployment = make_deployment() # uses chdir
     version = deployment.app_version
-    repo    = version.application.repository
+    repo    = version.application.repository(options.srv_path)
     tag     = version.scripts_tag
 
     os.unsetenv("GIT_DIR") # prevent some perverse errors
@@ -45,6 +45,7 @@ This command is meant to be run as the owner of the install
 it is upgrading (see the scripts AFS kernel patch).  Do
 NOT run this command as root."""
     parser = command.WizardOptionParser(usage)
+    baton.push(parser, "srv_path")
     parser.add_option("--dry-run", dest="dry_run", action="store_true",
             default=False, help="Prints would would be run without changing anything")
     parser.add_option("--force", "-f", dest="force", action="store_true",
index 6af054c58d8a8fe906807f1eee449f5209e05070..7bf73f5b13b99b3047a1dd559213886eb7e07f20 100644 (file)
@@ -15,7 +15,7 @@ from wizard import command, deploy, shell, util
 # different history tree, stuff is problems)
 
 def main(argv, baton):
-    options, args = parse_args(argv)
+    options, args = parse_args(argv, baton)
     dir = args[0]
     command.chdir(dir)
     if not os.path.isdir(".git"):
@@ -26,7 +26,7 @@ def main(argv, baton):
         if e.errno == errno.ENOENT:
             raise NotAutoinstallError()
         else: raise e
-    repo = d.application.repository
+    repo = d.application.repository(options.srv_path)
     # begin the command line process
     sh = shell.Shell()
     # setup environment
@@ -75,7 +75,7 @@ def main(argv, baton):
                 sh.call("git", "reset", "--hard")
                 return virtual_commit
             user_tree = sh.call("git", "rev-parse", "HEAD^{tree}")[0].rstrip()
-            base_virtual_commit = make_virtual_commit("v" + str(d.version))
+            base_virtual_commit = make_virtual_commit(d.app_version.pristine_tag)
             next_virtual_commit = make_virtual_commit(version, [base_virtual_commit])
             user_virtual_commit = sh.call("git", "commit-tree", user_tree, "-p", base_virtual_commit, input="")[0].rstrip()
             sh.call("git", "checkout", user_virtual_commit, "--")
@@ -86,7 +86,10 @@ def main(argv, baton):
         # Make it possible to resume here
         new_tree = sh.call("git", "rev-parse", "HEAD^{tree}")[0].rstrip()
         final_commit = sh.call("git", "commit-tree", new_tree, "-p", user_commit, "-p", next_commit, input=message)[0].rstrip()
-        sh.call("git", "checkout", "master")
+        try:
+            sh.call("git", "checkout", "-b", "master", "--")
+        except shell.CallError:
+            sh.call("git", "checkout", "master", "--")
         sh.call("git", "reset", "--hard", final_commit)
     # Till now, all of our operations were in a tmp sandbox.
     if options.dry_run:
@@ -115,7 +118,7 @@ def pre_upgrade_commit(sh):
         logging.info("No changes detected")
         pass
 
-def parse_args(argv):
+def parse_args(argv, baton):
     usage = """usage: %prog upgrade [ARGS] DIR
 
 Upgrades an autoinstall to the latest version.  This involves
@@ -125,6 +128,7 @@ WARNING: This is still experimental."""
     parser = command.WizardOptionParser(usage)
     parser.add_option("--dry-run", dest="dry_run", action="store_true",
             default=False, help="Prints would would be run without changing anything")
+    baton.push(parser, "srv_path")
     options, args = parser.parse_all(argv)
     if len(args) > 1:
         parser.error("too many arguments")
index 0c5ef88bea241b9dfc350ace431667d9d0bb45ac..1d16991bf6951c5a4f2ce774b941b4a0225ff733 100644 (file)
@@ -11,7 +11,7 @@ import distutils.version
 import tempfile
 
 import wizard
-from wizard import log
+from wizard import git, log, util
 
 ## -- Global Functions --
 
@@ -169,10 +169,9 @@ class Deployment(object):
     def app_version(self):
         """The :class:`ApplicationVersion` of this deployment."""
         if not self._app_version:
-            if os.path.isfile(self.version_file):
-                fh = open(self.version_file)
-                appname, _, version = fh.read().rstrip().partition('-')
-                fh.close()
+            if os.path.isdir(os.path.join(self.location, ".git")):
+                with util.ChangeDirectory(self.location):
+                    appname, _, version = git.describe().partition('-')
                 self._app_version = ApplicationVersion.make(appname, version)
             else:
                 self._app_version = self.log[-1].version
@@ -215,16 +214,18 @@ class Application(object):
         # cache variables
         self._extractors = {}
         self._parametrizers = {}
-    @property
-    def repository(self):
+    def repository(self, srv_path):
         """
         Returns the Git repository that would contain this application.
+        ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
         Throws :exc:`NoRepositoryError` if the calculated path does not
         exist.
         """
-        repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
+        repo = os.path.join(srv_path, self.name + ".git")
         if not os.path.isdir(repo):
-            raise NoRepositoryError(app)
+            repo = os.path.join(srv_path, self.name, ".git")
+            if not os.path.isdir(repo):
+                raise NoRepositoryError(self.name)
         return repo
     def makeVersion(self, version):
         """
@@ -282,7 +283,7 @@ class ApplicationVersion(object):
     ``application`` is a :class:`Application`."""
     #: The :class:`distutils.version.LooseVersion` of this instance.
     version = None
-    #: The :class:`Appliation` of this instance.
+    #: The :class:`Application` of this instance.
     application = None
     def __init__(self, version, application):
         self.version = version
@@ -297,7 +298,14 @@ class ApplicationVersion(object):
             Use this function only during migration, as it does
             not account for the existence of ``-scripts2``.
         """
-        return "v%s-scripts" % self.version
+        return "%s-scripts" % self.pristine_tag
+    @property
+    def pristine_tag(self):
+        """
+        Returns the name of the Git tag for the pristine version corresponding
+        to this version.
+        """
+        return "%s-%s" % (self.application.name, self.version)
     def __cmp__(x, y):
         return cmp(x.version, y.version)
     @staticmethod
index 7c99957afaf94f1c7e64811c8b2731aa32e580d3..c355601ba48db45f7c645634930e2408391ad9c4 100644 (file)
@@ -11,7 +11,11 @@ script needs in order to finish the installation (i.e., the password
 to the database, or the name of the new application).  Instances
 of :class:`Arg` can be registered to the :class:`ArgHandler`, which
 manages marshalling these objects to whatever object
-is actually managing user input.
+is actually managing user input.  An argument is any valid Python
+variable name, usually categorized using underscores (i.e.
+admin_user); the argument capitalized and with 'WIZARD_' prepended
+to it indicates a corresponding environment variable, i.e.
+'WIZARD_ADMIN_USER'.
 
 Because autoinstallers will often have a number of themed
 arguments (i.e. MySQL credentials) that are applicable across
@@ -45,7 +49,8 @@ the actual installation begins).
 
     Because Wizard is eventually intended for public use,
     some hook mechanism for overloading the default strategies will
-    need to be created.
+    need to be created.  Setting up environment variables may act
+    as a vaguely reasonable workaround in the interim.
 
 .. testsetup:: *
 
@@ -168,6 +173,10 @@ class Arg(object):
     def option(self):
         """Full string of the option."""
         return attr_to_option(self.name)
+    @property
+    def envname(self):
+        """Name of the environment variable containing this arg."""
+        return 'WIZARD_' + self.name.upper()
     def __init__(self, name, password=False, type=None, help="XXX: UNDOCUMENTED"):
         self.name = name
         self.password = password
@@ -229,7 +238,8 @@ class ArgHandler(object):
     """
     Generic controller which takes an argument specification of :class:`Arg`
     and configures either a command line flags parser
-    (:class:`optparse.OptionParser`), an interactive user prompt
+    (:class:`optparse.OptionParser`), environment variables,
+    an interactive user prompt
     (:class:`OptionPrompt`) or possibly a web interface to request
     these arguments appropriately.  This controller also
     handles :class:`ArgSet`, which group related
@@ -282,6 +292,12 @@ class ArgHandler(object):
         argsets_strategy = []
         argsets_strategy_with_side_effects = []
         for argset in self.argsets:
+            # fill in environment variables
+            for arg in argset.args:
+                if getattr(options, arg.name) is None:
+                    val = os.getenv(arg.envname)
+                    if val is not None:
+                        setattr(options, arg.name, val)
             if not argset.strategy:
                 argsets_nostrategy.append(argset)
             elif argset.strategy.side_effects:
@@ -336,4 +352,4 @@ class MissingRequiredParam(Error):
     def __init__(self, arg):
         self.arg = arg
     def __str__(self):
-        return "Missing required parameter %s; try specifying %s" % (self.arg.name, self.arg.option)
+        return "Missing required parameter %s; try specifying option %s or environment variable %s" % (self.arg.name, self.arg.option, self.arg.envname)