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 *
30 from wizard import deploy, shell, sql, util
32 def dsn_callback(options):
33 if not isinstance(options.dsn, sqlalchemy.engine.url.URL):
34 options.dsn = sqlalchemy.engine.url.make_url(options.dsn)
35 # do some guessing with sql
36 options.dsn = sql.auth(options.dsn)
37 # perform some sanity checks on the database
38 database = options.dsn.database
39 options.dsn.database = None
40 engine = sqlalchemy.create_engine(options.dsn)
41 # generates warnings http://groups.google.com/group/sqlalchemy/browse_thread/thread/b7123fefb7dd83d5
42 with warnings.catch_warnings():
43 warnings.simplefilter("ignore")
44 engine.execute("CREATE DATABASE IF NOT EXISTS `%s`" % database)
45 options.dsn.database = database
46 # XXX: another good thing to check might be that the database is empty
48 # XXX: This is in the wrong place
49 def fetch(options, path, post=None):
51 Fetches a web page from the autoinstall, usually to perform database
52 installation. ``path`` is the path of the file to retrieve, relative
53 to the autoinstall base (``options.web_path``), not the web root.
54 ``post`` is a dictionary to post. ``options`` is the options
55 object generated by :class:`OptionParser`.
57 return util.fetch(options.web_host, options.web_path, path, post)
59 class Strategy(object):
61 Represents a strategy for calculating arg values without user input.
63 Under many circumstances, making some assumptions about the server
64 environment means that we don't actually have to ask the user for values
65 such as the host or the path: these tend to be side effect free strategies.
66 Furthermore, we may have utility scripts present that can automatically
67 configure a new database for a user when one is necessary: these are side
68 effectful computations.
70 Note for an implementor: it is perfectly acceptable to calculate preliminary
71 results in :meth:`prepare`, store them as underscore prefixed variables,
72 and refer to them from :meth:`execute`.
74 #: Arguments that this strategy provides
75 provides = frozenset()
76 #: Whether or not this strategy has side effects.
79 #: The :class:`ArgSchema` being created
81 #: The :class:`wizard.app.Application` being installed.
83 #: The directory we are being installed to.
85 #: The directory web stub files are being installed to.
87 def __init__(self, schema, application, dir, web_stub_path):
89 self.application = application
91 self.web_stub_path = web_stub_path
94 Performs all side-effectless computation associated with this
95 strategy. It also detects if computation is possible, and
96 raises :exc:`StrategyFailed` if it isn't.
98 raise NotImplementedError
99 def execute(self, options):
101 Performs effectful computations associated with this strategy,
102 and mutates ``options`` with the new values. Behavior is
103 undefined if :meth:`prepare` was not called first. If this
104 method throws an exception, it should be treated as fatal.
106 raise NotImplementedError
108 class EnvironmentStrategy(Strategy):
110 Fills in values from environment variables, based off of
111 :attr:`Arg.envname` from ``schema``.
113 def __init__(self, *args):
114 Strategy.__init__(self, *args)
115 self.provides = set()
117 for arg in self.schema.args.values():
118 if os.getenv(arg.envname) is not None:
119 self.provides.add(arg.name)
120 self.envlookup[arg.name] = arg.envname
122 """This strategy is always available."""
124 def execute(self, options):
125 """Sets undefined options to their environment variables."""
126 for name, envname in self.envlookup.items():
127 if getattr(options, name) is not None:
129 setattr(options, name, os.getenv(envname))
131 class WebStrategy(Strategy):
132 """Performs guesses for web variables using the URL hook."""
133 provides = frozenset(["web_host", "web_path"])
135 """Uses :func:`deploy.web`."""
138 if not os.path.exists(self.dir):
141 urls = deploy.web(self.dir, None)
145 self._url = urls.next() # pylint: disable-msg=E1101
146 except StopIteration:
151 def execute(self, options):
153 options.web_host = self._url.netloc # pylint: disable-msg=E1101
154 options.web_path = self._url.path # pylint: disable-msg=E1101
155 options.web_inferred = True # hacky: needed to see if we need a .scripts/url file
159 Represent a required, named argument for installation.
161 #: Attribute name of the argument
165 #: String to display if prompting a user for a value
167 #: String "type" of the argument, used for metavar
169 #: If true, is a password
171 #: Callback that this argument wants to get run on options after finished
175 """Name of the environment variable containing this arg."""
176 return 'WIZARD_' + self.name.upper()
177 def __init__(self, name, **kwargs):
179 for k,v in kwargs.items(): # cuz I'm lazy
180 if not hasattr(self, k):
181 raise TypeError("Arg() got unexpected keyword argument '%s'" % k)
183 if self.prompt is None:
184 self.prompt = self.help
186 class ArgSet(object):
188 Represents a set of named installation arguments that are required
189 for an installation to complete successfully. Arguments in a set
190 should share a common prefix and be related in functionality (the
191 litmus test is if you need one of these arguments, you should need
192 all of them). Register them in :func:`preloads`.
194 #: The :class:`Arg` objects that compose this argument set.
196 # XXX: probably could also use a callback attribute
200 class WebArgSet(ArgSet):
201 """Common arguments for any application that lives on the web."""
204 Arg("web_host", type="HOST", help="Host that the application will live on"),
205 Arg("web_path", type="PATH", help="Relative path to your application root"),
208 class DbArgSet(ArgSet):
209 """Common arguments for applications that use a database."""
212 Arg("dsn", type="DSN", help="Database Source Name, i.e. mysql://user:pass@host/dbname", callback=dsn_callback),
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 prompt="You will be able to log in using a username of your choice. Please decide on a username and enter it below."),
221 Arg("admin_password", type="PWD", password=True, help="Password of admin user",
222 prompt="Please decide on an admin password."),
225 class EmailArgSet(ArgSet):
226 """Common arguments when an administrative email is required."""
229 Arg("email", help="Administrative email"),
232 class TitleArgSet(ArgSet):
233 """Common arguments when a title is required."""
236 Arg("title", help="Title of your new site",
237 prompt="Please decide on a title for your new website."),
242 Retrieves a dictionary of string names to precanned :class:`ArgSet` objects.
247 'admin': AdminArgSet(),
248 'email': EmailArgSet(),
249 'title': TitleArgSet(),
252 class ArgSchema(object):
254 Schema container for arguments.
256 Valid identifiers for subclasses of :class:`ArgSet` are:
258 * ``db``, which populates the option ``dsn``, which is a SQLAlchemy
259 database source name, with properties for ``drivername``,
260 ``username``, ``password``, ``host``, ``port``, ``database`` and
262 * ``admin``, which populates the options ``admin_name`` and
264 * ``email``, which populates the option ``email``.
265 * ``title``, which populates the option ``title``.
267 The options ``web_path`` and ``web_host`` are automatically required.
271 parser = ArgHandler("db", "admin", "email")
272 parser.add(Arg("title", help="Title of the new application"))
274 #: Dictionary of argument names to :class:`Arg` objects in schema.
276 #: List of :class:`Strategy` objects in schema.
278 #: Set of arguments that are already provided by :attr:`strategies`.
280 def __init__(self, *args):
282 preload_dict = preloads()
287 for arg in preload_dict[preload].args:
288 self.args[arg.name] = arg
290 raise UnrecognizedPreloads(preload)
292 """Adds an argument to our schema."""
293 self.args[arg.name] = arg
294 def commit(self, application, dir, web_stub_path):
295 """Populates :attr:`strategies` and :attr:`provides`"""
297 self.provides = set()
302 for entry in pkg_resources.iter_entry_points("wizard.install.strategy"):
304 raw_strategies.append(cls)
305 for strategy_cls in raw_strategies:
307 strategy = strategy_cls(self, application, dir, web_stub_path)
309 self.provides |= strategy.provides
310 self.strategies.append(strategy)
311 except StrategyFailed:
313 # do non-effectful strategies first; this is a stable sort
314 self.strategies.sort(key=lambda x: x.side_effects)
315 def fill(self, options):
317 Fills an object with all arguments pre-set
321 if not hasattr(options, i):
322 setattr(options, i, None)
323 def load(self, options):
325 Load values from strategy. Must be called after :meth:`commit`. We
326 omit strategies whose provided variables are completely specified
327 already. Will raise :exc:`MissingRequiredParam` if strategies aren't
328 sufficient to fill all options. It will then run any callbacks on
331 unfilled = set(name for name in self.args if getattr(options, name) is None)
332 missing = unfilled - self.provides
334 raise MissingRequiredParam(missing)
335 for strategy in self.strategies:
336 # If the application being installed doesn't need all of the
337 # parameters a strategy could provide, we don't use it.
338 if any(not hasattr(options, name) for name in strategy.provides):
339 if any(hasattr(options, name) for name in strategy.provides):
340 logging.warning("Ignored partial strategy %s" % strategy)
342 if all(getattr(options, name) is not None for name in strategy.provides):
344 for name in strategy.provides:
345 if getattr(options, name) is not None:
346 logging.warning("Overriding pre-specified value for %s", name)
347 strategy.execute(options)
348 for arg in self.args.values():
349 if arg.callback is None:
351 arg.callback(options)
353 class Error(wizard.Error):
354 """Base error class for this module."""
357 class StrategyFailed(Error):
358 """Strategy couldn't figure out values."""
361 class UnrecognizedPreloads(Error):
362 """You passed a preload that was not recognized."""
363 #: The preloads that were not recognized.
365 def __init__(self, preloads):
366 self.preloads = preloads
368 return "Did not recognize these preloads: " + ", ".join(self.preloads)
370 class MissingRequiredParam(Error):
371 """You missed a required argument, and we couldn't generate it.
372 Controllers should catch this exception and provide better behavior."""
373 #: The names of the arguments that were not specified.
375 def __init__(self, args):
378 return "Missing required parameters: %s" % ', '.join(self.args)