Skip to content

Commit

Permalink
storaged: add btrfs volume integration
Browse files Browse the repository at this point in the history
Btrfs is a filesystem which can span over multiple devices a bit
similar to LVM but not 100%. On a standard Fedora server image we have
one partition which holds a btrfs filesystem with two subvolumes for /
and /home. In Cockpit we represent this as:

-> btrfs subvolume
    -> btrfs volume (the filesystem)
        -> btrfs device (backing storage)
	   -> block device

A btrfs volume can have one or more devices and supports multiple raid
configurations. In Cockpit we show a btrfs volume with the devices it is
uses a backing and the space usage.

In UDisks a "volume" does not exist but only an
org.freedesktop.UDisks2.Filesystem.BTRFS object which per btrfs device,
setting the label or retrieving the subvolumes on one of these objects
whcih are part of the same volume affects the whole group.

This initial commit lacks support for Btrfs subvolumes.

The UDisks API lacks a few data points which would be useful for in
Cockpit such as the space usage per btrfs device, showing the health
status of a btrfs filesystem when one device is missing, balancing of
btrfs devices. The space use per btrfs device has been implemented using
btrfs-progs in Cockpit, health warnings are not yet shown in Cockpit as
it for now purely focuses on a single btrfs device volume.
  • Loading branch information
jelly committed Jan 15, 2024
1 parent 13c1990 commit 1135fcb
Show file tree
Hide file tree
Showing 18 changed files with 1,302 additions and 86 deletions.
11 changes: 9 additions & 2 deletions pkg/storaged/block/create-pages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { make_mdraid_disk_card } from "../mdraid/mdraid-disk.jsx";
import { make_stratis_blockdev_card } from "../stratis/blockdev.jsx";
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 { new_page } from "../pages.jsx";

Expand All @@ -52,6 +53,9 @@ export function make_block_page(parent, block, card) {
(block_stratis_blockdev && client.stratis_pools[block_stratis_blockdev.Pool]) ||
block_stratis_stopped_pool);

const is_btrfs = (fstab_config.length > 0 &&
(fstab_config[2].indexOf("subvol=") >= 0 || fstab_config[2].indexOf("subvolid=") >= 0));

if (client.blocks_ptable[block.path]) {
make_partition_table_page(parent, block, card);
return;
Expand All @@ -76,7 +80,7 @@ export function make_block_page(parent, block, card) {
// can not happen unless there is a bug in the code above.
console.error("Assertion failure: is_crypto == false");
}
if (fstab_config.length > 0) {
if (fstab_config.length > 0 && !is_btrfs) {
card = make_filesystem_card(card, block, null, fstab_config);
} else {
card = make_locked_encrypted_data_card(card, block);
Expand All @@ -85,8 +89,11 @@ export function make_block_page(parent, block, card) {
const is_filesystem = content_block.IdUsage == 'filesystem';
const block_pvol = client.blocks_pvol[content_block.path];
const block_swap = client.blocks_swap[content_block.path];
const block_btrfs_blockdev = client.blocks_fsys_btrfs[content_block.path];

if (is_filesystem) {
if (block_btrfs_blockdev) {
card = make_btrfs_device_card(card, block, content_block, block_btrfs_blockdev);
} else if (is_filesystem) {
card = make_filesystem_card(card, block, content_block, fstab_config);
} else if ((content_block.IdUsage == "raid" && content_block.IdType == "LVM2_member") ||
(block_pvol && client.vgroups[block_pvol.VolumeGroup])) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/storaged/block/format-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended,
if (old_dir === false)
return Promise.reject(_("This device can not be used for the installation target."));

const split_options = parse_options(old_opts);
// Strip out btrfs subvolume mount options
const split_options = parse_options(old_opts).filter(opt => !(opt.startsWith('subvol=') || opt.startsWith('subvolid=')));
extract_option(split_options, "noauto");
const opt_ro = extract_option(split_options, "ro");
const opt_never_auto = extract_option(split_options, "x-cockpit-never-auto");
Expand Down
90 changes: 90 additions & 0 deletions pkg/storaged/btrfs/device.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2023 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* 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 React from "react";
import client from "../client";

import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
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, register_crossref } from "../pages.jsx";
import { StorageUsageBar } from "../storage-controls.jsx";
import { std_lock_action } from "../crypto/actions.jsx";
import { format_dialog } from "../block/format-dialog.jsx";
import { btrfs_device_usage } from "./utils.jsx";

const _ = cockpit.gettext;

export function make_btrfs_device_card(next, backing_block, content_block, block_btrfs) {
const label = block_btrfs && block_btrfs.data.label;
const uuid = block_btrfs && block_btrfs.data.uuid;
const use = btrfs_device_usage(client, uuid, block_btrfs.path);

const btrfs_card = new_card({
title: _("btrfs device"),
location: label || uuid,
next,
component: BtrfsDeviceCard,
props: { backing_block, content_block },
actions: [
std_lock_action(backing_block, content_block),
{ title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
],
});

register_crossref({
key: uuid,
card: btrfs_card,
size: <StorageUsageBar stats={use} short />,
});

return btrfs_card;
}

export const BtrfsDeviceCard = ({ card, backing_block, content_block }) => {
const block_btrfs = client.blocks_fsys_btrfs[content_block.path];
const uuid = block_btrfs && block_btrfs.data.uuid;
const label = block_btrfs && block_btrfs.data.label;
const use = btrfs_device_usage(client, uuid, block_btrfs.path);

return (
<StorageCard card={card}>
<CardBody>
<DescriptionList className="pf-m-horizontal-on-sm">
<StorageDescription title={_("btrfs volume")}>
{uuid
? <Button variant="link" isInline role="link"
onClick={() => cockpit.location.go(["btrfs-volume", uuid])}>
{label || uuid}
</Button>
: "-"
}
</StorageDescription>
<StorageDescription title={_("UUID")} value={content_block.IdUUID} />
{ block_btrfs &&
<StorageDescription title={_("Usage")}>
<StorageUsageBar key="s" stats={use} />
</StorageDescription>
}
</DescriptionList>
</CardBody>
</StorageCard>);
};
106 changes: 106 additions & 0 deletions pkg/storaged/btrfs/subvolume.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2023 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* 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 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 } 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 { check_mismounted_fsys, MismountAlert } from "../filesystem/mismounting.jsx";
import { is_mounted, mount_point_text, MountPoint } from "../filesystem/utils.jsx";
import client from "../client.js";

const _ = cockpit.gettext;

function subvolume_unmount(volume, subvol, forced_options) {
const block = client.blocks[volume.path];
mounting_dialog(client, block, "unmount", forced_options, subvol);
}

function subvolume_mount(volume, subvol, forced_options) {
const block = client.blocks[volume.path];
mounting_dialog(client, block, "mount", forced_options, subvol);
}

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

const use = btrfs_usage(client, volume);
const block = client.blocks[volume.path];
const fstab_config = get_fstab_config_with_client(client, block, false, subvol);
const [, mount_point] = fstab_config;
const mismount_warning = check_mismounted_fsys(block, block, fstab_config, subvol);
const mounted = is_mounted(client, block, subvol);
const mp_text = mount_point_text(mount_point, mounted);
if (mp_text == null)
return null;
const forced_options = [`subvol=${subvol.pathname}`];

if (mounted) {
actions.push({
title: _("Unmount"),
action: () => subvolume_unmount(volume, subvol, forced_options),
});
} else {
actions.push({
title: _("Mount"),
action: () => subvolume_mount(volume, subvol, forced_options),
});
}

const card = new_card({
title: _("btrfs subvolume"),
next: null,
page_location: ["btrfs", volume.data.uuid, subvol.pathname],
page_name: subvol.pathname,
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 BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_config, forced_options }) => {
return (
<StorageCard card={card} alert={mismount_warning &&
<MismountAlert warning={mismount_warning}
fstab_config={fstab_config}
backing_block={block} content_block={block} subvol={subvol} />}>
<CardBody>
<DescriptionList className="pf-m-horizontal-on-sm">
<StorageDescription title={_("Name")} value={subvol.pathname} />
<StorageDescription title={_("ID")} value={subvol.id} />
<StorageDescription title={_("Mount point")}>
<MountPoint fstab_config={fstab_config}
backing_block={block} content_block={block}
forced_options={forced_options} subvol={subvol} />
</StorageDescription>
</DescriptionList>
</CardBody>
</StorageCard>);
};
77 changes: 77 additions & 0 deletions pkg/storaged/btrfs/utils.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2023 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* 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 { decode_filename } from "../utils.js";

/*
* 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
* per block device.
*/
export function btrfs_device_usage(client, uuid, path) {
const block = client.blocks[path];
const device = block && block.Device;
const uuid_usage = client.uuids_btrfs_usage[uuid];
if (uuid_usage && device) {
const usage = uuid_usage[decode_filename(device)];
if (usage) {
return [usage, block.Size];
}
}
return [0, block.Size];
}

/**
* Calculate the overal btrfs "volume" usage. UDisks only knows the usage per block.
*/
export function btrfs_usage(client, volume) {
const block_fsys = client.blocks_fsys[volume.path];
const mount_point = block_fsys && block_fsys.MountPoints[0];
let use = mount_point && client.fsys_sizes.data[decode_filename(mount_point)];
if (!use)
use = [volume.data.used, client.uuids_btrfs_blocks[volume.data.uuid].reduce((sum, b) => sum + b.Size, 0)];
return use;
}

/**
* Is the btrfs volume mounted anywhere
*/
export function btrfs_is_volume_mounted(client, block_devices) {
for (const block_device of block_devices) {
const block_fs = client.blocks_fsys[block_device.path];
if (block_fs && block_fs.MountPoints.length > 0) {
return true;
}
}
return false;
}

export function parse_subvol_from_options(options) {
const subvol = { };
const subvolid_match = options.match(/subvolid=(?<subvolid>\d+)/);
const subvol_match = options.match(/subvol=(?<subvol>[\w\\/]+)/);
if (subvolid_match)
subvol.id = subvolid_match.groups.subvolid;
if (subvol_match)
subvol.pathname = subvol_match.groups.subvol;

if (subvolid_match || subvol_match)
return subvol;
else
return null;
}
Loading

0 comments on commit 1135fcb

Please sign in to comment.