From d40b58597e93e4b846edc9a609e453f43f97a0dc Mon Sep 17 00:00:00 2001 From: Marius Vollmer Date: Wed, 21 Feb 2024 12:40:11 +0200 Subject: [PATCH] storage: Show btrfs subvolumes in a tree --- pkg/storaged/block/create-pages.jsx | 2 +- pkg/storaged/btrfs/subvolume.jsx | 95 +++++++++++++++++++++++++++-- pkg/storaged/btrfs/volume.jsx | 45 +------------- pkg/storaged/client.js | 2 +- test/reference | 2 +- test/verify/check-storage-basic | 2 +- test/verify/check-storage-btrfs | 33 +++++----- test/verify/check-storage-scaling | 2 +- 8 files changed, 113 insertions(+), 70 deletions(-) diff --git a/pkg/storaged/block/create-pages.jsx b/pkg/storaged/block/create-pages.jsx index 2463b2d4368b..3054c5ef01a3 100644 --- a/pkg/storaged/block/create-pages.jsx +++ b/pkg/storaged/block/create-pages.jsx @@ -35,7 +35,7 @@ import { make_swap_card } from "../swap/swap.jsx"; import { make_encryption_card } from "../crypto/encryption.jsx"; import { make_btrfs_device_card } from "../btrfs/device.jsx"; import { make_btrfs_filesystem_card } from "../btrfs/filesystem.jsx"; -import { make_btrfs_subvolume_pages } from "../btrfs/volume.jsx"; +import { make_btrfs_subvolume_pages } from "../btrfs/subvolume.jsx"; import { new_page } from "../pages.jsx"; diff --git a/pkg/storaged/btrfs/subvolume.jsx b/pkg/storaged/btrfs/subvolume.jsx index b4de220a2e43..b64d43f45b54 100644 --- a/pkg/storaged/btrfs/subvolume.jsx +++ b/pkg/storaged/btrfs/subvolume.jsx @@ -23,13 +23,16 @@ import React from "react"; import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js"; import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; -import { StorageCard, StorageDescription, new_card, new_page, navigate_away_from_card } from "../pages.jsx"; +import { + StorageCard, StorageDescription, ChildrenTable, new_card, new_page, navigate_away_from_card +} from "../pages.jsx"; import { StorageUsageBar } from "../storage-controls.jsx"; import { - encode_filename, get_fstab_config_with_client, reload_systemd, extract_option, parse_options, + encode_filename, decode_filename, + get_fstab_config_with_client, reload_systemd, extract_option, parse_options, flatten, teardown_active_usage, } from "../utils.js"; -import { btrfs_usage, validate_subvolume_name } from "./utils.jsx"; +import { btrfs_usage, validate_subvolume_name, parse_subvol_from_options } from "./utils.jsx"; import { at_boot_input, mounting_dialog, mount_options } from "../filesystem/mounting-dialog.jsx"; import { dialog_open, TextInput, @@ -242,7 +245,70 @@ function subvolume_delete(volume, subvol, mount_point_in_parent, card) { }); } -export function make_btrfs_subvolume_page(parent, volume, subvol) { +function dirname(path) { + const i = path.lastIndexOf("/"); + if (i < 0) + return null; + else + return path.substr(0, i); +} + +export function make_btrfs_subvolume_pages(parent, volume) { + let subvols = client.uuids_btrfs_subvols[volume.data.uuid]; + if (!subvols) { + const block = client.blocks[volume.path]; + /* + * Try to show subvolumes based on fstab entries, this is a bit tricky + * as mounts where subvolid cannot be shown userfriendly. + * + * The real subvolume data structure has "id" fields and + * "parent" fields that refer to the ids to form a tree. We + * want to do the same here, and we give fake ids to our fake + * subvolumes for this reason. We don't store these fake ids + * in the "id" field since we don't want them to be taken + * seriously by the rest of the code. + */ + let fake_id = 5; + subvols = [{ pathname: "/", id: 5, fake_id: fake_id++ }]; + const subvols_by_pathname = { }; + for (const config of block.Configuration) { + if (config[0] == "fstab") { + const opts = config[1].opts; + if (!opts) + continue; + + const fstab_subvol = parse_subvol_from_options(decode_filename(opts.v)); + + if (fstab_subvol && fstab_subvol.pathname && fstab_subvol.pathname !== "/") { + fstab_subvol.fake_id = fake_id++; + subvols_by_pathname[fstab_subvol.pathname] = fstab_subvol; + subvols.push(fstab_subvol); + } + } + } + + // Find parents + for (const pn in subvols_by_pathname) { + let dn = pn; + while (true) { + dn = dirname(dn); + if (!dn) { + subvols_by_pathname[pn].parent = 5; + break; + } else if (subvols_by_pathname[dn]) { + subvols_by_pathname[pn].parent = subvols_by_pathname[dn].fake_id; + break; + } + } + } + } + + const root = subvols.find(s => s.id == 5); + if (root) + make_btrfs_subvolume_page(parent, volume, root, "", subvols); +} + +function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) { const actions = []; const use = btrfs_usage(client, volume); @@ -307,11 +373,18 @@ export function make_btrfs_subvolume_page(parent, volume, subvol) { action: () => subvolume_delete(volume, subvol, mount_point_in_parent, card), }); + function strip_prefix(str, prefix) { + if (str.startsWith(prefix)) + return str.slice(prefix.length); + else + return str; + } + const card = new_card({ title: _("btrfs subvolume"), next: null, page_location: ["btrfs", volume.data.uuid, subvol.pathname], - page_name: subvol.pathname, + page_name: strip_prefix(subvol.pathname, path_prefix), page_size: is_mounted && , location: mp_text, component: BtrfsSubvolumeCard, @@ -319,7 +392,12 @@ export function make_btrfs_subvolume_page(parent, volume, subvol) { props: { subvol, mount_point, mismount_warning, block, fstab_config, forced_options }, actions, }); - new_page(parent, card); + const page = new_page(parent, card); + for (const sv of subvols) { + if (sv.parent && (sv.parent === subvol.id || sv.parent === subvol.fake_id)) { + make_btrfs_subvolume_page(page, volume, sv, subvol.pathname + "/", subvols); + } + } } const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_config, forced_options }) => { @@ -339,5 +417,10 @@ const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_confi + + + ); }; diff --git a/pkg/storaged/btrfs/volume.jsx b/pkg/storaged/btrfs/volume.jsx index 9d533f159bb0..9fecdb699b24 100644 --- a/pkg/storaged/btrfs/volume.jsx +++ b/pkg/storaged/btrfs/volume.jsx @@ -30,10 +30,10 @@ import { get_crossrefs, ChildrenTable, PageTable, StorageCard, StorageDescription } from "../pages.jsx"; import { StorageUsageBar, StorageLink } from "../storage-controls.jsx"; -import { fmt_size_long, validate_fsys_label, decode_filename, should_ignore } from "../utils.js"; -import { btrfs_usage, btrfs_is_volume_mounted, parse_subvol_from_options } from "./utils.jsx"; +import { fmt_size_long, validate_fsys_label, should_ignore } from "../utils.js"; +import { btrfs_usage, btrfs_is_volume_mounted } from "./utils.jsx"; import { dialog_open, TextInput } from "../dialog.jsx"; -import { make_btrfs_subvolume_page } from "./subvolume.jsx"; +import { make_btrfs_subvolume_pages } from "./subvolume.jsx"; import { btrfs_device_actions } from "./device.jsx"; const _ = cockpit.gettext; @@ -161,42 +161,3 @@ const BtrfsSubVolumesCard = ({ card }) => { ); }; - -export function make_btrfs_subvolume_pages(parent, volume) { - const subvols = client.uuids_btrfs_subvols[volume.data.uuid]; - if (subvols) { - for (const subvol of subvols) { - make_btrfs_subvolume_page(parent, volume, subvol); - } - } else { - const block = client.blocks[volume.path]; - /* - * Try to show subvolumes based on fstab entries, this is a bit tricky - * as mounts where subvolid cannot be shown userfriendly. - */ - let has_root = false; - for (const config of block.Configuration) { - if (config[0] == "fstab") { - const opts = config[1].opts; - if (!opts) - continue; - - const fstab_subvol = parse_subvol_from_options(decode_filename(opts.v)); - - if (fstab_subvol === null) - continue; - - if (fstab_subvol.pathname === "/") - has_root = true; - - if (fstab_subvol.pathname) - make_btrfs_subvolume_page(parent, volume, fstab_subvol); - } - } - - if (!has_root) { - // Always show the root subvolume even when the volume is not mounted. - make_btrfs_subvolume_page(parent, volume, { pathname: "/", id: 5 }); - } - } -} diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js index 67693161cce3..5457a35ef49e 100644 --- a/pkg/storaged/client.js +++ b/pkg/storaged/client.js @@ -259,7 +259,7 @@ export async function btrfs_poll() { // ID 257 gen 7 parent 256 top level 256 path one/two // ID 258 gen 7 parent 257 top level 257 path /one/two/three/four const output = await cockpit.spawn(["btrfs", "subvolume", "list", "-ap", mount_point], { superuser: true, err: "message" }); - const subvols = [{ pathname: "/", id: 5 }]; + const subvols = [{ pathname: "/", id: 5, parent: null }]; for (const line of output.split("\n")) { const m = line.match(/ID (\d+).*parent (\d+).*path (\/)?(.*)/); if (m) diff --git a/test/reference b/test/reference index 7f14132d3bd1..55fe3409efe8 160000 --- a/test/reference +++ b/test/reference @@ -1 +1 @@ -Subproject commit 7f14132d3bd19af16fb112a97a0d897e4794be56 +Subproject commit 55fe3409efe803b8cbc8a77309ee4131e4f06ca0 diff --git a/test/verify/check-storage-basic b/test/verify/check-storage-basic index 7208216c4af2..16f5a2ae1382 100755 --- a/test/verify/check-storage-basic +++ b/test/verify/check-storage-basic @@ -77,7 +77,7 @@ class TestStorageBasic(storagelib.StorageCase): long = "really-" * 15 + "long-name-that-will-be-truncated" m.execute(f"btrfs subvol create /{long}") self.addCleanup(m.execute, f"btrfs subvol delete /{long}") - b.wait_visible(self.card_row("Storage", name=f"root/{long}")) + b.wait_visible(self.card_row("Storage", name=long)) b.assert_pixels(self.card("Storage"), "overview", # Usage numbers are not stable and also cause # the table columns to shift. The usage bars diff --git a/test/verify/check-storage-btrfs b/test/verify/check-storage-btrfs index 992013eba336..c5ca64b94f1d 100755 --- a/test/verify/check-storage-btrfs +++ b/test/verify/check-storage-btrfs @@ -195,9 +195,9 @@ class TestStorageBtrfs(storagelib.StorageCase): # Finding the correct subvolume parent from a non-mounted subvolume m.execute(f"btrfs subvolume create {subvol_mount_point}/pizza") - self.click_dropdown(self.card_row("Storage", name=f"{subvol_mount}/pizza"), "Create subvolume") + self.click_dropdown(self.card_row("Storage", name="pizza"), "Create subvolume") self.dialog({"name": "pineapple"}, secondary=True) - b.wait_in_text(self.card_row("Storage", name=f"{subvol_mount}/pizza/pineapple"), "btrfs subvolume") + b.wait_in_text(self.card_row("Storage", name="pineapple"), "btrfs subvolume") left_subvol_mount = "/run/left" right_subvol_mount = "/run/right" @@ -214,11 +214,11 @@ class TestStorageBtrfs(storagelib.StorageCase): self.click_dropdown(self.card_row("Storage", location=left_subvol_mount), "Create subvolume") self.dialog({"name": "links"}, secondary=True) - b.wait_in_text(self.card_row("Storage", name="left/links"), "btrfs subvolume") + b.wait_in_text(self.card_row("Storage", name="links"), "btrfs subvolume") self.click_dropdown(self.card_row("Storage", location=right_subvol_mount), "Create subvolume") self.dialog({"name": "rechts"}, secondary=True) - b.wait_in_text(self.card_row("Storage", name="right/rechts"), "btrfs subvolume") + b.wait_in_text(self.card_row("Storage", name="rechts"), "btrfs subvolume") # Read only mount, cannot create subvolumes once /run/butter # is unmounted. @@ -232,7 +232,7 @@ class TestStorageBtrfs(storagelib.StorageCase): self.click_dropdown(self.card_row("Storage", location=ro_subvol), "Create subvolume") self.dialog({"name": "bot"}, secondary=True) - b.wait_visible(self.card_row("Storage", name="ro/bot")) + b.wait_visible(self.card_row("Storage", name="bot")) # But once /run/butter has been unmounted, we can't create # subvolumes of "ro" anymore. @@ -252,8 +252,7 @@ class TestStorageBtrfs(storagelib.StorageCase): mount -o remount,ro /dev/sda {ro_subvol} """) - subvol_loc = f"{os.path.basename(ro_subvol)}/readonly" - self.check_dropdown_action_disabled(self.card_row("Storage", name=subvol_loc), "Create subvolume", "Subvolume needs to be mounted") + self.check_dropdown_action_disabled(self.card_row("Storage", name="readonly"), "Create subvolume", "Subvolume needs to be mounted") def testDeleteSubvolume(self): m = self.machine @@ -301,16 +300,16 @@ class TestStorageBtrfs(storagelib.StorageCase): self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume") self.dialog({"name": child_subvol}, secondary=True) - b.wait_visible(self.card_row("Storage", name=f"{subvol}/{child_subvol}")) + b.wait_visible(self.card_row("Storage", name=child_subvol)) self.click_dropdown(self.card_row("Storage", name=subvol), "Delete") - self.checkTeardownAction(1, "Device", f"{subvol}/{child_subvol}") + self.checkTeardownAction(1, "Device", child_subvol) self.checkTeardownAction(1, "Action", "delete") self.checkTeardownAction(2, "Device", subvol) self.checkTeardownAction(2, "Action", "delete") self.confirm() - b.wait_not_present(self.card_row("Storage", name=f"{subvol}/{child_subvol}")) + b.wait_not_present(self.card_row("Storage", name=child_subvol)) b.wait_not_present(self.card_row("Storage", name=subvol)) # Delete with subvolume with children and self mounted @@ -322,10 +321,10 @@ class TestStorageBtrfs(storagelib.StorageCase): self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume") self.dialog({"name": child_subvol}, secondary=True) - b.wait_visible(self.card_row("Storage", name=f"{subvol}/{child_subvol}")) + b.wait_visible(self.card_row("Storage", name=child_subvol)) self.click_dropdown(self.card_row("Storage", name=subvol), "Delete") - self.checkTeardownAction(1, "Device", f"{subvol}/{child_subvol}") + self.checkTeardownAction(1, "Device", child_subvol) self.checkTeardownAction(1, "Action", "delete") self.checkTeardownAction(1, "Device", subvol) self.checkTeardownAction(2, "Location", subvol_mount_point) @@ -375,10 +374,10 @@ class TestStorageBtrfs(storagelib.StorageCase): self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume") self.dialog({"name": child_subvol}, secondary=True) - b.wait_visible(self.card_row("Storage", name=f"{subvol}/{child_subvol}")) + b.wait_visible(self.card_row("Storage", name=child_subvol)) # Allowed as root is mounted - self.click_dropdown(self.card_row("Storage", name=f"{subvol}/{child_subvol}"), "Delete") + self.click_dropdown(self.card_row("Storage", name=child_subvol), "Delete") self.dialog_wait_open() self.dialog_cancel() @@ -387,7 +386,7 @@ class TestStorageBtrfs(storagelib.StorageCase): self.confirm() b.wait_visible(self.card_row("Storage", location=f"{mount_point} (not mounted)")) - self.check_dropdown_action_disabled(self.card_row("Storage", name=f"{subvol}/{child_subvol}"), "Delete", "At least one parent needs to be mounted writable") + self.check_dropdown_action_disabled(self.card_row("Storage", name=child_subvol), "Delete", "At least one parent needs to be mounted writable") def testMultiDevice(self): m = self.machine @@ -575,7 +574,7 @@ class TestStorageBtrfs(storagelib.StorageCase): disk = self.add_ram_disk(size=128) - m.execute(f"mkfs.btrfs -L butter {disk}; mount {disk} /mnt; btrfs subvolume create /mnt/home; btrfs subvolume create /mnt/backups") + m.execute(f"mkfs.btrfs -L butter {disk}; mount {disk} /mnt; btrfs subvolume create /mnt/home; btrfs subvolume create /mnt/home/backups") m.execute("while mountpoint -q /mnt && ! umount /mnt; do sleep 0.2; done;") self.login_and_go("/storage") @@ -588,7 +587,7 @@ class TestStorageBtrfs(storagelib.StorageCase): # subvolumes mentioned in them and show them. m.execute(f"echo >>/etc/fstab '{disk} /mnt/home auto noauto,subvol=home 0 0'") - m.execute(f"echo >>/etc/fstab '{disk} /mnt/backups auto noauto,subvol=backups 0 0'") + m.execute(f"echo >>/etc/fstab '{disk} /mnt/backups auto noauto,subvol=home/backups 0 0'") b.wait_text(self.card_row_col("btrfs filesystem", row_name="home", col_index=3), "/mnt/home (not mounted)") b.wait_text(self.card_row_col("btrfs filesystem", row_name="backups", col_index=3), "/mnt/backups (not mounted)") diff --git a/test/verify/check-storage-scaling b/test/verify/check-storage-scaling index 9b4043f3deb1..9f080b62eea2 100755 --- a/test/verify/check-storage-scaling +++ b/test/verify/check-storage-scaling @@ -35,7 +35,7 @@ class TestStorageScaling(storagelib.StorageCase): b.wait_visible(self.card_row("Storage", name="/dev/vda")) # Wait on btrfs subvolumes on OS'es with the install on btrfs if m.image.startswith('fedora'): - b.wait_in_text(self.card_row("Storage", name="root/var/lib/machines"), "btrfs subvolume") + b.wait_in_text(self.card_row("Storage", name="var/lib/machines"), "btrfs subvolume") elif m.image == "arch": b.wait_in_text(self.card_row("Storage", name="swap"), "btrfs subvolume") n_rows = count_rows()