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 *
68 from wizard import shell, util
70 def fetch(options, path, post=None):
72 Fetches a web page from the autoinstall, usually to perform database
73 installation. ``path`` is the path of the file to retrieve, relative
74 to the autoinstall base (``options.web_path``), not the web root.
75 ``post`` is a dictionary to post. ``options`` is the options
76 object generated by :class:`OptionParser`.
78 h = httplib.HTTPConnection(options.web_host)
79 fullpath = options.web_path + "/" + path
81 headers = {"Content-type": "application/x-www-form-urlencoded"}
82 h.request("POST", fullpath, urllib.urlencode(post), headers)
84 h.request("GET", fullpath)
90 def attr_to_option(variable):
92 Converts Python attribute names to command line options.
94 >>> attr_to_option("foo_bar")
97 return '--' + variable.replace('_', '-')
101 Retrieves a dictionary of string names to precanned :class:`ArgSet` objects.
104 'mysql': MysqlArgSet(),
105 'admin': AdminArgSet(),
106 'email': EmailArgSet(),
109 class Strategy(object):
110 """Represents a strategy for calculating arg values without user input."""
111 #: Whether or not this strategy has side effects.
113 def execute(self, options):
115 Calculates values for the arguments that this strategy has been
116 associated with, and then mutates ``options`` to contain those new
117 values. This function is atomic; when control leaves, all of the
118 options should either have values, **or** a :exc:`FailedStrategy` was
119 raised and none of the options should have been changed.
120 Execution is bypassed if all options are explicitly specified, even
121 in the case of strategies with side effects..
125 class ScriptsWebStrategy(Strategy):
126 """Performs scripts specific guesses for web variables."""
127 # XXX: THIS CODE SUCKS
128 def execute(self, options):
129 """Guesses web path by splitting on web_scripts."""
130 _, _, web_path = os.getcwd().partition("/web_scripts")
133 options.web_path = web_path
134 options.web_host = util.get_dir_owner() + ".scripts.mit.edu"
136 class ScriptsMysqlStrategy(Strategy):
138 Performs scripts specific guesses for MySQL variables. This
139 may create an appropriate database for the user.
142 def execute(self, options):
143 """Attempts to create a database using Scripts utilities."""
146 triplet = sh.eval("/mit/scripts/sql/bin/get-password").split()
149 name = os.path.basename(os.getcwd())
150 username = os.getenv('USER')
151 options.mysql_host, options.mysql_user, options.mysql_password = triplet
153 options.mysql_db = username + '+' + sh.eval("/mit/scripts/sql/bin/get-next-database", name)
154 sh.call("/mit/scripts/sql/bin/create-database", options.mysql_db)
156 class ScriptsEmailStrategy(Strategy):
157 """Performs script specific guess for email."""
158 def execute(self, options):
159 """Guesses email using username."""
160 # XXX: should double-check that you're on a scripts server
161 # and fail if you're not.
162 options.email = os.getenv("USER") + "@mit.edu"
166 Represent a required, named argument for installation. These
167 cannot have strategies associated with them, so if you'd like
168 to have a strategy associated with a single argument, create
169 an :class:`ArgSet` with one item in it.
171 #: Attribute name of the argument
175 #: String "type" of the argument, used for metavar
177 #: If true, is a password
181 """Full string of the option."""
182 return attr_to_option(self.name)
185 """Name of the environment variable containing this arg."""
186 return 'WIZARD_' + self.name.upper()
187 def prompt(self, options):
188 """Interactively prompts for a value and sets it to options."""
189 # XXX: put a sane default implementation; we'll probably need
190 # "big" descriptions for this, since 'help' is too sparse.
192 def __init__(self, name, password=False, type=None, help=None):
194 self.password = password
195 self.help = help or "UNDOCUMENTED"
198 class ArgSet(object):
200 Represents a set of named installation arguments that are required
201 for an installation to complete successfully. Arguments in a set
202 should share a common prefix and be related in functionality (the
203 litmus test is if you need one of these arguments, you should need
206 #: The :class:`Arg` objects that compose this argument set.
208 #: The :class:`Strategy` objects for this option
213 class WebArgSet(ArgSet):
214 """Common arguments for any application that lives on the web."""
217 Arg("web_host", type="HOST", help="Host that the application will live on"),
218 Arg("web_path", type="PATH", help="Relative path to your application root"),
220 self.strategy = ScriptsWebStrategy()
222 class MysqlArgSet(ArgSet):
223 """Common arguments for applications that use a MySQL database."""
226 Arg("mysql_host", type="HOST", help="Host that your MySQL server lives on"),
227 Arg("mysql_db", type="DB", help="Name of the database to populate"),
228 Arg("mysql_user", type="USER", help="Name of user to access database with"),
229 Arg("mysql_password", type="PWD", password=True, help="Password of the database user"),
231 self.strategy = ScriptsMysqlStrategy()
233 class AdminArgSet(ArgSet):
234 """Common arguments when an admin account is to be created."""
237 Arg("admin_name", type="NAME", help="Name of admin user to create"),
238 Arg("admin_password", type="PWD", password=True, help="Password of admin user"),
241 class EmailArgSet(ArgSet):
242 """Common arguments when an administrative email is required."""
245 Arg("email", help="Administrative email"),
247 self.strategy = ScriptsEmailStrategy()
249 class ArgHandler(object):
251 Generic controller which takes an argument specification of :class:`Arg`
252 and configures either a command line flags parser
253 (:class:`optparse.OptionParser`), environment variables,
254 an interactive user prompt
255 (:class:`OptionPrompt`) or possibly a web interface to request
256 these arguments appropriately. This controller also
257 handles :class:`ArgSet`, which group related
258 functionality together and can be reused from installer to installer.
260 Valid identifiers for subclasses of :class:`ArgSet` are:
262 * ``mysql``, which populates the options ``mysql_host``, ``mysql_db``,
263 ``mysql_user`` and ``mysql_password``.
264 * ``admin``, which populates the options ``admin_name`` and
266 * ``email``, which populates the option ``email``.
268 The options ``web_path`` and ``web_host`` are automatically required.
272 parser = ArgHandler("sql", "admin", "email")
273 parser.add(Arg("title", help="Title of the new application"))
275 #: List of :class:`ArgSet` objects in schema. The element at
276 #: index 0 will always be an anonymous :class:`ArgSet` that you
277 #: can add stray instances of :class:`Arg` to.
279 def __init__(self, *args):
280 self.argsets = [ArgSet(), WebArgSet()]
281 preload_dict = preloads()
284 self.argsets.append(preload_dict[preload])
286 raise UnrecognizedPreloads(preload)
288 """Adds an argument to our schema."""
289 self.argsets[0].args.append(arg)
290 def push(self, parser):
291 """Pushes arg schema to :class:`optparse.OptionParser`."""
292 for argset in self.argsets:
293 for arg in argset.args:
294 parser.add_option(attr_to_option(arg.name), dest=arg.name, metavar=arg.type,
295 default=None, help=arg.help)
296 def handle(self, options):
298 Takes the result of :meth:`optparse.OptionParser.parse_args`
299 and performs user interaction and/or calculations to complete
302 # categorize the argsets
303 argsets_nostrategy = []
304 argsets_strategy = []
305 argsets_strategy_with_side_effects = []
306 for argset in self.argsets:
307 # fill in environment variables
308 for arg in argset.args:
309 if getattr(options, arg.name) is None:
310 val = os.getenv(arg.envname)
312 setattr(options, arg.name, val)
313 if not argset.strategy:
314 argsets_nostrategy.append(argset)
315 elif argset.strategy.side_effects:
316 argsets_strategy_with_side_effects.append(argset)
318 argsets_strategy.append(argset)
319 for argset in argsets_nostrategy:
320 for arg in argset.args:
321 if getattr(options, arg.name) is None:
322 # XXX: arg.prompt(options)
323 raise MissingRequiredParam(arg)
325 for arg in argset.args:
326 if getattr(options, arg.name) is None:
329 for sets in (argsets_strategy, argsets_strategy_with_side_effects):
331 if all_set(argset): continue
332 argset.strategy.execute(options)
333 for arg in argset.args:
334 if getattr(options, arg.name) is None:
335 # XXX: arg.prompt(options)
336 raise MissingRequiredParam(arg)
338 class Error(wizard.Error):
339 """Base error class for this module."""
342 class Failure(Error):
343 """Web install process failed."""
344 # XXX: we can give better error messages
347 class StrategyFailed(Error):
348 """Strategy couldn't figure out values."""
351 class UnrecognizedPreloads(Error):
352 """You passed a preload that was not recognized."""
353 #: The preloads that were not recognized.
355 def __init__(self, preloads):
356 self.preloads = preloads
358 return "Did not recognize these preloads: " + ", ".join(self.preloads)
360 class MissingRequiredParam(Error):
361 """You missed a required argument, and we couldn't generate it."""
362 #: The :class:`Arg` that was not specified.
364 def __init__(self, arg):
367 return "Missing required parameter %s; try specifying option %s or environment variable %s" % (self.arg.name, self.arg.option, self.arg.envname)