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 { 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 : import { ExclamationTriangleIcon, ExclamationCircleIcon } from "@patternfly/react-icons";
14 : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
15 :
16 : import { StorageButton, StorageLink } from "../storage-controls.jsx";
17 :
18 : import { check_unused_space, get_resize_info, grow_dialog, shrink_dialog } from "../block/resize.jsx";
19 : import { StorageCard, StorageDescription, new_card, navigate_to_new_card_location, navigate_away_from_card } from "../pages.jsx";
20 : import { block_name, fmt_size, get_active_usage, teardown_active_usage, reload_systemd } from "../utils.js";
21 : import {
22 : dialog_open, TextInput, SelectSpaces, BlockingMessage, TeardownMessage,
23 : init_teardown_usage
24 : } from "../dialog.jsx";
25 :
26 : import { lvm2_create_snapshot_action } from "./volume-group.jsx";
27 : import { pvs_to_spaces } from "./utils.jsx";
28 :
29 113 : const _ = cockpit.gettext;
30 :
31 0 : export function lvol_rename(lvol) {
32 0 : dialog_open({
33 0 : Title: _("Rename logical volume"),
34 0 : Fields: [
35 0 : TextInput("name", _("Name"),
36 0 : { value: lvol.Name })
37 0 : ],
38 0 : Action: {
39 0 : Title: _("Rename"),
40 0 : action: function (vals) {
41 0 : return lvol.Rename(vals.name, { });
42 0 : }
43 0 : }
44 0 : });
45 0 : }
46 :
47 3 : export function lvol_delete(lvol, card) {
48 3 : const vgroup = client.vgroups[lvol.VolumeGroup];
49 3 : const block = client.lvols_block[lvol.path];
50 1 : const usage = get_active_usage(client, block ? block.path : lvol.path, _("delete"));
51 :
52 0 : if (usage.Blocking) {
53 0 : dialog_open({
54 0 : Title: cockpit.format(_("$0 is in use"), lvol.Name),
55 0 : Body: BlockingMessage(usage)
56 0 : });
57 0 : return;
58 0 : }
59 :
60 3 : dialog_open({
61 3 : Title: cockpit.format(_("Permanently delete logical volume $0/$1?"), vgroup.Name, lvol.Name),
62 3 : Teardown: TeardownMessage(usage),
63 3 : Action: {
64 3 : Danger: _("Deleting a logical volume will delete all data in it."),
65 3 : Title: _("Delete"),
66 3 : action: async function () {
67 3 : await teardown_active_usage(client, usage);
68 3 : await lvol.Delete({ 'tear-down': { t: 'b', v: true } });
69 3 : await reload_systemd();
70 3 : navigate_away_from_card(card);
71 3 : }
72 3 : },
73 3 : Inits: [
74 3 : init_teardown_usage(client, usage)
75 3 : ]
76 3 : });
77 3 : }
78 :
79 1 : function repair(lvol) {
80 1 : const vgroup = lvol && client.vgroups[lvol.VolumeGroup];
81 1 : if (!vgroup)
82 1 : return;
83 :
84 1 : const summary = client.lvols_stripe_summary[lvol.path];
85 1 : const missing = summary.reduce((sum, sub) => sum + (sub["/"] ?? 0), 0);
86 :
87 1 : function usable(pvol) {
88 : // must have some free space and not already used for a
89 : // subvolume other than those that need to be repaired.
90 1 : return pvol.FreeSize > 0 && !summary.some(sub => !sub["/"] && sub[pvol.path]);
91 1 : }
92 :
93 1 : const pvs_as_spaces = pvs_to_spaces(client, client.vgroups_pvols[vgroup.path].filter(usable));
94 1 : const available = pvs_as_spaces.reduce((sum, spc) => sum + spc.size, 0);
95 :
96 1 : if (available < missing) {
97 1 : dialog_open({
98 1 : Title: cockpit.format(_("Unable to repair logical volume $0"), lvol.Name),
99 1 : 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."),
100 1 : fmt_size(missing))}</p>
101 1 : });
102 1 : return;
103 1 : }
104 :
105 1 : function enough_space(pvs) {
106 1 : const selected = pvs.reduce((sum, pv) => sum + pv.size, 0);
107 1 : if (selected < missing)
108 1 : return cockpit.format(_("An additional $0 must be selected"), fmt_size(missing - selected));
109 1 : }
110 :
111 1 : dialog_open({
112 1 : Title: cockpit.format(_("Repair logical volume $0"), lvol.Name),
113 1 : Body: <div><p>{cockpit.format(_("Select the physical volumes that should be used to repair the logical volume. At least $0 are needed."),
114 1 : fmt_size(missing))}</p><br /></div>,
115 1 : Fields: [
116 1 : SelectSpaces("pvs", _("Physical Volumes"),
117 1 : {
118 1 : spaces: pvs_as_spaces,
119 1 : validate: enough_space
120 1 : }),
121 1 : ],
122 1 : Action: {
123 1 : Title: _("Repair"),
124 1 : action: function (vals) {
125 1 : return lvol.Repair(vals.pvs.map(spc => spc.block.path), { });
126 1 : }
127 1 : }
128 1 : });
129 1 : }
130 :
131 2 : function deactivate(lvol, block) {
132 2 : const vgroup = client.vgroups[lvol.VolumeGroup];
133 2 : const usage = get_active_usage(client, block.path, _("deactivate"));
134 :
135 0 : if (usage.Blocking) {
136 0 : dialog_open({
137 0 : Title: cockpit.format(_("$0 is in use"), lvol.Name),
138 0 : Body: BlockingMessage(usage)
139 0 : });
140 0 : return;
141 0 : }
142 :
143 2 : dialog_open({
144 2 : Title: cockpit.format(_("Deactivate logical volume $0/$1?"), vgroup.Name, lvol.Name),
145 2 : Teardown: TeardownMessage(usage, true),
146 2 : Action: {
147 2 : Title: _("Deactivate"),
148 2 : action: async function () {
149 2 : await teardown_active_usage(client, usage);
150 2 : await lvol.Deactivate({ });
151 2 : await reload_systemd();
152 2 : }
153 2 : },
154 2 : Inits: [
155 2 : init_teardown_usage(client, usage, true)
156 2 : ]
157 2 : });
158 2 : }
159 :
160 22 : export function make_block_logical_volume_card(next, vgroup, lvol, block) {
161 22 : const unused_space_warning = check_unused_space(block.path);
162 22 : const unused_space = !!unused_space_warning;
163 22 : const status_code = client.lvols_status[lvol.path];
164 22 : const pool = client.lvols[lvol.ThinPool];
165 :
166 22 : let { info, shrink_excuse, grow_excuse } = get_resize_info(client, block, unused_space);
167 :
168 7 : if (!unused_space && !grow_excuse && !pool && vgroup.FreeSize == 0) {
169 7 : grow_excuse = _("Not enough space to grow");
170 7 : }
171 :
172 22 : let repair_action = null;
173 22 : if (status_code == "degraded" || status_code == "degraded-maybe-partial")
174 1 : repair_action = { title: _("Repair"), action: () => repair(lvol) };
175 :
176 22 : const card = new_card({
177 22 : title: _("LVM2 logical volume"),
178 22 : next,
179 22 : page_location: ["vg", vgroup.Name, lvol.Name],
180 22 : page_name: lvol.Name,
181 22 : page_size: block.Size,
182 22 : for_summary: true,
183 22 : has_warning: !!unused_space_warning || !!repair_action,
184 22 : has_danger: status_code == "partial",
185 22 : job_path: lvol.path,
186 22 : component: LVM2LogicalVolumeCard,
187 22 : props: { vgroup, lvol, block, unused_space_warning, resize_info: info },
188 22 : actions: [
189 22 : (!unused_space &&
190 22 : {
191 22 : title: _("Shrink"),
192 4 : action: () => shrink_dialog(client, lvol, info),
193 22 : excuse: shrink_excuse,
194 22 : }),
195 22 : (!unused_space &&
196 22 : {
197 22 : title: _("Grow"),
198 9 : action: () => grow_dialog(client, lvol, info),
199 22 : excuse: grow_excuse,
200 22 : }),
201 22 : {
202 22 : title: _("Deactivate"),
203 2 : action: () => deactivate(lvol, block),
204 22 : },
205 22 : lvm2_create_snapshot_action(lvol),
206 22 : repair_action,
207 22 : {
208 22 : title: _("Delete"),
209 3 : action: () => lvol_delete(lvol, card),
210 22 : danger: true,
211 22 : },
212 22 : ],
213 22 : });
214 22 : return card;
215 22 : }
216 :
217 21 : const LVM2LogicalVolumeCard = ({ card, vgroup, lvol, block, unused_space_warning, resize_info }) => {
218 21 : const unused_space = !!unused_space_warning;
219 :
220 1 : function rename() {
221 1 : dialog_open({
222 1 : Title: _("Rename logical volume"),
223 1 : Fields: [
224 1 : TextInput("name", _("Name"),
225 1 : { value: lvol.Name })
226 1 : ],
227 1 : Action: {
228 1 : Title: _("Rename"),
229 1 : action: async function (vals) {
230 1 : await lvol.Rename(vals.name, { });
231 1 : navigate_to_new_card_location(card, ["vg", vgroup.Name, vals.name]);
232 1 : }
233 1 : }
234 1 : });
235 1 : }
236 :
237 4 : function shrink_to_fit() {
238 4 : return shrink_dialog(client, lvol, resize_info, true);
239 4 : }
240 :
241 4 : function grow_to_fit() {
242 4 : return grow_dialog(client, lvol, resize_info, true);
243 4 : }
244 :
245 21 : const layout_desc = {
246 21 : raid0: _("Striped (RAID 0)"),
247 21 : raid1: _("Mirrored (RAID 1)"),
248 21 : raid10: _("Striped and mirrored (RAID 10)"),
249 21 : raid4: _("Dedicated parity (RAID 4)"),
250 21 : raid5: _("Distributed parity (RAID 5)"),
251 21 : raid6: _("Double distributed parity (RAID 6)")
252 21 : };
253 :
254 21 : const layout = lvol.Layout;
255 :
256 21 : return (
257 21 : <StorageCard card={card}
258 21 : alert={unused_space &&
259 4 : <Alert variant="warning"
260 4 : isInline
261 4 : title={_("This logical volume is not completely used by its content.")}>
262 4 : {cockpit.format(_("Volume size is $0. Content size is $1."),
263 4 : fmt_size(unused_space_warning.volume_size),
264 4 : fmt_size(unused_space_warning.content_size))}
265 4 : <div className='storage-alert-actions'>
266 4 : <StorageButton onClick={shrink_to_fit}>{_("Shrink volume")}</StorageButton>
267 4 : <StorageButton onClick={grow_to_fit}>{_("Grow content")}</StorageButton>
268 4 : </div>
269 4 : </Alert>}>
270 21 : <CardBody>
271 21 : <DescriptionList className="pf-m-horizontal-on-sm">
272 21 : <StorageDescription title={_("Name")} value={lvol.Name}
273 21 : action={<StorageLink onClick={rename}>
274 21 : {_("edit")}
275 21 : </StorageLink>} />
276 21 : { !unused_space &&
277 21 : <StorageDescription title={_("Size")} value={fmt_size(lvol.Size)} />
278 : }
279 21 : { (layout && layout != "linear") &&
280 1 : <StorageDescription title={_("Layout")} value={layout_desc[layout] || layout} />
281 : }
282 21 : <StructureDescription client={client} lvol={lvol} />
283 21 : </DescriptionList>
284 21 : </CardBody>
285 21 : </StorageCard>);
286 21 : };
287 :
288 21 : export const StructureDescription = ({ client, lvol }) => {
289 21 : const vgroup = client.vgroups[lvol.VolumeGroup];
290 0 : const pvs = (vgroup && client.vgroups_pvols[vgroup.path]) || [];
291 :
292 21 : if (!lvol.Structure || pvs.length <= 1)
293 13 : return null;
294 :
295 8 : let status = null;
296 8 : const status_code = client.lvols_status[lvol.path];
297 2 : if (status_code == "partial") {
298 2 : 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.");
299 1 : } else if (status_code == "degraded") {
300 2 : 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.");
301 1 : } else if (status_code == "degraded-maybe-partial") {
302 1 : 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.");
303 1 : }
304 :
305 8 : function nice_block_name(block) {
306 8 : return block_name(client.blocks[block.CryptoBackingDevice] || block);
307 8 : }
308 :
309 6 : function pvs_box(used, block_path) {
310 5 : if (block_path != "/") {
311 5 : const block = client.blocks[block_path];
312 5 : return <div key={block_path} className="storage-pvs-pv-box">
313 5 : <div className="storage-stripe-pv-box-dev">
314 1 : {block ? nice_block_name(block).replace("/dev/", "") : "???"}
315 5 : </div>
316 5 : <div>{fmt_size(used)}</div>
317 5 : </div>;
318 1 : } else {
319 2 : return <div key={block_path} className="storage-pvs-pv-box">
320 2 : <div className="storage-pvs-pv-box-dev">
321 2 : { status_code == "degraded"
322 0 : ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
323 2 : : <ExclamationCircleIcon className="ct-icon-times-circle" />
324 : }
325 2 : </div>
326 2 : <div>{fmt_size(used)}</div>
327 2 : </div>;
328 2 : }
329 6 : }
330 :
331 6 : if (lvol.Layout == "linear") {
332 6 : const pvs = client.lvols_stripe_summary[lvol.path];
333 6 : if (!pvs)
334 0 : return null;
335 :
336 6 : const stripe = Object.keys(pvs).map((path, i) =>
337 6 : <FlexItem key={i} className="storage-pvs-box">
338 6 : {pvs_box(pvs[path], path)}
339 6 : </FlexItem>);
340 :
341 6 : return (
342 6 : <StorageDescription title={_("Physical volumes")}>
343 6 : <Flex spaceItems={{ default: "spaceItemsNone" }}
344 6 : alignItems={{ default: "alignItemsStretch" }}>
345 6 : {stripe}
346 6 : </Flex>
347 6 : {status}
348 6 : </StorageDescription>);
349 6 : }
350 :
351 5 : function stripe_box(used, block_path) {
352 5 : if (block_path != "/") {
353 5 : const block = client.blocks[block_path];
354 5 : return <div key={block_path} className="storage-stripe-pv-box">
355 5 : <div className="storage-stripe-pv-box-dev">
356 2 : {block ? nice_block_name(block).replace("/dev/", "") : "???"}
357 5 : </div>
358 5 : <div>{fmt_size(used)}</div>
359 5 : </div>;
360 2 : } else {
361 2 : return <div key={block_path} className="storage-stripe-pv-box">
362 2 : <div className="storage-stripe-pv-box-dev">
363 2 : { status_code == "degraded"
364 2 : ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
365 1 : : <ExclamationCircleIcon className="ct-icon-times-circle" />
366 : }
367 2 : </div>
368 2 : <div>{fmt_size(used)}</div>
369 2 : </div>;
370 2 : }
371 5 : }
372 :
373 5 : if (lvol.Layout == "mirror" || lvol.Layout.indexOf("raid") == 0) {
374 5 : const summary = client.lvols_stripe_summary[lvol.path];
375 5 : if (!summary)
376 0 : return null;
377 :
378 5 : const stripes = summary.map((pvs, i) =>
379 5 : <FlexItem key={i} className="storage-stripe-box">
380 5 : {Object.keys(pvs).map(path => stripe_box(pvs[path], path))}
381 5 : </FlexItem>);
382 :
383 5 : return (
384 5 : <StorageDescription title={_("Stripes")}>
385 5 : <Flex alignItems={{ default: "alignItemsStretch" }}>{stripes}</Flex>
386 5 : {status}
387 5 : {lvol.SyncRatio != 1.0
388 2 : ? <div>{cockpit.format(_("$0 synchronized"), lvol.SyncRatio * 100 + "%")}</div>
389 5 : : null}
390 5 : </StorageDescription>);
391 5 : }
392 :
393 1 : return null;
394 21 : };
|