Skip to content

Commit

Permalink
webui: mount point assignment support
Browse files Browse the repository at this point in the history
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 <kkoukiou@redhat.com>
  • Loading branch information
jelly and KKoukiou committed Jun 28, 2023
1 parent 91c86ee commit 312d20e
Show file tree
Hide file tree
Showing 15 changed files with 808 additions and 78 deletions.
25 changes: 25 additions & 0 deletions ui/webui/src/actions/storage-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import cockpit from "cockpit";

import {
gatherRequests,
getAllDiskSelection,
getDeviceData,
getDevices,
getDiskFreeSpace,
getDiskTotalSpace,
getFormatData,
getPartitioningMethod,
getUsableDisks,
} from "../apis/storage.js";

Expand Down Expand Up @@ -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 }
});
};
};
107 changes: 105 additions & 2 deletions ui/webui/src/apis/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -401,6 +424,37 @@ export const setSelectedDisks = ({ drives }) => {
);
};

/*
* @param {string} partitioning DBus path to a partitioning
* @param {Array.<Object>} 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(
{ },
Expand All @@ -409,10 +463,59 @@ 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)
});
};
56 changes: 47 additions & 9 deletions ui/webui/src/components/AnacondaWizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* along with This program; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from "cockpit";
import React, { useState } from "react";
import React, { useState, useMemo } from "react";

import {
ActionList,
Expand All @@ -32,16 +32,18 @@ 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";
import { ReviewConfiguration, ReviewConfigurationConfirmModal } from "./review/ReviewConfiguration.jsx";
import { exitGui } from "../helpers/exit.js";
import { usePageLocation } from "hooks";
import {
resetPartitioning
applyStorage,
resetPartitioning,
} from "../apis/storage.js";

const _ = cockpit.gettext;
Expand All @@ -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 => 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";
Expand All @@ -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"),
},
Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -181,6 +198,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, onAddE
id="installation-wizard"
footer={<Footer
isFormValid={isFormValid}
partitioning={lastPartitioning?.path}
setIsFormValid={setIsFormValid}
setStepNotification={setStepNotification}
isInProgress={isInProgress}
Expand Down Expand Up @@ -209,6 +227,7 @@ const Footer = ({
setIsFormValid,
setStepNotification,
isInProgress,
partitioning,
setIsInProgress,
storageEncryption,
showPassphraseScreen,
Expand All @@ -230,7 +249,7 @@ const Footer = ({
}
setIsInProgress(true);

applyDefaultStorage({
applyStorage({
onFail: ex => {
console.error(ex);
setIsInProgress(false);
Expand All @@ -249,6 +268,25 @@ const Footer = ({
});
} else if (activeStep.id === "installation-review") {
setNextWaitsConfirmation(true);
} else if (activeStep.id === "custom-mountpoint") {
setIsInProgress(true);

applyStorage({
partitioning,
onFail: ex => {
console.error(ex);
setIsInProgress(false);
setStepNotification({ step: activeStep.id, ...ex });
},
onSuccess: () => {
onNext();

// Reset the state after the onNext call. Otherwise,
// React will try to render the current step again.
setIsInProgress(false);
setStepNotification();
},
});
} else {
onNext();
}
Expand Down
3 changes: 2 additions & 1 deletion ui/webui/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { HelpDrawer } from "./HelpDrawer.jsx";

import { BossClient } from "../apis/boss.js";
import { LocalizationClient } from "../apis/localization.js";
import { StorageClient, startEventMonitorStorage } from "../apis/storage.js";
import { StorageClient, initDataStorage, startEventMonitorStorage } from "../apis/storage.js";
import { PayloadsClient } from "../apis/payloads";

import { readBuildstamp, getIsFinal } from "../helpers/betanag.js";
Expand Down Expand Up @@ -62,6 +62,7 @@ export const Application = () => {

setAddress(address);

initDataStorage({ dispatch });
startEventMonitorStorage({ dispatch });
});

Expand Down
Loading

0 comments on commit 312d20e

Please sign in to comment.