From c47b31448fec123c8603674bf2d841ea8cec7c08 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Mon, 27 Feb 2012 21:55:27 -0600
Subject: [PATCH 1/4] Add new packaging module.

---
 pyanaconda/constants.py             |    3 +
 pyanaconda/errors.py                |   63 +++-
 pyanaconda/image.py                 |  149 +++----
 pyanaconda/packaging/__init__.py    |  519 +++++++++++++++++++++++
 pyanaconda/packaging/livepayload.py |   53 +++
 pyanaconda/packaging/yumpayload.py  |  779 +++++++++++++++++++++++++++++++++++
 6 files changed, 1481 insertions(+), 85 deletions(-)
 create mode 100644 pyanaconda/packaging/__init__.py
 create mode 100644 pyanaconda/packaging/livepayload.py
 create mode 100644 pyanaconda/packaging/yumpayload.py

diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py
index 4167f96..2eba572 100644
--- a/pyanaconda/constants.py
+++ b/pyanaconda/constants.py
@@ -87,3 +87,6 @@ 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..01e01cd 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,83 +107,74 @@ 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)
 
-def mountImage(isodir, tree, messageWindow):
-    def complain():
-        ans = messageWindow(_("Missing ISO 9660 Image"),
-                            _("The installer has tried to mount the "
-                              "installation image, but cannot find it on "
-                              "the hard drive.\n\n"
-                              "Please copy this image to the "
-                              "drive and click Retry.  Click Exit "
-                              "to abort the installation."),
-                              type="custom",
-                              custom_icon="warning",
-                              custom_buttons=[_("_Exit"), _("_Retry")])
-        if ans == 0:
-            sys.exit(0)
+        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):
     if os.path.ismount(tree):
         raise SystemError, "trying to mount already-mounted iso image!"
     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 +205,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/__init__.py b/pyanaconda/packaging/__init__.py
new file mode 100644
index 0000000..958b00f
--- /dev/null
+++ b/pyanaconda/packaging/__init__.py
@@ -0,0 +1,519 @@
+#!/usr/bin/python
+
+"""
+    TODO
+        - error handling!!!
+        - document all methods
+
+"""
+
+import os
+from urlgrabber.grabber import URLGrabber
+from urlgrabber.grabber import URLGrabError
+import ConfigParser
+import shutil
+import time
+
+if __name__ == "__main__":
+    from pyanaconda import anaconda_log
+    anaconda_log.init()
+
+try:
+    import tarfile
+except ImportError:
+    log.error("import of tarfile failed")
+    tarfile = 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
+
+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 ArchivePayload(ImagePayload):
+    """ An ArchivePayload unpacks source archives onto the target system. """
+    pass
+
+class TarPayload(ArchivePayload):
+    """ A TarPayload unpacks a single tar archive 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:
+            self.archive.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
+
+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
+    from pykickstart.version import makeVersion
+
+    # 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()
+
+    from pyanaconda.packaging.yumpayload import YumPayload
+
+    # 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()
+
diff --git a/pyanaconda/packaging/livepayload.py b/pyanaconda/packaging/livepayload.py
new file mode 100644
index 0000000..a4f9644
--- /dev/null
+++ b/pyanaconda/packaging/livepayload.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+
+"""
+    TODO
+        - error handling!!!
+        - document all methods
+        - LiveImagePayload
+            - register the live image, either via self.data.method or in setup
+              using storage
+
+"""
+import os
+import stat
+
+from . import *
+
+from pyanaconda.constants import *
+from pyanaconda.flags import flags
+
+from pyanaconda import iutil
+
+import logging
+log = logging.getLogger("anaconda")
+
+from pyanaconda.errors import *
+#from pyanaconda.progress import progress
+
+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"
+        # FIXME: this is broken. can't rsync from a device node to a directory.
+        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
diff --git a/pyanaconda/packaging/yumpayload.py b/pyanaconda/packaging/yumpayload.py
new file mode 100644
index 0000000..8a78123
--- /dev/null
+++ b/pyanaconda/packaging/yumpayload.py
@@ -0,0 +1,779 @@
+#!/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
+
+"""
+
+import os
+import shutil
+import time
+
+from . import *
+
+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
+
+import logging
+log = logging.getLogger("anaconda")
+
+from pyanaconda.errors import *
+#from pyanaconda.progress import progress
+
+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()
+
+        self._yum.use_txmbr_in_callback = True
+
+        # 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()
+
+        super(YumPayload, self).postInstall()
+
+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
+        else:
+            # cleanup/remove error
+            name = key
+            txmbr = None
+
+        return (name, txmbr)
+
+    def callback(self, event, amount, total, key, userdata):
+        """ Yum install callback. """
+        if event == _rpm.RPMCALLBACK_TRANS_START:
+            pass
+        elif event == _rpm.RPMCALLBACK_TRANS_PROGRESS:
+            # amount / total complete
+            pass
+        elif event == _rpm.RPMCALLBACK_TRANS_STOP:
+            # we are done
+            pass
+        elif event == _rpm.RPMCALLBACK_INST_OPEN_FILE:
+            # update status that we're installing/upgrading %h
+            # return an open fd to the file
+            txmbr = self._get_txmbr(key)[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 event == _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 event == _rpm.RPMCALLBACK_UNINST_START:
+            # update status that we're cleaning up %key
+            #progress.set_text(_("Cleaning up %s" % key))
+            pass
+        elif event in (_rpm.RPMCALLBACK_CPIO_ERROR,
+                       _rpm.RPMCALLBACK_UNPACK_ERROR,
+                       _rpm.RPMCALLBACK_SCRIPT_ERROR):
+            name = self._get_txmbr(key)[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 event != _rpm.RPMCALLBACK_SCRIPT_ERROR or total:
+                exn = PayloadInstallError("cpio, unpack, or fatal script error")
+                if errorHandler(exn, name) == ERROR_RAISE:
+                    raise exn
+
+
+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
+    from pykickstart.version import makeVersion
+
+    # 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

