2 This module contains an object model for specifying "required options",
3 also known as "Args". While the format of this schema is inspired
4 by :mod:`optparse`, this is not a controller (that is the job
5 of :mod:`wizard.install` submodules); it merely is a schema
6 that controllers can consume in order to determine their desired
9 An :class:`Arg` is the simplest unit of this
10 model, and merely represents some named argument that an installer
11 script needs in order to finish the installation (i.e., the password
12 to the database, or the name of the new application). Instances
13 of :class:`Arg` can be registered to the :class:`ArgHandler`, which
14 manages marshalling these objects to whatever object
15 is actually managing user input. An argument is any valid Python
16 variable name, usually categorized using underscores (i.e.
17 ``admin_user``); the argument capitalized and with ``WIZARD_`` prepended
18 to it indicates a corresponding environment variable, i.e.
19 ``WIZARD_ADMIN_USER``. Arguments must be unique; applications
20 that define custom arguments are expected to namespace them.
22 Because autoinstallers will often have a number of themed
23 arguments (i.e. MySQL credentials) that are applicable across
24 autoinstallers, :class:`ArgSet` can be use to group :class:`Arg`
25 instances together, as well as promote reuse of these arguments.
26 There are a number of precanned :class:`ArgSet` subclasses
27 that serve this purpose, such as :class:`MysqlArgSet`.
28 :class:`ArgHandler` also contains some convenience syntax in its
29 constructor for loading predefined instances of :class:`ArgSet`.
31 Certain arguments will vary from install to install, but
32 can be automatically calculated if certain assumptions about the
33 server environment are made. For example, an application might
34 request an email; if we are on an Athena machine, one would
35 reasonably expect the currently logged in user + @mit.edu to be
36 a valid email address. :class:`Strategy` objects are responsible
37 for this sort of calculation, and may be attached to any
38 :class:`ArgSet` instance. (If you would like to attach a strategy
39 to a single arg, you should put the arg in a :class:`ArgSet` and
40 then set the strategy).
42 Finally, certain :class:`Strategy` objects may perform operations
43 with side effects (as marked by :attr:`Strategy.side_effects`).
44 The primary use case for this is automatic creation of databases
45 during an autoinstall. Marking a :class:`Strategy` as having
46 side effects is important, so as to delay executing it until
47 absolutely necessary (at the end of options parsing, but before
48 the actual installation begins).
52 Because Wizard is eventually intended for public use,
53 some hook mechanism for overloading the default strategies will
54 need to be created. Setting up environment variables may act
55 as a vaguely reasonable workaround in the interim.
59 from wizard.install import *
66 from wizard import scripts, shell, util
68 def fetch(options, path, post=None):
70 Fetches a web page from the autoinstall, usually to perform database
71 installation. ``path`` is the path of the file to retrieve, relative
72 to the autoinstall base (``options.web_path``), not the web root.
73 ``post`` is a dictionary to post. ``options`` is the options
74 object generated by :class:`OptionParser`.
76 return util.fetch(options.web_host, options.web_path, path, post)
80 Retrieves a dictionary of string names to precanned :class:`ArgSet` objects.
84 'mysql': MysqlArgSet(),
85 'admin': AdminArgSet(),
86 'email': EmailArgSet(),
89 class Strategy(object):
90 """Represents a strategy for calculating arg values without user input."""
91 #: Arguments that this strategy provides
92 provides = frozenset()
93 #: Whether or not this strategy has side effects.
97 Performs all side-effectless computation associated with this
98 strategy. It also detects if computation is possible, and
99 raises :exc:`StrategyFailed` if it isn't.
102 def execute(self, options):
104 Performs effectful computations associated with this strategy,
105 and mutates ``options`` with the new values. Behavior is
106 undefined if :meth:`prepare` was not called first.
110 class EnvironmentStrategy(Strategy):
111 """Fills in values from environment variables."""
112 def __init__(self, schema):
113 self.provides = set()
115 for arg in schema.args.values():
116 if os.getenv(arg.envname) is not None:
117 self.provides.add(arg.name)
118 self.envlookup[arg.name] = arg.envname
120 """This strategy is always available."""
122 def execute(self, options):
123 """Sets undefined options to their environment variables."""
124 for name, envname in self.envlookup.items():
125 if getattr(options, name) is not None:
127 setattr(options, name, os.getenv(envname))
129 class ScriptsWebStrategy(Strategy):
130 """Performs scripts specific guesses for web variables."""
131 provides = frozenset(["web_host", "web_path"])
133 """Uses :func:`wizard.scripts.get_web_host_and_path`."""
134 self._tuple = scripts.get_web_host_and_path()
137 def execute(self, options):
139 options.web_host, options.web_path = self._tuple
141 class ScriptsMysqlStrategy(Strategy):
143 Performs scripts specific guesses for MySQL variables. This
144 may create an appropriate database for the user.
147 provides = frozenset(["mysql_host", "mysql_user", "mysql_password", "mysql_db"])
149 """Uses :func:`wizard.scripts.get_sql_credentials`"""
150 self._triplet = scripts.get_sql_credentials()
151 if not self._triplet:
153 self._username = os.getenv('USER')
154 if self._username is None:
156 def execute(self, options):
157 """Creates a new database for the user using ``get-next-database`` and ``create-database``."""
159 name = os.path.basename(os.getcwd())
160 options.mysql_host, options.mysql_user, options.mysql_password = self._triplet
162 options.mysql_db = self._username + '+' + sh.eval("/mit/scripts/sql/bin/get-next-database", name)
163 sh.call("/mit/scripts/sql/bin/create-database", options.mysql_db)
165 class ScriptsEmailStrategy(Strategy):
166 """Performs script specific guess for email."""
167 provides = frozenset(["email"])
169 """Uses :envvar:`USER` and assumes you are an MIT affiliate."""
170 # XXX: should double-check that you're on a scripts server
171 # and fail if you're not.
172 self._user = os.getenv("USER")
173 if self._user is None:
175 def execute(self, options):
177 options.email = self._user + "@mit.edu"
181 Represent a required, named argument for installation. These
182 cannot have strategies associated with them, so if you'd like
183 to have a strategy associated with a single argument, create
184 an :class:`ArgSet` with one item in it.
186 #: Attribute name of the argument
190 #: String "type" of the argument, used for metavar
192 #: If true, is a password
196 """Full string of the option."""
197 return attr_to_option(self.name)
200 """Name of the environment variable containing this arg."""
201 return 'WIZARD_' + self.name.upper()
202 def prompt(self, options):
203 """Interactively prompts for a value and sets it to options."""
204 # XXX: put a sane default implementation; we'll probably need
205 # "big" descriptions for this, since 'help' is too sparse.
207 def __init__(self, name, password=False, type=None, help=None):
209 self.password = password
210 self.help = help or "UNDOCUMENTED"
213 class ArgSet(object):
215 Represents a set of named installation arguments that are required
216 for an installation to complete successfully. Arguments in a set
217 should share a common prefix and be related in functionality (the
218 litmus test is if you need one of these arguments, you should need
221 #: The :class:`Arg` objects that compose this argument set.
226 class WebArgSet(ArgSet):
227 """Common arguments for any application that lives on the web."""
230 Arg("web_host", type="HOST", help="Host that the application will live on"),
231 Arg("web_path", type="PATH", help="Relative path to your application root"),
234 class MysqlArgSet(ArgSet):
235 """Common arguments for applications that use a MySQL database."""
238 Arg("mysql_host", type="HOST", help="Host that your MySQL server lives on"),
239 Arg("mysql_db", type="DB", help="Name of the database to populate"),
240 Arg("mysql_user", type="USER", help="Name of user to access database with"),
241 Arg("mysql_password", type="PWD", password=True, help="Password of the database user"),
244 class AdminArgSet(ArgSet):
245 """Common arguments when an admin account is to be created."""
248 Arg("admin_name", type="NAME", help="Name of admin user to create"),
249 Arg("admin_password", type="PWD", password=True, help="Password of admin user"),
252 class EmailArgSet(ArgSet):
253 """Common arguments when an administrative email is required."""
256 Arg("email", help="Administrative email"),
259 class ArgSchema(object):
261 Schema container for arguments.
263 Valid identifiers for subclasses of :class:`ArgSet` are:
265 * ``mysql``, which populates the options ``mysql_host``, ``mysql_db``,
266 ``mysql_user`` and ``mysql_password``.
267 * ``admin``, which populates the options ``admin_name`` and
269 * ``email``, which populates the option ``email``.
271 The options ``web_path`` and ``web_host`` are automatically required.
275 parser = ArgHandler("sql", "admin", "email")
276 parser.add(Arg("title", help="Title of the new application"))
278 #: Dictionary of argument names to :class:`Arg` objects in schema.
280 #: List of :class:`ArgStrategy` objects in schema.
282 #: Set of arguments that are already provided. (This doesn't
283 #: say how to get them: probably running strategies or environment variables.)
285 def __init__(self, *args):
287 preload_dict = preloads()
292 for arg in preload_dict[preload].args:
293 self.args[arg.name] = arg
295 raise UnrecognizedPreloads(preload)
297 """Adds an argument to our schema."""
298 self.args[arg.name] = arg
300 """Populates :attr:`strategies` and :attr:`provides`"""
302 self.provides = set()
303 # XXX: separate out soon
305 EnvironmentStrategy(self),
306 ScriptsWebStrategy(),
307 ScriptsMysqlStrategy(),
308 ScriptsEmailStrategy(),
310 for arg in self.args.values():
311 if os.getenv(arg.envname) is not None:
312 self.provides.add(arg.name)
313 for strategy in raw_strategies:
316 self.provides |= strategy.provides
317 self.strategies.append(strategy)
318 except StrategyFailed:
320 # do non-effectful strategies first; this is a stable sort
321 self.strategies.sort(key=lambda x: x.side_effects)
322 def load(self, options):
324 Load values from strategy. Must be called after :meth:`commit`. We
325 omit strategies whose provided variables are completely specified
326 already. Will raise :exc:`MissingRequiredParam` if strategies aren't
327 sufficient to fill all options.
329 unfilled = set(name for name in self.args if getattr(options, name) is None)
330 missing = unfilled - self.provides
332 raise MissingRequiredParam(missing)
333 for strategy in self.strategies:
334 if all(getattr(options, name) is not None for name in strategy.provides):
336 for name in strategy.provides:
337 if getattr(options, name) is not None:
338 logging.warning("Overriding pre-specified value for %s", name)
339 strategy.execute(options)
341 class Error(wizard.Error):
342 """Base error class for this module."""
345 class Failure(Error):
346 """Installation failed."""
349 class StrategyFailed(Error):
350 """Strategy couldn't figure out values."""
353 class UnrecognizedPreloads(Error):
354 """You passed a preload that was not recognized."""
355 #: The preloads that were not recognized.
357 def __init__(self, preloads):
358 self.preloads = preloads
360 return "Did not recognize these preloads: " + ", ".join(self.preloads)
362 class MissingRequiredParam(Error):
363 """You missed a required argument, and we couldn't generate it.
364 Controllers should catch this exception and provide better behavior."""
365 #: The names of the arguments that were not specified.
367 def __init__(self, args):
370 return "Missing required parameters: %s" % ', '.join(self.args)