/* * 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:

{message}

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

{fail_text}

}; 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 ; } 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(); }