diff options
author | Jo-Philipp Wich <jo@mein.io> | 2019-09-12 11:06:38 +0200 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2019-09-12 11:09:57 +0200 |
commit | 82743b3bd4b4603f2c6b85566d07182cb0f1ce52 (patch) | |
tree | 9444cba040a334821e6593c13541792c718084ea /modules/luci-mod-network/htdocs/luci-static | |
parent | 5fd21bc9efedb29ff8376f8043ed2a4b74788d6b (diff) |
luci-mod-network: reimplement switch configuration as client side view
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-mod-network/htdocs/luci-static')
-rw-r--r-- | modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js new file mode 100644 index 000000000..b281bb180 --- /dev/null +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js @@ -0,0 +1,369 @@ +'use strict'; +'require rpc'; +'require uci'; +'require form'; +'require network'; + +function parse_portvalue(section_id) { + var ports = L.toArray(uci.get('network', section_id, 'ports')); + + for (var i = 0; i < ports.length; i++) { + var m = ports[i].match(/^(\d+)([tu]?)/); + + if (m && m[1] == this.option) + return m[2] || 'u'; + } + + return ''; +} + +function validate_portvalue(section_id, value) { + if (value != 'u') + return true; + + var sections = this.section.cfgsections(); + + for (var i = 0; i < sections.length; i++) { + if (sections[i] == section_id) + continue; + + if (this.formvalue(sections[i]) == 'u') + return _('%s is untagged in multiple VLANs!').format(this.title); + } + + return true; +} + +function update_interfaces(old_ifname, new_ifname) { + var interfaces = uci.sections('network', 'interface'); + + for (var i = 0; i < interfaces.length; i++) { + var old_ifnames = L.toArray(interfaces[i].ifname), + new_ifnames = [], + changed = false; + + for (var j = 0; j < old_ifnames.length; j++) { + if (old_ifnames[j] == old_ifname) { + new_ifnames.push(new_ifname); + changed = true; + } + else { + new_ifnames.push(old_ifnames[j]); + } + } + + if (changed) { + uci.set('network', interfaces[i]['.name'], 'ifname', new_ifnames.join(' ')); + + L.ui.addNotification(null, E('p', _('Interface %q device auto-migrated from %q to %q.') + .replace(/%q/g, '"%s"').format(interfaces[i]['.name'], old_ifname, new_ifname))); + } + } +} + +function render_port_status(node, portstate) { + if (!node) + return null; + + if (!portstate.link) + L.dom.content(node, [ + E('img', { src: L.resource('icons/port_down.png') }), + E('br'), + _('no link') + ]); + else + L.dom.content(node, [ + E('img', { src: L.resource('icons/port_up.png') }), + E('br'), + '%d'.format(portstate.speed) + _('baseT'), + E('br'), + portstate.duplex ? _('full-duplex') : _('half-duplex') + ]); + + return node; +} + +function update_port_status(topologies) { + var tasks = []; + + for (var switch_name in topologies) + tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(switch_name, ports) { + for (var i = 0; i < ports.length; i++) { + var node = document.querySelector('[data-switch="%s"][data-port="%d"]'.format(switch_name, ports[i].port)); + render_port_status(node, ports[i]); + } + }, topologies[switch_name], switch_name))); + + return Promise.all(tasks); +} + +var callSwconfigFeatures = rpc.declare({ + object: 'luci', + method: 'getSwconfigFeatures', + params: [ 'switch' ], + expect: { '': {} } +}); + +var callSwconfigPortState = rpc.declare({ + object: 'luci', + method: 'getSwconfigPortState', + params: [ 'switch' ], + expect: { result: [] } +}); + +return L.view.extend({ + load: function() { + return network.getSwitchTopologies().then(function(topologies) { + var tasks = []; + + for (var switch_name in topologies) { + tasks.push(callSwconfigFeatures(switch_name).then(L.bind(function(features) { + this.features = features; + }, topologies[switch_name]))); + tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(ports) { + this.portstate = ports; + }, topologies[switch_name]))); + } + + return Promise.all(tasks).then(function() { return topologies }); + }); + }, + + render: function(topologies) { + var m, s, o; + + m = new form.Map('network', _('Switch'), _('The network ports on this device can be combined to several <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s in which computers can communicate directly with each other. <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s are often used to separate different network segments. Often there is by default one Uplink port for a connection to the next greater network like the internet and other ports for a local network.')); + + var switchSections = uci.sections('network', 'switch'); + + for (var i = 0; i < switchSections.length; i++) { + var switchSection = switchSections[i], + sid = switchSection['.name'], + switch_name = switchSection.name || sid, + topology = topologies[switch_name]; + + if (!topology) { + L.ui.addNotification(null, _('Switch %q has an unknown topology - the VLAN settings might not be accurate.').replace(/%q/, switch_name)); + + topology = { + features: {}, + netdevs: { + 5: 'eth0' + }, + ports: [ + { num: 0, label: 'Port 1' }, + { num: 1, label: 'Port 2' }, + { num: 2, label: 'Port 3' }, + { num: 3, label: 'Port 4' }, + { num: 4, label: 'Port 5' }, + { num: 5, label: 'CPU (eth0)', device: 'eth0', need_tag: false } + ] + }; + } + + var feat = topology.features, + min_vid = feat.min_vid || 0, + max_vid = feat.max_vid || 16, + num_vlans = feat.num_vlans || 16, + switch_title = _('Switch %q').replace(/%q/, '"%s"'.format(switch_name)), + vlan_title = _('VLANs on %q').replace(/%q/, '"%s"'.format(switch_name)); + + if (feat.switch_title) { + switch_title += ' (%s)'.format(feat.switch_title); + vlan_title += ' (%s)'.format(feat.switch_title); + } + + s = m.section(form.NamedSection, sid, 'switch', switch_title); + s.addremove = false; + + if (feat.vlan_option) + s.option(form.Flag, feat.vlan_option, _('Enable VLAN functionality')); + + if (feat.learning_option) { + o = s.option(form.Flag, feat.learning_option, _('Enable learning and aging')); + o.default = o.enabled; + } + + if (feat.jumbo_option) { + o = s.option(form.Flag, feat.jumbo_option, _('Enable Jumbo Frame passthrough')); + o.enabled = '3'; + o.rmempty = true; + } + + if (feat.mirror_option) { + s.option(form.Flag, 'enable_mirror_rx', _('Enable mirroring of incoming packets')); + s.option(form.Flag, 'enable_mirror_tx', _('Enable mirroring of outgoing packets')); + + var sp = s.option(form.ListValue, 'mirror_source_port', _('Mirror source port')), + mp = s.option(form.ListValue, 'mirror_monitor_port', _('Mirror monitor port')); + + sp.depends('enable_mirror_rx', '1'); + sp.depends('enable_mirror_tx', '1'); + + mp.depends('enable_mirror_rx', '1'); + mp.depends('enable_mirror_tx', '1'); + + for (var j = 0; j < topology.ports.length; j++) { + sp.value(topology.ports[j].num, topology.ports[j].label); + mp.value(topology.ports[j].num, topology.ports[j].label); + } + } + + s = m.section(form.TableSection, 'switch_vlan', vlan_title); + s.anonymous = true; + s.addremove = true; + s.addbtntitle = _('Add VLAN'); + s.topology = topology; + s.device = switch_name; + + s.filter = function(section_id) { + var device = uci.get('network', section_id, 'device'); + return (device == switch_name); + }; + + s.cfgsections = function() { + var sections = form.TableSection.prototype.cfgsections.apply(this); + + return sections.sort(function(a, b) { + var vidA = feat.vid_option ? uci.get('network', a, feat.vid_option) : null, + vidB = feat.vid_option ? uci.get('network', b, feat.vid_option) : null; + + vidA = +(vidA != null ? vidA : uci.get('network', a, 'vlan') || 9999); + vidB = +(vidB != null ? vidB : uci.get('network', b, 'vlan') || 9999); + + return (vidA - vidB); + }); + }; + + s.handleAdd = function(ev) { + var sections = uci.sections('network', 'switch_vlan'), + section_id = uci.add('network', 'switch_vlan'), + max_vlan = 0, + max_vid = 0; + + for (var j = 0; j < sections.length; j++) { + if (sections[j].device != s.device) + continue; + + var vlan = +sections[j].vlan, + vid = feat.vid_option ? +sections[j][feat.vid_option] : null; + + if (vlan > max_vlan) + max_vlan = vlan; + + if (vid > max_vid) + max_vid = vid; + } + + uci.set('network', section_id, 'device', s.device); + uci.set('network', section_id, 'vlan', max_vlan + 1); + + if (feat.vid_option) + uci.set('network', section_id, feat.vid_option, max_vid + 1); + + return this.map.save(null, true); + }; + + var port_opts = []; + + o = s.option(form.Value, feat.vid_option || 'vlan', 'VLAN ID'); + o.rmempty = false; + o.forcewrite = true; + o.vlan_used = {}; + o.datatype = 'range(%u,%u)'.format(min_vid, feat.vid_option ? 4094 : num_vlans - 1); + o.description = _('Port status:'); + + o.validate = function(section_id, value) { + var v = +value, + m = feat.vid_option ? 4094 : num_vlans - 1; + + if (isNaN(v) || v < min_vid || v > m) + return _('Invalid VLAN ID given! Only IDs between %d and %d are allowed.').format(min_vid, m); + + var sections = this.section.cfgsections(); + + for (var i = 0; i < sections.length; i++) { + if (sections[i] == section_id) + continue; + + if (this.formvalue(sections[i]) == v) + return _('Invalid VLAN ID given! Only unique IDs are allowed'); + } + + return true; + }; + + o.write = function(section_id, value) { + var topology = this.section.topology, + values = []; + + for (var i = 0; i < port_opts.length; i++) { + var tagging = port_opts[i].formvalue(section_id), + portspec = Array.isArray(topology.ports) ? topology.ports[i] : null; + + if (tagging == 't') + values.push(port_opts[i].option + tagging); + else if (tagging == 'u') + values.push(port_opts[i].option); + + if (portspec && portspec.device) { + var old_tag = port_opts[i].cfgvalue(section_id), + old_vid = this.cfgvalue(section_id); + + if (old_tag != tagging || old_vid != value) { + var old_ifname = portspec.device + (old_tag != 'u' ? '.' + old_vid : ''), + new_ifname = portspec.device + (tagging != 'u' ? '.' + value : ''); + + if (old_ifname != new_ifname) + update_interfaces(old_ifname, new_ifname); + } + } + } + + if (feat.vlan4k_option) + uci.set('network', sid, feat.vlan4k_option, '1'); + + uci.set('network', section_id, 'ports', values.join(' ')); + + return form.Value.prototype.write.apply(this, [section_id, value]); + }; + + o.cfgvalue = function(section_id) { + var value = feat.vid_option ? uci.get('network', section_id, feat.vid_option) : null; + return (value || uci.get('network', section_id, 'vlan')); + }; + + for (var j = 0; Array.isArray(topology.ports) && j < topology.ports.length; j++) { + var portspec = topology.ports[j], + portstate = Array.isArray(topology.portstate) ? topology.portstate[portspec.num] : null; + + o = s.option(form.ListValue, String(portspec.num), portspec.label); + o.value('', _('off')); + + if (!portspec.need_tag) + o.value('u', _('untagged')); + + o.value('t', _('tagged')); + + o.cfgvalue = parse_portvalue; + o.validate = validate_portvalue; + o.write = function() {}; + + o.description = render_port_status(E('small', { + 'data-switch': switch_name, + 'data-port': portspec.num + }), portstate); + + port_opts.push(o); + } + + port_opts.sort(function(a, b) { + return a.option < b.option; + }); + } + + L.Poll.add(L.bind(update_port_status, m, topologies)); + + return m.render(); + } +}); |