]> scripts.mit.edu Git - wizard.git/blobdiff - wizard/install/__init__.py
Convert ad hoc shell calls to singleton instance; fix upgrade bug.
[wizard.git] / wizard / install / __init__.py
index 36e90858c82e252fac1b8977821a5c4584b45439..8ca40a30103e6a8250473fda9b7c39fb4eb0b456 100644 (file)
@@ -22,10 +22,26 @@ 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
 
+def dsn_callback(options):
+    if not isinstance(options.dsn, sqlalchemy.engine.url.URL):
+        options.dsn = sqlalchemy.engine.url.make_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):
     """
@@ -43,9 +59,10 @@ def preloads():
     """
     return {
             'web': WebArgSet(),
-            'mysql': MysqlArgSet(),
+            'db': DbArgSet(),
             'admin': AdminArgSet(),
             'email': EmailArgSet(),
+            'title': TitleArgSet(),
             }
 
 class Strategy(object):
@@ -73,7 +90,7 @@ class Strategy(object):
         strategy.  It also detects if computation is possible, and
         raises :exc:`StrategyFailed` if it isn't.
         """
-        raise NotImplemented
+        raise NotImplementedError
     def execute(self, options):
         """
         Performs effectful computations associated with this strategy,
@@ -81,7 +98,7 @@ class Strategy(object):
         undefined if :meth:`prepare` was not called first.  If this
         method throws an exception, it should be treated as fatal.
         """
-        raise NotImplemented
+        raise NotImplementedError
 
 class EnvironmentStrategy(Strategy):
     """
@@ -108,14 +125,18 @@ class EnvironmentStrategy(Strategy):
 class ScriptsWebStrategy(Strategy):
     """Performs scripts specific guesses for web variables."""
     provides = frozenset(["web_host", "web_path"])
+    def __init__(self, dir):
+        self.dir = dir
     def prepare(self):
         """Uses :func:`wizard.scripts.get_web_host_and_path`."""
-        self._tuple = scripts.get_web_host_and_path()
-        if not self._tuple:
+        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):
     """
@@ -123,23 +144,28 @@ class ScriptsMysqlStrategy(Strategy):
     may create an appropriate database for the user.
     """
     side_effects = True
-    provides = frozenset(["mysql_host", "mysql_user", "mysql_password", "mysql_db"])
+    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
         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()
-        name = os.path.basename(os.getcwd())
-        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", name)
-        sh.call("/mit/scripts/sql/bin/create-database", options.mysql_db)
+        database = self._username + '+' + shell.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir))
+        shell.call("/mit/scripts/sql/bin/create-database", database)
+        options.dsn = sqlalchemy.engine.url.URL("mysql", username=username, password=password, host=host, database=database)
 
 class ScriptsEmailStrategy(Strategy):
     """Performs script specific guess for email."""
@@ -163,14 +189,14 @@ class Arg(object):
     name = None
     #: Help string
     help = None
+    #: String to display if prompting a user for a value
+    prompt = None
     #: String "type" of the argument, used for metavar
     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."""
@@ -181,6 +207,8 @@ class Arg(object):
             if not hasattr(self, k):
                 raise TypeError("Arg() got unexpected keyword argument '%s'" % k)
             setattr(self, k, v)
+        if self.prompt is None:
+            self.prompt = self.help
 
 class ArgSet(object):
     """
@@ -192,6 +220,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 = []
 
@@ -203,22 +232,21 @@ 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):
     """Common arguments when an admin account is to be created."""
     def __init__(self):
         self.args = [
-                Arg("admin_name", type="NAME", help="Name of admin user to create"),
-                Arg("admin_password", type="PWD", password=True, help="Password of admin user"),
+                Arg("admin_name", type="NAME", help="Name of admin user to create",
+                    prompt="You will be able to log in using a username of your choice.  Please decide on a username and enter it below."),
+                Arg("admin_password", type="PWD", password=True, help="Password of admin user",
+                    prompt="Please decide on an admin password."),
                 ]
 
 class EmailArgSet(ArgSet):
@@ -228,6 +256,14 @@ class EmailArgSet(ArgSet):
                 Arg("email", help="Administrative email"),
                 ]
 
+class TitleArgSet(ArgSet):
+    """Common arguments when a title is required."""
+    def __init__(self):
+        self.args = [
+                Arg("title", help="Title of your new site",
+                    prompt="Please decide on a title for your new website."),
+                ]
+
 class ArgSchema(object):
     """
     Schema container for arguments.
@@ -239,6 +275,7 @@ class ArgSchema(object):
     * ``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.
 
@@ -267,15 +304,15 @@ class ArgSchema(object):
     def add(self, arg):
         """Adds an argument to our schema."""
         self.args[arg.name] = arg
-    def commit(self):
+    def commit(self, application, dir):
         """Populates :attr:`strategies` and :attr:`provides`"""
         self.strategies = []
         self.provides = set()
         # XXX: separate out soon
         raw_strategies = [
                 EnvironmentStrategy(self),
-                ScriptsWebStrategy(),
-                ScriptsMysqlStrategy(),
+                ScriptsWebStrategy(dir),
+                ScriptsMysqlStrategy(application, dir),
                 ScriptsEmailStrategy(),
                 ]
         for arg in self.args.values():
@@ -290,12 +327,21 @@ class ArgSchema(object):
                 pass
         # do non-effectful strategies first; this is a stable sort
         self.strategies.sort(key=lambda x: x.side_effects)
+    def fill(self, options):
+        """
+        Fills an object with all arguments pre-set
+        to ``None``.
+        """
+        for i in self.args:
+            if not hasattr(options, i):
+                setattr(options, i, None)
     def load(self, options):
         """
         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
@@ -308,16 +354,15 @@ class ArgSchema(object):
                 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