# # Copyright (C) 2020 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 locale import re from blivet.size import Size from dasbus.error import DBusError from dasbus.typing import unwrap_variant from dasbus.client.proxy import get_object_path from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.constants import PARTITIONING_METHOD_AUTOMATIC, BOOTLOADER_DRIVE_UNSET, \ PARTITIONING_METHOD_CUSTOM from pyanaconda.core.i18n import P_, _ from pyanaconda.errors import errorHandler as error_handler, ERROR_RAISE from pyanaconda.flags import flags from pyanaconda.modules.common.constants.objects import DISK_SELECTION, BOOTLOADER, DEVICE_TREE, \ DISK_INITIALIZATION from pyanaconda.modules.common.constants.services import STORAGE from pyanaconda.modules.common.errors.configuration import StorageConfigurationError, \ BootloaderConfigurationError from pyanaconda.modules.common.structures.validation import ValidationReport from pyanaconda.modules.common.task import sync_run_task from pyanaconda.core.storage import device_matches log = get_module_logger(__name__) def create_partitioning(partitioning_method): """Create a partitioning. :param partitioning_method: a partitioning method :return: a proxy of a partitioning module """ storage_proxy = STORAGE.get_proxy() object_path = storage_proxy.CreatePartitioning( partitioning_method ) return STORAGE.get_proxy(object_path) def find_partitioning(): """Find a partitioning to use or create a new one. :return: a proxy of a partitioning module """ storage_proxy = STORAGE.get_proxy() object_paths = storage_proxy.CreatedPartitioning if object_paths: # Choose the last created partitioning. object_path = object_paths[-1] else: # Or create the automatic partitioning. object_path = storage_proxy.CreatePartitioning( PARTITIONING_METHOD_AUTOMATIC ) return STORAGE.get_proxy(object_path) def reset_storage(scan_all=False, retry=True): """Reset the storage model. :param scan_all: should we scan all devices in the system? :param retry: should we allow to retry the reset? """ # Clear the exclusive disks to scan all devices in the system. if scan_all: disk_select_proxy = STORAGE.get_proxy(DISK_SELECTION) disk_select_proxy.ExclusiveDisks = [] # Scan the devices. storage_proxy = STORAGE.get_proxy() while True: try: task_path = storage_proxy.ScanDevicesWithTask() task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) except DBusError as e: # Is the retry allowed? if not retry: raise # Does the user want to retry? elif error_handler.cb(e) == ERROR_RAISE: raise # Retry the storage reset. else: continue else: # No need to retry. break # Reset the partitioning. storage_proxy.ResetPartitioning() def reset_bootloader(): """Reset the bootloader.""" bootloader_proxy = STORAGE.get_proxy(BOOTLOADER) bootloader_proxy.Drive = BOOTLOADER_DRIVE_UNSET def select_default_disks(): """Select default disks for the partitioning. If there are some disks already selected, do nothing. In the automatic installation, select all disks. In the interactive installation, select a disk if there is only one available. :return: a list of selected disks """ disk_select_proxy = STORAGE.get_proxy(DISK_SELECTION) selected_disks = disk_select_proxy.SelectedDisks ignored_disks = disk_select_proxy.IgnoredDisks if selected_disks: # Do nothing if there are some disks selected. pass elif flags.automatedInstall: # Get all disks. device_tree = STORAGE.get_proxy(DEVICE_TREE) all_disks = device_tree.GetDisks() # Select all disks. selected_disks = [d for d in all_disks if d not in ignored_disks] disk_select_proxy.SelectedDisks = selected_disks log.debug("Selecting all disks by default: %s", ",".join(selected_disks)) else: # Get usable disks. usable_disks = disk_select_proxy.GetUsableDisks() available_disks = [d for d in usable_disks if d not in ignored_disks] # Select a usable disk if there is only one available. if len(available_disks) == 1: selected_disks = available_disks apply_disk_selection(selected_disks) log.debug("Selecting one or less disks by default: %s", ",".join(selected_disks)) return selected_disks def apply_disk_selection(selected_names, reset_boot_drive=False): """Apply the disks selection. :param selected_names: a list of selected disk names :param reset_boot_drive: reset the boot drive if it is not selected """ device_tree = STORAGE.get_proxy(DEVICE_TREE) # Get disks. disks = set(device_tree.GetDisks()) selected_disks = filter_disks_by_names(disks, selected_names) # Get ancestors. ancestors_names = device_tree.GetAncestors(selected_disks) ancestors_disks = filter_disks_by_names(disks, ancestors_names) # Set the disks to select. disk_select_proxy = STORAGE.get_proxy(DISK_SELECTION) disk_select_proxy.SelectedDisks = selected_names + ancestors_disks # Set the drives to clear. disk_init_proxy = STORAGE.get_proxy(DISK_INITIALIZATION) disk_init_proxy.DrivesToClear = selected_names # Reset the boot drive if it is not selected. # FIXME: Move this logic the Storage module? if reset_boot_drive: boot_loader = STORAGE.get_proxy(BOOTLOADER) boot_drive = boot_loader.Drive if boot_drive and boot_drive not in selected_names: reset_bootloader() def get_disks_summary(disks): """Get a summary of the selected disks :param disks: a list of names of selected disks :return: a string with a summary """ device_tree = STORAGE.get_proxy(DEVICE_TREE) count = len(disks) capacity = Size(device_tree.GetDiskTotalSpace(disks)) free_space = Size(device_tree.GetDiskFreeSpace(disks)) return P_( "{count} disk selected; {capacity} capacity; {free} free", "{count} disks selected; {capacity} capacity; {free} free", count ).format( count=count, capacity=capacity, free=free_space ) def try_populate_devicetree(): """Try to populate a device tree. Try to populate the device tree while catching errors and dealing with some special ones in a nice way (giving user chance to do something about them). """ device_tree = STORAGE.get_proxy(DEVICE_TREE) while True: try: task_path = device_tree.FindDevicesWithTask() task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) except DBusError as e: # Does the user want to retry? if error_handler.cb(e) == ERROR_RAISE: raise # Retry populating the device tree. else: continue else: # No need to retry. break def is_passphrase_required(partitioning): """Is a passphrase required by the partitioning? If the partitioning defines an encrypted device without a passphrase, it is necessary to provide a passphrase that will be used by all such devices. :param partitioning: a DBus proxy of a partitioning """ return partitioning.PartitioningMethod in ( PARTITIONING_METHOD_AUTOMATIC, PARTITIONING_METHOD_CUSTOM ) and partitioning.RequiresPassphrase() def set_required_passphrase(partitioning, passphrase): """Set a passphrase required by the partitioning. See the is_passphrase_required function. :param partitioning: a DBus proxy of a partitioning :param passphrase: a string with the passphrase """ partitioning.SetPassphrase(passphrase) def apply_partitioning(partitioning, show_message_cb, reset_storage_cb): """Apply the given partitioning. :param partitioning: a DBus proxy of a partitioning :param show_message_cb: a callback for showing a message :param reset_storage_cb: a callback for resetting the storage :return: an instance of ValidationReport """ log.debug("Applying partitioning") report = ValidationReport() try: show_message_cb(_("Saving storage configuration...")) task_path = partitioning.ConfigureWithTask() task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) except StorageConfigurationError as e: show_message_cb(_("Failed to save storage configuration")) report.error_messages.append(str(e)) reset_bootloader() reset_storage_cb() except BootloaderConfigurationError as e: show_message_cb(_("Failed to save boot loader configuration")) report.error_messages.append(str(e)) reset_bootloader() else: show_message_cb(_("Checking storage configuration...")) task_path = partitioning.ValidateWithTask() task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) result = unwrap_variant(task_proxy.GetResult()) report = ValidationReport.from_structure(result) log.debug("Validation has been completed: %s", report) if report.is_valid(): storage_proxy = STORAGE.get_proxy() storage_proxy.ApplyPartitioning( get_object_path(partitioning) ) log.debug("Partitioning has been applied.") return report def is_local_disk(device_type): """Is the disk local? A local disk doesn't require any additional setup unlike the advanced storage. While technically local disks, zFCP and NVDIMM devices are advanced storage and should not be considered local. :param str device_type: a device type :return bool: True or False """ return device_type not in ( "dm-multipath", "iscsi", "fcoe", "zfcp", "nvme-fabrics", ) def size_from_input(input_str, units=None): """Get a Size object from an input string. :param str input_str: a string forming some representation of a size :param units: use these units if none specified in input_str :type units: str or NoneType :returns: a Size object corresponding to input_str :rtype: :class:`blivet.size.Size` or NoneType Units default to bytes if no units in input_str or units. """ if not input_str: # Nothing to parse return None # A string ending with a digit contains no units information. if re.search(r'[\d.%s]$' % locale.nl_langinfo(locale.RADIXCHAR), input_str): input_str += units or "" try: size = Size(input_str) except ValueError: return None return size def ignore_oemdrv_disks(): """Ignore disks labeled OEMDRV.""" matched = device_matches("LABEL=OEMDRV", disks_only=True) for oemdrv_disk in matched: disk_select_proxy = STORAGE.get_proxy(DISK_SELECTION) ignored_disks = disk_select_proxy.IgnoredDisks if oemdrv_disk not in ignored_disks: log.info("Adding disk %s labeled OEMDRV to ignored disks.", oemdrv_disk) ignored_disks.append(oemdrv_disk) disk_select_proxy.IgnoredDisks = ignored_disks def filter_disks_by_names(disks, names): """Filter disks by the given names. :param disks: a list of disks name :param names: a list of names to filter :return: a list of filtered disk names """ return list(filter(lambda name: name in disks, names))