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 : import * as PK from 'packagekit';
8 : import { superuser } from 'superuser';
9 : import { get_manifest_config_matchlist, in_anaconda_mode, read_anaconda_session_storage } from 'utils';
10 :
11 : import * as utils from './utils.js';
12 :
13 : import * as python from "python.js";
14 : import { read_os_release } from "os-release.js";
15 :
16 : import inotify_py from "inotify.py";
17 : import mount_users_py from "./mount-users.py";
18 : import nfs_mounts_py from "./nfs/nfs-mounts.py";
19 : import vdo_monitor_py from "./legacy-vdo/vdo-monitor.py";
20 : import stratis3_set_key_py from "./stratis/stratis3-set-key.py";
21 :
22 : import { reset_pages } from "./pages.jsx";
23 : import { make_overview_page } from "./overview/overview.jsx";
24 : import { export_mount_point_mapping } from "./anaconda.jsx";
25 :
26 : import { dequal } from 'dequal/lite';
27 :
28 : import btrfs_tool_py from "./btrfs/btrfs-tool.py";
29 :
30 : /* STORAGED CLIENT
31 : */
32 :
33 112 : function debug() {
34 14 : if (window.debugging == "all" || window.debugging?.includes("storaged")) // not-covered: debugging
35 14 : console.debug.apply(console, arguments); // not-covered: debugging
36 112 : }
37 :
38 113 : const client = {
39 113 : busy: 0
40 113 : };
41 :
42 113 : cockpit.event_target(client);
43 :
44 93 : client.run = async (func) => {
45 93 : if (client.in_anaconda_mode())
46 11 : await btrfs_stop_monitoring();
47 91 : const prom = func() || Promise.resolve();
48 93 : client.busy += 1;
49 93 : await prom.finally(() => {
50 93 : client.busy -= 1;
51 93 : btrfs_start_monitor();
52 93 : client.dispatchEvent("changed");
53 93 : });
54 93 : };
55 :
56 : /* Superuser
57 : */
58 :
59 113 : client.superuser = superuser;
60 113 : client.superuser.reload_page_on_change();
61 113 : client.superuser.addEventListener("changed", () => client.dispatchEvent("changed"));
62 :
63 : /* Metrics
64 : */
65 :
66 113 : function instance_sampler(metrics, source) {
67 113 : let instances;
68 113 : const self = {
69 113 : data: { },
70 113 : close
71 113 : };
72 :
73 113 : cockpit.event_target(self);
74 :
75 113 : function handle_meta(msg) {
76 113 : self.data = { };
77 113 : instances = [];
78 113 : for (let m = 0; m < msg.metrics.length; m++) {
79 113 : instances[m] = msg.metrics[m].instances;
80 113 : for (let i = 0; i < instances[m].length; i++)
81 113 : self.data[instances[m][i]] = [];
82 113 : }
83 24 : if (Object.keys(self.data).length > 100) {
84 24 : close();
85 24 : self.data = { };
86 24 : }
87 113 : }
88 :
89 113 : function handle_data(msg) {
90 113 : let changed = false;
91 113 : for (let s = 0; s < msg.length; s++) {
92 113 : const metrics = msg[s];
93 113 : for (let m = 0; m < metrics.length; m++) {
94 113 : const inst = metrics[m];
95 113 : for (let i = 0; i < inst.length; i++) {
96 113 : if (inst[i] !== null && inst[i] != self.data[instances[m][i]][m]) {
97 113 : changed = true;
98 113 : self.data[instances[m][i]][m] = inst[i];
99 113 : }
100 113 : }
101 113 : }
102 113 : }
103 113 : if (changed)
104 113 : self.dispatchEvent('changed');
105 113 : }
106 :
107 113 : const channel = cockpit.channel({
108 113 : payload: "metrics1",
109 113 : source: source || "internal",
110 113 : metrics
111 113 : });
112 0 : channel.addEventListener("closed", function (event, error) {
113 0 : console.log("closed", error);
114 0 : });
115 113 : channel.addEventListener("message", function (event, message) {
116 113 : const msg = JSON.parse(message);
117 113 : if (msg.length)
118 113 : handle_data(msg);
119 : else
120 113 : handle_meta(msg);
121 113 : });
122 :
123 0 : function close() {
124 0 : channel.close();
125 0 : }
126 :
127 113 : return self;
128 113 : }
129 :
130 : /* D-Bus proxies
131 : */
132 :
133 113 : client.time_offset = undefined; /* Number of milliseconds that the server is ahead of us. */
134 113 : client.features = undefined;
135 :
136 113 : client.storaged_client = undefined;
137 :
138 112 : function proxy(iface, path) {
139 112 : return client.storaged_client.proxy("org.freedesktop.UDisks2." + iface,
140 112 : "/org/freedesktop/UDisks2/" + path,
141 112 : { watch: true });
142 112 : }
143 :
144 112 : function proxies(iface) {
145 : /* We create the proxies here with 'watch' set to false and
146 : * establish a general watch for all of them. This is more
147 : * efficient since it reduces the number of D-Bus calls done
148 : * by the cache.
149 : */
150 112 : return client.storaged_client.proxies("org.freedesktop.UDisks2." + iface,
151 112 : "/org/freedesktop/UDisks2",
152 112 : { watch: false });
153 112 : }
154 :
155 0 : client.call = function call(path, iface, method, args, options) {
156 0 : return client.storaged_client.call(path, "org.freedesktop.UDisks2." + iface, method, args, options);
157 0 : };
158 :
159 112 : function init_proxies () {
160 112 : client.mdraids = proxies("MDRaid");
161 112 : client.vgroups = proxies("VolumeGroup");
162 112 : client.lvols = proxies("LogicalVolume");
163 112 : client.drives = proxies("Drive");
164 112 : client.drives_ata = proxies("Drive.Ata");
165 112 : client.blocks = proxies("Block");
166 112 : client.blocks_ptable = proxies("PartitionTable");
167 112 : client.blocks_part = proxies("Partition");
168 112 : client.blocks_lvm2 = proxies("Block.LVM2");
169 112 : client.blocks_pvol = proxies("PhysicalVolume");
170 112 : client.blocks_fsys = proxies("Filesystem");
171 112 : client.blocks_crypto = proxies("Encrypted");
172 112 : client.blocks_swap = proxies("Swapspace");
173 112 : client.iscsi_sessions = proxies("ISCSI.Session");
174 112 : client.vdo_vols = proxies("VDOVolume");
175 112 : client.blocks_fsys_btrfs = proxies("Filesystem.BTRFS");
176 112 : client.jobs = proxies("Job");
177 112 : client.nvme_controller = proxies("NVMe.Controller");
178 :
179 112 : return client.storaged_client.watch({ path_namespace: "/org/freedesktop/UDisks2" });
180 112 : }
181 :
182 : /* Monitors
183 : */
184 :
185 113 : client.fsys_sizes = instance_sampler([{ name: "mount.used" },
186 113 : { name: "mount.total" }
187 113 : ]);
188 :
189 113 : client.swap_sizes = instance_sampler([{ name: "swapdev.length" },
190 113 : { name: "swapdev.free" },
191 113 : ], "direct");
192 :
193 112 : function btrfs_findmnt_poll() {
194 112 : if (!client.btrfs_mounts)
195 14 : client.btrfs_mounts = { };
196 :
197 112 : const update_btrfs_mounts = output => {
198 112 : const btrfs_mounts = {};
199 112 : try {
200 : // Extract the data into a { uuid: { subvolid: { subvol, target } } }
201 112 : const mounts = JSON.parse(output);
202 112 : if ("filesystems" in mounts) {
203 112 : for (const fs of mounts.filesystems) {
204 112 : const subvolid_match = fs.options.match(/subvolid=(?<subvolid>\d+)/);
205 112 : const subvol_match = fs.options.match(/subvol=(?<subvol>[\w\\/]+)/);
206 112 : const ro = fs.options.split(",").indexOf("ro") >= 0;
207 :
208 14 : if (!subvolid_match && !subvol_match) {
209 14 : console.warn("findmnt entry without subvol and subvolid", fs);
210 14 : break;
211 14 : }
212 :
213 112 : const { subvolid } = subvolid_match.groups;
214 112 : const { subvol } = subvol_match.groups;
215 112 : const subvolume = {
216 112 : pathname: subvol,
217 112 : id: subvolid,
218 112 : mount_points: [fs.target],
219 15 : rw_mount_points: ro ? [] : [fs.target],
220 112 : };
221 :
222 112 : if (!(fs.uuid in btrfs_mounts)) {
223 112 : btrfs_mounts[fs.uuid] = { };
224 112 : }
225 :
226 : // We need to handle multiple mounts, they are listed separate.
227 93 : if (subvolid in btrfs_mounts[fs.uuid]) {
228 93 : btrfs_mounts[fs.uuid][subvolid].mount_points.push(fs.target);
229 93 : if (!ro)
230 93 : btrfs_mounts[fs.uuid][subvolid].rw_mount_points.push(fs.target);
231 93 : } else {
232 112 : btrfs_mounts[fs.uuid][subvolid] = subvolume;
233 112 : }
234 112 : }
235 112 : }
236 14 : } catch (exc) {
237 14 : if (exc.message)
238 14 : console.error("unable to parse findmnt JSON output", exc);
239 14 : }
240 :
241 : // Update client state
242 112 : if (!dequal(client.btrfs_mounts, btrfs_mounts)) {
243 112 : client.btrfs_mounts = btrfs_mounts;
244 112 : debug("btrfs_findmnt_poll mounts:", client.btrfs_mounts);
245 112 : client.update();
246 112 : }
247 112 : };
248 :
249 112 : const findmnt_poll = () => {
250 11 : return cockpit.spawn(["findmnt", "--type", "btrfs", "--mtab", "--poll"], { superuser: "try", err: "message" }).stream(() => {
251 11 : cockpit.spawn(["findmnt", "--type", "btrfs", "--mtab", "-o", "UUID,OPTIONS,TARGET", "--json"],
252 11 : { superuser: "try", err: "message" }).then(output => update_btrfs_mounts(output)).catch(err => {
253 : // When there are no btrfs filesystems left this can fail and thus we need to manually reset the mount info.
254 0 : client.btrfs_mounts = {};
255 0 : client.update();
256 0 : if (err.message) {
257 0 : console.error("findmnt exited with an error", err);
258 0 : }
259 0 : });
260 0 : }).catch(err => {
261 0 : console.error("findmnt --poll exited with an error", err);
262 0 : throw new Error("findmnt --poll stopped working");
263 0 : });
264 112 : };
265 :
266 : // This fails when no btrfs filesystem is found with the --mtab option and exits with 1, so that is kinda useless, however without --mtab
267 : // we don't get a nice flat structure. So we ignore the errors
268 112 : cockpit.spawn(["findmnt", "--type", "btrfs", "--mtab", "-o", "UUID,OPTIONS,SOURCE,TARGET", "--json"],
269 112 : { superuser: "try", err: "message" }).then(output => {
270 112 : update_btrfs_mounts(output);
271 112 : findmnt_poll();
272 112 : }).catch(err => {
273 : // only log error when there is a real issue.
274 0 : if (client.superuser.allowed && err.message) {
275 0 : console.error(`unable to run findmnt ${err}`);
276 0 : }
277 0 : findmnt_poll();
278 0 : });
279 112 : }
280 :
281 112 : function btrfs_update(data) {
282 112 : if (!client.uuids_btrfs_subvols)
283 12 : client.uuids_btrfs_subvols = { };
284 112 : if (!client.uuids_btrfs_usage)
285 12 : client.uuids_btrfs_usage = { };
286 112 : if (!client.uuids_btrfs_default_subvol)
287 12 : client.uuids_btrfs_default_subvol = { };
288 :
289 112 : const uuids_subvols = { };
290 112 : const uuids_usage = { };
291 112 : const default_subvol = { };
292 :
293 112 : for (const uuid in data) {
294 14 : if (data[uuid].error) {
295 14 : console.warn("Error polling btrfs", uuid, data[uuid].error);
296 14 : } else {
297 112 : if (data[uuid].subvolumes) {
298 112 : uuids_subvols[uuid] = [{ pathname: "/", id: 5, parent: null }].concat(data[uuid].subvolumes);
299 112 : }
300 112 : if (data[uuid].usages) {
301 112 : uuids_usage[uuid] = data[uuid].usages;
302 112 : }
303 112 : if (data[uuid].default_subvolume) {
304 112 : default_subvol[uuid] = data[uuid].default_subvolume;
305 112 : }
306 112 : }
307 112 : }
308 :
309 112 : if (!dequal(client.uuids_btrfs_subvols, uuids_subvols) || !dequal(client.uuids_btrfs_usage, uuids_usage) ||
310 112 : !dequal(client.uuids_btrfs_default_subvol, default_subvol)) {
311 112 : debug("btrfs_pol new subvols:", uuids_subvols);
312 112 : client.uuids_btrfs_subvols = uuids_subvols;
313 112 : client.uuids_btrfs_usage = uuids_usage;
314 112 : debug("btrfs_pol usage:", uuids_usage);
315 112 : client.uuids_btrfs_default_subvol = default_subvol;
316 112 : debug("btrfs_pol default subvolumes:", default_subvol);
317 112 : client.update();
318 112 : }
319 112 : }
320 :
321 112 : export async function btrfs_tool(args) {
322 112 : return await python.spawn(btrfs_tool_py, args, { superuser: "require" });
323 112 : }
324 :
325 112 : function btrfs_poll_options() {
326 112 : if (client.in_anaconda_mode())
327 13 : return ["--mount"];
328 : else
329 103 : return [];
330 112 : }
331 :
332 112 : export async function btrfs_poll() {
333 13 : if (!client.superuser.allowed || !client.features.btrfs) {
334 13 : return;
335 13 : }
336 :
337 112 : const data = JSON.parse(await btrfs_tool(["poll", ...btrfs_poll_options()]));
338 112 : btrfs_update(data);
339 112 : }
340 :
341 113 : let btrfs_monitor_channel = null;
342 :
343 112 : function btrfs_start_monitor() {
344 14 : if (!client.superuser.allowed || !client.features.btrfs) {
345 14 : return;
346 14 : }
347 :
348 112 : if (btrfs_monitor_channel)
349 112 : return;
350 :
351 112 : const channel = python.spawn(btrfs_tool_py, ["monitor", ...btrfs_poll_options()], { superuser: "require" });
352 112 : let buf = "";
353 :
354 112 : channel.stream(output => {
355 112 : buf += output;
356 112 : const lines = buf.split("\n");
357 112 : buf = lines[lines.length - 1];
358 112 : if (lines.length >= 2) {
359 112 : const data = JSON.parse(lines[lines.length - 2]);
360 112 : btrfs_update(data);
361 112 : }
362 112 : });
363 :
364 0 : channel.catch(err => {
365 0 : throw new Error(err.toString());
366 0 : });
367 :
368 112 : btrfs_monitor_channel = channel;
369 112 : }
370 :
371 8 : function btrfs_stop_monitoring() {
372 8 : if (btrfs_monitor_channel) {
373 8 : const res = btrfs_monitor_channel.then(() => {
374 8 : btrfs_monitor_channel = null;
375 8 : });
376 8 : btrfs_monitor_channel.close();
377 8 : return res;
378 0 : } else {
379 0 : return Promise.resolve();
380 0 : }
381 8 : }
382 :
383 112 : function btrfs_start_polling() {
384 112 : debug("starting polling for btrfs subvolumes");
385 112 : client.uuids_btrfs_subvols = { };
386 112 : client.uuids_btrfs_usage = { };
387 112 : client.uuids_btrfs_default_subvol = { };
388 112 : client.btrfs_mounts = { };
389 112 : btrfs_findmnt_poll();
390 112 : btrfs_start_monitor();
391 112 : }
392 :
393 : /* Derived indices.
394 : */
395 :
396 112 : function is_multipath_master(block) {
397 : // The master has "mpath" in its device mapper UUID. In the
398 : // future, storaged will hopefully provide this information
399 : // directly.
400 112 : if (block.Symlinks && block.Symlinks.length) {
401 112 : for (let i = 0; i < block.Symlinks.length; i++)
402 112 : if (utils.decode_filename(block.Symlinks[i]).indexOf("/dev/disk/by-id/dm-uuid-mpath-") === 0)
403 14 : return true;
404 112 : }
405 112 : return false;
406 112 : }
407 :
408 112 : function is_toplevel_drive(block) {
409 : // We consider all Block objects that point to the same Drive
410 : // objects to be multipath members for a single actual device.
411 : //
412 : // However, objects for partitions point to the same Drive object
413 : // as the object for the partition table. We have to ignore them.
414 :
415 112 : if (client.blocks_part[block.path])
416 112 : return false;
417 :
418 : // Also, eMMCs have special partition-like sub-devices that point
419 : // to the main Drive. We identify them by their name, just like
420 : // UDisks2.
421 :
422 112 : if (utils.decode_filename(block.Device).match(/\/dev\/mmcblk[0-9]boot[0-9]$/))
423 13 : return false;
424 :
425 112 : return true;
426 112 : }
427 :
428 112 : function update_indices() {
429 112 : let path;
430 112 : let block;
431 112 : let mdraid;
432 112 : let vgroup;
433 112 : let pvol;
434 112 : let lvol;
435 112 : let pool;
436 112 : let blockdev;
437 112 : let fsys;
438 112 : let part;
439 112 : let i;
440 :
441 112 : client.broken_multipath_present = false;
442 112 : client.drives_multipath_blocks = { };
443 112 : client.drives_block = { };
444 112 : for (path in client.drives) {
445 112 : client.drives_multipath_blocks[path] = [];
446 112 : }
447 112 : for (path in client.blocks) {
448 112 : block = client.blocks[path];
449 112 : if (client.drives_multipath_blocks[block.Drive] !== undefined && is_toplevel_drive(block)) {
450 112 : if (is_multipath_master(block))
451 14 : client.drives_block[block.Drive] = block;
452 : else
453 112 : client.drives_multipath_blocks[block.Drive].push(block);
454 112 : }
455 112 : }
456 112 : for (path in client.drives_multipath_blocks) {
457 : /* If there is no multipath master and only a single
458 : * member, then this is actually a normal singlepath
459 : * device.
460 : */
461 :
462 112 : if (!client.drives_block[path] && client.drives_multipath_blocks[path].length == 1) {
463 112 : client.drives_block[path] = client.drives_multipath_blocks[path][0];
464 112 : client.drives_multipath_blocks[path] = [];
465 20 : } else {
466 20 : client.drives_multipath_blocks[path].sort(utils.block_cmp);
467 20 : if (!client.drives_block[path])
468 20 : client.broken_multipath_present = true;
469 20 : }
470 112 : }
471 :
472 112 : client.mdraids_block = { };
473 112 : for (path in client.blocks) {
474 112 : block = client.blocks[path];
475 112 : if (block.MDRaid != "/")
476 19 : client.mdraids_block[block.MDRaid] = block;
477 112 : }
478 :
479 112 : client.mdraids_members = { };
480 19 : for (path in client.mdraids) {
481 19 : client.mdraids_members[path] = [];
482 19 : }
483 112 : for (path in client.blocks) {
484 112 : block = client.blocks[path];
485 112 : if (client.mdraids_members[block.MDRaidMember] !== undefined)
486 19 : client.mdraids_members[block.MDRaidMember].push(block);
487 112 : }
488 19 : for (path in client.mdraids_members) {
489 19 : client.mdraids_members[path].sort(utils.block_cmp);
490 19 : }
491 :
492 112 : client.slashdevs_block = { };
493 112 : function enter_slashdev(block, enc) {
494 112 : client.slashdevs_block[utils.decode_filename(enc)] = block;
495 112 : }
496 112 : for (path in client.blocks) {
497 112 : block = client.blocks[path];
498 112 : enter_slashdev(block, block.Device);
499 112 : enter_slashdev(block, block.PreferredDevice);
500 112 : for (i = 0; i < block.Symlinks.length; i++)
501 112 : enter_slashdev(block, block.Symlinks[i]);
502 112 : }
503 :
504 112 : client.uuids_mdraid = { };
505 19 : for (path in client.mdraids) {
506 19 : mdraid = client.mdraids[path];
507 19 : client.uuids_mdraid[mdraid.UUID] = mdraid;
508 19 : }
509 :
510 112 : client.vgnames_vgroup = { };
511 38 : for (path in client.vgroups) {
512 38 : vgroup = client.vgroups[path];
513 38 : client.vgnames_vgroup[vgroup.Name] = vgroup;
514 38 : }
515 :
516 112 : const vgroups_with_dm_pvs = { };
517 :
518 112 : client.vgroups_pvols = { };
519 38 : for (path in client.vgroups) {
520 38 : client.vgroups_pvols[path] = [];
521 38 : }
522 38 : for (path in client.blocks_pvol) {
523 38 : pvol = client.blocks_pvol[path];
524 38 : if (client.vgroups_pvols[pvol.VolumeGroup] !== undefined) {
525 38 : client.vgroups_pvols[pvol.VolumeGroup].push(pvol);
526 38 : {
527 : // HACK - this is needed below to deal with a UDisks2 bug.
528 : // https://github.com/storaged-project/udisks/pull/1206
529 38 : const block = client.blocks[path];
530 38 : if (block && utils.decode_filename(block.Device).indexOf("/dev/dm-") == 0)
531 14 : vgroups_with_dm_pvs[pvol.VolumeGroup] = true;
532 38 : }
533 38 : }
534 38 : }
535 10 : function cmp_pvols(a, b) {
536 10 : return utils.block_cmp(client.blocks[a.path], client.blocks[b.path]);
537 10 : }
538 38 : for (path in client.vgroups_pvols) {
539 38 : client.vgroups_pvols[path].sort(cmp_pvols);
540 38 : }
541 :
542 112 : client.vgroups_lvols = { };
543 38 : for (path in client.vgroups) {
544 38 : client.vgroups_lvols[path] = [];
545 38 : }
546 35 : for (path in client.lvols) {
547 35 : lvol = client.lvols[path];
548 35 : if (client.vgroups_lvols[lvol.VolumeGroup] !== undefined)
549 35 : client.vgroups_lvols[lvol.VolumeGroup].push(lvol);
550 35 : }
551 38 : for (path in client.vgroups_lvols) {
552 5 : client.vgroups_lvols[path].sort(function (a, b) { return a.Name.localeCompare(b.Name) });
553 38 : }
554 :
555 112 : client.lvols_block = { };
556 35 : for (path in client.blocks_lvm2) {
557 35 : client.lvols_block[client.blocks_lvm2[path].LogicalVolume] = client.blocks[path];
558 35 : }
559 :
560 112 : client.lvols_pool_members = { };
561 35 : for (path in client.lvols) {
562 35 : if (client.lvols[path].Type == "pool")
563 15 : client.lvols_pool_members[path] = [];
564 35 : }
565 35 : for (path in client.lvols) {
566 35 : lvol = client.lvols[path];
567 35 : if (client.lvols_pool_members[lvol.ThinPool] !== undefined)
568 15 : client.lvols_pool_members[lvol.ThinPool].push(lvol);
569 35 : }
570 15 : for (path in client.lvols_pool_members) {
571 1 : client.lvols_pool_members[path].sort(function (a, b) { return a.Name.localeCompare(b.Name) });
572 15 : }
573 :
574 22 : function summarize_stripe(lv_size, segments) {
575 22 : const pvs = { };
576 22 : let total_size = 0;
577 22 : for (const [, size, pv] of segments) {
578 22 : if (!pvs[pv])
579 22 : pvs[pv] = 0;
580 22 : pvs[pv] += size;
581 22 : total_size += size;
582 22 : }
583 22 : if (total_size < lv_size)
584 3 : pvs["/"] = lv_size - total_size;
585 22 : return pvs;
586 22 : }
587 :
588 112 : client.lvols_stripe_summary = { };
589 112 : client.lvols_status = { };
590 35 : for (path in client.lvols) {
591 35 : const struct = client.lvols[path].Structure;
592 35 : const lvol = client.lvols[path];
593 :
594 : // HACK - UDisks2 befopre 2.11 can't find the PVs of a segment
595 : // when they are on a device mapper device.
596 : //
597 : // https://github.com/storaged-project/udisks/pull/1206
598 :
599 13 : if (!client.at_least("2.11") && vgroups_with_dm_pvs[lvol.VolumeGroup])
600 35 : continue;
601 :
602 35 : let summary;
603 35 : let status = "";
604 33 : if (lvol.Layout != "thin" && struct && struct.segments) {
605 33 : summary = summarize_stripe(struct.size.v, struct.segments.v);
606 33 : if (summary["/"])
607 15 : status = "partial";
608 18 : } else if (struct && struct.data && struct.metadata &&
609 16 : (struct.data.v.length == struct.metadata.v.length || struct.metadata.v.length == 0)) {
610 20 : summary = [];
611 20 : const n_total = struct.data.v.length;
612 20 : let n_missing = 0;
613 20 : for (let i = 0; i < n_total; i++) {
614 20 : const data_lv = struct.data.v[i];
615 16 : const metadata_lv = struct.metadata.v[i] || { size: { v: 0 }, segments: { v: [] } };
616 :
617 13 : if (!data_lv.segments || (metadata_lv && !metadata_lv.segments)) {
618 13 : summary = undefined;
619 13 : break;
620 13 : }
621 :
622 20 : const s = summarize_stripe(data_lv.size.v + metadata_lv.size.v,
623 20 : data_lv.segments.v.concat(metadata_lv.segments.v));
624 20 : if (s["/"])
625 15 : n_missing += 1;
626 :
627 20 : summary.push(s);
628 20 : }
629 15 : if (n_missing > 0) {
630 15 : status = "partial";
631 14 : if (lvol.Layout == "raid1") {
632 14 : if (n_total - n_missing >= 1)
633 14 : status = "degraded";
634 14 : }
635 14 : if (lvol.Layout == "raid10") {
636 : // This is correct for two-way mirroring, which is
637 : // the only setup supported by lvm2.
638 14 : if (n_missing > n_total / 2) {
639 : // More than half of the PVs are gone -> at
640 : // least one mirror has definitely lost both
641 : // halves.
642 14 : status = "partial";
643 14 : } else if (n_missing > 1) {
644 : // Two or more PVs are lost -> one mirror
645 : // might have lost both halves
646 14 : status = "degraded-maybe-partial";
647 14 : } else {
648 : // Only one PV is missing -> no mirror has
649 : // lost both halves.
650 14 : status = "degraded";
651 14 : }
652 14 : }
653 15 : if (lvol.Layout == "raid4" || lvol.Layout == "raid5") {
654 15 : if (n_missing <= 1)
655 15 : status = "degraded";
656 15 : }
657 14 : if (lvol.Layout == "raid6") {
658 14 : if (n_missing <= 2)
659 14 : status = "degraded";
660 14 : }
661 15 : }
662 20 : }
663 35 : if (summary) {
664 35 : client.lvols_stripe_summary[path] = summary;
665 35 : client.lvols_status[path] = status;
666 35 : }
667 35 : }
668 :
669 112 : client.stratis_poolnames_pool = { };
670 27 : for (path in client.stratis_pools) {
671 27 : pool = client.stratis_pools[path];
672 27 : client.stratis_poolnames_pool[pool.Name] = pool;
673 27 : }
674 :
675 112 : client.stratis_pooluuids_pool = { };
676 27 : for (path in client.stratis_pools) {
677 27 : pool = client.stratis_pools[path];
678 27 : client.stratis_pooluuids_pool[pool.Uuid] = pool;
679 27 : }
680 :
681 112 : client.stratis_pool_blockdevs = { };
682 27 : for (path in client.stratis_pools) {
683 27 : client.stratis_pool_blockdevs[path] = [];
684 27 : }
685 27 : for (path in client.stratis_blockdevs) {
686 27 : blockdev = client.stratis_blockdevs[path];
687 27 : if (client.stratis_pools[blockdev.Pool] !== undefined)
688 27 : client.stratis_pool_blockdevs[blockdev.Pool].push(blockdev);
689 27 : }
690 :
691 112 : client.stratis_pool_filesystems = { };
692 27 : for (path in client.stratis_pools) {
693 27 : client.stratis_pool_filesystems[path] = [];
694 27 : }
695 22 : for (path in client.stratis_filesystems) {
696 22 : fsys = client.stratis_filesystems[path];
697 22 : if (client.stratis_pools[fsys.Pool] !== undefined)
698 22 : client.stratis_pool_filesystems[fsys.Pool].push(fsys);
699 22 : }
700 :
701 112 : client.blocks_stratis_fsys = { };
702 22 : for (path in client.stratis_filesystems) {
703 22 : fsys = client.stratis_filesystems[path];
704 22 : block = client.slashdevs_block[fsys.Devnode];
705 22 : if (block)
706 22 : client.blocks_stratis_fsys[block.path] = fsys;
707 22 : }
708 :
709 112 : client.blocks_stratis_blockdev = { };
710 27 : for (path in client.stratis_blockdevs) {
711 27 : block = client.slashdevs_block[client.stratis_blockdevs[path].PhysicalPath];
712 27 : if (block)
713 27 : client.blocks_stratis_blockdev[block.path] = client.stratis_blockdevs[path];
714 27 : }
715 :
716 112 : client.blocks_stratis_stopped_pool = { };
717 20 : for (const uuid in client.stratis_manager.StoppedPools) {
718 20 : const devnodes = client.stratis_stopped_pool_devnodes(uuid);
719 20 : for (const d of devnodes) {
720 20 : block = client.slashdevs_block[d];
721 20 : if (block)
722 20 : client.blocks_stratis_stopped_pool[block.path] = uuid;
723 20 : }
724 20 : }
725 :
726 112 : client.stratis_pool_stats = { };
727 27 : for (path in client.stratis_pools) {
728 27 : const pool = client.stratis_pools[path];
729 27 : const filesystems = client.stratis_pool_filesystems[path];
730 :
731 27 : const fsys_offsets = [];
732 27 : let fsys_total_used = 0;
733 27 : let fsys_total_size = 0;
734 13 : filesystems.forEach(fs => {
735 13 : fsys_offsets.push(fsys_total_used);
736 4 : fsys_total_used += fs.Used[0] ? Number(fs.Used[1]) : 0;
737 13 : fsys_total_size += Number(fs.Size);
738 13 : });
739 :
740 13 : const overhead = pool.TotalPhysicalUsed[0] ? (Number(pool.TotalPhysicalUsed[1]) - fsys_total_used) : 0;
741 27 : const pool_total = Number(pool.TotalPhysicalSize) - overhead;
742 27 : let pool_free = pool_total - fsys_total_size;
743 :
744 : // leave some margin since the above computation does not seem to
745 : // be exactly right when snapshots are involved.
746 27 : pool_free -= filesystems.length * 1024 * 1024;
747 :
748 27 : client.stratis_pool_stats[path] = {
749 27 : fsys_offsets,
750 27 : fsys_total_used,
751 27 : fsys_total_size,
752 27 : pool_total,
753 27 : pool_free,
754 27 : };
755 27 : }
756 :
757 112 : client.blocks_cleartext = { };
758 112 : for (path in client.blocks) {
759 112 : block = client.blocks[path];
760 112 : if (block.CryptoBackingDevice != "/")
761 35 : client.blocks_cleartext[block.CryptoBackingDevice] = block;
762 112 : }
763 :
764 112 : client.blocks_partitions = { };
765 112 : for (path in client.blocks_ptable) {
766 112 : client.blocks_partitions[path] = [];
767 112 : }
768 112 : for (path in client.blocks_part) {
769 112 : part = client.blocks_part[path];
770 112 : if (client.blocks_partitions[part.Table] !== undefined)
771 112 : client.blocks_partitions[part.Table].push(part);
772 112 : }
773 112 : for (path in client.blocks_partitions) {
774 112 : client.blocks_partitions[path].sort(function (a, b) { return a.Offset - b.Offset });
775 112 : }
776 :
777 112 : client.iscsi_sessions_drives = { };
778 112 : client.drives_iscsi_session = { };
779 112 : for (path in client.drives) {
780 112 : const block = client.drives_block[path];
781 112 : if (!block)
782 112 : continue;
783 14 : for (const session_path in client.iscsi_sessions) {
784 14 : const session = client.iscsi_sessions[session_path];
785 14 : for (i = 0; i < block.Symlinks.length; i++) {
786 14 : if (utils.decode_filename(block.Symlinks[i]).includes(session.data.target_name)) {
787 14 : client.drives_iscsi_session[path] = session;
788 14 : if (!client.iscsi_sessions_drives[session_path])
789 14 : client.iscsi_sessions_drives[session_path] = [];
790 14 : client.iscsi_sessions_drives[session_path].push(client.drives[path]);
791 14 : }
792 14 : }
793 14 : }
794 112 : }
795 :
796 112 : client.path_jobs = { };
797 62 : function enter_job(job) {
798 62 : if (!job.Objects || !job.Objects.length)
799 62 : return;
800 62 : job.Objects.forEach(p => {
801 62 : if (!client.path_jobs[p])
802 62 : client.path_jobs[p] = [];
803 62 : client.path_jobs[p].push(job);
804 62 : });
805 62 : }
806 75 : for (path in client.jobs) {
807 75 : enter_job(client.jobs[path]);
808 75 : }
809 :
810 : // UDisks API does not provide a btrfs volume abstraction so we keep track of
811 : // volume's by uuid in an object. uuid => [org.freedesktop.UDisks2.Filesystem.BTRFS]
812 : // https://github.com/storaged-project/udisks/issues/1232
813 112 : const old_uuids = client.uuids_btrfs_volume;
814 112 : let need_poll = false;
815 112 : client.uuids_btrfs_volume = { };
816 112 : client.uuids_btrfs_blocks = { };
817 112 : for (const p in client.blocks_fsys_btrfs) {
818 112 : const bfs = client.blocks_fsys_btrfs[p];
819 112 : const uuid = bfs.data.uuid;
820 112 : const block_fsys = client.blocks_fsys[p];
821 112 : if (!uuid)
822 112 : continue;
823 23 : if ((block_fsys && block_fsys.MountPoints.length > 0) || !client.uuids_btrfs_volume[uuid]) {
824 112 : client.uuids_btrfs_volume[uuid] = bfs;
825 112 : if (!old_uuids || !old_uuids[uuid])
826 112 : need_poll = true;
827 112 : }
828 112 : if (!client.uuids_btrfs_blocks[uuid])
829 112 : client.uuids_btrfs_blocks[uuid] = [];
830 112 : client.uuids_btrfs_blocks[uuid].push(client.blocks[p]);
831 112 : }
832 :
833 112 : if (need_poll) {
834 112 : btrfs_poll();
835 112 : }
836 112 : }
837 :
838 113 : let lvm2_poll_timer = null;
839 :
840 113 : function update_lvm2_polling(for_visibility) {
841 25 : const need_polling = !cockpit.hidden && !!Object.values(client.vgroups).find(vg => vg.NeedsPolling);
842 :
843 2 : function poll() {
844 2 : for (const path in client.vgroups) {
845 2 : const vg = client.vgroups[path];
846 2 : if (vg.NeedsPolling) {
847 2 : vg.Poll();
848 2 : }
849 2 : }
850 2 : }
851 :
852 18 : if (need_polling && lvm2_poll_timer == null) {
853 18 : lvm2_poll_timer = window.setInterval(poll, 2000);
854 18 : if (for_visibility)
855 14 : poll();
856 17 : } else if (!need_polling && lvm2_poll_timer) {
857 17 : window.clearInterval(lvm2_poll_timer);
858 17 : lvm2_poll_timer = null;
859 17 : }
860 113 : }
861 :
862 112 : client.update = (first_time) => {
863 112 : if (first_time)
864 112 : client.ready = true;
865 112 : if (client.ready) {
866 112 : update_indices();
867 112 : update_lvm2_polling(false);
868 112 : reset_pages();
869 112 : make_overview_page();
870 112 : export_mount_point_mapping();
871 112 : client.dispatchEvent("changed");
872 112 : }
873 112 : };
874 :
875 112 : function init_model(callback) {
876 112 : function pull_time() {
877 112 : return cockpit.spawn(["date", "+%s"])
878 112 : .then(function (now) {
879 112 : client.time_offset = parseInt(now, 10) * 1000 - new Date().getTime();
880 112 : });
881 112 : }
882 :
883 112 : async function enable_udisks_features() {
884 112 : if (!client.manager.valid)
885 112 : return;
886 :
887 112 : try {
888 112 : await client.manager.EnableModule("btrfs", true);
889 112 : client.manager_btrfs = proxy("Manager.BTRFS", "Manager");
890 112 : await client.manager_btrfs.wait();
891 112 : client.features.btrfs = client.manager_btrfs.valid;
892 112 : if (client.features.btrfs)
893 112 : btrfs_start_polling();
894 20 : } catch (error) {
895 20 : console.warn("Can't enable storaged btrfs module", error.toString());
896 20 : }
897 :
898 112 : try {
899 112 : await client.manager.EnableModule("iscsi", true);
900 112 : client.manager_iscsi = proxy("Manager.ISCSI.Initiator", "Manager");
901 112 : await client.manager_iscsi.wait();
902 112 : client.features.iscsi = (client.manager_iscsi.valid && client.manager_iscsi.SessionsSupported !== false);
903 20 : } catch (error) {
904 20 : console.warn("Can't enable storaged iscsi module", error.toString());
905 20 : }
906 :
907 112 : try {
908 112 : await client.manager.EnableModule("lvm2", true);
909 112 : client.manager_lvm2 = proxy("Manager.LVM2", "Manager");
910 112 : await client.manager_lvm2.wait();
911 112 : client.features.lvm2 = client.manager_lvm2.valid;
912 20 : } catch (error) {
913 20 : console.warn("Can't enable storaged lvm2 module", error.toString());
914 20 : }
915 112 : }
916 :
917 112 : async function enable_lvm_create_vdo_feature() {
918 112 : async function exit_code(cmd) {
919 112 : try {
920 112 : await cockpit.spawn(cmd, { err: "ignore", superuser: "try" });
921 13 : return 0;
922 13 : } catch (ex) {
923 112 : return ex.exit_status || 1;
924 112 : }
925 112 : }
926 :
927 : /* We assume that if LVM has the option to format a VDO volume
928 : in the kernel, then it will be used and will work. If
929 : that's not true, the error message from LVM will hopefully
930 : be clear enough to help people figure out what needs to be
931 : done.
932 : */
933 112 : if (await exit_code(["vdoformat", "--version"]) == 0 ||
934 13 : await exit_code(["lvmconfig", "--list", "allocation/vdo_use_kernel_format"]) == 0) {
935 13 : client.features.lvm_create_vdo = true;
936 13 : }
937 112 : }
938 :
939 112 : function enable_legacy_vdo_features() {
940 112 : return client.legacy_vdo_overlay.start().then(
941 112 : function (success) {
942 : // hack here
943 112 : client.features.legacy_vdo = success;
944 112 : return Promise.resolve();
945 112 : },
946 0 : function () {
947 0 : return Promise.resolve();
948 0 : });
949 112 : }
950 :
951 112 : function enable_clevis_features() {
952 112 : return cockpit.script("type clevis-luks-bind", { err: "ignore" }).then(
953 111 : function () {
954 111 : client.features.clevis = true;
955 111 : return Promise.resolve();
956 111 : },
957 1 : function () {
958 1 : return Promise.resolve();
959 1 : });
960 112 : }
961 :
962 112 : function enable_nfs_features() {
963 : // mount.nfs might be in */sbin but that isn't always in
964 : // $PATH, such as when connecting from CentOS to another
965 : // machine via SSH as non-root.
966 112 : const std_path = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
967 112 : return cockpit.script("type mount.nfs", { err: "message", environ: [std_path] }).then(
968 111 : function () {
969 111 : client.features.nfs = true;
970 111 : client.nfs.start();
971 111 : return Promise.resolve();
972 111 : },
973 1 : function () {
974 1 : return Promise.resolve();
975 1 : });
976 112 : }
977 :
978 112 : function enable_pk_features() {
979 23 : if (client.in_anaconda_mode()) {
980 23 : client.features.packagekit = false;
981 23 : return Promise.resolve();
982 23 : }
983 101 : return PK.detect().then(function (available) { client.features.packagekit = available });
984 112 : }
985 :
986 112 : function enable_stratis_feature() {
987 18 : return client.stratis_start().catch(error => {
988 18 : if (error.problem != "not-found")
989 0 : console.warn("Failed to start Stratis support", error);
990 18 : return Promise.resolve();
991 18 : });
992 112 : }
993 :
994 112 : function enable_features() {
995 112 : client.features = { };
996 112 : return (enable_udisks_features()
997 112 : .then(enable_clevis_features)
998 112 : .then(enable_nfs_features)
999 112 : .then(enable_pk_features)
1000 112 : .then(enable_stratis_feature)
1001 112 : .then(enable_lvm_create_vdo_feature)
1002 112 : .then(enable_legacy_vdo_features));
1003 112 : }
1004 :
1005 112 : function query_fsys_info() {
1006 112 : const info = {};
1007 112 : return Promise.all(client.manager.SupportedFilesystems.map(fs =>
1008 112 : client.manager.CanFormat(fs).then(canformat_result => {
1009 112 : info[fs] = {
1010 112 : can_format: canformat_result[0],
1011 112 : can_shrink: false,
1012 112 : can_grow: false
1013 112 : };
1014 112 : return client.manager.CanResize(fs)
1015 112 : .then(canresize_result => {
1016 : // We assume that all filesystems support
1017 : // offline shrinking/growing if they
1018 : // support shrinking or growing at all.
1019 : // The actual resizing utility will
1020 : // temporarily mount the fs if necessary,
1021 112 : if (canresize_result[0]) {
1022 112 : info[fs].can_shrink = !!(canresize_result[1] & 2);
1023 112 : info[fs].shrink_needs_unmount = !(canresize_result[1] & 8);
1024 112 : info[fs].can_grow = !!(canresize_result[1] & 4);
1025 112 : info[fs].grow_needs_unmount = !(canresize_result[1] & 16);
1026 112 : }
1027 112 : })
1028 : // ignore unsupported filesystems
1029 112 : .catch(() => {});
1030 112 : }))
1031 112 : ).then(() => info);
1032 112 : }
1033 :
1034 112 : client.anaconda = read_anaconda_session_storage();
1035 :
1036 112 : pull_time().then(() => {
1037 112 : read_os_release().then(os_release => {
1038 112 : client.os_release = os_release;
1039 :
1040 112 : enable_features().then(() => {
1041 112 : query_fsys_info().then((fsys_info) => {
1042 112 : client.fsys_info = fsys_info;
1043 :
1044 98 : client.storaged_client.addEventListener('notify', () => client.update());
1045 :
1046 112 : update_indices();
1047 2 : cockpit.addEventListener("visibilitychange", () => update_lvm2_polling(true));
1048 112 : btrfs_poll().then(() => {
1049 112 : client.update(true);
1050 112 : callback();
1051 112 : });
1052 112 : });
1053 112 : });
1054 112 : });
1055 112 : });
1056 112 : }
1057 :
1058 112 : client.younger_than = function younger_than(version) {
1059 112 : return utils.compare_versions(this.manager.Version, version) < 0;
1060 112 : };
1061 :
1062 22 : client.at_least = function at_least(version) {
1063 22 : return utils.compare_versions(this.manager.Version, version) >= 0;
1064 22 : };
1065 :
1066 : /* Mount users
1067 : */
1068 :
1069 34 : client.find_mount_users = (target, is_mounted) => {
1070 2 : if (is_mounted === undefined || is_mounted)
1071 0 : return python.spawn(mount_users_py, ["users", target], { superuser: "try", err: "message" }).then(JSON.parse);
1072 : else
1073 0 : return Promise.resolve([]);
1074 34 : };
1075 :
1076 36 : client.stop_mount_users = (users) => {
1077 6 : if (users && users.length > 0) {
1078 6 : return python.spawn(mount_users_py, ["stop", JSON.stringify(users)],
1079 6 : { superuser: "try", err: "message" });
1080 6 : } else
1081 33 : return Promise.resolve();
1082 36 : };
1083 :
1084 : /* Direct mounting and unmounting
1085 : *
1086 : * We don't use UDisks2 for most of our mounting and unmounting in
1087 : * order to get better control over which entry from fstab is
1088 : * selected. Once UDisks2 allows that control, we can switch back to
1089 : * it.
1090 : *
1091 : * But note that these functions still require an fstab entry.
1092 : */
1093 :
1094 36 : client.mount_at = (block, target) => {
1095 36 : const entry = block.Configuration.find(c => c[0] == "fstab" && utils.decode_filename(c[1].dir.v) == target);
1096 36 : if (entry)
1097 36 : return cockpit.script('set -e; mkdir -p "$2"; mount "$1" "$2" -o "$3"',
1098 36 : [utils.decode_filename(block.Device), target, utils.get_block_mntopts(entry[1])],
1099 0 : { superuser: "require", err: "message" });
1100 : else
1101 0 : return Promise.reject(cockpit.format("Internal error: No fstab entry for $0 and $1",
1102 0 : utils.decode_filename(block.Device),
1103 0 : target));
1104 36 : };
1105 :
1106 35 : client.unmount_at = (target, users) => {
1107 35 : return client.stop_mount_users(users).then(() => cockpit.spawn(["umount", target],
1108 35 : { superuser: "require", err: "message" }));
1109 35 : };
1110 :
1111 : /* NFS mounts
1112 : */
1113 :
1114 113 : function nfs_mounts() {
1115 113 : const self = {
1116 113 : entries: [],
1117 113 : fsys_sizes: { },
1118 :
1119 113 : start,
1120 :
1121 113 : get_fsys_size,
1122 113 : entry_users,
1123 :
1124 113 : update_entry,
1125 113 : add_entry,
1126 113 : remove_entry,
1127 :
1128 113 : mount_entry,
1129 113 : unmount_entry,
1130 113 : stop_and_unmount_entry,
1131 113 : stop_and_remove_entry,
1132 :
1133 113 : find_entry
1134 113 : };
1135 :
1136 112 : function spawn_nfs_mounts(args) {
1137 112 : return python.spawn([inotify_py, nfs_mounts_py], args, { superuser: "try", err: "message" });
1138 112 : }
1139 :
1140 112 : function start() {
1141 112 : let buf = "";
1142 112 : spawn_nfs_mounts(["monitor"])
1143 112 : .stream(function (output) {
1144 112 : buf += output;
1145 112 : const lines = buf.split("\n");
1146 112 : buf = lines[lines.length - 1];
1147 112 : if (lines.length >= 2) {
1148 112 : self.entries = JSON.parse(lines[lines.length - 2]);
1149 112 : self.fsys_sizes = { };
1150 112 : client.update();
1151 112 : }
1152 112 : })
1153 0 : .catch(function (error) {
1154 0 : if (error != "closed") {
1155 0 : console.warn(error);
1156 0 : }
1157 0 : });
1158 112 : }
1159 :
1160 3 : function get_fsys_size(entry) {
1161 3 : const path = entry.fields[1];
1162 3 : if (self.fsys_sizes[path])
1163 3 : return self.fsys_sizes[path];
1164 :
1165 3 : if (self.fsys_sizes[path] === false)
1166 2 : return null;
1167 :
1168 3 : self.fsys_sizes[path] = false;
1169 3 : cockpit.spawn(["stat", "-f", "-c", "[ %S, %f, %b ]", path], { err: "message" })
1170 3 : .then(function (output) {
1171 3 : const data = JSON.parse(output);
1172 3 : self.fsys_sizes[path] = [(data[2] - data[1]) * data[0], data[2] * data[0]];
1173 3 : client.update();
1174 3 : })
1175 0 : .catch(function () {
1176 0 : self.fsys_sizes[path] = [0, 0];
1177 0 : client.update();
1178 0 : });
1179 :
1180 3 : return null;
1181 3 : }
1182 :
1183 1 : function update_entry(entry, new_fields) {
1184 1 : return spawn_nfs_mounts(["update", JSON.stringify(entry), JSON.stringify(new_fields)]);
1185 1 : }
1186 :
1187 3 : function add_entry(fields) {
1188 3 : return spawn_nfs_mounts(["add", JSON.stringify(fields)]);
1189 3 : }
1190 :
1191 2 : function remove_entry(entry) {
1192 2 : return spawn_nfs_mounts(["remove", JSON.stringify(entry)]);
1193 2 : }
1194 :
1195 2 : function mount_entry(entry) {
1196 2 : return spawn_nfs_mounts(["mount", JSON.stringify(entry)]);
1197 2 : }
1198 :
1199 2 : function unmount_entry(entry) {
1200 2 : return spawn_nfs_mounts(["unmount", JSON.stringify(entry)]);
1201 2 : }
1202 :
1203 1 : function stop_and_unmount_entry(users, entry) {
1204 1 : return client.stop_mount_users(users).then(() => unmount_entry(entry));
1205 1 : }
1206 :
1207 1 : function stop_and_remove_entry(users, entry) {
1208 1 : return client.stop_mount_users(users).then(() => remove_entry(entry));
1209 1 : }
1210 :
1211 2 : function entry_users(entry) {
1212 2 : return client.find_mount_users(entry.fields[1], entry.mounted);
1213 2 : }
1214 :
1215 0 : function find_entry(remote, local) {
1216 0 : for (let i = 0; i < self.entries.length; i++) {
1217 0 : if (self.entries[i].fields[0] == remote && self.entries[i].fields[1] == local)
1218 0 : return self.entries[i];
1219 0 : }
1220 0 : }
1221 :
1222 113 : return self;
1223 113 : }
1224 :
1225 113 : client.nfs = nfs_mounts();
1226 :
1227 : /* Legacy VDO CLI (RHEL 8), unsupported; newer versions use VDO through LVM API */
1228 :
1229 113 : function legacy_vdo_overlay() {
1230 113 : const self = {
1231 113 : start,
1232 :
1233 113 : volumes: [],
1234 :
1235 113 : by_name: { },
1236 113 : by_dev: { },
1237 113 : by_backing_dev: { },
1238 :
1239 113 : find_by_block,
1240 113 : find_by_backing_block,
1241 :
1242 113 : create
1243 113 : };
1244 :
1245 0 : function cmd(args) {
1246 0 : return cockpit.spawn(["vdo"].concat(args),
1247 0 : {
1248 0 : superuser: "require",
1249 0 : err: "message"
1250 0 : });
1251 0 : }
1252 :
1253 0 : function update(data) {
1254 0 : self.by_name = { };
1255 0 : self.by_dev = { };
1256 0 : self.by_backing_dev = { };
1257 :
1258 0 : self.volumes = data.map(function (vol, index) {
1259 0 : const name = vol.name;
1260 :
1261 0 : function volcmd(args) {
1262 0 : return cmd(args.concat(["--name", name]));
1263 0 : }
1264 :
1265 0 : const v = {
1266 0 : name,
1267 0 : broken: vol.broken,
1268 0 : dev: "/dev/mapper/" + name,
1269 0 : backing_dev: vol.device,
1270 0 : logical_size: vol.logical_size,
1271 0 : physical_size: vol.physical_size,
1272 0 : index_mem: vol.index_mem,
1273 0 : compression: vol.compression,
1274 0 : deduplication: vol.deduplication,
1275 0 : activated: vol.activated,
1276 :
1277 0 : set_compression: function(val) {
1278 0 : return volcmd([val ? "enableCompression" : "disableCompression"]);
1279 0 : },
1280 :
1281 0 : set_deduplication: function(val) {
1282 0 : return volcmd([val ? "enableDeduplication" : "disableDeduplication"]);
1283 0 : },
1284 :
1285 0 : set_activate: function(val) {
1286 0 : return volcmd([val ? "activate" : "deactivate"]);
1287 0 : },
1288 :
1289 0 : start: function() {
1290 0 : return volcmd(["start"]);
1291 0 : },
1292 :
1293 0 : stop: function() {
1294 0 : return volcmd(["stop"]);
1295 0 : },
1296 :
1297 0 : remove: function() {
1298 0 : return volcmd(["remove"]);
1299 0 : },
1300 :
1301 0 : force_remove: function() {
1302 0 : return volcmd(["remove", "--force"]);
1303 0 : },
1304 :
1305 0 : grow_physical: function() {
1306 0 : return volcmd(["growPhysical"]);
1307 0 : },
1308 :
1309 0 : grow_logical: function(lsize) {
1310 0 : return volcmd(["growLogical", "--vdoLogicalSize", lsize + "B"]);
1311 0 : }
1312 0 : };
1313 :
1314 0 : self.by_name[v.name] = v;
1315 0 : self.by_dev[v.dev] = v;
1316 0 : self.by_backing_dev[v.backing_dev] = v;
1317 :
1318 0 : return v;
1319 0 : });
1320 :
1321 0 : client.update();
1322 0 : }
1323 :
1324 112 : function start() {
1325 112 : let buf = "";
1326 :
1327 112 : return cockpit.spawn(["/bin/sh", "-c", "head -1 $(command -v vdo || echo /dev/null)"],
1328 112 : { err: "ignore" })
1329 112 : .then(function (shebang) {
1330 13 : if (shebang != "") {
1331 13 : self.python = shebang.replace(/#! */, "").trim("\n");
1332 13 : cockpit.spawn([self.python, "--", "-"], { superuser: "try", err: "message" })
1333 13 : .input(inotify_py + vdo_monitor_py)
1334 0 : .stream(function (output) {
1335 0 : buf += output;
1336 0 : const lines = buf.split("\n");
1337 0 : buf = lines[lines.length - 1];
1338 0 : if (lines.length >= 2) {
1339 0 : update(JSON.parse(lines[lines.length - 2]));
1340 0 : }
1341 0 : })
1342 0 : .catch(function (error) {
1343 0 : if (error != "closed") {
1344 0 : console.warn(error);
1345 0 : }
1346 0 : });
1347 13 : return true;
1348 13 : } else {
1349 112 : return false;
1350 112 : }
1351 112 : });
1352 112 : }
1353 :
1354 112 : function some(array, func) {
1355 112 : let i;
1356 112 : for (i = 0; i < array.length; i++) {
1357 112 : const val = func(array[i]);
1358 112 : if (val)
1359 12 : return val;
1360 112 : }
1361 112 : return null;
1362 112 : }
1363 :
1364 97 : function find_by_block(block) {
1365 97 : function check(encoded) { return self.by_dev[utils.decode_filename(encoded)] }
1366 97 : return check(block.Device) || some(block.Symlinks, check);
1367 97 : }
1368 :
1369 112 : function find_by_backing_block(block) {
1370 112 : function check(encoded) { return self.by_backing_dev[utils.decode_filename(encoded)] }
1371 112 : return check(block.Device) || some(block.Symlinks, check);
1372 112 : }
1373 :
1374 0 : function create(options) {
1375 0 : const args = ["create", "--name", options.name,
1376 0 : "--device", utils.decode_filename(options.block.PreferredDevice)];
1377 0 : if (options.logical_size !== undefined)
1378 0 : args.push("--vdoLogicalSize", options.logical_size + "B");
1379 0 : if (options.index_mem !== undefined)
1380 0 : args.push("--indexMem", options.index_mem / (1024 * 1024 * 1024));
1381 0 : if (options.compression !== undefined)
1382 0 : args.push("--compression", options.compression ? "enabled" : "disabled");
1383 0 : if (options.deduplication !== undefined)
1384 0 : args.push("--deduplication", options.deduplication ? "enabled" : "disabled");
1385 0 : if (options.emulate_512 !== undefined)
1386 0 : args.push("--emulate512", options.emulate_512 ? "enabled" : "disabled");
1387 0 : return cmd(args);
1388 0 : }
1389 :
1390 113 : return self;
1391 113 : }
1392 :
1393 113 : client.legacy_vdo_overlay = legacy_vdo_overlay();
1394 :
1395 : /* Stratis */
1396 :
1397 112 : client.stratis_start = () => {
1398 112 : return stratis3_start();
1399 112 : };
1400 :
1401 : // We need to use the same revision for all interfaces, mixing them is
1402 : // not allowed. If we need to bump it, it should be bumped here for all
1403 : // of them at the same time.
1404 : //
1405 : // We try all these versions in order, and use the first we find.
1406 : //
1407 113 : const stratis3_interface_revisions = [8, 6];
1408 :
1409 112 : async function stratis3_start() {
1410 112 : const stratis = cockpit.dbus("org.storage.stratis3", { superuser: "try" });
1411 :
1412 : // The rest of the code expects these to be initialized even if no
1413 : // stratisd is found.
1414 112 : client.stratis_pools = { };
1415 112 : client.stratis_blockdevs = { };
1416 112 : client.stratis_filesystems = { };
1417 :
1418 112 : client.stratis_interface_revision = null;
1419 112 : let last_error;
1420 112 : for (const rev of stratis3_interface_revisions) {
1421 112 : client.stratis_manager = stratis.proxy("org.storage.stratis3.Manager.r" + rev, "/org/storage/stratis3");
1422 112 : client.stratis_manager.StoppedPools = {};
1423 112 : try {
1424 112 : await client.stratis_manager.wait();
1425 95 : client.stratis_interface_revision = rev;
1426 95 : break;
1427 14 : } catch (e) {
1428 31 : last_error = e;
1429 31 : }
1430 112 : }
1431 :
1432 112 : if (!client.stratis_interface_revision)
1433 31 : throw last_error;
1434 :
1435 1 : client.stratis_store_passphrase = (desc, passphrase) => {
1436 1 : return python.spawn(stratis3_set_key_py, [desc], { superuser: "require" })
1437 1 : .input(passphrase);
1438 1 : };
1439 :
1440 4 : client.stratis_set_property = (proxy, prop, sig, value) => {
1441 : // DBusProxy is smart enough to allow "proxy.Prop
1442 : // = value" to just work, but we want to catch any
1443 : // error ourselves, and we want to wait for the
1444 : // method call to complete.
1445 4 : return stratis.call(proxy.path, "org.freedesktop.DBus.Properties", "Set",
1446 4 : [proxy.iface, prop, cockpit.variant(sig, value)]);
1447 4 : };
1448 :
1449 9 : client.stratis_stopped_pool_devnodes = (uuid) => {
1450 9 : const devs = client.stratis_manager.StoppedPools[uuid]?.devs;
1451 9 : if (!devs)
1452 2 : return [];
1453 9 : if (devs.t == 'aa{ss}')
1454 9 : return devs.v.map(d => d.devnode);
1455 2 : else if (devs.t == 'aa{sv}')
1456 0 : return devs.v.map(d => d.devnode.v);
1457 : else
1458 2 : return [];
1459 9 : };
1460 :
1461 95 : client.features.stratis = true;
1462 95 : client.stratis_pools = client.stratis_manager.client.proxies("org.storage.stratis3.pool.r" +
1463 95 : client.stratis_interface_revision,
1464 95 : "/org/storage/stratis3",
1465 95 : { watch: false });
1466 95 : client.stratis_blockdevs = client.stratis_manager.client.proxies("org.storage.stratis3.blockdev.r" +
1467 95 : client.stratis_interface_revision,
1468 95 : "/org/storage/stratis3",
1469 95 : { watch: false });
1470 95 : client.stratis_filesystems = client.stratis_manager.client.proxies("org.storage.stratis3.filesystem.r" +
1471 95 : client.stratis_interface_revision,
1472 95 : "/org/storage/stratis3",
1473 95 : { watch: false });
1474 :
1475 95 : await stratis.watch({ path_namespace: "/org/storage/stratis3" });
1476 16 : client.stratis_manager.client.addEventListener('notify', (event, data) => {
1477 16 : client.update();
1478 16 : });
1479 112 : }
1480 :
1481 112 : function init_client(manager, callback) {
1482 112 : if (client.manager)
1483 112 : return;
1484 :
1485 112 : client.storaged_client = manager.client;
1486 112 : client.manager = manager;
1487 :
1488 112 : init_proxies().then(() => init_model(callback));
1489 112 : }
1490 :
1491 113 : client.init = function init_storaged(callback) {
1492 113 : const udisks = cockpit.dbus("org.freedesktop.UDisks2", { superuser: "try" });
1493 113 : const udisks_manager = udisks.proxy("org.freedesktop.UDisks2.Manager",
1494 113 : "/org/freedesktop/UDisks2/Manager", { watch: true });
1495 :
1496 112 : udisks_manager.wait().then(() => init_client(udisks_manager, callback))
1497 0 : .catch(ex => {
1498 0 : console.warn("client.init(): udisks manager proxy failed:", JSON.stringify(ex));
1499 0 : client.features = false;
1500 0 : callback();
1501 0 : });
1502 :
1503 112 : udisks_manager.addEventListener("changed", () => init_client(udisks_manager, callback));
1504 113 : };
1505 :
1506 48 : client.wait_for = function wait_for(cond) {
1507 48 : return new Promise(resolve => {
1508 48 : function check() {
1509 48 : const res = cond();
1510 48 : if (res) {
1511 48 : client.removeEventListener("changed", check);
1512 48 : resolve(res);
1513 48 : }
1514 48 : }
1515 :
1516 48 : client.addEventListener("changed", check);
1517 48 : check();
1518 48 : });
1519 48 : };
1520 :
1521 106 : client.get_config = (name, def) =>
1522 106 : get_manifest_config_matchlist("storage", name, def, [client.os_release.PLATFORM_ID, client.os_release.ID]);
1523 :
1524 113 : client.in_anaconda_mode = in_anaconda_mode;
1525 :
1526 112 : client.strip_mount_point_prefix = (dir) => {
1527 22 : const mpp = client.anaconda?.mount_point_prefix;
1528 :
1529 22 : if (dir && mpp) {
1530 22 : if (dir.indexOf(mpp) != 0)
1531 22 : return false;
1532 :
1533 17 : dir = dir.substring(mpp.length);
1534 17 : if (dir == "")
1535 16 : dir = "/";
1536 22 : }
1537 :
1538 110 : return dir;
1539 112 : };
1540 :
1541 52 : client.add_mount_point_prefix = (dir) => {
1542 5 : const mpp = client.anaconda?.mount_point_prefix;
1543 5 : if (mpp && dir != "") {
1544 5 : if (dir == "/")
1545 3 : dir = mpp;
1546 : else
1547 4 : dir = mpp + dir;
1548 5 : }
1549 52 : return dir;
1550 52 : };
1551 :
1552 11 : client.should_ignore_device = (devname) => {
1553 11 : return client.anaconda?.available_devices && client.anaconda.available_devices.indexOf(devname) == -1;
1554 11 : };
1555 :
1556 11 : client.should_ignore_block = (block) => {
1557 11 : return client.should_ignore_device(utils.decode_filename(block.PreferredDevice));
1558 11 : };
1559 :
1560 113 : export default client;
|