2 This module deals with marshalling information from the user to the install
3 process for an application. We divide this process into two parts: this module
4 addresses the specification of what fields the application is requesting, and
5 submodules implement controller logic for actually getting this information
6 from the user. Common logic between controllers is stored in this module.
8 :class:`ArgSchema` is composed of two orthogonal components: a dictionary of
9 :class:`Arg` objects (which may be organized using :class:`ArgSet`) and a list
10 of :class:`Strategy` objects. An :class:`Arg` contains information about a
11 given argument, and specified at compile-time by an application, while a
12 :class:`Strategy` contains a possible procedure for automatically determining
13 what the contents of some argument are, and is specified at run-time by the
14 user (this is not quite true yet, but will be soon). Some arguments are most
15 commonly used together, so we group them together as an :class:`ArgSet` and
16 allow applications to refer to them as a single name.
20 from wizard.install import *
27 from wizard import scripts, shell, util
29 # XXX: This is in the wrong place
30 def fetch(options, path, post=None):
32 Fetches a web page from the autoinstall, usually to perform database
33 installation. ``path`` is the path of the file to retrieve, relative
34 to the autoinstall base (``options.web_path``), not the web root.
35 ``post`` is a dictionary to post. ``options`` is the options
36 object generated by :class:`OptionParser`.
38 return util.fetch(options.web_host, options.web_path, path, post)
42 Retrieves a dictionary of string names to precanned :class:`ArgSet` objects.
46 'mysql': MysqlArgSet(),
47 'admin': AdminArgSet(),
48 'email': EmailArgSet(),
49 'title': TitleArgSet(),
52 class Strategy(object):
54 Represents a strategy for calculating arg values without user input.
56 Under many circumstances, making some assumptions about the server
57 environment means that we don't actually have to ask the user for values
58 such as the host or the path: these tend to be side effect free strategies.
59 Furthermore, we may have utility scripts present that can automatically
60 configure a new database for a user when one is necessary: these are side
61 effectful computations.
63 Note for an implementor: it is perfectly acceptable to calculate preliminary
64 results in :meth:`prepare`, store them as underscore prefixed variables,
65 and refer to them from :meth:`execute`.
67 #: Arguments that this strategy provides
68 provides = frozenset()
69 #: Whether or not this strategy has side effects.
73 Performs all side-effectless computation associated with this
74 strategy. It also detects if computation is possible, and
75 raises :exc:`StrategyFailed` if it isn't.
78 def execute(self, options):
80 Performs effectful computations associated with this strategy,
81 and mutates ``options`` with the new values. Behavior is
82 undefined if :meth:`prepare` was not called first. If this
83 method throws an exception, it should be treated as fatal.
87 class EnvironmentStrategy(Strategy):
89 Fills in values from environment variables, based off of
90 :attr:`Arg.envname` from ``schema``.
92 def __init__(self, schema):
95 for arg in schema.args.values():
96 if os.getenv(arg.envname) is not None:
97 self.provides.add(arg.name)
98 self.envlookup[arg.name] = arg.envname
100 """This strategy is always available."""
102 def execute(self, options):
103 """Sets undefined options to their environment variables."""
104 for name, envname in self.envlookup.items():
105 if getattr(options, name) is not None:
107 setattr(options, name, os.getenv(envname))
109 class ScriptsWebStrategy(Strategy):
110 """Performs scripts specific guesses for web variables."""
111 provides = frozenset(["web_host", "web_path"])
112 def __init__(self, dir):
115 """Uses :func:`wizard.scripts.get_web_host_and_path`."""
116 self._tuple = scripts.get_web_host_and_path(self.dir)
119 def execute(self, options):
121 options.web_host, options.web_path = self._tuple
123 class ScriptsMysqlStrategy(Strategy):
125 Performs scripts specific guesses for MySQL variables. This
126 may create an appropriate database for the user.
129 provides = frozenset(["mysql_host", "mysql_user", "mysql_password", "mysql_db"])
130 def __init__(self, dir):
133 """Uses :func:`wizard.scripts.get_sql_credentials`"""
134 self._triplet = scripts.get_sql_credentials()
135 if not self._triplet:
137 self._username = os.getenv('USER')
138 if self._username is None:
140 def execute(self, options):
141 """Creates a new database for the user using ``get-next-database`` and ``create-database``."""
143 options.mysql_host, options.mysql_user, options.mysql_password = self._triplet
145 options.mysql_db = self._username + '+' + sh.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir))
146 sh.call("/mit/scripts/sql/bin/create-database", options.mysql_db)
148 class ScriptsEmailStrategy(Strategy):
149 """Performs script specific guess for email."""
150 provides = frozenset(["email"])
152 """Uses :envvar:`USER` and assumes you are an MIT affiliate."""
153 # XXX: should double-check that you're on a scripts server
154 # and fail if you're not.
155 self._user = os.getenv("USER")
156 if self._user is None:
158 def execute(self, options):
160 options.email = self._user + "@mit.edu"
164 Represent a required, named argument for installation.
166 #: Attribute name of the argument
170 #: String "type" of the argument, used for metavar
172 #: If true, is a password
176 """Full string of the option."""
177 return attr_to_option(self.name)
180 """Name of the environment variable containing this arg."""
181 return 'WIZARD_' + self.name.upper()
182 def __init__(self, name, **kwargs):
184 for k,v in kwargs.items(): # cuz I'm lazy
185 if not hasattr(self, k):
186 raise TypeError("Arg() got unexpected keyword argument '%s'" % k)
189 class ArgSet(object):
191 Represents a set of named installation arguments that are required
192 for an installation to complete successfully. Arguments in a set
193 should share a common prefix and be related in functionality (the
194 litmus test is if you need one of these arguments, you should need
195 all of them). Register them in :func:`preloads`.
197 #: The :class:`Arg` objects that compose this argument set.
202 class WebArgSet(ArgSet):
203 """Common arguments for any application that lives on the web."""
206 Arg("web_host", type="HOST", help="Host that the application will live on"),
207 Arg("web_path", type="PATH", help="Relative path to your application root"),
210 class MysqlArgSet(ArgSet):
211 """Common arguments for applications that use a MySQL database."""
214 Arg("mysql_host", type="HOST", help="Host that your MySQL server lives on"),
215 Arg("mysql_db", type="DB", help="Name of the database to populate"),
216 Arg("mysql_user", type="USER", help="Name of user to access database with"),
217 Arg("mysql_password", type="PWD", password=True, help="Password of the database user"),
220 class AdminArgSet(ArgSet):
221 """Common arguments when an admin account is to be created."""
224 Arg("admin_name", type="NAME", help="Name of admin user to create"),
225 Arg("admin_password", type="PWD", password=True, help="Password of admin user"),
228 class EmailArgSet(ArgSet):
229 """Common arguments when an administrative email is required."""
232 Arg("email", help="Administrative email"),
235 class TitleArgSet(ArgSet):
236 """Common arguments when a title is required."""
239 Arg("title", help="Title of your new site"),
242 class ArgSchema(object):
244 Schema container for arguments.
246 Valid identifiers for subclasses of :class:`ArgSet` are:
248 * ``mysql``, which populates the options ``mysql_host``, ``mysql_db``,
249 ``mysql_user`` and ``mysql_password``.
250 * ``admin``, which populates the options ``admin_name`` and
252 * ``email``, which populates the option ``email``.
254 The options ``web_path`` and ``web_host`` are automatically required.
258 parser = ArgHandler("mysql", "admin", "email")
259 parser.add(Arg("title", help="Title of the new application"))
261 #: Dictionary of argument names to :class:`Arg` objects in schema.
263 #: List of :class:`Strategy` objects in schema.
265 #: Set of arguments that are already provided by :attr:`strategies`.
267 def __init__(self, *args):
269 preload_dict = preloads()
274 for arg in preload_dict[preload].args:
275 self.args[arg.name] = arg
277 raise UnrecognizedPreloads(preload)
279 """Adds an argument to our schema."""
280 self.args[arg.name] = arg
281 def commit(self, dir):
282 """Populates :attr:`strategies` and :attr:`provides`"""
284 self.provides = set()
285 # XXX: separate out soon
287 EnvironmentStrategy(self),
288 ScriptsWebStrategy(dir),
289 ScriptsMysqlStrategy(dir),
290 ScriptsEmailStrategy(),
292 for arg in self.args.values():
293 if os.getenv(arg.envname) is not None:
294 self.provides.add(arg.name)
295 for strategy in raw_strategies:
298 self.provides |= strategy.provides
299 self.strategies.append(strategy)
300 except StrategyFailed:
302 # do non-effectful strategies first; this is a stable sort
303 self.strategies.sort(key=lambda x: x.side_effects)
304 def fill(self, options):
306 Fills an object with all arguments pre-set
310 if not hasattr(options, i):
311 setattr(options, i, None)
312 def load(self, options):
314 Load values from strategy. Must be called after :meth:`commit`. We
315 omit strategies whose provided variables are completely specified
316 already. Will raise :exc:`MissingRequiredParam` if strategies aren't
317 sufficient to fill all options.
319 unfilled = set(name for name in self.args if getattr(options, name) is None)
320 missing = unfilled - self.provides
322 raise MissingRequiredParam(missing)
323 for strategy in self.strategies:
324 if all(getattr(options, name) is not None for name in strategy.provides):
326 for name in strategy.provides:
327 if getattr(options, name) is not None:
328 logging.warning("Overriding pre-specified value for %s", name)
329 strategy.execute(options)
331 class Error(wizard.Error):
332 """Base error class for this module."""
335 # XXX: This is in the wrong place
336 class Failure(Error):
337 """Installation failed."""
340 class StrategyFailed(Error):
341 """Strategy couldn't figure out values."""
344 class UnrecognizedPreloads(Error):
345 """You passed a preload that was not recognized."""
346 #: The preloads that were not recognized.
348 def __init__(self, preloads):
349 self.preloads = preloads
351 return "Did not recognize these preloads: " + ", ".join(self.preloads)
353 class MissingRequiredParam(Error):
354 """You missed a required argument, and we couldn't generate it.
355 Controllers should catch this exception and provide better behavior."""
356 #: The names of the arguments that were not specified.
358 def __init__(self, args):
361 return "Missing required parameters: %s" % ', '.join(self.args)