'use strict'; 'require baseclass'; 'require fs'; 'require ui'; 'require uci'; 'require rpc'; 'require network'; 'require firewall'; var callLuciETHInfo = rpc.declare({ object: 'luci', method: 'getETHInfo', expect: { '': {} } }); function isString(v) { return typeof(v) === 'string' && v !== ''; } function resolveVLANChain(ifname, bridges, mapping) { while (!mapping[ifname]) { var m = ifname.match(/^(.+)\.([^.]+)$/); if (!m) break; if (bridges[m[1]]) { if (bridges[m[1]].vlan_filtering) mapping[ifname] = bridges[m[1]].vlans[m[2]]; else mapping[ifname] = bridges[m[1]].ports; } else if (/^[0-9]{1,4}$/.test(m[2]) && m[2] <= 4095) { mapping[ifname] = [ m[1] ]; } else { break; } ifname = m[1]; } } function buildVLANMappings(mapping) { var bridge_vlans = uci.sections('network', 'bridge-vlan'), vlan_devices = uci.sections('network', 'device'), interfaces = uci.sections('network', 'interface'), bridges = {}; /* find bridge VLANs */ for (var i = 0, s; (s = bridge_vlans[i]) != null; i++) { if (!isString(s.device) || !/^[0-9]{1,4}$/.test(s.vlan) || +s.vlan > 4095) continue; var aliases = L.toArray(s.alias), ports = L.toArray(s.ports), br = bridges[s.device] = (bridges[s.device] || { ports: [], vlans: {}, vlan_filtering: true }); br.vlans[s.vlan] = []; for (var j = 0; j < ports.length; j++) { var port = ports[j].replace(/:[ut*]+$/, ''); if (br.ports.indexOf(port) === -1) br.ports.push(port); br.vlans[s.vlan].push(port); } for (var j = 0; j < aliases.length; j++) if (aliases[j] != s.vlan) br.vlans[aliases[j]] = br.vlans[s.vlan]; } /* find bridges, VLAN devices */ for (var i = 0, s; (s = vlan_devices[i]) != null; i++) { if (s.type == 'bridge') { if (!isString(s.name)) continue; var ports = L.toArray(s.ports), br = bridges[s.name] || (bridges[s.name] = { ports: [], vlans: {}, vlan_filtering: false }); if (s.vlan_filtering == '0') br.vlan_filtering = false; else if (s.vlan_filtering == '1') br.vlan_filtering = true; for (var j = 0; j < ports.length; j++) if (br.ports.indexOf(ports[j]) === -1) br.ports.push(ports[j]); mapping[s.name] = br.ports; } else if (s.type == '8021q' || s.type == '8021ad') { if (!isString(s.name) || !isString(s.vid) || !isString(s.ifname)) continue; /* parent device is a bridge */ if (bridges[s.ifname]) { /* parent bridge is VLAN enabled, device refers to VLAN ports */ if (bridges[s.ifname].vlan_filtering) mapping[s.name] = bridges[s.ifname].vlans[s.vid]; /* parent bridge is not VLAN enabled, device refers to all bridge ports */ else mapping[s.name] = bridges[s.ifname].ports; } /* parent is a simple netdev */ else { mapping[s.name] = [ s.ifname ]; } resolveVLANChain(s.ifname, bridges, mapping); } } /* resolve VLAN tagged interfaces in bridge ports */ for (var brname in bridges) { for (var i = 0; i < bridges[brname].ports.length; i++) resolveVLANChain(bridges[brname].ports[i], bridges, mapping); for (var vid in bridges[brname].vlans) for (var i = 0; i < bridges[brname].vlans[vid].length; i++) resolveVLANChain(bridges[brname].vlans[vid][i], bridges, mapping); } /* find implicit VLAN devices */ for (var i = 0, s; (s = interfaces[i]) != null; i++) { if (!isString(s.device)) continue; resolveVLANChain(s.device, bridges, mapping); } } function resolveVLANPorts(ifname, mapping) { var ports = []; if (mapping[ifname]) for (var i = 0; i < mapping[ifname].length; i++) ports.push.apply(ports, resolveVLANPorts(mapping[ifname][i], mapping)); else ports.push(ifname); return ports.sort(L.naturalCompare); } function buildInterfaceMapping(zones, networks) { var vlanmap = {}, portmap = {}, netmap = {}; buildVLANMappings(vlanmap); for (var i = 0; i < networks.length; i++) { var l3dev = networks[i].getDevice(); if (!l3dev) continue; var ports = resolveVLANPorts(l3dev.getName(), vlanmap); for (var j = 0; j < ports.length; j++) { portmap[ports[j]] = portmap[ports[j]] || { networks: [], zones: [] }; portmap[ports[j]].networks.push(networks[i]); } netmap[networks[i].getName()] = networks[i]; } for (var i = 0; i < zones.length; i++) { var networknames = zones[i].getNetworks(); for (var j = 0; j < networknames.length; j++) { if (!netmap[networknames[j]]) continue; var l3dev = netmap[networknames[j]].getDevice(); if (!l3dev) continue; var ports = resolveVLANPorts(l3dev.getName(), vlanmap); for (var k = 0; k < ports.length; k++) { portmap[ports[k]] = portmap[ports[k]] || { networks: [], zones: [] }; if (portmap[ports[k]].zones.indexOf(zones[i]) === -1) portmap[ports[k]].zones.push(zones[i]); } } } return portmap; } function formatSpeed(speed, duplex) { if (speed && duplex) { var d = (duplex == 'half') ? '\u202f(H)' : '', e = E('span', { 'title': _('Speed: %d Mbit/s, Duplex: %s').format(speed, duplex) }); switch (speed) { case 10: e.innerText = '10\u202fM' + d; break; case 100: e.innerText = '100\u202fM' + d; break; case 1000: e.innerText = '1\u202fGbE' + d; break; case 2500: e.innerText = '2.5\u202fGbE'; break; case 5000: e.innerText = '5\u202fGbE'; break; case 10000: e.innerText = '10\u202fGbE'; break; case 25000: e.innerText = '25\u202fGbE'; break; case 40000: e.innerText = '40\u202fGbE'; break; default: e.innerText = '%d\u202fMbE%s'.format(speed, d); } return e; } return _('no link'); } function formatStats(portdev) { var stats = portdev._devstate('stats'); return ui.itemlist(E('span'), [ _('Received bytes'), '%1024mB'.format(stats.rx_bytes), _('Received packets'), '%1000mPkts.'.format(stats.rx_packets), _('Received multicast'), '%1000mPkts.'.format(stats.multicast), _('Receive errors'), '%1000mPkts.'.format(stats.rx_errors), _('Receive dropped'), '%1000mPkts.'.format(stats.rx_dropped), _('Transmitted bytes'), '%1024mB'.format(stats.tx_bytes), _('Transmitted packets'), '%1000mPkts.'.format(stats.tx_packets), _('Transmit errors'), '%1000mPkts.'.format(stats.tx_errors), _('Transmit dropped'), '%1000mPkts.'.format(stats.tx_dropped), _('Collisions seen'), stats.collisions ]); } function renderNetworkBadge(network, zonename) { var l3dev = network.getDevice(); var span = E('span', { 'class': 'ifacebadge', 'style': 'margin:.125em 0' }, [ E('span', { 'class': 'zonebadge', 'title': zonename ? _('Part of zone %q').format(zonename) : _('No zone assigned'), 'style': firewall.getZoneColorStyle(zonename) }, '\u202f'), '\u202f', network.getName(), ': ' ]); if (l3dev) span.appendChild(E('img', { 'title': l3dev.getI18n(), 'src': L.resource('icons/%s%s.png'.format(l3dev.getType(), l3dev.isUp() ? '' : '_disabled')) })); else span.appendChild(E('em', _('(no interfaces attached)'))); return span; } function renderNetworksTooltip(pmap) { var res = [ null ], zmap = {}; for (var i = 0; pmap && i < pmap.zones.length; i++) { var networknames = pmap.zones[i].getNetworks(); for (var k = 0; k < networknames.length; k++) zmap[networknames[k]] = pmap.zones[i].getName(); } for (var i = 0; pmap && i < pmap.networks.length; i++) res.push(E('br'), renderNetworkBadge(pmap.networks[i], zmap[pmap.networks[i].getName()])); if (res.length > 1) res[0] = N_((res.length - 1) / 2, 'Part of network:', 'Part of networks:'); else res[0] = _('Port is not part of any network'); return E([], res); } return baseclass.extend({ title: _('Ethernet Information'), load: function() { return Promise.all([ L.resolveDefault(callLuciETHInfo(), {}), firewall.getZones(), network.getNetworks(), uci.load('network') ]); }, render: function(data) { var known_ports = [], port_map = buildInterfaceMapping(data[1], data[2]); if (Array.isArray(data[0].ethinfo)) data[0].ethinfo.forEach((k) => known_ports.push({ device: k, netdev: network.instantiateDevice(k) })); known_ports.sort(function(a, b) { return L.naturalCompare(a.device, b.device); }); return E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(100px, 1fr));margin-bottom:1em;align-items:center;justify-items:center;text-align:center' }, known_ports.map(function(port) { var speed = port.netdev.getSpeed(), duplex = port.netdev.getDuplex(), pmap = port_map[port.netdev.getName()], pzones = (pmap && pmap.zones.length) ? pmap.zones.sort(function(a, b) { return L.naturalCompare(a.getName(), b.getName()) }) : [ null ]; return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100px' }, [ E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.netdev.getName() ]), E('div', { 'class': 'ifacebox-body' }, [ E('img', { 'src': L.resource('icons/port_%s.png').format((speed && duplex) ? 'up' : 'down') }), E('br'), formatSpeed(speed, duplex) ]), E('div', { 'class': 'ifacebox-head cbi-tooltip-container', 'style': 'display:flex' }, [ E([], pzones.map(function(zone) { return E('div', { 'class': 'zonebadge', 'style': 'cursor:help;flex:1;height:3px;' + firewall.getZoneColorStyle(zone) }); })), E('span', { 'class': 'cbi-tooltip left' }, [ renderNetworksTooltip(pmap) ]) ]), E('div', { 'class': 'ifacebox-body' }, [ E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, [ '\u25b2\u202f%1024.1mB'.format(port.netdev.getTXBytes()), E('br'), '\u25bc\u202f%1024.1mB'.format(port.netdev.getRXBytes()), E('span', { 'class': 'cbi-tooltip' }, formatStats(port.netdev)) ]), ]) ]); })); } });