LCOV - code coverage report
Current view: top level - pkg/networkmanager - interfaces.js Coverage Total Hit
Test: cockpit Lines: 35.9 % 1492 535
Test Date: 2026-06-16 14:09:37

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

Generated by: LCOV version 2.0-1