LCOV - code coverage report
Current view: top level - pkg/storaged/stratis - pool.jsx Coverage Total Hit
Test: cockpit Lines: 94.1 % 803 756
Test Date: 2026-07-17 07:33:01

            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 : };
        

Generated by: LCOV version 2.0-1