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