diff --git a/pkg/storaged/btrfs/subvolume.jsx b/pkg/storaged/btrfs/subvolume.jsx
index b11f3455af2..affb6c8840a 100644
--- a/pkg/storaged/btrfs/subvolume.jsx
+++ b/pkg/storaged/btrfs/subvolume.jsx
@@ -20,11 +20,13 @@
import cockpit from "cockpit";
import React from "react";
-import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+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";
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 {
@@ -383,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,
@@ -392,9 +404,17 @@ 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,
});
+
+ 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)) {
@@ -403,7 +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 &&
+
+
+
+ }
+ {crossrefs &&
+
+
+ {_("Snapshots")}
+
+
+
+
+
+ }
);
};
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) {
diff --git a/test/verify/check-storage-btrfs b/test/verify/check-storage-btrfs
index 247288c26fe..f901c8b9091 100755
--- a/test/verify/check-storage-btrfs
+++ b/test/verify/check-storage-btrfs
@@ -592,6 +592,48 @@ 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"))
+
+ # Snapshot details
+ self.click_card_row("Snapshots", name="snap-1")
+ b.wait_text(self.card_desc("btrfs subvolume", "Name"), "snapshots/snap-1")
+ b.wait_text(self.card_desc("btrfs subvolume", "Snapshot origin"), "subdir")
+
+ # Origin link works
+ b.click(self.card_button("btrfs subvolume", "subdir"))
+ b.wait_text(self.card_desc("btrfs subvolume", "Name"), "subdir")
+
if __name__ == '__main__':
testlib.test_main()