]> scripts.mit.edu Git - wizard.git/blob - wizard/command/migrate.py
Allow migration of non old-scripts autoinstalls.
[wizard.git] / wizard / command / migrate.py
1 import os
2 import os.path
3 import shutil
4 import logging
5
6 from wizard import app, command, deploy, shell, util
7
8 def main(argv, baton):
9     options, args = parse_args(argv, baton)
10     dir = os.path.abspath(args[0]) if args else os.getcwd()
11     shell.drop_priviledges(dir, options.log_file)
12     util.chdir(dir)
13
14     sh = shell.Shell(options.dry_run)
15
16     logging.info("Migrating %s" % dir)
17     logging.debug("uid is %d" % os.getuid())
18
19     deployment = deploy.ProductionCopy(".")
20
21     os.unsetenv("GIT_DIR") # prevent some perverse errors
22
23     try:
24         deployment.verify()
25         raise AlreadyMigratedError(deployment.location)
26     except deploy.NotAutoinstallError:
27         # Previously, this was a fatal error, but now let's try
28         # a little harder.
29         # XXX: The user still has to tell us what application ; a more
30         # user friendly thing to do is figure it out automatically
31         if not options.force_app:
32             raise
33         # actual version number will get overwritten shortly
34         deployment.setAppVersion(app.ApplicationVersion.make(options.force_app, "unknown"))
35     except deploy.NotMigratedError:
36         # LEGACY
37         pass
38     except (deploy.CorruptedAutoinstallError, AlreadyMigratedError):
39         if not options.force:
40             raise
41
42     if options.force_version:
43         deployment.setAppVersion(deployment.application.makeVersion(options.force_version))
44     else:
45         try:
46             deployment.verifyVersion()
47         except deploy.VersionMismatchError as e:
48             # well, we'll use that then
49             deployment.setAppVersion(deployment.application.makeVersion(str(e.real_version)))
50
51     deployment.verifyTag(options.srv_path)
52
53     repo = deployment.application.repository(options.srv_path)
54     tag = deployment.app_version.wizard_tag
55     try:
56         sh.call("git", "--git-dir=%s" % repo, "rev-parse", tag)
57     except shell.CallError:
58         raise UnsupportedVersion(deployment.version)
59
60     with util.LockDirectory(".wizard-migrate-lock"):
61         try:
62             if options.force:
63                 perform_force(options)
64             make_repository(sh, options, repo, tag)
65             check_variables(deployment, options)
66         except KeyboardInterrupt:
67             # revert it; barring zany race conditions this is safe
68             if os.path.exists(".wizard"):
69                 shutil.rmtree(".wizard")
70             if os.path.exists(".git"):
71                 shutil.rmtree(".git")
72
73 def parse_args(argv, baton):
74     usage = """usage: %prog migrate [ARGS] DIR
75
76 Migrates a directory to our Git-based autoinstall format.
77 Performs basic sanity checking and intelligently determines
78 what repository and tag to use.
79
80 This command is meant to be run as the owner of the install it is
81 upgrading .  Do NOT run this command as root."""
82     parser = command.WizardOptionParser(usage)
83     baton.push(parser, "srv_path")
84     parser.add_option("--dry-run", dest="dry_run", action="store_true",
85             default=False, help="Prints would would be run without changing anything")
86     parser.add_option("--force", "-f", dest="force", action="store_true",
87             default=False, help="If .git or .wizard directory already exists, "
88             "delete them and migrate")
89     parser.add_option("--force-version", dest="force_version",
90             default=None, help="If .scripts-version is corrupted or non-existent, explicitly specify "
91             "a version to migrate to.")
92     parser.add_option("--force-app", dest="force_app",
93             default=None, help="If .scripts-version is corrupted or non-existent, explicitly specify "
94             "an application to migrate to.")
95     options, args = parser.parse_all(argv)
96     if len(args) > 1:
97         parser.error("too many arguments")
98     return (options, args)
99
100 def perform_force(options):
101     has_git = os.path.isdir(".git")
102     has_wizard = os.path.isdir(".wizard")
103
104     if has_git:
105         logging.warning("Force removing .git directory")
106         if not options.dry_run: backup = util.safe_unlink(".git")
107         logging.info(".git backed up to %s" % backup)
108     if has_wizard:
109         logging.warning("Force removing .wizard directory")
110         if not options.dry_run: backup = util.safe_unlink(".wizard")
111         logging.info(".wizard backed up to %s" % backup)
112
113 def make_repository(sh, options, repo, tag):
114     sh.call("git", "init") # create repository
115     # configure our alternates (to save space and make this quick)
116     data = os.path.join(repo, "objects")
117     file = ".git/objects/info/alternates"
118     if not options.dry_run:
119         alternates = open(file, "w")
120         alternates.write(data)
121         alternates.close()
122         htaccess = open(".git/.htaccess", "w")
123         htaccess.write("Deny from all\n")
124         htaccess.close()
125     else:
126         logging.info("# create %s containing \"%s\"" % (file, data))
127         logging.info('# create .htaccess containing "Deny from all"')
128     # configure our remote (this is merely for convenience; wizard
129     # will not rely on this)
130     sh.call("git", "remote", "add", "origin", repo)
131     # configure what would normally be set up on a 'git clone' for consistency
132     sh.call("git", "config", "branch.master.remote", "origin")
133     sh.call("git", "config", "branch.master.merge", "refs/heads/master")
134     # perform the initial fetch
135     sh.call("git", "fetch", "origin")
136     # soft reset to our tag
137     sh.call("git", "reset", tag, "--")
138     # initialize the .wizard directory
139     util.init_wizard_dir()
140     logging.info("Diffstat:\n" + sh.eval("git", "diff", "--stat"))
141     # commit user local changes
142     message = "Autoinstall migration.\n\n%s" % util.get_git_footer()
143     util.set_git_env()
144     try:
145         message += "\nMigrated-by: " + util.get_operator_git()
146     except util.NoOperatorInfo:
147         pass
148     sh.call("git", "commit", "--allow-empty", "-a", "-m", message)
149
150 def check_variables(d, options):
151     """Attempt to extract variables and complain if some are missing."""
152     variables = d.extract()
153     for k,v in variables.items():
154         if v is None and k not in d.application.deprecated_keys:
155             logging.warning("Variable %s not found" % k)
156         else:
157             logging.debug("Variable %s is %s" % (k,v))
158
159 class Error(command.Error):
160     """Base exception for all exceptions raised by migrate"""
161     pass
162
163 class AlreadyMigratedError(Error):
164     quiet = True
165     def __init__(self, dir):
166         self.dir = dir
167     def __str__(self):
168         return """
169
170 This autoinstall is already migrated; move along, nothing to
171 see here.  (If you really want to, you can force a re-migration
172 with --force, but this will blow away the existing .git and
173 .scripts directories (i.e. your history and Wizard configuration).)
174 """
175
176 class UnsupportedVersion(Error):
177     def __init__(self, version):
178         self.version = version
179     def __str__(self):
180         return """
181
182 ERROR: This autoinstall is presently on %s, which is unsupported by
183 Wizard.  Please manually upgrade it to one that is supported,
184 and then retry the migration; usually the latest version is supported.
185 """ % self.version