X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/60e5bece267b3f7ca52c0309ebfcaab23dc82c4a..f3096b4a2a5498c38c7451397eebdc4ac5ed0788:/wizard/install/__init__.py diff --git a/wizard/install/__init__.py b/wizard/install/__init__.py index 87ade20..291018c 100644 --- a/wizard/install/__init__.py +++ b/wizard/install/__init__.py @@ -22,9 +22,27 @@ allow applications to refer to them as a single name. import os import logging +import sqlalchemy +import warnings import wizard -from wizard import scripts, shell, util +from wizard import scripts, shell, sql, util + +def dsn_callback(options): + if not isinstance(options.dsn, sqlalchemy.engine.url.URL): + options.dsn = sqlalchemy.engine.url.make_url(options.dsn) + # do some guessing with sql + options.dsn = sql.fill_url(options.dsn) + # perform some sanity checks on the database + database = options.dsn.database + options.dsn.database = None + engine = sqlalchemy.create_engine(options.dsn) + # generates warnings http://groups.google.com/group/sqlalchemy/browse_thread/thread/b7123fefb7dd83d5 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + engine.execute("CREATE DATABASE IF NOT EXISTS `%s`" % database) + options.dsn.database = database + # XXX: another good thing to check might be that the database is empty # XXX: This is in the wrong place def fetch(options, path, post=None): @@ -37,18 +55,6 @@ def fetch(options, path, post=None): """ return util.fetch(options.web_host, options.web_path, path, post) -def preloads(): - """ - Retrieves a dictionary of string names to precanned :class:`ArgSet` objects. - """ - return { - 'web': WebArgSet(), - 'mysql': MysqlArgSet(), - 'admin': AdminArgSet(), - 'email': EmailArgSet(), - 'title': TitleArgSet(), - } - class Strategy(object): """ Represents a strategy for calculating arg values without user input. @@ -113,12 +119,16 @@ class ScriptsWebStrategy(Strategy): self.dir = dir def prepare(self): """Uses :func:`wizard.scripts.get_web_host_and_path`.""" - self._tuple = scripts.get_web_host_and_path(self.dir) - if not self._tuple: + if self.dir is None: + raise StrategyFailed + self._url = scripts.fill_url(self.dir, None) + if not self._url: raise StrategyFailed def execute(self, options): """No-op.""" - options.web_host, options.web_path = self._tuple + options.web_host = self._url.netloc # pylint: disable-msg=E1101 + options.web_path = self._url.path # pylint: disable-msg=E1101 + options.web_inferred = True # hacky: needed to see if we need a .scripts/url file class ScriptsMysqlStrategy(Strategy): """ @@ -126,24 +136,30 @@ class ScriptsMysqlStrategy(Strategy): may create an appropriate database for the user. """ side_effects = True - provides = frozenset(["mysql_host", "mysql_user", "mysql_password", "mysql_db"]) - def __init__(self, dir): + provides = frozenset(["dsn"]) + def __init__(self, application, dir): + self.application = application self.dir = dir def prepare(self): """Uses :func:`wizard.scripts.get_sql_credentials`""" - self._triplet = scripts.get_sql_credentials() - if not self._triplet: + if self.application.database != "mysql": + raise StrategyFailed + try: + self._triplet = shell.eval("/mit/scripts/sql/bin/get-password").split() + except shell.CallError: + raise StrategyFailed + if len(self._triplet) != 3: raise StrategyFailed self._username = os.getenv('USER') if self._username is None: raise StrategyFailed def execute(self, options): """Creates a new database for the user using ``get-next-database`` and ``create-database``.""" - sh = shell.Shell() - options.mysql_host, options.mysql_user, options.mysql_password = self._triplet + host, username, password = self._triplet # race condition - options.mysql_db = self._username + '+' + sh.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir)) - sh.call("/mit/scripts/sql/bin/create-database", options.mysql_db) + name = shell.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir)) + database = shell.eval("/mit/scripts/sql/bin/create-database", name) + options.dsn = sqlalchemy.engine.url.URL("mysql", username=username, password=password, host=host, database=database) class ScriptsEmailStrategy(Strategy): """Performs script specific guess for email.""" @@ -152,6 +168,7 @@ class ScriptsEmailStrategy(Strategy): """Uses :envvar:`USER` and assumes you are an MIT affiliate.""" # XXX: should double-check that you're on a scripts server # and fail if you're not. + # XXX: This might be buggy, because locker might be set to USER self._user = os.getenv("USER") if self._user is None: raise StrategyFailed @@ -173,10 +190,8 @@ class Arg(object): type = None #: If true, is a password password = False - @property - def option(self): - """Full string of the option.""" - return attr_to_option(self.name) + #: Callback that this argument wants to get run on options after finished + callback = None @property def envname(self): """Name of the environment variable containing this arg.""" @@ -200,6 +215,7 @@ class ArgSet(object): """ #: The :class:`Arg` objects that compose this argument set. args = None + # XXX: probably could also use a callback attribute def __init__(self): self.args = [] @@ -211,14 +227,11 @@ class WebArgSet(ArgSet): Arg("web_path", type="PATH", help="Relative path to your application root"), ] -class MysqlArgSet(ArgSet): - """Common arguments for applications that use a MySQL database.""" +class DbArgSet(ArgSet): + """Common arguments for applications that use a database.""" def __init__(self): self.args = [ - Arg("mysql_host", type="HOST", help="Host that your MySQL server lives on"), - Arg("mysql_db", type="DB", help="Name of the database to populate"), - Arg("mysql_user", type="USER", help="Name of user to access database with"), - Arg("mysql_password", type="PWD", password=True, help="Password of the database user"), + Arg("dsn", type="DSN", help="Database Source Name, i.e. mysql://user:pass@host/dbname", callback=dsn_callback), ] class AdminArgSet(ArgSet): @@ -246,23 +259,38 @@ class TitleArgSet(ArgSet): prompt="Please decide on a title for your new website."), ] +def preloads(): + """ + Retrieves a dictionary of string names to precanned :class:`ArgSet` objects. + """ + return { + 'web': WebArgSet(), + 'db': DbArgSet(), + 'admin': AdminArgSet(), + 'email': EmailArgSet(), + 'title': TitleArgSet(), + } + class ArgSchema(object): """ Schema container for arguments. Valid identifiers for subclasses of :class:`ArgSet` are: - * ``mysql``, which populates the options ``mysql_host``, ``mysql_db``, - ``mysql_user`` and ``mysql_password``. + * ``db``, which populates the option ``dsn``, which is a SQLAlchemy + database source name, with properties for ``drivername``, + ``username``, ``password``, ``host``, ``port``, ``database`` and + ``query``. * ``admin``, which populates the options ``admin_name`` and ``admin_password``. * ``email``, which populates the option ``email``. + * ``title``, which populates the option ``title``. The options ``web_path`` and ``web_host`` are automatically required. Example:: - parser = ArgHandler("mysql", "admin", "email") + parser = ArgHandler("db", "admin", "email") parser.add(Arg("title", help="Title of the new application")) """ #: Dictionary of argument names to :class:`Arg` objects in schema. @@ -285,7 +313,7 @@ class ArgSchema(object): def add(self, arg): """Adds an argument to our schema.""" self.args[arg.name] = arg - def commit(self, dir): + def commit(self, application, dir, web_stub_path): """Populates :attr:`strategies` and :attr:`provides`""" self.strategies = [] self.provides = set() @@ -293,12 +321,10 @@ class ArgSchema(object): raw_strategies = [ EnvironmentStrategy(self), ScriptsWebStrategy(dir), - ScriptsMysqlStrategy(dir), + ScriptsWebStrategy(web_stub_path), + ScriptsMysqlStrategy(application, dir), ScriptsEmailStrategy(), ] - for arg in self.args.values(): - if os.getenv(arg.envname) is not None: - self.provides.add(arg.name) for strategy in raw_strategies: try: strategy.prepare() @@ -321,29 +347,35 @@ class ArgSchema(object): Load values from strategy. Must be called after :meth:`commit`. We omit strategies whose provided variables are completely specified already. Will raise :exc:`MissingRequiredParam` if strategies aren't - sufficient to fill all options. + sufficient to fill all options. It will then run any callbacks on + arguments. """ unfilled = set(name for name in self.args if getattr(options, name) is None) missing = unfilled - self.provides if missing: raise MissingRequiredParam(missing) for strategy in self.strategies: + # If the application being installed doesn't need all of the + # parameters a strategy could provide, we don't use it. + if any(not hasattr(options, name) for name in strategy.provides): + if any(hasattr(options, name) for name in strategy.provides): + logging.warning("Ignored partial strategy %s" % strategy) + continue if all(getattr(options, name) is not None for name in strategy.provides): continue for name in strategy.provides: if getattr(options, name) is not None: logging.warning("Overriding pre-specified value for %s", name) strategy.execute(options) + for arg in self.args.values(): + if arg.callback is None: + continue + arg.callback(options) class Error(wizard.Error): """Base error class for this module.""" pass -# XXX: This is in the wrong place -class Failure(Error): - """Installation failed.""" - pass - class StrategyFailed(Error): """Strategy couldn't figure out values.""" pass