🏡 index : quine-global-labs/mirror-cockpit-project-cockpit.git

/*
 * Copyright (C) 2013 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */
import React from "react";
import cockpit from 'cockpit';

import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";

import { fmt_to_fragments } from 'utils.jsx';
import * as utils from './utils.js';
import { v4 as uuidv4 } from 'uuid';

import "./networking.scss";

import { show_modal_dialog } from "cockpit-components-dialog.jsx";

const _ = cockpit.gettext;

export function show_error_dialog(title, message) {
    const props = {
        id: "error-popup",
        title,
        body: <p>{message}</p>
    };

    const footer = {
        actions: [],
        cancel_button: { text: _("Close"), variant: "secondary" }
    };

    show_modal_dialog(props, footer);
}

export function show_unexpected_error(error) {
    show_error_dialog(_("Unexpected error"), error.message || error);
}

function show_breaking_change_dialog({ fail_text, anyway_text, action }) {
    const props = {
        titleIconVariant: "warning",
        id: "confirm-breaking-change-popup",
        title: _("Connection will be lost"),
        body: <p>{fail_text}</p>
    };

    const footer = {
        actions: [
            {
                caption: anyway_text,
                clicked: action,
                style: "danger",
            }
        ],
        cancel_button: { text: _("Keep connection"), variant: "secondary" }
    };

    show_modal_dialog(props, footer);
}

export function connection_settings(c) {
    if (c && c.Settings && c.Settings.connection) {
        return c.Settings.connection;
    } else {
        // It is a programming error if we ever access a Connection
        // object that doesn't have it's settings yet, and we expect
        // each Connection object to have "connection" settings.
        console.warn("Incomplete 'Connection' object accessed", c);
        return { };
    }
}

/* NetworkManagerModel
 *
 * The NetworkManager model maintains a mostly-read-only data
 * structure that represents the state of the NetworkManager service
 * on a given machine.
 *
 * The data structure consists of JavaScript values such as objects,
 * arrays, and strings that point at each other.  It might have
 * cycles.  In general, it follows the NetworkManager D-Bus API but
 * tries to hide annoyances such as endian issues.
 *
 * For example,
 *
 *    const manager = model.get_manager();
 *    manager.Devices[0].ActiveConnection.Ipv4Config.Addresses[0][0]
 *
 * is the first IPv4 address of the first device as a string.
 *
 * The model initializes itself asynchronously and emits the 'changed'
 * event whenever anything changes.  If you only access the data
 * structure from within the 'changed' event handler, you should
 * always see it in a complete state.
 *
 * In other words, any change in the data structure from one 'changed'
 * event to the next represents a real change in the state of
 * NetworkManager.
 *
 * When a new model is created, its main 'manager' object starts out
 * as 'null'.  The first 'changed' event signals that initialization
 * is complete and that the whole data structure is now stable and
 * reachable from the 'manager' object.
 *
 * Methods are invoked directly on the objects in the data structure.
 * For example,
 *
 *    manager.Devices[0].disconnect();
 *    manager.Devices[0].ActiveConnection.deactivate();
 *
 * TODO - document the details of the data structure.
 */

/* HACK
 *
 * NetworkManager used to not implement the standard o.fd.DBus.Properties
 * interface and our code still operates under the assumptions stated below.
 *
 * 1) NM does not emit the PropertiesChanged signal on the
 *    o.fd.DBus.Properties interface but rather on its own interfaces
 *    like o.fd.NetworkManager.Device.Wired.
 *
 * 2) NM does not always emit the PropertiesChanged signal on the
 *    interface whose properties have changed.  For example, when a
 *    property on o.fd.NM.Device changes, this might be notified by a
 *    PropertiesChanged signal on the o.fd.NM.Device.Wired interface
 *    for the same object path.
 *
 * https://bugzilla.gnome.org/show_bug.cgi?id=729826
 *
 * We cope with this here by merging all properties of all interfaces
 * for a given object path.  This is appropriate and nice for
 * NetworkManager, and we should probably keep it that way even if
 * NetworkManager would use a standard o.fd.DBus.Properties API.
 * In the future this could be rewritten to use DBusProxies.
 */

export function NetworkManagerModel() {
    /*
     * The NetworkManager model doesn't need proxies in its DBus client.
     * It uses the 'raw' dbus events and methods and constructs its own data
     * structure.  This has the advantage of avoiding wasting
     * resources for maintaining the unused proxies, avoids some code
     * complexity, and allows to do the right thing with the
     * peculiarities of the NetworkManager API.
     */

    const self = this;
    cockpit.event_target(self);

    const client = cockpit.dbus("org.freedesktop.NetworkManager", { superuser: "try" });
    self.client = client;

    // set to false by default as newer API is backwards compatible
    // and supports both "dns" and "dns-data" properties
    // dns-data property was added in version 1.42.0
    self.supports_dns_data = false;

    function check_version_dns_data_support(version) {
        if (version.length < 2) {
            return false;
        }

        if (version[0] > 1) {
            return true;
        } else if (version[0] === 1 && version[1] >= 42) {
            return true;
        }

        return false;
    }

    /* resolved once first stage of initialization is done */
    self.preinit = new Promise((resolve, reject) => {
        client.call("/org/freedesktop/NetworkManager",
                    "org.freedesktop.DBus.Properties", "Get",
                    ["org.freedesktop.NetworkManager", "Version"], { flags: "" })
                .then((reply, options) => {
                    const nm_version = reply[0].v.split(".").map(n => Number.parseInt(n));
                    self.supports_dns_data = check_version_dns_data_support(nm_version);

                    if (options.flags) {
                        if (options.flags.indexOf(">") !== -1)
                            utils.set_byteorder("be");
                        else if (options.flags.indexOf("<") !== -1)
                            utils.set_byteorder("le");
                        resolve();
                    }
                })
                .catch(complain);
    });

    /* Mostly generic D-Bus stuff.  */

    const objects = { };

    self.set_curtain = (state) => {
        self.curtain = state;
        self.dispatchEvent("changed");
    };

    /* This is a test helper so that we wait for operations to finish before moving forward with the test */
    self.set_operation_in_progress = (value) => {
        self.operationInProgress = value;
        self.dispatchEvent("changed");
    };

    function complain() {
        self.ready = false;
        console.warn.apply(console, arguments);
    }

    function conv_Object(type) {
        return function (path) {
            return get_object(path, type);
        };
    }

    function conv_Array(conv) {
        return function (elts) {
            return elts.map(conv);
        };
    }

    function priv(obj) {
        return obj[' priv'];
    }

    let outstanding_refreshes = 0;

    function push_refresh() {
        outstanding_refreshes += 1;
    }

    function pop_refresh() {
        outstanding_refreshes -= 1;
        if (outstanding_refreshes === 0)
            export_model();
    }

    function get_object(path, type) {
        if (path === "/")
            return null;
        function Constructor() {
            this[' priv'] = { };
            priv(this).type = type;
            priv(this).path = path;
            for (const p in type.props)
                this[p] = type.props[p].def;
        }
        if (!objects[path]) {
            Constructor.prototype = type.prototype;
            objects[path] = new Constructor();
            if (type.refresh)
                type.refresh(objects[path]);
            if (type.exporters && type.exporters[0])
                type.exporters[0](objects[path]);
        }
        return objects[path];
    }

    function peek_object(path) {
        return objects[path] || null;
    }

    function drop_object(path) {
        const obj = objects[path];
        if (obj) {
            if (priv(obj).type.drop)
                priv(obj).type.drop(obj);
            delete objects[path];
            export_model();
        }
    }

    function set_object_properties(obj, props) {
        const decl = priv(obj).type.props;
        for (const p in decl) {
            let val = props[decl[p].prop || p];
            if (val !== undefined) {
                if (decl[p].conv)
                    val = decl[p].conv(val);
                if (val !== obj[p]) {
                    obj[p] = val;
                    if (decl[p].trigger)
                        decl[p].trigger(obj);
                }
            }
        }
    }

    function remove_signatures(props_with_sigs) {
        const props = { };
        for (const p in props_with_sigs) {
            if (props_with_sigs[p]) {
                props[p] = props_with_sigs[p].v;
            }
        }
        return props;
    }

    function objpath(obj) {
        if (obj && priv(obj).path)
            return priv(obj).path;
        else
            return "/";
    }

    function call_object_method(obj, iface, method) {
        return client.call(objpath(obj), iface, method, Array.prototype.slice.call(arguments, 3));
    }

    const interface_types = { };
    let max_export_phases = 0;
    let export_pending;

    function set_object_types(all_types) {
        all_types.forEach(function (type) {
            if (type.exporters && type.exporters.length > max_export_phases)
                max_export_phases = type.exporters.length;
            type.interfaces.forEach(function (iface) {
                interface_types[iface] = type;
            });
        });
    }

    function signal_emitted(path, iface, signal, args) {
        const obj = peek_object(path);

        if (obj) {
            const type = priv(obj).type;

            if (signal === "PropertiesChanged") {
                push_refresh();
                const props = remove_signatures(args[0]);
                set_object_properties(obj, props);
                pop_refresh();
            } else if (type.signals && type.signals[signal])
                type.signals[signal](obj, args);
        }
    }

    function interface_properties(path, iface, props) {
        const type = interface_types[iface];
        if (type)
            set_object_properties(get_object(path, type), props);
    }

    function interface_removed(path, iface) {
        /* For NetworkManager we can make this assumption */
        drop_object(path);
    }

    let export_model_promise = null;
    let export_model_promise_resolve = null;

    function export_model() {
        function doit() {
            for (let phase = 0; phase < max_export_phases; phase++) {
                for (const path in objects) {
                    const obj = objects[path];
                    const exp = priv(obj).type.exporters;
                    if (exp && exp[phase])
                        exp[phase](obj);
                }
            }

            self.ready = true;
            self.dispatchEvent('changed');
            if (export_model_promise) {
                export_model_promise_resolve();
                export_model_promise = null;
                export_model_promise_resolve = null;
            }
        }

        if (!export_pending) {
            export_pending = true;
            window.setTimeout(function () { export_pending = false; doit() }, 300);
        }
    }

    self.synchronize = function synchronize() {
        if (outstanding_refreshes === 0) {
            return Promise.resolve();
        } else {
            if (!export_model_promise)
                export_model_promise = new Promise(resolve => { export_model_promise_resolve = resolve });
            return export_model_promise;
        }
    };

    let subscription;
    let watch;

    function onNotifyEventHandler(event, data) {
        Object.keys(data).forEach(path => {
            const interfaces = data[path];

            Object.keys(interfaces).forEach(iface => {
                const props = interfaces[iface];

                /* Capture connection failure reasons for devices
                   NM transitions through PREPARE → CONFIG → NEED_AUTH → FAILED → DISCONNECTED very
                   quickly, and React batches these updates, so the UI often misses the FAILED state.
                   Store the failure reason here at the D-Bus event level so we can retrieve it later. */
                if (path.includes("/Devices/") && props?.StateReason) {
                    const obj = peek_object(path);
                    if (obj) {
                        const [state, reason] = props.StateReason;
                        if (state === 120 && reason !== 0) {
                            utils.debug("Captured", obj.Interface, "failure, reason:", reason);
                            priv(obj).lastFailureReason = reason;
                        }
                    }
                }

                if (props)
                    interface_properties(path, iface, props);
                else
                    interface_removed(path, iface);
            });
        });
    }

    self.preinit.then(() => {
        subscription = client.subscribe({ }, signal_emitted);
        client.addEventListener("notify", onNotifyEventHandler);
        watch = client.watch({ path_namespace: "/org/freedesktop" });
        client.addEventListener("owner", (event, owner) => {
            if (owner) {
                watch.remove();
                watch = client.watch({ path_namespace: "/org/freedesktop" });
            }
        });
    });

    self.close = function close() {
        subscription.remove();
        watch.remove();
        client.removeEventListener("notify", onNotifyEventHandler);
        client.close("unused");
    };

    /* NetworkManager specific data conversions and utility functions.
     */

    function ip_address_from_nm(addr) {
        return {
            address: addr.address.v,
            prefix: utils.ip_prefix_to_text(addr.prefix.v)
        };
    }

    function ip_address_to_nm(addr, ipv) {
        const prefix = ipv === "ipv4" ? utils.ip4_prefix_from_text(addr.prefix) : utils.ip_prefix_from_text(addr.prefix);

        if (!utils.validate_ip(addr.address)) {
            throw cockpit.format(_("Invalid IP address: $0"), addr.address);
        }

        return {
            address: { t: "s", v: addr.address },
            prefix: { t: "u", v: prefix },
        };
    }

    function route_from_nm(route) {
        const metric = route.metric ? utils.ip_metric_to_text(route.metric.v) : "";
        return {
            dest: route.dest.v,
            prefix: utils.ip_prefix_to_text(route.prefix.v),
            next_hop: route["next-hop"]?.v ?? "",
            metric,
        };
    }

    function route_to_nm(route, ipv) {
        const prefix = ipv === "ipv4" ? utils.ip4_prefix_from_text(route.prefix) : utils.ip_prefix_from_text(route.prefix);

        if (!utils.validate_ip(route.dest)) {
            throw cockpit.format(_("Invalid destination address: $0"), route.dest);
        }

        const route_nm = {
            dest: { t: "s", v: route.dest },
            prefix: { t: "u", v: prefix },
        };

        // next-hop is an optional property
        if (route.next_hop !== "") {
            if (!utils.validate_ip(route.next_hop)) {
                throw cockpit.format(_("Invalid gateway address: $0"), route.next_hop);
            }

            route_nm["next-hop"] = { t: "s", v: route.next_hop };
        }

        // metric is an optional property
        if (route.metric !== "") {
            route_nm.metric = { t: "u", v: utils.ip_metric_from_text(route.metric) };
        }

        return route_nm;
    }

    function settings_from_nm(settings) {
        function get(first, second, def) {
            if (settings[first] && settings[first][second])
                return settings[first][second].v;
            else
                return def;
        }

        function get_ip(first, ip_to_text) {
            const dns_data = self.supports_dns_data
                ? get(first, "dns-data", [])
                : get(first, "dns", []).map(ip_to_text);

            return {
                method: get(first, "method", "auto"),
                ignore_auto_dns: get(first, "ignore-auto-dns", false),
                ignore_auto_routes: get(first, "ignore-auto-routes", false),
                address_data: get(first, "address-data", []).map(ip_address_from_nm),
                gateway: get(first, "gateway", ""),
                dns_data,
                dns_search: get(first, "dns-search", []),
                route_data: get(first, "route-data", []).map(route_from_nm),
            };
        }

        const result = {
            connection: {
                type: get("connection", "type"),
                uuid: get("connection", "uuid"),
                interface_name: get("connection", "interface-name"),
                timestamp: get("connection", "timestamp", 0),
                id: get("connection", "id", _("Unknown")),
                autoconnect: get("connection", "autoconnect", true),
                autoconnect_priority: get("connection", "autoconnect-priority", 0),
                autoconnect_members:
                                get("connection", "autoconnect-slaves", -1),
                member_type: get("connection", "slave-type"),
                group: get("connection", "master"),
                multi_connect: get("connection", "multi-connect"),
            }
        };

        if (!settings.connection.master) {
            result.ipv4 = get_ip("ipv4", utils.ip4_to_text);
            result.ipv6 = get_ip("ipv6", utils.ip6_to_text);
        }

        if (settings["802-3-ethernet"]) {
            result.ethernet = {
                mtu: get("802-3-ethernet", "mtu"),
                assigned_mac_address: get("802-3-ethernet", "assigned-mac-address")
            };
        }

        if (settings.bond) {
            /* Options are documented as part of the Linux bonding driver.
               https://www.kernel.org/doc/Documentation/networking/bonding.txt
            */
            result.bond = {
                options: { ...get("bond", "options", { }) },
                interface_name: get("bond", "interface-name")
            };
        }

        function JSON_parse_carefully(str) {
            try {
                return JSON.parse(str);
            } catch (e) {
                return null;
            }
        }

        if (settings.team) {
            result.team = {
                config: JSON_parse_carefully(get("team", "config", "{}")),
                interface_name: get("team", "interface-name")
            };
        }

        if (settings["team-port"] || result.connection.member_type === "team") {
            result.team_port = { config: JSON_parse_carefully(get("team-port", "config", "{}")), };
        }

        if (settings.bridge) {
            result.bridge = {
                interface_name: get("bridge", "interface-name"),
                stp: get("bridge", "stp", true),
                priority: get("bridge", "priority", 32768),
                forward_delay: get("bridge", "forward-delay", 15),
                hello_time: get("bridge", "hello-time", 2),
                max_age: get("bridge", "max-age", 20),
                ageing_time: get("bridge", "ageing-time", 300)
            };
        }

        if (settings["bridge-port"] || result.connection.member_type === "bridge") {
            result.bridge_port = {
                priority: get("bridge-port", "priority", 32),
                path_cost: get("bridge-port", "path-cost", 100),
                hairpin_mode: get("bridge-port", "hairpin-mode", false)
            };
        }

        if (settings.vlan) {
            result.vlan = {
                parent: get("vlan", "parent"),
                id: get("vlan", "id"),
                interface_name: get("vlan", "interface-name")
            };
        }

        if (settings.wireguard) {
            result.wireguard = {
                listen_port: get("wireguard", "listen-port", 0),
                peers: get("wireguard", "peers", []).map(peer => ({
                    publicKey: peer['public-key'].v,
                    endpoint: peer.endpoint?.v, // endpoint of a peer is optional
                    allowedIps: peer['allowed-ips']?.v
                })),
            };
        }

        if (settings["802-11-wireless"]) {
            result["802-11-wireless"] = {
                ssid: get("802-11-wireless", "ssid"),
                mode: get("802-11-wireless", "mode"),
            };
        }

        return result;
    }

    function settings_to_nm(settings, orig) {
        const result = JSON.parse(JSON.stringify(orig || { }));

        function set(first, second, sig, val, def) {
            if (val === undefined)
                val = def;
            if (!result[first])
                result[first] = { };
            if (val !== undefined)
                result[first][second] = cockpit.variant(sig, val);
            else
                delete result[first][second];
        }

        function set_ip(first, dns_ip_sig, ip_from_text) {
            set(first, "method", 's', settings[first].method);
            set(first, "ignore-auto-dns", 'b', settings[first].ignore_auto_dns);
            set(first, "ignore-auto-routes", 'b', settings[first].ignore_auto_routes);
            set(first, "addr-gen-mode", 'i', settings[first].addr_gen_mode);

            const addresses = settings[first].address_data;
            if (addresses)
                set(first, "address-data", "aa{sv}", addresses.map(addr => ip_address_to_nm(addr, first)));

            const gateway = settings[first].gateway;
            if (gateway && addresses.length > 0) {
                if (!utils.validate_ip(gateway)) {
                    throw cockpit.format(_("Invalid gateway address: $0"), gateway);
                }
                set(first, "gateway", "s", gateway);
            } else {
                // gateway cannot be set if there are no addresses
                delete result[first].gateway;
            }

            const dns = settings[first].dns_data;
            if (dns) {
                const invalid = dns.find(addr => !utils.validate_ip(addr));
                if (invalid) {
                    throw cockpit.format(_("Invalid DNS address: $0"), invalid);
                }

                if (self.supports_dns_data) {
                    set(first, "dns-data", "as", dns);
                } else {
                    set(first, "dns", dns_ip_sig, dns.map(addr => ip_from_text(addr)));
                }
            }

            set(first, "dns-search", 'as', settings[first].dns_search);

            const routes = settings[first].route_data;
            if (routes)
                set(first, "route-data", "aa{sv}", routes.map(route => route_to_nm(route, first)));

            // Never pass "address-labels" back to NetworkManager.  It
            // is documented as "internal only", but needs to somehow
            // stay in sync with "addresses".  By not passing it back
            // we don't have to worry about that.
            //
            delete result[first]["address-labels"];

            // Never pass "addresses", instead use "address-data" + "gateway"
            delete result[first].addresses;
            // Never pass "routes", instead use "route-data"
            delete result[first].routes;
            // Never pass "dns" if "dns-data" is supported
            if (self.supports_dns_data)
                delete result[first].dns;
        }

        set("connection", "id", 's', settings.connection.id);
        set("connection", "autoconnect", 'b', settings.connection.autoconnect);
        set("connection", "autoconnect-priority", 'i', settings.connection.autoconnect_priority);
        set("connection", "autoconnect-slaves", 'i', settings.connection.autoconnect_members);
        set("connection", "uuid", 's', settings.connection.uuid);
        set("connection", "interface-name", 's', settings.connection.interface_name);
        set("connection", "type", 's', settings.connection.type);
        set("connection", "slave-type", 's', settings.connection.member_type);
        set("connection", "master", 's', settings.connection.group);
        set("connection", "multi-connect", 'i', settings.connection.multi_connect);

        if (settings.ipv4)
            set_ip("ipv4", 'au', utils.ip4_from_text);
        else
            delete result.ipv4;

        if (settings.ipv6)
            set_ip("ipv6", 'aay', utils.ip6_from_text);
        else
            delete result.ipv6;

        if (settings.bond) {
            set("bond", "options", 'a{ss}', settings.bond.options);
            set("bond", "interface-name", 's', settings.bond.interface_name);
        } else
            delete result.bond;

        if (settings.team) {
            set("team", "config", 's', JSON.stringify(settings.team.config));
            set("team", "interface-name", 's', settings.team.interface_name);
        } else
            delete result.team;

        if (settings.team_port)
            set("team-port", "config", 's', JSON.stringify(settings.team_port.config));
        else
            delete result["team-port"];

        if (settings.bridge) {
            set("bridge", "interface-name", 's', settings.bridge.interface_name);
            set("bridge", "stp", 'b', settings.bridge.stp);
            set("bridge", "priority", 'u', settings.bridge.priority);
            set("bridge", "forward-delay", 'u', settings.bridge.forward_delay);
            set("bridge", "hello-time", 'u', settings.bridge.hello_time);
            set("bridge", "max-age", 'u', settings.bridge.max_age);
            set("bridge", "ageing-time", 'u', settings.bridge.ageing_time);
        } else
            delete result.bridge;

        if (settings.bridge_port) {
            set("bridge-port", "priority", 'u', settings.bridge_port.priority);
            set("bridge-port", "path-cost", 'u', settings.bridge_port.path_cost);
            set("bridge-port", "hairpin-mode", 'b', settings.bridge_port.hairpin_mode);
        } else
            delete result["bridge-port"];

        if (settings.vlan) {
            set("vlan", "parent", 's', settings.vlan.parent);
            set("vlan", "id", 'u', settings.vlan.id);
            set("vlan", "interface-name", 's', settings.vlan.interface_name);
            // '1' is the default, but we need to set it explicitly anyway.
            set("vlan", "flags", 'u', 1);
        } else
            delete result.vlan;

        if (settings.ethernet) {
            set("802-3-ethernet", "mtu", 'u', settings.ethernet.mtu);
            set("802-3-ethernet", "assigned-mac-address", 's', settings.ethernet.assigned_mac_address);
            // Delete cloned-mac-address so that assigned-mac-address gets used.
            delete result["802-3-ethernet"]["cloned-mac-address"];
        } else
            delete result["802-3-ethernet"];

        if (settings.wireguard) {
            set("wireguard", "private-key", "s", settings.wireguard.private_key);
            set("wireguard", "listen-port", "u", settings.wireguard.listen_port);
            set("wireguard", "peers", "aa{sv}", settings.wireguard.peers.map(peer => {
                return {
                    "public-key": {
                        t: "s",
                        v: peer.publicKey
                    },
                    ...peer.endpoint
                        ? {
                            endpoint: {
                                t: "s",
                                v: peer.endpoint
                            }
                        }
                        : {},
                    "allowed-ips": {
                        t: "as",
                        v: peer.allowedIps
                    }
                };
            }));
        } else {
            delete result.wireguard;
        }

        if (settings["802-11-wireless"]) {
            set("802-11-wireless", "ssid", 'ay', settings["802-11-wireless"].ssid);
            set("802-11-wireless", "mode", 's', settings["802-11-wireless"].mode);
        } else {
            delete result["802-11-wireless"];
        }

        if (settings["802-11-wireless-security"]) {
            set("802-11-wireless-security", "key-mgmt", 's', settings["802-11-wireless-security"]["key-mgmt"]);
            set("802-11-wireless-security", "psk", 's', settings["802-11-wireless-security"].psk);
        } else {
            delete result["802-11-wireless-security"];
        }

        return result;
    }

    function device_type_to_symbol(type) {
        // This returns a string that is suitable for the connection.type field of
        // Connection.Settings, except for "ethernet".
        switch (type) {
        case 0: return 'unknown';
        case 1: return 'ethernet'; // 802-3-ethernet
        case 2: return '802-11-wireless';
        case 3: return 'unused1';
        case 4: return 'unused2';
        case 5: return 'bluetooth';
        case 6: return '802-11-olpc-mesh';
        case 7: return 'wimax';
        case 8: return 'modem';
        case 9: return 'infiniband';
        case 10: return 'bond';
        case 11: return 'vlan';
        case 12: return 'adsl';
        case 13: return 'bridge';
        case 14: return 'generic';
        case 15: return 'team';
        case 16: return 'tun';
        case 17: return 'ip_tunnel';
        case 18: return 'macvlan';
        case 19: return 'vxlan';
        case 20: return 'veth';
        case 21: return 'macsec';
        case 22: return 'dummy';
        case 23: return 'ppp';
        case 24: return 'ovs_interface';
        case 25: return 'ovs_port';
        case 26: return 'ovs_bridge';
        case 27: return 'wpan';
        case 28: return '6lowpan';
        case 29: return 'wireguard';
        case 30: return 'wifi_p2p';
        case 31: return 'vrf';
        case 32: return 'loopback';
        default: return '';
        }
    }

    function device_state_to_text(state) {
        switch (state) {
        // NM_DEVICE_STATE_UNKNOWN
        case 0: return "?";
        // NM_DEVICE_STATE_UNMANAGED
        case 10: return "";
        // NM_DEVICE_STATE_UNAVAILABLE
        case 20: return _("Not available");
        // NM_DEVICE_STATE_DISCONNECTED
        case 30: return _("Inactive");
        // NM_DEVICE_STATE_PREPARE
        case 40: return _("Preparing");
        // NM_DEVICE_STATE_CONFIG
        case 50: return _("Configuring");
        // NM_DEVICE_STATE_NEED_AUTH
        case 60: return _("Authenticating");
        // NM_DEVICE_STATE_IP_CONFIG
        case 70: return _("Configuring IP");
        // NM_DEVICE_STATE_IP_CHECK
        case 80: return _("Checking IP");
        // NM_DEVICE_STATE_SECONDARIES
        case 90: return _("Waiting");
        // NM_DEVICE_STATE_ACTIVATED
        case 100: return _("Active");
        // NM_DEVICE_STATE_DEACTIVATING
        case 110: return _("Deactivating");
        // NM_DEVICE_STATE_FAILED
        case 120: return _("Failed");
        default: return "";
        }
    }

    function access_point_mode_to_text(mode) {
        switch (mode) {
        // NM_802_11_MODE_ADHOC
        case 1: return _("Adhoc");
        // NM_802_11_MODE_INFRA
        case 2: return _("Infra");
        // NM_802_11_MODE_AP
        case 3: return _("AP");
        // NM_802_11_MODE_MESH
        case 4: return _("Mesh");
        // subsumes NM_802_11_MODE_UNKNOWN
        default: return _("Unknown");
        }
    }

    const connections_by_uuid = { };

    function set_settings(obj, settings) {
        if (obj.Settings && obj.Settings.connection && obj.Settings.connection.uuid)
            delete connections_by_uuid[obj.Settings.connection.uuid];
        obj.Settings = settings;
        if (settings && settings.connection && settings.connection.uuid)
            connections_by_uuid[settings.connection.uuid] = obj;
    }

    function refresh_settings(obj) {
        push_refresh();
        client.call(objpath(obj), "org.freedesktop.NetworkManager.Settings.Connection", "GetSettings")
                .then(function(reply) {
                    const result = reply[0];
                    if (result) {
                        priv(obj).orig = result;
                        set_settings(obj, settings_from_nm(result));
                    }
                })
                .catch(complain)
                .finally(pop_refresh);
    }

    function refresh_udev(obj) {
        if (obj.Udi.indexOf("/sys/") !== 0)
            return;

        push_refresh();
        cockpit.spawn(["udevadm", "info", obj.Udi], { err: 'message' })
                .then(function(res) {
                    const props = { };
                    function snarf_prop(line, env, prop) {
                        const prefix = "E: " + env + "=";
                        if (line.indexOf(prefix) === 0) {
                            props[prop] = line.substring(prefix.length);
                        }
                    }
                    res.split('\n').forEach(function(line) {
                        snarf_prop(line, "ID_MODEL_FROM_DATABASE", "IdModel");
                        snarf_prop(line, "ID_VENDOR_FROM_DATABASE", "IdVendor");
                    });
                    set_object_properties(obj, props);
                })
                .catch(function(ex) {
                /* udevadm info exits with 4 when device doesn't exist */
                    if (ex.exit_status !== 4) {
                        console.warn(ex.message);
                        console.warn(ex);
                    }
                })
                .finally(pop_refresh);
    }

    function handle_updated(obj) {
        refresh_settings(obj);
    }

    /* NetworkManager specific object types, used by the generic D-Bus
     * code and using the data conversion functions.
     */

    const type_Ipv4Config = {
        interfaces: [
            "org.freedesktop.NetworkManager.IP4Config"
        ],

        props: {
            AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
        }
    };

    const type_Ipv6Config = {
        interfaces: [
            "org.freedesktop.NetworkManager.IP6Config"
        ],

        props: {
            AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
        }
    };

    const type_AccessPoint = {
        interfaces: [
            "org.freedesktop.NetworkManager.AccessPoint"
        ],

        props: {
            Flags: { def: 0 },
            WpaFlags: { def: 0 },
            RsnFlags: { def: 0 },
            Ssid: { conv: utils.ssid_from_nm, def: "" },
            Frequency: { def: 0 }, // MHz
            HwAddress: { def: "" },
            Mode: { conv: access_point_mode_to_text, def: "" },
            MaxBitrate: { def: 0 }, // Kbit/s
            Bandwidth: { def: 0 }, // MHz
            Strength: { def: 0 },
            LastSeen: { def: -1 }, // CLOCK_BOOTTIME seconds, -1 if never seen
        },

        exporters: [
            function (obj) {
                // Find connection for this SSID (undefined if none exists)
                obj.Connection = (self.get_settings()?.Connections || []).find(con => {
                    if (con.Settings?.["802-11-wireless"]?.ssid)
                        return utils.ssid_from_nm(con.Settings["802-11-wireless"].ssid) === obj.Ssid;
                    return false;
                });
            }
        ]
    };

    const type_Connection = {
        interfaces: [
            "org.freedesktop.NetworkManager.Settings.Connection"
        ],

        props: {
            Unsaved: { }
        },

        signals: {
            Updated: handle_updated
        },

        refresh: refresh_settings,

        drop: function (obj) {
            set_settings(obj, null);
        },

        prototype: {
            apply_settings: function (settings) {
                const self = this;
                try {
                    return call_object_method(self,
                                              "org.freedesktop.NetworkManager.Settings.Connection", "Update",
                                              settings_to_nm(settings, priv(self).orig))
                            .then(() => {
                                set_settings(self, settings);
                            });
                } catch (e) {
                    return Promise.reject(e);
                }
            },

            activate: function (dev, specific_object) {
                return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
                                          "org.freedesktop.NetworkManager", "ActivateConnection",
                                          objpath(this), objpath(dev), objpath(specific_object))
                        .then(([active_connection]) => active_connection);
            },

            delete_: function () {
                return call_object_method(this, "org.freedesktop.NetworkManager.Settings.Connection", "Delete")
                        .then(() => undefined);
            }
        },

        exporters: [
            function (obj) {
                obj.Groups = [];
                obj.Members = [];
                obj.Interfaces = [];
            },

            null,

            null,

            // Needs: type_Interface.Connections
            //
            // Sets:  type_Connection.Members
            //        type_Connection.Groups
            //
            function (obj) {
                // Most of the time, a connection has zero or one groups,
                // but when a connection refers to its group by interface
                // name, we might end up with more than one group
                // connection so we just collect them all.
                //
                // TODO - Nail down how NM really handles this.

                function check_con(con) {
                    const group_settings = connection_settings(con);
                    const my_settings = connection_settings(obj);
                    if (group_settings.type === my_settings.member_type) {
                        obj.Groups.push(con);
                        con.Members.push(obj);
                    }
                }

                const cs = connection_settings(obj);
                if (cs.member_type) {
                    const group = connections_by_uuid[cs.group];
                    if (group) {
                        obj.Groups.push(group);
                        group.Members.push(obj);
                    } else {
                        const iface = peek_interface(cs.group);
                        if (iface) {
                            iface.Connections.forEach(check_con);
                        }
                    }
                }
            }
        ]

    };

    const type_ActiveConnection = {
        interfaces: [
            "org.freedesktop.NetworkManager.Connection.Active"
        ],

        props: {
            Connection: { conv: conv_Object(type_Connection) },
            Ip4Config: { conv: conv_Object(type_Ipv4Config) },
            Ip6Config: { conv: conv_Object(type_Ipv6Config) },
            State: { def: 0 }
            // See below for "Group"
        },

        prototype: {
            deactivate: function() {
                return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
                                          "org.freedesktop.NetworkManager", "DeactivateConnection",
                                          objpath(this))
                        .then(() => undefined);
            }
        }
    };

    const type_Device = {
        interfaces: [
            "org.freedesktop.NetworkManager.Device",
            "org.freedesktop.NetworkManager.Device.Wired",
            "org.freedesktop.NetworkManager.Device.Bond",
            "org.freedesktop.NetworkManager.Device.Team",
            "org.freedesktop.NetworkManager.Device.Bridge",
            "org.freedesktop.NetworkManager.Device.Vlan",
            "org.freedesktop.NetworkManager.Device.Wireless"
        ],

        props: {
            DeviceType: { conv: device_type_to_symbol },
            Interface: { },
            StateText: { prop: "State", conv: device_state_to_text, def: _("Unknown") },
            State: { },
            StateReason: { def: [0, 0] }, // [state, reason] tuple
            HwAddress: { },
            AvailableConnections: { conv: conv_Array(conv_Object(type_Connection)), def: [] },
            ActiveConnection: { conv: conv_Object(type_ActiveConnection) },
            Ip4Config: { conv: conv_Object(type_Ipv4Config) },
            Ip6Config: { conv: conv_Object(type_Ipv6Config) },
            Udi: { trigger: refresh_udev },
            IdVendor: { def: "" },
            IdModel: { def: "" },
            Driver: { def: "" },
            Carrier: { def: true },
            Speed: { },
            Managed: { def: false },
            // WiFi-specific properties
            AccessPoints: { conv: conv_Array(conv_Object(type_AccessPoint)), def: [] },
            ActiveAccessPoint: { conv: conv_Object(type_AccessPoint) },
            // See below for "Members"
        },

        prototype: {
            activate: function(connection, specific_object) {
                priv(this).lastFailureReason = undefined; // Clear stale failure reason from previous attempts
                return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
                                          "org.freedesktop.NetworkManager", "ActivateConnection",
                                          objpath(connection), objpath(this), objpath(specific_object))
                        .then(([active_connection]) => active_connection);
            },

            activate_with_settings: function(settings, specific_object) {
                priv(this).lastFailureReason = undefined; // Clear stale failure reason from previous attempts
                try {
                    return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
                                              "org.freedesktop.NetworkManager", "AddAndActivateConnection",
                                              settings_to_nm(settings), objpath(this), objpath(specific_object))
                            .then(([path, active_connection_path]) => ({
                                connection: get_object(path, type_Connection),
                                active_connection: get_object(active_connection_path, type_ActiveConnection)
                            }));
                } catch (e) {
                    return Promise.reject(e);
                }
            },

            disconnect: function () {
                return call_object_method(this, 'org.freedesktop.NetworkManager.Device', 'Disconnect')
                        .then(() => undefined);
            },

            // Request a WiFi scan to populate this.AccessPoints
            request_scan: function() {
                utils.debug("request_scan: requesting scan for", this.Interface);
                call_object_method(this, 'org.freedesktop.NetworkManager.Device.Wireless', 'RequestScan', {})
                        .catch(error => {
                            // RequestScan can fail if a scan was recently done, that's OK
                            console.warn("request_scan: scan failed for", this.Interface + ":", error.toString());
                        });
            },

            // Get and clear the last connection failure reason
            consume_failure_reason: function() {
                const reason = priv(this).lastFailureReason;
                priv(this).lastFailureReason = undefined;
                return reason;
            },

            // Mark that a pending connection is being cancelled by the user
            cancel_pending_connection: function() {
                priv(this).connectionCancelled = true;
            },

            // Wait for a connection to complete
            // For WiFi, pass expected_ssid to verify we connected to the right network
            // Returns a Promise that resolves on success or cancel, rejects with {reason} on failure
            wait_connection: function(expected_ssid) {
                priv(this).connectionCancelled = false;
                utils.debug("wait_connection: starting, iface:", this.Interface, "expected:", expected_ssid, "initial state:", this.State);
                return new Promise((resolve, reject) => {
                    let activationStarted = false;

                    const cleanup = () => self.removeEventListener("changed", check);

                    const check = () => {
                        utils.debug("wait_connection check: state:", this.State, "ssid:", this.ActiveAccessPoint?.Ssid,
                                    "activeConn:", !!this.ActiveConnection, "activationStarted:", activationStarted,
                                    "lastFailureReason:", priv(this).lastFailureReason,
                                    "connectionCancelled:", priv(this).connectionCancelled);

                        // captured a failure?
                        const reason = this.consume_failure_reason();
                        if (reason) {
                            cleanup();
                            console.warn("wait_connection: connection failed for", this.Interface, "reason:", reason);
                            const error = new Error("Connection failed");
                            error.reason = reason;
                            reject(error);
                            return;
                        }

                        // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState
                        switch (this.State) {
                        case 100: // NM_DEVICE_STATE_ACTIVATED
                            if (!expected_ssid || this.ActiveAccessPoint?.Ssid === expected_ssid) {
                                utils.debug("wait_connection: success");
                                cleanup();
                                resolve();
                            }
                            break;

                        case 30: // NM_DEVICE_STATE_DISCONNECTED; initial state, so wait for activation to start
                        case 120: // NM_DEVICE_STATE_FAILED
                            if (priv(this).connectionCancelled) {
                                cleanup();
                                utils.debug("wait_connection: cancelled by user");
                                resolve();
                            } else {
                                // Disconnected/failed after
                                // activation started means user
                                // cancelled or failure without reason
                                if (activationStarted && !this.ActiveConnection) {
                                    cleanup();
                                    console.warn("wait_connection: connection failed for", this.Interface, "without captured reason");
                                    reject(new Error("Connection failed"));
                                }
                            }
                            break;

                        case 20: // NM_DEVICE_STATE_UNAVAILABLE
                        case 110: // NM_DEVICE_STATE_DEACTIVATING
                            break;

                        // any other state means we're activating
                        default:
                            if (!activationStarted) {
                                utils.debug("wait_connection: activation started");
                                activationStarted = true;
                            }
                        }
                    };

                    self.addEventListener("changed", check);
                    check(); // Check current state immediately in case already connected
                });
            }
        },

        exporters: [
            function (obj) {
                if (obj.DeviceType === '802-11-wireless') {
                    // When a hidden network (no SSID broadcast) has a saved connection, NetworkManager
                    // duplicates it in AccessPoints: once without SSID (from the beacon), and once with
                    // SSID (synthesized from the saved connection). Both have the same HwAddress (MAC).
                    // We deduplicate by MAC first, preferring the one with a known connection
                    const apByMac = new Map();
                    (obj.AccessPoints || []).forEach(ap => {
                        utils.debug(`AP: Ssid '${ap.Ssid}' HwAddress: ${ap.HwAddress} Strength: ${ap.Strength} hasConnection: ${!!ap.Connection}`);
                        if (!apByMac.get(ap.HwAddress) || ap.Connection)
                            apByMac.set(ap.HwAddress, ap);
                    });

                    // Deduplicate visible APs by SSID, keeping the strongest signal for each network.
                    // Count remaining hidden APs (those without SSID after MAC deduplication).
                    const apBySsid = new Map();
                    let hiddenCount = 0;
                    Array.from(apByMac.values()).forEach(ap => {
                        if (ap.Ssid) {
                            const existing = apBySsid.get(ap.Ssid);
                            if (!existing || ap.Strength > existing.Strength) {
                                apBySsid.set(ap.Ssid, ap);
                            }
                        } else {
                            hiddenCount++;
                        }
                    });
                    obj.visibleSsids = Array.from(apBySsid.values());
                    obj.hiddenAPCount = hiddenCount;
                    utils.debug("Device exporter:", obj.Interface, "has", obj.visibleSsids.length, "visible SSIDs and", obj.hiddenAPCount, "hidden APs");
                }
            }
        ]
    };

    // The 'Interface' type does not correspond to any NetworkManager
    // object or interface.  We use it to represent a network device
    // that might or might not actually be known to the kernel, such
    // as the interface of a bond that is currently down.
    //
    // This is a HACK: NetworkManager should export Device nodes for
    // these.

    const type_Interface = {
        interfaces: [],

        exporters: [
            function (obj) {
                obj.Device = null;
                obj._NonDeviceConnections = [];
                obj.Connections = [];
                obj.MainConnection = null;
            },

            null,

            // Needs: type_Interface.Device
            //        type_Interface._NonDeviceConnections
            //
            // Sets:  type_Connection.Interfaces
            //        type_Interface.Connections
            //        type_Interface.MainConnection

            function (obj) {
                if (!obj.Device && obj._NonDeviceConnections.length === 0) {
                    drop_object(priv(obj).path);
                    return;
                }

                function consider_for_main(con) {
                    if (!obj.MainConnection ||
                        connection_settings(obj.MainConnection).timestamp < connection_settings(con).timestamp) {
                        obj.MainConnection = con;
                    }
                }

                obj.Connections = obj._NonDeviceConnections;

                if (obj.Device) {
                    obj.Device.AvailableConnections.forEach(function (con) {
                        if (obj.Connections.indexOf(con) === -1)
                            obj.Connections.push(con);
                    });
                }

                obj.Connections.forEach(function (con) {
                    consider_for_main(con);
                    con.Interfaces.push(obj);
                });

                // Explicitly prefer the active connection.  The
                // active connection should have the most recent
                // timestamp, but only when the activation was
                // successful.  Also, there don't seem to be change
                // notifications when the timestamp changes.

                if (obj.Device && obj.Device.ActiveConnection && obj.Device.ActiveConnection.Connection) {
                    obj.MainConnection = obj.Device.ActiveConnection.Connection;
                }
            }
        ]

    };

    function get_interface(iface) {
        const obj = get_object(":interface:" + iface, type_Interface);
        obj.Name = iface;
        return obj;
    }

    function peek_interface(iface) {
        return peek_object(":interface:" + iface);
    }

    const type_Settings = {
        interfaces: [
            "org.freedesktop.NetworkManager.Settings"
        ],

        props: {
            Connections: { conv: conv_Array(conv_Object(type_Connection)), def: [] }
        },

        prototype: {
            add_connection: function (conf) {
                // https://www.networkmanager.dev/docs/api/latest/nm-dbus-types.html
                const NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK = 0x1;
                const NM_SETTINGS_ADD_CONNECTION2_FLAG_BLOCK_AUTOCONNECT = 0x20;
                return call_object_method(this,
                                          'org.freedesktop.NetworkManager.Settings',
                                          'AddConnection2',
                                          settings_to_nm(conf, { }), NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK | NM_SETTINGS_ADD_CONNECTION2_FLAG_BLOCK_AUTOCONNECT, { })
                        .then(([path]) => get_object(path, type_Connection));
            }
        },

        exporters: [
            null,

            // Sets: type_Interface._NonDeviceConnections
            //
            function (obj) {
                if (obj.Connections) {
                    obj.Connections.forEach(function (con) {
                        function add_to_interface(name) {
                            if (name) {
                                const cons = get_interface(name)._NonDeviceConnections;
                                if (cons.indexOf(con) === -1)
                                    cons.push(con);
                            }
                        }

                        if (con.Settings) {
                            if (con.Settings.connection)
                                add_to_interface(con.Settings.connection.interface_name);
                            if (con.Settings.bond)
                                add_to_interface(con.Settings.bond.interface_name);
                            if (con.Settings.team)
                                add_to_interface(con.Settings.team.interface_name);
                            if (con.Settings.bridge)
                                add_to_interface(con.Settings.bridge.interface_name);
                            if (con.Settings.vlan)
                                add_to_interface(con.Settings.vlan.interface_name);
                        }
                    });
                }
            }
        ]
    };

    const type_Manager = {
        interfaces: [
            "org.freedesktop.NetworkManager"
        ],

        props: {
            Capabilities: { def: [] },
            Version: { },
            Devices: {
                conv: conv_Array(conv_Object(type_Device)),
                def: []
            },
            ActiveConnections: { conv: conv_Array(conv_Object(type_ActiveConnection)), def: [] }
        },

        prototype: {
            checkpoint_create: function (devices, timeout) {
                return call_object_method(this,
                                          'org.freedesktop.NetworkManager',
                                          'CheckpointCreate',
                                          devices.map(objpath),
                                          timeout,
                                          0)
                        .then(([checkpoint]) => checkpoint)
                        .catch(function (error) {
                            if (error.name !== "org.freedesktop.DBus.Error.UnknownMethod")
                                console.warn(error.message || error);
                        });
            },

            checkpoint_destroy: function (checkpoint) {
                if (checkpoint) {
                    return call_object_method(this,
                                              'org.freedesktop.NetworkManager',
                                              'CheckpointDestroy',
                                              checkpoint)
                            .then(() => undefined);
                } else
                    return Promise.resolve();
            },

            checkpoint_rollback: function (checkpoint) {
                if (checkpoint) {
                    return call_object_method(this,
                                              'org.freedesktop.NetworkManager',
                                              'CheckpointRollback',
                                              checkpoint)
                            .then(([result]) => result);
                } else
                    return Promise.resolve();
            }
        },

        exporters: [
            null,

            // Sets: type_Interface.Device
            //
            function (obj) {
                obj.Devices.forEach(function (dev) {
                    if (dev.Interface) {
                        const iface = get_interface(dev.Interface);
                        iface.Device = dev;
                    }
                });
            }
        ]
    };

    /* Now create the cyclic declarations.
     */
    type_ActiveConnection.props.Group = { conv: conv_Object(type_Device) };
    type_Device.props.Members = { conv: conv_Array(conv_Object(type_Device)), def: [] };

    /* Accessing the model.
     */

    self.list_interfaces = function list_interfaces() {
        const result = [];
        for (const path in objects) {
            const obj = objects[path];
            if (priv(obj).type === type_Interface)
                result.push(obj);
        }
        return result.sort(function (a, b) { return a.Name.localeCompare(b.Name) });
    };

    self.find_interface = peek_interface;

    self.get_manager = function () {
        return get_object("/org/freedesktop/NetworkManager",
                          type_Manager);
    };

    self.get_settings = function () {
        return get_object("/org/freedesktop/NetworkManager/Settings",
                          type_Settings);
    };

    /* Initialization.
     */

    set_object_types([type_Manager,
        type_Settings,
        type_Device,
        type_Ipv4Config,
        type_Ipv6Config,
        type_Connection,
        type_ActiveConnection,
        type_AccessPoint
    ]);

    get_object("/org/freedesktop/NetworkManager", type_Manager);
    get_object("/org/freedesktop/NetworkManager/Settings", type_Settings);

    self.ready = undefined;
    self.operationInProgress = undefined;
    self.curtain = undefined;
    return self;
}

export function syn_click(model, fun) {
    return function() {
        const self = this;
        const self_args = arguments;
        return model.synchronize().then(function() {
            fun.apply(self, self_args);
        });
    };
}

export function is_managed(dev) {
    // Never let the user manage loopback devices, nothing good can come from that.
    return dev.State !== 10 && dev.DeviceType !== "loopback" && dev.Interface !== "lo";
}

function render_interface_link(iface) {
    return <Button variant='link' tabindex="0"
                   isInline
                   onClick={() => cockpit.location.go([iface])}>{iface}
    </Button>;
}

export function device_state_text(dev) {
    if (!dev)
        return _("Inactive");
    if (dev.State === 100 && dev.Carrier === false)
        return _("No carrier");
    if (!is_managed(dev)) {
        if (!dev.ActiveConnection &&
            (!dev.Ip4Config || dev.Ip4Config.AddressData.length === 0) &&
            (!dev.Ip6Config || dev.Ip6Config.AddressData.length === 0))
            return _("Inactive");
    }
    return dev.StateText;
}

export function array_join(elts, sep) {
    const result = [];
    for (let i = 0; i < elts.length; i++) {
        result.push(elts[i]);
        if (i < elts.length - 1)
            result.push(sep);
    }
    return result;
}

export function render_active_connection(dev, with_link, hide_link_local) {
    const parts = [];

    if (!dev)
        return "";

    const con = dev.ActiveConnection;

    if (con && con.Group) {
        return fmt_to_fragments(_("Part of $0"), with_link ? render_interface_link(con.Group.Interface) : con.Group.Interface);
    }

    const ip4config = con ? con.Ip4Config : dev.Ip4Config;
    if (ip4config) {
        ip4config.AddressData.forEach(function (a) {
            parts.push(a.address + "/" + a.prefix);
        });
    }

    function is_ipv6_link_local(addr) {
        return (addr.indexOf("fe8") === 0 ||
                addr.indexOf("fe9") === 0 ||
                addr.indexOf("fea") === 0 ||
                addr.indexOf("feb") === 0);
    }

    const ip6config = con ? con.Ip6Config : dev.Ip6Config;
    if (ip6config) {
        ip6config.AddressData.forEach(function (a) {
            if (!(hide_link_local && is_ipv6_link_local(a.address)))
                parts.push(a.address + "/" + a.prefix);
        });
    }

    return parts.join(", ");
}

/* Resource usage monitoring
*/

export function complete_settings(settings, device) {
    if (!device) {
        console.warn("No device to complete settings", JSON.stringify(settings));
        return;
    }

    settings.connection.id = device.Interface;
    settings.connection.uuid = uuidv4();

    if (device.DeviceType === 'ethernet') {
        settings.connection.type = '802-3-ethernet';
        settings.ethernet = { };
    } else {
        // The remaining types are identical between Device and Settings, see
        // device_type_to_symbol.
        settings.connection.type = device.DeviceType;
    }
}

export function settings_applier(model, device, connection) {
    /* If we have a connection, we can just update it.
     * Otherwise if the settings has TYPE set, we can add
     * them as a stand-alone object.  Otherwise, we
     * activate the device with the settings which causes
     * NM to fill in the type and other details.
     * Non-persistent "Wired Connection" is a special case
     * and we make a new connection instead.
     *
     * HACK - The activation is a hack, we would rather
     * just have NM fill in the details and not activate
     * the connection.  See complete_settings above that
     * can do some of this completion.
     *
     * https://bugzilla.gnome.org/show_bug.cgi?id=775226
     */

    const specialCon = utils.isNonPersistentMultiCon(connection);

    return function (settings) {
        if (connection && !specialCon) {
            return connection.apply_settings(settings);
        } else if (settings.connection.type && !specialCon) {
            return model.get_settings().add_connection(settings);
        } else if (device && specialCon) {
            const newSettings = utils.createNewConnSettings(settings, device.Interface);
            return device.activate_with_settings(newSettings);
        } else if (device) {
            return device.activate_with_settings(settings);
        } else {
            console.warn("No way to apply settings", connection, settings);
            return Promise.resolve();
        }
    };
}

export function choice_title(choices, choice, def) {
    for (let i = 0; i < choices.length; i++) {
        if (choices[i].choice === choice)
            return choices[i].title;
    }
    return def;
}

/* Support for automatically rolling back changes that break the
 * connection to the server.
 *
 * The basic idea is to perform the following steps:
 *
 * 1) Create a checkpoint with automatic rollback
 * 2) Make the change
 * 3) Destroy the checkpoint
 *
 * If step 2 breaks the connection, step 3 won't happen and the
 * checkpoint will roll back after some time.  This is supposed to
 * restore connectivity, so steps 2 and 3 will complete at that time,
 * and step 3 will fail because the checkpoint doesn't exist anymore.
 *
 * The failure of step 3 is our indication that the connection was
 * temporarily broken, and we inform the user about that.
 *
 * Usually, step 2 completes successfully also for a change that
 * breaks the connection, and connectivity is only lost after some
 * delay.  Thus, we also delay step 3 by a short amount (settle_time,
 * below).
 *
 * For a change that _doesn't_ break connectivity, this whole process
 * is inherently a race: Steps 2 and 3 need to complete before the
 * checkpoint created in step 1 reaches its timeout.
 *
 * It is better to wait a bit longer for salvation after making a
 * mistake than to have many of your legitimate changes be cancelled
 * by an impatient nanny mechanism.  Thus, we use a rather long
 * checkpoint rollback timeout (rollback_time, below).
 *
 * For a good change, all three steps usually happen quickly, and the
 * time we wait between steps 2 and 3 doesn't need to be very long
 * either, apparently.  Thus, we delay any indication that something
 * might be wrong by a short delay (curtain_time, below), and most
 * changes can thus be made without the "Testing connection" curtain
 * coming up.
 *
 * Some changes will be rolled back although the user really wants to
 * make them.  For example, the user might want to change the IP
 * address of the machine, and although this will disconnect Cockpit,
 * the user can connect again on the new address.
 *
 * In order to give the user the option to avoid this unwanted
 * rollback, we let him/her do the same change without a checkpoint
 * directly from the dialog that explains the problem.
 */

/* To avoid interference, we switch off the global transport health
 * check while a checkpoint exists.  For example, if the rollback
 * takes a really long time, Cockpit would otherwise disconnect itself
 * forcefully and the user would not get to see the dialog with the
 * "Do it anyway" button.  This dialog is the only way to make certain
 * changes, and it is thus important to show it if at all possible.
 */

/* Considerations for choosing the times below
 *
 * curtain_time too short:  Curtain comes up too often for good changes.
 *
 * curtain_time too long:   User is left with a broken UI for a
 *                          significant time in the case of a mistake.
 *
 * settle_time too short:   Some bad changes that take time to have any
 *                          effect will be let through.
 *
 * settle_time too high:    All operations take a long time and the race
 *                          between Cockpit destroying the checkpoint
 *                          and NetworkManager rolling it back (see
 *                          above) gets tighter.  The curtain
 *                          needs to come up to prevent the user from
 *                          interacting with the page.  Thus
 *                          settle_time should be shorter than
 *                          curtain_time.
 *
 * rollback_time too short: Good changes that take a long time to complete
 *                          (on a loaded machine, say) are cancelled spuriously.
 *
 * rollback_time too long:  The user has to wait a long time before
 *                          his/her mistake is corrected and might
 *                          consider Cockpit to be dead already.
 *                          Also, the network connection machinery in
 *                          the kernels and browsers must recover
 *                          after no packages have been flowing for
 *                          this much time.  Windows seems to have
 *                          less patience than Linux in this regard.
 */
const curtain_time = 1.5;
let settle_time = 1.0;
const rollback_time = 7.0;

export function with_checkpoint(model, modify, options) {
    const manager = model.get_manager();

    let curtain_timeout;
    let curtain_title_timeout;

    function show_curtain() {
        cockpit.hint("ignore_transport_health_check", { data: true });
        curtain_timeout = window.setTimeout(function () {
            curtain_timeout = null;
            model.set_curtain('testing');
        }, curtain_time * 1000);
        curtain_title_timeout = window.setTimeout(function () {
            curtain_title_timeout = null;
            model.set_curtain('restoring');
        }, rollback_time * 1000);
    }

    function hide_curtain() {
        if (curtain_timeout)
            window.clearTimeout(curtain_timeout);
        curtain_timeout = null;
        if (curtain_title_timeout)
            window.clearTimeout(curtain_title_timeout);
        cockpit.hint("ignore_transport_health_check", { data: false });

        model.set_curtain(undefined);
    }

    // HACK - Let's not use checkpoints for changes that involve
    // adding or removing connections.
    //
    // https://bugzilla.redhat.com/show_bug.cgi?id=1378393
    // https://bugzilla.redhat.com/show_bug.cgi?id=1398316
    //
    // We also switch off checkpoints for most of the integration
    // tests.

    if (options.hack_does_add_or_remove || window.cockpit_tests_disable_checkpoints) {
        modify();
        return;
    }

    if (window.cockpit_tests_checkpoints_settle_time)
        settle_time = window.cockpit_tests_checkpoints_settle_time;

    manager.checkpoint_create(options.devices || [], rollback_time)
            .then(function (cp) {
                if (!cp) {
                    modify();
                    return;
                }

                // Signal that a checkpoint is active for anaconda-webui
                window.sessionStorage.setItem("cockpit_has_checkpoint", "true");

                show_curtain();
                modify()
                        .then(function () {
                            window.setTimeout(function () {
                                manager.checkpoint_destroy(cp)
                                        .catch(function () {
                                            show_breaking_change_dialog({
                                                ...options,
                                                action: syn_click(model, modify)
                                            });
                                        })
                                        .finally(function() {
                                            hide_curtain();

                                            // Clear checkpoint status when done
                                            window.sessionStorage.setItem("cockpit_has_checkpoint", "false");
                                        });
                            }, settle_time * 1000);
                        })
                        .catch(function () {
                            hide_curtain();

                            // HACK
                            //
                            // We want to avoid rollbacks for operations that don't actually change anything when they
                            // fail.  Rollback are always disruptive and always seem to reconnect all the included
                            // devices, even if nothing has actually changed.  Thus, if you give invalid input to
                            // NetworkManager and receive an error in a settings dialog, rolling back the checkpoint
                            // would cause a temporary disconnection on the interface.
                            //
                            // https://bugzilla.redhat.com/show_bug.cgi?id=1427187

                            if (options.rollback_on_failure)
                                manager.checkpoint_rollback(cp);
                            else
                                manager.checkpoint_destroy(cp);

                            // Clear checkpoint status on failure
                            window.sessionStorage.setItem("cockpit_has_checkpoint", "false");
                        });
            });
}

export function with_settings_checkpoint(model, modify, options) {
    with_checkpoint(model, modify,
                    {
                        ...options,
                        fail_text: _("Changing the settings will break the connection to the server, and will make the administration UI unavailable."),
                        anyway_text: _("Change the settings"),
                    });
}

export function connection_devices(con) {
    const devices = [];

    if (con)
        con.Interfaces.forEach(function (iface) { if (iface.Device) devices.push(iface.Device); });

    return devices;
}

export function is_interface_connection(iface, connection) {
    return connection && connection.Interfaces.indexOf(iface) !== -1;
}

export function is_interesting_interface(iface) {
    return !iface.Device || is_managed(iface.Device);
}

export function member_connection_for_interface(group, iface) {
    return group?.Members.find(s => is_interface_connection(iface, s));
}

export function member_interface_choices(model, group) {
    return model.list_interfaces().filter(function (iface) {
        return !is_interface_connection(iface, group) && is_interesting_interface(iface);
    });
}

export function free_member_connection(con) {
    const cs = connection_settings(con);
    if (cs.member_type) {
        delete cs.member_type;
        delete cs.group;
        delete con.Settings.team_port;
        delete con.Settings.bridge_port;
        return con.apply_settings(con.Settings).then(() => { con.activate(null, null) });
    }
}

export function set_member(model, group_connection, group_settings, member_type,
    iface_name, val) {
    const iface = model.find_interface(iface_name);
    if (!iface)
        return false;

    const main_connection = iface.MainConnection;

    if (val) {
        /* Turn the main_connection into a member for group.
         */

        const group_iface = group_settings.connection.interface_name;
        if (!group_iface)
            return false;

        let member_settings;
        if (main_connection) {
            member_settings = main_connection.Settings;

            if (member_settings.connection.group === group_settings.connection.uuid ||
                member_settings.connection.group === group_settings.connection.id ||
                member_settings.connection.group === group_iface)
                return Promise.resolve();

            member_settings.connection.member_type = member_type;
            member_settings.connection.group = group_iface;
            member_settings.connection.autoconnect = true;
            delete member_settings.ipv4;
            delete member_settings.ipv6;
            delete member_settings.team_port;
            delete member_settings.bridge_port;
        } else {
            member_settings = {
                connection:
                               {
                                   autoconnect: true,
                                   interface_name: iface.Name,
                                   member_type,
                                   group: group_iface
                               }
            };
            complete_settings(member_settings, iface.Device);
        }

        return settings_applier(model, iface.Device, main_connection)(member_settings).then(function () {
            // If the group already exists (with the correct name),
            // activate or deactivate the member immediately so that
            // the settings actually apply and the interface becomes a
            // member.  Otherwise we activate it later when the group
            // is created.
            if (group_connection && group_connection.Interfaces[0].Name === group_iface) {
                const group_dev = group_connection.Interfaces[0].Device;
                if (group_dev && group_dev.ActiveConnection)
                    return main_connection.activate(iface.Device);
                else if (iface.Device.ActiveConnection)
                    return iface.Device.ActiveConnection.deactivate();
            }
        });
    } else {
        /* Free the main_connection from being a member if it is our member.  If there is
         * no main_connection, we don't need to do anything.
         */
        if (main_connection && main_connection.Groups.indexOf(group_connection) !== -1) {
            free_member_connection(main_connection);
        }
    }

    return true;
}

export function apply_group_member(choices, model, apply_group, group_connection, group_settings, member_type) {
    const active_settings = [];

    if (!group_connection) {
        if (group_settings.bond &&
            group_settings.bond.options &&
            group_settings.bond.options.primary) {
            const iface = model.find_interface(group_settings.bond.options.primary);
            if (iface && iface.MainConnection)
                active_settings.push(iface.MainConnection.Settings);
        } else {
            Object.keys(choices)
                    .filter(choice => choices[choice])
                    .forEach(choice => {
                        const iface = model.find_interface(choice);
                        if (iface && iface.Device && iface.Device.ActiveConnection && iface.Device.ActiveConnection.Connection) {
                            active_settings.push(iface.Device.ActiveConnection.Connection.Settings);
                        }
                    });
        }

        if (active_settings.length === 1) {
            group_settings.ipv4 = JSON.parse(JSON.stringify(active_settings[0].ipv4));
            group_settings.ipv6 = JSON.parse(JSON.stringify(active_settings[0].ipv6));
        }

        group_settings.connection.autoconnect_members = 1;
    }

    /* For bonds, the order in which members are added to their group matters since the first members gets to
     * set the MAC address of the bond, which matters for DHCP.  We leave it to NetworkManager to determine
     * the order in which members are added so that the order is consistent with what happens when the bond is
     * activated the next time, such as after a reboot.
     */

    function set_all_members() {
        const deferreds = Object.keys(choices).map(iface => {
            return model.synchronize().then(function () {
                return set_member(model, group_connection, group_settings, member_type,
                                  iface, choices[iface]);
            });
        });
        return Promise.all(deferreds);
    }

    return set_all_members().then(function () {
        return apply_group(group_settings);
    });
}

export function is_loopback(iface) {
    return iface.Name === "lo" || (iface.Device && iface.Device.DeviceType === "loopback");
}

export function has_group(iface) {
    return (
        (iface.Device &&
            iface.Device.ActiveConnection &&
            iface.Device.ActiveConnection.Group &&
            iface.Device.ActiveConnection.Group.Members.length > 0) ||
        (iface.MainConnection && iface.MainConnection.Groups.length > 0)
    );
}

export function is_wireless(iface) {
    // NM_DEVICE_TYPE_WIFI
    return iface.Device?.DeviceType === '802-11-wireless';
}

export function init() {
    cockpit.translate();
}