diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js index 6dd9f43ec9c9..68492a8859c2 100644 --- a/pkg/storaged/client.js +++ b/pkg/storaged/client.js @@ -176,6 +176,7 @@ function init_proxies () { client.blocks_swap = proxies("Swapspace"); client.iscsi_sessions = proxies("ISCSI.Session"); client.vdo_vols = proxies("VDOVolume"); + client.blocks_fsys_btrfs = proxies("Filesystem.BTRFS"); client.jobs = proxies("Job"); return client.storaged_client.watch({ path_namespace: "/org/freedesktop/UDisks2" }); @@ -526,6 +527,18 @@ function update_indices() { }; } + // UDisks API does provide a btrfs volume abstraction so we keep track of + // volume's by uuid in an object. uuid => [org.freedesktop.UDisks2.Filesystem.BTRFS] + client.uuids_btrfs_blocks = { }; + for (const p in client.blocks_fsys_btrfs) { + const bfs = client.blocks_fsys_btrfs[p]; + const uuid = bfs.data.uuid; + if (!client.uuids_btrfs_blocks[uuid]) + client.uuids_btrfs_blocks[uuid] = []; + client.uuids_btrfs_blocks[uuid].push(client.blocks[p]); + } + console.log("uuids_btrfs_blocks", client.uuids_btrfs_blocks); + client.blocks_cleartext = { }; for (path in client.blocks) { block = client.blocks[path]; @@ -612,11 +625,13 @@ function init_model(callback) { function() { client.manager_lvm2 = proxy("Manager.LVM2", "Manager"); client.manager_iscsi = proxy("Manager.ISCSI.Initiator", "Manager"); - return Promise.allSettled([client.manager_lvm2.wait(), client.manager_iscsi.wait()]) + client.manager_btrfs = proxy("Manager.BTRFS", "Manager"); + return Promise.allSettled([client.manager_lvm2.wait(), client.manager_iscsi.wait(), client.manager_btrfs.wait()]) .then(() => { client.features.lvm2 = client.manager_lvm2.valid; client.features.iscsi = (client.manager_iscsi.valid && client.manager_iscsi.SessionsSupported !== false); + client.features.btrfs = client.manager_btrfs.valid; }); }, function(error) { console.warn("Can't enable storaged modules", error.toString()); diff --git a/pkg/storaged/dialog.jsx b/pkg/storaged/dialog.jsx index ec97684d52be..6c4125a36654 100644 --- a/pkg/storaged/dialog.jsx +++ b/pkg/storaged/dialog.jsx @@ -1081,7 +1081,8 @@ export const BlockingMessage = (usage) => { pvol: _("physical volume of LVM2 volume group"), "mdraid-member": _("member of RAID device"), vdo: _("backing device for VDO device"), - "stratis-pool-member": _("member of Stratis pool") + "stratis-pool-member": _("member of Stratis pool"), + "btrfs-member": _("member of BTRFS volume") }; console.log("U", usage); diff --git a/pkg/storaged/format-dialog.jsx b/pkg/storaged/format-dialog.jsx index 2da510c65df7..c126fdb9023a 100644 --- a/pkg/storaged/format-dialog.jsx +++ b/pkg/storaged/format-dialog.jsx @@ -175,6 +175,7 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, add_fsys("ext4", { value: "ext4", title: "EXT4" }); add_fsys("vfat", { value: "vfat", title: "VFAT" }); add_fsys("ntfs", { value: "ntfs", title: "NTFS" }); + add_fsys("btrfs", { value: "btrfs", title: "BTRFS" }); add_fsys(true, { value: "empty", title: _("No filesystem") }); if (create_partition && enable_dos_extended) add_fsys(true, { value: "dos-extended", title: _("Extended partition") }); diff --git a/pkg/storaged/pages/btrfs.jsx b/pkg/storaged/pages/btrfs.jsx new file mode 100644 index 000000000000..61132d9bd7b1 --- /dev/null +++ b/pkg/storaged/pages/btrfs.jsx @@ -0,0 +1,143 @@ +/* + * 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 . + */ + +import cockpit from "cockpit"; +import React from "react"; +import client from "../client"; + +import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js"; +import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js"; +import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; + +import { SCard } from "../utils/card.jsx"; +import { SDesc } from "../utils/desc.jsx"; +import { PageChildrenCard, PageCrossrefCard, ActionButtons, new_page, page_type, get_crossrefs } from "../pages.jsx"; +import { fmt_size, fmt_size_long } from "../utils.js"; + +const _ = cockpit.gettext; + +function btrfs_usage(uuid) { + const block_devices = client.uuids_btrfs_blocks[uuid]; + console.log(block_devices); + + let size = 0; + for (const block_device of block_devices) { + size += client.blocks[block_device.path].Size; + } + const used = client.blocks_fsys_btrfs[block_devices[0].path].data.used; + + // 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) { + // const blocks = []; + // Object.keys(client.blocks_fsys_btrfs).forEach(obj_path => { + // const blk = client.blocks_fsys_btrfs[obj_path]; + // if (blk.data.uuid === volume.data.uuid) { + // blocks.push(client.blocks[obj_path]); + // } + // }); + // use = [volume.data.used, blocks.reduce((sum, b) => sum + b.Size, 0)]; + // } + // console.log(use); + return [used, size]; +} + +/* + * Udisks is a disk/block library so it manages that, btrfs turns this a bit + * around and has one "volume" which can have multiple blocks by a unique uuid. + */ +export function make_btrfs_volume_page(parent, uuid) { + const block_devices = client.uuids_btrfs_blocks[uuid]; + + const device = client.blocks_fsys_btrfs[block_devices[0].path]; + // TODO: label is optional, but do we want to show uuid then? + const name = device.data.label || uuid; + const total_capacity = btrfs_usage(uuid)[1]; + const btrfs_volume_page = new_page({ + location: ["btrfs-volume", name], + parent, + name, + columns: [ + _("Btrfs volume"), + "", + fmt_size(total_capacity), + ], + component: BtrfsVolumePage, + props: { block_devices, name: device.data.label, uuid: device.data.uuid, total_capacity }, + actions: [], + }); + + for (const blk of block_devices) { + const device = client.blocks_fsys_btrfs[blk.path]; + device.GetSubvolumes(false, {}).then(subvolumes => { + for (const subvolume of subvolumes) { + make_btrfs_volume_subvolume(btrfs_volume_page, uuid, subvolume); + } + }); + } +} + +function make_btrfs_volume_subvolume(parent, uuid, subvol) { + const [id, parent_id, path] = subvol; + new_page({ + location: ["btrfs-volume", uuid, id], + parent, + name: path, + columns: [ + _("Btrfs subvolume"), + id, + parent_id, + ], + component: BtrfsVolumePage, + props: { uuid, id, parent_id, path }, + actions: [], + }); +} + +const BtrfsVolumePage = ({ page, block_devices, name, uuid, total_capacity, subvolumes }) => { + let crossrefs = []; + for (const blk of block_devices) { + crossrefs = crossrefs.concat(get_crossrefs(blk)); + } + return ( + + + }> + + + + + + + + + + + + + + + + + ); +}; diff --git a/pkg/storaged/pages/drive.jsx b/pkg/storaged/pages/drive.jsx index 7066e8eda162..5bed10354aee 100644 --- a/pkg/storaged/pages/drive.jsx +++ b/pkg/storaged/pages/drive.jsx @@ -29,7 +29,7 @@ import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { SCard } from "../utils/card.jsx"; import { SDesc } from "../utils/desc.jsx"; import { StorageButton } from "../storage-controls.jsx"; -import { PageChildrenCard, ParentPageLink, new_page, page_type, block_location } from "../pages.jsx"; +import { PageChildrenCard, ParentPageLink, new_page, page_type, block_location, register_crossref } from "../pages.jsx"; import { block_name, drive_name, format_temperature, fmt_size, fmt_size_long } from "../utils.js"; import { format_disk } from "../content-views.jsx"; // XXX diff --git a/pkg/storaged/pages/overview.jsx b/pkg/storaged/pages/overview.jsx index 0ba6ab074eb0..d68139d92cd1 100644 --- a/pkg/storaged/pages/overview.jsx +++ b/pkg/storaged/pages/overview.jsx @@ -39,6 +39,7 @@ import { get_other_devices } from "../utils.js"; // XXX import { new_page, PageChildrenCard } from "../pages.jsx"; import { make_drive_page } from "./drive.jsx"; +import { make_btrfs_volume_page } from "./btrfs.jsx"; import { make_lvm2_volume_group_page } from "./lvm2-volume-group.jsx"; import { make_mdraid_page } from "./mdraid.jsx"; import { make_stratis_pool_page } from "./stratis-pool.jsx"; @@ -65,6 +66,15 @@ export function make_overview_page() { Object.keys(client.mdraids).forEach(p => make_mdraid_page(overview_page, client.mdraids[p])); Object.keys(client.stratis_pools).map(p => make_stratis_pool_page(overview_page, client.stratis_pools[p])); Object.keys(client.stratis_manager.StoppedPools).map(uuid => make_stratis_stopped_pool_page(overview_page, uuid)); + // TODO: this needs to poll? What does udisks do for us? + const btrfs_uuids = new Set(); + Object.keys(client.blocks_fsys_btrfs).forEach(p => { + const bfs = client.blocks_fsys_btrfs[p]; + btrfs_uuids.add(bfs.data.uuid); + }); + for (const uuid of btrfs_uuids) { + make_btrfs_volume_page(overview_page, uuid); + } client.nfs.entries.forEach(e => make_nfs_page(overview_page, e)); get_other_devices(client).map(p => make_other_page(overview_page, client.blocks[p])); }