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