🏡 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 { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { ExclamationTriangleIcon, ExclamationCircleIcon } from "@patternfly/react-icons";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";

import { StorageButton, StorageLink } from "../storage-controls.jsx";

import { check_unused_space, get_resize_info, grow_dialog, shrink_dialog } from "../block/resize.jsx";
import { StorageCard, StorageDescription, new_card, navigate_to_new_card_location, navigate_away_from_card } from "../pages.jsx";
import { block_name, fmt_size, get_active_usage, teardown_active_usage, reload_systemd } from "../utils.js";
import {
    dialog_open, TextInput, SelectSpaces, BlockingMessage, TeardownMessage,
    init_teardown_usage
} from "../dialog.jsx";

import { lvm2_create_snapshot_action } from "./volume-group.jsx";
import { pvs_to_spaces } from "./utils.jsx";

const _ = cockpit.gettext;

export function lvol_rename(lvol) {
    dialog_open({
        Title: _("Rename logical volume"),
        Fields: [
            TextInput("name", _("Name"),
                      { value: lvol.Name })
        ],
        Action: {
            Title: _("Rename"),
            action: function (vals) {
                return lvol.Rename(vals.name, { });
            }
        }
    });
}

export function lvol_delete(lvol, card) {
    const vgroup = client.vgroups[lvol.VolumeGroup];
    const block = client.lvols_block[lvol.path];
    const usage = get_active_usage(client, block ? block.path : lvol.path, _("delete"));

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

    dialog_open({
        Title: cockpit.format(_("Permanently delete logical volume $0/$1?"), vgroup.Name, lvol.Name),
        Teardown: TeardownMessage(usage),
        Action: {
            Danger: _("Deleting a logical volume will delete all data in it."),
            Title: _("Delete"),
            action: async function () {
                await teardown_active_usage(client, usage);
                await lvol.Delete({ 'tear-down': { t: 'b', v: true } });
                await reload_systemd();
                navigate_away_from_card(card);
            }
        },
        Inits: [
            init_teardown_usage(client, usage)
        ]
    });
}

function repair(lvol) {
    const vgroup = lvol && client.vgroups[lvol.VolumeGroup];
    if (!vgroup)
        return;

    const summary = client.lvols_stripe_summary[lvol.path];
    const missing = summary.reduce((sum, sub) => sum + (sub["/"] ?? 0), 0);

    function usable(pvol) {
        // must have some free space and not already used for a
        // subvolume other than those that need to be repaired.
        return pvol.FreeSize > 0 && !summary.some(sub => !sub["/"] && sub[pvol.path]);
    }

    const pvs_as_spaces = pvs_to_spaces(client, client.vgroups_pvols[vgroup.path].filter(usable));
    const available = pvs_as_spaces.reduce((sum, spc) => sum + spc.size, 0);

    if (available < missing) {
        dialog_open({
            Title: cockpit.format(_("Unable to repair logical volume $0"), lvol.Name),
            Body: <p>{cockpit.format(_("There is not enough space available that could be used for a repair. At least $0 are needed on physical volumes that are not already used for this logical volume."),
                                     fmt_size(missing))}</p>
        });
        return;
    }

    function enough_space(pvs) {
        const selected = pvs.reduce((sum, pv) => sum + pv.size, 0);
        if (selected < missing)
            return cockpit.format(_("An additional $0 must be selected"), fmt_size(missing - selected));
    }

    dialog_open({
        Title: cockpit.format(_("Repair logical volume $0"), lvol.Name),
        Body: <div><p>{cockpit.format(_("Select the physical volumes that should be used to repair the logical volume. At least $0 are needed."),
                                      fmt_size(missing))}</p><br /></div>,
        Fields: [
            SelectSpaces("pvs", _("Physical Volumes"),
                         {
                             spaces: pvs_as_spaces,
                             validate: enough_space
                         }),
        ],
        Action: {
            Title: _("Repair"),
            action: function (vals) {
                return lvol.Repair(vals.pvs.map(spc => spc.block.path), { });
            }
        }
    });
}

function deactivate(lvol, block) {
    const vgroup = client.vgroups[lvol.VolumeGroup];
    const usage = get_active_usage(client, block.path, _("deactivate"));

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

    dialog_open({
        Title: cockpit.format(_("Deactivate logical volume $0/$1?"), vgroup.Name, lvol.Name),
        Teardown: TeardownMessage(usage, true),
        Action: {
            Title: _("Deactivate"),
            action: async function () {
                await teardown_active_usage(client, usage);
                await lvol.Deactivate({ });
                await reload_systemd();
            }
        },
        Inits: [
            init_teardown_usage(client, usage, true)
        ]
    });
}

export function make_block_logical_volume_card(next, vgroup, lvol, block) {
    const unused_space_warning = check_unused_space(block.path);
    const unused_space = !!unused_space_warning;
    const status_code = client.lvols_status[lvol.path];
    const pool = client.lvols[lvol.ThinPool];

    let { info, shrink_excuse, grow_excuse } = get_resize_info(client, block, unused_space);

    if (!unused_space && !grow_excuse && !pool && vgroup.FreeSize === 0) {
        grow_excuse = _("Not enough space to grow");
    }

    let repair_action = null;
    if (status_code === "degraded" || status_code === "degraded-maybe-partial")
        repair_action = { title: _("Repair"), action: () => repair(lvol) };

    const card = new_card({
        title: _("LVM2 logical volume"),
        next,
        page_location: ["vg", vgroup.Name, lvol.Name],
        page_name: lvol.Name,
        page_size: block.Size,
        for_summary: true,
        has_warning: !!unused_space_warning || !!repair_action,
        has_danger: status_code === "partial",
        job_path: lvol.path,
        component: LVM2LogicalVolumeCard,
        props: { vgroup, lvol, block, unused_space_warning, resize_info: info },
        actions: [
            (!unused_space &&
             {
                 title: _("Shrink"),
                 action: () => shrink_dialog(client, lvol, info),
                 excuse: shrink_excuse,
             }),
            (!unused_space &&
             {
                 title: _("Grow"),
                 action: () => grow_dialog(client, lvol, info),
                 excuse: grow_excuse,
             }),
            {
                title: _("Deactivate"),
                action: () => deactivate(lvol, block),
            },
            lvm2_create_snapshot_action(lvol),
            repair_action,
            {
                title: _("Delete"),
                action: () => lvol_delete(lvol, card),
                danger: true,
            },
        ],
    });
    return card;
}

const LVM2LogicalVolumeCard = ({ card, vgroup, lvol, block, unused_space_warning, resize_info }) => {
    const unused_space = !!unused_space_warning;

    function rename() {
        dialog_open({
            Title: _("Rename logical volume"),
            Fields: [
                TextInput("name", _("Name"),
                          { value: lvol.Name })
            ],
            Action: {
                Title: _("Rename"),
                action: async function (vals) {
                    await lvol.Rename(vals.name, { });
                    navigate_to_new_card_location(card, ["vg", vgroup.Name, vals.name]);
                }
            }
        });
    }

    function shrink_to_fit() {
        return shrink_dialog(client, lvol, resize_info, true);
    }

    function grow_to_fit() {
        return grow_dialog(client, lvol, resize_info, true);
    }

    const layout_desc = {
        raid0: _("Striped (RAID 0)"),
        raid1: _("Mirrored (RAID 1)"),
        raid10: _("Striped and mirrored (RAID 10)"),
        raid4: _("Dedicated parity (RAID 4)"),
        raid5: _("Distributed parity (RAID 5)"),
        raid6: _("Double distributed parity (RAID 6)")
    };

    const layout = lvol.Layout;

    return (
        <StorageCard card={card}
                     alert={unused_space &&
                     <Alert variant="warning"
                                   isInline
                                   title={_("This logical volume is not completely used by its content.")}>
                         {cockpit.format(_("Volume size is $0. Content size is $1."),
                                         fmt_size(unused_space_warning.volume_size),
                                         fmt_size(unused_space_warning.content_size))}
                         <div className='storage-alert-actions'>
                             <StorageButton onClick={shrink_to_fit}>{_("Shrink volume")}</StorageButton>
                             <StorageButton onClick={grow_to_fit}>{_("Grow content")}</StorageButton>
                         </div>
                     </Alert>}>
            <CardBody>
                <DescriptionList className="pf-m-horizontal-on-sm">
                    <StorageDescription title={_("Name")} value={lvol.Name}
                           action={<StorageLink onClick={rename}>
                               {_("edit")}
                           </StorageLink>} />
                    { !unused_space &&
                    <StorageDescription title={_("Size")} value={fmt_size(lvol.Size)} />
                    }
                    { (layout && layout !== "linear") &&
                    <StorageDescription title={_("Layout")} value={layout_desc[layout] || layout} />
                    }
                    <StructureDescription client={client} lvol={lvol} />
                </DescriptionList>
            </CardBody>
        </StorageCard>);
};

export const StructureDescription = ({ client, lvol }) => {
    const vgroup = client.vgroups[lvol.VolumeGroup];
    const pvs = (vgroup && client.vgroups_pvols[vgroup.path]) || [];

    if (!lvol.Structure || pvs.length <= 1)
        return null;

    let status = null;
    const status_code = client.lvols_status[lvol.path];
    if (status_code === "partial") {
        status = _("This logical volume has lost some of its physical volumes and can no longer be used. You need to delete it and create a new one to take its place.");
    } else if (status_code === "degraded") {
        status = _("This logical volume has lost some of its physical volumes but has not lost any data yet. You should repair it to restore its original redundancy.");
    } else if (status_code === "degraded-maybe-partial") {
        status = _("This logical volume has lost some of its physical volumes but might not have lost any data yet. You might be able to repair it.");
    }

    function nice_block_name(block) {
        return block_name(client.blocks[block.CryptoBackingDevice] || block);
    }

    function pvs_box(used, block_path) {
        if (block_path !== "/") {
            const block = client.blocks[block_path];
            return <div key={block_path} className="storage-pvs-pv-box">
                <div className="storage-stripe-pv-box-dev">
                    {block ? nice_block_name(block).replace("/dev/", "") : "???"}
                </div>
                <div>{fmt_size(used)}</div>
            </div>;
        } else {
            return <div key={block_path} className="storage-pvs-pv-box">
                <div className="storage-pvs-pv-box-dev">
                    { status_code === "degraded"
                        ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
                        : <ExclamationCircleIcon className="ct-icon-times-circle" />
                    }
                </div>
                <div>{fmt_size(used)}</div>
            </div>;
        }
    }

    if (lvol.Layout === "linear") {
        const pvs = client.lvols_stripe_summary[lvol.path];
        if (!pvs)
            return null;

        const stripe = Object.keys(pvs).map((path, i) =>
            <FlexItem key={i} className="storage-pvs-box">
                {pvs_box(pvs[path], path)}
            </FlexItem>);

        return (
            <StorageDescription title={_("Physical volumes")}>
                <Flex spaceItems={{ default: "spaceItemsNone" }}
                      alignItems={{ default: "alignItemsStretch" }}>
                    {stripe}
                </Flex>
                {status}
            </StorageDescription>);
    }

    function stripe_box(used, block_path) {
        if (block_path !== "/") {
            const block = client.blocks[block_path];
            return <div key={block_path} className="storage-stripe-pv-box">
                <div className="storage-stripe-pv-box-dev">
                    {block ? nice_block_name(block).replace("/dev/", "") : "???"}
                </div>
                <div>{fmt_size(used)}</div>
            </div>;
        } else {
            return <div key={block_path} className="storage-stripe-pv-box">
                <div className="storage-stripe-pv-box-dev">
                    { status_code === "degraded"
                        ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
                        : <ExclamationCircleIcon className="ct-icon-times-circle" />
                    }
                </div>
                <div>{fmt_size(used)}</div>
            </div>;
        }
    }

    if (lvol.Layout === "mirror" || lvol.Layout.indexOf("raid") === 0) {
        const summary = client.lvols_stripe_summary[lvol.path];
        if (!summary)
            return null;

        const stripes = summary.map((pvs, i) =>
            <FlexItem key={i} className="storage-stripe-box">
                {Object.keys(pvs).map(path => stripe_box(pvs[path], path))}
            </FlexItem>);

        return (
            <StorageDescription title={_("Stripes")}>
                <Flex alignItems={{ default: "alignItemsStretch" }}>{stripes}</Flex>
                {status}
                {lvol.SyncRatio !== 1.0
                    ? <div>{cockpit.format(_("$0 synchronized"), lvol.SyncRatio * 100 + "%")}</div>
                    : null}
            </StorageDescription>);
    }

    return null;
};