* 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),
};
});
};
* (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;
};
* (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 })];
}, []);
};
* @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")
);
};
* @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;
};
* @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])
);
});
};
* @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") {
* 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;
};
* @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));
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);
};
* @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;
}
};
* @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);
};
* @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;
};
* @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;
};
export const unitMultiplier = {
B: 1,
KB: 1000,
MB: 1000000,
GB: 1000000000,
TB: 1000000000000,
};
export const bootloaderTypes = ["efi", "biosboot", "appleboot", "prepboot"];
const _ = cockpit.gettext;
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)];
};
* 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 => {
const mountPoint = request["mount-point"];
const hasMountPoint = mountPoint &&
mountPoint !== "none";
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 }) => {
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;
}
if (devices[device].type.v === "btrfs snapshot") {
return false;
}
if (devices[device].protected.v || devices[device].size.v === 0) {
return false;
}
return selectedDisks.some(disk => [device, ...ancestors].includes(disk));
});
return usableDevices;
};
* @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], []);
};
* @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));
};
* @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));
};