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()