🏡 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 { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
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 { StorageButton, StorageLink } from "../storage-controls.jsx";
import {
    StorageCard, StorageDescription, PageTable,
    new_page, new_card, PAGE_CATEGORY_VIRTUAL,
    get_crossrefs, navigate_away_from_card
} from "../pages.jsx";
import { make_block_page } from "../block/create-pages.jsx";
import {
    block_short_name, mdraid_name, encode_filename, decode_filename,
    fmt_size, fmt_size_long, get_active_usage, teardown_active_usage,
    get_available_spaces, prepare_available_spaces,
    reload_systemd, should_ignore,
} from "../utils.js";

import {
    dialog_open, SelectSpaces,
    BlockingMessage, TeardownMessage,
    init_teardown_usage
} from "../dialog.jsx";

import { partitionable_block_actions } from "../partitions/actions.jsx";

const _ = cockpit.gettext;

function mdraid_start(mdraid) {
    return mdraid.Start({ "start-degraded": { t: 'b', v: true } });
}

function mdraid_stop(mdraid) {
    const block = client.mdraids_block[mdraid.path];
    const usage = get_active_usage(client, block ? block.path : "", _("stop"));

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

    if (usage.Teardown) {
        dialog_open({
            Title: cockpit.format(_("Confirm stopping of $0"),
                                  mdraid_name(mdraid)),
            Teardown: TeardownMessage(usage),
            Action: {
                Title: _("Stop device"),
                action: function () {
                    return teardown_active_usage(client, usage)
                            .then(function () {
                                return mdraid.Stop({});
                            });
                }
            },
            Inits: [
                init_teardown_usage(client, usage)
            ]
        });
        return;
    }

    return mdraid.Stop({});
}

function mdraid_delete(mdraid, block, card) {
    async function delete_() {
        const members = client.mdraids_members[mdraid.path];

        await mdraid.Delete({ 'tear-down': { t: 'b', v: true } });

        // HACK - https://github.com/storaged-project/udisks/issues/1375
        //
        // Now wipe the members again.  This is necessary when
        // deleting a MDRAID with metadata 1.0, which stores its
        // superblock at the end of each member, and the superblock of
        // the actual content of the mdraid is stored at the very
        // start of each member (for certain raid levels like a
        // mirror).  The Delete call above will only remove the mdraid
        // metadata and leave the content superblock in place. Thus,
        // after deleting, the members might magically turn into the
        // previous content.  Wiping the members again will remove any
        // such metadata that might cause them to be misidentified.

        for (const b of members) {
            await b.Format('empty', { });
        }

        await reload_systemd();
    }

    const usage = get_active_usage(client, block ? block.path : "", _("delete"));

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

    dialog_open({
        Title: cockpit.format(_("Permanently delete $0?"), mdraid_name(mdraid)),
        Teardown: TeardownMessage(usage),
        Action: {
            Title: _("Delete"),
            Danger: _("Deleting erases all data on a MDRAID device."),
            action: async function () {
                await teardown_active_usage(client, usage);
                await delete_();
                navigate_away_from_card(card);
            }
        },
        Inits: [
            init_teardown_usage(client, usage)
        ]
    });
}

function add_disk(mdraid) {
    function filter_inside_mdraid(spc) {
        let block = spc.block;
        if (client.blocks_part[block.path])
            block = client.blocks[client.blocks_part[block.path].Table];
        return block && block.MDRaid !== mdraid.path;
    }

    function rescan(path) {
        // mdraid often forgets to trigger udev, let's do it explicitly
        return client.wait_for(() => client.blocks[path]).then(block => block.Rescan({ }));
    }

    dialog_open({
        Title: _("Add disks"),
        Fields: [
            SelectSpaces("disks", _("Disks"),
                         {
                             empty_warning: _("No disks are available."),
                             validate: function (disks) {
                                 if (disks.length === 0)
                                     return _("At least one disk is needed.");
                             },
                             spaces: get_available_spaces().filter(filter_inside_mdraid)
                         })
        ],
        Action: {
            Title: _("Add"),
            action: function(vals) {
                return prepare_available_spaces(client, vals.disks).then(paths =>
                    Promise.all(paths.map(p => mdraid.AddDevice(p, {}).then(() => rescan(p)))));
            }
        }
    });
}

function missing_bitmap(mdraid) {
    let policy;
    if (mdraid.ConsistencyPolicy)
        policy = mdraid.ConsistencyPolicy;
    else if (mdraid.ActiveDevices.some(a => a[2].indexOf("journal") >= 0))
        policy = "journal";
    else if (mdraid.BitmapLocation && decode_filename(mdraid.BitmapLocation) !== "none")
        policy = "bitmap";
    else
        policy = "resync";

    return (mdraid.Level !== "raid0" &&
            client.mdraids_members[mdraid.path].some(m => m.Size > 100 * 1024 * 1024 * 1024) &&
            policy === "resync");
}

export function make_mdraid_page(parent, mdraid) {
    const block = client.mdraids_block[mdraid.path];

    if (should_ignore(client, mdraid.path))
        return;

    let add_excuse = false;
    if (!block)
        add_excuse = _("MDRAID device must be running");

    const mdraid_card = new_card({
        title: _("MDRAID device"),
        next: null,
        page_location: ["mdraid", mdraid.UUID],
        page_name: block ? block_short_name(block) : mdraid_name(mdraid),
        page_icon: VolumeIcon,
        page_category: PAGE_CATEGORY_VIRTUAL,
        page_size: mdraid.Size,
        type_extra: !block && _("stopped"),
        id_extra: block && _("MDRAID device"),
        for_summary: true,
        has_warning: mdraid.Degraded > 0 || missing_bitmap(mdraid),
        job_path: mdraid.path,
        component: MDRaidCard,
        props: { mdraid, block },
        actions: [
            (!block &&
             {
                 title: _("Start"),
                 action: () => mdraid_start(mdraid),
                 tag: "device"
             }),
            (mdraid.Level !== "raid0" &&
             {
                 title: _("Add disk"),
                 action: () => add_disk(mdraid),
                 excuse: add_excuse,
                 tag: "disks",
             }),
            ...(block ? partitionable_block_actions(block, "device") : []),
            {
                title: _("Delete"),
                action: () => mdraid_delete(mdraid, block, mdraid_card),
                danger: true,
            },
        ],
    });

    if (!block) {
        new_page(parent, mdraid_card);
    } else
        make_block_page(parent, block, mdraid_card, { partitionable: true });
}

const MDRaidCard = ({ card, mdraid, block }) => {
    function format_level(str) {
        return {
            raid0: _("RAID 0"),
            raid1: _("RAID 1"),
            raid4: _("RAID 4"),
            raid5: _("RAID 5"),
            raid6: _("RAID 6"),
            raid10: _("RAID 10")
        }[str] || cockpit.format(_("RAID ($0)"), str);
    }

    let level = format_level(mdraid.Level);
    if (mdraid.NumDevices > 0)
        level += ", " + cockpit.format(_("$0 disks"), mdraid.NumDevices);
    if (mdraid.ChunkSize > 0)
        level += ", " + cockpit.format(_("$0 chunk size"), fmt_size(mdraid.ChunkSize));

    const alerts = [];
    if (mdraid.Degraded > 0) {
        const text = cockpit.format(
            cockpit.ngettext("$0 disk is missing", "$0 disks are missing", mdraid.Degraded),
            mdraid.Degraded
        );
        alerts.push(
            <Alert isInline variant="danger" key="degraded"
                   title={_("The MDRAID device is in a degraded state")}>
                {text}
            </Alert>);
    }

    function fix_bitmap() {
        return mdraid.SetBitmapLocation(encode_filename("internal"), { });
    }

    if (missing_bitmap(mdraid)) {
        alerts.push(
            <Alert isInline variant="warning" key="bitmap"
                   title={_("This MDRAID device has no write-intent bitmap. Such a bitmap can reduce synchronization times significantly.")}>
                <div className="storage-alert-actions">
                    <StorageButton onClick={fix_bitmap}>{_("Add a bitmap")}</StorageButton>
                </div>
            </Alert>);
    }

    return (
        <StorageCard card={card} alerts={alerts}>
            <CardBody>
                <DescriptionList className="pf-m-horizontal-on-sm">
                    <StorageDescription title={_("Name")} value={mdraid_name(mdraid)} />
                    <StorageDescription title={_("RAID level")} value={level} />
                    <StorageDescription title={_("State")} value={block ? _("Running") : _("Not running")}
                                        action={block && <StorageLink onClick={() => mdraid_stop(mdraid)}>
                                            {_("Stop")}
                                        </StorageLink>} />
                    <StorageDescription title={_("UUID")} value={mdraid.UUID} />
                    <StorageDescription title={_("Device")} value={block ? decode_filename(block.PreferredDevice) : "-"} />
                    <StorageDescription title={_("Capacity")} value={fmt_size_long(mdraid.Size)} />
                </DescriptionList>
            </CardBody>
            <CardHeader><strong>{_("Disks")}</strong></CardHeader>
            <PageTable
                emptyCaption={_("No disks found")}
                aria-label={_("MDRAID disks")}
                crossrefs={get_crossrefs(mdraid)} />
        </StorageCard>
    );
};