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