/* * Copyright (C) 2023 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import { checkIfArraysAreEqual } from "./utils.js"; export const requestsFromDbus = (requests) => { return requests.map(request => Object.entries(request).reduce((acc, [key, value]) => ({ ...acc, [key]: value.v }), {})); }; export const requestsToDbus = (requests) => { return requests.map(row => { return { "device-spec": cockpit.variant("s", row["device-spec"] || ""), "format-options": cockpit.variant("s", row["format-options"] || ""), "format-type": cockpit.variant("s", row["format-type"] || ""), "mount-options": cockpit.variant("s", row["mount-options"] || ""), "mount-point": cockpit.variant("s", row["mount-point"] || ""), reformat: cockpit.variant("b", !!row.reformat), }; }); }; /* Get the list of IDs of all the ancestors of the given device * (excluding the device itself) * @param {Object} deviceData - The device data object * @param {string} device - The ID of the device * @returns {Array} */ export const getDeviceAncestors = (deviceData, device) => { const ancestors = []; const deviceParents = deviceData[device]?.parents?.v || []; deviceParents.forEach(parent => { ancestors.push(parent); ancestors.push(...getDeviceAncestors(deviceData, parent)); }); return ancestors; }; /* Get the list of IDs of all the descendants of the given device * (excluding the device itself) * @param {string} device - The ID of the device * @param {Object} deviceData - The device data object * @returns {Array} */ export const getDeviceChildren = ({ device, deviceData }) => { const deviceChildren = deviceData[device]?.children?.v || []; return deviceChildren.reduce((acc, child) => { return [...acc, child, ...getDeviceChildren({ device: child, deviceData })]; }, []); }; /* Check if a device is encrypted (LUKS format or encrypted Stratis pool) * @param {Object} device - The device data object * @returns {boolean} */ export const isDeviceEncrypted = (device) => { if (!device) { return false; } return ( device.formatData?.type?.v === "luks" || (device.type?.v === "stratis pool" && device.attrs?.v?.encrypted === "True") ); }; /* Check if an encrypted device has its encryption key set * @param {Object} device - The device data object * @returns {boolean} */ export const deviceHasEncryptionKey = (device) => { if (!device) { return false; } if (device.formatData?.type?.v === "luks") { return device.formatData.attrs?.v?.has_key === "True"; } if (device.type?.v === "stratis pool" && device.attrs?.v?.encrypted === "True") { return device.attrs?.v?.has_key === "True"; } return false; }; /* Get the list of IDs of all locked encrypted devices * @param {Array} selectedDisks - The list of selected disks * @param {Object} deviceData - The device data object * @returns {Array} */ export const getLockedEncryptedDevices = (selectedDisks, deviceData) => { const relevantDevs = selectedDisks.flatMap(disk => ( [disk, ...getDeviceChildren({ device: disk, deviceData })] )); return Object.keys(deviceData).filter(d => { return ( relevantDevs.includes(d) && isDeviceEncrypted(deviceData[d]) && !deviceHasEncryptionKey(deviceData[d]) ); }); }; /* Check if the requests array contains duplicate entries * @param {Array} requests - The list of requests from a partitioning * @param {string} fieldName - The ID of the field to check for duplicates, ex: "mount-point" * @returns {boolean} */ export const hasDuplicateFields = (requests, fieldName) => { let _requests = requests; if (fieldName === "mount-point") { /* Swap devices have empty mount points and multiple swap devices are allowed * so we need to remove these before checking for duplicates */ _requests = requests.filter(r => r["format-type"] !== "swap"); } const items = _requests.map(r => r[fieldName]); return new Set(items).size !== items.length; }; /* Check if the requests array contains duplicate entries for a given field value * @param {Array} requests - The list of requests from a partitioning * @param {string} fieldName - The name of the field to check for duplicates, ex: "mount-point" * @param {string} fieldValue - The value of the field to check for duplicates, ex: "/boot" * @returns {boolean} */ export const isDuplicateRequestField = (requests, fieldName, fieldValue) => { return requests.filter((request) => request[fieldName] === fieldValue).length > 1; }; export const getDeviceByPath = (deviceData, path) => { const devices = Object.keys(deviceData).filter(d => deviceData[d].path?.v === path || deviceData[d].links?.v.includes(path)); // If multiple devices can have the same path, pick the ancestor device return devices.find(device => !getDeviceAncestors(deviceData, device).some(ancestor => devices.includes(ancestor))); }; export const getDeviceByName = (deviceData, name) => { return Object.keys(deviceData).find(d => deviceData[d].name?.v === name); }; /* Check if a device is encrypted or resides on an encrypted device * @param {Object} deviceData - The device data object * @param {string} device - The ID of the device * @returns {boolean} * */ export const isOnEncryptedDevice = (deviceData, device) => { if (deviceData[device].type.v === "luks/dm-crypt") { return true; } if (isDeviceEncrypted(deviceData[device])) { return true; } const parent = deviceData[device].parents.v?.[0]; if (parent) { return isOnEncryptedDevice(deviceData, parent); } else { return false; } }; /* Get the parent partitions IDs of a given device * @param {Object} deviceData - The device data object * @param {string} device - The ID of the device * @returns {Array} * */ export const getParentPartitions = (deviceData, device) => { if (deviceData[device].type.v === "partition") { return [device]; } return deviceData[device].parents.v.map(parent => getParentPartitions(deviceData, parent)).flat(Infinity); }; /* Get the parent disk ID of a given device * @param {Object} deviceData - The device data object * @param {string} device - The ID of the device * @returns {string} * */ export const getDeviceDisk = (deviceData, device) => { const parents = getDeviceAncestors(deviceData, device); const parentDisk = parents.find(parent => deviceData[parent].formatData.type.v === "disklabel"); return parentDisk; }; /* Check if a device has parents of a given type * @param {Object} deviceData - The device data object * @param {string} device - The ID of the device * @param {string} type - The type of the device to check for * @returns {boolean} * */ export const checkDeviceOnStorageType = (deviceData, device, type) => { if (deviceData[device].type.v === type) { return true; } return deviceData[device].parents.v?.some(parent => checkDeviceOnStorageType(deviceData, parent, type)); }; export const getDeviceTypeInfo = (device) => { const deviceType = device.type.v; const deviceFormatType = device.formatData.type.v; if (deviceType === "partition") { return deviceFormatType; } return deviceType; }; /* Match the units to their respective sizes */ export const unitMultiplier = { B: 1, KB: 1000, MB: 1000000, // eslint-disable-next-line sort-keys GB: 1000000000, TB: 1000000000000, }; export const bootloaderTypes = ["efi", "biosboot", "appleboot", "prepboot"]; const _ = cockpit.gettext; /* Blivet reports plain FAT without ESP flags as "vfat", not "efi". */ const VFAT_FORMAT_TYPE = "vfat"; /** * Check whether a device satisfies the format constraint for a mount point. * * The backend's required-filesystem-type field actually carries blivet format * types (efi, biosboot, ext4, ...). Bootloader format types such as "efi" are * not filesystem types; use constraint.description (from GetFormatTypeData) * when presenting them to the user. * * @returns {[boolean, string]} [invalid, errorMessage] */ export const getMountPointFormatConstraintError = ({ constraint, device, devices, mountPoint }) => { const requiredFormatType = constraint?.["required-filesystem-type"]?.v; const deviceFormatType = devices[device]?.formatData?.type?.v; if (!requiredFormatType || !deviceFormatType || deviceFormatType === requiredFormatType) { return [false, ""]; } const formatLabel = constraint.description || requiredFormatType; if (requiredFormatType === "efi" && deviceFormatType === VFAT_FORMAT_TYPE) { return [true, cockpit.format( _("'$0' must be on an EFI System Partition. The selected partition uses a FAT filesystem, but is not marked as an ESP. Set the partition type to EFI System and enable the esp flag."), mountPoint )]; } if (bootloaderTypes.includes(requiredFormatType)) { return [true, cockpit.format(_("'$0' must be assigned to $1"), mountPoint, formatLabel)]; } return [true, cockpit.format(_("'$0' must be on a device formatted to '$1'"), mountPoint, formatLabel)]; }; /* Filter requests to remove devices that should not be sent to the backend. * Keeps: * - Requests with reformat set to true or with specified mount points * * Note: Devices whose parent is reformatted are already filtered by the frontend * (via isChildReformatValid in isReformatInvalid), so we don't need to check here. * * Note: The same filtering happens in TUI as well. * (https://github.com/rhinstaller/anaconda/blob/98a9e05674dcf48b3545d6a4f5977e733c813acf/pyanaconda/ui/tui/spokes/storage.py#L717-L725) * * @param {Array} requests - Array of request objects (DBus variant format or plain) * @returns {Array} Filtered requests (same format as input) */ export const filterPartitioningRequests = (requests) => { return requests.filter(request => { // Handle both DBus variant format and plain format const mountPoint = request["mount-point"]; const hasMountPoint = mountPoint && mountPoint !== "none"; // Keep requests with mount points or reformatted return request.reformat || hasMountPoint; }); }; export const systemMountPoints = ["/", "/usr"]; export const isBootloaderDevice = ({ device, devices }) => { const fmt = devices[device].formatData?.type?.v; const mntPt = devices[device].formatData?.attrs?.v?.["mount-point"]; return bootloaderTypes.includes(fmt) && !mntPt; }; export const getUsableDevicesManualPartitioning = ({ devices, selectedDisks }) => { // Calculate usable devices for partitioning by replicating the logic in the backend // FIXME: Create a backend API for that // https://github.com/rhinstaller/anaconda/blob/f79f019e22c87dc388dbcc637a7a5612a3c223a7/pyanaconda/modules/storage/partitioning/manual/manual_module.py#L127 const usableDevices = Object.keys(devices).filter(device => { const children = devices[device].children.v; const ancestors = getDeviceAncestors(devices, device); if ( (children.length > 0 || devices[device].attrs.v.isleaf === "False") && devices[device].type.v !== "btrfs subvolume" ) { return false; } // We don't want to allow to use snapshots in mount point assignment. if (devices[device].type.v === "btrfs snapshot") { return false; } // Is the device usable? if (devices[device].protected.v || devices[device].size.v === 0) { return false; } // All device's parent disks have to be in selected disks. // Or the device itself has to be in selected disks. return selectedDisks.some(disk => [device, ...ancestors].includes(disk)); }); return usableDevices; }; /* Get disks on which given existing OS is located * @param {Object} deviceData - The device data object * @param {Object} osData - the existing OS data object * @returns {Array} * */ export const getOSDisks = (deviceData, osData) => { return osData.devices.v.map(deviceId => getDeviceAncestors(deviceData, deviceId)).flat() .filter(dev => deviceData[dev]["is-disk"].v) .reduce((uniqueDisks, disk) => uniqueDisks.includes(disk) ? uniqueDisks : [...uniqueDisks, disk], []); }; /* Check if the OS is located completely on the disks * @param {Object} deviceData - The device data object * @param {Array[string]} selectedDisks - a list of disk names * @param {Object} osData - the existing OS data object * @returns {boolean} * */ export const isCompleteOSOnDisks = (deviceData, selectedDisks, osData) => { return getOSDisks(deviceData, osData).every(disk => selectedDisks.includes(disk)); }; /* Check if the there is a reusable Fedora + Windows installation * @param {Object} deviceData - The device data object * @param {Array[string]} selectedDisks - a list of disk names * @param {Array[Object]} existingSystems - a list of OS data objects * @returns {boolean} * */ export const hasReusableFedoraWithWindowsOS = (deviceData, selectedDisks, existingSystems) => { const completeSystems = existingSystems .filter(osdata => isCompleteOSOnDisks(deviceData, selectedDisks, osdata)); const fedoraSystems = completeSystems .filter(osdata => osdata["os-name"].v.includes("Fedora")); const windowsSystems = completeSystems .filter(osdata => osdata["os-name"].v.includes("Windows")); return ( fedoraSystems.length === 1 && windowsSystems.length === 1 && checkIfArraysAreEqual( getOSDisks(deviceData, fedoraSystems[0]), getOSDisks(deviceData, windowsSystems[0]) ) ); }; /** * Select default disks for the partitioning (mirrors Anaconda ``select_default_disks`` for * the Web UI disk list). * * If some selected disks are usable, keep those (filtered to usable). Otherwise select a disk * only when there is exactly one usable non-ignored candidate. */ export const selectDefaultDisks = ({ ignoredDisks, selectedDisks, usableDisks }) => { const availableDisks = usableDisks.filter(disk => !ignoredDisks.includes(disk)); if (selectedDisks.length && selectedDisks.some(disk => usableDisks.includes(disk))) { return selectedDisks.filter(disk => usableDisks.includes(disk)); } if (availableDisks.length === 1) { return availableDisks; } return []; }; /** * Selected disks that also appear in the module usable list. SelectedDisks can still name * removed disks after a rescan. * * @param {string[]} selectedDisks * @param {string[]} usableDisks * @returns {string[]} */ export const intersectSelectedDisksWithUsable = (selectedDisks, usableDisks) => { if (!usableDisks) { return selectedDisks; } return selectedDisks.filter(disk => usableDisks.includes(disk)); };