2 Plumbing object model for representing applications we want to
3 install. This module does the heavy lifting, but you probably
4 want to use :class:`wizard.deploy.Deployment` which is more user-friendly.
5 You'll need to know how to overload the :class:`Application` class
6 and use some of the functions in this module in order to specify
9 To specify custom applications as plugins, add the following ``entry_points``
13 yourappname = your.module:Application
14 otherappname = your.other.module:Application
18 Wizard will complain loudly if ``yourappname`` conflicts with an
19 application name defined by someone else.
21 There are some submodules for programming languages that define common
22 functions and data that may be used by applications in that language. See:
24 * :mod:`wizard.app.php`
31 from wizard import deploy, util
32 from wizard.app import *
38 import distutils.version
51 from wizard import resolve, scripts, shell, sql, util
54 _scripts_application_list = [
55 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
56 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
58 # these are technically deprecated
59 "advancedpoll", "gallery",
61 def _scripts_make(name):
62 """Makes an application, but uses the correct subtype if available."""
64 __import__("wizard.app." + name)
65 return getattr(wizard.app, name).Application(name)
66 except ImportError as error:
67 # XXX ugly hack to check if the import error is from the top level
68 # module we care about or a submodule. should be an archetectural change.
69 if error.args[0].split()[-1]==name:
70 return Application(name)
76 """Hash table for looking up string application name to instance"""
80 _applications = dict([(n,_scripts_make(n)) for n in _scripts_application_list ])
82 for dist in pkg_resources.working_set:
83 for appname, appclass in dist.get_entry_map("wizard.app").items():
84 if appname in _applications:
85 newname = dist.key + ":" + appname
86 if newname in _applications:
87 raise Exception("Unrecoverable application name conflict for %s from %s", appname, dist.key)
88 logging.warning("Could not overwrite %s, used %s instead", appname, newname)
90 _applications[appname] = appclass(appname)
93 def getApplication(appname):
94 """Retrieves application instance given a name"""
95 return applications()[appname]
97 class Application(object):
99 Represents an application, i.e. mediawiki or phpbb.
102 Many of these methods assume a specific working
103 directory; prefer using the corresponding methods
104 in :class:`wizard.deploy.Deployment` and its subclasses.
106 #: String name of the application
108 #: Dictionary of version strings to :class:`ApplicationVersion`.
109 #: See also :meth:`makeVersion`.
111 #: List of files that need to be modified when parametrizing.
112 #: This is a class-wide constant, and should not normally be modified.
113 parametrized_files = []
114 #: Keys that are used in older versions of the application, but
115 #: not for the most recent version.
116 deprecated_keys = set()
117 #: Keys that we can simply generate random strings for if they're missing
119 #: Values that are not sufficiently random for a random key. This can
120 #: include default values for a random configuration option,
121 random_blacklist = set()
122 #: Dictionary of variable names to extractor functions. These functions
123 #: take a :class:`wizard.deploy.Deployment` as an argument and return the value of
124 #: the variable, or ``None`` if it could not be found.
125 #: See also :func:`filename_regex_extractor`.
127 #: Dictionary of variable names to substitution functions. These functions
128 #: take a :class:`wizard.deploy.Deployment` as an argument and modify the deployment such
129 #: that an explicit instance of the variable is released with the generic
130 #: ``WIZARD_*`` constant. See also :func:`filename_regex_substitution`.
132 #: Dictionary of file names to a list of resolutions, which are tuples of
133 #: a conflict marker string and a result list. See :mod:`wizard.resolve`
134 #: for more information.
136 #: Instance of :class:`wizard.install.ArgSchema` that defines the arguments
137 #: this application requires.
138 install_schema = None
139 #: Name of the database that this application uses, i.e. ``mysql`` or
140 #: ``postgres``. If we end up supporting multiple databases for a single
141 #: application, there should also be a value for this in
142 #: :class:`wizard.deploy.Deployment`; the value here is merely the preferred
145 #: Indicates whether or not a web stub is necessary.
146 needs_web_stub = False
147 def __init__(self, name):
151 self._extractors = {}
152 self._substitutions = {}
153 def repository(self, srv_path):
155 Returns the Git repository that would contain this application.
156 ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
158 repo = os.path.join(srv_path, self.name + ".git")
159 if not os.path.isdir(repo):
160 repo = os.path.join(srv_path, self.name, ".git")
161 if not os.path.isdir(repo):
162 raise NoRepositoryError(self.name)
164 def makeVersion(self, version):
166 Creates or retrieves the :class:`ApplicationVersion` singleton for the
169 if version not in self.versions:
170 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
171 return self.versions[version]
172 def extract(self, deployment):
174 Extracts wizard variables from a deployment. Default implementation
175 uses :attr:`extractors`.
178 for k,extractor in self.extractors.items():
179 result[k] = extractor(deployment)
180 # XXX: ugh... we have to do quoting
181 for k in self.random_keys:
182 if result[k] is None or result[k] in self.random_blacklist:
183 result[k] = "'%s'" % util.random_key()
185 def dsn(self, deployment):
187 Returns the deployment specific database URL. Uses the override file
188 in :file:`.scripts` if it exists, and otherwise attempt to extract the
189 variables from the source files.
191 Under some cases, the database URL will contain only the database
192 property, and no other values. This indicates that the actual DSN
193 should be determined from the environment.
195 This function might return ``None``.
199 We are allowed to batch these two together, because the full precedence
200 chain for determining the database of an application combines these
201 two together. If this was not the case, we would have to call
202 :meth:`databaseUrlFromOverride` and :meth:`databaseUrlFromExtract` manually.
204 url = self.dsnFromOverride(deployment)
207 return self.dsnFromExtract(deployment)
208 def dsnFromOverride(self, deployment):
210 Extracts database URL from an explicit dsn override file.
213 return sqlalchemy.engine.url.make_url(open(deployment.dsn_file).read().strip())
216 def dsnFromExtract(self, deployment):
218 Extracts database URL from a deployment, and returns them as
219 a :class:`sqlalchemy.engine.url.URL`. Returns ``None`` if we
220 can't figure it out: i.e. the conventional variables are not defined
221 for this application.
223 if not self.database:
225 vars = self.extract(deployment)
226 names = ("WIZARD_DBSERVER", "WIZARD_DBUSER", "WIZARD_DBPASSWORD", "WIZARD_DBNAME")
227 host, user, password, database = (shlex.split(vars[x])[0] if vars[x] is not None else None for x in names)
228 # XXX: You'd have to put support for an explicit different database
230 return sqlalchemy.engine.url.URL(self.database, username=user, password=password, host=host, database=database)
231 def url(self, deployment):
233 Returns the deployment specific web URL. Uses the override file
234 in :file:`.scripts` if it exists, and otherwise attempt to extract
235 the variables from the source files.
237 This function might return ``None``, which indicates we couldn't figure
240 url = self.urlFromOverride(deployment)
243 return self.urlFromExtract(deployment)
244 def urlFromOverride(self, deployment):
246 Extracts URL from explicit url override file.
249 return urlparse.urlparse(open(deployment.url_file).read().strip())
252 def urlFromExtract(self, deployment):
254 Extracts URL from a deployment, and returns ``None`` if we can't
255 figure it out. Default implementation is to fail; we might
256 do something clever with extractable variables in the future.
259 def parametrize(self, deployment, ref_deployment):
261 Takes a generic source checkout and parametrizes it according to the
262 values of ``deployment``. This function operates on the current
263 working directory. ``deployment`` should **not** be the same as the
264 current working directory. Default implementation uses
265 :attr:`parametrized_files` and a simple search and replace on those
268 # deployment is not used in this implementation, but note that
269 # we do have the invariant the current directory matches
270 # deployment's directory
271 variables = ref_deployment.extract()
272 self.parametrizeWithVars(variables)
273 def parametrizeWithVars(self, variables):
274 for file in self.parametrized_files:
275 logging.debug("Parametrizing file '%s'\n" % (file, ))
277 contents = open(file, "r").read()
279 logging.debug("Failed to open file '%s'\n" % (file, ))
281 for key, value in variables.items():
282 if value is None: continue
283 contents = contents.replace(key, value)
286 def resolveConflicts(self, deployment):
288 Resolves conflicted files in the current working directory. Returns
289 whether or not all conflicted files were resolved or not. Fully
290 resolved files are added to the index, but no commit is made. The
291 default implementation uses :attr:`resolutions`.
296 for status in shell.eval("git", "ls-files", "--unmerged").splitlines():
297 mode, hash, role, name = status.split()
298 files.setdefault(name, set()).add(int(role))
299 for file, roles in files.items():
300 # some automatic resolutions
301 if 1 not in roles and 2 not in roles and 3 in roles:
302 # upstream added a file, but it conflicted for whatever reason
303 shell.call("git", "add", file)
305 elif 1 in roles and 2 not in roles and 3 in roles:
306 # user deleted the file, but upstream changed it
307 shell.call("git", "rm", file)
310 # XXX: this functionality is mostly subsumed by the rerere
312 if file in self.resolutions:
313 contents = open(file, "r").read()
314 for spec, result in self.resolutions[file]:
315 old_contents = contents
316 contents = resolve.resolve(contents, spec, result)
317 if old_contents != contents:
318 logging.info("Did resolution with spec:\n" + spec)
319 open(file, "w").write(contents)
320 if not resolve.is_conflict(contents):
321 shell.call("git", "add", file)
327 def prepareMerge(self, deployment):
329 Performs various edits to files in the current working directory in
330 order to make a merge go more smoothly. This is usually
331 used to fix botched line-endings. If you add new files,
332 you have to 'git add' them; this is not necessary for edits.
333 By default this is a no-op; subclasses should replace this
334 with useful behavior.
337 def prepareConfig(self, deployment):
339 Takes a deployment and replaces any explicit instances
340 of a configuration variable with generic ``WIZARD_*`` constants.
341 The default implementation uses :attr:`substitutions`, and
342 emits warnings when it encounters keys in :attr:`deprecated_keys`.
344 for key, subst in self.substitutions.items():
345 subs = subst(deployment)
346 if not subs and key not in self.deprecated_keys:
347 logging.warning("No substitutions for %s" % key)
348 def install(self, version, options):
350 Run for 'wizard configure' (and, by proxy, 'wizard install') to
351 configure an application. This assumes that the current working
352 directory is a deployment. (Unlike its kin, this function does not
353 take a :class:`wizard.deploy.Deployment` as a parameter.) Subclasses should
354 provide an implementation.
356 raise NotImplementedError
357 def upgrade(self, deployment, version, options):
359 Run for 'wizard upgrade' to upgrade database schemas and other
360 non-versioned data in an application after the filesystem has been
361 upgraded. This assumes that the current working directory is the
362 deployment. Subclasses should provide an implementation.
364 raise NotImplementedError
365 def backup(self, deployment, outdir, options):
367 Run for 'wizard backup' and upgrades to backup database schemas
368 and other non-versioned data in an application. ``outdir`` is
369 the directory that backup files should be placed. This assumes
370 that the current working directory is the deployment. Subclasses
371 should provide an implementation, even if it is a no-op.
374 Static user files may not need to be backed up, since in
375 many applications upgrades do not modify static files.
377 raise NotImplementedError
378 def restore(self, deployment, backup_dir, options):
380 Run for 'wizard restore' and failed upgrades to restore database
381 and other non-versioned data to a backed up version. This assumes
382 that the current working directory is the deployment. Subclasses
383 should provide an implementation.
385 raise NotImplementedError
386 def remove(self, deployment, options):
388 Run for 'wizard remove' to delete all database and non-local
389 file data. This assumes that the current working directory is
390 the deployment. Subclasses should provide an implementation.
392 raise NotImplementedError
393 def detectVersion(self, deployment):
395 Checks source files to determine the version manually. This assumes
396 that the current working directory is the deployment. Subclasses
397 should provide an implementation.
399 raise NotImplementedError
400 def detectVersionFromFile(self, filename, regex):
402 Helper method that detects a version by using a regular expression
403 from a file. The regexed value is passed through :mod:`shlex`.
404 This assumes that the current working directory is the deployment.
406 contents = open(filename).read()
407 match = regex.search(contents)
408 if not match: return None
409 return distutils.version.LooseVersion(shlex.split(match.group(2))[0])
410 def detectVersionFromGit(self, tagPattern, preStrip = ''):
412 Helper method that detects a version by using the most recent tag
413 in git that matches the specified pattern.
414 This assumes that the current working directory is the deployment.
416 cmd = ['git', 'describe', '--match', tagPattern, ]
417 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, )
418 tag, _ = p.communicate()
420 if tag and len(tag) > len(preStrip) and tag[:len(preStrip)] == preStrip:
421 tag = tag[len(preStrip):]
422 if not tag: return None
423 return distutils.version.LooseVersion(tag)
424 def download(self, version):
426 Returns a URL that can be used to download a tarball of ``version`` of
429 raise NotImplementedError
430 def checkWeb(self, deployment):
432 Checks if the autoinstall is viewable from the web. Subclasses should
433 provide an implementation.
436 Finding a reasonable heuristic that works across skinning
437 choices can be difficult. We've had reasonable success
438 searching for metadata. Be sure that the standard error
439 page does not contain the features you search for. Try
440 not to depend on pages that are not the main page.
442 raise NotImplementedError
443 def checkDatabase(self, deployment):
445 Checks if the database is accessible.
448 sql.connect(deployment.dsn)
450 except sqlalchemy.exc.DBAPIError:
452 def checkWebPage(self, deployment, page, outputs=[], exclude=[]):
454 Checks if a given page of an autoinstall contains a particular string.
456 page = deployment.fetch(page)
458 if page.find(x) != -1:
459 logging.info("checkWebPage (failed due to %s):\n\n%s", x, page)
462 for output in outputs:
463 votes += page.find(output) != -1
464 if votes > len(outputs) / 2:
465 logging.debug("checkWebPage (passed):\n\n" + page)
468 logging.info("checkWebPage (failed):\n\n" + page)
470 def checkConfig(self, deployment):
472 Checks whether or not an autoinstall has been configured/installed
473 for use. Assumes that the current working directory is the deployment.
474 Subclasses should provide an implementation.
476 # XXX: Unfortunately, this doesn't quite work because we package
477 # bogus config files in the -scripts versions of installs. Maybe
478 # we should check a hash or something?
479 raise NotImplementedError
480 def researchFilter(self, filename, added, deleted):
482 Allows an application to selectively ignore certain diffstat signatures
483 during research; for example, configuration files will have a very
484 specific set of changes, so ignore them; certain installation files
485 may be removed, etc. Return ``True`` if a diffstat signature should be
489 def researchVerbose(self, filename):
491 Allows an application to exclude certain dirty files from the output
492 report; usually this will just be parametrized files, since those are
493 guaranteed to have changes. Return ``True`` if a file should only
494 be displayed in verbose mode.
496 return filename in self.parametrized_files
498 class ApplicationVersion(object):
499 """Represents an abstract notion of a version for an application, where
500 ``version`` is a :class:`distutils.version.LooseVersion` and
501 ``application`` is a :class:`Application`."""
502 #: The :class:`distutils.version.LooseVersion` of this instance.
504 #: The :class:`Application` of this instance.
506 def __init__(self, version, application):
507 self.version = version
508 self.application = application
512 Returns the name of the git describe tag for the commit the user is
513 presently on, something like mediawiki-1.2.3-scripts-4-g123abcd
515 return "%s-%s" % (self.application, self.version)
517 def scripts_tag(self):
519 Returns the name of the Git tag for this version.
521 end = str(self.version).partition('-scripts')[2].partition('-')[0]
522 return "%s-scripts%s" % (self.pristine_tag, end)
524 def pristine_tag(self):
526 Returns the name of the Git tag for the pristine version corresponding
529 return "%s-%s" % (self.application.name, str(self.version).partition('-scripts')[0])
530 def __cmp__(self, y):
531 return cmp(self.version, y.version)
535 Parses a line from the :term:`versions store` and return
536 :class:`ApplicationVersion`.
538 Use this only for cases when speed is of primary importance;
539 the data in version is unreliable and when possible, you should
540 prefer directly instantiating a :class:`wizard.deploy.Deployment` and having it query
541 the autoinstall itself for information.
543 The `value` to parse will vary. For old style installs, it
546 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
548 For new style installs, it will look like::
552 name = value.split("/")[-1]
554 if name.find("-") != -1:
555 app, _, version = name.partition("-")
557 # kind of poor, maybe should error. Generally this
558 # will actually result in a not found error
562 raise DeploymentParseError(value)
563 return ApplicationVersion.make(app, version)
565 def make(app, version):
567 Makes/retrieves a singleton :class:`ApplicationVersion` from
568 a``app`` and ``version`` string.
571 # defer to the application for version creation to enforce
573 return applications()[app].makeVersion(version)
575 raise NoSuchApplication(app)
579 Takes a tree of values (implement using nested lists) and
580 transforms them into regular expressions.
584 >>> expand_re(['a', 'b'])
586 >>> expand_re(['*', ['b', 'c']])
589 if isinstance(val, str):
590 return re.escape(val)
592 return '(?:' + '|'.join(map(expand_re, val)) + ')'
594 def make_extractors(seed):
596 Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into
597 extractor functions (which take a :class:`wizard.deploy.Deployment`
598 and return the value of the second subpattern of ``regex`` when matched
599 with the contents of ``file``).
601 return util.dictmap(lambda a: filename_regex_extractor(*a), seed)
603 def make_substitutions(seed):
605 Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into substitution
606 functions (which take a :class:`wizard.deploy.Deployment`, replace the second subpattern
607 of ``regex`` with ``key`` in ``file``, and returns the number of substitutions made.)
609 return util.dictkmap(lambda k, v: filename_regex_substitution(k, *v), seed)
611 # The following two functions are *highly* functional, and I recommend
612 # not touching them unless you know what you're doing.
614 def filename_regex_extractor(file, regex):
616 .. highlight:: haskell
618 Given a relative file name ``file``, a regular expression ``regex``, and a
619 :class:`wizard.deploy.Deployment` extracts a value out of the file in that
620 deployment. This function is curried, so you pass just ``file`` and
621 ``regex``, and then pass ``deployment`` to the resulting function.
623 Its Haskell-style type signature would be::
625 Filename -> Regex -> (Deployment -> String)
627 The regular expression requires a very specific form, essentially ``()()()``
628 (with the second subgroup being the value to extract). These enables
629 the regular expression to be used equivalently with filename
631 .. highlight:: python
633 For convenience purposes, we also accept ``[Filename]``, in which case
634 we use the first entry (index 0). Passing an empty list is invalid.
636 >>> open("test-settings.extractor.ini", "w").write("config_var = 3\\n")
637 >>> f = filename_regex_extractor('test-settings.extractor.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
638 >>> f(deploy.Deployment("."))
640 >>> os.unlink("test-settings.extractor.ini")
643 The first application of ``regex`` and ``file`` is normally performed
644 at compile-time inside a submodule; the second application is
645 performed at runtime.
647 if not isinstance(file, str):
651 contents = deployment.read(file) # cached
654 match = regex.search(contents)
655 if not match: return None
656 # assumes that the second match is the one we want.
657 return match.group(2)
660 def filename_regex_substitution(key, files, regex):
662 .. highlight:: haskell
664 Given a Wizard ``key`` (``WIZARD_*``), a list of ``files``, a
665 regular expression ``regex``, and a :class:`wizard.deploy.Deployment`
666 performs a substitution of the second subpattern of ``regex``
667 with ``key``. Returns the number of replacements made. This function
668 is curried, so you pass just ``key``, ``files`` and ``regex``, and
669 then pass ``deployment`` to the resulting function.
671 Its Haskell-style type signature would be::
673 Key -> ([File], Regex) -> (Deployment -> IO Int)
675 .. highlight:: python
677 For convenience purposes, we also accept ``Filename``, in which case it is treated
678 as a single item list.
680 >>> open("test-settings.substitution.ini", "w").write("config_var = 3")
681 >>> f = filename_regex_substitution('WIZARD_KEY', 'test-settings.substitution.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
682 >>> f(deploy.Deployment("."))
684 >>> print open("test-settings.substitution.ini", "r").read()
685 config_var = WIZARD_KEY
686 >>> os.unlink("test-settings.substitution.ini")
688 if isinstance(files, str):
691 base = deployment.location
694 file = os.path.join(base, file)
696 contents = open(file, "r").read()
697 contents, n = regex.subn("\\1" + key + "\\3", contents)
699 open(file, "w").write(contents)
705 def backup_database(outdir, deployment):
707 Generic database backup function for MySQL.
709 # XXX: Change this once deployments support multiple dbs
710 if deployment.application.database == "mysql":
711 return backup_mysql_database(outdir, deployment)
713 raise NotImplementedError
715 def backup_mysql_database(outdir, deployment):
717 Database backups for MySQL using the :command:`mysqldump` utility.
719 outfile = os.path.join(outdir, "db.sql")
721 shell.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
722 shell.call("gzip", "--best", outfile)
723 except shell.CallError as e:
724 raise BackupFailure(e.stderr)
726 def restore_database(backup_dir, deployment):
728 Generic database restoration function for MySQL.
730 # XXX: see backup_database
731 if deployment.application.database == "mysql":
732 return restore_mysql_database(backup_dir, deployment)
734 raise NotImplementedError
736 def restore_mysql_database(backup_dir, deployment):
738 Database restoration for MySQL by piping SQL commands into :command:`mysql`.
740 if not os.path.exists(backup_dir):
741 raise RestoreFailure("Backup %s doesn't exist", backup_dir.rpartition("/")[2])
742 sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
743 shell.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
745 shell.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
748 def remove_database(deployment):
750 Generic database removal function. Actually, not so generic because we
751 go and check if we're on scripts and if we are run a different command.
753 if deployment.dsn.host == "sql.mit.edu":
755 shell.call("/mit/scripts/sql/bin/drop-database", deployment.dsn.database)
757 except shell.CallError:
759 engine = sqlalchemy.create_engine(deployment.dsn)
760 engine.execute("DROP DATABASE `%s`" % deployment.dsn.database)
762 def get_mysql_args(dsn):
764 Extracts arguments that would be passed to the command line mysql utility
769 args += ["-h", dsn.host]
771 args += ["-u", dsn.username]
773 args += ["-p" + dsn.password]
774 args += [dsn.database]
777 class Error(wizard.Error):
778 """Generic error class for this module."""
781 class NoRepositoryError(Error):
783 :class:`Application` does not appear to have a Git repository
784 in the normal location.
786 #: The name of the application that does not have a Git repository.
788 def __init__(self, app):
791 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
793 class DeploymentParseError(Error):
795 Could not parse ``value`` from :term:`versions store`.
797 #: The value that failed to parse.
799 #: The location of the autoinstall that threw this variable.
800 #: This should be set by error handling code when it is available.
802 def __init__(self, value):
805 class NoSuchApplication(Error):
807 You attempted to reference a :class:`Application` named
808 ``app``, which is not recognized by Wizard.
810 #: The name of the application that does not exist.
812 #: The location of the autoinstall that threw this variable.
813 #: This should be set by error handling code when it is availble.
815 def __init__(self, app):
818 class Failure(Error):
820 Represents a failure when performing some double-dispatched operation
821 such as an installation or an upgrade. Failure classes are postfixed
822 with Failure, not Error.
826 class InstallFailure(Error):
827 """Installation failed for unknown reason."""
831 ERROR: Installation failed for unknown reason. You can
832 retry the installation by appending --retry to the installation
835 class RecoverableInstallFailure(InstallFailure):
837 Installation failed, but we were able to determine what the
838 error was, and should give the user a second chance if we were
839 running interactively.
841 #: List of the errors that were found.
843 def __init__(self, errors):
848 ERROR: Installation failed due to the following errors: %s
850 You can retry the installation by appending --retry to the
851 installation command.""" % ", ".join(self.errors)
853 class UpgradeFailure(Failure):
854 """Upgrade script failed."""
855 #: String details of failure (possibly stdout or stderr output)
857 def __init__(self, details):
858 self.details = details
862 ERROR: Upgrade script failed, details:
866 class UpgradeVerificationFailure(Failure):
867 """Upgrade script passed, but website wasn't accessible afterwards"""
871 ERROR: Upgrade script passed, but website wasn't accessible afterwards. Check
872 the debug logs for the contents of the page."""
874 class BackupFailure(Failure):
875 """Backup script failed."""
876 #: String details of failure
878 def __init__(self, details):
879 self.details = details
883 ERROR: Backup script failed, details:
887 class RestoreFailure(Failure):
888 """Restore script failed."""
889 #: String details of failure
891 def __init__(self, details):
892 self.details = details
896 ERROR: Restore script failed, details:
900 class RemoveFailure(Failure):
901 """Remove script failed."""
902 #: String details of failure
904 def __init__(self, details):
905 self.details = details
909 ERROR: Remove script failed, details: