LCOV - code coverage report
Current view: top level - pkg/storaged/lvm2 - create-logical-volume-dialog.jsx Coverage Total Hit
Test: cockpit Lines: 85.4 % 247 211
Test Date: 2026-06-25 11:17:56

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2016 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          113 : import cockpit from "cockpit";
       7              : 
       8              : import { dialog_open, TextInput, SelectOne, Message, SelectSpaces, SelectOneRadio, SizeSlider, CheckBoxes } from "../dialog.jsx";
       9              : import { validate_lvm2_name } from "../utils.js";
      10              : 
      11              : import { pvs_to_spaces, next_default_logical_volume_name } from "./utils.jsx";
      12              : import { getPackageManager } from "packagemanager.js";
      13              : 
      14          113 : const _ = cockpit.gettext;
      15              : 
      16            0 : async function install_package(name, progress) {
      17            0 :     let package_manager = null;
      18            0 :     try {
      19            0 :         package_manager = await getPackageManager();
      20            0 :     } catch (err) {
      21            0 :         return;
      22            0 :     }
      23              : 
      24            0 :     return package_manager.check_missing_packages([name], p => progress(_("Checking installed software"), p.cancel))
      25            0 :             .then(data => {
      26            0 :                 if (data.unavailable_names.length > 0)
      27            0 :                     return Promise.reject(new Error(
      28            0 :                         cockpit.format(_("$0 is not available from any repository."), data.unavailable_names[0])));
      29              :                 // let's be cautious here, we really don't expect removals
      30            0 :                 if (data.remove_names.length > 0)
      31            0 :                     return Promise.reject(new Error(
      32            0 :                         cockpit.format(_("Installing $0 would remove $1."), name, data.remove_names[0])));
      33              : 
      34            0 :                 return package_manager.install_missing_packages(data, p => progress(_("Installing packages"), p.cancel));
      35            0 :             });
      36            0 : }
      37              : 
      38           10 : export function create_logical_volume(client, vgroup) {
      39           10 :     if (vgroup.FreeSize == 0)
      40           10 :         return;
      41              : 
      42           10 :     const pvs_as_spaces = pvs_to_spaces(client, client.vgroups_pvols[vgroup.path].filter(pvol => pvol.FreeSize > 0));
      43              : 
      44           10 :     const can_do_layouts = !!vgroup.CreatePlainVolumeWithLayout && pvs_as_spaces.length > 1;
      45              : 
      46           10 :     const purposes = [
      47           10 :         {
      48           10 :             value: "block",
      49           10 :             title: _("Block device for filesystems"),
      50           10 :         },
      51           10 :         { value: "pool", title: _("Pool for thinly provisioned volumes") }
      52              :         /* Not implemented
      53              :            { value: "cache", Title: _("Cache") }
      54              :         */
      55           10 :     ];
      56              : 
      57           10 :     const layouts = [
      58           10 :         {
      59           10 :             value: "linear",
      60           10 :             title: _("Linear"),
      61           10 :             min_pvs: 1,
      62           10 :         },
      63           10 :         {
      64           10 :             value: "raid0",
      65           10 :             title: _("Striped (RAID 0)"),
      66           10 :             min_pvs: 2,
      67           10 :         },
      68           10 :         {
      69           10 :             value: "raid1",
      70           10 :             title: _("Mirrored (RAID 1)"),
      71           10 :             min_pvs: 2,
      72           10 :         },
      73           10 :         {
      74           10 :             value: "raid10",
      75           10 :             title: _("Striped and mirrored (RAID 10)"),
      76           10 :             min_pvs: 4,
      77           10 :         },
      78           10 :         {
      79           10 :             value: "raid5",
      80           10 :             title: _("Distributed parity (RAID 5)"),
      81           10 :             min_pvs: 3,
      82           10 :         },
      83           10 :         {
      84           10 :             value: "raid6",
      85           10 :             title: _("Double distributed parity (RAID 6)"),
      86           10 :             min_pvs: 5,
      87           10 :         }
      88           10 :     ];
      89              : 
      90           10 :     const vdo_package = client.get_config("vdo_package", null);
      91            0 :     const need_vdo_install = vdo_package && !(client.features.lvm_create_vdo || client.features.legacy_vdo);
      92              : 
      93           10 :     if (client.features.lvm_create_vdo || client.features.legacy_vdo || vdo_package)
      94            0 :         purposes.push({ value: "vdo", title: _("VDO filesystem volume (compression/deduplication)") });
      95              : 
      96              :     /* For layouts with redundancy, CreatePlainVolumeWithLayout will
      97              :      * create as many subvolumes as there are selected PVs.  This has
      98              :      * the nice effect of making the calculation of the maximum size of
      99              :      * such a volume trivial.
     100              :      */
     101              : 
     102            8 :     function max_size(vals) {
     103            8 :         const layout = vals.layout;
     104            8 :         const pvs = vals.pvs.map(s => s.pvol);
     105            8 :         const n_pvs = pvs.length;
     106            8 :         const sum = pvs.reduce((sum, pv) => sum + pv.FreeSize, 0);
     107            8 :         const min = Math.min.apply(null, pvs.map(pv => pv.FreeSize));
     108              : 
     109            5 :         function metasize(datasize) {
     110            5 :             const default_regionsize = 2 * 1024 * 1024;
     111            5 :             const regions = Math.ceil(datasize / default_regionsize);
     112            5 :             const bytes = 2 * 4096 + Math.ceil(regions / 8);
     113            5 :             return vgroup.ExtentSize * Math.ceil(bytes / vgroup.ExtentSize);
     114            5 :         }
     115              : 
     116            8 :         if (layout == "linear") {
     117            8 :             return sum;
     118            3 :         } else if (layout == "raid0" && n_pvs >= 2) {
     119            3 :             return n_pvs * min;
     120            3 :         } else if (layout == "raid1" && n_pvs >= 2) {
     121            3 :             return min - metasize(min);
     122            3 :         } else if (layout == "raid10" && n_pvs >= 4) {
     123            3 :             return Math.floor(n_pvs / 2) * (min - metasize(min));
     124            3 :         } else if ((layout == "raid4" || layout == "raid5") && n_pvs >= 3) {
     125            5 :             return (n_pvs - 1) * (min - metasize(min));
     126            3 :         } else if (layout == "raid6" && n_pvs >= 5) {
     127            3 :             return (n_pvs - 2) * (min - metasize(min));
     128            3 :         } else
     129            0 :             return 0; // not-covered: internal error
     130            8 :     }
     131              : 
     132           10 :     const layout_descriptions = {
     133           10 :         linear: _("Data will be stored on the selected physical volumes without any additional redundancy or performance improvements."),
     134           10 :         raid0: _("Data will be stored on the selected physical volumes in an alternating fashion to improve performance. At least two volumes need to be selected."),
     135           10 :         raid1: _("Data will be stored as two or more copies on the selected physical volumes, to improve reliability. At least two volumes need to be selected."),
     136           10 :         raid10: _("Data will be stored as two copies and also in an alternating fashion on the selected physical volumes, to improve both reliability and performance. At least four volumes need to be selected."),
     137           10 :         raid4: _("Data will be stored on the selected physical volumes so that one of them can be lost without affecting the data. At least three volumes need to be selected."),
     138           10 :         raid5: _("Data will be stored on the selected physical volumes so that one of them can be lost without affecting the data. Data is also stored in an alternating fashion to improve performance. At least three volumes need to be selected."),
     139           10 :         raid6: _("Data will be stored on the selected physical volumes so that up to two of them can be lost at the same time without affecting the data. Data is also stored in an alternating fashion to improve performance. At least five volumes need to be selected."),
     140           10 :     };
     141              : 
     142           10 :     function compute_layout_choices(pvs) {
     143           10 :         return layouts.filter(l => l.min_pvs <= pvs.length);
     144           10 :     }
     145              : 
     146           10 :     for (const lay of layouts)
     147           10 :         lay.disabled = pvs_as_spaces.length < lay.min_pvs;
     148              : 
     149           10 :     function min_pvs_explanation(pvs, min) {
     150           10 :         if (pvs.length <= min)
     151            7 :             return cockpit.format(_("All $0 selected physical volumes are needed for the chosen layout."),
     152            7 :                                   pvs.length);
     153            7 :         return null;
     154           10 :     }
     155              : 
     156           10 :     dialog_open({
     157           10 :         Title: _("Create logical volume"),
     158           10 :         Fields: [
     159           10 :             TextInput("name", _("Name"),
     160           10 :                       {
     161           10 :                           value: next_default_logical_volume_name(client, vgroup, "lvol"),
     162           10 :                           validate: validate_lvm2_name
     163           10 :                       }),
     164           10 :             SelectOne("purpose", _("Purpose"),
     165           10 :                       {
     166           10 :                           value: "block",
     167           10 :                           choices: purposes
     168           10 :                       }),
     169           10 :             Message(cockpit.format(_("The $0 package will be installed to create VDO devices."), vdo_package),
     170           10 :                     {
     171            0 :                         visible: vals => vals.purpose === 'vdo' && need_vdo_install,
     172           10 :                     }),
     173           10 :             SelectSpaces("pvs", _("Physical Volumes"),
     174           10 :                          {
     175           10 :                              spaces: pvs_as_spaces,
     176           10 :                              value: pvs_as_spaces,
     177            7 :                              visible: vals => can_do_layouts && vals.purpose === 'block',
     178           10 :                              min_selected: 1,
     179            7 :                              validate: (val, vals) => {
     180            3 :                                  if (vals.layout == "raid10" && (vals.pvs.length % 2) !== 0)
     181            1 :                                      return _("RAID10 needs an even number of physical volumes");
     182            7 :                              },
     183           10 :                              explanation: min_pvs_explanation(pvs_as_spaces, 1)
     184           10 :                          }),
     185           10 :             SelectOneRadio("layout", _("Layout"),
     186           10 :                            {
     187           10 :                                vertical: true,
     188           10 :                                value: "linear",
     189           10 :                                choices: compute_layout_choices(pvs_as_spaces),
     190            7 :                                visible: vals => can_do_layouts && vals.purpose === 'block',
     191           10 :                                explanation: layout_descriptions.linear
     192           10 :                            }),
     193           10 :             SizeSlider("size", _("Size"),
     194           10 :                        {
     195           10 :                            visible: vals => vals.purpose !== 'vdo',
     196           10 :                            max: vgroup.FreeSize,
     197           10 :                            round: vgroup.ExtentSize
     198           10 :                        }),
     199              :             /* VDO parameters */
     200           10 :             SizeSlider("vdo_psize", _("Size"),
     201           10 :                        {
     202           10 :                            visible: vals => vals.purpose === 'vdo',
     203           10 :                            min: 5 * 1024 * 1024 * 1024,
     204           10 :                            max: vgroup.FreeSize,
     205           10 :                            round: vgroup.ExtentSize
     206           10 :                        }),
     207           10 :             SizeSlider("vdo_lsize", _("Logical size"),
     208           10 :                        {
     209           10 :                            visible: vals => vals.purpose === 'vdo',
     210           10 :                            value: vgroup.FreeSize,
     211              :                            // visually point out that this can be over-provisioned
     212           10 :                            max: vgroup.FreeSize * 3,
     213           10 :                            allow_infinite: true,
     214           10 :                            round: vgroup.ExtentSize
     215           10 :                        }),
     216              : 
     217           10 :             CheckBoxes("vdo_options", _("Options"),
     218           10 :                        {
     219           10 :                            visible: vals => vals.purpose === 'vdo',
     220           10 :                            fields: [
     221           10 :                                {
     222           10 :                                    tag: "compression",
     223           10 :                                    title: _("Compression"),
     224           10 :                                    tooltip: _("Save space by compressing individual blocks with LZ4")
     225           10 :                                },
     226           10 :                                {
     227           10 :                                    tag: "deduplication",
     228           10 :                                    title: _("Deduplication"),
     229           10 :                                    tooltip: _("Save space by storing identical data blocks just once")
     230           10 :                                },
     231           10 :                            ],
     232           10 :                            value: {
     233           10 :                                compression: true,
     234           10 :                                deduplication: true,
     235           10 :                            }
     236           10 :                        }),
     237           10 :         ],
     238            8 :         update: (dlg, vals, trigger) => {
     239            8 :             if (vals.purpose == 'block' && (trigger == "layout" || trigger == "pvs" || trigger == "purpose")) {
     240            8 :                 for (const lay of layouts) {
     241            8 :                     if (lay.value == vals.layout) {
     242            8 :                         dlg.set_options("pvs", {
     243            8 :                             min_selected: lay.min_pvs,
     244            8 :                             explanation: min_pvs_explanation(vals.pvs, lay.min_pvs)
     245            8 :                         });
     246            8 :                     }
     247            8 :                 }
     248            8 :                 dlg.set_options("layout",
     249            8 :                                 {
     250            8 :                                     choices: compute_layout_choices(vals.pvs),
     251            8 :                                     explanation: layout_descriptions[vals.layout]
     252            8 :                                 });
     253            8 :                 const max = max_size(vals);
     254            8 :                 const old_max = dlg.get_options("size").max;
     255            8 :                 if (vals.size > max || vals.size == old_max)
     256            8 :                     dlg.set_values({ size: max });
     257            8 :                 dlg.set_options("size", { max });
     258            1 :             } else if (trigger == "purpose") {
     259            1 :                 dlg.set_options("size", { max: vgroup.FreeSize });
     260            1 :             }
     261            8 :         },
     262           10 :         Action: {
     263           10 :             Title: _("Create"),
     264            9 :             action: (vals, progress) => {
     265            9 :                 if (vals.purpose == "block") {
     266            9 :                     if (!can_do_layouts)
     267            0 :                         return vgroup.CreatePlainVolume(vals.name, vals.size, { });
     268            7 :                     else {
     269            7 :                         return vgroup.CreatePlainVolumeWithLayout(vals.name, vals.size, vals.layout,
     270            7 :                                                                   vals.pvs.map(spc => spc.block.path),
     271            7 :                                                                   { });
     272            7 :                     }
     273            1 :                 } else if (vals.purpose == "pool")
     274            0 :                     return vgroup.CreateThinPoolVolume(vals.name, vals.size, { });
     275            0 :                 else if (vals.purpose == "vdo") {
     276            0 :                     return (need_vdo_install ? install_package(vdo_package, progress) : Promise.resolve())
     277            0 :                             .then(() => {
     278            0 :                                 progress(_("Creating VDO device")); // not cancellable any more
     279            0 :                                 return vgroup.CreateVDOVolume(
     280              :                                 // HACK: emulate lvcreate's automatic pool name creation until
     281              :                                 // https://github.com/storaged-project/udisks/issues/939
     282            0 :                                     vals.name, next_default_logical_volume_name(client, vgroup, "vpool"),
     283            0 :                                     vals.vdo_psize, vals.vdo_lsize,
     284            0 :                                     0, // default index memory
     285            0 :                                     vals.vdo_options.compression, vals.vdo_options.deduplication,
     286            0 :                                     "auto", { });
     287            0 :                             });
     288            0 :                 }
     289            9 :             }
     290           10 :         }
     291           10 :     });
     292           10 : }
        

Generated by: LCOV version 2.0-1