Skip to content

Commit

Permalink
storaged: btrfs: add subvolume creation support
Browse files Browse the repository at this point in the history
Allow a user to create a new subvolume if the parent subvolume is
mounted or any of it's parent subvolumes is mounted.

We don't use btrfs's CreateSubvolume as it uses the first mount point
from the block device, so with multiple subvol's mounted it is not
possible to predict / influence on what mount point it will be created.
  • Loading branch information
jelly committed Jan 17, 2024
1 parent 00d677c commit 2dfecb5
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 5 deletions.
123 changes: 118 additions & 5 deletions pkg/storaged/btrfs/subvolume.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ import { DescriptionList } from "@patternfly/react-core/dist/esm/components/Desc

import { StorageCard, StorageDescription, new_card, new_page } from "../pages.jsx";
import { StorageUsageBar } from "../storage-controls.jsx";
import { get_fstab_config_with_client } from "../utils.js";
import { btrfs_usage } from "./utils.jsx";
import { mounting_dialog } from "../filesystem/mounting-dialog.jsx";
import { encode_filename, get_fstab_config_with_client, reload_systemd } from "../utils.js";
import { btrfs_usage, validate_subvolume_name } from "./utils.jsx";
import { at_boot_input, mounting_dialog, mount_options } from "../filesystem/mounting-dialog.jsx";
import {
dialog_open, TextInput,
} from "../dialog.jsx";
import { check_mismounted_fsys, MismountAlert } from "../filesystem/mismounting.jsx";
import { is_mounted, mount_point_text, MountPoint } from "../filesystem/utils.jsx";
import client from "../client.js";
import { is_mounted, is_valid_mount_point, mount_point_text, MountPoint } from "../filesystem/utils.jsx";
import client, { btrfs_poll } from "../client.js";

const _ = cockpit.gettext;

Expand All @@ -44,6 +47,107 @@ function subvolume_mount(volume, subvol, forced_options) {
mounting_dialog(client, block, "mount", forced_options, subvol);
}

function get_mount_point_in_parent(volume, subvol) {
const block = client.blocks[volume.path];
const subvols = client.uuids_btrfs_subvols[volume.data.uuid];
if (!subvols)
return null;

for (const p of subvols) {
if ((p.pathname == "/" || (subvol.pathname.substring(0, p.pathname.length) == p.pathname &&
subvol.pathname[p.pathname.length] == "/")) &&
is_mounted(client, block, p)) {
const [, pmp] = get_fstab_config_with_client(client, block, false, p);
if (p.pathname == "/")
return pmp + "/" + subvol.pathname;
else
return pmp + subvol.pathname.substring(p.pathname.length);
}
}
return null;
}

function set_mount_options(subvol, block, vals) {
const mount_options = [];

if (vals.mount_options.ro)
mount_options.push("ro");
if (vals.at_boot == "never")
mount_options.push("x-cockpit-never-auto");
if (vals.at_boot == "nofail")
mount_options.push("nofail");
if (vals.at_boot == "netdev")
mount_options.push("_netdev");
if (!vals.mount_options.auto || vals.mount_options.never_auto)
mount_options.push("noauto");

const name = (subvol.pathname == "/" ? vals.name : subvol.pathname + "/" + vals.name);
mount_options.push("subvol=" + name);
if (vals.mount_options.extra)
mount_options.push(vals.mount_options.extra);

let mount_point = vals.mount_point;
if (mount_point[0] != "/")
mount_point = "/" + mount_point;

const config =
["fstab",
{
dir: { t: 'ay', v: encode_filename(mount_point) },
type: { t: 'ay', v: encode_filename("btrfs") },
opts: { t: 'ay', v: encode_filename(mount_options.join(",") || "defaults") },
freq: { t: 'i', v: 0 },
passno: { t: 'i', v: 0 },
}
];

return block.AddConfigurationItem(config, {})
.then(reload_systemd)
.then(() => {
if (vals.mount_options.auto) {
return client.mount_at(block, mount_point);
} else
return Promise.resolve();
});
}

function subvolume_create(volume, subvol, forced_options, parent_dir) {
const block = client.blocks[volume.path];
dialog_open({
Title: _("Create subvolume"),
Fields: [
TextInput("name", _("Name"),
{
validate: name => validate_subvolume_name(name)
}),
TextInput("mount_point", _("Mount Point"),
{
validate: val => {
// Mount points are optional for subvolumes
if (val !== "")
return is_valid_mount_point(client, block, client.add_mount_point_prefix(val), false, true, subvol);
}
}),
mount_options(false, false),
at_boot_input("local"),
],
Action: {
Title: _("Create"),
action: function (vals) {
// HACK: cannot use block_btrfs.CreateSubvolume as it always creates a subvolume relative to MountPoints[0] which
// makes it impossible to handle a situation where we have multiple subvolumes mounted.
// https://github.com/storaged-project/udisks/issues/1242
cockpit.spawn(["btrfs", "subvolume", "create", `${parent_dir}/${vals.name}`], { superuser: "require", err: "message" }).then(() => {
btrfs_poll();
if (vals.mount_point !== "") {
return set_mount_options(subvol, block, vals);
}
});
}
}
});
}

export function make_btrfs_subvolume_page(parent, volume, subvol) {
const actions = [];

Expand All @@ -57,6 +161,7 @@ export function make_btrfs_subvolume_page(parent, volume, subvol) {
if (mp_text == null)
return null;
const forced_options = [`subvol=${subvol.pathname}`];
const mount_point_in_parent = get_mount_point_in_parent(volume, subvol);

if (mounted) {
actions.push({
Expand All @@ -70,6 +175,14 @@ export function make_btrfs_subvolume_page(parent, volume, subvol) {
});
}

if (mounted || mount_point_in_parent) {
const parent_dir = mounted ? mount_point : mount_point_in_parent;
actions.push({
title: _("Create subvolume"),
action: () => subvolume_create(volume, subvol, forced_options, parent_dir),
});
}

const card = new_card({
title: _("btrfs subvolume"),
next: null,
Expand Down
13 changes: 13 additions & 0 deletions pkg/storaged/btrfs/utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from "cockpit";

import { decode_filename } from "../utils.js";

const _ = cockpit.gettext;

/*
* Calculate the usage based on the data from `btrfs filesystem show` which has
* been made available to client.uuids_btrfs_usage. The size/usage is provided
Expand Down Expand Up @@ -75,3 +79,12 @@ export function parse_subvol_from_options(options) {
else
return null;
}

export function validate_subvolume_name(name) {
if (name === "")
return _("Name cannot be empty.");
if (name.length > 255)
return _("Name cannot be longer than 255 characters.");
if (name.includes('/'))
return cockpit.format(_("Name cannot contain the character '/'"));
}
53 changes: 53 additions & 0 deletions test/verify/check-storage-btrfs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,59 @@ class TestStorageBtrfs(storagelib.StorageCase):
self.dialog_cancel()
self.dialog_wait_close()

def testCreateSubvolume(self):
m = self.machine
b = self.browser

self.login_and_go("/storage")

disk1 = self.add_ram_disk(size=140)
label = "test_subvol"
mount_point = "/run/butter"
subvol = "cake"

m.execute(f"mkfs.btrfs -L {label} {disk1}")
self.login_and_go("/storage")

# creation of btrfs partition can take a while on TF.
with b.wait_timeout(30):
b.wait_visible(self.card_row("Storage", name=label))

root_sel = self.card_row("Storage", name=label) + " + tr"
b.wait_not_present(f"{root_sel}:contains('Create subvolume')")
self.click_dropdown(self.card_row("Storage", name=label) + " + tr", "Mount")
self.dialog({"mount_point": mount_point})
self.addCleanup(self.machine.execute, f"umount {mount_point} || true")
b.wait_in_text(self.card_row("Storage", location=mount_point), "btrfs subvolume")
self.click_dropdown(self.card_row("Storage", location=mount_point),
"Create subvolume")
# Without mount point
self.dialog_wait_open()
self.dialog({"name": subvol})
b.wait_visible(self.card_row("Storage", name=subvol))

subvol_mount = "quiche"
subvol_mount_point = "/run/oven"
self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")

# With mount point
self.dialog_wait_open()
self.dialog({"name": subvol_mount, "mount_point": subvol_mount_point})
b.wait_in_text(self.card_row("Storage", location=f"{subvol_mount_point} (not mounted)"), "btrfs subvolume")

self.addCleanup(self.machine.execute, f"umount {subvol_mount_point} || true")
self.click_dropdown(self.card_row("Storage", location=f"{subvol_mount_point} (not mounted)"), "Mount")
self.confirm()

b.wait_in_text(self.card_row("Storage", location=subvol_mount_point), "btrfs subvolume")

# 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.dialog_wait_open()
self.dialog({"name": "pineapple"})
b.wait_in_text(self.card_row("Storage", name=f"{subvol_mount}/pizza/pineapple"), "btrfs subvolume")

def testMultiDevice(self):
m = self.machine
b = self.browser
Expand Down

0 comments on commit 2dfecb5

Please sign in to comment.