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