diff --git a/ui/webui/src/actions/storage-actions.js b/ui/webui/src/actions/storage-actions.js index df5b9cd7b21..0fe6e7583a7 100644 --- a/ui/webui/src/actions/storage-actions.js +++ b/ui/webui/src/actions/storage-actions.js @@ -18,12 +18,14 @@ import cockpit from "cockpit"; import { + gatherRequests, getAllDiskSelection, getDeviceData, getDevices, getDiskFreeSpace, getDiskTotalSpace, getFormatData, + getPartitioningMethod, getUsableDisks, } from "../apis/storage.js"; @@ -82,3 +84,26 @@ export const getDiskSelectionAction = () => { }); }; }; + +export const getPartitioningDataAction = ({ requests, partitioning, updateOnly }) => { + return async function fetchUserThunk (dispatch) { + const props = { path: partitioning }; + const convertRequests = reqs => reqs.map(request => Object.entries(request).reduce((acc, [key, value]) => ({ ...acc, [key]: value.v }), {})); + + if (!updateOnly) { + props.method = await getPartitioningMethod({ partitioning }); + if (props.method === "MANUAL") { + const reqs = await gatherRequests({ partitioning }); + + props.requests = convertRequests(reqs[0]); + } + } else { + props.requests = convertRequests(requests); + } + + return dispatch({ + type: "GET_PARTITIONING_DATA", + payload: { path: partitioning, partitioningData: props } + }); + }; +}; diff --git a/ui/webui/src/apis/storage.js b/ui/webui/src/apis/storage.js index 29adafd04c9..f8b881d12cf 100644 --- a/ui/webui/src/apis/storage.js +++ b/ui/webui/src/apis/storage.js @@ -16,7 +16,10 @@ */ import cockpit from "cockpit"; -import { getDiskSelectionAction } from "../actions/storage-actions.js"; +import { + getDiskSelectionAction, + getPartitioningDataAction +} from "../actions/storage-actions.js"; export class StorageClient { constructor (address) { @@ -222,6 +225,26 @@ export const getPartitioningRequest = ({ partitioning }) => { ); }; +/** + * @param {string} partitioning DBus path to a partitioning + * + * @returns {Promise} The partitioning method + */ +export const getPartitioningMethod = ({ partitioning }) => { + return ( + new StorageClient().client.call( + partitioning, + "org.freedesktop.DBus.Properties", + "Get", + [ + "org.fedoraproject.Anaconda.Modules.Storage.Partitioning", + "PartitioningMethod", + ] + ) + .then(res => res[0].v) + ); +}; + /** * @returns {Promise} The applied partitioning */ @@ -401,6 +424,37 @@ export const setSelectedDisks = ({ drives }) => { ); }; +/* + * @param {string} partitioning DBus path to a partitioning + * @param {Array.} requests An array of request objects + */ +export const setManualPartitioningRequests = ({ partitioning, requests }) => { + return new StorageClient().client.call( + partitioning, + "org.freedesktop.DBus.Properties", + "Set", + [ + "org.fedoraproject.Anaconda.Modules.Storage.Partitioning.Manual", + "Requests", + cockpit.variant("aa{sv}", requests) + ] + ); +}; + +/** + * @param {string} partitioning DBus path to a partitioning + * + * @returns {Promise} The gathered requests for manual partitioning + */ +export const gatherRequests = ({ partitioning }) => { + return new StorageClient().client.call( + partitioning, + "org.fedoraproject.Anaconda.Modules.Storage.Partitioning.Manual", + "GatherRequests", + [] + ); +}; + export const startEventMonitorStorage = ({ dispatch }) => { return new StorageClient().client.subscribe( { }, @@ -409,10 +463,58 @@ export const startEventMonitorStorage = ({ dispatch }) => { case "PropertiesChanged": if (args[0] === "org.fedoraproject.Anaconda.Modules.Storage.DiskSelection") { dispatch(getDiskSelectionAction()); + } else if (args[0] === "org.fedoraproject.Anaconda.Modules.Storage.Partitioning.Manual" && Object.hasOwn(args[1], "Requests")) { + dispatch(getPartitioningDataAction({ requests: args[1].Requests.v, partitioning: path, updateOnly: true })); + } else if (args[0] === "org.fedoraproject.Anaconda.Modules.Storage" && Object.hasOwn(args[1], "CreatedPartitioning")) { + const last = args[1].CreatedPartitioning.v.length - 1; + dispatch(getPartitioningDataAction({ partitioning: args[1].CreatedPartitioning.v[last] })); + } else { + console.debug(`Unhandled signal on ${path}: ${iface}.${signal} ${JSON.stringify(args)}`); } break; default: - console.debug(`Unhandled signal on ${path}: ${iface}.${signal}`); + console.debug(`Unhandled signal on ${path}: ${iface}.${signal} ${JSON.stringify(args)}`); } }); }; + +export const initDataStorage = ({ dispatch }) => { + return new StorageClient().client.call( + "/org/fedoraproject/Anaconda/Modules/Storage", + "org.freedesktop.DBus.Properties", + "Get", + [ + "org.fedoraproject.Anaconda.Modules.Storage", + "CreatedPartitioning", + ] + ) + .then(([res]) => { + if (res.v.length !== 0) { + return res.v.forEach(path => dispatch(getPartitioningDataAction({ partitioning: path }))); + } + }); +}; + +export const applyStorage = async ({ partitioning, encrypt, encryptPassword, onFail, onSuccess }) => { + await setInitializeLabelsEnabled({ enabled: true }); + await setBootloaderDrive({ drive: "" }); + + const [part] = partitioning ? [partitioning] : await createPartitioning({ method: "AUTOMATIC" }); + + if (encrypt) { + await partitioningSetEncrypt({ partitioning: part, encrypt }); + } + if (encryptPassword) { + await partitioningSetPassphrase({ partitioning: part, passphrase: encryptPassword }); + } + + const tasks = await partitioningConfigureWithTask({ partitioning: part }); + + runStorageTask({ + task: tasks[0], + onFail, + onSuccess: () => applyPartitioning({ partitioning: part }) + .then(onSuccess) + .catch(onFail) + }); +}; diff --git a/ui/webui/src/components/AnacondaWizard.jsx b/ui/webui/src/components/AnacondaWizard.jsx index 71f747e5e8b..858ebd3d761 100644 --- a/ui/webui/src/components/AnacondaWizard.jsx +++ b/ui/webui/src/components/AnacondaWizard.jsx @@ -15,7 +15,7 @@ * along with This program; If not, see . */ import cockpit from "cockpit"; -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import { ActionList, @@ -32,8 +32,9 @@ import { WizardContextConsumer, } from "@patternfly/react-core"; -import { InstallationDestination, applyDefaultStorage } from "./storage/InstallationDestination.jsx"; +import { InstallationDestination } from "./storage/InstallationDestination.jsx"; import { StorageConfiguration, getScenario, getDefaultScenario } from "./storage/StorageConfiguration.jsx"; +import { CustomMountPoint } from "./storage/CustomMountPoint.jsx"; import { DiskEncryption, StorageEncryptionState } from "./storage/DiskEncryption.jsx"; import { InstallationLanguage } from "./localization/InstallationLanguage.jsx"; import { InstallationProgress } from "./installation/InstallationProgress.jsx"; @@ -41,7 +42,8 @@ import { ReviewConfiguration, ReviewConfigurationConfirmModal } from "./review/R import { exitGui } from "../helpers/exit.js"; import { usePageLocation } from "hooks"; import { - resetPartitioning + applyStorage, + resetPartitioning, } from "../apis/storage.js"; const _ = cockpit.gettext; @@ -53,6 +55,11 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, onAddE const [storageEncryption, setStorageEncryption] = useState(new StorageEncryptionState()); const [showPassphraseScreen, setShowPassphraseScreen] = useState(false); const [storageScenarioId, setStorageScenarioId] = useState(window.sessionStorage.getItem("storage-scenario-id") || getDefaultScenario().id); + const lastPartitioning = useMemo(() => { + const lastPartitioningKey = Object.keys(storageData.partitioning || {}).find(path => parseInt(path[path.length - 1]) === Object.keys(storageData.partitioning).length); + + return storageData.partitioning?.[lastPartitioningKey]; + }, [storageData.partitioning]); // On live media rebooting the system will actually shut it off const isBootIso = conf["Installation System"].type === "BOOT_ISO"; @@ -75,18 +82,26 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, onAddE label: _("Storage devices") }, { component: StorageConfiguration, - data: { selectedDisks: storageData.diskSelection.selectedDisks }, + data: { deviceData: storageData.devices, diskSelection: storageData.diskSelection }, id: "storage-configuration", label: _("Storage configuration") + }, { + component: CustomMountPoint, + data: { deviceData: storageData.devices, partitioningData: lastPartitioning, dispatch }, + id: "custom-mountpoint", + label: _("Custom mount point"), + isHidden: storageScenarioId !== "custom-mount-point" + }, { component: DiskEncryption, id: "disk-encryption", - label: _("Disk encryption") + label: _("Disk encryption"), + isHidden: storageScenarioId === "custom-mount-point" }] }, { component: ReviewConfiguration, - data: { deviceData: storageData.devices, diskSelection: storageData.diskSelection }, + data: { deviceData: storageData.devices, diskSelection: storageData.diskSelection, requests: lastPartitioning ? lastPartitioning.requests : null }, id: "installation-review", label: _("Review and install"), }, @@ -101,7 +116,9 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, onAddE for (const step of steps) { if (step.steps) { for (const childStep of step.steps) { - stepIds.push(childStep.id); + if (childStep?.isHidden !== true) { + stepIds.push(childStep.id); + } } } else { stepIds.push(step.id); @@ -126,7 +143,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, onAddE }; const createSteps = (stepsOrder) => { - const steps = stepsOrder.map((s, idx) => { + const steps = stepsOrder.filter(s => !s.isHidden).map(s => { let step = ({ id: s.id, name: s.label, @@ -181,6 +198,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, onAddE id="installation-wizard" footer={