# # Bootloader module. # # Copyright (C) 2018 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.errors import KickstartParseError from pykickstart.constants import SECURE_BOOT_AUTO, SECURE_BOOT_ENABLED, SECURE_BOOT_DISABLED from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.modules.storage.bootloader import BootLoaderFactory from pyanaconda.modules.storage.bootloader.efi import EFIBase from pyanaconda.modules.storage.bootloader.grub2 import GRUB2 from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.configuration.bootloader import BootloaderType from pyanaconda.core.constants import BOOTLOADER_LOCATION_DEFAULT, BOOTLOADER_TIMEOUT_UNSET, \ BOOTLOADER_LOCATION_MBR, BOOTLOADER_LOCATION_PARTITION from pyanaconda.core.i18n import _ from pyanaconda.core.dbus import DBus from pyanaconda.core.signal import Signal from pyanaconda.modules.common.constants.objects import BOOTLOADER from pyanaconda.modules.common.structures.requirement import Requirement from pyanaconda.modules.storage.bootloader.bootloader_interface import BootloaderInterface from pyanaconda.modules.storage.bootloader.installation import ConfigureBootloaderTask, \ InstallBootloaderTask, FixZIPLBootloaderTask, FixBTRFSBootloaderTask, RecreateInitrdsTask, \ CreateRescueImagesTask, CreateBLSEntriesTask, CollectKernelArgumentsTask from pyanaconda.modules.storage.constants import BootloaderMode, ZIPLSecureBoot from pyanaconda.modules.storage.storage_subscriber import StorageSubscriberModule log = get_module_logger(__name__) class BootloaderModule(StorageSubscriberModule): """The bootloader module.""" def __init__(self): """Initialize the module.""" super().__init__() self.bootloader_mode_changed = Signal() self._bootloader_mode = BootloaderMode.ENABLED self._default_type = BootloaderType.DEFAULT self.set_default_type(conf.bootloader.type) self.preferred_location_changed = Signal() self._preferred_location = BOOTLOADER_LOCATION_DEFAULT self.drive_changed = Signal() self._drive = "" self.drive_order_changed = Signal() self._drive_order = [] self.keep_mbr_changed = Signal() self._keep_mbr = False self.keep_boot_order_changed = Signal() self._keep_boot_order = False self.extra_arguments_changed = Signal() self._extra_arguments = [] self.timeout_changed = Signal() self._timeout = BOOTLOADER_TIMEOUT_UNSET self.zipl_secure_boot_changed = Signal() self._zipl_secure_boot = None self.password_is_set_changed = Signal() self._password = "" self._password_is_encrypted = False def publish(self): """Publish the module.""" DBus.publish_object(BOOTLOADER.object_path, BootloaderInterface(self)) def process_kickstart(self, data): """Process the kickstart data.""" self._set_module_from_kickstart(data) self._validate_grub2_configuration(data) def _set_module_from_kickstart(self, data): """Set the module from the kickstart data.""" if not data.bootloader.seen: self.set_bootloader_mode(BootloaderMode.ENABLED) self.set_preferred_location(BOOTLOADER_LOCATION_DEFAULT) elif data.bootloader.disabled: self.set_bootloader_mode(BootloaderMode.DISABLED) elif data.bootloader.location == "none": self.set_bootloader_mode(BootloaderMode.SKIPPED) elif data.bootloader.location == "mbr": self.set_bootloader_mode(BootloaderMode.ENABLED) self.set_preferred_location(BOOTLOADER_LOCATION_MBR) elif data.bootloader.location == "partition": self.set_bootloader_mode(BootloaderMode.ENABLED) self.set_preferred_location(BOOTLOADER_LOCATION_PARTITION) if data.bootloader.extlinux: self.set_default_type(BootloaderType.EXTLINUX) if data.bootloader.sdboot: self.set_default_type(BootloaderType.SDBOOT) if data.bootloader.bootDrive: self.set_drive(data.bootloader.bootDrive) if data.bootloader.driveorder: self.set_drive_order(data.bootloader.driveorder) if data.bootloader.nombr: self.set_keep_mbr(True) if data.bootloader.leavebootorder: self.set_keep_boot_order(True) if data.bootloader.appendLine: args = data.bootloader.appendLine.split() self.set_extra_arguments(args) if data.bootloader.timeout is not None: self.set_timeout(data.bootloader.timeout) if data.bootloader.password: self.set_password(data.bootloader.password, data.bootloader.isCrypted) if data.zipl.secure_boot == SECURE_BOOT_ENABLED: self.set_zipl_secure_boot(ZIPLSecureBoot.ENABLED) elif data.zipl.secure_boot == SECURE_BOOT_DISABLED: self.set_zipl_secure_boot(ZIPLSecureBoot.DISABLED) elif data.zipl.secure_boot == SECURE_BOOT_AUTO: self.set_zipl_secure_boot(ZIPLSecureBoot.AUTO) def _validate_grub2_configuration(self, data): """Validate the GRUB2 configuration. :raise: KickstartParseError if not valid """ # Skip other types of the boot loader. if not issubclass(BootLoaderFactory.get_class(), GRUB2): return # Check the location support. if self.preferred_location == BOOTLOADER_LOCATION_PARTITION: raise KickstartParseError(_("GRUB2 does not support installation to a partition."), lineno=data.bootloader.lineno) # Check the password format. if self.password_is_set \ and self.password_is_encrypted \ and not self.password.startswith("grub.pbkdf2."): raise KickstartParseError(_("GRUB2 encrypted password must be in grub.pbkdf2 format."), lineno=data.bootloader.lineno) def setup_kickstart(self, data): """Setup the kickstart data.""" if self.get_default_type() == BootloaderType.EXTLINUX: data.bootloader.extlinux = True if self.get_default_type() == BootloaderType.SDBOOT: data.bootloader.sdboot = True if self.bootloader_mode == BootloaderMode.DISABLED: data.bootloader.disabled = True data.bootloader.location = "none" elif self.bootloader_mode == BootloaderMode.SKIPPED: data.bootloader.disabled = False data.bootloader.location = "none" elif self.preferred_location == BOOTLOADER_LOCATION_MBR: data.bootloader.disabled = False data.bootloader.location = "mbr" elif self.preferred_location == BOOTLOADER_LOCATION_PARTITION: data.bootloader.disabled = False data.bootloader.location = "partition" else: data.bootloader.disabled = False data.bootloader.location = None data.bootloader.bootDrive = self.drive data.bootloader.driveorder = self.drive_order data.bootloader.nombr = self.keep_mbr data.bootloader.leavebootorder = self.keep_boot_order data.bootloader.appendLine = " ".join(self.extra_arguments) if self.timeout == BOOTLOADER_TIMEOUT_UNSET: data.bootloader.timeout = None else: data.bootloader.timeout = self.timeout data.bootloader.password = self.password data.bootloader.isCrypted = self.password_is_encrypted if self._zipl_secure_boot == ZIPLSecureBoot.ENABLED: data.zipl.secure_boot = SECURE_BOOT_ENABLED elif self._zipl_secure_boot == ZIPLSecureBoot.DISABLED: data.zipl.secure_boot = SECURE_BOOT_DISABLED elif self._zipl_secure_boot == ZIPLSecureBoot.AUTO: data.zipl.secure_boot = SECURE_BOOT_AUTO return data @property def bootloader_mode(self): """The mode of the bootloader.""" return self._bootloader_mode def set_bootloader_mode(self, mode): """Set the type of the bootloader. :param mode: an instance of BootloaderMode """ self._bootloader_mode = mode self.bootloader_mode_changed.emit() log.debug("Bootloader mode is set to '%s'.", mode) def get_default_type(self): """Get the default type of the boot loader. FIXME: This is a temporary workaround for UI. :return: an instance of BootloaderType """ return self._default_type def set_default_type(self, default_type): """Set the default type of the boot loader. :param default_type: an instance of BootloaderType """ # Set up the bootloader factory. cls = BootLoaderFactory.get_class_by_name(default_type.value) BootLoaderFactory.set_default_class(cls) # Set up the property. self._default_type = default_type log.debug("The default type is set to '%s'.", default_type) @property def preferred_location(self): """Where the boot record is written.""" return self._preferred_location def set_preferred_location(self, location): """Specify where the boot record is written. Supported values: DEFAULT, MBR, PARTITION :param location: a string with the location """ self._preferred_location = location self.preferred_location_changed.emit() log.debug("Preferred location is set to '%s'.", location) @property def drive(self): """The drive where the bootloader should be written.""" return self._drive def set_drive(self, drive): """Set the drive where the bootloader should be written. :param drive: a name of the drive """ self._drive = drive self.drive_changed.emit() log.debug("Drive is set to '%s'.", drive) @property def drive_order(self): """Potentially partial order for disks.""" return self._drive_order def set_drive_order(self, drives): """Set the potentially partial order for disks. :param drives: a list of names of drives """ self._drive_order = drives self.drive_order_changed.emit() log.debug("Drive order is set to '%s'.", drives) @property def keep_mbr(self): """Don't update the MBR.""" return self._keep_mbr def set_keep_mbr(self, value): """Set if the MBR can be updated. :param value: True if the MBR cannot be updated, otherwise False """ self._keep_mbr = value self.keep_mbr_changed.emit() log.debug("Keep MBR is set to '%s'.", value) @property def keep_boot_order(self): """Don't change the existing boot order.""" return self._keep_boot_order def set_keep_boot_order(self, value): """Set if the the boot order can be changed. :param value: True to use the existing order, otherwise False :return: """ self._keep_boot_order = value self.keep_boot_order_changed.emit() log.debug("Keep boot order is set to '%s'.", value) @property def extra_arguments(self): """List of extra bootloader arguments.""" return self._extra_arguments def set_extra_arguments(self, args): """Set the extra bootloader arguments. :param args: a list of arguments """ self._extra_arguments = args self.extra_arguments_changed.emit() log.debug("Extra arguments are set to '%s'.", args) @property def timeout(self): """The bootloader timeout.""" return self._timeout def set_timeout(self, timeout): """Set the bootloader timeout. :param timeout: a number of seconds """ self._timeout = timeout self.timeout_changed.emit() log.debug("Timeout is set to '%s'.", timeout) @property def zipl_secure_boot(self): """The ZIPL Secure Boot for s390x. :return: an instance of ZIPLSecureBoot """ if self._zipl_secure_boot is None: return ZIPLSecureBoot.AUTO return self._zipl_secure_boot def set_zipl_secure_boot(self, value): """Set up the ZIPL Secure Boot for s390x. :param value: an instance of ZIPLSecureBoot """ self._zipl_secure_boot = value self.zipl_secure_boot_changed.emit() log.debug("ZIPL Secure Boot is set to '%s'.", value) @property def password(self): """The GRUB boot loader password.""" return self._password @property def password_is_set(self): """Is the GRUB boot loader password set?""" return self._password != "" @property def password_is_encrypted(self): """Is the GRUB boot loader password encrypted?""" return self._password_is_encrypted def set_password(self, password, encrypted): """Set the GRUB boot loader password. :param password: a string with the password :param encrypted: True if the password is encrypted, otherwise False """ self._password = password self._password_is_encrypted = encrypted self.password_is_set_changed.emit() log.debug("Password is set.") def is_efi(self): """Is the bootloader based on EFI? :return: True or False """ return isinstance(self.storage.bootloader, EFIBase) def get_arguments(self): """Get the bootloader arguments. Get kernel parameters that are currently set up for the bootloader. The list is complete and final after the bootloader installation. FIXME: Collect the bootloader arguments on demand if possible. :return: list of arguments """ return list(self.storage.bootloader.boot_args) def detect_windows(self): """Are Windows OS installed on the system? Guess by searching for bootable partitions of other operating systems whether there are Windows OS installed on the system. :return: True or False """ devices = filter(lambda d: d.format.name == "ntfs", self.storage.devices) return self.storage.bootloader.has_windows(devices) def collect_requirements(self): """Return installation requirements for this module. :return: a list of requirements """ if conf.target.is_directory: log.debug("The bootloader configuration is disabled for dir installations.") return [] if self.bootloader_mode == BootloaderMode.DISABLED: log.debug("The bootloader configuration is disabled.") return [] requirements = [] for name in self.storage.bootloader.packages: requirements.append(Requirement.for_package( name, reason="Necessary for the bootloader configuration." )) return requirements def install_bootloader_with_tasks(self, payload_type, kernel_versions): """Install the bootloader with a list of tasks. FIXME: This is just a temporary method. :param payload_type: a string with the payload type :param kernel_versions: a list of kernel versions :return: a list of tasks """ return [ CreateRescueImagesTask( payload_type=payload_type, kernel_versions=kernel_versions, sysroot=conf.target.system_root ), ConfigureBootloaderTask( storage=self.storage, mode=self.bootloader_mode, payload_type=payload_type, kernel_versions=kernel_versions, sysroot=conf.target.system_root ), CollectKernelArgumentsTask( storage=self.storage, mode=self.bootloader_mode ), InstallBootloaderTask( storage=self.storage, mode=self.bootloader_mode, payload_type=payload_type, sysroot=conf.target.system_root ), CreateBLSEntriesTask( storage=self.storage, payload_type=payload_type, kernel_versions=kernel_versions, sysroot=conf.target.system_root ) ] def generate_initramfs_with_tasks(self, payload_type, kernel_versions): """Generate initramfs with a list of tasks. FIXME: This is just a temporary method. :param payload_type: a string with the payload type :param kernel_versions: a list of kernel versions :return: a list of tasks """ return [ RecreateInitrdsTask( storage=self.storage, payload_type=payload_type, kernel_versions=kernel_versions, sysroot=conf.target.system_root ), FixBTRFSBootloaderTask( storage=self.storage, mode=self.bootloader_mode, payload_type=payload_type, kernel_versions=kernel_versions, sysroot=conf.target.system_root ), ] def fix_zipl_bootloader_with_task(self): """Fix ZIPL bootloader with a task. :return: an installation task """ return FixZIPLBootloaderTask( mode=self.bootloader_mode )