From 602f75166da468b575ef65c1d9bbc948050410b3 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Thu, 28 Mar 2024 14:50:05 +0100 Subject: [PATCH 1/3] btrfs: obtain subvolume snapshot information In btrfs a snapshot is a subvolume with a parent_uuid field filled in with the uuid of the subvolume of which the snapshot is made. Note that btrfs allows one to make a snapshot of a snapshot. --- pkg/storaged/client.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js index d5d7af5e070..93d49d22c82 100644 --- a/pkg/storaged/client.js +++ b/pkg/storaged/client.js @@ -243,12 +243,18 @@ export async function btrfs_poll() { // ID 256 gen 7 parent 5 top level 5 path /one // 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: "require", err: "message" }); + const output = await cockpit.spawn(["btrfs", "subvolume", "list", "-apuq", mount_point], { superuser: "require", err: "message" }); 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) - subvols.push({ pathname: m[4], id: Number(m[1]), parent: Number(m[2]) }); + const m = line.match(/ID (\d+).*parent (\d+).*parent_uuid (.*)uuid (.*) path (\/)?(.*)/); + if (m) { + // The parent uuid is the uuid of which this subvolume is a snapshot. + // https://github.com/torvalds/linux/blob/8d025e2092e29bfd13e56c78e22af25fac83c8ec/include/uapi/linux/btrfs.h#L885 + let parent_uuid = m[3].trim(); + // BTRFS_UUID_SIZE is 16 + parent_uuid = parent_uuid.length < 16 ? null : parent_uuid; + subvols.push({ pathname: m[6], id: Number(m[1]), parent: Number(m[2]), uuid: m[4], parent_uuid }); + } } uuids_subvols[uuid] = subvols; } catch (err) { From 294071514e8207639200955ac00168ebf13f4c20 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Thu, 28 Mar 2024 16:46:47 +0100 Subject: [PATCH 2/3] btrfs: show snapshots of a subvolume in the subvolume details page Register crossrefs for snapshots so they show under snapshots in the detail page of a subvolume. --- pkg/storaged/btrfs/subvolume.jsx | 26 +++++++++++++++++++++++-- test/verify/check-storage-btrfs | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/pkg/storaged/btrfs/subvolume.jsx b/pkg/storaged/btrfs/subvolume.jsx index b11f3455af2..2dfdfa8bc82 100644 --- a/pkg/storaged/btrfs/subvolume.jsx +++ b/pkg/storaged/btrfs/subvolume.jsx @@ -20,11 +20,12 @@ import cockpit from "cockpit"; import React from "react"; -import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js"; +import { Card, CardBody, CardHeader, CardTitle } 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, ChildrenTable, new_card, new_page, navigate_away_from_card + PageTable, StorageCard, StorageDescription, ChildrenTable, + new_card, new_page, navigate_away_from_card, register_crossref, get_crossrefs, } from "../pages.jsx"; import { StorageUsageBar } from "../storage-controls.jsx"; import { @@ -395,6 +396,14 @@ function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) props: { subvol, mount_point, mismount_warning, block, fstab_config, forced_options }, actions, }); + + if (subvol.id !== 5 && subvol.parent_uuid !== null) + register_crossref({ + key: subvol.parent_uuid, + card, + size: mounted && , + }); + const page = new_page(parent, card); for (const sv of subvols) { if (sv.parent && (sv.parent === subvol.id || sv.parent === subvol.fake_id)) { @@ -404,6 +413,7 @@ function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) } const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_config, forced_options }) => { + const crossrefs = get_crossrefs(subvol.uuid); return ( + {crossrefs && + + + {_("Snapshots")} + + + + + + } ); }; diff --git a/test/verify/check-storage-btrfs b/test/verify/check-storage-btrfs index 247288c26fe..86c1768ea7b 100755 --- a/test/verify/check-storage-btrfs +++ b/test/verify/check-storage-btrfs @@ -592,6 +592,39 @@ class TestStorageBtrfs(storagelib.StorageCase): 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)") + def testSnapshot(self): + m = self.machine + b = self.browser + + disk = self.add_ram_disk(size=128) + mount_point = "/run/butter" + + snapshot_dir = f"{mount_point}/snapshots" + subdir = f"{mount_point}/subdir" + + m.execute(f""" + mkfs.btrfs -L butter {disk} + mkdir -p {mount_point} + mount {disk} {mount_point} + echo '{disk} {mount_point} auto defaults 0 0' >> /etc/fstab + # Debian-testing/ubuntu-2204 btrfs-progrs version does not support creating multiple subvolumes at once + btrfs subvolume create {subdir} + btrfs subvolume create {snapshot_dir} + btrfs subvolume create {subdir}/foo + btrfs subvolume snapshot {subdir} {snapshot_dir}/snap-1 + btrfs subvolume snapshot {mount_point} {snapshot_dir}/snap-2 + """) + + self.login_and_go("/storage") + + self.click_card_row("Storage", name=os.path.basename(subdir)) + b.wait_text(self.card_desc("btrfs subvolume", "Name"), "subdir") + b.wait_visible(self.card_row("Snapshots", name="snap-1")) + + # Normal subvolume does not show under snapshots + b.wait_visible(self.card_row("btrfs subvolume", name="foo")) + b.wait_not_present(self.card_row("Snapshots", name="foo")) + if __name__ == '__main__': testlib.test_main() From 70ae75e639a6b8c2c178e3fcac6f39620a764371 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Fri, 7 Jun 2024 12:34:31 +0200 Subject: [PATCH 3/3] btrfs: show the origin of the snapshot in subvolume details --- pkg/storaged/btrfs/subvolume.jsx | 24 ++++++++++++++++++++++-- test/verify/check-storage-btrfs | 9 +++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pkg/storaged/btrfs/subvolume.jsx b/pkg/storaged/btrfs/subvolume.jsx index 2dfdfa8bc82..affb6c8840a 100644 --- a/pkg/storaged/btrfs/subvolume.jsx +++ b/pkg/storaged/btrfs/subvolume.jsx @@ -20,6 +20,7 @@ import cockpit from "cockpit"; import React from "react"; +import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js"; import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; @@ -384,6 +385,16 @@ function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) return str; } + let snapshot_origin = null; + if (subvol.id !== 5 && subvol.parent_uuid !== null) { + for (const sv of subvols) { + if (sv.uuid === subvol.parent_uuid) { + snapshot_origin = sv; + break; + } + } + } + const card = new_card({ title: _("btrfs subvolume"), next: null, @@ -393,7 +404,7 @@ function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) location: mp_text, component: BtrfsSubvolumeCard, has_warning: !!mismount_warning, - props: { subvol, mount_point, mismount_warning, block, fstab_config, forced_options }, + props: { volume, subvol, snapshot_origin, mount_point, mismount_warning, block, fstab_config, forced_options }, actions, }); @@ -412,8 +423,9 @@ function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) } } -const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_config, forced_options }) => { +const BtrfsSubvolumeCard = ({ card, volume, subvol, snapshot_origin, mismount_warning, block, fstab_config, forced_options }) => { const crossrefs = get_crossrefs(subvol.uuid); + return ( + {snapshot_origin !== null && + + + + }