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 29, 2023
1 parent 91c86ee commit 792d3e9
Show file tree
Hide file tree
Showing 17 changed files with 812 additions and 86 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 }
});
};
};
106 changes: 104 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,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)
});
};
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 => 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";
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
4 changes: 0 additions & 4 deletions ui/webui/src/components/Common.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ export const AddressContext = createContext("");
export const ConfContext = createContext();
export const LanguageContext = createContext("");

export const sleep = ({ seconds }) => {
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
};

export const FormGroupHelpPopover = ({ helpContent }) => {
return (
<Popover
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 792d3e9

Please sign in to comment.