From 2a99e198c40b22fd2f3bf3988902b8d02817ea82 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 30 Apr 2014 19:26:37 -0400 Subject: [PATCH 10/20] Add RPMOSTreePayload This is a new backend that uses ostree to install. However, it's named RPMOSTree because a future iteration will use rpm-ostree which will support a hybrid of tree + RPM packages. Resolves: rhbz#1113535 Port of rpmostreepayload patches from master commit 131cbcec9c113dd5f66d14d440297d50b2900e01 --- data/post-scripts/80-setfilecons.ks | 3 + po/POTFILES.in | 1 + pyanaconda/__init__.py | 5 +- pyanaconda/bootloader.py | 44 +++--- pyanaconda/install.py | 16 ++ pyanaconda/packaging/__init__.py | 6 + pyanaconda/packaging/rpmostreepayload.py | 246 +++++++++++++++++++++++++++++++ 7 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 pyanaconda/packaging/rpmostreepayload.py diff --git a/data/post-scripts/80-setfilecons.ks b/data/post-scripts/80-setfilecons.ks index 93ac962..f0d0414 100644 --- a/data/post-scripts/80-setfilecons.ks +++ b/data/post-scripts/80-setfilecons.ks @@ -4,6 +4,9 @@ restorecon -ir /etc/sysconfig/network-scripts /var/lib /etc/lvm \ /dev /etc/iscsi /var/lib/iscsi /root /var/lock /var/log \ /etc/modprobe.d /etc/sysconfig /var/cache/yum +# Also relabel the OSTree variants of the normal mounts (if they exist) +restorecon -ir /var/roothome /var/home /var/opt /var/srv /var/media /var/mnt + restorecon -i /etc/rpm/macros /etc/dasd.conf /etc/zfcp.conf /lib64 /usr/lib64 \ /etc/blkid.tab* /etc/mtab /etc/fstab /etc/resolv.conf \ /etc/modprobe.conf* /var/log/*tmp /etc/crypttab \ diff --git a/po/POTFILES.in b/po/POTFILES.in index 4b341fc..8df1636 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -25,6 +25,7 @@ pyanaconda/packaging/__init__.py pyanaconda/packaging/livepayload.py pyanaconda/packaging/tarpayload.py pyanaconda/packaging/yumpayload.py +pyanaconda/packaging/rpmostreepayload.py # Interfaces pyanaconda/ui/common.py diff --git a/pyanaconda/__init__.py b/pyanaconda/__init__.py index fad7a90..34d46e0 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.seen: + from pyanaconda.packaging.rpmostreepayload import RPMOSTreePayload + klass = RPMOSTreePayload + 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 860bde3..b6d5941 100644 --- a/pyanaconda/bootloader.py +++ b/pyanaconda/bootloader.py @@ -37,6 +37,7 @@ from pyanaconda.flags import flags from blivet.errors import StorageError from blivet.fcoe import fcoe import pyanaconda.network +from pyanaconda.packaging.rpmostreepayload import RPMOSTreePayload from pyanaconda.nm import nm_device_hwaddress from blivet import platform from pyanaconda.i18n import _, N_ @@ -2355,6 +2356,23 @@ 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(storage=storage, + payload=payload, + keyboard=ksdata.keyboard) + try: + storage.bootloader.write() + except BootLoaderError as e: + log.error("bootloader.write failed: %s" % e) + if errorHandler.cb(e) == ERROR_RAISE: + raise + def writeBootLoader(storage, payload, instClass, ksdata): """ Write bootloader configuration to disk. @@ -2362,14 +2380,19 @@ def writeBootLoader(storage, payload, instClass, ksdata): image. We only have to add images for the non-default kernels and adjust the default to reflect whatever the default variant is. """ - from pyanaconda.errors import errorHandler, ERROR_RAISE - if not storage.bootloader.skip_bootloader: stage1_device = storage.bootloader.stage1_device log.info("bootloader stage1 target device is %s" % stage1_device.name) stage2_device = storage.bootloader.stage2_device log.info("bootloader stage2 target device is %s" % stage2_device.name) + if isinstance(payload, RPMOSTreePayload): + 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: @@ -2414,19 +2437,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 68551cc..d771f5b 100644 --- a/pyanaconda/install.py +++ b/pyanaconda/install.py @@ -107,6 +107,17 @@ def doConfiguration(storage, payload, ksdata, instClass): progressQ.send_complete() +def moveBootMntToPhysical(storage): + """Move the /boot mount to /mnt/sysimage/boot.""" + if iutil.getSysroot() == iutil.getTargetPhysicalRoot(): + return + bootmnt = storage.mountpoints.get('/boot') + if bootmnt is None: + return + bootmnt.format.teardown() + bootmnt.teardown() + bootmnt.format.setup(bootmnt.format.options, chroot=iutil.getTargetPhysicalRoot()) + def doInstall(storage, payload, ksdata, instClass): """Perform an installation. This method takes the ksdata as prepared by the UI (the first hub, in graphical mode) and applies it to the disk. @@ -189,6 +200,8 @@ def doInstall(storage, payload, ksdata, instClass): rootmnt.setup() rootmnt.format.setup(rootmnt.format.options, chroot=iutil.getTargetPhysicalRoot()) + payload.prepareMountTargets(storage) + # Everything else goes in the target root, including /boot # since the bootloader code will expect to find /boot # inside the chroot. @@ -202,6 +215,9 @@ def doInstall(storage, payload, ksdata, instClass): writeBootLoader(storage, payload, instClass, ksdata) with progress_report(_("Performing post-installation setup tasks")): + # Now, let's reset the state here so that the payload has + # /boot in the system root. + moveBootMntToPhysical(storage) payload.postInstall() progressQ.send_complete() diff --git a/pyanaconda/packaging/__init__.py b/pyanaconda/packaging/__init__.py index 9b97149..4a5e793 100644 --- a/pyanaconda/packaging/__init__.py +++ b/pyanaconda/packaging/__init__.py @@ -128,6 +128,12 @@ class Payload(object): """ Reset the instance, not including ksdata. """ pass + def prepareMountTargets(self, storage): + """Run when physical storage is mounted, but other mount points may + not exist. Used by the RPMOSTreePayload subclass. + """ + pass + ### ### METHODS FOR WORKING WITH REPOSITORIES ### diff --git a/pyanaconda/packaging/rpmostreepayload.py b/pyanaconda/packaging/rpmostreepayload.py new file mode 100644 index 0000000..1b24a0f --- /dev/null +++ b/pyanaconda/packaging/rpmostreepayload.py @@ -0,0 +1,246 @@ +# 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 +# + +import os +import shutil + +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.packaging import ArchivePayload, PayloadInstallError +import pyanaconda.errors as errors + +class RPMOSTreePayload(ArchivePayload): + """ A RPMOSTreePayload deploys a tree (possibly with layered packages) onto the target system. """ + def __init__(self, data): + super(RPMOSTreePayload, self).__init__(data) + + def setup(self, storage): + super(RPMOSTreePayload, 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(spec="500 MB") + + 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 errors.errorHandler.cb(exn) == errors.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=" + iutil.getTargetPhysicalRoot(), + "init-fs", iutil.getTargetPhysicalRoot()]) + + repo_arg = "--repo=" + iutil.getTargetPhysicalRoot() + '/ostree/repo' + + # Set up the chosen remote + remote_args = [repo_arg, "remote", "add"] + if ((hasattr(ostreesetup, 'noGpg') and ostreesetup.noGpg) or + (hasattr(ostreesetup, 'nogpg') and 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(iutil.getTargetPhysicalRoot()) + sysroot = OSTree.Sysroot.new(sysroot_path) + sysroot.load(cancellable) + + repo = sysroot.get_repo(None)[1] + repo.set_disable_fsync(True) + 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) + + progressQ.send_message(_("Preparing deployment of %s") % (ostreesetup.ref, )) + + self._safeExecWithRedirect("ostree", + ["admin", "--sysroot=" + iutil.getTargetPhysicalRoot(), + "os-init", ostreesetup.osname]) + + admin_deploy_args = ["admin", "--sysroot=" + iutil.getTargetPhysicalRoot(), + "deploy", "--os=" + ostreesetup.osname] + + admin_deploy_args.append(ostreesetup.remote + ':' + ostreesetup.ref) + + log.info("ostree admin deploy starting") + progressQ.send_message(_("Deployment starting: %s") % (ostreesetup.ref, )) + self._safeExecWithRedirect("ostree", admin_deploy_args) + log.info("ostree admin deploy complete") + progressQ.send_message(_("Deployment complete: %s") % (ostreesetup.ref, )) + + # Reload now that we've deployed, find the path to the new deployment + sysroot.load(None) + deployments = sysroot.get_deployments() + assert len(deployments) > 0 + deployment = deployments[0] + deployment_path = sysroot.get_deployment_directory(deployment) + iutil.setSysroot(deployment_path.get_path()) + + varroot = iutil.getTargetPhysicalRoot() + '/ostree/deploy/' + ostreesetup.osname + '/var' + + # This is a bit of a hack; we precreate the targets of + # possible mounts of legacy paths like /home and /opt so the + # installer/%post scripts can find them. In particular, + # Anaconda itself writes to /root/anaconda-ks.cfg. What we + # really should do is export this data in some way the + # installer can read reliably. Right now it's just encoded in + # systemd-tmpfiles. + for (dname, mode) in [('root', 0700), ('home', 0755), + ('opt', 0755), ('srv', 0755), + ('media', 0755), ('mnt', 0755)]: + linksrc = iutil.getSysroot() + '/' + dname + if os.path.islink(linksrc) and not os.path.isdir(linksrc): + linkdata = os.readlink(linksrc) + if linkdata.startswith('var/'): + linkdest = varroot + '/' + linkdata[4:] + log.info("Creating %s" % linkdest) + os.mkdir(linkdest, mode) + + # Copy specific bootloader data files from the deployment + # checkout to the target root. See + # https://bugzilla.gnome.org/show_bug.cgi?id=726757 This + # happens once, at installation time. + # extlinux ships its modules directly in the RPM in /boot. + # For GRUB2, Anaconda installs device.map there. We may need + # to add other bootloaders here though (if they can't easily + # be fixed to *copy* data into /boot at install time, instead + # of shipping it in the RPM). + physboot = iutil.getTargetPhysicalRoot() + '/boot' + sysboot = iutil.getSysroot() + '/boot' + for fname in ['extlinux', 'grub2']: + srcpath = os.path.join(sysboot, fname) + if os.path.isdir(srcpath): + log.info("Copying bootloader data: " + fname) + shutil.copytree(srcpath, os.path.join(physboot, fname)) + + def prepareMountTargets(self, storage): + ostreesetup = self.data.ostreesetup + + varroot = iutil.getTargetPhysicalRoot() + '/ostree/deploy/' + ostreesetup.osname + '/var' + + # Set up bind mounts as if we've booted the target system, so + # that %post script work inside the target. + binds = [(iutil.getTargetPhysicalRoot(), + iutil.getSysroot() + '/sysroot'), + (varroot, + iutil.getSysroot() + '/var'), + (iutil.getSysroot() + '/usr', None)] + + for (src, dest) in binds: + self._safeExecWithRedirect("mount", + ["--bind", src, dest if dest else src]) + if dest is None: + self._safeExecWithRedirect("mount", + ["--bind", "-o", "ro", src, src]) + + def postInstall(self): + super(RPMOSTreePayload, self).postInstall() + + physboot = iutil.getTargetPhysicalRoot() + '/boot' + + # If we're using extlinux, rename extlinux.conf to + # syslinux.cfg, since that's what OSTree knows about. + # syslinux upstream supports both, but I'd say that upstream + # using syslinux.cfg is somewhat preferred. + physboot_extlinux = physboot + '/extlinux' + if os.path.isdir(physboot_extlinux): + physboot_syslinux = physboot + '/syslinux' + physboot_loader = physboot + '/loader' + assert os.path.isdir(physboot_loader) + orig_extlinux_conf = physboot_extlinux + '/extlinux.conf' + target_syslinux_cfg = physboot_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(physboot_syslinux) + os.symlink('../loader/syslinux.cfg', physboot_syslinux + '/syslinux.cfg') + # And *also* tell syslinux that the config is really in /boot/loader + os.symlink('loader/syslinux.cfg', physboot + '/syslinux.cfg') + + # 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=" + iutil.getTargetPhysicalRoot(), + "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(iutil.getTargetPhysicalRoot()) + fixfiles, except + # with a better name and semantics. + self._safeExecWithRedirect("ostree", + ["admin", "--sysroot=" + iutil.getTargetPhysicalRoot(), + "instutil", "selinux-ensure-labeled", iutil.getTargetPhysicalRoot(), ""]) -- 1.9.3