]> scripts.mit.edu Git - wizard.git/blobdiff - wizard/deploy.py
Use CLI installer for MediaWiki 1.17.0 and later.
[wizard.git] / wizard / deploy.py
index ed69dc37c428bd1d453dd557a93b6b088388449b..12a27818cff1b4b4e235cd7830863b9ab048ae10 100644 (file)
@@ -1,21 +1,24 @@
 """
 Object model for querying information and manipulating deployments
 """
 Object model for querying information and manipulating deployments
-of autoinstalls.  Every :class:`Deployment` has an :class:`ApplicationVersion`
-which in turn has an :class:`Application`.
+of autoinstalls.  Every :class:`Deployment` has an :class:`app.ApplicationVersion`
+which in turn has an :class:`app.Application`.
 """
 
 import os.path
 import fileinput
 """
 
 import os.path
 import fileinput
-import dateutil.parser
-import distutils.version
-import tempfile
 import logging
 import logging
-import shutil
 import decorator
 import decorator
-import functools
+import datetime
+import tempfile
+import time
+import traceback
+import shutil
+import errno
+import pkg_resources
+import urlparse
 
 import wizard
 
 import wizard
-from wizard import git, old_log, scripts, shell, util
+from wizard import app, git, old_log, shell, sql, util
 
 ## -- Global Functions --
 
 
 ## -- Global Functions --
 
@@ -40,7 +43,7 @@ def parse_install_lines(show, versions_store, yield_errors = False, user = None)
     log output.
     """
     if not show:
     log output.
     """
     if not show:
-        show = applications()
+        show = app.applications()
     elif isinstance(show, str):
         # otherwise, frozenset will treat string as an iterable
         show = frozenset([show])
     elif isinstance(show, str):
         # otherwise, frozenset will treat string as an iterable
         show = frozenset([show])
@@ -51,7 +54,12 @@ def parse_install_lines(show, versions_store, yield_errors = False, user = None)
         try:
             d = Deployment.parse(line)
             name = d.application.name
         try:
             d = Deployment.parse(line)
             name = d.application.name
-        except NoSuchApplication as e:
+        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
             if yield_errors:
                 yield e
             continue
@@ -60,13 +68,51 @@ def parse_install_lines(show, versions_store, yield_errors = False, user = None)
             logging.warning("Error with '%s'" % line.rstrip())
             continue
         # filter
             logging.warning("Error with '%s'" % line.rstrip())
             continue
         # filter
-        if name + "-" + str(d.version) in show or name in show:
+        if name + "-" + util.truncate(str(d.version)) in show or name in show:
             pass
         else:
             continue
         # yield
         yield d
 
             pass
         else:
             continue
         # yield
         yield d
 
+def web(dir, url=None):
+    """
+    Attempts to determine the URL a directory would be web-accessible at.
+    If ``url`` is specified, automatically use it.  Returns a generator which
+    produces a list of candidate urls.
+
+    This function implements a plugin interface named :ref:`wizard.deploy.web`.
+    """
+    if url:
+        if isinstance(url, str):
+            url = urlparse.urlparse(url)
+        logging.info("wizard.deploy.web: Using default URL %s", url)
+        yield url
+        return
+
+    for entry in pkg_resources.iter_entry_points("wizard.deploy.web"):
+        logging.debug("wizard.deploy.web: Processing %s", entry)
+        f = entry.load()
+        for r in f(dir):
+            if isinstance(r, str):
+                r = urlparse.urlparse(r)
+            logging.info("wizard.deploy.web: Using plugin-supplied URL %s", r)
+            yield r
+
+    # try the environment
+    host = os.getenv("WIZARD_WEB_HOST")
+    path = os.getenv("WIZARD_WEB_PATH")
+    if host is not None and path is not None:
+        r = urlparse.ParseResult(
+                "http",
+                host,
+                path.rstrip('/'),
+                "", "", "")
+        logging.info("wizard.deploy.web: Using environment URL %s", r)
+        yield r
+
+    logging.info("wizard.deploy.web: Exhausted URLs")
+
 ## -- Model Objects --
 
 @decorator.decorator
 ## -- Model Objects --
 
 @decorator.decorator
@@ -80,15 +126,20 @@ def chdir_to_location(f, self, *args, **kwargs):
 
 class Deployment(object):
     """
 
 class Deployment(object):
     """
-    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).
+    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.
 
     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
     """
     #: Absolute path to the deployment
     location = None
@@ -98,6 +149,10 @@ class Deployment(object):
         # some cache variables
         self._read_cache = {}
         self._old_log = None
         # 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
     def invalidateCache(self):
         """
         Invalidates all cached variables.  This currently applies to
@@ -106,6 +161,13 @@ class Deployment(object):
         self._app_version = None
         self._read_cache = {}
         self._old_log = None
         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``
     def read(self, file, force = False):
         """
         Reads a file's contents, possibly from cache unless ``force``
@@ -125,21 +187,29 @@ class Deployment(object):
         """
         return self.application.extract(self)
 
         """
         return self.application.extract(self)
 
-    def verify(self):
+    def verify(self, no_touch=False):
         """
         Checks if this is an autoinstall, throws an exception if there
         """
         Checks if this is an autoinstall, throws an exception if there
-        are problems.
+        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")
         """
         with util.ChangeDirectory(self.location):
             has_git = os.path.isdir(".git")
-            has_scripts = os.path.isdir(".scripts")
-            if not has_git and has_scripts:
+            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)
                 raise CorruptedAutoinstallError(self.location)
-            elif has_git and not has_scripts:
+            elif has_git and not has_wizard:
                 raise AlreadyVersionedError(self.location)
                 raise AlreadyVersionedError(self.location)
-            elif not has_git and not has_scripts:
+            # LEGACY
+            elif not has_git and not has_wizard:
                 if os.path.isfile(".scripts-version"):
                     raise NotMigratedError(self.location)
                 if os.path.isfile(".scripts-version"):
                     raise NotMigratedError(self.location)
+                else:
+                    raise NotAutoinstallError(self.location)
 
     def verifyTag(self, srv_path):
         """
 
     def verifyTag(self, srv_path):
         """
@@ -148,9 +218,9 @@ class Deployment(object):
         """
         repo = self.application.repository(srv_path)
         try:
         """
         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.wizard_tag, '--')
         except shell.CallError:
         except shell.CallError:
-            raise NoTagError(self.app_version.scripts_tag)
+            raise NoTagError(self.app_version.wizard_tag)
 
     def verifyGit(self, srv_path):
         """
 
     def verifyGit(self, srv_path):
         """
@@ -160,25 +230,24 @@ class Deployment(object):
         corresponds to the one in the remote repository.
         """
         with util.ChangeDirectory(self.location):
         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):
             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:
             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):
                 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)
                 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 = sh.safeCall("git", "merge-base", parent, "HEAD", strip=True)
+            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:
             if merge_base != parent:
-                raise HeadNotDescendantError(self.app_version.scripts_tag)
+                raise HeadNotDescendantError(self.app_version.wizard_tag)
 
     def verifyConfigured(self):
         """
 
     def verifyConfigured(self):
         """
@@ -193,11 +262,22 @@ class Deployment(object):
         Checks if our version and the version number recorded in a file
         are consistent.
         """
         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
         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
 
     @property
     @chdir_to_location
@@ -207,29 +287,45 @@ class Deployment(object):
     @property
     def migrated(self):
         """Whether or not the autoinstalls has been migrated."""
     @property
     def migrated(self):
         """Whether or not the autoinstalls has been migrated."""
-        return os.path.isdir(self.scripts_dir)
+        return os.path.isdir(self.wizard_dir)
     @property
     @property
-    def scripts_dir(self):
-        """The absolute path of the ``.scripts`` directory."""
-        return os.path.join(self.location, '.scripts')
+    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):
         """
     @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.
+        The absolute path of either ``.scripts-version``.
         """
         return os.path.join(self.location, '.scripts-version')
     @property
         """
         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):
     def version_file(self):
-        """The absolute path of the ``.scripts/version`` file."""
-        return os.path.join(self.scripts_dir, 'version')
+        """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):
     @property
     def application(self):
-        """The :class:`Application` of this deployment."""
+        """The :class:`app.Application` of this deployment."""
         return self.app_version.application
     @property
     def old_log(self):
         return self.app_version.application
     @property
     def old_log(self):
@@ -249,18 +345,50 @@ class Deployment(object):
         return self.app_version.version
     @property
     def app_version(self):
         return self.app_version.version
     @property
     def app_version(self):
-        """The :class:`ApplicationVersion` of this deployment."""
+        """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('-')
         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 = ApplicationVersion.make(appname, version)
+                    self._app_version = app.ApplicationVersion.make(appname, version)
                 except shell.CallError:
                     pass
         if not self._app_version:
                 except shell.CallError:
                     pass
         if not self._app_version:
-            self._app_version = self.old_log[-1].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
         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
     @staticmethod
     def parse(line):
         """
     @staticmethod
     def parse(line):
         """
@@ -278,7 +406,7 @@ class Deployment(object):
         except ValueError:
             return ProductionCopy(line) # lazy loaded version
         try:
         except ValueError:
             return ProductionCopy(line) # lazy loaded version
         try:
-            return ProductionCopy(location, version=ApplicationVersion.parse(deploydir))
+            return ProductionCopy(location, version=app.ApplicationVersion.parse(deploydir))
         except Error as e:
             e.location = location
             raise e
         except Error as e:
             e.location = location
             raise e
@@ -300,7 +428,44 @@ class ProductionCopy(Deployment):
         """
         Performs a backup of database schemas and other non-versioned data.
         """
         """
         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.
+        if not os.path.exists(self.backup_dir):
+            try:
+                os.mkdir(self.backup_dir)
+            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(self.backup_dir, "lock")):
+            while 1:
+                backup = str(self.version) + "-" + datetime.datetime.today().strftime("%Y-%m-%dT%H%M%S")
+                outdir = os.path.join(self.backup_dir, 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):
         """
     @chdir_to_location
     def restore(self, backup, options):
         """
@@ -309,23 +474,39 @@ 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).
         """
         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)
+        return self.application.restore(self, os.path.join(self.backup_dir, backup), 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):
         """
     def verifyWeb(self):
         """
-        Checks if the autoinstall is viewable from the web.
+        Checks if the autoinstall is viewable from the web.  If you do not run
+        this, there is no guarantee that the url returned by this application
+        is the correct one.
         """
         """
-        out = []
-        if not self.application.checkWeb(self, out):
-            raise WebVerificationError(out[0])
+        while True:
+            if not self.application.checkWeb(self):
+                try:
+                    self.nextUrl()
+                except UnknownWebPath:
+                    raise WebVerificationError
+            else:
+                break
     def fetch(self, path, post=None):
         """
         Performs a HTTP request on the website.
         """
     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):
     """
 
 class WorkingCopy(Deployment):
     """
@@ -334,13 +515,14 @@ class WorkingCopy(Deployment):
     deployment.  More operations are permitted on these copies.
     """
     @chdir_to_location
     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
         """
         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):
         """
     @chdir_to_location
     def prepareConfig(self):
         """
@@ -365,307 +547,17 @@ class WorkingCopy(Deployment):
         """
         return self.application.prepareMerge(self)
 
         """
         return self.application.prepareMerge(self)
 
-class Application(object):
-    """
-    Represents an application, i.e. mediawiki or phpbb.
-
-    .. note::
-        Many of these methods assume a specific working
-        directory; prefer using the corresponding methods
-        in :class:`Deployment` and its subclasses.
-    """
-    #: String name of the application
-    name = None
-    #: Dictionary of version strings to :class:`ApplicationVersion`.
-    #: See also :meth:`makeVersion`.
-    versions = None
-    #: List of files that need to be modified when parametrizing.
-    #: This is a class-wide constant, and should not normally be modified.
-    parametrized_files = []
-    #: Keys that are used in older versions of the application, but
-    #: not for the most recent version.
-    deprecated_keys = []
-    def __init__(self, name):
-        self.name = name
-        self.versions = {}
-        # cache variables
-        self._extractors = {}
-        self._substitutions = {}
-    def repository(self, srv_path):
-        """
-        Returns the Git repository that would contain this application.
-        ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
-        """
-        repo = os.path.join(srv_path, self.name + ".git")
-        if not os.path.isdir(repo):
-            repo = os.path.join(srv_path, self.name, ".git")
-            if not os.path.isdir(repo):
-                raise NoRepositoryError(self.name)
-        return repo
-    def makeVersion(self, version):
-        """
-        Creates or retrieves the :class:`ApplicationVersion` singleton for the
-        specified version.
-        """
-        if version not in self.versions:
-            self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
-        return self.versions[version]
-    def extract(self, deployment):
-        """Extracts wizard variables from a deployment."""
-        result = {}
-        for k,extractor in self.extractors.items():
-            result[k] = extractor(deployment)
-        return result
-    def parametrize(self, deployment):
-        """
-        Takes a generic source checkout and parametrizes
-        it according to the values of deployment.  This function
-        operates on the current working directory.
-        """
-        variables = deployment.extract()
-        for file in self.parametrized_files:
-            try:
-                contents = open(file, "r").read()
-            except IOError:
-                continue
-            for key, value in variables.items():
-                if value is None: continue
-                contents = contents.replace(key, value)
-            f = open(file, "w")
-            f.write(contents)
-    def resolveConflicts(self, deployment):
-        """
-        Resolves conflicted files in the current working
-        directory.  Returns whether or not all conflicted
-        files were resolved or not.  Fully resolved files are
-        added to the index, but no commit is made.  By default
-        this is a no-op and returns ``False``.
-        """
-        return False
-    def prepareMerge(self, deployment):
-        """
-        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.  If you add new files,
-        you have to 'git add' them; this is not necessary for edits.
-        By default this is a no-op.
-        """
-        pass
-    def prepareConfig(self, deployment):
-        """
-        Takes a deployment and replaces any explicit instances
-        of a configuration variable with generic ``WIZARD_*`` constants.
-        The default implementation uses :attr:`substitutions`;
-        you can override this method to provide arbitrary extra
-        behavior.
-        """
-        for key, subst in self.substitutions.items():
-            subs = subst(deployment)
-            if not subs and key not in self.deprecated_keys:
-                logging.warning("No substitutions for %s" % key)
-    def install(self, version, options):
-        """
-        Run for 'wizard configure' (and, by proxy, 'wizard install')
-        to configure an application.  This assumes that the current
-        working directory is a deployment.  (This function does not take
-        a :class:`Deployment` as a parameter, as those operations are
-        not meaningful yet.)
-        """
-        raise NotImplemented
-    def upgrade(self, deployment, version, options):
-        """
-        Run for 'wizard upgrade' to upgrade database schemas and other
-        non-versioned data in an application.  This assumes that
-        the current working directory is the deployment.
-        """
-        raise NotImplemented
-    def backup(self, deployment, options):
-        """
-        Run for 'wizard backup' and upgrades to backup database schemas
-        and other non-versioned data in an application.  This assumes
-        that the current working directory is the deployment.
-        """
-        raise NotImplemented
-    def restore(self, deployment, backup, options):
-        """
-        Run for 'wizard restore' and failed upgrades to restore database
-        and other non-versioned data to a backed up version.  This assumes
-        that the current working directory is the deployment.
-        """
-        raise NotImplemented
-    def detectVersion(self, deployment):
-        """
-        Checks source files to determine the version manually.  This assumes
-        that the current working directory is the deployment.
-        """
-        return None
-    def checkWeb(self, deployment, output=None):
-        """
-        Checks if the autoinstall is viewable from the web.  To get
-        the HTML source that was retrieved, pass a variable containing
-        an empty list to ``output``; it will be mutated to have its
-        first element be the output.
-        """
-        raise NotImplemented
-    def checkConfig(self, deployment):
-        """
-        Checks whether or not an autoinstall has been configured/installed
-        for use.  Assumes that the current working directory is the deployment.
-        """
-        raise NotImplemented
-    @property
-    def extractors(self):
-        """
-        Dictionary of variable names to extractor functions.  These functions
-        take a :class:`Deployment` as an argument and return the value of
-        the variable, or ``None`` if it could not be found.
-        See also :func:`wizard.app.filename_regex_extractor`.
-        """
-        return {}
-    @property
-    def substitutions(self):
-        """
-        Dictionary of variable names to substitution functions.  These functions
-        take a :class:`Deployment` as an argument and modify the deployment such
-        that an explicit instance of the variable is released with the generic
-        WIZARD_* constant.  See also :func:`wizard.app.filename_regex_substitution`.
-        """
-        return {}
-    @staticmethod
-    def make(name):
-        """Makes an application, but uses the correct subtype if available."""
-        try:
-            __import__("wizard.app." + name)
-            return getattr(wizard.app, name).Application(name)
-        except ImportError:
-            return Application(name)
-
-class ApplicationVersion(object):
-    """Represents an abstract notion of a version for an application, where
-    ``version`` is a :class:`distutils.version.LooseVersion` and
-    ``application`` is a :class:`Application`."""
-    #: The :class:`distutils.version.LooseVersion` of this instance.
-    version = None
-    #: The :class:`Application` of this instance.
-    application = None
-    def __init__(self, version, application):
-        self.version = version
-        self.application = application
-    @property
-    def tag(self):
-        """
-        Returns the name of the git describe tag for the commit the user is
-        presently on, something like mediawiki-1.2.3-scripts-4-g123abcd
-        """
-        return "%s-%s" % (self.application, self.version)
-    @property
-    def scripts_tag(self):
-        """
-        Returns the name of the Git tag for this version.
-        """
-        end = str(self.version).partition('-scripts')[2].partition('-')[0]
-        return "%s-scripts%s" % (self.pristine_tag, end)
-    @property
-    def pristine_tag(self):
-        """
-        Returns the name of the Git tag for the pristine version corresponding
-        to this version.
-        """
-        return "%s-%s" % (self.application.name, str(self.version).partition('-scripts')[0])
-    def __cmp__(self, y):
-        return cmp(self.version, y.version)
-    @staticmethod
-    def parse(value):
-        """
-        Parses a line from the :term:`versions store` and return
-        :class:`ApplicationVersion`.
-
-        Use this only for cases when speed is of primary importance;
-        the data in version is unreliable and when possible, you should
-        prefer directly instantiating a Deployment and having it query
-        the autoinstall itself for information.
-
-        The `value` to parse will vary.  For old style installs, it
-        will look like::
-
-           /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
-
-        For new style installs, it will look like::
-
-           APP-x.y.z-scripts
-        """
-        name = value.split("/")[-1]
-        try:
-            if name.find("-") != -1:
-                app, _, version = name.partition("-")
-            else:
-                # kind of poor, maybe should error.  Generally this
-                # will actually result in a not found error
-                app = name
-                version = "trunk"
-        except ValueError:
-            raise DeploymentParseError(value)
-        return ApplicationVersion.make(app, version)
-    @staticmethod
-    def make(app, version):
-        """
-        Makes/retrieves a singleton :class:`ApplicationVersion` from
-        a``app`` and ``version`` string.
-        """
-        try:
-            # defer to the application for version creation to enforce
-            # singletons
-            return applications()[app].makeVersion(version)
-        except KeyError:
-            raise NoSuchApplication(app)
-
 ## -- Exceptions --
 
 class Error(wizard.Error):
     """Base error class for this module"""
     pass
 
 ## -- Exceptions --
 
 class Error(wizard.Error):
     """Base error class for this module"""
     pass
 
-class NoSuchApplication(Error):
-    """
-    You attempted to reference a :class:`Application` named
-    ``app``, which is not recognized by Wizard.
-    """
-    #: The name of the application that does not exist.
-    app = None
-    #: The location of the autoinstall that threw this variable.
-    #: This should be set by error handling code when it is availble.
-    location = None
-    def __init__(self, app):
-        self.app = app
-
-class DeploymentParseError(Error):
-    """
-    Could not parse ``value`` from :term:`versions store`.
-    """
-    #: The value that failed to parse.
-    value = None
-    #: The location of the autoinstall that threw this variable.
-    #: This should be set by error handling code when it is available.
-    location = None
-    def __init__(self, value):
-        self.value = value
-
-class NoRepositoryError(Error):
-    """
-    :class:`Application` does not appear to have a Git repository
-    in the normal location.
-    """
-    #: The name of the application that does not have a Git repository.
-    app = None
-    def __init__(self, app):
-        self.app = app
-    def __str__(self):
-        return """Could not find Git repository for '%s'.  If you would like to use a local version, try specifying --srv-path or WIZARD_SRV_PATH.""" % self.app
-
+# LEGACY
 class NotMigratedError(Error):
     """
     The deployment contains a .scripts-version file, but no .git
 class NotMigratedError(Error):
     """
     The deployment contains a .scripts-version file, but no .git
-    or .scripts directory.
+    or .wizard directory.
     """
     #: Directory of deployment
     dir = None
     """
     #: Directory of deployment
     dir = None
@@ -675,7 +567,7 @@ class NotMigratedError(Error):
         return """This installation was not migrated"""
 
 class AlreadyVersionedError(Error):
         return """This installation was not migrated"""
 
 class AlreadyVersionedError(Error):
-    """The deployment contained a .git directory but no .scripts directory."""
+    """The deployment contained a .git directory but no .wizard directory."""
     #: Directory of deployment
     dir = None
     def __init__(self, dir):
     #: Directory of deployment
     dir = None
     def __init__(self, dir):
@@ -684,7 +576,7 @@ class AlreadyVersionedError(Error):
         return """
 
 ERROR: Directory contains a .git directory, but not
         return """
 
 ERROR: Directory contains a .git directory, but not
-a .scripts directory.  If this is not a corrupt
+a .wizard directory.  If this is not a corrupt
 migration, this means that the user was versioning their
 install using Git."""
 
 migration, this means that the user was versioning their
 install using Git."""
 
@@ -701,7 +593,7 @@ ERROR: The install was well-formed, but not configured
 (essential configuration files were not found.)"""
 
 class CorruptedAutoinstallError(Error):
 (essential configuration files were not found.)"""
 
 class CorruptedAutoinstallError(Error):
-    """The install was missing a .git directory, but had a .scripts directory."""
+    """The install was missing a .git directory, but had a .wizard directory."""
     #: Directory of the corrupted install
     dir = None
     def __init__(self, dir):
     #: Directory of the corrupted install
     dir = None
     def __init__(self, dir):
@@ -709,21 +601,25 @@ class CorruptedAutoinstallError(Error):
     def __str__(self):
         return """
 
     def __str__(self):
         return """
 
-ERROR: Directory contains a .scripts directory,
+ERROR: Directory contains a .wizard directory,
 but not a .git directory."""
 
 class NotAutoinstallError(Error):
 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 """
 
     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."""
 
 class NoTagError(Error):
     """Deployment has a tag that does not have an equivalent in upstream repository."""
@@ -759,8 +655,8 @@ class InconsistentPristineTagError(Error):
 ERROR: Local pristine tag %s did not match repository's.  This
 probably means an upstream rebase occured.""" % self.tag
 
 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."""
+class InconsistentWizardTagError(Error):
+    """Wizard tag commit ID does not match upstream wizard tag commit ID."""
     #: Inconsistent tag
     tag = None
     def __init__(self, tag):
     #: Inconsistent tag
     tag = None
     def __init__(self, tag):
@@ -768,7 +664,7 @@ class InconsistentScriptsTagError(Error):
     def __str__(self):
         return """
 
     def __str__(self):
         return """
 
-ERROR: Local scripts tag %s did not match repository's.  This
+ERROR: Local wizard tag %s did not match repository's.  This
 probably means an upstream rebase occurred.""" % self.tag
 
 class HeadNotDescendantError(Error):
 probably means an upstream rebase occurred.""" % self.tag
 
 class HeadNotDescendantError(Error):
@@ -808,19 +704,22 @@ version %s.""" % (self.real_version, self.git_version)
 
 class WebVerificationError(Error):
     """Could not access the application on the web"""
 
 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
     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:
+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 """
 
 
-%s""" % self.contents
+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."""
 
 class UnknownWebPath(Error):
     """Could not determine application's web path."""
@@ -832,19 +731,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."""
 on the application.  You can specify this manually using
 the WIZARD_WEB_HOST and WIZARD_WEB_PATH environment
 variables."""
-
-_application_list = [
-    "mediawiki", "wordpress", "joomla", "e107", "gallery2",
-    "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
-    # these are technically deprecated
-    "advancedpoll", "gallery",
-]
-_applications = None
-
-def applications():
-    """Hash table for looking up string application name to instance"""
-    global _applications
-    if not _applications:
-        _applications = dict([(n,Application.make(n)) for n in _application_list ])
-    return _applications
-