diff --git a/ui/webui/src/apis/storage.js b/ui/webui/src/apis/storage.js
index f8b881d12cf3..a3868e8e8a43 100644
--- a/ui/webui/src/apis/storage.js
+++ b/ui/webui/src/apis/storage.js
@@ -77,6 +77,20 @@ export const getAllDiskSelection = () => {
);
};
+/**
+ * @param {string} deviceName A device name
+ * @param {string} password A password
+ *
+ * @returns {Promise} Resolves true if success otherwise false
+ */
+export const unlockDevice = ({ deviceName, passphrase }) => {
+ return new StorageClient().client.call(
+ "/org/fedoraproject/Anaconda/Modules/Storage/DeviceTree",
+ "org.fedoraproject.Anaconda.Modules.Storage.DeviceTree.Handler",
+ "UnlockDevice", [deviceName, passphrase]
+ );
+};
+
/**
* @param {string} disk A device name
*
diff --git a/ui/webui/src/components/app.jsx b/ui/webui/src/components/app.jsx
index bbc05180226e..6ccdb397aba8 100644
--- a/ui/webui/src/components/app.jsx
+++ b/ui/webui/src/components/app.jsx
@@ -35,6 +35,8 @@ import { LocalizationClient, startEventMonitorLocalization } from "../apis/local
import { StorageClient, initDataStorage, startEventMonitorStorage } from "../apis/storage.js";
import { PayloadsClient } from "../apis/payloads";
+import { WithDialogs } from "dialogs.jsx";
+
import { readBuildstamp, getIsFinal } from "../helpers/betanag.js";
import { readConf } from "../helpers/conf.js";
import { useReducerWithThunk, reducer, initialState } from "../reducer.js";
@@ -141,16 +143,18 @@ export const Application = () => {
})}
}
- setIsHelpExpanded(false)}
- title={title}
- storageData={state.storage}
- localizationData={state.localization}
- dispatch={dispatch}
- conf={conf}
- />
+
+ setIsHelpExpanded(false)}
+ title={title}
+ storageData={state.storage}
+ localizationData={state.localization}
+ dispatch={dispatch}
+ conf={conf}
+ />
+
);
diff --git a/ui/webui/src/components/review/ReviewConfiguration.jsx b/ui/webui/src/components/review/ReviewConfiguration.jsx
index 3157aa7a008f..80a655248e1f 100644
--- a/ui/webui/src/components/review/ReviewConfiguration.jsx
+++ b/ui/webui/src/components/review/ReviewConfiguration.jsx
@@ -70,10 +70,24 @@ export const ReviewDescriptionList = ({ children }) => {
);
};
-const DeviceRow = ({ data, requests }) => {
- const name = data.name.v;
+const checkDeviceInSubTree = (device, rootDevice, deviceData) => {
+ const parents = device.parents.v;
+
+ if (parents.length && parents[0] === rootDevice) {
+ return true;
+ } else if (parents.length && parents[0] !== rootDevice) {
+ return checkDeviceInSubTree(deviceData[parents[0]], rootDevice, deviceData);
+ } else {
+ return false;
+ }
+};
+
+const DeviceRow = ({ deviceData, disk, requests }) => {
const [isExpanded, setIsExpanded] = useState(false);
+ const data = deviceData[disk];
+ const name = data.name.v;
+
const renderRow = row => {
const iconColumn = row.reformat.v ? : null;
return {
@@ -87,7 +101,12 @@ const DeviceRow = ({ data, requests }) => {
};
};
- const partitionRows = requests?.filter(req => req["device-spec"].includes(name)).map(renderRow) || [];
+ const partitionRows = requests?.filter(req => {
+ const partitionName = Object.keys(deviceData).find(device => deviceData[device].path.v === req["device-spec"]);
+ const device = deviceData[partitionName];
+
+ return checkDeviceInSubTree(device, name, deviceData);
+ }).map(renderRow) || [];
return (
@@ -205,7 +224,7 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, language, reque
{_("Storage devices and configurations")}
diff --git a/ui/webui/src/components/storage/CustomMountPoint.jsx b/ui/webui/src/components/storage/CustomMountPoint.jsx
index 62ffa6fe9dc7..bd745bca2266 100644
--- a/ui/webui/src/components/storage/CustomMountPoint.jsx
+++ b/ui/webui/src/components/storage/CustomMountPoint.jsx
@@ -16,25 +16,31 @@
*/
import cockpit from "cockpit";
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
import {
Alert,
+ Button,
Checkbox,
Flex,
+ FlexItem,
HelperText,
HelperTextItem,
Popover,
Select,
SelectOption,
SelectVariant,
+ Text,
TextContent,
+ TextVariants,
} from "@patternfly/react-core";
import { HelpIcon } from "@patternfly/react-icons";
import { ListingTable } from "cockpit-components-table.jsx";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+
import { AnacondaPage } from "../AnacondaPage.jsx";
+import { UnlockDialog } from "./UnlockDialog.jsx";
import {
createPartitioning,
@@ -119,6 +125,20 @@ const MountpointCheckbox = ({ reformat, isRootMountPoint, handleCheckReFormat, p
export const CustomMountPoint = ({ deviceData, diskSelection, partitioningData, dispatch, idPrefix, setIsFormValid, onAddErrorNotification, toggleContextHelp, stepNotification }) => {
const [creatingPartitioning, setCreatingPartitioning] = useState(true);
+ const [showUnlockDialog, setShowUnlockDialog] = useState(false);
+
+ const lockedLUKSPartitions = useMemo(() => {
+ const devs = partitioningData?.requests?.map(r => r["device-spec"]) || [];
+
+ return Object.keys(deviceData).filter(d => {
+ return (
+ devs.includes(deviceData[d].path.v) &&
+ deviceData[d].formatData.type.v === "luks" &&
+ deviceData[d].formatData.attrs.v.has_key !== "True"
+ );
+ });
+ }, [deviceData, partitioningData?.requests]);
+
useEffect(() => {
const validateMountPoints = requests => {
if (requests) {
@@ -136,6 +156,10 @@ export const CustomMountPoint = ({ deviceData, diskSelection, partitioningData,
const partitioningDevicesPaths = partitioningData?.requests.map(r => r["device-spec"]) || [];
const canReusePartitioning = selectedDevicesPaths.length === partitioningDevicesPaths.length && selectedDevicesPaths.every(d => partitioningDevicesPaths.includes(d));
+ useEffect(() => {
+ setShowUnlockDialog(lockedLUKSPartitions.length > 0);
+ }, [lockedLUKSPartitions]);
+
useEffect(() => {
if (canReusePartitioning) {
setCreatingPartitioning(false);
@@ -204,16 +228,28 @@ export const CustomMountPoint = ({ deviceData, diskSelection, partitioningData,
const isNotMountPoint = ["biosboot"].includes(row["format-type"]);
// TODO: Anaconda does not support formatting btrfs yet
const isBtrfs = row["format-type"] === "btrfs";
+ const isLockedLUKS = lockedLUKSPartitions.some(p => row["device-spec"].includes(p));
+
return {
props: { key: row["device-spec"] },
columns: [
{ title: row["device-spec"] },
- { title: row["format-type"] },
+ {
+ title: (
+
+ {row["format-type"]}
+ {isLockedLUKS &&
+ }
+
+ )
+ },
{
title: (
)
},
@@ -245,15 +281,19 @@ export const CustomMountPoint = ({ deviceData, diskSelection, partitioningData,
title={stepNotification.message}
variant="danger"
/>}
-
- {_("We discovered your partitioned and formatted filesystems, so now you can select your own custom mount point for each filesystem.")}
-
-
+ <>
+
+ {_("We discovered your partitioned and formatted filesystems, so now you can select your own custom mount point for each filesystem.")}
+
+
+ >
+ {showUnlockDialog &&
+ setShowUnlockDialog(false)} partition={lockedLUKSPartitions[0]} />}
);
};
diff --git a/ui/webui/src/components/storage/InstallationDestination.jsx b/ui/webui/src/components/storage/InstallationDestination.jsx
index 33d8e22949a0..703ff3503003 100644
--- a/ui/webui/src/components/storage/InstallationDestination.jsx
+++ b/ui/webui/src/components/storage/InstallationDestination.jsx
@@ -43,7 +43,7 @@ import {
Tooltip,
} from "@patternfly/react-core";
-import { HelpIcon, LockIcon } from "@patternfly/react-icons";
+import { HelpIcon, LockIcon, LockOpenIcon } from "@patternfly/react-icons";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { ListingTable } from "cockpit-components-table.jsx";
@@ -293,6 +293,16 @@ const LocalStandardDisks = ({ deviceData, diskSelection, dispatch, idPrefix, set
},
];
+ const lockIcon = partition => {
+ const isLocked = partition.formatData.attrs.v.has_key?.toLowerCase() !== "true";
+
+ if (isLocked) {
+ return ;
+ } else {
+ return ;
+ }
+ };
+
const expandedContent = (disk) => (
{partition.path.v}
- {partition.formatData.type.v === "luks" && }
+ {partition.formatData.type.v === "luks" && {lockIcon(partition)}}
)
};
diff --git a/ui/webui/src/components/storage/UnlockDialog.jsx b/ui/webui/src/components/storage/UnlockDialog.jsx
new file mode 100644
index 000000000000..0c77b2984d43
--- /dev/null
+++ b/ui/webui/src/components/storage/UnlockDialog.jsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with This program; If not, see .
+ */
+
+import cockpit from "cockpit";
+import React, { useState } from "react";
+
+import {
+ Button,
+ InputGroup,
+ Form,
+ FormGroup,
+ Modal,
+ Text,
+ TextContent,
+ TextVariants,
+ TextInput,
+} from "@patternfly/react-core";
+import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
+
+import { ModalError } from "cockpit-components-inline-notification.jsx";
+
+import { getDevicesAction } from "../../actions/storage-actions.js";
+
+import {
+ createPartitioning,
+ unlockDevice,
+} from "../../apis/storage.js";
+
+const _ = cockpit.gettext;
+
+export const UnlockDialog = ({ partition, onClose, dispatch }) => {
+ const [password, setPassword] = useState("");
+ const [passwordHidden, setPasswordHidden] = useState(true);
+ const [dialogError, dialogErrorSet] = useState();
+ const [inProgress, setInProgress] = useState(false);
+
+ const onSubmit = () => {
+ setInProgress(true);
+ return unlockDevice({ deviceName: partition, passphrase: password })
+ .then(
+ res => {
+ if (res[0]) {
+ // Blivet does not send a signal when a device is unlocked,
+ // so we need to refresh the device data manually.
+ dispatch(getDevicesAction());
+ // Also refresh the partitioning data which will now show the children
+ // of the unlocked device.
+ createPartitioning({ method: "MANUAL" });
+ onClose();
+ } else {
+ dialogErrorSet(_("Incorrect passphrase"));
+ setInProgress(false);
+ }
+ },
+ exc => {
+ dialogErrorSet(exc.message);
+ setInProgress(false);
+ });
+ };
+
+ return (
+ onClose()}
+ title={cockpit.format(_("Unlock encrypted partition $0"), partition)}
+ description={
+
+ {_("You need to unlock encrypted partitions before you can continue.")}
+
+ }
+ footer={
+ <>
+
+
+ >
+ }>
+
+
+ );
+};
diff --git a/ui/webui/test/check-storage b/ui/webui/test/check-storage
index ab504049a98f..1276a86e57ac 100755
--- a/ui/webui/test/check-storage
+++ b/ui/webui/test/check-storage
@@ -320,6 +320,9 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase):
def select_mountpoint(self, i ,s, disks, expected_partitions):
i.open()
i.next()
+
+ s.check_disk_visible("vda")
+
s.rescan_disks()
for disk in disks:
dev = disk.split('/')[-1]
@@ -352,6 +355,15 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase):
def check_partition(self, row, partition):
self.browser.wait_in_text(f"#custom-mountpoint-table tbody tr:nth-child({row}) td[data-label='Partition']", partition)
+ def unlock_device(self, passphrase, xfail=None):
+ self.browser.wait_visible("#unlock-device-dialog")
+ self.browser.set_input_text("#unlock-device-dialog-luks-password", passphrase)
+ self.browser.click("#unlock-device-dialog-submit-btn")
+ if xfail:
+ self.browser.wait_in_text("#unlock-device-dialog .pf-c-alert", xfail)
+ self.browser.click("#unlock-device-dialog-cancel-btn")
+ self.browser.wait_not_present("#unlock-device-dialog")
+
def select_reformat(self, row):
self.browser.set_checked(f"#custom-mountpoint-table tbody tr:nth-child({row}) td[data-label='Reformat'] input", True)
@@ -589,5 +601,60 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase):
r.check_disk_row(disk, row, "Format type", "xfs")
r.check_disk_row(disk, row, "Mount point", "/home")
+ def testEncryptedUnlock(self):
+ b = self.browser
+ m = self.machine
+ i = Installer(b, m)
+ s = Storage(b, m)
+ r = Review(b)
+
+ m.add_disk(10)
+
+ # BIOS boot partition, /boot partition, /
+ disk1 = "/dev/vda"
+ m.execute(f"""
+ sgdisk --zap-all {disk1}
+ sgdisk --new=0:0:+1MiB -t 0:ef02 {disk1}
+ sgdisk --new=0:0:+1GiB {disk1}
+ sgdisk --new=0:0:0 {disk1}
+ mkfs.xfs {disk1}2
+ mkfs.xfs {disk1}3
+ """)
+
+
+ disk2 = "vdb"
+ m.execute(f"""
+ sgdisk --zap-all /dev/{disk2}
+ sgdisk --new=0:0 /dev/{disk2}
+ echo einszweidrei | cryptsetup luksFormat /dev/{disk2}1
+ echo einszweidrei | cryptsetup luksOpen /dev/{disk2}1 encrypted-vol
+ mkfs.xfs /dev/mapper/encrypted-vol
+ cryptsetup luksClose encrypted-vol
+ """)
+ self.udevadm_settle()
+
+ partitions = {}
+ partitions[disk1] = [f"{disk1}2", f"{disk1}3"]
+ partitions[disk2] = [f"{disk2}1"]
+ self.select_mountpoint(i, s, [disk1, disk2], partitions)
+
+ self.unlock_device("1234", "Failed to unlock LUKS partition")
+
+ b.click("#custom-mountpoint-table tbody tr:nth-child(4) td[data-label='Format type'] #unlock-luks-btn")
+ self.unlock_device("einszweidrei")
+ b.wait_not_present("#custom-mountpoint-table tbody tr:nth-child(4) td[data-label='Format type'] #unlock-luks-btn")
+
+ self.check_partition(4, "/dev/mapper/luks")
+ self.check_format_type(4, "xfs")
+
+ self.select_from_dropdown(2, "boot", "/boot")
+ self.select_from_dropdown(3, "root", "/")
+ self.select_from_dropdown(4, "home", "/home")
+
+ i.next()
+
+ r.expand_disk_table(disk2)
+ r.check_disk_row(disk2, 1, "Partition", "/dev/mapper/luks")
+
if __name__ == '__main__':
test_main()