2024-08-15 21:28:45 -06:00
|
|
|
{%
|
|
|
|
/*
|
|
|
|
* Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks
|
|
|
|
* Copyright (C) 2024 Tim Wilkinson
|
|
|
|
* See Contributors file for additional contributors
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation version 3 of the License.
|
|
|
|
*
|
|
|
|
* This program 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 General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* Additional Terms:
|
|
|
|
*
|
|
|
|
* Additional use restrictions exist on the AREDN® trademark and logo.
|
|
|
|
* See AREDNLicense.txt for more info.
|
|
|
|
*
|
|
|
|
* Attributions to the AREDN® Project must be retained in the source code.
|
|
|
|
* If importing this code into a new or existing project attribution
|
|
|
|
* to the AREDN® project must be added to the source code.
|
|
|
|
*
|
|
|
|
* You must not misrepresent the origin of the material contained within.
|
|
|
|
*
|
|
|
|
* Modified versions must be modified to attribute to the original source
|
|
|
|
* and be marked in reasonable ways as differentiate it from the original
|
|
|
|
* version
|
|
|
|
*/
|
|
|
|
%}
|
|
|
|
{%
|
|
|
|
function loadPorts(type, def)
|
|
|
|
{
|
|
|
|
const f = fs.open(`/etc/aredn_include/${type}.network.user`);
|
|
|
|
if (!f) {
|
|
|
|
return def;
|
|
|
|
}
|
|
|
|
const r = { vlan: null, ports: {} };
|
|
|
|
let pcount = 0;
|
|
|
|
for (let l = f.read("line"); length(l); l = f.read("line")) {
|
|
|
|
let m = match(l, /list ports '(.+):([ut])'/);
|
|
|
|
if (m) {
|
|
|
|
r.ports[m[1]] = true;
|
|
|
|
if (m[2] === "u") {
|
|
|
|
r.vlan = 0;
|
|
|
|
}
|
|
|
|
pcount++;
|
|
|
|
}
|
|
|
|
m = match(l, /option vlan '(\d+)'/);
|
|
|
|
if (m) {
|
|
|
|
r.vlan = int(m[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (pcount === 0 && ((type === "wan" && r.vlan === 1) || (type === "lan" && r.vlan === 3))) {
|
|
|
|
r.vlan = 0;
|
|
|
|
}
|
|
|
|
f.close();
|
|
|
|
return r;
|
|
|
|
}
|
|
|
|
if (request.env.REQUEST_METHOD === "PUT") {
|
|
|
|
configuration.prepareChanges();
|
|
|
|
if ("xlinks" in request.args) {
|
|
|
|
const xlinks = {};
|
|
|
|
const xs = json(request.args.xlinks);
|
|
|
|
for (let i = 0; i < length(xs); i++) {
|
|
|
|
xlinks[xs[i].name] = xs[i];
|
|
|
|
}
|
2024-08-25 15:34:24 -06:00
|
|
|
let defport = "eth0";
|
|
|
|
const ports = hardware.getEthernetPorts();
|
|
|
|
if (length(ports) > 0) {
|
|
|
|
defport = ports[0].k;
|
|
|
|
}
|
2024-08-15 21:28:45 -06:00
|
|
|
uciMesh.foreach("xlink", "interface", x => {
|
|
|
|
const ux = xlinks[x[".name"]];
|
|
|
|
if (ux) {
|
|
|
|
uciMesh.set("xlink", `${ux.name}bridge`, "vlan", ux.vlan);
|
2024-08-25 15:34:24 -06:00
|
|
|
uciMesh.set("xlink", `${ux.name}bridge`, "ports", [ `${ux.port || defport}:t` ]);
|
2024-08-15 21:28:45 -06:00
|
|
|
uciMesh.commit("xlink");
|
|
|
|
|
|
|
|
uciMesh.set("xlink", ux.name, "ifname", `br0.${ux.vlan}`);
|
|
|
|
uciMesh.set("xlink", ux.name, "ipaddr", ux.ipaddr);
|
|
|
|
if (uciMesh.get("xlink", ux.name, "peer") !== ux.peer) {
|
|
|
|
uciMesh.set("xlink", ux.name, "peer", ux.peer);
|
|
|
|
uciMesh.commit("xlink");
|
|
|
|
uciMesh.set("xlink", ux.name, "peer", ux.peer);
|
|
|
|
if (ux.peer) {
|
|
|
|
uciMesh.set("xlink", `${ux.name}route`, "route");
|
|
|
|
uciMesh.set("xlink", `${ux.name}route`, "interface", ux.name);
|
|
|
|
uciMesh.set("xlink", `${ux.name}route`, "target", ux.peer);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
uciMesh.delete("xlink", `${name}route`);
|
|
|
|
}
|
|
|
|
uciMesh.commit("xlink");
|
|
|
|
}
|
|
|
|
uciMesh.set("xlink", ux.name, "weight", ux.weight);
|
|
|
|
uciMesh.set("xlink", ux.name, "netmask", network.CIDRToNetmask(ux.cidr));
|
|
|
|
delete xlinks[ux.name];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
const name = x[".name"];
|
|
|
|
uciMesh.delete("xlink", name);
|
|
|
|
uciMesh.delete("xlink", `${name}bridge`);
|
|
|
|
uciMesh.delete("xlink", `${name}route`);
|
|
|
|
}
|
|
|
|
uciMesh.commit("xlink");
|
|
|
|
});
|
|
|
|
for (let name in xlinks) {
|
|
|
|
const ux = xlinks[name];
|
|
|
|
uciMesh.set("xlink", `${ux.name}bridge`, "bridge-vlan");
|
|
|
|
uciMesh.set("xlink", `${ux.name}bridge`, "device", "br0");
|
|
|
|
uciMesh.set("xlink", `${ux.name}bridge`, "vlan", ux.vlan);
|
2024-08-25 15:34:24 -06:00
|
|
|
uciMesh.set("xlink", `${ux.name}bridge`, "ports", [ `${ux.port || defport}:t` ]);
|
2024-08-15 21:28:45 -06:00
|
|
|
uciMesh.commit("xlink");
|
|
|
|
|
|
|
|
uciMesh.set("xlink", name, "interface");
|
|
|
|
uciMesh.set("xlink", name, "ifname", `br0.${ux.vlan}`);
|
|
|
|
uciMesh.set("xlink", name, "ipaddr", ux.ipaddr);
|
|
|
|
uciMesh.set("xlink", name, "weight", ux.weight);
|
|
|
|
uciMesh.set("xlink", name, "netmask", network.CIDRToNetmask(ux.cidr));
|
|
|
|
if (ux.peer) {
|
|
|
|
uciMesh.set("xlink", name, "peer", ux.peer);
|
|
|
|
uciMesh.set("xlink", `${ux.name}route`, "route");
|
|
|
|
uciMesh.set("xlink", `${ux.name}route`, "interface", ux.name);
|
|
|
|
uciMesh.set("xlink", `${ux.name}route`, "target", ux.peer);
|
|
|
|
uciMesh.commit("xlink");
|
|
|
|
}
|
|
|
|
uciMesh.set("xlink", name, "proto", "static");
|
|
|
|
uciMesh.set("xlink", name, "macaddr", replace("x2:xx:xx:xx:xx:xx", "x", _ => sprintf("%X",math.rand()&15)));
|
|
|
|
uciMesh.commit("xlink");
|
|
|
|
}
|
|
|
|
delete request.args.xlinks;
|
|
|
|
}
|
|
|
|
const k = keys(request.args);
|
|
|
|
if (length(k) > 0) {
|
|
|
|
if (length(hardware.getEthernetPorts()) > 1) {
|
|
|
|
const defnet = hardware.getDefaultNetworkConfiguration();
|
|
|
|
const nets = {
|
|
|
|
wan: loadPorts("wan", defnet.wan),
|
|
|
|
lan: loadPorts("lan", defnet.lan),
|
|
|
|
dtdlink: loadPorts("dtdlink", defnet.dtdlink)
|
|
|
|
};
|
|
|
|
for (let i = 0; i < length(k); i++) {
|
|
|
|
if (k[i] === "port_wan_vlan") {
|
|
|
|
nets.wan.vlan = int(request.args[k[i]] || 0);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
const ptp = split(k[i], "_");
|
|
|
|
const net = nets[ptp[1]];
|
|
|
|
if (net) {
|
|
|
|
if (request.args[k[i]] === "on") {
|
|
|
|
net.ports[ptp[2]] = true;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
delete net.ports[ptp[2]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nets.wan.vlan === 0) {
|
|
|
|
for (let p in nets.wan.ports) {
|
|
|
|
if (nets.lan.ports[p]) {
|
|
|
|
delete nets.wan.ports[p];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function savePort(type, c)
|
|
|
|
{
|
|
|
|
const f = fs.open(`/etc/aredn_include/${type}.network.user`, "w");
|
|
|
|
if (f) {
|
|
|
|
f.write(`# Generated by advancednetwork
|
|
|
|
|
|
|
|
config bridge-vlan
|
|
|
|
option device 'br0'
|
|
|
|
option vlan '${c.vlan ? c.vlan : type == "lan" ? 3 : 1}'
|
|
|
|
${join("\n", map(keys(c.ports), p => "\tlist ports '" + p + (c.vlan ? ":t'" : ":u'")))}
|
|
|
|
|
|
|
|
config device
|
|
|
|
option name 'br-${type}'
|
|
|
|
option type 'bridge'
|
|
|
|
option macaddr '<${type}_mac>'
|
|
|
|
list ports 'br0.${c.vlan ? c.vlan : type == "lan" ? 3 : 1}'
|
|
|
|
|
|
|
|
config interface '${type}'
|
|
|
|
option device 'br-${type}'
|
|
|
|
${type === "dtdlink" ? "\toption proto 'static'" : "\toption proto '<" + type + "_proto>'"}
|
|
|
|
option ipaddr '<${type}_ip>'
|
|
|
|
${type === "dtdlink" ? "\toption netmask '255.0.0.0'" : "\toption netmask '<" + type + "_mask>'"}
|
|
|
|
${type === "wan" ? "\toption gateway '<wan_gw>'" : ""}${type === "lan" ? "\toption dns '<wan_dns1> <wan_dns2>'" : ""}
|
|
|
|
`);
|
|
|
|
f.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
savePort("wan", nets.wan);
|
|
|
|
savePort("lan", nets.lan);
|
|
|
|
savePort("dtdlink", nets.dtdlink);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
print(_R("changes"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (request.env.REQUEST_METHOD === "DELETE") {
|
|
|
|
configuration.revertModalChanges();
|
|
|
|
print(_R("changes"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let haveports = false;
|
|
|
|
let wan_vlan = 0;
|
|
|
|
const ports = hardware.getEthernetPorts();
|
|
|
|
if (length(ports) > 1) {
|
|
|
|
haveports = true;
|
|
|
|
const defnet = hardware.getDefaultNetworkConfiguration();
|
|
|
|
const wan = loadPorts("wan", defnet.wan);
|
|
|
|
const lan = loadPorts("lan", defnet.lan);
|
|
|
|
const dtdlink = loadPorts("dtdlink", defnet.dtdlink);
|
|
|
|
for (let i = 0; i < length(ports); i++) {
|
|
|
|
const v = {
|
|
|
|
name: ports[i].k,
|
|
|
|
display: ports[i].d,
|
|
|
|
info: hardware.getEthernetPortInfo(ports[i].k),
|
|
|
|
dtdlink: dtdlink.ports[ports[i].k],
|
|
|
|
lan: lan.ports[ports[i].k],
|
|
|
|
wan: wan.ports[ports[i].k]
|
|
|
|
};
|
|
|
|
ports[i] = v;
|
|
|
|
}
|
|
|
|
wan_vlan = wan.vlan;
|
|
|
|
}
|
|
|
|
const xlinks = [];
|
|
|
|
uciMesh.foreach("xlink", "interface", x => {
|
|
|
|
push(xlinks, {
|
|
|
|
name: x[".name"],
|
|
|
|
vlan: int(split(x.ifname, ".")[1]),
|
|
|
|
ipaddr: x.ipaddr,
|
|
|
|
peer: x.peer || "",
|
|
|
|
weight: int(x.weight),
|
|
|
|
port: match(uciMesh.get("xlink", `${x[".name"]}bridge`, "ports")[0], /^(.+):/)[1],
|
|
|
|
cidr: network.netmaskToCIDR(x.netmask)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
%}
|
|
|
|
<div class="dialog">
|
|
|
|
{{_R("dialog-header", `${haveports ? "Ports & " : ""}XLinks`)}}
|
|
|
|
<div>
|
|
|
|
{% if (haveports) { %}
|
|
|
|
<div class="ports">
|
|
|
|
<div>
|
|
|
|
<table>
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<td></td>
|
|
|
|
{%
|
|
|
|
for (let i = 0; i < length(ports); i++) {
|
|
|
|
const p = ports[i];
|
|
|
|
print(`<td ${p.info.active ? 'class="active"' : ''}><div>${p.display}</div></td>`);
|
|
|
|
}
|
|
|
|
%}
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
<tr>
|
|
|
|
<td><div>dtd</div><div>vlan: 2</div></td>
|
|
|
|
{%
|
|
|
|
for (let i = 0; i < length(ports); i++) {
|
|
|
|
const p = ports[i];
|
|
|
|
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_dtdlink_${p.name}" ${p.dtdlink ? "checked" : ""} hx-vals='js:{port_dtdlink_${p.name}:htmx.find("[name=port_dtdlink_${p.name}]").checked ? "on" : "off"}'></td>`);
|
|
|
|
}
|
|
|
|
%}
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td><div>lan</div><div>vlan: untagged</div></td>
|
|
|
|
{%
|
|
|
|
for (let i = 0; i < length(ports); i++) {
|
|
|
|
const p = ports[i];
|
|
|
|
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_lan_${p.name}" ${p.lan ? "checked" : ""} hx-vals='js:{port_lan_${p.name}:htmx.find("[name=port_lan_${p.name}]").checked ? "on" : "off"}'></td>`);
|
|
|
|
}
|
|
|
|
%}
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td><div>wan</div><div>vlan: <input hx-put="{{request.env.REQUEST_URI}}" type="text" name="port_wan_vlan" hx-include=".ports [name^='port_wan_']" size="5" placeholder="untagged" pattern="([14-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-4])" value="{{wan_vlan != 0 ? wan_vlan : ''}}"></div></td>
|
|
|
|
{%
|
|
|
|
for (let i = 0; i < length(ports); i++) {
|
|
|
|
const p = ports[i];
|
|
|
|
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_wan_${p.name}" ${p.wan ? "checked" : ""} hx-include="previous [name='port_wan_vlan']" hx-vals='js:{port_wan_${p.name}:htmx.find("[name=port_wan_${p.name}]").checked ? "on" : "off"}'></td>`);
|
|
|
|
}
|
|
|
|
%}
|
|
|
|
</tr>
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{{_H("<p>AREDN nodes have three primary networks; DTD, LAN and WAN (shown above in the left column). You can modify the default assignment
|
|
|
|
of these networks to ports (shown across the top) using the checkboxes at the intersection of a network and a port. You can also choose the
|
|
|
|
VLAN to assign to the WAN network. Networks can be assigned to multiple ports, or no ports.
|
|
|
|
Note that on some devices, ports may have names like <i>WAN</i> or <i>LAN</i>. These are just arbitrary names given by the manufacturer
|
|
|
|
and you are not forced to assign networks of the same name to these ports.
|
|
|
|
<p>Active network ports, where a cable is present and attached to another device, are shown in green.")}}
|
|
|
|
<hr>
|
|
|
|
{% } %}
|
|
|
|
<div class="xlinks">
|
|
|
|
<div>
|
|
|
|
<div class="cols">
|
|
|
|
<div>
|
|
|
|
<div class="o">XLinks</div>
|
|
|
|
<div class="m">Inter-device links across non-AREDN networks</div>
|
|
|
|
</div>
|
|
|
|
<button class="add">+</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="xlink-list">
|
|
|
|
<div class="cols xlink-label">
|
|
|
|
<div style="white-space:nowrap">
|
|
|
|
<div>vlan</div>
|
|
|
|
<div>ip a‌ddress</div>
|
|
|
|
<div>peer a‌ddress</div>
|
|
|
|
<div>cidr</div>
|
|
|
|
<div>weight</div>
|
|
|
|
{% if (haveports) { %}
|
|
|
|
<div>port</div>
|
|
|
|
{% } %}
|
|
|
|
</div>
|
|
|
|
<div></div>
|
|
|
|
</div>
|
|
|
|
{% for (let i = 0; i < length(xlinks); i++) {
|
|
|
|
const x = xlinks[i];
|
|
|
|
%}
|
|
|
|
<div class="cols xlink" data-name="{{x.name}}">
|
|
|
|
<div>
|
|
|
|
<input name="vlan" type="text" size="5" required pattern="([4-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-5])" placeholder="VLan" value="{{x.vlan}}">
|
|
|
|
<input name="ipaddr" type="text" size="25" required pattern="{{constants.patIP}}" placeholder="IP A‌ddress" value="{{x.ipaddr}}">
|
|
|
|
<input name="peer" type="text" size="25" pattern="{{constants.patIP}}" placeholder="IP A‌ddress" value="{{x.peer}}">
|
|
|
|
<select name="cidr">
|
|
|
|
<option value="32" {{x.cidr === 32 ? "selected" : ""}}>PtP</option>
|
|
|
|
<option value="31" {{x.cidr === 31 ? "selected" : ""}}>/ 31</option>
|
|
|
|
<option value="30" {{x.cidr === 30 ? "selected" : ""}}>/ 30</option>
|
|
|
|
<option value="29" {{x.cidr === 29 ? "selected" : ""}}>/ 29</option>
|
|
|
|
<option value="28" {{x.cidr === 28 ? "selected" : ""}}>/ 28</option>
|
|
|
|
<option value="27" {{x.cidr === 27 ? "selected" : ""}}>/ 27</option>
|
|
|
|
<option value="26" {{x.cidr === 26 ? "selected" : ""}}>/ 26</option>
|
|
|
|
<option value="25" {{x.cidr === 25 ? "selected" : ""}}>/ 25</option>
|
|
|
|
<option value="24" {{x.cidr === 24 ? "selected" : ""}}>/ 24</option>
|
|
|
|
<option value="23" {{x.cidr === 23 ? "selected" : ""}}>/ 23</option>
|
|
|
|
<option value="22" {{x.cidr === 22 ? "selected" : ""}}>/ 22</option>
|
|
|
|
<option value="21" {{x.cidr === 21 ? "selected" : ""}}>/ 21</option>
|
|
|
|
<option value="20" {{x.cidr === 20 ? "selected" : ""}}>/ 20</option>
|
|
|
|
<option value="19" {{x.cidr === 19 ? "selected" : ""}}>/ 19</option>
|
|
|
|
<option value="18" {{x.cidr === 18 ? "selected" : ""}}>/ 18</option>
|
|
|
|
<option value="17" {{x.cidr === 17 ? "selected" : ""}}>/ 17</option>
|
|
|
|
<option value="16" {{x.cidr === 16 ? "selected" : ""}}>/ 16</option>
|
|
|
|
</select>
|
|
|
|
<input name="weight" type="text" size="2" required pattern="([0-9]|[1-9][0-9]|100)" placeholder="Wt" value="{{x.weight}}">
|
|
|
|
{% if (haveports) { %}
|
|
|
|
<select name="port">
|
|
|
|
{%
|
|
|
|
for (let i = 0; i < length(ports); i++) {
|
|
|
|
const p = ports[i];
|
|
|
|
print(`<option value="${p.name}" ${x.port === p.name ? "selected" : ""}>${p.display}</option>`);
|
|
|
|
}
|
|
|
|
%}
|
|
|
|
</select>
|
|
|
|
{% } %}
|
|
|
|
</div>
|
|
|
|
<button>-</button>
|
|
|
|
</div>
|
|
|
|
{% } %}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{{_H("<p>XLinks provide a way of routing AREDN traffic across external non-AREDN networks. Each is created with a specific VLAN,
|
|
|
|
IP address for both ends, a weight of how likely to link is to be used, and an optional network size and port (on multi-port
|
|
|
|
devices). Think of xlinks as extra dtds between devices. How xlink traffic is routed once it leaves the node is dependent
|
|
|
|
on the non-AREDN network, which allows for the greatest flexibility.")}}
|
|
|
|
</div>
|
|
|
|
{{_R("dialog-footer")}}
|
|
|
|
<script>
|
|
|
|
(function(){
|
|
|
|
{{_R("open")}}
|
|
|
|
const patIP = "((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}";
|
|
|
|
const xlinks = {{sprintf("%J", xlinks)}};
|
|
|
|
function update()
|
|
|
|
{
|
|
|
|
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
|
|
|
|
swap: "none",
|
|
|
|
values: { xlinks: JSON.stringify(xlinks) }
|
|
|
|
});
|
|
|
|
}
|
|
|
|
htmx.on("#ctrl-modal .dialog #xlink-list", "change", event => {
|
|
|
|
const target = htmx.closest(event.target, "[data-name]");
|
|
|
|
const xlink = xlinks.find(x => x.name == target.dataset.name);
|
|
|
|
const vlan = htmx.find(target, "[name=vlan]");
|
|
|
|
const ipaddr = htmx.find(target, "[name=ipaddr]");
|
|
|
|
const peer = htmx.find(target, "[name=peer]");
|
|
|
|
const weight = htmx.find(target, "[name=weight]");
|
|
|
|
const port = htmx.find(target, "[name=port]");
|
|
|
|
const cidr = htmx.find(target, "[name=cidr]");
|
|
|
|
if (vlan.validity.valid && ipaddr.validity.valid && peer.validity.valid && weight.validity.valid) {
|
|
|
|
xlink.vlan = parseInt(vlan.value);
|
|
|
|
xlink.ipaddr = ipaddr.value;
|
|
|
|
xlink.peer = peer.value;
|
|
|
|
xlink.weight = parseInt(weight.value);
|
|
|
|
xlink.port = port ? port.value : null;
|
|
|
|
xlink.cidr = parseInt(cidr.value);
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
htmx.on("#ctrl-modal .dialog #xlink-list", "click", event => {
|
|
|
|
if (event.target.nodeName === "BUTTON") {
|
|
|
|
const target = htmx.closest(event.target, "[data-name]");
|
|
|
|
const xlink = xlinks.findIndex(x => x.name == target.dataset.name);
|
|
|
|
htmx.remove(target);
|
|
|
|
xlinks.splice(xlink, 1);
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
});
|
2024-08-27 01:15:03 -06:00
|
|
|
function newXlinkEntry()
|
|
|
|
{
|
2024-08-15 21:28:45 -06:00
|
|
|
let name;
|
|
|
|
for (let i = 0;; i++) {
|
|
|
|
name = `xlink${i}`;
|
|
|
|
if (!htmx.find(`#ctrl-modal .dialog #xlink-list .xlink[data-name=${name}]`)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const x = {
|
|
|
|
name: name,
|
|
|
|
vlan: "",
|
|
|
|
ipaddr: "",
|
|
|
|
peer: "",
|
|
|
|
weight: 0,
|
|
|
|
port: null,
|
|
|
|
cidr: 32
|
|
|
|
};
|
|
|
|
xlinks.push(x);
|
|
|
|
const div = document.createElement("div");
|
|
|
|
const ls = htmx.find("#ctrl-modal .dialog #xlink-list");
|
|
|
|
div.innerHTML = `<div class="cols xlink" data-name="${x.name}">
|
|
|
|
<div>
|
|
|
|
<input name="vlan" type="text" size="5" required pattern="([4-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-5])" placeholder="VLan">
|
|
|
|
<input name="ipaddr" type="text" size="25" required pattern="${patIP}" placeholder="IP A‌ddress">
|
|
|
|
<input name="peer" type="text" size="25" pattern="${patIP}" placeholder="IP A‌ddress">
|
|
|
|
<select name="cidr">
|
|
|
|
<option value="32">PtP</option>
|
|
|
|
<option value="31">/ 31</option>
|
|
|
|
<option value="30">/ 30</option>
|
|
|
|
<option value="29">/ 29</option>
|
|
|
|
<option value="28">/ 28</option>
|
|
|
|
<option value="27">/ 27</option>
|
|
|
|
<option value="26">/ 26</option>
|
|
|
|
<option value="25">/ 25</option>
|
|
|
|
<option value="24">/ 24</option>
|
|
|
|
<option value="23">/ 23</option>
|
|
|
|
<option value="22">/ 22</option>
|
|
|
|
<option value="21">/ 21</option>
|
|
|
|
<option value="20">/ 20</option>
|
|
|
|
<option value="19">/ 19</option>
|
|
|
|
<option value="18">/ 18</option>
|
|
|
|
<option value="17">/ 17</option>
|
|
|
|
<option value="16">/ 16</option>
|
|
|
|
</select>
|
|
|
|
<input name="weight" type="text" size="2" required pattern="([0-9]|[1-9][0-9]|100)" placeholder="Wt">
|
|
|
|
{% if (haveports) { %}<select name="port">
|
|
|
|
{%
|
|
|
|
for (let i = 0; i < length(ports); i++) {
|
|
|
|
const p = ports[i];
|
|
|
|
print(`<option value="${p.name}">${p.display}</option>`);
|
|
|
|
}
|
|
|
|
%}
|
|
|
|
</select>{% } %}
|
|
|
|
</div>
|
|
|
|
<button>-</button>
|
|
|
|
</div>`;
|
2024-08-26 12:55:42 -06:00
|
|
|
const fc = div.firstChild;
|
|
|
|
ls.appendChild(fc);
|
|
|
|
htmx.find(fc, "input").focus();
|
2024-08-27 01:15:03 -06:00
|
|
|
htmx.on(fc, "keypress", event => {
|
|
|
|
if (event.keyCode === 13 && event.target.nodeName == "INPUT" && htmx.closest(event.target, ".xlink") === htmx.find("#xlink-list .xlink:last-child")) {
|
|
|
|
event.preventDefault();
|
|
|
|
newXlinkEntry();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
htmx.on("#ctrl-modal .dialog .xlinks .add", "click", newXlinkEntry);
|
2024-08-15 21:28:45 -06:00
|
|
|
})();
|
|
|
|
</script>
|
|
|
|
</div>
|