X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/72177f26b9e2eb66c307b0ff15d4840903b434f6..6554c6378a6e801b4fe47c50688cdae1d627bc18:/wizard/deploy.py?ds=sidebyside diff --git a/wizard/deploy.py b/wizard/deploy.py index 7bc2c76..6d3ec67 100644 --- a/wizard/deploy.py +++ b/wizard/deploy.py @@ -9,6 +9,11 @@ import fileinput import logging import decorator import datetime +import tempfile +import time +import traceback +import shutil +import errno import wizard from wizard import app, git, old_log, scripts, shell, sql, util @@ -48,6 +53,11 @@ def parse_install_lines(show, versions_store, yield_errors = False, user = None) d = Deployment.parse(line) name = d.application.name except app.NoSuchApplication as e: + if not e.location: + try: + e.location = line.split(':')[0] + except IndexError: + e.location = line if yield_errors: yield e continue @@ -138,6 +148,8 @@ class Deployment(object): elif not has_git and not has_scripts: if os.path.isfile(".scripts-version"): raise NotMigratedError(self.location) + else: + raise NotAutoinstallError(self.location) def verifyTag(self, srv_path): """ @@ -146,7 +158,7 @@ class Deployment(object): """ repo = self.application.repository(srv_path) try: - shell.Shell().eval("git", "--git-dir", repo, "rev-parse", self.app_version.scripts_tag, '--') + shell.eval("git", "--git-dir", repo, "rev-parse", self.app_version.scripts_tag, '--') except shell.CallError: raise NoTagError(self.app_version.scripts_tag) @@ -158,13 +170,12 @@ class Deployment(object): corresponds to the one in the remote repository. """ with util.ChangeDirectory(self.location): - sh = shell.Shell() repo = self.application.repository(srv_path) def repo_rev_parse(tag): - return sh.eval("git", "--git-dir", repo, "rev-parse", tag) + return shell.eval("git", "--git-dir", repo, "rev-parse", tag) def self_rev_parse(tag): try: - return sh.safeCall("git", "rev-parse", tag, strip=True) + return shell.safeCall("git", "rev-parse", tag, strip=True) except shell.CallError: raise NoLocalTagError(tag) def compare_tags(tag): @@ -174,7 +185,7 @@ class Deployment(object): if not compare_tags(self.app_version.scripts_tag): raise InconsistentScriptsTagError(self.app_version.scripts_tag) parent = repo_rev_parse(self.app_version.scripts_tag) - merge_base = sh.safeCall("git", "merge-base", parent, "HEAD", strip=True) + merge_base = shell.safeCall("git", "merge-base", parent, "HEAD", strip=True) if merge_base != parent: raise HeadNotDescendantError(self.app_version.scripts_tag) @@ -191,11 +202,22 @@ class Deployment(object): Checks if our version and the version number recorded in a file are consistent. """ + real = self.detectVersion() + if not str(real) == self.app_version.pristine_tag.partition('-')[2]: + raise VersionMismatchError(real, self.version) + + @chdir_to_location + def detectVersion(self): + """ + Returns the real version, based on filesystem, of install. + + Throws a :class:`VersionDetectionError` if we couldn't figure out + what the real version was. + """ real = self.application.detectVersion(self) if not real: raise VersionDetectionError - elif not str(real) == self.app_version.pristine_tag.partition('-')[2]: - raise VersionMismatchError(real, self.version) + return real @property @chdir_to_location @@ -270,7 +292,7 @@ class Deployment(object): except old_log.ScriptsVersionNoSuchFile: pass if not self._app_version: - appname = shell.Shell().eval("git", "config", "remote.origin.url").rpartition("/")[2].partition(".")[0] + appname = shell.eval("git", "config", "remote.origin.url").rpartition("/")[2].partition(".")[0] self._app_version = app.ApplicationVersion.make(appname, "unknown") return self._app_version @property @@ -287,6 +309,12 @@ class Deployment(object): if not self._url: raise UnknownWebPath return self._url + def enableOldStyleUrls(self): + """ + Switches to using http://user.scripts.mit.edu/~user/app URLs. + No effect if they have an explicit .scripts/url override. + """ + self._url = scripts.fill_url(self.location, self.application.url(self), old_style = True) @staticmethod def parse(line): """ @@ -326,15 +354,44 @@ class ProductionCopy(Deployment): """ Performs a backup of database schemas and other non-versioned data. """ - backupdir = os.path.join(self.location, ".scripts", "backups") - backup = str(self.version) + "-" + datetime.date.today().isoformat() - outdir = os.path.join(backupdir, backup) + # There are retarded amounts of race-safety in this function, + # because we do NOT want to claim to have made a backup, when + # actually something weird happened to it. + backupdir = os.path.join(self.scripts_dir, "backups") if not os.path.exists(backupdir): - os.mkdir(backupdir) - if os.path.exists(outdir): - util.safe_unlink(outdir) - os.mkdir(outdir) - self.application.backup(self, outdir, options) + try: + os.mkdir(backupdir) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + tmpdir = tempfile.mkdtemp() # actually will be kept around + try: + self.application.backup(self, tmpdir, options) + except app.BackupFailure: + # the backup is bogus, don't let it show up + shutil.rmtree(tmpdir) + raise + backup = None + with util.LockDirectory(os.path.join(backupdir, "lock")): + while 1: + backup = str(self.version) + "-" + datetime.datetime.today().strftime("%Y-%m-%dT%H%M%S") + outdir = os.path.join(backupdir, backup) + if os.path.exists(outdir): + logging.warning("Backup: A backup occurred in the last second. Trying again in a second...") + time.sleep(1) + continue + try: + shutil.move(tmpdir, outdir) + except: + # don't leave half-baked stuff lying around + try: + shutil.rmtree(outdir) + except OSError: + pass + raise + break return backup @chdir_to_location def restore(self, backup, options): @@ -353,6 +410,12 @@ class ProductionCopy(Deployment): this application uses. """ self.application.remove(self, options) + def verifyDatabase(self): + """ + Checks if the autoinstall has a properly configured database. + """ + if not self.application.checkDatabase(self): + raise DatabaseVerificationError def verifyWeb(self): """ Checks if the autoinstall is viewable from the web. @@ -363,7 +426,7 @@ class ProductionCopy(Deployment): """ Performs a HTTP request on the website. """ - return util.fetch(self.url.netloc, self.url.path, path, post) + return util.fetch(self.url.netloc, self.url.path, path, post) # pylint: disable-msg=E1103 class WorkingCopy(Deployment): """ @@ -371,6 +434,13 @@ class WorkingCopy(Deployment): modifications to without fear of interfering with a production deployment. More operations are permitted on these copies. """ + def setAppVersion(self, app_version): + """ + Manually resets the application version; useful if the working + copy is off in space (i.e. not anchored to something we can + git describe off of.) + """ + self._app_version = app_version @chdir_to_location def parametrize(self, deployment): """ @@ -461,17 +531,21 @@ ERROR: Directory contains a .scripts directory, but not a .git directory.""" class NotAutoinstallError(Error): - """The directory was not an autoinstall, due to missing .scripts-version file.""" - #: Directory in question + """Application is not an autoinstall.""" + #: Directory of the not autoinstall dir = None def __init__(self, dir): self.dir = dir def __str__(self): return """ -ERROR: Could not find .scripts-version file. Are you sure -this is an autoinstalled application? -""" +ERROR: The directory + + %s + +does not appear to be an autoinstall. If you are in a +subdirectory of an autoinstall, you need to use the root +directory for the autoinstall.""" % self.dir class NoTagError(Error): """Deployment has a tag that does not have an equivalent in upstream repository.""" @@ -564,6 +638,15 @@ web. This may indicate that the website is behind authentication on the htaccess level. You can find the contents of the page from the debug backtraces.""" +class DatabaseVerificationError(Error): + """Could not access the database""" + def __str__(self): + return """ + +ERROR: We were not able to access the database for +this application; this probably means that your database +configuration is misconfigured.""" + class UnknownWebPath(Error): """Could not determine application's web path.""" def __str__(self): @@ -574,4 +657,3 @@ host and path were in order to perform a web request on the application. You can specify this manually using the WIZARD_WEB_HOST and WIZARD_WEB_PATH environment variables.""" -