LCOV - code coverage report
Current view: top level - pkg/networkmanager - interfaces.js Coverage Total Hit
Test: cockpit Lines: 85.9 % 1492 1282
Test Date: 2026-06-25 09:20:42

            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 : }
        

Generated by: LCOV version 2.0-1