]> scripts.mit.edu Git - wizard.git/commitdiff
Merge branch 'master' of /mit/ezyang/wizard
authorEdward Z. Yang <ezyang@mit.edu>
Fri, 21 Aug 2009 06:13:46 +0000 (02:13 -0400)
committerEdward Z. Yang <ezyang@mit.edu>
Fri, 21 Aug 2009 06:13:46 +0000 (02:13 -0400)
Conflicts:
wizard/command/migrate.py
wizard/command/upgrade.py

Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
TODO
tests/test-continue-upgrade-mediawiki.sh
tests/test-upgrade-mediawiki.sh
wizard/command/__init__.py
wizard/command/migrate.py
wizard/command/upgrade.py
wizard/deploy.py
wizard/shell.py
wizard/util.py

diff --git a/TODO b/TODO
index 547b5b1985f82fbae725412420eaf239900ba47d..2f44e559c86b6fb996cc32271446da15c741e62b 100644 (file)
--- a/TODO
+++ b/TODO
@@ -12,7 +12,8 @@ TODO NOW:
 
 - Make parallel-find.pl use `sudo -u username git describe --tags`
   to determine version.  Make parallel-find.pl have this have greater
-  precedence. (Have patch, pending testing and commit)
+  precedence.  This also means, however, that we get
+  full mediawiki-1.2.3-2-abcdef names (Have patch, pending testing and commit)
 - Make the installer use 'wizard install' /or/ do a migration
   after doing a normal install (the latter makes it easier
   for mass-rollbacks).
index 741132533087953ba5fc51b860d4b344880a637d..0afd054ed03ea3bdad90097f7bfe83d320fc4ec2 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash -e
 
 TESTNAME="continue_upgrade_mediawiki"
-source setup
+source ./setup
 
 wizard install mediawiki-$VERSION-scripts "$TESTDIR" -- --title="TestApp"
 
index 5eba8ca1949c264fcbb331bffde2587fbb07a706..2dd38fcbb01e4c8383b4512faf499c904ca3f1c7 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash -e
 
 TESTNAME="upgrade_mediawiki"
-source setup
+source ./setup
 
 wizard install mediawiki-$VERSION-scripts "$TESTDIR" -- --title="TestApp"
 wizard upgrade "$TESTDIR"
index 505142009796322c387d3f4640b9f6e65b335ef5..eeb5336a236dcaffa999bf3f8867b2a08cfab99b 100644 (file)
@@ -9,33 +9,6 @@ import wizard
 
 logging_setup = False
 
-class Error(wizard.Error):
-    """Base error class for all command errors"""
-    pass
-
-class PermissionsError(Error):
-    def __init__(self, dir):
-        self.dir = dir
-    def __str__(self):
-        return """
-
-ERROR: You don't have permissions to access this directory.
-Do you have tokens for AFS with your root instance, and
-is your root instance on scripts-security-upd?
-
-You can check by running the commands 'klist' and
-'blanche scripts-security-upd'.
-"""
-
-class NoSuchDirectoryError(Error):
-    def __init__(self, dir):
-        self.dir = dir
-    def __str__(self):
-        return """
-
-ERROR: No such directory... check your typing
-"""
-
 def boolish(val):
     """
     Parse the contents of an environment variable as a boolean.
@@ -55,16 +28,6 @@ def boolish(val):
             return False
         return bool(val)
 
-def chdir(dir):
-    try:
-        os.chdir(dir)
-    except OSError as e:
-        if e.errno == errno.EACCES:
-            raise PermissionsError(dir)
-        elif e.errno == errno.ENOENT:
-            raise NoSuchDirectoryError(dir)
-        else: raise e
-
 def makeLogger(options, numeric_args):
     global logging_setup
     if logging_setup: return logging.getLogger()
@@ -151,3 +114,7 @@ class OptionBaton(object):
         """Hands off parameters to option parser"""
         for key in args:
             option_parser.add_option(self.store[key])
+
+class Error(wizard.Error):
+    """Base error class for all command errors"""
+    pass
index 10de6d0841e130b67e79283cd5023489a2bee486..22cb38e99169d8af15de27622436815fe38b7960 100644 (file)
@@ -10,28 +10,39 @@ from wizard import command, deploy, shell, util
 def main(argv, baton):
     options, args = parse_args(argv, baton)
     dir = args[0]
-    command.chdir(dir)
 
-    shell.drop_priviledges(options)
+    shell.drop_priviledges(dir, options)
+
+    util.chdir(dir)
     sh = shell.Shell(options.dry_run)
 
     logging.info("Migrating %s" % dir)
     logging.debug("uid is %d" % os.getuid())
 
-    deployment = make_deployment() # uses chdir
+    deployment = deploy.Deployment(".")
+
+    # deal with old-style migration, remove this later
+    if os.path.isfile(".scripts/old-version") and not os.path.isfile(".scripts-version"):
+        os.rename(".scripts/old-version", ".scripts-version")
+
+    os.unsetenv("GIT_DIR") # prevent some perverse errors
+
+    try:
+        deployment.verify()
+        raise AlreadyMigratedError(deployment.location)
+    except deploy.NotMigratedError:
+        pass
+    except (deploy.CorruptedAutoinstallError, AlreadyMigratedError):
+        if options.force:
+            perform_force(options)
+        else:
+            raise
 
-    # turn this into some kind of assert
-    if not deployment.configured:
-        raise NotConfiguredError(deployment.location)
+    deployment.verifyTag(options.srv_path)
 
     version = deployment.app_version
     repo    = version.application.repository(options.srv_path)
     tag     = version.scripts_tag
-    check_if_tag_exists(sh, repo, tag, version)
-
-    check_if_already_migrated(options)
-
-    os.unsetenv("GIT_DIR") # prevent some perverse errors
 
     # XXX: turn this into a context
     try:
@@ -75,10 +86,11 @@ NOT run this command as root."""
         parser.error("must specify directory")
     return (options, args)
 
-def check_if_already_migrated(options):
+def perform_force(options):
     has_git = os.path.isdir(".git")
     has_scripts = os.path.isdir(".scripts")
 
+<<<<<<< HEAD
     # deal with old-style migration
     if os.path.isfile(".scripts/old-version") and not os.path.isfile(".scripts-version"):
         os.rename(".scripts/old-version", ".scripts-version")
@@ -126,6 +138,27 @@ def check_if_tag_exists(sh, repo, tag, version):
         sh.call("git", "--git-dir", repo, "rev-parse", tag)
     except shell.CallError:
         raise NoTagError(version)
+=======
+    if has_git:
+        logging.warning("Force removing .git directory")
+        if not options.dry_run: backup = 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")
+        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
+>>>>>>> 255245146b500845fa13a75bfbeb6cece074417c
 
 def make_repository(sh, options, repo, tag):
     sh.call("git", "init") # create repository
@@ -188,49 +221,6 @@ ERROR: Directory already contains a .git and
 both of these directories will be removed.
 """
 
-class AlreadyVersionedError(Error):
-    def __init__(self, dir):
-        self.dir = dir
-    def __str__(self):
-        return """
-
-ERROR: Directory contains a .git directory, but not
-a .scripts directory.  If this is not a corrupt
-migration, this means that the user was versioning their
-install using Git.  You cannot force this case.
-"""
-
-class NotConfiguredError(Error):
-    def __init__(self, dir):
-        self.dir = dir
-    def __str__(self):
-        return """
-
-ERROR: The install was well-formed, but not configured
-(essential configuration files were not found.)
-"""
-
-class CorruptedAutoinstallError(Error):
-    def __init__(self, dir):
-        self.dir = dir
-    def __str__(self):
-        return """
-
-ERROR: Directory contains a .scripts directory,
-but not a .git directory.  If you force this migration,
-the .scripts directory will be removed.
-"""
-
-class NotAutoinstallError(Error):
-    def __init__(self, dir):
-        self.dir = dir
-    def __str__(self):
-        return """
-
-ERROR: Could not find .scripts-version file. Are you sure
-this is an autoinstalled application?
-"""
-
 class DirectoryLockedError(Error):
     def __init__(self, dir):
         self.dir = dir
@@ -241,13 +231,3 @@ ERROR: Could not acquire lock on directory.  Maybe there is
 another migration process running?
 """
 
-class NoTagError(Error):
-    def __init__(self, version):
-        self.version = version
-    def __str__(self):
-        return """
-
-ERROR: Could not find tag v%s-scripts in repository
-for %s.  Double check and make sure
-the repository was prepared with all necessary tags!
-""" % (self.version.version, self.version.application.name)
index f4acaf7856dea58a6ba686e283483eb4657fbe32..1867d51affb316b89689f4f00ecf0f2b872e2bec 100644 (file)
@@ -9,15 +9,10 @@ import itertools
 
 from wizard import command, deploy, shell, util
 
-# XXX: need errors for history sanity checking (if the user is on a completely
-# different history tree, we should abort, and manually figure out the
-# appropriate rebase)
-# $(git merge-base P C) == $(git rev-parse P)
-
 def main(argv, baton):
     options, args = parse_args(argv, baton)
     if args:
-        command.chdir(args[0])
+        util.chdir(args[0])
     sh = shell.Shell()
     util.set_git_env()
     if options.continue_:
@@ -25,12 +20,18 @@ def main(argv, baton):
         user_commit, next_commit = open(".git/WIZARD_PARENTS", "r").read().split()
         repo = open(".git/WIZARD_REPO", "r").read()
         version = open(".git/WIZARD_UPGRADE_VERSION", "r").read()
-        command.chdir(sh.eval("git", "config", "remote.origin.url"))
-        d = make_deployment_from_cwd()
+        util.chdir(sh.eval("git", "config", "remote.origin.url"))
+        d = deploy.Deployment(".")
     else:
-        d = make_deployment_from_cwd()
+        d = deploy.Deployment(".")
+        d.verify()
+        d.verifyTag(options.srv_path)
+        d.verifyGit(options.srv_path)
+        d.verifyConfigured()
         repo = d.application.repository(options.srv_path)
         version = calculate_newest_version(sh, repo)
+        if version == d.app_version.scripts_tag:
+            raise AlreadyUpgraded
         if not options.dry_run:
             perform_pre_commit(sh)
         temp_wc_dir = perform_tmp_clone(sh)
@@ -77,17 +78,6 @@ def main(argv, baton):
     #       process might have frobbed it.  Don't be
     #       particularly worried if the segment dissappeared
 
-def make_deployment_from_cwd():
-    if not os.path.isdir(".git"):
-        raise NotAutoinstallError()
-    try:
-        d = deploy.Deployment(".")
-    except IOError as e:
-        if e.errno == errno.ENOENT:
-            raise NotAutoinstallError()
-        else: raise e
-    return d
-
 def make_commit_message(version):
     message = "Upgraded autoinstall in %s to %s.\n\n%s" % (util.get_dir_owner(), version, util.get_git_footer())
     try:
@@ -178,13 +168,11 @@ class Error(command.Error):
     """Base exception for all exceptions raised by upgrade"""
     pass
 
-class NotAutoinstallError(Error):
+class AlreadyUpgraded(Error):
     def __str__(self):
         return """
 
-ERROR: Could not find .git file. Are you sure
-this is an autoinstalled application? Did you remember
-to migrate it?"""
+ERROR: This autoinstall is already at the latest version."""
 
 class MergeFailed(Error):
     def __str__(self):
index 1b10204fa0190001e0046d4514123b3141176883..f6292b9d091cc68f66a033b39c1559fb128f3608 100644 (file)
@@ -32,7 +32,7 @@ def parse_install_lines(show, versions_store, yield_errors = False):
     a :class:`wizard.deploy.Error` if ``yield_errors`` is ``True``.  You can
     filter out applications and versions by specifying ``app``
     or ``app-1.2.3`` in ``show``.  This function may generate
-    log output.
+    log output.  Make sure that ``show`` is a list and not a string.
     """
     if not show: show = applications()
     show = frozenset(show)
@@ -111,6 +111,66 @@ class Deployment(object):
         Checks if the application is configured.
         """
         raise NotImplemented
+
+    def verify(self):
+        """
+        Checks if this is an autoinstall, throws an exception if there
+        are problems.
+        """
+        with util.ChangeDirectory(self.location):
+            has_git = os.path.isdir(".git")
+            has_scripts = os.path.isdir(".scripts")
+            if not has_git and has_scripts:
+                raise CorruptedAutoinstallError(self.location)
+            elif has_git and not has_scripts:
+                raise AlreadyVersionedError(self.location)
+            elif not has_git and not has_scripts:
+                if os.path.isfile(".scripts-version"):
+                    raise NotMigratedError(self.location)
+
+    def verifyTag(self, srv_path):
+        """
+        Checks if the purported version has a corresponding tag
+        in the upstream repository.
+        """
+        repo = self.application.repository(srv_path)
+        try:
+            shell.Shell().eval("git", "--git-dir", repo, "rev-parse", self.app_version.scripts_tag)
+        except shell.CallError:
+            raise NoTagError(self.app_version.scripts_tag)
+
+    def verifyGit(self, srv_path):
+        """
+        Checks if the autoinstall's Git repository makes sense,
+        checking if the tag is parseable and corresponds to
+        a real application, and if the tag in this repository
+        corresponds to the one in the remote repository.
+        """
+        with util.ChangeDirectory(self.location):
+            sh = shell.Shell()
+            repo = self.application.repository(srv_path)
+            def repo_rev_parse(tag):
+                return sh.eval("git", "--git-dir", repo, "rev-parse", tag)
+            def self_rev_parse(tag):
+                return sh.safeCall("git", "rev-parse", tag, strip=True)
+            def compare_tags(tag):
+                return repo_rev_parse(tag) == self_rev_parse(tag)
+            if not compare_tags(self.app_version.pristine_tag):
+                raise InconsistentPristineTagError()
+            if not compare_tags(self.app_version.scripts_tag):
+                raise InconsistentScriptsTagError()
+            parent = repo_rev_parse(self.app_version.scripts_tag)
+            merge_base = sh.safeCall("git", "merge-base", parent, "HEAD", strip=True)
+            if merge_base != parent:
+                raise HeadNotDescendantError()
+
+    def verifyConfigured(self):
+        """
+        Checks if the autoinstall is configured running.
+        """
+        if not self.configured:
+            raise NotConfiguredError(self.location)
+
     @property
     def configured(self):
         """Whether or not an autoinstall has been configured/installed for use."""
@@ -214,8 +274,6 @@ class Application(object):
         """
         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(srv_path, self.name + ".git")
         if not os.path.isdir(repo):
@@ -246,17 +304,14 @@ class Application(object):
         for file in self.parametrized_files:
             fullpath = os.path.join(dir, file)
             try:
-                f = open(fullpath, "r")
+                contents = open(fullpath, "r").read()
             except IOError:
                 continue
-            contents = f.read()
-            f.close()
             for key, value in variables.items():
                 if value is None: continue
                 contents = contents.replace(key, value)
             tmp = tempfile.NamedTemporaryFile(delete=False)
             tmp.write(contents)
-            tmp.close()
             os.rename(tmp.name, fullpath)
     def prepareConfig(self, deployment):
         """
@@ -321,6 +376,13 @@ class ApplicationVersion(object):
         self.version = version
         self.application = application
     @property
+    def tag(self):
+        """
+        Returns the name of the git describe tag for the commit the user is
+        presently on, something like mediawiki-1.2.3-scripts-4-g123abcd
+        """
+        return "%s-%s" % (self.application, self.version)
+    @property
     def scripts_tag(self):
         """
         Returns the name of the Git tag for this version.
@@ -424,8 +486,62 @@ class NoRepositoryError(Error):
     def __str__(self):
         return """Could not find Git repository for '%s'.  If you would like to use a local version, try specifying --srv-path or WIZARD_SRV_PATH.""" % self.app
 
-# If you want, you can wrap this up into a registry and access things
-# through that, but it's not really necessary
+class NotMigratedError(Error):
+    """
+    The deployment contains a .scripts-version file, but no .git
+    or .scripts directory.
+    """
+    def __init__(self, dir):
+        self.dir = dir
+    def __str__(self):
+        return """This installation was not migrated"""
+
+class AlreadyVersionedError(Error):
+    def __init__(self, dir):
+        self.dir = dir
+    def __str__(self):
+        return """
+
+ERROR: Directory contains a .git directory, but not
+a .scripts directory.  If this is not a corrupt
+migration, this means that the user was versioning their
+install using Git."""
+
+class NotConfiguredError(Error):
+    def __init__(self, dir):
+        self.dir = dir
+    def __str__(self):
+        return """
+
+ERROR: The install was well-formed, but not configured
+(essential configuration files were not found.)"""
+
+class CorruptedAutoinstallError(Error):
+    def __init__(self, dir):
+        self.dir = dir
+    def __str__(self):
+        return """
+
+ERROR: Directory contains a .scripts directory,
+but not a .git directory."""
+
+class NotAutoinstallError(Error):
+    def __init__(self, dir):
+        self.dir = dir
+    def __str__(self):
+        return """
+
+ERROR: Could not find .scripts-version file. Are you sure
+this is an autoinstalled application?
+"""
+
+class NoTagError(Error):
+    def __init__(self, tag):
+        self.tag = tag
+    def __str__(self):
+        return """
+
+ERROR: Could not find tag %s in repository.""" % self.tag
 
 _application_list = [
     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
index 794811fe6d1042b82645416821b6ced1b9b9ad91..c66147a0f9d876fe9fe10b2249d4cfd7f6771a49 100644 (file)
@@ -25,16 +25,16 @@ def is_python(args):
     """Detects whether or not an argument list invokes a Python program."""
     return args[0] == "python" or args[0] == "wizard"
 
-def drop_priviledges(options):
+def drop_priviledges(dir, options):
     """
     Checks if we are running as root.  If we are, attempt to drop
-    priviledges to the user who owns the current directory, by re-calling
+    priviledges to the user who owns ``dir``, by re-calling
     itself using sudo with exec, such that the new process subsumes our
     current one.
     """
     if os.getuid():
         return
-    uid = util.get_dir_uid('.')
+    uid = util.get_dir_uid(dir)
     if not uid:
         return
     args = []
index 4c5b7c725a8c69c6618c7f259cec552ccfbe1920..2c28e61baadc70aab2254c38ed811f4f38c7c11b 100644 (file)
@@ -12,6 +12,7 @@ import subprocess
 import pwd
 import sys
 import socket
+import errno
 
 import wizard
 
@@ -28,9 +29,9 @@ class ChangeDirectory(object):
         self.olddir = None
     def __enter__(self):
         self.olddir = os.getcwd()
-        os.chdir(self.dir)
+        chdir(self.dir)
     def __exit__(self, *args):
-        os.chdir(self.olddir)
+        chdir(self.olddir)
 
 class Counter(object):
     """
@@ -68,6 +69,20 @@ class PipeToLess(object):
             self.proc.wait()
             sys.stdout = self.old_stdout
 
+def chdir(dir):
+    """
+    Changes a directory, but has special exceptions for certain
+    classes of errors.
+    """
+    try:
+        os.chdir(dir)
+    except OSError as e:
+        if e.errno == errno.EACCES:
+            raise PermissionsError()
+        elif e.errno == errno.ENOENT:
+            raise NoSuchDirectoryError()
+        else: raise e
+
 def dictmap(f, d):
     """
     A map function for dictionaries.  Only changes values.
@@ -235,3 +250,8 @@ class NoOperatorInfo(wizard.Error):
     """No information could be found about the operator from Kerberos."""
     pass
 
+class PermissionsError(IOError):
+    errno = errno.EACCES
+
+class NoSuchDirectoryError(IOError):
+    errno = errno.ENOENT