# # Viewer of the device tree # # 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. # from abc import abstractmethod, ABC from functools import partial from blivet.formats import get_format from blivet.size import Size from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.i18n import _ from pyanaconda.modules.common.errors.storage import UnknownDeviceError from pyanaconda.modules.common.structures.storage import DeviceData, DeviceActionData, \ DeviceFormatData, OSData, MountPointConstraintsData from pyanaconda.modules.storage.devicetree.utils import get_required_device_size, \ get_supported_filesystems from pyanaconda.modules.storage.platform import platform from pyanaconda.modules.storage.partitioning.specification import PartSpec log = get_module_logger(__name__) __all__ = ["DeviceTreeViewer"] class DeviceTreeViewer(ABC): """The viewer of the device tree.""" @property @abstractmethod def storage(self): """The storage model. :return: an instance of Blivet """ return None def get_root_device(self): """Get the root device. :return: a name of the root device """ device = self.storage.root_device return device.name if device else "" def get_devices(self): """Get all devices in the device tree. :return: a list of device names """ return [d.name for d in self.storage.devices] def get_disks(self): """Get all disks in the device tree. Ignored disks are excluded, as are disks with no media present. :return: a list of device names """ return [d.name for d in self.storage.disks] def get_mount_points(self): """Get all mount points in the device tree. :return: a dictionary of mount points and device names """ return { mount_point: device.name for mount_point, device in self.storage.mountpoints.items() } def get_device_data(self, name): """Get the device data. :param name: a device name :return: an instance of DeviceData :raise: UnknownDeviceError if the device is not found """ # Find the device. device = self._get_device(name) # Collect the device data. data = DeviceData() self._set_device_data(device, data) # Collect the specialized data. if device.type == "dasd": self._set_device_data_dasd(device, data) elif device.type == "fcoe": self._set_device_data_fcoe(device, data) elif device.type == "iscsi": self._set_device_data_iscsi(device, data) elif device.type == "nvme-fabrics": self._set_device_data_nvme_fabrics(device, data) elif device.type == "zfcp": self._set_device_data_zfcp(device, data) # Prune the attributes. data.attrs = self._prune_attributes(data.attrs) return data def _set_device_data(self, device, data): """Set data for a device of any type.""" data.type = device.type data.name = device.name data.path = device.path data.links = device.device_links data.size = device.size.get_bytes() data.parents = [d.name for d in device.parents] data.children = [d.name for d in device.children] data.is_disk = device.is_disk data.protected = device.protected data.removable = device.removable # FIXME: We should generate the description from the device data. data.description = getattr(device, "description", "") data.attrs["serial"] = self._get_attribute(device, "serial") data.attrs["vendor"] = self._get_attribute(device, "vendor") data.attrs["model"] = self._get_attribute(device, "model") data.attrs["bus"] = self._get_attribute(device, "bus") data.attrs["wwn"] = self._get_attribute(device, "wwn") data.attrs["uuid"] = self._get_attribute(device, "uuid") def _set_device_data_dasd(self, device, data): """Set data for a DASD device.""" data.attrs["bus-id"] = self._get_attribute(device, "busid") def _set_device_data_fcoe(self, device, data): """Set data for an FCoE device.""" data.attrs["path-id"] = self._get_attribute(device, "id_path") def _set_device_data_iscsi(self, device, data): """Set data for an iSCSI device.""" data.attrs["port"] = self._get_attribute(device, "port") data.attrs["initiator"] = self._get_attribute(device, "initiator") data.attrs["lun"] = self._get_attribute(device, "lun") data.attrs["target"] = self._get_attribute(device, "target") data.attrs["path-id"] = self._get_attribute(device, "id_path") def _set_device_data_nvme_fabrics(self, device, data): """Set data for an NVMe Fabrics device.""" data.attrs["nsid"] = self._get_attribute(device, "nsid") data.attrs["eui64"] = self._get_attribute(device, "eui64") data.attrs["nguid"] = self._get_attribute(device, "nguid") get_attrs = partial(self._get_attribute_list, device.controllers) data.attrs["controllers-id"] = get_attrs("id") data.attrs["transports-type"] = get_attrs("transport") data.attrs["transports-address"] = get_attrs("transport_address") data.attrs["subsystems-nqn"] = get_attrs("subsysnqn") def _set_device_data_zfcp(self, device, data): """Set data for a ZFCP device.""" data.attrs["fcp-lun"] = self._get_attribute(device, "fcp_lun") data.attrs["wwpn"] = self._get_attribute(device, "wwpn") data.attrs["hba-id"] = self._get_attribute(device, "hba_id") data.attrs["path-id"] = self._get_attribute(device, "id_path") def get_format_data(self, device_name): """Get the device format data. Return data about a format of the specified device. For example: sda1 :param device_name: a name of the device :return: an instance of DeviceFormatData """ device = self._get_device(device_name) return self._get_format_data(device.format) def _get_format_data(self, fmt): """Get the format data. Retrieve data about a device format from the given format instance. :param fmt: an instance of DeviceFormat :return: an instance of DeviceFormatData """ # Collect the format data. data = DeviceFormatData() data.type = fmt.type or "" data.mountable = fmt.mountable data.formattable = fmt.formattable data.description = fmt.name or "" # Collect the additional attributes. data.attrs["has_key"] = self._get_attribute(fmt, "has_key") data.attrs["uuid"] = self._get_attribute(fmt, "uuid") data.attrs["label"] = self._get_attribute(fmt, "label") data.attrs["mount-point"] = self._get_attribute(fmt, "mountpoint") # Prune the attributes. data.attrs = self._prune_attributes(data.attrs) return data def get_format_type_data(self, format_name): """Get the format type data. Return data about the specified format type. For example: ext4 :param format_name: a name of the format type :return: an instance of DeviceFormatData """ fmt = get_format(format_name) return self._get_format_type_data(fmt) def _get_format_type_data(self, fmt): """Get the format type data. Retrieve data about a format type from the given format instance. :param fmt: an instance of DeviceFormat :return: an instance of DeviceFormatData """ data = DeviceFormatData() data.type = fmt.type or "" data.mountable = fmt.mountable data.description = fmt.name or "" return data def _get_device(self, name): """Find a device by its name. :param name: a name of the device :return: an instance of the Blivet's device :raise: UnknownDeviceError if no device is found """ device = self.storage.devicetree.get_device_by_name( name, hidden=True, incomplete=True ) if not device: raise UnknownDeviceError(name) return device def _get_devices(self, names): """Find devices by their names. :param names: names of the devices :return: a list of instances of the Blivet's device """ return list(map(self._get_device, names)) def _get_attribute(self, obj, name): """Get the attribute of the given object. If the attribute doesn't exist or it is not set, return None. Otherwise, return a string representation of the attribute value. :param obj: an object :param name: an attribute name :return: a string or None """ try: value = getattr(obj, name) except AttributeError: # Skip if the attribute doesn't exist. return None if value in (None, ""): # Skip it the attribute is not set. return None return str(value) def _get_attribute_list(self, iterable, name): """Get a list of attributes of the given objects. Create a comma-separated list of sorted unique attribute values. See the _get_attribute method for more info. :param iterable: a list of objects :param name: an attribute name :return: a string or None """ # Collect values. values = [self._get_attribute(obj, name) for obj in iterable] # Skip duplicates and unset values. values = set(filter(None, values)) # Format sorted values if any. return ", ".join(sorted(values)) or None def _prune_attributes(self, attrs): """Prune the unset values of attributes. :param attrs: a dictionary of attributes :return: a pruned dictionary of attributes """ return {k: v for k, v in attrs.items() if v is not None} def get_actions(self): """Get the device actions. The actions are pruned and sorted. :return: a list of DeviceActionData """ actions = [] self.storage.devicetree.actions.prune() self.storage.devicetree.actions.sort() for action in self.storage.devicetree.actions.find(): actions.append(self._get_action_data(action)) return actions def _get_action_data(self, action): """Get the action data. :param action: an instance of DeviceAction :return: an instance of DeviceActionData """ data = DeviceActionData() # Collect the action data. data.action_type = action.type_string.lower() data.action_description = action.type_desc # Collect the object data. data.object_type = action.object_string.lower() data.object_description = action.object_type_string # Collect the device data. device = action.device data.device_name = device.name if action.is_create or action.is_device or action.is_format: data.attrs["mount-point"] = self._get_attribute(action.format, "mountpoint") if getattr(device, "description", ""): data.attrs["serial"] = self._get_attribute(device, "serial") data.device_description = _("{device_description} ({device_name})").format( device_description=device.description, device_name=device.name ) elif getattr(device, "disk", None): data.attrs["serial"] = self._get_attribute(device.disk, "serial") data.device_description = _("{device_name} on {container_name}").format( device_name=device.name, container_name=device.disk.description ) else: data.attrs["serial"] = self._get_attribute(device, "serial") data.device_description = device.name # Prune the attributes. data.attrs = self._prune_attributes(data.attrs) return data def resolve_device(self, dev_spec): """Get the device matching the provided device specification. The spec can be anything from a device name (eg: 'sda3') to a device node path (eg: '/dev/mapper/fedora-root') to something like 'UUID=xyz-tuv-qrs' or 'LABEL=rootfs'. If no device is found, return an empty string. :param dev_spec: a string describing a block device :return: a device name or an empty string """ device = self.storage.devicetree.resolve_device(dev_spec) if not device: return "" return device.name def get_ancestors(self, device_names): """Collect ancestors of the specified devices. Ancestors of a device don't include the device itself. The list is sorted by names of the devices. :param device_names: a list of device names :return: a list of device names """ devices = self._get_devices(device_names) ancestors = set() for device in devices: for ancestor in device.ancestors: if ancestor != device: ancestors.add(ancestor.name) return sorted(ancestors) def get_supported_file_systems(self): """Get the supported types of filesystems. :return: a list of filesystem names """ return get_supported_filesystems() def get_required_device_size(self, required_space): """Get device size we need to get the required space on the device. :param int required_space: a required space in bytes :return int: a required device size in bytes """ return get_required_device_size(Size(required_space)).get_bytes() def get_file_system_free_space(self, mount_points): """Get total file system free space on the given mount points. :param mount_points: a list of mount points :return: a total size in bytes """ return self.storage.get_file_system_free_space(mount_points).get_bytes() def get_disk_free_space(self, disk_names): """Get total free space on the given disks. Calculates free space available for use. :param disk_names: a list of disk names :return: a total size in bytes """ disks = self._get_devices(disk_names) return self.storage.get_disk_free_space(disks).get_bytes() def get_disk_reclaimable_space(self, disk_names): """Get total reclaimable space on the given disks. Calculates free space unavailable but reclaimable from existing partitions. :param disk_names: a list of disk names :return: a total size in bytes """ disks = self._get_devices(disk_names) return self.storage.get_disk_reclaimable_space(disks).get_bytes() def get_disk_total_space(self, disk_names): """Get total space on the given disks. :param disk_names: a list of disk names :return: a total size in bytes """ disks = self._get_devices(disk_names) return sum((d.size for d in disks), Size(0)).get_bytes() def get_fstab_spec(self, name): """Get the device specifier for use in /etc/fstab. :param name: a name of the device :return: a device specifier for /etc/fstab """ device = self._get_device(name) return device.fstab_spec def get_existing_systems(self): """Get existing GNU/Linux installations. :return: a list of data about found installations """ return list(map(self._get_os_data, self.storage.roots)) def _get_os_data(self, root): """Get the OS data. :param root: an instance of Root :return: an instance of OSData """ data = OSData() data.os_name = root.name or "" data.devices = [ device.name for device in root.devices ] data.mount_points = { path: device.name for path, device in root.mounts.items() } return data def _get_mount_point_constraints_data(self, spec): """Get the mount point data. :param spec: an instance of PartSpec :return: an instance of MountPointConstraintsData """ data = MountPointConstraintsData() data.mount_point = spec.mountpoint or "" data.required_filesystem_type = spec.fstype or "" data.encryption_allowed = spec.encrypted data.logical_volume_allowed = spec.lv return data def get_mount_point_constraints(self): """Get list of constraints on mountpoints for the current platform Also provides hints if the partition is required or recommended. This includes mount points required to boot (e.g. /boot/efi, /boot) and the / partition which is always considered to be required. /boot is not required in general but can be required in some cases, depending on the filesystem on the root partition (ie crypted root). :return: a list of mount points with its constraints """ constraints = [] # Root partition is required root_partition = PartSpec(mountpoint="/", lv=True, thin=True, encrypted=True) root_constraint = self._get_mount_point_constraints_data(root_partition) root_constraint.required = True constraints.append(root_constraint) # Platform partitions are required except for /boot partiotion which is recommended for p in platform.partitions: if p: constraint = self._get_mount_point_constraints_data(p) if p.mountpoint == "/boot": constraint.recommended = True else: constraint.required = True constraints.append(constraint) return constraints