2 This module contains an object model for specifying "required options",
3 also known as "Args". Whereas :class:`optparse.OptionParser` might
4 normally be configured by performing a bunch of function calls, we
5 generalize this configuration in order to support other types
6 of input methods (most notably interactive).
8 Briefly, a :class:`Arg` is the simplest unit of this
9 model, and merely represents some named argument that an installer
10 script needs in order to finish the installation (i.e., the password
11 to the database, or the name of the new application). Instances
12 of :class:`Arg` can be registered to the :class:`ArgHandler`, which
13 manages marshalling these objects to whatever object
14 is actually managing user input. An argument is any valid Python
15 variable name, usually categorized using underscores (i.e.
16 admin_user); the argument capitalized and with ``WIZARD_`` prepended
17 to it indicates a corresponding environment variable, i.e.
18 ``WIZARD_ADMIN_USER``.
20 Because autoinstallers will often have a number of themed
21 arguments (i.e. MySQL credentials) that are applicable across
22 autoinstallers, :class:`ArgSet` can be use to group :class:`Arg`
23 instances together, as well as promote reuse of these arguments.
24 There are a number of precanned :class:`ArgSet` subclasses
25 that serve this purpose, such as :class:`MysqlArgSet`.
26 :class:`ArgHandler` also contains some convenience syntax in its
27 constructor for loading predefined instances of :class:`ArgSet`.
29 Certain arguments will vary from install to install, but
30 can be automatically calculated if certain assumptions about the
31 server environment are made. For example, an application might
32 request an email; if we are on an Athena machine, one would
33 reasonably expect the currently logged in user + @mit.edu to be
34 a valid email address. :class:`Strategy` objects are responsible
35 for this sort of calculation, and may be attached to any
36 :class:`ArgSet` instance. (If you would like to attach a strategy
37 to a single arg, you should put the arg in a :class:`ArgSet` and
38 then set the strategy).
40 Finally, certain :class:`Strategy` objects may perform operations
41 with side effects (as marked by :attr:`Strategy.side_effects`).
42 The primary use case for this is automatic creation of databases
43 during an autoinstall. Marking a :class:`Strategy` as having
44 side effects is important, so as to delay executing it until
45 absolutely necessary (at the end of options parsing, but before
46 the actual installation begins).
50 Because Wizard is eventually intended for public use,
51 some hook mechanism for overloading the default strategies will
52 need to be created. Setting up environment variables may act
53 as a vaguely reasonable workaround in the interim.
57 from wizard.install import *
63 from wizard import scripts, shell, util
65 def fetch(options, path, post=None):
67 Fetches a web page from the autoinstall, usually to perform database
68 installation. ``path`` is the path of the file to retrieve, relative
69 to the autoinstall base (``options.web_path``), not the web root.
70 ``post`` is a dictionary to post. ``options`` is the options
71 object generated by :class:`OptionParser`.
73 return util.fetch(options.web_host, options.web_path, path, post)
75 def attr_to_option(variable):
77 Converts Python attribute names to command line options.
79 >>> attr_to_option("foo_bar")
82 return '--' + variable.replace('_', '-')
86 Retrieves a dictionary of string names to precanned :class:`ArgSet` objects.
89 'mysql': MysqlArgSet(),
90 'admin': AdminArgSet(),
91 'email': EmailArgSet(),
94 class Strategy(object):
95 """Represents a strategy for calculating arg values without user input."""
96 #: Whether or not this strategy has side effects.
98 def execute(self, options):
100 Calculates values for the arguments that this strategy has been
101 associated with, and then mutates ``options`` to contain those new
102 values. This function is atomic; when control leaves, all of the
103 options should either have values, **or** a :exc:`FailedStrategy` was
104 raised and none of the options should have been changed.
105 Execution is bypassed if all options are explicitly specified, even
106 in the case of strategies with side effects..
110 class ScriptsWebStrategy(Strategy):
111 """Performs scripts specific guesses for web variables."""
112 def execute(self, options):
113 """Guesses web path by splitting on web_scripts."""
114 tuple = scripts.get_web_host_and_path()
117 options.web_host, options.web_path = tuple
119 class ScriptsMysqlStrategy(Strategy):
121 Performs scripts specific guesses for MySQL variables. This
122 may create an appropriate database for the user.
125 def execute(self, options):
126 """Attempts to create a database using Scripts utilities."""
128 triplet = scripts.get_sql_credentials()
131 name = os.path.basename(os.getcwd())
132 username = os.getenv('USER')
133 options.mysql_host, options.mysql_user, options.mysql_password = triplet
135 options.mysql_db = username + '+' + sh.eval("/mit/scripts/sql/bin/get-next-database", name)
136 sh.call("/mit/scripts/sql/bin/create-database", options.mysql_db)
138 class ScriptsEmailStrategy(Strategy):
139 """Performs script specific guess for email."""
140 def execute(self, options):
141 """Guesses email using username."""
142 # XXX: should double-check that you're on a scripts server
143 # and fail if you're not.
144 options.email = os.getenv("USER") + "@mit.edu"
148 Represent a required, named argument for installation. These
149 cannot have strategies associated with them, so if you'd like
150 to have a strategy associated with a single argument, create
151 an :class:`ArgSet` with one item in it.
153 #: Attribute name of the argument
157 #: String "type" of the argument, used for metavar
159 #: If true, is a password
163 """Full string of the option."""
164 return attr_to_option(self.name)
167 """Name of the environment variable containing this arg."""
168 return 'WIZARD_' + self.name.upper()
169 def prompt(self, options):
170 """Interactively prompts for a value and sets it to options."""
171 # XXX: put a sane default implementation; we'll probably need
172 # "big" descriptions for this, since 'help' is too sparse.
174 def __init__(self, name, password=False, type=None, help=None):
176 self.password = password
177 self.help = help or "UNDOCUMENTED"
180 class ArgSet(object):
182 Represents a set of named installation arguments that are required
183 for an installation to complete successfully. Arguments in a set
184 should share a common prefix and be related in functionality (the
185 litmus test is if you need one of these arguments, you should need
188 #: The :class:`Arg` objects that compose this argument set.
190 #: The :class:`Strategy` objects for this option
195 class WebArgSet(ArgSet):
196 """Common arguments for any application that lives on the web."""
199 Arg("web_host", type="HOST", help="Host that the application will live on"),
200 Arg("web_path", type="PATH", help="Relative path to your application root"),
202 self.strategy = ScriptsWebStrategy()
204 class MysqlArgSet(ArgSet):
205 """Common arguments for applications that use a MySQL database."""
208 Arg("mysql_host", type="HOST", help="Host that your MySQL server lives on"),
209 Arg("mysql_db", type="DB", help="Name of the database to populate"),
210 Arg("mysql_user", type="USER", help="Name of user to access database with"),
211 Arg("mysql_password", type="PWD", password=True, help="Password of the database user"),
213 self.strategy = ScriptsMysqlStrategy()
215 class AdminArgSet(ArgSet):
216 """Common arguments when an admin account is to be created."""
219 Arg("admin_name", type="NAME", help="Name of admin user to create"),
220 Arg("admin_password", type="PWD", password=True, help="Password of admin user"),
223 class EmailArgSet(ArgSet):
224 """Common arguments when an administrative email is required."""
227 Arg("email", help="Administrative email"),
229 self.strategy = ScriptsEmailStrategy()
231 class ArgHandler(object):
233 Generic controller which takes an argument specification of :class:`Arg`
234 and configures either a command line flags parser
235 (:class:`optparse.OptionParser`), environment variables,
236 an interactive user prompt
237 (:class:`OptionPrompt`) or possibly a web interface to request
238 these arguments appropriately. This controller also
239 handles :class:`ArgSet`, which group related
240 functionality together and can be reused from installer to installer.
242 Valid identifiers for subclasses of :class:`ArgSet` are:
244 * ``mysql``, which populates the options ``mysql_host``, ``mysql_db``,
245 ``mysql_user`` and ``mysql_password``.
246 * ``admin``, which populates the options ``admin_name`` and
248 * ``email``, which populates the option ``email``.
250 The options ``web_path`` and ``web_host`` are automatically required.
254 parser = ArgHandler("sql", "admin", "email")
255 parser.add(Arg("title", help="Title of the new application"))
257 #: List of :class:`ArgSet` objects in schema. The element at
258 #: index 0 will always be an anonymous :class:`ArgSet` that you
259 #: can add stray instances of :class:`Arg` to.
261 def __init__(self, *args):
262 self.argsets = [ArgSet(), WebArgSet()]
263 preload_dict = preloads()
266 self.argsets.append(preload_dict[preload])
268 raise UnrecognizedPreloads(preload)
270 """Adds an argument to our schema."""
271 self.argsets[0].args.append(arg)
272 def push(self, parser):
273 """Pushes arg schema to :class:`optparse.OptionParser`."""
274 for argset in self.argsets:
275 for arg in argset.args:
276 parser.add_option(attr_to_option(arg.name), dest=arg.name, metavar=arg.type,
277 default=None, help=arg.help)
278 def handle(self, options):
280 Takes the result of :meth:`optparse.OptionParser.parse_args`
281 and performs user interaction and/or calculations to complete
284 # categorize the argsets
285 argsets_nostrategy = []
286 argsets_strategy = []
287 argsets_strategy_with_side_effects = []
288 for argset in self.argsets:
289 # fill in environment variables
290 for arg in argset.args:
291 if getattr(options, arg.name) is None:
292 val = os.getenv(arg.envname)
294 setattr(options, arg.name, val)
295 if not argset.strategy:
296 argsets_nostrategy.append(argset)
297 elif argset.strategy.side_effects:
298 argsets_strategy_with_side_effects.append(argset)
300 argsets_strategy.append(argset)
301 for argset in argsets_nostrategy:
302 for arg in argset.args:
303 if getattr(options, arg.name) is None:
304 # XXX: arg.prompt(options)
305 raise MissingRequiredParam(arg)
307 for arg in argset.args:
308 if getattr(options, arg.name) is None:
311 for sets in (argsets_strategy, argsets_strategy_with_side_effects):
313 if all_set(argset): continue
315 argset.strategy.execute(options)
316 except StrategyFailed:
318 for arg in argset.args:
319 if getattr(options, arg.name) is None:
320 # XXX: arg.prompt(options)
321 raise MissingRequiredParam(arg)
323 class Error(wizard.Error):
324 """Base error class for this module."""
327 class Failure(Error):
328 """Web install process failed."""
329 # XXX: we can give better error messages
332 class StrategyFailed(Error):
333 """Strategy couldn't figure out values."""
336 class UnrecognizedPreloads(Error):
337 """You passed a preload that was not recognized."""
338 #: The preloads that were not recognized.
340 def __init__(self, preloads):
341 self.preloads = preloads
343 return "Did not recognize these preloads: " + ", ".join(self.preloads)
345 class MissingRequiredParam(Error):
346 """You missed a required argument, and we couldn't generate it."""
347 #: The :class:`Arg` that was not specified.
349 def __init__(self, arg):
352 return "Missing required parameter %s; try specifying option %s or environment variable %s" % (self.arg.name, self.arg.option, self.arg.envname)