]> scripts.mit.edu Git - wizard.git/blob - wizard/command/mass_upgrade.py
Implement 'append-by-default' reports, --redo for mass-upgrade.
[wizard.git] / wizard / command / mass_upgrade.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 import itertools
10
11 import wizard
12 from wizard import deploy, util, scripts, shell, sset, command
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 = shell.ParallelShell.make(options.no_parallelize, options.max_processes)
19     command.create_logdir(options.log_dir)
20     seen = sset.make(options.seen)
21     is_root = not os.getuid()
22     report = command.open_reports(options.log_dir, ('lookup', 'warnings', 'errors'), options.redo, ('merge', 'verify'))
23     # loop stuff
24     errors = {}
25     i = 0
26     fails = {
27             "merge": 0,
28             "verify": 0,
29         }
30     deploys = deploy.parse_install_lines(app, options.versions_path, user=options.user)
31     requested_deploys = itertools.islice(deploys, options.limit)
32     for i, d in enumerate(requested_deploys, 1):
33         report.lookup.write("%04d %s\n" % (i, d.location))
34         # check if we want to punt due to --limit
35         if d.location in seen:
36             continue
37         if i in report.skip:
38             continue
39         if is_root and not command.security_check_homedir(d.location):
40             continue
41         # XXX: we may be able to punt based on detected versions from d, which
42         # would be faster than spinning up a new process. On the other hand,
43         # `seen` makes this mostly not a problem
44         logging.info("[%04d] Processing %s" % (i, d.location))
45         child_args = list(base_args)
46         # calculate the log file, if a log dir was specified
47         if options.log_dir:
48             log_file = command.calculate_log_name(options.log_dir, i)
49             child_args.append("--log-file=" + log_file)
50         # actual meat
51         def make_on_pair(d, i):
52             # we need to make another stack frame so that d and i get specific bindings.
53             def on_success(stdout, stderr):
54                 if stderr:
55                     report.lookup.write("[%04d] %s\n" % (i, d.location))
56                     logging.warning("[%04d] Warnings at [%s]:\n%s" % (i, d.location, stderr))
57                 seen.add(d.location)
58             def on_error(e):
59                 if e.name == "AlreadyUpgraded":
60                     seen.add(d.location)
61                     logging.info("[%04d] Skipped already upgraded %s" % (i, d.location))
62                 elif e.name == "MergeFailed":
63                     seen.add(d.location)
64                     tmpdir = e.stdout.rstrip()
65                     logging.warning("[%04d] Merge failed: resolve at [%s], source at [%s]" % (i, tmpdir, d.location))
66                     report.merge.write("[%04d] %s %s\n" % (i, tmpdir, d.location))
67                     fails['merge'] += 1
68                 else:
69                     name = e.name
70                     if name == "WebVerificationError":
71                         try:
72                             host, path = scripts.get_web_host_and_path(d.location)
73                             url = "http://%s%s" % (host, path)
74                         except ValueError:
75                             url = d.location
76                         # This should actually be a warning, but
77                         # it's a really common error
78                         logging.info("[%04d] Could not verify application at %s" % (i, url))
79                         report.verify.write("[%04d] %s\n" % (i, url))
80                         fails['verify'] += 1
81                     else:
82                         if name not in errors: errors[name] = []
83                         errors[name].append(d)
84                         msg = "[%04d] %s in %s" % (i, name, d.location)
85                         logging.error(msg)
86                         report.errors.write(msg + "\n")
87             return (on_success, on_error)
88         on_success, on_error = make_on_pair(d, i)
89         sh.call("wizard", "upgrade", 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     def printPercent(description, number, total):
95         logging.warning("%d out of %d installs (%.1f%%) had %s" % (number, total, float(number)/total*100, description))
96     if fails['merge']:
97         printPercent("merge conflicts", fails['merge'], i)
98     if fails['verify']:
99         printPercent("web verification failure", fails['verify'], i)
100
101 def parse_args(argv, baton):
102     usage = """usage: %prog mass-upgrade [ARGS] APPLICATION
103
104 Mass upgrades an application to the latest scripts version.
105 Essentially equivalent to running '%prog upgrade' on all
106 autoinstalls for a particular application found by parallel-find,
107 but with advanced reporting.
108
109 This command is intended to be run as root on a server with
110 the scripts AFS patch."""
111     parser = command.WizardOptionParser(usage)
112     baton.push(parser, "log_dir")
113     baton.push(parser, "seen")
114     baton.push(parser, "no_parallelize")
115     baton.push(parser, "dry_run")
116     baton.push(parser, "max_processes")
117     baton.push(parser ,"limit")
118     baton.push(parser, "versions_path")
119     baton.push(parser, "srv_path")
120     baton.push(parser, "user")
121     parser.add_option("--force", dest="force", action="store_true",
122             default=False, help="Force running upgrade even if it's already at latest version.")
123     parser.add_option("--redo", dest="redo", action="store_true",
124             default=False, help="Redo failed upgrades; use this if you updated Wizard's code.")
125     options, args, = parser.parse_all(argv)
126     if len(args) > 1:
127         parser.error("too many arguments")
128     elif not args:
129         parser.error("must specify application to upgrade")
130     return options, args
131
132 def calculate_base_args(options):
133     return command.makeBaseArgs(options, dry_run="--dry-run", srv_path="--srv-path", force="--force")
134