X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/47ae1926b9abe141e50a618d753a80fe5c63cb18..3f18b7fff1c5d5cd3d91897a85a425704ba4295e:/wizard/app/__init__.py diff --git a/wizard/app/__init__.py b/wizard/app/__init__.py index 05fe281..ee9f58d 100644 --- a/wizard/app/__init__.py +++ b/wizard/app/__init__.py @@ -6,6 +6,11 @@ You'll need to know how to overload the :class:`Application` class and use some of the functions in this module in order to specify new applications. +There are some submodules for programming languages that define common +functions and data that may be used by applications in that language. See: + +* :mod:`wizard.app.php` + .. testsetup:: * import re @@ -76,6 +81,9 @@ class Application(object): #: a conflict marker string and a result list. See :mod:`wizard.resolve` #: for more information. resolutions = {} + #: Instance of :class:`wizard.install.ArgSchema` that defines the arguments + #: this application requires. + install_schema = None def __init__(self, name): self.name = name self.versions = {} @@ -185,7 +193,7 @@ class Application(object): take a :class:`wizard.deploy.Deployment` as a parameter.) Subclasses should provide an implementation. """ - raise NotImplemented + raise NotImplementedError def upgrade(self, deployment, version, options): """ Run for 'wizard upgrade' to upgrade database schemas and other @@ -193,7 +201,7 @@ class Application(object): upgraded. This assumes that the current working directory is the deployment. Subclasses should provide an implementation. """ - raise NotImplemented + raise NotImplementedError def backup(self, deployment, outdir, options): """ Run for 'wizard backup' and upgrades to backup database schemas @@ -206,7 +214,7 @@ class Application(object): Static user files may not need to be backed up, since in many applications upgrades do not modify static files. """ - raise NotImplemented + raise NotImplementedError def restore(self, deployment, backup_dir, options): """ Run for 'wizard restore' and failed upgrades to restore database @@ -214,21 +222,34 @@ class Application(object): that the current working directory is the deployment. Subclasses should provide an implementation. """ - raise NotImplemented + raise NotImplementedError def detectVersion(self, deployment): """ Checks source files to determine the version manually. This assumes that the current working directory is the deployment. Subclasses should provide an implementation. """ - raise NotImplemented - def checkWeb(self, deployment, output=None): + raise NotImplementedError + def detectVersionFromFile(self, filename, regex): + """ + Helper method that detects a version by using a regular expression + from a file. The regexed value is passed through :mod:`shlex`. + This assumes that the current working directory is the deployment. + """ + contents = open(filename).read() + match = regex.search(contents) + if not match: return None + return distutils.version.LooseVersion(shlex.split(match.group(2))[0]) + def download(self, version): + """ + Returns a URL that can be used to download a tarball of ``version`` of + this application. """ - 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. Subclasses should provide an - implementation. + raise NotImplementedError + def checkWeb(self, deployment): + """ + Checks if the autoinstall is viewable from the web. Subclasses should + provide an implementation. .. note:: Finding a reasonable heuristic that works across skinning @@ -237,14 +258,28 @@ class Application(object): page does not contain the features you search for. Try not to depend on pages that are not the main page. """ - raise NotImplemented + raise NotImplementedError + def checkWebPage(self, deployment, page, output): + """ + Checks if a given page of an autoinstall contains a particular string. + """ + page = deployment.fetch(page) + result = page.find(output) != -1 + if result: + logging.debug("checkWebPage (passed):\n\n" + page) + else: + logging.info("checkWebPage (failed):\n\n" + page) + return result 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. Subclasses should provide an implementation. """ - raise NotImplemented + # XXX: Unfortunately, this doesn't quite work because we package + # bogus config files in the -scripts versions of installs. Maybe + # we should check a hash or something? + raise NotImplementedError @staticmethod def make(name): """Makes an application, but uses the correct subtype if available.""" @@ -352,7 +387,7 @@ def expand_re(val): def make_extractors(seed): """ - Take a dictionary of ``key``s to ``(file, regex)`` tuples and convert them into + Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into extractor functions (which take a :class:`wizard.deploy.Deployment` and return the value of the second subpattern of ``regex`` when matched with the contents of ``file``). @@ -361,7 +396,7 @@ def make_extractors(seed): def make_substitutions(seed): """ - Take a dictionary of ``key``s to ``(file, regex)`` tuples and convert them into substitution + Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into substitution functions (which take a :class:`wizard.deploy.Deployment`, replace the second subpattern of ``regex`` with ``key`` in ``file``, and returns the number of substitutions made.) """ @@ -499,7 +534,7 @@ def get_mysql_args(d): # XXX: add support for getting these out of options vars = d.extract() if 'WIZARD_DBNAME' not in vars: - raise app.BackupFailure("Could not determine database name") + raise BackupFailure("Could not determine database name") triplet = scripts.get_sql_credentials(vars) args = [] if triplet is not None: @@ -550,7 +585,33 @@ class NoSuchApplication(Error): def __init__(self, app): self.app = app -class UpgradeFailure(Error): +class Failure(Error): + """ + Represents a failure when performing some double-dispatched operation + such as an installation or an upgrade. Failure classes are postfixed + with Failure, not Error. + """ + pass + +class InstallFailure(Error): + """Installation failed for unknown reason.""" + def __str__(self): + return """Installation failed for unknown reason.""" + +class RecoverableInstallFailure(InstallFailure): + """ + Installation failed, but we were able to determine what the + error was, and should give the user a second chance if we were + running interactively. + """ + #: List of the errors that were found. + errors = None + def __init__(self, errors): + self.errors = errors + def __str__(self): + return """Installation failed due to the following errors: %s""" % ", ".join(self.errors) + +class UpgradeFailure(Failure): """Upgrade script failed.""" #: String details of failure (possibly stdout or stderr output) details = None @@ -563,20 +624,15 @@ ERROR: Upgrade script failed, details: %s""" % self.details -class UpgradeVerificationFailure(Error): +class UpgradeVerificationFailure(Failure): """Upgrade script passed, but website wasn't accessible afterwards""" - #: String details of failure (possibly stdout or stderr output) - details = None - def __init__(self, details): - self.details = details def __str__(self): return """ -ERROR: Upgrade script passed, but website wasn't accessible afterwards. Details: - -%s""" % self.details +ERROR: Upgrade script passed, but website wasn't accessible afterwards. Check +the debug logs for the contents of the page.""" -class BackupFailure(Error): +class BackupFailure(Failure): """Backup script failed.""" #: String details of failure details = None @@ -589,7 +645,7 @@ ERROR: Backup script failed, details: %s""" % self.details -class RestoreFailure(Error): +class RestoreFailure(Failure): """Restore script failed.""" #: String details of failure details = None