3 import distutils.version
6 import logging.handlers
11 from wizard import app, command, deploy, shell, util
13 def main(argv, baton):
14 options, args = parse_args(argv, baton)
19 shell.drop_priviledges(dir, options.log_file)
21 if os.path.exists(".scripts/blacklisted"):
22 raise BlacklistedError()
26 temp_wc_dir = os.getcwd()
27 user_commit, next_commit = open(".git/WIZARD_PARENTS", "r").read().split()
28 repo = open(".git/WIZARD_REPO", "r").read()
29 version = open(".git/WIZARD_UPGRADE_VERSION", "r").read()
30 util.chdir(sh.eval("git", "config", "remote.origin.url"))
31 d = deploy.Deployment(".")
33 sh.call("git", "status")
34 raise LocalChangesError()
35 except shell.CallError:
38 d = deploy.Deployment(".")
40 d.verifyTag(options.srv_path)
41 d.verifyGit(options.srv_path)
44 if not options.dry_run:
46 repo = d.application.repository(options.srv_path)
47 version = calculate_newest_version(sh, repo)
48 if version == d.app_version.scripts_tag and not options.force:
50 if not options.dry_run:
51 perform_pre_commit(sh)
52 temp_wc_dir = perform_tmp_clone(sh)
53 with util.ChangeDirectory(temp_wc_dir):
54 sh.call("git", "remote", "add", "scripts", repo)
55 sh.call("git", "fetch", "scripts")
56 user_commit, next_commit = calculate_parents(sh, version)
57 # save variables so that --continue will work
58 # yeah yeah no trailing newline whatever
59 open(".git/WIZARD_REPO", "w").write(repo)
60 open(".git/WIZARD_UPGRADE_VERSION", "w").write(version)
61 open(".git/WIZARD_PARENTS", "w").write("%s\n%s" % (user_commit, next_commit))
62 perform_merge(sh, repo, d, version)
63 # variables: version, user_commit, next_commit, temp_wc_dir
64 with util.ChangeDirectory(temp_wc_dir):
66 sh.call("git", "status")
67 sh.call("git", "commit", "-m", "throw-away commit")
68 except shell.CallError:
70 message = make_commit_message(version)
71 new_tree = sh.eval("git", "rev-parse", "HEAD^{tree}")
72 final_commit = sh.eval("git", "commit-tree", new_tree,
73 "-p", user_commit, "-p", next_commit, input=message, log=True)
74 # a master branch may not necessarily exist if the user
75 # was manually installed to an earlier version
77 sh.call("git", "checkout", "-b", "master", "--")
78 except shell.CallError:
79 sh.call("git", "checkout", "master", "--")
80 sh.call("git", "reset", "--hard", final_commit)
82 # Till now, all of our operations were in a tmp sandbox.
84 logging.info("Dry run, bailing. See results at %s" % temp_wc_dir)
86 # perform database backup
87 backup = d.backup(options)
88 # XXX: frob .htaccess to make site inaccessible
89 with util.IgnoreKeyboardInterrupts():
90 with util.LockDirectory(".scripts-upgrade-lock"):
91 # git merge (which performs a fast forward)
92 sh.call("git", "pull", temp_wc_dir, "master")
93 version_obj = distutils.version.LooseVersion(version.partition('-')[2])
96 d.upgrade(version_obj, options)
98 except app.UpgradeFailure:
99 logging.warning("Upgrade failed: rolling back")
100 perform_restore(d, backup)
102 except deploy.WebVerificationError as e:
103 logging.warning("Web verification failed: rolling back")
104 logging.info("Web page that was output was:\n\n%s" % e.contents)
105 perform_restore(d, backup)
106 raise app.UpgradeVerificationFailure("Upgrade caused website to become inaccessible; site was rolled back")
107 # XXX: frob .htaccess to make site accessible
108 # to do this, check if .htaccess changed, first. Upgrade
109 # process might have frobbed it. Don't be
110 # particularly worried if the segment disappeared
112 def perform_restore(d, backup):
113 # You don't want d.restore() because it doesn't perform
114 # the file level backup
115 shell.Shell().call("wizard", "restore", backup)
118 except deploy.WebVerificationError:
119 logging.critical("Web verification failed after rollback")
121 def make_commit_message(version):
122 message = "Upgraded autoinstall in %s to %s.\n\n%s" % (util.get_dir_owner(), version, util.get_git_footer())
124 message += "\nUpgraded-by: " + util.get_operator_git()
125 except util.NoOperatorInfo:
129 def calculate_newest_version(sh, repo):
130 # XXX: put this in Application
131 return sh.eval("git", "--git-dir="+repo, "describe", "--tags", "master")
133 def calculate_parents(sh, version):
134 user_commit = sh.eval("git", "rev-parse", "HEAD")
135 next_commit = sh.eval("git", "rev-parse", version)
136 return user_commit, next_commit
138 def perform_pre_commit(sh):
139 message = "Pre-commit of %s locker before autoinstall upgrade.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
141 message += "\nPre-commit-by: " + util.get_operator_git()
142 except util.NoOperatorInfo:
145 sh.call("git", "commit", "-a", "-m", message)
146 except shell.CallError:
147 logging.info("No changes detected")
149 def perform_tmp_clone(sh):
150 temp_dir = tempfile.mkdtemp(prefix="wizard")
151 temp_wc_dir = os.path.join(temp_dir, "repo")
152 logging.info("Using temporary directory: " + temp_wc_dir)
153 sh.call("git", "clone", "--shared", ".", temp_wc_dir)
156 def perform_merge(sh, repo, d, version):
157 # naive merge algorithm:
158 # sh.call("git", "merge", "-m", message, "scripts/master")
159 # crazy merge algorithm:
160 def make_virtual_commit(tag, parents = []):
161 """WARNING: Changes state of Git repository"""
162 sh.call("git", "checkout", tag, "--")
164 for file in d.application.parametrized_files:
166 sh.call("git", "add", "--", file)
167 except shell.CallError:
169 virtual_tree = sh.eval("git", "write-tree", log=True)
170 parent_args = itertools.chain(*(["-p", p] for p in parents))
171 virtual_commit = sh.eval("git", "commit-tree", virtual_tree,
172 *parent_args, input="", log=True)
173 sh.call("git", "reset", "--hard")
174 return virtual_commit
175 user_tree = sh.eval("git", "rev-parse", "HEAD^{tree}")
176 base_virtual_commit = make_virtual_commit(d.app_version.scripts_tag)
177 next_virtual_commit = make_virtual_commit(version, [base_virtual_commit])
178 user_virtual_commit = sh.eval("git", "commit-tree", user_tree,
179 "-p", base_virtual_commit, input="", log=True)
180 sh.call("git", "checkout", user_virtual_commit, "--")
182 sh.call("git", "merge", next_virtual_commit)
183 except shell.CallError:
185 logging.info("Conflict info:\n", sh.eval("git", "diff"))
188 def parse_args(argv, baton):
189 usage = """usage: %prog upgrade [ARGS] [DIR]
191 Upgrades an autoinstall to the latest version. This involves
192 updating files and running .scripts/update.
194 WARNING: This is still experimental."""
195 parser = command.WizardOptionParser(usage)
196 parser.add_option("--dry-run", dest="dry_run", action="store_true",
197 default=False, help="Prints would would be run without changing anything")
198 # notice trailing underscore
199 parser.add_option("--continue", dest="continue_", action="store_true",
200 default=False, help="Continues an upgrade that has had its merge manually "
201 "resolved using the current working directory as the resolved copy.")
202 parser.add_option("--force", dest="force", action="store_true",
203 default=False, help="Force running upgrade even if it's already at latest version.")
204 baton.push(parser, "srv_path")
205 options, args = parser.parse_all(argv)
207 parser.error("too many arguments")
210 class Error(command.Error):
211 """Base exception for all exceptions raised by upgrade"""
214 class AlreadyUpgraded(Error):
218 ERROR: This autoinstall is already at the latest version."""
220 class MergeFailed(Error):
224 ERROR: Merge failed. Resolve the merge by cd'ing to the
225 temporary directory, finding conflicted files with `git status`,
226 resolving the files, adding them using `git add` and then
227 running `wizard upgrade --continue`."""
229 class LocalChangesError(Error):
233 ERROR: Local changes occurred in the install while the merge was
234 being processed so that a pull would not result in a fast-forward.
235 The best way to resolve this is probably to attempt an upgrade again,
236 with git rerere to remember merge resolutions (XXX: not sure if
237 this actually works)."""
239 class BlacklistedError(Error):
243 ERROR: This autoinstall was manually blacklisted against errors;
244 if the user has not been notified of this, please send them