From fd5fe498e35b85a060ee375bd0829a714b1dc8ed Mon Sep 17 00:00:00 2001
From: Colin Walters <walters@verbum.org>
Date: Wed, 19 Mar 2014 06:06:12 -0400
Subject: [PATCH] WIP Anaconda+OSTree

---
 pyanaconda/__init__.py                |   5 +-
 pyanaconda/bootloader.py              | 107 ++++++++++++++-------
 pyanaconda/install.py                 |  36 +++++--
 pyanaconda/iutil.py                   |  31 ++++++-
 pyanaconda/kickstart.py               |   8 ++
 pyanaconda/packaging/__init__.py      |  12 +++
 pyanaconda/packaging/ostreepayload.py | 170 ++++++++++++++++++++++++++++++++++
 pyanaconda/packaging/yumpayload.py    |   4 +
 pyanaconda/ui/gui/hubs/__init__.py    |  12 ++-
 pyanaconda/ui/gui/spokes/software.py  |   4 +-
 pyanaconda/ui/gui/spokes/source.py    |   4 +-
 11 files changed, 348 insertions(+), 45 deletions(-)
 create mode 100644 pyanaconda/packaging/ostreepayload.py

diff --git a/pyanaconda/__init__.py b/pyanaconda/__init__.py
index 9b58f92..49b60d7 100644
--- a/pyanaconda/__init__.py
+++ b/pyanaconda/__init__.py
@@ -124,7 +124,10 @@ class Anaconda(object):
             if not klass:
                 from flags import flags
 
-                if flags.livecdInstall:
+                if self.ksdata.ostreesetup:
+                    from pyanaconda.packaging.ostreepayload import OSTreePayload
+                    klass = OSTreePayload
+                elif flags.livecdInstall:
                     from pyanaconda.packaging.livepayload import LiveImagePayload
                     klass = LiveImagePayload
                 elif self.ksdata.method.method == "liveimg":
diff --git a/pyanaconda/bootloader.py b/pyanaconda/bootloader.py
index 0d54893..3efcdde 100644
--- a/pyanaconda/bootloader.py
+++ b/pyanaconda/bootloader.py
@@ -952,10 +952,13 @@ class BootLoader(object):
 
         self.add_crash_args()
 
-        config_path = os.path.normpath(ROOT_PATH + self.config_file)
+        config_path = os.path.normpath(iutil.getSysroot() + self.config_file)
         if os.access(config_path, os.R_OK):
             os.rename(config_path, config_path + ".anacbak")
 
+        config_parent = os.path.dirname(config_path)
+        if not os.path.isdir(config_parent):
+            os.makedirs(config_parent)
         config = open(config_path, "w")
         self.write_config_header(config)
         self.write_config_images(config)
@@ -1180,7 +1183,7 @@ class GRUB(BootLoader):
 
         if iutil.isConsoleOnVirtualTerminal(self.console):
             splash = "splash.xpm.gz"
-            splash_path = os.path.normpath("%s/boot/%s/%s" % (ROOT_PATH,
+            splash_path = os.path.normpath("%s/boot/%s/%s" % (iutil.getSysroot(),
                                                         self.splash_dir,
                                                         splash))
             if os.access(splash_path, os.R_OK):
@@ -1233,7 +1236,7 @@ class GRUB(BootLoader):
 
     def write_device_map(self):
         """ Write out a device map containing all supported devices. """
-        map_path = os.path.normpath(ROOT_PATH + self.device_map_file)
+        map_path = os.path.normpath(iutil.getSysroot() + self.device_map_file)
         if os.access(map_path, os.R_OK):
             os.rename(map_path, map_path + ".anacbak")
 
@@ -1249,7 +1252,7 @@ class GRUB(BootLoader):
         super(GRUB, self).write_config_post()
 
         # make symlink for menu.lst (grub's default config file name)
-        menu_lst = "%s%s/menu.lst" % (ROOT_PATH, self.config_dir)
+        menu_lst = "%s%s/menu.lst" % (iutil.getSysroot(), self.config_dir)
         if os.access(menu_lst, os.R_OK):
             try:
                 os.rename(menu_lst, menu_lst + '.anacbak')
@@ -1262,7 +1265,7 @@ class GRUB(BootLoader):
             log.error("failed to create grub menu.lst symlink: %s" % e)
 
         # make symlink to grub.conf in /etc since that's where configs belong
-        etc_grub = "%s/etc/%s" % (ROOT_PATH, self._config_file)
+        etc_grub = "%s/etc/%s" % (iutil.getSysroot(), self._config_file)
         if os.access(etc_grub, os.R_OK):
             try:
                 os.unlink(etc_grub)
@@ -1452,7 +1455,7 @@ class GRUB2(GRUB):
 
     def write_device_map(self):
         """ Write out a device map containing all supported devices. """
-        map_path = os.path.normpath(ROOT_PATH + self.device_map_file)
+        map_path = os.path.normpath(iutil.getSysroot() + self.device_map_file)
         if os.access(map_path, os.R_OK):
             os.rename(map_path, map_path + ".anacbak")
 
@@ -1477,7 +1480,7 @@ class GRUB2(GRUB):
         dev_map.close()
 
     def write_defaults(self):
-        defaults_file = "%s%s" % (ROOT_PATH, self.defaults_file)
+        defaults_file = "%s%s" % (iutil.getSysroot(), self.defaults_file)
         defaults = open(defaults_file, "w+")
         defaults.write("GRUB_TIMEOUT=%d\n" % self.timeout)
         defaults.write("GRUB_DISTRIBUTOR=\"$(sed 's, release .*$,,g' /etc/system-release)\"\n")
@@ -1521,7 +1524,7 @@ class GRUB2(GRUB):
         if not self.password and not self.encrypted_password:
             return
 
-        users_file = ROOT_PATH + "/etc/grub.d/01_users"
+        users_file = iutil.getSysroot() + "/etc/grub.d/01_users"
         header = open(users_file, "w")
         header.write("#!/bin/sh -e\n\n")
         header.write("cat << \"EOF\"\n")
@@ -1889,8 +1892,9 @@ class Yaboot(YabootBase):
                 log.error("failed to create /etc/yaboot.conf symlink: %s" % e)
 
     def write_config(self):
-        if not os.path.isdir(ROOT_PATH + self.config_dir):
-            os.mkdir(ROOT_PATH + self.config_dir)
+        configdir = iutil.getSysroot() + self.config_dir
+        if not os.path.isdir(configdir):
+            os.mkdir(configdir)
 
         # this writes the config
         super(Yaboot, self).write_config()
@@ -2251,17 +2255,20 @@ class EXTLINUX(BootLoader):
             config.write(stanza)
 
     def write_config_header(self, config):
+        # FIXME re-add this
+        # "ui menu.c32\n\n"
+        # after this is fixed:
+        # https://bugzilla.gnome.org/show_bug.cgi?id=726757
         header = ("# extlinux.conf generated by anaconda\n\n"
-                  "ui menu.c32\n\n"
                   "menu autoboot Welcome to %(productName)s. Automatic boot in # second{,s}. Press a key for options.\n"
                   "menu title %(productName)s Boot Options.\n"
                   "menu hidden\n\n"
                   "timeout %(timeout)d\n"
                   "#totaltimeout 9000\n\n"
-                  "default %(default)s\n\n"
-                  % { "productName": productName, "timeout": self.timeout *10,
-                     "default": self.image_label(self.default)})
+                  % { "productName": productName, "timeout": self.timeout *10 })
         config.write(header)
+        if self.default is not None:
+            config.write("default %(default)s\n\n" % { "default" : self.image_label(self.default) })
         self.write_config_password(config)
 
     def write_config_password(self, config):
@@ -2270,7 +2277,7 @@ class EXTLINUX(BootLoader):
             config.write("menu notabmsg Press [Tab] and enter the password to edit options")
 
     def write_config_post(self):
-        etc_extlinux = os.path.normpath(ROOT_PATH + "/etc/" + self._config_file)
+        etc_extlinux = os.path.normpath(iutil.getSysroot() + "/etc/" + self._config_file)
         if not os.access(etc_extlinux, os.R_OK):
             try:
                 os.symlink("../boot/%s" % self._config_file, etc_extlinux)
@@ -2361,6 +2368,52 @@ def writeSysconfigKernel(storage, version):
         f.write("HYPERVISOR_ARGS=logging=vga,serial,memory\n")
     f.close()
 
+def writeBootLoaderFinal(storage, payload, instClass, ksdata):
+    """ Do the final write of the bootloader. """
+
+    from pyanaconda.errors import errorHandler, ERROR_RAISE
+
+    # set up dracut/fips boot args
+    # XXX FIXME: do this from elsewhere?
+    #storage.bootloader.set_boot_args(keyboard=anaconda.keyboard,
+    #                                 storage=anaconda.storage,
+    #                                 language=anaconda.instLanguage,
+    #                                 network=anaconda.network)
+    storage.bootloader.set_boot_args(storage=storage,
+                                     payload=payload,
+                                     keyboard=ksdata.keyboard)
+
+    # Now we need to ensure /boot is really mounted in the target root
+    # (for OSTree-based installs), since the bootloader installer will
+    # run chrooted.
+    boot_device = storage.mountpoints.get("/boot")
+    sysroot_boot = None
+    if boot_device and (iutil.getSysroot() != ROOT_PATH):
+        sysroot_boot = os.path.join(ROOT_PATH, 'boot')
+        ostree_sysroot_boot = os.path.join(iutil.getSysroot(), 'boot')
+        # It'd be nice to mount --move, but the root may be set up as
+        # shared, and let's not screw with that now...
+        iutil.execWithRedirect('umount', [sysroot_boot])
+        iutil.execWithRedirect('mount', [boot_device.format.device, ostree_sysroot_boot])
+
+    try:
+        storage.bootloader.write()
+    except BootLoaderError as e:
+        if errorHandler.cb(e) == ERROR_RAISE:
+            raise
+    if boot_device:
+        iutil.execWithRedirect('umount', [ostree_sysroot_boot])
+        iutil.execWithRedirect('mount', [boot_device.format.device, sysroot_boot])
+
+def getBootLoaderDefaultArgs(storage):
+    """ Return kernel args such as root=.
+
+        This is used by the OSTree payload, which
+        requires the bootloader arguments at the time
+        of deployment.
+    """
+    return storage.bootloader.boot_args
+
 def writeBootLoader(storage, payload, instClass, ksdata):
     """ Write bootloader configuration to disk.
 
@@ -2376,6 +2429,13 @@ def writeBootLoader(storage, payload, instClass, ksdata):
         stage2_device = storage.bootloader.stage2_device
         log.info("bootloader stage2 target device is %s" % stage2_device.name)
 
+    if payload.handlesBootloaderConfiguration:
+        if storage.bootloader.skip_bootloader:
+            log.info("skipping bootloader install per user request")
+            return
+        writeBootLoaderFinal(storage, payload, instClass, ksdata)
+        return
+
     # get a list of installed kernel packages
     kernel_versions = payload.kernelVersionList
     if not kernel_versions:
@@ -2420,19 +2480,4 @@ def writeBootLoader(storage, payload, instClass, ksdata):
                                          label=label, short=short)
         storage.bootloader.add_image(image)
 
-    # set up dracut/fips boot args
-    # XXX FIXME: do this from elsewhere?
-    #storage.bootloader.set_boot_args(keyboard=anaconda.keyboard,
-    #                                 storage=anaconda.storage,
-    #                                 language=anaconda.instLanguage,
-    #                                 network=anaconda.network)
-    storage.bootloader.set_boot_args(storage=storage,
-                                     payload=payload,
-                                     keyboard=ksdata.keyboard)
-
-    try:
-        storage.bootloader.write()
-    except BootLoaderError as e:
-        if errorHandler.cb(e) == ERROR_RAISE:
-            raise
-
+    writeBootLoaderFinal(storage, payload, instClass, ksdata)
diff --git a/pyanaconda/install.py b/pyanaconda/install.py
index 5796086..fe3ff0a 100644
--- a/pyanaconda/install.py
+++ b/pyanaconda/install.py
@@ -22,20 +22,22 @@
 
 from pyanaconda.constants import ROOT_PATH, THREAD_PAYLOAD
 from blivet import turnOnFilesystems
-from pyanaconda.bootloader import writeBootLoader
+from pyanaconda.bootloader import writeBootLoader, writeBootLoaderFinal
 from pyanaconda.progress import progress_report, progressQ
 from pyanaconda.users import createLuserConf, getPassAlgo, Users
+from pyanaconda import iutil
 from pyanaconda import flags
 from pyanaconda import timezone
 from pyanaconda.i18n import _
 from pyanaconda.threads import threadMgr
 import logging
+import blivet
 log = logging.getLogger("anaconda")
 
 def _writeKS(ksdata):
     import os
 
-    path = ROOT_PATH + "/root/anaconda-ks.cfg"
+    path = iutil.getSysroot() + "/root/anaconda-ks.cfg"
 
     # Clear out certain sensitive information that kickstart doesn't have a
     # way of representing encrypted.
@@ -148,7 +150,8 @@ def doInstall(storage, payload, ksdata, instClass):
     payload.preStorage()
 
     turnOnFilesystems(storage, mountOnly=flags.flags.dirInstall)
-    if not flags.flags.livecdInstall and not flags.flags.dirInstall:
+    write_storage_late = flags.flags.livecdInstall or ksdata.ostreesetup.osname
+    if not write_storage_late and not flags.flags.dirInstall:
         storage.write()
 
     # Do packaging.
@@ -169,15 +172,30 @@ def doInstall(storage, payload, ksdata, instClass):
     payload.preInstall(packages=packages, groups=payload.languageGroups())
     payload.install()
 
-    if flags.flags.livecdInstall:
-        storage.write()
-
-    with progress_report(_("Performing post-installation setup tasks")):
-        payload.postInstall()
-
+    if write_storage_late:
+        if iutil.getSysroot() != ROOT_PATH:
+            blivet.targetSysroot = iutil.getSysroot()
+            storage.write()
+            # Now that we have the FS layout in the target,
+            # umount things that were in the legacy sysroot,
+            # and put them in the target root, except for the
+            # physical /
+            storage.umountFilesystems()
+            rootmnt = storage.mountpoints.get('/')
+            rootmnt.setup()
+            # Explicitly mount the root on the physical sysroot
+            rootmnt.format.setup(rootmnt.format.options, chroot=ROOT_PATH)
+            # But everything else goes in the target root
+            storage.mountFilesystems(skipRoot=True)
+        else:
+            storage.write()
+    
     # Do bootloader.
     if not flags.flags.dirInstall:
         with progress_report(_("Installing bootloader")):
             writeBootLoader(storage, payload, instClass, ksdata)
 
+    with progress_report(_("Performing post-installation setup tasks")):
+        payload.postInstall()
+
     progressQ.send_complete()
diff --git a/pyanaconda/iutil.py b/pyanaconda/iutil.py
index 8d6b31b..20cf9e7 100644
--- a/pyanaconda/iutil.py
+++ b/pyanaconda/iutil.py
@@ -105,6 +105,23 @@ def _run_program(argv, root='/', stdin=None, stdout=None, env_prune=None, log_ou
 
     return (proc.returncode, output_string)
 
+_sysroot = ROOT_PATH
+def setSysroot(path):
+    global _sysroot
+    _sysroot = path
+
+def getSysroot():
+    return _sysroot
+
+def execInSysimageRoot(command, argv, **kwargs):
+    """ Run a command from the installed target root.
+        @param command The command to run
+        @param argv The argument list
+    """
+
+    argv = [command] + argv
+    return _run_program(argv, root=_sysroot, **kwargs)[0]
+
 def execWithRedirect(command, argv, stdin=None, stdout=None,
                      stderr=None, root='/', env_prune=[], log_output=True, binary_output=False):
     """ Run an external program and redirect the output to a file.
@@ -124,8 +141,14 @@ def execWithRedirect(command, argv, stdin=None, stdout=None,
                    % (command, " ".join(argv)))
         return 0
 
+    # Transparently redirect callers requesting root=ROOT_PATH to the
+    # configured system root.
+    target_root = root
+    if target_root == ROOT_PATH:
+        target_root = _sysroot
+
     argv = [command] + argv
-    return _run_program(argv, stdin=stdin, stdout=stdout, root=root, env_prune=env_prune,
+    return _run_program(argv, stdin=stdin, stdout=stdout, root=target_root, env_prune=env_prune,
             log_output=log_output, binary_output=binary_output)[0]
 
 def execWithCapture(command, argv, stdin=None, stderr=None, root='/',
@@ -172,6 +195,12 @@ def execReadlines(command, argv, stdin=None, root='/', env_prune=None):
             queue.put(line.strip())
         out.close()
 
+    # Transparently redirect callers requesting root=ROOT_PATH to the
+    # configured system root.
+    target_root = root
+    if target_root == ROOT_PATH:
+        target_root = _sysroot
+
     def chroot():
         if root and root != '/':
             os.chroot(root)
diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py
index df7b5e6..8290d89 100644
--- a/pyanaconda/kickstart.py
+++ b/pyanaconda/kickstart.py
@@ -490,6 +490,13 @@ class Realm(commands.realm.F19_Realm):
 
         log.info("Joined realm %s", self.join_realm)
 
+class OSTreeSetup(commands.ostreesetup.F20_OSTreeSetup):
+    def __init__(self, *args):
+        commands.ostreesetup.F20_OSTreeSetup.__init__(self, *args)
+
+    def execute(self, *args):
+        # This is implemented in packaging/ostreepayload.py
+        pass
 
 class ClearPart(commands.clearpart.F17_ClearPart):
     def parse(self, args):
@@ -1580,6 +1587,7 @@ commandMap = {
         "logvol": LogVol,
         "multipath": MultiPath,
         "network": Network,
+        "ostreesetup": OSTreeSetup,
         "part": Partition,
         "partition": Partition,
         "raid": Raid,
diff --git a/pyanaconda/packaging/__init__.py b/pyanaconda/packaging/__init__.py
index f0453c5..c9691b6 100644
--- a/pyanaconda/packaging/__init__.py
+++ b/pyanaconda/packaging/__init__.py
@@ -128,6 +128,14 @@ class Payload(object):
         """ Reset the instance, not including ksdata. """
         pass
 
+    @property
+    def handlesBootloaderConfiguration(self):
+        """ Set if the payload requires the bootloader be installed
+        but unconfigured before doing an install of the system.  This
+        is used by the OSTreePayload subclass.
+        """
+        return False
+
     ###
     ### METHODS FOR WORKING WITH REPOSITORIES
     ###
@@ -325,6 +333,10 @@ class Payload(object):
     ### METHODS FOR WORKING WITH PACKAGES
     ###
     @property
+    def supportsPackages(self):
+        return False
+
+    @property
     def packages(self):
         raise NotImplementedError()
 
diff --git a/pyanaconda/packaging/ostreepayload.py b/pyanaconda/packaging/ostreepayload.py
new file mode 100644
index 0000000..19c101d
--- /dev/null
+++ b/pyanaconda/packaging/ostreepayload.py
@@ -0,0 +1,170 @@
+# ostreepayload.py
+# Deploy OSTree trees to target
+#
+# Copyright (C) 2012,2014  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.  You should have received a copy of the
+# GNU General Public License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): Colin Walters <walters@redhat.com>
+#
+
+import shutil
+
+from . import *
+
+
+from pyanaconda.constants import *
+from pyanaconda.flags import flags
+
+from pyanaconda import iutil
+from pyanaconda.bootloader import getBootLoaderDefaultArgs
+from pyanaconda.i18n import _
+from pyanaconda.progress import progressQ
+from gi.repository import GLib
+from gi.repository import Gio
+
+from blivet.size import Size
+
+import logging
+log = logging.getLogger("anaconda")
+
+from pyanaconda.errors import *
+#from pyanaconda.progress import progress
+
+class OSTreePayload(ArchivePayload):
+    """ A OSTreePayload deploys a tree onto the target system. """
+    def __init__(self, data):
+        super(OSTreePayload, self).__init__(data)
+
+    def setup(self, storage):
+        super(OSTreePayload, self).setup(storage)
+
+    @property
+    def handlesBootloaderConfiguration(self):
+        return True
+
+    @property
+    def kernelVersionList(self):
+        # OSTree handles bootloader configuration
+        return []
+
+    @property
+    def spaceRequired(self):
+        # We don't have this data with OSTree at the moment
+        return Size(500*1000*1000)
+
+    def _safeExecWithRedirect(self, cmd, argv, **kwargs):
+        """Like iutil.execWithRedirect, but treat errors as fatal"""
+        rc = iutil.execWithRedirect(cmd, argv, **kwargs)
+        if rc != 0:
+            exn = PayloadInstallError("%s %s exited with code %d" % (cmd, argv, rc))
+            if errorHandler.cb(exn) == ERROR_RAISE:
+                raise exn
+
+    def _pullProgressCb(self, asyncProgress):
+        status = asyncProgress.get_status()
+        outstanding_fetches = asyncProgress.get_uint('outstanding-fetches')
+        if status:
+            progressQ.send_message(status)
+        elif outstanding_fetches > 0:
+            bytes_transferred = asyncProgress.get_uint64('bytes-transferred')
+            fetched = asyncProgress.get_uint('fetched')
+            requested = asyncProgress.get_uint('requested')
+            formatted_bytes = GLib.format_size_full(bytes_transferred, 0)
+
+            if requested == 0:
+                percent = 0.0
+            else:
+                percent = (fetched*1.0 / requested) * 100
+            
+            progressQ.send_message("Receiving objects: %d%% (%d/%d) %s" % (percent, fetched, requested, formatted_bytes))
+        else:
+            progressQ.send_message("Writing objects")
+
+    def install(self):
+        cancellable = None
+        from gi.repository import OSTree
+        ostreesetup = self.data.ostreesetup
+        log.info("executing ostreesetup=%r" % (ostreesetup, ))
+
+        # Initialize the filesystem - this will create the repo as well
+        self._safeExecWithRedirect("ostree", ["admin", "--sysroot=" + ROOT_PATH,
+                                              "init-fs", ROOT_PATH])
+
+        repo_arg = "--repo=" + ROOT_PATH + '/ostree/repo'
+
+        # Set up the chosen remote
+        remote_args = [repo_arg, "remote", "add"]
+        if ostreesetup.noGpg:
+            remote_args.append("--set=gpg-verify=false")
+        remote_args.extend([ostreesetup.remote,
+                            ostreesetup.url])
+        self._safeExecWithRedirect("ostree", remote_args)
+
+        sysroot_path = Gio.File.new_for_path(ROOT_PATH)
+        sysroot = OSTree.Sysroot.new(sysroot_path)
+        sysroot.load(cancellable)
+
+        repo = sysroot.get_repo(None)[1]
+        progressQ.send_message(_("Starting pull of %s from %s") % (ostreesetup.ref, ostreesetup.remote))
+
+        progress = OSTree.AsyncProgress.new()
+        progress.connect('changed', self._pullProgressCb)
+        repo.pull(ostreesetup.remote, [ostreesetup.ref], 0, progress, cancellable)
+
+        self._safeExecWithRedirect("ostree", ["admin", "--sysroot=" + ROOT_PATH,
+                                              "os-init", ostreesetup.osname])
+            
+        admin_deploy_args = ["admin", "--sysroot=" + ROOT_PATH,
+                             "deploy", "--os=" + ostreesetup.osname]
+
+        admin_deploy_args.append(ostreesetup.osname + ':' + ostreesetup.ref)
+
+        log.info("ostree admin deploy starting")
+        self._safeExecWithRedirect("ostree", admin_deploy_args)
+        log.info("ostree admin deploy complete")
+
+        ostree_sysroot_path = os.path.join(ROOT_PATH, 'ostree/deploy', ostreesetup.osname, 'current')
+        iutil.setSysroot(ostree_sysroot_path)
+
+    def postInstall(self):
+        super(OSTreePayload, self).postInstall()
+
+        # FIXME - Move extlinux.conf to syslinux.cfg, since that's
+        # what OSTree knows about.
+        sysroot = iutil.getSysroot()
+        sysroot_extlinux_boot = os.path.join(sysroot, 'boot/extlinux')
+        sysroot_boot_loader = os.path.join(sysroot, 'boot/loader')
+        if os.path.isdir(sysroot_extlinux_boot):
+            assert os.path.isdir(sysroot_boot_loader)
+            orig_extlinux_conf = os.path.join(sysroot_extlinux_boot, 'extlinux.conf')
+            os.rename(orig_extlinux_conf,
+                      os.path.join(sysroot_boot_loader, 'syslinux.cfg'))
+            os.symlink('../loader/syslinux.cfg', orig_extlinux_conf)
+
+        # OSTree owns the bootloader configuration, so here we give it
+        # the argument list we computed from storage, architecture and
+        # such.
+        kargs = getBootLoaderDefaultArgs(self.storage)
+        set_kargs_args = ["admin", "--sysroot=" + ROOT_PATH,
+                          "instutil", "set-kargs"]
+        set_kargs_args.extend(kargs)
+        self._safeExecWithRedirect("ostree", set_kargs_args)
+        # This command iterates over all files we might have created
+        # and ensures they're labeled. It's like running
+        # chroot(ROOT_PATH) + fixfiles, except with a better name and
+        # semantics.
+        self._safeExecWithRedirect("ostree", ["admin", "--sysroot=" + ROOT_PATH,
+                                              "instutil", "selinux-ensure-labeled", ROOT_PATH, ""])
diff --git a/pyanaconda/packaging/yumpayload.py b/pyanaconda/packaging/yumpayload.py
index 824c8a4..7b863b5 100644
--- a/pyanaconda/packaging/yumpayload.py
+++ b/pyanaconda/packaging/yumpayload.py
@@ -1273,6 +1273,10 @@ reposdir=%s
     ### METHODS FOR WORKING WITH PACKAGES
     ###
     @property
+    def supportsPackages(self):
+        return True
+
+    @property
     def packages(self):
         from yum.Errors import RepoError
 
diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py
index f8b016e..e739f1d 100644
--- a/pyanaconda/ui/gui/hubs/__init__.py
+++ b/pyanaconda/ui/gui/hubs/__init__.py
@@ -287,7 +287,17 @@ class Hub(GUIObject, common.Hub):
 
     @property
     def continuePossible(self):
-        return len(self._incompleteSpokes) == 0 and len(self._notReadySpokes) == 0 and getattr(self._checker, "success", True)
+        if len(self._incompleteSpokes) > 0:
+            log.info("Incomplete spokes: %r" % (self._incompleteSpokes, ))
+            return False
+        if len(self._notReadySpokes) > 0:
+            log.info("NotReady spokes: %r" % (self._notReadySpokes, ))
+            return False
+        checkSuccess=getattr(self._checker, "success", True)
+        if not checkSuccess:
+            log.info("Checker returned %r" % (checkSuccess, ))
+            return False
+        return True
         
     def _updateContinueButton(self):
         self.continueButton.set_sensitive(self.continuePossible)
diff --git a/pyanaconda/ui/gui/spokes/software.py b/pyanaconda/ui/gui/spokes/software.py
index b21866b..46e7ede 100644
--- a/pyanaconda/ui/gui/spokes/software.py
+++ b/pyanaconda/ui/gui/spokes/software.py
@@ -164,7 +164,9 @@ class SoftwareSelectionSpoke(NormalSpoke):
 
     @property
     def showable(self):
-        return not flags.livecdInstall and not self.data.method.method == "liveimg"
+        return (self.payload.supportsPackages
+                and not flags.livecdInstall
+                and not self.data.method.method == "liveimg")
 
     @property
     def status(self):
diff --git a/pyanaconda/ui/gui/spokes/source.py b/pyanaconda/ui/gui/spokes/source.py
index 59ff82c..add7fbc 100644
--- a/pyanaconda/ui/gui/spokes/source.py
+++ b/pyanaconda/ui/gui/spokes/source.py
@@ -761,7 +761,9 @@ class SourceSpoke(NormalSpoke):
 
     @property
     def showable(self):
-        return not flags.livecdInstall and not self.data.method.method == "liveimg"
+        return (self.payload.supportsPackages
+                and not flags.livecdInstall
+                and not self.data.method.method == "liveimg")
 
     def _mirror_active(self):
         return self._protocolComboBox.get_active() == PROTOCOL_MIRROR
-- 
1.8.3.1

