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

Generated by: LCOV version 2.0-1