/* * Copyright (C) 2022 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import React, { useContext } from "react"; import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js"; import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js"; import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js"; import { checkDeviceOnStorageType, getDeviceAncestors, getDeviceChildren, getParentPartitions, isBootloaderDevice, isOnEncryptedDevice, systemMountPoints, } from "../../helpers/storage.js"; import { StorageContext, } from "../../contexts/Common.jsx"; import { useFreeSpaceForSystem, useOriginalDevices, useOriginalExistingSystems, usePlannedActions, usePlannedDevices, usePlannedMountPoints, useRequiredSpace, } from "../../hooks/Storage.jsx"; import { ListingTable } from "cockpit-components-table.jsx"; import "./StorageReview.scss"; const _ = cockpit.gettext; const idPrefix = "storage-review"; export const StorageReview = ({ isReviewScreen = false }) => { const { diskSelection } = useContext(StorageContext); const selectedDisks = diskSelection.selectedDisks; return ( {selectedDisks.map(disk => { return ( ); })} ); }; const DeviceRow = ({ disk, isReviewScreen }) => { const { partitioning } = useContext(StorageContext); const originalDevices = useOriginalDevices(); const actions = usePlannedActions(); const mountPoints = usePlannedMountPoints(); const devices = usePlannedDevices(); const requiredSpace = useRequiredSpace(); const freeSpace = useFreeSpaceForSystem(); const requests = partitioning.requests; const deviceData = devices?.[disk]; const reusedMountPoints = requests.find(request => request["reused-mount-points"])?.["reused-mount-points"]; const reformattedMountPoints = requests.find(request => request["reformatted-mount-points"])?.["reformatted-mount-points"]; const plannedSystemMountPoints = Object.entries(mountPoints).filter(mp => systemMountPoints.includes(mp[0])); if (!deviceData) { return null; } let insufficientSizeMessage = ""; if (requiredSpace > freeSpace) { if (Object.keys(plannedSystemMountPoints).length === 1) { insufficientSizeMessage = cockpit.format(_("Needs at least $0"), cockpit.format_bytes(requiredSpace)); } else if (Object.keys(plannedSystemMountPoints).length > 1) { insufficientSizeMessage = _("May need more free space"); } } const getDeviceRow = ([mount, device]) => { const size = cockpit.format_bytes(devices[device].size.v); const request = requests.find(request => request["device-spec"] === device); let format = devices[device].formatData.type.v; const isImplicitlyReusedMountPoint = originalDevices[device] && !reformattedMountPoints?.includes(mount) && !isDeviceDeleted({ actions, device }) && !isDeviceResized({ actions, device }); const isReformattedMountPoint = (!request && !reusedMountPoints?.includes(mount) && !isImplicitlyReusedMountPoint) || request?.reformat; // If the format is btrfs, we want to show the type of the device (aka btrfs subvolume) if (format === "btrfs") { format = devices[device].type.v; } let action = null; if (isReformattedMountPoint && format) { action = cockpit.format(_("format as $0"), format); } else if (format === "biosboot") { action = format; } else { action = _("mount"); } const parents = getParentPartitions(devices, device); const showMaybeType = () => { if (checkDeviceOnStorageType(devices, device, "lvmvg")) { return ", LVM"; } else if (checkDeviceOnStorageType(devices, device, "mdarray")) { return ", RAID"; } else if (checkDeviceOnStorageType(devices, device, "stratis pool")) { return ", Stratis"; } else { return ""; } }; const helperText = ( systemMountPoints.includes(mount) && insufficientSizeMessage ? ( {insufficientSizeMessage} ) : "" ); const encryptedText = isOnEncryptedDevice(devices, device) ? (isReformattedMountPoint ? _("encrypt") : _("encrypted")) : ""; const deviceText = cockpit.format("$0$1", parents.join(", "), showMaybeType()); return ( { columns: [ { title: deviceText, width: 17 }, { title: size, width: 15 }, { title: action, width: 17 }, { title: encryptedText, width: 17 }, { title: mount, width: 17 }, ...(isReviewScreen ? [{ title: helperText, width: 17 }] : []), ], props: { "data-action": action, "data-device": deviceText, "data-mount": mount, "data-size": size, ...(encryptedText ? { "data-encrypted": encryptedText } : {}), key: device }, } ); }; const getActionRow = action => { const device = action["device-name"].v; const actionType = action["action-type"].v; const actionDescription = action["action-description"].v; let sizeText = ""; let actionDescriptionText = actionDescription; if (actionType === "destroy") { actionDescriptionText = _("delete"); } else if (actionType === "resize") { const prevSize = cockpit.format_bytes(originalDevices[device].size.v); sizeText = cockpit.format_bytes(devices[device].size.v); actionDescriptionText = cockpit.format(_("resized from $0"), prevSize); } return ( { columns: [ { title: device }, { title: sizeText }, { props: { className: idPrefix + "-table-column-action-" + actionType }, title: actionDescriptionText, }, { title: "" }, { title: "" }, ...(isReviewScreen ? [{ title: "" }] : []), ], props: { "data-action": actionDescriptionText, "data-device": device, "data-size": sizeText, key: device + actionType, }, } ); }; const newMountPointRows = Object.entries(mountPoints).filter(mp => { const parents = getDeviceAncestors(devices, mp[1]); return parents.includes(disk) || mp[1] === disk; }); // Add rows for every defined bootloader Object.keys(devices) .filter(device => isBootloaderDevice({ device, devices })) .forEach(blDev => { const parents = getDeviceAncestors(devices, blDev); if (parents.includes(disk) || blDev === disk) { newMountPointRows.unshift(["", blDev]); } }); const swap = Object.keys(devices).find(device => devices[device].formatData.type.v === "swap"); if (swap) { const parents = getDeviceAncestors(devices, swap); if (parents.includes(disk) || swap === disk) { newMountPointRows.push(["swap", swap]); } } // For deleted device information we need to take a look at the original device tree const actionRows = actions.filter(action => { // Show only delete actions for partitions to not overload the summary with deleted children if ( !["destroy", "resize"].includes(action["action-type"].v) || action["object-description"].v !== "partition" ) { return false; } const parents = getDeviceAncestors(originalDevices, action["device-id"].v); return parents.includes(disk) || action["device-id"].v === disk; }); return (
{cockpit.format_bytes(deviceData.size.v)} {disk} {"(" + deviceData.description.v + ")"}
); }; /** * @returns {boolean} True is the device will be deleted according to the actions */ const isDeviceDeleted = ({ actions, device }) => ( actions.find(action => action["device-id"].v === device && action["action-type"].v === "destroy") ); /** * @returns {boolean} True is the device will be resized according to the actions */ const isDeviceResized = ({ actions, device }) => ( actions.find(action => action["device-id"].v === device && action["action-type"].v === "resize") ); const DeletedSystems = () => { const originalExistingSystems = useOriginalExistingSystems(); const plannedActions = usePlannedActions(); const deletedSystems = originalExistingSystems.filter( system => system.devices.v.every(device => isDeviceDeleted({ actions: plannedActions, device })) ); return deletedSystems.map(system => ( {cockpit.format(_("$0 will be deleted"), system["os-name"].v)} )); }; const AffectedSystems = ({ type }) => { const originalExistingSystems = useOriginalExistingSystems(); const originalDevices = useOriginalDevices(); const plannedActions = usePlannedActions(); const check = device => ( type === "delete" ? isDeviceDeleted({ actions: plannedActions, device }) : isDeviceResized({ actions: plannedActions, device }) ); const affectedSystems = originalExistingSystems.filter( system => { const systemDevices = system.devices.v; // If all partitions belonging to an OS are deleted the system is not handled // as affected but as deleted return ( systemDevices.some(check) && (type !== "delete" || !systemDevices.every(check)) ); } ); const getAffectedDevicesText = system => { const affectedDevices = system.devices.v.filter(check); const affectedDevicesPartitiongMap = Object.keys(originalDevices).reduce((acc, device) => { if (originalDevices[device].type.v !== "partition") { return acc; } const children = getDeviceChildren({ device, deviceData: originalDevices }); const affectedChildren = children.filter(child => affectedDevices.includes(child)); if (affectedDevices.includes(device) || affectedChildren.length) { acc[device] = affectedChildren.map(child => originalDevices[child].name.v); } return acc; }, {}); return Object.keys(affectedDevicesPartitiongMap) .map(device => { const deviceName = originalDevices[device].name.v; if (affectedDevicesPartitiongMap[device].length === 0) { return deviceName; } return `${deviceName} (${affectedDevicesPartitiongMap[device].join(", ")})`; }) .join(", "); }; const deleteText = system => cockpit.format( _("Deletion of certain partitions may prevent $0 from booting: $1"), system["os-name"].v, getAffectedDevicesText(system) ); const resizeText = system => cockpit.format( _("Resizing the following partitions from $0: $1"), system["os-name"].v, getAffectedDevicesText(system) ); return affectedSystems.map(system => ( {type === "delete" && deleteText(system)} {type === "resize" && resizeText(system)} )); }; export const StorageReviewNote = () => { const originalExistingSystems = useOriginalExistingSystems(); const plannedActions = usePlannedActions(); const hasNote = ( originalExistingSystems.filter( system => system.devices.v.some(device => ( isDeviceResized({ actions: plannedActions, device }) || isDeviceDeleted({ actions: plannedActions, device })) ) ).length ); if (!hasNote) return null; return ( ); };