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, ChildrenTable, PageTable,
18 : new_page, new_card, get_crossrefs, PAGE_CATEGORY_VIRTUAL,
19 : navigate_to_new_card_location, navigate_away_from_card
20 : } from "../pages.jsx";
21 : import {
22 : fmt_size_long, get_active_usage, teardown_active_usage, for_each_async,
23 : validate_lvm2_name,
24 : get_available_spaces, prepare_available_spaces,
25 : reload_systemd, should_ignore,
26 : contains_rootfs,
27 : } from "../utils.js";
28 :
29 : import {
30 : dialog_open, SelectSpaces, TextInput,
31 : BlockingMessage, TeardownMessage,
32 : init_teardown_usage
33 : } from "../dialog.jsx";
34 :
35 : import { create_logical_volume } from "./create-logical-volume-dialog.jsx";
36 : import { make_block_logical_volume_card } from "./block-logical-volume.jsx";
37 : import { make_vdo_pool_card } from "./vdo-pool.jsx";
38 : import { make_thin_pool_logical_volume_page } from "./thin-pool-logical-volume.jsx";
39 : import { make_inactive_logical_volume_page } from "./inactive-logical-volume.jsx";
40 : import { make_unsupported_logical_volume_page } from "./unsupported-logical-volume.jsx";
41 : import { make_block_page } from "../block/create-pages.jsx";
42 :
43 113 : const _ = cockpit.gettext;
44 :
45 1 : function vgroup_rename(client, vgroup, card) {
46 1 : dialog_open({
47 1 : Title: _("Rename volume group"),
48 1 : Fields: [
49 1 : TextInput("name", _("Name"),
50 1 : {
51 1 : value: vgroup.Name,
52 1 : validate: validate_lvm2_name
53 1 : })
54 1 : ],
55 1 : Action: {
56 1 : Title: _("Rename"),
57 1 : Danger: contains_rootfs(client, vgroup.path)
58 0 : ? _("This volume group contains the root filesystem. Renaming it might require further changes to the bootloader configuration or kernel command line.")
59 1 : : null,
60 1 : action: async function (vals) {
61 1 : await vgroup.Rename(vals.name, { });
62 1 : navigate_to_new_card_location(card, ["vg", vals.name]);
63 1 : }
64 1 : }
65 1 : });
66 1 : }
67 :
68 3 : function vgroup_delete(client, vgroup, card) {
69 3 : const usage = get_active_usage(client, vgroup.path, _("delete"));
70 :
71 0 : if (usage.Blocking) {
72 0 : dialog_open({
73 0 : Title: cockpit.format(_("$0 is in use"),
74 0 : vgroup.Name),
75 0 : Body: BlockingMessage(usage)
76 0 : });
77 0 : return;
78 0 : }
79 :
80 3 : dialog_open({
81 3 : Title: cockpit.format(_("Permanently delete $0?"), vgroup.Name),
82 3 : Teardown: TeardownMessage(usage),
83 3 : Action: {
84 3 : Danger: _("Deleting erases all data on a volume group."),
85 3 : Title: _("Delete"),
86 3 : disable_on_error: usage.Teardown,
87 3 : action: async function () {
88 3 : await teardown_active_usage(client, usage);
89 3 : await vgroup.Delete(true, { 'tear-down': { t: 'b', v: true } });
90 3 : await reload_systemd();
91 3 : navigate_away_from_card(card);
92 3 : }
93 3 : },
94 3 : Inits: [
95 3 : init_teardown_usage(client, usage)
96 3 : ]
97 3 : });
98 3 : }
99 :
100 1 : function create_snapshot(lvol) {
101 1 : dialog_open({
102 1 : Title: _("Create snapshot"),
103 1 : Fields: [
104 1 : TextInput("name", _("Name"),
105 1 : { validate: validate_lvm2_name }),
106 1 : ],
107 1 : Action: {
108 1 : Title: _("Create"),
109 1 : action: function (vals) {
110 1 : return lvol.CreateSnapshot(vals.name, vals.size || 0, { });
111 1 : }
112 1 : }
113 1 : });
114 1 : }
115 :
116 22 : export function lvm2_create_snapshot_action(lvol) {
117 22 : if (!client.lvols[lvol.ThinPool])
118 22 : return null;
119 :
120 1 : return { title: _("Create snapshot"), action: () => create_snapshot(lvol) };
121 22 : }
122 :
123 22 : function make_generic_logical_volume_card(next, vgroup, lvol) {
124 22 : let result = next;
125 22 : if (client.vdo_vols[lvol.path])
126 0 : result = make_vdo_pool_card(result, vgroup, lvol);
127 22 : return result;
128 22 : }
129 :
130 22 : export function make_lvm2_logical_volume_page(parent, vgroup, lvol) {
131 22 : const generic_card = make_generic_logical_volume_card(null, vgroup, lvol);
132 :
133 2 : if (lvol.Type == "pool") {
134 2 : make_thin_pool_logical_volume_page(parent, vgroup, lvol);
135 2 : } else {
136 22 : const block = client.lvols_block[lvol.path];
137 22 : if (block) {
138 22 : const lv_card = make_block_logical_volume_card(generic_card, vgroup, lvol, block);
139 22 : make_block_page(parent, block, lv_card);
140 22 : } else {
141 : // If we can't find the block for a active volume, UDisks2
142 : // or something below is probably misbehaving, and we show
143 : // it as "unsupported".
144 22 : if (lvol.Active) {
145 22 : make_unsupported_logical_volume_page(parent, vgroup, lvol, generic_card);
146 4 : } else {
147 4 : make_inactive_logical_volume_page(parent, vgroup, lvol, generic_card);
148 4 : }
149 22 : }
150 22 : }
151 22 : }
152 :
153 25 : function make_logical_volume_pages(parent, vgroup) {
154 0 : const isVDOPool = lvol => Object.keys(client.vdo_vols).some(v => client.vdo_vols[v].VDOPool == lvol.path);
155 :
156 0 : (client.vgroups_lvols[vgroup.path] || []).forEach(lvol => {
157 : // We ignore volumes in a thin pool; they appear as children
158 : // of their pool.
159 : //
160 : // We ignore old-style snapshots because Cockpit would need to
161 : // treat them specially, and we haven't bothered to write the
162 : // code for that.
163 : //
164 : // We ignore vdo pools; they appear as a card for their
165 : // single contained logical volume.
166 : //
167 22 : if (lvol.ThinPool == "/" && lvol.Origin == "/" && !isVDOPool(lvol))
168 22 : make_lvm2_logical_volume_page(parent, vgroup, lvol);
169 22 : });
170 25 : }
171 :
172 3 : function add_disk(vgroup) {
173 3 : function filter_inside_vgroup(spc) {
174 3 : let block = spc.block;
175 3 : if (client.blocks_part[block.path])
176 0 : block = client.blocks[client.blocks_part[block.path].Table];
177 3 : const lvol = (block &&
178 3 : client.blocks_lvm2[block.path] &&
179 2 : client.lvols[client.blocks_lvm2[block.path].LogicalVolume]);
180 2 : return !lvol || lvol.VolumeGroup != vgroup.path;
181 3 : }
182 :
183 3 : dialog_open({
184 3 : Title: _("Add disks"),
185 3 : Fields: [
186 3 : SelectSpaces("disks", _("Disks"),
187 3 : {
188 3 : empty_warning: _("No disks are available."),
189 3 : validate: function(disks) {
190 3 : if (disks.length === 0)
191 0 : return _("At least one disk is needed.");
192 3 : },
193 3 : spaces: get_available_spaces().filter(filter_inside_vgroup)
194 3 : })
195 3 : ],
196 3 : Action: {
197 3 : Title: _("Add"),
198 3 : action: function(vals) {
199 3 : return prepare_available_spaces(client, vals.disks).then(paths =>
200 3 : Promise.all(paths.map(p => vgroup.AddDevice(p, {}))));
201 3 : }
202 3 : }
203 3 : });
204 3 : }
205 :
206 25 : export function make_lvm2_volume_group_page(parent, vgroup) {
207 25 : const has_missing_pvs = vgroup.MissingPhysicalVolumes && vgroup.MissingPhysicalVolumes.length > 0;
208 :
209 25 : let lvol_excuse = null;
210 25 : if (has_missing_pvs)
211 3 : lvol_excuse = _("Volume group is missing physical volumes");
212 25 : else if (vgroup.FreeSize == 0)
213 7 : lvol_excuse = _("No free space");
214 :
215 25 : if (should_ignore(client, vgroup.path))
216 25 : return;
217 :
218 25 : const vgroup_card = new_card({
219 25 : title: _("LVM2 volume group"),
220 25 : next: null,
221 25 : page_location: ["vg", vgroup.Name],
222 25 : page_name: vgroup.Name,
223 25 : page_icon: VolumeIcon,
224 25 : page_category: PAGE_CATEGORY_VIRTUAL,
225 25 : page_size: vgroup.Size,
226 25 : job_path: vgroup.path,
227 25 : component: LVM2VolumeGroupCard,
228 25 : props: { vgroup },
229 25 : actions: [
230 25 : {
231 25 : title: _("Add physical volume"),
232 3 : action: () => add_disk(vgroup),
233 25 : tag: "pvols",
234 25 : },
235 25 : {
236 25 : title: _("Delete group"),
237 3 : action: () => vgroup_delete(client, vgroup, vgroup_card),
238 25 : danger: true,
239 25 : tag: "group",
240 25 : },
241 25 : ],
242 25 : });
243 :
244 25 : const lvols_card = new_card({
245 25 : title: _("LVM2 logical volumes"),
246 25 : next: vgroup_card,
247 25 : has_warning: has_missing_pvs,
248 25 : component: LVM2LogicalVolumesCard,
249 25 : props: { vgroup },
250 25 : actions: [
251 25 : {
252 25 : title: _("Create new logical volume"),
253 10 : action: () => create_logical_volume(client, vgroup),
254 25 : excuse: lvol_excuse,
255 25 : tag: "group",
256 25 : },
257 25 : ],
258 25 : });
259 :
260 25 : const vgroup_page = new_page(parent, lvols_card);
261 25 : make_logical_volume_pages(vgroup_page, vgroup);
262 25 : }
263 :
264 21 : const LVM2LogicalVolumesCard = ({ card, vgroup }) => {
265 21 : return (
266 21 : <StorageCard card={card}>
267 21 : <ChildrenTable
268 21 : emptyCaption={_("No logical volumes")}
269 21 : aria-label={_("LVM2 logical volumes")}
270 21 : page={card.page} />
271 21 : </StorageCard>
272 : );
273 21 : };
274 :
275 21 : const LVM2VolumeGroupCard = ({ card, vgroup }) => {
276 21 : const has_missing_pvs = vgroup.MissingPhysicalVolumes && vgroup.MissingPhysicalVolumes.length > 0;
277 :
278 2 : function is_partial_linear_lvol(block) {
279 2 : const lvm2 = client.blocks_lvm2[block.path];
280 2 : const lvol = lvm2 && client.lvols[lvm2.LogicalVolume];
281 1 : return lvol && lvol.Layout == "linear" && client.lvols_status[lvol.path] == "partial";
282 2 : }
283 :
284 2 : function remove_missing() {
285 : /* Calling vgroup.RemoveMissingPhysicalVolumes will
286 : implicitly delete all partial, linear logical volumes.
287 : Instead of allowing this, we explicitly delete these
288 : volumes before calling RemoveMissingPhysicalVolumes.
289 : This allows us to kill processes that keep them busy
290 : and remove their fstab entries.
291 :
292 : RemoveMissingPhysicalVolumes leaves non-linear volumes
293 : alone, even if they can't be repaired anymore. This is
294 : a bit inconsistent, but *shrug*.
295 : */
296 :
297 2 : const all_usage = get_active_usage(client, vgroup.path, _("delete"));
298 2 : const usage = all_usage.filter(u => u.block && is_partial_linear_lvol(u.block));
299 2 : usage.Blocking = all_usage.Blocking;
300 2 : usage.Teardown = all_usage.Teardown;
301 :
302 0 : if (usage.Blocking) {
303 0 : dialog_open({
304 0 : Title: cockpit.format(_("$0 is in use"),
305 0 : vgroup.Name),
306 0 : Body: BlockingMessage(usage)
307 0 : });
308 0 : return;
309 0 : }
310 :
311 2 : dialog_open({
312 2 : Title: _("Remove missing physical volumes?"),
313 2 : Teardown: TeardownMessage(usage),
314 2 : Action: {
315 2 : Title: _("Remove"),
316 2 : action: function () {
317 2 : return teardown_active_usage(client, usage)
318 2 : .then(function () {
319 2 : return for_each_async(usage,
320 1 : u => {
321 1 : const lvm2 = client.blocks_lvm2[u.block.path];
322 1 : const lvol = lvm2 && client.lvols[lvm2.LogicalVolume];
323 1 : return lvol.Delete({ 'tear-down': { t: 'b', v: true } });
324 1 : })
325 2 : .then(() => vgroup.RemoveMissingPhysicalVolumes({}));
326 2 : });
327 2 : }
328 2 : },
329 2 : Inits: [
330 2 : init_teardown_usage(client, usage)
331 2 : ]
332 2 : });
333 2 : }
334 :
335 21 : const alerts = [];
336 21 : if (has_missing_pvs)
337 3 : alerts.push(
338 3 : <Alert variant='warning' isInline key="missing"
339 3 : actionClose={<StorageButton onClick={remove_missing}>{_("Dismiss")}</StorageButton>}
340 3 : title={_("This volume group is missing some physical volumes.")}>
341 3 : {vgroup.MissingPhysicalVolumes.map(uuid => <div key={uuid}>{uuid}</div>)}
342 3 : </Alert>);
343 :
344 21 : return (
345 21 : <StorageCard card={card} alerts={alerts}>
346 21 : <CardBody>
347 21 : <DescriptionList className="pf-m-horizontal-on-sm">
348 21 : <StorageDescription title={_("Name")}
349 21 : value={vgroup.Name}
350 1 : action={<StorageLink onClick={() => vgroup_rename(client, vgroup, card)}
351 3 : excuse={has_missing_pvs && _("A volume group with missing physical volumes can not be renamed.")}>
352 21 : {_("edit")}
353 21 : </StorageLink>} />
354 21 : <StorageDescription title={_("UUID")} value={vgroup.UUID} />
355 21 : <StorageDescription title={_("Capacity")} value={fmt_size_long(vgroup.Size)} />
356 21 : </DescriptionList>
357 21 : </CardBody>
358 21 : <CardHeader><strong>{_("Physical volumes")}</strong></CardHeader>
359 21 : <PageTable
360 21 : emptyCaption={_("No physical volumes found")}
361 21 : aria-label={_("LVM2 physical volumes")}
362 21 : crossrefs={get_crossrefs(vgroup)} />
363 21 : </StorageCard>
364 : );
365 21 : };
|