Skip to content

Commit

Permalink
storage: Show btrfs subvolumes in a tree
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Mar 4, 2024
1 parent 8e9ffe2 commit d40b585
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 70 deletions.
2 changes: 1 addition & 1 deletion pkg/storaged/block/create-pages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
95 changes: 89 additions & 6 deletions pkg/storaged/btrfs/subvolume.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -307,19 +373,31 @@ 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 && <StorageUsageBar stats={use} short />,
location: mp_text,
component: BtrfsSubvolumeCard,
has_warning: !!mismount_warning,
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 }) => {
Expand All @@ -339,5 +417,10 @@ const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_confi
</StorageDescription>
</DescriptionList>
</CardBody>
<CardBody className="contains-list">
<ChildrenTable emptyCaption={_("No subvolumes")}
aria-label={_("btrfs subvolumes")}
page={card.page} />
</CardBody>
</StorageCard>);
};
45 changes: 3 additions & 42 deletions pkg/storaged/btrfs/volume.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -161,42 +161,3 @@ const BtrfsSubVolumesCard = ({ card }) => {
</StorageCard>
);
};

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 });
}
}
}
2 changes: 1 addition & 1 deletion pkg/storaged/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FS_TREE>/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 (<FS_TREE>\/)?(.*)/);
if (m)
Expand Down
2 changes: 1 addition & 1 deletion test/verify/check-storage-basic
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 16 additions & 17 deletions test/verify/check-storage-btrfs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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)")
Expand Down
2 changes: 1 addition & 1 deletion test/verify/check-storage-scaling
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit d40b585

Please sign in to comment.