Linux Archive

Linux Archive (http://www.linux-archive.org/)
-   Gentoo User (http://www.linux-archive.org/gentoo-user/)
-   -   Add new packaging module. (http://www.linux-archive.org/gentoo-user/636779-add-new-packaging-module.html)

David Lehman 02-23-2012 07:50 PM

Add new packaging module.
 
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."
+ "

"
+ "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.

"
+ "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."
- "

"
- "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@redhat.com
https://www.redhat.com/mailman/listinfo/anaconda-devel-list

Chris Lumens 02-24-2012 07:27 PM

Add new packaging module.
 
I really like the direction this is going in. It so far looks very
clean, though I think that is likely to change as the realities of
package installation start to creep in.

One overriding comment I have is that I'd prefer code for different
payloads to be in different files. These are going to grow in a hurry.

> +from pykickstart.version import makeVersion

This import appears to only get used in testing code down at the bottom,
so it should be moved there.

> + 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

I think a lot of this can also be done via %post scripts. We'll have to
see, though. Some of it looks hopelessly tangled together.

> +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

If we are clever, we can progress report this, too. I'm not yet clever
enough to do so, however.

> +class TarPayload(ArchivePayload):
> + """ A TarPayload unpacks tar archives onto the target system. """

According to the setup method, this method looks to really only cover
unpacking a single archive onto the system, not multiple as the above
comment says. I think multiple would be more correct, though.

> + 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))

Missing a dot between "self" and "archive".

> + 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

I hope some of this can either move to %post scripts or go away
entirely. I don't think we need to be creating those directories
ourselves any more. It looks like the only reason we currently do so is
because we then write some files out into some of those directories,
which we don't really need to do until after packages are written out,
at which point the directories will exist.

Anyway that's just one example.

> + 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()

Don't forget to call the superclass's method.

> + 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):

We talked about this in IRC, but for everyone else's benefit I'll sum up
here. First, the old-style code can all go. That was introduced almost
a year ago and the new yum should have propagated by now. We know what
distributions this new anaconda will run on, and we can enforce a yum
via Requires.

We'll also have to ask for using that new version of the callback since
yum sets up for the old style one by default.

Finally, we should refer to "key" throughout the callback instead of
"h", due to largely historical reasons that involve us not wanting to do
anything with RPM headers or even give the appearance that we are doing
so.

> +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

We talked a bit about this class in IRC. It is likely only needed for
bouncing the progress bar while deps are being solved, but we'll be
doing that entirely as a background process. Thus, I would propose
first checking if yum absolutely needs the callback and if so, just have
a class like so:

class YumDepsolveCallback(object):
def getattr(self, attr):
return fake

def fake(self, *args, **kwargs):
pass

- Chris

_______________________________________________
Anaconda-devel-list mailing list
Anaconda-devel-list@redhat.com
https://www.redhat.com/mailman/listinfo/anaconda-devel-list


All times are GMT. The time now is 12:10 PM.

VBulletin, Copyright ©2000 - 2014, Jelsoft Enterprises Ltd.
Content Relevant URLs by vBSEO ©2007, Crawlability, Inc.