# # Installation tasks # # Copyright (C) 2019 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. # import itertools import os import shutil import parted from datetime import timedelta from time import sleep from blivet import callbacks as blivet_callbacks, util as blivet_util, arch, blockdev from blivet.errors import FSResizeError, FormatResizeError, StorageError from blivet.util import get_current_entropy from blivet.devicelibs.lvm import HAVE_LVMDEVICES from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.i18n import _ from pyanaconda.core.util import join_paths, execWithRedirect from pyanaconda.core.path import make_directories from pyanaconda.core.configuration.anaconda import conf from pyanaconda.modules.common.constants.objects import ISCSI, FCOE, NVME from pyanaconda.modules.common.constants.services import STORAGE from pyanaconda.modules.common.errors.installation import StorageInstallationError from pyanaconda.modules.common.task import Task log = get_module_logger(__name__) __all__ = ["CreateStorageLayoutTask", "MountFilesystemsTask", "WriteConfigurationTask"] class CreateStorageLayoutTask(Task): """Installation task for execution of the storage configuration.""" def __init__(self, storage, entropy_timeout=600): """Create a new task. :param storage: the storage model :param entropy_timeout: a number of seconds for entropy gathering """ super().__init__() self._storage = storage self._entropy_timeout = entropy_timeout @property def name(self): return "Create storage layout" def run(self): """Do the execution. :raise: StorageInstallationError if the execution fails """ if conf.target.is_directory: log.debug("Don't create the storage layout during " "the installation to a directory.") return register = blivet_callbacks.create_new_callbacks_register( create_format_pre=self._report_message, resize_format_pre=self._report_message, wait_for_entropy=self._wait_for_entropy ) try: self._turn_on_filesystems( self._storage, callbacks=register ) except (FSResizeError, FormatResizeError) as e: log.exception("Failed to resize device %s: %s", e.details, str(e)) message = _("An error occurred while resizing the device {}: {}").format( e.details, str(e) ) raise StorageInstallationError(message) from None except StorageError as e: log.exception("Failed to create storage layout: %s", str(e)) raise StorageInstallationError(str(e)) from None def _report_message(self, data): """Report a Blivet message. :param data: Blivet's callback data """ self.report_progress(data.msg) def _wait_for_entropy(self, data): """Wait for entropy. :param data: Blivet's callback data :return: True if we are out of time, otherwise False """ log.debug(data.msg) required_entropy = data.min_entropy total_time = self._entropy_timeout current_time = 0 while True: # Report the current status. current_entropy = get_current_entropy() current_percents = min(int(current_entropy / required_entropy * 100), 100) remaining_time = max(total_time - current_time, 0) self._report_entropy_message(current_percents, remaining_time) sleep(5) current_time += 5 # Enough entropy gathered. if current_percents == 100: return False # Out of time. if remaining_time == 0: return True def _report_entropy_message(self, percents, time): """Report an entropy message. :param percents: the percentage of gathered entropy :param time: a number of seconds of remaining time """ if percents == 100: self.report_progress(_("Gathering entropy 100%")) return if time == 0: self.report_progress(_("Gathering entropy (time ran out)")) return message = _("Gathering entropy {percents}% (remaining time {time})").format( percents=percents, time=timedelta(seconds=time) ) self.report_progress(message) def _turn_on_filesystems(self, storage, callbacks=None): """Perform installer-specific execution of storage configuration. :param storage: the storage object :type storage: an instance of InstallerStorage :param callbacks: callbacks to be invoked when actions are executed :type callbacks: return value of the :func:`blivet.callbacks.create_new_callbacks_register` """ storage.devicetree.teardown_all() storage.do_it(callbacks) self._setup_bootable_devices(storage) storage.dump_state("final") storage.turn_on_swap() def _setup_bootable_devices(self, storage): """Set up the bootable devices. Mark the boot devices as bootable. :param storage: an instance of the storage """ if storage.bootloader.skip_bootloader: return if storage.bootloader.stage2_bootable: boot = storage.boot_device else: boot = storage.bootloader.stage1_device if boot.type == "mdarray": boot_devs = boot.parents else: boot_devs = [boot] for dev in boot_devs: if not hasattr(dev, "bootable"): log.info("Skipping %s, not bootable", dev) continue # Dos labels can only have one partition marked as active # and unmarking ie the windows partition is not a good idea skip = False if dev.disk.format.parted_disk.type == "msdos": for p in dev.disk.format.parted_disk.partitions: if p.type == parted.PARTITION_NORMAL and \ p.getFlag(parted.PARTITION_BOOT): skip = True break # GPT labeled disks should only have bootable set on the # EFI system partition (parted sets the EFI System GUID on # GPT partitions with the boot flag) if dev.disk.format.label_type == "gpt" and \ dev.format.type not in ["efi", "macefi"]: skip = True if skip: log.info("Skipping %s", dev.name) continue # hfs+ partitions on gpt can't be marked bootable via parted if dev.disk.format.parted_disk.type != "gpt" or \ dev.format.type not in ["hfs+", "macefi"]: log.info("setting boot flag on %s", dev.name) dev.bootable = True # Set the boot partition's name on disk labels that support it if dev.parted_partition.disk.supportsFeature(parted.DISK_TYPE_PARTITION_NAME): ped_partition = dev.parted_partition.getPedPartition() ped_partition.set_name(dev.format.name) log.info("Setting label on %s to '%s'", dev, dev.format.name) dev.disk.setup() dev.disk.format.commit_to_disk() class MountFilesystemsTask(Task): """Installation task for mounting the filesystems.""" def __init__(self, storage): """Create a new task.""" super().__init__() self._storage = storage @property def name(self): return "Mount filesystems" def run(self): """Mount the filesystems.""" self._storage.mount_filesystems() class WriteConfigurationTask(Task): """Installation task for writing out the storage configuration.""" def __init__(self, storage): """Create a new task.""" super().__init__() self._storage = storage @property def name(self): return "Write the storage configuration" def run(self): """Mount the filesystems.""" if conf.target.is_directory: log.debug("Don't write the storage configuration " "during the installation to a directory.") return self._write_storage_configuration(self._storage) def _write_storage_configuration(self, storage, sysroot=None): """Write the storage configuration to sysroot. :param storage: the storage object :param sysroot: a path to the target OS installation """ if sysroot is None: sysroot = conf.target.system_root if not os.path.isdir("%s/etc" % sysroot): os.mkdir("%s/etc" % sysroot) self._write_escrow_packets(storage, sysroot) storage.make_mtab() storage.fsset.write() self._write_lvm_devices_file(self._storage, sysroot) iscsi_proxy = STORAGE.get_proxy(ISCSI) iscsi_proxy.WriteConfiguration() fcoe_proxy = STORAGE.get_proxy(FCOE) fcoe_proxy.WriteConfiguration() nvme_proxy = STORAGE.get_proxy(NVME) nvme_proxy.WriteConfiguration() self._write_s390_device_config(sysroot) def _write_escrow_packets(self, storage, sysroot): """Write the escrow packets. :param storage: the storage object :type storage: an instance of InstallerStorage :param sysroot: a path to the target OS installation :type sysroot: str """ escrow_devices = [ d for d in storage.devices if d.format.type == 'luks' and d.format.escrow_cert ] if not escrow_devices: return log.debug("escrow: write_escrow_packets start") backup_passphrase = blockdev.crypto.generate_backup_passphrase() try: escrow_dir = sysroot + "/root" log.debug("escrow: writing escrow packets to %s", escrow_dir) blivet_util.makedirs(escrow_dir) for device in escrow_devices: log.debug("escrow: device %s: %s", repr(device.path), repr(device.format.type)) device.format.escrow(escrow_dir, backup_passphrase) except (OSError, RuntimeError) as e: # TODO: real error handling log.error("failed to store encryption key: %s", e) log.debug("escrow: write_escrow_packets done") @staticmethod def _write_lvm_devices_file(storage, sysroot): """Create the LVM devices file for the target system. Adds all present PVs according to https://bugzilla.redhat.com/show_bug.cgi?id=2011329#c9 The file is located at /etc/lvm/devices/system.devices :param Blivet storage: instance of Blivet or a subclass :param str sysroot: path to the target OS installation """ if conf.target.is_image: log.debug("Don't write the LVM devices file during image installation.") return if not HAVE_LVMDEVICES: return for device in itertools.chain(storage.devices, storage.devicetree._hidden): if device.format and device.format.type == "lvmpv": device.format.lvmdevices_add() in_filename = "/etc/lvm/devices/system.devices" out_filename = join_paths(sysroot, in_filename) if os.path.exists(in_filename): make_directories(os.path.dirname(out_filename)) shutil.copyfile(in_filename, out_filename) def _write_s390_device_config(self, sysroot): """Copy entire persistent config of any s390 devices to sysroot. This includes config imported from initrd as well as anything the user configured via the installer user interface. :param sysroot: a path to the target OS installation """ if arch.is_s390(): execWithRedirect("chzdev", ["--export", "/tmp/zdev.config", "--all", "--type", "--persistent", "--verbose"]) execWithRedirect("chzdev", ["--import", "/tmp/zdev.config", "--persistent", "--yes", "--no-root-update", "--force", "--verbose", "--base", "/etc=%s/etc" % sysroot])