]> scripts.mit.edu Git - wizard.git/blob - wizard/command/migrate.py
Fix bugs, add better logging.
[wizard.git] / wizard / command / migrate.py
1 import os
2 import itertools
3 import shutil
4 import logging
5 import errno
6 import sys
7
8 from wizard import command, deploy, shell, util
9
10 def main(argv, baton):
11     options, args = parse_args(argv, baton)
12     dir = args[0]
13     command.chdir(dir)
14
15     shell.drop_priviledges(options)
16     sh = shell.Shell(options.dry_run)
17
18     logging.info("Migrating %s" % dir)
19     logging.debug("uid is %d" % os.getuid())
20
21     deployment = make_deployment() # uses chdir
22     version = deployment.app_version
23     repo    = version.application.repository(options.srv_path)
24     tag     = version.scripts_tag
25     check_if_tag_exists(sh, repo, tag, version)
26
27     check_if_already_migrated(options)
28
29     os.unsetenv("GIT_DIR") # prevent some perverse errors
30
31     try:
32         try:
33             os.fdopen(os.open(".scripts-migrate-lock", os.O_CREAT | os.O_EXCL)).write(str(os.getpid()))
34         except OSError as e:
35             if e.errno == errno.EEXIST:
36                 raise DirectoryLockedError
37             elif e.errno == errno.EACCES:
38                 raise command.PermissionsError(dir)
39         make_repository(sh, options, repo, tag)
40         check_variables(deployment, options)
41     finally:
42         try:
43             os.unlink(".scripts-migrate-lock")
44         except OSError:
45             pass
46
47 def parse_args(argv, baton):
48     usage = """usage: %prog migrate [ARGS] DIR
49
50 Migrates a directory to our Git-based autoinstall format.
51 Performs basic sanity checking and intelligently determines
52 what repository and tag to use.
53
54 This command is meant to be run as the owner of the install
55 it is upgrading (see the scripts AFS kernel patch).  Do
56 NOT run this command as root."""
57     parser = command.WizardOptionParser(usage)
58     baton.push(parser, "srv_path")
59     parser.add_option("--dry-run", dest="dry_run", action="store_true",
60             default=False, help="Prints would would be run without changing anything")
61     parser.add_option("--force", "-f", dest="force", action="store_true",
62             default=False, help="If .git or .scripts directory already exists, delete them and migrate")
63     options, args = parser.parse_all(argv)
64     if len(args) > 1:
65         parser.error("too many arguments")
66     elif not args:
67         parser.error("must specify directory")
68     return (options, args)
69
70 def check_if_already_migrated(options):
71     has_git = os.path.isdir(".git")
72     has_scripts = os.path.isdir(".scripts")
73
74     # deal with old-style migration
75     if os.path.isfile(".scripts/old-version") and not os.path.isfile(".scripts-version"):
76         os.rename(".scripts/old-version", ".scripts-version")
77
78     if not has_git and has_scripts:
79         if not options.force:
80             raise CorruptedAutoinstallError(dir)
81     elif has_git and not has_scripts:
82         # can't force this
83         raise AlreadyVersionedError(dir)
84     elif has_git and has_scripts:
85         if not options.force:
86             raise AlreadyMigratedError(dir)
87
88     if options.force:
89         def rm_with_backup(file):
90             prefix = "%s.bak" % file
91             name = None
92             for i in itertools.count():
93                 name = "%s.%d" % (prefix, i)
94                 if not os.path.exists(name):
95                     break
96             os.rename(file, name)
97         if has_git:
98             logging.warning("Force removing .git directory")
99             if not options.dry_run:
100                 rm_with_backup(".git")
101         if has_scripts:
102             logging.warning("Force removing .scripts directory")
103             if not options.dry_run:
104                 rm_with_backup(".scripts")
105
106 def make_deployment():
107     try:
108         return deploy.Deployment(".")
109     except IOError as e:
110         if e.errno == errno.ENOENT:
111             raise NotAutoinstallError(dir)
112         else: raise e
113
114 def check_if_tag_exists(sh, repo, tag, version):
115     # check if the version we're trying to convert exists. We assume
116     # a convention here, namely, v1.2.3-scripts is what we want. If
117     # you broke the convention... shame on you.
118     try:
119         sh.call("git", "--git-dir", repo, "rev-parse", tag)
120     except shell.CallError:
121         raise NoTagError(version)
122
123 def make_repository(sh, options, repo, tag):
124     sh.call("git", "init") # create repository
125     # configure our alternates (to save space and make this quick)
126     data = os.path.join(repo, "objects")
127     file = ".git/objects/info/alternates"
128     if not options.dry_run:
129         alternates = open(file, "w")
130         alternates.write(data)
131         alternates.close()
132         htaccess = open(".git/.htaccess", "w")
133         htaccess.write("Deny from all\n")
134         htaccess.close()
135     else:
136         logging.info("# create %s containing \"%s\"" % (file, data))
137         logging.info('# create .htaccess containing "Deny from all"')
138     # configure our remote (this is merely for convenience; wizard scripts
139     # will not rely on this)
140     sh.call("git", "remote", "add", "origin", repo)
141     # configure what would normally be set up on a 'git clone' for consistency
142     sh.call("git", "config", "branch.master.remote", "origin")
143     sh.call("git", "config", "branch.master.merge", "refs/heads/master")
144     # perform the initial fetch
145     sh.call("git", "fetch", "origin")
146     # soft reset to our tag
147     sh.call("git", "reset", tag, "--")
148     # checkout the .scripts directory
149     sh.call("git", "checkout", ".scripts")
150     logging.info("Diffstat:\n" + sh.eval("git", "diff", "--stat"))
151     # commit user local changes
152     message = "Autoinstall migration of %s locker.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
153     util.set_git_env()
154     try:
155         message += "\nMigrated-by: " + util.get_operator_git()
156     except util.NoOperatorInfo:
157         pass
158     sh.call("git", "commit", "--allow-empty", "-a", "-m", message)
159
160 def check_variables(d, options):
161     """Attempt to extract variables and complain if some are missing."""
162     variables = d.extract()
163     for k,v in variables.items():
164         if v is None and k not in d.application.deprecated_keys:
165             logging.warning("Variable %s not found" % k)
166         else:
167             logging.debug("Variable %s is %s" % (k,v))
168
169 class Error(command.Error):
170     """Base exception for all exceptions raised by migrate"""
171     pass
172
173 class AlreadyMigratedError(Error):
174     def __init__(self, dir):
175         self.dir = dir
176     def __str__(self):
177         return """
178
179 ERROR: Directory already contains a .git and
180 .scripts directory.  If you force this migration,
181 both of these directories will be removed.
182 """
183
184 class AlreadyVersionedError(Error):
185     def __init__(self, dir):
186         self.dir = dir
187     def __str__(self):
188         return """
189
190 ERROR: Directory contains a .git directory, but not
191 a .scripts directory.  If this is not a corrupt
192 migration, this means that the user was versioning their
193 install using Git.  You cannot force this case.
194 """
195
196 class CorruptedAutoinstallError(Error):
197     def __init__(self, dir):
198         self.dir = dir
199     def __str__(self):
200         return """
201
202 ERROR: Directory contains a .scripts directory,
203 but not a .git directory.  If you force this migration,
204 the .scripts directory will be removed.
205 """
206
207 class NotAutoinstallError(Error):
208     def __init__(self, dir):
209         self.dir = dir
210     def __str__(self):
211         return """
212
213 ERROR: Could not find .scripts-version file. Are you sure
214 this is an autoinstalled application?
215 """
216
217 class DirectoryLockedError(Error):
218     def __init__(self, dir):
219         self.dir = dir
220     def __str__(self):
221         return """
222
223 ERROR: Could not acquire lock on directory.  Maybe there is
224 another migration process running?
225 """
226
227 class NoTagError(Error):
228     def __init__(self, version):
229         self.version = version
230     def __str__(self):
231         return """
232
233 ERROR: Could not find tag v%s-scripts in repository
234 for %s.  Double check and make sure
235 the repository was prepared with all necessary tags!
236 """ % (self.version.version, self.version.application.name)