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