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 sh = wizard.shell.Shell()
417 cmd = ['git', 'describe', '--match', tagPattern, ]
418 tag = sh.call(*cmd, strip=True)
419 if tag and len(tag) > len(preStrip) and tag[:len(preStrip)] == preStrip:
420 tag = tag[len(preStrip):]
421 if not tag: return None
422 return distutils.version.LooseVersion(tag)
423 def download(self, version):
425 Returns a URL that can be used to download a tarball of ``version`` of
428 raise NotImplementedError
429 def checkWeb(self, deployment):
431 Checks if the autoinstall is viewable from the web. Subclasses should
432 provide an implementation.
435 Finding a reasonable heuristic that works across skinning
436 choices can be difficult. We've had reasonable success
437 searching for metadata. Be sure that the standard error
438 page does not contain the features you search for. Try
439 not to depend on pages that are not the main page.
441 raise NotImplementedError
442 def checkDatabase(self, deployment):
444 Checks if the database is accessible.
447 sql.connect(deployment.dsn)
449 except sqlalchemy.exc.DBAPIError:
451 def checkWebPage(self, deployment, page, outputs=[], exclude=[]):
453 Checks if a given page of an autoinstall contains a particular string.
455 page = deployment.fetch(page)
457 if page.find(x) != -1:
458 logging.info("checkWebPage (failed due to %s):\n\n%s", x, page)
461 for output in outputs:
462 votes += page.find(output) != -1
463 if votes > len(outputs) / 2:
464 logging.debug("checkWebPage (passed):\n\n" + page)
467 logging.info("checkWebPage (failed):\n\n" + page)
469 def checkConfig(self, deployment):
471 Checks whether or not an autoinstall has been configured/installed
472 for use. Assumes that the current working directory is the deployment.
473 Subclasses should provide an implementation.
475 # XXX: Unfortunately, this doesn't quite work because we package
476 # bogus config files in the -scripts versions of installs. Maybe
477 # we should check a hash or something?
478 raise NotImplementedError
479 def researchFilter(self, filename, added, deleted):
481 Allows an application to selectively ignore certain diffstat signatures
482 during research; for example, configuration files will have a very
483 specific set of changes, so ignore them; certain installation files
484 may be removed, etc. Return ``True`` if a diffstat signature should be
488 def researchVerbose(self, filename):
490 Allows an application to exclude certain dirty files from the output
491 report; usually this will just be parametrized files, since those are
492 guaranteed to have changes. Return ``True`` if a file should only
493 be displayed in verbose mode.
495 return filename in self.parametrized_files
497 class ApplicationVersion(object):
498 """Represents an abstract notion of a version for an application, where
499 ``version`` is a :class:`distutils.version.LooseVersion` and
500 ``application`` is a :class:`Application`."""
501 #: The :class:`distutils.version.LooseVersion` of this instance.
503 #: The :class:`Application` of this instance.
505 def __init__(self, version, application):
506 self.version = version
507 self.application = application
511 Returns the name of the git describe tag for the commit the user is
512 presently on, something like mediawiki-1.2.3-scripts-4-g123abcd
514 return "%s-%s" % (self.application, self.version)
516 def scripts_tag(self):
518 Returns the name of the Git tag for this version.
520 end = str(self.version).partition('-scripts')[2].partition('-')[0]
521 return "%s-scripts%s" % (self.pristine_tag, end)
523 def pristine_tag(self):
525 Returns the name of the Git tag for the pristine version corresponding
528 return "%s-%s" % (self.application.name, str(self.version).partition('-scripts')[0])
529 def __cmp__(self, y):
530 return cmp(self.version, y.version)
534 Parses a line from the :term:`versions store` and return
535 :class:`ApplicationVersion`.
537 Use this only for cases when speed is of primary importance;
538 the data in version is unreliable and when possible, you should
539 prefer directly instantiating a :class:`wizard.deploy.Deployment` and having it query
540 the autoinstall itself for information.
542 The `value` to parse will vary. For old style installs, it
545 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
547 For new style installs, it will look like::
551 name = value.split("/")[-1]
553 if name.find("-") != -1:
554 app, _, version = name.partition("-")
556 # kind of poor, maybe should error. Generally this
557 # will actually result in a not found error
561 raise DeploymentParseError(value)
562 return ApplicationVersion.make(app, version)
564 def make(app, version):
566 Makes/retrieves a singleton :class:`ApplicationVersion` from
567 a``app`` and ``version`` string.
570 # defer to the application for version creation to enforce
572 return applications()[app].makeVersion(version)
574 raise NoSuchApplication(app)
578 Takes a tree of values (implement using nested lists) and
579 transforms them into regular expressions.
583 >>> expand_re(['a', 'b'])
585 >>> expand_re(['*', ['b', 'c']])
588 if isinstance(val, str):
589 return re.escape(val)
591 return '(?:' + '|'.join(map(expand_re, val)) + ')'
593 def make_extractors(seed):
595 Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into
596 extractor functions (which take a :class:`wizard.deploy.Deployment`
597 and return the value of the second subpattern of ``regex`` when matched
598 with the contents of ``file``).
600 return util.dictmap(lambda a: filename_regex_extractor(*a), seed)
602 def make_substitutions(seed):
604 Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into substitution
605 functions (which take a :class:`wizard.deploy.Deployment`, replace the second subpattern
606 of ``regex`` with ``key`` in ``file``, and returns the number of substitutions made.)
608 return util.dictkmap(lambda k, v: filename_regex_substitution(k, *v), seed)
610 # The following two functions are *highly* functional, and I recommend
611 # not touching them unless you know what you're doing.
613 def filename_regex_extractor(file, regex):
615 .. highlight:: haskell
617 Given a relative file name ``file``, a regular expression ``regex``, and a
618 :class:`wizard.deploy.Deployment` extracts a value out of the file in that
619 deployment. This function is curried, so you pass just ``file`` and
620 ``regex``, and then pass ``deployment`` to the resulting function.
622 Its Haskell-style type signature would be::
624 Filename -> Regex -> (Deployment -> String)
626 The regular expression requires a very specific form, essentially ``()()()``
627 (with the second subgroup being the value to extract). These enables
628 the regular expression to be used equivalently with filename
630 .. highlight:: python
632 For convenience purposes, we also accept ``[Filename]``, in which case
633 we use the first entry (index 0). Passing an empty list is invalid.
635 >>> open("test-settings.extractor.ini", "w").write("config_var = 3\\n")
636 >>> f = filename_regex_extractor('test-settings.extractor.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
637 >>> f(deploy.Deployment("."))
639 >>> os.unlink("test-settings.extractor.ini")
642 The first application of ``regex`` and ``file`` is normally performed
643 at compile-time inside a submodule; the second application is
644 performed at runtime.
646 if not isinstance(file, str):
650 contents = deployment.read(file) # cached
653 match = regex.search(contents)
654 if not match: return None
655 # assumes that the second match is the one we want.
656 return match.group(2)
659 def filename_regex_substitution(key, files, regex):
661 .. highlight:: haskell
663 Given a Wizard ``key`` (``WIZARD_*``), a list of ``files``, a
664 regular expression ``regex``, and a :class:`wizard.deploy.Deployment`
665 performs a substitution of the second subpattern of ``regex``
666 with ``key``. Returns the number of replacements made. This function
667 is curried, so you pass just ``key``, ``files`` and ``regex``, and
668 then pass ``deployment`` to the resulting function.
670 Its Haskell-style type signature would be::
672 Key -> ([File], Regex) -> (Deployment -> IO Int)
674 .. highlight:: python
676 For convenience purposes, we also accept ``Filename``, in which case it is treated
677 as a single item list.
679 >>> open("test-settings.substitution.ini", "w").write("config_var = 3")
680 >>> f = filename_regex_substitution('WIZARD_KEY', 'test-settings.substitution.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
681 >>> f(deploy.Deployment("."))
683 >>> print open("test-settings.substitution.ini", "r").read()
684 config_var = WIZARD_KEY
685 >>> os.unlink("test-settings.substitution.ini")
687 if isinstance(files, str):
690 base = deployment.location
693 file = os.path.join(base, file)
695 contents = open(file, "r").read()
696 contents, n = regex.subn("\\1" + key + "\\3", contents)
698 open(file, "w").write(contents)
704 def backup_database(outdir, deployment):
706 Generic database backup function for MySQL.
708 # XXX: Change this once deployments support multiple dbs
709 if deployment.application.database == "mysql":
710 return backup_mysql_database(outdir, deployment)
712 raise NotImplementedError
714 def backup_mysql_database(outdir, deployment):
716 Database backups for MySQL using the :command:`mysqldump` utility.
718 outfile = os.path.join(outdir, "db.sql")
720 shell.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
721 shell.call("gzip", "--best", outfile)
722 except shell.CallError as e:
723 raise BackupFailure(e.stderr)
725 def restore_database(backup_dir, deployment):
727 Generic database restoration function for MySQL.
729 # XXX: see backup_database
730 if deployment.application.database == "mysql":
731 return restore_mysql_database(backup_dir, deployment)
733 raise NotImplementedError
735 def restore_mysql_database(backup_dir, deployment):
737 Database restoration for MySQL by piping SQL commands into :command:`mysql`.
739 if not os.path.exists(backup_dir):
740 raise RestoreFailure("Backup %s doesn't exist", backup_dir.rpartition("/")[2])
741 sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
742 shell.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
744 shell.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
747 def remove_database(deployment):
749 Generic database removal function. Actually, not so generic because we
750 go and check if we're on scripts and if we are run a different command.
752 if deployment.dsn.host == "sql.mit.edu":
754 shell.call("/mit/scripts/sql/bin/drop-database", deployment.dsn.database)
756 except shell.CallError:
758 engine = sqlalchemy.create_engine(deployment.dsn)
759 engine.execute("DROP DATABASE `%s`" % deployment.dsn.database)
761 def get_mysql_args(dsn):
763 Extracts arguments that would be passed to the command line mysql utility
768 args += ["-h", dsn.host]
770 args += ["-u", dsn.username]
772 args += ["-p" + dsn.password]
773 args += [dsn.database]
776 class Error(wizard.Error):
777 """Generic error class for this module."""
780 class NoRepositoryError(Error):
782 :class:`Application` does not appear to have a Git repository
783 in the normal location.
785 #: The name of the application that does not have a Git repository.
787 def __init__(self, app):
790 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
792 class DeploymentParseError(Error):
794 Could not parse ``value`` from :term:`versions store`.
796 #: The value that failed to parse.
798 #: The location of the autoinstall that threw this variable.
799 #: This should be set by error handling code when it is available.
801 def __init__(self, value):
804 class NoSuchApplication(Error):
806 You attempted to reference a :class:`Application` named
807 ``app``, which is not recognized by Wizard.
809 #: The name of the application that does not exist.
811 #: The location of the autoinstall that threw this variable.
812 #: This should be set by error handling code when it is availble.
814 def __init__(self, app):
817 class Failure(Error):
819 Represents a failure when performing some double-dispatched operation
820 such as an installation or an upgrade. Failure classes are postfixed
821 with Failure, not Error.
825 class InstallFailure(Error):
826 """Installation failed for unknown reason."""
830 ERROR: Installation failed for unknown reason. You can
831 retry the installation by appending --retry to the installation
834 class RecoverableInstallFailure(InstallFailure):
836 Installation failed, but we were able to determine what the
837 error was, and should give the user a second chance if we were
838 running interactively.
840 #: List of the errors that were found.
842 def __init__(self, errors):
847 ERROR: Installation failed due to the following errors: %s
849 You can retry the installation by appending --retry to the
850 installation command.""" % ", ".join(self.errors)
852 class UpgradeFailure(Failure):
853 """Upgrade script failed."""
854 #: String details of failure (possibly stdout or stderr output)
856 def __init__(self, details):
857 self.details = details
861 ERROR: Upgrade script failed, details:
865 class UpgradeVerificationFailure(Failure):
866 """Upgrade script passed, but website wasn't accessible afterwards"""
870 ERROR: Upgrade script passed, but website wasn't accessible afterwards. Check
871 the debug logs for the contents of the page."""
873 class BackupFailure(Failure):
874 """Backup script failed."""
875 #: String details of failure
877 def __init__(self, details):
878 self.details = details
882 ERROR: Backup script failed, details:
886 class RestoreFailure(Failure):
887 """Restore script failed."""
888 #: String details of failure
890 def __init__(self, details):
891 self.details = details
895 ERROR: Restore script failed, details:
899 class RemoveFailure(Failure):
900 """Remove script failed."""
901 #: String details of failure
903 def __init__(self, details):
904 self.details = details
908 ERROR: Remove script failed, details: