Line data Source code
1 : /*
2 : * Copyright (C) 2023 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 113 : import cockpit from "cockpit";
7 113 : import React from "react";
8 : import client from "../client";
9 :
10 : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
11 : import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
12 : import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
13 :
14 : import { VolumeIcon } from "../icons/gnome-icons.jsx";
15 : import { StorageButton, StorageLink } from "../storage-controls.jsx";
16 : import {
17 : StorageCard, StorageDescription, PageTable,
18 : new_page, new_card, PAGE_CATEGORY_VIRTUAL,
19 : get_crossrefs, navigate_away_from_card
20 : } from "../pages.jsx";
21 : import { make_block_page } from "../block/create-pages.jsx";
22 : import {
23 : block_short_name, mdraid_name, encode_filename, decode_filename,
24 : fmt_size, fmt_size_long, get_active_usage, teardown_active_usage,
25 : get_available_spaces, prepare_available_spaces,
26 : reload_systemd, should_ignore,
27 : } from "../utils.js";
28 :
29 : import {
30 : dialog_open, SelectSpaces,
31 : BlockingMessage, TeardownMessage,
32 : init_teardown_usage
33 : } from "../dialog.jsx";
34 :
35 : import { partitionable_block_actions } from "../partitions/actions.jsx";
36 :
37 113 : const _ = cockpit.gettext;
38 :
39 2 : function mdraid_start(mdraid) {
40 2 : return mdraid.Start({ "start-degraded": { t: 'b', v: true } });
41 2 : }
42 :
43 2 : function mdraid_stop(mdraid) {
44 2 : const block = client.mdraids_block[mdraid.path];
45 0 : const usage = get_active_usage(client, block ? block.path : "", _("stop"));
46 :
47 0 : if (usage.Blocking) {
48 0 : dialog_open({
49 0 : Title: cockpit.format(_("$0 is in use"), mdraid_name(mdraid)),
50 0 : Body: BlockingMessage(usage),
51 0 : });
52 0 : return;
53 0 : }
54 :
55 1 : if (usage.Teardown) {
56 1 : dialog_open({
57 1 : Title: cockpit.format(_("Confirm stopping of $0"),
58 1 : mdraid_name(mdraid)),
59 1 : Teardown: TeardownMessage(usage),
60 1 : Action: {
61 1 : Title: _("Stop device"),
62 1 : action: function () {
63 1 : return teardown_active_usage(client, usage)
64 1 : .then(function () {
65 1 : return mdraid.Stop({});
66 1 : });
67 1 : }
68 1 : },
69 1 : Inits: [
70 1 : init_teardown_usage(client, usage)
71 1 : ]
72 1 : });
73 1 : return;
74 1 : }
75 :
76 2 : return mdraid.Stop({});
77 2 : }
78 :
79 4 : function mdraid_delete(mdraid, block, card) {
80 4 : async function delete_() {
81 4 : const members = client.mdraids_members[mdraid.path];
82 :
83 4 : await mdraid.Delete({ 'tear-down': { t: 'b', v: true } });
84 :
85 : // HACK - https://github.com/storaged-project/udisks/issues/1375
86 : //
87 : // Now wipe the members again. This is necessary when
88 : // deleting a MDRAID with metadata 1.0, which stores its
89 : // superblock at the end of each member, and the superblock of
90 : // the actual content of the mdraid is stored at the very
91 : // start of each member (for certain raid levels like a
92 : // mirror). The Delete call above will only remove the mdraid
93 : // metadata and leave the content superblock in place. Thus,
94 : // after deleting, the members might magically turn into the
95 : // previous content. Wiping the members again will remove any
96 : // such metadata that might cause them to be misidentified.
97 :
98 4 : for (const b of members) {
99 4 : await b.Format('empty', { });
100 4 : }
101 :
102 4 : await reload_systemd();
103 4 : }
104 :
105 0 : const usage = get_active_usage(client, block ? block.path : "", _("delete"));
106 :
107 0 : if (usage.Blocking) {
108 0 : dialog_open({
109 0 : Title: cockpit.format(_("$0 is in use"), mdraid_name(mdraid)),
110 0 : Body: BlockingMessage(usage)
111 0 : });
112 0 : return;
113 0 : }
114 :
115 4 : dialog_open({
116 4 : Title: cockpit.format(_("Permanently delete $0?"), mdraid_name(mdraid)),
117 4 : Teardown: TeardownMessage(usage),
118 4 : Action: {
119 4 : Title: _("Delete"),
120 4 : Danger: _("Deleting erases all data on a MDRAID device."),
121 4 : action: async function () {
122 4 : await teardown_active_usage(client, usage);
123 4 : await delete_();
124 4 : navigate_away_from_card(card);
125 4 : }
126 4 : },
127 4 : Inits: [
128 4 : init_teardown_usage(client, usage)
129 4 : ]
130 4 : });
131 4 : }
132 :
133 2 : function add_disk(mdraid) {
134 2 : function filter_inside_mdraid(spc) {
135 2 : let block = spc.block;
136 2 : if (client.blocks_part[block.path])
137 0 : block = client.blocks[client.blocks_part[block.path].Table];
138 2 : return block && block.MDRaid != mdraid.path;
139 2 : }
140 :
141 2 : function rescan(path) {
142 : // mdraid often forgets to trigger udev, let's do it explicitly
143 2 : return client.wait_for(() => client.blocks[path]).then(block => block.Rescan({ }));
144 2 : }
145 :
146 2 : dialog_open({
147 2 : Title: _("Add disks"),
148 2 : Fields: [
149 2 : SelectSpaces("disks", _("Disks"),
150 2 : {
151 2 : empty_warning: _("No disks are available."),
152 2 : validate: function (disks) {
153 2 : if (disks.length === 0)
154 0 : return _("At least one disk is needed.");
155 2 : },
156 2 : spaces: get_available_spaces().filter(filter_inside_mdraid)
157 2 : })
158 2 : ],
159 2 : Action: {
160 2 : Title: _("Add"),
161 2 : action: function(vals) {
162 2 : return prepare_available_spaces(client, vals.disks).then(paths =>
163 2 : Promise.all(paths.map(p => mdraid.AddDevice(p, {}).then(() => rescan(p)))));
164 2 : }
165 2 : }
166 2 : });
167 2 : }
168 :
169 6 : function missing_bitmap(mdraid) {
170 6 : let policy;
171 6 : if (mdraid.ConsistencyPolicy)
172 5 : policy = mdraid.ConsistencyPolicy;
173 0 : else if (mdraid.ActiveDevices.some(a => a[2].indexOf("journal") >= 0))
174 0 : policy = "journal";
175 5 : else if (mdraid.BitmapLocation && decode_filename(mdraid.BitmapLocation) != "none")
176 0 : policy = "bitmap";
177 : else
178 0 : policy = "resync";
179 :
180 6 : return (mdraid.Level != "raid0" &&
181 6 : client.mdraids_members[mdraid.path].some(m => m.Size > 100 * 1024 * 1024 * 1024) &&
182 1 : policy == "resync");
183 6 : }
184 :
185 7 : export function make_mdraid_page(parent, mdraid) {
186 7 : const block = client.mdraids_block[mdraid.path];
187 :
188 7 : if (should_ignore(client, mdraid.path))
189 7 : return;
190 :
191 7 : let add_excuse = false;
192 7 : if (!block)
193 7 : add_excuse = _("MDRAID device must be running");
194 :
195 7 : const mdraid_card = new_card({
196 7 : title: _("MDRAID device"),
197 7 : next: null,
198 7 : page_location: ["mdraid", mdraid.UUID],
199 7 : page_name: block ? block_short_name(block) : mdraid_name(mdraid),
200 7 : page_icon: VolumeIcon,
201 7 : page_category: PAGE_CATEGORY_VIRTUAL,
202 7 : page_size: mdraid.Size,
203 7 : type_extra: !block && _("stopped"),
204 7 : id_extra: block && _("MDRAID device"),
205 7 : for_summary: true,
206 7 : has_warning: mdraid.Degraded > 0 || missing_bitmap(mdraid),
207 7 : job_path: mdraid.path,
208 7 : component: MDRaidCard,
209 7 : props: { mdraid, block },
210 7 : actions: [
211 7 : (!block &&
212 7 : {
213 7 : title: _("Start"),
214 2 : action: () => mdraid_start(mdraid),
215 7 : tag: "device"
216 7 : }),
217 7 : (mdraid.Level != "raid0" &&
218 7 : {
219 7 : title: _("Add disk"),
220 2 : action: () => add_disk(mdraid),
221 7 : excuse: add_excuse,
222 7 : tag: "disks",
223 7 : }),
224 7 : ...(block ? partitionable_block_actions(block, "device") : []),
225 7 : {
226 7 : title: _("Delete"),
227 4 : action: () => mdraid_delete(mdraid, block, mdraid_card),
228 7 : danger: true,
229 7 : },
230 7 : ],
231 7 : });
232 :
233 7 : if (!block) {
234 7 : new_page(parent, mdraid_card);
235 7 : } else
236 7 : make_block_page(parent, block, mdraid_card, { partitionable: true });
237 7 : }
238 :
239 4 : const MDRaidCard = ({ card, mdraid, block }) => {
240 4 : function format_level(str) {
241 4 : return {
242 4 : raid0: _("RAID 0"),
243 4 : raid1: _("RAID 1"),
244 4 : raid4: _("RAID 4"),
245 4 : raid5: _("RAID 5"),
246 4 : raid6: _("RAID 6"),
247 4 : raid10: _("RAID 10")
248 0 : }[str] || cockpit.format(_("RAID ($0)"), str);
249 4 : }
250 :
251 4 : let level = format_level(mdraid.Level);
252 4 : if (mdraid.NumDevices > 0)
253 4 : level += ", " + cockpit.format(_("$0 disks"), mdraid.NumDevices);
254 4 : if (mdraid.ChunkSize > 0)
255 4 : level += ", " + cockpit.format(_("$0 chunk size"), fmt_size(mdraid.ChunkSize));
256 :
257 4 : const alerts = [];
258 2 : if (mdraid.Degraded > 0) {
259 2 : const text = cockpit.format(
260 2 : cockpit.ngettext("$0 disk is missing", "$0 disks are missing", mdraid.Degraded),
261 2 : mdraid.Degraded
262 2 : );
263 2 : alerts.push(
264 2 : <Alert isInline variant="danger" key="degraded"
265 2 : title={_("The MDRAID device is in a degraded state")}>
266 2 : {text}
267 2 : </Alert>);
268 2 : }
269 :
270 1 : function fix_bitmap() {
271 1 : return mdraid.SetBitmapLocation(encode_filename("internal"), { });
272 1 : }
273 :
274 1 : if (missing_bitmap(mdraid)) {
275 1 : alerts.push(
276 1 : <Alert isInline variant="warning" key="bitmap"
277 1 : title={_("This MDRAID device has no write-intent bitmap. Such a bitmap can reduce synchronization times significantly.")}>
278 1 : <div className="storage-alert-actions">
279 1 : <StorageButton onClick={fix_bitmap}>{_("Add a bitmap")}</StorageButton>
280 1 : </div>
281 1 : </Alert>);
282 1 : }
283 :
284 4 : return (
285 4 : <StorageCard card={card} alerts={alerts}>
286 4 : <CardBody>
287 4 : <DescriptionList className="pf-m-horizontal-on-sm">
288 4 : <StorageDescription title={_("Name")} value={mdraid_name(mdraid)} />
289 4 : <StorageDescription title={_("RAID level")} value={level} />
290 2 : <StorageDescription title={_("State")} value={block ? _("Running") : _("Not running")}
291 2 : action={block && <StorageLink onClick={() => mdraid_stop(mdraid)}>
292 4 : {_("Stop")}
293 4 : </StorageLink>} />
294 4 : <StorageDescription title={_("UUID")} value={mdraid.UUID} />
295 2 : <StorageDescription title={_("Device")} value={block ? decode_filename(block.PreferredDevice) : "-"} />
296 4 : <StorageDescription title={_("Capacity")} value={fmt_size_long(mdraid.Size)} />
297 4 : </DescriptionList>
298 4 : </CardBody>
299 4 : <CardHeader><strong>{_("Disks")}</strong></CardHeader>
300 4 : <PageTable
301 4 : emptyCaption={_("No disks found")}
302 4 : aria-label={_("MDRAID disks")}
303 4 : crossrefs={get_crossrefs(mdraid)} />
304 4 : </StorageCard>
305 : );
306 4 : };
|