From 91c86ee4834b57dd99dd27f252da002985103e15 Mon Sep 17 00:00:00 2001 From: Katerina Koukiou Date: Wed, 28 Jun 2023 14:32:50 +0200 Subject: [PATCH 1/4] webui: remove forgotten console.info object This get's reprinted on each re-render of the component polluting the browser console and therefore the test run outputs. --- ui/webui/src/components/app.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/webui/src/components/app.jsx b/ui/webui/src/components/app.jsx index b97ba6d1314..f2b56104784 100644 --- a/ui/webui/src/components/app.jsx +++ b/ui/webui/src/components/app.jsx @@ -100,7 +100,6 @@ export const Application = () => { if (!address || !conf) { return null; } - console.info("conf: ", conf); const title = cockpit.format("$0 installation", prettyName); From 792d3e94dba09b5a003ae1dcbe5ca211ffcb0b39 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Thu, 11 May 2023 12:55:05 +0200 Subject: [PATCH 2/4] webui: mount point assignment support Support a new storage configuration method where a user first has to manually partition and create file systems using the tools on the installation image. When selecting the custom mount point option the user can now select which partition to map to mount point either from a well known list of Mount points or a manually created one. Co-authored-by: Katerina Koukiou --- ui/webui/src/actions/storage-actions.js | 25 ++ ui/webui/src/apis/storage.js | 106 ++++++- ui/webui/src/components/AnacondaWizard.jsx | 56 +++- ui/webui/src/components/Common.jsx | 4 - ui/webui/src/components/app.jsx | 3 +- .../components/review/ReviewConfiguration.jsx | 66 ++++- .../components/storage/CustomMountPoint.jsx | 254 +++++++++++++++++ .../components/storage/CustomMountPoint.scss | 3 + .../storage/HelpAutopartOptions.jsx | 8 + .../storage/InstallationDestination.jsx | 44 +-- .../storage/StorageConfiguration.jsx | 37 ++- ui/webui/src/reducer.js | 5 + ui/webui/test/check-storage | 262 ++++++++++++++++++ ui/webui/test/helpers/installer.py | 5 +- ui/webui/test/helpers/review.py | 9 + ui/webui/test/helpers/storage.py | 9 +- ui/webui/test/reference | 2 +- 17 files changed, 812 insertions(+), 86 deletions(-) create mode 100644 ui/webui/src/components/storage/CustomMountPoint.jsx create mode 100644 ui/webui/src/components/storage/CustomMountPoint.scss 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={