31af6f460c0315955d33c6477c8ba0968ebb146e
[project/luci.git] / modules / luci-mod-status / htdocs / luci-static / resources / view / status / include / 29_ports.js
1 'use strict';
2 'require baseclass';
3 'require fs';
4 'require ui';
5 'require uci';
6 'require network';
7 'require firewall';
8
9 function isString(v)
10 {
11 return typeof(v) === 'string' && v !== '';
12 }
13
14 function resolveVLANChain(ifname, bridges, mapping)
15 {
16 while (!mapping[ifname]) {
17 var m = ifname.match(/^(.+)\.([^.]+)$/);
18
19 if (!m)
20 break;
21
22 if (bridges[m[1]]) {
23 if (bridges[m[1]].vlan_filtering)
24 mapping[ifname] = bridges[m[1]].vlans[m[2]];
25 else
26 mapping[ifname] = bridges[m[1]].ports;
27 }
28 else if (/^[0-9]{1,4}$/.test(m[2]) && m[2] <= 4095) {
29 mapping[ifname] = [ m[1] ];
30 }
31 else {
32 break;
33 }
34
35 ifname = m[1];
36 }
37 }
38
39 function buildVLANMappings(mapping)
40 {
41 var bridge_vlans = uci.sections('network', 'bridge-vlan'),
42 vlan_devices = uci.sections('network', 'device'),
43 interfaces = uci.sections('network', 'interface'),
44 bridges = {};
45
46 /* find bridge VLANs */
47 for (var i = 0, s; (s = bridge_vlans[i]) != null; i++) {
48 if (!isString(s.device) || !/^[0-9]{1,4}$/.test(s.vlan) || +s.vlan > 4095)
49 continue;
50
51 var aliases = L.toArray(s.alias),
52 ports = L.toArray(s.ports),
53 br = bridges[s.device] = (bridges[s.device] || { ports: [], vlans: {}, vlan_filtering: true });
54
55 br.vlans[s.vlan] = [];
56
57 for (var j = 0; j < ports.length; j++) {
58 var port = ports[j].replace(/:[ut*]+$/, '');
59
60 if (br.ports.indexOf(port) === -1)
61 br.ports.push(port);
62
63 br.vlans[s.vlan].push(port);
64 }
65
66 for (var j = 0; j < aliases.length; j++)
67 if (aliases[j] != s.vlan)
68 br.vlans[aliases[j]] = br.vlans[s.vlan];
69 }
70
71 /* find bridges, VLAN devices */
72 for (var i = 0, s; (s = vlan_devices[i]) != null; i++) {
73 if (s.type == 'bridge') {
74 if (!isString(s.name))
75 continue;
76
77 var ports = L.toArray(s.ports),
78 br = bridges[s.name] || (bridges[s.name] = { ports: [], vlans: {}, vlan_filtering: false });
79
80 if (s.vlan_filtering == '0')
81 br.vlan_filtering = false;
82 else if (s.vlan_filtering == '1')
83 br.vlan_filtering = true;
84
85 for (var j = 0; j < ports.length; j++)
86 if (br.ports.indexOf(ports[j]) === -1)
87 br.ports.push(ports[j]);
88
89 mapping[s.name] = br.ports;
90 }
91 else if (s.type == '8021q' || s.type == '8021ad') {
92 if (!isString(s.name) || !isString(s.vid) || !isString(s.ifname))
93 continue;
94
95 /* parent device is a bridge */
96 if (bridges[s.ifname]) {
97 /* parent bridge is VLAN enabled, device refers to VLAN ports */
98 if (bridges[s.ifname].vlan_filtering)
99 mapping[s.name] = bridges[s.ifname].vlans[s.vid];
100
101 /* parent bridge is not VLAN enabled, device refers to all bridge ports */
102 else
103 mapping[s.name] = bridges[s.ifname].ports;
104 }
105
106 /* parent is a simple netdev */
107 else {
108 mapping[s.name] = [ s.ifname ];
109 }
110
111 resolveVLANChain(s.ifname, bridges, mapping);
112 }
113 }
114
115 /* resolve VLAN tagged interfaces in bridge ports */
116 for (var brname in bridges) {
117 for (var i = 0; i < bridges[brname].ports.length; i++)
118 resolveVLANChain(bridges[brname].ports[i], bridges, mapping);
119
120 for (var vid in bridges[brname].vlans)
121 for (var i = 0; i < bridges[brname].vlans[vid].length; i++)
122 resolveVLANChain(bridges[brname].vlans[vid][i], bridges, mapping);
123 }
124
125 /* find implicit VLAN devices */
126 for (var i = 0, s; (s = interfaces[i]) != null; i++) {
127 if (!isString(s.device))
128 continue;
129
130 resolveVLANChain(s.device, bridges, mapping);
131 }
132 }
133
134 function resolveVLANPorts(ifname, mapping)
135 {
136 var ports = [];
137
138 if (mapping[ifname])
139 for (var i = 0; i < mapping[ifname].length; i++)
140 ports.push.apply(ports, resolveVLANPorts(mapping[ifname][i], mapping));
141 else
142 ports.push(ifname);
143
144 return ports.sort(L.naturalCompare);
145 }
146
147 function buildInterfaceMapping(zones, networks) {
148 var vlanmap = {},
149 portmap = {},
150 netmap = {};
151
152 buildVLANMappings(vlanmap);
153
154 for (var i = 0; i < networks.length; i++) {
155 var l3dev = networks[i].getDevice();
156
157 if (!l3dev)
158 continue;
159
160 var ports = resolveVLANPorts(l3dev.getName(), vlanmap);
161
162 for (var j = 0; j < ports.length; j++) {
163 portmap[ports[j]] = portmap[ports[j]] || { networks: [], zones: [] };
164 portmap[ports[j]].networks.push(networks[i]);
165 }
166
167 netmap[networks[i].getName()] = networks[i];
168 }
169
170 for (var i = 0; i < zones.length; i++) {
171 var networknames = zones[i].getNetworks();
172
173 for (var j = 0; j < networknames.length; j++) {
174 if (!netmap[networknames[j]])
175 continue;
176
177 var l3dev = netmap[networknames[j]].getDevice();
178
179 if (!l3dev)
180 continue;
181
182 var ports = resolveVLANPorts(l3dev.getName(), vlanmap);
183
184 for (var k = 0; k < ports.length; k++) {
185 portmap[ports[k]] = portmap[ports[k]] || { networks: [], zones: [] };
186
187 if (portmap[ports[k]].zones.indexOf(zones[i]) === -1)
188 portmap[ports[k]].zones.push(zones[i]);
189 }
190 }
191 }
192
193 return portmap;
194 }
195
196 function formatSpeed(speed, duplex) {
197 if (speed && duplex) {
198 var d = (duplex == 'half') ? '\u202f(H)' : '',
199 e = E('span', { 'title': _('Speed: %d Mibit/s, Duplex: %s').format(speed, duplex) });
200
201 switch (speed) {
202 case 10: e.innerText = '10\u202fM' + d; break;
203 case 100: e.innerText = '100\u202fM' + d; break;
204 case 1000: e.innerText = '1\u202fGbE' + d; break;
205 case 2500: e.innerText = '2.5\u202fGbE'; break;
206 case 5000: e.innerText = '5\u202fGbE'; break;
207 case 10000: e.innerText = '10\u202fGbE'; break;
208 case 25000: e.innerText = '25\u202fGbE'; break;
209 case 40000: e.innerText = '40\u202fGbE'; break;
210 default: e.innerText = '%d\u202fMbE%s'.format(speed, d);
211 }
212
213 return e;
214 }
215
216 return _('no link');
217 }
218
219 function formatStats(portdev) {
220 var stats = portdev._devstate('stats');
221
222 return ui.itemlist(E('span'), [
223 _('Received bytes'), '%1024mB'.format(stats.rx_bytes),
224 _('Received packets'), '%1000mPkts.'.format(stats.rx_packets),
225 _('Received multicast'), '%1000mPkts.'.format(stats.multicast),
226 _('Receive errors'), '%1000mPkts.'.format(stats.rx_errors),
227 _('Receive dropped'), '%1000mPkts.'.format(stats.rx_dropped),
228
229 _('Transmitted bytes'), '%1024mB'.format(stats.tx_bytes),
230 _('Transmitted packets'), '%1000mPkts.'.format(stats.tx_packets),
231 _('Transmit errors'), '%1000mPkts.'.format(stats.tx_errors),
232 _('Transmit dropped'), '%1000mPkts.'.format(stats.tx_dropped),
233
234 _('Collisions seen'), stats.collisions
235 ]);
236 }
237
238 function renderNetworkBadge(network, zonename) {
239 var l3dev = network.getDevice();
240 var span = E('span', { 'class': 'ifacebadge', 'style': 'margin:.125em 0' }, [
241 E('span', {
242 'class': 'zonebadge',
243 'title': zonename ? _('Part of zone %q').format(zonename) : _('No zone assigned'),
244 'style': firewall.getZoneColorStyle(zonename)
245 }, '\u202f'),
246 '\u202f', network.getName(), ': '
247 ]);
248
249 if (l3dev)
250 span.appendChild(E('img', {
251 'title': l3dev.getI18n(),
252 'src': L.resource('icons/%s%s.png'.format(l3dev.getType(), l3dev.isUp() ? '' : '_disabled'))
253 }));
254 else
255 span.appendChild(E('em', _('(no interfaces attached)')));
256
257 return span;
258 }
259
260 function renderNetworksTooltip(pmap) {
261 var res = [ null ],
262 zmap = {};
263
264 for (var i = 0; pmap && i < pmap.zones.length; i++) {
265 var networknames = pmap.zones[i].getNetworks();
266
267 for (var k = 0; k < networknames.length; k++)
268 zmap[networknames[k]] = pmap.zones[i].getName();
269 }
270
271 for (var i = 0; pmap && i < pmap.networks.length; i++)
272 res.push(E('br'), renderNetworkBadge(pmap.networks[i], zmap[pmap.networks[i].getName()]));
273
274 if (res.length > 1)
275 res[0] = N_((res.length - 1) / 2, 'Part of network:', 'Part of networks:');
276 else
277 res[0] = _('Port is not part of any network');
278
279 return E([], res);
280 }
281
282 return baseclass.extend({
283 title: _('Port status'),
284
285 load: function() {
286 return Promise.all([
287 L.resolveDefault(fs.read('/etc/board.json'), '{}'),
288 firewall.getZones(),
289 network.getNetworks(),
290 uci.load('network')
291 ]);
292 },
293
294 render: function(data) {
295 if (L.hasSystemFeature('swconfig'))
296 return null;
297
298 var board = JSON.parse(data[0]),
299 known_ports = [],
300 port_map = buildInterfaceMapping(data[1], data[2]);
301
302 if (L.isObject(board) && L.isObject(board.network)) {
303 for (var k = 'lan'; k != null; k = (k == 'lan') ? 'wan' : null) {
304 if (!L.isObject(board.network[k]))
305 continue;
306
307 if (Array.isArray(board.network[k].ports))
308 for (let i = 0; i < board.network[k].ports.length; i++)
309 known_ports.push({
310 role: k,
311 device: board.network[k].ports[i],
312 netdev: network.instantiateDevice(board.network[k].ports[i])
313 });
314 else if (typeof(board.network[k].device) == 'string')
315 known_ports.push({
316 role: k,
317 device: board.network[k].device,
318 netdev: network.instantiateDevice(board.network[k].device)
319 });
320 }
321 }
322
323 known_ports.sort(function(a, b) {
324 return L.naturalCompare(a.device, b.device);
325 });
326
327 return E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(70px, 1fr));margin-bottom:1em' }, known_ports.map(function(port) {
328 var speed = port.netdev.getSpeed(),
329 duplex = port.netdev.getDuplex(),
330 pmap = port_map[port.netdev.getName()],
331 pzones = (pmap && pmap.zones.length) ? pmap.zones.sort(function(a, b) { return L.naturalCompare(a.getName(), b.getName()) }) : [ null ];
332
333 return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;min-width:70px;max-width:100px' }, [
334 E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.netdev.getName() ]),
335 E('div', { 'class': 'ifacebox-body' }, [
336 E('img', { 'src': L.resource('icons/port_%s.png').format((speed && duplex) ? 'up' : 'down') }),
337 E('br'),
338 formatSpeed(speed, duplex)
339 ]),
340 E('div', { 'class': 'ifacebox-head cbi-tooltip-container', 'style': 'display:flex' }, [
341 E([], pzones.map(function(zone) {
342 return E('div', {
343 'class': 'zonebadge',
344 'style': 'cursor:help;flex:1;height:3px;' + firewall.getZoneColorStyle(zone)
345 });
346 })),
347 E('span', { 'class': 'cbi-tooltip left' }, [ renderNetworksTooltip(pmap) ])
348 ]),
349 E('div', { 'class': 'ifacebox-body' }, [
350 E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, [
351 '\u25b2\u202f%1024.1mB'.format(port.netdev.getTXBytes()),
352 E('br'),
353 '\u25bc\u202f%1024.1mB'.format(port.netdev.getRXBytes()),
354 E('span', { 'class': 'cbi-tooltip' }, formatStats(port.netdev))
355 ]),
356 ])
357 ]);
358 }));
359 }
360 });