Line data Source code
1 : /*
2 : * Copyright (C) 2013 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 37 : import React from "react";
6 37 : import cockpit from 'cockpit';
7 :
8 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
9 :
10 : import { fmt_to_fragments } from 'utils.jsx';
11 : import * as utils from './utils.js';
12 : import { v4 as uuidv4 } from 'uuid';
13 :
14 : import "./networking.scss";
15 :
16 : import { show_modal_dialog } from "cockpit-components-dialog.jsx";
17 :
18 37 : const _ = cockpit.gettext;
19 :
20 1 : export function show_error_dialog(title, message) {
21 1 : const props = {
22 1 : id: "error-popup",
23 1 : title,
24 1 : body: <p>{message}</p>
25 1 : };
26 :
27 1 : const footer = {
28 1 : actions: [],
29 1 : cancel_button: { text: _("Close"), variant: "secondary" }
30 1 : };
31 :
32 1 : show_modal_dialog(props, footer);
33 1 : }
34 :
35 0 : export function show_unexpected_error(error) {
36 0 : show_error_dialog(_("Unexpected error"), error.message || error);
37 0 : }
38 :
39 3 : function show_breaking_change_dialog({ fail_text, anyway_text, action }) {
40 3 : const props = {
41 3 : titleIconVariant: "warning",
42 3 : id: "confirm-breaking-change-popup",
43 3 : title: _("Connection will be lost"),
44 3 : body: <p>{fail_text}</p>
45 3 : };
46 :
47 3 : const footer = {
48 3 : actions: [
49 3 : {
50 3 : caption: anyway_text,
51 3 : clicked: action,
52 3 : style: "danger",
53 3 : }
54 3 : ],
55 3 : cancel_button: { text: _("Keep connection"), variant: "secondary" }
56 3 : };
57 :
58 3 : show_modal_dialog(props, footer);
59 3 : }
60 :
61 37 : export function connection_settings(c) {
62 37 : if (c && c.Settings && c.Settings.connection) {
63 37 : return c.Settings.connection;
64 7 : } else {
65 : // It is a programming error if we ever access a Connection
66 : // object that doesn't have it's settings yet, and we expect
67 : // each Connection object to have "connection" settings.
68 7 : console.warn("Incomplete 'Connection' object accessed", c);
69 7 : return { };
70 7 : }
71 37 : }
72 :
73 : /* NetworkManagerModel
74 : *
75 : * The NetworkManager model maintains a mostly-read-only data
76 : * structure that represents the state of the NetworkManager service
77 : * on a given machine.
78 : *
79 : * The data structure consists of JavaScript values such as objects,
80 : * arrays, and strings that point at each other. It might have
81 : * cycles. In general, it follows the NetworkManager D-Bus API but
82 : * tries to hide annoyances such as endian issues.
83 : *
84 : * For example,
85 : *
86 : * const manager = model.get_manager();
87 : * manager.Devices[0].ActiveConnection.Ipv4Config.Addresses[0][0]
88 : *
89 : * is the first IPv4 address of the first device as a string.
90 : *
91 : * The model initializes itself asynchronously and emits the 'changed'
92 : * event whenever anything changes. If you only access the data
93 : * structure from within the 'changed' event handler, you should
94 : * always see it in a complete state.
95 : *
96 : * In other words, any change in the data structure from one 'changed'
97 : * event to the next represents a real change in the state of
98 : * NetworkManager.
99 : *
100 : * When a new model is created, its main 'manager' object starts out
101 : * as 'null'. The first 'changed' event signals that initialization
102 : * is complete and that the whole data structure is now stable and
103 : * reachable from the 'manager' object.
104 : *
105 : * Methods are invoked directly on the objects in the data structure.
106 : * For example,
107 : *
108 : * manager.Devices[0].disconnect();
109 : * manager.Devices[0].ActiveConnection.deactivate();
110 : *
111 : * TODO - document the details of the data structure.
112 : */
113 :
114 : /* HACK
115 : *
116 : * NetworkManager used to not implement the standard o.fd.DBus.Properties
117 : * interface and our code still operates under the assumptions stated below.
118 : *
119 : * 1) NM does not emit the PropertiesChanged signal on the
120 : * o.fd.DBus.Properties interface but rather on its own interfaces
121 : * like o.fd.NetworkManager.Device.Wired.
122 : *
123 : * 2) NM does not always emit the PropertiesChanged signal on the
124 : * interface whose properties have changed. For example, when a
125 : * property on o.fd.NM.Device changes, this might be notified by a
126 : * PropertiesChanged signal on the o.fd.NM.Device.Wired interface
127 : * for the same object path.
128 : *
129 : * https://bugzilla.gnome.org/show_bug.cgi?id=729826
130 : *
131 : * We cope with this here by merging all properties of all interfaces
132 : * for a given object path. This is appropriate and nice for
133 : * NetworkManager, and we should probably keep it that way even if
134 : * NetworkManager would use a standard o.fd.DBus.Properties API.
135 : * In the future this could be rewritten to use DBusProxies.
136 : */
137 :
138 37 : export function NetworkManagerModel() {
139 : /*
140 : * The NetworkManager model doesn't need proxies in its DBus client.
141 : * It uses the 'raw' dbus events and methods and constructs its own data
142 : * structure. This has the advantage of avoiding wasting
143 : * resources for maintaining the unused proxies, avoids some code
144 : * complexity, and allows to do the right thing with the
145 : * peculiarities of the NetworkManager API.
146 : */
147 :
148 37 : const self = this;
149 37 : cockpit.event_target(self);
150 :
151 37 : const client = cockpit.dbus("org.freedesktop.NetworkManager", { superuser: "try" });
152 37 : self.client = client;
153 :
154 : // set to false by default as newer API is backwards compatible
155 : // and supports both "dns" and "dns-data" properties
156 : // dns-data property was added in version 1.42.0
157 37 : self.supports_dns_data = false;
158 :
159 37 : function check_version_dns_data_support(version) {
160 7 : if (version.length < 2) {
161 7 : return false;
162 7 : }
163 :
164 7 : if (version[0] > 1) {
165 7 : return true;
166 7 : } else if (version[0] === 1 && version[1] >= 42) {
167 37 : return true;
168 37 : }
169 :
170 7 : return false;
171 37 : }
172 :
173 : /* resolved once first stage of initialization is done */
174 37 : self.preinit = new Promise((resolve, reject) => {
175 37 : client.call("/org/freedesktop/NetworkManager",
176 37 : "org.freedesktop.DBus.Properties", "Get",
177 37 : ["org.freedesktop.NetworkManager", "Version"], { flags: "" })
178 37 : .then((reply, options) => {
179 37 : const nm_version = reply[0].v.split(".").map(n => Number.parseInt(n));
180 37 : self.supports_dns_data = check_version_dns_data_support(nm_version);
181 :
182 37 : if (options.flags) {
183 37 : if (options.flags.indexOf(">") !== -1)
184 7 : utils.set_byteorder("be");
185 37 : else if (options.flags.indexOf("<") !== -1)
186 37 : utils.set_byteorder("le");
187 37 : resolve();
188 37 : }
189 37 : })
190 37 : .catch(complain);
191 37 : });
192 :
193 : /* Mostly generic D-Bus stuff. */
194 :
195 37 : const objects = { };
196 :
197 5 : self.set_curtain = (state) => {
198 5 : self.curtain = state;
199 5 : self.dispatchEvent("changed");
200 5 : };
201 :
202 : /* This is a test helper so that we wait for operations to finish before moving forward with the test */
203 21 : self.set_operation_in_progress = (value) => {
204 21 : self.operationInProgress = value;
205 21 : self.dispatchEvent("changed");
206 21 : };
207 :
208 0 : function complain() {
209 0 : self.ready = false;
210 0 : console.warn.apply(console, arguments);
211 0 : }
212 :
213 37 : function conv_Object(type) {
214 37 : return function (path) {
215 37 : return get_object(path, type);
216 37 : };
217 37 : }
218 :
219 37 : function conv_Array(conv) {
220 37 : return function (elts) {
221 37 : return elts.map(conv);
222 37 : };
223 37 : }
224 :
225 37 : function priv(obj) {
226 37 : return obj[' priv'];
227 37 : }
228 :
229 37 : let outstanding_refreshes = 0;
230 :
231 37 : function push_refresh() {
232 37 : outstanding_refreshes += 1;
233 37 : }
234 :
235 37 : function pop_refresh() {
236 37 : outstanding_refreshes -= 1;
237 37 : if (outstanding_refreshes === 0)
238 37 : export_model();
239 37 : }
240 :
241 37 : function get_object(path, type) {
242 37 : if (path == "/")
243 37 : return null;
244 37 : function Constructor() {
245 37 : this[' priv'] = { };
246 37 : priv(this).type = type;
247 37 : priv(this).path = path;
248 37 : for (const p in type.props)
249 37 : this[p] = type.props[p].def;
250 37 : }
251 37 : if (!objects[path]) {
252 37 : Constructor.prototype = type.prototype;
253 37 : objects[path] = new Constructor();
254 37 : if (type.refresh)
255 37 : type.refresh(objects[path]);
256 37 : if (type.exporters && type.exporters[0])
257 37 : type.exporters[0](objects[path]);
258 37 : }
259 37 : return objects[path];
260 37 : }
261 :
262 37 : function peek_object(path) {
263 36 : return objects[path] || null;
264 37 : }
265 :
266 30 : function drop_object(path) {
267 30 : const obj = objects[path];
268 30 : if (obj) {
269 30 : if (priv(obj).type.drop)
270 9 : priv(obj).type.drop(obj);
271 30 : delete objects[path];
272 30 : export_model();
273 30 : }
274 30 : }
275 :
276 37 : function set_object_properties(obj, props) {
277 37 : const decl = priv(obj).type.props;
278 37 : for (const p in decl) {
279 37 : let val = props[decl[p].prop || p];
280 37 : if (val !== undefined) {
281 37 : if (decl[p].conv)
282 37 : val = decl[p].conv(val);
283 37 : if (val !== obj[p]) {
284 37 : obj[p] = val;
285 37 : if (decl[p].trigger)
286 37 : decl[p].trigger(obj);
287 37 : }
288 37 : }
289 37 : }
290 37 : }
291 :
292 31 : function remove_signatures(props_with_sigs) {
293 31 : const props = { };
294 31 : for (const p in props_with_sigs) {
295 31 : if (props_with_sigs[p]) {
296 31 : props[p] = props_with_sigs[p].v;
297 31 : }
298 31 : }
299 31 : return props;
300 31 : }
301 :
302 37 : function objpath(obj) {
303 37 : if (obj && priv(obj).path)
304 22 : return priv(obj).path;
305 : else
306 22 : return "/";
307 37 : }
308 :
309 28 : function call_object_method(obj, iface, method) {
310 28 : return client.call(objpath(obj), iface, method, Array.prototype.slice.call(arguments, 3));
311 28 : }
312 :
313 37 : const interface_types = { };
314 37 : let max_export_phases = 0;
315 37 : let export_pending;
316 :
317 37 : function set_object_types(all_types) {
318 37 : all_types.forEach(function (type) {
319 37 : if (type.exporters && type.exporters.length > max_export_phases)
320 37 : max_export_phases = type.exporters.length;
321 37 : type.interfaces.forEach(function (iface) {
322 37 : interface_types[iface] = type;
323 37 : });
324 37 : });
325 37 : }
326 :
327 31 : function signal_emitted(path, iface, signal, args) {
328 31 : const obj = peek_object(path);
329 :
330 31 : if (obj) {
331 31 : const type = priv(obj).type;
332 :
333 31 : if (signal == "PropertiesChanged") {
334 31 : push_refresh();
335 31 : const props = remove_signatures(args[0]);
336 31 : set_object_properties(obj, props);
337 31 : pop_refresh();
338 24 : } else if (type.signals && type.signals[signal])
339 23 : type.signals[signal](obj, args);
340 31 : }
341 31 : }
342 :
343 37 : function interface_properties(path, iface, props) {
344 37 : const type = interface_types[iface];
345 37 : if (type)
346 37 : set_object_properties(get_object(path, type), props);
347 37 : }
348 :
349 30 : function interface_removed(path, iface) {
350 : /* For NetworkManager we can make this assumption */
351 30 : drop_object(path);
352 30 : }
353 :
354 37 : let export_model_promise = null;
355 37 : let export_model_promise_resolve = null;
356 :
357 37 : function export_model() {
358 37 : function doit() {
359 37 : for (let phase = 0; phase < max_export_phases; phase++) {
360 37 : for (const path in objects) {
361 37 : const obj = objects[path];
362 37 : const exp = priv(obj).type.exporters;
363 37 : if (exp && exp[phase])
364 37 : exp[phase](obj);
365 37 : }
366 37 : }
367 :
368 37 : self.ready = true;
369 37 : self.dispatchEvent('changed');
370 20 : if (export_model_promise) {
371 20 : export_model_promise_resolve();
372 20 : export_model_promise = null;
373 20 : export_model_promise_resolve = null;
374 20 : }
375 37 : }
376 :
377 37 : if (!export_pending) {
378 37 : export_pending = true;
379 37 : window.setTimeout(function () { export_pending = false; doit() }, 300);
380 37 : }
381 37 : }
382 :
383 23 : self.synchronize = function synchronize() {
384 23 : if (outstanding_refreshes === 0) {
385 23 : return Promise.resolve();
386 16 : } else {
387 16 : if (!export_model_promise)
388 14 : export_model_promise = new Promise(resolve => { export_model_promise_resolve = resolve });
389 16 : return export_model_promise;
390 16 : }
391 23 : };
392 :
393 37 : let subscription;
394 37 : let watch;
395 :
396 37 : function onNotifyEventHandler(event, data) {
397 37 : Object.keys(data).forEach(path => {
398 37 : const interfaces = data[path];
399 :
400 37 : Object.keys(interfaces).forEach(iface => {
401 37 : const props = interfaces[iface];
402 :
403 : /* Capture connection failure reasons for devices
404 : NM transitions through PREPARE → CONFIG → NEED_AUTH → FAILED → DISCONNECTED very
405 : quickly, and React batches these updates, so the UI often misses the FAILED state.
406 : Store the failure reason here at the D-Bus event level so we can retrieve it later. */
407 37 : if (path.includes("/Devices/") && props?.StateReason) {
408 37 : const obj = peek_object(path);
409 37 : if (obj) {
410 37 : const [state, reason] = props.StateReason;
411 8 : if (state === 120 && reason !== 0) {
412 8 : utils.debug("Captured", obj.Interface, "failure, reason:", reason);
413 8 : priv(obj).lastFailureReason = reason;
414 8 : }
415 37 : }
416 37 : }
417 :
418 37 : if (props)
419 33 : interface_properties(path, iface, props);
420 : else
421 33 : interface_removed(path, iface);
422 37 : });
423 37 : });
424 37 : }
425 :
426 37 : self.preinit.then(() => {
427 37 : subscription = client.subscribe({ }, signal_emitted);
428 37 : client.addEventListener("notify", onNotifyEventHandler);
429 37 : watch = client.watch({ path_namespace: "/org/freedesktop" });
430 1 : client.addEventListener("owner", (event, owner) => {
431 1 : if (owner) {
432 1 : watch.remove();
433 1 : watch = client.watch({ path_namespace: "/org/freedesktop" });
434 1 : }
435 1 : });
436 37 : });
437 :
438 0 : self.close = function close() {
439 0 : subscription.remove();
440 0 : watch.remove();
441 0 : client.removeEventListener("notify", onNotifyEventHandler);
442 0 : client.close("unused");
443 0 : };
444 :
445 : /* NetworkManager specific data conversions and utility functions.
446 : */
447 :
448 37 : function ip_address_from_nm(addr) {
449 37 : return {
450 37 : address: addr.address.v,
451 37 : prefix: utils.ip_prefix_to_text(addr.prefix.v)
452 37 : };
453 37 : }
454 :
455 10 : function ip_address_to_nm(addr, ipv) {
456 3 : const prefix = ipv === "ipv4" ? utils.ip4_prefix_from_text(addr.prefix) : utils.ip_prefix_from_text(addr.prefix);
457 :
458 2 : if (!utils.validate_ip(addr.address)) {
459 2 : throw cockpit.format(_("Invalid IP address: $0"), addr.address);
460 2 : }
461 :
462 10 : return {
463 10 : address: { t: "s", v: addr.address },
464 10 : prefix: { t: "u", v: prefix },
465 10 : };
466 10 : }
467 :
468 2 : function route_from_nm(route) {
469 1 : const metric = route.metric ? utils.ip_metric_to_text(route.metric.v) : "";
470 2 : return {
471 2 : dest: route.dest.v,
472 2 : prefix: utils.ip_prefix_to_text(route.prefix.v),
473 1 : next_hop: route["next-hop"]?.v ?? "",
474 2 : metric,
475 2 : };
476 2 : }
477 :
478 2 : function route_to_nm(route, ipv) {
479 1 : const prefix = ipv === "ipv4" ? utils.ip4_prefix_from_text(route.prefix) : utils.ip_prefix_from_text(route.prefix);
480 :
481 1 : if (!utils.validate_ip(route.dest)) {
482 1 : throw cockpit.format(_("Invalid destination address: $0"), route.dest);
483 1 : }
484 :
485 2 : const route_nm = {
486 2 : dest: { t: "s", v: route.dest },
487 2 : prefix: { t: "u", v: prefix },
488 2 : };
489 :
490 : // next-hop is an optional property
491 2 : if (route.next_hop !== "") {
492 1 : if (!utils.validate_ip(route.next_hop)) {
493 1 : throw cockpit.format(_("Invalid gateway address: $0"), route.next_hop);
494 1 : }
495 :
496 2 : route_nm["next-hop"] = { t: "s", v: route.next_hop };
497 2 : }
498 :
499 : // metric is an optional property
500 2 : if (route.metric !== "") {
501 2 : route_nm.metric = { t: "u", v: utils.ip_metric_from_text(route.metric) };
502 2 : }
503 :
504 2 : return route_nm;
505 2 : }
506 :
507 37 : function settings_from_nm(settings) {
508 37 : function get(first, second, def) {
509 37 : if (settings[first] && settings[first][second])
510 37 : return settings[first][second].v;
511 : else
512 37 : return def;
513 37 : }
514 :
515 37 : function get_ip(first, ip_to_text) {
516 37 : const dns_data = self.supports_dns_data
517 37 : ? get(first, "dns-data", [])
518 7 : : get(first, "dns", []).map(ip_to_text);
519 :
520 37 : return {
521 37 : method: get(first, "method", "auto"),
522 37 : ignore_auto_dns: get(first, "ignore-auto-dns", false),
523 37 : ignore_auto_routes: get(first, "ignore-auto-routes", false),
524 37 : address_data: get(first, "address-data", []).map(ip_address_from_nm),
525 37 : gateway: get(first, "gateway", ""),
526 37 : dns_data,
527 37 : dns_search: get(first, "dns-search", []),
528 37 : route_data: get(first, "route-data", []).map(route_from_nm),
529 37 : };
530 37 : }
531 :
532 37 : const result = {
533 37 : connection: {
534 37 : type: get("connection", "type"),
535 37 : uuid: get("connection", "uuid"),
536 37 : interface_name: get("connection", "interface-name"),
537 37 : timestamp: get("connection", "timestamp", 0),
538 37 : id: get("connection", "id", _("Unknown")),
539 37 : autoconnect: get("connection", "autoconnect", true),
540 37 : autoconnect_priority: get("connection", "autoconnect-priority", 0),
541 37 : autoconnect_members:
542 37 : get("connection", "autoconnect-slaves", -1),
543 37 : member_type: get("connection", "slave-type"),
544 37 : group: get("connection", "master"),
545 37 : multi_connect: get("connection", "multi-connect"),
546 37 : }
547 37 : };
548 :
549 37 : if (!settings.connection.master) {
550 37 : result.ipv4 = get_ip("ipv4", utils.ip4_to_text);
551 37 : result.ipv6 = get_ip("ipv6", utils.ip6_to_text);
552 37 : }
553 :
554 37 : if (settings["802-3-ethernet"]) {
555 37 : result.ethernet = {
556 37 : mtu: get("802-3-ethernet", "mtu"),
557 37 : assigned_mac_address: get("802-3-ethernet", "assigned-mac-address")
558 37 : };
559 37 : }
560 :
561 15 : if (settings.bond) {
562 : /* Options are documented as part of the Linux bonding driver.
563 : https://www.kernel.org/doc/Documentation/networking/bonding.txt
564 : */
565 15 : result.bond = {
566 15 : options: { ...get("bond", "options", { }) },
567 15 : interface_name: get("bond", "interface-name")
568 15 : };
569 15 : }
570 :
571 2 : function JSON_parse_carefully(str) {
572 2 : try {
573 2 : return JSON.parse(str);
574 1 : } catch (e) {
575 1 : return null;
576 1 : }
577 2 : }
578 :
579 8 : if (settings.team) {
580 8 : result.team = {
581 8 : config: JSON_parse_carefully(get("team", "config", "{}")),
582 8 : interface_name: get("team", "interface-name")
583 8 : };
584 8 : }
585 :
586 8 : if (settings["team-port"] || result.connection.member_type == "team") {
587 8 : result.team_port = { config: JSON_parse_carefully(get("team-port", "config", "{}")), };
588 8 : }
589 :
590 10 : if (settings.bridge) {
591 10 : result.bridge = {
592 10 : interface_name: get("bridge", "interface-name"),
593 10 : stp: get("bridge", "stp", true),
594 10 : priority: get("bridge", "priority", 32768),
595 10 : forward_delay: get("bridge", "forward-delay", 15),
596 10 : hello_time: get("bridge", "hello-time", 2),
597 10 : max_age: get("bridge", "max-age", 20),
598 10 : ageing_time: get("bridge", "ageing-time", 300)
599 10 : };
600 10 : }
601 :
602 9 : if (settings["bridge-port"] || result.connection.member_type == "bridge") {
603 9 : result.bridge_port = {
604 9 : priority: get("bridge-port", "priority", 32),
605 9 : path_cost: get("bridge-port", "path-cost", 100),
606 9 : hairpin_mode: get("bridge-port", "hairpin-mode", false)
607 9 : };
608 9 : }
609 :
610 8 : if (settings.vlan) {
611 8 : result.vlan = {
612 8 : parent: get("vlan", "parent"),
613 8 : id: get("vlan", "id"),
614 8 : interface_name: get("vlan", "interface-name")
615 8 : };
616 8 : }
617 :
618 8 : if (settings.wireguard) {
619 8 : result.wireguard = {
620 8 : listen_port: get("wireguard", "listen-port", 0),
621 1 : peers: get("wireguard", "peers", []).map(peer => ({
622 1 : publicKey: peer['public-key'].v,
623 1 : endpoint: peer.endpoint?.v, // endpoint of a peer is optional
624 1 : allowedIps: peer['allowed-ips']?.v
625 1 : })),
626 8 : };
627 8 : }
628 :
629 8 : if (settings["802-11-wireless"]) {
630 8 : result["802-11-wireless"] = {
631 8 : ssid: get("802-11-wireless", "ssid"),
632 8 : mode: get("802-11-wireless", "mode"),
633 8 : };
634 8 : }
635 :
636 37 : return result;
637 37 : }
638 :
639 25 : function settings_to_nm(settings, orig) {
640 7 : const result = JSON.parse(JSON.stringify(orig || { }));
641 :
642 25 : function set(first, second, sig, val, def) {
643 25 : if (val === undefined)
644 25 : val = def;
645 25 : if (!result[first])
646 19 : result[first] = { };
647 25 : if (val !== undefined)
648 25 : result[first][second] = cockpit.variant(sig, val);
649 : else
650 25 : delete result[first][second];
651 25 : }
652 :
653 20 : function set_ip(first, dns_ip_sig, ip_from_text) {
654 20 : set(first, "method", 's', settings[first].method);
655 20 : set(first, "ignore-auto-dns", 'b', settings[first].ignore_auto_dns);
656 20 : set(first, "ignore-auto-routes", 'b', settings[first].ignore_auto_routes);
657 20 : set(first, "addr-gen-mode", 'i', settings[first].addr_gen_mode);
658 :
659 20 : const addresses = settings[first].address_data;
660 20 : if (addresses)
661 10 : set(first, "address-data", "aa{sv}", addresses.map(addr => ip_address_to_nm(addr, first)));
662 :
663 20 : const gateway = settings[first].gateway;
664 8 : if (gateway && addresses.length > 0) {
665 2 : if (!utils.validate_ip(gateway)) {
666 2 : throw cockpit.format(_("Invalid gateway address: $0"), gateway);
667 2 : }
668 8 : set(first, "gateway", "s", gateway);
669 8 : } else {
670 : // gateway cannot be set if there are no addresses
671 20 : delete result[first].gateway;
672 20 : }
673 :
674 20 : const dns = settings[first].dns_data;
675 20 : if (dns) {
676 3 : const invalid = dns.find(addr => !utils.validate_ip(addr));
677 2 : if (invalid) {
678 2 : throw cockpit.format(_("Invalid DNS address: $0"), invalid);
679 2 : }
680 :
681 20 : if (self.supports_dns_data) {
682 20 : set(first, "dns-data", "as", dns);
683 1 : } else {
684 0 : set(first, "dns", dns_ip_sig, dns.map(addr => ip_from_text(addr)));
685 1 : }
686 20 : }
687 :
688 20 : set(first, "dns-search", 'as', settings[first].dns_search);
689 :
690 20 : const routes = settings[first].route_data;
691 20 : if (routes)
692 2 : set(first, "route-data", "aa{sv}", routes.map(route => route_to_nm(route, first)));
693 :
694 : // Never pass "address-labels" back to NetworkManager. It
695 : // is documented as "internal only", but needs to somehow
696 : // stay in sync with "addresses". By not passing it back
697 : // we don't have to worry about that.
698 : //
699 20 : delete result[first]["address-labels"];
700 :
701 : // Never pass "addresses", instead use "address-data" + "gateway"
702 20 : delete result[first].addresses;
703 : // Never pass "routes", instead use "route-data"
704 20 : delete result[first].routes;
705 : // Never pass "dns" if "dns-data" is supported
706 20 : if (self.supports_dns_data)
707 20 : delete result[first].dns;
708 20 : }
709 :
710 25 : set("connection", "id", 's', settings.connection.id);
711 25 : set("connection", "autoconnect", 'b', settings.connection.autoconnect);
712 25 : set("connection", "autoconnect-priority", 'i', settings.connection.autoconnect_priority);
713 25 : set("connection", "autoconnect-slaves", 'i', settings.connection.autoconnect_members);
714 25 : set("connection", "uuid", 's', settings.connection.uuid);
715 25 : set("connection", "interface-name", 's', settings.connection.interface_name);
716 25 : set("connection", "type", 's', settings.connection.type);
717 25 : set("connection", "slave-type", 's', settings.connection.member_type);
718 25 : set("connection", "master", 's', settings.connection.group);
719 25 : set("connection", "multi-connect", 'i', settings.connection.multi_connect);
720 :
721 25 : if (settings.ipv4)
722 14 : set_ip("ipv4", 'au', utils.ip4_from_text);
723 : else
724 16 : delete result.ipv4;
725 :
726 25 : if (settings.ipv6)
727 14 : set_ip("ipv6", 'aay', utils.ip6_from_text);
728 : else
729 16 : delete result.ipv6;
730 :
731 11 : if (settings.bond) {
732 11 : set("bond", "options", 'a{ss}', settings.bond.options);
733 11 : set("bond", "interface-name", 's', settings.bond.interface_name);
734 11 : } else
735 25 : delete result.bond;
736 :
737 5 : if (settings.team) {
738 5 : set("team", "config", 's', JSON.stringify(settings.team.config));
739 5 : set("team", "interface-name", 's', settings.team.interface_name);
740 5 : } else
741 25 : delete result.team;
742 :
743 25 : if (settings.team_port)
744 5 : set("team-port", "config", 's', JSON.stringify(settings.team_port.config));
745 : else
746 25 : delete result["team-port"];
747 :
748 7 : if (settings.bridge) {
749 7 : set("bridge", "interface-name", 's', settings.bridge.interface_name);
750 7 : set("bridge", "stp", 'b', settings.bridge.stp);
751 7 : set("bridge", "priority", 'u', settings.bridge.priority);
752 7 : set("bridge", "forward-delay", 'u', settings.bridge.forward_delay);
753 7 : set("bridge", "hello-time", 'u', settings.bridge.hello_time);
754 7 : set("bridge", "max-age", 'u', settings.bridge.max_age);
755 7 : set("bridge", "ageing-time", 'u', settings.bridge.ageing_time);
756 7 : } else
757 25 : delete result.bridge;
758 :
759 5 : if (settings.bridge_port) {
760 5 : set("bridge-port", "priority", 'u', settings.bridge_port.priority);
761 5 : set("bridge-port", "path-cost", 'u', settings.bridge_port.path_cost);
762 5 : set("bridge-port", "hairpin-mode", 'b', settings.bridge_port.hairpin_mode);
763 5 : } else
764 25 : delete result["bridge-port"];
765 :
766 5 : if (settings.vlan) {
767 5 : set("vlan", "parent", 's', settings.vlan.parent);
768 5 : set("vlan", "id", 'u', settings.vlan.id);
769 5 : set("vlan", "interface-name", 's', settings.vlan.interface_name);
770 : // '1' is the default, but we need to set it explicitly anyway.
771 5 : set("vlan", "flags", 'u', 1);
772 5 : } else
773 24 : delete result.vlan;
774 :
775 22 : if (settings.ethernet) {
776 22 : set("802-3-ethernet", "mtu", 'u', settings.ethernet.mtu);
777 22 : set("802-3-ethernet", "assigned-mac-address", 's', settings.ethernet.assigned_mac_address);
778 : // Delete cloned-mac-address so that assigned-mac-address gets used.
779 22 : delete result["802-3-ethernet"]["cloned-mac-address"];
780 22 : } else
781 11 : delete result["802-3-ethernet"];
782 :
783 5 : if (settings.wireguard) {
784 5 : set("wireguard", "private-key", "s", settings.wireguard.private_key);
785 5 : set("wireguard", "listen-port", "u", settings.wireguard.listen_port);
786 1 : set("wireguard", "peers", "aa{sv}", settings.wireguard.peers.map(peer => {
787 1 : return {
788 1 : "public-key": {
789 1 : t: "s",
790 1 : v: peer.publicKey
791 1 : },
792 1 : ...peer.endpoint
793 1 : ? {
794 1 : endpoint: {
795 1 : t: "s",
796 1 : v: peer.endpoint
797 1 : }
798 1 : }
799 1 : : {},
800 1 : "allowed-ips": {
801 1 : t: "as",
802 1 : v: peer.allowedIps
803 1 : }
804 1 : };
805 1 : }));
806 5 : } else {
807 25 : delete result.wireguard;
808 25 : }
809 :
810 5 : if (settings["802-11-wireless"]) {
811 5 : set("802-11-wireless", "ssid", 'ay', settings["802-11-wireless"].ssid);
812 5 : set("802-11-wireless", "mode", 's', settings["802-11-wireless"].mode);
813 4 : } else {
814 24 : delete result["802-11-wireless"];
815 24 : }
816 :
817 5 : if (settings["802-11-wireless-security"]) {
818 5 : set("802-11-wireless-security", "key-mgmt", 's', settings["802-11-wireless-security"]["key-mgmt"]);
819 5 : set("802-11-wireless-security", "psk", 's', settings["802-11-wireless-security"].psk);
820 5 : } else {
821 25 : delete result["802-11-wireless-security"];
822 25 : }
823 :
824 25 : return result;
825 25 : }
826 :
827 37 : function device_type_to_symbol(type) {
828 : // This returns a string that is suitable for the connection.type field of
829 : // Connection.Settings, except for "ethernet".
830 37 : switch (type) {
831 7 : case 0: return 'unknown';
832 37 : case 1: return 'ethernet'; // 802-3-ethernet
833 8 : case 2: return '802-11-wireless';
834 7 : case 3: return 'unused1';
835 7 : case 4: return 'unused2';
836 7 : case 5: return 'bluetooth';
837 7 : case 6: return '802-11-olpc-mesh';
838 7 : case 7: return 'wimax';
839 7 : case 8: return 'modem';
840 7 : case 9: return 'infiniband';
841 15 : case 10: return 'bond';
842 8 : case 11: return 'vlan';
843 7 : case 12: return 'adsl';
844 10 : case 13: return 'bridge';
845 8 : case 14: return 'generic';
846 8 : case 15: return 'team';
847 8 : case 16: return 'tun';
848 7 : case 17: return 'ip_tunnel';
849 7 : case 18: return 'macvlan';
850 7 : case 19: return 'vxlan';
851 25 : case 20: return 'veth';
852 7 : case 21: return 'macsec';
853 7 : case 22: return 'dummy';
854 7 : case 23: return 'ppp';
855 7 : case 24: return 'ovs_interface';
856 7 : case 25: return 'ovs_port';
857 7 : case 26: return 'ovs_bridge';
858 7 : case 27: return 'wpan';
859 7 : case 28: return '6lowpan';
860 8 : case 29: return 'wireguard';
861 8 : case 30: return 'wifi_p2p';
862 7 : case 31: return 'vrf';
863 37 : case 32: return 'loopback';
864 7 : default: return '';
865 37 : }
866 37 : }
867 :
868 37 : function device_state_to_text(state) {
869 37 : switch (state) {
870 : // NM_DEVICE_STATE_UNKNOWN
871 7 : case 0: return "?";
872 : // NM_DEVICE_STATE_UNMANAGED
873 32 : case 10: return "";
874 : // NM_DEVICE_STATE_UNAVAILABLE
875 29 : case 20: return _("Not available");
876 : // NM_DEVICE_STATE_DISCONNECTED
877 37 : case 30: return _("Inactive");
878 : // NM_DEVICE_STATE_PREPARE
879 34 : case 40: return _("Preparing");
880 : // NM_DEVICE_STATE_CONFIG
881 34 : case 50: return _("Configuring");
882 : // NM_DEVICE_STATE_NEED_AUTH
883 9 : case 60: return _("Authenticating");
884 : // NM_DEVICE_STATE_IP_CONFIG
885 34 : case 70: return _("Configuring IP");
886 : // NM_DEVICE_STATE_IP_CHECK
887 33 : case 80: return _("Checking IP");
888 : // NM_DEVICE_STATE_SECONDARIES
889 33 : case 90: return _("Waiting");
890 : // NM_DEVICE_STATE_ACTIVATED
891 37 : case 100: return _("Active");
892 : // NM_DEVICE_STATE_DEACTIVATING
893 32 : case 110: return _("Deactivating");
894 : // NM_DEVICE_STATE_FAILED
895 8 : case 120: return _("Failed");
896 7 : default: return "";
897 37 : }
898 37 : }
899 :
900 2 : function access_point_mode_to_text(mode) {
901 2 : switch (mode) {
902 : // NM_802_11_MODE_ADHOC
903 1 : case 1: return _("Adhoc");
904 : // NM_802_11_MODE_INFRA
905 2 : case 2: return _("Infra");
906 : // NM_802_11_MODE_AP
907 1 : case 3: return _("AP");
908 : // NM_802_11_MODE_MESH
909 1 : case 4: return _("Mesh");
910 : // subsumes NM_802_11_MODE_UNKNOWN
911 1 : default: return _("Unknown");
912 2 : }
913 2 : }
914 :
915 37 : const connections_by_uuid = { };
916 :
917 37 : function set_settings(obj, settings) {
918 27 : if (obj.Settings && obj.Settings.connection && obj.Settings.connection.uuid)
919 27 : delete connections_by_uuid[obj.Settings.connection.uuid];
920 37 : obj.Settings = settings;
921 37 : if (settings && settings.connection && settings.connection.uuid)
922 37 : connections_by_uuid[settings.connection.uuid] = obj;
923 37 : }
924 :
925 37 : function refresh_settings(obj) {
926 37 : push_refresh();
927 37 : client.call(objpath(obj), "org.freedesktop.NetworkManager.Settings.Connection", "GetSettings")
928 37 : .then(function(reply) {
929 37 : const result = reply[0];
930 37 : if (result) {
931 37 : priv(obj).orig = result;
932 37 : set_settings(obj, settings_from_nm(result));
933 37 : }
934 37 : })
935 37 : .catch(complain)
936 37 : .finally(pop_refresh);
937 37 : }
938 :
939 37 : function refresh_udev(obj) {
940 37 : if (obj.Udi.indexOf("/sys/") !== 0)
941 37 : return;
942 :
943 37 : push_refresh();
944 37 : cockpit.spawn(["udevadm", "info", obj.Udi], { err: 'message' })
945 37 : .then(function(res) {
946 37 : const props = { };
947 37 : function snarf_prop(line, env, prop) {
948 37 : const prefix = "E: " + env + "=";
949 37 : if (line.indexOf(prefix) === 0) {
950 37 : props[prop] = line.substring(prefix.length);
951 37 : }
952 37 : }
953 37 : res.split('\n').forEach(function(line) {
954 37 : snarf_prop(line, "ID_MODEL_FROM_DATABASE", "IdModel");
955 37 : snarf_prop(line, "ID_VENDOR_FROM_DATABASE", "IdVendor");
956 37 : });
957 37 : set_object_properties(obj, props);
958 37 : })
959 0 : .catch(function(ex) {
960 : /* udevadm info exits with 4 when device doesn't exist */
961 0 : if (ex.exit_status !== 4) {
962 0 : console.warn(ex.message);
963 0 : console.warn(ex);
964 0 : }
965 0 : })
966 37 : .finally(pop_refresh);
967 37 : }
968 :
969 23 : function handle_updated(obj) {
970 23 : refresh_settings(obj);
971 23 : }
972 :
973 : /* NetworkManager specific object types, used by the generic D-Bus
974 : * code and using the data conversion functions.
975 : */
976 :
977 37 : const type_Ipv4Config = {
978 37 : interfaces: [
979 37 : "org.freedesktop.NetworkManager.IP4Config"
980 37 : ],
981 :
982 37 : props: {
983 37 : AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
984 37 : }
985 37 : };
986 :
987 37 : const type_Ipv6Config = {
988 37 : interfaces: [
989 37 : "org.freedesktop.NetworkManager.IP6Config"
990 37 : ],
991 :
992 37 : props: {
993 37 : AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
994 37 : }
995 37 : };
996 :
997 37 : const type_AccessPoint = {
998 37 : interfaces: [
999 37 : "org.freedesktop.NetworkManager.AccessPoint"
1000 37 : ],
1001 :
1002 37 : props: {
1003 37 : Flags: { def: 0 },
1004 37 : WpaFlags: { def: 0 },
1005 37 : RsnFlags: { def: 0 },
1006 37 : Ssid: { conv: utils.ssid_from_nm, def: "" },
1007 37 : Frequency: { def: 0 }, // MHz
1008 37 : HwAddress: { def: "" },
1009 37 : Mode: { conv: access_point_mode_to_text, def: "" },
1010 37 : MaxBitrate: { def: 0 }, // Kbit/s
1011 37 : Bandwidth: { def: 0 }, // MHz
1012 37 : Strength: { def: 0 },
1013 37 : LastSeen: { def: -1 }, // CLOCK_BOOTTIME seconds, -1 if never seen
1014 37 : },
1015 :
1016 37 : exporters: [
1017 2 : function (obj) {
1018 : // Find connection for this SSID (undefined if none exists)
1019 1 : obj.Connection = (self.get_settings()?.Connections || []).find(con => {
1020 2 : if (con.Settings?.["802-11-wireless"]?.ssid)
1021 2 : return utils.ssid_from_nm(con.Settings["802-11-wireless"].ssid) == obj.Ssid;
1022 2 : return false;
1023 2 : });
1024 2 : }
1025 37 : ]
1026 37 : };
1027 :
1028 37 : const type_Connection = {
1029 37 : interfaces: [
1030 37 : "org.freedesktop.NetworkManager.Settings.Connection"
1031 37 : ],
1032 :
1033 37 : props: {
1034 37 : Unsaved: { }
1035 37 : },
1036 :
1037 37 : signals: {
1038 37 : Updated: handle_updated
1039 37 : },
1040 :
1041 37 : refresh: refresh_settings,
1042 :
1043 8 : drop: function (obj) {
1044 8 : set_settings(obj, null);
1045 8 : },
1046 :
1047 37 : prototype: {
1048 21 : apply_settings: function (settings) {
1049 21 : const self = this;
1050 21 : try {
1051 21 : return call_object_method(self,
1052 21 : "org.freedesktop.NetworkManager.Settings.Connection", "Update",
1053 21 : settings_to_nm(settings, priv(self).orig))
1054 21 : .then(() => {
1055 21 : set_settings(self, settings);
1056 21 : });
1057 3 : } catch (e) {
1058 3 : return Promise.reject(e);
1059 3 : }
1060 21 : },
1061 :
1062 18 : activate: function (dev, specific_object) {
1063 18 : return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
1064 18 : "org.freedesktop.NetworkManager", "ActivateConnection",
1065 18 : objpath(this), objpath(dev), objpath(specific_object))
1066 17 : .then(([active_connection]) => active_connection);
1067 18 : },
1068 :
1069 8 : delete_: function () {
1070 8 : return call_object_method(this, "org.freedesktop.NetworkManager.Settings.Connection", "Delete")
1071 7 : .then(() => undefined);
1072 8 : }
1073 37 : },
1074 :
1075 37 : exporters: [
1076 37 : function (obj) {
1077 37 : obj.Groups = [];
1078 37 : obj.Members = [];
1079 37 : obj.Interfaces = [];
1080 37 : },
1081 :
1082 37 : null,
1083 :
1084 37 : null,
1085 :
1086 : // Needs: type_Interface.Connections
1087 : //
1088 : // Sets: type_Connection.Members
1089 : // type_Connection.Groups
1090 : //
1091 37 : function (obj) {
1092 : // Most of the time, a connection has zero or one groups,
1093 : // but when a connection refers to its group by interface
1094 : // name, we might end up with more than one group
1095 : // connection so we just collect them all.
1096 : //
1097 : // TODO - Nail down how NM really handles this.
1098 :
1099 13 : function check_con(con) {
1100 13 : const group_settings = connection_settings(con);
1101 13 : const my_settings = connection_settings(obj);
1102 13 : if (group_settings.type == my_settings.member_type) {
1103 13 : obj.Groups.push(con);
1104 13 : con.Members.push(obj);
1105 13 : }
1106 13 : }
1107 :
1108 37 : const cs = connection_settings(obj);
1109 18 : if (cs.member_type) {
1110 18 : const group = connections_by_uuid[cs.group];
1111 7 : if (group) {
1112 7 : obj.Groups.push(group);
1113 7 : group.Members.push(obj);
1114 7 : } else {
1115 18 : const iface = peek_interface(cs.group);
1116 18 : if (iface) {
1117 18 : iface.Connections.forEach(check_con);
1118 18 : }
1119 18 : }
1120 18 : }
1121 37 : }
1122 37 : ]
1123 :
1124 37 : };
1125 :
1126 37 : const type_ActiveConnection = {
1127 37 : interfaces: [
1128 37 : "org.freedesktop.NetworkManager.Connection.Active"
1129 37 : ],
1130 :
1131 37 : props: {
1132 37 : Connection: { conv: conv_Object(type_Connection) },
1133 37 : Ip4Config: { conv: conv_Object(type_Ipv4Config) },
1134 37 : Ip6Config: { conv: conv_Object(type_Ipv6Config) },
1135 37 : State: { def: 0 }
1136 : // See below for "Group"
1137 37 : },
1138 :
1139 37 : prototype: {
1140 0 : deactivate: function() {
1141 0 : return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
1142 0 : "org.freedesktop.NetworkManager", "DeactivateConnection",
1143 0 : objpath(this))
1144 0 : .then(() => undefined);
1145 0 : }
1146 37 : }
1147 37 : };
1148 :
1149 37 : const type_Device = {
1150 37 : interfaces: [
1151 37 : "org.freedesktop.NetworkManager.Device",
1152 37 : "org.freedesktop.NetworkManager.Device.Wired",
1153 37 : "org.freedesktop.NetworkManager.Device.Bond",
1154 37 : "org.freedesktop.NetworkManager.Device.Team",
1155 37 : "org.freedesktop.NetworkManager.Device.Bridge",
1156 37 : "org.freedesktop.NetworkManager.Device.Vlan",
1157 37 : "org.freedesktop.NetworkManager.Device.Wireless"
1158 37 : ],
1159 :
1160 37 : props: {
1161 37 : DeviceType: { conv: device_type_to_symbol },
1162 37 : Interface: { },
1163 37 : StateText: { prop: "State", conv: device_state_to_text, def: _("Unknown") },
1164 37 : State: { },
1165 37 : StateReason: { def: [0, 0] }, // [state, reason] tuple
1166 37 : HwAddress: { },
1167 37 : AvailableConnections: { conv: conv_Array(conv_Object(type_Connection)), def: [] },
1168 37 : ActiveConnection: { conv: conv_Object(type_ActiveConnection) },
1169 37 : Ip4Config: { conv: conv_Object(type_Ipv4Config) },
1170 37 : Ip6Config: { conv: conv_Object(type_Ipv6Config) },
1171 37 : Udi: { trigger: refresh_udev },
1172 37 : IdVendor: { def: "" },
1173 37 : IdModel: { def: "" },
1174 37 : Driver: { def: "" },
1175 37 : Carrier: { def: true },
1176 37 : Speed: { },
1177 37 : Managed: { def: false },
1178 : // WiFi-specific properties
1179 37 : AccessPoints: { conv: conv_Array(conv_Object(type_AccessPoint)), def: [] },
1180 37 : ActiveAccessPoint: { conv: conv_Object(type_AccessPoint) },
1181 : // See below for "Members"
1182 37 : },
1183 :
1184 37 : prototype: {
1185 0 : activate: function(connection, specific_object) {
1186 0 : priv(this).lastFailureReason = undefined; // Clear stale failure reason from previous attempts
1187 0 : return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
1188 0 : "org.freedesktop.NetworkManager", "ActivateConnection",
1189 0 : objpath(connection), objpath(this), objpath(specific_object))
1190 0 : .then(([active_connection]) => active_connection);
1191 0 : },
1192 :
1193 4 : activate_with_settings: function(settings, specific_object) {
1194 4 : priv(this).lastFailureReason = undefined; // Clear stale failure reason from previous attempts
1195 4 : try {
1196 4 : return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
1197 4 : "org.freedesktop.NetworkManager", "AddAndActivateConnection",
1198 4 : settings_to_nm(settings), objpath(this), objpath(specific_object))
1199 4 : .then(([path, active_connection_path]) => ({
1200 4 : connection: get_object(path, type_Connection),
1201 4 : active_connection: get_object(active_connection_path, type_ActiveConnection)
1202 4 : }));
1203 1 : } catch (e) {
1204 1 : return Promise.reject(e);
1205 1 : }
1206 4 : },
1207 :
1208 9 : disconnect: function () {
1209 9 : return call_object_method(this, 'org.freedesktop.NetworkManager.Device', 'Disconnect')
1210 9 : .then(() => undefined);
1211 9 : },
1212 :
1213 : // Request a WiFi scan to populate this.AccessPoints
1214 2 : request_scan: function() {
1215 2 : utils.debug("request_scan: requesting scan for", this.Interface);
1216 2 : call_object_method(this, 'org.freedesktop.NetworkManager.Device.Wireless', 'RequestScan', {})
1217 0 : .catch(error => {
1218 : // RequestScan can fail if a scan was recently done, that's OK
1219 0 : console.warn("request_scan: scan failed for", this.Interface + ":", error.toString());
1220 0 : });
1221 2 : },
1222 :
1223 : // Get and clear the last connection failure reason
1224 2 : consume_failure_reason: function() {
1225 2 : const reason = priv(this).lastFailureReason;
1226 2 : priv(this).lastFailureReason = undefined;
1227 2 : return reason;
1228 2 : },
1229 :
1230 : // Mark that a pending connection is being cancelled by the user
1231 1 : cancel_pending_connection: function() {
1232 1 : priv(this).connectionCancelled = true;
1233 1 : },
1234 :
1235 : // Wait for a connection to complete
1236 : // For WiFi, pass expected_ssid to verify we connected to the right network
1237 : // Returns a Promise that resolves on success or cancel, rejects with {reason} on failure
1238 2 : wait_connection: function(expected_ssid) {
1239 2 : priv(this).connectionCancelled = false;
1240 2 : utils.debug("wait_connection: starting, iface:", this.Interface, "expected:", expected_ssid, "initial state:", this.State);
1241 2 : return new Promise((resolve, reject) => {
1242 2 : let activationStarted = false;
1243 :
1244 2 : const cleanup = () => self.removeEventListener("changed", check);
1245 :
1246 2 : const check = () => {
1247 2 : utils.debug("wait_connection check: state:", this.State, "ssid:", this.ActiveAccessPoint?.Ssid,
1248 2 : "activeConn:", !!this.ActiveConnection, "activationStarted:", activationStarted,
1249 2 : "lastFailureReason:", priv(this).lastFailureReason,
1250 2 : "connectionCancelled:", priv(this).connectionCancelled);
1251 :
1252 : // captured a failure?
1253 2 : const reason = this.consume_failure_reason();
1254 2 : if (reason) {
1255 2 : cleanup();
1256 2 : console.warn("wait_connection: connection failed for", this.Interface, "reason:", reason);
1257 2 : const error = new Error("Connection failed");
1258 2 : error.reason = reason;
1259 2 : reject(error);
1260 2 : return;
1261 2 : }
1262 :
1263 : // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState
1264 2 : switch (this.State) {
1265 2 : case 100: // NM_DEVICE_STATE_ACTIVATED
1266 2 : if (!expected_ssid || this.ActiveAccessPoint?.Ssid === expected_ssid) {
1267 2 : utils.debug("wait_connection: success");
1268 2 : cleanup();
1269 2 : resolve();
1270 2 : }
1271 2 : break;
1272 :
1273 2 : case 30: // NM_DEVICE_STATE_DISCONNECTED; initial state, so wait for activation to start
1274 2 : case 120: // NM_DEVICE_STATE_FAILED
1275 1 : if (priv(this).connectionCancelled) {
1276 1 : cleanup();
1277 1 : utils.debug("wait_connection: cancelled by user");
1278 1 : resolve();
1279 1 : } else {
1280 : // Disconnected/failed after
1281 : // activation started means user
1282 : // cancelled or failure without reason
1283 1 : if (activationStarted && !this.ActiveConnection) {
1284 1 : cleanup();
1285 1 : console.warn("wait_connection: connection failed for", this.Interface, "without captured reason");
1286 1 : reject(new Error("Connection failed"));
1287 1 : }
1288 2 : }
1289 2 : break;
1290 :
1291 1 : case 20: // NM_DEVICE_STATE_UNAVAILABLE
1292 1 : case 110: // NM_DEVICE_STATE_DEACTIVATING
1293 1 : break;
1294 :
1295 : // any other state means we're activating
1296 2 : default:
1297 2 : if (!activationStarted) {
1298 2 : utils.debug("wait_connection: activation started");
1299 2 : activationStarted = true;
1300 2 : }
1301 2 : }
1302 2 : };
1303 :
1304 2 : self.addEventListener("changed", check);
1305 2 : check(); // Check current state immediately in case already connected
1306 2 : });
1307 2 : }
1308 37 : },
1309 :
1310 37 : exporters: [
1311 37 : function (obj) {
1312 8 : if (obj.DeviceType === '802-11-wireless') {
1313 : // When a hidden network (no SSID broadcast) has a saved connection, NetworkManager
1314 : // duplicates it in AccessPoints: once without SSID (from the beacon), and once with
1315 : // SSID (synthesized from the saved connection). Both have the same HwAddress (MAC).
1316 : // We deduplicate by MAC first, preferring the one with a known connection
1317 8 : const apByMac = new Map();
1318 1 : (obj.AccessPoints || []).forEach(ap => {
1319 2 : utils.debug(`AP: Ssid '${ap.Ssid}' HwAddress: ${ap.HwAddress} Strength: ${ap.Strength} hasConnection: ${!!ap.Connection}`);
1320 2 : if (!apByMac.get(ap.HwAddress) || ap.Connection)
1321 2 : apByMac.set(ap.HwAddress, ap);
1322 2 : });
1323 :
1324 : // Deduplicate visible APs by SSID, keeping the strongest signal for each network.
1325 : // Count remaining hidden APs (those without SSID after MAC deduplication).
1326 8 : const apBySsid = new Map();
1327 8 : let hiddenCount = 0;
1328 2 : Array.from(apByMac.values()).forEach(ap => {
1329 2 : if (ap.Ssid) {
1330 2 : const existing = apBySsid.get(ap.Ssid);
1331 2 : if (!existing || ap.Strength > existing.Strength) {
1332 2 : apBySsid.set(ap.Ssid, ap);
1333 2 : }
1334 2 : } else {
1335 2 : hiddenCount++;
1336 2 : }
1337 2 : });
1338 8 : obj.visibleSsids = Array.from(apBySsid.values());
1339 8 : obj.hiddenAPCount = hiddenCount;
1340 8 : utils.debug("Device exporter:", obj.Interface, "has", obj.visibleSsids.length, "visible SSIDs and", obj.hiddenAPCount, "hidden APs");
1341 8 : }
1342 37 : }
1343 37 : ]
1344 37 : };
1345 :
1346 : // The 'Interface' type does not correspond to any NetworkManager
1347 : // object or interface. We use it to represent a network device
1348 : // that might or might not actually be known to the kernel, such
1349 : // as the interface of a bond that is currently down.
1350 : //
1351 : // This is a HACK: NetworkManager should export Device nodes for
1352 : // these.
1353 :
1354 37 : const type_Interface = {
1355 37 : interfaces: [],
1356 :
1357 37 : exporters: [
1358 37 : function (obj) {
1359 37 : obj.Device = null;
1360 37 : obj._NonDeviceConnections = [];
1361 37 : obj.Connections = [];
1362 37 : obj.MainConnection = null;
1363 37 : },
1364 :
1365 37 : null,
1366 :
1367 : // Needs: type_Interface.Device
1368 : // type_Interface._NonDeviceConnections
1369 : //
1370 : // Sets: type_Connection.Interfaces
1371 : // type_Interface.Connections
1372 : // type_Interface.MainConnection
1373 :
1374 37 : function (obj) {
1375 11 : if (!obj.Device && obj._NonDeviceConnections.length === 0) {
1376 11 : drop_object(priv(obj).path);
1377 11 : return;
1378 11 : }
1379 :
1380 37 : function consider_for_main(con) {
1381 37 : if (!obj.MainConnection ||
1382 10 : connection_settings(obj.MainConnection).timestamp < connection_settings(con).timestamp) {
1383 37 : obj.MainConnection = con;
1384 37 : }
1385 37 : }
1386 :
1387 37 : obj.Connections = obj._NonDeviceConnections;
1388 :
1389 37 : if (obj.Device) {
1390 37 : obj.Device.AvailableConnections.forEach(function (con) {
1391 37 : if (obj.Connections.indexOf(con) == -1)
1392 37 : obj.Connections.push(con);
1393 37 : });
1394 37 : }
1395 :
1396 37 : obj.Connections.forEach(function (con) {
1397 37 : consider_for_main(con);
1398 37 : con.Interfaces.push(obj);
1399 37 : });
1400 :
1401 : // Explicitly prefer the active connection. The
1402 : // active connection should have the most recent
1403 : // timestamp, but only when the activation was
1404 : // successful. Also, there don't seem to be change
1405 : // notifications when the timestamp changes.
1406 :
1407 37 : if (obj.Device && obj.Device.ActiveConnection && obj.Device.ActiveConnection.Connection) {
1408 37 : obj.MainConnection = obj.Device.ActiveConnection.Connection;
1409 37 : }
1410 37 : }
1411 37 : ]
1412 :
1413 37 : };
1414 :
1415 37 : function get_interface(iface) {
1416 37 : const obj = get_object(":interface:" + iface, type_Interface);
1417 37 : obj.Name = iface;
1418 37 : return obj;
1419 37 : }
1420 :
1421 35 : function peek_interface(iface) {
1422 35 : return peek_object(":interface:" + iface);
1423 35 : }
1424 :
1425 37 : const type_Settings = {
1426 37 : interfaces: [
1427 37 : "org.freedesktop.NetworkManager.Settings"
1428 37 : ],
1429 :
1430 37 : props: {
1431 37 : Connections: { conv: conv_Array(conv_Object(type_Connection)), def: [] }
1432 37 : },
1433 :
1434 37 : prototype: {
1435 14 : add_connection: function (conf) {
1436 : // https://www.networkmanager.dev/docs/api/latest/nm-dbus-types.html
1437 14 : const NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK = 0x1;
1438 14 : return call_object_method(this,
1439 14 : 'org.freedesktop.NetworkManager.Settings',
1440 14 : 'AddConnection2',
1441 14 : settings_to_nm(conf, { }), NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK, { })
1442 14 : .then(([path]) => get_object(path, type_Connection));
1443 14 : }
1444 37 : },
1445 :
1446 37 : exporters: [
1447 37 : null,
1448 :
1449 : // Sets: type_Interface._NonDeviceConnections
1450 : //
1451 37 : function (obj) {
1452 37 : if (obj.Connections) {
1453 37 : obj.Connections.forEach(function (con) {
1454 37 : function add_to_interface(name) {
1455 37 : if (name) {
1456 37 : const cons = get_interface(name)._NonDeviceConnections;
1457 37 : if (cons.indexOf(con) == -1)
1458 37 : cons.push(con);
1459 37 : }
1460 37 : }
1461 :
1462 37 : if (con.Settings) {
1463 37 : if (con.Settings.connection)
1464 37 : add_to_interface(con.Settings.connection.interface_name);
1465 37 : if (con.Settings.bond)
1466 15 : add_to_interface(con.Settings.bond.interface_name);
1467 37 : if (con.Settings.team)
1468 8 : add_to_interface(con.Settings.team.interface_name);
1469 37 : if (con.Settings.bridge)
1470 10 : add_to_interface(con.Settings.bridge.interface_name);
1471 37 : if (con.Settings.vlan)
1472 8 : add_to_interface(con.Settings.vlan.interface_name);
1473 37 : }
1474 37 : });
1475 37 : }
1476 37 : }
1477 37 : ]
1478 37 : };
1479 :
1480 37 : const type_Manager = {
1481 37 : interfaces: [
1482 37 : "org.freedesktop.NetworkManager"
1483 37 : ],
1484 :
1485 37 : props: {
1486 37 : Capabilities: { def: [] },
1487 37 : Version: { },
1488 37 : Devices: {
1489 37 : conv: conv_Array(conv_Object(type_Device)),
1490 37 : def: []
1491 37 : },
1492 37 : ActiveConnections: { conv: conv_Array(conv_Object(type_ActiveConnection)), def: [] }
1493 37 : },
1494 :
1495 37 : prototype: {
1496 6 : checkpoint_create: function (devices, timeout) {
1497 6 : return call_object_method(this,
1498 6 : 'org.freedesktop.NetworkManager',
1499 6 : 'CheckpointCreate',
1500 6 : devices.map(objpath),
1501 6 : timeout,
1502 6 : 0)
1503 6 : .then(([checkpoint]) => checkpoint)
1504 1 : .catch(function (error) {
1505 1 : if (error.name != "org.freedesktop.DBus.Error.UnknownMethod")
1506 0 : console.warn(error.message || error);
1507 1 : });
1508 6 : },
1509 :
1510 5 : checkpoint_destroy: function (checkpoint) {
1511 5 : if (checkpoint) {
1512 5 : return call_object_method(this,
1513 5 : 'org.freedesktop.NetworkManager',
1514 5 : 'CheckpointDestroy',
1515 5 : checkpoint)
1516 2 : .then(() => undefined);
1517 5 : } else
1518 0 : return Promise.resolve();
1519 5 : },
1520 :
1521 0 : checkpoint_rollback: function (checkpoint) {
1522 0 : if (checkpoint) {
1523 0 : return call_object_method(this,
1524 0 : 'org.freedesktop.NetworkManager',
1525 0 : 'CheckpointRollback',
1526 0 : checkpoint)
1527 0 : .then(([result]) => result);
1528 0 : } else
1529 0 : return Promise.resolve();
1530 0 : }
1531 37 : },
1532 :
1533 37 : exporters: [
1534 37 : null,
1535 :
1536 : // Sets: type_Interface.Device
1537 : //
1538 37 : function (obj) {
1539 37 : obj.Devices.forEach(function (dev) {
1540 37 : if (dev.Interface) {
1541 37 : const iface = get_interface(dev.Interface);
1542 37 : iface.Device = dev;
1543 37 : }
1544 37 : });
1545 37 : }
1546 37 : ]
1547 37 : };
1548 :
1549 : /* Now create the cyclic declarations.
1550 : */
1551 37 : type_ActiveConnection.props.Group = { conv: conv_Object(type_Device) };
1552 37 : type_Device.props.Members = { conv: conv_Array(conv_Object(type_Device)), def: [] };
1553 :
1554 : /* Accessing the model.
1555 : */
1556 :
1557 37 : self.list_interfaces = function list_interfaces() {
1558 37 : const result = [];
1559 37 : for (const path in objects) {
1560 37 : const obj = objects[path];
1561 37 : if (priv(obj).type === type_Interface)
1562 37 : result.push(obj);
1563 37 : }
1564 37 : return result.sort(function (a, b) { return a.Name.localeCompare(b.Name) });
1565 37 : };
1566 :
1567 37 : self.find_interface = peek_interface;
1568 :
1569 35 : self.get_manager = function () {
1570 35 : return get_object("/org/freedesktop/NetworkManager",
1571 35 : type_Manager);
1572 35 : };
1573 :
1574 16 : self.get_settings = function () {
1575 16 : return get_object("/org/freedesktop/NetworkManager/Settings",
1576 16 : type_Settings);
1577 16 : };
1578 :
1579 : /* Initialization.
1580 : */
1581 :
1582 37 : set_object_types([type_Manager,
1583 37 : type_Settings,
1584 37 : type_Device,
1585 37 : type_Ipv4Config,
1586 37 : type_Ipv6Config,
1587 37 : type_Connection,
1588 37 : type_ActiveConnection,
1589 37 : type_AccessPoint
1590 37 : ]);
1591 :
1592 37 : get_object("/org/freedesktop/NetworkManager", type_Manager);
1593 37 : get_object("/org/freedesktop/NetworkManager/Settings", type_Settings);
1594 :
1595 37 : self.ready = undefined;
1596 37 : self.operationInProgress = undefined;
1597 37 : self.curtain = undefined;
1598 37 : return self;
1599 37 : }
1600 :
1601 36 : export function syn_click(model, fun) {
1602 23 : return function() {
1603 23 : const self = this;
1604 23 : const self_args = arguments;
1605 23 : return model.synchronize().then(function() {
1606 23 : fun.apply(self, self_args);
1607 23 : });
1608 23 : };
1609 36 : }
1610 :
1611 37 : export function is_managed(dev) {
1612 : // Never let the user manage loopback devices, nothing good can come from that.
1613 36 : return dev.State != 10 && dev.DeviceType != "loopback" && dev.Interface != "lo";
1614 37 : }
1615 :
1616 0 : function render_interface_link(iface) {
1617 0 : return <Button variant='link' tabindex="0"
1618 0 : isInline
1619 0 : onClick={() => cockpit.location.go([iface])}>{iface}
1620 0 : </Button>;
1621 0 : }
1622 :
1623 35 : export function device_state_text(dev) {
1624 35 : if (!dev)
1625 7 : return _("Inactive");
1626 7 : if (dev.State == 100 && dev.Carrier === false)
1627 7 : return _("No carrier");
1628 8 : if (!is_managed(dev)) {
1629 8 : if (!dev.ActiveConnection &&
1630 7 : (!dev.Ip4Config || dev.Ip4Config.AddressData.length === 0) &&
1631 7 : (!dev.Ip6Config || dev.Ip6Config.AddressData.length === 0))
1632 8 : return _("Inactive");
1633 8 : }
1634 35 : return dev.StateText;
1635 35 : }
1636 :
1637 2 : export function array_join(elts, sep) {
1638 2 : const result = [];
1639 2 : for (let i = 0; i < elts.length; i++) {
1640 2 : result.push(elts[i]);
1641 2 : if (i < elts.length - 1)
1642 0 : result.push(sep);
1643 2 : }
1644 2 : return result;
1645 2 : }
1646 :
1647 37 : export function render_active_connection(dev, with_link, hide_link_local) {
1648 37 : const parts = [];
1649 :
1650 37 : if (!dev)
1651 8 : return "";
1652 :
1653 37 : const con = dev.ActiveConnection;
1654 :
1655 7 : if (con && con.Group) {
1656 7 : return fmt_to_fragments(_("Part of $0"), with_link ? render_interface_link(con.Group.Interface) : con.Group.Interface);
1657 7 : }
1658 :
1659 36 : const ip4config = con ? con.Ip4Config : dev.Ip4Config;
1660 37 : if (ip4config) {
1661 37 : ip4config.AddressData.forEach(function (a) {
1662 37 : parts.push(a.address + "/" + a.prefix);
1663 37 : });
1664 37 : }
1665 :
1666 36 : function is_ipv6_link_local(addr) {
1667 36 : return (addr.indexOf("fe8") === 0 ||
1668 36 : addr.indexOf("fe9") === 0 ||
1669 36 : addr.indexOf("fea") === 0 ||
1670 36 : addr.indexOf("feb") === 0);
1671 36 : }
1672 :
1673 36 : const ip6config = con ? con.Ip6Config : dev.Ip6Config;
1674 37 : if (ip6config) {
1675 37 : ip6config.AddressData.forEach(function (a) {
1676 36 : if (!(hide_link_local && is_ipv6_link_local(a.address)))
1677 37 : parts.push(a.address + "/" + a.prefix);
1678 37 : });
1679 37 : }
1680 :
1681 37 : return parts.join(", ");
1682 37 : }
1683 :
1684 : /* Resource usage monitoring
1685 : */
1686 :
1687 4 : export function complete_settings(settings, device) {
1688 0 : if (!device) {
1689 0 : console.warn("No device to complete settings", JSON.stringify(settings));
1690 0 : return;
1691 0 : }
1692 :
1693 4 : settings.connection.id = device.Interface;
1694 4 : settings.connection.uuid = uuidv4();
1695 :
1696 1 : if (device.DeviceType == 'ethernet') {
1697 1 : settings.connection.type = '802-3-ethernet';
1698 1 : settings.ethernet = { };
1699 0 : } else {
1700 : // The remaining types are identical between Device and Settings, see
1701 : // device_type_to_symbol.
1702 3 : settings.connection.type = device.DeviceType;
1703 3 : }
1704 4 : }
1705 :
1706 21 : export function settings_applier(model, device, connection) {
1707 : /* If we have a connection, we can just update it.
1708 : * Otherwise if the settings has TYPE set, we can add
1709 : * them as a stand-alone object. Otherwise, we
1710 : * activate the device with the settings which causes
1711 : * NM to fill in the type and other details.
1712 : * Non-persistent "Wired Connection" is a special case
1713 : * and we make a new connection instead.
1714 : *
1715 : * HACK - The activation is a hack, we would rather
1716 : * just have NM fill in the details and not activate
1717 : * the connection. See complete_settings above that
1718 : * can do some of this completion.
1719 : *
1720 : * https://bugzilla.gnome.org/show_bug.cgi?id=775226
1721 : */
1722 :
1723 21 : const specialCon = utils.isNonPersistentMultiCon(connection);
1724 :
1725 21 : return function (settings) {
1726 19 : if (connection && !specialCon) {
1727 19 : return connection.apply_settings(settings);
1728 12 : } else if (settings.connection.type && !specialCon) {
1729 14 : return model.get_settings().add_connection(settings);
1730 1 : } else if (device && specialCon) {
1731 2 : const newSettings = utils.createNewConnSettings(settings, device.Interface);
1732 2 : return device.activate_with_settings(newSettings);
1733 1 : } else if (device) {
1734 1 : return device.activate_with_settings(settings);
1735 1 : } else {
1736 1 : console.warn("No way to apply settings", connection, settings);
1737 1 : return Promise.resolve();
1738 1 : }
1739 21 : };
1740 21 : }
1741 :
1742 30 : export function choice_title(choices, choice, def) {
1743 30 : for (let i = 0; i < choices.length; i++) {
1744 30 : if (choices[i].choice == choice)
1745 30 : return choices[i].title;
1746 30 : }
1747 4 : return def;
1748 30 : }
1749 :
1750 : /* Support for automatically rolling back changes that break the
1751 : * connection to the server.
1752 : *
1753 : * The basic idea is to perform the following steps:
1754 : *
1755 : * 1) Create a checkpoint with automatic rollback
1756 : * 2) Make the change
1757 : * 3) Destroy the checkpoint
1758 : *
1759 : * If step 2 breaks the connection, step 3 won't happen and the
1760 : * checkpoint will roll back after some time. This is supposed to
1761 : * restore connectivity, so steps 2 and 3 will complete at that time,
1762 : * and step 3 will fail because the checkpoint doesn't exist anymore.
1763 : *
1764 : * The failure of step 3 is our indication that the connection was
1765 : * temporarily broken, and we inform the user about that.
1766 : *
1767 : * Usually, step 2 completes successfully also for a change that
1768 : * breaks the connection, and connectivity is only lost after some
1769 : * delay. Thus, we also delay step 3 by a short amount (settle_time,
1770 : * below).
1771 : *
1772 : * For a change that _doesn't_ break connectivity, this whole process
1773 : * is inherently a race: Steps 2 and 3 need to complete before the
1774 : * checkpoint created in step 1 reaches its timeout.
1775 : *
1776 : * It is better to wait a bit longer for salvation after making a
1777 : * mistake than to have many of your legitimate changes be cancelled
1778 : * by an impatient nanny mechanism. Thus, we use a rather long
1779 : * checkpoint rollback timeout (rollback_time, below).
1780 : *
1781 : * For a good change, all three steps usually happen quickly, and the
1782 : * time we wait between steps 2 and 3 doesn't need to be very long
1783 : * either, apparently. Thus, we delay any indication that something
1784 : * might be wrong by a short delay (curtain_time, below), and most
1785 : * changes can thus be made without the "Testing connection" curtain
1786 : * coming up.
1787 : *
1788 : * Some changes will be rolled back although the user really wants to
1789 : * make them. For example, the user might want to change the IP
1790 : * address of the machine, and although this will disconnect Cockpit,
1791 : * the user can connect again on the new address.
1792 : *
1793 : * In order to give the user the option to avoid this unwanted
1794 : * rollback, we let him/her do the same change without a checkpoint
1795 : * directly from the dialog that explains the problem.
1796 : */
1797 :
1798 : /* To avoid interference, we switch off the global transport health
1799 : * check while a checkpoint exists. For example, if the rollback
1800 : * takes a really long time, Cockpit would otherwise disconnect itself
1801 : * forcefully and the user would not get to see the dialog with the
1802 : * "Do it anyway" button. This dialog is the only way to make certain
1803 : * changes, and it is thus important to show it if at all possible.
1804 : */
1805 :
1806 : /* Considerations for choosing the times below
1807 : *
1808 : * curtain_time too short: Curtain comes up too often for good changes.
1809 : *
1810 : * curtain_time too long: User is left with a broken UI for a
1811 : * significant time in the case of a mistake.
1812 : *
1813 : * settle_time too short: Some bad changes that take time to have any
1814 : * effect will be let through.
1815 : *
1816 : * settle_time too high: All operations take a long time and the race
1817 : * between Cockpit destroying the checkpoint
1818 : * and NetworkManager rolling it back (see
1819 : * above) gets tighter. The curtain
1820 : * needs to come up to prevent the user from
1821 : * interacting with the page. Thus
1822 : * settle_time should be shorter than
1823 : * curtain_time.
1824 : *
1825 : * rollback_time too short: Good changes that take a long time to complete
1826 : * (on a loaded machine, say) are cancelled spuriously.
1827 : *
1828 : * rollback_time too long: The user has to wait a long time before
1829 : * his/her mistake is corrected and might
1830 : * consider Cockpit to be dead already.
1831 : * Also, the network connection machinery in
1832 : * the kernels and browsers must recover
1833 : * after no packages have been flowing for
1834 : * this much time. Windows seems to have
1835 : * less patience than Linux in this regard.
1836 : */
1837 37 : const curtain_time = 1.5;
1838 37 : let settle_time = 1.0;
1839 37 : const rollback_time = 7.0;
1840 :
1841 26 : export function with_checkpoint(model, modify, options) {
1842 26 : const manager = model.get_manager();
1843 :
1844 26 : let curtain_timeout;
1845 26 : let curtain_title_timeout;
1846 :
1847 6 : function show_curtain() {
1848 6 : cockpit.hint("ignore_transport_health_check", { data: true });
1849 3 : curtain_timeout = window.setTimeout(function () {
1850 3 : curtain_timeout = null;
1851 3 : model.set_curtain('testing');
1852 3 : }, curtain_time * 1000);
1853 3 : curtain_title_timeout = window.setTimeout(function () {
1854 3 : curtain_title_timeout = null;
1855 3 : model.set_curtain('restoring');
1856 3 : }, rollback_time * 1000);
1857 6 : }
1858 :
1859 5 : function hide_curtain() {
1860 5 : if (curtain_timeout)
1861 2 : window.clearTimeout(curtain_timeout);
1862 5 : curtain_timeout = null;
1863 5 : if (curtain_title_timeout)
1864 2 : window.clearTimeout(curtain_title_timeout);
1865 5 : cockpit.hint("ignore_transport_health_check", { data: false });
1866 :
1867 5 : model.set_curtain(undefined);
1868 5 : }
1869 :
1870 : // HACK - Let's not use checkpoints for changes that involve
1871 : // adding or removing connections.
1872 : //
1873 : // https://bugzilla.redhat.com/show_bug.cgi?id=1378393
1874 : // https://bugzilla.redhat.com/show_bug.cgi?id=1398316
1875 : //
1876 : // We also switch off checkpoints for most of the integration
1877 : // tests.
1878 :
1879 14 : if (options.hack_does_add_or_remove || window.cockpit_tests_disable_checkpoints) {
1880 21 : modify();
1881 21 : return;
1882 21 : }
1883 :
1884 8 : if (window.cockpit_tests_checkpoints_settle_time)
1885 3 : settle_time = window.cockpit_tests_checkpoints_settle_time;
1886 :
1887 3 : manager.checkpoint_create(options.devices || [], rollback_time)
1888 6 : .then(function (cp) {
1889 1 : if (!cp) {
1890 1 : modify();
1891 1 : return;
1892 1 : }
1893 :
1894 : // Signal that a checkpoint is active for anaconda-webui
1895 6 : window.sessionStorage.setItem("cockpit_has_checkpoint", "true");
1896 :
1897 6 : show_curtain();
1898 6 : modify()
1899 6 : .then(function () {
1900 5 : window.setTimeout(function () {
1901 5 : manager.checkpoint_destroy(cp)
1902 3 : .catch(function () {
1903 3 : show_breaking_change_dialog({
1904 3 : ...options,
1905 3 : action: syn_click(model, modify)
1906 3 : });
1907 3 : })
1908 5 : .finally(function() {
1909 5 : hide_curtain();
1910 :
1911 : // Clear checkpoint status when done
1912 5 : window.sessionStorage.setItem("cockpit_has_checkpoint", "false");
1913 5 : });
1914 5 : }, settle_time * 1000);
1915 6 : })
1916 0 : .catch(function () {
1917 0 : hide_curtain();
1918 :
1919 : // HACK
1920 : //
1921 : // We want to avoid rollbacks for operations that don't actually change anything when they
1922 : // fail. Rollback are always disruptive and always seem to reconnect all the included
1923 : // devices, even if nothing has actually changed. Thus, if you give invalid input to
1924 : // NetworkManager and receive an error in a settings dialog, rolling back the checkpoint
1925 : // would cause a temporary disconnection on the interface.
1926 : //
1927 : // https://bugzilla.redhat.com/show_bug.cgi?id=1427187
1928 :
1929 0 : if (options.rollback_on_failure)
1930 0 : manager.checkpoint_rollback(cp);
1931 : else
1932 0 : manager.checkpoint_destroy(cp);
1933 :
1934 : // Clear checkpoint status on failure
1935 0 : window.sessionStorage.setItem("cockpit_has_checkpoint", "false");
1936 0 : });
1937 6 : });
1938 26 : }
1939 :
1940 14 : export function with_settings_checkpoint(model, modify, options) {
1941 14 : with_checkpoint(model, modify,
1942 14 : {
1943 14 : ...options,
1944 14 : fail_text: _("Changing the settings will break the connection to the server, and will make the administration UI unavailable."),
1945 14 : anyway_text: _("Change the settings"),
1946 14 : });
1947 14 : }
1948 :
1949 14 : export function connection_devices(con) {
1950 14 : const devices = [];
1951 :
1952 14 : if (con)
1953 14 : con.Interfaces.forEach(function (iface) { if (iface.Device) devices.push(iface.Device); });
1954 :
1955 14 : return devices;
1956 14 : }
1957 :
1958 11 : export function is_interface_connection(iface, connection) {
1959 3 : return connection && connection.Interfaces.indexOf(iface) != -1;
1960 11 : }
1961 :
1962 15 : export function is_interesting_interface(iface) {
1963 15 : return !iface.Device || is_managed(iface.Device);
1964 15 : }
1965 :
1966 10 : export function member_connection_for_interface(group, iface) {
1967 3 : return group?.Members.find(s => is_interface_connection(iface, s));
1968 10 : }
1969 :
1970 10 : export function member_interface_choices(model, group) {
1971 10 : return model.list_interfaces().filter(function (iface) {
1972 10 : return !is_interface_connection(iface, group) && is_interesting_interface(iface);
1973 10 : });
1974 10 : }
1975 :
1976 4 : export function free_member_connection(con) {
1977 4 : const cs = connection_settings(con);
1978 4 : if (cs.member_type) {
1979 4 : delete cs.member_type;
1980 4 : delete cs.group;
1981 4 : delete con.Settings.team_port;
1982 4 : delete con.Settings.bridge_port;
1983 4 : return con.apply_settings(con.Settings).then(() => { con.activate(null, null) });
1984 4 : }
1985 4 : }
1986 :
1987 10 : export function set_member(model, group_connection, group_settings, member_type,
1988 10 : iface_name, val) {
1989 10 : const iface = model.find_interface(iface_name);
1990 10 : if (!iface)
1991 0 : return false;
1992 :
1993 10 : const main_connection = iface.MainConnection;
1994 :
1995 10 : if (val) {
1996 : /* Turn the main_connection into a member for group.
1997 : */
1998 :
1999 10 : const group_iface = group_settings.connection.interface_name;
2000 10 : if (!group_iface)
2001 0 : return false;
2002 :
2003 10 : let member_settings;
2004 10 : if (main_connection) {
2005 10 : member_settings = main_connection.Settings;
2006 :
2007 10 : if (member_settings.connection.group == group_settings.connection.uuid ||
2008 10 : member_settings.connection.group == group_settings.connection.id ||
2009 10 : member_settings.connection.group == group_iface)
2010 1 : return Promise.resolve();
2011 :
2012 10 : member_settings.connection.member_type = member_type;
2013 10 : member_settings.connection.group = group_iface;
2014 10 : member_settings.connection.autoconnect = true;
2015 10 : delete member_settings.ipv4;
2016 10 : delete member_settings.ipv6;
2017 10 : delete member_settings.team_port;
2018 10 : delete member_settings.bridge_port;
2019 0 : } else {
2020 0 : member_settings = {
2021 0 : connection:
2022 0 : {
2023 0 : autoconnect: true,
2024 0 : interface_name: iface.Name,
2025 0 : member_type,
2026 0 : group: group_iface
2027 0 : }
2028 0 : };
2029 0 : complete_settings(member_settings, iface.Device);
2030 0 : }
2031 :
2032 10 : return settings_applier(model, iface.Device, main_connection)(member_settings).then(function () {
2033 : // If the group already exists (with the correct name),
2034 : // activate or deactivate the member immediately so that
2035 : // the settings actually apply and the interface becomes a
2036 : // member. Otherwise we activate it later when the group
2037 : // is created.
2038 0 : if (group_connection && group_connection.Interfaces[0].Name == group_iface) {
2039 0 : const group_dev = group_connection.Interfaces[0].Device;
2040 0 : if (group_dev && group_dev.ActiveConnection)
2041 0 : return main_connection.activate(iface.Device);
2042 0 : else if (iface.Device.ActiveConnection)
2043 0 : return iface.Device.ActiveConnection.deactivate();
2044 0 : }
2045 10 : });
2046 10 : } else {
2047 : /* Free the main_connection from being a member if it is our member. If there is
2048 : * no main_connection, we don't need to do anything.
2049 : */
2050 0 : if (main_connection && main_connection.Groups.indexOf(group_connection) != -1) {
2051 0 : free_member_connection(main_connection);
2052 0 : }
2053 10 : }
2054 :
2055 10 : return true;
2056 10 : }
2057 :
2058 10 : export function apply_group_member(choices, model, apply_group, group_connection, group_settings, member_type) {
2059 10 : const active_settings = [];
2060 :
2061 10 : if (!group_connection) {
2062 10 : if (group_settings.bond &&
2063 7 : group_settings.bond.options &&
2064 1 : group_settings.bond.options.primary) {
2065 1 : const iface = model.find_interface(group_settings.bond.options.primary);
2066 1 : if (iface && iface.MainConnection)
2067 1 : active_settings.push(iface.MainConnection.Settings);
2068 0 : } else {
2069 9 : Object.keys(choices)
2070 9 : .filter(choice => choices[choice])
2071 9 : .forEach(choice => {
2072 9 : const iface = model.find_interface(choice);
2073 9 : if (iface && iface.Device && iface.Device.ActiveConnection && iface.Device.ActiveConnection.Connection) {
2074 9 : active_settings.push(iface.Device.ActiveConnection.Connection.Settings);
2075 9 : }
2076 9 : });
2077 9 : }
2078 :
2079 8 : if (active_settings.length == 1) {
2080 8 : group_settings.ipv4 = JSON.parse(JSON.stringify(active_settings[0].ipv4));
2081 8 : group_settings.ipv6 = JSON.parse(JSON.stringify(active_settings[0].ipv6));
2082 8 : }
2083 :
2084 10 : group_settings.connection.autoconnect_members = 1;
2085 10 : }
2086 :
2087 : /* For bonds, the order in which members are added to their group matters since the first members gets to
2088 : * set the MAC address of the bond, which matters for DHCP. We leave it to NetworkManager to determine
2089 : * the order in which members are added so that the order is consistent with what happens when the bond is
2090 : * activated the next time, such as after a reboot.
2091 : */
2092 :
2093 10 : function set_all_members() {
2094 10 : const deferreds = Object.keys(choices).map(iface => {
2095 10 : return model.synchronize().then(function () {
2096 10 : return set_member(model, group_connection, group_settings, member_type,
2097 10 : iface, choices[iface]);
2098 10 : });
2099 10 : });
2100 10 : return Promise.all(deferreds);
2101 10 : }
2102 :
2103 10 : return set_all_members().then(function () {
2104 10 : return apply_group(group_settings);
2105 10 : });
2106 10 : }
2107 :
2108 37 : export function init() {
2109 37 : cockpit.translate();
2110 37 : }
|