LCOV - code coverage report
Current view: top level - pkg/networkmanager - interfaces.js Coverage Total Hit
Test: cockpit Lines: 93.8 % 1490 1398
Test Date: 2026-07-17 14:32:59

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

Generated by: LCOV version 2.0-1