#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)## Copyright (C) 2015 Red Hat, Inc.# SPDX-License-Identifier: LGPL-2.1-or-laterimport mathimport osimport unittestimport storagelibimport testlib# We use basename a lot, let's reduce the noisedef bn(path):return os.path.basename(path)@testlib.nondestructiveclass TestStorageLvm2(storagelib.StorageCase):def can_do_layouts(self):return self.storaged_version >= [2, 10]def skip_if_no_layouts(self):if not self.can_do_layouts():raise unittest.SkipTest("raid layouts not supported")def testLvm(self):m = self.machineb = self.browsermount_point_one = "/run/one"mount_point_thin = "/run/thin"self.login_and_go("/storage")dev_1 = self.add_ram_disk()dev_2 = self.add_loopback_disk(name="loop10")b.wait_visible(self.card_row("Storage", name=dev_1))b.wait_visible(self.card_row("Storage", name=dev_2))# Create a volume group out of two disksm.execute(f"vgcreate TEST1 {dev_1} {dev_2}")# just in case the test failsself.addCleanupVG("TEST1")self.click_card_row("Storage", name="TEST1")b.wait_visible(self.card_row("LVM2 volume group", name=dev_1))b.wait_visible(self.card_row("LVM2 volume group", name=dev_2))# Create two logical volumesm.execute("lvcreate TEST1 -n one -L 20m")b.wait_visible(self.card_row("LVM2 logical volumes", name="one"))m.execute("lvcreate TEST1 -n two -L 20m")b.wait_visible(self.card_row("LVM2 logical volumes", name="two"))# Deactivate onem.execute("lvchange TEST1/one -a n")b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "Inactive logical volume")# and remove itm.execute("until lvremove -f TEST1/one; do sleep 5; done")b.wait_not_present(self.card_row("LVM2 logical volumes", name="one"))# remove a disk from the volume groupm.execute(f"pvmove {dev_2} &>/dev/null || true")m.execute(f"vgreduce TEST1 {dev_2}")b.wait_not_present(self.card_row("LVM2 volume group", name=dev_2))# The remaining lone disk is not removableself.check_dropdown_action_disabled(self.card_row("LVM2 volume group", name=dev_1), "Remove", "Last cannot be removed")# Thin volumesm.execute("lvcreate TEST1 --thinpool pool -L 20m")b.wait_visible(self.card_row("LVM2 logical volumes", name="pool"))m.execute("lvcreate -T TEST1/pool -n thin -V 100m")b.wait_visible(self.card_row("LVM2 logical volumes", name="thin"))self.click_card_row("LVM2 logical volumes", name="pool")b.wait_visible(self.card_row("Thinly provisioned LVM2 logical volumes", name="thin"))m.execute("dd if=/dev/urandom of=/dev/mapper/TEST1-thin bs=1M count=10 status=none")b.wait_text(self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Data used"), "50%")m.execute("until lvremove -f TEST1/thin; do sleep 5; done")b.wait_text(self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Data used"), "0%")# remove the volume groupb.go("#/")m.execute("vgremove -f TEST1")b.wait_visible(self.card("Storage"))b.wait_not_present(self.card_row("Storage", name="TEST1"))# Wipe the disks and make sure lvmetad forgets about them. This# might help with reusing it in the second half of this test.## HACK - the pvscan is necessary because of# https://bugzilla.redhat.com/show_bug.cgi?id=1063813#m.execute(f"wipefs -a {dev_1} {dev_2}")m.execute(f"pvscan --cache {dev_1} {dev_2}")# create volume group in the UIself.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),expect=lambda: (self.dialog_is_present('disks', dev_1) andself.dialog_is_present('disks', dev_2) andself.dialog_check({"name": "vgroup0"})),values={"disks": {dev_1: True,dev_2: True}})# just in case the test failsself.addCleanupVG("vgroup0")b.wait_visible(self.card_row("Storage", name="vgroup0"))# Check that the next name is "vgroup1"self.click_devices_dropdown("Create LVM2 volume group")self.dialog_wait_open()self.dialog_wait_val("name", "vgroup1")self.dialog_cancel()self.dialog_wait_close()self.click_card_row("Storage", name="vgroup0")# create a logical volumeb.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog(expect={"name": "lvol0"},values={"purpose": "block","size": 20})b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")# check that the next default name is "lvol1"b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog_wait_open()self.dialog_wait_val("name", "lvol1")self.dialog_cancel()self.dialog_wait_close()# rename lvol0self.click_card_row("LVM2 logical volumes", 1)b.click(self.card_desc_action("LVM2 logical volume", "Name"))self.dialog_wait_open()self.dialog({"name": "lvol1"})b.wait_text(self.card_desc("LVM2 logical volume", "Name"), "lvol1")if self.can_do_layouts():# check that it is stored on dev_2b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(dev_2))# grow itb.click(self.card_button("LVM2 logical volume", "Grow"))self.dialog({"size": 30})# format and mount itself.click_card_dropdown("Unformatted data", "Format")self.dialog({"type": "ext4","mount_point": mount_point_one})self.wait_mounted("ext4 filesystem")self.assert_in_configuration("/dev/vgroup0/lvol1", "fstab", "dir", mount_point_one)b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), mount_point_one)# unmount itb.click(self.card_button("ext4 filesystem", "Unmount"))self.confirm()self.wait_not_mounted("ext4 filesystem")# shrink it, this time with a filesystem.b.click(self.card_button("LVM2 logical volume", "Shrink"))self.dialog({"size": 10})# delete itself.click_card_dropdown("LVM2 logical volume", "Delete")self.confirm()b.wait_visible(self.card("LVM2 logical volumes"))b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol1"))self.assertEqual(m.execute(f"grep {mount_point_one} /etc/fstab || true"), "")# remove disk2self.click_dropdown(self.card_row("LVM2 volume group", name=dev_2), "Remove")b.wait_not_present(self.card_row("LVM2 volume group", name=dev_2))b.wait_in_text(self.card_desc("LVM2 volume group", "Capacity"), "50.3 MB")# loopback needs a explicit trigger...m.execute("udevadm trigger")# create thin pool and volume# the pool will be maximum size, 50.3 MBb.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog(expect={"size": 50.3},values={"purpose": "pool","name": "pool","size": 38})b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "pool")self.click_dropdown(self.card_row("LVM2 logical volumes", 1),"Create new thinly provisioned logical volume")self.dialog(expect={"name": "lvol0"},values={"name": "thin","size": 50})b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "thin")# add a disk and resize the poolb.click(self.card_button("LVM2 volume group", "Add physical volume"))self.dialog_wait_open()self.dialog_set_val('disks', {dev_2: True})self.dialog_apply()self.dialog_wait_close()b.wait_visible(self.card_row("LVM2 volume group", name=dev_2))# this is sometimes 96, sometimes 100 MBcap = self.card_desc("LVM2 volume group", "Capacity")b.wait_js_func("(sel => { const c = Number(ph_text(sel).split(' ')[0]); return c >= 96 && c <= 101 })", cap)self.click_card_row("LVM2 logical volumes", name="pool")b.click(self.card_button("Pool for thinly provisioned LVM2 logical volumes", "Grow"))self.dialog({"size": 70})# use almost all of the pool by erasing the thin volumeself.click_card_row("Thinly provisioned LVM2 logical volumes", 1)self.click_card_dropdown("Unformatted data", "Format")self.dialog({"erase.on": True,"type": "ext4","mount_point": mount_point_thin})self.assert_in_configuration("/dev/vgroup0/thin", "fstab", "dir", mount_point_thin)b.click(self.card_parent_link())b.wait_text(self.card_row_col("Thinly provisioned LVM2 logical volumes", 1, 3), mount_point_thin)# Unmount to prevent unstable usage numbers from failing the pixel testself.click_dropdown(self.card_row("Thinly provisioned LVM2 logical volumes", 1),"Unmount")self.confirm()b.assert_pixels('body', "pool-page",ignore=[self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Data used"),self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Metadata used")])# remove poolself.click_card_dropdown("Pool for thinly provisioned LVM2 logical volumes", "Delete")self.confirm()b.wait_visible(self.card("LVM2 logical volumes"))b.wait_not_present(self.card_row("LVM2 logical volumes", name="pool"))self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")# make another logical volume and format it, just so that we# can see whether deleting the volume group will clean it all# up.b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog(expect={"name": "lvol0"},values={"purpose": "block"})b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")self.click_card_row("LVM2 logical volumes", 1)self.click_card_dropdown("Unformatted data", "Format")self.dialog({"type": "ext4","mount_point": mount_point_one})self.assert_in_configuration("/dev/vgroup0/lvol0", "fstab", "dir", mount_point_one)b.click(self.card_parent_link())b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem")b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), mount_point_one)b.wait_visible(self.card_row_col("LVM2 logical volumes", 1, 4) + " .usage-bar[role=progressbar]")b.assert_pixels('body', "page",# Usage numbers are not stable and also cause# the table columns to shift. The usage bars# are not stable but are always the same size,# so it is good enough to ignore them.mock={self.card_desc("LVM2 volume group", "UUID"): "a12978a1-5d6e-f24f-93de-11789977acde",".usage-text": "---"},ignore=[".usage-bar"])# rename volume groupb.click(self.card_desc_action("LVM2 volume group", "Name"))self.dialog_wait_open()# No warning about the rootfs should be displayedb.wait_visible("#dialog button.apply.pf-m-primary")b.wait_not_in_text("#dialog", "This volume group contains the root filesystem.")self.dialog_set_val("name", "vgroup1")self.dialog_apply()self.dialog_wait_close()# remove volume groupself.click_card_dropdown("LVM2 volume group", "Delete")self.confirm()b.wait_visible(self.card("Storage"))b.wait_not_present(self.card_row("Storage", name="vgroup1"))self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")def testUnpartitionedSpace(self):m = self.machineb = self.browserself.login_and_go("/storage")disk1 = self.add_ram_disk()disk2 = self.add_loopback_disk()b.wait_visible(self.card_row("Storage", name=disk1))b.wait_visible(self.card_row("Storage", name=disk2))# Put a partition table on disk1 and disk2m.execute(f'parted -s {disk1} mktable gpt')m.execute(f'parted -s {disk2} mktable gpt')# Create a volume group out of disk1self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown('Create LVM2 volume group'),expect=lambda: self.dialog_is_present('disks', disk1) andself.dialog_is_present('disks', "unpartitioned space on Linux"),values={"disks": {disk1: True}})self.click_card_row("Storage", name="vgroup0")# Check the we are really using a partition on disk1 nowb.wait_in_text(self.card_row_col("LVM2 volume group", 1, 2), "Partition - Linux")# Add the unused space of disk2self.dialog_with_retry(trigger=lambda: b.click(self.card_button("LVM2 volume group","Add physical volume")),expect=lambda: self.dialog_is_present('disks', "unpartitioned space on " + disk2),values={"disks": {disk2: True}})b.wait_in_text(self.card_row_col("LVM2 volume group", 1, 2), "Partition - Block device")def testSnapshots(self):m = self.machineb = self.browserself.login_and_go("/storage")disk = self.add_ram_disk(100)b.wait_visible(self.card_row("Storage", name=disk))m.execute(f"vgcreate TEST {disk}")self.addCleanupVG("TEST")self.click_card_row("Storage", name="TEST")# Create a thinpool and a thin volume in itm.execute("lvcreate TEST --thinpool pool -L 10m")m.execute("lvcreate -T TEST/pool -n thin -V 30m")b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "thin")# Create a logical volume and take a snapshot of it. We will# later check that the snapshot isn't shown.m.execute("lvcreate TEST -n lvol0 -L 10m")m.execute("lvcreate -s -n snap0 -L 10m TEST/lvol0")# the above lvol0 will be the new first entry in the table, so# TEST/thin moves to he third rowb.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "pool")b.wait_text(self.card_row_col("LVM2 logical volumes", 3, 1), "thin")# Take a snapshot of the thin volume and check that it appearsself.click_dropdown(self.card_row("LVM2 logical volumes", 3), "Create snapshot")self.dialog({"name": "mysnapshot"})b.wait_text(self.card_row_col("LVM2 logical volumes", 3, 1), "mysnapshot")# Now check that the traditional snapshot is not shown. We do# this here to be sure that Cockpit is fully caught up and has# actually ignored it instead of just not having gotten around# to showing it.b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")b.wait_not_present(self.card_row("LVM2 logical volumes", name="snap0"))def testRaid(self):b = self.browserself.skip_if_no_layouts()self.login_and_go("/storage")disk1 = self.add_ram_disk()disk2 = self.add_loopback_disk(name="loop10")disk3 = self.add_loopback_disk(name="loop11")disk4 = self.add_loopback_disk(name="loop12")# Make a volume group with four physical volumeswith b.wait_timeout(60):self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),expect=lambda: (self.dialog_is_present('disks', disk1) andself.dialog_is_present('disks', disk2) andself.dialog_is_present('disks', disk3) andself.dialog_is_present('disks', disk4) andself.dialog_check({"name": "vgroup0"})),values={"disks": {disk1: True,disk2: True,disk3: True,disk4: True}})self.addCleanupVG("vgroup0")# Make a raid5 on three PVs, using about half of eachself.click_card_row("Storage", name="vgroup0")b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog_wait_open()self.dialog_wait_val("name", "lvol0")self.dialog_set_val("purpose", "block")self.dialog_set_val("layout", "raid5")self.dialog_set_val("pvs", {disk1: False})self.dialog_set_val("size", 40)b.assert_pixels("#dialog", "create-raid5")self.dialog_apply()self.dialog_wait_close()b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")self.click_card_row("LVM2 logical volumes", 1)b.wait_text(self.card_desc("LVM2 logical volume", "Layout"), "Distributed parity (RAID 5)")b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk2))b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk3))b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk4))b.assert_pixels(self.card("LVM2 logical volume"), "raid5-card")# Make linear volume to fully use second PVb.click(self.card_parent_link())b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog(expect={"name": "lvol1"},values={"purpose": "block","layout": "linear","pvs": {disk1: False, disk3: False, disk4: False}})b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "lvol1")self.click_card_row("LVM2 logical volumes", 2)b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk2))b.click(self.card_parent_link())self.click_card_row("LVM2 logical volumes", 1)# Grow raid5 to about maximumb.click(self.card_button("LVM2 logical volume", "Grow"))self.dialog_wait_open()self.dialog_set_val("size", 80)b.assert_pixels("#dialog", "grow-raid5")self.dialog_apply()self.dialog_wait_close()b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))# Check that each PV is used exactly once.card = self.card("LVM2 logical volume")b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)b.wait_not_in_text(card, "synchronized")b.assert_pixels(card, "raid5-card2")def testBrokenLinear(self):b = self.browserself.skip_if_no_layouts()self.login_and_go("/storage")disk1 = self.add_ram_disk()disk2 = self.add_loopback_disk()disk3 = self.add_loopback_disk()# Make a volume group with three physical volumeswith b.wait_timeout(60):self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),expect=lambda: (self.dialog_is_present('disks', disk1) andself.dialog_is_present('disks', disk2) andself.dialog_is_present('disks', disk3) andself.dialog_check({"name": "vgroup0"})),values={"disks": {disk1: True,disk2: True,disk3: True}})self.addCleanupVG("vgroup0")# Make a linear volume on two of themself.click_card_row("Storage", name="vgroup0")b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog(expect={"name": "lvol0"},values={"purpose": "block","layout": "linear","pvs": {disk2: False}})b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")self.click_card_row("LVM2 logical volumes", 1)b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk1))b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk3))# Kill one PVself.force_remove_disk(disk1)b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"),"This logical volume has lost some of its physical volumes and can no longer be used.")b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk1))b.click(self.card_parent_link())b.wait_in_text(".pf-v6-c-alert", "This volume group is missing some physical volumes.")b.wait_visible(self.card_desc_action("LVM2 volume group", "Name") + ":disabled")b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-times-circle')# Dismiss alert, this will delete the volumeb.click(".pf-v6-c-alert button:contains('Dismiss')")self.dialog_wait_open()b.wait_in_text("#dialog", "vgroup0/lvol0")self.dialog_apply()self.dialog_wait_close()b.wait_not_present(".pf-v6-c-alert")b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))def testMaxLayoutSizes(self):b = self.browserself.skip_if_no_layouts()# Make sure that Cockpit gets the computation of the maximum# size right.self.login_and_go("/storage")disk1 = self.add_loopback_disk(name="loop10")disk2 = self.add_loopback_disk(name="loop11")disk3 = self.add_loopback_disk(name="loop12")disk4 = self.add_loopback_disk(name="loop13")disk5 = self.add_loopback_disk(name="loop14")disk6 = self.add_loopback_disk(name="loop15")with b.wait_timeout(60):self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),expect=lambda: (self.dialog_is_present('disks', disk1) andself.dialog_is_present('disks', disk2) andself.dialog_is_present('disks', disk3) andself.dialog_is_present('disks', disk4) andself.dialog_is_present('disks', disk5) andself.dialog_is_present('disks', disk6) andself.dialog_check({"name": "vgroup0"})),values={"disks": {disk1: True,disk2: True,disk3: True,disk4: True,disk5: True,disk6: True}})self.addCleanupVG("vgroup0")self.click_card_row("Storage", name="vgroup0")b.wait_visible(self.card_row("LVM2 volume group", name=disk1))b.wait_visible(self.card_row("LVM2 volume group", name=disk2))b.wait_visible(self.card_row("LVM2 volume group", name=disk3))b.wait_visible(self.card_row("LVM2 volume group", name=disk4))b.wait_visible(self.card_row("LVM2 volume group", name=disk5))b.wait_visible(self.card_row("LVM2 volume group", name=disk6))def mb(size):if size < 100e6:return str(round(size / 1e6, 1)) + " MB"else:return str(round(size / 1e6)) + " MB"def test(layout, expected_size):b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog_wait_open()self.dialog_wait_val("name", "lvol0")self.dialog_set_val("purpose", "block")self.dialog_set_val("layout", layout)if layout == "raid10":self.dialog_set_val("pvs", {disk6: False})self.dialog_apply()self.dialog_wait_error("pvs", "an even number")self.dialog_set_val("pvs", {disk6: True})self.dialog_apply()self.dialog_wait_close()b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")self.click_card_row("LVM2 logical volumes", 1)field = "Physical volumes" if layout.startswith("linear") else "Stripes"b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk1))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk2))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk3))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk4))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk5))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk6))b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), mb(expected_size))self.click_card_dropdown("LVM2 logical volume", "Delete")with b.wait_timeout(60):self.confirm()b.wait_visible(self.card("LVM2 logical volumes"))b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))ext_size = 4 * 1024 * 1024pv_size = math.floor(50e6 / ext_size) * ext_sizen_pvs = 6test("linear", n_pvs * pv_size)test("raid0", n_pvs * pv_size)test("raid1", pv_size - ext_size)test("raid10", (n_pvs / 2) * (pv_size - ext_size))test("raid5", (n_pvs - 1) * (pv_size - ext_size))test("raid6", (n_pvs - 2) * (pv_size - ext_size))def testMaxLayoutGrowth(self):b = self.browserself.skip_if_no_layouts()# Make sure that Cockpit gets the computation of the maximum# size right when growing a logical volume.self.login_and_go("/storage")disk1 = self.add_loopback_disk(name="loop10")disk2 = self.add_loopback_disk(name="loop11")disk3 = self.add_loopback_disk(name="loop12")disk4 = self.add_loopback_disk(name="loop13")disk5 = self.add_loopback_disk(name="loop14")disk6 = self.add_loopback_disk(name="loop15")with b.wait_timeout(60):self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),expect=lambda: (self.dialog_is_present('disks', disk1) andself.dialog_is_present('disks', disk2) andself.dialog_is_present('disks', disk3) andself.dialog_is_present('disks', disk4) andself.dialog_is_present('disks', disk5) andself.dialog_is_present('disks', disk6) andself.dialog_check({"name": "vgroup0"})),values={"disks": {disk1: True,disk2: True,disk3: True,disk4: True,disk5: True,disk6: True}})self.addCleanupVG("vgroup0")self.click_card_row("Storage", name="vgroup0")b.wait_visible(self.card_row("LVM2 volume group", name=disk1))b.wait_visible(self.card_row("LVM2 volume group", name=disk2))b.wait_visible(self.card_row("LVM2 volume group", name=disk3))b.wait_visible(self.card_row("LVM2 volume group", name=disk4))b.wait_visible(self.card_row("LVM2 volume group", name=disk5))b.wait_visible(self.card_row("LVM2 volume group", name=disk6))def mb(size):if size < 100e6:return str(round(size / 1e6, 1)) + " MB"else:return str(round(size / 1e6)) + " MB"def test(layout, expected_size):b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog(expect={"name": "lvol0"},values={"purpose": "block","layout": layout,"size": round(expected_size / 2e6)})b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")self.click_card_row("LVM2 logical volumes", 1)field = "Stripes"b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk1))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk2))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk3))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk4))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk5))b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk6))# Grow to max with default pvsb.click(self.card_button("LVM2 logical volume", "Grow"))self.dialog_wait_open()slider = self.dialog_field("size") + " .pf-v6-c-slider .pf-v6-c-slider__rail"width = b.call_js_func('(function (sel) { return ph_find(sel).offsetWidth; })', slider)b.mouse(slider, "click", width, 0)self.dialog_apply()self.dialog_wait_close()card = self.card("LVM2 logical volume")b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk5)}')", 1)b.wait_count(card + f" .storage-stripe-pv-box-dev:contains('{bn(disk6)}')", 1)b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), mb(expected_size))self.click_card_dropdown("LVM2 logical volume", "Delete")with b.wait_timeout(60):self.confirm()b.wait_visible(self.card("LVM2 logical volumes"))b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))ext_size = 4 * 1024 * 1024pv_size = math.floor(50e6 / ext_size) * ext_sizen_pvs = 6test("raid0", n_pvs * pv_size)test("raid1", pv_size - ext_size)test("raid10", (n_pvs / 2) * (pv_size - ext_size))test("raid5", (n_pvs - 1) * (pv_size - ext_size))test("raid6", (n_pvs - 2) * (pv_size - ext_size))@testlib.skipImage("No targetd in Arch Linux", "arch")def testDegradation(self):b = self.browserself.skip_if_no_layouts()# Make one (very small) logical volume for each RAID type and# then break them.self.login_and_go("/storage")disk1 = self.add_targetd_loopback_disk(index=1, size=100)disk2 = self.add_targetd_loopback_disk(index=2, size=100)disk3 = self.add_targetd_loopback_disk(index=3, size=100)disk4 = self.add_targetd_loopback_disk(index=4, size=100)disk5 = self.add_targetd_loopback_disk(index=5, size=100)with b.wait_timeout(60):self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),expect=lambda: (self.dialog_is_present('disks', disk1) andself.dialog_is_present('disks', disk2) andself.dialog_is_present('disks', disk3) andself.dialog_is_present('disks', disk4) andself.dialog_is_present('disks', disk5) andself.dialog_check({"name": "vgroup0"})),values={"disks": {disk1: True,disk2: True,disk3: True,disk4: True,disk5: True}})self.addCleanupVG("vgroup0")self.click_card_row("Storage", name="vgroup0")def create(row, layout, expected_name):b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog_wait_open()self.dialog_wait_val("name", expected_name)self.dialog_set_val("layout", layout)if layout == "raid10":self.dialog_set_val("pvs", {disk5: False})elif layout == "linear":self.dialog_set_val("pvs", {disk2: False, disk3: False, disk4: False, disk5: False})self.dialog_set_val("size", 10)self.dialog_apply()self.dialog_wait_close()b.wait_text(self.card_row_col("LVM2 logical volumes", row, 1), expected_name)create(1, "linear", "lvol0")create(2, "raid0", "lvol1")create(3, "raid1", "lvol2")create(4, "raid10", "lvol3")create(5, "raid5", "lvol4")create(6, "raid6", "lvol5")def wait_msg(row, msg):self.click_card_row("LVM2 logical volumes", row)b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes" if row == 1 else "Stripes"),msg)b.click(self.card_parent_link())def wait_partial(row):wait_msg(row, "This logical volume has lost some of its physical volumes and can no longer be used.")def wait_degraded(row):wait_msg(row, "This logical volume has lost some of its physical volumes but has not lost any data yet.")def wait_maybe_partial(row):wait_msg(row, "This logical volume has lost some of its physical volumes but might not have lost any data yet.")self.force_remove_disk(disk1)wait_partial(1) # linear is broken nowwait_partial(2) # striped as wellwait_degraded(3) # mirror is finewait_degraded(4) # striped mirror as wellwait_degraded(5) # raid5 and ...wait_degraded(6) # ... raid6 are doing their jobself.force_remove_disk(disk2)wait_degraded(3) # mirror is still finewait_maybe_partial(4) # striped mirror is not sure anymorewait_partial(5) # raid5 is now toastwait_degraded(6) # but raid6 still hangs onself.force_remove_disk(disk3)wait_degraded(3) # mirror is _still_ finewait_partial(4) # striped mirror has lost more than half and is kaputtwait_partial(6) # raid6 is finally toast as welldef testLvmOnLuks(self):b = self.browserm = self.machineself.skip_if_no_layouts()# Make sure that Cockpit either gets the layout description# right for encrypted physical volumes, or doesn't show any# layout (for older versions of UDisks2 without# https://github.com/storaged-project/udisks/pull/1206.self.login_and_go("/storage")disk = self.add_ram_disk()disk2 = self.add_loopback_disk()b.wait_visible(self.card_row("Storage", name=disk2))m.execute(f"echo einszweidrei | cryptsetup luksFormat --pbkdf-memory 32768 {disk}")m.execute(f"echo einszweidrei | cryptsetup luksOpen {disk} dm-test")self.addCleanup(m.execute, "cryptsetup close dm-test || true")m.execute(f"vgcreate vgroup0 /dev/mapper/dm-test {disk2}")self.addCleanupVG("vgroup0")m.execute("lvcreate vgroup0 -n lvol0 -l100%FREE")self.click_card_row("Storage", name="lvol0")if (self.storaged_version < [2, 11]):# "Physical volumes" should not be displayed since we# might miss the encrypted ones dues to a bug in UDisks2.b.wait_text(self.card_desc("LVM2 logical volume", "Name"), "lvol0")b.wait_not_present(self.card_desc("LVM2 logical volume", "Physical volumes"))else:# We should have both disks listed as physical volumesb.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk))b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk2))def testUnsupportedPartitions(self):b = self.browserm = self.machineself.login_and_go("/storage")dev_1 = self.add_ram_disk(100)m.execute(f"vgcreate vgroup0 {dev_1}")self.addCleanupVG("vgroup0")m.execute("lvcreate vgroup0 -n lvol0 -l100%FREE")m.execute("parted /dev/vgroup0/lvol0 -s mktable gpt mkpart primary ext2 1M 50M")self.addCleanup(m.execute, "parted /dev/vgroup0/lvol0 -s mktable gpt; wipefs -a /dev/vgroup0/lvol0")b.wait_text(self.card_row_col("Storage", row_name="lvol0", col_index=3), "GPT partitions")self.click_card_row("Storage", name="lvol0")b.wait_in_text(self.card("GPT partitions"), "Partitions are not supported on this block device.")# Neither the partition nor the free space should be offered# for creating new things.b.click(self.card_parent_link())b.click(self.card_parent_link())self.click_devices_dropdown("Create LVM2 volume group")b.wait_in_text("#dialog", "No disks are available")class TestStorageLvm2Destructive(storagelib.StorageCase):def can_do_layouts(self):return self.storaged_version >= [2, 10]def skip_if_no_layouts(self):if not self.can_do_layouts():raise unittest.SkipTest("raid layouts not supported")# This is only "destructive" because of## https://bugzilla.redhat.com/show_bug.cgi?id=2256432## We can't revocer the machine after that bug has hit us.def testRaidRepair(self):b = self.browserm = self.machineself.skip_if_no_layouts()self.login_and_go("/storage")disk1 = self.add_ram_disk()disk2 = self.add_loopback_disk(name="loop10")disk3 = self.add_loopback_disk(name="loop11")disk4 = self.add_loopback_disk(name="loop12")# Make a volume group with three physical volumeswith b.wait_timeout(60):self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),expect=lambda: (self.dialog_is_present('disks', disk1) andself.dialog_is_present('disks', disk2) andself.dialog_is_present('disks', disk3) andself.dialog_check({"name": "vgroup0"})),values={"disks": {disk1: True,disk2: True,disk3: True}})self.addCleanupVG("vgroup0")# Make a raid5 on the three PVsself.click_card_row("Storage", name="vgroup0")b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))self.dialog(expect={"name": "lvol0"},values={"purpose": "block","layout": "raid5"})b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")self.click_card_row("LVM2 logical volumes", 1)b.wait_text(self.card_desc("LVM2 logical volume", "Layout"), "Distributed parity (RAID 5)")b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk2))b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk3))# Kill one PVself.force_remove_disk(disk1)b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"),"This logical volume has lost some of its physical volumes but has not lost any data yet.")b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))b.click(self.card_parent_link())b.wait_in_text(".pf-v6-c-alert", "This volume group is missing some physical volumes.")b.wait_visible(self.card_desc_action("LVM2 volume group", "Name") + ":disabled")b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-exclamation-triangle')# Repair with fourthself.click_card_row("LVM2 logical volumes", 1)b.click(self.card_button("LVM2 logical volume", "Repair"))self.dialog_wait_open()b.wait_in_text("#dialog", "There is not enough space available")self.dialog_cancel()self.dialog_wait_close()b.click(self.card_parent_link())b.click(self.card_button("LVM2 volume group", "Add physical volume"))self.dialog_wait_open()self.dialog_set_val('disks', {disk4: True})self.dialog_apply()self.dialog_wait_close()b.wait_visible(self.card_row("LVM2 volume group", name=disk4))self.click_card_row("LVM2 logical volumes", 1)b.click(self.card_button("LVM2 logical volume", "Repair"))self.dialog_wait_open()self.dialog_apply()self.dialog_wait_error("pvs", "An additional 46.1 MB must be selected")self.dialog_set_val("pvs", {disk4: True})self.dialog_apply()try:self.dialog_wait_close() # wait for repair to finishexcept testlib.Error:# Produce evidence of lvconvert hanging, so that our maughties can match on itprint(m.execute("for p in $(pgrep lvm); do cat /proc/$p/stack; done"))raiseb.wait_not_in_text(self.card_desc("LVM2 logical volume", "Stripes"),"This logical volume has lost some")b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk4))# Dismiss alertb.click(self.card_parent_link())b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0"))b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-exclamation-triangle')b.click(".pf-v6-c-alert button:contains('Dismiss')")self.dialog({})b.wait_not_present(".pf-v6-c-alert")if __name__ == '__main__':testlib.test_main()