LCOV - code coverage report
Current view: top level - pkg/storaged/lvm2 - volume-group.jsx Coverage Total Hit
Test: cockpit Lines: 91.9 % 270 248
Test Date: 2026-07-03 07:31:16

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

Generated by: LCOV version 2.0-1