]> scripts.mit.edu Git - wizard.git/blob - wizard/app/__init__.py
391f8d8956aeb0f1192d04e7fa07cc2e51c0aeda
[wizard.git] / wizard / app / __init__.py
1 """
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
7 new applications.
8
9 There are some submodules for programming languages that define common
10 functions and data that may be used by applications in that language.  See:
11
12 * :mod:`wizard.app.php`
13
14 .. testsetup:: *
15
16     import re
17     import shutil
18     import os
19     from wizard import deploy, util
20     from wizard.app import *
21 """
22
23 import os.path
24 import re
25 import distutils.version
26 import decorator
27 import shlex
28 import logging
29 import shutil
30 import sqlalchemy
31 import random
32 import string
33 import urlparse
34 import tempfile
35
36 import wizard
37 from wizard import resolve, scripts, shell, util
38
39 _application_list = [
40     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
41     "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
42     # these are technically deprecated
43     "advancedpoll", "gallery",
44 ]
45 _applications = None
46
47 def applications():
48     """Hash table for looking up string application name to instance"""
49     global _applications
50     if not _applications:
51         _applications = dict([(n,Application.make(n)) for n in _application_list ])
52     return _applications
53
54 def getApplication(appname):
55     """Retrieves application instance given a name"""
56     return applications()[appname]
57
58 class Application(object):
59     """
60     Represents an application, i.e. mediawiki or phpbb.
61
62     .. note::
63         Many of these methods assume a specific working
64         directory; prefer using the corresponding methods
65         in :class:`wizard.deploy.Deployment` and its subclasses.
66     """
67     #: String name of the application
68     name = None
69     #: Dictionary of version strings to :class:`ApplicationVersion`.
70     #: See also :meth:`makeVersion`.
71     versions = None
72     #: List of files that need to be modified when parametrizing.
73     #: This is a class-wide constant, and should not normally be modified.
74     parametrized_files = []
75     #: Keys that are used in older versions of the application, but
76     #: not for the most recent version.
77     deprecated_keys = set()
78     #: Keys that we can simply generate random strings for if they're missing
79     random_keys = set()
80     #: Dictionary of variable names to extractor functions.  These functions
81     #: take a :class:`wizard.deploy.Deployment` as an argument and return the value of
82     #: the variable, or ``None`` if it could not be found.
83     #: See also :func:`filename_regex_extractor`.
84     extractors = {}
85     #: Dictionary of variable names to substitution functions.  These functions
86     #: take a :class:`wizard.deploy.Deployment` as an argument and modify the deployment such
87     #: that an explicit instance of the variable is released with the generic
88     #: ``WIZARD_*`` constant.  See also :func:`filename_regex_substitution`.
89     substitutions = {}
90     #: Dictionary of file names to a list of resolutions, which are tuples of
91     #: a conflict marker string and a result list.  See :mod:`wizard.resolve`
92     #: for more information.
93     resolutions = {}
94     #: Instance of :class:`wizard.install.ArgSchema` that defines the arguments
95     #: this application requires.
96     install_schema = None
97     #: Name of the database that this application uses, i.e. ``mysql`` or
98     #: ``postgres``.  If we end up supporting multiple databases for a single
99     #: application, there should also be a value for this in
100     #: :class:`wizard.deploy.Deployment`; the value here is merely the preferred
101     #: value.
102     database = None
103     def __init__(self, name):
104         self.name = name
105         self.versions = {}
106         # cache variables
107         self._extractors = {}
108         self._substitutions = {}
109     def repository(self, srv_path):
110         """
111         Returns the Git repository that would contain this application.
112         ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
113         """
114         repo = os.path.join(srv_path, self.name + ".git")
115         if not os.path.isdir(repo):
116             repo = os.path.join(srv_path, self.name, ".git")
117             if not os.path.isdir(repo):
118                 raise NoRepositoryError(self.name)
119         return repo
120     def makeVersion(self, version):
121         """
122         Creates or retrieves the :class:`ApplicationVersion` singleton for the
123         specified version.
124         """
125         if version not in self.versions:
126             self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
127         return self.versions[version]
128     def extract(self, deployment):
129         """
130         Extracts wizard variables from a deployment.  Default implementation
131         uses :attr:`extractors`.
132         """
133         result = {}
134         for k,extractor in self.extractors.items():
135             result[k] = extractor(deployment)
136         # XXX: ugh... we have to do quoting
137         for k in self.random_keys:
138             if result[k] is None:
139                 result[k] = "'%s'" % ''.join(random.choice(string.letters + string.digits) for i in xrange(30))
140         return result
141     def dsn(self, deployment):
142         """
143         Returns the deployment specific database URL.  Uses the override file
144         in :file:`.scripts` if it exists, and otherwise attempt to extract the
145         variables from the source files.
146
147         Under some cases, the database URL will contain only the database
148         property, and no other values.  This indicates that the actual DSN
149         should be determined from the environment.
150
151         This function might return ``None``.
152
153         .. note::
154
155             We are allowed to batch these two together, because the full precedence
156             chain for determining the database of an application combines these
157             two together.  If this was not the case, we would have to call
158             :meth:`databaseUrlFromOverride` and :meth:`databaseUrlFromExtract` manually.
159         """
160         url = self.dsnFromOverride(deployment)
161         if url:
162             return url
163         return self.dsnFromExtract(deployment)
164     def dsnFromOverride(self, deployment):
165         """
166         Extracts database URL from an explicit dsn override file.
167         """
168         try:
169             return sqlalchemy.engine.url.make_url(open(deployment.dsn_file).read().strip())
170         except IOError:
171             return None
172     def dsnFromExtract(self, deployment):
173         """
174         Extracts database URL from a deployment, and returns them as
175         a :class:`sqlalchemy.engine.url.URL`.  Returns ``None`` if we
176         can't figure it out: i.e. the conventional variables are not defined
177         for this application.
178         """
179         if not self.database:
180             return None
181         vars = self.extract(deployment)
182         names = ("WIZARD_DBSERVER", "WIZARD_DBUSER", "WIZARD_DBPASSWORD", "WIZARD_DBNAME")
183         host, user, password, database = (shlex.split(vars[x])[0] if vars[x] is not None else None for x in names)
184         # XXX: You'd have to put support for an explicit different database
185         # type here
186         return sqlalchemy.engine.url.URL(self.database, username=user, password=password, host=host, database=database)
187     def url(self, deployment):
188         """
189         Returns the deployment specific web URL.  Uses the override file
190         in :file:`.scripts` if it exists, and otherwise attempt to extract
191         the variables from the source files.
192
193         This function might return ``None``, which indicates we couldn't figure
194         it out.
195         """
196         url = self.urlFromOverride(deployment)
197         if url:
198             return url
199         return self.urlFromExtract(deployment)
200     def urlFromOverride(self, deployment):
201         """
202         Extracts URL from explicit url override file.
203         """
204         try:
205             return urlparse.urlparse(open(deployment.url_file).read().strip())
206         except IOError:
207             return None
208     def urlFromExtract(self, deployment):
209         """
210         Extracts URL from a deployment, and returns ``None`` if we can't
211         figure it out.  Default implementation is to fail; we might
212         do something clever with extractable variables in the future.
213         """
214         return None
215     def parametrize(self, deployment, ref_deployment):
216         """
217         Takes a generic source checkout and parametrizes it according to the
218         values of ``deployment``.  This function operates on the current
219         working directory.  ``deployment`` should **not** be the same as the
220         current working directory.  Default implementation uses
221         :attr:`parametrized_files` and a simple search and replace on those
222         files.
223         """
224         variables = ref_deployment.extract()
225         for file in self.parametrized_files:
226             try:
227                 contents = open(file, "r").read()
228             except IOError:
229                 continue
230             for key, value in variables.items():
231                 if value is None: continue
232                 contents = contents.replace(key, value)
233             f = open(file, "w")
234             f.write(contents)
235     def resolveConflicts(self, deployment):
236         """
237         Resolves conflicted files in the current working directory.  Returns
238         whether or not all conflicted files were resolved or not.  Fully
239         resolved files are added to the index, but no commit is made.  The
240         default implementation uses :attr:`resolutions`.
241         """
242         resolved = True
243         files = set()
244         for status in shell.eval("git", "ls-files", "--unmerged").splitlines():
245             files.add(status.split()[-1])
246         for file in files:
247             # manual resolutions
248             if file in self.resolutions:
249                 contents = open(file, "r").read()
250                 for spec, result in self.resolutions[file]:
251                     old_contents = contents
252                     contents = resolve.resolve(contents, spec, result)
253                     if old_contents != contents:
254                         logging.info("Did resolution with spec:\n" + spec)
255                 open(file, "w").write(contents)
256                 if not resolve.is_conflict(contents):
257                     shell.call("git", "add", file)
258                 else:
259                     resolved = False
260             else:
261                 resolved = False
262         return resolved
263     def prepareMerge(self, deployment):
264         """
265         Performs various edits to files in the current working directory in
266         order to make a merge go more smoothly.  This is usually
267         used to fix botched line-endings.  If you add new files,
268         you have to 'git add' them; this is not necessary for edits.
269         By default this is a no-op; subclasses should replace this
270         with useful behavior.
271         """
272         pass
273     def prepareConfig(self, deployment):
274         """
275         Takes a deployment and replaces any explicit instances
276         of a configuration variable with generic ``WIZARD_*`` constants.
277         The default implementation uses :attr:`substitutions`, and
278         emits warnings when it encounters keys in :attr:`deprecated_keys`.
279         """
280         for key, subst in self.substitutions.items():
281             subs = subst(deployment)
282             if not subs and key not in self.deprecated_keys:
283                 logging.warning("No substitutions for %s" % key)
284     def install(self, version, options):
285         """
286         Run for 'wizard configure' (and, by proxy, 'wizard install') to
287         configure an application.  This assumes that the current working
288         directory is a deployment.  (Unlike its kin, this function does not
289         take a :class:`wizard.deploy.Deployment` as a parameter.)  Subclasses should
290         provide an implementation.
291         """
292         raise NotImplementedError
293     def upgrade(self, deployment, version, options):
294         """
295         Run for 'wizard upgrade' to upgrade database schemas and other
296         non-versioned data in an application after the filesystem has been
297         upgraded.  This assumes that the current working directory is the
298         deployment.  Subclasses should provide an implementation.
299         """
300         raise NotImplementedError
301     def backup(self, deployment, outdir, options):
302         """
303         Run for 'wizard backup' and upgrades to backup database schemas
304         and other non-versioned data in an application.  ``outdir`` is
305         the directory that backup files should be placed.  This assumes
306         that the current working directory is the deployment.  Subclasses
307         should provide an implementation, even if it is a no-op.
308
309         .. note::
310             Static user files may not need to be backed up, since in
311             many applications upgrades do not modify static files.
312         """
313         raise NotImplementedError
314     def restore(self, deployment, backup_dir, options):
315         """
316         Run for 'wizard restore' and failed upgrades to restore database
317         and other non-versioned data to a backed up version.  This assumes
318         that the current working directory is the deployment.  Subclasses
319         should provide an implementation.
320         """
321         raise NotImplementedError
322     def remove(self, deployment, options):
323         """
324         Run for 'wizard remove' to delete all database and non-local
325         file data.  This assumes that the current working directory is
326         the deployment.  Subclasses should provide an implementation.
327         """
328         raise NotImplementedError
329     def detectVersion(self, deployment):
330         """
331         Checks source files to determine the version manually.  This assumes
332         that the current working directory is the deployment.  Subclasses
333         should provide an implementation.
334         """
335         raise NotImplementedError
336     def detectVersionFromFile(self, filename, regex):
337         """
338         Helper method that detects a version by using a regular expression
339         from a file.  The regexed value is passed through :mod:`shlex`.
340         This assumes that the current working directory is the deployment.
341         """
342         contents = open(filename).read()
343         match = regex.search(contents)
344         if not match: return None
345         return distutils.version.LooseVersion(shlex.split(match.group(2))[0])
346     def download(self, version):
347         """
348         Returns a URL that can be used to download a tarball of ``version`` of
349         this application.
350         """
351         raise NotImplementedError
352     def checkWeb(self, deployment):
353         """
354         Checks if the autoinstall is viewable from the web.  Subclasses should
355         provide an implementation.
356
357         .. note::
358             Finding a reasonable heuristic that works across skinning
359             choices can be difficult.  We've had reasonable success
360             searching for metadata.  Be sure that the standard error
361             page does not contain the features you search for.  Try
362             not to depend on pages that are not the main page.
363         """
364         raise NotImplementedError
365     def checkWebPage(self, deployment, page, output):
366         """
367         Checks if a given page of an autoinstall contains a particular string.
368         """
369         page = deployment.fetch(page)
370         result = page.find(output) != -1
371         if result:
372             logging.debug("checkWebPage (passed):\n\n" + page)
373         else:
374             logging.info("checkWebPage (failed):\n\n" + page)
375         return result
376     def checkConfig(self, deployment):
377         """
378         Checks whether or not an autoinstall has been configured/installed
379         for use.  Assumes that the current working directory is the deployment.
380         Subclasses should provide an implementation.
381         """
382         # XXX: Unfortunately, this doesn't quite work because we package
383         # bogus config files in the -scripts versions of installs.  Maybe
384         # we should check a hash or something?
385         raise NotImplementedError
386     def researchFilter(self, filename, added, deleted):
387         """
388         Allows an application to selectively ignore certain diffstat signatures
389         during research; for example, configuration files will have a very
390         specific set of changes, so ignore them; certain installation files
391         may be removed, etc.  Return ``True`` if a diffstat signature should be
392         ignored,
393         """
394         return False
395     def researchVerbose(self, filename):
396         """
397         Allows an application to exclude certain dirty files from the output
398         report; usually this will just be parametrized files, since those are
399         guaranteed to have changes.  Return ``True`` if a file should only
400         be displayed in verbose mode.
401         """
402         return filename in self.parametrized_files
403     @staticmethod
404     def make(name):
405         """Makes an application, but uses the correct subtype if available."""
406         try:
407             __import__("wizard.app." + name)
408             return getattr(wizard.app, name).Application(name)
409         except ImportError as error:
410             # XXX ugly hack to check if the import error is from the top level
411             # module we care about or a submodule. should be an archetectural change.
412             if error.args[0].split()[-1]==name:
413                 return Application(name)
414             else:
415                 raise
416
417 class ApplicationVersion(object):
418     """Represents an abstract notion of a version for an application, where
419     ``version`` is a :class:`distutils.version.LooseVersion` and
420     ``application`` is a :class:`Application`."""
421     #: The :class:`distutils.version.LooseVersion` of this instance.
422     version = None
423     #: The :class:`Application` of this instance.
424     application = None
425     def __init__(self, version, application):
426         self.version = version
427         self.application = application
428     @property
429     def tag(self):
430         """
431         Returns the name of the git describe tag for the commit the user is
432         presently on, something like mediawiki-1.2.3-scripts-4-g123abcd
433         """
434         return "%s-%s" % (self.application, self.version)
435     @property
436     def scripts_tag(self):
437         """
438         Returns the name of the Git tag for this version.
439         """
440         end = str(self.version).partition('-scripts')[2].partition('-')[0]
441         return "%s-scripts%s" % (self.pristine_tag, end)
442     @property
443     def pristine_tag(self):
444         """
445         Returns the name of the Git tag for the pristine version corresponding
446         to this version.
447         """
448         return "%s-%s" % (self.application.name, str(self.version).partition('-scripts')[0])
449     def __cmp__(self, y):
450         return cmp(self.version, y.version)
451     @staticmethod
452     def parse(value):
453         """
454         Parses a line from the :term:`versions store` and return
455         :class:`ApplicationVersion`.
456
457         Use this only for cases when speed is of primary importance;
458         the data in version is unreliable and when possible, you should
459         prefer directly instantiating a :class:`wizard.deploy.Deployment` and having it query
460         the autoinstall itself for information.
461
462         The `value` to parse will vary.  For old style installs, it
463         will look like::
464
465            /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
466
467         For new style installs, it will look like::
468
469            APP-x.y.z-scripts
470         """
471         name = value.split("/")[-1]
472         try:
473             if name.find("-") != -1:
474                 app, _, version = name.partition("-")
475             else:
476                 # kind of poor, maybe should error.  Generally this
477                 # will actually result in a not found error
478                 app = name
479                 version = "trunk"
480         except ValueError:
481             raise DeploymentParseError(value)
482         return ApplicationVersion.make(app, version)
483     @staticmethod
484     def make(app, version):
485         """
486         Makes/retrieves a singleton :class:`ApplicationVersion` from
487         a``app`` and ``version`` string.
488         """
489         try:
490             # defer to the application for version creation to enforce
491             # singletons
492             return applications()[app].makeVersion(version)
493         except KeyError:
494             raise NoSuchApplication(app)
495
496 def expand_re(val):
497     """
498     Takes a tree of values (implement using nested lists) and
499     transforms them into regular expressions.
500
501         >>> expand_re('*')
502         '\\\\*'
503         >>> expand_re(['a', 'b'])
504         '(?:a|b)'
505         >>> expand_re(['*', ['b', 'c']])
506         '(?:\\\\*|(?:b|c))'
507     """
508     if isinstance(val, str):
509         return re.escape(val)
510     else:
511         return '(?:' + '|'.join(map(expand_re, val)) + ')'
512
513 def make_extractors(seed):
514     """
515     Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into
516     extractor functions (which take a :class:`wizard.deploy.Deployment`
517     and return the value of the second subpattern of ``regex`` when matched
518     with the contents of ``file``).
519     """
520     return util.dictmap(lambda a: filename_regex_extractor(*a), seed)
521
522 def make_substitutions(seed):
523     """
524     Take a dictionary of ``key`` to ``(file, regex)`` tuples and convert them into substitution
525     functions (which take a :class:`wizard.deploy.Deployment`, replace the second subpattern
526     of ``regex`` with ``key`` in ``file``, and returns the number of substitutions made.)
527     """
528     return util.dictkmap(lambda k, v: filename_regex_substitution(k, *v), seed)
529
530 # The following two functions are *highly* functional, and I recommend
531 # not touching them unless you know what you're doing.
532
533 def filename_regex_extractor(file, regex):
534     """
535     .. highlight:: haskell
536
537     Given a relative file name ``file``, a regular expression ``regex``, and a
538     :class:`wizard.deploy.Deployment` extracts a value out of the file in that
539     deployment.  This function is curried, so you pass just ``file`` and
540     ``regex``, and then pass ``deployment`` to the resulting function.
541
542     Its Haskell-style type signature would be::
543
544         Filename -> Regex -> (Deployment -> String)
545
546     The regular expression requires a very specific form, essentially ``()()()``
547     (with the second subgroup being the value to extract).  These enables
548     the regular expression to be used equivalently with filename
549
550     .. highlight:: python
551
552     For convenience purposes, we also accept ``[Filename]``, in which case
553     we use the first entry (index 0).  Passing an empty list is invalid.
554
555         >>> open("test-settings.extractor.ini", "w").write("config_var = 3\\n")
556         >>> f = filename_regex_extractor('test-settings.extractor.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
557         >>> f(deploy.Deployment("."))
558         '3'
559         >>> os.unlink("test-settings.extractor.ini")
560
561     .. note::
562         The first application of ``regex`` and ``file`` is normally performed
563         at compile-time inside a submodule; the second application is
564         performed at runtime.
565     """
566     if not isinstance(file, str):
567         file = file[0]
568     def h(deployment):
569         try:
570             contents = deployment.read(file) # cached
571         except IOError:
572             return None
573         match = regex.search(contents)
574         if not match: return None
575         # assumes that the second match is the one we want.
576         return match.group(2)
577     return h
578
579 def filename_regex_substitution(key, files, regex):
580     """
581     .. highlight:: haskell
582
583     Given a Wizard ``key`` (``WIZARD_*``), a list of ``files``, a
584     regular expression ``regex``, and a :class:`wizard.deploy.Deployment`
585     performs a substitution of the second subpattern of ``regex``
586     with ``key``.  Returns the number of replacements made.  This function
587     is curried, so you pass just ``key``, ``files`` and ``regex``, and
588     then pass ``deployment`` to the resulting function.
589
590     Its Haskell-style type signature would be::
591
592         Key -> ([File], Regex) -> (Deployment -> IO Int)
593
594     .. highlight:: python
595
596     For convenience purposes, we also accept ``Filename``, in which case it is treated
597     as a single item list.
598
599         >>> open("test-settings.substitution.ini", "w").write("config_var = 3")
600         >>> f = filename_regex_substitution('WIZARD_KEY', 'test-settings.substitution.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
601         >>> f(deploy.Deployment("."))
602         1
603         >>> print open("test-settings.substitution.ini", "r").read()
604         config_var = WIZARD_KEY
605         >>> os.unlink("test-settings.substitution.ini")
606     """
607     if isinstance(files, str):
608         files = (files,)
609     def h(deployment):
610         base = deployment.location
611         subs = 0
612         for file in files:
613             file = os.path.join(base, file)
614             try:
615                 contents = open(file, "r").read()
616                 contents, n = regex.subn("\\1" + key + "\\3", contents)
617                 subs += n
618                 open(file, "w").write(contents)
619             except IOError:
620                 pass
621         return subs
622     return h
623
624 def backup_database(outdir, deployment):
625     """
626     Generic database backup function for MySQL.
627     """
628     # XXX: Change this once deployments support multiple dbs
629     if deployment.application.database == "mysql":
630         return backup_mysql_database(outdir, deployment)
631     else:
632         raise NotImplementedError
633
634 def backup_mysql_database(outdir, deployment):
635     """
636     Database backups for MySQL using the :command:`mysqldump` utility.
637     """
638     outfile = os.path.join(outdir, "db.sql")
639     try:
640         shell.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
641         shell.call("gzip", "--best", outfile)
642     except shell.CallError as e:
643         raise BackupFailure(e.stderr)
644
645 def restore_database(backup_dir, deployment):
646     """
647     Generic database restoration function for MySQL.
648     """
649     # XXX: see backup_database
650     if deployment.application.database == "mysql":
651         return restore_mysql_database(backup_dir, deployment)
652     else:
653         raise NotImplementedError
654
655 def restore_mysql_database(backup_dir, deployment):
656     """
657     Database restoration for MySQL by piping SQL commands into :command:`mysql`.
658     """
659     if not os.path.exists(backup_dir):
660         raise RestoreFailure("Backup %s doesn't exist", backup_dir.rpartition("/")[2])
661     sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
662     shell.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
663     sql.seek(0)
664     shell.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
665     sql.close()
666
667 def remove_database(deployment):
668     """
669     Generic database removal function.  Actually, not so generic because we
670     go and check if we're on scripts and if we are run a different command.
671     """
672     if deployment.dsn.host == "sql.mit.edu":
673         try:
674             shell.call("/mit/scripts/sql/bin/drop-database", deployment.dsn.database)
675             return
676         except shell.CallError:
677             pass
678     engine = sqlalchemy.create_engine(deployment.dsn)
679     engine.execute("DROP DATABASE `%s`" % deployment.dsn.database)
680
681 def get_mysql_args(dsn):
682     """
683     Extracts arguments that would be passed to the command line mysql utility
684     from a deployment.
685     """
686     args = []
687     if dsn.host:
688         args += ["-h", dsn.host]
689     if dsn.username:
690         args += ["-u", dsn.username]
691     if dsn.password:
692         args += ["-p" + dsn.password]
693     args += [dsn.database]
694     return args
695
696 class Error(wizard.Error):
697     """Generic error class for this module."""
698     pass
699
700 class NoRepositoryError(Error):
701     """
702     :class:`Application` does not appear to have a Git repository
703     in the normal location.
704     """
705     #: The name of the application that does not have a Git repository.
706     app = None
707     def __init__(self, app):
708         self.app = app
709     def __str__(self):
710         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
711
712 class DeploymentParseError(Error):
713     """
714     Could not parse ``value`` from :term:`versions store`.
715     """
716     #: The value that failed to parse.
717     value = None
718     #: The location of the autoinstall that threw this variable.
719     #: This should be set by error handling code when it is available.
720     location = None
721     def __init__(self, value):
722         self.value = value
723
724 class NoSuchApplication(Error):
725     """
726     You attempted to reference a :class:`Application` named
727     ``app``, which is not recognized by Wizard.
728     """
729     #: The name of the application that does not exist.
730     app = None
731     #: The location of the autoinstall that threw this variable.
732     #: This should be set by error handling code when it is availble.
733     location = None
734     def __init__(self, app):
735         self.app = app
736
737 class Failure(Error):
738     """
739     Represents a failure when performing some double-dispatched operation
740     such as an installation or an upgrade.  Failure classes are postfixed
741     with Failure, not Error.
742     """
743     pass
744
745 class InstallFailure(Error):
746     """Installation failed for unknown reason."""
747     def __str__(self):
748         return """
749
750 ERROR: Installation failed for unknown reason.  You can
751 retry the installation by appending --retry to the installation
752 command."""
753
754 class RecoverableInstallFailure(InstallFailure):
755     """
756     Installation failed, but we were able to determine what the
757     error was, and should give the user a second chance if we were
758     running interactively.
759     """
760     #: List of the errors that were found.
761     errors = None
762     def __init__(self, errors):
763         self.errors = errors
764     def __str__(self):
765         return """
766
767 ERROR: Installation failed due to the following errors:  %s
768
769 You can retry the installation by appending --retry to the
770 installation command.""" % ", ".join(self.errors)
771
772 class UpgradeFailure(Failure):
773     """Upgrade script failed."""
774     #: String details of failure (possibly stdout or stderr output)
775     details = None
776     def __init__(self, details):
777         self.details = details
778     def __str__(self):
779         return """
780
781 ERROR: Upgrade script failed, details:
782
783 %s""" % self.details
784
785 class UpgradeVerificationFailure(Failure):
786     """Upgrade script passed, but website wasn't accessible afterwards"""
787     def __str__(self):
788         return """
789
790 ERROR: Upgrade script passed, but website wasn't accessible afterwards.  Check
791 the debug logs for the contents of the page."""
792
793 class BackupFailure(Failure):
794     """Backup script failed."""
795     #: String details of failure
796     details = None
797     def __init__(self, details):
798         self.details = details
799     def __str__(self):
800         return """
801
802 ERROR: Backup script failed, details:
803
804 %s""" % self.details
805
806 class RestoreFailure(Failure):
807     """Restore script failed."""
808     #: String details of failure
809     details = None
810     def __init__(self, details):
811         self.details = details
812     def __str__(self):
813         return """
814
815 ERROR: Restore script failed, details:
816
817 %s""" % self.details
818
819 class RemoveFailure(Failure):
820     """Remove script failed."""
821     #: String details of failure
822     details = None
823     def __init__(self, details):
824         self.details = details
825     def __str__(self):
826         return """
827
828 ERROR: Remove script failed, details:
829
830 %s""" % self.details