]> scripts.mit.edu Git - wizard.git/blob - wizard/command/mass_migrate.py
Enhancements from our first migration.
[wizard.git] / wizard / command / mass_migrate.py
1 import optparse
2 import logging
3 import os
4 import os.path
5 import pwd
6 import hashlib
7 import errno
8 import time
9
10 import wizard
11 from wizard import deploy, util, shell, sset, command
12 from wizard.command import migrate
13
14 def main(argv, baton):
15     options, args = parse_args(argv, baton)
16     app = args[0]
17     base_args = calculate_base_args(options)
18     sh = make_shell(options)
19     seen = make_serialized_set(options)
20     my_uid = os.getuid() # to see if we have root
21     warnings_logname = "/tmp/wizard-migrate-warnings.log"
22     errors_logname = "/tmp/wizard-migrate-errors.log"
23     if options.log_dir:
24         # must not be on AFS
25         try:
26             os.mkdir(options.log_dir)
27         except OSError as e:
28             if e.errno != errno.EEXIST:
29                 raise
30             if options.force:
31                 options.log_dir = os.path.join(options.log_dir, str(int(time.time())))
32                 os.mkdir(options.log_dir) # if fails, be fatal
33         os.chmod(options.log_dir, 0o777)
34         warnings_logname = os.path.join(options.log_dir, "warnings.log")
35         errors_logname = os.path.join(options.log_dir, "errors.log")
36     warnings_log = open(warnings_logname, "a")
37     errors_log = open(errors_logname, "a")
38     # loop stuff
39     errors = {}
40     i = 0
41     for line in deploy.get_install_lines(options.versions_path):
42         child_args = list(base_args)
43         # validate and filter the deployments
44         try:
45             d = deploy.Deployment.parse(line)
46         except deploy.DeploymentParseError, deploy.NoSuchApplication:
47             continue
48         name = d.application.name
49         if name != app: continue
50         # check if we want to punt due to --limit
51         i += 1
52         if options.limit and i > options.limit:
53             break
54         if d.location in seen:
55             continue
56         # security check: see if the user's directory is the prefix of
57         # the deployment we're upgrading
58         if not my_uid:
59             uid = util.get_dir_uid(d.location)
60             real = os.path.realpath(d.location)
61             try:
62                 if not real.startswith(pwd.getpwuid(uid).pw_dir + "/"):
63                     logging.error("Security check failed, owner of deployment and owner of home directory mismatch for %s" % d.location)
64                     continue
65             except KeyError:
66                 logging.error("Security check failed, could not look up owner of %s (uid %d)" % (d.location, uid))
67                 continue
68         # calculate the log file, if a log dir was specified
69         if options.log_dir:
70             log_file = os.path.join(options.log_dir, shorten(i, d.location))
71             child_args.append("--log-file=" + log_file)
72         # actual meat
73         def make_on_pair(d, i):
74             def on_success(stdout, stderr):
75                 if stderr:
76                     warnings_log.write("%s\n" % d.location)
77                     logging.warning("Warnings [%04d] %s:\n%s" % (i, d.location, stderr))
78                 seen.add(d.location)
79             def on_error(e):
80                 if e.name == "wizard.command.migrate.AlreadyMigratedError" or \
81                    e.name == "AlreadyMigratedError":
82                     seen.add(d.location)
83                     logging.info("Skipped already migrated %s" % d.location)
84                 else:
85                     name = e.name
86                     if name not in errors: errors[name] = []
87                     errors[name].append(d)
88                     logging.error("%s in [%04d] %s" % (name, i, d.location))
89                     errors_log.write("%s\n" % d.location)
90             return (on_success, on_error)
91         on_success, on_error = make_on_pair(d, i)
92         sh.wait() # wait for a parallel processing slot to be available
93         sh.call("wizard", "migrate", d.location, *child_args,
94                       on_success=on_success, on_error=on_error)
95     sh.join()
96     for name, deploys in errors.items():
97         logging.warning("%s from %d installs" % (name, len(deploys)))
98
99 def parse_args(argv, baton):
100     usage = """usage: %prog mass-migrate [ARGS] APPLICATION
101
102 Mass migrates an application to the new repository format.
103 Essentially equivalent to running '%prog migrate' on all
104 autoinstalls for a particular application found by parallel-find,
105 but with advanced reporting.
106
107 When doing an actual run, it is recommended to use --seen to
108 be able to resume gracefully (without it, mass-migrate must
109 stat every install to find out if it migrated it yet).
110
111 This command is intended to be run as root on a server with
112 the scripts AFS patch.  You may run it as an unpriviledged
113 user for testing purposes, but then you MUST NOT run this on
114 untrusted repositories."""
115     parser = command.WizardOptionParser(usage)
116     parser.add_option("--no-parallelize", dest="no_parallelize", action="store_true",
117             default=False, help="Turn off parallelization")
118     parser.add_option("--dry-run", dest="dry_run", action="store_true",
119             default=False, help="Print commands that would be run. Implies --no-parallelize")
120     parser.add_option("--max", dest="max",
121             default=10, help="Maximum subprocesses to run concurrently")
122     parser.add_option("--seen", dest="seen",
123             default=None, help="File to read/write paths of already processed installs. These will be skipped.")
124     parser.add_option("--force", dest="force", action="store_true",
125             default=False, help="Force migrations to occur even if .scripts or .git exists.")
126     parser.add_option("--limit", dest="limit", type="int",
127             default=0, help="Limit the number of autoinstalls to look at.")
128     baton.push(parser, "versions_path")
129     baton.push(parser, "srv_path")
130     baton.push(parser, "log_dir")
131     options, args, = parser.parse_all(argv)
132     if len(args) > 1:
133         parser.error("too many arguments")
134     elif not args:
135         parser.error("must specify application to migrate")
136     if options.dry_run:
137         options.no_parallelize = True
138     return options, args
139
140 def calculate_base_args(options):
141     base_args = command.makeBaseArgs(options, dry_run="--dry-run", srv_path="--srv-path", force="--force")
142     return base_args
143
144 def shorten(i, dir):
145     return "%04d" % i + dir.replace('/', '-') + ".log"
146
147 def make_shell(options):
148     if options.no_parallelize:
149         sh = shell.DummyParallelShell()
150     else:
151         sh = shell.ParallelShell(max=int(options.max))
152     return sh
153
154 def make_serialized_set(options):
155     if options.seen:
156         seen = sset.SerializedSet(options.seen)
157     else:
158         seen = sset.DummySerializedSet()
159     return seen