🏡 index : quine-global-labs/mirror-cockpit-project-cockpit.git

/*
 * Copyright (C) 2023 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import cockpit from "cockpit";
import React from "react";
import client from "../client";

import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { VolumeIcon } from "../icons/gnome-icons.jsx";

import {
    new_card, new_page, PAGE_CATEGORY_VIRTUAL,
    get_crossrefs, ChildrenTable, PageTable, StorageCard, StorageDescription,
    navigate_away_from_card
} from "../pages.jsx";
import { StorageUsageBar, StorageLink } from "../storage-controls.jsx";
import {
    fmt_size_long, validate_fsys_label, should_ignore,
    get_active_usage, teardown_active_usage,
    reload_systemd,
} from "../utils.js";
import { btrfs_usage } from "./utils.jsx";
import {
    dialog_open, TextInput,
    BlockingMessage, TeardownMessage,
    init_teardown_usage
} from "../dialog.jsx";
import { make_btrfs_subvolume_pages } from "./subvolume.jsx";
import { btrfs_device_actions } from "./device.jsx";

const _ = cockpit.gettext;

/*
 * 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.
 *
 * Cockpit shows Btrfs as following:
 *
 * -> btrfs subvolume
 *    -> btrfs volume
 *        -> btrfs device
 *            -> block device
 */

async function btrfs_delete(uuid, card) {
    const volume = client.uuids_btrfs_volume[uuid];
    const name = volume.data.label || uuid;

    const usage = get_active_usage(client, volume.path, _("delete"), undefined, false, undefined, true);

    if (usage.Blocking) {
        dialog_open({
            Title: cockpit.format(_("$0 is in use"), name),
            Body: BlockingMessage(usage)
        });
        return;
    }

    dialog_open({
        Title: cockpit.format(_("Permanently delete $0?"), name),
        Teardown: TeardownMessage(usage),
        Action: {
            Danger: _("Deleting erases all data on a btrfs volume."),
            Title: _("Delete"),
            disable_on_error: usage.Teardown,
            action: async function () {
                await teardown_active_usage(client, usage);
                const blocks = client.uuids_btrfs_blocks[uuid];
                for (let i = 0; i < blocks.length; i++) {
                    // All block devices share the same Configuration
                    // property, so we only tear down the first.
                    await blocks[i].Format("empty", { 'tear-down': { t: 'b', v: i === 0 } });
                }
                await reload_systemd();
                navigate_away_from_card(card);
            }
        },
        Inits: [
            init_teardown_usage(client, usage)
        ]
    });
}

export function make_btrfs_volume_page(parent, uuid) {
    const block_devices = client.uuids_btrfs_blocks[uuid];
    const block_btrfs = client.blocks_fsys_btrfs[block_devices[0].path];
    const volume = client.uuids_btrfs_volume[uuid];
    const use = btrfs_usage(client, volume);

    if (block_devices.some(blk => should_ignore(client, blk.path)))
        return;

    // Single-device btrfs volumes are shown directly on the page of
    // their device; they don't get a standalone "btrfs volume" page.
    if (block_btrfs.data.num_devices === 1)
        return;

    const name = block_btrfs.data.label || uuid;
    const btrfs_volume_card = new_card({
        title: _("btrfs volume"),
        next: null,
        page_location: ["btrfs-volume", uuid],
        page_name: name,
        page_icon: VolumeIcon,
        page_category: PAGE_CATEGORY_VIRTUAL,
        page_size: use[1],
        component: BtrfsVolumeCard,
        props: { block_devices, uuid, use },
        actions: [
            {
                title: _("Delete filesystem"),
                action: () => btrfs_delete(uuid, btrfs_volume_card),
                danger: true,
            },
        ],
    });

    const subvolumes_card = make_btrfs_subvolumes_card(btrfs_volume_card, null, null);

    const subvolumes_page = new_page(parent, subvolumes_card);
    make_btrfs_subvolume_pages(subvolumes_page, volume);
}

function rename_dialog(block_btrfs, label, rw_mount_point) {
    dialog_open({
        Title: _("Change label"),
        Fields: [
            TextInput("name", _("Name"),
                      {
                          validate: name => validate_fsys_label(name, "btrfs"),
                          value: label
                      })
        ],
        Action: {
            Title: _("Save"),
            action: async function (vals) {
                if (rw_mount_point) {
                    await cockpit.spawn(["btrfs", "filesystem", "label", rw_mount_point, vals.name],
                                        { superuser: true });
                    const block = client.blocks[block_btrfs.path];
                    await block.Rescan({});
                } else
                    await block_btrfs.SetLabel(vals.name, {});
            }
        }
    });
}

export const BtrfsLabelDescription = ({ block_btrfs }) => {
    const label = block_btrfs.data.label || "-";

    // We can change the label when at least one filesystem subvolume
    // is mounted rw, or when nothing is mounted.

    let rw_mount_point = null;
    let is_mounted = false;
    const mount_points = client.btrfs_mounts[block_btrfs.data.uuid];
    for (const id in mount_points) {
        const mp = mount_points[id];
        if (mp.mount_points.length > 0)
            is_mounted = true;
        if (mp.rw_mount_points.length > 0 && !rw_mount_point)
            rw_mount_point = mp.rw_mount_points[0];
    }

    let excuse = null;
    if (is_mounted && !rw_mount_point)
        excuse = _("Filesystem is mounted read-only");

    return <StorageDescription title={_("Label")}
                               value={label}
                               action={
                                   <StorageLink onClick={() => rename_dialog(block_btrfs, label, rw_mount_point)}
                                                excuse={excuse}>
                                       {_("edit")}
                                   </StorageLink>}
    />;
};

const BtrfsVolumeCard = ({ card, block_devices, uuid, use }) => {
    const block_btrfs = client.blocks_fsys_btrfs[block_devices[0].path];

    return (
        <StorageCard card={card}>
            <CardBody>
                <DescriptionList className="pf-m-horizontal-on-sm">
                    <BtrfsLabelDescription block_btrfs={block_btrfs} />
                    <StorageDescription title={_("UUID")} value={uuid} />
                    <StorageDescription title={_("Capacity")} value={fmt_size_long(use[1])} />
                    <StorageDescription title={_("Usage")}>
                        <StorageUsageBar key="s" stats={use} />
                    </StorageDescription>
                </DescriptionList>
            </CardBody>
            <CardHeader><strong>{_("btrfs devices")}</strong></CardHeader>
            <PageTable
                emptyCaption={_("No devices found")}
                aria-label={_("btrfs device")}
                crossrefs={get_crossrefs(uuid)} />
        </StorageCard>
    );
};

export function make_btrfs_subvolumes_card(next, block, backing_block) {
    return new_card({
        title: _("btrfs subvolumes"),
        next,
        actions: btrfs_device_actions(block, backing_block),
        component: BtrfsSubVolumesCard,
    });
}

const BtrfsSubVolumesCard = ({ card }) => {
    return (
        <StorageCard card={card}>
            <ChildrenTable
                emptyCaption={_("No subvolumes")}
                aria-label={_("btrfs subvolumes")}
                page={card.page} />
        </StorageCard>
    );
};