X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/c6bc1bde279253712b4c85d99f6f6b883fac4f1c..30380c4b5b28df9670ea5952e14bc485d1d34133:/wizard/deploy.py diff --git a/wizard/deploy.py b/wizard/deploy.py index 005a602..67c4118 100644 --- a/wizard/deploy.py +++ b/wizard/deploy.py @@ -6,14 +6,17 @@ which in turn has an :class:`app.Application`. import os.path import fileinput -import dateutil.parser -import tempfile import logging -import shutil 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, util +from wizard import app, git, old_log, scripts, shell, sql, util ## -- Global Functions -- @@ -96,6 +99,8 @@ class Deployment(object): # some cache variables self._read_cache = {} self._old_log = None + self._dsn = None + self._url = None def invalidateCache(self): """ Invalidates all cached variables. This currently applies to @@ -146,7 +151,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 +163,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 +178,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) @@ -226,6 +230,14 @@ class Deployment(object): """The absolute path of the ``.scripts/version`` file.""" return os.path.join(self.scripts_dir, 'version') @property + def dsn_file(self): + """The absolute path of the :file:`.scripts/dsn` override file.""" + return os.path.join(self.scripts_dir, 'dsn') + @property + def url_file(self): + """The absolute path of the :file:`.scripts/url` override file.""" + return os.path.join(self.scripts_dir, 'url') + @property def application(self): """The :class:`app.Application` of this deployment.""" return self.app_version.application @@ -257,8 +269,28 @@ class Deployment(object): except shell.CallError: pass if not self._app_version: - self._app_version = self.old_log[-1].version + try: + self._app_version = self.old_log[-1].version + except old_log.ScriptsVersionNoSuchFile: + pass + if not self._app_version: + 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 + def dsn(self): + """The :class:`sqlalchemy.engine.url.URL` for this deployment.""" + if not self._dsn: + self._dsn = sql.fill_url(self.application.dsn(self)) + return self._dsn + @property + def url(self): + """The :class:`urlparse.ParseResult` for this deployment.""" + if not self._url: + self._url = scripts.fill_url(self.location, self.application.url(self)) + if not self._url: + raise UnknownWebPath + return self._url @staticmethod def parse(line): """ @@ -298,7 +330,45 @@ class ProductionCopy(Deployment): """ Performs a backup of database schemas and other non-versioned data. """ - return self.application.backup(self, options) + # 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): + 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): """ @@ -307,23 +377,26 @@ class ProductionCopy(Deployment): does, so you probably do NOT want to call this elsewhere unless you know what you're doing (call 'wizard restore' instead). """ - return self.application.restore(self, backup, options) + backup_dir = os.path.join(".scripts", "backups", backup) + return self.application.restore(self, backup_dir, options) + @chdir_to_location + def remove(self, options): + """ + Deletes all non-local or non-filesystem data (such as databases) that + this application uses. + """ + self.application.remove(self, options) def verifyWeb(self): """ Checks if the autoinstall is viewable from the web. """ - out = [] - if not self.application.checkWeb(self, out): - raise WebVerificationError(out[0]) + if not self.application.checkWeb(self): + raise WebVerificationError def fetch(self, path, post=None): """ Performs a HTTP request on the website. """ - try: - host, basepath = scripts.get_web_host_and_path(self.location) - except (ValueError, TypeError): - raise UnknownWebPath - return util.fetch(host, basepath, path, post) + return util.fetch(self.url.netloc, self.url.path, path, post) # pylint: disable-msg=E1103 class WorkingCopy(Deployment): """ @@ -332,13 +405,14 @@ class WorkingCopy(Deployment): deployment. More operations are permitted on these copies. """ @chdir_to_location - def parametrize(self): + def parametrize(self, deployment): """ Edits files in ``dir`` to replace WIZARD_* variables with literal - instances. This is used for constructing virtual merge bases, and - as such dir will generally not equal :attr:`location`. + instances based on ``deployment``. This is used for constructing + virtual merge bases, and as such ``deployment`` will generally not + equal ``self``. """ - return self.application.parametrize(self) + return self.application.parametrize(self, deployment) @chdir_to_location def prepareConfig(self): """ @@ -515,19 +589,13 @@ version %s.""" % (self.real_version, self.git_version) class WebVerificationError(Error): """Could not access the application on the web""" - #: Contents of web page access - contents = None - def __init__(self, contents): - self.contents = contents def __str__(self): return """ ERROR: We were not able to access the application on the web. This may indicate that the website is behind -authentication on the htaccess level. The contents -of the page were: - -%s""" % self.contents +authentication on the htaccess level. You can find +the contents of the page from the debug backtraces.""" class UnknownWebPath(Error): """Could not determine application's web path."""