diff --git a/packaging/anaconda-webui.spec.in b/packaging/anaconda-webui.spec.in
index 1c80df3cbc..0eb43295cf 100644
--- a/packaging/anaconda-webui.spec.in
+++ b/packaging/anaconda-webui.spec.in
@@ -13,7 +13,7 @@ BuildRequires: gettext
%global anacondacorever 40.20
%global cockpitver 275
-%global cockpitstorver 310
+%global cockpitstorver 311
Requires: cockpit-storaged >= %{cockpitstorver}
Requires: cockpit-bridge >= %{cockpitver}
diff --git a/src/components/AnacondaWizard.jsx b/src/components/AnacondaWizard.jsx
index 136e409915..bd2891a873 100644
--- a/src/components/AnacondaWizard.jsx
+++ b/src/components/AnacondaWizard.jsx
@@ -65,6 +65,13 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
const osRelease = useContext(OsReleaseContext);
const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO";
const selectedDisks = storageData.diskSelection.selectedDisks;
+ const [scenarioPartitioningMapping, setScenarioPartitioningMapping] = useState({});
+
+ useEffect(() => {
+ if (storageScenarioId && storageData.partitioning.path) {
+ setScenarioPartitioningMapping({ [storageScenarioId]: storageData.partitioning.path });
+ }
+ }, [storageData.partitioning.path, storageScenarioId]);
const availableDevices = useMemo(() => {
return Object.keys(storageData.devices);
@@ -108,6 +115,8 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
deviceNames: storageData.deviceNames,
diskSelection: storageData.diskSelection,
dispatch,
+ partitioning: storageData.partitioning.path,
+ scenarioPartitioningMapping,
storageScenarioId,
setStorageScenarioId: (scenarioId) => {
window.sessionStorage.setItem("storage-scenario-id", scenarioId);
@@ -275,6 +284,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
deviceData={storageData.devices}
dispatch={dispatch}
onCritFail={onCritFail}
+ scenarioPartitioningMapping={scenarioPartitioningMapping}
selectedDisks={selectedDisks}
setShowStorage={setShowStorage}
setStorageScenarioId={setStorageScenarioId}
diff --git a/src/components/review/ReviewConfiguration.jsx b/src/components/review/ReviewConfiguration.jsx
index a7f51b37c0..4f58a65cd1 100644
--- a/src/components/review/ReviewConfiguration.jsx
+++ b/src/components/review/ReviewConfiguration.jsx
@@ -177,7 +177,14 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, language, local
{diskSelection.selectedDisks.map(disk => {
- return ;
+ return (
+
+ );
})}
diff --git a/src/components/storage/CockpitStorageIntegration.jsx b/src/components/storage/CockpitStorageIntegration.jsx
index 4175afed42..771678358f 100644
--- a/src/components/storage/CockpitStorageIntegration.jsx
+++ b/src/components/storage/CockpitStorageIntegration.jsx
@@ -16,17 +16,21 @@
* along with This program; If not, see .
*/
import cockpit from "cockpit";
-import React, { useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import {
+ ActionList,
Alert,
Button,
Card,
CardBody,
Flex,
FlexItem,
+ HelperText,
+ HelperTextItem,
List,
ListItem,
+ Modal,
PageSection,
PageSectionVariants,
Text,
@@ -35,61 +39,68 @@ import {
} from "@patternfly/react-core";
import { ArrowLeftIcon } from "@patternfly/react-icons";
-import { useRequiredSize, useMountPointConstraints } from "./Common.jsx";
+import { EmptyStatePanel } from "cockpit-components-empty-state";
+import { checkConfiguredStorage, checkUseFreeSpace } from "./InstallationScenario.jsx";
+import { useDiskTotalSpace, useDiskFreeSpace, useRequiredSize, useMountPointConstraints } from "./Common.jsx";
import {
runStorageTask,
scanDevicesWithTask,
} from "../../apis/storage.js";
+import {
+ unlockDevice,
+} from "../../apis/storage_devicetree.js";
+import {
+ setBootloaderDrive,
+} from "../../apis/storage_bootloader.js";
+import {
+ setInitializationMode,
+} from "../../apis/storage_disk_initialization.js";
+import {
+ applyStorage,
+ createPartitioning,
+ gatherRequests,
+ resetPartitioning,
+ setManualPartitioningRequests
+} from "../../apis/storage_partitioning.js";
+
import { getDevicesAction } from "../../actions/storage-actions.js";
+import { getDeviceNameByPath } from "../../helpers/storage.js";
import "./CockpitStorageIntegration.scss";
const _ = cockpit.gettext;
const idPrefix = "cockpit-storage-integration";
-const ReturnToInstallationButton = ({ dispatch, setShowStorage, onCritFail }) => {
- const [isInProgress, setIsInProgress] = useState(false);
-
- const rescanStorage = () => {
- setIsInProgress(true);
-
- return scanDevicesWithTask()
- .then(task => {
- return runStorageTask({
- task,
- onSuccess: () => dispatch(getDevicesAction())
- .then(() => {
- setIsInProgress(false);
- setShowStorage(false);
- }),
- onFail: exc => {
- setIsInProgress(false);
- onCritFail()(exc);
- }
- });
- });
- };
-
- return (
- }
- id={idPrefix + "-return-to-installation-button"}
- isLoading={isInProgress}
- isDisabled={isInProgress}
- variant="secondary"
- onClick={rescanStorage}>
- {_("Return to installation")}
-
- );
-};
+const ReturnToInstallationButton = ({ isDisabled, onAction }) => (
+ }
+ id={idPrefix + "-return-to-installation-button"}
+ isDisabled={isDisabled}
+ variant="secondary"
+ onClick={onAction}>
+ {_("Return to installation")}
+
+);
export const CockpitStorageIntegration = ({
+ scenarioAvailability,
+ scenarioPartitioningMapping,
+ selectedDisks,
+ setStorageScenarioId,
+ deviceData,
dispatch,
onCritFail,
setShowStorage,
}) => {
+ const [showDialog, setShowDialog] = useState(false);
+ const [needsResetPartitioning, setNeedsResetPartitioning] = useState(true);
+
+ useEffect(() => {
+ resetPartitioning().then(() => setNeedsResetPartitioning(false), onCritFail);
+ }, [onCritFail]);
+
return (
<>
+ isDisabled={needsResetPartitioning}
+ onAction={() => setShowDialog(true)} />
+ {showDialog &&
+ }
>
);
};
+export const preparePartitioning = async ({ deviceData, newMountPoints }) => {
+ try {
+ await setBootloaderDrive({ drive: "" });
+
+ const partitioning = await createPartitioning({ method: "MANUAL" });
+ const requests = await gatherRequests({ partitioning });
+
+ const addRequest = (devicePath, object, isSubVolume = false) => {
+ const { dir, type, subvolumes, content } = object;
+ let deviceSpec;
+ if (!isSubVolume) {
+ deviceSpec = getDeviceNameByPath(deviceData, devicePath);
+ } else if (deviceData[devicePath]) {
+ deviceSpec = devicePath;
+ } else {
+ return;
+ }
+
+ if (deviceSpec && (dir || type === "swap")) {
+ const existingRequestIndex = (
+ requests.findIndex(request => request["device-spec"].v === deviceSpec)
+ );
+
+ if (existingRequestIndex !== -1) {
+ requests[existingRequestIndex] = {
+ "mount-point": cockpit.variant("s", dir || type),
+ "device-spec": cockpit.variant("s", deviceSpec),
+ };
+ } else {
+ requests.push({
+ "mount-point": cockpit.variant("s", dir || type),
+ "device-spec": cockpit.variant("s", deviceSpec),
+ });
+ }
+ } else if (subvolumes) {
+ Object.keys(subvolumes).forEach(subvolume => addRequest(subvolume, subvolumes[subvolume], true));
+ } else if (type === "crypto") {
+ const clearTextDevice = deviceData[deviceSpec].children.v[0];
+ const clearTextDevicePath = deviceData[clearTextDevice].path.v;
+
+ addRequest(clearTextDevicePath, content);
+ }
+ };
+
+ Object.keys(newMountPoints).forEach(usedDevice => {
+ addRequest(usedDevice, newMountPoints[usedDevice]);
+ });
+
+ await setManualPartitioningRequests({ partitioning, requests });
+ return partitioning;
+ } catch (error) {
+ console.error("Failed to prepare partitioning", error);
+ }
+};
+
+const CheckStorageDialog = ({
+ deviceData,
+ dispatch,
+ onCritFail,
+ scenarioPartitioningMapping,
+ selectedDisks,
+ setShowDialog,
+ setShowStorage,
+ setStorageScenarioId,
+}) => {
+ const [error, setError] = useState();
+ const [checkStep, setCheckStep] = useState("rescan");
+ const diskTotalSpace = useDiskTotalSpace({ selectedDisks, devices: deviceData });
+ const diskFreeSpace = useDiskFreeSpace({ selectedDisks, devices: deviceData });
+ const mountPointConstraints = useMountPointConstraints();
+ const requiredSize = useRequiredSize();
+
+ const newMountPoints = useMemo(() => JSON.parse(window.sessionStorage.getItem("cockpit_mount_points") || "{}"), []);
+ const cockpitPassphrases = useMemo(() => JSON.parse(window.sessionStorage.getItem("cockpit_passphrases") || "{}"), []);
+
+ const useConfiguredStorage = useMemo(() => {
+ const availability = checkConfiguredStorage({
+ deviceData,
+ mountPointConstraints,
+ scenarioPartitioningMapping,
+ newMountPoints,
+ });
+
+ return availability.available;
+ }, [deviceData, mountPointConstraints, newMountPoints, scenarioPartitioningMapping]);
+
+ const useFreeSpace = useMemo(() => {
+ const availability = checkUseFreeSpace({ diskFreeSpace, diskTotalSpace, requiredSize });
+
+ return availability.available && !availability.hidden;
+ }, [diskFreeSpace, diskTotalSpace, requiredSize]);
+
+ const loading = !error && checkStep !== undefined;
+ const storageRequirementsNotMet = !loading && (error || (!useConfiguredStorage && !useFreeSpace));
+
+ useEffect(() => {
+ if (!useConfiguredStorage && checkStep === "prepare-partitioning") {
+ setCheckStep();
+ }
+ }, [useConfiguredStorage, checkStep]);
+
+ useEffect(() => {
+ if (checkStep !== "luks") {
+ return;
+ }
+
+ const devicesToUnlock = (
+ Object.keys(cockpitPassphrases)
+ .map(dev => ({
+ deviceName: deviceData[dev] ? dev : getDeviceNameByPath(deviceData, dev),
+ passphrase: cockpitPassphrases[dev]
+ })))
+ .filter(({ devicePath, deviceName }) => {
+ return (
+ deviceData[deviceName].formatData.type.v === "luks" &&
+ deviceData[deviceName].formatData.attrs.v.has_key !== "True"
+ );
+ });
+
+ if (devicesToUnlock.some(dev => !dev.passphrase)) {
+ onCritFail()({ message: _("Cockpit storage did not provide the passphrase to unlock encrypted device.") });
+ }
+
+ if (devicesToUnlock.length === 0) {
+ setCheckStep("prepare-partitioning");
+ return;
+ }
+
+ Promise.all(devicesToUnlock.map(unlockDevice))
+ .catch(exc => {
+ setCheckStep();
+ setError(exc);
+ })
+ .then(() => {
+ dispatch(getDevicesAction());
+ });
+ }, [dispatch, checkStep, cockpitPassphrases, newMountPoints, deviceData, onCritFail, setError]);
+
+ useEffect(() => {
+ // If the required devices needed for manual partitioning are set up,
+ // and prepare the partitioning
+ if (checkStep !== "prepare-partitioning") {
+ return;
+ }
+
+ const applyNewPartitioning = async () => {
+ // CLEAR_PARTITIONS_NONE = 0
+ try {
+ await setInitializationMode({ mode: 0 });
+ const partitioning = await preparePartitioning({ deviceData, newMountPoints });
+
+ applyStorage({
+ partitioning,
+ onFail: exc => {
+ setCheckStep();
+ setError(exc);
+ },
+ onSuccess: () => setCheckStep(),
+ });
+ } catch (exc) {
+ setCheckStep();
+ setError(exc);
+ }
+ };
+
+ applyNewPartitioning();
+ }, [deviceData, checkStep, newMountPoints, useConfiguredStorage]);
+
+ useEffect(() => {
+ if (checkStep !== "rescan" || useConfiguredStorage === undefined) {
+ return;
+ }
+
+ // When the dialog is shown rescan to get latest configured storage
+ // and check if we need to prepare manual partitioning
+ scanDevicesWithTask()
+ .then(task => {
+ return runStorageTask({
+ task,
+ onSuccess: () => dispatch(getDevicesAction())
+ .then(() => {
+ setCheckStep("luks");
+ })
+ .catch(exc => {
+ setCheckStep();
+ setError(exc);
+ }),
+ onFail: exc => {
+ setCheckStep();
+ setError(exc);
+ }
+ });
+ });
+ }, [useConfiguredStorage, checkStep, dispatch, setError]);
+
+ const goBackToInstallation = () => {
+ const mode = useConfiguredStorage ? "use-configured-storage" : "use-free-space";
+
+ setStorageScenarioId(mode);
+ setShowStorage(false);
+ };
+
+ const loadingDescription = (
+
+ );
+
+ const modalProps = {};
+ if (!loading) {
+ if (storageRequirementsNotMet) {
+ modalProps.title = _("Storage requirements not met");
+ } else {
+ modalProps.title = _("Continue with installation");
+ }
+ } else {
+ modalProps["aria-label"] = _("Checking storage configuration");
+ }
+
+ return (
+ setShowDialog(false)}
+ titleIconVariant={!loading && storageRequirementsNotMet && "warning"}
+ position="top" variant="small" isOpen
+ {...modalProps}
+ footer={
+ !loading &&
+ <>
+
+ {!storageRequirementsNotMet &&
+ <>
+
+
+ >}
+ {storageRequirementsNotMet &&
+ <>
+
+
+ >}
+
+ >
+ }
+ >
+ <>
+ {loading && loadingDescription}
+ {!loading &&
+ <>
+ {storageRequirementsNotMet ? error?.message : null}
+
+ {!storageRequirementsNotMet &&
+
+ {_("Current configuration can be used for installation.")}
+ }
+
+ >}
+ >
+
+
+ );
+};
+
const ModifyStorageSideBar = () => {
const mountPointConstraints = useMountPointConstraints();
const requiredSize = useRequiredSize();
diff --git a/src/components/storage/CockpitStorageIntegration.scss b/src/components/storage/CockpitStorageIntegration.scss
index e2952616e8..11e27b9d63 100644
--- a/src/components/storage/CockpitStorageIntegration.scss
+++ b/src/components/storage/CockpitStorageIntegration.scss
@@ -3,6 +3,10 @@
width: 100%;
}
+.cockpit-storage-integration-page-section-storage-alert {
+ padding-bottom: 0;
+}
+
.cockpit-storage-integration-iframe-cockpit-storage {
height: 100%;
}
@@ -32,3 +36,13 @@ ul.cockpit-storage-integration-requirements-hint-list {
.cockpit-storage-integration-requirements-hint-detail {
font-size: small;
}
+
+// Hide the [x] button in the loading mode
+.cockpit-storage-integration-check-storage-dialog--loading .pf-v5-c-modal-box__close {
+ display: none;
+}
+
+// Make Spinner smaller - default EmptyStatePanel svg size is too big
+.cockpit-storage-integration-check-storage-dialog--loading svg.pf-v5-c-spinner {
+ --pf-v5-c-spinner--diameter: var(--pf-v5-c-spinner--m-lg--diameter);
+}
diff --git a/src/components/storage/InstallationMethod.jsx b/src/components/storage/InstallationMethod.jsx
index e9f017e575..aa1e6172ec 100644
--- a/src/components/storage/InstallationMethod.jsx
+++ b/src/components/storage/InstallationMethod.jsx
@@ -37,6 +37,8 @@ export const InstallationMethod = ({
isEfi,
isFormDisabled,
onCritFail,
+ partitioning,
+ scenarioPartitioningMapping,
setIsFormDisabled,
setIsFormValid,
setShowStorage,
@@ -64,11 +66,13 @@ export const InstallationMethod = ({
{
const availability = new AvailabilityState();
+
+ availability.hidden = false;
+
if (diskTotalSpace < requiredSize) {
availability.available = false;
availability.reason = _("Not enough space on selected disks.");
@@ -58,8 +60,11 @@ const checkEraseAll = ({ requiredSize, diskTotalSpace }) => {
return availability;
};
-const checkUseFreeSpace = ({ diskFreeSpace, diskTotalSpace, requiredSize }) => {
+export const checkUseFreeSpace = ({ diskFreeSpace, diskTotalSpace, requiredSize }) => {
const availability = new AvailabilityState();
+
+ availability.hidden = false;
+
if (diskFreeSpace > 0 && diskTotalSpace > 0) {
availability.hidden = diskFreeSpace === diskTotalSpace;
}
@@ -79,6 +84,8 @@ const checkUseFreeSpace = ({ diskFreeSpace, diskTotalSpace, requiredSize }) => {
const checkMountPointMapping = ({ hasFilesystems, duplicateDeviceNames }) => {
const availability = new AvailabilityState();
+ availability.hidden = false;
+
if (!hasFilesystems) {
availability.available = false;
availability.reason = _("No usable devices on the selected disks.");
@@ -92,7 +99,57 @@ const checkMountPointMapping = ({ hasFilesystems, duplicateDeviceNames }) => {
return availability;
};
-const scenarios = [{
+export const checkConfiguredStorage = ({ deviceData, mountPointConstraints, partitioning, newMountPoints, scenarioPartitioningMapping }) => {
+ const availability = new AvailabilityState();
+
+ const currentPartitioningMatches = partitioning !== undefined && scenarioPartitioningMapping["use-configured-storage"] === partitioning;
+ availability.hidden = partitioning === undefined || !currentPartitioningMatches;
+
+ availability.available = (
+ newMountPoints === undefined ||
+ (
+ mountPointConstraints
+ ?.filter(m => m.required.v)
+ .every(m => {
+ const allDirs = [];
+ const getNestedDirs = (object) => {
+ if (!object) {
+ return;
+ }
+ const { dir, content, subvolumes } = object;
+
+ if (dir) {
+ allDirs.push(dir);
+ }
+ if (content) {
+ getNestedDirs(content);
+ }
+ if (subvolumes) {
+ Object.keys(subvolumes).forEach(sv => getNestedDirs(subvolumes[sv]));
+ }
+ };
+
+ if (m["mount-point"].v) {
+ Object.keys(newMountPoints).forEach(key => getNestedDirs(newMountPoints[key]));
+
+ return allDirs.includes(m["mount-point"].v);
+ }
+
+ if (m["required-filesystem-type"].v === "biosboot") {
+ const biosboot = Object.keys(deviceData).find(d => deviceData[d].formatData.type.v === "biosboot");
+
+ return biosboot !== undefined;
+ }
+
+ return false;
+ })
+ )
+ );
+
+ return availability;
+};
+
+export const scenarios = [{
id: "erase-all",
label: _("Erase data and install"),
detail: helpEraseAll,
@@ -134,7 +191,22 @@ const scenarios = [{
dialogTitleIconVariant: "",
dialogWarningTitle: _("Install on the custom mount points?"),
dialogWarning: _("The installation will use your configured partitioning layout."),
-}];
+}, {
+ id: "use-configured-storage",
+ label: _("Use configured storage"),
+ default: false,
+ detail: helpConfiguredStorage,
+ check: checkConfiguredStorage,
+ // CLEAR_PARTITIONS_NONE = 0
+ initializationMode: 0,
+ buttonLabel: _("Install"),
+ buttonVariant: "danger",
+ screenWarning: _("To prevent loss, make sure to backup your data."),
+ dialogTitleIconVariant: "",
+ dialogWarningTitle: _("Install using the configured storage?"),
+ dialogWarning: _("The installation will use your configured partitioning layout."),
+}
+];
export const getScenario = (scenarioId) => {
return scenarios.filter(s => s.id === scenarioId)[0];
@@ -157,12 +229,13 @@ const InstallationScenarioSelector = ({
idPrefix,
isFormDisabled,
onCritFail,
+ partitioning,
+ scenarioPartitioningMapping,
selectedDisks,
setIsFormValid,
setStorageScenarioId,
storageScenarioId,
}) => {
- const [selectedScenario, setSelectedScenario] = useState();
const [scenarioAvailability, setScenarioAvailability] = useState(Object.fromEntries(
scenarios.map((s) => [s.id, new AvailabilityState()])
));
@@ -170,21 +243,56 @@ const InstallationScenarioSelector = ({
const diskFreeSpace = useDiskFreeSpace({ selectedDisks, devices: deviceData });
const duplicateDeviceNames = useDuplicateDeviceNames({ deviceNames });
const hasFilesystems = useHasFilesystems({ selectedDisks, devices: deviceData });
+ const mountPointConstraints = useMountPointConstraints();
const requiredSize = useRequiredSize();
+ useEffect(() => {
+ if ([diskTotalSpace, diskFreeSpace, hasFilesystems, mountPointConstraints, requiredSize].some(itm => itm === undefined)) {
+ return;
+ }
+
+ setScenarioAvailability(oldAvailability => {
+ const newAvailability = {};
+
+ for (const scenario of scenarios) {
+ const availability = scenario.check({
+ diskFreeSpace,
+ diskTotalSpace,
+ duplicateDeviceNames,
+ hasFilesystems,
+ mountPointConstraints,
+ partitioning,
+ requiredSize,
+ scenarioPartitioningMapping,
+ storageScenarioId,
+ });
+ newAvailability[scenario.id] = availability;
+ }
+ return newAvailability;
+ });
+ }, [
+ diskFreeSpace,
+ diskTotalSpace,
+ duplicateDeviceNames,
+ hasFilesystems,
+ mountPointConstraints,
+ partitioning,
+ requiredSize,
+ storageScenarioId,
+ scenarioPartitioningMapping,
+ ]);
+
useEffect(() => {
let selectedScenarioId = "";
let availableScenarioExists = false;
- if ([diskTotalSpace, diskFreeSpace, hasFilesystems, requiredSize].some(itm => itm === undefined)) {
+ if (storageScenarioId && scenarioAvailability[storageScenarioId].available === undefined) {
return;
}
- const newAvailability = {};
for (const scenario of scenarios) {
- const availability = scenario.check({ diskTotalSpace, diskFreeSpace, hasFilesystems, requiredSize, duplicateDeviceNames });
- newAvailability[scenario.id] = availability;
- if (availability.available) {
+ const availability = scenarioAvailability[scenario.id];
+ if (!availability.hidden && availability.available) {
availableScenarioExists = true;
if (scenario.id === storageScenarioId) {
console.log(`Selecting backend scenario ${scenario.id}`);
@@ -196,26 +304,24 @@ const InstallationScenarioSelector = ({
}
}
}
- setSelectedScenario(selectedScenarioId);
- setScenarioAvailability(newAvailability);
+ if (availableScenarioExists) {
+ setStorageScenarioId(selectedScenarioId);
+ }
setIsFormValid(availableScenarioExists);
- }, [deviceData, hasFilesystems, requiredSize, diskFreeSpace, diskTotalSpace, duplicateDeviceNames, setIsFormValid, storageScenarioId]);
+ }, [scenarioAvailability, setStorageScenarioId, setIsFormValid, storageScenarioId]);
useEffect(() => {
const applyScenario = async (scenarioId) => {
const scenario = getScenario(scenarioId);
- setStorageScenarioId(scenarioId);
- console.log("Updating scenario selected in backend to", scenario.id);
-
await setInitializationMode({ mode: scenario.initializationMode }).catch(console.error);
};
- if (selectedScenario) {
- applyScenario(selectedScenario);
+ if (storageScenarioId) {
+ applyScenario(storageScenarioId);
}
- }, [selectedScenario, setStorageScenarioId]);
+ }, [storageScenarioId]);
const onScenarioToggled = (scenarioId) => {
- setSelectedScenario(scenarioId);
+ setStorageScenarioId(scenarioId);
};
const scenarioItems = scenarios.filter(scenario => !scenarioAvailability[scenario.id].hidden).map(scenario => (
@@ -244,7 +350,19 @@ const InstallationScenarioSelector = ({
return scenarioItems;
};
-export const InstallationScenario = ({ deviceData, deviceNames, diskSelection, idPrefix, isFormDisabled, onCritFail, setIsFormValid, storageScenarioId, setStorageScenarioId }) => {
+export const InstallationScenario = ({
+ deviceData,
+ deviceNames,
+ idPrefix,
+ isFormDisabled,
+ onCritFail,
+ partitioning,
+ scenarioPartitioningMapping,
+ selectedDisks,
+ setIsFormValid,
+ setStorageScenarioId,
+ storageScenarioId,
+}) => {
const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO";
const headingLevel = isBootIso ? "h2" : "h3";
@@ -255,13 +373,15 @@ export const InstallationScenario = ({ deviceData, deviceNames, diskSelection, i
>
diff --git a/src/components/storage/ModifyStorage.jsx b/src/components/storage/ModifyStorage.jsx
index 0626a9c69f..3afc2444b8 100644
--- a/src/components/storage/ModifyStorage.jsx
+++ b/src/components/storage/ModifyStorage.jsx
@@ -47,8 +47,6 @@ export const ModifyStorage = ({ idPrefix, onCritFail, onRescan, setShowStorage,
icon={}
onClick={() => {
window.sessionStorage.setItem("cockpit_anaconda", cockpitAnaconda);
- // FIXME: Remove when cockpit-storaged 311 is available in Rawhide
- window.localStorage.setItem("cockpit_anaconda", cockpitAnaconda);
setShowStorage(true);
}}
>
diff --git a/test/check-storage b/test/check-storage
index 9954db3acb..5102611b10 100755
--- a/test/check-storage
+++ b/test/check-storage
@@ -539,6 +539,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageCase):
s = Storage(b, m)
disk1 = "/dev/vda"
+ dev1 = "vda"
s.partition_disk(
disk1,
[("1MiB", "biosboot"), ("1GB", "xfs"), ("1GB", "xfs"), ("1GB", "xfs"), ("", "xfs")]
@@ -563,15 +564,6 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageCase):
i.reach(i.steps.INSTALLATION_METHOD)
s.rescan_disks()
- self._testEncryptedUnlock(b, m)
-
- def _testEncryptedUnlock(self, b, m):
- dev1 = "vda"
-
- i = Installer(b, m)
- s = Storage(b, m)
- r = Review(b)
-
s.select_mountpoint([(dev1, True)], encrypted=True)
s.unlock_all_encrypted()
@@ -595,6 +587,15 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageCase):
s.unlock_device("einszweidreivier", ["vda4"], ["vda4"])
b.wait_not_present("#mount-point-mapping-table tbody tr:nth-child(4) td[data-label='Format type'] #unlock-luks-btn")
+ self._testEncryptedUnlock(b, m)
+
+ def _testEncryptedUnlock(self, b, m):
+ dev1 = "vda"
+
+ i = Installer(b, m)
+ s = Storage(b, m)
+ r = Review(b)
+
s.check_mountpoint_row_mountpoint(2, "/boot")
s.select_mountpoint_row_device(2, "vda5")
@@ -620,7 +621,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageCase):
i.open()
i.reach(i.steps.INSTALLATION_METHOD)
- s.check_single_disk_destination("vda", "16.1 GB")
+ s.check_single_disk_destination("vda")
s.modify_storage()
b.wait_visible(".cockpit-storage-integration-sidebar")
@@ -672,8 +673,11 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageCase):
# Exit the cockpit-storage iframe
b.switch_to_top()
s.return_to_installation()
+ s.return_to_installation_confirm()
i.wait_current_page(i.steps.INSTALLATION_METHOD)
+ s.select_mountpoint([("vda", True)])
+
self._testEncryptedUnlock(b, m)
def testEncryptedUnlockRAIDonLUKS(self):
@@ -1047,6 +1051,215 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageCase):
s.check_mountpoint_row_device_available(1, f"{dev}3", False)
+class TestStorageCockpitIntegration(anacondalib.VirtInstallMachineCase, StorageCase):
+ @nondestructive
+ def testEncryptedUnlock(self):
+ b = self.browser
+ m = self.machine
+ i = Installer(b, m, scenario="use-configured-storage")
+ s = Storage(b, m)
+ r = Review(b)
+
+ dev = "vda"
+
+ i.open()
+ i.reach(i.steps.INSTALLATION_METHOD)
+ s.wait_scenario_visible("use-configured-storage", False)
+ s.check_single_disk_destination("vda")
+ s.modify_storage()
+ b.wait_visible(".cockpit-storage-integration-sidebar")
+
+ frame = "iframe[name='cockpit-storage']"
+ b._wait_present(frame)
+ b.switch_to_frame("cockpit-storage")
+ b._wait_present("#storage.ct-page-fill")
+
+ self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
+ self.confirm()
+
+ self.click_dropdown(self.card_row("Storage", 2), "Create partition")
+ self.dialog({"size": 1, "type": "biosboot"})
+
+ self.click_dropdown(self.card_row("Storage", 3), "Create partition")
+ self.dialog({"size": 1070, "type": "ext4", "mount_point": "/boot"})
+
+ self.click_dropdown(self.card_row("Storage", 4), "Create partition")
+ self.dialog({
+ "type": "ext4", "mount_point": "/",
+ "crypto": self.default_crypto_type,
+ "passphrase": "redhat",
+ "passphrase2": "redhat",
+ })
+ # FIXME: Cockpit should leave open LUKS devices afetr creation
+ self.click_card_row("Storage", name=(dev + "3"))
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog({"passphrase": "redhat"})
+ self.wait_mounted("ext4 filesystem")
+
+ # Exit the cockpit-storage iframe
+ b.switch_to_top()
+
+ s.return_to_installation()
+ s.return_to_installation_confirm()
+
+ s.set_partitioning("use-configured-storage")
+
+ self.addCleanup(lambda: dbus_reset_users(m))
+ i.reach(i.steps.REVIEW)
+
+ r.check_in_disk_row(dev, 2, "luks-")
+
+ @nondestructive
+ def testLVM(self):
+ b = self.browser
+ m = self.machine
+ i = Installer(b, m, scenario="use-configured-storage")
+ s = Storage(b, m)
+ r = Review(b)
+
+ vgname = "fedoravg"
+
+ self.addCleanup(m.execute, f"vgremove -y -ff {vgname}")
+
+ disk = "/dev/vda"
+ dev = "vda"
+
+ i.open()
+ i.reach(i.steps.INSTALLATION_METHOD)
+ s.wait_scenario_visible("use-configured-storage", False)
+ s.check_single_disk_destination("vda")
+ s.modify_storage()
+ b.wait_visible(".cockpit-storage-integration-sidebar")
+
+ frame = "iframe[name='cockpit-storage']"
+ b._wait_present(frame)
+ b.switch_to_frame("cockpit-storage")
+ b._wait_present("#storage.ct-page-fill")
+
+ self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
+ self.confirm()
+
+ self.click_dropdown(self.card_row("Storage", 2), "Create partition")
+ self.dialog({"size": 1, "type": "biosboot"})
+
+ self.click_dropdown(self.card_row("Storage", 3), "Create partition")
+ self.dialog({"size": 1070, "type": "ext4", "mount_point": "/boot"})
+
+ self.click_devices_dropdown("Create LVM2 volume group")
+ self.dialog({"name": vgname, "disks": {dev: True}})
+
+ self.click_card_row("Storage", name=vgname)
+
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog({"name": "root", "size": 6010})
+ self.click_card_row("LVM2 logical volumes", 1)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4", "mount_point": "/"})
+
+ b.click(self.card_parent_link())
+
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog({"name": "home", "size": 8120})
+ self.click_card_row("LVM2 logical volumes", 1)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4", "mount_point": "/home"})
+
+ b.click(self.card_parent_link())
+
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog({"name": "swap", "size": 898})
+ self.click_card_row("LVM2 logical volumes", 3)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "swap"})
+
+ # Exit the cockpit-storage iframe
+ b.switch_to_top()
+
+ s.return_to_installation()
+ s.return_to_installation_confirm()
+
+ s.set_partitioning("use-configured-storage")
+
+ self.addCleanup(lambda: dbus_reset_users(m))
+ i.reach(i.steps.REVIEW)
+
+ # verify review screen
+ disk = "vda"
+ r.check_disk(disk, "16.1 GB vda (0x1af4)")
+
+ r.check_disk_row(disk, 1, "vda2, 1.07 GB: mount, /boot")
+ r.check_disk_row(disk, 2, f"{vgname}-root, 6.01 GB: mount, /")
+ r.check_disk_row(disk, 3, f"{vgname}-home, 8.12 GB: mount, /home")
+ r.check_disk_row(disk, 4, f"{vgname}-swap, 898 MB: mount, swap")
+
+ @nondestructive
+ def testBtrfsSubvolumes(self):
+ b = self.browser
+ m = self.machine
+ i = Installer(b, m, scenario="use-configured-storage")
+ s = Storage(b, m)
+ r = Review(b)
+
+ tmp_mount = "/btrfs-mount-test"
+
+ i.open()
+ i.reach(i.steps.INSTALLATION_METHOD)
+ s.wait_scenario_visible("use-configured-storage", False)
+ s.check_single_disk_destination("vda")
+ s.modify_storage()
+ b.wait_visible(".cockpit-storage-integration-sidebar")
+
+ frame = "iframe[name='cockpit-storage']"
+ b._wait_present(frame)
+ b.switch_to_frame("cockpit-storage")
+ b._wait_present("#storage.ct-page-fill")
+
+ self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
+ self.confirm()
+
+ self.click_dropdown(self.card_row("Storage", 2), "Create partition")
+ self.dialog({"size": 1, "type": "biosboot"})
+
+ self.click_dropdown(self.card_row("Storage", 3), "Create partition")
+ self.dialog({"size": 1070, "type": "ext4", "mount_point": "/boot"})
+
+ self.click_dropdown(self.card_row("Storage", 4), "Create partition")
+ self.dialog({"type": "btrfs"})
+
+ self.click_card_row("Storage", name="/")
+
+ b.click(self.card_button("btrfs subvolume", "Mount"))
+ self.dialog({"mount_point": tmp_mount})
+
+ b.click(self.card_button("btrfs subvolume", "Create subvolume"))
+ self.dialog({"name": "root", "mount_point": "/"})
+
+ b.click(self.card_button("btrfs subvolume", "Create subvolume"))
+ self.dialog({"name": "unused"})
+
+ b.click(self.card_button("btrfs subvolume", "Create subvolume"))
+ self.dialog({"name": "home", "mount_point": "/home"})
+
+ # Exit the cockpit-storage iframe
+ b.switch_to_top()
+
+ s.return_to_installation()
+ s.return_to_installation_confirm()
+
+ s.set_partitioning("use-configured-storage")
+
+ self.addCleanup(lambda: dbus_reset_users(m))
+ i.reach(i.steps.REVIEW)
+
+ # verify review screen
+ dev = "vda"
+ r.check_disk(dev, "16.1 GB vda (0x1af4)")
+
+ r.check_disk_row(dev, 1, "vda2, 1.07 GB: mount, /boot")
+ r.check_disk_row(dev, 2, "root, 15.0 GB: mount, /")
+ r.check_disk_row(dev, 3, "home, 15.0 GB: mount, /home")
+
+
class TestStorageMountPointsEFI(anacondalib.VirtInstallMachineCase):
efi = True
diff --git a/test/helpers/installer.py b/test/helpers/installer.py
index a23d234b6a..fa73878637 100644
--- a/test/helpers/installer.py
+++ b/test/helpers/installer.py
@@ -31,7 +31,6 @@ class InstallerSteps(UserDict):
_steps_jump = {}
_steps_jump[WELCOME] = [INSTALLATION_METHOD]
- _steps_jump[INSTALLATION_METHOD] = [DISK_ENCRYPTION, CUSTOM_MOUNT_POINT]
_steps_jump[DISK_ENCRYPTION] = [ACCOUNTS]
_steps_jump[CUSTOM_MOUNT_POINT] = [ACCOUNTS]
_steps_jump[ACCOUNTS] = [REVIEW]
@@ -47,12 +46,18 @@ class InstallerSteps(UserDict):
class Installer():
- def __init__(self, browser, machine, hidden_steps=None):
+ def __init__(self, browser, machine, hidden_steps=None, scenario=None):
self.browser = browser
self.machine = machine
self.steps = InstallerSteps()
self.hidden_steps = hidden_steps or []
+ if (scenario == 'use-configured-storage'):
+ self.steps._steps_jump[self.steps.INSTALLATION_METHOD] = [self.steps.ACCOUNTS]
+ self.hidden_steps.extend([self.steps.CUSTOM_MOUNT_POINT, self.steps.DISK_ENCRYPTION])
+ else:
+ self.steps._steps_jump[self.steps.INSTALLATION_METHOD] = [self.steps.DISK_ENCRYPTION, self.steps.CUSTOM_MOUNT_POINT]
+
@log_step(snapshot_before=True)
def begin_installation(self, should_fail=False, confirm_erase=True):
current_page = self.get_current_page()
diff --git a/test/helpers/storage.py b/test/helpers/storage.py
index 5b192e3870..2dd8ff8629 100644
--- a/test/helpers/storage.py
+++ b/test/helpers/storage.py
@@ -113,6 +113,10 @@ def check_constraint(self, constraint, required=True):
def return_to_installation(self):
self.browser.click("#cockpit-storage-integration-return-to-installation-button")
+ def return_to_installation_confirm(self):
+ with self.browser.wait_timeout(30):
+ self.browser.click("#cockpit-storage-integration-check-storage-dialog-continue")
+
def modify_storage(self):
self.browser.click(f"#{self._step}-modify-storage")
diff --git a/test/vm.install b/test/vm.install
index cb31e81f4d..a75b208864 100755
--- a/test/vm.install
+++ b/test/vm.install
@@ -12,7 +12,7 @@ import sys
BOTS_DIR = os.path.realpath(f'{__file__}/../../bots')
sys.path.append(BOTS_DIR)
-missing_packages = "cockpit-ws cockpit-bridge fedora-logos"
+missing_packages = "cockpit-ws cockpit-bridge fedora-logos udisks2-lvm2 udisks2-btrfs"
# Install missing firefox dependencies.
# Resolving all dependencies with dnf download is possible,
# but it packs to many packages to updates.img
@@ -49,8 +49,7 @@ def vm_install(image, verbose, quick):
# cockpit-storaged is also available in the default rawhide compose, make sure we don't pull it from there
download_from_copr(f"packit/cockpit-project-cockpit-{cockpit_pr}", "cockpit-storaged", machine)
else:
- # FIXME: Download cockpit-storaged from custom COPR
- # till https://github.com/cockpit-project/cockpit/pull/19924#event-11785480839 is released
+ # FIXME: Download cockpit-storaged from custom COPR till release 311 is available in rawhide
download_from_copr("@cockpit/main-builds", "cockpit-storaged", machine)
# Build anaconda-webui from SRPM unless we are testing a anaconda-webui PR scenario