This will eventually replace backend.py, livecd.py, and yuminstall.py. --- pyanaconda/constants.py | 4 + pyanaconda/errors.py | 63 +++- pyanaconda/image.py | 135 +++--- pyanaconda/packaging.py | 1225 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1356 insertions(+), 71 deletions(-) create mode 100644 pyanaconda/packaging.py diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py index 4167f96..589c25f 100644 --- a/pyanaconda/constants.py +++ b/pyanaconda/constants.py @@ -87,3 +87,7 @@ relabelDirs = ["/etc/sysconfig/network-scripts", "/var/lib/rpm", "/var/lib/yum" ANACONDA_CLEANUP = "anaconda-cleanup" ROOT_PATH = "/mnt/sysimage" +ISO_DIR = "/mnt/install/isodir" +INSTALL_TREE = "/mnt/install/source" +BASE_REPO_NAME = "Installation Repo" + diff --git a/pyanaconda/errors.py b/pyanaconda/errors.py index 682c3bb..ea5d20f 100644 --- a/pyanaconda/errors.py +++ b/pyanaconda/errors.py @@ -23,8 +23,22 @@ _ = lambda x: gettext.ldgettext("anaconda", x) __all__ = ["ERROR_RAISE", "ERROR_CONTINUE", "ERROR_RETRY", "ErrorHandler", + "InvalidImageSizeError", "MissingImageError", "MediaUnmountError", + "MediaMountError", "errorHandler"] +class InvalidImageSizeError(Exception): + pass + +class MissingImageError(Exception): + pass + +class MediaMountError(Exception): + pass + +class MediaUnmountError(Exception): + pass + import pyanaconda.storage.errors as StorageError """These constants are returned by the callback in the ErrorHandler class. @@ -97,6 +111,48 @@ class ErrorHandler(object): message += " " + str(kwargs["exception"]) self.ui.showError(message) + def _invalidImageSizeHandler(self, *args, **kwargs): + filename = args[0] + message = _("The ISO image %s has a size which is not " + "a multiple of 2048 bytes. This may mean " + "it was corrupted on transfer to this computer." + "\n\n" + "It is recommended that you exit and abort your " + "installation, but you can choose to continue if " + "you think this is in error. Would you like to " + "continue using this image?") % filename + if self.ui.showYesNoQuestion(message): + return ERROR_CONTINUE + else: + return ERROR_RAISE + + def _missingImageHandler(self, *args, **kwargs): + message = _("The installer has tried to mount the " + "installation image, but cannot find it on " + "the hard drive.\n\n" + "Should I try again to locate the image?") + if self.ui.showYesNoQuestion(message): + return ERROR_RETRY + else: + return ERROR_RAISE + + def _mediaMountHandler(self, *args, **kwargs): + device = args[0] + message = _("An error occurred mounting the source " + "device %s. Retry?") % device.name + if self.ui.showYesNoQuestion(message): + return ERROR_RETRY + else: + return ERROR_RAISE + + def mediaUnmountHandler(self, *args, **kwargs): + device = args[0] + message = _("An error occurred unmounting the disc. " + "Please make sure you're not accessing " + "%s from the shell on tty2 " + "and then click OK to retry.") % device.path + self.ui.showError(message) + def cb(self, exn, *args, **kwargs): """This method is the callback that all error handling should pass through. The return value is one of the ERROR_* constants defined @@ -117,7 +173,12 @@ class ErrorHandler(object): _map = {StorageError.NoDisksError: self._noDisksHandler, StorageError.DirtyFSError: self._dirtyFSHandler, - StorageError.FSTabTypeMismatchError: self._fstabTypeMismatchHandler} + StorageError.FSTabTypeMismatchError: self._fstabTypeMismatchHandler, + InvalidImageSizeError: self._invalidImageSizeHandler, + MissingImageError: self._missingImageHandler, + MediaMountError: self._mediaMountError, + MediaUnmountError: self._mediaUnmountError} + if exn in _map: kwargs["exception"] = exn rc = _map[exn](*args, **kwargs) diff --git a/pyanaconda/image.py b/pyanaconda/image.py index 200645a..8881a26 100644 --- a/pyanaconda/image.py +++ b/pyanaconda/image.py @@ -21,6 +21,8 @@ import isys, iutil import os, os.path, stat, sys from constants import * +from errors import * + import gettext _ = lambda x: gettext.ldgettext("anaconda", x) @@ -29,12 +31,12 @@ log = logging.getLogger("anaconda") _arch = iutil.getArch() -def findFirstIsoImage(path, messageWindow): +def findFirstIsoImage(path): """ Find the first iso image in path This also supports specifying a specific .iso image - Returns the full path to the image + Returns the basename of the image """ flush = os.stat(path) arch = _arch @@ -84,23 +86,14 @@ def findFirstIsoImage(path, messageWindow): # warn user if images appears to be wrong size if os.stat(what)[stat.ST_SIZE] % 2048: - rc = messageWindow(_("Warning"), - _("The ISO image %s has a size which is not " - "a multiple of 2048 bytes. This may mean " - "it was corrupted on transfer to this computer." - "\n\n" - "It is recommended that you exit and abort your " - "installation, but you can choose to continue if " - "you think this is in error.") % (fn,), - type="custom", custom_icon="warning", - custom_buttons= [_("_Exit installer"), - _("_Continue")]) - if rc == 0: - sys.exit(0) + log.warning("%s appears to be corrupted" % what) + exn = InvalidImageSizeError("size is not a multiple of 2048 bytes") + if errorHandler(exn) == ERROR_RAISE: + raise exn log.info("Found disc at %s" % fn) isys.umount("/mnt/install/cdimage", removeDir=False) - return what + return fn return None @@ -114,54 +107,50 @@ def getMediaId(path): else: return None -# This mounts the directory containing the iso images, and places the -# mount point in /mnt/install/isodir. -def mountDirectory(methodstr, messageWindow): +# This mounts the directory containing the iso images on ISO_DIR. +def mountImageDirectory(method, storage): # No need to mount it again. - if os.path.ismount("/mnt/install/isodir"): + if os.path.ismount(ISO_DIR): return - if methodstr.startswith("hd:"): - method = methodstr[3:] - options = '' - if method.count(":") == 1: - (device, path) = method.split(":") - fstype = "auto" + if method.method == "harddrive": + if method.biospart: + log.warning("biospart support is not implemented") + devspec = method.biospart else: - (device, fstype, path) = method.split(":") - - if not device.startswith("/dev/") and not device.startswith("UUID=") \ - and not device.startswith("LABEL="): - device = "/dev/%s" % device + devspec = method.partition + + # FIXME: teach DeviceTree.resolveDevice about biospart + device = storage.devicetree.resolveDevice(devspec) + + while True: + try: + device.setup() + device.format.setup(mountpoint=ISO_DIR) + except StorageError as e: + log.error("couldn't mount ISO source directory: %s" % e) + exn = MediaMountError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn elif methodstr.startswith("nfsiso:"): - (options, host, path) = iutil.parseNfsUrl(methodstr) - if path.endswith(".iso"): - path = os.path.dirname(path) - device = "%s:%s" % (host, path) - fstype = "nfs" - else: - return + # XXX what if we mount it on ISO_DIR and then create a symlink + # if there are no isos instead of the remount? - while True: - try: - isys.mount(device, "/mnt/install/isodir", fstype=fstype, options=options) - break - except SystemError as msg: - log.error("couldn't mount ISO source directory: %s" % msg) - ans = messageWindow(_("Couldn't Mount ISO Source"), - _("An error occurred mounting the source " - "device %s. This may happen if your ISO " - "images are located on an advanced storage " - "device like LVM or RAID, or if there was a " - "problem mounting a partition. Click exit " - "to abort the installation.") - % (device,), type="custom", custom_icon="error", - custom_buttons=[_("_Exit"), _("_Retry")]) - - if ans == 0: - sys.exit(0) - else: - continue + # mount the specified directory + path = method.dir + if method.dir.endswith(".iso"): + path = os.path.dirname(method.dir) + + url = "%s:%s" % (method.server, path) + + while True: + try: + isys.mount(url, ISO_DIR, options=method.options) + except SystemError as e: + log.error("couldn't mount ISO source directory: %s" % e) + exn = MediaMountError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn def mountImage(isodir, tree, messageWindow): def complain(): @@ -183,14 +172,23 @@ def mountImage(isodir, tree, messageWindow): while True: image = findFirstIsoImage(isodir, messageWindow) if image is None: - complain() - continue + exn = MissingImageError() + if errorHandler(exn) == ERROR_RAISE: + raise exn + else: + continue + image = os.path.normpath("%s/%s" % (isodir, image)) try: isys.mount(image, tree, fstype = 'iso9660', readOnly = True) - break except SystemError: - complain() + exn = MissingImageError() + if errorHandler(exn) == ERROR_RAISE: + raise exn + else: + continue + else: + break # Return a list of Device instances containing valid optical install media # for this product. @@ -221,22 +219,19 @@ def umountImage(tree): if os.path.ismount(tree): isys.umount(tree, removeDir=False) -def unmountCD(dev, messageWindow): +def unmountCD(dev): if not dev: return while True: try: dev.format.unmount() - break except Exception as e: log.error("exception in _unmountCD: %s" %(e,)) - messageWindow(_("Error"), - _("An error occurred unmounting the disc. " - "Please make sure you're not accessing " - "%s from the shell on tty2 " - "and then click OK to retry.") - % (dev.path,)) + exn = MediaUnmountError() + errorHandler(exn, dev) + else: + break def verifyMedia(tree, timestamp=None): if os.access("%s/.discinfo" % tree, os.R_OK): diff --git a/pyanaconda/packaging.py b/pyanaconda/packaging.py new file mode 100644 index 0000000..97f37ac --- /dev/null +++ b/pyanaconda/packaging.py @@ -0,0 +1,1225 @@ +#!/usr/bin/python + +""" + TODO + - error handling!!! + - document all methods + - YumPayload + - preupgrade + - rpm macros + - __file_context_path + - _excludedocs + - handling of proxy needs cleanup + - passed to anaconda as --proxy, --proxyUsername, and + --proxyPassword + - drop the use of a file for proxy and ftp auth info + - specified via KS as a URL + - LiveImagePayload + - register the live image, either via self.data.method or in setup + using storage + +""" + +from urlgrabber.grabber import URLGrabber +from urlgrabber.grabber import URLGrabError +import ConfigParser +import shutil + +from pyanaconda import anaconda_log +anaconda_log.init() + +try: + import tarfile +except ImportError: + log.error("import of tarfile failed") + tarfile = None + +try: + import rpm +except ImportError: + log.error("import of rpm failed") + rpm = None + +try: + import yum +except ImportError: + log.error("import of yum failed") + yum = None + +from pyanaconda.constants import * +from pyanaconda.flags import flags + +from pyanaconda import iutil +from pyanaconda.network import hasActiveNetDev + +from pyanaconda.image import opticalInstallMedia +from pyanaconda.image import mountImage +from pyanaconda.image import findFirstIsoImage + +from pykickstart.parser import Group +from pykickstart.version import makeVersion + +import logging +log = logging.getLogger("anaconda") + +from pyanaconda.backend_log import log as instlog + +from pyanaconda.errors import * +#from pyanaconda.progress import progress + +### +### ERROR HANDLING +### +class PayloadError(Exception): + pass + +class MetadataError(PayloadError): + pass + +class NoNetworkError(PayloadError): + pass + +# setup +class PayloadSetupError(PayloadError): + pass + +class ImageMissingError(PayloadSetupError): + pass + +class ImageDirectoryMountError(PayloadSetupError): + pass + +# software selection +class NoSuchGroup(PayloadError): + pass + +class NoSuchPackage(PayloadError): + pass + +class DependencyError(PayloadError): + pass + +# installation +class PayloadInstallError(PayloadError): + pass + + +class Payload(object): + """ Payload is an abstract class for OS install delivery methods. """ + def __init__(self, data): + self.data = data + + def setup(self, storage): + """ Do any payload-specific setup. """ + raise NotImplementedError() + + ### + ### METHODS FOR WORKING WITH REPOSITORIES + ### + @property + def repos(self): + """Return a list of repo identifiers, not objects themselves.""" + raise NotImplementedError() + + def addRepo(self, newrepo): + """Add the repo given by the pykickstart Repo object newrepo to the + system. The repo will be automatically enabled and its metadata + fetched. + + Duplicate repos will not raise an error. They should just silently + take the place of the previous value. + """ + # Add the repo to the ksdata so it'll appear in the output ks file. + self.data.repo.dataList().append(newrepo) + + def removeRepo(self, repo_id): + repos = self.data.repo.dataList() + try: + idx = [repo.name for repo in repos].index(repo_id) + except ValueError: + log.error("failed to remove repo %s: not found" % repo_id) + else: + repos.pop(idx) + + def enableRepo(self, repo_id): + raise NotImplementedError() + + def disableRepo(self, repo_id): + raise NotImplementedError() + + ### + ### METHODS FOR WORKING WITH GROUPS + ### + @property + def groups(self): + raise NotImplementedError() + + def description(self, groupid): + raise NotImplementedError() + + def selectGroup(self, groupid, default=True, optional=False): + if optional: + include = GROUP_ALL + elif default: + include = GROUP_DEFAULT + else: + include = GROUP_REQUIRED + + grp = Group(groupid, include=include) + + if grp in self.data.packages.groupList: + # I'm not sure this would ever happen, but ensure that re-selecting + # a group with a different types set works as expected. + if grp.include != include: + grp.include = include + + return + + if grp in self.data.packages.excludedGroupList: + self.data.packages.excludedGroupList.remove(grp) + + self.data.packages.groupList.append(grp) + + def deselectGroup(self, groupid): + grp = Group(groupid) + + if grp in self.data.packages.excludedGroupList: + return + + if grp in self.data.packages.groupList: + self.data.packages.groupList.remove(grp) + + self.data.packages.excludedGroupList.append(grp) + + ### + ### METHODS FOR WORKING WITH PACKAGES + ### + @property + def packages(self): + raise NotImplementedError() + + def selectPackage(self, pkgid): + """Mark a package for installation. + + pkgid - The name of a package to be installed. This could include + a version or architecture component. + """ + if pkgid in self.data.packages.packageList: + return + + if pkgid in self.data.packages.excludedList: + self.data.packages.excludedList.remove(pkgid) + + self.data.packages.packageList.append(pkgid) + + def deselectPackage(self, pkgid): + """Mark a package to be excluded from installation. + + pkgid - The name of a package to be excluded. This could include + a version or architecture component. + """ + if pkgid in self.data.packages.excludedList: + return + + if pkgid in self.data.packages.packageList: + self.data.packages.packageList.remove(pkgid) + + self.data.packages.excludedList.append(pkgid) + + ### + ### METHODS FOR QUERYING STATE + ### + @property + def spaceRequired(self): + raise NotImplementedError() + + @property + def kernelVersionList(self): + raise NotImplementedError() + + ## + ## METHODS FOR TREE VERIFICATION + ## + def _getTreeInfo(self, url, sslverify, proxies): + """ Retrieve treeinfo and return the path to the local file. """ + if not url: + return None + + log.debug("retrieving treeinfo from %s (proxies: %s ; sslverify: %s" + % (url, proxies, sslverify)) + + ugopts = {"ssl_verify_peer": sslverify, + "ssl_verify_host": sslverify} + + ug = URLGrabber() + try: + treeinfo = ug.urlgrab("%s/.treeinfo" % url, + "/tmp/.treeinfo", copy_local=True, + proxies=proxies, **ugopts) + except URLGrabError as e: + try: + treeinfo = ug.urlgrab("%s/treeinfo" % url, + "/tmp/.treeinfo", copy_local=True, + proxies=proxies, **ugopts) + except URLGrabError as e: + log.info("Error downloading treeinfo: %s" % e) + treeinfo = None + + return treeinfo + + def _getReleaseVersion(self, url): + """ Return the release version of the tree at the specified URL. """ + version = productVersion.split("-")[0] + + log.debug("getting release version from tree at %s (%s)" % (url, + version)) + + proxies = {} + if self.proxy: + proxies = {"http": self.proxy, + "https": self.proxy} + + treeinfo = self._getTreeInfo(url, not flags.noverifyssl, proxies) + if treeinfo: + c = ConfigParser.ConfigParser() + c.read(treeinfo) + try: + # Trim off any -Alpha or -Beta + version = c.get("general", "version").split("-")[0] + except ConfigParser.Error: + pass + + log.debug("got a release version of %s" % version) + return version + + ## + ## METHODS FOR MEDIA MANAGEMENT (XXX should these go in another module?) + ## + def _setupDevice(self, device, mountpoint): + """ Prepare an install CD/DVD for use as a package source. """ + log.info("setting up device %s and mounting on %s" % (device.name, + mountpoint)) + if os.path.ismount(mountpoint): + log.debug("%s already has something mounted on it" % mountpoint) + return + + try: + device.setup() + device.format.setup(mountpoint=mountpoint) + except StorageError as e: + exn = PayloadSetupError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn + + def _setupNFS(self, mountpoint, server, path, options): + """ Prepare an NFS directory for use as a package source. """ + log.info("mounting %s:%s:%s on %s" % (server, path, options, mountpoint)) + if os.path.ismount(mountpoint): + log.debug("%s already has something mounted on it" % mountpoint) + return + + # mount the specified directory + url = "%s:%s" % (server, path) + + try: + isys.mount(url, mountpoint, options=options) + except SystemError as e: + exn = PayloadSetupError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn + + + ### + ### METHODS FOR INSTALLING THE PAYLOAD + ### + def preInstall(self): + """ Perform pre-installation tasks. """ + # XXX this should be handled already + iutil.mkdirChain(ROOT_PATH + "/root") + + if self.data.upgrade.upgrade: + mode = "upgrade" + else: + mode = "install" + + log_file_name = "%s.log" % mode + log_file_path = "%s/root/%s" % (ROOT_PATH, log_file_name) + try: + shutil.rmtree (log_file_path) + except OSError: + pass + + self.install_log = open(log_file_path, "w+") + + syslogname = "%s%s.syslog" % log_file_path + try: + shutil.rmtree (syslogname) + except OSError: + pass + instlog.start(ROOT_PATH, syslogname) + + def install(self): + """ Install the payload. """ + raise NotImplementedError() + + def postInstall(self): + """ Perform post-installation tasks. """ + pass + + # set default runlevel/target (?) + # write out static config (storage, modprobe, keyboard, ??) + # kickstart should handle this before we get here + # copy firmware + # recreate initrd + # postInstall or bootloader.install + # copy dd rpms (yum/rpm only?) + # kickstart + # copy dd modules and firmware (yum/rpm only?) + # kickstart + # write escrow packets + # stop logger + +class ImagePayload(Payload): + """ An ImagePayload installs an OS image to the target system. """ + def __init__(self, data): + super(ImagePayload, self).__init__(data) + self.image_file = None + + def setup(self, storage): + if not self.image_file: + exn = PayloadSetupError("image file not set") + if errorHandler(exn) == ERROR_RAISE: + raise exn + +class LiveImagePayload(ImagePayload): + """ A LivePayload copies the source image onto the target system. """ + def setup(self, storage): + super(LiveImagePayload, self).setup() + if not stat.S_ISBLK(os.stat(self.image_file)[stat.ST_MODE]): + raise PayloadSetupError("unable to find image") + + def install(self): + """ Install the payload. """ + cmd = "rsync" + args = ["-rlptgoDHAXv", self.os_image, ROOT_PATH] + try: + rc = iutil.execWithRedirect(cmd, args, + stderr="/dev/tty5", stdout="/dev/tty5") + except (OSError, RuntimeError) as e: + err = str(e) + else: + err = None + if rc != 0: + err = "%s exited with code %d" % (cmd, rc) + + if err: + exn = PayloadInstallError(err) + if errorHandler(exn) == ERROR_RAISE: + raise exn + + +class ArchivePayload(ImagePayload): + """ An ArchivePayload unpacks source archives onto the target system. """ + pass + +class TarPayload(ArchivePayload): + """ A TarPayload unpacks tar archives onto the target system. """ + def __init__(self, data): + if tarfile is None: + raise PayloadError("unsupported payload type") + + super(TarPayload, self).__init__(data) + self.archive = None + + def setup(self, storage): + super(TarPayload, self).setup() + + try: + self.archive = tarfile.open(self.image_file) + except (tarfile.ReadError, tarfile.CompressionError) as e: + # maybe we only need to catch ReadError and CompressionError here + log.error("opening tar archive %s: %s" % (self.image_file, e)) + raise PayloadError("invalid payload format") + + @property + def requiredSpace(self): + byte_count = sum([m.size for m in self.archive.getmembers()]) + return byte_count / (1024.0 * 1024.0) # FIXME: Size + + @property + def kernelVersionList(self): + names = self.archive.getnames() + kernels = [n for n in names if "boot/vmlinuz-" in n] + + def install(self): + try: + selfarchive.extractall(path=ROOT_PATH) + except (tarfile.ExtractError, tarfile.CompressionError) as e: + log.error("extracting tar archive %s: %s" % (self.image_file, e)) + +class PackagePayload(Payload): + """ A PackagePayload installs a set of packages onto the target system. """ + pass + +class YumPayload(PackagePayload): + """ A YumPayload installs packages onto the target system using yum. """ + def __init__(self, data): + if rpm is None or yum is None: + raise PayloadError("unsupported payload type") + + PackagePayload.__init__(self, data) + + self._groups = [] + self._packages = [] + + self.install_device = None + self.proxy = None # global proxy + + self._yum = yum.YumBase() + + # Set some configuration parameters that don't get set through a config + # file. yum will know what to do with these. + # XXX We have to try to set releasever before we trigger a read of the + # repo config files. We do that from setup before adding any repos. + self._yum.preconf.enabled_plugins = ["blacklist", "whiteout"] + self._yum.preconf.fn = "/tmp/anaconda-yum.conf" + self._yum.preconf.root = ROOT_PATH + + def setup(self, storage, proxy=None): + buf = """ +[main] +installroot=%s +cachedir=/tmp/cache/yum +keepcache=0 +logfile=/tmp/yum.log +metadata_expire=never +pluginpath=/usr/lib/yum-plugins,/tmp/updates/yum-plugins +pluginconfpath=/etc/yum/pluginconf.d,/tmp/updates/pluginconf.d +plugins=1 +reposdir=/etc/yum.repos.d,/etc/anaconda.repos.d,/tmp/updates/anaconda.repos.d,/tmp/product/anaconda.repos.d +""" % ROOT_PATH + + if proxy: + # FIXME: include proxy_username, proxy_password + buf += "proxy=%s" % proxy + + fd = open("/tmp/anaconda-yum.conf", "w") + fd.write(buf) + fd.close() + + self.proxy = proxy + self._configureMethod(storage) + self._configureRepos(storage) + if flags.testing: + self._yum.setCacheDir() + + ### + ### METHODS FOR WORKING WITH REPOSITORIES + ### + @property + def repos(self): + # FIXME: should this return pykickstart Repo or YumRepo? + return self._yum.repos.repos.keys() + + def _repoNeedsNetwork(self, repo): + """ Returns True if the ksdata repo requires networking. """ + urls = [repo.baseurl] + repo.mirrorlist + network_protocols = ["http:", "ftp:", "nfs:", "nfsiso:"] + for url in urls: + if any([url.startswith(p) for p in network_protocols]): + return True + + return False + + def _configureRepos(self, storage): + """ Configure the initial repository set. """ + log.info("configuring repos") + # FIXME: driverdisk support + + # add/enable the repos anaconda knows about + # identify repos based on ksdata. + for repo in self.data.repo.dataList(): + self._configureKSRepo(storage, repo) + + # remove/disable repos that don't make sense during system install. + # If a method was given, disable any repos that aren't in ksdata. + for repo in self._yum.repos.repos.values(): + if "-source" in repo.id or "-debuginfo" in repo.id: + log.info("excluding source or debug repo %s" % repo.id) + self.removeRepo(repo.id) + elif isFinal and ("rawhide" in repo.id or "development" in repo.id): + log.info("excluding devel repo %s for non-devel anaconda" % repo.id) + self.removeRepo(repo.id) + elif not isFinal and not repo.enabled: + log.info("excluding disabled repo %s for prerelease" % repo.id) + self.removeRepo(repo.id) + elif self.data.method.method and repo.id not in self.repos: + log.info("excluding repo %s" % repo.id) + self._yum.disableRepo(repo.id) + + def _configureMethod(self, storage): + """ Configure the base repo. """ + log.info("configuring base repo") + # set up the main repo specified by method=, repo=, or ks method + # XXX FIXME: does this need to handle whatever was set up by dracut? + # XXX FIXME: most of this probably belongs up in Payload + method = self.data.method + sslverify = True + url = None + + if method.method == "cdrom": + devices = opticalInstallMedia(storage.devicetree) + if devices: + self._setupDevice(devices[0], mountpoint=INSTALL_TREE) + self.install_device = devices[0] + url = "file://" + INSTALL_TREE + elif method.method == "harddrive": + if method.biospart: + log.warning("biospart support is not implemented") + devspec = method.biospart + else: + devspec = method.partition + + # FIXME: teach DeviceTree.resolveDevice about biospart + device = storage.devicetree.resolveDevice(devspec) + self._setupDevice(device, mountpoint=ISO_DIR) + + # check for ISO images in the newly mounted dir + path = ISO_DIR + if method.dir: + path = os.path.normpath("%s/%s" % (path, method.dir)) + + image = findFirstIsoImage(path) + if not image: + exn = PayloadSetupError("failed to find valid iso image") + if errorHandler(exn) == ERROR_RAISE: + raise exn + + if path.endswith(".iso"): + path = os.path.dirname(path) + + # mount the ISO on a loop + image = os.path.normpath("%s/%s" % (path, image)) + mountImage(image, INSTALL_TREE) + + self.install_device = device + url = "file://" + INSTALL_TREE + elif method.method == "nfs": + # XXX what if we mount it on ISO_DIR and then create a symlink + # if there are no isos instead of the remount? + self._setupNFS(INSTALL_TREE, method.server, method.dir, + method.opts) + + # check for ISO images in the newly mounted dir + path = ISO_DIR + if method.dir.endswith(".iso"): + # if the given URL includes a specific ISO image file, use it + image_file = os.path.basename(method.dir) + path = os.path.normpath("%s/%s" % (path, image_file)) + + image = findFirstIsoImage(path) + + # it appears there are ISO images in the dir, so assume they want to + # install from one of them + if image: + isys.umount(INSTALL_TREE) + self._setupNFS(ISO_DIR, method.server, method.path, + method.options) + + # mount the ISO on a loop + image = os.path.normpath("%s/%s" % (ISO_DIR, image)) + mountImage(image, INSTALL_TREE) + + url = "file://" + INSTALL_TREE + elif method.method == "url": + url = method.url + sslverify = not (method.noverifyssl or flags.noverifyssl) + proxy = method.proxy or self.proxy + + self._yum.preconf.releasever = self._getReleaseVersion(url) + + if method.method: + # FIXME: handle MetadataError + self._addYumRepo(BASE_REPO_NAME, url, + proxy=proxy, sslverify=sslverify) + + def _configureKSRepo(self, storage, repo): + """ Configure a single ksdata repo. """ + url = getattr(repo, "baseurl", repo.mirrorlist) + if url.startswith("nfs:"): + # FIXME: create a directory other than INSTALL_TREE based on + # the repo's id/name to avoid crashes if the base repo is NFS + (opts, server, path) = iutil.parseNfsUrl(url) + self._setupNFS(INSTALL_TREE, server, path, opts) + else: + # check for media, fall back to default repo + devices = opticalInstallMedia(storage.devicetree) + if devices: + self._setupDevice(devices[0], mountpoint=INSTALL_TREE) + self.install_device = devices[0] + + if self._repoNeedsNetwork(repo) and not hasActiveNetDev(): + raise NoNetworkError + + proxy = repo.proxy or self.proxy + sslverify = not (flags.noverifyssl or repo.noverifyssl) + + # this repo does not go into ksdata -- only yum + self.addYumRepo(repo.id, repo.baseurl, repo.mirrorlist, cost=repo.cost, + exclude=repo.excludepkgs, includepkgs=repo.includepkgs, + proxy=proxy, sslverify=sslverify) + + # TODO: enable addons + + def _addYumRepo(self, name, baseurl, mirrorlist=None, **kwargs): + """ Add a yum repo to the YumBase instance. """ + from yum.Errors import RepoError, RepoMDError + + # First, delete any pre-existing repo with the same name. + if name in self._yum.repos.repos: + self._yum.repos.delete(name) + + # Replace anything other than HTTP/FTP with file:// + if baseurl and \ + not baseurl.startswith("http:") and \ + not baseurl.startswith("ftp:"): + baseurl = "file:/" + INSTALL_TREE + + log.debug("adding yum repo %s with baseurl %s and mirrorlist %s" + % (name, baseurl, mirrorlist)) + # Then add it to yum's internal structures. + obj = self._yum.add_enable_repo(name, + baseurl=[baseurl], + mirrorlist=mirrorlist, + **kwargs) + + # And try to grab its metadata. We do this here so it can be done + # on a per-repo basis, so we can then get some finer grained error + # handling and recovery. + try: + obj.getPrimaryXML() + obj.getOtherXML() + except RepoError as e: + raise MetadataError(e.value) + + # Not getting group info is bad, but doesn't seem like a fatal error. + # At the worst, it just means the groups won't be displayed in the UI + # which isn't too bad, because you may be doing a kickstart install and + # picking packages instead. + try: + obj.getGroups() + except RepoMDError: + log.error("failed to get groups for repo %s" % repo.id) + + # Adding a new repo means the cached packages and groups lists + # are out of date. Clear them out now so the next reference to + # either will cause it to be regenerated. + self._groups = [] + self._packages = [] + + def addRepo(self, newrepo): + """ Add a ksdata repo. """ + log.debug("adding new repo %s" % newrepo.name) + self._addYumRepo(newrepo) # FIXME: handle MetadataError + super(YumRepo, self).addRepo(newrepo) + + def removeRepo(self, repo_id): + """ Remove a repo as specified by id. """ + log.debug("removing repo %s" % repo_id) + if repo_id in self.repos: + self._yum.repos.delete(repo_id) + + super(YumPayload, self).removeRepo(repo_id) + + def enableRepo(self, repo_id): + """ Enable a repo as specified by id. """ + log.debug("enabling repo %s" % repo_id) + if repo_id in self.repos: + self._yum.repos.enableRepo(repo_id) + + def disableRepo(self, repo_id): + """ Disable a repo as specified by id. """ + log.debug("disabling repo %s" % repo_id) + if repo_id in self.repos: + self._yum.repos.disableRepo(repo_id) + + ### + ### METHODS FOR WORKING WITH GROUPS + ### + @property + def groups(self): + from yum.Errors import RepoError + + if not self._groups: + if not hasActiveNetDev(): + raise NoNetworkError + + try: + self._groups = self._yum.comps + except RepoError as e: + raise MetadataError(e.value) + + return [g.groupid for g in self._groups.get_groups()] + + def description(self, groupid): + """ Return name/description tuple for the group specified by id. """ + if not self._groups.has_group(groupid): + raise NoSuchGroup(groupid) + + group = self._groups.return_group(groupid) + + return (group.ui_name, group.ui_description) + + def selectGroup(self, groupid, default=True, optional=False): + super(YumPayload, self).selectGroup(groupid, default=default, + optional=optional) + # select the group in comps + pkg_types = ['mandatory'] + if default: + pkg_types.append("default") + + if optional: + pkg_types.append("optional") + + log.debug("select group %s" % groupid) + try: + self._yum.selectGroup(groupid, group_package_types=pkg_types) + except yum.Errors.GroupsError: + log.error("no such group: %s" % groupid) + + def deselectGroup(self, groupid): + super(YumPayload, self).deselectGroup(groupid) + # deselect the group in comps + log.debug("deselect group %s" % groupid) + try: + self._yum.deselectGroup(groupid, force=True) + except yum.Errors.GroupsError: + log.error("no such group: %s" % groupid) + + ### + ### METHODS FOR WORKING WITH PACKAGES + ### + @property + def packages(self): + from yum.Errors import RepoError + + if not self._packages: + if not hasActiveNetDev(): + raise NoNetworkError + + try: + self._packages = self._yum.pkgSack.returnPackages() + except RepoError as e: + raise MetadataError(e.value) + + return self._packages + + def selectPackage(self, pkgid): + """Mark a package for installation. + + pkgid - The name of a package to be installed. This could include + a version or architecture component. + """ + super(YumPayload, self).selectPackage(pkgid) + log.debug("select package %s" % pkgid) + try: + mbrs = self._yum.install(pattern=pkgid) + except yum.Errors.InstallError: + log.error("no package matching %s" % pkgid) + + def deselectPackage(self, pkgid): + """Mark a package to be excluded from installation. + + pkgid - The name of a package to be excluded. This could include + a version or architecture component. + """ + super(YumPayload, self).deselectPackage(pkgid) + log.debug("deselect package %s" % pkgid) + self._yum.tsInfo.deselect(pkgid) + + ### + ### METHODS FOR INSTALLING THE PAYLOAD + ### + def _removeTxSaveFile(self): + # remove the transaction save file + if self._yum._ts_save_file: + try: + os.unlink(self._yum._ts_save_file) + except (OSError, IOError): + pass + else: + self._yum._ts_save_file = None + + def checkSoftwareSelection(self): + log.info("checking software selection") + + self._yum._undoDepInstalls() + + # doPostSelection + # select kernel packages + # select packages needed for storage, bootloader + + # check dependencies + # XXX FIXME: set self._yum.dsCallback before entering this loop? + while True: + log.info("checking dependencies") + (code, msgs) = self._yum.buildTransaction(unfinished_transactions_check=False) + + if code == 0: + # empty transaction? + log.debug("empty transaction") + break + elif code == 2: + # success + log.debug("success") + break + elif self.data.packages.handleMissing == KS_MISSING_IGNORE: + log.debug("ignoring missing due to ks config") + break + elif self.data.upgrade.upgrade: + log.debug("ignoring unresolved deps on upgrade") + break + + for msg in msgs: + log.warning(msg) + + exn = DependencyError(msgs) + rc = errorHandler(exn) + if rc == ERROR_RAISE: + raise exn + elif rc == ERROR_RETRY: + # FIXME: figure out how to allow modification of software set + self._yum._undoDepInstalls() + return False + elif rc == ERROR_CONTINUE: + break + + # check free space (?) + + self._removeTxSaveFile() + + def preInstall(self): + """ Perform pre-installation tasks. """ + super(YumPayload, self).preInstall() + + # doPreInstall + # create a bunch of directories like /var, /var/lib/rpm, /root, &c (?) + # create mountpoints for protected device mountpoints (?) + # initialize the backend logger + # write static configs (storage, modprobe.d/anaconda.conf, network, keyboard) + # on upgrade, just make sure /etc/mtab is a symlink to /proc/self/mounts + + def install(self): + """ Install the payload. """ + log.info("preparing transaction") + log.debug("initialize transaction set") + self._yum.initActionTs() + + log.debug("populate transaction set") + try: + # uses dsCallback.transactionPopulation + self._yum.populateTs(keepold=0) + except RepoError as e: + log.error("error populating transaction: %s" % e) + exn = PayloadInstallError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn + + log.debug("check transaction set") + self._yum.ts.check() + log.debug("order transaction set") + self._yum.ts.order() + + # set up rpm logging to go to our log + self._yum.ts.ts.scriptFd = self.install_log.fileno() + rpm.setLogFile(self.install_log) + + # create the install callback + rpmcb = RPMCallback(self._yum, self.install_log, + upgrade=self.data.upgrade.upgrade) + + if flags.testing: + #self._yum.ts.setFlags(rpm.RPMTRANS_FLAG_TEST) + return + + log.info("running transaction") + try: + self._yum.runTransaction(cb=rpmcb) + except PackageSackError as e: + log.error("error running transaction: %s" % e) + exn = PayloadInstallError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn + except YumRPMTransError as e: + log.error("error running transaction: %s" % e) + for error in e.errors: + log.error(e[0]) + exn = PayloadInstallError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn + except YumBaseError as e: + log.error("error [2] running transaction: %s" % e) + for error in e.errors: + log.error("%s" % e[0]) + exn = PayloadInstallError(str(e)) + if errorHandler(exn) == ERROR_RAISE: + raise exn + finally: + self._yum.ts.close() + iutil.resetRpmDb() + + def postInstall(self): + """ Perform post-installation tasks. """ + self._yum.close() + + # clean up repo tmpdirs + self._yum.cleanPackages() + self._yum.cleanHeaders() + + # remove cache dirs of install-specific repos + for repo in self._yum.repos.listEnabled(): + if repo.name == BASE_REPO_NAME or repo.id.startswith("anaconda-"): + shutil.rmtree(repo.cachedir) + + # clean the yum cache on upgrade + if self.data.upgrade.upgrade: + self._yum.cleanMetadata() + + # TODO: on preupgrade, remove the preupgrade dir + + self._removeTxSaveFile() + +class RPMCallback(object): + def __init__(self, yb, log, upgrade): + self._yum = yb # yum.YumBase + self.install_log = log # file instance + self.upgrade = upgrade # boolean + + self.package_file = None # file instance (package file management) + + def _get_txmbr(self, key): + """ Return a (name, TransactionMember) tuple from cb key. """ + if hasattr(key, "po"): + # New-style callback, key is a TransactionMember + txmbr = key.po + name = key.name + elif isinstance(key, tuple): + # Old-style (hdr, path) callback + h = key[0] + name = h['name'] + epoch = '0' + if h['epoch'] is not None: + epoch = str(h['epoch']) + pkgtup = (h['name'], h['arch'], epoch, h['version'], h['release']) + txmbrs = self._yum.tsInfo.getMembers(pkgtup=pkgtup) + if len(txmbrs) != 1: + log.error("unable to find package %s" % pkgtup) + exn = PayloadInstallError("failed to find package") + if errorHandler(exn, pkgtup) == ERROR_RAISE: + raise exn + + txmbr = txmbrs[0] + else: + # cleanup/remove error + name = key + txmbr = None + + return (name, txmbr) + + def callback(self, what, amount, total, h, user): + """ Yum install callback. """ + if what == rpm.RPMCALLBACK_TRANS_START: + pass + elif what == rpm.RPMCALLBACK_TRANS_PROGRESS: + # amount / total complete + pass + elif what == rpm.RPMCALLBACK_TRANS_STOP: + # we are done + pass + elif what == rpm.RPMCALLBACK_INST_OPEN_FILE: + # update status that we're installing/upgrading %h + # return an open fd to the file + txmbr = self._get_txmbr(h)[1] + + if self.upgrade: + mode = _("Upgrading") + else: + mode = _("Installing") + + self.install_log.write("%s %s %s" % (time.strftime("%H:%M:%S"), + mode, txmbr.po)) + self.install_log.flush() + + self.package_file = None + repo = self._yum.repos.getRepo(po.repoid) + + while self.package_file is None: + try: + package_path = repo.getPackage(po) + except (yum.Errors.NoMoreMirrorsRepoError, IOError): + exn = PayloadInstallError("failed to open package") + if errorHandler(exn, po) == ERROR_RAISE: + raise exn + except yum.Errors.RepoError: + continue + + self.package_file = open(package_path) + + return self.package_file.fileno() + elif what == rpm.RPMCALLBACK_INST_CLOSE_FILE: + # close and remove the last opened file + # update count of installed/upgraded packages + package_path = self.package_file.name + self.package_file.close() + self.package_file = None + + if package_path.startswith("%s/var/cache/yum/" % ROOT_PATH): + try: + os.unlink(package_file) + except OSError as e: + log.debug("unable to remove file %s" % e.strerror) + elif what == rpm.RPMCALLBACK_UNINST_START: + # update status that we're cleaning up %h + #progress.set_text(_("Cleaning up %s" % h)) + pass + elif what in (rpm.RPMCALLBACK_CPIO_ERROR, + rpm.RPMCALLBACK_UNPACK_ERROR, + rpm.RPMCALLBACK_SCRIPT_ERROR): + name = self._get_txmbr(h)[0] + + # Script errors store whether or not they're fatal in "total". So, + # we should only error out for fatal script errors or the cpio and + # unpack problems. + if what != rpm.RPMCALLBACK_SCRIPT_ERROR or total: + exn = PayloadInstallError("cpio, unpack, or fatal script error") + if errorHandler(exn, name) == ERROR_RAISE: + raise exn + + +class YumDepsolveCallback(object): + def __init__(self): + pass + + def transactionPopulation(self): + pass + + def pkgAdded(self, pkgtup, state): + pass + + def tscheck(self): + pass + + def downloadHeader(self, name): + pass + + def procReq(self, name, need): + pass + + def procConflict(self, name, need): + pass + + def end(self): + pass + +def show_groups(): + ksdata = makeVersion() + obj = YumPayload(ksdata) + obj.setup() + + repo = ksdata.RepoData(name="anaconda", baseurl="http://cannonball/install/rawhide/os/") + obj.addRepo(repo) + + desktops = [] + addons = [] + + for grp in obj.groups: + if not desktops and not addons: + print dir(grp) + if grp.endswith("-desktop"): + desktops.append(obj.description(grp)) + elif not grp.endswith("-support"): + addons.append(obj.description(grp)) + + import pprint + + print "==== DESKTOPS ====" + pprint.pprint(desktops) + print "==== ADDONS ====" + pprint.pprint(addons) + + print obj.groups + +def print_txmbrs(payload, f=None): + if f is None: + f = sys.stdout + + print >> f, "###########" + for txmbr in payload._yum.tsInfo.getMembers(): + print >> f, txmbr + print >> f, "###########" + +def write_txmbrs(payload, filename): + if os.path.exists(filename): + os.unlink(filename) + + f = open(filename, 'w') + print_txmbrs(payload, f) + f.close() + + +### +### MAIN +### +if __name__ == "__main__": + import os + import sys + import pyanaconda.storage as _storage + import pyanaconda.platform as _platform + + # set some things specially since we're just testing + flags.testing = True + global ROOT_PATH + ROOT_PATH = "/tmp/test-root" + + # set up ksdata + ksdata = makeVersion() + ksdata.method.method = "url" + ksdata.method.url = "http://husky/install/f17/os/" + #ksdata.method.url = "http://dl.fedoraproject.org/pub/fedora/linux/development/17/x86_64/os/" + + # set up storage + platform = _platform.getPlatform() + storage = _storage.Storage(data=ksdata, platform=platform) + storage.reset() + + # set up the payload + payload = YumPayload(ksdata) + payload.setup(storage) + + payload.install_log = sys.stdout + for repo in payload._yum.repos.repos.values(): + print repo.name, repo.enabled + + #for gid in payload.groups: + # payload.deselectGroup(gid) + + payload.selectGroup("core") + payload.selectGroup("base") + + payload.checkSoftwareSelection() + write_txmbrs(payload, "/tmp/tx.1") + + payload.selectGroup("development-tools") + payload.selectGroup("development-libs") + payload.checkSoftwareSelection() + write_txmbrs(payload, "/tmp/tx.2") + + payload.deselectGroup("development-tools") + payload.deselectGroup("development-libs") + payload.selectPackage("vim-enhanced") + payload.checkSoftwareSelection() + write_txmbrs(payload, "/tmp/tx.3") + + #payload.install() + payload.postInstall() + -- 1.7.9.1 _______________________________________________ Anaconda-devel-list mailing list Anaconda-devel-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/anaconda-devel-list