370 lines
15 KiB
JavaScript
370 lines
15 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2017 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 {
|
|
Alert, Card, CardTitle, CardHeader, CardActions, CardBody,
|
|
Text, TextVariants,
|
|
DescriptionList,
|
|
DescriptionListTerm,
|
|
DescriptionListGroup,
|
|
DescriptionListDescription
|
|
} from "@patternfly/react-core";
|
|
import { MinusIcon, PlusIcon } from "@patternfly/react-icons";
|
|
import * as utils from "./utils.js";
|
|
import { StdDetailsLayout } from "./details.jsx";
|
|
import { SidePanel } from "./side-panel.jsx";
|
|
import { Block } from "./content-views.jsx";
|
|
import { StorageButton, StorageOnOff } from "./storage-controls.jsx";
|
|
import {
|
|
dialog_open, SelectSpaces, BlockingMessage, TeardownMessage,
|
|
init_active_usage_processes
|
|
} from "./dialog.jsx";
|
|
|
|
const _ = cockpit.gettext;
|
|
|
|
class MDRaidSidebar extends React.Component {
|
|
render() {
|
|
const self = this;
|
|
const client = self.props.client;
|
|
const mdraid = self.props.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({ }));
|
|
}
|
|
|
|
function add_disk() {
|
|
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: utils.get_available_spaces(client).filter(filter_inside_mdraid)
|
|
})
|
|
],
|
|
Action: {
|
|
Title: _("Add"),
|
|
action: function(vals) {
|
|
return utils.prepare_available_spaces(client, vals.disks).then(paths =>
|
|
Promise.all(paths.map(p => mdraid.AddDevice(p, {}).then(() => rescan(p)))));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const members = client.mdraids_members[mdraid.path] || [];
|
|
const dynamic_members = (mdraid.Level != "raid0");
|
|
|
|
let n_spares = 0;
|
|
let n_recovering = 0;
|
|
mdraid.ActiveDevices.forEach(function(as) {
|
|
if (as[2].indexOf("spare") >= 0) {
|
|
if (as[1] < 0)
|
|
n_spares += 1;
|
|
else
|
|
n_recovering += 1;
|
|
}
|
|
});
|
|
|
|
/* Older versions of Udisks/storaged don't have a Running property */
|
|
let running = mdraid.Running;
|
|
if (running === undefined)
|
|
running = mdraid.ActiveDevices && mdraid.ActiveDevices.length > 0;
|
|
|
|
function render_member(block) {
|
|
const active_state = utils.array_find(mdraid.ActiveDevices, function(as) {
|
|
return as[0] == block.path;
|
|
});
|
|
|
|
function state_text(state) {
|
|
return {
|
|
faulty: _("Failed"),
|
|
in_sync: _("In sync"),
|
|
spare: active_state[1] < 0 ? _("Spare") : _("Recovering"),
|
|
write_mostly: _("Write-mostly"),
|
|
blocked: _("Blocked")
|
|
}[state] || cockpit.format(_("Unknown ($0)"), state);
|
|
}
|
|
|
|
const slot = active_state && active_state[1] >= 0 && active_state[1].toString();
|
|
let states = active_state && active_state[2].map(state_text).join(", ");
|
|
|
|
if (slot)
|
|
states = cockpit.format(_("Slot $0"), slot) + ", " + states;
|
|
|
|
const is_in_sync = (active_state && active_state[2].indexOf("in_sync") >= 0);
|
|
const is_recovering = (active_state && active_state[2].indexOf("spare") >= 0 && active_state[1] >= 0);
|
|
|
|
let remove_excuse = false;
|
|
if (!running)
|
|
remove_excuse = _("The RAID device must be running in order to remove disks.");
|
|
else if ((is_in_sync && n_recovering > 0) || is_recovering)
|
|
remove_excuse = _("This disk cannot be removed while the device is recovering.");
|
|
else if (is_in_sync && n_spares < 1)
|
|
remove_excuse = _("A spare disk needs to be added first before this disk can be removed.");
|
|
else if (members.length <= 1)
|
|
remove_excuse = _("The last disk of a RAID device cannot be removed.");
|
|
|
|
function remove() {
|
|
return mdraid.RemoveDevice(block.path, { wipe: { t: 'b', v: true } });
|
|
}
|
|
|
|
let action = null;
|
|
if (dynamic_members)
|
|
action = (
|
|
<StorageButton ariaLabel={_("Remove")} onClick={remove} excuse={remove_excuse}>
|
|
<MinusIcon />
|
|
</StorageButton>);
|
|
|
|
return { client, block, actions: action, detail: states, key: block.path };
|
|
}
|
|
|
|
let add_excuse = false;
|
|
if (!running)
|
|
add_excuse = _("The RAID device must be running in order to add spare disks.");
|
|
|
|
let action = null;
|
|
if (dynamic_members)
|
|
action = (
|
|
<StorageButton ariaLabel={_("Add")} onClick={add_disk} excuse={add_excuse}>
|
|
<PlusIcon />
|
|
</StorageButton>);
|
|
|
|
return <SidePanel title={_("Disks")} actions={action} rows={members.map(render_member)} />;
|
|
}
|
|
}
|
|
|
|
export class MDRaidDetails extends React.Component {
|
|
render() {
|
|
const client = this.props.client;
|
|
const mdraid = this.props.mdraid;
|
|
const block = mdraid && client.mdraids_block[mdraid.path];
|
|
|
|
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"), utils.fmt_size(mdraid.ChunkSize));
|
|
|
|
function toggle_bitmap(val) {
|
|
return mdraid.SetBitmapLocation(utils.encode_filename(val ? 'internal' : 'none'), {});
|
|
}
|
|
|
|
let bitmap = null;
|
|
if (mdraid.BitmapLocation) {
|
|
const value = utils.decode_filename(mdraid.BitmapLocation) != "none";
|
|
bitmap = (
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("storage", "Bitmap")}</DescriptionListTerm>
|
|
<DescriptionListDescription>
|
|
<StorageOnOff state={value} aria-label={_("Toggle bitmap")} onChange={toggle_bitmap} />
|
|
</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
);
|
|
}
|
|
|
|
let degraded_message = null;
|
|
if (mdraid.Degraded > 0) {
|
|
const text = cockpit.format(
|
|
cockpit.ngettext("$0 disk is missing", "$0 disks are missing", mdraid.Degraded),
|
|
mdraid.Degraded
|
|
);
|
|
degraded_message = (
|
|
<Alert isInline variant="danger" title={_("The RAID array is in a degraded state")}> {text} </Alert>
|
|
);
|
|
}
|
|
|
|
/* Older versions of Udisks/storaged don't have a Running property */
|
|
let running = mdraid.Running;
|
|
if (running === undefined)
|
|
running = mdraid.ActiveDevices && mdraid.ActiveDevices.length > 0;
|
|
|
|
function start() {
|
|
return mdraid.Start({ "start-degraded": { t: 'b', v: true } });
|
|
}
|
|
|
|
function stop() {
|
|
const usage = utils.get_active_usage(client, block ? block.path : "", _("stop"));
|
|
|
|
if (usage.Blocking) {
|
|
dialog_open({
|
|
Title: cockpit.format(_("$0 is in use"), utils.mdraid_name(mdraid)),
|
|
Body: BlockingMessage(usage),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (usage.Teardown) {
|
|
dialog_open({
|
|
Title: cockpit.format(_("Confirm stopping of $0"),
|
|
utils.mdraid_name(mdraid)),
|
|
Teardown: TeardownMessage(usage),
|
|
Action: {
|
|
Title: _("Stop device"),
|
|
action: function () {
|
|
return utils.teardown_active_usage(client, usage)
|
|
.then(function () {
|
|
return mdraid.Stop({});
|
|
});
|
|
}
|
|
},
|
|
Inits: [
|
|
init_active_usage_processes(client, usage)
|
|
]
|
|
});
|
|
return;
|
|
}
|
|
|
|
return mdraid.Stop({});
|
|
}
|
|
|
|
function delete_dialog() {
|
|
const location = cockpit.location;
|
|
|
|
function delete_() {
|
|
if (mdraid.Delete)
|
|
return mdraid.Delete({ 'tear-down': { t: 'b', v: true } }).then(utils.reload_systemd);
|
|
|
|
// If we don't have a Delete method, we simulate
|
|
// it by stopping the array and wiping all
|
|
// members.
|
|
|
|
function wipe_members() {
|
|
return Promise.all(client.mdraids_members[mdraid.path].map(member => member.Format('empty', { })));
|
|
}
|
|
|
|
if (mdraid.ActiveDevices && mdraid.ActiveDevices.length > 0)
|
|
return mdraid.Stop({}).then(wipe_members);
|
|
else
|
|
return wipe_members();
|
|
}
|
|
|
|
const usage = utils.get_active_usage(client, block ? block.path : "", _("delete"));
|
|
|
|
if (usage.Blocking) {
|
|
dialog_open({
|
|
Title: cockpit.format(_("$0 is in use"), utils.mdraid_name(mdraid)),
|
|
Body: BlockingMessage(usage)
|
|
});
|
|
return;
|
|
}
|
|
|
|
dialog_open({
|
|
Title: cockpit.format(_("Permanently delete $0?"), utils.mdraid_name(mdraid)),
|
|
Teardown: TeardownMessage(usage),
|
|
Action: {
|
|
Title: _("Delete"),
|
|
Danger: _("Deleting erases all data on a RAID device."),
|
|
action: function () {
|
|
return utils.teardown_active_usage(client, usage)
|
|
.then(delete_)
|
|
.then(function () {
|
|
location.go('/');
|
|
});
|
|
}
|
|
},
|
|
Inits: [
|
|
init_active_usage_processes(client, usage)
|
|
]
|
|
});
|
|
}
|
|
|
|
const header = (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><Text component={TextVariants.h2}>{ cockpit.format(_("RAID device $0"), utils.mdraid_name(mdraid)) }</Text></CardTitle>
|
|
<CardActions>
|
|
{ running
|
|
? <StorageButton onClick={stop}>{_("Stop")}</StorageButton>
|
|
: <StorageButton onClick={start}>{_("Start")}</StorageButton>
|
|
}
|
|
{ "\n" }
|
|
<StorageButton kind="danger" onClick={delete_dialog}>{_("Delete")}</StorageButton>
|
|
</CardActions>
|
|
</CardHeader>
|
|
<CardBody>
|
|
<DescriptionList className="pf-m-horizontal-on-sm">
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("storage", "Device")}</DescriptionListTerm>
|
|
<DescriptionListDescription>{ block ? utils.decode_filename(block.PreferredDevice) : "-" }</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("storage", "UUID")}</DescriptionListTerm>
|
|
<DescriptionListDescription>{ mdraid.UUID }</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("storage", "Capacity")}</DescriptionListTerm>
|
|
<DescriptionListDescription>{ utils.fmt_size_long(mdraid.Size) }</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("storage", "RAID level")}</DescriptionListTerm>
|
|
<DescriptionListDescription>{ level }</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
|
|
{ bitmap }
|
|
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("storage", "State")}</DescriptionListTerm>
|
|
<DescriptionListDescription>{ running ? _("Running") : _("Not running") }</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
</DescriptionList>
|
|
</CardBody>
|
|
</Card>
|
|
);
|
|
|
|
const sidebar = <MDRaidSidebar client={this.props.client} mdraid={mdraid} />;
|
|
|
|
const content = <Block client={this.props.client} block={block} />;
|
|
|
|
return <StdDetailsLayout client={this.props.client} alert={degraded_message}
|
|
header={ header }
|
|
sidebar={ sidebar }
|
|
content={ content }
|
|
/>;
|
|
}
|
|
}
|