X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/6027252d83254d8d42ca72da6a6f51700c9cee80..10fea9a7ddab6a654922514b13b135772cc98a01:/wizard/command/migrate.py diff --git a/wizard/command/migrate.py b/wizard/command/migrate.py index ce180c8..d3dce85 100644 --- a/wizard/command/migrate.py +++ b/wizard/command/migrate.py @@ -1,162 +1,185 @@ -import optparse -import sys import os +import os.path import shutil -import logging.handlers -import errno +import logging -from wizard import deploy -from wizard import shell -from wizard.command import _base +from wizard import app, command, deploy, shell, util -class Error(_base.Error): - """Base exception for all exceptions raised by migrate""" - pass +def main(argv, baton): + options, args = parse_args(argv, baton) + dir = os.path.abspath(args[0]) if args else os.getcwd() + shell.drop_priviledges(dir, options.log_file) + util.chdir(dir) -class AlreadyMigratedError(Error): - def __init__(self, dir): - self.dir = dir - def __str__(self): - return """ + sh = shell.Shell(options.dry_run) -ERROR: Directory already contains a .git and/or -.scripts directory. Did you already migrate it? -""" + logging.info("Migrating %s" % dir) + logging.debug("uid is %d" % os.getuid()) -class NotAutoinstallError(Error): - def __init__(self, dir): - self.dir = dir - def __str__(self): - return """ + deployment = deploy.ProductionCopy(".") -ERROR: Could not find .scripts-version file. Are you sure -this is an autoinstalled application? -""" + os.unsetenv("GIT_DIR") # prevent some perverse errors -class NoRepositoryError(Error): - def __init__(self, app): - self.app = app - def __str__(self): - return """ + try: + deployment.verify() + raise AlreadyMigratedError(deployment.location) + except deploy.NotAutoinstallError: + # Previously, this was a fatal error, but now let's try + # a little harder. + # XXX: The user still has to tell us what application ; a more + # user friendly thing to do is figure it out automatically + if not options.force_app: + raise + # actual version number will get overwritten shortly + deployment.setAppVersion(app.ApplicationVersion.make(options.force_app, "unknown")) + except deploy.NotMigratedError: + # LEGACY + pass + except (deploy.CorruptedAutoinstallError, AlreadyMigratedError): + if not options.force: + raise -ERROR: Could not find repository for this application. Have -you converted the repository over? Is the name %s -the same as the name of the .git folder? -""" % self.app + if options.force_version: + deployment.setAppVersion(deployment.application.makeVersion(options.force_version)) + else: + try: + deployment.verifyVersion() + except deploy.VersionMismatchError as e: + # well, we'll use that then + deployment.setAppVersion(deployment.application.makeVersion(str(e.real_version))) -class NoTagError(Error): - def __init__(self, version): - self.version = version - def __str__(self): - return """ + deployment.verifyTag(options.srv_path) + + repo = deployment.application.repository(options.srv_path) + tag = deployment.app_version.wizard_tag + try: + sh.call("git", "--git-dir=%s" % repo, "rev-parse", tag) + except shell.CallError: + raise UnsupportedVersion(deployment.version) -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) + with util.LockDirectory(".wizard-migrate-lock"): + try: + if options.force: + perform_force(options) + make_repository(sh, options, repo, tag) + check_variables(deployment, options) + except KeyboardInterrupt: + # revert it; barring zany race conditions this is safe + if os.path.exists(".wizard"): + shutil.rmtree(".wizard") + if os.path.exists(".git"): + shutil.rmtree(".git") -def main(argv, global_options, logger = None): +def parse_args(argv, baton): usage = """usage: %prog migrate [ARGS] DIR Migrates a directory to our Git-based autoinstall format. Performs basic sanity checking and intelligently determines -what repository and tag to use.""" - parser = _base.WizardOptionParser(usage) +what repository and tag to use. + +This command is meant to be run as the owner of the install it is +upgrading . 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", - default=False, help="If .git or .scripts directory already exists, delete them and migrate") - options, args, logger = parser.parse_all(argv, logger) + default=False, help="If .git or .wizard directory already exists, " + "delete them and migrate") + parser.add_option("--force-version", dest="force_version", + default=None, help="If .scripts-version is corrupted or non-existent, explicitly specify " + "a version to migrate to.") + parser.add_option("--force-app", dest="force_app", + default=None, help="If .scripts-version is corrupted or non-existent, explicitly specify " + "an application to migrate to.") + options, args = parser.parse_all(argv) if len(args) > 1: parser.error("too many arguments") - elif not args: - parser.error("must specify directory") - logger.debug("uid is %d" % os.getuid()) - dir = args[0] - try: - os.chdir(dir) - except OSError as e: - if e.errno == errno.EACCES: - raise _base.PermissionsError(dir) - elif e.errno == errno.ENOENT: - raise _base.NoSuchDirectoryError(dir) - else: raise e - if os.path.isdir(".git") or os.path.isdir(".scripts"): - if not options.force: - raise AlreadyMigratedError(dir) - else: - if os.path.isdir(".git"): - logger.warning("Force removing .git directory") - if not options.dry_run: shutil.rmtree(".git") - if os.path.isdir(".scripts"): - logger.warning("Force removing .scripts directory") - if not options.dry_run: shutil.rmtree(".scripts") - try: - d = deploy.Deployment.fromDir(".") - version = d.getAppVersion() - except IOError as e: - if e.errno == errno.ENOENT: - raise NotAutoinstallError(dir) - else: raise e - # calculate the repository we'll be pulling out of - application = version.application - app = application.name - repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/wizard/srv", app + ".git") - if not os.path.isdir(repo): - raise NoRepositoryError(app) - # begin the command line process - sh = shell.Shell(logger, options.dry_run) - # check if the version we're trying to convert exists. We assume - # a convention here, namely, v1.2.3-scripts is what we want. If - # you broke the convention... shame on you. - try: - tag = "v%s-scripts" % version.version - sh.call("git", "--git-dir", repo, "rev-parse", tag) - except shell.CallError: - raise NoTagError(version) - did_git_init = False - did_git_checkout_scripts = False + return (options, args) + +def perform_force(options): + has_git = os.path.isdir(".git") + has_wizard = os.path.isdir(".wizard") + + if has_git: + logging.warning("Force removing .git directory") + if not options.dry_run: backup = util.safe_unlink(".git") + logging.info(".git backed up to %s" % backup) + if has_wizard: + logging.warning("Force removing .wizard directory") + if not options.dry_run: backup = util.safe_unlink(".wizard") + logging.info(".wizard backed up to %s" % backup) + +def make_repository(sh, options, repo, tag): + sh.call("git", "init") # create repository + # configure our alternates (to save space and make this quick) + data = os.path.join(repo, "objects") + file = ".git/objects/info/alternates" + if not options.dry_run: + alternates = open(file, "w") + alternates.write(data) + alternates.close() + htaccess = open(".git/.htaccess", "w") + htaccess.write("Deny from all\n") + htaccess.close() + else: + logging.info("# create %s containing \"%s\"" % (file, data)) + logging.info('# create .htaccess containing "Deny from all"') + # configure our remote (this is merely for convenience; wizard + # will not rely on this) + sh.call("git", "remote", "add", "origin", repo) + # configure what would normally be set up on a 'git clone' for consistency + sh.call("git", "config", "branch.master.remote", "origin") + sh.call("git", "config", "branch.master.merge", "refs/heads/master") + # perform the initial fetch + sh.call("git", "fetch", "origin") + # soft reset to our tag + sh.call("git", "reset", tag, "--") + # initialize the .wizard directory + util.init_wizard_dir() + logging.info("Diffstat:\n" + sh.eval("git", "diff", "--stat")) + # commit user local changes + message = "Autoinstall migration.\n\n%s" % util.get_git_footer() + util.set_git_env() try: - # create repository - sh.call("git", "--git-dir=.git", "init") - did_git_init = True - # configure our alternates (to save space and make this quick) - data = os.path.join(repo, "objects") - file = ".git/objects/info/alternates" - if not options.dry_run: - alternates = open(file, "w") - alternates.write(data) - alternates.close() + message += "\nMigrated-by: " + util.get_operator_git() + except util.NoOperatorInfo: + pass + sh.call("git", "commit", "--allow-empty", "-a", "-m", message) + +def check_variables(d, options): + """Attempt to extract variables and complain if some are missing.""" + variables = d.extract() + for k,v in variables.items(): + if v is None and k not in d.application.deprecated_keys: + logging.warning("Variable %s not found" % k) else: - logger.info("# create %s containing \"%s\"" % (file, data)) - # configure our remote - sh.call("git", "remote", "add", "origin", repo) - # configure what would normally be set up on a 'git clone' for consistency - sh.call("git", "config", "branch.master.remote", "origin") - sh.call("git", "config", "branch.master.merge", "refs/heads/master") - # perform the initial fetch - sh.call("git", "fetch", "origin") - # soft reset to our tag - sh.call("git", "reset", tag, "--") - # checkout the .scripts directory - sh.call("git", "checkout", ".scripts") - did_git_checkout_scripts = True - # XXX: setup .scripts/version??? - # for verbose purposes, give us a git status and git diff - if options.verbose: - try: - sh.call("git", "status") - except shell.CallError: - pass - try: - sh.call("git", "diff") - except shell.CallError: - pass - except: - # this... is pretty bad - logger.critical("ERROR: Exception detected! Rolling back...") - if did_git_init: - sh.call("rm", "-Rf", ".git") - if did_git_checkout_scripts: - sh.call("rm", "-Rf", ".scripts") - raise + logging.debug("Variable %s is %s" % (k,v)) + +class Error(command.Error): + """Base exception for all exceptions raised by migrate""" + pass + +class AlreadyMigratedError(Error): + quiet = True + def __init__(self, dir): + self.dir = dir + def __str__(self): + return """ + +This autoinstall is already migrated; move along, nothing to +see here. (If you really want to, you can force a re-migration +with --force, but this will blow away the existing .git and +.scripts directories (i.e. your history and Wizard configuration).) +""" + +class UnsupportedVersion(Error): + def __init__(self, version): + self.version = version + def __str__(self): + return """ + +ERROR: This autoinstall is presently on %s, which is unsupported by +Wizard. Please manually upgrade it to one that is supported, +and then retry the migration; usually the latest version is supported. +""" % self.version