mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-07 12:44:06 +08:00
⛄ Sync 2026-04-23 20:32:35
This commit is contained in:
1 parent
17738c6f79
commit
fe51d2b304
206 files changed
+725671
No files matched your search
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callGetUnixtime = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getUnixtime',
|
||||
expect: { result: 0 }
|
||||
});
|
||||
|
||||
var callLuciVersion = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getVersion'
|
||||
});
|
||||
|
||||
var callSystemBoard = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'board'
|
||||
});
|
||||
|
||||
var callSystemInfo = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'info'
|
||||
});
|
||||
|
||||
var callCPUBench = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getCPUBench'
|
||||
});
|
||||
|
||||
var callCPUInfo = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getCPUInfo'
|
||||
});
|
||||
|
||||
var callTempInfo = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getTempInfo'
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('System'),
|
||||
disableCache: true,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callSystemBoard(), {}),
|
||||
L.resolveDefault(callSystemInfo(), {}),
|
||||
L.resolveDefault(callCPUBench(), {}),
|
||||
L.resolveDefault(callCPUInfo(), {}),
|
||||
L.resolveDefault(callTempInfo(), {}),
|
||||
L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }),
|
||||
L.resolveDefault(callGetUnixtime(), 0)
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var boardinfo = data[0],
|
||||
systeminfo = data[1],
|
||||
cpubench = data[2],
|
||||
cpuinfo = data[3],
|
||||
tempinfo = data[4],
|
||||
luciversion = data[5],
|
||||
unixtime = data[6];
|
||||
|
||||
luciversion = luciversion.branch;
|
||||
|
||||
var datestr = null;
|
||||
|
||||
if (unixtime) {
|
||||
var date = new Date(unixtime * 1000);
|
||||
|
||||
datestr = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'long'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
var fields = [
|
||||
_('Hostname'), boardinfo.hostname,
|
||||
_('Architecture'), (cpuinfo.cpuinfo || boardinfo.system) + ' ' + cpubench.cpubench,
|
||||
_('Target Platform'), (L.isObject(boardinfo.release) ? boardinfo.release.target : ''),
|
||||
_('Firmware Version'), (L.isObject(boardinfo.release)
|
||||
? '%s%s / '.format(
|
||||
boardinfo.release.description || '',
|
||||
boardinfo.release.revision ? boardinfo.release.revision : ''
|
||||
)
|
||||
: '') + (luciversion || ''),
|
||||
_('Kernel Version'), boardinfo.kernel,
|
||||
_('Local Time'), datestr,
|
||||
_('Uptime'), systeminfo.uptime ? '%t'.format(systeminfo.uptime) : null,
|
||||
_('Load Average'), Array.isArray(systeminfo.load) ? '%.2f, %.2f, %.2f'.format(
|
||||
systeminfo.load[0] / 65535.0,
|
||||
systeminfo.load[1] / 65535.0,
|
||||
systeminfo.load[2] / 65535.0
|
||||
) : null
|
||||
];
|
||||
|
||||
if (tempinfo.tempinfo) {
|
||||
fields.splice(6, 0, _('Temperature'));
|
||||
fields.splice(7, 0, tempinfo.tempinfo);
|
||||
}
|
||||
if (boardinfo.model != "Default string Default string") {
|
||||
fields.splice(2, 0, _('Model'));
|
||||
fields.splice(3, 0, boardinfo.model);
|
||||
}
|
||||
|
||||
var table = E('table', { 'class': 'table' });
|
||||
|
||||
for (var i = 0; i < fields.length; i += 2) {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ fields[i] ]),
|
||||
E('td', { 'class': 'td left' }, [ (fields[i + 1] != null) ? fields[i + 1] : '?' ])
|
||||
]));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,777 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require network';
|
||||
'require firewall';
|
||||
|
||||
const callGetBuiltinEthernetPorts = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getBuiltinEthernetPorts',
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callSwconfigPortState = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getSwconfigPortState',
|
||||
params: [ 'switch' ],
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callNetworkDeviceStatus = rpc.declare({
|
||||
object: 'network.device',
|
||||
method: 'status',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function isString(v)
|
||||
{
|
||||
return typeof(v) === 'string' && v !== '';
|
||||
}
|
||||
|
||||
function parseBoardPortList(value)
|
||||
{
|
||||
if (Array.isArray(value))
|
||||
return value.filter(isString);
|
||||
|
||||
if (isString(value))
|
||||
return value.trim().split(/[\s,]+/).filter(isString);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function addKnownPort(knownPorts, seenPorts, role, device, extra)
|
||||
{
|
||||
if (!isString(device) || seenPorts[device])
|
||||
return;
|
||||
|
||||
seenPorts[device] = true;
|
||||
|
||||
const port = Object.assign({
|
||||
role,
|
||||
device,
|
||||
label: device
|
||||
}, extra || {});
|
||||
|
||||
if (!port.netdev && !port.swstate)
|
||||
port.netdev = network.instantiateDevice(device);
|
||||
|
||||
knownPorts.push(port);
|
||||
}
|
||||
|
||||
function parseSwitchPortToken(token)
|
||||
{
|
||||
if (!isString(token))
|
||||
return null;
|
||||
|
||||
const match = token.match(/^(\d+)([a-z*]+)?$/i);
|
||||
|
||||
return match ? { port: +match[1], flags: (match[2] || '').toLowerCase() } : null;
|
||||
}
|
||||
|
||||
function appendSwitchMemberPorts(sw, ports, cpuPorts, memberPorts)
|
||||
{
|
||||
for (const value of ports) {
|
||||
const token = parseSwitchPortToken(value);
|
||||
|
||||
if (!token)
|
||||
continue;
|
||||
|
||||
for (const port of sw.ports) {
|
||||
if (port.num != token.port)
|
||||
continue;
|
||||
|
||||
if (isString(port.device))
|
||||
cpuPorts[port.device] = true;
|
||||
else
|
||||
memberPorts.push('%s:%d'.format(sw['.name'] || sw.name || '', token.port));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildLegacySwitchMappings(mapping, board)
|
||||
{
|
||||
const switchVlans = uci.sections('network', 'switch_vlan');
|
||||
|
||||
if (!L.isObject(board) || !L.isObject(board.switch))
|
||||
return;
|
||||
|
||||
for (const section of switchVlans) {
|
||||
const swname = section.device;
|
||||
const sw = board.switch[swname];
|
||||
const vid = section.vid || section.vlan;
|
||||
|
||||
if (!L.isObject(sw) || !/^[0-9]{1,4}$/.test(vid) || +vid > 4095)
|
||||
continue;
|
||||
|
||||
const ports = L.toArray(section.ports);
|
||||
const cpuPorts = {};
|
||||
let memberPorts = [];
|
||||
|
||||
sw['.name'] = swname;
|
||||
appendSwitchMemberPorts(sw, ports, cpuPorts, memberPorts);
|
||||
|
||||
memberPorts = memberPorts.filter((port, index) => memberPorts.indexOf(port) === index);
|
||||
|
||||
for (const cpuDev in cpuPorts)
|
||||
mapping['%s.%s'.format(cpuDev, vid)] = memberPorts;
|
||||
}
|
||||
|
||||
for (const swname in board.switch) {
|
||||
const sw = board.switch[swname];
|
||||
|
||||
if (!L.isObject(sw) || !Array.isArray(sw.roles) || !Array.isArray(sw.ports))
|
||||
continue;
|
||||
|
||||
sw['.name'] = swname;
|
||||
|
||||
for (const role of sw.roles) {
|
||||
if (!L.isObject(role) || !isString(role.device))
|
||||
continue;
|
||||
|
||||
const cpuPorts = {};
|
||||
let memberPorts = [];
|
||||
|
||||
appendSwitchMemberPorts(sw, parseBoardPortList(role.ports), cpuPorts, memberPorts);
|
||||
|
||||
if (!cpuPorts[role.device])
|
||||
cpuPorts[role.device] = true;
|
||||
|
||||
memberPorts = memberPorts.filter((port, index) => memberPorts.indexOf(port) === index);
|
||||
|
||||
if (!mapping[role.device])
|
||||
mapping[role.device] = [];
|
||||
|
||||
for (const port of memberPorts)
|
||||
if (mapping[role.device].indexOf(port) === -1)
|
||||
mapping[role.device].push(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSwitchPortLabel(board, switchName, portNum)
|
||||
{
|
||||
const sw = L.isObject(board) && L.isObject(board.switch) ? board.switch[switchName] : null;
|
||||
|
||||
if (!L.isObject(sw) || !Array.isArray(sw.ports))
|
||||
return '%s:%d'.format(switchName, portNum);
|
||||
|
||||
for (let i = 0; i < sw.ports.length; i++) {
|
||||
if (sw.ports[i].num != portNum)
|
||||
continue;
|
||||
|
||||
if (isString(sw.ports[i].label))
|
||||
return sw.ports[i].label;
|
||||
|
||||
if (isString(sw.ports[i].role)) {
|
||||
let index = 1;
|
||||
|
||||
for (let j = 0; j < i; j++)
|
||||
if (sw.ports[j].role == sw.ports[i].role)
|
||||
index++;
|
||||
|
||||
return sw.ports[i].role + index;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return '%s:%d'.format(switchName, portNum);
|
||||
}
|
||||
|
||||
function addResolvedPort(knownPorts, seenPorts, role, device, mapping, board, swstate)
|
||||
{
|
||||
const resolved = resolveVLANPorts(device, mapping);
|
||||
|
||||
for (const value of resolved) {
|
||||
const match = value.match(/^([^:]+):(\d+)$/);
|
||||
|
||||
if (match) {
|
||||
const switchName = match[1];
|
||||
const portNum = +match[2];
|
||||
const switchPorts = swstate[switchName] || {};
|
||||
|
||||
addKnownPort(knownPorts, seenPorts, role, value, {
|
||||
label: getSwitchPortLabel(board, switchName, portNum),
|
||||
swstate: switchPorts[portNum] || {
|
||||
port: portNum,
|
||||
link: false,
|
||||
speed: 0,
|
||||
duplex: false,
|
||||
rx_bytes: 0,
|
||||
tx_bytes: 0,
|
||||
rx_packets: 0,
|
||||
tx_packets: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
addKnownPort(knownPorts, seenPorts, role, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addBoardNetworkPorts(knownPorts, seenPorts, role, entry, mapping, board, swstate)
|
||||
{
|
||||
if (!L.isObject(entry))
|
||||
return;
|
||||
|
||||
const values = parseBoardPortList(entry.ports).concat(parseBoardPortList(entry.ifname));
|
||||
|
||||
if (isString(entry.device))
|
||||
values.push(entry.device);
|
||||
|
||||
for (const value of values)
|
||||
addResolvedPort(knownPorts, seenPorts, role, value, mapping, board, swstate);
|
||||
}
|
||||
|
||||
function resolveVLANChain(ifname, bridges, mapping)
|
||||
{
|
||||
while (!mapping[ifname]) {
|
||||
const 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, board)
|
||||
{
|
||||
const bridge_vlans = uci.sections('network', 'bridge-vlan');
|
||||
const vlan_devices = uci.sections('network', 'device');
|
||||
const interfaces = uci.sections('network', 'interface');
|
||||
const bridges = {};
|
||||
|
||||
/* find bridge VLANs */
|
||||
for (let i = 0, s; (s = bridge_vlans[i]) != null; i++) {
|
||||
if (!isString(s.device) || !/^[0-9]{1,4}$/.test(s.vlan) || +s.vlan > 4095)
|
||||
continue;
|
||||
|
||||
const aliases = L.toArray(s.alias);
|
||||
const ports = L.toArray(s.ports);
|
||||
const br = bridges[s.device] = (bridges[s.device] || { ports: [], vlans: {}, vlan_filtering: true });
|
||||
|
||||
br.vlans[s.vlan] = [];
|
||||
|
||||
for (let p of ports) {
|
||||
const port = p.replace(/:[ut*]+$/, '');
|
||||
|
||||
if (br.ports.indexOf(port) === -1)
|
||||
br.ports.push(port);
|
||||
|
||||
br.vlans[s.vlan].push(port);
|
||||
}
|
||||
|
||||
for (let a of aliases)
|
||||
if (a != s.vlan)
|
||||
br.vlans[a] = br.vlans[s.vlan];
|
||||
}
|
||||
|
||||
/* find bridges, VLAN devices */
|
||||
for (let i = 0, s; (s = vlan_devices[i]) != null; i++) {
|
||||
if (s.type == 'bridge') {
|
||||
if (!isString(s.name))
|
||||
continue;
|
||||
|
||||
const ports = L.toArray(s.ports);
|
||||
const 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 (let p of ports)
|
||||
if (br.ports.indexOf(p) === -1)
|
||||
br.ports.push(p);
|
||||
|
||||
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 (let brname in bridges) {
|
||||
for (let bp of bridges[brname].ports)
|
||||
resolveVLANChain(bp, bridges, mapping);
|
||||
|
||||
for (let vid in bridges[brname].vlans)
|
||||
for (let v of bridges[brname].vlans[vid])
|
||||
resolveVLANChain(v, bridges, mapping);
|
||||
}
|
||||
|
||||
buildLegacySwitchMappings(mapping, board);
|
||||
|
||||
/* find implicit VLAN devices */
|
||||
for (let i = 0, s; (s = interfaces[i]) != null; i++) {
|
||||
if (!isString(s.device))
|
||||
continue;
|
||||
|
||||
resolveVLANChain(s.device, bridges, mapping);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVLANPorts(ifname, mapping, seen)
|
||||
{
|
||||
const ports = [];
|
||||
|
||||
if (!seen)
|
||||
seen = {};
|
||||
|
||||
if (mapping[ifname]) {
|
||||
for (let m of mapping[ifname]) {
|
||||
if (!seen[m]) {
|
||||
seen[m] = true;
|
||||
ports.push.apply(ports, resolveVLANPorts(m, mapping, seen));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
ports.push(ifname);
|
||||
}
|
||||
|
||||
return ports.sort(L.naturalCompare);
|
||||
}
|
||||
|
||||
function buildInterfaceMapping(zones, networks, board) {
|
||||
const vlanmap = {};
|
||||
const portmap = {};
|
||||
const netmap = {};
|
||||
|
||||
buildVLANMappings(vlanmap, board);
|
||||
|
||||
for (let net of networks) {
|
||||
const l3dev = net.getDevice();
|
||||
|
||||
if (!l3dev)
|
||||
continue;
|
||||
|
||||
const ports = resolveVLANPorts(l3dev.getName(), vlanmap);
|
||||
|
||||
for (let p of ports) {
|
||||
portmap[p] = portmap[p] || { networks: [], zones: [] };
|
||||
portmap[p].networks.push(net);
|
||||
}
|
||||
|
||||
netmap[net.getName()] = net;
|
||||
}
|
||||
|
||||
for (let z of zones) {
|
||||
const networknames = z.getNetworks();
|
||||
|
||||
for (let nn of networknames) {
|
||||
if (!netmap[nn])
|
||||
continue;
|
||||
|
||||
const l3dev = netmap[nn].getDevice();
|
||||
|
||||
if (!l3dev)
|
||||
continue;
|
||||
|
||||
const ports = resolveVLANPorts(l3dev.getName(), vlanmap);
|
||||
|
||||
for (let p of ports) {
|
||||
portmap[p] = portmap[p] || { networks: [], zones: [] };
|
||||
|
||||
if (portmap[p].zones.indexOf(z) === -1)
|
||||
portmap[p].zones.push(z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return portmap;
|
||||
}
|
||||
|
||||
function formatSpeed(carrier, speed, duplex) {
|
||||
if ((speed > 0) && duplex) {
|
||||
const d = (duplex == 'half') ? '\u202f(H)' : '';
|
||||
const e = E('span', { 'title': _('Speed: %d Mbit/s, Duplex: %s').format(speed, duplex) });
|
||||
|
||||
switch (true) {
|
||||
case (speed < 1000):
|
||||
e.innerText = '%d\u202fM%s'.format(speed, d);
|
||||
break;
|
||||
case (speed == 1000):
|
||||
e.innerText = '1\u202fGbE' + d;
|
||||
break;
|
||||
case (speed >= 1e6 && speed < 1e9):
|
||||
e.innerText = '%f\u202fTbE'.format(speed / 1e6);
|
||||
break;
|
||||
case (speed >= 1e9):
|
||||
e.innerText = '%f\u202fPbE'.format(speed / 1e9);
|
||||
break;
|
||||
default: e.innerText = '%f\u202fGbE'.format(speed / 1000);
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
return carrier ? _('Connected') : _('no link');
|
||||
}
|
||||
|
||||
function getPSEStatus(pse) {
|
||||
if (!pse)
|
||||
return null;
|
||||
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'];
|
||||
|
||||
return {
|
||||
status: status,
|
||||
power: power,
|
||||
isDelivering: status === 'delivering' && power > 0
|
||||
};
|
||||
}
|
||||
|
||||
function formatPSEPower(pse) {
|
||||
if (!pse)
|
||||
return null;
|
||||
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'];
|
||||
|
||||
if (status === 'delivering' && power) {
|
||||
const watts = (power / 1000).toFixed(1);
|
||||
/* Format: "⚡ 15.4 W" - lightning bolt + narrow space + watts + narrow space + W */
|
||||
return E('span', { 'style': 'color:#000' },
|
||||
[ '\u26a1\ufe0e\u202f%s\u202fW'.format(watts) ]);
|
||||
}
|
||||
else if (status === 'searching') {
|
||||
return E('span', { 'style': 'color:#000' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('searching') ]);
|
||||
}
|
||||
else if (status === 'fault' || status === 'otherfault' || status === 'error') {
|
||||
return E('span', { 'style': 'color:#d9534f' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('fault') ]);
|
||||
}
|
||||
else if (status === 'disabled') {
|
||||
return E('span', { 'style': 'color:#888' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('off') ]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatStats(port, pse) {
|
||||
const stats = port.netdev
|
||||
? (port.netdev._devstate('stats') || {})
|
||||
: {
|
||||
rx_bytes: port.swstate ? port.swstate.rx_bytes : null,
|
||||
rx_packets: port.swstate ? port.swstate.rx_packets : null,
|
||||
multicast: null,
|
||||
rx_errors: null,
|
||||
rx_dropped: null,
|
||||
tx_bytes: port.swstate ? port.swstate.tx_bytes : null,
|
||||
tx_packets: port.swstate ? port.swstate.tx_packets : null,
|
||||
tx_errors: null,
|
||||
tx_dropped: null,
|
||||
collisions: null
|
||||
};
|
||||
const items = [
|
||||
_('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
|
||||
];
|
||||
|
||||
if (pse) {
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'],
|
||||
powerClass = pse['c33-power-class'],
|
||||
powerLimit = pse['c33-available-power-limit'];
|
||||
|
||||
items.push(_('PoE status'), status || _('unknown'));
|
||||
|
||||
if (power)
|
||||
items.push(_('PoE power'), '%.1f W'.format(power / 1000));
|
||||
|
||||
if (powerClass)
|
||||
items.push(_('PoE class'), powerClass);
|
||||
|
||||
if (powerLimit)
|
||||
items.push(_('PoE limit'), '%.1f W'.format(powerLimit / 1000));
|
||||
}
|
||||
|
||||
return ui.itemlist(E('span'), items);
|
||||
}
|
||||
|
||||
function getPortSpeed(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getSpeed() : (port.swstate ? port.swstate.speed : null);
|
||||
}
|
||||
|
||||
function getPortDuplex(port)
|
||||
{
|
||||
if (port.netdev)
|
||||
return port.netdev.getDuplex();
|
||||
|
||||
return (port.swstate && port.swstate.link) ? (port.swstate.duplex ? 'full' : 'half') : null;
|
||||
}
|
||||
|
||||
function getPortCarrier(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getCarrier() : !!(port.swstate && port.swstate.link);
|
||||
}
|
||||
|
||||
function getPortTXBytes(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getTXBytes() : (port.swstate ? port.swstate.tx_bytes : null);
|
||||
}
|
||||
|
||||
function getPortRXBytes(port)
|
||||
{
|
||||
return port.netdev ? port.netdev.getRXBytes() : (port.swstate ? port.swstate.rx_bytes : null);
|
||||
}
|
||||
|
||||
function renderNetworkBadge(network, zonename) {
|
||||
const l3dev = network.getDevice();
|
||||
const 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.svg'.format(l3dev.getType(), l3dev.isUp() ? '' : '_disabled'))
|
||||
}));
|
||||
else
|
||||
span.appendChild(E('em', _('(no interfaces attached)')));
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
function renderNetworksTooltip(pmap) {
|
||||
const res = [ null ];
|
||||
const zmap = {};
|
||||
|
||||
const zones = (pmap && Array.isArray(pmap.zones)) ? pmap.zones : [];
|
||||
const networks = (pmap && Array.isArray(pmap.networks)) ? pmap.networks : [];
|
||||
|
||||
for (let pmz of zones) {
|
||||
const networknames = pmz.getNetworks();
|
||||
|
||||
for (let nn of networknames)
|
||||
zmap[nn] = pmz.getName();
|
||||
}
|
||||
|
||||
for (let pmn of networks)
|
||||
res.push(E('br'), renderNetworkBadge(pmn, zmap[pmn.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: _('Port status'),
|
||||
deferFirstLoad: true,
|
||||
disableCache: true,
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callGetBuiltinEthernetPorts(), []),
|
||||
L.resolveDefault(fs.read('/etc/board.json'), '{}'),
|
||||
firewall.getZones(),
|
||||
network.getNetworks(),
|
||||
uci.load('network')
|
||||
]).then((data) => {
|
||||
const builtinPorts = data[0] || [];
|
||||
const board = JSON.parse(data[1] || '{}');
|
||||
const allPorts = new Set();
|
||||
const swstate = {};
|
||||
const tasks = [];
|
||||
|
||||
builtinPorts.forEach((port) => {
|
||||
if (port.device)
|
||||
allPorts.add(port.device);
|
||||
});
|
||||
|
||||
if (allPorts.size === 0 && board.network) {
|
||||
['lan', 'wan'].forEach((role) => {
|
||||
if (board.network[role]) {
|
||||
if (Array.isArray(board.network[role].ports))
|
||||
board.network[role].ports.forEach((p) => allPorts.add(p));
|
||||
else if (board.network[role].device)
|
||||
allPorts.add(board.network[role].device);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (L.isObject(board) && L.isObject(board.switch)) {
|
||||
for (const switchName in board.switch) {
|
||||
tasks.push(L.resolveDefault(callSwconfigPortState(switchName), []).then((ports) => {
|
||||
swstate[switchName] = ports.reduce((map, port) => {
|
||||
map[port.port] = port;
|
||||
return map;
|
||||
}, {});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const psePromises = Array.from(allPorts).map((devname) => {
|
||||
return L.resolveDefault(callNetworkDeviceStatus(devname), {}).then((status) => {
|
||||
return { name: devname, pse: status.pse || null };
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(tasks).then(() => Promise.all(psePromises)).then((pseResults) => {
|
||||
const pseMap = {};
|
||||
|
||||
pseResults.forEach((result) => {
|
||||
if (result.pse)
|
||||
pseMap[result.name] = result.pse;
|
||||
});
|
||||
|
||||
data.push(swstate);
|
||||
data.push(pseMap);
|
||||
|
||||
return data;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render(data) {
|
||||
const board = JSON.parse(data[1]),
|
||||
swstate = data[5] || {},
|
||||
port_map = buildInterfaceMapping(data[2], data[3], board),
|
||||
pseMap = data[6] || {};
|
||||
let known_ports = [];
|
||||
const seenPorts = {};
|
||||
const vlanMap = {};
|
||||
|
||||
buildVLANMappings(vlanMap, board);
|
||||
|
||||
if (Array.isArray(data[0]) && data[0].length > 0) {
|
||||
known_ports = data[0].reduce((ports, port) => {
|
||||
addResolvedPort(ports, seenPorts, port.role, port.device, vlanMap, board, swstate);
|
||||
return ports;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (L.isObject(board) && L.isObject(board.network)) {
|
||||
for (let k = 'lan'; k != null; k = (k == 'lan') ? 'wan' : null) {
|
||||
if (!L.isObject(board.network[k]))
|
||||
continue;
|
||||
|
||||
addBoardNetworkPorts(known_ports, seenPorts, k, board.network[k], vlanMap, board, swstate);
|
||||
}
|
||||
}
|
||||
|
||||
if (!known_ports.length)
|
||||
return null;
|
||||
|
||||
known_ports.sort(function(a, b) {
|
||||
return L.naturalCompare(a.label || a.device, b.label || 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) {
|
||||
const speed = getPortSpeed(port);
|
||||
const duplex = getPortDuplex(port);
|
||||
const carrier = getPortCarrier(port);
|
||||
const pmap = port_map[port.device];
|
||||
const pzones = (pmap && pmap.zones.length) ? pmap.zones.sort((a, b) => L.naturalCompare(a.getName(), b.getName())) : [ null ];
|
||||
const pse = pseMap[port.device];
|
||||
const pseInfo = getPSEStatus(pse);
|
||||
const psePower = formatPSEPower(pse);
|
||||
|
||||
/* Select port icon based on carrier and PSE status */
|
||||
let portIcon;
|
||||
if (pseInfo && pseInfo.isDelivering) {
|
||||
portIcon = carrier ? 'pse_up' : 'pse_down';
|
||||
} else {
|
||||
portIcon = carrier ? 'up' : 'down';
|
||||
}
|
||||
|
||||
const statsContent = [
|
||||
'\u25b2\u202f%1024.1mB'.format(getPortTXBytes(port)),
|
||||
E('br'),
|
||||
'\u25bc\u202f%1024.1mB'.format(getPortRXBytes(port))
|
||||
];
|
||||
|
||||
if (psePower) {
|
||||
statsContent.push(E('br'));
|
||||
statsContent.push(psePower);
|
||||
}
|
||||
|
||||
statsContent.push(E('span', { 'class': 'cbi-tooltip' }, formatStats(port, pse)));
|
||||
|
||||
return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100px' }, [
|
||||
E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.label || port.device ]),
|
||||
E('div', { 'class': 'ifacebox-body' }, [
|
||||
E('img', { 'src': L.resource('icons/port_%s.svg').format(portIcon) }),
|
||||
E('br'),
|
||||
formatSpeed(carrier, 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;opacity:' + (carrier ? 1 : 0.25) + ';' + 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%' }, statsContent)
|
||||
])
|
||||
]);
|
||||
}));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callSystemInfo = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'info'
|
||||
});
|
||||
|
||||
function progressbar(value, max, byte) {
|
||||
var vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
|
||||
return E('div', {
|
||||
'class': 'cbi-progressbar',
|
||||
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
|
||||
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('Memory'),
|
||||
|
||||
load: function() {
|
||||
return L.resolveDefault(callSystemInfo(), {});
|
||||
},
|
||||
|
||||
render: function(systeminfo) {
|
||||
var mem = L.isObject(systeminfo.memory) ? systeminfo.memory : {},
|
||||
swap = L.isObject(systeminfo.swap) ? systeminfo.swap : {};
|
||||
|
||||
var fields = [
|
||||
_('Used'), (mem.total && mem.available) ? (mem.total - mem.free - mem.buffered - mem.cached) : null, mem.total,
|
||||
];
|
||||
|
||||
if (mem.buffered)
|
||||
fields.push(_('Buffered'), mem.buffered, mem.total);
|
||||
|
||||
if (mem.cached)
|
||||
fields.push(_('Cached'), mem.cached, mem.total);
|
||||
|
||||
if (swap.total > 0)
|
||||
fields.push(_('Swap used'), swap.total - swap.free, swap.total);
|
||||
|
||||
var table = E('table', { 'class': 'table' });
|
||||
|
||||
for (var i = 0; i < fields.length; i += 3) {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ fields[i] ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
(fields[i + 1] != null) ? progressbar(fields[i + 1], fields[i + 2], true) : '?'
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callMountPoints = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getMountPoints',
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
function progressbar(value, max, byte) {
|
||||
var vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
|
||||
return E('div', {
|
||||
'class': 'cbi-progressbar',
|
||||
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
|
||||
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('Storage'),
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callMountPoints(), {}),
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var mounts = data[0],
|
||||
overlay = null;
|
||||
|
||||
for (var i = 0; i < mounts.length; i++) {
|
||||
if (mounts[i].mount == '/overlay') {
|
||||
overlay = mounts[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overlay) {
|
||||
for (var j = 0; j < mounts.length; j++) {
|
||||
if (mounts[j].mount == '/') {
|
||||
overlay = mounts[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var table = E('table', { 'class': 'table' });
|
||||
|
||||
if (overlay) {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ overlay.mount ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
progressbar(overlay.size - overlay.free, overlay.size, true)
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require network';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
|
||||
|
||||
/* returns per odhcp6c active interface JSON like:
|
||||
{"result":{"eth1":{"dhcp_solicit":3,"dhcp_advertise":3,"dhcp_request":3,...}}} */
|
||||
const callOdhcp6cStats = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getOdhcp6cStats',
|
||||
expect: { '': {} },
|
||||
});
|
||||
|
||||
var callOnlineUsers = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getOnlineUsers'
|
||||
});
|
||||
|
||||
function progressbar(value, max, byte) {
|
||||
const vn = parseInt(value) || 0;
|
||||
const mn = parseInt(max) || 100;
|
||||
const fv = byte ? String.format('%1024.2mB', value) : value;
|
||||
const fm = byte ? String.format('%1024.2mB', max) : max;
|
||||
const pc = Math.floor((100 / mn) * vn);
|
||||
|
||||
return E('div', {
|
||||
'class': 'cbi-progressbar',
|
||||
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
|
||||
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
|
||||
}
|
||||
|
||||
function renderbox(ifc, ipv6, dhcpv6_stats) {
|
||||
const dev = ifc.getL3Device();
|
||||
const active = (dev && ifc.getProtocol() != 'none');
|
||||
const addrs = (ipv6 ? ifc.getIP6Addrs() : ifc.getIPAddrs()) || [];
|
||||
const dnssrv = (ipv6 ? ifc.getDNS6Addrs() : ifc.getDNSAddrs()) || [];
|
||||
const expires = ifc.getExpiry();
|
||||
const uptime = ifc.getUptime();
|
||||
|
||||
function addEntries(label, array) {
|
||||
return Array.isArray(array) ? array.flatMap((item) => [label, item]) : [label, null];
|
||||
}
|
||||
|
||||
function addDhcpv6Stats() {
|
||||
if (ipv6 && ifc.getProtocol() === 'dhcpv6' && dhcpv6_stats && dhcpv6_stats[dev.device]) {
|
||||
const arr = [];
|
||||
for (const [pkt_type, count] of Object.entries(dhcpv6_stats[dev.device]))
|
||||
arr.push(pkt_type.replace('dhcp_', _('DHCPv6') + ' '), `${count} ${_('pkts', 'packets, abbreviated')}`);
|
||||
return [_('DHCPv6 Statistics'), E('span', { 'class': 'cbi-tooltip-container'}, [
|
||||
'📊',
|
||||
E('span', { 'class': 'cbi-tooltip' }, ui.itemlist(E('span'), arr))
|
||||
])];
|
||||
}
|
||||
return ['', null];
|
||||
}
|
||||
|
||||
return E('div', { class: 'ifacebox' }, [
|
||||
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
|
||||
E('strong', ipv6 ? _('IPv6 Upstream') : _('IPv4 Upstream'))),
|
||||
E('div', { class: 'ifacebox-body left' }, [
|
||||
L.itemlist(E('span'), [
|
||||
_('Protocol'), ifc.getI18n() || E('em', _('Not connected')),
|
||||
...addEntries(_('Prefix Delegated'), ipv6 ? ifc.getIP6Prefixes?.() : null),
|
||||
...addEntries(_('Address'), addrs),
|
||||
_('Gateway'), ipv6 ? (ifc.getGateway6Addr() || '::') : (ifc.getGatewayAddr() || '0.0.0.0'),
|
||||
...addEntries(_('DNS'), dnssrv),
|
||||
_('Expires'), (expires != null && expires > -1) ? '%t'.format(expires) : null,
|
||||
_('Connected'), (uptime > 0) ? '%t'.format(uptime) : null,
|
||||
...addDhcpv6Stats(),
|
||||
]),
|
||||
E('div', {}, renderBadge(
|
||||
L.resource('icons/%s.svg').format(dev ? dev.getType() : 'ethernet_disabled'), null,
|
||||
_('Device'), dev ? dev.getI18n() : '-',
|
||||
_('MAC address'), dev.getMAC())
|
||||
)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('Network'),
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_count'),
|
||||
fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_max'),
|
||||
network.getWANNetworks(),
|
||||
network.getWAN6Networks(),
|
||||
callOdhcp6cStats(),
|
||||
L.resolveDefault(callOnlineUsers(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
render([ct_count, ct_max, wan_nets, wan6_nets, dhcpv6_stats, onlineusers]) {
|
||||
|
||||
const fields = [
|
||||
{ label: _('Active Connections'), value: ct_max ? ct_count : null },
|
||||
{ label: _('Online Users'), value: onlineusers ? onlineusers.onlineusers : null }
|
||||
];
|
||||
|
||||
const ctstatus = E('table', { 'class': 'table' });
|
||||
|
||||
for (const { label, value } of fields) {
|
||||
if (label == _('Online Users')) {
|
||||
ctstatus.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ label ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
(value != null) ? value : '?'
|
||||
])
|
||||
]));
|
||||
} else {
|
||||
ctstatus.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [ label ]),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
(value != null) ? progressbar(value, ct_max) : '?'
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
const netstatus = E('div', { 'class': 'network-status-table' });
|
||||
|
||||
for (const wan_net of wan_nets)
|
||||
netstatus.appendChild(renderbox(wan_net, false));
|
||||
|
||||
for (const wan6_net of wan6_nets)
|
||||
netstatus.appendChild(renderbox(wan6_net, true, dhcpv6_stats?.result));
|
||||
|
||||
return E([
|
||||
netstatus,
|
||||
ctstatus
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require network';
|
||||
'require validation';
|
||||
|
||||
const callLuciDHCPLeases = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getDHCPLeases',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callUfpList = rpc.declare({
|
||||
object: 'fingerprint',
|
||||
method: 'fingerprint',
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
title: _('DHCP Leases'),
|
||||
deferFirstLoad: true,
|
||||
disableCache: true,
|
||||
|
||||
isMACStatic: {},
|
||||
isDUIDStatic: {},
|
||||
isDUIDIAIDStatic: {},
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
callLuciDHCPLeases(),
|
||||
network.getHostHints(),
|
||||
L.hasSystemFeature('ufpd') ? callUfpList() : null,
|
||||
L.resolveDefault(uci.load('dhcp'))
|
||||
]);
|
||||
},
|
||||
|
||||
render([dhcp_leases, host_hints, ufp_list]) {
|
||||
if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd'))
|
||||
return this.renderLeases(dhcp_leases, host_hints, ufp_list);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
handleCreateStaticLease(lease, ev) {
|
||||
ev.currentTarget.classList.add('spinning');
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.blur();
|
||||
|
||||
const cfg = uci.add('dhcp', 'host');
|
||||
uci.set('dhcp', cfg, 'name', lease.hostname);
|
||||
uci.set('dhcp', cfg, 'ip', lease.ipaddr);
|
||||
uci.set('dhcp', cfg, 'mac', [lease.macaddr.toLowerCase()]);
|
||||
|
||||
return uci.save()
|
||||
.then(L.bind(L.ui.changes.init, L.ui.changes))
|
||||
.then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
|
||||
},
|
||||
|
||||
handleCreateStaticLease6(lease, ev) {
|
||||
ev.currentTarget.classList.add('spinning');
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.blur();
|
||||
|
||||
const cfg = uci.add('dhcp', 'host');
|
||||
const ip6addr = lease.ip6addrs?.[0]?.replace(/\/128$/, '');
|
||||
const ip6arr = ip6addr ? validation.parseIPv6(ip6addr) : null;
|
||||
|
||||
// Combine DUID and IAID if both available
|
||||
// (note that we know that lease.duid is set here)
|
||||
let duid_iaid = lease.duid.toLowerCase();
|
||||
if (lease.iaid)
|
||||
duid_iaid += `%${lease.iaid}`.toLowerCase();
|
||||
|
||||
uci.set('dhcp', cfg, 'name', lease.hostname);
|
||||
uci.set('dhcp', cfg, 'duid', [duid_iaid]);
|
||||
if (lease.macaddr)
|
||||
uci.set('dhcp', cfg, 'mac', [lease.macaddr.toLowerCase()]);
|
||||
if (ip6arr)
|
||||
uci.set('dhcp', cfg, 'hostid', (ip6arr[6] * 0xFFFF + ip6arr[7]).toString(16));
|
||||
|
||||
return uci.save()
|
||||
.then(L.bind(L.ui.changes.init, L.ui.changes))
|
||||
.then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
|
||||
},
|
||||
|
||||
renderLeases(dhcp_leases, host_hints, macaddr) {
|
||||
const leases = Array.isArray(dhcp_leases.dhcp_leases) ? dhcp_leases.dhcp_leases : [];
|
||||
const leases6 = Array.isArray(dhcp_leases.dhcp6_leases) ? dhcp_leases.dhcp6_leases : [];
|
||||
if (leases.length == 0 && leases6.length == 0)
|
||||
return E('em', _('No active leases found'));
|
||||
const machints = host_hints.getMACHints(false);
|
||||
const isReadonlyView = !L.hasViewPermission();
|
||||
|
||||
for (const host of uci.sections('dhcp', 'host')) {
|
||||
|
||||
for (const mac of L.toArray(host.mac).map(m => m.toLowerCase()))
|
||||
this.isMACStatic[mac] = true;
|
||||
|
||||
for (const duid_iaid of L.toArray(host.duid).map(m => m.toLowerCase())) {
|
||||
const parts = duid_iaid.split('%').length;
|
||||
|
||||
if (parts == 1)
|
||||
this.isDUIDStatic[duid_iaid] = true;
|
||||
else if (parts == 2)
|
||||
this.isDUIDIAIDStatic[duid_iaid] = true;
|
||||
}
|
||||
};
|
||||
|
||||
const table = E('table', { 'id': 'status_leases', 'class': 'table leases' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
L.hasSystemFeature('odhcpd', 'dhcpv4') ? E('th', { 'class': 'th' }, _('Interface')) : E([]),
|
||||
E('th', { 'class': 'th' }, _('Hostname')),
|
||||
E('th', { 'class': 'th' }, _('IPv4 address')),
|
||||
E('th', { 'class': 'th' }, _('MAC address')),
|
||||
E('th', { 'class': 'th' }, _('DUID')),
|
||||
E('th', { 'class': 'th' }, _('IAID')),
|
||||
E('th', { 'class': 'th' }, _('Remaining time')),
|
||||
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease'))
|
||||
])
|
||||
]);
|
||||
|
||||
cbi_update_table(table, leases.map(L.bind(function(lease) {
|
||||
let exp;
|
||||
let vendor;
|
||||
|
||||
if (lease.expires === false)
|
||||
exp = E('em', _('unlimited'));
|
||||
else if (lease.expires <= 0)
|
||||
exp = E('em', _('expired'));
|
||||
else
|
||||
exp = '%t'.format(lease.expires);
|
||||
|
||||
const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null;
|
||||
let host = null;
|
||||
|
||||
if (hint && lease.hostname && lease.hostname != hint[1])
|
||||
host = '%s (%s)'.format(lease.hostname, hint[1]);
|
||||
else if (lease.hostname)
|
||||
host = lease.hostname;
|
||||
|
||||
if (macaddr)
|
||||
vendor = macaddr[lease.macaddr.toLowerCase()]?.vendor ?? null;
|
||||
|
||||
const columns = [
|
||||
host || '-',
|
||||
lease.ipaddr,
|
||||
vendor ? lease.macaddr + ` (${vendor})` : lease.macaddr,
|
||||
lease.duid || '-',
|
||||
lease.iaid || '-',
|
||||
exp,
|
||||
];
|
||||
|
||||
if (L.hasSystemFeature('odhcpd', 'dhcpv4'))
|
||||
columns.unshift(lease.interface || '-');
|
||||
|
||||
if (!isReadonlyView && lease.macaddr != null) {
|
||||
columns.push(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': L.bind(this.handleCreateStaticLease, this, lease),
|
||||
'data-tooltip': _('Reserve a specific IP address for this device'),
|
||||
'disabled': this.isMACStatic[lease.macaddr.toLowerCase()]
|
||||
}, [ _('Reserve IP') ]));
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, this)), E('em', _('No active leases found')));
|
||||
|
||||
const table6 = E('table', { 'id': 'status_leases6', 'class': 'table leases6' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
L.hasSystemFeature('odhcpd', 'dhcpv6') ? E('th', { 'class': 'th' }, _('Interface')) : E([]),
|
||||
E('th', { 'class': 'th' }, _('Hostname')),
|
||||
E('th', { 'class': 'th' }, _('IPv6 addresses')),
|
||||
E('th', { 'class': 'th' }, _('DUID')),
|
||||
E('th', { 'class': 'th' }, _('IAID')),
|
||||
E('th', { 'class': 'th' }, _('Remaining time')),
|
||||
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease'))
|
||||
])
|
||||
]);
|
||||
|
||||
cbi_update_table(table6, leases6.map(L.bind(function(lease) {
|
||||
let exp;
|
||||
|
||||
if (lease.expires === false)
|
||||
exp = E('em', _('unlimited'));
|
||||
else if (lease.expires <= 0)
|
||||
exp = E('em', _('expired'));
|
||||
else
|
||||
exp = '%t'.format(lease.expires);
|
||||
|
||||
const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null;
|
||||
let host = null;
|
||||
|
||||
if (hint && lease.hostname && lease.hostname != hint[1] && lease.ip6addr != hint[1])
|
||||
host = '%s (%s)'.format(lease.hostname, hint[1]);
|
||||
else if (lease.hostname)
|
||||
host = lease.hostname;
|
||||
else if (hint)
|
||||
host = hint[1];
|
||||
|
||||
const duid = lease.duid?.toLowerCase();
|
||||
const iaid = lease.iaid?.toLowerCase();
|
||||
|
||||
// Note: "disabled: false" doesn't work
|
||||
let disabled = null;
|
||||
if (!duid)
|
||||
disabled = true;
|
||||
else if (duid && this.isDUIDStatic[duid])
|
||||
disabled = true;
|
||||
else if (duid && iaid && this.isDUIDIAIDStatic[`${duid}%${iaid}`])
|
||||
disabled = true;
|
||||
|
||||
const columns = [
|
||||
host || '-',
|
||||
lease.ip6addrs ? lease.ip6addrs.join('<br />') : lease.ip6addr,
|
||||
duid || '-',
|
||||
iaid || '-',
|
||||
exp
|
||||
];
|
||||
|
||||
if (L.hasSystemFeature('odhcpd', 'dhcpv6'))
|
||||
columns.unshift(lease.interface || '-');
|
||||
|
||||
if (!isReadonlyView && lease.duid) {
|
||||
columns.push(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': L.bind(this.handleCreateStaticLease6, this, lease),
|
||||
'data-tooltip': _('Reserve a specific IP address for this device'),
|
||||
'disabled': disabled
|
||||
}, [ _('Reserve IP') ]));
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, this)), E('em', _('No active leases found')));
|
||||
|
||||
return E([
|
||||
E('h3', _('Active DHCPv4 Leases')),
|
||||
table,
|
||||
E('h3', _('Active DHCPv6 Leases')),
|
||||
table6
|
||||
]);
|
||||
},
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user