diff --git a/pkg/storaged/stratis-details.jsx b/pkg/storaged/stratis-details.jsx index e969f6e6d4d8..75ba5803eeec 100644 --- a/pkg/storaged/stratis-details.jsx +++ b/pkg/storaged/stratis-details.jsx @@ -20,6 +20,7 @@ import cockpit from "cockpit"; import React from "react"; +import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; @@ -55,6 +56,17 @@ import { std_reply, with_keydesc, with_stored_passphrase, confirm_tang_trust, ge const _ = cockpit.gettext; +export function check_stratis_warnings(client, enter_warning) { + if (!client.features.stratis_grow_blockdevs) + return; + + for (const p in client.stratis_pools) { + const blockdevs = client.stratis_pool_blockdevs[p] || []; + if (blockdevs.some(bd => bd.NewPhysicalSize[0] && Number(bd.NewPhysicalSize[1]) > Number(bd.TotalPhysicalSize))) + enter_warning(p, { warning: "unused-blockdevs" }); + } +} + function teardown_block(block) { return for_each_async(block.Configuration, c => block.RemoveConfigurationItem(c, {})); } @@ -185,10 +197,29 @@ export const StratisPoolDetails = ({ client, pool }) => { pool.ClevisInfo[0] && // pool has consistent clevis config (!pool.ClevisInfo[1][0] || pool.ClevisInfo[1][1][0] == "tang")); // not bound or bound to "tang" const tang_url = can_tang && pool.ClevisInfo[1][0] ? JSON.parse(pool.ClevisInfo[1][1][1]).url : null; + const blockdevs = client.stratis_pool_blockdevs[pool.path] || []; const forced_options = ["x-systemd.requires=stratis-fstab-setup@" + pool.Uuid + ".service"]; const managed_fsys_sizes = client.features.stratis_managed_fsys_sizes && !pool.Overprovisioning; + function grow_blockdevs() { + return for_each_async(blockdevs, bd => pool.GrowPhysicalDevice(bd.Uuid)); + } + + let alert = null; + if (client.features.stratis_grow_blockdevs && + blockdevs.some(bd => bd.NewPhysicalSize[0] && Number(bd.NewPhysicalSize[1]) > Number(bd.TotalPhysicalSize))) { + alert = ( + {_("Some block devices of this pool have grown in size after the pool was created. The pool can be safely grown to use the newly available space.")} +
+ {_("Grow the pool to take all space")} +
+
); + } + function delete_() { const location = cockpit.location; const usage = get_active_usage(client, pool.path, _("delete")); @@ -869,6 +900,7 @@ export const StratisPoolDetails = ({ client, pool }) => { ); return ; diff --git a/pkg/storaged/warnings.jsx b/pkg/storaged/warnings.jsx index 78aedf5c10c9..a346d2c7fb2b 100644 --- a/pkg/storaged/warnings.jsx +++ b/pkg/storaged/warnings.jsx @@ -19,6 +19,7 @@ import { get_parent } from "./utils.js"; import { check_mismounted_fsys } from "./fsys-tab.jsx"; +import { check_stratis_warnings } from "./stratis-details.jsx"; export function find_warnings(client) { const path_warnings = { }; @@ -104,5 +105,7 @@ export function find_warnings(client) { check_mismounted_fsys(client, path, enter_warning); } + check_stratis_warnings(client, enter_warning); + return path_warnings; } diff --git a/test/verify/check-storage-stratis b/test/verify/check-storage-stratis index b337d11e1a60..c0c710588bd3 100755 --- a/test/verify/check-storage-stratis +++ b/test/verify/check-storage-stratis @@ -617,6 +617,64 @@ class TestStorageStratis(storagelib.StorageCase): # And the pool should be full again b.wait_visible("button:contains(Create new filesystem):disabled") + def testPoolResize(self): + m = self.machine + b = self.browser + + self.login_and_go("/storage") + + dev = "/dev/sda" + m.add_disk("4G", serial="DISK1") + b.wait_in_text("#drives", dev) + + # Create a logical volume that we will later grow + m.execute(f"vgcreate vgroup0 {dev}; lvcreate vgroup0 -n lvol0 -L 1500000256b") + b.wait_in_text("#devices", "vgroup0") + + # Create a pool + self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create Stratis pool"), + expect=lambda: self.dialog_is_present('disks', "lvol0"), + values={"disks": {"lvol0": True}}) + b.wait_in_text("#devices", "pool0") + b.wait_in_text("#devices", "1.50 GB Stratis pool") + + # Grow the logical volume in Cockpit, the pool should grow automatically + b.click('.sidepanel-row:contains("vgroup0")') + b.wait_visible('#storage-detail') + self.content_tab_action(1, 1, "Grow") + self.dialog({"size": 1600}) + b.go("#/") + b.reload() # HACK - https://github.com/stratis-storage/stratisd/issues/3371 + b.enter_page("/storage") + b.wait_in_text("#devices", "1.60 GB Stratis pool") + + # Grow the logical volume from outside of Cockpit, the pool should complain + m.execute("lvresize vgroup0/lvol0 -L +100000256b") + b.wait_visible('.sidepanel-row:contains(pool0) .ct-icon-exclamation-triangle') + b.click('.sidepanel-row:contains("pool0")') + b.wait_visible('#storage-detail') + b.wait_visible('.pf-v5-c-alert:contains("This pool does not use all the space")') + b.click('button:contains("Grow the pool")') + b.reload() # HACK - https://github.com/stratis-storage/stratisd/issues/3371 + b.enter_page("/storage") + b.wait_visible('#storage-detail') + b.wait_not_present('.pf-v5-c-alert') + + b.go("#/") + + # Grow the logical volume from outside of Cockpit, the logical volume should also complain + m.execute("lvresize vgroup0/lvol0 -L +100000256b") + b.wait_visible('.sidepanel-row:contains(vgroup0) .ct-icon-exclamation-triangle') + b.click('.sidepanel-row:contains("vgroup0")') + b.wait_visible('#storage-detail') + vol_tab = self.content_tab_expand(1, 1) + b.wait_visible(vol_tab + " button:contains(Grow content)") + self.content_tab_action(1, 1, "Grow content") + b.reload() # HACK - https://github.com/stratis-storage/stratisd/issues/3371 + b.enter_page("/storage") + vol_tab = self.content_tab_expand(1, 1) + b.wait_not_present(vol_tab + " button:contains(Grow content)") + @testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "arch") class TestStoragePackagesStratis(packagelib.PackageCase, storagelib.StorageCase):