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 { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
11 : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
12 : import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
13 : import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
14 : import { Table, Tbody, Tr, Td } from '@patternfly/react-table';
15 : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
16 :
17 : import { VolumeIcon } from "../icons/gnome-icons.jsx";
18 : import { fmt_to_fragments } from "utils.jsx";
19 :
20 : import { StorageButton, StorageUsageBar, StorageLink, StorageOnOff } from "../storage-controls.jsx";
21 : import {
22 : StorageCard, StorageDescription, ChildrenTable, PageTable, Actions,
23 : new_page, new_card, PAGE_CATEGORY_VIRTUAL,
24 : get_crossrefs, navigate_away_from_card
25 : } from "../pages.jsx";
26 : import {
27 : get_active_usage, teardown_active_usage, for_each_async,
28 : get_available_spaces, prepare_available_spaces,
29 : decode_filename, should_ignore,
30 : } from "../utils.js";
31 :
32 : import {
33 : dialog_open, SelectSpaces, TextInput, PassInput, SelectOne, SizeSlider, CheckBoxes, Group, Message,
34 : BlockingMessage, TeardownMessage,
35 : init_teardown_usage
36 : } from "../dialog.jsx";
37 :
38 : import { validate_url, get_tang_adv } from "../crypto/tang.jsx";
39 : import { is_valid_mount_point } from "../filesystem/utils.jsx";
40 : import { at_boot_input, update_at_boot_input, mount_options } from "../filesystem/mounting-dialog.jsx";
41 :
42 : import {
43 : validate_pool_name, std_reply, with_stored_passphrase,
44 : confirm_tang_trust,
45 : validate_fs_name, set_mount_options, destroy_filesystem
46 : } from "./utils.jsx";
47 : import { make_stratis_filesystem_page } from "./filesystem.jsx";
48 :
49 113 : const _ = cockpit.gettext;
50 :
51 113 : const fsys_min_size = 512 * 1024 * 1024;
52 113 : const fsys_round_size = 1024 * 1024;
53 :
54 : /* Abstractions over the r6 and r8 API revisions, and V1 and V2 pool
55 : * formats.
56 : */
57 :
58 : /* Key descriptions are returned as an array with elements of type
59 : *
60 : * { slot: number | null, keydesc: string }
61 : *
62 : * A V1 pool will have at most one entry in the array, and it will
63 : * have `slot == null`. V2 pools have zero or more entries with
64 : * numbered slots.
65 : */
66 :
67 6 : function get_key_descriptions(pool) {
68 6 : const result = [];
69 :
70 6 : if (!pool.Encrypted)
71 4 : return result;
72 :
73 2 : if (client.stratis_interface_revision < 8) {
74 2 : const key_desc = pool.KeyDescription;
75 : /* The KeyDescription property has type "(b(bs))".
76 :
77 : The first "b" is false when the encryption of the pool is
78 : inconsistent (not all block devices have the same LUKS
79 : header). We pretend that the pool doesn't have any
80 : passphrase in that case.
81 :
82 : The second "b" is true when the pool has a passphrase, and
83 : the "s" is the description of the key for it.
84 :
85 : Thus, we detect an object of shape
86 :
87 : [ true, [ true, "..." ] ]
88 :
89 : and fish the string out of it.
90 : */
91 2 : if (key_desc[0] && key_desc[1][0])
92 2 : result.push({ slot: null, keydesc: key_desc[1][1] });
93 2 : } else {
94 4 : let key_descs = pool.KeyDescriptions;
95 : /* The KeyDescriptions property itself has type "v". For a V1
96 : pool this variant carries a value of type "b(bs)" just as
97 : with the r6 API (see above). For a V2 pool, it carries a
98 : "a(us)" value, an array of "slot number plus key
99 : description".
100 :
101 : Since the property itself has type "v", we need to unwrap
102 : this variant explicitly.
103 : */
104 :
105 : /* HACK - https://github.com/stratis-storage/stratisd/issues/3895
106 :
107 : Introduced in stratisd 3.8.0, fixed in stratisd 3.8.3.
108 :
109 : Change notifications drop the variant wrapping for some
110 : reason, so we only do the unwrapping when "key_descs" is
111 : indeed a variant.
112 : */
113 4 : if ("t" in key_descs)
114 2 : key_descs = key_descs.v;
115 3 : if (pool.MetadataVersion == 1) {
116 3 : if (key_descs[0] && key_descs[1][0])
117 3 : result.push({ slot: null, keydesc: key_descs[1][1] });
118 2 : } else if (pool.MetadataVersion == 2) {
119 3 : for (const key_desc of key_descs)
120 3 : result.push({ slot: key_desc[0], keydesc: key_desc[1] });
121 3 : }
122 4 : }
123 :
124 4 : return result;
125 6 : }
126 :
127 : /* Clevis information is returned as an array with elements of type
128 : *
129 : * { slot: number | null, pin: "tang", url: string }
130 : * | { slot: number | null, pin: string }
131 : *
132 : * A V1 pool will have at most one entry in the array, and it will
133 : * have `slot == null`. V2 pools have zero or more entries with
134 : * numbered slots.
135 : */
136 :
137 5 : function get_clevis_infos(pool) {
138 5 : const result = [];
139 :
140 5 : if (!pool.Encrypted)
141 3 : return result;
142 :
143 2 : if (client.stratis_interface_revision < 8) {
144 2 : const clevis_info = pool.ClevisInfo;
145 : /* The ClevisInfo property has type "(b(b(ss)))".
146 :
147 : The first "b" is false when the encryption of the pool is
148 : inconsistent (not all block devices have the same LUKS
149 : header). We pretend that the pool doesn't have any Clevis
150 : infos in that case.
151 :
152 : The second "b" is true when the pool has a Clevis info, and
153 : the two "s" are the pin and its JSON encoded configuration,
154 : respectively.
155 :
156 : Thus, we detect an object of shape
157 :
158 : [ true, [ true, [ "...", "..." ] ] ]
159 :
160 : and fish the strings out of it.
161 :
162 : Cockpit only knows about the "tang" pin in detail, so we
163 : detect that as well and only then decode the configuration
164 : in order to extract the url from it.
165 : */
166 2 : if (clevis_info[0] && clevis_info[1][0]) {
167 2 : if (clevis_info[1][1][0] == "tang") {
168 2 : const config = JSON.parse(clevis_info[1][1][1]);
169 2 : result.push({ slot: null, pin: "tang", url: config.url });
170 2 : } else {
171 2 : result.push({ slot: null, pin: clevis_info[1][1][0] });
172 2 : }
173 2 : }
174 2 : } else {
175 4 : let clevis_infos = pool.ClevisInfos;
176 : /* The ClevisInfos property itself has type "v". For a V1 pool
177 : this variant carries a value of type "b(b(ss)" just as with
178 : the r6 API (see above). For a V2 pool, it carries a "a(u(ss))"
179 : value, an array of "slot number plus pin-plus-config".
180 :
181 : Since the property itself has type "v", we need to unwrap
182 : this variant explicitly.
183 : */
184 :
185 : /* HACK - https://github.com/stratis-storage/stratisd/issues/3895
186 :
187 : Introduced in stratisd 3.8.0, fixed in stratisd 3.8.3.
188 :
189 : Change notifications drop the variant wrapping for some
190 : reason, so we only do the unwrapping when "clevis_infos" is
191 : indeed a variant.
192 : */
193 4 : if ("t" in clevis_infos)
194 2 : clevis_infos = clevis_infos.v;
195 3 : if (pool.MetadataVersion == 1) {
196 2 : if (clevis_infos[0] && clevis_infos[1][0]) {
197 2 : if (clevis_infos[1][1][0] == "tang") {
198 2 : const config = JSON.parse(clevis_infos[1][1][1]);
199 2 : result.push({ slot: null, pin: "tang", url: config.url });
200 2 : } else {
201 2 : result.push({ slot: null, pin: clevis_infos[1][1][0] });
202 2 : }
203 2 : }
204 2 : } else if (pool.MetadataVersion == 2) {
205 3 : for (const clevis_info of clevis_infos) {
206 3 : if (clevis_info[1][0] == "tang") {
207 3 : const config = JSON.parse(clevis_info[1][1]);
208 3 : result.push({ slot: clevis_info[0], pin: "tang", url: config.url });
209 3 : } else {
210 3 : result.push({ slot: clevis_info[0], pin: clevis_info[1][0] });
211 3 : }
212 3 : }
213 3 : }
214 4 : }
215 :
216 4 : return result;
217 5 : }
218 :
219 1 : function bind_keyring(pool, keydesc) {
220 1 : if (client.stratis_interface_revision < 8)
221 0 : return pool.BindKeyring(keydesc);
222 : else
223 1 : return pool.BindKeyring(keydesc, [false, 0]);
224 1 : }
225 :
226 1 : function rebind_keyring(pool, keydesc, slot) {
227 1 : if (client.stratis_interface_revision < 8)
228 0 : return pool.RebindKeyring(keydesc);
229 : else
230 0 : return pool.RebindKeyring(keydesc, [slot !== null, slot || 0]);
231 1 : }
232 :
233 1 : function unbind_keyring(pool, slot) {
234 1 : if (client.stratis_interface_revision < 8)
235 0 : return pool.UnbindKeyring();
236 : else
237 1 : return pool.UnbindKeyring([slot !== null, slot || 0]);
238 1 : }
239 :
240 1 : function bind_clevis(pool, pin, config) {
241 1 : if (client.stratis_interface_revision < 8)
242 0 : return pool.BindClevis(pin, config);
243 : else
244 1 : return pool.BindClevis(pin, config, [false, 0]);
245 1 : }
246 :
247 1 : function unbind_clevis(pool, slot) {
248 1 : if (client.stratis_interface_revision < 8)
249 0 : return pool.UnbindClevis();
250 : else
251 0 : return pool.UnbindClevis([slot !== null, slot || 0]);
252 1 : }
253 :
254 : /* Utilities for key descriptions and passphrases
255 : */
256 :
257 3 : export async function get_stored_keydescs() {
258 0 : return await client.stratis_manager.ListKeys().catch(() => []);
259 3 : }
260 :
261 1 : async function get_new_keydesc(pool) {
262 1 : const key_descs = get_key_descriptions(pool);
263 1 : const stored_keydescs = await get_stored_keydescs();
264 :
265 : // Let's arbitrarily stop after 1000 tries to avoid excessive
266 : // looping. That shouldn't happen, but...
267 1 : let desc;
268 1 : for (let i = 0; i < 1000; i++) {
269 1 : desc = pool.Name + (i > 0 ? "." + i.toFixed() : "");
270 1 : if (!key_descs.find(kd => kd.keydesc == desc) && !stored_keydescs.includes(desc))
271 1 : break;
272 1 : }
273 1 : return desc;
274 1 : }
275 :
276 2 : function PoolPassphrase(tag, pool, main, stored_keydescs, force) {
277 2 : const all_key_descs = get_key_descriptions(pool);
278 2 : const clevis_infos = get_clevis_infos(pool);
279 :
280 1 : const available_key_descs = all_key_descs.filter(kd => !stored_keydescs.includes(kd.keydesc));
281 2 : const can_use_passphrase = available_key_descs.length > 0;
282 2 : const have_stored_passphrase = all_key_descs.length > available_key_descs.length;
283 1 : const need_passphrase = (force || clevis_infos.length == 0) && !have_stored_passphrase;
284 1 : const single_tang_url = (clevis_infos.length == 1 && clevis_infos[0].pin == "tang" && !have_stored_passphrase && clevis_infos[0].url);
285 1 : const only_tang = clevis_infos.every(ci => ci.pin == "tang") && !have_stored_passphrase;
286 :
287 1 : if (can_use_passphrase) {
288 1 : let extra_explanation;
289 1 : if (need_passphrase) {
290 1 : extra_explanation = _("Please provide an existing pool passphrase.");
291 1 : } else if (single_tang_url) {
292 1 : extra_explanation = cockpit.format(_("If the keyserver at $0 is not reachable, you can provide an existing passphrase."), single_tang_url);
293 0 : } else if (only_tang) {
294 0 : extra_explanation = _("If none of the keyservers is reachable, you can provide an existing passphrase.");
295 0 : } else {
296 : /* Clevis other than "tang" and/or passphrases already in the keyring.
297 : */
298 1 : extra_explanation = _("If none of the non-interactive unlock methods works, you can provide an existing passphrase.");
299 1 : }
300 :
301 1 : return PassInput(tag, _("Pool passphrase"), {
302 0 : validate: val => need_passphrase && !val.length && _("Passphrase cannot be empty"),
303 1 : explanation: main + " " + extra_explanation,
304 1 : });
305 1 : } else if (single_tang_url) {
306 1 : return Message(main + " " + cockpit.format(_("The keyserver at $0 must be reachable."), single_tang_url));
307 1 : } else if (only_tang) {
308 2 : return Message(main + " " + _("At least one keyserver must be reachable."));
309 1 : } else {
310 : /* Clevis other than "tang" and/or passphrases already in the keyring.
311 : */
312 1 : return Message(main + " " + _("At least one of the non-interactive unlock methods must work."));
313 1 : }
314 2 : }
315 :
316 3 : async function with_pool_passphrase(pool, passphrase, func) {
317 3 : const stored_keydescs = await get_stored_keydescs();
318 1 : const key_descs = get_key_descriptions(pool).filter(kd => !stored_keydescs.includes(kd.key_descs));
319 :
320 0 : if (!passphrase || key_descs.length == 0)
321 3 : return func();
322 :
323 0 : let err;
324 :
325 0 : for (const kd of key_descs) {
326 0 : try {
327 0 : return await with_stored_passphrase(client, kd.keydesc, passphrase, func);
328 0 : } catch (e) {
329 0 : err = e;
330 0 : }
331 0 : }
332 :
333 0 : throw err;
334 3 : }
335 :
336 : /* Operations
337 : */
338 :
339 5 : function destroy_pool(pool) {
340 5 : return for_each_async(client.stratis_pool_filesystems[pool.path], fsys => destroy_filesystem(fsys))
341 5 : .then(() => client.stratis_manager.DestroyPool(pool.path).then(std_reply));
342 5 : }
343 :
344 9 : function create_fs(pool) {
345 9 : const filesystems = client.stratis_pool_filesystems[pool.path];
346 9 : const stats = client.stratis_pool_stats[pool.path];
347 9 : const forced_options = ["x-systemd.requires=stratis-fstab-setup@" + pool.Uuid + ".service"];
348 :
349 9 : let action_variants;
350 8 : if (!client.in_anaconda_mode()) {
351 8 : action_variants = [
352 8 : { tag: null, Title: _("Create and mount") },
353 8 : { tag: "nomount", Title: _("Create only") },
354 8 : ];
355 0 : } else {
356 1 : action_variants = [
357 1 : { tag: "nomount", Title: _("Create") },
358 1 : ];
359 1 : }
360 :
361 9 : dialog_open({
362 9 : Title: _("Create filesystem"),
363 9 : Fields: [
364 9 : TextInput("name", _("Name"),
365 9 : {
366 9 : validate: name => validate_fs_name(null, name, filesystems)
367 9 : }),
368 9 : Group(_("Stratis filesystem"), [
369 9 : CheckBoxes("set_custom_size", null,
370 9 : {
371 9 : value: {
372 9 : enabled: !pool.Overprovisioning,
373 9 : },
374 9 : fields: [
375 9 : { tag: "enabled", title: _("Set initial size") },
376 9 : ]
377 9 : }),
378 9 : SizeSlider("size", null,
379 9 : {
380 9 : visible: vals => vals.set_custom_size.enabled,
381 9 : min: fsys_min_size,
382 2 : max: pool.Overprovisioning ? stats.pool_total : stats.pool_free,
383 9 : allow_infinite: pool.Overprovisioning,
384 9 : round: fsys_round_size,
385 9 : }),
386 9 : CheckBoxes("set_custom_limit", null,
387 9 : {
388 9 : value: {
389 9 : enabled: false,
390 9 : },
391 9 : fields: [
392 9 : { tag: "enabled", title: _("Limit size") },
393 9 : ]
394 9 : }),
395 9 : SizeSlider("limit", null,
396 9 : {
397 9 : visible: vals => vals.set_custom_limit.enabled,
398 9 : min: fsys_min_size,
399 2 : max: pool.Overprovisioning ? stats.pool_total : stats.pool_free,
400 9 : allow_infinite: true,
401 9 : round: fsys_round_size,
402 9 : }),
403 9 : ]),
404 9 : TextInput("mount_point", _("Mount point"),
405 9 : {
406 9 : validate: (val, values, variant) => {
407 9 : return is_valid_mount_point(client,
408 9 : null,
409 9 : client.add_mount_point_prefix(val),
410 9 : variant == "nomount");
411 9 : }
412 9 : }),
413 9 : mount_options(false, false),
414 9 : at_boot_input(),
415 9 : ],
416 9 : update: update_at_boot_input,
417 9 : Action: {
418 9 : Variants: action_variants,
419 9 : action: async function (vals) {
420 9 : let size_spec = [false, ""]; let limit_spec = [false, ""];
421 9 : if (vals.set_custom_size.enabled)
422 4 : size_spec = [true, vals.size.toString()];
423 9 : if (vals.set_custom_limit.enabled)
424 2 : limit_spec = [true, vals.limit.toString()];
425 9 : const result = await pool.CreateFilesystems([[vals.name, size_spec, limit_spec]]).then(std_reply);
426 8 : if (result[0])
427 8 : await set_mount_options(result[1][0][0], vals, forced_options);
428 9 : }
429 9 : }
430 9 : });
431 9 : }
432 :
433 5 : function delete_pool(pool, card) {
434 5 : const usage = get_active_usage(client, pool.path, _("delete"));
435 :
436 0 : if (usage.Blocking) {
437 0 : dialog_open({
438 0 : Title: cockpit.format(_("$0 is in use"),
439 0 : pool.Name),
440 0 : Body: BlockingMessage(usage)
441 0 : });
442 0 : return;
443 0 : }
444 :
445 5 : dialog_open({
446 5 : Title: cockpit.format(_("Permanently delete $0?"), pool.Name),
447 5 : Teardown: TeardownMessage(usage),
448 5 : Action: {
449 5 : Danger: _("Deleting a Stratis pool will erase all data it contains."),
450 5 : Title: _("Delete"),
451 5 : action: async function () {
452 5 : await teardown_active_usage(client, usage);
453 5 : await destroy_pool(pool);
454 5 : navigate_away_from_card(card);
455 5 : }
456 5 : },
457 5 : Inits: [
458 5 : init_teardown_usage(client, usage)
459 5 : ]
460 5 : });
461 5 : }
462 :
463 2 : function rename_pool(pool) {
464 2 : dialog_open({
465 2 : Title: _("Rename Stratis pool"),
466 2 : Fields: [
467 2 : TextInput("name", _("Name"),
468 2 : {
469 2 : value: pool.Name,
470 2 : validate: name => validate_pool_name(pool, name)
471 2 : })
472 2 : ],
473 2 : Action: {
474 2 : Title: _("Rename"),
475 2 : action: function (vals) {
476 2 : return pool.SetName(vals.name).then(std_reply);
477 2 : }
478 2 : }
479 2 : });
480 2 : }
481 :
482 2 : async function add_disks(pool) {
483 0 : const blockdevs = client.stratis_pool_blockdevs[pool.path] || [];
484 2 : const is_v1_pool = client.stratis_interface_revision < 8 || pool.MetadataVersion == 1;
485 2 : const stored_keydescs = await get_stored_keydescs();
486 :
487 2 : dialog_open({
488 2 : Title: _("Add block devices"),
489 2 : Fields: [
490 2 : SelectOne("tier", _("Tier"), {
491 2 : choices: [
492 2 : { value: "data", title: _("Data") },
493 2 : {
494 2 : value: "cache",
495 2 : title: _("Cache"),
496 2 : }
497 2 : ]
498 2 : }),
499 2 : SelectSpaces("disks", _("Block devices"), {
500 2 : empty_warning: _("No disks are available."),
501 2 : validate: function(disks) {
502 2 : if (disks.length === 0)
503 2 : return _("At least one disk is needed.");
504 2 : },
505 2 : spaces: get_available_spaces()
506 2 : }),
507 2 : ...(is_v1_pool
508 1 : ? [PoolPassphrase("pool_passphrase", pool, _("Adding blockdevices requires unlocking the pool."), stored_keydescs, true)]
509 1 : : []),
510 2 : ],
511 2 : Action: {
512 2 : Title: _("Add"),
513 2 : action: function(vals) {
514 2 : return prepare_available_spaces(client, vals.disks)
515 2 : .then(paths => {
516 2 : const devs = paths.map(p => decode_filename(client.blocks[p].PreferredDevice));
517 :
518 2 : function add() {
519 2 : if (vals.tier == "data") {
520 2 : return pool.AddDataDevs(devs).then(std_reply);
521 2 : } else if (vals.tier == "cache") {
522 2 : const has_cache = blockdevs.some(bd => bd.Tier == 1);
523 2 : const method = has_cache ? "AddCacheDevs" : "InitCache";
524 2 : return pool[method](devs).then(std_reply);
525 2 : }
526 2 : }
527 :
528 2 : return with_pool_passphrase(pool, vals.pool_passphrase, add);
529 2 : });
530 2 : }
531 2 : }
532 2 : });
533 2 : }
534 :
535 18 : function make_stratis_filesystem_pages(parent, pool) {
536 18 : const filesystems = client.stratis_pool_filesystems[pool.path];
537 18 : const stats = client.stratis_pool_stats[pool.path];
538 18 : const forced_options = ["x-systemd.requires=stratis-fstab-setup@" + pool.Uuid + ".service"];
539 :
540 13 : filesystems.forEach((fs, i) => make_stratis_filesystem_page(parent, pool, fs,
541 13 : stats.fsys_offsets[i],
542 13 : forced_options));
543 18 : }
544 :
545 18 : export function make_stratis_pool_page(parent, pool) {
546 18 : const degraded_ops = pool.AvailableActions && pool.AvailableActions !== "fully_operational";
547 4 : const blockdevs = client.stratis_pool_blockdevs[pool.path] || [];
548 18 : const can_grow = blockdevs.some(bd => (bd.NewPhysicalSize[0] &&
549 6 : Number(bd.NewPhysicalSize[1]) > Number(bd.TotalPhysicalSize)));
550 18 : const stats = client.stratis_pool_stats[pool.path];
551 :
552 18 : const use = pool.TotalPhysicalUsed[0] && [Number(pool.TotalPhysicalUsed[1]), Number(pool.TotalPhysicalSize)];
553 :
554 18 : if (should_ignore(client, pool.path))
555 18 : return;
556 :
557 18 : const pool_card = new_card({
558 4 : title: pool.Encrypted ? _("Encrypted Stratis pool") : _("Stratis pool"),
559 18 : next: null,
560 18 : page_location: ["pool", pool.Uuid],
561 18 : page_name: pool.Name,
562 18 : page_icon: VolumeIcon,
563 18 : page_category: PAGE_CATEGORY_VIRTUAL,
564 18 : page_size: (use
565 18 : ? <StorageUsageBar key="s" stats={use} short />
566 4 : : Number(pool.TotalPhysicalSize)),
567 18 : component: StratisPoolCard,
568 18 : props: { pool, degraded_ops, can_grow, stats },
569 18 : actions: [
570 18 : {
571 18 : title: _("Add block devices"),
572 2 : action: () => add_disks(pool),
573 18 : },
574 18 : {
575 18 : title: _("Delete pool"),
576 5 : action: () => delete_pool(pool, pool_card),
577 18 : danger: true,
578 18 : },
579 18 : ],
580 18 : });
581 :
582 18 : let crypto_card = null;
583 6 : if (pool.Encrypted) {
584 6 : crypto_card = new_card({
585 6 : title: _("Encryption tokens"),
586 6 : next: pool_card,
587 6 : component: StratisEncryptionCard,
588 6 : props: { pool },
589 6 : });
590 6 : }
591 :
592 18 : const fsys_card = new_card({
593 18 : title: _("Stratis filesystems"),
594 16 : next: crypto_card || pool_card,
595 18 : has_warning: degraded_ops || can_grow,
596 18 : component: StratisFilesystemsCard,
597 18 : props: { pool, degraded_ops, can_grow, stats },
598 18 : actions: [
599 18 : {
600 18 : title: _("Create new filesystem"),
601 9 : action: () => create_fs(pool),
602 6 : excuse: ((!pool.Overprovisioning && stats.pool_free < fsys_min_size)
603 6 : ? _("Not enough free space")
604 18 : : null),
605 18 : },
606 18 : ],
607 18 : });
608 :
609 18 : const p = new_page(parent, fsys_card);
610 18 : make_stratis_filesystem_pages(p, pool);
611 18 : }
612 :
613 17 : const StratisFilesystemsCard = ({ card, pool, degraded_ops, can_grow, stats }) => {
614 4 : const blockdevs = client.stratis_pool_blockdevs[pool.path] || [];
615 :
616 2 : function grow_blockdevs() {
617 2 : return for_each_async(blockdevs, bd => pool.GrowPhysicalDevice(bd.Uuid));
618 2 : }
619 :
620 17 : const alerts = [];
621 6 : if (can_grow) {
622 6 : alerts.push(
623 6 : <Alert isInline key="unused"
624 6 : variant="warning"
625 6 : title={_("This pool does not use all the space on its block devices.")}>
626 6 : {_("Some block devices of this pool have grown in size after the pool was created. The pool can be safely grown to use the newly available space.")}
627 6 : <div className="storage-alert-actions">
628 6 : <StorageButton onClick={grow_blockdevs}>
629 6 : {_("Grow the pool to take all space")}
630 6 : </StorageButton>
631 6 : </div>
632 6 : </Alert>);
633 6 : }
634 :
635 5 : if (degraded_ops) {
636 0 : const goToStratisLogs = () => cockpit.jump("/system/logs/#/?prio=warn&_SYSTEMD_UNIT=stratisd.service");
637 5 : alerts.push(
638 5 : <Alert isInline key="degraded"
639 5 : variant="warning"
640 5 : title={_("This pool is in a degraded state.")}>
641 5 : <div className="storage-alert-actions">
642 5 : <Button variant="link" isInline onClick={goToStratisLogs}>
643 5 : {_("View logs")}
644 5 : </Button>
645 5 : </div>
646 5 : </Alert>);
647 5 : }
648 :
649 17 : return (
650 17 : <StorageCard card={card} alerts={alerts}>
651 17 : <ChildrenTable
652 17 : emptyCaption={_("No filesystems")}
653 17 : aria-label={_("Stratis filesystems pool")}
654 17 : page={card.page} />
655 17 : </StorageCard>
656 : );
657 17 : };
658 :
659 2 : const StratisV1TokenDescriptions = ({
660 2 : key_descs,
661 2 : add_passphrase,
662 2 : change_passphrase,
663 2 : remove_passphrase,
664 2 : clevis_infos,
665 2 : add_tang,
666 2 : remove_clevis,
667 2 : }) => {
668 2 : let remove_passphrase_excuse;
669 2 : let remove_tang_excuse;
670 :
671 2 : if (key_descs.length + clevis_infos.length <= 1) {
672 2 : remove_passphrase_excuse = _("This passphrase is the only way to unlock the pool and can not be removed.");
673 2 : remove_tang_excuse = _("This keyserver is the only way to unlock the pool and can not be removed.");
674 2 : }
675 :
676 2 : return (
677 2 : <>
678 2 : <StorageDescription title={_("Passphrase")}>
679 2 : <Flex>
680 2 : { key_descs.length == 0
681 2 : ? <FlexItem>
682 2 : <StorageLink
683 2 : onClick={add_passphrase}
684 : >
685 2 : {_("Add passphrase")}
686 2 : </StorageLink>
687 2 : </FlexItem>
688 2 : : <>
689 2 : <FlexItem>
690 2 : <StorageLink
691 0 : onClick={() => change_passphrase(key_descs[0])}
692 : >
693 2 : {_("Change")}
694 2 : </StorageLink>
695 2 : </FlexItem>
696 2 : <FlexItem>
697 2 : <StorageLink
698 0 : onClick={() => remove_passphrase(key_descs[0])}
699 2 : excuse={remove_passphrase_excuse}
700 : >
701 2 : {_("Remove")}
702 2 : </StorageLink>
703 2 : </FlexItem>
704 2 : </>
705 : }
706 2 : </Flex>
707 2 : </StorageDescription>
708 2 : <StorageDescription title={_("Keyserver")}>
709 2 : <Flex>
710 2 : { clevis_infos.length == 0
711 2 : ? <FlexItem>
712 2 : <StorageLink
713 2 : onClick={add_tang}
714 : >
715 2 : {_("Add keyserver")}
716 2 : </StorageLink>
717 2 : </FlexItem>
718 1 : : (clevis_infos[0].pin == "tang"
719 1 : ? <>
720 1 : <FlexItem>
721 1 : {clevis_infos[0].url}
722 1 : </FlexItem>
723 1 : <FlexItem>
724 1 : <StorageLink
725 0 : onClick={() => remove_clevis(clevis_infos[0])}
726 1 : excuse={remove_tang_excuse}
727 : >
728 1 : {_("Remove")}
729 1 : </StorageLink>
730 1 : </FlexItem>
731 1 : </>
732 1 : : <FlexItem>
733 1 : {cockpit.format(_("Clevis \"$0\""), clevis_infos[0].pin)}
734 1 : </FlexItem>
735 : )
736 : }
737 2 : </Flex>
738 2 : </StorageDescription>
739 2 : </>
740 : );
741 2 : };
742 :
743 2 : const StratisV2TokenTable = ({
744 2 : pool,
745 2 : tokens,
746 2 : add_passphrase,
747 2 : change_passphrase,
748 2 : remove_passphrase,
749 2 : add_tang,
750 2 : remove_clevis,
751 2 : }) => {
752 2 : let remove_excuse;
753 2 : if (tokens.length <= 1)
754 2 : remove_excuse = _("Last token can not be removed");
755 :
756 2 : const free_token_slots = (pool.FreeTokenSlots && pool.FreeTokenSlots[0])
757 2 : ? pool.FreeTokenSlots[1]
758 1 : : 15 - tokens.length;
759 :
760 2 : let add_excuse;
761 2 : if (free_token_slots <= 0)
762 1 : add_excuse = _("No more space for passphrases or keyservers.");
763 :
764 2 : function make_row(info) {
765 2 : let test_location;
766 2 : let description;
767 2 : let actions;
768 :
769 2 : if (info.keydesc) {
770 2 : test_location = "passphrase";
771 2 : description = _("Passphrase");
772 2 : actions = [
773 2 : {
774 2 : title: _("Change"),
775 1 : action: () => change_passphrase(info),
776 2 : },
777 2 : {
778 2 : title: _("Remove"),
779 1 : action: () => remove_passphrase(info),
780 2 : excuse: remove_excuse,
781 2 : danger: true,
782 2 : },
783 2 : ];
784 2 : } else if (info.pin == "tang") {
785 2 : test_location = info.url;
786 2 : description = info.url;
787 2 : actions = [
788 2 : {
789 2 : title: _("Remove"),
790 0 : action: () => remove_clevis(info),
791 2 : excuse: remove_excuse,
792 2 : danger: true,
793 2 : },
794 2 : ];
795 2 : } else {
796 2 : test_location = info.pin;
797 2 : description = cockpit.format(_("Clevis \"$0\""), info.pin);
798 2 : actions = [
799 2 : {
800 2 : title: _("Remove"),
801 1 : action: () => remove_clevis(info),
802 2 : excuse: remove_excuse,
803 2 : danger: true,
804 2 : },
805 2 : ];
806 2 : }
807 :
808 2 : return (
809 2 : <Tr key={info.slot} data-test-row-location={test_location}>
810 2 : <Td>{cockpit.format(_("Slot $0"), info.slot)}</Td>
811 2 : <Td>{description}</Td>
812 2 : <Td modifier="nowrap" className="pf-v6-c-table__action">
813 2 : <Actions onlyMenu actions={actions} />
814 2 : </Td>
815 2 : </Tr>
816 : );
817 2 : }
818 :
819 2 : const actions = [
820 2 : {
821 2 : title: _("Add passphrase"),
822 2 : action: add_passphrase,
823 2 : excuse: add_excuse
824 2 : },
825 2 : {
826 2 : title: _("Add keyserver"),
827 2 : action: add_tang,
828 2 : excuse: add_excuse
829 2 : },
830 2 : ];
831 :
832 2 : const v2_table = (
833 2 : <Table variant="compact">
834 2 : <Tbody>
835 2 : { tokens.map(make_row) }
836 2 : </Tbody>
837 2 : </Table>
838 : );
839 :
840 2 : return [v2_table, actions];
841 2 : };
842 :
843 4 : const StratisEncryptionCard = ({ card, pool }) => {
844 4 : const key_descs = get_key_descriptions(pool);
845 4 : const clevis_infos = get_clevis_infos(pool);
846 4 : const is_v1_pool = client.stratis_interface_revision < 8 || pool.MetadataVersion == 1;
847 :
848 1 : const tokens = key_descs.concat(clevis_infos).sort((a, b) => a.slot - b.slot);
849 :
850 1 : async function add_passphrase() {
851 1 : const stored_keydescs = await get_stored_keydescs();
852 :
853 1 : dialog_open({
854 1 : Title: _("Add passphrase"),
855 1 : Fields: [
856 1 : PassInput("passphrase", _("Passphrase"),
857 0 : { validate: val => !val.length && _("Passphrase cannot be empty") }),
858 1 : PassInput("passphrase2", _("Confirm"),
859 0 : { validate: (val, vals) => vals.passphrase.length && vals.passphrase != val && _("Passphrases do not match") }),
860 1 : PoolPassphrase("pool_passphrase", pool, _("Adding a passphrase requires unlocking the pool."), stored_keydescs),
861 1 : ],
862 1 : Action: {
863 1 : Title: _("Save"),
864 1 : action: async vals => {
865 1 : const key_desc = await get_new_keydesc(pool);
866 1 : return await with_pool_passphrase(pool, vals.pool_passphrase,
867 1 : () => with_stored_passphrase(client, key_desc, vals.passphrase,
868 1 : () => bind_keyring(pool, key_desc).then(std_reply)));
869 1 : }
870 1 : }
871 1 : });
872 1 : }
873 :
874 1 : async function change_passphrase(info) {
875 1 : const stored_keydescs = await get_stored_keydescs();
876 1 : const keydesc_set = stored_keydescs.includes(info.keydesc);
877 :
878 1 : dialog_open({
879 1 : Title: _("Change passphrase"),
880 1 : Fields: [
881 1 : PassInput("old_passphrase", _("Old passphrase"),
882 1 : {
883 1 : visible: vals => !keydesc_set,
884 0 : validate: val => !val.length && _("Passphrase cannot be empty")
885 1 : }),
886 1 : PassInput("new_passphrase", _("New passphrase"),
887 0 : { validate: val => !val.length && _("Passphrase cannot be empty") }),
888 1 : PassInput("new_passphrase2", _("Confirm"),
889 0 : { validate: (val, vals) => vals.new_passphrase.length && vals.new_passphrase != val && _("Passphrases do not match") })
890 1 : ],
891 1 : Action: {
892 1 : Title: _("Save"),
893 1 : action: async vals => {
894 1 : const new_keydesc = await get_new_keydesc(pool);
895 :
896 1 : function rebind() {
897 1 : return with_stored_passphrase(client, new_keydesc, vals.new_passphrase,
898 1 : () => rebind_keyring(pool, new_keydesc, info.slot).then(std_reply));
899 1 : }
900 :
901 1 : if (vals.old_passphrase) {
902 1 : await with_stored_passphrase(client, info.keydesc, vals.old_passphrase, rebind);
903 0 : } else {
904 0 : await rebind();
905 0 : }
906 1 : }
907 1 : }
908 1 : });
909 1 : }
910 :
911 1 : function remove_passphrase(info) {
912 1 : dialog_open({
913 1 : Title: _("Remove passphrase?"),
914 1 : Body: <div>
915 1 : <p className="slot-warning">{ fmt_to_fragments(_("Passphrase removal may prevent unlocking $0."), <b>{pool.Name}</b>) }</p>
916 1 : </div>,
917 1 : Action: {
918 1 : DangerButton: true,
919 1 : Title: _("Remove"),
920 1 : action: function (vals) {
921 1 : return unbind_keyring(pool, info.slot).then(std_reply);
922 1 : }
923 1 : }
924 1 : });
925 1 : }
926 :
927 1 : async function add_tang() {
928 1 : const stored_keydescs = await get_stored_keydescs();
929 :
930 1 : dialog_open({
931 1 : Title: _("Add Tang keyserver"),
932 1 : Fields: [
933 1 : TextInput("tang_url", _("Keyserver address"),
934 1 : {
935 1 : validate: validate_url,
936 1 : }),
937 1 : PoolPassphrase("pool_passphrase", pool, _("Adding a keyserver requires unlocking the pool."), stored_keydescs),
938 1 : ],
939 1 : Action: {
940 1 : Title: _("Save"),
941 1 : action: function (vals, progress) {
942 1 : return get_tang_adv(vals.tang_url)
943 1 : .then(adv => {
944 1 : function bind() {
945 1 : return bind_clevis(pool, "tang", JSON.stringify({ url: vals.tang_url, adv }))
946 1 : .then(std_reply);
947 1 : }
948 1 : confirm_tang_trust(vals.tang_url, adv,
949 1 : () => with_pool_passphrase(pool, vals.pool_passphrase, bind));
950 1 : });
951 1 : }
952 1 : }
953 1 : });
954 1 : }
955 :
956 1 : function remove_clevis(info) {
957 1 : dialog_open({
958 0 : Title: info.pin == "tang" ? _("Remove Tang keyserver?") : _("Remove Clevis token?"),
959 1 : Body: <div>
960 0 : <p>{ fmt_to_fragments(_("Remove $0?"), <b>{info.pin == "tang" ? info.url : info.pin}</b>) }</p>
961 1 : <p className="slot-warning">{ fmt_to_fragments(_("Removal may prevent unlocking $0."), <b>{pool.Name}</b>) }</p>
962 1 : </div>,
963 1 : Action: {
964 1 : DangerButton: true,
965 1 : Title: _("Remove"),
966 1 : action: function (vals) {
967 1 : return unbind_clevis(pool, info.slot).then(std_reply);
968 1 : }
969 1 : }
970 1 : });
971 1 : }
972 :
973 4 : let v1_descriptions;
974 4 : let v2_table;
975 4 : let actions;
976 :
977 3 : if (is_v1_pool) {
978 3 : v1_descriptions = StratisV1TokenDescriptions({
979 3 : key_descs,
980 3 : add_passphrase,
981 3 : change_passphrase,
982 3 : remove_passphrase,
983 3 : clevis_infos,
984 3 : add_tang,
985 3 : remove_clevis,
986 3 : });
987 2 : } else if (pool.MetadataVersion == 2) {
988 3 : [v2_table, actions] = StratisV2TokenTable({
989 3 : pool,
990 3 : tokens,
991 3 : add_passphrase,
992 3 : change_passphrase,
993 3 : remove_passphrase,
994 3 : add_tang,
995 3 : remove_clevis,
996 3 : });
997 3 : }
998 :
999 4 : return (
1000 4 : <StorageCard card={card} actions={<Actions actions={actions} />}>
1001 4 : <CardBody>
1002 4 : <DescriptionList className="pf-m-horizontal-on-sm">
1003 4 : <StorageDescription
1004 4 : title={_("Metadata format")}
1005 2 : value={is_v1_pool ? "V1" : "V" + pool.MetadataVersion}
1006 3 : help={is_v1_pool && _("Pools with metadata format V1 are restricted to at most one passphrase and at most one keyserver.")}
1007 4 : />
1008 4 : {v1_descriptions}
1009 4 : </DescriptionList>
1010 4 : </CardBody>
1011 4 : {v2_table}
1012 4 : </StorageCard>
1013 : );
1014 4 : };
1015 :
1016 17 : const StratisPoolCard = ({ card, pool, degraded_ops, can_grow, stats }) => {
1017 17 : const use = pool.TotalPhysicalUsed[0] && [Number(pool.TotalPhysicalUsed[1]), Number(pool.TotalPhysicalSize)];
1018 :
1019 17 : return (
1020 17 : <StorageCard card={card}>
1021 17 : <CardBody>
1022 17 : <DescriptionList className="pf-m-horizontal-on-sm">
1023 17 : <StorageDescription title={_("Name")}
1024 17 : value={pool.Name}
1025 2 : action={<StorageLink onClick={() => rename_pool(pool)}>
1026 17 : {_("edit")}
1027 17 : </StorageLink>} />
1028 17 : <StorageDescription title={_("UUID")} value={pool.Uuid} />
1029 17 : { use &&
1030 17 : <StorageDescription title={_("Usage")}>
1031 17 : <StorageUsageBar stats={use} critical={0.80} />
1032 17 : </StorageDescription>
1033 : }
1034 17 : <StorageDescription title={_("Overprovisioning")}>
1035 17 : <StorageOnOff state={pool.Overprovisioning}
1036 17 : aria-label={_("Allow overprovisioning")}
1037 2 : onChange={() => client.stratis_set_property(pool,
1038 2 : "Overprovisioning",
1039 2 : "b", !pool.Overprovisioning)}
1040 17 : excuse={(pool.Overprovisioning && stats.fsys_total_size > stats.pool_total)
1041 11 : ? _("Virtual filesystem sizes are larger than the pool. Overprovisioning can not be disabled.")
1042 16 : : null} />
1043 17 : </StorageDescription>
1044 17 : { !pool.Overprovisioning &&
1045 6 : <StorageDescription title={_("Allocated")}>
1046 6 : <StorageUsageBar stats={[stats.fsys_total_size, stats.pool_total]} critical={2} />
1047 6 : </StorageDescription>
1048 : }
1049 17 : </DescriptionList>
1050 17 : </CardBody>
1051 17 : <CardHeader><strong>{_("Block devices")}</strong></CardHeader>
1052 17 : <PageTable
1053 17 : emptyCaption={_("No block devices found")}
1054 17 : aria-label={_("Stratis block devices")}
1055 17 : crossrefs={get_crossrefs(pool)} />
1056 17 : </StorageCard>
1057 : );
1058 17 : };
|