X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/71e6a449afbad015a25a39eedc808aca0c92db00..c4a68ce8e6a291ca66d2f73c7e3fdded87aa5f1f:/wizard/app/__init__.py diff --git a/wizard/app/__init__.py b/wizard/app/__init__.py index 5f318dc..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,59 +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: - # check for newline mismatch - # HACK: using git diff to tell if files are binary or not - if not len(shell.eval("git", "diff", file).splitlines()) == 1 and util.mixed_newlines(file): - # this code only works on Unix - def get_newline(filename): - f = open(filename, "U") - # for some reason I need two - s = f.readline() - if s != "" and f.newlines is None: - f.readline() - if not isinstance(f.newlines, str): - raise Exception("Assert: expected newlines to be string, instead was %s in %s" % (repr(f.newlines), file)) - return f.newlines - def create_reference(id): - f = tempfile.NamedTemporaryFile(prefix="wizardResolve", delete=False) - shell.call("git", "cat-file", "blob", ":%d:%s" % (id, file), stdout=f) - f.close() - return get_newline(f.name), f.name - def convert(filename, dest_nl): - contents = open(filename, "U").read().replace("\n", dest_nl) - open(filename, "wb").write(contents) - logging.info("Mixed newlines detected in %s", file) - common_nl, common_file = create_reference(1) - our_nl, our_file = create_reference(2) - their_nl, their_file = create_reference(3) - remerge = False - if common_nl != their_nl: - # upstream can't keep their newlines straight - logging.info("Converting common file (1) from %s to %s newlines", repr(common_nl), repr(their_nl)) - convert(common_file, their_nl) - remerge = True - if our_nl != their_nl: - # common case - logging.info("Converting our file (2) from %s to %s newlines", repr(our_nl), repr(their_nl)) - convert(our_file, their_nl) - remerge = True - if remerge: - logging.info("Remerging %s", file) - with open(file, "wb") as f: - try: - shell.call("git", "merge-file", "--stdout", our_file, common_file, their_file, stdout=f) - logging.info("New merge was clean") - shell.call("git", "add", file) - continue - except shell.CallError: - pass - logging.info("Merge was still unclean") - else: - logging.warning("Mixed newlines detected in %s, but no remerge possible", file) + 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]: @@ -411,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 @@ -449,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