X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/fe692f2f318f566a6d21b1eacda5ce9658647570..c4a68ce8e6a291ca66d2f73c7e3fdded87aa5f1f:/wizard/app/__init__.py diff --git a/wizard/app/__init__.py b/wizard/app/__init__.py index 391f8d8..a3664db 100644 --- a/wizard/app/__init__.py +++ b/wizard/app/__init__.py @@ -6,6 +6,18 @@ You'll need to know how to overload the :class:`Application` class and use some of the functions in this module in order to specify new applications. +To specify custom applications as plugins, add the following ``entry_points`` +configuration:: + + [wizard.app] + yourappname = your.module:Application + otherappname = your.other.module:Application + +.. note:: + + Wizard will complain loudly if ``yourappname`` conflicts with an + application name defined by someone else. + There are some submodules for programming languages that define common functions and data that may be used by applications in that language. See: @@ -28,27 +40,52 @@ import shlex import logging import shutil import sqlalchemy -import random +import sqlalchemy.exc import string import urlparse import tempfile +import pkg_resources import wizard -from wizard import resolve, scripts, shell, util +from wizard import resolve, scripts, shell, sql, util -_application_list = [ +# SCRIPTS SPECIFIC +_scripts_application_list = [ "mediawiki", "wordpress", "joomla", "e107", "gallery2", "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django", # these are technically deprecated "advancedpoll", "gallery", ] -_applications = None +def _scripts_make(name): + """Makes an application, but uses the correct subtype if available.""" + try: + __import__("wizard.app." + name) + return getattr(wizard.app, name).Application(name) + except ImportError as error: + # XXX ugly hack to check if the import error is from the top level + # module we care about or a submodule. should be an archetectural change. + if error.args[0].split()[-1]==name: + return Application(name) + else: + raise +_applications = None def applications(): """Hash table for looking up string application name to instance""" global _applications if not _applications: - _applications = dict([(n,Application.make(n)) for n in _application_list ]) + # SCRIPTS SPECIFIC + _applications = dict([(n,_scripts_make(n)) for n in _scripts_application_list ]) + # setup plugins + for dist in pkg_resources.working_set: + for appname, appclass in dist.get_entry_map("wizard.app").items(): + if appname in _applications: + newname = dist.key + ":" + appname + if newname in _applications: + raise Exception("Unrecoverable application name conflict for %s from %s", appname, dist.key) + logging.warning("Could not overwrite %s, used %s instead", appname, newname) + appname = newname + _applications[appname] = appclass(appname) return _applications def getApplication(appname): @@ -77,6 +114,9 @@ class Application(object): deprecated_keys = set() #: Keys that we can simply generate random strings for if they're missing random_keys = set() + #: Values that are not sufficiently random for a random key. This can + #: include default values for a random configuration option, + random_blacklist = set() #: Dictionary of variable names to extractor functions. These functions #: take a :class:`wizard.deploy.Deployment` as an argument and return the value of #: the variable, or ``None`` if it could not be found. @@ -135,8 +175,8 @@ class Application(object): result[k] = extractor(deployment) # XXX: ugh... we have to do quoting for k in self.random_keys: - if result[k] is None: - result[k] = "'%s'" % ''.join(random.choice(string.letters + string.digits) for i in xrange(30)) + if result[k] is None or result[k] in self.random_blacklist: + result[k] = "'%s'" % util.random_key() return result def dsn(self, deployment): """ @@ -221,6 +261,9 @@ class Application(object): :attr:`parametrized_files` and a simple search and replace on those files. """ + # deployment is not used in this implementation, but note that + # we do have the invariant the current directory matches + # deployment's directory variables = ref_deployment.extract() for file in self.parametrized_files: try: @@ -241,10 +284,23 @@ class Application(object): """ resolved = True files = set() + files = {} for status in shell.eval("git", "ls-files", "--unmerged").splitlines(): - files.add(status.split()[-1]) - for file in files: + mode, hash, role, name = status.split() + files.setdefault(name, set()).add(int(role)) + for file, roles in files.items(): + # some automatic resolutions + if 1 not in roles and 2 not in roles and 3 in roles: + # upstream added a file, but it conflicted for whatever reason + shell.call("git", "add", file) + continue + elif 1 in roles and 2 not in roles and 3 in roles: + # user deleted the file, but upstream changed it + shell.call("git", "rm", file) + continue # manual resolutions + # XXX: this functionality is mostly subsumed by the rerere + # tricks we do if file in self.resolutions: contents = open(file, "r").read() for spec, result in self.resolutions[file]: @@ -362,17 +418,33 @@ class Application(object): not to depend on pages that are not the main page. """ raise NotImplementedError - def checkWebPage(self, deployment, page, output): + def checkDatabase(self, deployment): + """ + Checks if the database is accessible. + """ + try: + sql.connect(deployment.dsn) + return True + except sqlalchemy.exc.DBAPIError: + return False + def checkWebPage(self, deployment, page, outputs=[], exclude=[]): """ Checks if a given page of an autoinstall contains a particular string. """ page = deployment.fetch(page) - result = page.find(output) != -1 - if result: + for x in exclude: + if page.find(x) != -1: + logging.info("checkWebPage (failed due to %s):\n\n%s", x, page) + return False + votes = 0 + for output in outputs: + votes += page.find(output) != -1 + if votes > len(outputs) / 2: logging.debug("checkWebPage (passed):\n\n" + page) + return True else: logging.info("checkWebPage (failed):\n\n" + page) - return result + return False def checkConfig(self, deployment): """ Checks whether or not an autoinstall has been configured/installed @@ -400,19 +472,6 @@ class Application(object): be displayed in verbose mode. """ return filename in self.parametrized_files - @staticmethod - def make(name): - """Makes an application, but uses the correct subtype if available.""" - try: - __import__("wizard.app." + name) - return getattr(wizard.app, name).Application(name) - except ImportError as error: - # XXX ugly hack to check if the import error is from the top level - # module we care about or a submodule. should be an archetectural change. - if error.args[0].split()[-1]==name: - return Application(name) - else: - raise class ApplicationVersion(object): """Represents an abstract notion of a version for an application, where