8 from wizard import deploy, scripts, shell, sset, command
9 from command import upgrade
11 def main(argv, baton):
12 options, args = parse_args(argv, baton)
14 base_args = calculate_base_args(options)
15 sh = shell.ParallelShell.make(options.no_parallelize, options.max_processes)
16 command.create_logdir(options.log_dir)
17 seen = sset.make(options.seen)
18 is_root = not os.getuid()
19 report = command.open_reports(options.log_dir, ('lookup', 'warnings', 'errors'),
20 options.redo, ('merge', 'verify', 'backup_failure', 'blacklisted'))
24 deploys = deploy.parse_install_lines(app, options.versions_path, user=options.user)
25 requested_deploys = itertools.islice(deploys, options.limit)
26 # clean up /dev/shm/wizard
27 if os.path.exists("/dev/shm/wizard"):
28 shutil.rmtree("/dev/shm/wizard")
29 os.mkdir("/dev/shm/wizard")
30 os.chmod("/dev/shm/wizard", 0o777)
32 for i, d in enumerate(requested_deploys, 1):
33 report.lookup.write("%04d %s\n" % (i, d.location)) # pylint: disable-msg=E1101
35 # check if we want to punt due to --limit
36 if d.location in seen:
40 if is_root and not command.security_check_homedir(d.location):
42 # XXX: we may be able to punt based on detected versions from d, which
43 # would be faster than spinning up a new process. On the other hand,
44 # `seen` makes this mostly not a problem
45 logging.info("[%04d] Processing %s" % (i, d.location))
46 child_args = list(base_args)
47 child.args.append("--non-interactive")
48 # calculate the log file, if a log dir was specified
50 log_file = command.calculate_log_name(options.log_dir, i)
51 child_args.append("--log-file=" + log_file)
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):
57 report.lookup.write("[%04d] %s\n" % (i, d.location)) # pylint: disable-msg=E1101
58 logging.warning("[%04d] Warnings at [%s]:\n%s" % (i, d.location, stderr))
62 if e.name == "AlreadyUpgraded":
64 logging.info("[%04d] Skipped already upgraded %s" % (i, d.location))
65 elif e.name == "MergeFailed":
67 conflicts, _, tmpdir = e.stdout.rstrip().partition(" ")
68 logging.warning("[%04d] Conflicts in %d files: resolve at [%s], source at [%s]" % (i, int(conflicts), tmpdir, d.location))
69 report.merge.write("[%04d] %s %d %s\n" % (i, tmpdir, int(conflicts), d.location)) # pylint: disable-msg=E1101
70 report.fails['merge'] += 1
71 elif e.name == "BlacklistedError":
72 reason = e.stdout.rstrip()
73 reason = reason.replace("\n", " ")
74 shortmsg = "[%04d] %s %s\n" % (i, d.location, reason)
75 report.blacklisted.write(shortmsg) # pylint: disable-msg=E1101
76 report.fails['blacklisted'] += 1
77 logging.warning("[%04d] Blacklisted because of '%s' at %s" % (i, reason, d.location))
80 if name == "WebVerificationError":
82 # This should actually be a warning, but
83 # it's a really common error
84 logging.info("[%04d] Could not verify application at %s" % (i, url))
85 report.verify.write("[%04d] %s\n" % (i, url)) # pylint: disable-msg=E1101
86 report.fails['verify'] += 1
88 if name not in errors: errors[name] = []
89 errors[name].append(d)
90 msg = "[%04d] %s in %s" % (i, name, d.location)
92 report.errors.write(msg + "\n") # pylint: disable-msg=E1101
93 shortmsg = "[%04d] %s\n" % (i, d.location)
94 if name == "BackupFailure":
95 report.backup_failure.write(shortmsg) # pylint: disable-msg=E1101
96 report.fails['backup_failure'] += 1
98 return (on_success, on_error)
99 on_success, on_error = make_on_pair(d, i)
100 sh.call("wizard", "upgrade", d.location, *child_args,
101 on_success=on_success, on_error=on_error)
104 sys.stderr.write("\n")
105 for name, deploys in errors.items():
106 logging.warning("%s from %d installs" % (name, len(deploys)))
107 def printPercent(description, number, total):
108 logging.warning("%d out of %d installs (%.1f%%) had %s" % (number, total, float(number)/total*100, description))
109 if report.fails['merge']:
110 printPercent("merge conflicts", report.fails['merge'], i)
111 if report.fails['verify']:
112 printPercent("web verification failure", report.fails['verify'], i)
114 def parse_args(argv, baton):
115 usage = """usage: %prog mass-upgrade [ARGS] APPLICATION
117 Mass upgrades an application to the latest scripts version.
118 Essentially equivalent to running '%prog upgrade' on all
119 autoinstalls for a particular application found by parallel-find,
120 but with advanced reporting.
122 This command is intended to be run as root on a server with
123 the scripts AFS patch."""
124 parser = command.WizardOptionParser(usage)
125 baton.push(parser, "log_dir")
126 baton.push(parser, "seen")
127 baton.push(parser, "no_parallelize")
128 baton.push(parser, "dry_run")
129 baton.push(parser, "max_processes")
130 baton.push(parser ,"limit")
131 baton.push(parser, "versions_path")
132 baton.push(parser, "srv_path")
133 baton.push(parser, "user")
134 parser.add_option("--force", dest="force", action="store_true",
135 default=False, help="Force running upgrade even if it's already at latest version.")
136 parser.add_option("--redo", dest="redo", action="store_true",
137 default=False, help="Redo failed upgrades; use this if you updated Wizard's code.")
138 options, args, = parser.parse_all(argv)
140 parser.error("too many arguments")
142 parser.error("must specify application to upgrade")
145 def calculate_base_args(options):
146 return command.make_base_args(options, dry_run="--dry-run", srv_path="--srv-path", force="--force")