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 8151dd1 commit a0baf0e
Show file tree
Hide file tree
Showing 14 changed files with 808 additions and 33 deletions.
14 changes: 14 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,15 @@ export const getDiskSelectionAction = () => {
});
};
};

export const getPartitioningDataAction = ({ requests, partitioning }) => {
return async function fetchUserThunk (dispatch) {
const reqs = requests ? [requests] : await gatherRequests({ partitioning });
const method = requests ? "manual" : await getPartitioningMethod({ partitioning });

return dispatch({
type: "GET_PARTITIONING_DATA",
payload: { path: partitioning, method, requests: reqs?.[0].map(request => Object.entries(request).reduce((acc, [key, value]) => ({ ...acc, [key]: value.v }), {})) }
});
};
};
110 changes: 109 additions & 1 deletion 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,40 @@ export const createPartitioning = ({ method }) => {
);
};

/**
* @param {string} method A partitioning method
*
* @returns {Promise} Resolves the DBus path to the partitioning
*/
export const findPartitioning = ({ method }) => {
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 createPartitioning({ method });
} else {
const lastPartitiong = res.v[res.v.length - 1];
return getPartitioningMethod({ partitioning: lastPartitiong }).then(partitioningMethod => {
if (partitioningMethod !== method) {
return createPartitioning({ method });
} else {
return new Promise((resolve) => {
// Return as list to mimmick createPartitioning
resolve([res.v[res.v.length - 1]]);
});
}
});
}
});
};

/**
* @returns {Promise} Resolves all properties of DiskSelection interface
*/
Expand Down Expand Up @@ -222,6 +259,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 +458,55 @@ 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)
]
);
};

/**
* @returns {Promise} The request of automatic partitioning
*/
export const getManualPartitioningRequests = ({ partitioning }) => {
return (
new StorageClient().client.call(
partitioning,
"org.freedesktop.DBus.Properties",
"Get",
[
"org.fedoraproject.Anaconda.Modules.Storage.Partitioning.Manual",
"Requests",
]
)
.then(res => res[0].v)
);
};

/**
* @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,6 +515,8 @@ 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, path }));
}
break;
default:
Expand Down
38 changes: 34 additions & 4 deletions ui/webui/src/components/AnacondaWizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 Down Expand Up @@ -75,17 +76,26 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, onAddE
label: _("Storage devices")
}, {
component: StorageConfiguration,
data: { deviceData: storageData.devices, diskSelection: storageData.diskSelection },
id: "storage-configuration",
label: _("Storage configuration")
}, {
component: CustomMountPoint,
data: { deviceData: storageData.devices, requests: storageData.partitioning.requests, 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: storageData.partitioning.requests },
id: "installation-review",
label: _("Review and install"),
},
Expand All @@ -100,7 +110,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 @@ -125,7 +137,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 @@ -248,6 +260,24 @@ const Footer = ({
});
} else if (activeStep.id === "installation-review") {
setNextWaitsConfirmation(true);
} else if (activeStep.id === "custom-mountpoint") {
setIsInProgress(true);

applyMountPointStorage({
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
66 changes: 51 additions & 15 deletions ui/webui/src/components/review/ReviewConfiguration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
DataListToggle,
DataListItemRow, DataListItemCells,
DataListCell,
DataListContent,
DescriptionList, DescriptionListGroup,
DescriptionListTerm, DescriptionListDescription,
ExpandableSection,
Expand All @@ -37,6 +38,7 @@ import {
import {
getAppliedPartitioning,
getPartitioningRequest,
getPartitioningMethod,
} from "../../apis/storage.js";

import {
Expand All @@ -45,6 +47,9 @@ import {
import { AnacondaPage } from "../AnacondaPage.jsx";

import { getScenario } from "../storage/StorageConfiguration.jsx";
import { CheckCircleIcon } from "@patternfly/react-icons";

import { ListingTable } from "cockpit-components-table.jsx";

import "./ReviewConfiguration.scss";

Expand All @@ -68,15 +73,31 @@ export const ReviewDescriptionList = ({ children }) => {
);
};

const DeviceRow = ({ name, data }) => {
const DeviceRow = ({ data, requests }) => {
const name = data.name.v;
const [isExpanded, setIsExpanded] = useState(false);

const renderRow = row => {
const iconColumn = row.reformat.v ? <CheckCircleIcon /> : null;
return {
props: { key: row["device-spec"] },
columns: [
{ title: row["device-spec"] },
{ title: row["format-type"] },
{ title: row["mount-point"] },
{ title: iconColumn },
]
};
};

const partitionRows = requests?.filter(req => req["device-spec"].includes(name)).map(renderRow) || [];

return (
<DataListItem id={`data-list-${name}`} isExpanded={isExpanded} key={name}>
<DataListItemRow>
<DataListToggle
buttonProps={{ isDisabled: true }}
onClick={() => setIsExpanded(!isExpanded)}
buttonProps={{ isDisabled: requests === null }}
onClick={() => requests !== null ? setIsExpanded(!isExpanded) : {}}
isExpanded={isExpanded}
id={name + "-expander"}
/>
Expand All @@ -94,11 +115,20 @@ const DeviceRow = ({ name, data }) => {
]}
/>
</DataListItemRow>
<DataListContent isHidden={!isExpanded}>
<ListingTable
id="partitions-table"
aria-label={_("Disk partitions")}
emptyCaption={_("No partitions found")}
variant="compact"
columns={[_("Partition"), _("Format type"), _("Mount point"), _("Reformat")]}
rows={partitionRows} />
</DataListContent>
</DataListItem>
);
};

export const ReviewConfiguration = ({ deviceData, diskSelection, idPrefix, storageScenarioId }) => {
export const ReviewConfiguration = ({ deviceData, diskSelection, requests, idPrefix, storageScenarioId }) => {
const [systemLanguage, setSystemLanguage] = useState();
const [encrypt, setEncrypt] = useState();
const [showLanguageSection, setShowLanguageSection] = useState(true);
Expand All @@ -112,8 +142,11 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, idPrefix, stora
};
const initializeEncrypt = async () => {
const partitioning = await getAppliedPartitioning().catch(console.error);
const request = await getPartitioningRequest({ partitioning }).catch(console.error);
setEncrypt(request.encrypted.v);
const method = await getPartitioningMethod({ partitioning }).catch(console.error);
if (method === "AUTOMATIC") {
const request = await getPartitioningRequest({ partitioning }).catch(console.error);
setEncrypt(request.encrypted.v);
}
};
initializeLanguage();
initializeEncrypt();
Expand Down Expand Up @@ -168,19 +201,22 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, idPrefix, stora
<DescriptionListDescription className="description-list-description" id={idPrefix + "-target-system-mode"}>
{getScenario(storageScenarioId).label}
</DescriptionListDescription>
<DescriptionListTerm className="description-list-term">
{_("Disk Encryption")}
</DescriptionListTerm>
<DescriptionListDescription className="description-list-description" id={idPrefix + "-target-system-encrypt"}>
{encrypt ? _("Enabled") : _("Disabled")}
</DescriptionListDescription>
{storageScenarioId !== "custom-mount-point" &&
<>
<DescriptionListTerm className="description-list-term">
{_("Disk Encryption")}
</DescriptionListTerm>
<DescriptionListDescription className="description-list-description" id={idPrefix + "-target-system-encrypt"}>
{encrypt ? _("Enabled") : _("Disabled")}
</DescriptionListDescription>
</>}
</DescriptionListGroup>
</ReviewDescriptionList>
<Title className="storage-devices-configuration-title" headingLevel="h4">{_("Storage devices and configurations")}</Title>
<DataList isCompact>
{diskSelection.selectedDisks.map(deviceName =>
<DeviceRow key={deviceName} name={deviceName} data={deviceData[deviceName]} />
)}
{diskSelection.selectedDisks.map(disk => {
return <DeviceRow key={disk} data={deviceData[disk]} requests={storageScenarioId === "custom-mount-point" ? requests : null} />;
})}
</DataList>
</ExpandableSection>
</AnacondaPage>
Expand Down
Loading

0 comments on commit a0baf0e

Please sign in to comment.