1787 lines
62 KiB
JavaScript
1787 lines
62 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2013 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
import React from "react";
|
|
import cockpit from 'cockpit';
|
|
|
|
import { Button } from '@patternfly/react-core';
|
|
|
|
import { fmt_to_fragments } from 'utils.jsx';
|
|
import * as utils from './utils';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
import "./networking.scss";
|
|
|
|
import { show_modal_dialog } from "cockpit-components-dialog.jsx";
|
|
|
|
const _ = cockpit.gettext;
|
|
|
|
function show_error_dialog(title, message) {
|
|
const props = {
|
|
id: "error-popup",
|
|
title: 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);
|
|
// HACK - phantomjs console.trace() prints nothing
|
|
try { throw new Error() } catch (e) { console.log(e.stack) }
|
|
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 doesn't implement the standard o.fd.DBus.Properties
|
|
* interface.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
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;
|
|
|
|
/* 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", "State"], { flags: "" })
|
|
.then((reply, options) => {
|
|
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();
|
|
set_object_properties(obj, remove_signatures(args[0]));
|
|
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_deferred = 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_deferred) {
|
|
export_model_deferred.resolve();
|
|
export_model_deferred = null;
|
|
}
|
|
}
|
|
|
|
if (!export_pending) {
|
|
export_pending = true;
|
|
window.setTimeout(function () { export_pending = false; doit() }, 300);
|
|
}
|
|
}
|
|
|
|
self.synchronize = function synchronize() {
|
|
if (outstanding_refreshes === 0) {
|
|
return cockpit.resolve();
|
|
} else {
|
|
if (!export_model_deferred)
|
|
export_model_deferred = cockpit.defer();
|
|
return export_model_deferred.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];
|
|
|
|
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" });
|
|
});
|
|
|
|
self.close = function close() {
|
|
subscription.remove();
|
|
watch.remove();
|
|
client.removeEventListener("notify", onNotifyEventHandler);
|
|
client.close("unused");
|
|
};
|
|
|
|
/* NetworkManager specific data conversions and utility functions.
|
|
*/
|
|
|
|
function ip4_address_from_nm(addr) {
|
|
return [utils.ip4_to_text(addr[0]),
|
|
utils.ip_prefix_to_text(addr[1]),
|
|
utils.ip4_to_text(addr[2], true)
|
|
];
|
|
}
|
|
|
|
function ip4_address_to_nm(addr) {
|
|
return [utils.ip4_from_text(addr[0]),
|
|
utils.ip4_prefix_from_text(addr[1]),
|
|
utils.ip4_from_text(addr[2], true)
|
|
];
|
|
}
|
|
|
|
function ip4_route_from_nm(addr) {
|
|
return [utils.ip4_to_text(addr[0]),
|
|
utils.ip_prefix_to_text(addr[1]),
|
|
utils.ip4_to_text(addr[2], true),
|
|
utils.ip_metric_to_text(addr[3])
|
|
];
|
|
}
|
|
|
|
function ip4_route_to_nm(addr) {
|
|
return [utils.ip4_from_text(addr[0]),
|
|
utils.ip4_prefix_from_text(addr[1]),
|
|
utils.ip4_from_text(addr[2], true),
|
|
utils.ip_metric_from_text(addr[3])
|
|
];
|
|
}
|
|
function ip6_address_from_nm(addr) {
|
|
return [utils.ip6_to_text(addr[0]),
|
|
utils.ip_prefix_to_text(addr[1]),
|
|
utils.ip6_to_text(addr[2], true)
|
|
];
|
|
}
|
|
|
|
function ip6_address_to_nm(addr) {
|
|
return [utils.ip6_from_text(addr[0]),
|
|
parseInt(addr[1], 10) || 64,
|
|
utils.ip6_from_text(addr[2], true)
|
|
];
|
|
}
|
|
|
|
function ip6_route_from_nm(addr) {
|
|
return [utils.ip6_to_text(addr[0]),
|
|
utils.ip_prefix_to_text(addr[1]),
|
|
utils.ip6_to_text(addr[2], true),
|
|
utils.ip_metric_to_text(addr[3]),
|
|
];
|
|
}
|
|
|
|
function ip6_route_to_nm(addr) {
|
|
return [utils.ip6_from_text(addr[0]),
|
|
utils.ip_prefix_from_text(addr[1]),
|
|
utils.ip6_from_text(addr[2], true),
|
|
utils.ip_metric_from_text(addr[3])
|
|
];
|
|
}
|
|
|
|
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, addr_from_nm, route_from_nm, 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),
|
|
addresses: get(first, "addresses", []).map(addr_from_nm),
|
|
dns: get(first, "dns", []).map(ip_to_text),
|
|
dns_search: get(first, "dns-search", []),
|
|
routes: get(first, "routes", []).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_members:
|
|
get("connection", "autoconnect-slaves", -1),
|
|
member_type: get("connection", "slave-type"),
|
|
group: get("connection", "master")
|
|
}
|
|
};
|
|
|
|
if (!settings.connection.master) {
|
|
result.ipv4 = get_ip("ipv4", ip4_address_from_nm, ip4_route_from_nm, utils.ip4_to_text);
|
|
result.ipv6 = get_ip("ipv6", ip6_address_from_nm, ip6_route_from_nm, 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")
|
|
};
|
|
}
|
|
|
|
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, addrs_sig, addr_to_nm, routes_sig, route_to_nm, ips_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);
|
|
|
|
const addresses = settings[first].addresses;
|
|
if (addresses)
|
|
set(first, "addresses", addrs_sig, addresses.map(addr_to_nm));
|
|
|
|
const dns = settings[first].dns;
|
|
if (dns)
|
|
set(first, "dns", ips_sig, dns.map(ip_from_text));
|
|
set(first, "dns-search", 'as', settings[first].dns_search);
|
|
|
|
const routes = settings[first].routes;
|
|
if (routes)
|
|
set(first, "routes", routes_sig, routes.map(route_to_nm));
|
|
|
|
// 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"];
|
|
}
|
|
|
|
set("connection", "id", 's', settings.connection.id);
|
|
set("connection", "autoconnect", 'b', settings.connection.autoconnect);
|
|
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);
|
|
|
|
if (settings.ipv4)
|
|
set_ip("ipv4", 'aau', ip4_address_to_nm, 'aau', ip4_route_to_nm, 'au', utils.ip4_from_text);
|
|
else
|
|
delete result.ipv4;
|
|
|
|
if (settings.ipv6)
|
|
set_ip("ipv6", 'a(ayuay)', ip6_address_to_nm, 'a(ayuayu)', ip6_route_to_nm, '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"];
|
|
|
|
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 'loopback';
|
|
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';
|
|
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 "";
|
|
}
|
|
}
|
|
|
|
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.substr(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: {
|
|
Addresses: { conv: conv_Array(ip4_address_from_nm), def: [] }
|
|
}
|
|
};
|
|
|
|
const type_Ipv6Config = {
|
|
interfaces: [
|
|
"org.freedesktop.NetworkManager.IP6Config"
|
|
],
|
|
|
|
props: {
|
|
Addresses: { conv: conv_Array(ip6_address_from_nm), def: [] }
|
|
}
|
|
};
|
|
|
|
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: {
|
|
copy_settings: function () {
|
|
return JSON.parse(JSON.stringify(this.Settings));
|
|
},
|
|
|
|
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 cockpit.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) }
|
|
// 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"
|
|
],
|
|
|
|
props: {
|
|
DeviceType: { conv: device_type_to_symbol },
|
|
Interface: { },
|
|
StateText: { prop: "State", conv: device_state_to_text, def: _("Unknown") },
|
|
State: { },
|
|
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 },
|
|
// See below for "Members"
|
|
},
|
|
|
|
prototype: {
|
|
activate: function(connection, specific_object) {
|
|
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) {
|
|
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]) => active_connection);
|
|
} catch (e) {
|
|
return cockpit.reject(e);
|
|
}
|
|
},
|
|
|
|
disconnect: function () {
|
|
return call_object_method(this, 'org.freedesktop.NetworkManager.Device', 'Disconnect')
|
|
.then(() => undefined);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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) {
|
|
return call_object_method(this,
|
|
'org.freedesktop.NetworkManager.Settings',
|
|
'AddConnection',
|
|
settings_to_nm(conf, { }))
|
|
.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: {
|
|
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
|
|
]);
|
|
|
|
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) {
|
|
return dev.State != 10;
|
|
}
|
|
|
|
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.Addresses.length === 0) &&
|
|
(!dev.Ip6Config || dev.Ip6Config.Addresses.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.Addresses.forEach(function (a) {
|
|
parts.push(a[0] + "/" + a[1]);
|
|
});
|
|
}
|
|
|
|
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.Addresses.forEach(function (a) {
|
|
if (!(hide_link_local && is_ipv6_link_local(a[0])))
|
|
parts.push(a[0] + "/" + a[1]);
|
|
});
|
|
}
|
|
|
|
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.
|
|
*
|
|
* 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
|
|
*/
|
|
|
|
return function (settings) {
|
|
if (connection) {
|
|
return connection.apply_settings(settings);
|
|
} else if (settings.connection.type) {
|
|
return model.get_settings().add_connection(settings);
|
|
} else if (device) {
|
|
return device.activate_with_settings(settings);
|
|
} else {
|
|
cockpit.warn("No way to apply settings", connection, settings);
|
|
return cockpit.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;
|
|
}
|
|
|
|
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(hide_curtain);
|
|
}, 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);
|
|
});
|
|
});
|
|
}
|
|
|
|
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 array_find(array, predicate) {
|
|
if (array === null || array === undefined) {
|
|
throw new TypeError('Array.prototype.find called on null or undefined');
|
|
}
|
|
if (typeof predicate !== 'function') {
|
|
throw new TypeError('predicate must be a function');
|
|
}
|
|
const list = Object(array);
|
|
const length = list.length >>> 0;
|
|
const thisArg = arguments[1];
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
if (i in list) {
|
|
const value = list[i];
|
|
if (predicate.call(thisArg, value, i, list)) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function member_connection_for_interface(group, iface) {
|
|
return group && array_find(group.Members, function (s) {
|
|
return 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_connection
|
|
? group_connection.Interfaces[0].Name
|
|
: 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 cockpit.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: 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, 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) {
|
|
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 init() {
|
|
cockpit.translate();
|
|
}
|