4725 lines
173 KiB
JavaScript
4725 lines
173 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 $ from 'jquery';
|
|
import React from "react";
|
|
import ReactDOM from "react-dom";
|
|
import { OnOffSwitch } from "cockpit-components-onoff.jsx";
|
|
import cockpit from 'cockpit';
|
|
|
|
import firewall from './firewall-client.js';
|
|
import * as utils from './utils';
|
|
import * as service from 'service';
|
|
|
|
import { mustache } from 'mustache';
|
|
import * as plot from 'plot.js';
|
|
import { journal } from 'journal';
|
|
|
|
/* jQuery extensions */
|
|
import 'patterns';
|
|
|
|
import "page.css";
|
|
import "table.css";
|
|
import "plot.css";
|
|
import "journal.css";
|
|
import "./networking.css";
|
|
import "form-layout.less";
|
|
|
|
const _ = cockpit.gettext;
|
|
var C_ = cockpit.gettext;
|
|
|
|
function nm_debug() {
|
|
if (window.debugging == "all" || window.debugging == "nm")
|
|
console.debug.apply(console, arguments);
|
|
}
|
|
|
|
function generate_uuid() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
var r = Math.random() * 16 | 0;
|
|
var v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
function show_unexpected_error(error) {
|
|
var msg = error.message || error || "???";
|
|
console.warn(msg);
|
|
$("#error-popup-message").text(msg);
|
|
$('.modal[role="dialog"]').modal('hide');
|
|
$('#error-popup').modal('show');
|
|
}
|
|
|
|
function select_btn(func, spec, klass) {
|
|
var choice = spec[0] ? spec[0].choice : null;
|
|
|
|
function option_mapper(opt) {
|
|
return $('<option>', { value: opt.choice, 'data-value': opt.title }).text(opt.title);
|
|
}
|
|
|
|
var btn = $('<select class="ct-select">').append(spec.map(option_mapper));
|
|
btn.on('change', function() {
|
|
choice = $(this).val();
|
|
select(choice);
|
|
func(choice);
|
|
});
|
|
|
|
function select(a) {
|
|
$(btn).val(a);
|
|
}
|
|
|
|
function selected() {
|
|
return choice;
|
|
}
|
|
|
|
select(choice);
|
|
$.data(btn[0], 'cockpit-select-btn-funcs', { select: select, selected: selected });
|
|
if (klass)
|
|
btn.addClass(klass);
|
|
|
|
return btn;
|
|
}
|
|
|
|
function select_btn_select(btn, choice) {
|
|
$.data(btn[0], 'cockpit-select-btn-funcs').select(choice);
|
|
}
|
|
|
|
function select_btn_selected(btn) {
|
|
return $.data(btn[0], 'cockpit-select-btn-funcs').selected();
|
|
}
|
|
|
|
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,
|
|
*
|
|
* var 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.
|
|
*/
|
|
|
|
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
|
|
* pecularities of the NetworkManager API.
|
|
*
|
|
* However, we do use a fake object manager since that allows us
|
|
* to avoid a lot of 'GetAll' round trips during initialization
|
|
* and helps with removing obsolete objects.
|
|
*/
|
|
|
|
var self = this;
|
|
|
|
/* HACK: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=808162 */
|
|
var hacks = { };
|
|
if (cockpit.manifests.network && cockpit.manifests.network.hacks)
|
|
hacks = cockpit.manifests.network.hacks;
|
|
var options = { };
|
|
if (hacks.with_networkmanager_needs_root)
|
|
options.superuser = "try";
|
|
|
|
var client = cockpit.dbus("org.freedesktop.NetworkManager", options);
|
|
|
|
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: "" })
|
|
.fail(complain)
|
|
.done((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();
|
|
}
|
|
});
|
|
});
|
|
|
|
/* Mostly generic D-Bus stuff. */
|
|
|
|
var objects = { };
|
|
|
|
function complain() {
|
|
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'];
|
|
}
|
|
|
|
var 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 (var 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) {
|
|
var 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) {
|
|
var p, decl, val;
|
|
decl = priv(obj).type.props;
|
|
for (p in decl) {
|
|
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) {
|
|
var props = { };
|
|
for (var 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) {
|
|
var dfd = new $.Deferred();
|
|
client.call(objpath(obj), iface, method, Array.prototype.slice.call(arguments, 3))
|
|
.fail(function(ex) {
|
|
dfd.reject(ex);
|
|
})
|
|
.done(function(reply) {
|
|
dfd.resolve.apply(dfd, reply);
|
|
});
|
|
return dfd.promise();
|
|
}
|
|
|
|
var interface_types = { };
|
|
var max_export_phases = 0;
|
|
var 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) {
|
|
var obj = peek_object(path);
|
|
|
|
if (obj) {
|
|
var 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) {
|
|
var 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);
|
|
}
|
|
|
|
var export_model_deferred = null;
|
|
|
|
function export_model() {
|
|
function doit() {
|
|
var phase, path, obj, exp;
|
|
for (phase = 0; phase < max_export_phases; phase++) {
|
|
for (path in objects) {
|
|
obj = objects[path];
|
|
exp = priv(obj).type.exporters;
|
|
if (exp && exp[phase])
|
|
exp[phase](obj);
|
|
}
|
|
}
|
|
|
|
$(self).trigger('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();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle NM not running
|
|
*/
|
|
var nm_service = service.proxy("NetworkManager");
|
|
var nm_enabled = null;
|
|
var nm_running = null;
|
|
|
|
function update_nm_trouble() {
|
|
nm_debug("update_nm_trouble; enabled", nm_enabled, "running", nm_running);
|
|
// need to wait until we have both pieces of information
|
|
if (nm_enabled === null || nm_running === null)
|
|
return;
|
|
|
|
// running
|
|
if (nm_running) {
|
|
$("#networking-nm-crashed").hide();
|
|
$("#networking-nm-disabled").hide();
|
|
$("#networking-graphs").show();
|
|
$("#networking-interfaces").show();
|
|
// NM appearing will also trigger a device update, which hides it if necessary
|
|
$("#networking-unmanaged-interfaces").show();
|
|
} else {
|
|
$("#networking-graphs").hide();
|
|
$("#networking-interfaces").hide();
|
|
$("#networking-unmanaged-interfaces").hide();
|
|
if (nm_enabled) {
|
|
$("#networking-nm-disabled").hide();
|
|
$("#networking-nm-crashed").show();
|
|
} else {
|
|
$("#networking-nm-disabled").show();
|
|
$("#networking-nm-crashed").hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
nm_service.addEventListener('changed', function() {
|
|
nm_enabled = nm_service.enabled;
|
|
update_nm_trouble();
|
|
});
|
|
|
|
// track NM going away or reappearing
|
|
client.addEventListener("owner", function(event, owner) {
|
|
nm_debug("NetworkManager owner changed:", JSON.stringify(owner));
|
|
nm_running = (owner !== null);
|
|
update_nm_trouble();
|
|
});
|
|
|
|
// Troubleshoot link and start button
|
|
$("#networking-nm-crashed a").click(function() {
|
|
cockpit.jump("/system/services#/NetworkManager.service", cockpit.transport.host);
|
|
});
|
|
$("#networking-nm-crashed button").click(nm_service.start);
|
|
|
|
// Enable NM button
|
|
$("#networking-nm-disabled button").click(function() {
|
|
nm_service.enable();
|
|
nm_service.start();
|
|
});
|
|
|
|
var subscription;
|
|
var watch;
|
|
|
|
self.preinit.then(() => {
|
|
subscription = client.subscribe({ }, signal_emitted);
|
|
watch = client.watch({ });
|
|
$(client).on("notify", function(event, data) {
|
|
$.each(data, function(path, ifaces) {
|
|
$.each(ifaces, function(iface, props) {
|
|
if (props)
|
|
interface_properties(path, iface, props);
|
|
else
|
|
interface_removed(path, iface);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
self.close = function close() {
|
|
subscription.remove();
|
|
watch.remove();
|
|
$(client).off("notify");
|
|
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)
|
|
};
|
|
}
|
|
|
|
var 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_slaves:
|
|
get("connection", "autoconnect-slaves", -1),
|
|
slave_type: get("connection", "slave-type"),
|
|
master: 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: $.extend({}, 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.slave_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.slave_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) {
|
|
var result = $.extend(true, {}, 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);
|
|
|
|
var addresses = settings[first].addresses;
|
|
if (addresses)
|
|
set(first, "addresses", addrs_sig, addresses.map(addr_to_nm));
|
|
|
|
var 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);
|
|
|
|
var 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_slaves);
|
|
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.slave_type);
|
|
set("connection", "master", 's', settings.connection.master);
|
|
|
|
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 "";
|
|
}
|
|
}
|
|
|
|
var 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")
|
|
.always(pop_refresh)
|
|
.fail(complain)
|
|
.done(function(reply) {
|
|
var result = reply[0];
|
|
if (result) {
|
|
priv(obj).orig = result;
|
|
set_settings(obj, settings_from_nm(result));
|
|
}
|
|
});
|
|
}
|
|
|
|
function refresh_udev(obj) {
|
|
if (obj.Udi.indexOf("/sys/") !== 0)
|
|
return;
|
|
|
|
push_refresh();
|
|
cockpit.spawn(["udevadm", "info", obj.Udi], { err: 'message' })
|
|
.done(function(res) {
|
|
var props = { };
|
|
function snarf_prop(line, env, prop) {
|
|
var 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);
|
|
})
|
|
.fail(function(ex) {
|
|
/* udevadm info exits with 4 when device doesn't exist */
|
|
if (ex.exit_status !== 4) {
|
|
console.warn(ex.message);
|
|
console.warn(ex);
|
|
}
|
|
})
|
|
.always(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.
|
|
*/
|
|
|
|
var type_Manager;
|
|
|
|
var type_Ipv4Config = {
|
|
interfaces: [
|
|
"org.freedesktop.NetworkManager.IP4Config"
|
|
],
|
|
|
|
props: {
|
|
Addresses: { conv: conv_Array(ip4_address_from_nm), def: [] }
|
|
}
|
|
};
|
|
|
|
var type_Ipv6Config = {
|
|
interfaces: [
|
|
"org.freedesktop.NetworkManager.IP6Config"
|
|
],
|
|
|
|
props: {
|
|
Addresses: { conv: conv_Array(ip6_address_from_nm), def: [] }
|
|
}
|
|
};
|
|
|
|
var 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 $.extend(true, { }, this.Settings);
|
|
},
|
|
|
|
apply_settings: function (settings) {
|
|
var self = this;
|
|
try {
|
|
return call_object_method(self,
|
|
"org.freedesktop.NetworkManager.Settings.Connection", "Update",
|
|
settings_to_nm(settings, priv(self).orig))
|
|
.done(function () {
|
|
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));
|
|
},
|
|
|
|
delete_: function () {
|
|
return call_object_method(this, "org.freedesktop.NetworkManager.Settings.Connection", "Delete");
|
|
}
|
|
},
|
|
|
|
exporters: [
|
|
function (obj) {
|
|
obj.Masters = [];
|
|
obj.Slaves = [];
|
|
obj.Interfaces = [];
|
|
},
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
// Needs: type_Interface.Connections
|
|
//
|
|
// Sets: type_Connection.Slaves
|
|
// type_Connection.Masters
|
|
//
|
|
function (obj) {
|
|
var master, iface;
|
|
|
|
// Most of the time, a connection has zero or one masters,
|
|
// but when a connection refers to its master by interface
|
|
// name, we might end up with more than one master
|
|
// connection so we just collect them all.
|
|
//
|
|
// TODO - Nail down how NM really handles this.
|
|
|
|
function check_con(con) {
|
|
var master_settings = connection_settings(con);
|
|
var my_settings = connection_settings(obj);
|
|
if (master_settings.type == my_settings.slave_type) {
|
|
obj.Masters.push(con);
|
|
con.Slaves.push(obj);
|
|
}
|
|
}
|
|
|
|
var cs = connection_settings(obj);
|
|
if (cs.slave_type) {
|
|
master = connections_by_uuid[cs.master];
|
|
if (master) {
|
|
obj.Masters.push(master);
|
|
master.Slaves.push(obj);
|
|
} else {
|
|
iface = peek_interface(cs.master);
|
|
if (iface) {
|
|
iface.Connections.forEach(check_con);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
};
|
|
|
|
var 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 "Master"
|
|
},
|
|
|
|
prototype: {
|
|
deactivate: function() {
|
|
return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
|
|
"org.freedesktop.NetworkManager", "DeactivateConnection",
|
|
objpath(this));
|
|
}
|
|
}
|
|
};
|
|
|
|
var 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 "Slaves"
|
|
},
|
|
|
|
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));
|
|
},
|
|
|
|
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));
|
|
} catch (e) {
|
|
return cockpit.reject(e);
|
|
}
|
|
},
|
|
|
|
disconnect: function () {
|
|
return call_object_method(this, 'org.freedesktop.NetworkManager.Device', 'Disconnect');
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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.
|
|
|
|
var 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) {
|
|
var obj = get_object(":interface:" + iface, type_Interface);
|
|
obj.Name = iface;
|
|
return obj;
|
|
}
|
|
|
|
function peek_interface(iface) {
|
|
return peek_object(":interface:" + iface);
|
|
}
|
|
|
|
var type_Settings = {
|
|
interfaces: [
|
|
"org.freedesktop.NetworkManager.Settings"
|
|
],
|
|
|
|
props: {
|
|
Connections: { conv: conv_Array(conv_Object(type_Connection)), def: [] }
|
|
},
|
|
|
|
prototype: {
|
|
add_connection: function (conf) {
|
|
var dfd = $.Deferred();
|
|
try {
|
|
call_object_method(this,
|
|
'org.freedesktop.NetworkManager.Settings',
|
|
'AddConnection',
|
|
settings_to_nm(conf, { }))
|
|
.done(function (path) {
|
|
dfd.resolve(get_object(path, type_Connection));
|
|
})
|
|
.fail(function (error) {
|
|
dfd.reject(error);
|
|
});
|
|
} catch (e) {
|
|
dfd.reject(e);
|
|
}
|
|
return dfd.promise();
|
|
}
|
|
},
|
|
|
|
exporters: [
|
|
null,
|
|
|
|
// Sets: type_Interface._NonDeviceConnections
|
|
//
|
|
function (obj) {
|
|
if (obj.Connections) {
|
|
obj.Connections.forEach(function (con) {
|
|
function add_to_interface(name) {
|
|
if (name) {
|
|
var 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);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
]
|
|
};
|
|
|
|
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) {
|
|
var dfd = $.Deferred();
|
|
call_object_method(this,
|
|
'org.freedesktop.NetworkManager',
|
|
'CheckpointCreate',
|
|
devices.map(objpath),
|
|
timeout,
|
|
0)
|
|
.done(function (path) {
|
|
dfd.resolve(path);
|
|
})
|
|
.fail(function (error) {
|
|
if (error.name != "org.freedesktop.DBus.Error.UnknownMethod")
|
|
console.warn(error.message || error);
|
|
dfd.resolve(null);
|
|
});
|
|
return dfd.promise();
|
|
},
|
|
|
|
checkpoint_destroy: function (checkpoint) {
|
|
if (checkpoint) {
|
|
return call_object_method(this,
|
|
'org.freedesktop.NetworkManager',
|
|
'CheckpointDestroy',
|
|
checkpoint);
|
|
} else
|
|
return $.when();
|
|
},
|
|
|
|
checkpoint_rollback: function (checkpoint) {
|
|
if (checkpoint) {
|
|
return call_object_method(this,
|
|
'org.freedesktop.NetworkManager',
|
|
'CheckpointRollback',
|
|
checkpoint);
|
|
} else
|
|
return $.when();
|
|
}
|
|
},
|
|
|
|
exporters: [
|
|
null,
|
|
|
|
// Sets: type_Interface.Device
|
|
//
|
|
function (obj) {
|
|
obj.Devices.forEach(function (dev) {
|
|
if (dev.Interface) {
|
|
var iface = get_interface(dev.Interface);
|
|
iface.Device = dev;
|
|
}
|
|
});
|
|
}
|
|
]
|
|
};
|
|
|
|
/* Now create the cyclic declarations.
|
|
*/
|
|
type_ActiveConnection.props.Master = { conv: conv_Object(type_Device) };
|
|
type_Device.props.Slaves = { conv: conv_Array(conv_Object(type_Device)), def: [] };
|
|
|
|
/* Accessing the model.
|
|
*/
|
|
|
|
self.list_interfaces = function list_interfaces() {
|
|
var path, obj;
|
|
var result = [];
|
|
for (path in objects) {
|
|
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);
|
|
return self;
|
|
}
|
|
|
|
// Add a "syn_click" method to jQuery. This will invoke the event
|
|
// handler with the additional guarantee that the model is consistent.
|
|
|
|
$.fn.extend({
|
|
syn_click: function(model, fun) {
|
|
return this.click(function() {
|
|
var self = this;
|
|
var self_args = arguments;
|
|
model.synchronize().then(function() {
|
|
fun.apply(self, self_args);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
function render_interface_link(iface) {
|
|
return $('<a tabindex="0">')
|
|
.text(iface)
|
|
.click(function () {
|
|
cockpit.location.go([iface]);
|
|
});
|
|
}
|
|
|
|
function device_state_text(dev) {
|
|
if (!dev)
|
|
return _("Inactive");
|
|
if (dev.State == 100 && dev.Carrier === false)
|
|
return _("No carrier");
|
|
if (!dev.Managed) {
|
|
if (!dev.ActiveConnection &&
|
|
(!dev.Ip4Config || dev.Ip4Config.Addresses.length === 0) &&
|
|
(!dev.Ip6Config || dev.Ip6Config.Addresses.length === 0))
|
|
return _("Inactive");
|
|
}
|
|
return dev.StateText;
|
|
}
|
|
|
|
function render_connection_link(con) {
|
|
var res =
|
|
$('<span>').append(
|
|
array_join(
|
|
con.Interfaces.map(function (iface) {
|
|
return $('<a tabindex="0">')
|
|
.text(iface.Name)
|
|
.click(function () {
|
|
cockpit.location.go([iface.Name]);
|
|
});
|
|
}),
|
|
", "));
|
|
return res;
|
|
}
|
|
|
|
function array_join(elts, sep) {
|
|
var result = [];
|
|
for (var i = 0; i < elts.length; i++) {
|
|
result.push(elts[i]);
|
|
if (i < elts.length - 1)
|
|
result.push(sep);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function render_active_connection(dev, with_link, hide_link_local) {
|
|
var parts = [];
|
|
var con;
|
|
|
|
if (!dev)
|
|
return "";
|
|
|
|
con = dev.ActiveConnection;
|
|
|
|
if (con && con.Master) {
|
|
return $('<span>').append(
|
|
$('<span>').text(_("Part of ")),
|
|
(with_link ? render_interface_link(con.Master.Interface) : con.Master.Interface));
|
|
}
|
|
|
|
var 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);
|
|
}
|
|
|
|
var 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 $('<span>').text(parts.join(", "));
|
|
}
|
|
|
|
function network_plot_setup_hook(pl) {
|
|
var axes = pl.getAxes();
|
|
if (axes.yaxis.datamax < 100000)
|
|
axes.yaxis.options.max = 100000;
|
|
else
|
|
axes.yaxis.options.max = null;
|
|
axes.yaxis.options.min = 0;
|
|
}
|
|
|
|
function make_network_plot_post_hook(unit) {
|
|
return function (pl) {
|
|
var axes = pl.getAxes();
|
|
$(unit).text(plot.bits_per_sec_tick_unit(axes.yaxis));
|
|
};
|
|
}
|
|
|
|
var permission = cockpit.permission({ admin: true });
|
|
$(permission).on("changed", update_network_privileged);
|
|
|
|
function update_network_privileged() {
|
|
$(".network-privileged").update_privileged(
|
|
permission, cockpit.format(
|
|
_("The user <b>$0</b> is not permitted to modify network settings"),
|
|
permission.user ? permission.user.name : '')
|
|
);
|
|
}
|
|
|
|
/* Resource usage monitoring
|
|
*/
|
|
|
|
var usage_metrics_channel;
|
|
var usage_samples;
|
|
var usage_grid;
|
|
|
|
function ensure_usage_monitor() {
|
|
if (usage_metrics_channel)
|
|
return;
|
|
|
|
usage_samples = { };
|
|
usage_metrics_channel = cockpit.metrics(1000,
|
|
[{
|
|
source: "direct",
|
|
metrics: [{
|
|
name: "network.interface.in.bytes",
|
|
units: "bytes",
|
|
derive: "rate"
|
|
},
|
|
{
|
|
name: "network.interface.out.bytes",
|
|
units: "bytes",
|
|
derive: "rate"
|
|
},
|
|
],
|
|
metrics_path_names: ["rx", "tx"]
|
|
},
|
|
{
|
|
source: "internal",
|
|
metrics: [{
|
|
name: "network.interface.rx",
|
|
units: "bytes",
|
|
derive: "rate"
|
|
},
|
|
{
|
|
name: "network.interface.tx",
|
|
units: "bytes",
|
|
derive: "rate"
|
|
},
|
|
],
|
|
metrics_path_names: ["rx", "tx"]
|
|
}
|
|
]);
|
|
usage_grid = cockpit.grid(1000, -1, -0);
|
|
usage_metrics_channel.follow();
|
|
usage_grid.walk();
|
|
}
|
|
|
|
function add_usage_monitor(iface) {
|
|
usage_samples[iface] = [usage_grid.add(usage_metrics_channel, ["rx", iface]),
|
|
usage_grid.add(usage_metrics_channel, ["tx", iface]),
|
|
];
|
|
}
|
|
|
|
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 = generate_uuid();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
};
|
|
}
|
|
|
|
PageNetworking.prototype = {
|
|
_init: function (model) {
|
|
this.id = "networking";
|
|
this.model = model;
|
|
},
|
|
|
|
getTitle: function() {
|
|
return C_("page-title", "Networking");
|
|
},
|
|
|
|
setup: function () {
|
|
var self = this;
|
|
|
|
update_network_privileged();
|
|
$("#networking-add-bond").syn_click(self.model, $.proxy(this, "add_bond"));
|
|
$("#networking-add-team").syn_click(self.model, $.proxy(this, "add_team"));
|
|
$("#networking-add-bridge").syn_click(self.model, $.proxy(this, "add_bridge"));
|
|
$("#networking-add-vlan").syn_click(self.model, $.proxy(this, "add_vlan"));
|
|
|
|
/* HACK - hide "Add Team" if it doesn't work due to missing bits
|
|
* https://bugzilla.redhat.com/show_bug.cgi?id=1375967
|
|
*/
|
|
|
|
$("#networking-add-team").hide();
|
|
// We need both the plugin and teamd
|
|
cockpit.script("test -f /usr/bin/teamd && " +
|
|
"( test -f /usr/lib*/NetworkManager/libnm-device-plugin-team.so || " +
|
|
" test -f /usr/lib*/NetworkManager/*/libnm-device-plugin-team.so || " +
|
|
" test -f /usr/lib/*-linux-gnu/NetworkManager/libnm-device-plugin-team.so || " +
|
|
" test -f /usr/lib/*-linux-gnu/NetworkManager/*/libnm-device-plugin-team.so)",
|
|
{ err: "ignore" })
|
|
.done(function () {
|
|
$("#networking-add-team").show();
|
|
})
|
|
.always(function () {
|
|
$("#networking-add-team").attr("data-test-stable", "yes");
|
|
});
|
|
|
|
function highlight_netdev_row(event, id) {
|
|
$('#networking-interfaces tr').removeClass('highlight-ct');
|
|
if (id) {
|
|
$('#networking-interfaces tr[data-interface="' + encodeURIComponent(id) + '"]').addClass('highlight-ct');
|
|
}
|
|
}
|
|
|
|
var rx_plot_data = {
|
|
direct: "network.interface.in.bytes",
|
|
internal: "network.interface.rx",
|
|
units: "bytes",
|
|
derive: "rate",
|
|
threshold: 200
|
|
};
|
|
|
|
var rx_plot_options = plot.plot_simple_template();
|
|
$.extend(rx_plot_options.yaxis, { tickFormatter: plot.format_bits_per_sec_tick_no_unit });
|
|
$.extend(rx_plot_options.grid, {
|
|
hoverable: true,
|
|
autoHighlight: false
|
|
});
|
|
rx_plot_options.setup_hook = network_plot_setup_hook;
|
|
rx_plot_options.post_hook = make_network_plot_post_hook("#networking-rx-unit");
|
|
this.rx_plot = new plot.Plot($("#networking-rx-graph"), 300);
|
|
this.rx_plot.set_options(rx_plot_options);
|
|
this.rx_series = this.rx_plot.add_metrics_stacked_instances_series(rx_plot_data, { });
|
|
this.rx_plot.start_walking();
|
|
$(this.rx_series).on('hover', highlight_netdev_row);
|
|
|
|
var tx_plot_data = {
|
|
direct: "network.interface.out.bytes",
|
|
internal: "network.interface.tx",
|
|
units: "bytes",
|
|
derive: "rate",
|
|
threshold: 200
|
|
};
|
|
|
|
var tx_plot_options = plot.plot_simple_template();
|
|
$.extend(tx_plot_options.yaxis, { tickFormatter: plot.format_bits_per_sec_tick_no_unit });
|
|
$.extend(tx_plot_options.grid, {
|
|
hoverable: true,
|
|
autoHighlight: false
|
|
});
|
|
tx_plot_options.setup_hook = network_plot_setup_hook;
|
|
tx_plot_options.post_hook = make_network_plot_post_hook("#networking-tx-unit");
|
|
this.tx_plot = new plot.Plot($("#networking-tx-graph"), 300);
|
|
this.tx_plot.set_options(tx_plot_options);
|
|
this.tx_series = this.tx_plot.add_metrics_stacked_instances_series(tx_plot_data, { });
|
|
this.tx_plot.start_walking();
|
|
$(this.tx_series).on('hover', highlight_netdev_row);
|
|
|
|
$(cockpit).on('resize', function () {
|
|
self.rx_plot.resize();
|
|
self.tx_plot.resize();
|
|
});
|
|
|
|
var plot_controls = plot.setup_plot_controls($('#networking'), $('#networking-graph-toolbar'));
|
|
plot_controls.reset([this.rx_plot, this.tx_plot]);
|
|
|
|
ensure_usage_monitor();
|
|
$(usage_grid).on('notify', function (event, index, count) {
|
|
handle_usage_samples();
|
|
});
|
|
|
|
function handle_usage_samples() {
|
|
// console.log(JSON.stringify(usage_samples));
|
|
for (var iface in usage_samples) {
|
|
var samples = usage_samples[iface];
|
|
var rx = samples[0][0];
|
|
var tx = samples[1][0];
|
|
var row = $('#networking-interfaces tr[data-sample-id="' + encodeURIComponent(iface) + '"]');
|
|
if (rx !== undefined && tx !== undefined && row.length > 0) {
|
|
row.find('td:nth-child(3)').text(cockpit.format_bits_per_sec(tx * 8));
|
|
row.find('td:nth-child(4)').text(cockpit.format_bits_per_sec(rx * 8));
|
|
}
|
|
}
|
|
}
|
|
|
|
$(window).on('resize', function () {
|
|
self.rx_plot.resize();
|
|
self.tx_plot.resize();
|
|
});
|
|
},
|
|
|
|
enter: function () {
|
|
this.log_box = journal.logbox(["_SYSTEMD_UNIT=NetworkManager.service",
|
|
"_SYSTEMD_UNIT=firewalld.service"], 10);
|
|
$('#networking-log').empty()
|
|
.append(this.log_box);
|
|
|
|
$(this.model).on('changed.networking', $.proxy(this, "update_devices"));
|
|
this.update_devices();
|
|
},
|
|
|
|
show: function() {
|
|
this.rx_plot.resize();
|
|
this.tx_plot.resize();
|
|
},
|
|
|
|
leave: function() {
|
|
if (this.log_box)
|
|
this.log_box.stop();
|
|
|
|
$(this.model).off(".networking");
|
|
},
|
|
|
|
update_devices: function() {
|
|
var self = this;
|
|
var managed_tbody, unmanaged_tbody;
|
|
|
|
managed_tbody = $('#networking-interfaces tbody');
|
|
managed_tbody.empty();
|
|
|
|
unmanaged_tbody = $('#networking-unmanaged-interfaces tbody');
|
|
unmanaged_tbody.empty();
|
|
$('#networking-unmanaged-interfaces').hide();
|
|
|
|
self.model.list_interfaces().forEach(function (iface) {
|
|
function has_master(iface) {
|
|
return ((iface.Device &&
|
|
iface.Device.ActiveConnection &&
|
|
iface.Device.ActiveConnection.Master &&
|
|
iface.Device.ActiveConnection.Master.Slaves.length > 0) ||
|
|
(iface.MainConnection &&
|
|
iface.MainConnection.Masters.length > 0));
|
|
}
|
|
|
|
// Skip loopback
|
|
if (iface.Device && iface.Device.DeviceType == 'loopback')
|
|
return;
|
|
|
|
// Skip slaves
|
|
if (has_master(iface))
|
|
return;
|
|
|
|
var dev = iface.Device;
|
|
var show_traffic = (dev && (dev.State == 100 || dev.State == 10) && dev.Carrier === true);
|
|
|
|
self.rx_series.add_instance(iface.Name);
|
|
self.tx_series.add_instance(iface.Name);
|
|
add_usage_monitor(iface.Name);
|
|
|
|
var row = $('<tr>', {
|
|
"data-interface": encodeURIComponent(iface.Name),
|
|
"data-sample-id": show_traffic ? encodeURIComponent(iface.Name) : null
|
|
})
|
|
.append($('<td>').text(iface.Name),
|
|
$('<td>').html(render_active_connection(dev, false, true)),
|
|
(show_traffic
|
|
? [$('<td>').text(""), $('<td>').text("")]
|
|
: $('<td colspan="2">').text(device_state_text(dev))));
|
|
|
|
if (!dev || dev.Managed) {
|
|
managed_tbody.append(row.click(function () {
|
|
cockpit.location.go([iface.Name]);
|
|
}));
|
|
} else {
|
|
unmanaged_tbody.append(row);
|
|
$('#networking-unmanaged-interfaces').show();
|
|
}
|
|
});
|
|
},
|
|
|
|
add_bond: function () {
|
|
var iface, i, uuid;
|
|
|
|
uuid = generate_uuid();
|
|
for (i = 0; i < 100; i++) {
|
|
iface = "bond" + i;
|
|
if (!this.model.find_interface(iface))
|
|
break;
|
|
}
|
|
|
|
PageNetworkBondSettings.model = this.model;
|
|
PageNetworkBondSettings.done = null;
|
|
PageNetworkBondSettings.connection = null;
|
|
PageNetworkBondSettings.apply_settings = settings_applier(this.model);
|
|
PageNetworkBondSettings.ghost_settings =
|
|
{
|
|
connection: {
|
|
id: iface,
|
|
autoconnect: true,
|
|
type: "bond",
|
|
uuid: uuid,
|
|
interface_name: iface
|
|
},
|
|
bond: {
|
|
options: {
|
|
mode: "active-backup"
|
|
},
|
|
interface_name: iface
|
|
}
|
|
};
|
|
|
|
$('#network-bond-settings-dialog').modal('show');
|
|
},
|
|
|
|
add_team: function () {
|
|
var iface, i, uuid;
|
|
|
|
uuid = generate_uuid();
|
|
for (i = 0; i < 100; i++) {
|
|
iface = "team" + i;
|
|
if (!this.model.find_interface(iface))
|
|
break;
|
|
}
|
|
|
|
PageNetworkTeamSettings.model = this.model;
|
|
PageNetworkTeamSettings.done = null;
|
|
PageNetworkTeamSettings.connection = null;
|
|
PageNetworkTeamSettings.apply_settings = settings_applier(this.model);
|
|
PageNetworkTeamSettings.ghost_settings =
|
|
{
|
|
connection: {
|
|
id: iface,
|
|
autoconnect: true,
|
|
type: "team",
|
|
uuid: uuid,
|
|
interface_name: iface
|
|
},
|
|
team: {
|
|
config: { },
|
|
interface_name: iface
|
|
}
|
|
};
|
|
|
|
$('#network-team-settings-dialog').modal('show');
|
|
},
|
|
|
|
add_bridge: function () {
|
|
var iface, i, uuid;
|
|
|
|
uuid = generate_uuid();
|
|
for (i = 0; i < 100; i++) {
|
|
iface = "bridge" + i;
|
|
if (!this.model.find_interface(iface))
|
|
break;
|
|
}
|
|
|
|
PageNetworkBridgeSettings.model = this.model;
|
|
PageNetworkBridgeSettings.done = null;
|
|
PageNetworkBridgeSettings.connection = null;
|
|
PageNetworkBridgeSettings.apply_settings = settings_applier(this.model);
|
|
PageNetworkBridgeSettings.ghost_settings =
|
|
{
|
|
connection: {
|
|
id: iface,
|
|
autoconnect: true,
|
|
type: "bridge",
|
|
uuid: uuid,
|
|
interface_name: iface
|
|
},
|
|
bridge: {
|
|
interface_name: iface,
|
|
stp: false,
|
|
priority: 32768,
|
|
forward_delay: 15,
|
|
hello_time: 2,
|
|
max_age: 20,
|
|
ageing_time: 300
|
|
}
|
|
};
|
|
|
|
$('#network-bridge-settings-dialog').modal('show');
|
|
},
|
|
|
|
add_vlan: function () {
|
|
var uuid;
|
|
|
|
uuid = generate_uuid();
|
|
|
|
PageNetworkVlanSettings.model = this.model;
|
|
PageNetworkVlanSettings.done = null;
|
|
PageNetworkVlanSettings.connection = null;
|
|
PageNetworkVlanSettings.apply_settings = settings_applier(this.model);
|
|
PageNetworkVlanSettings.ghost_settings =
|
|
{
|
|
connection: {
|
|
id: "",
|
|
autoconnect: true,
|
|
type: "vlan",
|
|
uuid: uuid,
|
|
interface_name: ""
|
|
},
|
|
vlan: {
|
|
interface_name: "",
|
|
parent: ""
|
|
}
|
|
};
|
|
|
|
$('#network-vlan-settings-dialog').modal('show');
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworking(model) {
|
|
this._init(model);
|
|
}
|
|
|
|
var ipv4_method_choices =
|
|
[
|
|
{ choice: 'auto', title: _("Automatic (DHCP)") },
|
|
{ choice: 'link-local', title: _("Link local") },
|
|
{ choice: 'manual', title: _("Manual") },
|
|
{ choice: 'shared', title: _("Shared") },
|
|
{ choice: 'disabled', title: _("Disabled") }
|
|
];
|
|
|
|
var ipv6_method_choices =
|
|
[
|
|
{ choice: 'auto', title: _("Automatic") },
|
|
{ choice: 'dhcp', title: _("Automatic (DHCP only)") },
|
|
{ choice: 'link-local', title: _("Link local") },
|
|
{ choice: 'manual', title: _("Manual") },
|
|
{ choice: 'ignore', title: _("Ignore") }
|
|
];
|
|
|
|
var bond_mode_choices =
|
|
[
|
|
{ choice: 'balance-rr', title: _("Round Robin") },
|
|
{ choice: 'active-backup', title: _("Active Backup") },
|
|
{ choice: 'balance-xor', title: _("XOR") },
|
|
{ choice: 'broadcast', title: _("Broadcast") },
|
|
{ choice: '802.3ad', title: _("802.3ad") },
|
|
{ choice: 'balance-tlb', title: _("Adaptive transmit load balancing") },
|
|
{ choice: 'balance-alb', title: _("Adaptive load balancing") }
|
|
];
|
|
|
|
var bond_monitoring_choices =
|
|
[
|
|
{ choice: 'mii', title: _("MII (Recommended)") },
|
|
{ choice: 'arp', title: _("ARP") }
|
|
];
|
|
|
|
var team_runner_choices =
|
|
[
|
|
{ choice: 'roundrobin', title: _("Round Robin") },
|
|
{ choice: 'activebackup', title: _("Active Backup") },
|
|
{ choice: 'loadbalance', title: _("Load Balancing") },
|
|
{ choice: 'broadcast', title: _("Broadcast") },
|
|
{ choice: 'lacp', title: _("802.3ad LACP") },
|
|
];
|
|
|
|
var team_balancer_choices =
|
|
[
|
|
{ choice: 'none', title: _("Passive") },
|
|
{ choice: 'basic', title: _("Active") }
|
|
];
|
|
|
|
var team_watch_choices =
|
|
[
|
|
{ choice: 'ethtool', title: _("Ethtool") },
|
|
{ choice: 'arp-ping', title: _("ARP Ping") },
|
|
{ choice: 'nsna-ping', title: _("NSNA Ping") }
|
|
];
|
|
|
|
function choice_title(choices, choice, def) {
|
|
for (var 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 wont 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 chosing 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
|
|
* 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.
|
|
*/
|
|
|
|
var curtain_time = 1.5;
|
|
var settle_time = 1.0;
|
|
var rollback_time = 7.0;
|
|
|
|
function with_checkpoint(model, modify, options) {
|
|
var manager = model.get_manager();
|
|
var curtain = $('#testing-connection-curtain');
|
|
var curtain_testing = $('#testing-connection-curtain-testing');
|
|
var curtain_restoring = $('#testing-connection-curtain-restoring');
|
|
var dialog = $('#confirm-breaking-change-popup');
|
|
|
|
var curtain_timeout;
|
|
var curtain_title_timeout;
|
|
|
|
function show_curtain() {
|
|
cockpit.hint("ignore_transport_health_check", { data: true });
|
|
curtain_timeout = window.setTimeout(function () {
|
|
curtain_timeout = null;
|
|
curtain_testing.show();
|
|
curtain_restoring.hide();
|
|
curtain.show();
|
|
}, curtain_time * 1000);
|
|
curtain_title_timeout = window.setTimeout(function () {
|
|
curtain_title_timeout = null;
|
|
curtain_testing.hide();
|
|
curtain_restoring.show();
|
|
}, 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);
|
|
curtain.hide();
|
|
cockpit.hint("ignore_transport_health_check", { data: false });
|
|
}
|
|
|
|
// 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
|
|
|
|
if (options.hack_does_add_or_remove) {
|
|
modify();
|
|
return;
|
|
}
|
|
|
|
manager.checkpoint_create(options.devices || [], rollback_time)
|
|
.done(function (cp) {
|
|
if (!cp) {
|
|
modify();
|
|
return;
|
|
}
|
|
|
|
show_curtain();
|
|
modify()
|
|
.then(function () {
|
|
window.setTimeout(function () {
|
|
manager.checkpoint_destroy(cp)
|
|
.always(hide_curtain)
|
|
.fail(function () {
|
|
dialog.find('#confirm-breaking-change-text').html(options.fail_text);
|
|
dialog.find('.modal-footer .btn-danger')
|
|
.off('click')
|
|
.text(options.anyway_text)
|
|
.syn_click(model, function () {
|
|
dialog.modal('hide');
|
|
modify();
|
|
});
|
|
dialog.modal('show');
|
|
});
|
|
}, 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);
|
|
});
|
|
});
|
|
}
|
|
|
|
PageNetworkInterface.prototype = {
|
|
_init: function (model) {
|
|
this.id = "network-interface";
|
|
this.model = model;
|
|
},
|
|
|
|
getTitle: function() {
|
|
return C_("page-title", "Networking");
|
|
},
|
|
|
|
setup: function () {
|
|
var self = this;
|
|
|
|
$('#network-interface .breadcrumb a').on("click", function() {
|
|
cockpit.location.go('/');
|
|
});
|
|
|
|
$("#networking-firewall-link, #networking-firewall-summary").on("click", function() {
|
|
cockpit.jump("/network/firewall", cockpit.transport.host);
|
|
return false;
|
|
});
|
|
|
|
$('#network-interface-delete').syn_click(self.model, $.proxy(this, "delete_connections"));
|
|
|
|
function highlight_netdev_row(event, id) {
|
|
$('#network-interface-slaves tr').removeClass('highlight-ct');
|
|
if (id) {
|
|
$('#network-interface-slaves tr[data-interface="' + encodeURIComponent(id) + '"]').addClass('highlight-ct');
|
|
}
|
|
}
|
|
|
|
var rx_plot_data = {
|
|
direct: "network.interface.in.bytes",
|
|
internal: "network.interface.rx",
|
|
units: "bytes",
|
|
derive: "rate"
|
|
};
|
|
|
|
var rx_plot_options = plot.plot_simple_template();
|
|
$.extend(rx_plot_options.yaxis, { tickFormatter: plot.format_bits_per_sec_tick_no_unit });
|
|
$.extend(rx_plot_options.grid, {
|
|
hoverable: true,
|
|
autoHighlight: false
|
|
});
|
|
rx_plot_options.setup_hook = network_plot_setup_hook;
|
|
rx_plot_options.post_hook = make_network_plot_post_hook("#network-interface-rx-unit");
|
|
this.rx_plot = new plot.Plot($("#network-interface-rx-graph"), 300);
|
|
this.rx_plot.set_options(rx_plot_options);
|
|
this.rx_series = this.rx_plot.add_metrics_stacked_instances_series(rx_plot_data, { });
|
|
this.rx_plot.start_walking();
|
|
$(this.rx_series).on('hover', highlight_netdev_row);
|
|
|
|
var tx_plot_data = {
|
|
direct: "network.interface.out.bytes",
|
|
internal: "network.interface.tx",
|
|
units: "bytes",
|
|
derive: "rate"
|
|
};
|
|
|
|
var tx_plot_options = plot.plot_simple_template();
|
|
$.extend(tx_plot_options.yaxis, { tickFormatter: plot.format_bits_per_sec_tick_no_unit });
|
|
$.extend(tx_plot_options.grid, {
|
|
hoverable: true,
|
|
autoHighlight: false
|
|
});
|
|
tx_plot_options.setup_hook = network_plot_setup_hook;
|
|
tx_plot_options.post_hook = make_network_plot_post_hook("#network-interface-tx-unit");
|
|
this.tx_plot = new plot.Plot($("#network-interface-tx-graph"), 300);
|
|
this.tx_plot.set_options(tx_plot_options);
|
|
this.tx_series = this.tx_plot.add_metrics_stacked_instances_series(tx_plot_data, { });
|
|
this.tx_plot.start_walking();
|
|
$(this.tx_series).on('hover', highlight_netdev_row);
|
|
|
|
$(cockpit).on('resize', function () {
|
|
self.rx_plot.resize();
|
|
self.tx_plot.resize();
|
|
});
|
|
|
|
var plot_controls = plot.setup_plot_controls($('#network-interface'), $('#network-interface-graph-toolbar'));
|
|
plot_controls.reset([this.rx_plot, this.tx_plot]);
|
|
|
|
ensure_usage_monitor();
|
|
$(usage_grid).on('notify', function (event, index, count) {
|
|
handle_usage_samples();
|
|
});
|
|
|
|
function handle_usage_samples() {
|
|
// console.log(usage_samples);
|
|
for (var iface in usage_samples) {
|
|
var samples = usage_samples[iface];
|
|
var rx = samples[0][0];
|
|
var tx = samples[1][0];
|
|
var row = $('#network-interface-slaves tr[data-sample-id="' + encodeURIComponent(iface) + '"]');
|
|
if (row.length > 0) {
|
|
row.find('td:nth-child(2)').text(cockpit.format_bits_per_sec(tx * 8));
|
|
row.find('td:nth-child(3)').text(cockpit.format_bits_per_sec(rx * 8));
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderFirewallState(pending) {
|
|
ReactDOM.render(
|
|
React.createElement(OnOffSwitch, {
|
|
id: 'networking-firewall-switch',
|
|
state: firewall.enabled,
|
|
disabled: pending,
|
|
onChange: onFirewallSwitchChange
|
|
}),
|
|
document.querySelector('#networking-firewall .panel-actions')
|
|
);
|
|
}
|
|
|
|
function onFirewallSwitchChange(enable) {
|
|
renderFirewallState(true);
|
|
if (enable)
|
|
firewall.enable().then(() => renderFirewallState());
|
|
else
|
|
firewall.disable().then(() => renderFirewallState());
|
|
}
|
|
|
|
function onFirewallChanged() {
|
|
if (!firewall.installed) {
|
|
$('#networking-firewall').hide();
|
|
return;
|
|
}
|
|
|
|
$('#networking-firewall').show();
|
|
renderFirewallState();
|
|
|
|
const summary = cockpit.format(cockpit.ngettext(_("$0 Active Zone"), _("$0 Active Zones"), firewall.activeZones.size), firewall.activeZones.size);
|
|
|
|
$('#networking-firewall-summary').text(summary);
|
|
}
|
|
|
|
firewall.addEventListener('changed', onFirewallChanged);
|
|
onFirewallChanged();
|
|
|
|
$(window).on('resize', function () {
|
|
self.rx_plot.resize();
|
|
self.tx_plot.resize();
|
|
});
|
|
},
|
|
|
|
enter: function (dev_name) {
|
|
var self = this;
|
|
|
|
$(self.model).on('changed.network-interface', $.proxy(self, "update"));
|
|
|
|
self.dev_name = dev_name;
|
|
|
|
$('#network-interface .breadcrumb .active').text(self.dev_name);
|
|
|
|
self.rx_series.clear_instances();
|
|
self.tx_series.clear_instances();
|
|
|
|
$('#network-interface-delete').hide();
|
|
self.dev = null;
|
|
self.update();
|
|
},
|
|
|
|
show: function() {
|
|
this.rx_plot.resize();
|
|
this.tx_plot.resize();
|
|
},
|
|
|
|
leave: function() {
|
|
$(this.model).off(".network-interface");
|
|
this.dev = null;
|
|
},
|
|
|
|
show_dialog: function(dialog, id) {
|
|
var self = this;
|
|
var con = self.main_connection;
|
|
var dev = self.dev;
|
|
|
|
function reactivate_connection() {
|
|
if (con && dev && dev.ActiveConnection && dev.ActiveConnection.Connection === con) {
|
|
if (con.Settings.connection.interface_name &&
|
|
con.Settings.connection.interface_name != dev.Interface) {
|
|
return dev.disconnect().then(function () { return con.activate(null, null) })
|
|
.fail(show_unexpected_error);
|
|
} else {
|
|
return con.activate(dev, null)
|
|
.fail(show_unexpected_error);
|
|
}
|
|
}
|
|
}
|
|
|
|
dialog.model = self.model;
|
|
dialog.connection = self.main_connection;
|
|
dialog.ghost_settings = self.ghost_settings;
|
|
dialog.apply_settings = settings_applier(self.model, self.dev, con);
|
|
dialog.done = reactivate_connection;
|
|
$(id).modal('show');
|
|
},
|
|
|
|
set_mac: function() {
|
|
this.show_dialog(PageNetworkMacSettings, "#network-mac-settings-dialog");
|
|
},
|
|
|
|
delete_connections: function() {
|
|
var self = this;
|
|
|
|
function delete_connection_and_slaves(con) {
|
|
return Promise.all(con.Slaves.map(s => free_slave_connection(s))).then(() => con.delete_());
|
|
}
|
|
|
|
function delete_connections(cons) {
|
|
return Promise.all(cons.map(delete_connection_and_slaves));
|
|
}
|
|
|
|
function delete_iface_connections(iface) {
|
|
return delete_connections(iface.Connections);
|
|
}
|
|
|
|
var location = cockpit.location;
|
|
|
|
function modify () {
|
|
return delete_iface_connections(self.iface)
|
|
.then(function () {
|
|
location.go("/");
|
|
})
|
|
.catch(show_unexpected_error);
|
|
}
|
|
|
|
if (self.iface) {
|
|
with_checkpoint(self.model, modify,
|
|
{
|
|
devices: self.dev ? [self.dev] : [],
|
|
fail_text: cockpit.format(_("Deleting <b>$0</b> will break the connection to the server, and will make the administration UI unavailable."), self.dev_name),
|
|
anyway_text: cockpit.format(_("Delete $0"), self.dev_name),
|
|
hack_does_add_or_remove: true,
|
|
rollback_on_failure: true
|
|
});
|
|
}
|
|
},
|
|
|
|
connect: function() {
|
|
var self = this;
|
|
|
|
if (!self.main_connection && !(self.dev && self.ghost_settings)) {
|
|
self.update();
|
|
return;
|
|
}
|
|
|
|
function fail(error) {
|
|
show_unexpected_error(error);
|
|
self.update();
|
|
}
|
|
|
|
function modify() {
|
|
if (self.main_connection) {
|
|
return self.main_connection.activate(self.dev, null).fail(fail);
|
|
} else {
|
|
return self.dev.activate_with_settings(self.ghost_settings, null).fail(fail);
|
|
}
|
|
}
|
|
|
|
with_checkpoint(self.model, modify,
|
|
{
|
|
devices: self.dev ? [self.dev] : [],
|
|
fail_text: cockpit.format(_("Switching on <b>$0</b> will break the connection to the server, and will make the administration UI unavailable."), self.dev_name),
|
|
anyway_text: cockpit.format(_("Switch on $0"), self.dev_name)
|
|
});
|
|
},
|
|
|
|
disconnect: function() {
|
|
var self = this;
|
|
|
|
if (!self.dev) {
|
|
console.log("Trying to switch off without a device?");
|
|
self.update();
|
|
return;
|
|
}
|
|
|
|
function modify () {
|
|
return self.dev.disconnect()
|
|
.fail(function (error) {
|
|
show_unexpected_error(error);
|
|
self.update();
|
|
});
|
|
}
|
|
|
|
with_checkpoint(self.model, modify,
|
|
{
|
|
devices: [self.dev],
|
|
fail_text: cockpit.format(_("Switching off <b>$0</b> will break the connection to the server, and will make the administration UI unavailable."), self.dev_name),
|
|
anyway_text: cockpit.format(_("Switch off $0"), self.dev_name)
|
|
});
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var iface = self.model.find_interface(self.dev_name);
|
|
var dev = iface && iface.Device;
|
|
var managed = iface && (!dev || dev.Managed);
|
|
|
|
self.iface = iface;
|
|
self.dev = dev;
|
|
|
|
var desc, cs;
|
|
if (dev) {
|
|
if (dev.DeviceType == 'ethernet' || dev.IdVendor || dev.IdModel) {
|
|
desc = cockpit.format("$IdVendor $IdModel $Driver", dev);
|
|
} else if (dev.DeviceType == 'bond') {
|
|
desc = _("Bond");
|
|
} else if (dev.DeviceType == 'team') {
|
|
desc = _("Team");
|
|
} else if (dev.DeviceType == 'vlan') {
|
|
desc = _("VLAN");
|
|
} else if (dev.DeviceType == 'bridge') {
|
|
desc = _("Bridge");
|
|
} else
|
|
desc = cockpit.format(_("Unknown \"$0\""), dev.DeviceType);
|
|
} else if (iface) {
|
|
cs = connection_settings(iface.Connections[0]);
|
|
if (cs.type == "bond")
|
|
desc = _("Bond");
|
|
else if (cs.type == "team")
|
|
desc = _("Team");
|
|
else if (cs.type == "vlan")
|
|
desc = _("VLAN");
|
|
else if (cs.type == "bridge")
|
|
desc = _("Bridge");
|
|
else if (cs.type)
|
|
desc = cockpit.format(_("Unknown \"$0\""), cs.type);
|
|
else
|
|
desc = _("Unknown");
|
|
} else
|
|
desc = _("Unknown");
|
|
|
|
$('#network-interface-name').text(self.dev_name);
|
|
$('#network-interface-hw').text(desc);
|
|
|
|
var mac;
|
|
if (dev &&
|
|
dev.HwAddress) {
|
|
mac = dev.HwAddress;
|
|
} else if (iface &&
|
|
iface.MainConnection &&
|
|
iface.MainConnection.Settings &&
|
|
iface.MainConnection.Settings.ethernet &&
|
|
iface.MainConnection.Settings.ethernet.assigned_mac_address) {
|
|
mac = iface.MainConnection.Settings.ethernet.assigned_mac_address;
|
|
}
|
|
|
|
var can_edit_mac = (iface && iface.MainConnection &&
|
|
(connection_settings(iface.MainConnection).type == "802-3-ethernet" ||
|
|
connection_settings(iface.MainConnection).type == "bond"));
|
|
|
|
$('#network-interface-mac').empty();
|
|
if (can_edit_mac) {
|
|
$('#network-interface-mac').append(
|
|
$('<a tabindex="0">')
|
|
.text(mac)
|
|
.syn_click(self.model, function () {
|
|
self.set_mac();
|
|
}));
|
|
} else {
|
|
$('#network-interface-mac').text(mac);
|
|
}
|
|
|
|
/* Disable the On/Off button for interfaces that we don't know about at all,
|
|
and for devices that NM declares to be unavailable. Neither can be activated.
|
|
*/
|
|
var onoff = null;
|
|
if (managed) {
|
|
onoff = React.createElement(OnOffSwitch, {
|
|
state: !!(dev && dev.ActiveConnection),
|
|
disabled: !iface || (dev && dev.State == 20),
|
|
onChange: enable => enable ? self.connect() : self.disconnect()
|
|
});
|
|
}
|
|
ReactDOM.render(onoff, document.getElementById('network-interface-delete-switch'));
|
|
|
|
var is_deletable = (iface && !dev) || (dev && (dev.DeviceType == 'bond' ||
|
|
dev.DeviceType == 'team' ||
|
|
dev.DeviceType == 'vlan' ||
|
|
dev.DeviceType == 'bridge'));
|
|
$('#network-interface-delete').toggle(is_deletable && managed);
|
|
|
|
function render_carrier_status_row() {
|
|
if (dev && dev.Carrier !== undefined) {
|
|
return $('<tr>').append(
|
|
$('<td>').text(_("Carrier")),
|
|
$('<td>').append(
|
|
dev.Carrier
|
|
? (dev.Speed ? cockpit.format_bits_per_sec(dev.Speed * 1e6) : _("Yes"))
|
|
: _("No")));
|
|
} else
|
|
return null;
|
|
}
|
|
|
|
function render_active_status_row() {
|
|
var state;
|
|
|
|
if (self.main_connection && self.main_connection.Masters.length > 0)
|
|
return null;
|
|
|
|
if (!dev)
|
|
state = _("Inactive");
|
|
else if (managed && dev.State != 100)
|
|
state = dev.StateText;
|
|
else
|
|
state = null;
|
|
|
|
return $('<tr>').append(
|
|
$('<td>').text(_("Status")),
|
|
$('<td>').append(
|
|
render_active_connection(dev, true, false),
|
|
" ",
|
|
state ? $('<span>').text(state) : null));
|
|
}
|
|
|
|
function render_connection_settings_rows(con, settings) {
|
|
if (!managed) {
|
|
return $('<tr>').append(
|
|
$('<td>'),
|
|
$('<td>').text(_("This device cannot be managed here.")));
|
|
}
|
|
|
|
if (!settings)
|
|
return [];
|
|
|
|
var master_settings = null;
|
|
if (con && con.Masters.length > 0)
|
|
master_settings = con.Masters[0].Settings;
|
|
|
|
function render_ip_settings(topic) {
|
|
var params = settings[topic];
|
|
var parts = [];
|
|
|
|
if (params.method != "manual")
|
|
parts.push(choice_title((topic == "ipv4") ? ipv4_method_choices : ipv6_method_choices,
|
|
params.method, _("Unknown configuration")));
|
|
|
|
var addr_is_extra = (params.method != "manual");
|
|
var addrs = [];
|
|
params.addresses.forEach(function (a) {
|
|
var addr = a[0] + "/" + a[1];
|
|
if (a[2] && a[2] != "0.0.0.0" && a[2] != "0:0:0:0:0:0:0:0")
|
|
addr += " via " + a[2];
|
|
addrs.push(addr);
|
|
});
|
|
if (addrs.length > 0)
|
|
parts.push(cockpit.format(addr_is_extra ? _("Additional address $val") : _("Address $val"),
|
|
{ val: addrs.join(", ") }));
|
|
|
|
var dns_is_extra = (!params["ignore-auto-dns"] && params.method != "manual");
|
|
if (params.dns.length > 0)
|
|
parts.push(cockpit.format(dns_is_extra ? _("Additional DNS $val") : _("DNS $val"),
|
|
{ val: params.dns.join(", ") }));
|
|
if (params.dns_search.length > 0)
|
|
parts.push(cockpit.format(dns_is_extra ? _("Additional DNS Search Domains $val") : _("DNS Search Domains $val"),
|
|
{ val: params.dns_search.join(", ") }));
|
|
|
|
return parts;
|
|
}
|
|
|
|
function configure_ip_settings(topic) {
|
|
PageNetworkIpSettings.topic = topic;
|
|
self.show_dialog(PageNetworkIpSettings, '#network-ip-settings-dialog');
|
|
}
|
|
|
|
function configure_bond_settings() {
|
|
self.show_dialog(PageNetworkBondSettings, '#network-bond-settings-dialog');
|
|
}
|
|
|
|
function configure_team_settings() {
|
|
self.show_dialog(PageNetworkTeamSettings, '#network-team-settings-dialog');
|
|
}
|
|
|
|
function configure_team_port_settings() {
|
|
PageNetworkTeamPortSettings.master_settings = master_settings;
|
|
self.show_dialog(PageNetworkTeamPortSettings, '#network-teamport-settings-dialog');
|
|
}
|
|
|
|
function configure_bridge_settings() {
|
|
self.show_dialog(PageNetworkBridgeSettings, '#network-bridge-settings-dialog');
|
|
}
|
|
|
|
function configure_bridge_port_settings() {
|
|
self.show_dialog(PageNetworkBridgePortSettings, '#network-bridgeport-settings-dialog');
|
|
}
|
|
|
|
function configure_vlan_settings() {
|
|
self.show_dialog(PageNetworkVlanSettings, '#network-vlan-settings-dialog');
|
|
}
|
|
|
|
function configure_mtu_settings() {
|
|
self.show_dialog(PageNetworkMtuSettings, '#network-mtu-settings-dialog');
|
|
}
|
|
|
|
function render_autoconnect_row() {
|
|
if (settings.connection.autoconnect !== undefined) {
|
|
return (
|
|
$('<tr>').append(
|
|
$('<td>').text(_("General")),
|
|
$('<td class="networking-controls">').append(
|
|
$('<label for="autoreconnect">').append(
|
|
$('<input type="checkbox" id="autoreconnect">')
|
|
.prop('checked', settings.connection.autoconnect)
|
|
.change(function () {
|
|
settings.connection.autoconnect = $(this).prop('checked');
|
|
settings_applier(self.model, self.dev, con)(settings);
|
|
}),
|
|
$('<span>').text(_("Connect automatically")))))
|
|
);
|
|
}
|
|
}
|
|
|
|
function render_settings_row(title, rows, configure) {
|
|
var link_text = [];
|
|
for (var i = 0; i < rows.length; i++) {
|
|
link_text.push(rows[i]);
|
|
if (i < rows.length - 1)
|
|
link_text.push($('<br>'));
|
|
}
|
|
if (link_text.length === 0)
|
|
link_text.push(_("Configure"));
|
|
|
|
return $('<tr>').append(
|
|
$('<td>')
|
|
.text(title)
|
|
.css('vertical-align', rows.length > 1 ? "top" : "center"),
|
|
$('<td>').append(
|
|
$('<a tabindex="0" class="network-privileged">')
|
|
.append(link_text)
|
|
.syn_click(self.model, function () { configure() })));
|
|
}
|
|
|
|
function render_ip_settings_row(topic, title) {
|
|
if (!settings[topic])
|
|
return null;
|
|
|
|
return render_settings_row(title, render_ip_settings(topic),
|
|
function () { configure_ip_settings(topic) });
|
|
}
|
|
|
|
function render_mtu_settings_row() {
|
|
var rows = [];
|
|
var options = settings.ethernet;
|
|
|
|
if (!options)
|
|
return null;
|
|
|
|
function add_row(fmt, args) {
|
|
rows.push(cockpit.format(fmt, args));
|
|
}
|
|
|
|
if (options.mtu)
|
|
add_row(_("$mtu"), options);
|
|
else
|
|
add_row(_("Automatic"), options);
|
|
|
|
return render_settings_row(_("MTU"), rows, configure_mtu_settings);
|
|
}
|
|
|
|
function render_master() {
|
|
if (con && con.Masters.length > 0) {
|
|
return $('<tr>').append(
|
|
$('<td>').text(_("Master")),
|
|
$('<td>').append(
|
|
array_join(con.Masters.map(render_connection_link), ", ")));
|
|
} else
|
|
return null;
|
|
}
|
|
|
|
function render_bond_settings_row() {
|
|
var parts = [];
|
|
var rows = [];
|
|
var options;
|
|
|
|
if (!settings.bond)
|
|
return null;
|
|
|
|
options = settings.bond.options;
|
|
|
|
parts.push(choice_title(bond_mode_choices, options.mode, options.mode));
|
|
if (options.arp_interval)
|
|
parts.push(_("ARP Monitoring"));
|
|
|
|
if (parts.length > 0)
|
|
rows.push(parts.join(", "));
|
|
|
|
return render_settings_row(_("Bond"), rows, configure_bond_settings);
|
|
}
|
|
|
|
function render_team_settings_row() {
|
|
var parts = [];
|
|
var rows = [];
|
|
|
|
if (!settings.team)
|
|
return null;
|
|
|
|
var config = settings.team.config;
|
|
|
|
if (config === null)
|
|
parts.push(_("Broken configuration"));
|
|
else {
|
|
if (config.runner)
|
|
parts.push(choice_title(team_runner_choices, config.runner.name, config.runner.name));
|
|
if (config.link_watch && config.link_watch.name != "ethtool")
|
|
parts.push(choice_title(team_watch_choices, config.link_watch.name, config.link_watch.name));
|
|
}
|
|
|
|
if (parts.length > 0)
|
|
rows.push(parts.join(", "));
|
|
return render_settings_row(_("Team"), rows, configure_team_settings);
|
|
}
|
|
|
|
function render_team_port_settings_row() {
|
|
var parts = [];
|
|
var rows = [];
|
|
|
|
if (!settings.team_port)
|
|
return null;
|
|
|
|
/* Only "activebackup" and "lacp" team ports have
|
|
* something to configure.
|
|
*/
|
|
if (!master_settings ||
|
|
!master_settings.team ||
|
|
!master_settings.team.config ||
|
|
!master_settings.team.config.runner ||
|
|
!(master_settings.team.config.runner.name == "activebackup" ||
|
|
master_settings.team.config.runner.name == "lacp"))
|
|
return null;
|
|
|
|
var config = settings.team_port.config;
|
|
|
|
if (config === null)
|
|
parts.push(_("Broken configuration"));
|
|
|
|
if (parts.length > 0)
|
|
rows.push(parts.join(", "));
|
|
return render_settings_row(_("Team Port"), rows, configure_team_port_settings);
|
|
}
|
|
|
|
function render_bridge_settings_row() {
|
|
var rows = [];
|
|
var options = settings.bridge;
|
|
|
|
if (!options)
|
|
return null;
|
|
|
|
function add_row(fmt, args) {
|
|
rows.push(cockpit.format(fmt, args));
|
|
}
|
|
|
|
if (options.stp) {
|
|
add_row(_("Spanning Tree Protocol"));
|
|
if (options.priority != 32768)
|
|
add_row(_("Priority $priority"), options);
|
|
if (options.forward_delay != 15)
|
|
add_row(_("Forward delay $forward_delay"), options);
|
|
if (options.hello_time != 2)
|
|
add_row(_("Hello time $hello_time"), options);
|
|
if (options.max_age != 20)
|
|
add_row(_("Maximum message age $max_age"), options);
|
|
}
|
|
|
|
return render_settings_row(_("Bridge"), rows, configure_bridge_settings);
|
|
}
|
|
|
|
function render_bridge_port_settings_row() {
|
|
var rows = [];
|
|
var options = settings.bridge_port;
|
|
|
|
if (!options)
|
|
return null;
|
|
|
|
function add_row(fmt, args) {
|
|
rows.push(cockpit.format(fmt, args));
|
|
}
|
|
|
|
if (options.priority != 32)
|
|
add_row(_("Priority $priority"), options);
|
|
if (options.path_cost != 100)
|
|
add_row(_("Path cost $path_cost"), options);
|
|
if (options.hairpin_mode)
|
|
add_row(_("Hairpin mode"));
|
|
|
|
return render_settings_row(_("Bridge port"), rows, configure_bridge_port_settings);
|
|
}
|
|
|
|
function render_vlan_settings_row() {
|
|
var rows = [];
|
|
var options = settings.vlan;
|
|
|
|
if (!options)
|
|
return null;
|
|
|
|
function add_row(fmt, args) {
|
|
rows.push(cockpit.format(fmt, args));
|
|
}
|
|
|
|
add_row(_("Parent $parent"), options);
|
|
add_row(_("Id $id"), options);
|
|
|
|
return render_settings_row(_("VLAN"), rows,
|
|
configure_vlan_settings);
|
|
}
|
|
|
|
return [render_master(),
|
|
render_autoconnect_row(),
|
|
render_ip_settings_row("ipv4", _("IPv4")),
|
|
render_ip_settings_row("ipv6", _("IPv6")),
|
|
render_mtu_settings_row(),
|
|
render_vlan_settings_row(),
|
|
render_bridge_settings_row(),
|
|
render_bridge_port_settings_row(),
|
|
render_bond_settings_row(),
|
|
render_team_settings_row(),
|
|
render_team_port_settings_row()
|
|
];
|
|
}
|
|
|
|
function create_ghost_connection_settings() {
|
|
var settings = {
|
|
connection: {
|
|
interface_name: iface.Name
|
|
},
|
|
ipv4: {
|
|
method: "auto",
|
|
addresses: [],
|
|
dns: [],
|
|
dns_search: [],
|
|
routes: []
|
|
},
|
|
ipv6: {
|
|
method: "auto",
|
|
addresses: [],
|
|
dns: [],
|
|
dns_search: [],
|
|
routes: []
|
|
}
|
|
};
|
|
complete_settings(settings, dev);
|
|
return settings;
|
|
}
|
|
|
|
self.ghost_settings = null;
|
|
self.main_connection = null;
|
|
self.connection_settings = null;
|
|
|
|
if (iface) {
|
|
self.main_connection = iface.MainConnection;
|
|
if (self.main_connection) {
|
|
self.connection_settings = self.main_connection.Settings;
|
|
} else {
|
|
self.ghost_settings = create_ghost_connection_settings();
|
|
self.connection_settings = self.ghost_settings;
|
|
}
|
|
}
|
|
|
|
$('#network-interface-settings')
|
|
.empty()
|
|
.append(render_active_status_row())
|
|
.append(render_carrier_status_row())
|
|
.append(render_connection_settings_rows(self.main_connection, self.connection_settings));
|
|
update_network_privileged();
|
|
|
|
function update_connection_slaves(con) {
|
|
var tbody = $('#network-interface-slaves tbody');
|
|
var rows = { };
|
|
var slave_ifaces = { };
|
|
|
|
tbody.empty();
|
|
self.rx_series.clear_instances();
|
|
self.tx_series.clear_instances();
|
|
|
|
var cs = connection_settings(con);
|
|
if (!con || (cs.type != "bond" && cs.type != "team" && cs.type != "bridge")) {
|
|
self.rx_series.add_instance(self.dev_name);
|
|
self.tx_series.add_instance(self.dev_name);
|
|
return;
|
|
}
|
|
|
|
$('#network-interface-slaves thead th:first-child')
|
|
.text(cs.type == "bond" ? _("Interfaces") : _("Ports"));
|
|
|
|
con.Slaves.forEach(function (slave_con) {
|
|
slave_con.Interfaces.forEach(function(iface) {
|
|
if (iface.MainConnection != slave_con)
|
|
return;
|
|
|
|
var dev = iface.Device;
|
|
var is_active = (dev && dev.State == 100 && dev.Carrier === true);
|
|
|
|
/* Unmanaged devices shouldn't show up as slaves
|
|
* but let's not take any chances.
|
|
*/
|
|
if (dev && !dev.Managed)
|
|
return;
|
|
|
|
self.rx_series.add_instance(iface.Name);
|
|
self.tx_series.add_instance(iface.Name);
|
|
add_usage_monitor(iface.Name);
|
|
slave_ifaces[iface.Name] = true;
|
|
|
|
rows[iface.Name] =
|
|
$('<tr>', {
|
|
"data-interface": encodeURIComponent(iface.Name),
|
|
"data-sample-id": is_active ? encodeURIComponent(iface.Name) : null
|
|
})
|
|
.append($('<td>').text(iface.Name),
|
|
(is_active
|
|
? [$('<td>').text(""), $('<td>').text("")]
|
|
: $('<td colspan="2">').text(device_state_text(dev))),
|
|
$('<td class="networking-row-configure">').append(
|
|
switchbox(!!(dev && dev.ActiveConnection), function(val) {
|
|
if (val) {
|
|
with_checkpoint(
|
|
self.model,
|
|
function () {
|
|
return slave_con.activate(dev)
|
|
.fail(show_unexpected_error);
|
|
},
|
|
{
|
|
devices: dev ? [dev] : [],
|
|
fail_text: cockpit.format(_("Switching on <b>$0</b> will break the connection to the server, and will make the administration UI unavailable."), iface.Name),
|
|
anyway_text: cockpit.format(_("Switch on $0"), iface.Name)
|
|
});
|
|
} else if (dev) {
|
|
with_checkpoint(
|
|
self.model,
|
|
function () {
|
|
return dev.disconnect()
|
|
.fail(show_unexpected_error);
|
|
},
|
|
{
|
|
devices: [dev],
|
|
fail_text: cockpit.format(_("Switching off <b>$0</b> will break the connection to the server, and will make the administration UI unavailable."), iface.Name),
|
|
anyway_text: cockpit.format(_("Switch off $0"), iface.Name)
|
|
});
|
|
}
|
|
}, "network-privileged")),
|
|
$('<td width="28px">').append(
|
|
$('<button class="btn btn-default btn-control-ct network-privileged fa fa-minus">')
|
|
.syn_click(self.model, function () {
|
|
with_checkpoint(
|
|
self.model,
|
|
function () {
|
|
return (free_slave_connection(slave_con)
|
|
.fail(show_unexpected_error));
|
|
},
|
|
{
|
|
devices: dev ? [dev] : [],
|
|
fail_text: cockpit.format(_("Removing <b>$0</b> will break the connection to the server, and will make the administration UI unavailable."), iface.Name),
|
|
anyway_text: cockpit.format(_("Remove $0"), iface.Name),
|
|
hack_does_add_or_remove: true
|
|
});
|
|
return false;
|
|
})))
|
|
.click(function (event) {
|
|
// Somehow the clicks on the switchbox
|
|
// bubble up to here. Let's catch them.
|
|
if ($(event.target).hasClass("btn"))
|
|
return;
|
|
cockpit.location.go([iface.Name]);
|
|
});
|
|
});
|
|
});
|
|
|
|
Object.keys(rows).sort()
|
|
.forEach(function(name) {
|
|
tbody.append(rows[name]);
|
|
});
|
|
|
|
var add_btn =
|
|
$('<div>', { class: 'dropdown' }).append(
|
|
$('<button>', {
|
|
class: 'network-privileged btn btn-default btn-control-ct dropdown-toggle fa fa-plus',
|
|
'data-toggle': 'dropdown'
|
|
}),
|
|
$('<ul>', {
|
|
class: 'dropdown-menu add-button',
|
|
role: 'menu'
|
|
})
|
|
.append(
|
|
self.model.list_interfaces().map(function (iface) {
|
|
if (is_interesting_interface(iface) &&
|
|
!slave_ifaces[iface.Name] &&
|
|
iface != self.iface) {
|
|
return $('<li role="presentation">').append(
|
|
$('<a tabindex="0" role="menuitem" class="network-privileged">')
|
|
.text(iface.Name)
|
|
.syn_click(self.model, function () {
|
|
with_checkpoint(
|
|
self.model,
|
|
function () {
|
|
var cs = connection_settings(con);
|
|
return set_slave(self.model, con, con.Settings,
|
|
cs.type, iface.Name, true)
|
|
.fail(show_unexpected_error);
|
|
},
|
|
{
|
|
devices: iface.Device ? [iface.Device] : [],
|
|
fail_text: cockpit.format(_("Adding <b>$0</b> will break the connection to the server, and will make the administration UI unavailable."), iface.Name),
|
|
anyway_text: cockpit.format(_("Add $0"), iface.Name),
|
|
hack_does_add_or_remove: true
|
|
});
|
|
}));
|
|
}
|
|
return null;
|
|
})));
|
|
|
|
$('#network-interface-slaves thead th:nth-child(5)').html(add_btn);
|
|
|
|
$('#network-interface-slaves').show();
|
|
update_network_privileged();
|
|
}
|
|
|
|
$('#network-interface-slaves').hide();
|
|
if (self.main_connection)
|
|
update_connection_slaves(self.main_connection);
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkInterface(model) {
|
|
this._init(model);
|
|
}
|
|
|
|
function switchbox(val, callback) {
|
|
var onoff = $('<span>');
|
|
var enabled = true;
|
|
function render () {
|
|
ReactDOM.render(
|
|
React.createElement(OnOffSwitch, {
|
|
state: val,
|
|
enabled: enabled,
|
|
onChange: callback
|
|
}),
|
|
onoff[0]);
|
|
}
|
|
onoff.enable = function (val) {
|
|
enabled = val;
|
|
render();
|
|
};
|
|
render();
|
|
return onoff;
|
|
}
|
|
|
|
function with_settings_checkpoint(model, modify, options) {
|
|
with_checkpoint(model, modify,
|
|
$.extend(
|
|
{
|
|
fail_text: _("Changing the settings will break the connection to the server, and will make the administration UI unavailable."),
|
|
anyway_text: _("Change the settings"),
|
|
}, options));
|
|
}
|
|
|
|
function show_dialog_error(error_id, error) {
|
|
var msg = error.message || error.toString();
|
|
console.warn(msg);
|
|
$(error_id).show()
|
|
.find('span')
|
|
.text(msg);
|
|
}
|
|
|
|
function connection_devices(con) {
|
|
var devices = [];
|
|
|
|
if (con)
|
|
con.Interfaces.forEach(function (iface) { if (iface.Device) devices.push(iface.Device); });
|
|
|
|
return devices;
|
|
}
|
|
|
|
PageNetworkIpSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-ip-settings-dialog";
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-ip-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-ip-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-ip-settings-error').hide();
|
|
this.settings = PageNetworkIpSettings.ghost_settings || PageNetworkIpSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var topic = PageNetworkIpSettings.topic;
|
|
var params = self.settings[topic];
|
|
|
|
var addresses_table;
|
|
var auto_dns_btn, dns_table;
|
|
var auto_dns_search_btn, dns_search_table;
|
|
var auto_routes_btn, routes_table;
|
|
|
|
function choicebox(p, choices) {
|
|
var btn = select_btn(
|
|
function (choice) {
|
|
params[p] = choice;
|
|
self.update();
|
|
},
|
|
choices);
|
|
btn.addClass("col-left");
|
|
select_btn_select(btn, params[p]);
|
|
return btn;
|
|
}
|
|
|
|
function inverted_switchbox(title, p) {
|
|
var onoff;
|
|
var btn = $('<span>').append(
|
|
$('<span class="inverted-switchbox">').text(title),
|
|
onoff = switchbox(!params[p], function(val) {
|
|
params[p] = !val;
|
|
self.update();
|
|
}));
|
|
btn.enable = function enable(val) {
|
|
onoff.enable(val);
|
|
};
|
|
return btn;
|
|
}
|
|
|
|
function tablebox(title, p, columns, def, header_buttons) {
|
|
var direct = false;
|
|
var add_btn;
|
|
|
|
if (typeof columns == "string") {
|
|
direct = true;
|
|
columns = [columns];
|
|
}
|
|
|
|
function get(i, j) {
|
|
if (direct)
|
|
return params[p][i];
|
|
else
|
|
return params[p][i][j];
|
|
}
|
|
|
|
function set(i, j, val) {
|
|
if (direct)
|
|
params[p][i] = val;
|
|
else
|
|
params[p][i][j] = val;
|
|
}
|
|
|
|
function add() {
|
|
return function() {
|
|
params[p].push(def);
|
|
self.update();
|
|
};
|
|
}
|
|
|
|
function remove(index) {
|
|
return function () {
|
|
params[p].splice(index, 1);
|
|
self.update();
|
|
};
|
|
}
|
|
|
|
var panel =
|
|
$('<div class="network-ip-settings-row">').append(
|
|
$('<div>').append(
|
|
$('<strong>').text(title),
|
|
$('<div class="pull-right">').append(
|
|
header_buttons,
|
|
add_btn = $('<button class="btn btn-default fa fa-plus">')
|
|
.css("margin-left", "10px")
|
|
.click(add()))),
|
|
$('<table width="100%">').append(
|
|
params[p].map(function (a, i) {
|
|
return ($('<tr>').append(
|
|
columns.map(function (c, j) {
|
|
return $('<td>').append(
|
|
$('<input class="form-control">')
|
|
.val(get(i, j))
|
|
.attr('placeholder', c)
|
|
.change(function (event) {
|
|
set(i, j, $(event.target).val());
|
|
}));
|
|
}),
|
|
$('<td>').append(
|
|
$('<button class="btn btn-default fa fa-minus">')
|
|
.click(remove(i)))));
|
|
})));
|
|
|
|
// For testing
|
|
panel.attr("data-field", p);
|
|
|
|
panel.enable_add = function enable_add(val) {
|
|
add_btn.prop('disabled', !val);
|
|
};
|
|
|
|
return panel;
|
|
}
|
|
|
|
function render_ip_settings() {
|
|
var prefix_text = (topic == "ipv4") ? _("Prefix length or Netmask") : _("Prefix length");
|
|
var body =
|
|
$('<div>').append(
|
|
addresses_table = tablebox(_("Addresses"), "addresses", ["Address", prefix_text, "Gateway"],
|
|
["", "", ""],
|
|
choicebox("method", (topic == "ipv4")
|
|
? ipv4_method_choices : ipv6_method_choices)
|
|
.css('display', 'inline-block')),
|
|
$('<br>'),
|
|
dns_table =
|
|
tablebox(_("DNS"), "dns", "Server", "",
|
|
auto_dns_btn = inverted_switchbox(_("Automatic"), "ignore_auto_dns")),
|
|
$('<br>'),
|
|
dns_search_table =
|
|
tablebox(_("DNS Search Domains"), "dns_search", "Search Domain", "",
|
|
auto_dns_search_btn = inverted_switchbox(_("Automatic"),
|
|
"ignore_auto_dns")),
|
|
$('<br>'),
|
|
routes_table =
|
|
tablebox(_("Routes"), "routes",
|
|
["Address", prefix_text, "Gateway", "Metric"], ["", "", "", ""],
|
|
auto_routes_btn = inverted_switchbox(_("Automatic"), "ignore_auto_routes")));
|
|
return body;
|
|
}
|
|
|
|
// The manual method needs at least one address
|
|
//
|
|
if (params.method == "manual" && params.addresses.length === 0)
|
|
params.addresses = [["", "", ""]];
|
|
|
|
// The link local, shared, and disabled methods can't take any
|
|
// addresses, dns servers, or dns search domains. Routes,
|
|
// however, are ok, even for "disabled" and "ignored". But
|
|
// since that doesn't make sense, we remove routes as well for
|
|
// these methods.
|
|
|
|
var is_off = (params.method == "disabled" ||
|
|
params.method == "ignore");
|
|
|
|
var can_have_extra = !(params.method == "link-local" ||
|
|
params.method == "shared" ||
|
|
is_off);
|
|
|
|
if (!can_have_extra) {
|
|
params.addresses = [];
|
|
params.dns = [];
|
|
params.dns_search = [];
|
|
}
|
|
if (is_off) {
|
|
params.routes = [];
|
|
}
|
|
|
|
$('#network-ip-settings-dialog .modal-title').text(
|
|
(topic == "ipv4") ? _("IPv4 Settings") : _("IPv6 Settings"));
|
|
$('#network-ip-settings-body').html(render_ip_settings());
|
|
|
|
// The auto_*_btns only make sense when the address method
|
|
// is "auto" or "dhcp".
|
|
//
|
|
var can_auto = (params.method == "auto" || params.method == "dhcp");
|
|
auto_dns_btn.enable(can_auto);
|
|
auto_dns_search_btn.enable(can_auto);
|
|
auto_routes_btn.enable(can_auto);
|
|
|
|
addresses_table.enable_add(can_have_extra);
|
|
dns_table.enable_add(can_have_extra);
|
|
dns_search_table.enable_add(can_have_extra);
|
|
routes_table.enable_add(!is_off);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-ip-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
|
|
function modify() {
|
|
return PageNetworkIpSettings.apply_settings(self.settings)
|
|
.then(function () {
|
|
$('#network-ip-settings-dialog').modal('hide');
|
|
if (PageNetworkIpSettings.done)
|
|
return PageNetworkIpSettings.done();
|
|
})
|
|
.fail(function (error) {
|
|
show_dialog_error('#network-ip-settings-error', error);
|
|
});
|
|
}
|
|
|
|
with_settings_checkpoint(PageNetworkIpSettings.model, modify,
|
|
{ devices: connection_devices(PageNetworkIpSettings.connection) });
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkIpSettings() {
|
|
this._init();
|
|
}
|
|
|
|
function is_interface_connection(iface, connection) {
|
|
return connection && connection.Interfaces.indexOf(iface) != -1;
|
|
}
|
|
|
|
function is_interesting_interface(iface) {
|
|
return !iface.Device || iface.Device.Managed;
|
|
}
|
|
|
|
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');
|
|
}
|
|
var list = Object(array);
|
|
var length = list.length >>> 0;
|
|
var thisArg = arguments[1];
|
|
var value;
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
if (i in list) {
|
|
value = list[i];
|
|
if (predicate.call(thisArg, value, i, list)) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function slave_connection_for_interface(master, iface) {
|
|
return master && array_find(master.Slaves, function (s) {
|
|
return is_interface_connection(iface, s);
|
|
});
|
|
}
|
|
|
|
function slave_interface_choices(model, master) {
|
|
return model.list_interfaces().filter(function (iface) {
|
|
return !is_interface_connection(iface, master) && is_interesting_interface(iface);
|
|
});
|
|
}
|
|
|
|
function render_slave_interface_choices(model, master) {
|
|
return $('<ul class="list-group dialog-list-ct">').append(
|
|
slave_interface_choices(model, master).map(function (iface) {
|
|
return $('<li class="list-group-item">').append(
|
|
$('<div class="checkbox">')
|
|
.css('margin', "0px")
|
|
.append(
|
|
$('<label>').append(
|
|
$('<input>', {
|
|
type: "checkbox",
|
|
'data-iface': iface.Name
|
|
})
|
|
.prop('checked', !!slave_connection_for_interface(master, iface)),
|
|
$('<span>').text(iface.Name))));
|
|
}));
|
|
}
|
|
|
|
function slave_chooser_btn(change, slave_choices) {
|
|
var choices = [{ title: "-", choice: "", is_default: true }];
|
|
slave_choices.find('input[data-iface]').each(function (i, elt) {
|
|
var name = $(elt).attr("data-iface");
|
|
if ($(elt).prop('checked'))
|
|
choices.push({ title: name, choice: name });
|
|
});
|
|
return select_btn(change, choices, "form-control");
|
|
}
|
|
|
|
function free_slave_connection(con) {
|
|
var cs = connection_settings(con);
|
|
if (cs.slave_type) {
|
|
delete cs.slave_type;
|
|
delete cs.master;
|
|
delete con.Settings.team_port;
|
|
delete con.Settings.bridge_port;
|
|
return con.apply_settings(con.Settings).then(() => { con.activate(null, null) });
|
|
}
|
|
}
|
|
|
|
function set_slave(model, master_connection, master_settings, slave_type,
|
|
iface_name, val) {
|
|
var iface;
|
|
var main_connection;
|
|
|
|
iface = model.find_interface(iface_name);
|
|
if (!iface)
|
|
return false;
|
|
|
|
main_connection = iface.MainConnection;
|
|
|
|
if (val) {
|
|
/* Turn the main_connection into a slave for master.
|
|
*/
|
|
|
|
var master_iface;
|
|
if (master_connection) {
|
|
master_iface = master_connection.Interfaces[0].Name;
|
|
} else {
|
|
master_iface = master_settings.connection.interface_name;
|
|
}
|
|
|
|
if (!master_iface)
|
|
return false;
|
|
|
|
var slave_settings;
|
|
if (main_connection) {
|
|
slave_settings = main_connection.Settings;
|
|
|
|
if (slave_settings.connection.master == master_settings.connection.uuid ||
|
|
slave_settings.connection.master == master_settings.connection.id ||
|
|
slave_settings.connection.master == master_iface)
|
|
return cockpit.resolve();
|
|
|
|
slave_settings.connection.slave_type = slave_type;
|
|
slave_settings.connection.master = master_iface;
|
|
slave_settings.connection.autoconnect = true;
|
|
delete slave_settings.ipv4;
|
|
delete slave_settings.ipv6;
|
|
delete slave_settings.team_port;
|
|
delete slave_settings.bridge_port;
|
|
} else {
|
|
slave_settings = {
|
|
connection:
|
|
{
|
|
autoconnect: true,
|
|
interface_name: iface.Name,
|
|
slave_type: slave_type,
|
|
master: master_iface
|
|
}
|
|
};
|
|
complete_settings(slave_settings, iface.Device);
|
|
}
|
|
|
|
return settings_applier(model, iface.Device, main_connection)(slave_settings).then(function () {
|
|
// If the master already exists, activate or deactivate the slave immediatly so that
|
|
// the settings actually apply and the interface becomes a slave. Otherwise we
|
|
// activate it later when the master is created.
|
|
if (master_connection) {
|
|
var master_dev = master_connection.Interfaces[0].Device;
|
|
if (master_dev && master_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 slave if it is our slave. If there is
|
|
* no main_connection, we don't need to do anything.
|
|
*/
|
|
if (main_connection && main_connection.Masters.indexOf(master_connection) != -1) {
|
|
free_slave_connection(main_connection);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function apply_master_slave(choices, model, apply_master, master_connection, master_settings, slave_type) {
|
|
var active_settings = [];
|
|
var iface;
|
|
|
|
if (!master_connection) {
|
|
if (master_settings.bond &&
|
|
master_settings.bond.options &&
|
|
master_settings.bond.options.primary) {
|
|
iface = model.find_interface(master_settings.bond.options.primary);
|
|
if (iface && iface.MainConnection)
|
|
active_settings.push(iface.MainConnection.Settings);
|
|
} else {
|
|
choices.find('input[data-iface]').map(function (i, elt) {
|
|
var iface;
|
|
if ($(elt).prop('checked')) {
|
|
iface = model.find_interface($(elt).attr("data-iface"));
|
|
if (iface.Device && iface.Device.ActiveConnection && iface.Device.ActiveConnection.Connection) {
|
|
active_settings.push(iface.Device.ActiveConnection.Connection.Settings);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (active_settings.length == 1) {
|
|
master_settings.ipv4 = $.extend(true, { }, active_settings[0].ipv4);
|
|
master_settings.ipv6 = $.extend(true, { }, active_settings[0].ipv6);
|
|
}
|
|
|
|
master_settings.connection.autoconnect_slaves = 1;
|
|
}
|
|
|
|
/* For bonds, the order in which slaves are added to their master matters since the first slaves gets to
|
|
* set the MAC address of the bond, which matters for DHCP. We leave it to NetworkManager to determine
|
|
* the order in which slaves 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_slaves() {
|
|
var deferreds = choices.find('input[data-iface]').map(function (i, elt) {
|
|
return model.synchronize().then(function () {
|
|
return set_slave(model, master_connection, master_settings, slave_type,
|
|
$(elt).attr("data-iface"), $(elt).prop('checked'));
|
|
});
|
|
});
|
|
return Promise.all(deferreds.get());
|
|
}
|
|
|
|
return set_all_slaves().then(function () {
|
|
return apply_master(master_settings);
|
|
});
|
|
}
|
|
|
|
function fill_mac_menu(menu, input, model) {
|
|
menu.empty();
|
|
|
|
function menu_append(title, value) {
|
|
menu.append(
|
|
$('<li class="presentation">').append(
|
|
$('<a tabindex="0">')
|
|
.text(title)
|
|
.click(function () {
|
|
input.val(value).trigger("change");
|
|
})));
|
|
}
|
|
|
|
model.list_interfaces().forEach(function (iface) {
|
|
if (iface.Device && iface.Device.HwAddress && iface.Device.HwAddress !== "00:00:00:00:00:00")
|
|
menu_append(cockpit.format("$0 ($1)", iface.Device.HwAddress, iface.Name), iface.Device.HwAddress);
|
|
});
|
|
|
|
menu_append(_("Permanent"), "permanent");
|
|
menu_append(_("Preserve"), "preserve");
|
|
menu_append(_("Random"), "random");
|
|
menu_append(_("Stable"), "stable");
|
|
}
|
|
|
|
PageNetworkBondSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-bond-settings-dialog";
|
|
this.bond_settings_template = $("#network-bond-settings-template").html();
|
|
mustache.parse(this.bond_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-bond-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-bond-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-bond-settings-error').hide();
|
|
this.settings = PageNetworkBondSettings.ghost_settings || PageNetworkBondSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
find_slave_con: function(iface) {
|
|
if (!PageNetworkBondSettings.connection)
|
|
return null;
|
|
|
|
return array_find(PageNetworkBondSettings.connection.Slaves, function (s) {
|
|
return s.Interfaces.indexOf(iface) >= 0;
|
|
}) || null;
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var model = PageNetworkBondSettings.model;
|
|
var master = PageNetworkBondSettings.connection;
|
|
var options = self.settings.bond.options;
|
|
|
|
var slaves_element;
|
|
var mac_input, mode_btn, primary_btn;
|
|
var monitoring_btn, interval_input, targets_input, updelay_input, downdelay_input;
|
|
|
|
function change_slaves() {
|
|
var btn = slave_chooser_btn(change_mode, slaves_element);
|
|
primary_btn.replaceWith(btn);
|
|
primary_btn = btn;
|
|
select_btn_select(primary_btn, options.primary);
|
|
change_mode();
|
|
self.slaves_changed = true;
|
|
}
|
|
|
|
function change_mac() {
|
|
console.log("mac");
|
|
if (!self.settings.ethernet)
|
|
self.settings.ethernet = { };
|
|
self.settings.ethernet.assigned_mac_address = mac_input.val();
|
|
}
|
|
|
|
function change_mode() {
|
|
options.mode = select_btn_selected(mode_btn);
|
|
|
|
primary_btn.toggle(options.mode == "active-backup");
|
|
primary_btn.prev().toggle(options.mode == "active-backup");
|
|
if (options.mode == "active-backup")
|
|
options.primary = select_btn_selected(primary_btn);
|
|
else
|
|
delete options.primary;
|
|
}
|
|
|
|
function change_monitoring() {
|
|
var use_mii = select_btn_selected(monitoring_btn) == "mii";
|
|
|
|
targets_input.toggle(!use_mii);
|
|
targets_input.prev().toggle(!use_mii);
|
|
updelay_input.toggle(use_mii);
|
|
updelay_input.prev().toggle(use_mii);
|
|
downdelay_input.toggle(use_mii);
|
|
downdelay_input.prev().toggle(use_mii);
|
|
|
|
if (use_mii) {
|
|
options.miimon = interval_input.val();
|
|
options.updelay = updelay_input.val();
|
|
options.downdelay = downdelay_input.val();
|
|
delete options.arp_interval;
|
|
delete options.arp_ip_target;
|
|
} else {
|
|
delete options.miimon;
|
|
delete options.updelay;
|
|
delete options.downdelay;
|
|
options.arp_interval = interval_input.val();
|
|
options.arp_ip_target = targets_input.val();
|
|
}
|
|
}
|
|
|
|
var mac = (self.settings.ethernet && self.settings.ethernet.assigned_mac_address) || "";
|
|
var body = $(mustache.render(self.bond_settings_template, {
|
|
interface_name: self.settings.bond.interface_name,
|
|
assigned_mac_address: mac,
|
|
monitoring_interval: options.miimon || options.arp_interval || "100",
|
|
monitoring_targets: options.arp_ip_targets,
|
|
link_up_delay: options.updelay || "0",
|
|
link_down_delay: options.downdelay || "0"
|
|
}));
|
|
body.find('#network-bond-settings-interface-name-input')
|
|
.change(function (event) {
|
|
var val = $(event.target).val();
|
|
self.settings.bond.interface_name = val;
|
|
self.settings.connection.id = val;
|
|
self.settings.connection.interface_name = val;
|
|
});
|
|
body.find('#network-bond-settings-members')
|
|
.replaceWith(slaves_element = render_slave_interface_choices(model, master)
|
|
.change(change_slaves));
|
|
fill_mac_menu(body.find('#network-bond-settings-mac-menu'),
|
|
mac_input = body.find('#network-bond-settings-mac-input'),
|
|
model);
|
|
mac_input.change(change_mac);
|
|
body.find('#network-bond-settings-mode-select')
|
|
.replaceWith(mode_btn = select_btn(change_mode, bond_mode_choices, "form-control"));
|
|
body.find('#network-bond-settings-primary-select')
|
|
.replaceWith(primary_btn = slave_chooser_btn(change_mode, slaves_element, "form-control"));
|
|
body.find('#network-bond-settings-link-monitoring-select')
|
|
.replaceWith(monitoring_btn = select_btn(change_monitoring, bond_monitoring_choices, "form-control"));
|
|
mode_btn.attr("id", "network-bond-settings-mode-select");
|
|
primary_btn.attr("id", "network-bond-settings-primary-select");
|
|
monitoring_btn.attr("id", "network-bond-settings-link-monitoring-select");
|
|
|
|
interval_input = body.find('#network-bond-settings-monitoring-interval-input');
|
|
interval_input.change(change_monitoring);
|
|
targets_input = body.find('#network-bond-settings-monitoring-targets-input');
|
|
targets_input.change(change_monitoring);
|
|
updelay_input = body.find('#network-bond-settings-link-up-delay-input');
|
|
updelay_input.change(change_monitoring);
|
|
downdelay_input = body.find('#network-bond-settings-link-down-delay-input');
|
|
downdelay_input.change(change_monitoring);
|
|
|
|
select_btn_select(mode_btn, options.mode);
|
|
select_btn_select(monitoring_btn, (options.miimon !== 0) ? "mii" : "arp");
|
|
change_slaves();
|
|
change_mode();
|
|
change_monitoring();
|
|
|
|
self.slaves_changed = false;
|
|
|
|
$('#network-bond-settings-body').html(body);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-bond-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
|
|
function modify() {
|
|
return apply_master_slave($('#network-bond-settings-body'),
|
|
PageNetworkBondSettings.model,
|
|
PageNetworkBondSettings.apply_settings,
|
|
PageNetworkBondSettings.connection,
|
|
self.settings,
|
|
"bond")
|
|
.then(function() {
|
|
$('#network-bond-settings-dialog').modal('hide');
|
|
if (PageNetworkBondSettings.connection)
|
|
cockpit.location.go([self.settings.connection.interface_name]);
|
|
if (PageNetworkBondSettings.done)
|
|
return PageNetworkBondSettings.done();
|
|
})
|
|
.catch(function (error) {
|
|
show_dialog_error('#network-bond-settings-error', error);
|
|
});
|
|
}
|
|
|
|
if (PageNetworkBondSettings.connection) {
|
|
with_settings_checkpoint(PageNetworkBondSettings.model, modify,
|
|
{
|
|
devices: (self.slaves_changed
|
|
? [] : connection_devices(PageNetworkBondSettings.connection)),
|
|
hack_does_add_or_remove: self.slaves_changed,
|
|
rollback_on_failure: self.slaves_changed
|
|
});
|
|
} else {
|
|
with_checkpoint(
|
|
PageNetworkBondSettings.model,
|
|
modify,
|
|
{
|
|
fail_text: _("Creating this bond will break the connection to the server, and will make the administration UI unavailable."),
|
|
anyway_text: _("Create it"),
|
|
hack_does_add_or_remove: true,
|
|
rollback_on_failure: true
|
|
});
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkBondSettings() {
|
|
this._init();
|
|
}
|
|
|
|
PageNetworkTeamSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-team-settings-dialog";
|
|
this.team_settings_template = $("#network-team-settings-template").html();
|
|
mustache.parse(this.team_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-team-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-team-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-team-settings-error').hide();
|
|
this.settings = PageNetworkTeamSettings.ghost_settings || PageNetworkTeamSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
find_slave_con: function(iface) {
|
|
if (!PageNetworkTeamSettings.connection)
|
|
return null;
|
|
|
|
return array_find(PageNetworkTeamSettings.connection.Slaves, function (s) {
|
|
return s.Interfaces.indexOf(iface) >= 0;
|
|
}) || null;
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var model = PageNetworkTeamSettings.model;
|
|
var master = PageNetworkTeamSettings.connection;
|
|
var config = self.settings.team.config;
|
|
|
|
var runner_btn, balancer_btn, watch_btn;
|
|
var interval_input, target_input, updelay_input, downdelay_input;
|
|
|
|
if (!config)
|
|
self.settings.team.config = config = { };
|
|
if (!config.runner)
|
|
config.runner = { };
|
|
if (!config.runner.name)
|
|
config.runner.name = "activebackup";
|
|
if (!config.link_watch)
|
|
config.link_watch = { };
|
|
if (!config.link_watch.name)
|
|
config.link_watch.name = "ethtool";
|
|
if (config.link_watch.interval === undefined)
|
|
config.link_watch.interval = 100;
|
|
if (config.link_watch.delay_up === undefined)
|
|
config.link_watch.delay_up = 0;
|
|
if (config.link_watch.delay_down === undefined)
|
|
config.link_watch.delay_down = 0;
|
|
|
|
function change_slaves() {
|
|
self.slaves_changed = true;
|
|
}
|
|
|
|
function change_runner() {
|
|
config.runner.name = select_btn_selected(runner_btn);
|
|
var toggle_condition = config.runner.name == "loadbalance" || config.runner.name == "lacp";
|
|
balancer_btn.toggle(toggle_condition);
|
|
balancer_btn.prev().toggle(toggle_condition);
|
|
}
|
|
|
|
function change_balancer() {
|
|
var balancer = select_btn_selected(balancer_btn);
|
|
if (balancer == "none") {
|
|
if (config.runner.tx_balancer)
|
|
delete config.runner.tx_balancer.name;
|
|
} else {
|
|
if (!config.runner.tx_balancer)
|
|
config.runner.tx_balancer = { };
|
|
config.runner.tx_balancer.name = balancer;
|
|
}
|
|
}
|
|
|
|
function change_watch() {
|
|
var name = select_btn_selected(watch_btn);
|
|
var toggle_condition = name != "ethtool";
|
|
|
|
interval_input.toggle(toggle_condition);
|
|
interval_input.prev().toggle(toggle_condition);
|
|
target_input.toggle(toggle_condition);
|
|
target_input.prev().toggle(toggle_condition);
|
|
updelay_input.toggle(!toggle_condition);
|
|
updelay_input.prev().toggle(!toggle_condition);
|
|
downdelay_input.toggle(!toggle_condition);
|
|
downdelay_input.prev().toggle(!toggle_condition);
|
|
|
|
config.link_watch = { name: name };
|
|
|
|
if (name == "ethtool") {
|
|
config.link_watch.delay_up = updelay_input.val();
|
|
config.link_watch.delay_down = downdelay_input.val();
|
|
} else {
|
|
config.link_watch.interval = interval_input.val();
|
|
config.link_watch.target_host = target_input.val();
|
|
}
|
|
}
|
|
|
|
var body = $(mustache.render(self.team_settings_template,
|
|
{
|
|
interface_name: self.settings.team.interface_name,
|
|
config: config
|
|
}));
|
|
body.find('#network-team-settings-interface-name-input')
|
|
.change(function (event) {
|
|
var val = $(event.target).val();
|
|
self.settings.team.interface_name = val;
|
|
self.settings.connection.id = val;
|
|
self.settings.connection.interface_name = val;
|
|
});
|
|
body.find('#network-team-settings-members')
|
|
.replaceWith(render_slave_interface_choices(model, master).change(change_slaves));
|
|
body.find('#network-team-settings-runner-select')
|
|
.replaceWith(runner_btn = select_btn(change_runner, team_runner_choices, "form-control"));
|
|
body.find('#network-team-settings-balancer-select')
|
|
.replaceWith(balancer_btn = select_btn(change_balancer, team_balancer_choices, "form-control"));
|
|
body.find('#network-team-settings-link-watch-select')
|
|
.replaceWith(watch_btn = select_btn(change_watch, team_watch_choices, "form-control"));
|
|
runner_btn.attr("id", "network-team-settings-runner-select");
|
|
balancer_btn.attr("id", "network-team-settings-balancer-select");
|
|
watch_btn.attr("id", "network-team-settings-link-watch-select");
|
|
|
|
interval_input = body.find('#network-team-settings-ping-interval-input');
|
|
interval_input.change(change_watch);
|
|
target_input = body.find('#network-team-settings-ping-target-input');
|
|
target_input.change(change_watch);
|
|
updelay_input = body.find('#network-team-settings-link-up-delay-input');
|
|
updelay_input.change(change_watch);
|
|
downdelay_input = body.find('#network-team-settings-link-down-delay-input');
|
|
downdelay_input.change(change_watch);
|
|
|
|
select_btn_select(runner_btn, config.runner.name);
|
|
select_btn_select(balancer_btn, (config.runner.tx_balancer && config.runner.tx_balancer.name) || "none");
|
|
select_btn_select(watch_btn, config.link_watch.name);
|
|
change_runner();
|
|
change_watch();
|
|
|
|
self.slaves_changed = false;
|
|
|
|
$('#network-team-settings-body').html(body);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-team-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
|
|
function modify () {
|
|
return apply_master_slave($('#network-team-settings-body'),
|
|
PageNetworkTeamSettings.model,
|
|
PageNetworkTeamSettings.apply_settings,
|
|
PageNetworkTeamSettings.connection,
|
|
self.settings,
|
|
"team")
|
|
.then(function() {
|
|
$('#network-team-settings-dialog').modal('hide');
|
|
if (PageNetworkTeamSettings.connection)
|
|
cockpit.location.go([self.settings.connection.interface_name]);
|
|
if (PageNetworkTeamSettings.done)
|
|
return PageNetworkTeamSettings.done();
|
|
})
|
|
.catch(function (error) {
|
|
show_dialog_error('#network-team-settings-error', error);
|
|
});
|
|
}
|
|
|
|
if (PageNetworkTeamSettings.connection) {
|
|
with_settings_checkpoint(PageNetworkTeamSettings.model, modify,
|
|
{
|
|
devices: (self.slaves_changed
|
|
? [] : connection_devices(PageNetworkTeamSettings.connection)),
|
|
hack_does_add_or_remove: self.slaves_changed,
|
|
rollback_on_failure: self.slaves_changed
|
|
});
|
|
} else {
|
|
with_checkpoint(
|
|
PageNetworkTeamSettings.model,
|
|
modify,
|
|
{
|
|
fail_text: _("Creating this team will break the connection to the server, and will make the administration UI unavailable."),
|
|
anyway_text: _("Create it"),
|
|
hack_does_add_or_remove: true,
|
|
rollback_on_failure: true
|
|
});
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkTeamSettings() {
|
|
this._init();
|
|
}
|
|
|
|
PageNetworkTeamPortSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-teamport-settings-dialog";
|
|
this.team_port_settings_template = $("#network-team-port-settings-template").html();
|
|
mustache.parse(this.team_port_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-teamport-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-teamport-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-teamport-settings-error').hide();
|
|
this.settings = PageNetworkTeamPortSettings.ghost_settings || PageNetworkTeamPortSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var master_config = PageNetworkTeamPortSettings.master_settings.team.config;
|
|
var config = self.settings.team_port.config;
|
|
|
|
var ab_prio_input, ab_sticky_input, lacp_prio_input, lacp_key_input;
|
|
|
|
if (!config)
|
|
self.settings.team_port.config = config = { };
|
|
|
|
function change() {
|
|
// XXX - handle parse errors
|
|
if (master_config.runner.name == "activebackup") {
|
|
config.prio = parseInt(ab_prio_input.val(), 10);
|
|
config.sticky = ab_sticky_input.prop('checked');
|
|
} else if (master_config.runner.name == "lacp") {
|
|
config.lacp_prio = parseInt(lacp_prio_input.val(), 10);
|
|
config.lacp_key = parseInt(lacp_key_input.val(), 10);
|
|
}
|
|
}
|
|
|
|
var body = $(mustache.render(self.team_port_settings_template, config));
|
|
ab_prio_input = body.find('#network-team-port-settings-ab-prio-input');
|
|
ab_prio_input.change(change);
|
|
ab_sticky_input = body.find('#network-team-port-settings-ab-sticky-input');
|
|
ab_sticky_input.change(change);
|
|
lacp_prio_input = body.find('#network-team-port-settings-lacp-prio-input');
|
|
lacp_prio_input.change(change);
|
|
lacp_key_input = body.find('#network-team-port-settings-lacp-key-input');
|
|
lacp_key_input.change(change);
|
|
|
|
ab_prio_input.toggle(master_config.runner.name == "activebackup");
|
|
ab_prio_input.prev().toggle(master_config.runner.name == "activebackup");
|
|
ab_sticky_input.toggle(master_config.runner.name == "activebackup");
|
|
ab_sticky_input
|
|
.parent()
|
|
.prev()
|
|
.toggle(master_config.runner.name == "activebackup");
|
|
lacp_prio_input.toggle(master_config.runner.name == "lacp");
|
|
lacp_prio_input.prev().toggle(master_config.runner.name == "lacp");
|
|
lacp_key_input.toggle(master_config.runner.name == "lacp");
|
|
lacp_key_input.prev().toggle(master_config.runner.name == "lacp");
|
|
|
|
$('#network-teamport-settings-body').html(body);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-teamport-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
var model = PageNetworkTeamPortSettings.model;
|
|
|
|
function modify () {
|
|
return PageNetworkTeamPortSettings.apply_settings(self.settings)
|
|
.then(function () {
|
|
$('#network-teamport-settings-dialog').modal('hide');
|
|
if (PageNetworkTeamPortSettings.done)
|
|
return PageNetworkTeamPortSettings.done();
|
|
})
|
|
.fail(function (error) {
|
|
show_dialog_error('#network-teamport-settings-error', error);
|
|
});
|
|
}
|
|
|
|
with_settings_checkpoint(model, modify,
|
|
{ devices: connection_devices(PageNetworkTeamPortSettings.connection) });
|
|
}
|
|
};
|
|
|
|
function PageNetworkTeamPortSettings() {
|
|
this._init();
|
|
}
|
|
|
|
PageNetworkBridgeSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-bridge-settings-dialog";
|
|
this.bridge_settings_template = $("#network-bridge-settings-template").html();
|
|
mustache.parse(this.bridge_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-bridge-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-bridge-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-bridge-settings-error').hide();
|
|
this.settings = PageNetworkBridgeSettings.ghost_settings || PageNetworkBridgeSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
find_slave_con: function(iface) {
|
|
if (!PageNetworkBridgeSettings.connection)
|
|
return null;
|
|
|
|
return array_find(PageNetworkBridgeSettings.connection.Slaves, function (s) {
|
|
return s.Interfaces.indexOf(iface) >= 0;
|
|
}) || null;
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var model = PageNetworkBridgeSettings.model;
|
|
var con = PageNetworkBridgeSettings.connection;
|
|
var options = self.settings.bridge;
|
|
|
|
var stp_input, priority_input, forward_delay_input, hello_time_input, max_age_input;
|
|
|
|
function change_slaves() {
|
|
self.slaves_changed = true;
|
|
}
|
|
|
|
function change_stp() {
|
|
// XXX - handle parse errors
|
|
options.stp = stp_input.prop('checked');
|
|
options.priority = parseInt(priority_input.val(), 10);
|
|
options.forward_delay = parseInt(forward_delay_input.val(), 10);
|
|
options.hello_time = parseInt(hello_time_input.val(), 10);
|
|
options.max_age = parseInt(max_age_input.val(), 10);
|
|
|
|
priority_input.toggle(options.stp);
|
|
priority_input.prev().toggle(options.stp);
|
|
forward_delay_input.toggle(options.stp);
|
|
forward_delay_input.prev().toggle(options.stp);
|
|
hello_time_input.toggle(options.stp);
|
|
hello_time_input.prev().toggle(options.stp);
|
|
max_age_input.toggle(options.stp);
|
|
max_age_input.prev().toggle(options.stp);
|
|
}
|
|
|
|
var body = $(mustache.render(self.bridge_settings_template, {
|
|
bridge_name: options.interface_name,
|
|
stp_checked: options.stp,
|
|
stp_priority: options.priority,
|
|
stp_forward_delay: options.forward_delay,
|
|
stp_hello_time: options.hello_time,
|
|
stp_max_age: options.max_age
|
|
}));
|
|
body.find('#network-bridge-settings-name-input')
|
|
.change(function (event) {
|
|
var val = $(event.target).val();
|
|
options.interface_name = val;
|
|
self.settings.connection.id = val;
|
|
self.settings.connection.interface_name = val;
|
|
});
|
|
var slave_interfaces = body.find('#network-bridge-settings-slave-interfaces')
|
|
.replaceWith(render_slave_interface_choices(model, con).change(change_slaves));
|
|
slave_interfaces.toggle(!con);
|
|
slave_interfaces.prev().toggle(!con);
|
|
|
|
stp_input = body.find('#network-bridge-settings-stp-enabled-input');
|
|
stp_input.change(change_stp);
|
|
priority_input = body.find('#network-bridge-settings-stp-priority-input');
|
|
priority_input.change(change_stp);
|
|
forward_delay_input = body.find('#network-bridge-settings-stp-forward-delay-input');
|
|
forward_delay_input.change(change_stp);
|
|
hello_time_input = body.find('#network-bridge-settings-stp-hello-time-input');
|
|
hello_time_input.change(change_stp);
|
|
max_age_input = body.find('#network-bridge-settings-stp-max-age-input');
|
|
max_age_input.change(change_stp);
|
|
|
|
change_stp();
|
|
|
|
self.slaves_changed = false;
|
|
|
|
$('#network-bridge-settings-body').html(body);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-bridge-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
|
|
function modify () {
|
|
return apply_master_slave($('#network-bridge-settings-body'),
|
|
PageNetworkBridgeSettings.model,
|
|
PageNetworkBridgeSettings.apply_settings,
|
|
PageNetworkBridgeSettings.connection,
|
|
self.settings,
|
|
"bridge")
|
|
.then(function() {
|
|
$('#network-bridge-settings-dialog').modal('hide');
|
|
if (PageNetworkBridgeSettings.connection)
|
|
cockpit.location.go([self.settings.connection.interface_name]);
|
|
if (PageNetworkBridgeSettings.done)
|
|
return PageNetworkBridgeSettings.done();
|
|
})
|
|
.catch(function (error) {
|
|
$('#network-bridge-settings-error').show()
|
|
.find('span')
|
|
.text(error.message || error.toString());
|
|
});
|
|
}
|
|
|
|
if (PageNetworkBridgeSettings.connection) {
|
|
with_settings_checkpoint(PageNetworkBridgeSettings.model, modify,
|
|
{
|
|
devices: (self.slaves_changed
|
|
? [] : connection_devices(PageNetworkBridgeSettings.connection)),
|
|
hack_does_add_or_remove: self.slaves_changed,
|
|
rollback_on_failure: self.slaves_changed
|
|
});
|
|
} else {
|
|
with_checkpoint(
|
|
PageNetworkBridgeSettings.model,
|
|
modify,
|
|
{
|
|
fail_text: _("Creating this bridge will break the connection to the server, and will make the administration UI unavailable."),
|
|
anyway_text: _("Create it"),
|
|
hack_does_add_or_remove: true,
|
|
rollback_on_failure: true
|
|
});
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkBridgeSettings() {
|
|
this._init();
|
|
}
|
|
|
|
PageNetworkBridgePortSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-bridgeport-settings-dialog";
|
|
this.bridge_port_settings_template = $("#network-bridge-port-settings-template").html();
|
|
mustache.parse(this.bridge_port_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-bridgeport-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-bridgeport-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-bridgeport-settings-error').hide();
|
|
this.settings = PageNetworkBridgePortSettings.ghost_settings || PageNetworkBridgePortSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var options = self.settings.bridge_port;
|
|
|
|
var priority_input, path_cost_input, hairpin_mode_input;
|
|
|
|
function change() {
|
|
// XXX - handle parse errors
|
|
options.priority = parseInt(priority_input.val(), 10);
|
|
options.path_cost = parseInt(path_cost_input.val(), 10);
|
|
options.hairpin_mode = hairpin_mode_input.prop('checked');
|
|
}
|
|
|
|
var body = $(mustache.render(self.bridge_port_settings_template, {
|
|
priority: options.priority,
|
|
path_cost: options.path_cost,
|
|
hairpin_mode_checked: options.hairpin_mode
|
|
}));
|
|
priority_input = body.find('#network-bridge-port-settings-priority-input');
|
|
priority_input.change(change);
|
|
path_cost_input = body.find('#network-bridge-port-settings-path-cost-input');
|
|
path_cost_input.change(change);
|
|
hairpin_mode_input = body.find('#network-bridge-port-settings-hairpin-mode-input');
|
|
hairpin_mode_input.change(change);
|
|
|
|
$('#network-bridgeport-settings-body').html(body);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-bridgeport-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
var model = PageNetworkBridgePortSettings.model;
|
|
|
|
function modify () {
|
|
return PageNetworkBridgePortSettings.apply_settings(self.settings)
|
|
.then(function () {
|
|
$('#network-bridgeport-settings-dialog').modal('hide');
|
|
if (PageNetworkBridgePortSettings.done)
|
|
return PageNetworkBridgePortSettings.done();
|
|
})
|
|
.fail(function (error) {
|
|
show_dialog_error('#network-bridgeport-settings-error', error);
|
|
});
|
|
}
|
|
|
|
with_settings_checkpoint(model, modify,
|
|
{ devices: connection_devices(PageNetworkBridgePortSettings.connection) });
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkBridgePortSettings() {
|
|
this._init();
|
|
}
|
|
|
|
PageNetworkVlanSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-vlan-settings-dialog";
|
|
this.vlan_settings_template = $("#network-vlan-settings-template").html();
|
|
mustache.parse(this.vlan_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-vlan-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-vlan-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-vlan-settings-error').hide();
|
|
this.settings = PageNetworkVlanSettings.ghost_settings || PageNetworkVlanSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var model = PageNetworkVlanSettings.model;
|
|
var options = self.settings.vlan;
|
|
|
|
var auto_update_name = true;
|
|
var parent_btn, id_input, name_input;
|
|
|
|
function change() {
|
|
// XXX - parse errors
|
|
options.parent = select_btn_selected(parent_btn);
|
|
$("#network-vlan-settings-apply").toggleClass("disabled", !options.parent);
|
|
|
|
options.id = parseInt(id_input.val(), 10);
|
|
|
|
if (auto_update_name && options.parent && options.id)
|
|
name_input.val(options.parent + "." + options.id);
|
|
|
|
options.interface_name = name_input.val();
|
|
self.settings.connection.id = options.interface_name;
|
|
self.settings.connection.interface_name = options.interface_name;
|
|
}
|
|
|
|
function change_name() {
|
|
auto_update_name = false;
|
|
change();
|
|
}
|
|
|
|
var parent_choices = [];
|
|
model.list_interfaces().forEach(function (i) {
|
|
if (!is_interface_connection(i, PageNetworkVlanSettings.connection) &&
|
|
is_interesting_interface(i))
|
|
parent_choices.push({ title: i.Name, choice: i.Name });
|
|
});
|
|
|
|
var body = $(mustache.render(self.vlan_settings_template, {
|
|
vlan_id: options.id || "1",
|
|
interface_name: options.interface_name
|
|
}));
|
|
parent_btn = select_btn(change, parent_choices, "form-control");
|
|
parent_btn.attr('id', 'network-vlan-settings-parent-select');
|
|
body.find('#network-vlan-settings-parent-select').replaceWith(parent_btn);
|
|
id_input = body.find('#network-vlan-settings-vlan-id-input')
|
|
.change(change)
|
|
.on('input', change);
|
|
name_input = body.find('#network-vlan-settings-interface-name-input')
|
|
.change(change_name)
|
|
.on('input', change_name);
|
|
|
|
select_btn_select(parent_btn, (options.parent ||
|
|
(parent_choices[0]
|
|
? parent_choices[0].choice
|
|
: "")));
|
|
change();
|
|
$('#network-vlan-settings-body').html(body);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-vlan-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
var model = PageNetworkVlanSettings.model;
|
|
|
|
function modify () {
|
|
return PageNetworkVlanSettings.apply_settings(self.settings)
|
|
.then(function () {
|
|
$('#network-vlan-settings-dialog').modal('hide');
|
|
if (PageNetworkVlanSettings.connection)
|
|
cockpit.location.go([self.settings.connection.interface_name]);
|
|
if (PageNetworkVlanSettings.done)
|
|
return PageNetworkVlanSettings.done();
|
|
})
|
|
.fail(function (error) {
|
|
show_dialog_error('#network-vlan-settings-error', error);
|
|
});
|
|
}
|
|
|
|
if (PageNetworkVlanSettings.connection)
|
|
with_settings_checkpoint(model, modify, { hack_does_add_or_remove: true });
|
|
else
|
|
with_checkpoint(
|
|
PageNetworkVlanSettings.model,
|
|
modify,
|
|
{
|
|
fail_text: _("Creating this VLAN will break the connection to the server, and will make the administration UI unavailable."),
|
|
anyway_text: _("Create it"),
|
|
hack_does_add_or_remove: true
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkVlanSettings() {
|
|
this._init();
|
|
}
|
|
|
|
PageNetworkMtuSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-mtu-settings-dialog";
|
|
this.ethernet_settings_template = $("#network-mtu-settings-template").html();
|
|
mustache.parse(this.ethernet_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-mtu-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-mtu-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-mtu-settings-error').hide();
|
|
this.settings = PageNetworkMtuSettings.ghost_settings || PageNetworkMtuSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var options = self.settings.ethernet;
|
|
|
|
var body = $(mustache.render(self.ethernet_settings_template, options));
|
|
$('#network-mtu-settings-body').html(body);
|
|
$('#network-mtu-settings-input').focus(function () {
|
|
$('#network-mtu-settings-custom').prop('checked', true);
|
|
});
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-mtu-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
var model = PageNetworkMtuSettings.model;
|
|
|
|
function show_error(error) {
|
|
show_dialog_error('#network-mtu-settings-error', error);
|
|
}
|
|
|
|
if ($("#network-mtu-settings-auto").prop('checked'))
|
|
self.settings.ethernet.mtu = 0;
|
|
else {
|
|
var mtu = $("#network-mtu-settings-input").val();
|
|
if (/^[0-9]+$/.test(mtu))
|
|
self.settings.ethernet.mtu = parseInt(mtu, 10);
|
|
else {
|
|
show_error(_("MTU must be a positive number"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
function modify () {
|
|
return PageNetworkMtuSettings.apply_settings(self.settings)
|
|
.then(function () {
|
|
$('#network-mtu-settings-dialog').modal('hide');
|
|
if (PageNetworkMtuSettings.done)
|
|
return PageNetworkMtuSettings.done();
|
|
})
|
|
.fail(show_error);
|
|
}
|
|
|
|
with_settings_checkpoint(model, modify,
|
|
{ devices: connection_devices(PageNetworkMtuSettings.connection) });
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkMtuSettings() {
|
|
this._init();
|
|
}
|
|
|
|
PageNetworkMacSettings.prototype = {
|
|
_init: function () {
|
|
this.id = "network-mac-settings-dialog";
|
|
this.ethernet_settings_template = $("#network-mac-settings-template").html();
|
|
mustache.parse(this.ethernet_settings_template);
|
|
},
|
|
|
|
setup: function () {
|
|
$('#network-mac-settings-cancel').click($.proxy(this, "cancel"));
|
|
$('#network-mac-settings-apply').click($.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function () {
|
|
$('#network-mac-settings-error').hide();
|
|
this.settings = PageNetworkMacSettings.ghost_settings || PageNetworkMacSettings.connection.copy_settings();
|
|
this.update();
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
update: function() {
|
|
var self = this;
|
|
var options = self.settings.ethernet;
|
|
|
|
var body = $(mustache.render(self.ethernet_settings_template, options));
|
|
$('#network-mac-settings-body').html(body);
|
|
|
|
fill_mac_menu($('#network-mac-settings-menu'),
|
|
$('#network-mac-settings-input'),
|
|
PageNetworkMacSettings.model);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#network-mac-settings-dialog').modal('hide');
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
var model = PageNetworkMacSettings.model;
|
|
|
|
function show_error(error) {
|
|
show_dialog_error('#network-mac-settings-error', error);
|
|
}
|
|
|
|
if (!self.settings.ethernet)
|
|
self.settings.ethernet = { };
|
|
self.settings.ethernet.assigned_mac_address = $("#network-mac-settings-input").val();
|
|
|
|
function modify () {
|
|
return PageNetworkMacSettings.apply_settings(self.settings)
|
|
.then(function () {
|
|
$('#network-mac-settings-dialog').modal('hide');
|
|
if (PageNetworkMacSettings.done)
|
|
return PageNetworkMacSettings.done();
|
|
})
|
|
.fail(show_error);
|
|
}
|
|
|
|
with_settings_checkpoint(model, modify,
|
|
{ devices: connection_devices(PageNetworkMacSettings.connection) });
|
|
}
|
|
|
|
};
|
|
|
|
function PageNetworkMacSettings() {
|
|
this._init();
|
|
}
|
|
|
|
/* INITIALIZATION AND NAVIGATION
|
|
*
|
|
* The code above still uses the legacy 'Page' abstraction for both
|
|
* pages and dialogs, and expects page.setup, page.enter, page.show,
|
|
* and page.leave to be called at the right times.
|
|
*
|
|
* We cater to this with a little compatability shim consisting of
|
|
* 'dialog_setup', 'page_show', and 'page_hide'.
|
|
*/
|
|
|
|
function dialog_setup(d) {
|
|
d.setup();
|
|
$('#' + d.id)
|
|
.on('show.bs.modal', function () { d.enter() })
|
|
.on('shown.bs.modal', function () { d.show() })
|
|
.on('hidden.bs.modal', function () { d.leave() });
|
|
}
|
|
|
|
function page_show(p, arg) {
|
|
if (p._entered_)
|
|
p.leave();
|
|
p.enter(arg);
|
|
p._entered_ = true;
|
|
$('#' + p.id).show();
|
|
p.show();
|
|
}
|
|
|
|
function page_hide(p) {
|
|
$('#' + p.id).hide();
|
|
if (p._entered_) {
|
|
p.leave();
|
|
p._entered_ = false;
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
var model;
|
|
var overview_page;
|
|
var interface_page;
|
|
|
|
function navigate() {
|
|
var path = cockpit.location.path;
|
|
|
|
model.synchronize().then(function() {
|
|
if (path.length === 0) {
|
|
page_hide(interface_page);
|
|
page_show(overview_page);
|
|
} else if (path.length === 1) {
|
|
page_hide(overview_page);
|
|
page_show(interface_page, path[0]);
|
|
} else { /* redirect */
|
|
console.warn("not a networking location: " + path);
|
|
cockpit.location = '';
|
|
}
|
|
|
|
$("body").show();
|
|
});
|
|
}
|
|
|
|
cockpit.translate();
|
|
|
|
model = new NetworkManagerModel();
|
|
|
|
model.preinit.then(() => {
|
|
overview_page = new PageNetworking(model);
|
|
overview_page.setup();
|
|
|
|
interface_page = new PageNetworkInterface(model);
|
|
interface_page.setup();
|
|
|
|
dialog_setup(new PageNetworkIpSettings());
|
|
dialog_setup(new PageNetworkBondSettings());
|
|
dialog_setup(new PageNetworkTeamSettings());
|
|
dialog_setup(new PageNetworkTeamPortSettings());
|
|
dialog_setup(new PageNetworkBridgeSettings());
|
|
dialog_setup(new PageNetworkBridgePortSettings());
|
|
dialog_setup(new PageNetworkVlanSettings());
|
|
dialog_setup(new PageNetworkMtuSettings());
|
|
dialog_setup(new PageNetworkMacSettings());
|
|
|
|
$(cockpit).on("locationchanged", navigate);
|
|
navigate();
|
|
});
|
|
}
|
|
|
|
$(init);
|