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 27, 2023
1 parent d0f6267 commit 39bb620
Show file tree
Hide file tree
Showing 15 changed files with 804 additions and 36 deletions.
28 changes: 28 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,29 @@ export const getDiskSelectionAction = () => {
});
};
};

export const getPartitioningDataAction = ({ requests, partitioning, idx, 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]);
}
if (idx) {
props.idx = idx;
}
} else {
props.requests = convertRequests(requests);
}

return dispatch({
type: "GET_PARTITIONING_DATA",
payload: { path: partitioning, partitioningData: props }
});
};
};
99 changes: 97 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 @@ -62,6 +65,22 @@ export const createPartitioning = ({ method }) => {
);
};

/**
* @param {string} method A partitioning method
*
* @returns {Promise} Resolves the DBus path to the partitioning
*/
export const findPartitioning = ({ partitioning, method }) => {
if (partitioning?.method !== method) {
return createPartitioning({ method });
} else {
return new Promise((resolve) => {
// Return as list to mimmick createPartitioning
resolve([partitioning[partitioning.length - 1]]);
});
}
};

/**
* @returns {Promise} Resolves all properties of DiskSelection interface
*/
Expand Down Expand Up @@ -222,6 +241,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 +440,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 +479,35 @@ 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 idx = args[1].CreatedPartitioning.v.length - 1;

dispatch(getPartitioningDataAction({ partitioning: args[1].CreatedPartitioning.v[idx], idx }));
} 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, idx) => dispatch(getPartitioningDataAction({ partitioning: path, idx })));
}
});
};
49 changes: 43 additions & 6 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 @@ -34,6 +34,7 @@ import {

import { InstallationDestination, applyDefaultStorage } from "./storage/InstallationDestination.jsx";
import { StorageConfiguration, getScenario, getDefaultScenario } from "./storage/StorageConfiguration.jsx";
import { CustomMountPoint, applyMountPointStorage } from "./storage/CustomMountPoint.jsx";
import { DiskEncryption, StorageEncryptionState } from "./storage/DiskEncryption.jsx";
import { InstallationLanguage } from "./localization/InstallationLanguage.jsx";
import { InstallationProgress } from "./installation/InstallationProgress.jsx";
Expand All @@ -53,6 +54,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 => storageData.partitioning[path].idx === Object.keys(storageData.partitioning).length - 1);

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 +81,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 +115,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 +142,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 +197,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 +226,7 @@ const Footer = ({
setIsFormValid,
setStepNotification,
isInProgress,
partitioning,
setIsInProgress,
storageEncryption,
showPassphraseScreen,
Expand Down Expand Up @@ -249,6 +267,25 @@ const Footer = ({
});
} else if (activeStep.id === "installation-review") {
setNextWaitsConfirmation(true);
} else if (activeStep.id === "custom-mountpoint") {
setIsInProgress(true);

applyMountPointStorage({
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 39bb620

Please sign in to comment.