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