X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/538e3c912f767bc12785455d107f2b3e11c28a73..6554c6378a6e801b4fe47c50688cdae1d627bc18:/wizard/deploy.py diff --git a/wizard/deploy.py b/wizard/deploy.py index 9e5697b..6d3ec67 100644 --- a/wizard/deploy.py +++ b/wizard/deploy.py @@ -1,318 +1,659 @@ +""" +Object model for querying information and manipulating deployments +of autoinstalls. Every :class:`Deployment` has an :class:`app.ApplicationVersion` +which in turn has an :class:`app.Application`. +""" + import os.path import fileinput -import dateutil.parser -import distutils.version +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 + +## -- Global Functions -- + +def get_install_lines(versions_store, user=None): + """ + Low level function that retrieves a list of lines from the + :term:`versions store` that can be passed to :meth:`Deployment.parse`. + """ + if os.path.isfile(versions_store): + return fileinput.input([versions_store]) + if user: + return fileinput.input([versions_store + "/" + user]) + return fileinput.input([versions_store + "/" + f for f in sorted(os.listdir(versions_store))]) -def getInstallLines(vs): - """Retrieves a list of lines from the version directory that - can be passed to Deployment.parse()""" - if os.path.isfile(vs): - return fileinput.input([vs]) - return fileinput.input([vs + "/" + f for f in os.listdir(vs)]) +def parse_install_lines(show, versions_store, yield_errors = False, user = None): + """ + Generator function for iterating through all autoinstalls. + Each item is an instance of :class:`Deployment`, or possibly + a :class:`wizard.deploy.Error` if ``yield_errors`` is ``True``. You can + filter out applications and versions by specifying ``app`` + or ``app-1.2.3`` in ``show``. This function may generate + log output. + """ + if not show: + show = app.applications() + elif isinstance(show, str): + # otherwise, frozenset will treat string as an iterable + show = frozenset([show]) + else: + show = frozenset(show) + for line in get_install_lines(versions_store, user): + # construction + try: + 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 + except Error: + # we consider this a worse error + logging.warning("Error with '%s'" % line.rstrip()) + continue + # filter + if name + "-" + str(d.version) in show or name in show: + pass + else: + continue + # yield + yield d + +## -- Model Objects -- + +@decorator.decorator +def chdir_to_location(f, self, *args, **kwargs): + """ + Decorator for making a function have working directory + :attr:`Deployment.location`. + """ + with util.ChangeDirectory(self.location): + return f(self, *args, **kwargs) class Deployment(object): - """Represents a deployment of an autoinstall; i.e. a concrete - directory in web_scripts that has .scripts-version in it.""" - def __init__(self, location, log=None, version=None): - """ `location` Location of the deployment - `version` ApplicationVersion of the app (this is cached info) - `log` DeployLog of the app""" - self.location = location - self._version = version - self._log = log - # XXX: factory - @staticmethod - def parse(line): - """Parses a line from the results of parallel-find.pl. - This will work out of the box with fileinput, see - getInstallLines()""" - line = line.rstrip() + """ + Represents a deployment of an autoinstall, e.g. directory + that has ``.scripts`` directory or ``.scripts-version`` + file 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. + """ + #: 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 + 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 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): + """ + Checks if this is an autoinstall, throws an exception if there + are problems. + """ + with util.ChangeDirectory(self.location): + has_git = os.path.isdir(".git") + has_scripts = os.path.isdir(".scripts") + if not has_git and has_scripts: + raise CorruptedAutoinstallError(self.location) + elif has_git and not has_scripts: + raise AlreadyVersionedError(self.location) + 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): + """ + Checks if the purported version has a corresponding tag + in the upstream repository. + """ + repo = self.application.repository(srv_path) try: - location, deploydir = line.split(":") - except ValueError: - return Deployment(line) # lazy loaded version - return Deployment(location, version=ApplicationVersion.parse(deploydir, location)) - @staticmethod - def fromDir(dir): - """Lazily creates a deployment from a directory""" - return Deployment(dir) + shell.eval("git", "--git-dir", repo, "rev-parse", self.app_version.scripts_tag, '--') + except shell.CallError: + raise NoTagError(self.app_version.scripts_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.scripts_tag): + raise InconsistentScriptsTagError(self.app_version.scripts_tag) + parent = repo_rev_parse(self.app_version.scripts_tag) + merge_base = shell.safeCall("git", "merge-base", parent, "HEAD", strip=True) + if merge_base != parent: + raise HeadNotDescendantError(self.app_version.scripts_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 - def version_file(self): + @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.scripts_dir) + @property + def scripts_dir(self): + """The absolute path of the ``.scripts`` directory.""" + return os.path.join(self.location, '.scripts') + @property + def old_version_file(self): + """ + The absolute path of either ``.scripts-version`` (for unmigrated + installs) or ``.scripts/version``. + + .. note:: + + Use of this is discouraged for migrated installs. + """ return os.path.join(self.location, '.scripts-version') @property + def version_file(self): + """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 @property - def log(self): - if not self._log: - self._log = DeployLog.load(self.version_file) - return self._log + 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): - """Returns the distutils Version of the deployment""" + """ + The :class:`distutils.version.LooseVersion` of this + deployment. + """ return self.app_version.version @property - def app_version(self, force = False): - """Returns the ApplicationVersion of the deployment""" - if self._version and not force: return self._version - else: return self.log[-1].version - -class Application(object): - """Represents the generic notion of an application, i.e. - mediawiki or phpbb.""" - def __init__(self, name): - self.name = name - self.versions = {} + 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: + 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 repository(self): - """Returns the Git repository that would contain this application.""" - repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git") - if not os.path.isdir(repo): - raise NoRepositoryError(app) - return repo - def makeVersion(self, version): - if version not in self.versions: - self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self) - return self.versions[version] - -class DeployLog(list): - # As per #python; if you decide to start overloading magic methods, - # we should remove this subclass - """Equivalent to .scripts-version: a series of DeployRevisions.""" - def __init__(self, revs = []): - """`revs` List of DeployRevision objects""" - list.__init__(self, revs) # pass to list - # XXX: factory - @staticmethod - def load(file): - """Loads a scripts version file and parses it into - DeployLog and DeployRevision objects""" - # XXX: DIRTY DIRTY HACK - # What we should actually do is parse the git logs - scriptsdir = os.path.join(os.path.dirname(file), ".scripts") - if os.path.isdir(scriptsdir): - file = os.path.join(scriptsdir, "old-version") - i = 0 - rev = DeployRevision() - revs = [] - def append(rev): - if i: - if i != 4: - raise ScriptsVersionNotEnoughFieldsError(file) - revs.append(rev) - try: - fh = open(file) - except IOError: - raise ScriptsVersionNoSuchFile(file) - for line in fh: - line = line.rstrip() - if not line: - append(rev) - i = 0 - rev = DeployRevision() - continue - if i == 0: - # we need the dateutil parser in order to - # be able to parse time offsets - rev.datetime = dateutil.parser.parse(line) - elif i == 1: - rev.user = line - elif i == 2: - rev.source = DeploySource.parse(line) - elif i == 3: - rev.version = ApplicationVersion.parse(line, rev.source) - else: - # ruh oh - raise ScriptsVersionTooManyFieldsError(file) - i += 1 - append(rev) - return DeployLog(revs) - def __repr__(self): - return '' % list.__repr__(self) - -class DeployRevision(object): - """A single entry in the .scripts-version file. Contains who deployed - this revision, what application version this is, etc.""" - def __init__(self, datetime=None, user=None, source=None, version=None): - """ `datetime` Time this revision was deployed - `user` Person who deployed this revision, in user@host format. - `source` Instance of DeploySource - `version` Instance of ApplicationVersion - Note: This object is typically built incrementally.""" - self.datetime = datetime - self.user = user - self.source = source - self.version = version - -class DeploySource(object): - """Source of the deployment; see subclasses for examples""" - def __init__(self): - raise NotImplementedError # abstract class - # XXX: factory + 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 + 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): - # munge out common prefix - rel = os.path.relpath(line, "/afs/athena.mit.edu/contrib/scripts/") - parts = rel.split("/") - if parts[0] == "wizard": - return WizardUpdate() - elif parts[0] == "deploy" or parts[0] == "deploydev": - isDev = ( parts[0] == "deploydev" ) + """ + Parses a line from the :term:`versions store`. + + .. note:: + + Use this method only when speed is of the utmost + importance. You should prefer to directly create a deployment + with only a ``location`` when possible. + """ + line = line.rstrip() + try: + location, deploydir = line.split(":") + except ValueError: + return ProductionCopy(line) # lazy loaded version + try: + return ProductionCopy(location, version=app.ApplicationVersion.parse(deploydir)) + except Error as e: + e.location = location + raise e + +class ProductionCopy(Deployment): + """ + Represents the production copy of a deployment. This copy + is canonical, and is the only one guaranteed to be accessible + via web, have a database, etc. + """ + @chdir_to_location + def upgrade(self, version, options): + """ + Performs an upgrade of database schemas and other non-versioned data. + """ + return self.application.upgrade(self, version, options) + @chdir_to_location + def backup(self, options): + """ + Performs a backup of database schemas and other non-versioned data. + """ + # 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: - if parts[1] == "updates": - return OldUpdate(isDev) + os.mkdir(backupdir) + except OSError as e: + if e.errno == errno.EEXIST: + pass else: - return TarballInstall(line, isDev) - except IndexError: - pass - return UnknownDeploySource(line) + 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): + """ + Restores a backup. Destroys state, so be careful! Also, this does + NOT restore the file-level backup, which is what 'wizard restore' + does, so you probably do NOT want to call this elsewhere unless + you know what you're doing (call 'wizard restore' instead). + """ + 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 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. + """ + if not self.application.checkWeb(self): + raise WebVerificationError + def fetch(self, path, post=None): + """ + Performs a HTTP request on the website. + """ + return util.fetch(self.url.netloc, self.url.path, path, post) # pylint: disable-msg=E1103 -class TarballInstall(DeploySource): - """Original installation from tarball, characterized by - /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z.tar.gz +class WorkingCopy(Deployment): """ - def __init__(self, location, isDev): - self.location = location - self.isDev = isDev - -class OldUpdate(DeploySource): - """Upgrade using old upgrade infrastructure, characterized by - /afs/athena.mit.edu/contrib/scripts/deploydev/updates/update-scripts-version.pl + Represents a temporary clone of a deployment that we can make + modifications to without fear of interfering with a production + deployment. More operations are permitted on these copies. """ - def __init__(self, isDev): - self.isDev = isDev + 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): + """ + Edits files in ``dir`` to replace WIZARD_* variables with literal + 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, deployment) + @chdir_to_location + def prepareConfig(self): + """ + Edits files in the deployment such that any user-specific configuration + is replaced with generic WIZARD_* variables. + """ + return self.application.prepareConfig(self) + @chdir_to_location + def resolveConflicts(self): + """ + Resolves conflicted files in this working copy. Returns whether or + not all conflicted files were resolved or not. Fully resolved + files are added to the index, but no commit is made. + """ + return self.application.resolveConflicts(self) + @chdir_to_location + def prepareMerge(self): + """ + Performs various edits to files in the current working directory in + order to make a merge go more smoothly. This is usually + used to fix botched line-endings. + """ + return self.application.prepareMerge(self) -class WizardUpdate(DeploySource): - """Upgrade using wizard infrastructure, characterized by - /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK - """ - def __init__(self): - pass - -class UnknownDeploySource(DeploySource): - """Deployment that we don't know the meaning of. Wot!""" - def __init__(self, line): - self.line = line - -class ApplicationVersion(object): - """Represents an abstract notion of a version for an application""" - def __init__(self, version, application): - """ `version` Instance of distutils.LooseVersion - `application` Instance of Application - WARNING: Please don't call this directly; instead, use getVersion() - on the application you want, so that this version gets registered.""" - self.version = version - self.application = application - @property - def scripts_tag(self): - """Returns the name of the Git tag for this version""" - # XXX: This assumes that there's only a -scripts version - # which will not be true in the future. Unfortunately, finding - # the "true" latest version is computationally expensive - return "v%s-scripts" % self.version - def __cmp__(x, y): - return cmp(x.version, y.version) - # XXX: move to factory - @staticmethod - def parse(deploydir,location,applookup=None): - # The version of the deployment, will be: - # /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs - name = deploydir.split("/")[-1] - try: - if name.find(" ") != -1: - raw_app, raw_version = name.split(" ") - version = raw_version[1:] # remove leading v - app, _ = raw_app.split(".") # remove trailing .git - elif name.find("-") != -1: - app, _, version = name.partition("-") - else: - app = name - version = "trunk" - except ValueError: # mostly from the a, b = foo.split(' ') - raise DeploymentParseError(deploydir, location) - if not applookup: applookup = applications - try: - # defer to the application for version creation - return applookup[app].makeVersion(version) - except KeyError: - raise NoSuchApplication(app, location) +## -- Exceptions -- class Error(wizard.Error): - """Base error class for deploy errors""" - def __init__(self, location): - self.location = location + """Base error class for this module""" + pass + +class NotMigratedError(Error): + """ + The deployment contains a .scripts-version file, but no .git + or .scripts directory. + """ + #: Directory of deployment + dir = None + def __init__(self, dir): + self.dir = dir def __str__(self): - return "ERROR: Generic error at %s" % self.location + return """This installation was not migrated""" -class NoRepositoryError(Error): - def __init__(self, app): - self.app = app - self.location = "unknown" +class AlreadyVersionedError(Error): + """The deployment contained a .git directory but no .scripts directory.""" + #: Directory of deployment + dir = None + def __init__(self, dir): + self.dir = dir def __str__(self): return """ -ERROR: Could not find repository for this application. Have -you converted the repository over? Is the name %s -the same as the name of the .git folder? -""" % self.app +ERROR: Directory contains a .git directory, but not +a .scripts directory. If this is not a corrupt +migration, this means that the user was versioning their +install using Git.""" -class NoSuchApplication(Error): - def __init__(self, name, location): - self.name = name - self.location = location +class NotConfiguredError(Error): + """The install was missing essential configuration.""" + #: Directory of unconfigured install + dir = None + def __init__(self, dir): + self.dir = dir def __str__(self): - return "ERROR: Unrecognized app '%s' at %s" % (self.name, self.location) + return """ -class DeploymentParseError(Error): - def __init__(self, malformed, location): - self.malformed = malformed - self.location = location +ERROR: The install was well-formed, but not configured +(essential configuration files were not found.)""" + +class CorruptedAutoinstallError(Error): + """The install was missing a .git directory, but had a .scripts directory.""" + #: Directory of the corrupted install + dir = None + def __init__(self, dir): + self.dir = dir def __str__(self): - return """ERROR: Unparseable '%s' at %s""" % (self.malformed, self.location) + return """ -class ScriptsVersionError(Error): - """Errors specific to the parsing of a full .scripts-version file - (errors that could also be triggered while parsing a parallel-find - output should not be this subclass.)""" - pass +ERROR: Directory contains a .scripts directory, +but not a .git directory.""" -class ScriptsVersionTooManyFieldsError(ScriptsVersionError): +class NotAutoinstallError(Error): + """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 parse .scripts-version file. It -contained too many fields. -""" +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 ScriptsVersionNotEnoughFieldsError(ScriptsVersionError): +class NoTagError(Error): + """Deployment has a tag that does not have an equivalent in upstream repository.""" + #: Missing tag + tag = None + def __init__(self, tag): + self.tag = tag def __str__(self): return """ -ERROR: Could not parse .scripts-version file. It -didn't contain enough fields. -""" +ERROR: Could not find tag %s in repository.""" % self.tag + +class NoLocalTagError(Error): + """Could not find tag in local repository.""" + #: Missing tag + tag = None + def __init__(self, tag): + self.tag = tag + def __str__(self): + return """ + +ERROR: Could not find tag %s in local repository.""" % self.tag + +class InconsistentPristineTagError(Error): + """Pristine tag commit ID does not match upstream pristine tag commit ID.""" + #: Inconsistent tag + tag = None + def __init__(self, tag): + self.tag = tag + def __str__(self): + return """ + +ERROR: Local pristine tag %s did not match repository's. This +probably means an upstream rebase occured.""" % self.tag + +class InconsistentScriptsTagError(Error): + """Scripts tag commit ID does not match upstream scripts tag commit ID.""" + #: Inconsistent tag + tag = None + def __init__(self, tag): + self.tag = tag + def __str__(self): + return """ + +ERROR: Local scripts tag %s did not match repository's. This +probably means an upstream rebase occurred.""" % self.tag + +class HeadNotDescendantError(Error): + """HEAD is not connected to tag.""" + #: Tag that HEAD should have been descendant of. + tag = None + def __init__(self, tag): + self.tag = tag + def __str__(self): + return """ -class ScriptsVersionNoSuchFile(ScriptsVersionError): - def __init__(self, file): - self.file = file +ERROR: HEAD is not a descendant of %s. This probably +means that an upstream rebase occurred, and new tags were +pulled, but local user commits were never rebased.""" % self.tag + +class VersionDetectionError(Error): + """Could not detect real version of application.""" def __str__(self): return """ -ERROR: File %s didn't exist. -""" % self.file +ERROR: Could not detect the real version of the application.""" + +class VersionMismatchError(Error): + """Git version of application does not match detected version.""" + #: Detected version + real_version = None + #: Version from Git + git_version = None + def __init__(self, real_version, git_version): + self.real_version = real_version + self.git_version = git_version + def __str__(self): + return """ -# If you want, you can wrap this up into a registry and access things -# through that, but it's not really necessary +ERROR: The detected version %s did not match the Git +version %s.""" % (self.real_version, self.git_version) + +class WebVerificationError(Error): + """Could not access the application on the web""" + def __str__(self): + return """ -application_list = [ - "mediawiki", "wordpress", "joomla", "e107", "gallery2", - "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django", - # these are technically deprecated - "advancedpoll", "gallery", -] +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. You can find +the contents of the page from the debug backtraces.""" -"""Hash table for looking up string application name to instance""" -applications = dict([(n,Application(n)) for n in application_list ]) +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): + return """ +ERROR: We were not able to determine what the application's +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."""