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()
25 use_shm = False # if you are running --continue, this is guaranteed to be False
27 temp_wc_dir = os.getcwd()
28 user_commit, next_commit = open(".git/WIZARD_PARENTS", "r").read().split()
29 repo = open(".git/WIZARD_REPO", "r").read()
30 version = open(".git/WIZARD_UPGRADE_VERSION", "r").read()
31 util.chdir(sh.eval("git", "config", "remote.origin.url"))
32 d = deploy.Deployment(".")
34 sh.call("git", "status")
35 raise LocalChangesError()
36 except shell.CallError:
39 d = deploy.Deployment(".")
41 d.verifyTag(options.srv_path)
42 d.verifyGit(options.srv_path)
45 if not options.dry_run:
47 repo = d.application.repository(options.srv_path)
48 version = calculate_newest_version(sh, repo)
49 if version == d.app_version.scripts_tag and not options.force:
50 # don't log this error
51 # XXX: maybe we should build this in as a flag to add
52 # to exceptions w/ our exception handler
53 sys.stderr.write("Traceback:\n (n/a)\nAlreadyUpgraded\n")
55 logging.info("Upgrading %s" % os.getcwd())
56 if not options.dry_run:
57 perform_pre_commit(sh)
58 # If /dev/shm exists, it's a tmpfs and we can use it
59 # to do a fast git merge. Don't forget to move it to
61 # XXX: git merge-tree is another possibility for
62 # reducing filesystem interactions, and also probably
63 # works better with extra merging functionality. However,
64 # I don't know how I would put the results back in the
66 use_shm = os.path.exists("/dev/shm")
67 temp_dir, temp_wc_dir = perform_tmp_clone(sh, use_shm)
68 with util.ChangeDirectory(temp_wc_dir):
69 sh.call("git", "remote", "add", "scripts", repo)
70 sh.call("git", "fetch", "-q", "scripts")
71 user_commit, next_commit = calculate_parents(sh, version)
72 # save variables so that --continue will work
73 # yeah yeah no trailing newline whatever
74 open(".git/WIZARD_REPO", "w").write(repo)
75 open(".git/WIZARD_UPGRADE_VERSION", "w").write(version)
76 open(".git/WIZARD_PARENTS", "w").write("%s\n%s" % (user_commit, next_commit))
77 perform_merge(sh, repo, d, version, use_shm)
78 # variables: version, user_commit, next_commit, temp_wc_dir
79 with util.ChangeDirectory(temp_wc_dir):
81 sh.call("git", "status")
82 sh.call("git", "commit", "-m", "throw-away commit")
83 except shell.CallError:
85 message = make_commit_message(version)
86 new_tree = sh.eval("git", "rev-parse", "HEAD^{tree}")
87 final_commit = sh.eval("git", "commit-tree", new_tree,
88 "-p", user_commit, "-p", next_commit, input=message, log=True)
89 # a master branch may not necessarily exist if the user
90 # was manually installed to an earlier version
92 sh.call("git", "checkout", "-q", "-b", "master", "--")
93 except shell.CallError:
94 sh.call("git", "checkout", "-q", "master", "--")
95 sh.call("git", "reset", "-q", "--hard", final_commit)
96 # This is a quick sanity check to make sure we didn't completely
99 # Till now, all of our operations were in a tmp sandbox.
101 logging.info("Dry run, bailing. See results at %s" % temp_wc_dir)
103 # perform database backup
104 backup = d.backup(options)
105 # XXX: frob .htaccess to make site inaccessible
106 with util.IgnoreKeyboardInterrupts():
107 with util.LockDirectory(".scripts-upgrade-lock"):
108 # git merge (which performs a fast forward)
109 sh.call("git", "pull", "-q", temp_wc_dir, "master")
110 # after the pull is successful, the directory now
111 # has the objects for this commit, so we can safely
112 # nuke the shm directory. We refrain from nuking the
113 # tmp directory in case we messed up the merge resolution
114 # and want to be able to use it again.
116 shutil.rmtree(temp_dir)
117 version_obj = distutils.version.LooseVersion(version.partition('-')[2])
120 d.upgrade(version_obj, options)
122 except app.UpgradeFailure:
123 logging.warning("Upgrade failed: rolling back")
124 perform_restore(d, backup)
126 except deploy.WebVerificationError as e:
127 logging.warning("Web verification failed: rolling back")
128 logging.info("Web page that was output was:\n\n%s" % e.contents)
129 perform_restore(d, backup)
130 raise app.UpgradeVerificationFailure("Upgrade caused website to become inaccessible; site was rolled back")
131 # XXX: frob .htaccess to make site accessible
132 # to do this, check if .htaccess changed, first. Upgrade
133 # process might have frobbed it. Don't be
134 # particularly worried if the segment disappeared
136 def perform_restore(d, backup):
137 # You don't want d.restore() because it doesn't perform
138 # the file level backup
139 shell.Shell().call("wizard", "restore", backup)
142 except deploy.WebVerificationError:
143 logging.critical("Web verification failed after rollback")
145 def make_commit_message(version):
146 message = "Upgraded autoinstall in %s to %s.\n\n%s" % (util.get_dir_owner(), version, util.get_git_footer())
148 message += "\nUpgraded-by: " + util.get_operator_git()
149 except util.NoOperatorInfo:
153 def calculate_newest_version(sh, repo):
154 # XXX: put this in Application
155 return sh.eval("git", "--git-dir="+repo, "describe", "--tags", "master")
157 def calculate_parents(sh, version):
158 user_commit = sh.eval("git", "rev-parse", "HEAD")
159 next_commit = sh.eval("git", "rev-parse", version)
160 return user_commit, next_commit
162 def perform_pre_commit(sh):
163 message = "Pre-commit of %s locker before autoinstall upgrade.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
165 message += "\nPre-commit-by: " + util.get_operator_git()
166 except util.NoOperatorInfo:
169 sh.call("git", "commit", "-a", "-m", message)
170 except shell.CallError:
171 logging.info("No changes detected")
173 def perform_tmp_clone(sh, use_shm):
175 dir = "/dev/shm/wizard"
176 if not os.path.exists(dir):
180 temp_dir = tempfile.mkdtemp(prefix="wizard", dir=dir)
181 temp_wc_dir = os.path.join(temp_dir, "repo")
182 logging.info("Using temporary directory: " + temp_wc_dir)
183 sh.call("git", "clone", "-q", "--shared", ".", temp_wc_dir)
184 return temp_dir, temp_wc_dir
186 def perform_merge(sh, repo, d, version, use_shm):
187 # naive merge algorithm:
188 # sh.call("git", "merge", "-m", message, "scripts/master")
189 # crazy merge algorithm:
190 def make_virtual_commit(tag, parents = []):
191 """WARNING: Changes state of Git repository"""
192 sh.call("git", "checkout", "-q", tag, "--")
194 for file in d.application.parametrized_files:
196 sh.call("git", "add", "--", file)
197 except shell.CallError:
199 virtual_tree = sh.eval("git", "write-tree", log=True)
200 parent_args = itertools.chain(*(["-p", p] for p in parents))
201 virtual_commit = sh.eval("git", "commit-tree", virtual_tree,
202 *parent_args, input="", log=True)
203 sh.call("git", "reset", "--hard")
204 return virtual_commit
205 user_tree = sh.eval("git", "rev-parse", "HEAD^{tree}")
206 base_virtual_commit = make_virtual_commit(d.app_version.scripts_tag)
207 next_virtual_commit = make_virtual_commit(version, [base_virtual_commit])
208 user_virtual_commit = sh.eval("git", "commit-tree", user_tree,
209 "-p", base_virtual_commit, input="", log=True)
210 sh.call("git", "checkout", user_virtual_commit, "--")
212 sh.call("git", "merge", next_virtual_commit)
213 except shell.CallError as e:
214 conflicts = e.stderr.count("CONFLICT") # not perfect, if there is a file named CONFLICT
215 logging.info("Merge failed with these messages:\n\n" + e.stderr)
216 # Run the application's specific merge resolution algorithms
217 # and see if we can salvage it
219 if d.application.resolveConflicts(curdir):
220 logging.info("Resolved conflicts with application specific knowledge")
221 sh.call("git", "commit", "-a", "-m", "merge")
223 # XXX: Maybe should recalculate conflicts
224 logging.info("Conflict info:\n" + sh.eval("git", "diff"))
226 # Keeping all of our autoinstalls in shared memory is
227 # a recipe for disaster, so let's move them to slightly
228 # less volatile storage (a temporary directory)
229 os.chdir(tempfile.gettempdir())
230 newdir = tempfile.mkdtemp(prefix="wizard")
231 # shutil, not os; at least on Ubuntu os.move fails
232 # with "[Errno 18] Invalid cross-device link"
233 shutil.move(curdir, newdir)
234 shutil.rmtree(os.path.dirname(curdir))
235 curdir = os.path.join(newdir, "repo")
236 print "%d %s" % (conflicts, curdir)
239 def parse_args(argv, baton):
240 usage = """usage: %prog upgrade [ARGS] [DIR]
242 Upgrades an autoinstall to the latest version. This involves
243 updating files and running .scripts/update. If the merge fails,
244 this program will write the number of conflicts and the directory
245 of the conflicted working tree to stdout, separated by a space."""
246 parser = command.WizardOptionParser(usage)
247 parser.add_option("--dry-run", dest="dry_run", action="store_true",
248 default=False, help="Prints would would be run without changing anything")
249 # notice trailing underscore
250 parser.add_option("--continue", dest="continue_", action="store_true",
251 default=False, help="Continues an upgrade that has had its merge manually "
252 "resolved using the current working directory as the resolved copy.")
253 parser.add_option("--force", dest="force", action="store_true",
254 default=False, help="Force running upgrade even if it's already at latest version.")
255 baton.push(parser, "srv_path")
256 options, args = parser.parse_all(argv)
258 parser.error("too many arguments")
261 class Error(command.Error):
262 """Base exception for all exceptions raised by upgrade"""
265 class AlreadyUpgraded(Error):
269 ERROR: This autoinstall is already at the latest version."""
271 class MergeFailed(Error):
275 ERROR: Merge failed. Resolve the merge by cd'ing to the
276 temporary directory, finding conflicted files with `git status`,
277 resolving the files, adding them using `git add` and then
278 running `wizard upgrade --continue`."""
280 class LocalChangesError(Error):
284 ERROR: Local changes occurred in the install while the merge was
285 being processed so that a pull would not result in a fast-forward.
286 The best way to resolve this is probably to attempt an upgrade again,
287 with git rerere to remember merge resolutions (XXX: not sure if
288 this actually works)."""
290 class BlacklistedError(Error):
294 ERROR: This autoinstall was manually blacklisted against errors;
295 if the user has not been notified of this, please send them