]> scripts.mit.edu Git - wizard.git/commitdiff
Add preliminary draft of install module.
authorEdward Z. Yang <ezyang@mit.edu>
Mon, 3 Aug 2009 05:45:41 +0000 (01:45 -0400)
committerEdward Z. Yang <ezyang@mit.edu>
Mon, 3 Aug 2009 06:39:41 +0000 (02:39 -0400)
Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
doc/index.rst
doc/module/wizard.install.rst [new file with mode: 0644]
wizard/install.py [new file with mode: 0644]

index 97a77543fb09e788a95bf16f545613ba83500d92..4e408cec6f02ddcc7bbf21966d4b2e3cc3412689 100644 (file)
@@ -57,6 +57,7 @@ Modules
 
     module/wizard
     module/wizard.deploy
+    module/wizard.install
     module/wizard.shell
     module/wizard.util
 
diff --git a/doc/module/wizard.install.rst b/doc/module/wizard.install.rst
new file mode 100644 (file)
index 0000000..a05d468
--- /dev/null
@@ -0,0 +1,22 @@
+:mod:`wizard.install`
+=====================
+
+.. automodule:: wizard.install
+
+Classes
+-------
+.. autoclass:: OptionParser
+    :members:
+
+Functions
+---------
+.. autofunction:: attr_to_option
+.. autofunction:: calculate_web
+.. autofunction:: calculate_mysql
+.. autofunction:: calculate_email
+
+Exceptions
+----------
+.. autoexception:: Error
+.. autoexception:: UnrecognizedPreloads
+.. autoexception:: MissingRequiredParam
diff --git a/wizard/install.py b/wizard/install.py
new file mode 100644 (file)
index 0000000..49f7376
--- /dev/null
@@ -0,0 +1,183 @@
+"""
+Common code for installation scripts that live in .scripts/install
+
+.. testsetup:: *
+
+    from wizard.install import *
+"""
+
+import optparse
+import os
+import httplib
+import urllib
+import subprocess
+
+import wizard
+from wizard import util
+
+def fetch(options, path, post=None):
+    h = httplib.HTTPConnection(options.web_host)
+    fullpath = options.web_path + "/" + path
+    headers = {"Content-type": "application/x-www-form-urlencoded"}
+    if post:
+        h.request("POST", fullpath, urllib.urlencode(post), headers)
+    else:
+        h.request("GET", fullpath)
+    r = h.getresponse()
+    data = r.read()
+    h.close()
+    return data
+
+def attr_to_option(variable):
+    """
+    Converts Python attribute names to command line options.
+
+    >>> attr_to_option("foo_bar")
+    '--foo-bar'
+    """
+    return '--' + variable.replace('_', '-')
+
+def calculate_web(options):
+    """Performs scripts specific guesses for web variables."""
+    # XXX: THIS CODE SUCKS
+    if options.web_path or options.web_host: return
+    _, _, web_path = os.getcwd().partition("/web_scripts")
+    if not web_path: return
+    options.web_path = web_path
+    options.web_host = util.get_dir_owner() + ".scripts.mit.edu"
+
+def calculate_mysql(options):
+    """
+    Performs scripts specific guesses for mysql variables.
+
+    .. note::
+
+        This might create the database using the sql signup service.
+    """
+    if options.mysql_host or options.mysql_db or options.mysql_user or options.mysql_password: return
+    try:
+        triplet = subprocess.Popen("/mit/scripts/sql/bin/get-password", stdout=subprocess.PIPE).communicate()[0].rstrip().split()
+    except:
+        raise
+    name = os.path.basename(os.getcwd())
+    username = os.getenv('USER')
+    options.mysql_host, options.mysql_user, options.mysql_password = triplet
+    options.mysql_db = username + '+' + subprocess.Popen(["/mit/scripts/sql/bin/get-next-database", name], stdout=subprocess.PIPE).communicate()[0].rstrip()
+    subprocess.Popen(["/mit/scripts/sql/bin/create-database", options.mysql_db], stdout=subprocess.PIPE).communicate()
+
+def calculate_email(options):
+    """Performs script specific guess for email."""
+    options.email = os.getenv("USER") + "@mit.edu"
+
+class OptionParser(object):
+    """
+    Wrapper around :class:`optparse.OptionParser` which adds support
+    for required name parameters (i.e. "required options") and also
+    includes support for common parameters (from ``preloads``) that you'd want
+    when installing an application.
+
+    Valid preloads are:
+
+    * ``mysql``, which populates the options ``mysql_host``, ``mysql_db``,
+      ``mysql_user`` and ``mysql_password``.
+    * ``admin``, which populates the options ``admin_name`` and
+      ``admin_password``.
+    * ``email``, which populates the option ``email``.
+
+    The options ``web_path`` and ``web_host`` are automatically required.
+
+    Example::
+
+        parser = OptionParser("sql", "admin", "email")
+    """
+    #: Instance of :class:`optparse.OptionParser`, for specifying
+    #: non-interactive options.
+    parser = None
+    #: Parameters that are required by this parser.
+    params = None
+    def __init__(self, *preloads):
+        self.parser = optparse.OptionParser()
+        self.params = set()
+        self._user_params = set()
+        self._calculators = []
+        preloads = set(preloads)
+        self.add_param("web_path", auto=True, help="Path to your install, e.g. /myapp")
+        self.add_param("web_host", auto=True, help="Host of your install, e.g. example.com")
+        self._calculators.append(calculate_web)
+        if "mysql" in preloads:
+            preloads.remove("mysql")
+            self.add_param("mysql_host", auto=True, help="Host that your MySQL server lives on")
+            self.add_param("mysql_db", auto=True, help="Name of the database to populate")
+            self.add_param("mysql_user", auto=True, help="Name of user to access database with")
+            self.add_param("mysql_password", auto=True, help="Password of the database user")
+            self._calculators.append(calculate_mysql)
+        if "admin" in preloads:
+            preloads.remove("admin")
+            self.add_param("admin_name", help="Name of admin user to create")
+            self.add_param("admin_password", help="Password of admin user")
+        if "email" in preloads:
+            preloads.remove("email")
+            self.add_param("email", auto=True, help="Administrative email")
+            self._calculators.append(calculate_email)
+        if preloads:
+            raise UnrecognizedPreloads(preloads)
+    def add_param(self, name, auto=False, help=None):
+        """
+        Adds a required parameter ``name``.  This parameter can be asked
+        for interactively or specified command line with ``--name`` (with
+        underscores replaced with dashes).  The ``help`` shows up both
+        in ``--help`` output as well as interactive operation.  If
+        ``auto`` is ``True``, that means that we might be able to
+        automatically calculate a sensible value.
+
+        .. note:
+
+            This API is most certainly going to change.
+        """
+        self.parser.add_option(attr_to_option(name), dest=name,
+                default=None, help=help)
+        self.params.add(name)
+        if not auto: self._user_params.add(name)
+    def parse_args(self):
+        """Performs option parsing and required option validation."""
+        options, args = self.parser.parse_args()
+        # XXX: Something more robust, as in "here are the parameters
+        # that need to be set at a certain point in time, and incrementally
+        # increase validation as things go on"
+        for param in self._user_params:
+            if getattr(options, param) is None:
+                raise MissingRequiredParam(param)
+        for calculator in self._calculators:
+            calculator(options)
+        for param in self.params:
+            if getattr(options, param) is None:
+                raise MissingRequiredParam(param)
+        return options, param
+
+class Error(wizard.Error):
+    """Base error class for this module"""
+    pass
+
+class Failure(Error):
+    """Web install process failed"""
+    pass
+
+class UnrecognizedPreloads(Error):
+    """You passed a preload that was not recognized."""
+    #: The preloads that were not recognized
+    preloads = None
+    def __init__(self, preloads):
+        self.preloads = preloads
+        self.message = str(self)
+    def __str__(self):
+        return "Did not recognize these preloads: " + ", ".join(self.preloads)
+
+class MissingRequiredParam(Error):
+    """You missed a required parameter, and we couldn't generate it."""
+    #: The param variable name that was not recognized
+    param = None
+    def __init__(self, param):
+        self.param = param
+        self.message = str(self)
+    def __str__(self):
+        return "Missing required parameter %s; try specifying %s" % (self.param, attr_to_option(self.param))