Line data Source code
1 : /*
2 : * Copyright (C) 2013 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 35 : import React from "react";
6 35 : 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 35 : 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 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 35 : export function connection_settings(c) {
62 35 : if (c && c.Settings && c.Settings.connection) {
63 35 : 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 35 : }
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 35 : 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 35 : const self = this;
149 35 : cockpit.event_target(self);
150 :
151 35 : const client = cockpit.dbus("org.freedesktop.NetworkManager", { superuser: "try" });
152 35 : 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 35 : self.supports_dns_data = false;
158 :
159 35 : function check_version_dns_data_support(version) {
160 6 : if (version.length < 2) {
161 6 : return false;
162 6 : }
163 :
164 6 : if (version[0] > 1) {
165 6 : return true;
166 6 : } else if (version[0] === 1 && version[1] >= 42) {
167 35 : return true;
168 35 : }
169 :
170 6 : return false;
171 35 : }
172 :
173 : /* resolved once first stage of initialization is done */
174 35 : self.preinit = new Promise((resolve, reject) => {
175 35 : client.call("/org/freedesktop/NetworkManager",
176 35 : "org.freedesktop.DBus.Properties", "Get",
177 35 : ["org.freedesktop.NetworkManager", "Version"], { flags: "" })
178 35 : .then((reply, options) => {
179 35 : const nm_version = reply[0].v.split(".").map(n => Number.parseInt(n));
180 35 : self.supports_dns_data = check_version_dns_data_support(nm_version);
181 :
182 35 : if (options.flags) {
183 35 : if (options.flags.indexOf(">") !== -1)
184 6 : utils.set_byteorder("be");
185 35 : else if (options.flags.indexOf("<") !== -1)
186 35 : utils.set_byteorder("le");
187 35 : resolve();
188 35 : }
189 35 : })
190 35 : .catch(complain);
191 35 : });
192 :
193 : /* Mostly generic D-Bus stuff. */
194 :
195 35 : const objects = { };
196 :
197 6 : self.set_curtain = (state) => {
198 6 : self.curtain = state;
199 6 : self.dispatchEvent("changed");
200 6 : };
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 35 : function conv_Object(type) {
214 35 : return function (path) {
215 35 : return get_object(path, type);
216 35 : };
217 35 : }
218 :
219 35 : function conv_Array(conv) {
220 35 : return function (elts) {
221 35 : return elts.map(conv);
222 35 : };
223 35 : }
224 :
225 35 : function priv(obj) {
226 35 : return obj[' priv'];
227 35 : }
228 :
229 35 : let outstanding_refreshes = 0;
230 :
231 35 : function push_refresh() {
232 35 : outstanding_refreshes += 1;
233 35 : }
234 :
235 35 : function pop_refresh() {
236 35 : outstanding_refreshes -= 1;
237 35 : if (outstanding_refreshes === 0)
238 35 : export_model();
239 35 : }
240 :
241 35 : function get_object(path, type) {
242 35 : if (path == "/")
243 35 : return null;
244 35 : function Constructor() {
245 35 : this[' priv'] = { };
246 35 : priv(this).type = type;
247 35 : priv(this).path = path;
248 35 : for (const p in type.props)
249 35 : this[p] = type.props[p].def;
250 35 : }
251 35 : if (!objects[path]) {
252 35 : Constructor.prototype = type.prototype;
253 35 : objects[path] = new Constructor();
254 35 : if (type.refresh)
255 35 : type.refresh(objects[path]);
256 35 : if (type.exporters && type.exporters[0])
257 35 : type.exporters[0](objects[path]);
258 35 : }
259 35 : return objects[path];
260 35 : }
261 :
262 35 : function peek_object(path) {
263 34 : return objects[path] || null;
264 35 : }
265 :
266 28 : function drop_object(path) {
267 28 : const obj = objects[path];
268 28 : if (obj) {
269 28 : if (priv(obj).type.drop)
270 7 : priv(obj).type.drop(obj);
271 28 : delete objects[path];
272 28 : export_model();
273 28 : }
274 28 : }
275 :
276 35 : function set_object_properties(obj, props) {
277 35 : const decl = priv(obj).type.props;
278 35 : for (const p in decl) {
279 35 : let val = props[decl[p].prop || p];
280 35 : if (val !== undefined) {
281 35 : if (decl[p].conv)
282 35 : val = decl[p].conv(val);
283 35 : if (val !== obj[p]) {
284 35 : obj[p] = val;
285 35 : if (decl[p].trigger)
286 35 : decl[p].trigger(obj);
287 35 : }
288 35 : }
289 35 : }
290 35 : }
291 :
292 29 : function remove_signatures(props_with_sigs) {
293 29 : const props = { };
294 29 : for (const p in props_with_sigs) {
295 29 : if (props_with_sigs[p]) {
296 29 : props[p] = props_with_sigs[p].v;
297 29 : }
298 29 : }
299 29 : return props;
300 29 : }
301 :
302 35 : function objpath(obj) {
303 35 : if (obj && priv(obj).path)
304 20 : return priv(obj).path;
305 : else
306 20 : return "/";
307 35 : }
308 :
309 26 : function call_object_method(obj, iface, method) {
310 26 : return client.call(objpath(obj), iface, method, Array.prototype.slice.call(arguments, 3));
311 26 : }
312 :
313 35 : const interface_types = { };
314 35 : let max_export_phases = 0;
315 35 : let export_pending;
316 :
317 35 : function set_object_types(all_types) {
318 35 : all_types.forEach(function (type) {
319 35 : if (type.exporters && type.exporters.length > max_export_phases)
320 35 : max_export_phases = type.exporters.length;
321 35 : type.interfaces.forEach(function (iface) {
322 35 : interface_types[iface] = type;
323 35 : });
324 35 : });
325 35 : }
326 :
327 29 : function signal_emitted(path, iface, signal, args) {
328 29 : const obj = peek_object(path);
329 :
330 29 : if (obj) {
331 29 : const type = priv(obj).type;
332 :
333 29 : if (signal == "PropertiesChanged") {
334 29 : push_refresh();
335 29 : const props = remove_signatures(args[0]);
336 29 : set_object_properties(obj, props);
337 29 : pop_refresh();
338 22 : } else if (type.signals && type.signals[signal])
339 21 : type.signals[signal](obj, args);
340 29 : }
341 29 : }
342 :
343 35 : function interface_properties(path, iface, props) {
344 35 : const type = interface_types[iface];
345 35 : if (type)
346 35 : set_object_properties(get_object(path, type), props);
347 35 : }
348 :
349 28 : function interface_removed(path, iface) {
350 : /* For NetworkManager we can make this assumption */
351 28 : drop_object(path);
352 28 : }
353 :
354 35 : let export_model_promise = null;
355 35 : let export_model_promise_resolve = null;
356 :
357 35 : function export_model() {
358 35 : function doit() {
359 35 : for (let phase = 0; phase < max_export_phases; phase++) {
360 35 : for (const path in objects) {
361 35 : const obj = objects[path];
362 35 : const exp = priv(obj).type.exporters;
363 35 : if (exp && exp[phase])
364 35 : exp[phase](obj);
365 35 : }
366 35 : }
367 :
368 35 : self.ready = true;
369 35 : self.dispatchEvent('changed');
370 6 : if (export_model_promise) {
371 6 : export_model_promise_resolve();
372 6 : export_model_promise = null;
373 6 : export_model_promise_resolve = null;
374 6 : }
375 35 : }
376 :
377 35 : if (!export_pending) {
378 35 : export_pending = true;
379 35 : window.setTimeout(function () { export_pending = false; doit() }, 300);
380 35 : }
381 35 : }
382 :
383 23 : self.synchronize = function synchronize() {
384 23 : if (outstanding_refreshes === 0) {
385 23 : return Promise.resolve();
386 3 : } else {
387 3 : if (!export_model_promise)
388 0 : export_model_promise = new Promise(resolve => { export_model_promise_resolve = resolve });
389 3 : return export_model_promise;
390 3 : }
391 23 : };
392 :
393 35 : let subscription;
394 35 : let watch;
395 :
396 35 : function onNotifyEventHandler(event, data) {
397 35 : Object.keys(data).forEach(path => {
398 35 : const interfaces = data[path];
399 :
400 35 : Object.keys(interfaces).forEach(iface => {
401 35 : 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 35 : if (path.includes("/Devices/") && props?.StateReason) {
408 35 : const obj = peek_object(path);
409 35 : if (obj) {
410 35 : const [state, reason] = props.StateReason;
411 6 : if (state === 120 && reason !== 0) {
412 6 : utils.debug("Captured", obj.Interface, "failure, reason:", reason);
413 6 : priv(obj).lastFailureReason = reason;
414 6 : }
415 35 : }
416 35 : }
417 :
418 35 : if (props)
419 31 : interface_properties(path, iface, props);
420 : else
421 31 : interface_removed(path, iface);
422 35 : });
423 35 : });
424 35 : }
425 :
426 35 : self.preinit.then(() => {
427 35 : subscription = client.subscribe({ }, signal_emitted);
428 35 : client.addEventListener("notify", onNotifyEventHandler);
429 35 : 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 35 : });
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 35 : function ip_address_from_nm(addr) {
449 35 : return {
450 35 : address: addr.address.v,
451 35 : prefix: utils.ip_prefix_to_text(addr.prefix.v)
452 35 : };
453 35 : }
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 35 : function settings_from_nm(settings) {
508 35 : function get(first, second, def) {
509 35 : if (settings[first] && settings[first][second])
510 35 : return settings[first][second].v;
511 : else
512 35 : return def;
513 35 : }
514 :
515 35 : function get_ip(first, ip_to_text) {
516 35 : const dns_data = self.supports_dns_data
517 35 : ? get(first, "dns-data", [])
518 6 : : get(first, "dns", []).map(ip_to_text);
519 :
520 35 : return {
521 35 : method: get(first, "method", "auto"),
522 35 : ignore_auto_dns: get(first, "ignore-auto-dns", false),
523 35 : ignore_auto_routes: get(first, "ignore-auto-routes", false),
524 35 : address_data: get(first, "address-data", []).map(ip_address_from_nm),
525 35 : gateway: get(first, "gateway", ""),
526 35 : dns_data,
527 35 : dns_search: get(first, "dns-search", []),
528 35 : route_data: get(first, "route-data", []).map(route_from_nm),
529 35 : };
530 35 : }
531 :
532 35 : const result = {
533 35 : connection: {
534 35 : type: get("connection", "type"),
535 35 : uuid: get("connection", "uuid"),
536 35 : interface_name: get("connection", "interface-name"),
537 35 : timestamp: get("connection", "timestamp", 0),
538 35 : id: get("connection", "id", _("Unknown")),
539 35 : autoconnect: get("connection", "autoconnect", true),
540 35 : autoconnect_priority: get("connection", "autoconnect-priority", 0),
541 35 : autoconnect_members:
542 35 : get("connection", "autoconnect-slaves", -1),
543 35 : member_type: get("connection", "slave-type"),
544 35 : group: get("connection", "master"),
545 35 : multi_connect: get("connection", "multi-connect"),
546 35 : }
547 35 : };
548 :
549 35 : if (!settings.connection.master) {
550 35 : result.ipv4 = get_ip("ipv4", utils.ip4_to_text);
551 35 : result.ipv6 = get_ip("ipv6", utils.ip6_to_text);
552 35 : }
553 :
554 35 : if (settings["802-3-ethernet"]) {
555 35 : result.ethernet = {
556 35 : mtu: get("802-3-ethernet", "mtu"),
557 35 : assigned_mac_address: get("802-3-ethernet", "assigned-mac-address")
558 35 : };
559 35 : }
560 :
561 14 : 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 14 : result.bond = {
566 14 : options: { ...get("bond", "options", { }) },
567 14 : interface_name: get("bond", "interface-name")
568 14 : };
569 14 : }
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 7 : if (settings.team) {
580 7 : result.team = {
581 7 : config: JSON_parse_carefully(get("team", "config", "{}")),
582 7 : interface_name: get("team", "interface-name")
583 7 : };
584 7 : }
585 :
586 7 : if (settings["team-port"] || result.connection.member_type == "team") {
587 7 : result.team_port = { config: JSON_parse_carefully(get("team-port", "config", "{}")), };
588 7 : }
589 :
590 9 : if (settings.bridge) {
591 9 : result.bridge = {
592 9 : interface_name: get("bridge", "interface-name"),
593 9 : stp: get("bridge", "stp", true),
594 9 : priority: get("bridge", "priority", 32768),
595 9 : forward_delay: get("bridge", "forward-delay", 15),
596 9 : hello_time: get("bridge", "hello-time", 2),
597 9 : max_age: get("bridge", "max-age", 20),
598 9 : ageing_time: get("bridge", "ageing-time", 300)
599 9 : };
600 9 : }
601 :
602 8 : if (settings["bridge-port"] || result.connection.member_type == "bridge") {
603 8 : result.bridge_port = {
604 8 : priority: get("bridge-port", "priority", 32),
605 8 : path_cost: get("bridge-port", "path-cost", 100),
606 8 : hairpin_mode: get("bridge-port", "hairpin-mode", false)
607 8 : };
608 8 : }
609 :
610 7 : if (settings.vlan) {
611 7 : result.vlan = {
612 7 : parent: get("vlan", "parent"),
613 7 : id: get("vlan", "id"),
614 7 : interface_name: get("vlan", "interface-name")
615 7 : };
616 7 : }
617 :
618 7 : if (settings.wireguard) {
619 7 : result.wireguard = {
620 7 : 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 7 : };
627 7 : }
628 :
629 6 : if (settings["802-11-wireless"]) {
630 6 : result["802-11-wireless"] = {
631 6 : ssid: get("802-11-wireless", "ssid"),
632 6 : mode: get("802-11-wireless", "mode"),
633 6 : };
634 6 : }
635 :
636 35 : return result;
637 35 : }
638 :
639 23 : function settings_to_nm(settings, orig) {
640 5 : const result = JSON.parse(JSON.stringify(orig || { }));
641 :
642 23 : function set(first, second, sig, val, def) {
643 23 : if (val === undefined)
644 23 : val = def;
645 23 : if (!result[first])
646 17 : result[first] = { };
647 23 : if (val !== undefined)
648 23 : result[first][second] = cockpit.variant(sig, val);
649 : else
650 23 : delete result[first][second];
651 23 : }
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 23 : set("connection", "id", 's', settings.connection.id);
711 23 : set("connection", "autoconnect", 'b', settings.connection.autoconnect);
712 23 : set("connection", "autoconnect-priority", 'i', settings.connection.autoconnect_priority);
713 23 : set("connection", "autoconnect-slaves", 'i', settings.connection.autoconnect_members);
714 23 : set("connection", "uuid", 's', settings.connection.uuid);
715 23 : set("connection", "interface-name", 's', settings.connection.interface_name);
716 23 : set("connection", "type", 's', settings.connection.type);
717 23 : set("connection", "slave-type", 's', settings.connection.member_type);
718 23 : set("connection", "master", 's', settings.connection.group);
719 23 : set("connection", "multi-connect", 'i', settings.connection.multi_connect);
720 :
721 23 : if (settings.ipv4)
722 13 : set_ip("ipv4", 'au', utils.ip4_from_text);
723 : else
724 14 : delete result.ipv4;
725 :
726 23 : if (settings.ipv6)
727 13 : set_ip("ipv6", 'aay', utils.ip6_from_text);
728 : else
729 14 : delete result.ipv6;
730 :
731 10 : if (settings.bond) {
732 10 : set("bond", "options", 'a{ss}', settings.bond.options);
733 10 : set("bond", "interface-name", 's', settings.bond.interface_name);
734 10 : } else
735 23 : delete result.bond;
736 :
737 4 : if (settings.team) {
738 4 : set("team", "config", 's', JSON.stringify(settings.team.config));
739 4 : set("team", "interface-name", 's', settings.team.interface_name);
740 4 : } else
741 23 : delete result.team;
742 :
743 23 : if (settings.team_port)
744 4 : set("team-port", "config", 's', JSON.stringify(settings.team_port.config));
745 : else
746 23 : delete result["team-port"];
747 :
748 6 : if (settings.bridge) {
749 6 : set("bridge", "interface-name", 's', settings.bridge.interface_name);
750 6 : set("bridge", "stp", 'b', settings.bridge.stp);
751 6 : set("bridge", "priority", 'u', settings.bridge.priority);
752 6 : set("bridge", "forward-delay", 'u', settings.bridge.forward_delay);
753 6 : set("bridge", "hello-time", 'u', settings.bridge.hello_time);
754 6 : set("bridge", "max-age", 'u', settings.bridge.max_age);
755 6 : set("bridge", "ageing-time", 'u', settings.bridge.ageing_time);
756 6 : } else
757 23 : delete result.bridge;
758 :
759 4 : if (settings.bridge_port) {
760 4 : set("bridge-port", "priority", 'u', settings.bridge_port.priority);
761 4 : set("bridge-port", "path-cost", 'u', settings.bridge_port.path_cost);
762 4 : set("bridge-port", "hairpin-mode", 'b', settings.bridge_port.hairpin_mode);
763 4 : } else
764 23 : delete result["bridge-port"];
765 :
766 4 : if (settings.vlan) {
767 4 : set("vlan", "parent", 's', settings.vlan.parent);
768 4 : set("vlan", "id", 'u', settings.vlan.id);
769 4 : set("vlan", "interface-name", 's', settings.vlan.interface_name);
770 : // '1' is the default, but we need to set it explicitly anyway.
771 4 : set("vlan", "flags", 'u', 1);
772 4 : } else
773 22 : delete result.vlan;
774 :
775 21 : if (settings.ethernet) {
776 21 : set("802-3-ethernet", "mtu", 'u', settings.ethernet.mtu);
777 21 : 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 21 : delete result["802-3-ethernet"]["cloned-mac-address"];
780 21 : } else
781 9 : delete result["802-3-ethernet"];
782 :
783 4 : if (settings.wireguard) {
784 4 : set("wireguard", "private-key", "s", settings.wireguard.private_key);
785 4 : 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 4 : } else {
807 23 : delete result.wireguard;
808 23 : }
809 :
810 3 : if (settings["802-11-wireless"]) {
811 3 : set("802-11-wireless", "ssid", 'ay', settings["802-11-wireless"].ssid);
812 3 : set("802-11-wireless", "mode", 's', settings["802-11-wireless"].mode);
813 3 : } else {
814 23 : delete result["802-11-wireless"];
815 23 : }
816 :
817 3 : if (settings["802-11-wireless-security"]) {
818 3 : set("802-11-wireless-security", "key-mgmt", 's', settings["802-11-wireless-security"]["key-mgmt"]);
819 3 : set("802-11-wireless-security", "psk", 's', settings["802-11-wireless-security"].psk);
820 3 : } else {
821 23 : delete result["802-11-wireless-security"];
822 23 : }
823 :
824 23 : return result;
825 23 : }
826 :
827 35 : 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 35 : switch (type) {
831 6 : case 0: return 'unknown';
832 35 : case 1: return 'ethernet'; // 802-3-ethernet
833 6 : case 2: return '802-11-wireless';
834 6 : case 3: return 'unused1';
835 6 : case 4: return 'unused2';
836 6 : case 5: return 'bluetooth';
837 6 : case 6: return '802-11-olpc-mesh';
838 6 : case 7: return 'wimax';
839 6 : case 8: return 'modem';
840 6 : case 9: return 'infiniband';
841 14 : case 10: return 'bond';
842 7 : case 11: return 'vlan';
843 6 : case 12: return 'adsl';
844 9 : case 13: return 'bridge';
845 6 : case 14: return 'generic';
846 7 : case 15: return 'team';
847 7 : case 16: return 'tun';
848 6 : case 17: return 'ip_tunnel';
849 6 : case 18: return 'macvlan';
850 6 : case 19: return 'vxlan';
851 24 : case 20: return 'veth';
852 6 : case 21: return 'macsec';
853 6 : case 22: return 'dummy';
854 6 : case 23: return 'ppp';
855 6 : case 24: return 'ovs_interface';
856 6 : case 25: return 'ovs_port';
857 6 : case 26: return 'ovs_bridge';
858 6 : case 27: return 'wpan';
859 6 : case 28: return '6lowpan';
860 7 : case 29: return 'wireguard';
861 6 : case 30: return 'wifi_p2p';
862 6 : case 31: return 'vrf';
863 35 : case 32: return 'loopback';
864 6 : default: return '';
865 35 : }
866 35 : }
867 :
868 35 : function device_state_to_text(state) {
869 35 : switch (state) {
870 : // NM_DEVICE_STATE_UNKNOWN
871 6 : case 0: return "?";
872 : // NM_DEVICE_STATE_UNMANAGED
873 30 : case 10: return "";
874 : // NM_DEVICE_STATE_UNAVAILABLE
875 28 : case 20: return _("Not available");
876 : // NM_DEVICE_STATE_DISCONNECTED
877 35 : case 30: return _("Inactive");
878 : // NM_DEVICE_STATE_PREPARE
879 32 : case 40: return _("Preparing");
880 : // NM_DEVICE_STATE_CONFIG
881 32 : case 50: return _("Configuring");
882 : // NM_DEVICE_STATE_NEED_AUTH
883 7 : case 60: return _("Authenticating");
884 : // NM_DEVICE_STATE_IP_CONFIG
885 32 : case 70: return _("Configuring IP");
886 : // NM_DEVICE_STATE_IP_CHECK
887 31 : case 80: return _("Checking IP");
888 : // NM_DEVICE_STATE_SECONDARIES
889 31 : case 90: return _("Waiting");
890 : // NM_DEVICE_STATE_ACTIVATED
891 35 : case 100: return _("Active");
892 : // NM_DEVICE_STATE_DEACTIVATING
893 30 : case 110: return _("Deactivating");
894 : // NM_DEVICE_STATE_FAILED
895 6 : case 120: return _("Failed");
896 6 : default: return "";
897 35 : }
898 35 : }
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 35 : const connections_by_uuid = { };
916 :
917 35 : function set_settings(obj, settings) {
918 25 : if (obj.Settings && obj.Settings.connection && obj.Settings.connection.uuid)
919 25 : delete connections_by_uuid[obj.Settings.connection.uuid];
920 35 : obj.Settings = settings;
921 35 : if (settings && settings.connection && settings.connection.uuid)
922 35 : connections_by_uuid[settings.connection.uuid] = obj;
923 35 : }
924 :
925 35 : function refresh_settings(obj) {
926 35 : push_refresh();
927 35 : client.call(objpath(obj), "org.freedesktop.NetworkManager.Settings.Connection", "GetSettings")
928 35 : .then(function(reply) {
929 35 : const result = reply[0];
930 35 : if (result) {
931 35 : priv(obj).orig = result;
932 35 : set_settings(obj, settings_from_nm(result));
933 35 : }
934 35 : })
935 35 : .catch(complain)
936 35 : .finally(pop_refresh);
937 35 : }
938 :
939 35 : function refresh_udev(obj) {
940 35 : if (obj.Udi.indexOf("/sys/") !== 0)
941 35 : return;
942 :
943 35 : push_refresh();
944 35 : cockpit.spawn(["udevadm", "info", obj.Udi], { err: 'message' })
945 35 : .then(function(res) {
946 35 : const props = { };
947 35 : function snarf_prop(line, env, prop) {
948 35 : const prefix = "E: " + env + "=";
949 35 : if (line.indexOf(prefix) === 0) {
950 35 : props[prop] = line.substring(prefix.length);
951 35 : }
952 35 : }
953 35 : res.split('\n').forEach(function(line) {
954 35 : snarf_prop(line, "ID_MODEL_FROM_DATABASE", "IdModel");
955 35 : snarf_prop(line, "ID_VENDOR_FROM_DATABASE", "IdVendor");
956 35 : });
957 35 : set_object_properties(obj, props);
958 35 : })
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 35 : .finally(pop_refresh);
967 35 : }
968 :
969 21 : function handle_updated(obj) {
970 21 : refresh_settings(obj);
971 21 : }
972 :
973 : /* NetworkManager specific object types, used by the generic D-Bus
974 : * code and using the data conversion functions.
975 : */
976 :
977 35 : const type_Ipv4Config = {
978 35 : interfaces: [
979 35 : "org.freedesktop.NetworkManager.IP4Config"
980 35 : ],
981 :
982 35 : props: {
983 35 : AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
984 35 : }
985 35 : };
986 :
987 35 : const type_Ipv6Config = {
988 35 : interfaces: [
989 35 : "org.freedesktop.NetworkManager.IP6Config"
990 35 : ],
991 :
992 35 : props: {
993 35 : AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
994 35 : }
995 35 : };
996 :
997 35 : const type_AccessPoint = {
998 35 : interfaces: [
999 35 : "org.freedesktop.NetworkManager.AccessPoint"
1000 35 : ],
1001 :
1002 35 : props: {
1003 35 : Flags: { def: 0 },
1004 35 : WpaFlags: { def: 0 },
1005 35 : RsnFlags: { def: 0 },
1006 35 : Ssid: { conv: utils.ssid_from_nm, def: "" },
1007 35 : Frequency: { def: 0 }, // MHz
1008 35 : HwAddress: { def: "" },
1009 35 : Mode: { conv: access_point_mode_to_text, def: "" },
1010 35 : MaxBitrate: { def: 0 }, // Kbit/s
1011 35 : Bandwidth: { def: 0 }, // MHz
1012 35 : Strength: { def: 0 },
1013 35 : LastSeen: { def: -1 }, // CLOCK_BOOTTIME seconds, -1 if never seen
1014 35 : },
1015 :
1016 35 : 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 35 : ]
1026 35 : };
1027 :
1028 35 : const type_Connection = {
1029 35 : interfaces: [
1030 35 : "org.freedesktop.NetworkManager.Settings.Connection"
1031 35 : ],
1032 :
1033 35 : props: {
1034 35 : Unsaved: { }
1035 35 : },
1036 :
1037 35 : signals: {
1038 35 : Updated: handle_updated
1039 35 : },
1040 :
1041 35 : refresh: refresh_settings,
1042 :
1043 6 : drop: function (obj) {
1044 6 : set_settings(obj, null);
1045 6 : },
1046 :
1047 35 : prototype: {
1048 0 : copy_settings: function () {
1049 0 : return JSON.parse(JSON.stringify(this.Settings));
1050 0 : },
1051 :
1052 21 : apply_settings: function (settings) {
1053 21 : const self = this;
1054 21 : try {
1055 21 : return call_object_method(self,
1056 21 : "org.freedesktop.NetworkManager.Settings.Connection", "Update",
1057 21 : settings_to_nm(settings, priv(self).orig))
1058 21 : .then(() => {
1059 21 : set_settings(self, settings);
1060 21 : });
1061 3 : } catch (e) {
1062 3 : return Promise.reject(e);
1063 3 : }
1064 21 : },
1065 :
1066 17 : activate: function (dev, specific_object) {
1067 17 : return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
1068 17 : "org.freedesktop.NetworkManager", "ActivateConnection",
1069 17 : objpath(this), objpath(dev), objpath(specific_object))
1070 17 : .then(([active_connection]) => active_connection);
1071 17 : },
1072 :
1073 6 : delete_: function () {
1074 6 : return call_object_method(this, "org.freedesktop.NetworkManager.Settings.Connection", "Delete")
1075 5 : .then(() => undefined);
1076 6 : }
1077 35 : },
1078 :
1079 35 : exporters: [
1080 35 : function (obj) {
1081 35 : obj.Groups = [];
1082 35 : obj.Members = [];
1083 35 : obj.Interfaces = [];
1084 35 : },
1085 :
1086 35 : null,
1087 :
1088 35 : null,
1089 :
1090 : // Needs: type_Interface.Connections
1091 : //
1092 : // Sets: type_Connection.Members
1093 : // type_Connection.Groups
1094 : //
1095 35 : 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 13 : function check_con(con) {
1104 13 : const group_settings = connection_settings(con);
1105 13 : const my_settings = connection_settings(obj);
1106 13 : if (group_settings.type == my_settings.member_type) {
1107 13 : obj.Groups.push(con);
1108 13 : con.Members.push(obj);
1109 13 : }
1110 13 : }
1111 :
1112 35 : const cs = connection_settings(obj);
1113 17 : if (cs.member_type) {
1114 17 : const group = connections_by_uuid[cs.group];
1115 6 : if (group) {
1116 6 : obj.Groups.push(group);
1117 6 : group.Members.push(obj);
1118 6 : } else {
1119 17 : const iface = peek_interface(cs.group);
1120 17 : if (iface) {
1121 17 : iface.Connections.forEach(check_con);
1122 17 : }
1123 17 : }
1124 17 : }
1125 35 : }
1126 35 : ]
1127 :
1128 35 : };
1129 :
1130 35 : const type_ActiveConnection = {
1131 35 : interfaces: [
1132 35 : "org.freedesktop.NetworkManager.Connection.Active"
1133 35 : ],
1134 :
1135 35 : props: {
1136 35 : Connection: { conv: conv_Object(type_Connection) },
1137 35 : Ip4Config: { conv: conv_Object(type_Ipv4Config) },
1138 35 : Ip6Config: { conv: conv_Object(type_Ipv6Config) },
1139 35 : State: { def: 0 }
1140 : // See below for "Group"
1141 35 : },
1142 :
1143 35 : 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 35 : }
1151 35 : };
1152 :
1153 35 : const type_Device = {
1154 35 : interfaces: [
1155 35 : "org.freedesktop.NetworkManager.Device",
1156 35 : "org.freedesktop.NetworkManager.Device.Wired",
1157 35 : "org.freedesktop.NetworkManager.Device.Bond",
1158 35 : "org.freedesktop.NetworkManager.Device.Team",
1159 35 : "org.freedesktop.NetworkManager.Device.Bridge",
1160 35 : "org.freedesktop.NetworkManager.Device.Vlan",
1161 35 : "org.freedesktop.NetworkManager.Device.Wireless"
1162 35 : ],
1163 :
1164 35 : props: {
1165 35 : DeviceType: { conv: device_type_to_symbol },
1166 35 : Interface: { },
1167 35 : StateText: { prop: "State", conv: device_state_to_text, def: _("Unknown") },
1168 35 : State: { },
1169 35 : StateReason: { def: [0, 0] }, // [state, reason] tuple
1170 35 : HwAddress: { },
1171 35 : AvailableConnections: { conv: conv_Array(conv_Object(type_Connection)), def: [] },
1172 35 : ActiveConnection: { conv: conv_Object(type_ActiveConnection) },
1173 35 : Ip4Config: { conv: conv_Object(type_Ipv4Config) },
1174 35 : Ip6Config: { conv: conv_Object(type_Ipv6Config) },
1175 35 : Udi: { trigger: refresh_udev },
1176 35 : IdVendor: { def: "" },
1177 35 : IdModel: { def: "" },
1178 35 : Driver: { def: "" },
1179 35 : Carrier: { def: true },
1180 35 : Speed: { },
1181 35 : Managed: { def: false },
1182 : // WiFi-specific properties
1183 35 : AccessPoints: { conv: conv_Array(conv_Object(type_AccessPoint)), def: [] },
1184 35 : ActiveAccessPoint: { conv: conv_Object(type_AccessPoint) },
1185 : // See below for "Members"
1186 35 : },
1187 :
1188 35 : 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 2 : activate_with_settings: function(settings, specific_object) {
1198 2 : priv(this).lastFailureReason = undefined; // Clear stale failure reason from previous attempts
1199 2 : try {
1200 2 : return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
1201 2 : "org.freedesktop.NetworkManager", "AddAndActivateConnection",
1202 2 : settings_to_nm(settings), objpath(this), objpath(specific_object))
1203 2 : .then(([path, active_connection_path]) => ({
1204 2 : connection: get_object(path, type_Connection),
1205 2 : active_connection: get_object(active_connection_path, type_ActiveConnection)
1206 2 : }));
1207 0 : } catch (e) {
1208 0 : return Promise.reject(e);
1209 0 : }
1210 2 : },
1211 :
1212 7 : disconnect: function () {
1213 7 : return call_object_method(this, 'org.freedesktop.NetworkManager.Device', 'Disconnect')
1214 7 : .then(() => undefined);
1215 7 : },
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 35 : },
1313 :
1314 35 : exporters: [
1315 35 : function (obj) {
1316 6 : 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 6 : 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 6 : const apBySsid = new Map();
1331 6 : 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 6 : obj.visibleSsids = Array.from(apBySsid.values());
1343 6 : obj.hiddenAPCount = hiddenCount;
1344 6 : utils.debug("Device exporter:", obj.Interface, "has", obj.visibleSsids.length, "visible SSIDs and", obj.hiddenAPCount, "hidden APs");
1345 6 : }
1346 35 : }
1347 35 : ]
1348 35 : };
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 35 : const type_Interface = {
1359 35 : interfaces: [],
1360 :
1361 35 : exporters: [
1362 35 : function (obj) {
1363 35 : obj.Device = null;
1364 35 : obj._NonDeviceConnections = [];
1365 35 : obj.Connections = [];
1366 35 : obj.MainConnection = null;
1367 35 : },
1368 :
1369 35 : 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 35 : function (obj) {
1379 11 : if (!obj.Device && obj._NonDeviceConnections.length === 0) {
1380 11 : drop_object(priv(obj).path);
1381 11 : return;
1382 11 : }
1383 :
1384 35 : function consider_for_main(con) {
1385 35 : if (!obj.MainConnection ||
1386 8 : connection_settings(obj.MainConnection).timestamp < connection_settings(con).timestamp) {
1387 35 : obj.MainConnection = con;
1388 35 : }
1389 35 : }
1390 :
1391 35 : obj.Connections = obj._NonDeviceConnections;
1392 :
1393 35 : if (obj.Device) {
1394 35 : obj.Device.AvailableConnections.forEach(function (con) {
1395 35 : if (obj.Connections.indexOf(con) == -1)
1396 35 : obj.Connections.push(con);
1397 35 : });
1398 35 : }
1399 :
1400 35 : obj.Connections.forEach(function (con) {
1401 35 : consider_for_main(con);
1402 35 : con.Interfaces.push(obj);
1403 35 : });
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 35 : if (obj.Device && obj.Device.ActiveConnection && obj.Device.ActiveConnection.Connection) {
1412 35 : obj.MainConnection = obj.Device.ActiveConnection.Connection;
1413 35 : }
1414 35 : }
1415 35 : ]
1416 :
1417 35 : };
1418 :
1419 35 : function get_interface(iface) {
1420 35 : const obj = get_object(":interface:" + iface, type_Interface);
1421 35 : obj.Name = iface;
1422 35 : return obj;
1423 35 : }
1424 :
1425 33 : function peek_interface(iface) {
1426 33 : return peek_object(":interface:" + iface);
1427 33 : }
1428 :
1429 35 : const type_Settings = {
1430 35 : interfaces: [
1431 35 : "org.freedesktop.NetworkManager.Settings"
1432 35 : ],
1433 :
1434 35 : props: {
1435 35 : Connections: { conv: conv_Array(conv_Object(type_Connection)), def: [] }
1436 35 : },
1437 :
1438 35 : prototype: {
1439 14 : add_connection: function (conf) {
1440 14 : return call_object_method(this,
1441 14 : 'org.freedesktop.NetworkManager.Settings',
1442 14 : 'AddConnection',
1443 14 : settings_to_nm(conf, { }))
1444 14 : .then(([path]) => get_object(path, type_Connection));
1445 14 : }
1446 35 : },
1447 :
1448 35 : exporters: [
1449 35 : null,
1450 :
1451 : // Sets: type_Interface._NonDeviceConnections
1452 : //
1453 35 : function (obj) {
1454 35 : if (obj.Connections) {
1455 35 : obj.Connections.forEach(function (con) {
1456 35 : function add_to_interface(name) {
1457 35 : if (name) {
1458 35 : const cons = get_interface(name)._NonDeviceConnections;
1459 35 : if (cons.indexOf(con) == -1)
1460 35 : cons.push(con);
1461 35 : }
1462 35 : }
1463 :
1464 35 : if (con.Settings) {
1465 35 : if (con.Settings.connection)
1466 35 : add_to_interface(con.Settings.connection.interface_name);
1467 35 : if (con.Settings.bond)
1468 14 : add_to_interface(con.Settings.bond.interface_name);
1469 35 : if (con.Settings.team)
1470 7 : add_to_interface(con.Settings.team.interface_name);
1471 35 : if (con.Settings.bridge)
1472 9 : add_to_interface(con.Settings.bridge.interface_name);
1473 35 : if (con.Settings.vlan)
1474 7 : add_to_interface(con.Settings.vlan.interface_name);
1475 35 : }
1476 35 : });
1477 35 : }
1478 35 : }
1479 35 : ]
1480 35 : };
1481 :
1482 35 : const type_Manager = {
1483 35 : interfaces: [
1484 35 : "org.freedesktop.NetworkManager"
1485 35 : ],
1486 :
1487 35 : props: {
1488 35 : Capabilities: { def: [] },
1489 35 : Version: { },
1490 35 : Devices: {
1491 35 : conv: conv_Array(conv_Object(type_Device)),
1492 35 : def: []
1493 35 : },
1494 35 : ActiveConnections: { conv: conv_Array(conv_Object(type_ActiveConnection)), def: [] }
1495 35 : },
1496 :
1497 35 : prototype: {
1498 6 : checkpoint_create: function (devices, timeout) {
1499 6 : return call_object_method(this,
1500 6 : 'org.freedesktop.NetworkManager',
1501 6 : 'CheckpointCreate',
1502 6 : devices.map(objpath),
1503 6 : timeout,
1504 6 : 0)
1505 6 : .then(([checkpoint]) => checkpoint)
1506 1 : .catch(function (error) {
1507 1 : if (error.name != "org.freedesktop.DBus.Error.UnknownMethod")
1508 0 : console.warn(error.message || error);
1509 1 : });
1510 6 : },
1511 :
1512 6 : checkpoint_destroy: function (checkpoint) {
1513 6 : if (checkpoint) {
1514 6 : return call_object_method(this,
1515 6 : 'org.freedesktop.NetworkManager',
1516 6 : 'CheckpointDestroy',
1517 6 : checkpoint)
1518 3 : .then(() => undefined);
1519 6 : } else
1520 0 : return Promise.resolve();
1521 6 : },
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 35 : },
1534 :
1535 35 : exporters: [
1536 35 : null,
1537 :
1538 : // Sets: type_Interface.Device
1539 : //
1540 35 : function (obj) {
1541 35 : obj.Devices.forEach(function (dev) {
1542 35 : if (dev.Interface) {
1543 35 : const iface = get_interface(dev.Interface);
1544 35 : iface.Device = dev;
1545 35 : }
1546 35 : });
1547 35 : }
1548 35 : ]
1549 35 : };
1550 :
1551 : /* Now create the cyclic declarations.
1552 : */
1553 35 : type_ActiveConnection.props.Group = { conv: conv_Object(type_Device) };
1554 35 : type_Device.props.Members = { conv: conv_Array(conv_Object(type_Device)), def: [] };
1555 :
1556 : /* Accessing the model.
1557 : */
1558 :
1559 35 : self.list_interfaces = function list_interfaces() {
1560 35 : const result = [];
1561 35 : for (const path in objects) {
1562 35 : const obj = objects[path];
1563 35 : if (priv(obj).type === type_Interface)
1564 35 : result.push(obj);
1565 35 : }
1566 35 : return result.sort(function (a, b) { return a.Name.localeCompare(b.Name) });
1567 35 : };
1568 :
1569 35 : self.find_interface = peek_interface;
1570 :
1571 33 : self.get_manager = function () {
1572 33 : return get_object("/org/freedesktop/NetworkManager",
1573 33 : type_Manager);
1574 33 : };
1575 :
1576 14 : self.get_settings = function () {
1577 14 : return get_object("/org/freedesktop/NetworkManager/Settings",
1578 14 : type_Settings);
1579 14 : };
1580 :
1581 : /* Initialization.
1582 : */
1583 :
1584 35 : set_object_types([type_Manager,
1585 35 : type_Settings,
1586 35 : type_Device,
1587 35 : type_Ipv4Config,
1588 35 : type_Ipv6Config,
1589 35 : type_Connection,
1590 35 : type_ActiveConnection,
1591 35 : type_AccessPoint
1592 35 : ]);
1593 :
1594 35 : get_object("/org/freedesktop/NetworkManager", type_Manager);
1595 35 : get_object("/org/freedesktop/NetworkManager/Settings", type_Settings);
1596 :
1597 35 : self.ready = undefined;
1598 35 : self.operationInProgress = undefined;
1599 35 : self.curtain = undefined;
1600 35 : return self;
1601 35 : }
1602 :
1603 34 : export function syn_click(model, fun) {
1604 23 : return function() {
1605 23 : const self = this;
1606 23 : const self_args = arguments;
1607 23 : return model.synchronize().then(function() {
1608 23 : fun.apply(self, self_args);
1609 23 : });
1610 23 : };
1611 34 : }
1612 :
1613 35 : export function is_managed(dev) {
1614 : // Never let the user manage loopback devices, nothing good can come from that.
1615 34 : return dev.State != 10 && dev.DeviceType != "loopback" && dev.Interface != "lo";
1616 35 : }
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 33 : export function device_state_text(dev) {
1626 33 : if (!dev)
1627 9 : return _("Inactive");
1628 6 : if (dev.State == 100 && dev.Carrier === false)
1629 6 : return _("No carrier");
1630 9 : if (!is_managed(dev)) {
1631 9 : if (!dev.ActiveConnection &&
1632 9 : (!dev.Ip4Config || dev.Ip4Config.AddressData.length === 0) &&
1633 9 : (!dev.Ip6Config || dev.Ip6Config.AddressData.length === 0))
1634 9 : return _("Inactive");
1635 9 : }
1636 33 : return dev.StateText;
1637 33 : }
1638 :
1639 2 : export function array_join(elts, sep) {
1640 2 : const result = [];
1641 2 : for (let i = 0; i < elts.length; i++) {
1642 2 : result.push(elts[i]);
1643 2 : if (i < elts.length - 1)
1644 0 : result.push(sep);
1645 2 : }
1646 2 : return result;
1647 2 : }
1648 :
1649 35 : export function render_active_connection(dev, with_link, hide_link_local) {
1650 35 : const parts = [];
1651 :
1652 35 : if (!dev)
1653 7 : return "";
1654 :
1655 35 : const con = dev.ActiveConnection;
1656 :
1657 6 : if (con && con.Group) {
1658 6 : return fmt_to_fragments(_("Part of $0"), with_link ? render_interface_link(con.Group.Interface) : con.Group.Interface);
1659 6 : }
1660 :
1661 33 : const ip4config = con ? con.Ip4Config : dev.Ip4Config;
1662 35 : if (ip4config) {
1663 35 : ip4config.AddressData.forEach(function (a) {
1664 35 : parts.push(a.address + "/" + a.prefix);
1665 35 : });
1666 35 : }
1667 :
1668 34 : function is_ipv6_link_local(addr) {
1669 34 : return (addr.indexOf("fe8") === 0 ||
1670 34 : addr.indexOf("fe9") === 0 ||
1671 34 : addr.indexOf("fea") === 0 ||
1672 34 : addr.indexOf("feb") === 0);
1673 34 : }
1674 :
1675 33 : const ip6config = con ? con.Ip6Config : dev.Ip6Config;
1676 35 : if (ip6config) {
1677 35 : ip6config.AddressData.forEach(function (a) {
1678 34 : if (!(hide_link_local && is_ipv6_link_local(a.address)))
1679 35 : parts.push(a.address + "/" + a.prefix);
1680 35 : });
1681 35 : }
1682 :
1683 35 : return parts.join(", ");
1684 35 : }
1685 :
1686 : /* Resource usage monitoring
1687 : */
1688 :
1689 3 : 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 3 : settings.connection.id = device.Interface;
1696 3 : settings.connection.uuid = uuidv4();
1697 :
1698 1 : if (device.DeviceType == 'ethernet') {
1699 1 : settings.connection.type = '802-3-ethernet';
1700 1 : settings.ethernet = { };
1701 0 : } else {
1702 : // The remaining types are identical between Device and Settings, see
1703 : // device_type_to_symbol.
1704 2 : settings.connection.type = device.DeviceType;
1705 2 : }
1706 3 : }
1707 :
1708 21 : 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 21 : const specialCon = utils.isNonPersistentMultiCon(connection);
1726 :
1727 21 : return function (settings) {
1728 19 : if (connection && !specialCon) {
1729 19 : return connection.apply_settings(settings);
1730 12 : } else if (settings.connection.type && !specialCon) {
1731 14 : return model.get_settings().add_connection(settings);
1732 1 : } else if (device && specialCon) {
1733 2 : const newSettings = utils.createNewConnSettings(settings, device.Interface);
1734 2 : return device.activate_with_settings(newSettings);
1735 1 : } else if (device) {
1736 1 : return device.activate_with_settings(settings);
1737 1 : } else {
1738 1 : console.warn("No way to apply settings", connection, settings);
1739 1 : return Promise.resolve();
1740 1 : }
1741 21 : };
1742 21 : }
1743 :
1744 28 : export function choice_title(choices, choice, def) {
1745 28 : for (let i = 0; i < choices.length; i++) {
1746 28 : if (choices[i].choice == choice)
1747 28 : return choices[i].title;
1748 28 : }
1749 2 : return def;
1750 28 : }
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 35 : const curtain_time = 1.5;
1840 35 : let settle_time = 1.0;
1841 35 : const rollback_time = 7.0;
1842 :
1843 26 : export function with_checkpoint(model, modify, options) {
1844 26 : const manager = model.get_manager();
1845 :
1846 26 : let curtain_timeout;
1847 26 : let curtain_title_timeout;
1848 :
1849 6 : function show_curtain() {
1850 6 : cockpit.hint("ignore_transport_health_check", { data: true });
1851 3 : curtain_timeout = window.setTimeout(function () {
1852 3 : curtain_timeout = null;
1853 3 : model.set_curtain('testing');
1854 3 : }, curtain_time * 1000);
1855 3 : curtain_title_timeout = window.setTimeout(function () {
1856 3 : curtain_title_timeout = null;
1857 3 : model.set_curtain('restoring');
1858 3 : }, rollback_time * 1000);
1859 6 : }
1860 :
1861 6 : function hide_curtain() {
1862 6 : if (curtain_timeout)
1863 3 : window.clearTimeout(curtain_timeout);
1864 6 : curtain_timeout = null;
1865 6 : if (curtain_title_timeout)
1866 3 : window.clearTimeout(curtain_title_timeout);
1867 6 : cockpit.hint("ignore_transport_health_check", { data: false });
1868 :
1869 6 : model.set_curtain(undefined);
1870 6 : }
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 14 : if (options.hack_does_add_or_remove || window.cockpit_tests_disable_checkpoints) {
1882 21 : modify();
1883 21 : return;
1884 21 : }
1885 :
1886 8 : if (window.cockpit_tests_checkpoints_settle_time)
1887 3 : settle_time = window.cockpit_tests_checkpoints_settle_time;
1888 :
1889 3 : manager.checkpoint_create(options.devices || [], rollback_time)
1890 6 : .then(function (cp) {
1891 1 : if (!cp) {
1892 1 : modify();
1893 1 : return;
1894 1 : }
1895 :
1896 : // Signal that a checkpoint is active for anaconda-webui
1897 6 : window.sessionStorage.setItem("cockpit_has_checkpoint", "true");
1898 :
1899 6 : show_curtain();
1900 6 : modify()
1901 6 : .then(function () {
1902 6 : window.setTimeout(function () {
1903 6 : manager.checkpoint_destroy(cp)
1904 3 : .catch(function () {
1905 3 : show_breaking_change_dialog({
1906 3 : ...options,
1907 3 : action: syn_click(model, modify)
1908 3 : });
1909 3 : })
1910 6 : .finally(function() {
1911 6 : hide_curtain();
1912 :
1913 : // Clear checkpoint status when done
1914 6 : window.sessionStorage.setItem("cockpit_has_checkpoint", "false");
1915 6 : });
1916 6 : }, settle_time * 1000);
1917 6 : })
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 6 : });
1940 26 : }
1941 :
1942 14 : export function with_settings_checkpoint(model, modify, options) {
1943 14 : with_checkpoint(model, modify,
1944 14 : {
1945 14 : ...options,
1946 14 : fail_text: _("Changing the settings will break the connection to the server, and will make the administration UI unavailable."),
1947 14 : anyway_text: _("Change the settings"),
1948 14 : });
1949 14 : }
1950 :
1951 14 : export function connection_devices(con) {
1952 14 : const devices = [];
1953 :
1954 14 : if (con)
1955 14 : con.Interfaces.forEach(function (iface) { if (iface.Device) devices.push(iface.Device); });
1956 :
1957 14 : return devices;
1958 14 : }
1959 :
1960 11 : export function is_interface_connection(iface, connection) {
1961 3 : return connection && connection.Interfaces.indexOf(iface) != -1;
1962 11 : }
1963 :
1964 15 : export function is_interesting_interface(iface) {
1965 15 : return !iface.Device || is_managed(iface.Device);
1966 15 : }
1967 :
1968 10 : export function member_connection_for_interface(group, iface) {
1969 3 : return group?.Members.find(s => is_interface_connection(iface, s));
1970 10 : }
1971 :
1972 10 : export function member_interface_choices(model, group) {
1973 10 : return model.list_interfaces().filter(function (iface) {
1974 10 : return !is_interface_connection(iface, group) && is_interesting_interface(iface);
1975 10 : });
1976 10 : }
1977 :
1978 4 : export function free_member_connection(con) {
1979 4 : const cs = connection_settings(con);
1980 4 : if (cs.member_type) {
1981 4 : delete cs.member_type;
1982 4 : delete cs.group;
1983 4 : delete con.Settings.team_port;
1984 4 : delete con.Settings.bridge_port;
1985 4 : return con.apply_settings(con.Settings).then(() => { con.activate(null, null) });
1986 4 : }
1987 4 : }
1988 :
1989 10 : export function set_member(model, group_connection, group_settings, member_type,
1990 10 : iface_name, val) {
1991 10 : const iface = model.find_interface(iface_name);
1992 10 : if (!iface)
1993 0 : return false;
1994 :
1995 10 : const main_connection = iface.MainConnection;
1996 :
1997 10 : if (val) {
1998 : /* Turn the main_connection into a member for group.
1999 : */
2000 :
2001 10 : const group_iface = group_settings.connection.interface_name;
2002 10 : if (!group_iface)
2003 0 : return false;
2004 :
2005 10 : let member_settings;
2006 10 : if (main_connection) {
2007 10 : member_settings = main_connection.Settings;
2008 :
2009 10 : if (member_settings.connection.group == group_settings.connection.uuid ||
2010 10 : member_settings.connection.group == group_settings.connection.id ||
2011 10 : member_settings.connection.group == group_iface)
2012 1 : return Promise.resolve();
2013 :
2014 10 : member_settings.connection.member_type = member_type;
2015 10 : member_settings.connection.group = group_iface;
2016 10 : member_settings.connection.autoconnect = true;
2017 10 : delete member_settings.ipv4;
2018 10 : delete member_settings.ipv6;
2019 10 : delete member_settings.team_port;
2020 10 : 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 10 : 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 10 : });
2048 10 : } 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 10 : }
2056 :
2057 10 : return true;
2058 10 : }
2059 :
2060 10 : export function apply_group_member(choices, model, apply_group, group_connection, group_settings, member_type) {
2061 10 : const active_settings = [];
2062 :
2063 10 : if (!group_connection) {
2064 10 : if (group_settings.bond &&
2065 7 : group_settings.bond.options &&
2066 1 : group_settings.bond.options.primary) {
2067 1 : const iface = model.find_interface(group_settings.bond.options.primary);
2068 1 : if (iface && iface.MainConnection)
2069 1 : active_settings.push(iface.MainConnection.Settings);
2070 0 : } else {
2071 9 : Object.keys(choices)
2072 9 : .filter(choice => choices[choice])
2073 9 : .forEach(choice => {
2074 9 : const iface = model.find_interface(choice);
2075 9 : if (iface && iface.Device && iface.Device.ActiveConnection && iface.Device.ActiveConnection.Connection) {
2076 9 : active_settings.push(iface.Device.ActiveConnection.Connection.Settings);
2077 9 : }
2078 9 : });
2079 9 : }
2080 :
2081 8 : if (active_settings.length == 1) {
2082 8 : group_settings.ipv4 = JSON.parse(JSON.stringify(active_settings[0].ipv4));
2083 8 : group_settings.ipv6 = JSON.parse(JSON.stringify(active_settings[0].ipv6));
2084 8 : }
2085 :
2086 10 : group_settings.connection.autoconnect_members = 1;
2087 10 : }
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 10 : function set_all_members() {
2096 10 : const deferreds = Object.keys(choices).map(iface => {
2097 10 : return model.synchronize().then(function () {
2098 10 : return set_member(model, group_connection, group_settings, member_type,
2099 10 : iface, choices[iface]);
2100 10 : });
2101 10 : });
2102 10 : return Promise.all(deferreds);
2103 10 : }
2104 :
2105 10 : return set_all_members().then(function () {
2106 10 : return apply_group(group_settings);
2107 10 : });
2108 10 : }
2109 :
2110 35 : export function init() {
2111 35 : cockpit.translate();
2112 35 : }
|