Line data Source code
1 : /*
2 : * Copyright (C) 2015 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 113 : import cockpit from "cockpit";
7 :
8 : import * as service from "service";
9 : import * as timeformat from "timeformat";
10 :
11 113 : const _ = cockpit.gettext;
12 113 : const C_ = cockpit.gettext;
13 :
14 113 : export const BTRFS_TOOL_MOUNT_PATH = "/run/cockpit/btrfs/";
15 :
16 : /* UTILITIES
17 : */
18 :
19 112 : export function compare_versions(a, b) {
20 112 : function to_ints(str) {
21 12 : return str.split(".").map(function (s) { return s ? parseInt(s, 10) : 0 });
22 112 : }
23 :
24 112 : const a_ints = to_ints(a);
25 112 : const b_ints = to_ints(b);
26 112 : const len = Math.min(a_ints.length, b_ints.length);
27 112 : let i;
28 :
29 112 : for (i = 0; i < len; i++) {
30 112 : if (a_ints[i] == b_ints[i])
31 112 : continue;
32 112 : return a_ints[i] - b_ints[i];
33 112 : }
34 :
35 34 : return a_ints.length - b_ints.length;
36 112 : }
37 :
38 108 : export function parse_options(options) {
39 108 : if (options)
40 105 : return (options.split(",")
41 105 : .map(function (s) { return s.trim() })
42 104 : .filter(function (s) { return s != "" }));
43 : else
44 107 : return [];
45 108 : }
46 :
47 64 : export function unparse_options(split) {
48 64 : return split.join(",");
49 64 : }
50 :
51 109 : export function extract_option(split, opt) {
52 109 : const index = split.indexOf(opt);
53 64 : if (index >= 0) {
54 64 : split.splice(index, 1);
55 64 : return true;
56 64 : } else {
57 109 : return false;
58 109 : }
59 109 : }
60 :
61 16 : export function edit_crypto_config(block, modify) {
62 16 : let old_config;
63 16 : let new_config;
64 :
65 16 : function commit() {
66 16 : new_config[1]["track-parents"] = { t: 'b', v: true };
67 16 : if (old_config)
68 1 : return block.UpdateConfigurationItem(old_config, new_config, { });
69 : else
70 4 : return block.AddConfigurationItem(new_config, { });
71 16 : }
72 :
73 16 : return block.GetSecretConfiguration({}).then(
74 16 : function (items) {
75 15 : old_config = items.find(c => c[0] == "crypttab");
76 1 : new_config = ["crypttab", old_config ? Object.assign({ }, old_config[1]) : { }];
77 :
78 : // UDisks insists on always having a "passphrase-contents" field when
79 : // adding a crypttab entry, but doesn't include one itself when returning
80 : // an entry without a stored passphrase.
81 : //
82 16 : if (!new_config[1]['passphrase-contents'])
83 16 : new_config[1]['passphrase-contents'] = { t: 'ay', v: encode_filename("") };
84 :
85 16 : return modify(new_config[1], commit);
86 16 : });
87 16 : }
88 :
89 13 : export function set_crypto_options(block, readonly, auto, nofail, netdev) {
90 13 : return edit_crypto_config(block, (config, commit) => {
91 1 : const opts = config.options ? parse_options(decode_filename(config.options.v)) : [];
92 7 : if (readonly !== null) {
93 7 : extract_option(opts, "readonly");
94 7 : extract_option(opts, "read-only");
95 7 : if (readonly)
96 0 : opts.push("readonly");
97 7 : }
98 13 : if (auto !== null) {
99 13 : extract_option(opts, "noauto");
100 13 : if (!auto)
101 9 : opts.push("noauto");
102 13 : }
103 7 : if (nofail !== null) {
104 7 : extract_option(opts, "nofail");
105 7 : if (nofail)
106 4 : opts.push("nofail");
107 7 : }
108 7 : if (netdev !== null) {
109 7 : extract_option(opts, "_netdev");
110 7 : if (netdev)
111 2 : opts.push("_netdev");
112 7 : }
113 13 : config.options = { t: 'ay', v: encode_filename(unparse_options(opts)) };
114 13 : return commit();
115 13 : });
116 13 : }
117 :
118 9 : export function set_crypto_auto_option(block, flag) {
119 9 : return set_crypto_options(block, null, flag, null, null);
120 9 : }
121 :
122 113 : export let hostnamed = cockpit.dbus("org.freedesktop.hostname1").proxy();
123 :
124 : // for unit tests
125 : let orig_hostnamed;
126 :
127 : export function mock_hostnamed(value) {
128 : if (value) {
129 : orig_hostnamed = hostnamed;
130 : hostnamed = value;
131 : } else {
132 : hostnamed = orig_hostnamed;
133 : }
134 : }
135 :
136 51 : export function flatten(array_of_arrays) {
137 51 : if (array_of_arrays.length > 0)
138 20 : return Array.prototype.concat.apply([], array_of_arrays);
139 : else
140 51 : return [];
141 51 : }
142 :
143 112 : export const decode_filename = encoded => window.atob(encoded).replace('\u0000', '');
144 55 : export const encode_filename = decoded => window.btoa(decoded + '\u0000');
145 :
146 106 : export function get_block_mntopts(config) {
147 : // treat an absent field as "default", like util-linux
148 12 : return (config.opts ? decode_filename(config.opts.v) : "defaults");
149 106 : }
150 :
151 110 : export function fmt_size(bytes) {
152 110 : return cockpit.format_bytes(bytes);
153 110 : }
154 :
155 62 : export function fmt_size_long(bytes) {
156 62 : const with_decimal_unit = cockpit.format_bytes(bytes);
157 62 : const with_binary_unit = cockpit.format_bytes(bytes, { base2: true });
158 : /* Translators: Used in "..." */
159 62 : return with_decimal_unit + ", " + with_binary_unit + ", " + bytes + " " + C_("format-bytes", "bytes");
160 62 : }
161 :
162 : export function fmt_rate(bytes_per_sec) {
163 : return cockpit.format_bytes_per_sec(bytes_per_sec);
164 : }
165 :
166 1 : export function format_temperature(kelvin) {
167 1 : const celsius = kelvin - 273.15;
168 1 : const fahrenheit = 9.0 * celsius / 5.0 + 32.0;
169 1 : return celsius.toFixed(1) + "° C / " + fahrenheit.toFixed(1) + "° F";
170 1 : }
171 :
172 101 : export function format_fsys_usage(used, total) {
173 101 : let text = "";
174 101 : let parts = cockpit.format_bytes(total, { separate: true, precision: 2 });
175 101 : text = " / " + parts.join(" ");
176 101 : const unit = parts[1];
177 :
178 : // FIXME: passing explicit unit is deprecated, redesign this
179 101 : parts = cockpit.format_bytes(used, unit, { separate: true, precision: 2 });
180 101 : return parts[0] + text;
181 101 : }
182 :
183 5 : export function format_delay(d) {
184 5 : return timeformat.distanceToNow(new Date().valueOf() + d);
185 5 : }
186 :
187 25 : export function format_size_and_text(size, text) {
188 25 : return fmt_size(size) + " " + text;
189 25 : }
190 :
191 4 : export function validate_mdraid_name(name) {
192 4 : return validate_lvm2_name(name);
193 4 : }
194 :
195 15 : export function validate_lvm2_name(name) {
196 15 : if (name === "")
197 0 : return _("Name cannot be empty.");
198 15 : if (name.length > 127)
199 0 : return _("Name cannot be longer than 127 characters.");
200 15 : const m = name.match(/[^a-zA-Z0-9+._-]/);
201 1 : if (m) {
202 1 : if (m[0].search(/\s+/) === -1)
203 0 : return cockpit.format(_("Name cannot contain the character '$0'."), m[0]);
204 : else
205 1 : return cockpit.format(_("Name cannot contain whitespace."), m[0]);
206 1 : }
207 15 : }
208 :
209 37 : export function validate_fsys_label(label, type) {
210 37 : const fs_label_max = {
211 37 : xfs: 12,
212 37 : ext4: 16,
213 37 : vfat: 11,
214 37 : ntfs: 128,
215 37 : btrfs: 256,
216 37 : };
217 :
218 37 : const limit = fs_label_max[type.replace("luks+", "")];
219 37 : const bytes = new TextEncoder().encode(label);
220 1 : if (limit && bytes.length > limit) {
221 : // Let's not confuse people with encoding issues unless
222 : // they use funny characters.
223 1 : if (bytes.length == label.length)
224 0 : return cockpit.format(_("Name cannot be longer than $0 characters"), limit);
225 : else
226 0 : return cockpit.format(_("Name cannot be longer than $0 bytes"), limit);
227 1 : }
228 37 : }
229 :
230 112 : export function block_name(block) {
231 112 : return decode_filename(block.PreferredDevice);
232 112 : }
233 :
234 112 : export function block_short_name(block) {
235 112 : return block_name(block).replace(/^\/dev\//, "");
236 112 : }
237 :
238 7 : export function mdraid_name(mdraid) {
239 7 : if (!mdraid.Name)
240 1 : return "";
241 :
242 7 : const parts = mdraid.Name.split(":");
243 :
244 7 : if (parts.length != 2)
245 3 : return mdraid.Name;
246 :
247 : /* Check the static (from /etc/hostname) and transient (acquired from DHCP server via
248 : * NetworkManager → hostnamed, may not exist) host name -- if either one matches, we
249 : * consider the RAID a local one and just show the device name.
250 : * Otherwise it's a remote one, and include the host in the name.
251 : *
252 : * However: if we call hostnamed too early, before the dbus.proxy() promise is
253 : * fulfilled, it will not be valid yet (hostnamed properties are undefined);
254 : * it's too inconvenient to make this function asynchronous, so just don't
255 : * show the host name in this case. */
256 1 : if (hostnamed.StaticHostname === undefined || parts[0] == hostnamed.StaticHostname || parts[0] == hostnamed.Hostname)
257 1 : return parts[1];
258 : else
259 1 : return cockpit.format(_("$name (from $host)"),
260 1 : {
261 1 : name: parts[1],
262 1 : host: parts[0]
263 1 : });
264 7 : }
265 :
266 0 : export function lvol_name(lvol) {
267 0 : let type;
268 0 : if (lvol.Type == "pool")
269 0 : type = _("Pool for thin logical volumes");
270 0 : else if (lvol.ThinPool != "/")
271 0 : type = _("Thin logical volume");
272 0 : else if (lvol.Origin != "/")
273 0 : type = _("Logical volume (snapshot)");
274 : else
275 0 : type = _("Logical volume");
276 0 : return cockpit.format('$0 "$1"', type, lvol.Name);
277 0 : }
278 :
279 108 : export function drive_name(drive) {
280 108 : const name_parts = [];
281 108 : if (drive.Vendor)
282 81 : name_parts.push(drive.Vendor);
283 108 : if (drive.Model)
284 108 : name_parts.push(drive.Model);
285 :
286 108 : let name = name_parts.join(" ");
287 108 : if (drive.Serial)
288 11 : name += " (" + drive.Serial + ")";
289 11 : else if (drive.WWN)
290 11 : name += " (" + drive.WWN + ")";
291 :
292 108 : return name;
293 108 : }
294 :
295 89 : export function get_block_link_parts(client, path) {
296 89 : let is_part;
297 89 : let is_crypt;
298 89 : let is_lvol;
299 :
300 89 : while (true) {
301 17 : if (client.blocks_part[path] && client.blocks_ptable[client.blocks_part[path].Table]) {
302 17 : is_part = true;
303 17 : path = client.blocks_part[path].Table;
304 4 : } else if (client.blocks[path] && client.blocks[client.blocks[path].CryptoBackingDevice]) {
305 20 : is_crypt = true;
306 20 : path = client.blocks[path].CryptoBackingDevice;
307 20 : } else
308 89 : break;
309 89 : }
310 :
311 23 : if (client.blocks_lvm2[path] && client.lvols[client.blocks_lvm2[path].LogicalVolume])
312 23 : is_lvol = true;
313 :
314 89 : const block = client.blocks[path];
315 89 : if (!block)
316 89 : return;
317 :
318 89 : let location;
319 89 : let link;
320 7 : if (client.mdraids[block.MDRaid]) {
321 7 : location = ["mdraid", client.mdraids[block.MDRaid].UUID];
322 7 : link = cockpit.format(_("MDRAID device $0"), mdraid_name(client.mdraids[block.MDRaid]));
323 7 : } else if (client.blocks_lvm2[path] &&
324 23 : client.lvols[client.blocks_lvm2[path].LogicalVolume] &&
325 23 : client.vgroups[client.lvols[client.blocks_lvm2[path].LogicalVolume].VolumeGroup]) {
326 23 : const target = client.vgroups[client.lvols[client.blocks_lvm2[path].LogicalVolume].VolumeGroup].Name;
327 23 : location = ["vg", target];
328 23 : link = cockpit.format(_("LVM2 volume group $0"), target);
329 23 : } else {
330 89 : const vdo = client.legacy_vdo_overlay.find_by_block(block);
331 1 : if (vdo) {
332 1 : location = ["vdo", vdo.name];
333 1 : link = cockpit.format(_("VDO device $0"), vdo.name);
334 1 : } else {
335 89 : location = [block_short_name(block)];
336 89 : if (client.drives[block.Drive])
337 24 : link = drive_name(client.drives[block.Drive]);
338 : else
339 49 : link = block_name(block);
340 89 : }
341 89 : }
342 :
343 : // Partitions of logical volumes are shown as just logical volumes.
344 89 : let format;
345 23 : if (is_lvol && is_crypt)
346 6 : format = _("Encrypted logical volume of $0");
347 17 : else if (is_part && is_crypt)
348 4 : format = _("Encrypted partition of $0");
349 89 : else if (is_lvol)
350 23 : format = _("Logical volume of $0");
351 89 : else if (is_part)
352 17 : format = _("Partition of $0");
353 89 : else if (is_crypt)
354 13 : format = _("Encrypted $0");
355 : else
356 89 : format = "$0";
357 :
358 89 : return {
359 89 : location,
360 89 : format,
361 89 : link
362 89 : };
363 89 : }
364 :
365 : export function go_to_block(client, path) {
366 : const parts = get_block_link_parts(client, path);
367 : cockpit.location.go(parts.location);
368 : }
369 :
370 104 : export function get_partitions(client, block) {
371 104 : const partitions = client.blocks_partitions[block.path];
372 :
373 104 : function process_level(level, container_start, container_size) {
374 104 : let n;
375 104 : let last_end = container_start;
376 104 : const total_end = container_start + container_size;
377 104 : let block;
378 104 : let start;
379 104 : let size;
380 104 : let is_container;
381 104 : let is_contained;
382 :
383 104 : const result = [];
384 :
385 104 : function append_free_space(start, size) {
386 : // There is a lot of rounding and aligning going on in
387 : // the storage stack. All of udisks2, libblockdev,
388 : // and libparted seem to contribute their own ideas of
389 : // where a partition really should start.
390 : //
391 : // The start of partitions are aggressively rounded
392 : // up, sometimes twice, but the end is not aligned in
393 : // the same way. This means that a few megabytes of
394 : // free space will show up between partitions.
395 : //
396 : // We hide these small free spaces because they are
397 : // unexpected and can't be used for anything anyway.
398 : //
399 : // "Small" is anything less than 3 MiB, which seems to
400 : // work okay. (The worst case is probably creating
401 : // the first logical partition inside a extended
402 : // partition with udisks+libblockdev. It leads to a 2
403 : // MiB gap.)
404 :
405 28 : if (size >= 3 * 1024 * 1024) {
406 28 : result.push({ type: 'free', start, size });
407 28 : }
408 104 : }
409 :
410 104 : for (n = 0; n < partitions.length; n++) {
411 104 : block = client.blocks[partitions[n].path];
412 104 : start = partitions[n].Offset;
413 104 : size = partitions[n].Size;
414 104 : is_container = partitions[n].IsContainer;
415 104 : is_contained = partitions[n].IsContained;
416 :
417 104 : if (block === null)
418 104 : continue;
419 :
420 104 : if (level === 0 && is_contained)
421 104 : continue;
422 :
423 13 : if (level == 1 && !is_contained)
424 104 : continue;
425 :
426 104 : if (start < container_start || start + size > container_start + container_size)
427 104 : continue;
428 :
429 104 : append_free_space(last_end, start - last_end);
430 13 : if (is_container) {
431 13 : result.push({
432 13 : type: 'container',
433 13 : block,
434 13 : size,
435 13 : partitions: process_level(level + 1, start, size)
436 13 : });
437 13 : } else {
438 104 : result.push({ type: 'block', block });
439 104 : }
440 104 : last_end = start + size;
441 104 : }
442 :
443 104 : append_free_space(last_end, total_end - last_end);
444 :
445 104 : return result;
446 104 : }
447 :
448 104 : return process_level(0, 0, block.Size);
449 104 : }
450 :
451 113 : let available_spaces = [];
452 :
453 26 : export function get_available_spaces() {
454 15 : return available_spaces.sort((a, b) => block_cmp(a.block, b.block));
455 26 : }
456 :
457 112 : export function reset_available_spaces() {
458 112 : available_spaces = [];
459 112 : }
460 :
461 89 : export function register_available_block_space(client, block) {
462 89 : const parts = get_block_link_parts(client, block.path);
463 89 : const text = cockpit.format(parts.format, parts.link);
464 89 : available_spaces.push({ type: 'block', block, size: block.Size, desc: text });
465 89 : }
466 :
467 18 : export function register_available_free_space(client, block, partition) {
468 18 : const link_parts = get_block_link_parts(client, block.path);
469 18 : const text = cockpit.format(link_parts.format, link_parts.link);
470 18 : available_spaces.push({
471 18 : type: 'free',
472 18 : block,
473 18 : start: partition.start,
474 18 : size: partition.size,
475 18 : desc: cockpit.format(_("unpartitioned space on $0"), text)
476 18 : });
477 18 : }
478 :
479 23 : export function prepare_available_spaces(client, spcs) {
480 23 : function prepare(spc) {
481 23 : if (spc.type == 'block')
482 0 : return Promise.resolve(spc.block.path);
483 1 : else if (spc.type == 'free') {
484 1 : const block_ptable = client.blocks_ptable[spc.block.path];
485 1 : return block_ptable.CreatePartition(spc.start, spc.size, "", "", { });
486 1 : }
487 23 : }
488 :
489 23 : return Promise.all(spcs.map(prepare));
490 23 : }
491 :
492 55 : export function is_snap(client, block) {
493 55 : const block_fsys = client.blocks_fsys[block.path];
494 3 : return block_fsys && block_fsys.MountPoints.map(decode_filename).some(mp => mp.indexOf("/snap/") == 0 || mp.indexOf("/var/lib/snapd/snap/") == 0);
495 55 : }
496 :
497 112 : export function get_other_devices(client) {
498 112 : return Object.keys(client.blocks).filter(path => {
499 112 : const block = client.blocks[path];
500 112 : const block_part = client.blocks_part[path];
501 112 : const block_lvm2 = client.blocks_lvm2[path];
502 :
503 112 : return ((!block_part || block_part.Table == "/") &&
504 112 : block.Drive == "/" &&
505 112 : block.CryptoBackingDevice == "/" &&
506 112 : block.MDRaid == "/" &&
507 34 : (!block_lvm2 || block_lvm2.LogicalVolume == "/") &&
508 112 : !block.HintIgnore &&
509 90 : block.Size > 0 &&
510 63 : !client.legacy_vdo_overlay.find_by_block(block) &&
511 63 : !client.blocks_stratis_fsys[block.path] &&
512 63 : !is_snap(client, block) &&
513 63 : !should_ignore(client, block.path));
514 112 : });
515 112 : }
516 :
517 : /* Comparison function for sorting lists of block devices.
518 :
519 : We sort by major:minor numbers to get the expected order when
520 : there are more than 10 devices of a kind. For example, if you
521 : have 20 loopback devices named loop0 to loop19, sorting them
522 : alphabetically would put them in the wrong order
523 :
524 : loop0, loop1, loop10, loop11, ..., loop2, ...
525 :
526 : Sorting by major:minor is an easy way to do the right thing.
527 : */
528 :
529 21 : export function block_cmp(a, b) {
530 21 : return a.DeviceNumber - b.DeviceNumber;
531 21 : }
532 :
533 : export function make_block_path_cmp(client) {
534 : return function(path_a, path_b) {
535 : return block_cmp(client.blocks[path_a], client.blocks[path_b]);
536 : };
537 : }
538 :
539 113 : let multipathd_service;
540 :
541 112 : export function get_multipathd_service () {
542 112 : if (!multipathd_service)
543 112 : multipathd_service = service.proxy("multipathd");
544 112 : return multipathd_service;
545 112 : }
546 :
547 56 : function get_parent(client, path) {
548 14 : if (client.blocks_part[path] && client.blocks[client.blocks_part[path].Table])
549 14 : return client.blocks_part[path].Table;
550 56 : if (client.blocks[path] && client.blocks[client.blocks[path].CryptoBackingDevice])
551 3 : return client.blocks[path].CryptoBackingDevice;
552 56 : if (client.blocks[path] && client.drives[client.blocks[path].Drive])
553 50 : return client.blocks[path].Drive;
554 21 : if (client.blocks[path] && client.mdraids[client.blocks[path].MDRaid])
555 3 : return client.blocks[path].MDRaid;
556 11 : if (client.blocks_lvm2[path] && client.lvols[client.blocks_lvm2[path].LogicalVolume])
557 11 : return client.blocks_lvm2[path].LogicalVolume;
558 1 : if (client.lvols[path] && client.vgroups[client.lvols[path].VolumeGroup])
559 1 : return client.lvols[path].VolumeGroup;
560 13 : if (client.blocks_stratis_fsys[path])
561 1 : return client.blocks_stratis_fsys[path].Pool;
562 13 : if (client.vgroups[path])
563 1 : return path;
564 13 : if (client.stratis_pools[path])
565 1 : return path;
566 56 : }
567 :
568 56 : function get_direct_parent_blocks(client, path) {
569 56 : if (client.blocks[path])
570 56 : path = get_parent(client, path);
571 56 : if (!path)
572 13 : return [];
573 52 : if (client.blocks[path])
574 15 : return [path];
575 52 : if (client.mdraids[path])
576 3 : return client.mdraids_members[path].map(function (m) { return m.path });
577 50 : if (client.lvols[path])
578 11 : path = client.lvols[path].VolumeGroup;
579 50 : if (client.vgroups[path])
580 10 : return client.vgroups_pvols[path].map(function (pv) { return pv.path });
581 50 : if (client.stratis_pools[path])
582 1 : return client.stratis_pool_blockdevs[path].map(bd => client.slashdevs_block[bd.Devnode].path);
583 50 : return [];
584 56 : }
585 :
586 51 : export function get_parent_blocks(client, path) {
587 51 : const direct_parents = get_direct_parent_blocks(client, path);
588 19 : const direct_and_indirect_parents = flatten(direct_parents.map(function (p) {
589 19 : return get_parent_blocks(client, p);
590 19 : }));
591 51 : return [path].concat(direct_and_indirect_parents);
592 51 : }
593 :
594 51 : export function is_netdev(client, path) {
595 51 : const block = client.blocks[path];
596 51 : const drive = block && client.drives[block.Drive];
597 45 : if (drive && drive.Vendor == "LIO-ORG")
598 1 : return true;
599 50 : if (block && block.Major == 43) // NBD
600 0 : return true;
601 50 : return false;
602 51 : }
603 :
604 112 : export function should_ignore(client, path) {
605 112 : const block = client.blocks[path];
606 :
607 : // HACK - https://github.com/stratis-storage/stratisd/issues/3801
608 : //
609 : // Filter out Stratis private device mapper devices. This normally
610 : // happens by setting the DM_UDEV_DISABLE_OTHER_RULES_FLAG in the
611 : // udev database (which causes UDisks2 to ignore the block
612 : // device), but since Stratis 3.8 the "*-crypt" devices don't have
613 : // them.
614 :
615 112 : if (block && decode_filename(block.PreferredDevice).startsWith("/dev/mapper/stratis-1-private"))
616 12 : return true;
617 :
618 : // Check what Anaconda tells us.
619 :
620 112 : if (!client.in_anaconda_mode())
621 102 : return false;
622 :
623 22 : const parents = get_direct_parent_blocks(client, path);
624 22 : if (parents.length == 0) {
625 22 : return block && client.should_ignore_block(block);
626 22 : } else {
627 11 : return parents.some(p => should_ignore(client, p));
628 22 : }
629 112 : }
630 :
631 : /* GET_CHILDREN gets the direct children of the storage object at
632 : PATH, like the partitions of a partitioned block device, or the
633 : volume group of a physical volume. By calling GET_CHILDREN
634 : recursively, you can traverse the whole storage hierarchy from
635 : hardware drives at the bottom to filesystems at the top.
636 :
637 : GET_CHILDREN_FOR_TEARDOWN is similar but doesn't consider things
638 : like volume groups to be children of their physical volumes. This
639 : is appropriate for teardown processing, where tearing down a
640 : physical volume does not imply tearing down the whole volume group
641 : with everything that it contains.
642 : */
643 :
644 70 : function get_children_for_teardown(client, path) {
645 70 : const children = [];
646 :
647 11 : if (client.blocks_cleartext[path]) {
648 11 : children.push(client.blocks_cleartext[path].path);
649 11 : }
650 :
651 5 : if (client.blocks_ptable[path]) {
652 4 : client.blocks_partitions[path].forEach(function (part) {
653 4 : if (!part.IsContainer)
654 4 : children.push(part.path);
655 4 : });
656 5 : }
657 :
658 1 : if (client.blocks_part[path] && client.blocks_part[path].IsContainer) {
659 1 : const ptable_path = client.blocks_part[path].Table;
660 1 : client.blocks_partitions[ptable_path].forEach(function (part) {
661 1 : if (part.IsContained)
662 1 : children.push(part.path);
663 1 : });
664 1 : }
665 :
666 5 : if (client.vgroups[path]) {
667 5 : client.vgroups_lvols[path].forEach(function (lvol) {
668 5 : if (client.lvols_block[lvol.path])
669 4 : children.push(client.lvols_block[lvol.path].path);
670 5 : });
671 5 : }
672 :
673 1 : if (client.lvols_pool_members[path]) {
674 1 : for (const lvol of client.lvols_pool_members[path]) {
675 1 : const block = client.lvols_block[lvol.path];
676 1 : if (block)
677 1 : children.push(block.path);
678 1 : }
679 1 : }
680 :
681 3 : if (client.stratis_pools[path]) {
682 3 : client.stratis_pool_filesystems[path].forEach(function (fsys) {
683 3 : const block = client.slashdevs_block[fsys.Devnode];
684 3 : if (block)
685 3 : children.push(block.path);
686 3 : });
687 3 : }
688 :
689 70 : return children;
690 70 : }
691 :
692 1 : export function get_children(client, path) {
693 1 : const children = get_children_for_teardown(client, path);
694 :
695 0 : if (client.blocks[path]) {
696 0 : const mdraid = client.blocks[path].MDRaidMember;
697 0 : if (mdraid != "/")
698 0 : children.push(mdraid);
699 0 : }
700 :
701 0 : if (client.blocks_pvol[path]) {
702 0 : const vgroup = client.blocks_pvol[path].VolumeGroup;
703 0 : if (vgroup != "/")
704 0 : children.push(vgroup);
705 0 : }
706 :
707 0 : if (client.blocks_stratis_blockdev[path]) {
708 0 : const pool = client.blocks_stratis_blockdev[path].Pool;
709 0 : if (pool != "/")
710 0 : children.push(pool);
711 0 : }
712 :
713 1 : return children;
714 1 : }
715 :
716 : /**
717 : * True if `active_mount` refers to a path strictly below `parent_mount`.
718 : *
719 : * Examples: `/home` is below `/`; `/srv/jail` is below `/srv`. `/etc` is not below `/srv`.
720 : *
721 : * Root is special: the only character shared by `/` and `/home` is the leading slash, so we
722 : * cannot require a "/" immediately after the parent string (that would look at `h` in `/home`).
723 : */
724 42 : function is_strict_descendant_mount_path(parent_mount, active_mount) {
725 42 : if (active_mount.length <= parent_mount.length)
726 42 : return false;
727 15 : if (!active_mount.startsWith(parent_mount))
728 14 : return false;
729 2 : if (parent_mount === "/")
730 0 : return true;
731 2 : return active_mount[parent_mount.length] === "/";
732 42 : }
733 :
734 42 : export function find_children_for_mount_point(client, mount_point, self, self_subvol) {
735 42 : const children = {};
736 :
737 42 : function is_self(b) {
738 36 : return self && (b == self || client.blocks[b.CryptoBackingDevice] == self);
739 42 : }
740 :
741 42 : for (const p in client.blocks) {
742 42 : const b = client.blocks[p];
743 42 : const fs = client.blocks_fsys[p];
744 :
745 42 : if (!fs)
746 42 : continue;
747 :
748 : // Skip self block except btrfs (subvolumes share one block); then ignore only this subvol's mounts.
749 18 : if (is_self(b) && !client.blocks_fsys_btrfs[self.path])
750 42 : continue;
751 :
752 8 : const skip = is_self(b) && self_subvol ? get_mount_points(client, fs, self_subvol) : [];
753 :
754 42 : for (const mp of fs.MountPoints) {
755 42 : const active = decode_filename(mp);
756 42 : if (skip.includes(active))
757 42 : continue;
758 42 : if (is_strict_descendant_mount_path(mount_point, active))
759 2 : children[active] = b;
760 42 : }
761 42 : }
762 :
763 42 : return children;
764 42 : }
765 :
766 112 : export function get_fstab_config_with_client(client, block, also_child_config, subvol) {
767 108 : function match(c) {
768 108 : if (c[0] != "fstab")
769 26 : return false;
770 102 : if (subvol !== undefined) {
771 102 : if (!c[1].opts)
772 11 : return false;
773 :
774 102 : const opts = decode_filename(c[1].opts.v).split(",");
775 102 : if (opts.indexOf("subvolid=" + subvol.id) >= 0)
776 11 : return true;
777 102 : if (opts.indexOf("subvol=" + subvol.pathname) >= 0 || opts.indexOf("subvol=/" + subvol.pathname) >= 0)
778 102 : return true;
779 :
780 : // btrfs mounted without subvol argument.
781 102 : const btrfs_volume = client.blocks_fsys_btrfs[block.path];
782 102 : const default_subvolid = client.uuids_btrfs_default_subvol[btrfs_volume.data.uuid];
783 101 : if (default_subvolid === subvol.id && !opts.find(o => o.indexOf("subvol=") >= 0 || o.indexOf("subvolid=") >= 0))
784 13 : return true;
785 :
786 102 : return false;
787 102 : }
788 107 : return true;
789 108 : }
790 :
791 112 : let config = block.Configuration.find(match);
792 :
793 112 : if (!config && also_child_config && client.blocks_crypto[block.path])
794 13 : config = client.blocks_crypto[block.path]?.ChildConfiguration.find(c => c[0] == "fstab");
795 :
796 107 : if (config && decode_filename(config[1].type.v) != "swap") {
797 107 : const mnt_opts = get_block_mntopts(config[1]).split(",");
798 107 : let dir = decode_filename(config[1].dir.v);
799 107 : let opts = mnt_opts
800 106 : .filter(function (s) { return s.indexOf("x-parent") !== 0 })
801 107 : .join(",");
802 107 : const parents = mnt_opts
803 106 : .filter(function (s) { return s.indexOf("x-parent") === 0 })
804 107 : .join(",");
805 107 : if (dir[0] != "/")
806 12 : dir = "/" + dir;
807 107 : if (opts == "defaults")
808 16 : opts = "";
809 107 : return [config, dir, opts, parents];
810 107 : } else
811 112 : return [];
812 112 : }
813 :
814 70 : export function get_active_usage(client, path, top_action, child_action, is_temporary, subvol, allow_multi_device_delete) {
815 70 : function get_usage(usage, path, level) {
816 70 : const block = client.blocks[path];
817 70 : const fsys = client.blocks_fsys[path];
818 70 : const swap = client.blocks_swap[path];
819 67 : const mdraid = block && client.mdraids[block.MDRaidMember];
820 70 : const pvol = client.blocks_pvol[path];
821 1 : const vgroup = pvol && client.vgroups[pvol.VolumeGroup];
822 67 : const vdo = block && client.legacy_vdo_overlay.find_by_backing_block(block);
823 67 : const stratis_blockdev = block && client.blocks_stratis_blockdev[path];
824 0 : const stratis_pool = stratis_blockdev && client.stratis_pools[stratis_blockdev.Pool];
825 70 : const btrfs_volume = client.blocks_fsys_btrfs[path];
826 :
827 20 : get_children_for_teardown(client, path).map(p => get_usage(usage, p, level + 1));
828 :
829 67 : function get_actions(teardown_action) {
830 67 : const actions = [];
831 67 : if (teardown_action)
832 32 : actions.push(teardown_action);
833 1 : const global_action = (level == 0 || (block && client.blocks[block.CryptoBackingDevice] && level == 1)) ? top_action : child_action || top_action;
834 67 : if (global_action)
835 62 : actions.push(global_action);
836 67 : return actions;
837 67 : }
838 :
839 30 : function enter_unmount(block, location, is_top) {
840 30 : const [, mount_point] = get_fstab_config_with_client(client, block);
841 3 : const has_fstab_entry = is_temporary && location == mount_point;
842 :
843 : // Ignore the secret btrfs mount point unless we are
844 : // formatting (in which case subvol is false).
845 6 : if (btrfs_volume && subvol && location.startsWith(BTRFS_TOOL_MOUNT_PATH))
846 30 : return;
847 :
848 5 : for (const u of usage) {
849 1 : if (u.usage == 'mounted' && u.location == location) {
850 1 : if (is_top) {
851 1 : u.actions = get_actions(_("unmount"));
852 1 : u.set_noauto = false;
853 1 : }
854 1 : return;
855 1 : }
856 5 : }
857 30 : usage.push({
858 30 : level,
859 30 : block,
860 30 : usage: 'mounted',
861 30 : location,
862 30 : has_fstab_entry,
863 1 : set_noauto: !is_top && !is_temporary,
864 0 : actions: (is_top ? get_actions(_("unmount")) : [_("unmount")]).concat(has_fstab_entry ? [_("mount")] : []),
865 0 : blocking: client.strip_mount_point_prefix(location) === false && !location.startsWith(BTRFS_TOOL_MOUNT_PATH),
866 30 : });
867 30 : }
868 :
869 : // HACK: get_active_usage is used for mounting and formatting so we use the absence of the subvol argument
870 : // to figure out that we want to format this device.
871 : // This is separate from the if's below as we also always have to umount the filesystem.
872 :
873 : // We allow a btrfs volume with one device to be formatted as this
874 : // looks the most like a normal filesystem use case.
875 1 : if (btrfs_volume && btrfs_volume.data.num_devices !== 1 && !subvol && !allow_multi_device_delete) {
876 1 : usage.push({
877 1 : level,
878 1 : usage: 'btrfs-device',
879 1 : block,
880 1 : btrfs_volume,
881 0 : location: btrfs_volume.data.label || btrfs_volume.data.uuid,
882 1 : actions: get_actions(_("remove from btrfs volume")),
883 1 : blocking: true,
884 1 : });
885 1 : }
886 :
887 70 : const mount_points = get_mount_points(client, fsys, subvol);
888 30 : if (mount_points.length > 0) {
889 30 : mount_points.forEach(mp => {
890 30 : const children = find_children_for_mount_point(client, mp, null);
891 30 : for (const c in children)
892 1 : enter_unmount(children[c], c, false);
893 30 : enter_unmount(block, mp, true);
894 30 : });
895 0 : } else if (swap) {
896 1 : if (swap.Active) {
897 1 : usage.push({
898 1 : level,
899 1 : usage: 'swap',
900 1 : block,
901 1 : actions: get_actions(_("stop")),
902 1 : });
903 1 : }
904 0 : } else if (mdraid) {
905 1 : const active_state = mdraid.ActiveDevices.find(as => as[0] == block.path);
906 1 : usage.push({
907 1 : level,
908 1 : usage: 'mdraid-member',
909 1 : block,
910 1 : mdraid,
911 1 : location: mdraid_name(mdraid),
912 1 : actions: get_actions(_("remove from MDRAID")),
913 1 : blocking: !(active_state && active_state[1] < 0)
914 1 : });
915 0 : } else if (vgroup) {
916 1 : usage.push({
917 1 : level,
918 1 : usage: 'pvol',
919 1 : block,
920 1 : vgroup,
921 1 : pvol,
922 1 : location: vgroup.Name,
923 1 : actions: get_actions(_("remove from LVM2")),
924 1 : blocking: pvol.FreeSize != pvol.Size
925 1 : });
926 0 : } else if (vdo) {
927 0 : usage.push({
928 0 : level,
929 0 : usage: 'vdo-backing',
930 0 : block,
931 0 : vdo,
932 0 : location: vdo.name,
933 0 : blocking: true
934 0 : });
935 0 : } else if (stratis_pool) {
936 0 : usage.push({
937 0 : level,
938 0 : usage: 'stratis-pool-member',
939 0 : block,
940 0 : stratis_pool,
941 0 : location: stratis_pool.Name,
942 0 : blocking: true
943 0 : });
944 0 : } else if (block && !client.blocks_cleartext[block.path]) {
945 64 : usage.push({
946 64 : level,
947 64 : usage: 'none',
948 64 : block,
949 64 : actions: get_actions(null),
950 64 : blocking: false
951 64 : });
952 64 : }
953 :
954 70 : return usage;
955 70 : }
956 :
957 70 : const usage = [];
958 70 : get_usage(usage, path, 0);
959 :
960 67 : usage.Blocking = usage.some(u => u.blocking);
961 67 : usage.Teardown = usage.some(u => !u.blocking);
962 :
963 64 : if (usage.length == 1 && usage[0].level == 0 && usage[0].usage == "none")
964 61 : usage.Teardown = false;
965 :
966 70 : return usage;
967 70 : }
968 :
969 1 : async function set_fsys_noauto(client, block, mount_point) {
970 1 : for (const conf of block.Configuration) {
971 1 : if (conf[0] == "fstab" &&
972 1 : decode_filename(conf[1].dir.v) == mount_point) {
973 1 : const options = parse_options(get_block_mntopts(conf[1]));
974 1 : if (options.indexOf("noauto") >= 0)
975 1 : continue;
976 1 : options.push("noauto");
977 1 : const new_conf = [
978 1 : "fstab",
979 1 : Object.assign({ }, conf[1],
980 1 : {
981 1 : opts: {
982 1 : t: 'ay',
983 1 : v: encode_filename(unparse_options(options))
984 1 : }
985 1 : })
986 1 : ];
987 1 : await block.UpdateConfigurationItem(conf, new_conf, { });
988 1 : }
989 1 : }
990 :
991 1 : const crypto_backing = client.blocks[block.CryptoBackingDevice];
992 1 : if (crypto_backing) {
993 1 : const crypto_backing_crypto = client.blocks_crypto[crypto_backing.path];
994 1 : await set_crypto_auto_option(crypto_backing, false);
995 1 : if (crypto_backing_crypto)
996 1 : await crypto_backing_crypto.Lock({});
997 1 : }
998 1 : }
999 :
1000 69 : export function teardown_active_usage(client, usage) {
1001 : // The code below is complicated by the fact that the last
1002 : // physical volume of a volume group can not be removed
1003 : // directly (even if it is completely empty). We want to
1004 : // remove the whole volume group instead in this case.
1005 : //
1006 : // However, we might be removing the last two (or more)
1007 : // physical volumes here, and it is easiest to catch this
1008 : // condition upfront by reshuffling the data structures.
1009 :
1010 69 : async function unmount(mounteds) {
1011 30 : for (const m of mounteds) {
1012 30 : await client.unmount_at(m.location, m.users);
1013 30 : if (m.set_noauto)
1014 1 : await set_fsys_noauto(client, m.block, m.location);
1015 30 : }
1016 69 : }
1017 :
1018 69 : async function stop_swap(swaps) {
1019 1 : for (const s of swaps) {
1020 1 : await client.blocks_swap[s.block.path].Stop({});
1021 1 : }
1022 69 : }
1023 :
1024 69 : function mdraid_remove(members) {
1025 1 : return Promise.all(members.map(m => m.mdraid.RemoveDevice(m.block.path, { wipe: { t: 'b', v: true } })));
1026 69 : }
1027 :
1028 69 : function pvol_remove(pvols) {
1029 69 : const by_vgroup = { };
1030 1 : pvols.forEach(function (p) {
1031 1 : if (!by_vgroup[p.vgroup.path])
1032 1 : by_vgroup[p.vgroup.path] = [];
1033 1 : by_vgroup[p.vgroup.path].push(p.block);
1034 1 : });
1035 :
1036 1 : function handle_vg(p) {
1037 1 : const vg = client.vgroups[p];
1038 1 : const pvs = by_vgroup[p];
1039 : // If we would remove all physical volumes of a volume
1040 : // group, remove the whole volume group instead.
1041 1 : if (pvs.length == client.vgroups_pvols[p].length) {
1042 1 : return vg.Delete(true, { 'tear-down': { t: 'b', v: true } }).then(reload_systemd);
1043 1 : } else {
1044 1 : return Promise.all(pvs.map(pv => vg.RemoveDevice(pv.path, true, {})));
1045 1 : }
1046 1 : }
1047 :
1048 69 : return Promise.all(Object.keys(by_vgroup).map(handle_vg));
1049 69 : }
1050 :
1051 69 : return Promise.all(Array.prototype.concat(
1052 65 : unmount(usage.filter(function(use) { return use.usage == "mounted" })),
1053 65 : stop_swap(usage.filter(function(use) { return use.usage == "swap" })),
1054 65 : mdraid_remove(usage.filter(function(use) { return use.usage == "mdraid-member" })),
1055 65 : pvol_remove(usage.filter(function(use) { return use.usage == "pvol" }))
1056 69 : ));
1057 69 : }
1058 :
1059 11 : export async function undo_temporary_teardown(client, usage) {
1060 5 : for (let i = usage.length - 1; i >= 0; i--) {
1061 5 : const u = usage[i];
1062 3 : if (u.usage == "mounted" && u.has_fstab_entry) {
1063 3 : await client.mount_at(u.block, u.location);
1064 3 : }
1065 5 : }
1066 11 : }
1067 :
1068 : // TODO - generalize this to arbitrary number of arguments (when needed)
1069 : export function fmt_to_array(fmt, arg) {
1070 : const index = fmt.indexOf("$0");
1071 : if (index >= 0)
1072 : return [fmt.slice(0, index), arg, fmt.slice(index + 2)];
1073 : else
1074 : return [fmt];
1075 : }
1076 :
1077 64 : export function reload_systemd() {
1078 64 : return cockpit.spawn(["systemctl", "daemon-reload"], { superuser: "require", err: "message" });
1079 64 : }
1080 :
1081 9 : export function is_mounted_synch(block) {
1082 9 : return (cockpit.spawn(["findmnt", "-n", "-o", "TARGET", "-S", decode_filename(block.Device)],
1083 9 : { superuser: "require", err: "message" })
1084 5 : .then(data => data.trim())
1085 6 : .catch(() => false));
1086 9 : }
1087 :
1088 91 : export function for_each_async(arr, func) {
1089 69 : return arr.reduce((promise, elt) => promise.then(() => func(elt)), Promise.resolve());
1090 91 : }
1091 :
1092 : /*
1093 : * Get mount points for a given org.freedesktop.UDisks2.Filesystem object
1094 : *
1095 : * This generalises getting the given MountPoints of a Filesystem for btrfs and
1096 : * other filesystems, for btrfs we want to know if a subvolume is mounted
1097 : * anywhere. UDisks is currently not aware of subvolumes and it's Filesystem
1098 : * object gives us the MountPoints for all subvolumes while we want it per
1099 : * subvolume.
1100 : *
1101 : * @param {Object} block_fsys
1102 : * @param {Object|null} subvol
1103 : * @returns {Array} an array of MountPoints
1104 : */
1105 110 : export function get_mount_points(client, block_fsys, subvol) {
1106 110 : let mounted_at = [];
1107 :
1108 103 : if (subvol && block_fsys) {
1109 103 : const btrfs_volume = client.blocks_fsys_btrfs[block_fsys.path];
1110 103 : const volume_mounts = client.btrfs_mounts[btrfs_volume.data.uuid];
1111 103 : if (volume_mounts)
1112 101 : mounted_at = subvol.id in volume_mounts ? volume_mounts[subvol.id].mount_points : [];
1113 102 : } else {
1114 80 : mounted_at = block_fsys ? block_fsys.MountPoints.map(decode_filename) : [];
1115 109 : }
1116 :
1117 110 : return mounted_at;
1118 110 : }
1119 :
1120 32 : export function get_byte_units(guide_value) {
1121 32 : const units = [
1122 32 : { factor: 1000 ** 2, name: "MB" },
1123 32 : { factor: 1000 ** 3, name: "GB" },
1124 32 : { factor: 1000 ** 4, name: "TB" },
1125 32 : ];
1126 : // Find the biggest unit which gives two digits left of the decimal point (>= 10)
1127 32 : let unit;
1128 32 : for (unit = units.length - 1; unit >= 0; unit--)
1129 32 : if (guide_value / units[unit].factor >= 10)
1130 32 : break;
1131 : // Mark it selected. If we couldn't find one (-1), then use MB.
1132 32 : units[Math.max(0, unit)].selected = true;
1133 32 : return units;
1134 32 : }
1135 :
1136 : /** @type (client: any, path: string) => boolean */
1137 1 : export function contains_rootfs(client, path) {
1138 1 : const block = client.blocks[path];
1139 1 : const crypto = client.blocks_crypto[path];
1140 1 : let fsys_config = null;
1141 :
1142 1 : if (block)
1143 1 : fsys_config = block.Configuration.find(c => c[0] == "fstab");
1144 1 : if (!fsys_config && crypto)
1145 0 : fsys_config = crypto.ChildConfiguration.find(c => c[0] == "fstab");
1146 :
1147 1 : if (fsys_config) {
1148 1 : const dir = decode_filename(fsys_config[1].dir.v);
1149 1 : return dir == "/";
1150 1 : }
1151 :
1152 1 : return get_children(client, path).some(p => contains_rootfs(client, p));
1153 1 : }
|