This builds on dlehman's image install work and adds the ability to run anaconda from inside a mock when doing image installs using a kickstart to drive things.
I started out with a stripped down anaconda script (ksimage) but it ended up not being needed. I include it only because it may be insteresting to some and won't commit it to the repo.
This adds a new command line mode called 'script' that raises errors instead of falling into infinite loops when there is a problem with the kickstart. It also short-circuits the network, assuming that the host already has netowrking up and running.
Additionally, it adds image cleanup to anaconda proper instead of in the anaconda-cleanup script -- it ended up that yum.log was being held open by logging and a simple logging.shutdown() freed that up so we can handle the unmounts inside anaconda. This is a big plus for error handling, it means you can fix your kickstart and rerun anaconda without and manual cleanup of mounted disk images.
I am proposing adding patch 2,3,5,6 to master. The other 2 are for Informational Purposes Only :)
Brian C. Lane (6): Add ksimage, execute kickstart image installs Add a script interface that raises errors Check for live when umounting Add cleanup of mounts to ksimage Add switch for script mode and fix graphics check in anaconda Add filesystem cleanup to anaconda
anaconda | 29 ++++- ksimage | 264 ++++++++++++++++++++++++++++++++++++++++++++++ pyanaconda/__init__.py | 3 + pyanaconda/script.py | 195 ++++++++++++++++++++++++++++++++++ scripts/anaconda-cleanup | 7 +- 5 files changed, 491 insertions(+), 7 deletions(-) create mode 100755 ksimage create mode 100644 pyanaconda/script.py
A stripped down version of the anaconda program that only does what is needed for kickstart and images. --- ksimage | 249 ++++++++++++++++++++++++++++++++++++++++++++++++ pyanaconda/__init__.py | 3 + 2 files changed, 252 insertions(+), 0 deletions(-) create mode 100755 ksimage
diff --git a/ksimage b/ksimage new file mode 100755 index 0000000..f9001fa --- /dev/null +++ b/ksimage @@ -0,0 +1,249 @@ +#!/usr/bin/python +# +# ksimage: The Red Hat Linux Installation program +# +# Copyright (C) 2011 +# Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author(s): Brian C. Lane bcl@redhat.com + +import sys +import os +import signal +from argparse import ArgumentParser + + +def AnacondaShowWarning(message, category, filename, lineno, file=sys.stderr, line=None): + """ Log warnings from python's warnings module to the anaconda log + """ + log.warning("%s" % warnings.formatwarning(message, category, filename, lineno, line)) + +def getAnacondaVersion(): + """ Return the anaconda version + + In the form of XX.YY + """ + # Using _isys here so we don't drag in the logging stuff, which is always + # complicated. + from pyanaconda import _isys + return _isys.getAnacondaVersion() + +def parseArguments(): + """ Parse commandline arguments (not /proc/cmdline) + """ + ap = ArgumentParser() + ap.add_argument('--version', action='version', version="%prog " + getAnacondaVersion()) + + ap.add_argument("-d", "--debug", dest="debug", action="store_true", + default=False, + help="Enable verbose debugging output") + ap.add_argument("--kickstart", dest="ksfile", required=True, + help="kickstart file to execute") + ap.add_argument("--image", action="append", dest="images", default=[], + required=True, + help="Drive images to install onto") + ap.add_argument("--loglevel", choices=["debug", "info", "warning", "error", + "critical"], + help="Set the logging level for the terminal. The default " + "value is info") + ap.add_argument("--syslog", + help="Use remote syslog server for logging. Value is <host>[:<port>] ") + + args = ap.parse_args() + if not args.ksfile: + print("missing kickstart file") + if not args.images: + print("missing drive image(s)") + if not args.ksfile or not args.images: + ap.print_help() + sys.exit(0) + + return args + +def setupEnvironment(): + """ Setup environmental variables + """ + os.environ['HOME'] = '/tmp' + os.environ['LC_NUMERIC'] = 'C' + os.environ["GCONF_GLOBAL_LOCKS"] = "1" + + # In theory, this gets rid of our LVM file descriptor warnings + os.environ["LVM_SUPPRESS_FD_WARNINGS"] = "1" + + # make sure we have /sbin and /usr/sbin in our path + os.environ["PATH"] += ":/sbin:/usr/sbin" + + # we can't let the LD_PRELOAD hang around because it will leak into + # rpm %post and the like. ick :/ + if os.environ.has_key("LD_PRELOAD"): + del os.environ["LD_PRELOAD"] + +def setupLoggingFromArgs(args): + if args.loglevel and anaconda_log.logLevelMap.has_key(args.loglevel): + level = anaconda_log.logLevelMap[args.loglevel] + anaconda_log.logger.tty_loglevel = level + anaconda_log.setHandlersLevel(log, level) + storage_log = logging.getLogger("storage") + anaconda_log.setHandlersLevel(storage_log, level) + + if args.syslog: + anaconda_log.logger.remote_syslog = args.syslog + +def setupPythonUpdates(): + from distutils.sysconfig import get_python_lib + + if not os.path.exists("/tmp/updates"): + return + + for pkg in os.listdir("/tmp/updates"): + d = "/tmp/updates/%s" % pkg + + if not os.path.isdir(d): + continue + + # See if the package exists in /usr/lib{64,}/python/?.?/site-packages. + # If it does, we can set it up as an update. If not, the pkg is + # likely a completely new directory and should not be looked at. + dest = "%s/%s" % (get_python_lib(), pkg) + if not os.access(dest, os.R_OK): + dest = "%s/%s" % (get_python_lib(1), pkg) + if not os.access(dest, os.R_OK): + continue + # Symlink over everything that's in the python libdir but not in + # the updates directory. + symlink_updates(dest, d) + + import glob + import shutil + for rule in glob.glob("/tmp/updates/*.rules"): + target = "/etc/udev/rules.d/" + rule.split('/')[-1] + shutil.copyfile(rule, target) + +def symlink_updates(dest_dir, update_dir): + contents = os.listdir(update_dir) + + for f in os.listdir(dest_dir): + dest_path = os.path.join(dest_dir, f) + update_path = os.path.join(update_dir, f) + if f in contents: + # recurse into directories, there might be files missing in updates + if os.path.isdir(dest_path) and os.path.isdir(update_path): + symlink_updates(dest_path, update_path) + else: + if f.endswith(".pyc") or f.endswith(".pyo"): + continue + os.symlink(dest_path, update_path) + +if __name__ == "__main__": + setupPythonUpdates() + + # do this early so we can set flags before initializing logging + args = parseArguments() + + from pyanaconda.flags import flags + if args.images: + flags.imageInstall = True + if args.debug: + flags.debug = True + + # Set up logging as early as possible. + import logging + from pyanaconda import anaconda_log + anaconda_log.init() + + log = logging.getLogger("anaconda") + stdoutLog = logging.getLogger("anaconda.stdout") + + if os.geteuid() != 0: + stdoutLog.error("anaconda must be run as root.") + sys.exit(0) + + log.info("%s %s" % (sys.argv[0], getAnacondaVersion())) + + # pull this in to get product name and versioning + from pyanaconda import product + + from pyanaconda import isys + isys.initLog() + + import warnings + + from pyanaconda import iutil + from pyanaconda import kickstart + + from pyanaconda import Anaconda + anaconda = Anaconda() + warnings.showwarning = AnacondaShowWarning + + # reset python's default SIGINT handler + signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGSEGV, isys.handleSegv) + + setupEnvironment() + + anaconda.opts = args + setupLoggingFromArgs(args) + anaconda.displayMode = 's' + anaconda.isHeadless = True + + log.info("anaconda called with cmdline = %s" %(sys.argv,)) + + os.system("udevadm control --env=ANACONDA=1") + + kickstart.preScriptPass(anaconda, args.ksfile) + anaconda.ksdata = kickstart.parseKickstart(anaconda, args.ksfile) + + anaconda.initInterface() + anaconda.instClass.configure(anaconda) + + kickstart.setSteps(anaconda) + + image_count = 0 + for image in args.images: + image_spec = image.rsplit(":", 1) + path = image_spec[0] + if len(image_spec) == 2 and image_spec[1].strip(): + name = image_spec[1].strip() + else: + name = os.path.splitext(os.path.basename(path))[0] + + if "/" in name or name in anaconda.storage.config.diskImages.keys(): + name = "diskimg%d" % image_count + + log.info("naming disk image '%s' '%s'" % (path, name)) + anaconda.storage.config.diskImages[name] = path + image_count += 1 + + if image_count: + anaconda.storage.setupDiskImages() + else: + log.error("No disk images to install to") + # TODO: Need to call some kind of cleanup here... + sys.exit(0) + + from pyanaconda.exception import initExceptionHandling + anaconda.mehConfig = initExceptionHandling(anaconda) + + # add our own additional signal handlers + signal.signal(signal.SIGUSR2, lambda signum, frame: anaconda.dumpState()) + + try: + anaconda.dispatch.run() + except SystemExit, code: + anaconda.intf.shutdown() + + +# vim:tw=78:ts=4:et:sw=4 diff --git a/pyanaconda/__init__.py b/pyanaconda/__init__.py index ccbf07b..839a160 100644 --- a/pyanaconda/__init__.py +++ b/pyanaconda/__init__.py @@ -252,6 +252,9 @@ class Anaconda(object): if self.displayMode == 'c': from cmdline import InstallInterface
+ if self.displayMode == 's': + from script import InstallInterface + self._intf = InstallInterface() return self._intf
Instead of falling into an infinite loop like cmdline raise an error with a useful description of the problem. --- pyanaconda/script.py | 195 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 195 insertions(+), 0 deletions(-) create mode 100644 pyanaconda/script.py
diff --git a/pyanaconda/script.py b/pyanaconda/script.py new file mode 100644 index 0000000..0394f0a --- /dev/null +++ b/pyanaconda/script.py @@ -0,0 +1,195 @@ +# +# script.py - non-interactive, script based anaconda interface +# +# Copyright (C) 2011 +# Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. +# +# Author(s): Brian C. Lane bcl@redhat.com +# + +import time +import signal +import parted +from constants import * +from flags import flags +from iutil import strip_markup +from installinterfacebase import InstallInterfaceBase + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("anaconda") + +stepToClasses = { "install" : "setupProgressDisplay", + "complete": "Finished" } + +class WaitWindow: + def pop(self): + pass + def refresh(self): + pass + def __init__(self, title, text): + print(text) + +class ProgressWindow: + def pop(self): + print("") + + def pulse(self): + pass + + def set(self, amount): + if amount == self.total: + print(_("Completed")) + + def refresh(self): + pass + + def __init__(self, title, text, total, updpct = 0.05, pulse = False): + self.total = total + print(text) + print(_("In progress")) + +class InstallInterface(InstallInterfaceBase): + def __init__(self): + InstallInterfaceBase.__init__(self) + signal.signal(signal.SIGTSTP, signal.SIG_DFL) + self.instProgress = None + + def __del__(self): + pass + + def reinitializeWindow(self, title, path, size, description): + raise RuntimeError, ("Script mode requires all choices to be " + "specified in a kickstart configuration file.") + + def shutdown(self): + pass + + def suspend(self): + pass + + def resume(self): + pass + + def progressWindow(self, title, text, total, updpct = 0.05, pulse = False): + return ProgressWindow(title, text, total, updpct, pulse) + + def kickstartErrorWindow(self, text): + raise RuntimeError, ("The following error was found while parsing the " + "kickstart configuration file:\n\n%s" %(text,)) + + def messageWindow(self, title, text, type="ok", default = None, + custom_icon = None, custom_buttons = []): + if type == "ok": + print(text) + else: + print(title) + print(text) + print(type, custom_buttons) + raise RuntimeError, ("Script mode requires all choices to be specified in a kickstart configuration file.") + + def detailedMessageWindow(self, title, text, longText=None, type="ok", + default=None, custom_buttons=None, + custom_icon=None, expanded=False): + if longText: + text += "\n\n%s" % longText + + self.messageWindow(title, text, type=type, default=default, + custom_buttons=custom_buttons, custom_icon=custom_icon) + + def passphraseEntryWindow(self, device): + print("(passphraseEntryWindow: '%s')" % device) + raise RuntimeErrorm, ("Can't have a question in script mode!") + + def getLUKSPassphrase(self, passphrase = "", isglobal = False): + print("(getLUKSPassphrase)") + raise RuntimeError, ("Can't have a question in script mode!") + + def enableNetwork(self): + # Assume we want networking + return True + + def questionInitializeDASD(self, c, devs): + print("questionInitializeDASD") + raise RuntimeError, ("Can't have a question in script mode!") + + def mainExceptionWindow(self, shortText, longTextFile): + print(shortText) + + def waitWindow(self, title, text): + return WaitWindow(title, text) + + def beep(self): + pass + + def run(self, anaconda): + self.anaconda = anaconda + self.anaconda.dispatch.dispatch() + + def display_step(self, step): + if stepToClasses.has_key(step): + s = "nextWin = %s" %(stepToClasses[step],) + exec s + nextWin(self.anaconda) + else: + raise RuntimeError, ("In interactive step %s, can't continue" % (step,)) + + def setInstallProgressClass(self, c): + self.instProgress = c + + def setSteps(self, anaconda): + pass + +class progressDisplay: + def __init__(self): + self.pct = 0 + self.display = "" + + def __del__(self): + pass + + def processEvents(self): + pass + def setShowPercentage(self, val): + pass + def get_fraction(self): + return self.pct + def set_fraction(self, pct): + self.pct = pct + def set_text(self, txt): + print(txt) + def set_label(self, txt): + stripped = strip_markup(txt) + if stripped != self.display: + self.display = stripped + print(self.display) + +def setupProgressDisplay(anaconda): + if anaconda.dir == DISPATCH_BACK: + anaconda.intf.setInstallProgressClass(None) + return DISPATCH_BACK + else: + anaconda.intf.setInstallProgressClass(progressDisplay()) + + return DISPATCH_FORWARD + +def Finished(anaconda): + """ Install is finished. Lets just exit. + """ + return 0 +
Instead of falling into an infinite loop like cmdline raise an error with a useful description of the problem.
I wonder if this could either be merged with cmdline mode, or made so that one is a subclass of the other.
It doesn't seem overly difficult. For the useful description part, perhaps cmdline mode should be doing that anyway. I think our exception handling behavior will stop with the error displayed and wait, which is the same thing we're basically doing in cmdline mode now.
For the network part, a subclass would be easy. Just make whichever is the subclass have the stub like you've got in your patch.
- Chris
Also don't traceback on devices where the lookup fails, print their name instead and continue. --- scripts/anaconda-cleanup | 7 +++++-- 1 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/scripts/anaconda-cleanup b/scripts/anaconda-cleanup index 4af96c5..9375458 100755 --- a/scripts/anaconda-cleanup +++ b/scripts/anaconda-cleanup @@ -78,7 +78,7 @@ for mounted in reversed(open("/proc/mounts").readlines()): # If this is for a live install, unmount any non-nodev filesystem that # isn't related to the live image. if (mountpoint.startswith("/media") or device.startswith("/dev")) and \ - not "live" in mounted: + live_install and not "live" in mounted: os.system("umount %s" % mountpoint)
os.system("udevadm control --env=ANACONDA=1") @@ -89,6 +89,9 @@ devicetree.populate(cleanupOnly=True) devicetree.teardownAll() for name in devicetree.diskImages.keys(): device = devicetree.getDeviceByName(name) - device.deactivate(recursive=True) + if device: + device.deactivate(recursive=True) + else: + print "No device found for (%s)" % (name,) os.system("udevadm control --env=ANACONDA=0")
--- ksimage | 19 +++++++++++++++++-- 1 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/ksimage b/ksimage index f9001fa..4c3335c 100755 --- a/ksimage +++ b/ksimage @@ -242,8 +242,23 @@ if __name__ == "__main__":
try: anaconda.dispatch.run() - except SystemExit, code: + finally: + # Make sure we let go of logs inside /mnt/sysimage + logging.shutdown() + + from pyanaconda.baseudev import udev_trigger, udev_settle + udev_trigger() + udev_settle() + + anaconda.storage.umountFilesystems() + anaconda.storage.devicetree.teardownAll() + for name in anaconda.storage.devicetree.diskImages.keys(): + device = anaconda.storage.devicetree.getDeviceByName(name) + if device: + device.deactivate(recursive=True) + else: + print "No device found for (%s)" % (name,) anaconda.intf.shutdown() - + os.system("udevadm control --env=ANACONDA=0")
# vim:tw=78:ts=4:et:sw=4
If mode is already set to something other than gui there is no need to force it to text mode. --- anaconda | 8 +++++--- 1 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/anaconda b/anaconda index 70dd717..f21ed47 100755 --- a/anaconda +++ b/anaconda @@ -157,6 +157,7 @@ def parseOptions(argv = None): default="g") op.add_option("-G", "--graphical", dest="display_mode", action="store_const", const="g") op.add_option("-T", "--text", dest="display_mode", action="store_const", const="t") + op.add_option("-S", "--script", dest="display_mode", action="store_const", const="s")
# Network op.add_option("--noipv4", action="store_true", default=False) @@ -348,7 +349,7 @@ def check_memory(anaconda, opts, display_mode=None): sys.exit(1)
# override display mode if machine cannot nicely run X - if display_mode not in ('t', 'c') and not flags.usevnc: + if display_mode not in ('t', 'c', 's') and not flags.usevnc: needed_ram += int(isys.GUI_INSTALL_EXTRA_RAM / 1024) reason = reason_graphical
@@ -456,8 +457,9 @@ def setupDisplay(anaconda, opts): # now determine if we're going to run in GUI or TUI mode # # if no X server, we have to use text mode - if not flags.livecdInstall and not iutil.isS390() and \ - not os.access("/usr/bin/Xorg", os.X_OK): + if anaconda.displayMode == 'g' and not flags.livecdInstall \ + and not iutil.isS390() \ + and not os.access("/usr/bin/Xorg", os.X_OK): stdoutLog.warning(_("Graphical installation is not available. " "Starting text mode.")) time.sleep(2)
--- anaconda | 21 +++++++++++++++++++-- 1 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/anaconda b/anaconda index f21ed47..069e867 100755 --- a/anaconda +++ b/anaconda @@ -807,14 +807,31 @@ if __name__ == "__main__": try: anaconda.dispatch.run() except SystemExit, code: - anaconda.intf.shutdown() - if "nokill" in flags.cmdline: + anaconda.intf.shutdown() isys.vtActivate(1) print "anaconda halting due to nokill flag." print "The system will be rebooted when you press Ctrl-Alt-Delete." while True: time.sleep(10000) + finally: + # Make sure we let go of logs inside /mnt/sysimage + logging.shutdown() + + from pyanaconda.baseudev import udev_trigger, udev_settle + udev_trigger() + udev_settle() + + anaconda.storage.umountFilesystems() + anaconda.storage.devicetree.teardownAll() + for name in anaconda.storage.devicetree.diskImages.keys(): + device = anaconda.storage.devicetree.getDeviceByName(name) + if device: + device.deactivate(recursive=True) + else: + print "No device found for (%s)" % (name,) + anaconda.intf.shutdown() + os.system("udevadm control --env=ANACONDA=0")
if anaconda.ksdata: from pykickstart.constants import KS_SHUTDOWN, KS_WAIT, KS_REBOOT
This definitely feels like the sort of thing we always should have been doing. I'm glad to see it. One question...
anaconda.storage.umountFilesystems()
This will attempt to unmount all the special things like /dev/, /proc/, and so forth. Right now these are all being mounted by systemd during bootup. I don't really like going behind its back like this.
- Chris
On Mon, 2011-05-23 at 16:00 -0400, Chris Lumens wrote:
This definitely feels like the sort of thing we always should have been doing. I'm glad to see it. One question...
anaconda.storage.umountFilesystems()
This will attempt to unmount all the special things like /dev/, /proc/, and so forth. Right now these are all being mounted by systemd during bootup. I don't really like going behind its back like this.
It'll only try to unmount the ones under /mnt/sysimage, right? They're just bind mounts.
Dave
- Chris
Anaconda-devel-list mailing list Anaconda-devel-list@redhat.com https://www.redhat.com/mailman/listinfo/anaconda-devel-list
On Mon, May 23, 2011 at 03:11:25PM -0500, David Lehman wrote:
On Mon, 2011-05-23 at 16:00 -0400, Chris Lumens wrote:
This definitely feels like the sort of thing we always should have been doing. I'm glad to see it. One question...
anaconda.storage.umountFilesystems()
This will attempt to unmount all the special things like /dev/, /proc/, and so forth. Right now these are all being mounted by systemd during bootup. I don't really like going behind its back like this.
It'll only try to unmount the ones under /mnt/sysimage, right? They're just bind mounts.
Correct, that umounts everything in FSSet that was bind mounted to /mnt/sysimage so that the teardown can umount /mnt/sysimage and the image file loop device.
anaconda-devel@lists.stg.fedoraproject.org