Mini Shell
#
# 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 os
import parted
from datetime import timedelta
from time import sleep
from blivet import callbacks as blivet_callbacks, util as blivet_util, arch
from blivet.errors import FSResizeError, FormatResizeError, StorageError
from blivet.util import get_current_entropy
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.i18n import _
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.modules.common.constants.objects import ISCSI, FCOE, ZFCP
from pyanaconda.modules.common.constants.services import STORAGE
from pyanaconda.modules.common.errors.installation import StorageInstallationError
from pyanaconda.modules.common.task import Task
import gi
gi.require_version("BlockDev", "2.0")
from gi.repository import BlockDev as blockdev
log = get_module_logger(__name__)
__all__ = ["ActivateFilesystemsTask", "MountFilesystemsTask", "WriteConfigurationTask"]
class ActivateFilesystemsTask(Task):
"""Installation task for activation 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 "Activate filesystems"
def run(self):
"""Do the activation.
:raise: StorageInstallationError if the activation fails
"""
if conf.target.is_directory:
log.debug("Don't activate file systems 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.error("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.error("Failed to activate filesystems: %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 activation 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()
iscsi_proxy = STORAGE.get_proxy(ISCSI)
iscsi_proxy.WriteConfiguration()
fcoe_proxy = STORAGE.get_proxy(FCOE)
fcoe_proxy.WriteConfiguration()
zfcp_proxy = STORAGE.get_proxy(ZFCP)
zfcp_proxy.WriteConfiguration()
self._write_dasd_conf(storage, 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 (IOError, RuntimeError) as e:
# TODO: real error handling
log.error("failed to store encryption key: %s", e)
log.debug("escrow: write_escrow_packets done")
def _write_dasd_conf(self, storage, sysroot):
"""Write DASD configuration to sysroot.
Write /etc/dasd.conf to target system for all DASD devices
configured during installation.
:param storage: the storage object
:param sysroot: a path to the target OS installation
"""
dasds = [d for d in storage.devices if d.type == "dasd"]
dasds.sort(key=lambda d: d.name)
if not (arch.is_s390() and dasds):
return
with open(os.path.realpath(sysroot + "/etc/dasd.conf"), "w") as f:
for dasd in dasds:
fields = [dasd.busid] + dasd.get_opts()
f.write("%s\n" % " ".join(fields),)
# check for hyper PAV aliases; they need to get added to dasd.conf as well
sysfs = "/sys/bus/ccw/drivers/dasd-eckd"
# in the case that someone is installing with *only* FBA DASDs,the above
# sysfs path will not exist; so check for it and just bail out of here if
# that's the case
if not os.path.exists(sysfs):
return
# this does catch every DASD, even non-aliases, but we're only going to be
# checking for a very specific flag, so there won't be any duplicate entries
# in dasd.conf
devs = [d for d in os.listdir(sysfs) if d.startswith("0.0")]
with open(os.path.realpath(sysroot + "/etc/dasd.conf"), "a") as f:
for d in devs:
aliasfile = "%s/%s/alias" % (sysfs, d)
with open(aliasfile, "r") as falias:
alias = falias.read().strip()
# if alias == 1, then the device is an alias; otherwise it is a
# normal dasd (alias == 0) and we can skip it, since it will have
# been added to dasd.conf in the above block of code
if alias == "1":
f.write("%s\n" % d)