+ """
+ Represents a deployment of an autoinstall, e.g. directory that has a
+ ``.wizard`` directory in it. Supply ``version`` with an
+ :class:`ApplicationVersion` only if you were reading from the
+ :term:`versions store` and care about speed (data from there can be
+ stale).
+
+ The Deployment interface is somewhat neutered, so you may
+ want to use :class:`WorkingCopy` or :class:`ProductionCopy` for
+ more powerful operations.
+
+ .. note::
+
+ For legacy purposes, deployments can also be marked by a
+ ``.scripts`` directory or a ``.scripts-version`` file.
+ """
+ #: Absolute path to the deployment
+ location = None
+ def __init__(self, location, version=None):
+ self.location = os.path.abspath(location)
+ self._app_version = version
+ # some cache variables
+ self._read_cache = {}
+ self._old_log = None
+ self._dsn = None
+ self._url = None
+ self._urlGen = None
+ self._wizard_dir = None
+ def invalidateCache(self):
+ """
+ Invalidates all cached variables. This currently applies to
+ :attr:`app_version`, :attr:`old_log` and :meth:`read`.
+ """
+ self._app_version = None
+ self._read_cache = {}
+ self._old_log = None
+ 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) or there is no metadata to be heard of.
+ """
+ self._app_version = app_version
+ def read(self, file, force = False):
+ """
+ Reads a file's contents, possibly from cache unless ``force``
+ is ``True``.
+ """
+ if force or file not in self._read_cache:
+ f = open(os.path.join(self.location, file))
+ self._read_cache[file] = f.read()
+ f.close()
+ return self._read_cache[file]
+ def extract(self):
+ """
+ Extracts all the values of all variables from deployment.
+ These variables may be used for parametrizing generic parent
+ commits and include things such as database access credentials
+ and local configuration.
+ """
+ return self.application.extract(self)
+
+ def verify(self, no_touch=False):
+ """
+ Checks if this is an autoinstall, throws an exception if there
+ are problems. If ``no_touch`` is ``True``, it will not attempt
+ edit the installation.
+ """
+ with util.ChangeDirectory(self.location):
+ has_git = os.path.isdir(".git")
+ has_wizard = os.path.isdir(".wizard")
+ if not has_wizard and os.path.isdir(".scripts"):
+ # LEGACY
+ os.symlink(".scripts", ".wizard")
+ has_wizard = True
+ if not has_git and has_wizard:
+ raise CorruptedAutoinstallError(self.location)
+ elif has_git and not has_wizard:
+ raise AlreadyVersionedError(self.location)
+ # LEGACY
+ elif not has_git and not has_wizard:
+ if os.path.isfile(".scripts-version"):
+ raise NotMigratedError(self.location)
+ else:
+ raise NotAutoinstallError(self.location)
+
+ def verifyTag(self, srv_path):
+ """
+ Checks if the purported version has a corresponding tag
+ in the upstream repository.
+ """
+ repo = self.application.repository(srv_path)
+ try:
+ shell.eval("git", "--git-dir", repo, "rev-parse", self.app_version.wizard_tag, '--')
+ except shell.CallError:
+ raise NoTagError(self.app_version.wizard_tag)
+
+ def verifyGit(self, srv_path):
+ """
+ Checks if the autoinstall's Git repository makes sense,
+ checking if the tag is parseable and corresponds to
+ a real application, and if the tag in this repository
+ corresponds to the one in the remote repository.
+ """
+ with util.ChangeDirectory(self.location):
+ repo = self.application.repository(srv_path)
+ def repo_rev_parse(tag):
+ return shell.eval("git", "--git-dir", repo, "rev-parse", tag)
+ def self_rev_parse(tag):
+ try:
+ return shell.safeCall("git", "rev-parse", tag, strip=True)
+ except shell.CallError:
+ raise NoLocalTagError(tag)
+ def compare_tags(tag):
+ return repo_rev_parse(tag) == self_rev_parse(tag)
+ if not compare_tags(self.app_version.pristine_tag):
+ raise InconsistentPristineTagError(self.app_version.pristine_tag)
+ if not compare_tags(self.app_version.wizard_tag):
+ raise InconsistentWizardTagError(self.app_version.wizard_tag)
+ parent = repo_rev_parse(self.app_version.wizard_tag)
+ merge_base = shell.safeCall("git", "merge-base", parent, "HEAD", strip=True)
+ if merge_base != parent:
+ raise HeadNotDescendantError(self.app_version.wizard_tag)
+
+ def verifyConfigured(self):
+ """
+ Checks if the autoinstall is configured running.
+ """
+ if not self.configured:
+ raise NotConfiguredError(self.location)
+
+ @chdir_to_location
+ def verifyVersion(self):
+ """
+ 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
+ return real
+
+ @property
+ @chdir_to_location
+ def configured(self):
+ """Whether or not an autoinstall has been configured/installed for use."""
+ return self.application.checkConfig(self)
+ @property
+ def migrated(self):
+ """Whether or not the autoinstalls has been migrated."""
+ return os.path.isdir(self.wizard_dir)
+ @property
+ def wizard_dir(self):
+ """The absolute path of the Wizard directory."""
+ return os.path.join(self.location, ".wizard")
+ @property
+ def backup_dir(self):
+ """The absolute path to ``.wizard/backups``."""
+ return os.path.join(self.wizard_dir, "backups")
+ # LEGACY
+ @property
+ def old_version_file(self):
+ """
+ The absolute path of either ``.scripts-version``.
+ """
+ return os.path.join(self.location, '.scripts-version')
+ @property
+ def blacklisted_file(self):
+ """The absolute path of the ``.wizard/blacklisted`` file."""
+ return os.path.join(self.wizard_dir, 'blacklisted')
+ @property
+ def pending_file(self):
+ """The absolute path of the ``.wizard/pending`` file."""
+ return os.path.join(self.wizard_dir, 'pending')
+ @property
+ def version_file(self):
+ """The absolute path of the ``.wizard/version`` file."""
+ return os.path.join(self.wizard_dir, 'version')
+ @property
+ def dsn_file(self):
+ """The absolute path of the :file:`.wizard/dsn` override file."""
+ return os.path.join(self.wizard_dir, 'dsn')
+ @property
+ def url_file(self):
+ """The absolute path of the :file:`.wizard/url` override file."""
+ return os.path.join(self.wizard_dir, 'url')
+ @property
+ def application(self):
+ """The :class:`app.Application` of this deployment."""
+ return self.app_version.application
+ @property
+ def old_log(self):
+ """
+ The :class:`wizard.old_log.Log` of this deployment. This
+ is only applicable to un-migrated autoinstalls.
+ """
+ if not self._old_log:
+ self._old_log = old_log.DeployLog.load(self)
+ return self._old_log
+ @property
+ def version(self):
+ """
+ The :class:`distutils.version.LooseVersion` of this
+ deployment.
+ """
+ return self.app_version.version
+ @property
+ def app_version(self):
+ """The :class:`app.ApplicationVersion` of this deployment."""
+ if not self._app_version:
+ if os.path.isdir(os.path.join(self.location, ".git")):
+ try:
+ with util.ChangeDirectory(self.location):
+ appname, _, version = git.describe().partition('-')
+ self._app_version = app.ApplicationVersion.make(appname, version)
+ except shell.CallError:
+ pass
+ if not self._app_version:
+ # LEGACY
+ 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.auth(self.application.dsn(self))
+ return self._dsn
+ @property
+ def url(self):
+ """The :class:`urlparse.ParseResult` for this deployment."""
+ if not self._url:
+ self.nextUrl()
+ return self._url
+ def nextUrl(self):
+ """
+ Initializes :attr:`url` with a possible URL the web application may
+ be located at. It may be called again to switch to another possible
+ URL, usually in the event of a web access failure.
+ """
+ if not self._urlGen:
+ self._urlGen = web(self.location, self.application.url(self))
+ try:
+ self._url = self._urlGen.next() # pylint: disable-msg=E1101
+ return self._url
+ except StopIteration:
+ raise UnknownWebPath