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