diff --git a/ui/webui/src/components/storage/InstallationScenario.jsx b/ui/webui/src/components/storage/InstallationScenario.jsx index 89fd7c6f128..1a6a0a4b43f 100644 --- a/ui/webui/src/components/storage/InstallationScenario.jsx +++ b/ui/webui/src/components/storage/InstallationScenario.jsx @@ -84,12 +84,12 @@ const checkUseFreeSpace = ({ diskFreeSpace, diskTotalSpace, requiredSize }) => { return availability; }; -const checkMountPointMapping = ({ hasPartitions }) => { +const checkMountPointMapping = ({ hasFilesystems }) => { const availability = new AvailabilityState(); - if (!hasPartitions) { + if (!hasFilesystems) { availability.available = false; - availability.reason = _("No existing partitions on the selected disks."); + availability.reason = _("No usable devices on the selected disks."); } else { availability.available = true; } @@ -163,7 +163,7 @@ const InstallationScenarioSelector = ({ deviceData, selectedDisks, idPrefix, sto const [requiredSize, setRequiredSize] = useState(); const [diskTotalSpace, setDiskTotalSpace] = useState(); const [diskFreeSpace, setDiskFreeSpace] = useState(); - const [hasPartitions, setHasPartitions] = useState(); + const [hasFilesystems, setHasFilesystems] = useState(); useEffect(() => { const updateSizes = async () => { @@ -187,22 +187,22 @@ const InstallationScenarioSelector = ({ deviceData, selectedDisks, idPrefix, sto }, []); useEffect(() => { - const hasPartitions = selectedDisks.some(device => deviceData[device]?.children.v.some(child => deviceData[child]?.type.v === "partition")); + const hasFilesystems = selectedDisks.some(device => deviceData[device]?.children.v.some(child => deviceData[child]?.formatData.mountable.v || deviceData[child]?.formatData.type.v === "luks")); - setHasPartitions(hasPartitions); + setHasFilesystems(hasFilesystems); }, [selectedDisks, deviceData]); useEffect(() => { let selectedScenarioId = ""; let availableScenarioExists = false; - if ([diskTotalSpace, diskFreeSpace, hasPartitions, requiredSize].some(itm => itm === undefined)) { + if ([diskTotalSpace, diskFreeSpace, hasFilesystems, requiredSize].some(itm => itm === undefined)) { return; } const newAvailability = {}; for (const scenario of scenarios) { - const availability = scenario.check({ diskTotalSpace, diskFreeSpace, hasPartitions, requiredSize }); + const availability = scenario.check({ diskTotalSpace, diskFreeSpace, hasFilesystems, requiredSize }); newAvailability[scenario.id] = availability; if (availability.available) { availableScenarioExists = true; @@ -219,7 +219,7 @@ const InstallationScenarioSelector = ({ deviceData, selectedDisks, idPrefix, sto setSelectedScenario(selectedScenarioId); setScenarioAvailability(newAvailability); setIsFormValid(availableScenarioExists); - }, [deviceData, hasPartitions, requiredSize, diskFreeSpace, diskTotalSpace, setIsFormValid, storageScenarioId]); + }, [deviceData, hasFilesystems, requiredSize, diskFreeSpace, diskTotalSpace, setIsFormValid, storageScenarioId]); useEffect(() => { const applyScenario = async (scenarioId) => { diff --git a/ui/webui/src/components/storage/MountPointMapping.jsx b/ui/webui/src/components/storage/MountPointMapping.jsx index 9acce5984c0..b6b8212aaf6 100644 --- a/ui/webui/src/components/storage/MountPointMapping.jsx +++ b/ui/webui/src/components/storage/MountPointMapping.jsx @@ -53,11 +53,6 @@ import "./MountPointMapping.scss"; const _ = cockpit.gettext; -const requiredMountPointOptions = [ - { value: "/boot", name: "boot" }, - { value: "/", name: "root" }, -]; - const getDeviceChildren = ({ deviceData, device }) => { const children = []; const deviceChildren = deviceData[device]?.children?.v || []; @@ -73,11 +68,12 @@ const getDeviceChildren = ({ deviceData, device }) => { return children; }; -const getInitialRequests = (partitioningData) => { - const bootOriginalRequest = partitioningData.requests.find(r => r["mount-point"] === "/boot"); - const rootOriginalRequest = partitioningData.requests.find(r => r["mount-point"] === "/"); +const getInitialRequests = (usablePartitioningRequests, requiredMountPoints) => { + const bootOriginalRequest = usablePartitioningRequests.find(r => r["mount-point"] === "/boot"); + const rootOriginalRequest = usablePartitioningRequests.find(r => r["mount-point"] === "/"); + const bootEfiOriginalRequest = usablePartitioningRequests.find(r => r["mount-point"] === "/boot/efi"); - const requests = requiredMountPointOptions.map((mountPoint, idx) => { + const requests = requiredMountPoints.map((mountPoint, idx) => { const request = ({ "mount-point": mountPoint.value, reformat: mountPoint.name === "root" }); if (mountPoint.name === "boot" && bootOriginalRequest) { @@ -88,10 +84,14 @@ const getInitialRequests = (partitioningData) => { return { ...rootOriginalRequest, ...request }; } + if (mountPoint.name === "boot-efi" && bootEfiOriginalRequest) { + return { ...bootEfiOriginalRequest, ...request }; + } + return request; }); - const extraRequests = partitioningData.requests.filter(r => r["mount-point"] && r["mount-point"] !== "/" && r["mount-point"] !== "/boot" && r["format-type"] !== "biosboot") || []; + const extraRequests = usablePartitioningRequests.filter(r => r["mount-point"] && r["mount-point"] !== "/" && r["mount-point"] !== "/boot" && r["mount-point"] !== "/boot/efi" && r["format-type"] !== "biosboot") || []; return [...requests, ...extraRequests].map((request, idx) => ({ ...request, "request-id": idx + 1 })); }; @@ -119,6 +119,23 @@ const isReformatValid = (deviceData, request, requests) => { }); }; +const isDeviceMountPointInvalid = (deviceData, request) => { + const device = request["device-spec"]; + + if (!device || !request["mount-point"]) { + return [false, ""]; + } + + // /boot/efi must be on EFI System Partition + if (request["mount-point"] === "/boot/efi") { + if (deviceData[device].formatData.type.v !== "efi") { + return [true, _("/boot/efi must be on a EFI System Partition device")]; + } + } + + return [false, ""]; +}; + const getLockedLUKSDevices = (requests, deviceData) => { const devs = requests?.map(r => r["device-spec"]) || []; @@ -233,6 +250,7 @@ const DeviceColumnSelect = ({ deviceData, devices, idPrefix, lockedLUKSDevices, const DeviceColumn = ({ deviceData, devices, idPrefix, handleRequestChange, lockedLUKSDevices, request, requests }) => { const device = request["device-spec"]; const duplicatedDevice = isDuplicateRequestField(requests, "device-spec", device); + const [deviceInvalid, errorMessage] = isDeviceMountPointInvalid(deviceData, request); return ( @@ -250,6 +268,12 @@ const DeviceColumn = ({ deviceData, devices, idPrefix, handleRequestChange, lock {_("Duplicate device.")} } + {deviceInvalid && + + + {errorMessage} + + } ); }; @@ -314,6 +338,7 @@ const MountPointRowRemove = ({ request, setRequests }) => { const RequestsTable = ({ allDevices, deviceData, + requiredMountPoints, handleRequestChange, idPrefix, lockedLUKSDevices, @@ -322,7 +347,7 @@ const RequestsTable = ({ }) => { const columnClassName = idPrefix + "__column"; const getRequestRow = (request) => { - const isRequiredMountPoint = !!requiredMountPointOptions.find(val => val.value === request["mount-point"]); + const isRequiredMountPoint = !!requiredMountPoints.find(val => val.value === request["mount-point"]); const duplicatedMountPoint = isDuplicateRequestField(requests, "mount-point", request["mount-point"]); const rowId = idPrefix + "-row-" + request["request-id"]; @@ -392,19 +417,19 @@ const RequestsTable = ({ ); }; -const MountPointMappingContent = ({ deviceData, partitioningData, dispatch, idPrefix, setIsFormValid, onAddErrorNotification }) => { +const MountPointMappingContent = ({ deviceData, partitioningData, usablePartitioningRequests, requiredMountPoints, dispatch, idPrefix, setIsFormValid, onAddErrorNotification }) => { const [skipUnlock, setSkipUnlock] = useState(false); - const [requests, setRequests] = useState(getInitialRequests(partitioningData)); + const [requests, setRequests] = useState(getInitialRequests(usablePartitioningRequests, requiredMountPoints)); const [updateRequestCnt, setUpdateRequestCnt] = useState(0); const currentUpdateRequestCnt = useRef(0); const allDevices = useMemo(() => { - return partitioningData.requests?.map(r => r["device-spec"]) || []; - }, [partitioningData.requests]); + return usablePartitioningRequests?.map(r => r["device-spec"]) || []; + }, [usablePartitioningRequests]); const lockedLUKSDevices = useMemo( - () => getLockedLUKSDevices(partitioningData.requests, deviceData), - [deviceData, partitioningData.requests] + () => getLockedLUKSDevices(usablePartitioningRequests, deviceData), + [deviceData, usablePartitioningRequests] ); const handlePartitioningRequestsChange = useCallback(_requests => { @@ -426,12 +451,12 @@ const MountPointMappingContent = ({ deviceData, partitioningData, dispatch, idPr setManualPartitioningRequests({ partitioning: partitioningData.path, - requests: requestsToDbus(partitioningData.requests) + requests: requestsToDbus(usablePartitioningRequests) }).catch(ex => { onAddErrorNotification(ex); setIsFormValid(false); }); - }, [partitioningData.path, onAddErrorNotification, partitioningData.requests, setIsFormValid]); + }, [partitioningData.path, onAddErrorNotification, usablePartitioningRequests, setIsFormValid]); /* When requests change apply directly to the backend */ useEffect(() => { @@ -515,6 +540,7 @@ const MountPointMappingContent = ({ deviceData, partitioningData, dispatch, idPr { const [usedPartitioning, setUsedPartitioning] = useState(partitioningData?.path); + const requiredMountPoints = useMemo(() => { + const mounts = [ + { value: "/boot", name: "boot" }, + { value: "/", name: "root" }, + ]; + + cockpit.spawn(["ls", "/sys/firmware/efi"], { err: "ignore" }) + .then(() => { mounts.push({ value: "/boot/efi", name: "boot-efi" }) }); + + return mounts; + }, []); + + const isUsableDevice = (devSpec, deviceData) => { + const device = deviceData[devSpec]; + if (device === undefined || device.formatData === undefined) { + return false; + } + + // luks is allowed -- we need to be able to unlock it + if (device.formatData.type.v === "luks") { + return true; + } + + // only swap and mountable filesystems should be shown in the mount point assignment + if (device.formatData.type.v === "swap" || device.formatData.mountable.v === true) { + return true; + } + + return false; + }; + + const usablePartitioningRequests = useMemo(() => { + return partitioningData.requests?.filter(r => isUsableDevice(r["device-spec"], deviceData)) || []; + }, [partitioningData.requests, deviceData]); useEffect(() => { if (!reusePartitioning || partitioningData?.method !== "MANUAL") { @@ -571,7 +631,9 @@ export const MountPointMapping = ({ deviceData, diskSelection, partitioningData, idPrefix={idPrefix} onAddErrorNotification={onAddErrorNotification} partitioningData={partitioningData} + usablePartitioningRequests={usablePartitioningRequests} setIsFormValid={setIsFormValid} + requiredMountPoints={requiredMountPoints} /> ); diff --git a/ui/webui/test/anacondalib.py b/ui/webui/test/anacondalib.py index afefaf8e1c9..f15afa425d1 100644 --- a/ui/webui/test/anacondalib.py +++ b/ui/webui/test/anacondalib.py @@ -31,8 +31,13 @@ class VirtInstallMachineCase(MachineCase): + efi = False MachineCase.machine_class = VirtInstallMachine + @classmethod + def setUpClass(cls): + VirtInstallMachine.efi = cls.efi + def setUp(self): # FIXME: running this in destructive tests fails because the SSH session closes before this is run if self.is_nondestructive(): diff --git a/ui/webui/test/check-storage b/ui/webui/test/check-storage index 0cdac2f0dda..a79929223d8 100755 --- a/ui/webui/test/check-storage +++ b/ui/webui/test/check-storage @@ -26,6 +26,8 @@ from storagelib import StorageHelpers # pylint: disable=import-error @nondestructive class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers): + efi = False + def set_valid_password(self, password="abcdefgh"): s = Storage(self.browser, self.machine) @@ -250,6 +252,7 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers): # TODO add next back test keeping the choice @nondestructive class TestStorageExtraDisks(anacondalib.VirtInstallMachineCase, StorageHelpers): + efi = False def testLocalDisksSyncNew(self): b = self.browser @@ -292,7 +295,7 @@ class TestStorageExtraDisks(anacondalib.VirtInstallMachineCase, StorageHelpers): for disk in disks: s.check_disk_selected(disk) -class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): +class TestUtils(): def table_row(self, row): return f"#mount-point-mapping-table-row-{row}"; @@ -305,8 +308,8 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): return True - def select_mountpoint(self, i , s, disks): - self.browser.wait(lambda : self.disks_loaded(s, disks)) + def select_mountpoint(self, b, i , s, disks): + b.wait(lambda : self.disks_loaded(s, disks)) for disk in disks: current_selection = s.get_disk_selected(disk) @@ -316,92 +319,89 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): s.set_partitioning("mount-point-mapping") i.next(next_page=i.steps.CUSTOM_MOUNT_POINT) - def select_row_mountpoint(self, row, mountpoint): - b = self.browser + def select_row_mountpoint(self, b, row, mountpoint): b.set_input_text(f"{self.table_row(row)} td[data-label='Mount point'] input", mountpoint) - def select_row_device(self, row, device): - b = self.browser + def select_row_device(self, b, row, device): selector = f"{self.table_row(row)} .pf-v5-c-select__toggle" - self.browser.click(f"{selector}:not([disabled]):not([aria-disabled=true])") + b.click(f"{selector}:not([disabled]):not([aria-disabled=true])") select_entry = f"{selector} + ul button[data-value='{device}']" - self.browser.click(select_entry) - self.browser.wait_in_text(f"{selector} .pf-v5-c-select__toggle-text", device) + b.click(select_entry) + b.wait_in_text(f"{selector} .pf-v5-c-select__toggle-text", device) - def toggle_row_device(self, row): - b = self.browser + def toggle_row_device(self, b, row): b.click(f"{self.table_row(row)}-device-select-toggle") - def check_row_device(self, row, device): - self.browser.wait_text(f"{self.table_row(row)} .pf-v5-c-select__toggle-text", device) + def check_row_device(self, b, row, device): + b.wait_text(f"{self.table_row(row)} .pf-v5-c-select__toggle-text", device) - def check_row_mountpoint(self, row, mountpoint, isRequired=True): + def check_row_mountpoint(self, b, row, mountpoint, isRequired=True): if isRequired: - self.browser.wait_text(f"{self.table_row(row)}-mountpoint", mountpoint) + b.wait_text(f"{self.table_row(row)}-mountpoint", mountpoint) else: - self.browser.wait_val(f"{self.table_row(row)}-mountpoint", mountpoint) + b.wait_val(f"{self.table_row(row)}-mountpoint", mountpoint) - def check_format_type(self, row, format_type): - self.toggle_row_device(row) - self.browser.wait_in_text(f"{self.table_row(row)} ul li button.pf-m-selected", format_type) - self.toggle_row_device(row) + def check_format_type(self, b, row, format_type): + self.toggle_row_device(b, row) + b.wait_in_text(f"{self.table_row(row)} ul li button.pf-m-selected", format_type) + self.toggle_row_device(b, row) - def check_device_available(self, row, device, available=True): - self.toggle_row_device(row) + def check_device_available(self, b, row, device, available=True): + self.toggle_row_device(b, row) if available: - self.browser.wait_visible(f"{self.table_row(row)} ul li button:contains({device})") + b.wait_visible(f"{self.table_row(row)} ul li button:contains({device})") else: - self.browser.wait_not_present(f"{self.table_row(row)} ul li button:contains({device})") - self.toggle_row_device(row) + b.wait_not_present(f"{self.table_row(row)} ul li button:contains({device})") + self.toggle_row_device(b, row) - def unlock_device(self, passphrase, xfail=None): + def unlock_device(self, b, passphrase, xfail=None): # FIXME: https://github.com/patternfly/patternfly-react/issues/9512 - self.browser.wait_visible("#unlock-device-dialog.pf-v5-c-modal-box") - self.browser.set_input_text("#unlock-device-dialog-luks-password", passphrase) - self.browser.click("#unlock-device-dialog-submit-btn") + b.wait_visible("#unlock-device-dialog.pf-v5-c-modal-box") + b.set_input_text("#unlock-device-dialog-luks-password", passphrase) + b.click("#unlock-device-dialog-submit-btn") if xfail: - self.browser.wait_in_text("#unlock-device-dialog .pf-v5-c-alert", xfail) - self.browser.click("#unlock-device-dialog-cancel-btn") - self.browser.wait_not_present("#unlock-device-dialog.pf-v5-c-modal-box") + b.wait_in_text("#unlock-device-dialog .pf-v5-c-alert", xfail) + b.click("#unlock-device-dialog-cancel-btn") + b.wait_not_present("#unlock-device-dialog.pf-v5-c-modal-box") - def select_reformat(self, row, selected=True): - self.browser.set_checked(f"{self.table_row(row)} td[data-label='Reformat'] input", selected) + def select_reformat(self, b, row, selected=True): + b.set_checked(f"{self.table_row(row)} td[data-label='Reformat'] input", selected) - def remove_row(self, row): - self.browser.click(f"{self.table_row(row)} button[aria-label='Remove']") + def remove_row(self, b, row): + b.click(f"{self.table_row(row)} button[aria-label='Remove']") - def check_reformat(self, row, checked): + def check_reformat(self, b, row, checked): checked_selector = "input:checked" if checked else "input:not(:checked)" - self.browser.wait_visible(f"{self.table_row(row)} td[data-label='Reformat'] {checked_selector}") + b.wait_visible(f"{self.table_row(row)} td[data-label='Reformat'] {checked_selector}") - def check_select_disabled(self, row): - self.browser.wait_visible(f"{self.table_row(row)} td[data-label='Device'] .pf-v5-c-select__toggle.pf-m-disabled") + def check_select_disabled(self, b, row): + b.wait_visible(f"{self.table_row(row)} td[data-label='Device'] .pf-v5-c-select__toggle.pf-m-disabled") - def check_reformat_disabled(self, row): - self.browser.wait_visible(f"{self.table_row(row)} td[data-label='Reformat'] .pf-v5-c-check__input:disabled") + def check_reformat_disabled(self, b, row): + b.wait_visible(f"{self.table_row(row)} td[data-label='Reformat'] .pf-v5-c-check__input:disabled") - def add_mount(self): - self.browser.click("button:contains('Add mount')") + def add_mount(self, b): + b.click("button:contains('Add mount')") - def unlock_all_encrypted(self): - self.browser.click("button:contains('Unlock')") + def unlock_all_encrypted(self, b): + b.click("button:contains('Unlock')") - def unlock_all_encrypted_skip(self): - self.browser.click("button:contains('Skip')") + def unlock_all_encrypted_skip(self, b): + b.click("button:contains('Skip')") - def assert_inline_error(self, text): - self.browser.wait_in_text(".pf-v5-c-alert.pf-m-inline.pf-m-danger", text) - - - def wait_mount_point_table_column_helper(self, row, column, text=None, present=True): - b = self.browser + def assert_inline_error(self, b, text): + b.wait_in_text(".pf-v5-c-alert.pf-m-inline.pf-m-danger", text) + def wait_mount_point_table_column_helper(self, b, row, column, text=None, present=True): if present: b.wait_in_text(f"#mount-point-mapping-table-row-{row}-{column} .pf-v5-c-helper-text__item.pf-m-error", text) else: b.wait_not_present(f"#mount-point-mapping-table-row-{row}-{column} .pf-v5-c-helper-text__item.pf-m-error") +class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, TestUtils): + efi = False + @nondestructive def testBasic(self): b = self.browser @@ -423,31 +423,31 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): i.next() s.rescan_disks() - self.select_mountpoint(i, s, [(dev, True)]) + self.select_mountpoint(b, i, s, [(dev, True)]) # verify gathered requests # root partition is not auto-mapped - self.check_row_mountpoint(1, "/boot") - self.check_row_device(1, "Select a device") - self.check_reformat(1, False) - self.select_row_device(1, f"{dev}2") - self.check_format_type(1, "ext4") - - self.check_row_mountpoint(2, "/") - self.check_row_device(2, "Select a device") - self.check_reformat(2, True) - self.select_row_device(2, f"btrfstest") - self.check_format_type(2, "btrfs") - - self.add_mount() - self.select_row_device(3, f"{dev}4") - self.check_reformat(3, False) - self.select_row_mountpoint(3, "/home") - self.check_format_type(3, "xfs") + self.check_row_mountpoint(b, 1, "/boot") + self.check_row_device(b, 1, "Select a device") + self.check_reformat(b, 1, False) + self.select_row_device(b, 1, f"{dev}2") + self.check_format_type(b, 1, "ext4") + + self.check_row_mountpoint(b, 2, "/") + self.check_row_device(b, 2, "Select a device") + self.check_reformat(b, 2, True) + self.select_row_device(b, 2, f"btrfstest") + self.check_format_type(b, 2, "btrfs") + + self.add_mount(b) + self.select_row_device(b, 3, f"{dev}4") + self.check_reformat(b, 3, False) + self.select_row_mountpoint(b, 3, "/home") + self.check_format_type(b, 3, "xfs") # Toggle reformat option - self.select_reformat(1) - self.check_reformat(1, True) + self.select_reformat(b, 1) + self.check_reformat(b, 1, True) b.assert_pixels( "#app", @@ -472,11 +472,11 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): m.execute(f"sgdisk --new=0:0:0 {disk}") s.rescan_disks() - self.select_mountpoint(i, s, [(dev, True)]) - self.check_row_device(1, "Select a device") - self.check_row_device(2, "Select a device") - self.select_row_device(1, f"{dev}2") - self.select_row_device(2, f"btrfstest") + self.select_mountpoint(b, i, s, [(dev, True)]) + self.check_row_device(b, 1, "Select a device") + self.check_row_device(b, 2, "Select a device") + self.select_row_device(b, 1, f"{dev}2") + self.select_row_device(b, 2, f"btrfstest") i.next() new_applied_partitioning = s.dbus_get_applied_partitioning() @@ -501,23 +501,23 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): i.next() s.rescan_disks() - self.select_mountpoint(i, s, [(dev, True)]) + self.select_mountpoint(b, i, s, [(dev, True)]) # verify gathered requests - self.select_row_device(1, f"{dev}2") - self.check_format_type(1, "ext4") - self.check_row_mountpoint(1, "/boot") - self.check_reformat(1, False) + self.select_row_device(b, 1, f"{dev}2") + self.check_format_type(b, 1, "ext4") + self.check_row_mountpoint(b, 1, "/boot") + self.check_reformat(b, 1, False) - self.check_row_mountpoint(2, "/") - self.check_row_device(2, "Select a device") - self.check_reformat(2, True) + self.check_row_mountpoint(b, 2, "/") + self.check_row_device(b, 2, "Select a device") + self.check_reformat(b, 2, True) - self.add_mount() - self.select_row_device(3, f"{dev}4") - self.check_format_type(3, "ext4") - self.select_row_mountpoint(3, "/home") - self.check_reformat(3, False) + self.add_mount(b) + self.select_row_device(b, 3, f"{dev}4") + self.check_format_type(b, 3, "ext4") + self.select_row_mountpoint(b, 3, "/home") + self.check_reformat(b, 3, False) i.check_next_disabled() @@ -546,35 +546,35 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): i.next() s.rescan_disks() - self.select_mountpoint(i, s, [(dev1, False), (dev2, True)]) + self.select_mountpoint(b, i, s, [(dev1, False), (dev2, True)]) - self.check_device_available(1, "vda2", False) - self.check_device_available(1, "vdb1") + self.check_device_available(b, 1, "vda2", False) + self.check_device_available(b, 1, "vdb1") # Go back and change the disk selection. The partitioning should be re-created i.back() - self.browser.click("#installation-method-disk-selector-clear") - self.select_mountpoint(i, s, [(dev1, True), (dev2, True)]) + b.click("#installation-method-disk-selector-clear") + self.select_mountpoint(b, i, s, [(dev1, True), (dev2, True)]) - self.check_device_available(1, "vda2", True) - self.check_device_available(1, "vdb1") + self.check_device_available(b, 1, "vda2", True) + self.check_device_available(b, 1, "vdb1") - self.select_row_device(1, f"{dev1}2") - self.check_format_type(1, "xfs") - self.check_row_mountpoint(1, "/boot") - self.check_reformat(1, False) + self.select_row_device(b, 1, f"{dev1}2") + self.check_format_type(b, 1, "xfs") + self.check_row_mountpoint(b, 1, "/boot") + self.check_reformat(b, 1, False) - self.select_row_device(2, f"{dev1}3") - self.check_format_type(2, "xfs") - self.check_row_mountpoint(2, "/") - self.check_reformat(2, True) + self.select_row_device(b, 2, f"{dev1}3") + self.check_format_type(b, 2, "xfs") + self.check_row_mountpoint(b, 2, "/") + self.check_reformat(b, 2, True) - self.add_mount() - self.select_row_device(3, f"{dev2}1") - self.check_format_type(3, "xfs") - self.select_row_mountpoint(3, "/home") - self.check_reformat(3, False) + self.add_mount(b) + self.select_row_device(b, 3, f"{dev2}1") + self.check_format_type(b, 3, "xfs") + self.select_row_mountpoint(b, 3, "/home") + self.check_reformat(b, 3, False) i.next() @@ -622,40 +622,40 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): i.next() s.rescan_disks() - self.select_mountpoint(i, s, [(dev1, True)]) + self.select_mountpoint(b, i, s, [(dev1, True)]) - self.unlock_all_encrypted() - self.unlock_device("1234", "Some LUKS devices were not unlocked") + self.unlock_all_encrypted(b) + self.unlock_device(b, "1234", "Some LUKS devices were not unlocked") - self.unlock_all_encrypted_skip() + self.unlock_all_encrypted_skip(b) b.wait_not_present("button:contains(Unlock)") i.back() i.next(next_page=i.steps.CUSTOM_MOUNT_POINT) - self.unlock_all_encrypted() - self.unlock_device("einszweidrei", "Some LUKS devices were not unlocked") + self.unlock_all_encrypted(b) + self.unlock_device(b, "einszweidrei", "Some LUKS devices were not unlocked") - self.unlock_all_encrypted_skip() + self.unlock_all_encrypted_skip(b) b.wait_not_present("button:contains(Unlock)") i.back() i.next(next_page=i.steps.CUSTOM_MOUNT_POINT) - self.unlock_all_encrypted() - self.unlock_device("einszweidreivier") + self.unlock_all_encrypted(b) + self.unlock_device(b, "einszweidreivier") b.wait_not_present("#mount-point-mapping-table tbody tr:nth-child(4) td[data-label='Format type'] #unlock-luks-btn") - self.check_row_mountpoint(1, "/boot") - self.select_row_device(1, f"{dev1}2") + self.check_row_mountpoint(b, 1, "/boot") + self.select_row_device(b, 1, f"{dev1}2") - self.check_row_mountpoint(2, "/") + self.check_row_mountpoint(b, 2, "/") selector = "#mount-point-mapping-table-row-2 .pf-v5-c-select__toggle" - self.browser.click(f"{selector}:not([disabled]):not([aria-disabled=true])") + b.click(f"{selector}:not([disabled]):not([aria-disabled=true])") select_entry = f"{selector} + ul li:nth-of-type(3) button" - self.browser.click(select_entry) - self.browser.wait_in_text(f"{selector} .pf-v5-c-select__toggle-text", "luks") - self.check_format_type(2, "xfs") + b.click(select_entry) + b.wait_in_text(f"{selector} .pf-v5-c-select__toggle-text", "luks") + self.check_format_type(b, 2, "xfs") i.next() @@ -692,34 +692,34 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): i.next() s.rescan_disks() - self.select_mountpoint(i, s, [(dev, True)]) + self.select_mountpoint(b, i, s, [(dev, True)]) # btrfs snapshots should not be available - self.check_device_available(1, "snapshot1", False) + self.check_device_available(b, 1, "snapshot1", False) # verify gathered requests # root partition is not auto-mapped - self.check_row_mountpoint(1, "/boot") - self.check_row_device(1, "Select a device") - self.check_reformat(1, False) - self.select_row_device(1, f"{dev}2") - self.check_format_type(1, "ext4") - - self.check_row_mountpoint(2, "/") - self.check_row_device(2, "Select a device") - self.check_reformat(2, True) - self.select_row_device(2, "root") - self.check_format_type(2, "btrfs") - - self.add_mount() - self.select_row_device(3, "home") - self.check_reformat(3, False) - self.select_row_mountpoint(3, "/home") - self.check_format_type(3, "btrfs") + self.check_row_mountpoint(b, 1, "/boot") + self.check_row_device(b, 1, "Select a device") + self.check_reformat(b, 1, False) + self.select_row_device(b, 1, f"{dev}2") + self.check_format_type(b, 1, "ext4") + + self.check_row_mountpoint(b, 2, "/") + self.check_row_device(b, 2, "Select a device") + self.check_reformat(b, 2, True) + self.select_row_device(b, 2, "root") + self.check_format_type(b, 2, "btrfs") + + self.add_mount(b) + self.select_row_device(b, 3, "home") + self.check_reformat(b, 3, False) + self.select_row_mountpoint(b, 3, "/home") + self.check_format_type(b, 3, "btrfs") # Toggle reformat option - self.select_reformat(1) - self.check_reformat(1, True) + self.select_reformat(b, 1) + self.check_reformat(b, 1, True) i.next() @@ -746,48 +746,48 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): rmdir {tmp_mount} """) s.rescan_disks() - self.select_mountpoint(i, s, [(dev, True)]) - - self.select_row_device(1, f"{dev}2") - self.select_row_device(2, "root") - self.add_mount() - self.select_row_device(3, "home") - self.select_row_mountpoint(3, "/home") - self.add_mount() - self.select_row_device(4, "home/Movies") - self.select_row_mountpoint(4, "/home/Movies") - self.add_mount() - self.select_row_device(5, "home/Movies/Good_Movies") - self.select_row_mountpoint(5, "/home/Movies/Good_Movies") - self.add_mount() - self.select_row_device(6, "home/Movies/Bad_Movies") - self.select_row_mountpoint(6, "/home/Movies/Bad_Movies") + self.select_mountpoint(b, i, s, [(dev, True)]) + + self.select_row_device(b, 1, f"{dev}2") + self.select_row_device(b, 2, "root") + self.add_mount(b) + self.select_row_device(b, 3, "home") + self.select_row_mountpoint(b, 3, "/home") + self.add_mount(b) + self.select_row_device(b, 4, "home/Movies") + self.select_row_mountpoint(b, 4, "/home/Movies") + self.add_mount(b) + self.select_row_device(b, 5, "home/Movies/Good_Movies") + self.select_row_mountpoint(b, 5, "/home/Movies/Good_Movies") + self.add_mount(b) + self.select_row_device(b, 6, "home/Movies/Bad_Movies") + self.select_row_mountpoint(b, 6, "/home/Movies/Bad_Movies") # No error when no devices are reformatted for row in range(3, 6): - self.wait_mount_point_table_column_helper(row, "format", present=False) + self.wait_mount_point_table_column_helper(b, row, "format", present=False) # When parent is re-formatted all child devices must be reformatted - self.select_row_device(4, "home/Movies") - self.select_reformat(4) - self.wait_mount_point_table_column_helper(4, "format", text="Mismatch") - self.select_reformat(5) - self.select_reformat(6) - self.wait_mount_point_table_column_helper(4, "format", present=False) + self.select_row_device(b, 4, "home/Movies") + self.select_reformat(b, 4) + self.wait_mount_point_table_column_helper(b, 4, "format", text="Mismatch") + self.select_reformat(b, 5) + self.select_reformat(b, 6) + self.wait_mount_point_table_column_helper(b, 4, "format", present=False) # Check also that the rules apply to children deeper in the device tree - self.select_reformat(3) - self.wait_mount_point_table_column_helper(3, "format", present=False) - self.select_reformat(6, False) - self.wait_mount_point_table_column_helper(3, "format", text="Mismatch") + self.select_reformat(b, 3) + self.wait_mount_point_table_column_helper(b, 3, "format", present=False) + self.select_reformat(b, 6, False) + self.wait_mount_point_table_column_helper(b, 3, "format", text="Mismatch") # When parent is re-formmated all child devices should be # * either also reformatted if selected # * either not selected (not part of the mountpoint assignment table) - self.remove_row(5) - self.remove_row(5) - self.wait_mount_point_table_column_helper(3, "format", present=False) - self.wait_mount_point_table_column_helper(4, "format", present=False) + self.remove_row(b, 5) + self.remove_row(b, 5) + self.wait_mount_point_table_column_helper(b, 3, "format", present=False) + self.wait_mount_point_table_column_helper(b, 4, "format", present=False) i.check_next_disabled(False) @@ -823,37 +823,37 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): i.next() s.rescan_disks() - self.select_mountpoint(i, s, [(dev, True)]) + self.select_mountpoint(b, i, s, [(dev, True)]) # verify gathered requests # root partition is not auto-mapped - self.check_row_mountpoint(1, "/boot") - self.check_row_device(1, "Select a device") - self.check_reformat(1, False) - self.select_row_device(1, f"{dev}2") - self.check_format_type(1, "ext4") - - self.check_row_mountpoint(2, "/") - self.check_row_device(2, "Select a device") - self.check_reformat(2, True) - self.select_row_device(2, f"{vgname}-root") - self.check_format_type(2, "ext4") - - self.add_mount() - self.select_row_device(3, f"{vgname}-home") - self.check_reformat(3, False) - self.select_row_mountpoint(3, "/home") - self.check_format_type(3, "ext4") - - self.add_mount() - self.select_row_device(4, f"{vgname}-swap") - self.check_reformat(4, False) - self.check_row_mountpoint(4, "swap") - self.check_format_type(4, "swap") + self.check_row_mountpoint(b, 1, "/boot") + self.check_row_device(b, 1, "Select a device") + self.check_reformat(b, 1, False) + self.select_row_device(b, 1, f"{dev}2") + self.check_format_type(b, 1, "ext4") + + self.check_row_mountpoint(b, 2, "/") + self.check_row_device(b, 2, "Select a device") + self.check_reformat(b, 2, True) + self.select_row_device(b, 2, f"{vgname}-root") + self.check_format_type(b, 2, "ext4") + + self.add_mount(b) + self.select_row_device(b, 3, f"{vgname}-home") + self.check_reformat(b, 3, False) + self.select_row_mountpoint(b, 3, "/home") + self.check_format_type(b, 3, "ext4") + + self.add_mount(b) + self.select_row_device(b, 4, f"{vgname}-swap") + self.check_reformat(b, 4, False) + self.check_row_mountpoint(b, 4, "swap") + self.check_format_type(b, 4, "swap") # Toggle reformat option - self.select_reformat(1) - self.check_reformat(1, True) + self.select_reformat(b, 1) + self.check_reformat(b, 1, True) i.next() @@ -869,10 +869,10 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): i.back(previous_page=i.steps.CUSTOM_MOUNT_POINT) # remove the /home row and check that row 3 is now swap - self.remove_row(3) + self.remove_row(b, 3) - self.check_row_mountpoint(3, "swap") - self.check_row_device(3, f"{vgname}-swap") + self.check_row_mountpoint(b, 3, "swap") + self.check_row_device(b, 3, f"{vgname}-swap") i.next() @@ -884,5 +884,84 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase): r.check_disk_row(disk, 2, f"{vgname}-root, 6.01 GB: format as ext4, /") r.check_disk_row(disk, 3, f"{vgname}-swap, 902 MB: mount, swap") + @nondestructive + def testUnusableFormats(self): + b = self.browser + m = self.machine + i = Installer(b, m) + s = Storage(b, m) + + self.addCleanup(m.execute, "wipefs --all /dev/vda") + + disk = "/dev/vda" + dev = "vda" + s.partition_disk(disk, [("1GB", "ext4"), ("1GB", None), ("1GB", "lvmpv")]) + s.udevadm_settle() + + i.open() + i.next() + s.rescan_disks() + + self.select_mountpoint(b, i, s, [(dev, True)]) + + # unformatted and unmountable devices should not be available + self.check_device_available(b, 1, f"{dev}2", False) + self.check_device_available(b, 1, f"{dev}3", False) + + +class TestStorageMountPointsEFI(anacondalib.VirtInstallMachineCase, TestUtils): + efi = True + + def testBasic(self): + b = self.browser + m = self.machine + i = Installer(b, m) + s = Storage(b, m) + r = Review(b) + + self.addCleanup(m.execute, "wipefs --all /dev/vda") + + disk = "/dev/vda" + dev = "vda" + s.partition_disk(disk, [("500MiB", "efi"), ("1GB", "ext4"), ("", "xfs")]) + + s.udevadm_settle() + + i.open() + i.next() + s.rescan_disks() + + self.select_mountpoint(b, i, s, [(dev, True)]) + + # verify gathered requests + # root partition is not auto-mapped + self.check_row_mountpoint(b, 1, "/boot") + self.check_row_device(b, 1, "Select a device") + self.check_reformat(b, 1, False) + self.select_row_device(b, 1, f"{dev}2") + self.check_format_type(b, 1, "ext4") + + self.check_row_mountpoint(b, 2, "/") + self.check_row_device(b, 2, "Select a device") + self.check_reformat(b, 2, True) + self.select_row_device(b, 2, f"{dev}3") + self.check_format_type(b, 2, "xfs") + + self.check_row_mountpoint(b, 3, "/boot/efi") + self.check_row_device(b, 3, "Select a device") + self.check_reformat(b, 3, False) + self.select_row_device(b, 3, f"{dev}1") + self.check_format_type(b, 3, "EFI System Partition") + + i.next() + + # verify review screen + r.check_disk(dev, "16.1 GB vda (0x1af4)") + + r.check_disk_row(dev, 1, "vda1, 524 MB: mount, /boot/efi") + r.check_disk_row(dev, 2, "vda2, 1.07 GB: mount, /boot") + r.check_disk_row(dev, 3, "vda3, 14.5 GB: format as xfs, /") + + if __name__ == '__main__': test_main() diff --git a/ui/webui/test/helpers/storage.py b/ui/webui/test/helpers/storage.py index f9ad15d5f35..a8febabab8a 100644 --- a/ui/webui/test/helpers/storage.py +++ b/ui/webui/test/helpers/storage.py @@ -292,16 +292,25 @@ def partition_disk(self, disk, partitions_params): if params[1] == "biosboot": sgdisk.append("--typecode=0:ef02") + if params[1] == "efi": + sgdisk.append("--typecode=0:ef00") sgdisk.append(disk) command += f"\n{' '.join(sgdisk)}" if params[1] not in ("biosboot", None): - mkfs = [f"mkfs.{params[1]}"] + if params[1] == "lvmpv": + mkfs = ["pvcreate"] + else: + if params[1] == "efi": + fs = "vfat" + else: + fs = params[1] + mkfs = [f"mkfs.{fs}"] # force flag - if params[1] in ["xfs", "btrfs"]: + if params[1] in ["xfs", "btrfs", "lvmpv"]: mkfs.append("-f") elif params[1] in ["ext4", "etx3", "ext2", "ntfs"]: mkfs.append("-F") diff --git a/ui/webui/test/machine_install.py b/ui/webui/test/machine_install.py index 1f38781004a..61165c37c39 100755 --- a/ui/webui/test/machine_install.py +++ b/ui/webui/test/machine_install.py @@ -43,6 +43,8 @@ class VirtInstallMachine(VirtMachine): + efi = False + def _execute(self, cmd): return subprocess.check_call(cmd, stderr=subprocess.STDOUT, shell=True) @@ -125,12 +127,18 @@ def start(self): location = f"{iso_path}" extra_args = f"inst.ks=file:/{os.path.basename(self.payload_ks_path)}" + if self.efi: + boot_arg = "--boot uefi " + else: + boot_arg = "" + try: self._execute( "virt-install " "--wait " "--connect qemu:///session " "--quiet " + f"{boot_arg} " f"--name {self.label} " f"--os-variant=detect=on " "--memory 2048 " @@ -173,7 +181,7 @@ def start(self): def kill(self): self._execute(f"virsh -q -c qemu:///session destroy {self.label} || true") self._execute( - f"virsh -q -c qemu:///session undefine " + f"virsh -q -c qemu:///session undefine --nvram " # tell undefine to also delete the EFI NVRAM device f"--remove-all-storage {self.label} || true" ) os.remove(self.payload_ks_path) @@ -204,3 +212,7 @@ def is_live(self): def get_volume_id(self, iso_path): return subprocess.check_output(fr"isoinfo -d -i {iso_path} | grep -oP 'Volume id: \K.*'", shell=True).decode(sys.stdout.encoding).strip() + + +class VirtInstallEFIMachine(VirtInstallMachine): + efi = True diff --git a/ui/webui/test/reference b/ui/webui/test/reference index 905b97a37f8..8e686c8c532 160000 --- a/ui/webui/test/reference +++ b/ui/webui/test/reference @@ -1 +1 @@ -Subproject commit 905b97a37f82ce6f9ea338629fbd22da776bebf4 +Subproject commit 8e686c8c532df3e346d4939dac0065abd780b025 diff --git a/ui/webui/test/webui_testvm.py b/ui/webui/test/webui_testvm.py index c002d81adb6..1b9adad0ef3 100755 --- a/ui/webui/test/webui_testvm.py +++ b/ui/webui/test/webui_testvm.py @@ -19,7 +19,7 @@ import subprocess import argparse -from machine_install import VirtInstallMachine +from machine_install import VirtInstallMachine, VirtInstallEFIMachine def cmd_cli(): @@ -27,9 +27,13 @@ def cmd_cli(): parser.add_argument("image", help="Image name") parser.add_argument("--rsync", help="Rsync development files over on startup", action='store_true') parser.add_argument("--host", help="Hostname to rsync", default='test-updates') + parser.add_argument("--efi", help="Start the VM with an EFI firmware", action='store_true') args = parser.parse_args() - machine = VirtInstallMachine(image=args.image) + if args.efi: + machine = VirtInstallEFIMachine(image=args.image) + else: + machine = VirtInstallMachine(image=args.image) try: machine.start()