2 This module contains a 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.
16 Because autoinstallers will often have a number of themed
17 arguments (i.e. MySQL credentials) that are applicable across
18 autoinstallers, :class:`ArgSet` can be use to group :class:`Arg`
19 instances together, as well as promote reuse of these arguments.
20 There are a number of precanned :class:`ArgSet` subclasses
21 that serve this purpose, such as :class:`MysqlArgSet`.
22 :class:`ArgHandler` also contains some convenience syntax in its
23 constructor for loading :class:`ArgSet`.
25 Certain arguments will vary from install to install, but
26 can be automatically calculated if certain assumptions about the
27 server environment are made. For example, an application might
28 request an email; if we are on an Athena machine, one would
29 reasonably expect the currently logged in user + @mit.edu to be
30 a valid email address. :class:`Strategy` objects are responsible
31 for this sort of calculation, and may be attached to any
32 :class:`ArgSet` instance. (If you would like to attach a strategy
33 to a single arg, you should put the arg in a :class:`ArgSet` and
34 then set the strategy).
36 Finally, certain :class:`Strategy` objects may perform operations
37 with side effects (as marked by :attr:`Strategy.side_effects`).
38 The primary use case for this is automatic creation of databases
39 during an autoinstall. Marking a :class:`Strategy` as having
40 side effects is important, so as to delay executing it until
41 absolutely necessary (at the end of options parsing, but before
42 the actual installation begins).
46 Because Wizard is eventually intended for public use,
47 some hook mechanism for overloading the default strategies will
52 from wizard.install import *
62 from wizard import util
64 def fetch(options, path, post=None):
66 Fetches a web page from the autoinstall, usually to perform database
67 installation. ``path`` is the path of the file to retrieve, relative
68 to the autoinstall base (``options.web_path``), not the web root.
69 ``post`` is a dictionary to post. ``options`` is the options
70 object generated by :class:`OptionParser`.
72 h = httplib.HTTPConnection(options.web_host)
73 fullpath = options.web_path + "/" + path
75 headers = {"Content-type": "application/x-www-form-urlencoded"}
76 h.request("POST", fullpath, urllib.urlencode(post), headers)
78 h.request("GET", fullpath)
84 def attr_to_option(variable):
86 Converts Python attribute names to command line options.
88 >>> attr_to_option("foo_bar")
91 return '--' + variable.replace('_', '-')
95 Retrieves a dictionary of string names to precanned :class:`ArgSet` objects.
98 'mysql': MysqlArgSet(),
99 'admin': AdminArgSet(),
100 'email': EmailArgSet(),
103 class Strategy(object):
104 """Represents a strategy for calculating arg values without user input."""
105 #: Whether or not this strategy has side effects.
107 def execute(self, options):
109 Calculates values for the arguments that this strategy has been
110 associated with, and then mutates ``options`` to contain those new
111 values. This function is atomic; when control leaves, all of the
112 options should either have values, **or** a :exc:`FailedStrategy` was
113 raised and none of the options should have been changed.
114 Execution is bypassed if all options are explicitly specified, even
115 in the case of strategies with side effects..
119 class ScriptsWebStrategy(Strategy):
120 """Performs scripts specific guesses for web variables."""
121 # XXX: THIS CODE SUCKS
122 def execute(self, options):
123 _, _, web_path = os.getcwd().partition("/web_scripts")
126 options.web_path = web_path
127 options.web_host = util.get_dir_owner() + ".scripts.mit.edu"
129 class ScriptsMysqlStrategy(Strategy):
131 Performs scripts specific guesses for mysql variables. This
132 may create an appropriate database for the user.
135 def execute(self, options):
137 triplet = subprocess.Popen("/mit/scripts/sql/bin/get-password", stdout=subprocess.PIPE).communicate()[0].rstrip().split()
140 name = os.path.basename(os.getcwd())
141 username = os.getenv('USER')
142 options.mysql_host, options.mysql_user, options.mysql_password = triplet
144 options.mysql_db = username + '+' + subprocess.Popen(["/mit/scripts/sql/bin/get-next-database", name], stdout=subprocess.PIPE).communicate()[0].rstrip()
145 subprocess.Popen(["/mit/scripts/sql/bin/create-database", options.mysql_db], stdout=subprocess.PIPE).communicate()
147 class ScriptsEmailStrategy(Strategy):
148 """Performs script specific guess for email."""
149 def execute(self, options):
150 # XXX: should double-check that you're on a scripts server
151 # and fail if you're not.
152 options.email = os.getenv("USER") + "@mit.edu"
156 Represent a required, named argument for installation. These
157 cannot have strategies associated with them, so if you'd like
158 to have a strategy associated with a single argument, create
159 set with one item in it.
161 #: Attribute name of the argument
165 #: String "type" of the argument, used for metavar
169 """Full string of the option."""
170 return attr_to_option(self.name)
171 def __init__(self, name, password=False, type=None, help="XXX: UNDOCUMENTED"):
173 self.password = password
177 class ArgSet(object):
179 Represents a set of named installation arguments that are required
180 for an installation to complete successfully. Arguments in a set
181 should share a common prefix and be related in functionality (the
182 litmus test is if you need one of these arguments, you should need
185 #: The :class:`Arg` objects that compose this argument set.
187 #: The :class:`Strategy` objects for this option
192 class WebArgSet(ArgSet):
193 """Common arguments for any application that lives on the web."""
196 Arg("web_host", type="HOST", help="Host that the application will live on"),
197 Arg("web_path", type="PATH", help="Relative path to your application root"),
199 self.strategy = ScriptsWebStrategy()
201 class MysqlArgSet(ArgSet):
202 """Common arguments for applications that use a MySQL database."""
205 Arg("mysql_host", type="HOST", help="Host that your MySQL server lives on"),
206 Arg("mysql_db", type="DB", help="Name of the database to populate"),
207 Arg("mysql_user", type="USER", help="Name of user to access database with"),
208 Arg("mysql_password", type="PWD", password=True, help="Password of the database user"),
210 self.strategy = ScriptsMysqlStrategy()
212 class AdminArgSet(ArgSet):
213 """Common arguments when an admin account is to be created."""
216 Arg("admin_name", type="NAME", help="Name of admin user to create"),
217 Arg("admin_password", type="PWD", password=True, help="Password of admin user"),
220 class EmailArgSet(ArgSet):
221 """Common arguments when an administrative email is required."""
224 Arg("email", help="Administrative email"),
226 self.strategy = ScriptsEmailStrategy()
228 class ArgHandler(object):
230 Generic controller which takes an argument specification of :class:`Arg`
231 and configures either a command line flags parser
232 (:class:`optparse.OptionParser`), an interactive user prompt
233 (:class:`OptionPrompt`) or possibly a web interface to request
234 these arguments appropriately. This controller also
235 handles :class:`ArgSet`, which group related
236 functionality together and can be reused from installer to installer.
238 Valid identifiers for subclasses of :class:`ArgSet` are:
240 * ``mysql``, which populates the options ``mysql_host``, ``mysql_db``,
241 ``mysql_user`` and ``mysql_password``.
242 * ``admin``, which populates the options ``admin_name`` and
244 * ``email``, which populates the option ``email``.
246 The options ``web_path`` and ``web_host`` are automatically required.
250 parser = ArgHandler("sql", "admin", "email")
251 parser.add(Arg("title", help="Title of the new application"))
253 #: List of :class:`ArgSet` objects in schema. The element at
254 #: index 0 will always be an anonymous :class:`ArgSet` that you
255 #: can add stray :class:`Arg`s to.
257 def __init__(self, *args):
258 self.argsets = [ArgSet(), WebArgSet()]
259 preload_dict = preloads()
262 self.argsets.append(preload_dict[preload])
264 raise UnrecognizedPreloads(preload)
266 """Adds an argument to our schema."""
267 self.argsets[0].args.append(arg)
268 def push(self, parser):
269 """Pushes arg schema to :class:`optparse.OptionParser`."""
270 for argset in self.argsets:
271 for arg in argset.args:
272 parser.add_option(attr_to_option(arg.name), dest=arg.name, metavar=arg.type,
273 default=None, help=arg.help)
274 def handle(self, options):
276 Takes the result of :meth:`optparse.OptionParser.parse_args`
277 and performs user interaction and/or calculations to complete
280 # categorize the argsets
281 argsets_nostrategy = []
282 argsets_strategy = []
283 argsets_strategy_with_side_effects = []
284 for argset in self.argsets:
285 if not argset.strategy:
286 argsets_nostrategy.append(argset)
287 elif argset.strategy.side_effects:
288 argsets_strategy_with_side_effects.append(argset)
290 argsets_strategy.append(argset)
291 for argset in argsets_nostrategy:
292 for arg in argset.args:
293 if getattr(options, arg.name) is None:
294 # XXX: do something interactive
295 raise MissingRequiredParam(arg)
297 for arg in argset.args:
298 if getattr(options, arg.name) is None:
301 for argset in argsets_strategy:
302 if all_set(argset): continue
303 argset.strategy.execute(options)
304 # XXX: do something interactive
305 for argset in argsets_strategy_with_side_effects:
306 if all_set(argset): continue
307 argset.strategy.execute(options)
310 class Error(wizard.Error):
311 """Base error class for this module."""
314 class Failure(Error):
315 """Web install process failed."""
316 # XXX: we can give better error messages
319 class StrategyFailed(Error):
320 """Strategy couldn't figure out values."""
323 class UnrecognizedPreloads(Error):
324 """You passed a preload that was not recognized."""
325 #: The preloads that were not recognized
327 def __init__(self, preloads):
328 self.preloads = preloads
330 return "Did not recognize these preloads: " + ", ".join(self.preloads)
332 class MissingRequiredParam(Error):
333 """You missed a required argument, and we couldn't generate it."""
334 #: The :class:`Arg` that was not specified.
336 def __init__(self, arg):
339 return "Missing required parameter %s; try specifying %s" % (self.arg.name, self.arg.option)