]> scripts.mit.edu Git - wizard.git/blob - wizard/command/mass_migrate.py
88f008736d26599a27fc2b90326a61b95f9471df
[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         if d.location in seen:
51             continue
52         # security check: see if the user's directory is the prefix of
53         # the deployment we're upgrading
54         if not my_uid:
55             uid = util.get_dir_uid(d.location)
56             real = os.path.realpath(d.location)
57             if not real.startswith(pwd.getpwuid(uid).pw_dir + "/"):
58                 logging.error("Security check failed, owner of deployment and owner of home directory mismatch for %s" % d.location)
59                 continue
60         # check if we want to punt due to --limit
61         i += 1
62         if options.limit and i > options.limit:
63             break
64         # calculate the log file, if a log dir was specified
65         if options.log_dir:
66             log_file = os.path.join(options.log_dir, shorten(i, d.location))
67             child_args.append("--log-file=" + log_file)
68         # actual meat
69         def make_on_pair(d, i):
70             def on_success(stdout, stderr):
71                 if stderr:
72                     warnings_log.write("%s\n" % d.location)
73                     logging.warning("Warnings [%04d] %s:\n%s" % (d.location, i, stderr))
74                 seen.add(d.location)
75             def on_error(e):
76                 if e.name == "wizard.command.migrate.AlreadyMigratedError" or \
77                    e.name == "AlreadyMigratedError":
78                     seen.add(d.location)
79                     logging.info("Skipped already migrated %s" % d.location)
80                 else:
81                     name = e.name
82                     if name not in errors: errors[name] = []
83                     errors[name].append(d)
84                     logging.error("%s in [%04d] %s" % (name, i, d.location))
85                     errors_log.write("%s\n" % d.location)
86             return (on_success, on_error)
87         on_success, on_error = make_on_pair(d, i)
88         sh.wait() # wait for a parallel processing slot to be available
89         sh.call("wizard", "migrate", d.location, *child_args,
90                       on_success=on_success, on_error=on_error)
91     sh.join()
92     for name, deploys in errors.items():
93         logging.warning("%s from %d installs" % (name, len(deploys)))
94
95 def parse_args(argv, baton):
96     usage = """usage: %prog mass-migrate [ARGS] APPLICATION
97
98 Mass migrates an application to the new repository format.
99 Essentially equivalent to running '%prog migrate' on all
100 autoinstalls for a particular application found by parallel-find,
101 but with advanced reporting.
102
103 When doing an actual run, it is recommended to use --seen to
104 be able to resume gracefully (without it, mass-migrate must
105 stat every install to find out if it migrated it yet).
106
107 This command is intended to be run as root on a server with
108 the scripts AFS patch.  You may run it as an unpriviledged
109 user for testing purposes, but then you MUST NOT run this on
110 untrusted repositories."""
111     parser = command.WizardOptionParser(usage)
112     parser.add_option("--no-parallelize", dest="no_parallelize", action="store_true",
113             default=False, help="Turn off parallelization")
114     parser.add_option("--dry-run", dest="dry_run", action="store_true",
115             default=False, help="Print commands that would be run. Implies --no-parallelize")
116     parser.add_option("--max", dest="max",
117             default=10, help="Maximum subprocesses to run concurrently")
118     parser.add_option("--seen", dest="seen",
119             default=None, help="File to read/write paths of already processed installs. These will be skipped.")
120     parser.add_option("--force", dest="force", action="store_true",
121             default=False, help="Force migrations to occur even if .scripts or .git exists.")
122     parser.add_option("--limit", dest="limit", type="int",
123             default=0, help="Limit the number of autoinstalls to look at.")
124     baton.push(parser, "versions_path")
125     baton.push(parser, "srv_path")
126     baton.push(parser, "log_dir")
127     options, args, = parser.parse_all(argv)
128     if len(args) > 1:
129         parser.error("too many arguments")
130     elif not args:
131         parser.error("must specify application to migrate")
132     if options.dry_run:
133         options.no_parallelize = True
134     return options, args
135
136 def calculate_base_args(options):
137     base_args = command.makeBaseArgs(options, dry_run="--dry-run", srv_path="--srv-path", force="--force")
138     return base_args
139
140 def shorten(i, dir):
141     return "%04d" % i + dir.replace('/', '-') + ".log"
142
143 def make_shell(options):
144     if options.no_parallelize:
145         sh = shell.DummyParallelShell()
146     else:
147         sh = shell.ParallelShell(max=int(options.max))
148     return sh
149
150 def make_serialized_set(options):
151     if options.seen:
152         seen = sset.SerializedSet(options.seen)
153     else:
154         seen = sset.DummySerializedSet()
155     return seen