[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

OSTree <3 Anaconda



Background
----------

OSTree is a tool to replicate pre-constructed *bootable* filesystem
trees over plain HTTP, including support for things like GPG
keysigning. rpm-ostree is a tool which takes RPM content on a
"compose" server, and stores it into an OSTree repository.

This model is very useful for any deployment scenario where one wants
to compose a system from the tested RPM parts, and then efficiently
replicate it to many client systems. Examples are many cloud
deployments, "Corporate Standard Build" laptops, all the way down to
cash registers and embedded devices.

To make this work, OSTree is not just a tool to download content -
it's also a set of *required changes* to the operating system content,
such as /usr/lib/passwd:
https://sourceware.org/bugzilla/show_bug.cgi?id=16142

Anaconda
--------

For Fedora and derived systems like Red Hat Enterprise Linux, Anaconda
is by far subsystem impacted the most. While the system is booted
normally, while you are inside a chroot, for the most part the system
"feels" like a traditional Unix system. You have writable /etc and
/var.

However, really on disk, the operating system lives in
e.g. /ostree/deploy/fedora-atomic/deploy/c79ba9b3c245fa5e256aa9217db5f4954b2639d1504d02462277cae3c30473ae.0

Anaconda however has a constant ROOT_PATH that needs to be split -
some things like in the physical root, others live in what I placed in
"iutil.getSysroot()".

There is no /etc/fstab on the *physical* filesystem for example - it
is really in
/ostree/deploy/fedora-atomic/deploy/c79ba9b3c245fa5e256aa9217db5f4954b2639d1504d02462277cae3c30473ae.0/etc/fstab.

Furthermore, OSTree wants to be in charge of setting bootloader
configuration. (This is how the atomic updates work). So we have a
multi-stage setup where Anaconda lays down the defaults (but no boot
entries), then we tell OSTree to write the boot entries.

pykickstart
-----------

This patch is pretty simple - it's defining a new "ostreesetup" verb
that holds data.

blivet
------

The required changes here are similar to Anaconda - we need a
distinction between ROOT_PATH and "targetSysroot".

Future
------

These patches are for early architectural feedback. The next step
I'll take is to factor out some of the "quick hack" things that should
be fixed elsewhere. For example, OSTree should know about
/boot/extlinux in addition to /boot/syslinux. Also,
https://bugzilla.gnome.org/show_bug.cgi?id=726757


>From 7f8490665887e78190d095830c2009cd12b202ea 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              |  81 ++++++++------
 pyanaconda/install.py                 |  41 +++++--
 pyanaconda/iutil.py                   |  31 +++++-
 pyanaconda/kickstart.py               |  43 +++++---
 pyanaconda/packaging/__init__.py      |  12 +++
 pyanaconda/packaging/ostreepayload.py | 194 ++++++++++++++++++++++++++++++++++
 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 +-
 pyanaconda/users.py                   |  65 +++++++++---
 12 files changed, 419 insertions(+), 77 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..ee44927 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,26 @@ 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)
+    try:
+        storage.bootloader.write()
+    except BootLoaderError as e:
+        if errorHandler.cb(e) == ERROR_RAISE:
+            raise
+
 def writeBootLoader(storage, payload, instClass, ksdata):
     """ Write bootloader configuration to disk.
 
@@ -2376,6 +2403,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 +2454,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..8b9d7ec 100644
--- a/pyanaconda/install.py
+++ b/pyanaconda/install.py
@@ -25,17 +25,26 @@ from blivet import turnOnFilesystems
 from pyanaconda.bootloader import writeBootLoader
 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"
+    roothome = os.path.join(iutil.getSysroot(), "root")
+    if os.path.islink(roothome) and not os.path.isdir(roothome):
+        # In the OSTree case, /root -> /var/roothome, but
+        # it doesn't exist yet.  If we created it here,
+        # we'd need to ensure it was labeled correctly.
+        # Punt on that for now and just stick it in /var.
+        roothome = os.path.join(iutil.getSysroot(), "var")
+    path = os.path.join(roothome, "anaconda-ks.cfg")
 
     # Clear out certain sensitive information that kickstart doesn't have a
     # way of representing encrypted.
@@ -148,7 +157,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 +179,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..c788074 100644
--- a/pyanaconda/kickstart.py
+++ b/pyanaconda/kickstart.py
@@ -227,8 +227,8 @@ class Authconfig(commands.authconfig.FC3_Authconfig):
         args = ["--update", "--nostart"] + shlex.split(self.authconfig)
 
         if not flags.automatedInstall and \
-           (os.path.exists(ROOT_PATH + "/lib64/security/pam_fprintd.so") or \
-            os.path.exists(ROOT_PATH + "/lib/security/pam_fprintd.so")):
+           (os.path.exists(iutil.getSysroot() + "/lib64/security/pam_fprintd.so") or \
+            os.path.exists(iutil.getSysroot() + "/lib/security/pam_fprintd.so")):
             args += ["--enablefingerprint"]
 
         try:
@@ -470,7 +470,7 @@ class Realm(commands.realm.F19_Realm):
             # no explicit password arg using implicit --no-password
             pw_args = ["--no-password"]
 
-        argv = ["realm", "join", "--install", ROOT_PATH, "--verbose"] + \
+        argv = ["realm", "join", "--install", iutil.getSysroot(), "--verbose"] + \
                pw_args + self.join_args
         rc = -1
         try:
@@ -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):
@@ -581,7 +588,7 @@ class Firewall(commands.firewall.F20_Firewall):
             args += [ "--service=%s" % (service,) ]
 
         cmd = "/usr/bin/firewall-offline-cmd"
-        if not os.path.exists(ROOT_PATH+cmd):
+        if not os.path.exists(iutil.getSysroot()+cmd):
             msg = _("%s is missing. Cannot setup firewall.") % (cmd,)
             raise KickstartError(msg)
         else:
@@ -598,7 +605,7 @@ class Firstboot(commands.firstboot.FC3_Firstboot):
                          "/lib/systemd/system/initial-setup-graphical.service",
                          "/lib/systemd/system/initial-setup-text.service")
 
-        if not any(os.path.exists(ROOT_PATH + path) for path in service_paths):
+        if not any(os.path.exists(iutil.getSysroot() + path) for path in service_paths):
             # none of the first boot utilities installed, nothing to do here
             return
 
@@ -607,7 +614,7 @@ class Firstboot(commands.firstboot.FC3_Firstboot):
         if self.firstboot == FIRSTBOOT_SKIP:
             action = "disable"
         elif self.firstboot == FIRSTBOOT_RECONFIG:
-            f = open(ROOT_PATH + "/etc/reconfigSys", "w+")
+            f = open(iutil.getSysroot() + "/etc/reconfigSys", "w+")
             f.close()
 
         iutil.execWithRedirect("systemctl", [action, "firstboot-graphical.service",
@@ -621,7 +628,7 @@ class Group(commands.group.F12_Group):
 
         for grp in self.groupList:
             kwargs = grp.__dict__
-            kwargs.update({"root": ROOT_PATH})
+            kwargs.update({"root": iutil.getSysroot()})
             if not users.createGroup(grp.name, **kwargs):
                 log.error("Group %s already exists, not creating." % grp.name)
 
@@ -692,7 +699,7 @@ class IscsiName(commands.iscsiname.FC6_IscsiName):
 
 class Lang(commands.lang.F19_Lang):
     def execute(self, *args, **kwargs):
-        localization.write_language_configuration(self, ROOT_PATH)
+        localization.write_language_configuration(self, iutil.getSysroot())
 
 # no overrides needed here
 Eula = commands.eula.F20_Eula
@@ -902,7 +909,7 @@ class Logging(commands.logging.FC6_Logging):
 
 class Network(commands.network.RHEL7_Network):
     def execute(self, storage, ksdata, instClass):
-        network.write_network_config(storage, ksdata, instClass, ROOT_PATH)
+        network.write_network_config(storage, ksdata, instClass, iutil.getSysroot())
 
 class MultiPath(commands.multipath.FC6_MultiPath):
     def parse(self, args):
@@ -1313,7 +1320,8 @@ class RootPw(commands.rootpw.F18_RootPw):
             self.lock = True
 
         algo = getPassAlgo(ksdata.authconfig.authconfig)
-        users.setRootPassword(self.password, self.isCrypted, self.lock, algo)
+        users.setRootPassword(self.password, self.isCrypted, self.lock, algo,
+                              iutil.getSysroot())
 
 class SELinux(commands.selinux.FC3_SELinux):
     def execute(self, *args):
@@ -1326,7 +1334,7 @@ class SELinux(commands.selinux.FC3_SELinux):
             return
 
         try:
-            selinux_cfg = SimpleConfigFile(ROOT_PATH+"/etc/selinux/config")
+            selinux_cfg = SimpleConfigFile(iutil.getSysroot()+"/etc/selinux/config")
             selinux_cfg.read()
             selinux_cfg.set(("SELINUX", selinux_states[self.selinux]))
             selinux_cfg.write()
@@ -1393,11 +1401,11 @@ class Timezone(commands.timezone.F18_Timezone):
                         "back to default (America/New_York)." % (self.timezone,))
             self.timezone = "America/New_York"
 
-        timezone.write_timezone_config(self, ROOT_PATH)
+        timezone.write_timezone_config(self, iutil.getSysroot())
 
         # write out NTP configuration (if set)
         if not self.nontp and self.ntpservers:
-            chronyd_conf_path = os.path.normpath(ROOT_PATH + ntp.NTP_CONFIG_FILE)
+            chronyd_conf_path = os.path.normpath(iutil.getSysroot() + ntp.NTP_CONFIG_FILE)
             try:
                 ntp.save_servers_to_config(self.ntpservers,
                                            conf_file_path=chronyd_conf_path)
@@ -1410,7 +1418,7 @@ class User(commands.user.F12_User):
 
         for usr in self.userList:
             kwargs = usr.__dict__
-            kwargs.update({"algo": algo, "root": ROOT_PATH})
+            kwargs.update({"algo": algo, "root": iutil.getSysroot()})
 
             # If the user password came from a kickstart and it is blank we
             # need to make sure the account is locked, not created with an
@@ -1515,7 +1523,7 @@ class ZFCP(commands.zfcp.F14_ZFCP):
 
 class Keyboard(commands.keyboard.F18_Keyboard):
     def execute(self, *args):
-        keyboard.write_keyboard_config(self, ROOT_PATH)
+        keyboard.write_keyboard_config(self, iutil.getSysroot())
 
     def dracutSetupArgs(self, *args):
         return keyboard.dracut_setup_args(self)
@@ -1542,7 +1550,7 @@ class SpokeRegistry(dict):
 
     # pylint: disable-msg=C0103
     def execute(self, storage, ksdata, instClass, users):
-        path = os.path.join(ROOT_PATH, "var", "lib", "inital-setup")
+        path = os.path.join(iutil.getSysroot(), "var", "lib", "inital-setup")
         try:
             os.makedirs(path, 0755)
         except OSError:
@@ -1580,6 +1588,7 @@ commandMap = {
         "logvol": LogVol,
         "multipath": MultiPath,
         "network": Network,
+        "ostreesetup": OSTreeSetup,
         "part": Partition,
         "partition": Partition,
         "raid": Raid,
@@ -1748,7 +1757,7 @@ def runPostScripts(scripts):
             del(os.environ[var])
 
     log.info("Running kickstart %%post script(s)")
-    map (lambda s: s.run(ROOT_PATH), postScripts)
+    map (lambda s: s.run(iutil.getSysroot()), postScripts)
     log.info("All kickstart %%post script(s) have been run")
 
 def runPreScripts(scripts):
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..40ceb07
--- /dev/null
+++ b/pyanaconda/packaging/ostreepayload.py
@@ -0,0 +1,194 @@
+# 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.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()
+        
+        bootmnt = self.storage.mountpoints.get('/boot')
+        if bootmnt is not None:
+            # Okay, now /boot goes back to the physical /, since
+            # that's where ostree --sysroot expects to find it.  The
+            # real fix for the madness is for tools like extlinux to
+            # have a --sysroot option too.
+            bootmnt.format.teardown()
+            bootmnt.teardown()
+            bootmnt.format.setup(bootmnt.format.options, chroot=ROOT_PATH)
+
+        # Set up the sysroot bind mount after this so that we have the
+        # tmp -> sysroot/tmp when executing %post scripts and the
+        # like.
+        self._safeExecWithRedirect("mount", ["--bind", ROOT_PATH, os.path.join(iutil.getSysroot(), 'sysroot')])
+
+        # FIXME - Move extlinux.conf to syslinux.cfg, since that's
+        # what OSTree knows about.
+        sysroot_boot_extlinux = os.path.join(ROOT_PATH, 'boot/extlinux')
+        sysroot_boot_syslinux = os.path.join(ROOT_PATH, 'boot/syslinux')
+        sysroot_boot_loader = os.path.join(ROOT_PATH, 'boot/loader')
+        if os.path.isdir(sysroot_boot_extlinux):
+            assert os.path.isdir(sysroot_boot_loader)
+            orig_extlinux_conf = os.path.join(sysroot_boot_extlinux, 'extlinux.conf')
+            target_syslinux_cfg = os.path.join(sysroot_boot_loader, 'syslinux.cfg')
+            log.info("Moving %s -> %s" % (orig_extlinux_conf, target_syslinux_cfg))
+            os.rename(orig_extlinux_conf, target_syslinux_cfg)
+            # A compatibility bit for OSTree
+            os.mkdir(sysroot_boot_syslinux)
+            os.symlink('../loader/syslinux.cfg', os.path.join(sysroot_boot_syslinux, 'syslinux.cfg'))
+            # And *also* tell syslinux that the config is really in /boot/loader
+            os.symlink('loader/syslinux.cfg', os.path.join(ROOT_PATH, 'boot/syslinux.cfg'))
+
+        # FIXME - stub out firewalld
+        firewalloffline = os.path.join(iutil.getSysroot(), 'usr/bin/firewall-offline-cmd')
+        if not os.path.exists(firewalloffline):
+            os.symlink('true', firewalloffline)
+
+        # OSTree owns the bootloader configuration, so here we give it
+        # the argument list we computed from storage, architecture and
+        # such.
+        set_kargs_args = ["admin", "--sysroot=" + ROOT_PATH,
+                          "instutil", "set-kargs"]
+        set_kargs_args.extend(self.storage.bootloader.boot_args)
+        set_kargs_args.append("root=" + self.storage.rootDevice.fstabSpec)
+        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
diff --git a/pyanaconda/users.py b/pyanaconda/users.py
index d43ad29..e00460e 100644
--- a/pyanaconda/users.py
+++ b/pyanaconda/users.py
@@ -192,7 +192,7 @@ class Users:
         """
 
         childpid = os.fork()
-        root = kwargs.get("root", "/mnt/sysimage")
+        root = kwargs.get("root", iutil.getSysroot())
 
         if not childpid:
             if not root in ["","/"]:
@@ -258,7 +258,7 @@ class Users:
                         available one is used.
         """
         childpid = os.fork()
-        root = kwargs.get("root", "/mnt/sysimage")
+        root = kwargs.get("root", iutil.getSysroot())
 
         if not childpid:
             if not root in ["","/"]:
@@ -358,12 +358,16 @@ class Users:
         else:
             return False
 
-    def checkUserExists(self, username, root="/mnt/sysimage"):
+    def checkUserExists(self, username, root=None):
         childpid = os.fork()
+        if root is not None:
+            rootval = root
+        else:
+            rootval = iutil.getSysroot()
 
         if not childpid:
-            if not root in ["","/"]:
-                os.chroot(root)
+            if not rootval in ["","/"]:
+                os.chroot(rootval)
                 os.chdir("/")
                 del(os.environ["LIBUSER_CONF"])
 
@@ -387,18 +391,47 @@ class Users:
         else:
             return False
 
-    def setUserPassword(self, username, password, isCrypted, lock, algo=None):
-        user = self.admin.lookupUserByName(username)
-
-        if isCrypted:
-            self.admin.setpassUser(user, password, True)
+    def setUserPassword(self, username, password, isCrypted, lock, algo=None, root=None):
+        childpid = os.fork()
+        if root is not None:
+            rootval = root
         else:
-            self.admin.setpassUser(user, cryptPassword(password, algo=algo), True)
+            rootval = iutil.getSysroot()
+
+        if not childpid:
+            if not rootval in ["","/"]:
+                os.chroot(rootval)
+                os.chdir("/")
+                del(os.environ["LIBUSER_CONF"])
+
+            self.admin = libuser.admin()
+            try:
+                user = self.admin.lookupUserByName(username)
+
+                if isCrypted:
+                    self.admin.setpassUser(user, password, True)
+                else:
+                    self.admin.setpassUser(user, cryptPassword(password, algo=algo), True)
 
-        if lock:
-            self.admin.lockUser(user)
+                if lock:
+                    self.admin.lockUser(user)
 
-        self.admin.modifyUser(user)
+                self.admin.modifyUser(user)
+                os._exit(0)
+            except Exception as e:
+                log.critical("Error when setting user password: %s" % str(e))
+                os._exit(1)
+
+        try:
+            (pid, status) = os.waitpid(childpid, 0)
+        except OSError as e:
+            log.critical("exception from waitpid while creating a user: %s %s" % (e.errno, e.strerror))
+            return False
+
+        if os.WIFEXITED(status) and (os.WEXITSTATUS(status) == 0):
+            return True
+        else:
+            return False
 
-    def setRootPassword(self, password, isCrypted=False, isLocked=False, algo=None):
-        return self.setUserPassword("root", password, isCrypted, isLocked, algo)
+    def setRootPassword(self, password, isCrypted=False, isLocked=False, algo=None, root=None):
+        return self.setUserPassword("root", password, isCrypted, isLocked, algo, root)
-- 
1.8.3.1

>From cad071d736cb35e67245ca11cf8750931c0d98ba Mon Sep 17 00:00:00 2001
From: Colin Walters <walters verbum org>
Date: Tue, 11 Mar 2014 09:33:36 -0400
Subject: [PATCH] ostreesetup: New command

This tells the installer to handle an OSTree repository.
---
 pykickstart/commands/__init__.py    |  3 +-
 pykickstart/commands/ostreesetup.py | 74 +++++++++++++++++++++++++++++++++++++
 pykickstart/handlers/control.py     |  2 +
 tests/commands/ostreesetup.py       | 37 +++++++++++++++++++
 4 files changed, 115 insertions(+), 1 deletion(-)
 create mode 100644 pykickstart/commands/ostreesetup.py
 create mode 100644 tests/commands/ostreesetup.py

diff --git a/pykickstart/commands/__init__.py b/pykickstart/commands/__init__.py
index c5145ba..ed4d649 100644
--- a/pykickstart/commands/__init__.py
+++ b/pykickstart/commands/__init__.py
@@ -21,6 +21,7 @@ import authconfig, autopart, autostep, bootloader, btrfs, clearpart, cdrom, devi
 import deviceprobe, displaymode, dmraid, driverdisk, eula, fcoe, firewall, firstboot
 import group, harddrive, ignoredisk, interactive, iscsi, iscsiname, key, keyboard, lang
 import langsupport, lilocheck, liveimg, logging, logvol, mediacheck, method, monitor
-import mouse, multipath, network, nfs, partition, raid, realm, reboot, repo, rescue
+import mouse, multipath, network, nfs, ostreesetup
+import partition, raid, realm, reboot, repo, rescue
 import rootpw, selinux, services, skipx, sshpw, timezone, updates, upgrade, url, user
 import unsupported_hardware, vnc, volgroup, xconfig, zerombr, zfcp
diff --git a/pykickstart/commands/ostreesetup.py b/pykickstart/commands/ostreesetup.py
new file mode 100644
index 0000000..b722c2c
--- /dev/null
+++ b/pykickstart/commands/ostreesetup.py
@@ -0,0 +1,74 @@
+#
+# Copyright (C) 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.
+#
+
+from pykickstart.base import *
+from pykickstart.errors import *
+from pykickstart.options import *
+
+import gettext
+_ = lambda x: gettext.ldgettext("pykickstart", x)
+
+class F20_OSTreeSetup(KickstartCommand):
+    removedKeywords = KickstartCommand.removedKeywords
+    removedAttrs = KickstartCommand.removedAttrs
+
+    def __init__(self, *args, **kwargs):
+        KickstartCommand.__init__(self, *args, **kwargs)
+        self.op = self._getParser()
+        self.osname = kwargs.get('osname', None)
+        self.remote = kwargs.get("remote", self.osname)
+        self.url = kwargs.get('url', None)
+        self.ref = kwargs.get('ref', None)
+        self.noGpg = kwargs.get('noGpg', True)
+
+    def __str__(self):
+        retval = KickstartCommand.__str__(self)
+
+        if self.osname:
+            retval += "# OSTree setup\n"
+            retval += "ostreesetup %s\n" % self._getArgsAsStr()
+
+        return retval
+
+    def _getArgsAsStr(self):
+        retval = ""
+        if self.osname:
+            retval += "--osname=%s" % self.osname
+        if self.remote:
+            retval += "--remote=%s" % self.remote
+        if self.url:
+            retval += "--url=%s" % self.url
+        if self.ref:
+            retval += "--ref=%s" % self.ref
+        if self.noGpg:
+            retval += "--nogpg"
+        return retval
+
+    def _getParser(self):
+        op = KSOptionParser()
+        op.add_option("--osname", dest="osname", required=1)
+        op.add_option("--remote", dest="remote")
+        op.add_option("--url", dest="url", required=1)
+        op.add_option("--ref", dest="ref", required=1)
+        op.add_option("--nogpg", action="store_true")
+        return op
+
+    def parse(self, args):
+        (opts, extra) = self.op.parse_args(args=args, lineno=self.lineno)
+        self._setToSelf(self.op, opts)
+        return self
diff --git a/pykickstart/handlers/control.py b/pykickstart/handlers/control.py
index 9d3bbf0..7b2bc0c 100644
--- a/pykickstart/handlers/control.py
+++ b/pykickstart/handlers/control.py
@@ -1060,6 +1060,7 @@ commandMap = {
         "multipath": multipath.FC6_MultiPath,
         "network": network.F20_Network,
         "nfs": nfs.FC6_NFS,
+        "ostreesetup": ostreesetup.F20_OSTreeSetup,
         "part": partition.F20_Partition,
         "partition": partition.F20_Partition,
         "poweroff": reboot.F18_Reboot,
@@ -1349,6 +1350,7 @@ commandMap = {
         "multipath": multipath.FC6_MultiPath,
         "network": network.RHEL7_Network,
         "nfs": nfs.FC6_NFS,
+        "ostreesetup": ostreesetup.F20_OSTreeSetup,
         "part": partition.F20_Partition,
         "partition": partition.F20_Partition,
         "poweroff": reboot.F18_Reboot,
diff --git a/tests/commands/ostreesetup.py b/tests/commands/ostreesetup.py
new file mode 100644
index 0000000..b837e5d
--- /dev/null
+++ b/tests/commands/ostreesetup.py
@@ -0,0 +1,37 @@
+#
+# Colin Walters <walters redhat com>
+#
+# Copyright 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.  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.
+#
+
+import unittest
+from tests.baseclass import *
+
+class F20_TestCase(CommandTest):
+    def runTest(self):
+        # pass
+        self.assert_parse("ostreesetup --os=fedora-atomic --url http://example.com/repo --ref fedora-atomic/sometest/base/core\n")
+        self.assert_parse("ostreesetup --os=fedora-atomic --remote=testremote --url http://example.com/repo --ref=fedora-atomic/sometest/base/core\n")
+
+        # fail - we have required arguments
+        self.assert_parse_error("ostreesetup", KickstartValueError)
+        self.assert_parse_error("ostreesetup --os=fedora-atomic", KickstartValueError)
+        self.assert_parse_error("ostreesetup --os=fedora-atomic --url=http://example.com/repo";, KickstartValueError)
+        self.assert_parse_error("ostreesetup --bacon=tasty")
+
+if __name__ == "__main__":
+    unittest.main()
-- 
1.8.3.1

>From 034ef92643ae8f79febae4cb5f61d654b970cc54 Mon Sep 17 00:00:00 2001
From: Colin Walters <walters verbum org>
Date: Wed, 19 Mar 2014 10:56:24 -0400
Subject: [PATCH] Add "targetSysroot" which differs from ROOT_PATH

For OSTree, the location of the OS checkout (and e.g. /etc/fstab) is
really in /ostree/deploy/$osname/deploy/$revision/etc/fstab.

In order to properly support OSTree, Blivet will need to gain an
understanding of the separation between the physical system / and the
target root.

This patch will be used in Anaconda, which will set the targetSysroot
attribute after the root being installed is laid out.

After that, when we call write(), the fstab data will be correctly
written into the target root.
---
 blivet/__init__.py | 50 +++++++++++++++++++++++++++-----------------------
 blivet/util.py     |  4 ++--
 2 files changed, 29 insertions(+), 25 deletions(-)

diff --git a/blivet/__init__.py b/blivet/__init__.py
index 068c310..9ca661e 100644
--- a/blivet/__init__.py
+++ b/blivet/__init__.py
@@ -31,6 +31,7 @@ __version__ = '0.18.32'
 ##
 isys = None
 ROOT_PATH = '/'
+targetSysroot = ROOT_PATH
 shortProductName = 'blivet'
 productName = 'blivet'
 bootLoaderError = Exception
@@ -100,6 +101,7 @@ log = logging.getLogger("blivet")
 def enable_installer_mode():
     global isys
     global ROOT_PATH
+    global targetSysroot
     global shortProductName
     global productName
     global get_bootloader
@@ -116,6 +118,8 @@ def enable_installer_mode():
     from pyanaconda.errors import errorHandler
     from pyanaconda.errors import ERROR_RAISE
 
+    targetSysroot = ROOT_PATH
+
     from pyanaconda.anaconda_log import program_log_lock
     util.program_log_lock = program_log_lock
 
@@ -202,7 +206,7 @@ def writeEscrowPackets(storage):
     backupPassphrase = generateBackupPassphrase()
 
     try:
-        escrowDir = ROOT_PATH + "/root"
+        escrowDir = targetSysroot + "/root"
         log.debug("escrow: writing escrow packets to %s", escrowDir)
         util.makedirs(escrowDir)
         for device in escrowDevices:
@@ -1665,22 +1669,22 @@ class Blivet(object):
         return list(pkgs)
 
     def write(self):
-        if not os.path.isdir("%s/etc" % ROOT_PATH):
-            os.mkdir("%s/etc" % ROOT_PATH)
+        if not os.path.isdir("%s/etc" % targetSysroot):
+            os.mkdir("%s/etc" % targetSysroot)
 
         self.fsset.write()
         self.makeMtab()
-        self.iscsi.write(ROOT_PATH, self)
-        self.fcoe.write(ROOT_PATH)
-        self.zfcp.write(ROOT_PATH)
-        write_dasd_conf(self.dasd, ROOT_PATH)
+        self.iscsi.write(targetSysroot, self)
+        self.fcoe.write(targetSysroot)
+        self.zfcp.write(targetSysroot)
+        write_dasd_conf(self.dasd, targetSysroot)
 
     def turnOnSwap(self, upgrading=None):
-        self.fsset.turnOnSwap(rootPath=ROOT_PATH,
+        self.fsset.turnOnSwap(rootPath=targetSysroot,
                               upgrading=upgrading)
 
     def mountFilesystems(self, raiseErrors=None, readOnly=None, skipRoot=False):
-        self.fsset.mountFilesystems(rootPath=ROOT_PATH,
+        self.fsset.mountFilesystems(rootPath=targetSysroot,
                                     raiseErrors=raiseErrors,
                                     readOnly=readOnly, skipRoot=skipRoot)
 
@@ -1797,7 +1801,7 @@ class Blivet(object):
     def makeMtab(self):
         path = "/etc/mtab"
         target = "/proc/self/mounts"
-        path = os.path.normpath("%s/%s" % (ROOT_PATH, path))
+        path = os.path.normpath("%s/%s" % (targetSysroot, path))
 
         if os.path.islink(path):
             # return early if the mtab symlink is already how we like it
@@ -2130,7 +2134,7 @@ def mountExistingSystem(fsset, rootDevice,
                         allowDirty=None, dirtyCB=None,
                         readOnly=None):
     """ Mount filesystems specified in rootDevice's /etc/fstab file. """
-    rootPath = ROOT_PATH
+    rootPath = targetSysroot
     if dirtyCB is None:
         dirtyCB = lambda l: False
 
@@ -2172,7 +2176,7 @@ def mountExistingSystem(fsset, rootDevice,
     if dirtyDevs and (not allowDirty or dirtyCB(dirtyDevs)):
         raise DirtyFSError("\n".join(dirtyDevs))
 
-    fsset.mountFilesystems(rootPath=ROOT_PATH, readOnly=readOnly, skipRoot=True)
+    fsset.mountFilesystems(rootPath=targetSysroot, readOnly=readOnly, skipRoot=True)
 
 
 class BlkidTab(object):
@@ -2529,7 +2533,7 @@ class FSSet(object):
                 loop mounts?
         """
         if not chroot or not os.path.isdir(chroot):
-            chroot = ROOT_PATH
+            chroot = targetSysroot
 
         path = "%s/etc/fstab" % chroot
         if not os.access(path, os.R_OK):
@@ -2727,10 +2731,10 @@ class FSSet(object):
 
     def mkDevRoot(self):
         root = self.rootDevice
-        dev = "%s/%s" % (ROOT_PATH, root.path)
-        if not os.path.exists("%s/dev/root" %(ROOT_PATH,)) and os.path.exists(dev):
+        dev = "%s/%s" % (targetSysroot, root.path)
+        if not os.path.exists("%s/dev/root" %(targetSysroot,)) and os.path.exists(dev):
             rdev = os.stat(dev).st_rdev
-            os.mknod("%s/dev/root" % (ROOT_PATH,), stat.S_IFBLK | 0600, rdev)
+            os.mknod("%s/dev/root" % (targetSysroot,), stat.S_IFBLK | 0600, rdev)
 
     @property
     def swapDevices(self):
@@ -2742,7 +2746,7 @@ class FSSet(object):
 
     @property
     def rootDevice(self):
-        for path in ["/", ROOT_PATH]:
+        for path in ["/", ROOT_PATH, targetSysroot]:
             for device in self.devices:
                 try:
                     mountpoint = device.format.mountpoint
@@ -2755,19 +2759,19 @@ class FSSet(object):
     def write(self):
         """ write out all config files based on the set of filesystems """
         # /etc/fstab
-        fstab_path = os.path.normpath("%s/etc/fstab" % ROOT_PATH)
+        fstab_path = os.path.normpath("%s/etc/fstab" % targetSysroot)
         fstab = self.fstab()
         open(fstab_path, "w").write(fstab)
 
         # /etc/crypttab
-        crypttab_path = os.path.normpath("%s/etc/crypttab" % ROOT_PATH)
+        crypttab_path = os.path.normpath("%s/etc/crypttab" % targetSysroot)
         crypttab = self.crypttab()
         origmask = os.umask(0077)
         open(crypttab_path, "w").write(crypttab)
         os.umask(origmask)
 
         # /etc/mdadm.conf
-        mdadm_path = os.path.normpath("%s/etc/mdadm.conf" % ROOT_PATH)
+        mdadm_path = os.path.normpath("%s/etc/mdadm.conf" % targetSysroot)
         mdadm_conf = self.mdadmConf()
         if mdadm_conf:
             open(mdadm_path, "w").write(mdadm_conf)
@@ -2900,11 +2904,11 @@ def getReleaseString():
     relVer = None
 
     try:
-        relArch = util.capture_output(["arch"], root=ROOT_PATH).strip()
+        relArch = util.capture_output(["arch"], root=targetSysroot).strip()
     except:
         relArch = None
 
-    filename = "%s/etc/redhat-release" % ROOT_PATH
+    filename = "%s/etc/redhat-release" % targetSysroot
     if os.access(filename, os.R_OK):
         with open(filename) as f:
             try:
@@ -3000,7 +3004,7 @@ class Root(object):
 def parseFSTab(devicetree, chroot=None):
     """ parse /etc/fstab and return a tuple of a mount dict and swap list """
     if not chroot or not os.path.isdir(chroot):
-        chroot = ROOT_PATH
+        chroot = targetSysroot 
 
     mounts = {}
     swaps = []
diff --git a/blivet/util.py b/blivet/util.py
index d43b252..179823b 100644
--- a/blivet/util.py
+++ b/blivet/util.py
@@ -277,13 +277,13 @@ def makedirs(path):
 
 def copy_to_system(source):
     # do the import now because enable_installer_mode() has finally been called.
-    from . import ROOT_PATH
+    from . import targetSysroot
 
     if not os.access(source, os.R_OK):
         log.info("copy_to_system: source '%s' does not exist." % source)
         return False
 
-    target = ROOT_PATH + source
+    target = targetSysroot + source
     target_dir = os.path.dirname(target)
     log.debug("copy_to_system: '%s' -> '%s'." % (source, target))
     if not os.path.isdir(target_dir):
-- 
1.8.3.1


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]