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, 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, entry 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 appclass = entry.load()
91 _applications[appname] = appclass(appname)
94 def getApplication(appname):
95 """Retrieves application instance given a name"""
96 return applications()[appname]
98 class Application(object):
100 Represents an application, i.e. mediawiki or phpbb.
103 Many of these methods assume a specific working
104 directory; prefer using the corresponding methods
105 in :class:`wizard.deploy.Deployment` and its subclasses.
107 #: String name of the application
109 #: Dictionary of version strings to :class:`ApplicationVersion`.
110 #: See also :meth:`makeVersion`.
112 #: List of files that need to be modified when parametrizing.
113 #: This is a class-wide constant, and should not normally be modified.
114 parametrized_files = []
115 #: Keys that are used in older versions of the application, but
116 #: not for the most recent version.
117 deprecated_keys = set()
118 #: Keys that we can simply generate random strings for if they're missing
120 #: Values that are not sufficiently random for a random key. This can
121 #: include default values for a random configuration option,
122 random_blacklist = set()
123 #: Dictionary of variable names to extractor functions. These functions
124 #: take a :class:`wizard.deploy.Deployment` as an argument and return the value of
125 #: the variable, or ``None`` if it could not be found.
126 #: See also :func:`filename_regex_extractor`.
128 #: Dictionary of variable names to substitution functions. These functions
129 #: take a :class:`wizard.deploy.Deployment` as an argument and modify the deployment such
130 #: that an explicit instance of the variable is released with the generic
131 #: ``WIZARD_*`` constant. See also :func:`filename_regex_substitution`.
133 #: Dictionary of file names to a list of resolutions, which are tuples of
134 #: a conflict marker string and a result list. See :mod:`wizard.resolve`
135 #: for more information.
137 #: Instance of :class:`wizard.install.ArgSchema` that defines the arguments
138 #: this application requires.
139 install_schema = None
140 #: Name of the database that this application uses, i.e. ``mysql`` or
141 #: ``postgres``. If we end up supporting multiple databases for a single
142 #: application, there should also be a value for this in
143 #: :class:`wizard.deploy.Deployment`; the value here is merely the preferred
146 #: Indicates whether or not a web stub is necessary.
147 needs_web_stub = False
148 def __init__(self, name):
152 self._extractors = {}
153 self._substitutions = {}
154 def repository(self, srv_path):
156 Returns the Git repository that would contain this application.
157 ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
159 repo = os.path.join(srv_path, self.name + ".git")
160 if not os.path.isdir(repo):
161 repo = os.path.join(srv_path, self.name, ".git")
162 if not os.path.isdir(repo):
163 raise NoRepositoryError(self.name)
165 def makeVersion(self, version):
167 Creates or retrieves the :class:`ApplicationVersion` singleton for the
170 if version not in self.versions:
171 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
172 return self.versions[version]
173 def extract(self, deployment):
175 Extracts wizard variables from a deployment. Default implementation
176 uses :attr:`extractors`.
179 for k,extractor in self.extractors.items():
180 result[k] = extractor(deployment)
181 # XXX: ugh... we have to do quoting
182 for k in self.random_keys:
183 if result[k] is None or result[k] in self.random_blacklist:
184 result[k] = "'%s'" % util.random_key()
186 def dsn(self, deployment):
188 Returns the deployment specific database URL. Uses the override file
189 in :file:`.wizard` if it exists, and otherwise attempt to extract the
190 variables from the source files.
192 Under some cases, the database URL will contain only the database
193 property, and no other values. This indicates that the actual DSN
194 should be determined from the environment.
196 This function might return ``None``.
200 We are allowed to batch these two together, because the full precedence
201 chain for determining the database of an application combines these
202 two together. If this was not the case, we would have to call
203 :meth:`databaseUrlFromOverride` and :meth:`databaseUrlFromExtract` manually.
205 url = self.dsnFromOverride(deployment)
208 return self.dsnFromExtract(deployment)
209 def dsnFromOverride(self, deployment):
211 Extracts database URL from an explicit dsn override file.
214 return sqlalchemy.engine.url.make_url(open(deployment.dsn_file).read().strip())
217 def dsnFromExtract(self, deployment):
219 Extracts database URL from a deployment, and returns them as
220 a :class:`sqlalchemy.engine.url.URL`. Returns ``None`` if we
221 can't figure it out: i.e. the conventional variables are not defined
222 for this application.
224 if not self.database:
226 vars = self.extract(deployment)
227 names = ("WIZARD_DBSERVER", "WIZARD_DBUSER", "WIZARD_DBPASSWORD", "WIZARD_DBNAME")
228 host, user, password, database = (shlex.split(vars[x])[0] if vars[x] is not None else None for x in names)
229 # XXX: You'd have to put support for an explicit different database
231 return sqlalchemy.engine.url.URL(self.database, username=user, password=password, host=host, database=database)
232 def url(self, deployment):
234 Returns the deployment specific web URL. Uses the override file
235 in :file:`.wizard` if it exists, and otherwise attempt to extract
236 the variables from the source files.
238 This function might return ``None``, which indicates we couldn't figure
241 url = self.urlFromOverride(deployment)
244 return self.urlFromExtract(deployment)
245 def urlFromOverride(self, deployment):
247 Extracts URL from explicit url override file.
250 return urlparse.urlparse(open(deployment.url_file).read().strip())
253 def urlFromExtract(self, deployment):
255 Extracts URL from a deployment, and returns ``None`` if we can't
256 figure it out. Default implementation is to fail; we might
257 do something clever with extractable variables in the future.
260 def parametrize(self, deployment, ref_deployment):
262 Takes a generic source checkout and parametrizes it according to the
263 values of ``deployment``. This function operates on the current
264 working directory. ``deployment`` should **not** be the same as the
265 current working directory. See :meth:`parametrizeWithVars` for details
266 on the parametrization.
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):
275 Takes a generic source checkout and parametrizes it according to
276 the values of ``variables``. Default implementation uses
277 :attr:`parametrized_files` and a simple search and replace on
280 for file in self.parametrized_files:
281 logging.debug("Parametrizing file '%s'\n" % (file, ))
283 contents = open(file, "r").read()
285 logging.debug("Failed to open file '%s'\n" % (file, ))
287 for key, value in variables.items():
288 if value is None: continue
289 contents = contents.replace(key, value)
292 def resolveConflicts(self, deployment):
294 Resolves conflicted files in the current working directory. Returns
295 whether or not all conflicted files were resolved or not. Fully
296 resolved files are added to the index, but no commit is made. The
297 default implementation uses :attr:`resolutions`.
302 for status in shell.eval("git", "ls-files", "--unmerged").splitlines():
303 mode, hash, role, name = status.split()
304 files.setdefault(name, set()).add(int(role))
305 for file, roles in files.items():
306 # some automatic resolutions
307 if 1 not in roles and 2 not in roles and 3 in roles:
308 # upstream added a file, but it conflicted for whatever reason
309 shell.call("git", "add", file)
311 elif 1 in roles and 2 not in roles and 3 in roles:
312 # user deleted the file, but upstream changed it
313 shell.call("git", "rm", file)
316 # XXX: this functionality is mostly subsumed by the rerere
318 if file in self.resolutions:
319 contents = open(file, "r").read()
320 for spec, result in self.resolutions[file]:
321 old_contents = contents
322 contents = resolve.resolve(contents, spec, result)
323 if old_contents != contents:
324 logging.info("Did resolution with spec:\n" + spec)
325 open(file, "w").write(contents)
326 if not resolve.is_conflict(contents):
327 shell.call("git", "add", file)
333 def prepareMerge(self, deployment):
335 Performs various edits to files in the current working directory in
336 order to make a merge go more smoothly. This is usually
337 used to fix botched line-endings. If you add new files,
338 you have to 'git add' them; this is not necessary for edits.
339 By default this is a no-op; subclasses should replace this
340 with useful behavior.
343 def prepareConfig(self, deployment):
345 Takes a deployment and replaces any explicit instances
346 of a configuration variable with generic ``WIZARD_*`` constants.
347 The default implementation uses :attr:`substitutions`, and
348 emits warnings when it encounters keys in :attr:`deprecated_keys`.
350 for key, subst in self.substitutions.items():
351 subs = subst(deployment)
352 if not subs and key not in self.deprecated_keys and key not in self.random_keys:
353 logging.warning("No substitutions for %s" % key)
354 def install(self, version, options):
356 Run for 'wizard configure' (and, by proxy, 'wizard install') to
357 configure an application. This assumes that the current working
358 directory is a deployment. (Unlike its kin, this function does not
359 take a :class:`wizard.deploy.Deployment` as a parameter.) Subclasses should
360 provide an implementation.
362 raise NotImplementedError
363 def upgrade(self, deployment, version, options):
365 Run for 'wizard upgrade' to upgrade database schemas and other
366 non-versioned data in an application after the filesystem has been
367 upgraded. This assumes that the current working directory is the
368 deployment. Subclasses should provide an implementation.
370 raise NotImplementedError
371 def backup(self, deployment, outdir, options):
373 Run for 'wizard backup' and upgrades to backup database schemas
374 and other non-versioned data in an application. ``outdir`` is
375 the directory that backup files should be placed. This assumes
376 that the current working directory is the deployment. Subclasses
377 should provide an implementation, even if it is a no-op.
380 Static user files may not need to be backed up, since in
381 many applications upgrades do not modify static files.
383 raise NotImplementedError
384 def restore(self, deployment, backup_dir, options):
386 Run for 'wizard restore' and failed upgrades to restore database
387 and other non-versioned data to a backed up version. This assumes
388 that the current working directory is the deployment. Subclasses
389 should provide an implementation.
391 raise NotImplementedError
392 def remove(self, deployment, options):
394 Run for 'wizard remove' to delete all database and non-local
395 file data. This assumes that the current working directory is
396 the deployment. Subclasses should provide an implementation.
398 raise NotImplementedError
399 def detectVersion(self, deployment):
401 Checks source files to determine the version manually. This assumes
402 that the current working directory is the deployment. Subclasses
403 should provide an implementation.
405 raise NotImplementedError
406 def detectVersionFromFile(self, filename, regex):
408 Helper method that detects a version by using a regular expression
409 from a file. The regexed value is passed through :mod:`shlex`.
410 This assumes that the current working directory is the deployment.
412 contents = open(filename).read()
413 match = regex.search(contents)
414 if not match: return None
415 return distutils.version.LooseVersion(shlex.split(match.group(2))[0])
416 # XXX: This signature doesn't really make too much sense...
417 def detectVersionFromGit(self, tagPattern, preStrip = ''):
419 Helper method that detects a version by using the most recent tag
420 in git that matches the specified pattern.
421 This assumes that the current working directory is the deployment.
423 sh = wizard.shell.Shell()
424 cmd = ['git', 'describe', '--tags', '--match', tagPattern, ]
425 tag = sh.call(*cmd, strip=True)
426 if tag and len(tag) > len(preStrip) and tag[:len(preStrip)] == preStrip:
427 tag = tag[len(preStrip):]
428 if not tag: return None
429 return distutils.version.LooseVersion(tag)
430 def download(self, version):
432 Returns a URL that can be used to download a tarball of ``version`` of
435 raise NotImplementedError
436 def checkWeb(self, deployment):
438 Checks if the autoinstall is viewable from the web. Subclasses should
439 provide an implementation.
442 Finding a reasonable heuristic that works across skinning
443 choices can be difficult. We've had reasonable success
444 searching for metadata. Be sure that the standard error
445 page does not contain the features you search for. Try
446 not to depend on pages that are not the main page.
448 raise NotImplementedError
449 def checkDatabase(self, deployment):
451 Checks if the database is accessible.
454 sql.connect(deployment.dsn)
456 except sqlalchemy.exc.DBAPIError:
458 def checkWebPage(self, deployment, page, outputs=[], exclude=[]):
460 Checks if a given page of an autoinstall contains a particular string.
462 page = deployment.fetch(page)
464 if page.find(x) != -1:
465 logging.info("checkWebPage (failed due to %s):\n\n%s", x, page)
468 for output in outputs:
469 votes += page.find(output) != -1
470 if votes > len(outputs) / 2:
471 logging.debug("checkWebPage (passed):\n\n" + page)
474 logging.info("checkWebPage (failed):\n\n" + page)
476 def checkConfig(self, deployment):
478 Checks whether or not an autoinstall has been configured/installed
479 for use. Assumes that the current working directory is the deployment.
480 Subclasses should provide an implementation.
482 # XXX: Unfortunately, this doesn't quite work because we package
483 # bogus config files. Maybe we should check a hash or
485 raise NotImplementedError
486 def researchFilter(self, filename, added, deleted):
488 Allows an application to selectively ignore certain diffstat signatures
489 during research; for example, configuration files will have a very
490 specific set of changes, so ignore them; certain installation files
491 may be removed, etc. Return ``True`` if a diffstat signature should be
495 def researchVerbose(self, filename):
497 Allows an application to exclude certain dirty files from the output
498 report; usually this will just be parametrized files, since those are
499 guaranteed to have changes. Return ``True`` if a file should only
500 be displayed in verbose mode.
502 return filename in self.parametrized_files
504 class ApplicationVersion(object):
505 """Represents an abstract notion of a version for an application, where
506 ``version`` is a :class:`distutils.version.LooseVersion` and
507 ``application`` is a :class:`Application`."""
508 #: The :class:`distutils.version.LooseVersion` of this instance.
510 #: The :class:`Application` of this instance.
512 def __init__(self, version, application):
513 self.version = version
514 self.application = application
518 Returns the name of the git describe tag for the commit the user is
519 presently on, something like mediawiki-1.2.3-scripts-4-g123abcd
521 return "%s-%s" % (self.application, self.version)
523 def wizard_tag(self):
525 Returns the name of the Git tag for this version.
527 # XXX: Scripts specific
528 end = str(self.version).partition('-scripts')[2].partition('-')[0]
529 return "%s-scripts%s" % (self.pristine_tag, end)
531 def pristine_tag(self):
533 Returns the name of the Git tag for the pristine version corresponding
536 return "%s-%s" % (self.application.name, str(self.version).partition('-scripts')[0])
537 def __cmp__(self, y):
538 return cmp(self.version, y.version)
542 Parses a line from the :term:`versions store` and return
543 :class:`ApplicationVersion`.
545 Use this only for cases when speed is of primary importance;
546 the data in version is unreliable and when possible, you should
547 prefer directly instantiating a :class:`wizard.deploy.Deployment` and having it query
548 the autoinstall itself for information.
550 The `value` to parse will vary. For old style installs, it
553 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
555 For new style installs, it will look like::
559 name = value.split("/")[-1]
561 if name.find("-") != -1:
562 app, _, version = name.partition("-")
564 # kind of poor, maybe should error. Generally this
565 # will actually result in a not found error
569 raise DeploymentParseError(value)
570 return ApplicationVersion.make(app, version)
572 def make(app, version):
574 Makes/retrieves a singleton :class:`ApplicationVersion` from
575 a``app`` and ``version`` string.
578 # defer to the application for version creation to enforce
580 return applications()[app].makeVersion(version)
582 raise NoSuchApplication(app)
586 Takes a tree of values (implement using nested lists) and
587 transforms them into regular expressions.
591 >>> expand_re(['a', 'b'])
593 >>> expand_re(['*', ['b', 'c']])
596 if isinstance(val, str):
597 return re.escape(val)
599 return '(?:' + '|'.join(map(expand_re, val)) + ')'
601 def make_extractors(seed):
603 Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into
604 extractor functions (which take a :class:`wizard.deploy.Deployment`
605 and return the value of the second subpattern of ``regex`` when matched
606 with the contents of ``file``).
608 return util.dictmap(lambda a: filename_regex_extractor(*a), seed)
610 def make_substitutions(seed):
612 Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into substitution
613 functions (which take a :class:`wizard.deploy.Deployment`, replace the second subpattern
614 of ``regex`` with ``key`` in ``file``, and returns the number of substitutions made.)
616 return util.dictkmap(lambda k, v: filename_regex_substitution(k, *v), seed)
618 # The following two functions are *highly* functional, and I recommend
619 # not touching them unless you know what you're doing.
621 def filename_regex_extractor(file, regex):
623 .. highlight:: haskell
625 Given a relative file name ``file``, a regular expression ``regex``, and a
626 :class:`wizard.deploy.Deployment` extracts a value out of the file in that
627 deployment. This function is curried, so you pass just ``file`` and
628 ``regex``, and then pass ``deployment`` to the resulting function.
630 Its Haskell-style type signature would be::
632 Filename -> Regex -> (Deployment -> String)
634 The regular expression requires a very specific form, essentially ``()()()``
635 (with the second subgroup being the value to extract). These enables
636 the regular expression to be used equivalently with filename
638 .. highlight:: python
640 For convenience purposes, we also accept ``[Filename]``, in which case
641 we use the first entry (index 0). Passing an empty list is invalid.
643 >>> open("test-settings.extractor.ini", "w").write("config_var = 3\\n")
644 >>> f = filename_regex_extractor('test-settings.extractor.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
645 >>> f(deploy.Deployment("."))
647 >>> os.unlink("test-settings.extractor.ini")
650 The first application of ``regex`` and ``file`` is normally performed
651 at compile-time inside a submodule; the second application is
652 performed at runtime.
654 if not isinstance(file, str):
658 contents = deployment.read(file) # cached
661 match = regex.search(contents)
662 if not match: return None
663 # assumes that the second match is the one we want.
664 return match.group(2)
667 def filename_regex_substitution(key, files, regex):
669 .. highlight:: haskell
671 Given a Wizard ``key`` (``WIZARD_*``), a list of ``files``, a
672 regular expression ``regex``, and a :class:`wizard.deploy.Deployment`
673 performs a substitution of the second subpattern of ``regex``
674 with ``key``. Returns the number of replacements made. This function
675 is curried, so you pass just ``key``, ``files`` and ``regex``, and
676 then pass ``deployment`` to the resulting function.
678 Its Haskell-style type signature would be::
680 Key -> ([File], Regex) -> (Deployment -> IO Int)
682 .. highlight:: python
684 For convenience purposes, we also accept ``Filename``, in which case it is treated
685 as a single item list.
687 >>> open("test-settings.substitution.ini", "w").write("config_var = 3")
688 >>> f = filename_regex_substitution('WIZARD_KEY', 'test-settings.substitution.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
689 >>> f(deploy.Deployment("."))
691 >>> print open("test-settings.substitution.ini", "r").read()
692 config_var = WIZARD_KEY
693 >>> os.unlink("test-settings.substitution.ini")
695 if isinstance(files, str):
698 base = deployment.location
701 file = os.path.join(base, file)
703 contents = open(file, "r").read()
704 contents, n = regex.subn("\\1" + key + "\\3", contents)
706 open(file, "w").write(contents)
712 def backup_database(outdir, deployment):
714 Generic database backup function for MySQL.
716 # XXX: Change this once deployments support multiple dbs
717 if deployment.application.database == "mysql":
718 return backup_mysql_database(outdir, deployment)
720 raise NotImplementedError
722 def backup_mysql_database(outdir, deployment):
724 Database backups for MySQL using the :command:`mysqldump` utility.
726 outfile = os.path.join(outdir, "db.sql")
728 shell.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
729 shell.call("gzip", "--best", outfile)
730 except shell.CallError as e:
731 raise BackupFailure(e.stderr)
733 def restore_database(backup_dir, deployment):
735 Generic database restoration function for MySQL.
737 # XXX: see backup_database
738 if deployment.application.database == "mysql":
739 return restore_mysql_database(backup_dir, deployment)
741 raise NotImplementedError
743 def restore_mysql_database(backup_dir, deployment):
745 Database restoration for MySQL by piping SQL commands into :command:`mysql`.
747 if not os.path.exists(backup_dir):
748 raise RestoreFailure("Backup %s doesn't exist", backup_dir.rpartition("/")[2])
749 sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
750 shell.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
752 shell.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
756 def remove_database(deployment):
758 Generic database removal function. Actually, not so generic because we
759 go and check if we're on scripts and if we are run a different command.
761 if deployment.dsn.host == "sql.mit.edu":
763 shell.call("/mit/scripts/sql/bin/drop-database", deployment.dsn.database)
765 except shell.CallError:
767 engine = sqlalchemy.create_engine(deployment.dsn)
768 engine.execute("DROP DATABASE `%s`" % deployment.dsn.database)
770 def get_mysql_args(dsn):
772 Extracts arguments that would be passed to the command line mysql utility
777 args += ["-h", dsn.host]
779 args += ["-u", dsn.username]
781 args += ["-p" + dsn.password]
782 args += [dsn.database]
785 class Error(wizard.Error):
786 """Generic error class for this module."""
789 class NoRepositoryError(Error):
791 :class:`Application` does not appear to have a Git repository
792 in the normal location.
794 #: The name of the application that does not have a Git repository.
796 def __init__(self, app):
799 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
801 class DeploymentParseError(Error):
803 Could not parse ``value`` from :term:`versions store`.
805 #: The value that failed to parse.
807 #: The location of the autoinstall that threw this variable.
808 #: This should be set by error handling code when it is available.
810 def __init__(self, value):
813 class NoSuchApplication(Error):
815 You attempted to reference a :class:`Application` named
816 ``app``, which is not recognized by Wizard.
818 #: The name of the application that does not exist.
820 #: The location of the autoinstall that threw this variable.
821 #: This should be set by error handling code when it is availble.
823 def __init__(self, app):
826 class Failure(Error):
828 Represents a failure when performing some double-dispatched operation
829 such as an installation or an upgrade. Failure classes are postfixed
830 with Failure, not Error.
834 class InstallFailure(Error):
835 """Installation failed for unknown reason."""
839 ERROR: Installation failed for unknown reason. You can
840 retry the installation by appending --retry to the installation
843 class RecoverableInstallFailure(InstallFailure):
845 Installation failed, but we were able to determine what the
846 error was, and should give the user a second chance if we were
847 running interactively.
849 #: List of the errors that were found.
851 def __init__(self, errors):
856 ERROR: Installation failed due to the following errors: %s
858 You can retry the installation by appending --retry to the
859 installation command.""" % ", ".join(self.errors)
861 class UpgradeFailure(Failure):
862 """Upgrade script failed."""
863 #: String details of failure (possibly stdout or stderr output)
865 def __init__(self, details):
866 self.details = details
870 ERROR: Upgrade script failed, details:
874 class UpgradeVerificationFailure(Failure):
875 """Upgrade script passed, but website wasn't accessible afterwards"""
879 ERROR: Upgrade script passed, but website wasn't accessible afterwards. Check
880 the debug logs for the contents of the page."""
882 class BackupFailure(Failure):
883 """Backup script failed."""
884 #: String details of failure
886 def __init__(self, details):
887 self.details = details
891 ERROR: Backup script failed, details:
895 class RestoreFailure(Failure):
896 """Restore script failed."""
897 #: String details of failure
899 def __init__(self, details):
900 self.details = details
904 ERROR: Restore script failed, details:
908 class RemoveFailure(Failure):
909 """Remove script failed."""
910 #: String details of failure
912 def __init__(self, details):
913 self.details = details
917 ERROR: Remove script failed, details: