* Added environment variables for all installation arguments.
* Added --srv-path option, for specifying local repositories.
* Added environment variables for some common command options,
namely WIZARD_SRV_PATH, WIZARD_VERBOSE and WIZARD_DEBUG.
* API change: 'wizard install --app APP DIR' to 'wizard
install APP DIR'
* Changed Application.repository to be a method that takes
srv_path as an argument (since this can now vary). Also
look for "$APP/.git" if "$APP.git" doesn't exist.
* Added AppVersion.pristine_tag
* Change Deployment.app_version algorithm to use
'git describe' over .scripts/version
* Miscellaneous typo fixes
Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
rest_argv = args[1:]
baton = command.OptionBaton()
baton.add("--versions-path", dest="versions_path",
- default="/afs/athena.mit.edu/contrib/scripts/sec-tools/store/versions",
- help="Location of parallel-find output directory, or a file containing a newline separated list of 'all autoinstalls' (for testing).")
+ default=os.getenv("WIZARD_VERSIONS_PATH") or "/afs/athena.mit.edu/contrib/scripts/sec-tools/store/versions",
+ help="Location of parallel-find output directory, or a file containing a newline separated list of 'all autoinstalls' (for development work). Environment variable is WIZARD_VERSIONS_PATH.")
+ baton.add("--srv-path", dest="srv_path",
+ default=os.getenv("WIZARD_SRV_PATH") or "/afs/athena.mit.edu/contrib/scripts/git/autoinstalls",
+ help="Location of autoinstall Git repositories, such that $REPO_PATH/$APP.git is a repository (for development work). Environment variable is WIZARD_SRV_PATH.")
try:
command_name = args[0]
except IndexError:
--- /dev/null
+config
+testdir*
--- /dev/null
+# this file is meant to be source'd by any test script
+
+export WIZARD_ADMIN_NAME="admin"
+export WIZARD_ADMIN_PASSWORD="wizard"
+
+if [ -e "config" ]; then
+ source config
+fi
--- /dev/null
+#!/bin/bash -e
+
+TMPNAME="testdir-upgrade-mediawiki-1.15.0"
+APPNAME="mediawiki"
+source setup
+
+if [ -e "$TMPNAME" ]; then
+ echo "Removing previous $TMPNAME folder..."
+ rm -Rf "$TMPNAME"
+fi
+
+wizard install mediawiki-1.15.0-scripts "$TMPNAME" -- --title="TestApp"
+wizard upgrade "$TMPNAME"
ERROR: No such directory... check your typing
"""
+def boolish(val):
+ """
+ Parse the contents of an environment variable as a boolean.
+ This recognizes more values as ``False`` than :func:`bool` would.
+
+ >>> boolish("0")
+ False
+ >>> boolish("no")
+ False
+ >>> boolish("1")
+ True
+ """
+ try:
+ return bool(int(val))
+ except (ValueError, TypeError):
+ if val == "No" or val == "no" or val == "false" or val == "False":
+ return False
+ return bool(val)
+
def chdir(dir):
try:
os.chdir(dir)
self.add_option("-h", "--help", action="help", help=optparse.SUPPRESS_HELP)
group = optparse.OptionGroup(self, "Common Options")
group.add_option("-v", "--verbose", dest="verbose", action="store_true",
- default=False, help="Turns on verbose output")
+ default=boolish(os.getenv("WIZARD_VERBOSE")), help="Turns on verbose output. Environment variable is WIZARD_VERBOSE")
group.add_option("--debug", dest="debug", action="store_true",
- default=False, help="Turns on debugging output")
+ default=boolish("WIZARD_DEBUG"), help="Turns on debugging output. Environment variable is WIZARD_DEBUG")
group.add_option("-q", "--quiet", dest="quiet", action="store_true",
default=False, help="Turns off output to stdout")
- group.add_option("--log-file", dest="log_file",
+ group.add_option("--log-file", dest="log_file", metavar="FILE",
default=None, help="Logs verbose output to file")
- group.add_option("--log-file-chmod", dest="log_file_chmod",
+ group.add_option("--log-file-chmod", dest="log_file_chmod", metavar="CHMOD",
default=None, help="Chmod the log file after opening. Number is octal. You must chmod the log file 666 and place the file in /tmp if subprocesses are running as different users.")
- group.add_option("--indent", dest="indent",
+ group.add_option("--indent", dest="indent", metavar="WIDTH",
default=0, help="Indents stdout, useful for nested calls")
group.add_option("--context", dest="context", action="store_true",
default=False, help="Adds context to logs, useful for parallel processing")
from wizard import command, deploy, shell, util
def main(argv, baton):
- options, args = parse_args(argv)
+ options, args = parse_args(argv, baton)
# XXX: do something smart if -scripts is not at the end
- dir = args[0]
+ app = args[0]
+ dir = args[1]
if os.path.exists(dir):
raise DirectoryExistsError
- appname, _, version = options.app.partition('-')
- app = deploy.applications()[appname]
+ appname, _, version = app.partition('-')
+ application = deploy.applications()[appname]
sh = shell.Shell()
- sh.call("git", "clone", "--shared", app.repository, dir)
+ sh.call("git", "clone", "--shared", application.repository(options.srv_path), dir)
with util.ChangeDirectory(dir):
if version:
- sh.call("git", "checkout", options.app)
+ sh.call("git", "checkout", app)
# this command's stdin should be hooked up to ours
try:
- sh.call("wizard", "configure", *args[1:], interactive=True)
+ configure_args = args[2:] + command.makeBaseArgs(options)
+ sh.call("wizard", "configure", *configure_args, interactive=True)
except shell.PythonCallError:
sys.exit(1)
-def parse_args(argv):
+def parse_args(argv, baton):
usage = """usage: %prog install [APP [DIR -- [SETUPARGS]]]
Autoinstalls the application APP in the directory
WARNING: This command's API may change."""
parser = command.WizardOptionParser(usage)
- parser.add_option("--app", dest="app",
- help="Application to install, optionally specifying a version as APP-VERSION")
+ baton.push(parser, "srv_path")
options, args = parser.parse_all(argv)
+ # XXX: in the future, not specifying stuff is supported, since
+ # we'll prompt for it interactively
if not args:
- # XXX: in the future, not specifying stuff is supported, since
- # we'll prompt for it interactively
- parser.error("must specify application")
+ parser.error("must specify application and directory")
+ elif len(args) == 1:
+ parser.error("must specify directory")
return options, args
class DirectoryExistsError(wizard.Error):
parser.add_option("--seen", dest="seen",
default=None, help="File to read/write paths of already processed installs. These will be skipped.")
baton.push(parser, "versions_path")
+ baton.push(parser, "srv_path")
options, args, = parser.parse_all(argv)
if len(args) > 1:
parser.error("too many arguments")
return options, args
def calculate_base_args(options):
- base_args = command.makeBaseArgs(options, dry_run="--dry-run")
+ base_args = command.makeBaseArgs(options, dry_run="--dry-run", srv_path="--srv-path")
if not options.debug:
base_args.append("--quiet")
return base_args
from wizard import command, deploy, shell, util
def main(argv, baton):
- options, args = parse_args(argv)
+ options, args = parse_args(argv, baton)
dir = args[0]
logging.debug("uid is %d" % os.getuid())
deployment = make_deployment() # uses chdir
version = deployment.app_version
- repo = version.application.repository
+ repo = version.application.repository(options.srv_path)
tag = version.scripts_tag
os.unsetenv("GIT_DIR") # prevent some perverse errors
it is upgrading (see the scripts AFS kernel patch). Do
NOT run this command as root."""
parser = command.WizardOptionParser(usage)
+ baton.push(parser, "srv_path")
parser.add_option("--dry-run", dest="dry_run", action="store_true",
default=False, help="Prints would would be run without changing anything")
parser.add_option("--force", "-f", dest="force", action="store_true",
# different history tree, stuff is problems)
def main(argv, baton):
- options, args = parse_args(argv)
+ options, args = parse_args(argv, baton)
dir = args[0]
command.chdir(dir)
if not os.path.isdir(".git"):
if e.errno == errno.ENOENT:
raise NotAutoinstallError()
else: raise e
- repo = d.application.repository
+ repo = d.application.repository(options.srv_path)
# begin the command line process
sh = shell.Shell()
# setup environment
sh.call("git", "reset", "--hard")
return virtual_commit
user_tree = sh.call("git", "rev-parse", "HEAD^{tree}")[0].rstrip()
- base_virtual_commit = make_virtual_commit("v" + str(d.version))
+ base_virtual_commit = make_virtual_commit(d.app_version.pristine_tag)
next_virtual_commit = make_virtual_commit(version, [base_virtual_commit])
user_virtual_commit = sh.call("git", "commit-tree", user_tree, "-p", base_virtual_commit, input="")[0].rstrip()
sh.call("git", "checkout", user_virtual_commit, "--")
# Make it possible to resume here
new_tree = sh.call("git", "rev-parse", "HEAD^{tree}")[0].rstrip()
final_commit = sh.call("git", "commit-tree", new_tree, "-p", user_commit, "-p", next_commit, input=message)[0].rstrip()
- sh.call("git", "checkout", "master")
+ try:
+ sh.call("git", "checkout", "-b", "master", "--")
+ except shell.CallError:
+ sh.call("git", "checkout", "master", "--")
sh.call("git", "reset", "--hard", final_commit)
# Till now, all of our operations were in a tmp sandbox.
if options.dry_run:
logging.info("No changes detected")
pass
-def parse_args(argv):
+def parse_args(argv, baton):
usage = """usage: %prog upgrade [ARGS] DIR
Upgrades an autoinstall to the latest version. This involves
parser = command.WizardOptionParser(usage)
parser.add_option("--dry-run", dest="dry_run", action="store_true",
default=False, help="Prints would would be run without changing anything")
+ baton.push(parser, "srv_path")
options, args = parser.parse_all(argv)
if len(args) > 1:
parser.error("too many arguments")
import tempfile
import wizard
-from wizard import log
+from wizard import git, log, util
## -- Global Functions --
def app_version(self):
"""The :class:`ApplicationVersion` of this deployment."""
if not self._app_version:
- if os.path.isfile(self.version_file):
- fh = open(self.version_file)
- appname, _, version = fh.read().rstrip().partition('-')
- fh.close()
+ if os.path.isdir(os.path.join(self.location, ".git")):
+ with util.ChangeDirectory(self.location):
+ appname, _, version = git.describe().partition('-')
self._app_version = ApplicationVersion.make(appname, version)
else:
self._app_version = self.log[-1].version
# cache variables
self._extractors = {}
self._parametrizers = {}
- @property
- def repository(self):
+ def repository(self, srv_path):
"""
Returns the Git repository that would contain this application.
+ ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
Throws :exc:`NoRepositoryError` if the calculated path does not
exist.
"""
- repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
+ repo = os.path.join(srv_path, self.name + ".git")
if not os.path.isdir(repo):
- raise NoRepositoryError(app)
+ repo = os.path.join(srv_path, self.name, ".git")
+ if not os.path.isdir(repo):
+ raise NoRepositoryError(self.name)
return repo
def makeVersion(self, version):
"""
``application`` is a :class:`Application`."""
#: The :class:`distutils.version.LooseVersion` of this instance.
version = None
- #: The :class:`Appliation` of this instance.
+ #: The :class:`Application` of this instance.
application = None
def __init__(self, version, application):
self.version = version
Use this function only during migration, as it does
not account for the existence of ``-scripts2``.
"""
- return "v%s-scripts" % self.version
+ return "%s-scripts" % self.pristine_tag
+ @property
+ def pristine_tag(self):
+ """
+ Returns the name of the Git tag for the pristine version corresponding
+ to this version.
+ """
+ return "%s-%s" % (self.application.name, self.version)
def __cmp__(x, y):
return cmp(x.version, y.version)
@staticmethod
to the database, or the name of the new application). Instances
of :class:`Arg` can be registered to the :class:`ArgHandler`, which
manages marshalling these objects to whatever object
-is actually managing user input.
+is actually managing user input. An argument is any valid Python
+variable name, usually categorized using underscores (i.e.
+admin_user); the argument capitalized and with 'WIZARD_' prepended
+to it indicates a corresponding environment variable, i.e.
+'WIZARD_ADMIN_USER'.
Because autoinstallers will often have a number of themed
arguments (i.e. MySQL credentials) that are applicable across
Because Wizard is eventually intended for public use,
some hook mechanism for overloading the default strategies will
- need to be created.
+ need to be created. Setting up environment variables may act
+ as a vaguely reasonable workaround in the interim.
.. testsetup:: *
def option(self):
"""Full string of the option."""
return attr_to_option(self.name)
+ @property
+ def envname(self):
+ """Name of the environment variable containing this arg."""
+ return 'WIZARD_' + self.name.upper()
def __init__(self, name, password=False, type=None, help="XXX: UNDOCUMENTED"):
self.name = name
self.password = password
"""
Generic controller which takes an argument specification of :class:`Arg`
and configures either a command line flags parser
- (:class:`optparse.OptionParser`), an interactive user prompt
+ (:class:`optparse.OptionParser`), environment variables,
+ an interactive user prompt
(:class:`OptionPrompt`) or possibly a web interface to request
these arguments appropriately. This controller also
handles :class:`ArgSet`, which group related
argsets_strategy = []
argsets_strategy_with_side_effects = []
for argset in self.argsets:
+ # fill in environment variables
+ for arg in argset.args:
+ if getattr(options, arg.name) is None:
+ val = os.getenv(arg.envname)
+ if val is not None:
+ setattr(options, arg.name, val)
if not argset.strategy:
argsets_nostrategy.append(argset)
elif argset.strategy.side_effects:
def __init__(self, arg):
self.arg = arg
def __str__(self):
- return "Missing required parameter %s; try specifying %s" % (self.arg.name, self.arg.option)
+ return "Missing required parameter %s; try specifying option %s or environment variable %s" % (self.arg.name, self.arg.option, self.arg.envname)