list Lists autoinstalls, with optional filtering
massmigrate Performs mass migration of autoinstalls of an application
migrate Migrate autoinstalls from old format to Git-based format
+ prepare-config Prepares configuration files for versioning
summary Generate statistics (see help for subcommands)
upgrade Upgrades an autoinstall to the latest version
command_module.main(rest_argv, baton)
def get_command(name):
+ name = name.replace("-", "_")
__import__("wizard.command." + name)
return getattr(wizard.command, name)
import os.path
+import re
+
+def expand_re(val):
+ if isinstance(val, str):
+ return re.escape(val)
+ else:
+ return '(?:' + '|'.join(map(expand_re, val)) + ')'
def filename_regex_extractor(f):
- """This is a decorator to apply to functions that take a name and return
+ """
+ This is a decorator to apply to functions that take a name and return
(filename, RegexObject) tuples. It converts it into a function
that takes a name and returns another function (the actual extractor)
which takes a deployment and returns the value of the extracted variable.
(with the second subgroup being the value we care about), so that we can
reuse the regex for other things.
- Its Haskell-style type signature would be:
+ Its Haskell-style type signature would be::
+
(String -> (Filename, Regex)) -> (String -> (Deployment -> String))
- It's a little bit like a transformer."""
+ For convenience purposes, we also accept [Filename], in which case
+ we use the first entry (index 0). Passing an empty list is invalid.
+ """
def g(var):
file, regex = f(var)
+ if not isinstance(file, str):
+ file = file[0]
def h(deployment):
contents = deployment.read(file) # cached
match = regex.search(contents)
return h
return g
+def filename_regex_substitution(f):
+ """
+ This is a decorator to apply to functions that take a name and return
+ (filename, RegexObject) tuples. It converts it into a function
+ that takes a name and returns another function (that does substitution)
+ which takes a deployment and modifies its files to replace explicit
+ values with their generic WIZARD_* equivalents. The final function returns
+ the number of replacements made.
+
+ The regular expression requires a very specific form, essentially ()()()
+ (with the second subgroup being the value to be replaced).
+
+ Its Haskell-style type signature would be::
+
+ (String -> ([Filename], Regex)) -> (String -> (Deployment -> IO Int))
+
+ For convenience purposes, we also accept Filename, in which case it is treated
+ as a single item list.
+ """
+ def g(key, var):
+ files, regex = f(var)
+ if isinstance(files, str):
+ files = (files,)
+ def h(deployment):
+ base = deployment.location
+ subs = 0
+ for file in files:
+ file = os.path.join(base, file)
+ contents = open(file, "r").read()
+ contents, n = regex.subn("\\1" + key + "\\3", contents)
+ subs += n
+ open(file, "w").write(contents)
+ return subs
+ return h
+ return g
+
from wizard.app import php
def make_filename_regex(var):
- return 'LocalSettings.php', re.compile('^(\$' + re.escape(var) + r'''\s*=\s*)(.*)(;)$''', re.M)
+ return 'LocalSettings.php', re.compile('^(\$' + app.expand_re(var) + r'''\s*=\s*)(.*)(;)$''', re.M)
make_extractor = app.filename_regex_extractor(make_filename_regex)
+make_substitution = app.filename_regex_substitution(make_filename_regex)
+seed = {
+ 'WIZARD_IP': 'IP', # obsolete, remove after we're done
+ 'WIZARD_SITENAME': 'wgSitename',
+ 'WIZARD_SCRIPTPATH': 'wgScriptPath',
+ 'WIZARD_EMERGENCYCONTACT': ('wgEmergencyContact', 'wgPasswordSender'),
+ 'WIZARD_DBSERVER': 'wgDBserver',
+ 'WIZARD_DBNAME': 'wgDBname',
+ 'WIZARD_DBUSER': 'wgDBuser',
+ 'WIZARD_DBPASSWORD': 'wgDBpassword',
+ 'WIZARD_SECRETKEY': ('wgSecretKey', 'wgProxyKey'),
+ }
class Application(deploy.Application):
parametrized_files = ['LocalSettings.php', 'php.ini']
+ deprecated_keys = set(['WIZARD_IP']) | php.deprecated_keys
@property
def extractors(self):
if not self._extractors:
- self._extractors = util.dictmap(make_extractor,
- {'WIZARD_IP': 'IP' # obsolete, remove after we're done
- ,'WIZARD_SITENAME': 'wgSitename'
- ,'WIZARD_SCRIPTPATH': 'wgScriptPath'
- ,'WIZARD_EMERGENCYCONTACT': 'wgEmergencyContact'
- ,'WIZARD_DBSERVER': 'wgDBserver'
- ,'WIZARD_DBNAME': 'wgDBname'
- ,'WIZARD_DBUSER': 'wgDBuser'
- ,'WIZARD_DBPASSWORD': 'wgDBpassword'
- ,'WIZARD_PROXYKEY': 'wgProxyKey'
- })
+ self._extractors = util.dictmap(make_extractor, seed)
self._extractors.update(php.extractors)
return self._extractors
@property
+ def substitutions(self):
+ if not self._substitutions:
+ self._substitutions = util.dictkmap(make_substitution, seed)
+ self._substitutions.update(php.substitutions)
+ return self._substitutions
+ @property
def install_handler(self):
handler = install.ArgHandler("mysql", "admin", "email")
handler.add(install.Arg("title", help="Title of your new MediaWiki install"))
from wizard import app, util
def make_filename_regex(var):
- return 'php.ini', re.compile('^(' + re.escape(var) + r'\s*=\s*)(.*)()$', re.M)
+ return 'php.ini', re.compile('^(' + app.expand_re(var) + r'\s*=\s*)(.*)()$', re.M)
make_extractor = app.filename_regex_extractor(make_filename_regex)
+make_substitution = app.filename_regex_substitution(make_filename_regex)
+seed = {
+ 'WIZARD_SESSIONNAME': 'session.name',
+ 'WIZARD_TMPDIR': ('upload_tmp_dir', 'session.save_path'),
+ }
-extractors = util.dictmap(make_extractor,
- {'WIZARD_SESSIONNAME': 'session.name'
- ,'WIZARD_TMPDIR': 'upload_tmp_dir'
- })
+extractors = util.dictmap(make_extractor, seed)
+substitutions = util.dictkmap(make_substitution, seed)
+deprecated_keys = set([])
"""Attempt to extract variables and complain if some are missing."""
variables = d.extract()
for k,v in variables.items():
- if v is None:
- # once we get everything on the same version, you should
- # actually start paying attention to these warnings
+ if v is None and k not in d.application.deprecated_keys:
logging.warning("Variable %s not found" % k)
else:
logging.debug("Variable %s is %s" % (k,v))
--- /dev/null
+import os
+import shutil
+import logging
+import errno
+import sys
+
+import wizard
+from wizard import command, deploy, shell, util
+
+def main(argv, baton):
+ options, args = parse_args(argv, baton)
+ sh = shell.Shell()
+ d = deploy.Deployment(".")
+ try:
+ configure_args = args + command.makeBaseArgs(options)
+ sh.call("wizard", "configure", *configure_args, interactive=True)
+ except shell.PythonCallError:
+ sys.exit(1)
+ d.prepareConfig()
+
+def parse_args(argv, baton):
+ usage = """usage: %prog prepare-config -- [SETUPARGS]
+
+During the preparation of an upgrade, changes to configuration files
+must be taken into account. Unfortunately, configuration files
+are not commonly versioned, and are commonly autogenerated. Running
+this command will update all configuration files.
+
+To be more specific, it takes a checked out repository, and performs a
+configuration. Then, it replaces the specific values from the installation
+back with generic values, which can be committed to the repository. The final
+working copy state has HEAD pointing to the -scripts version that the commit
+was based off of, but with local changes that can be incorporated using
+`git commit --amend -a`. You should inspect the changes with `git diff`;
+it is possible that the regular expressions in wizard.app.APPNAME are
+now non-functional.
+
+Wizard should be run in the environment installations are planned
+to be deployed to, because installers can have subtle differences
+in configuration files based on detected server configuration.
+"""
+ parser = command.WizardOptionParser(usage)
+ return parser.parse_all(argv)
+
import dateutil.parser
import distutils.version
import tempfile
+import logging
import wizard
from wizard import git, log, util
as such dir will generally not equal :attr:`location`.
"""
return self.application.parametrize(self, dir)
+ def prepareConfig(self):
+ """
+ Edits files in the deployment such that any user-specific configuration
+ is replaced with generic WIZARD_* variables.
+ """
+ return self.application.prepareConfig(self)
def updateVersion(self, version):
"""
Update the version of this deployment.
self.versions = {}
# cache variables
self._extractors = {}
- self._parametrizers = {}
+ self._substitutions = {}
def repository(self, srv_path):
"""
Returns the Git repository that would contain this application.
tmp.write(contents)
tmp.close()
os.rename(tmp.name, fullpath)
+ def prepareConfig(self, deployment):
+ """
+ Takes a deployment and replaces any explicit instances
+ of a configuration variable with generic WIZARD_* constants.
+ There is a sane default implementation built on substitutions;
+ you can override this method to provide arbitrary extra
+ behavior.
+ """
+ for key, subst in self.substitutions.items():
+ subs = subst(deployment)
+ if not subs and key not in self.deprecated_keys:
+ logging.warning("No substitutions for %s" % key)
@property
def extractors(self):
"""
See also :func:`wizard.app.filename_regex_extractor`.
"""
return {}
+ @property
+ def substitutions(self):
+ """
+ Dictionary of variable names to substitution functions. These functions
+ take a :class:`Deployment` as an argument and modify the deployment such
+ that an explicit instance of the variable is released with the generic
+ WIZARD_* constant. See also :func:`wizard.app.filename_regex_substitution`.
+ """
+ return {}
@staticmethod
def make(name):
"""Makes an application, but uses the correct subtype if available."""
"""
return dict((k,f(v)) for k,v in d.items())
+def dictkmap(f, d):
+ """
+ A map function for dictionaries that passes key and value.
+
+ >>> dictkmap(lambda x, y: x + y, {1: 4, 3: 4})
+ {1: 5, 3: 6}
+ """
+ return dict((k,f(k,v)) for k,v in d.items())
+
def get_exception_name(output):
"""
Reads the traceback from a Python program and grabs the