@takodotid/circuit 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunks/{index-jtzfw679.js → index-n4zpd25n.js} +92 -92
- package/dist/chunks/{index-xm9nma2t.js → index-v8zgr7wm.js} +14 -14
- package/dist/chunks/{new-7n8qvtjd.js → new-ny89s7y7.js} +4 -6
- package/dist/chunks/{server-5shmsmse.js → server-ckdy25w2.js} +16 -11
- package/dist/cli/index.js +406 -411
- package/dist/index.js +20 -21
- package/dist/presets/index.js +20 -21
- package/package.json +7 -7
- package/src/core/files.ts +5 -3
- package/src/studio/public/app.js +37 -9
- package/src/studio/public/dom.js +20 -0
- package/src/studio/public/export.js +6 -0
- package/src/studio/public/favicon.svg +10 -4
- package/src/studio/public/graph.js +396 -26
- package/src/studio/public/index.html +26 -12
- package/src/studio/public/special-gothic-latin-ext.woff2 +0 -0
- package/src/studio/public/special-gothic-latin.woff2 +0 -0
- package/src/studio/public/special-gothic-license.txt +93 -0
- package/src/studio/public/style.css +1125 -356
- package/src/studio/server.ts +10 -1
- package/dist/chunks/index-fq57hya4.js +0 -20
package/dist/cli/index.js
CHANGED
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
portsOf,
|
|
4
|
+
spell,
|
|
3
5
|
LAYERS,
|
|
4
|
-
addressesOf,
|
|
5
6
|
deviceId,
|
|
6
|
-
freeOfLength,
|
|
7
|
-
graphOf,
|
|
8
|
-
holdersOf,
|
|
9
7
|
keyOf,
|
|
8
|
+
holdersOf,
|
|
9
|
+
graphOf,
|
|
10
10
|
only,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
addressesOf,
|
|
12
|
+
freeOfLength,
|
|
13
13
|
studioData
|
|
14
|
-
} from "../chunks/index-
|
|
14
|
+
} from "../chunks/index-n4zpd25n.js";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
familyOf,
|
|
18
|
-
host,
|
|
19
|
-
isAddress,
|
|
20
|
-
isCidr,
|
|
21
|
-
isGlobal,
|
|
22
|
-
netmask,
|
|
23
|
-
network,
|
|
16
|
+
familyOf2,
|
|
24
17
|
split,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
isAddress2,
|
|
19
|
+
isCidr2,
|
|
20
|
+
network,
|
|
21
|
+
contains2,
|
|
22
|
+
netmask,
|
|
23
|
+
wildcard,
|
|
24
|
+
host2,
|
|
25
|
+
isGlobal2
|
|
26
|
+
} from "../chunks/index-v8zgr7wm.js";
|
|
31
27
|
|
|
32
28
|
// src/cli/index.ts
|
|
33
29
|
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
@@ -98,10 +94,10 @@ function valueOf(reference) {
|
|
|
98
94
|
const fullPath = join(root, path);
|
|
99
95
|
if (!existsSync(fullPath))
|
|
100
96
|
throw new Error(`secret file ${path} does not exist`);
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
97
|
+
const value = readFileSync(fullPath, "utf8").trimEnd();
|
|
98
|
+
if (!value)
|
|
103
99
|
throw new Error(`secret file ${path} is empty`);
|
|
104
|
-
return
|
|
100
|
+
return value;
|
|
105
101
|
}
|
|
106
102
|
const value = process.env[reference] || dotenv()[reference];
|
|
107
103
|
if (!value)
|
|
@@ -131,9 +127,9 @@ function offset(zone) {
|
|
|
131
127
|
// src/transport/ssh.ts
|
|
132
128
|
import { Client } from "ssh2";
|
|
133
129
|
function connect(login) {
|
|
134
|
-
return new Promise((
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
135
131
|
const client = new Client;
|
|
136
|
-
client.on("ready", () =>
|
|
132
|
+
client.on("ready", () => resolve(client)).on("error", reject).connect({
|
|
137
133
|
host: login.host,
|
|
138
134
|
port: login.port ?? 22,
|
|
139
135
|
username: login.user,
|
|
@@ -144,29 +140,29 @@ function connect(login) {
|
|
|
144
140
|
});
|
|
145
141
|
}
|
|
146
142
|
function exec(client, command, timeout = 120000) {
|
|
147
|
-
return new Promise((
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
148
144
|
client.exec(command, (error, stream) => {
|
|
149
145
|
if (error)
|
|
150
146
|
return reject(error);
|
|
151
147
|
let output = "";
|
|
152
148
|
const append = (chunk) => output += chunk.toString("utf8");
|
|
153
|
-
const timer = setTimeout(() =>
|
|
149
|
+
const timer = setTimeout(() => resolve(output.trim()), timeout);
|
|
154
150
|
stream.on("data", append);
|
|
155
151
|
stream.stderr.on("data", append);
|
|
156
152
|
stream.on("close", () => {
|
|
157
153
|
clearTimeout(timer);
|
|
158
|
-
|
|
154
|
+
resolve(output.trim());
|
|
159
155
|
});
|
|
160
156
|
});
|
|
161
157
|
});
|
|
162
158
|
}
|
|
163
159
|
function shell(client, columns = 200, rows = 1000) {
|
|
164
|
-
return new Promise((
|
|
165
|
-
client.shell({ cols: columns, rows }, (error, channel) => error ? reject(error) :
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
client.shell({ cols: columns, rows }, (error, channel) => error ? reject(error) : resolve(channel));
|
|
166
162
|
});
|
|
167
163
|
}
|
|
168
164
|
function readUntil(channel, done, timeout, settle = 400) {
|
|
169
|
-
return new Promise((
|
|
165
|
+
return new Promise((resolve) => {
|
|
170
166
|
let output = "";
|
|
171
167
|
let finished = false;
|
|
172
168
|
const finish = () => {
|
|
@@ -174,7 +170,7 @@ function readUntil(channel, done, timeout, settle = 400) {
|
|
|
174
170
|
return;
|
|
175
171
|
finished = true;
|
|
176
172
|
channel.removeListener("data", onData);
|
|
177
|
-
|
|
173
|
+
resolve(output);
|
|
178
174
|
};
|
|
179
175
|
const onData = (chunk) => {
|
|
180
176
|
output += chunk.toString("utf8");
|
|
@@ -188,12 +184,12 @@ function readUntil(channel, done, timeout, settle = 400) {
|
|
|
188
184
|
function send(channel, line, timeout = 20000, settle = 1500) {
|
|
189
185
|
channel.write(line + `
|
|
190
186
|
`);
|
|
191
|
-
return new Promise((
|
|
187
|
+
return new Promise((resolve) => {
|
|
192
188
|
let output = "";
|
|
193
189
|
let timer;
|
|
194
190
|
const finish = () => {
|
|
195
191
|
channel.removeListener("data", onData);
|
|
196
|
-
|
|
192
|
+
resolve(output);
|
|
197
193
|
};
|
|
198
194
|
const onData = (chunk) => {
|
|
199
195
|
output += chunk.toString("utf8");
|
|
@@ -205,7 +201,7 @@ function send(channel, line, timeout = 20000, settle = 1500) {
|
|
|
205
201
|
});
|
|
206
202
|
}
|
|
207
203
|
function upload(client, path, content) {
|
|
208
|
-
return new Promise((
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
209
205
|
client.sftp((error, sftp) => {
|
|
210
206
|
if (error)
|
|
211
207
|
return reject(error);
|
|
@@ -214,7 +210,7 @@ function upload(client, path, content) {
|
|
|
214
210
|
if (writeError)
|
|
215
211
|
reject(writeError);
|
|
216
212
|
else
|
|
217
|
-
|
|
213
|
+
resolve();
|
|
218
214
|
});
|
|
219
215
|
});
|
|
220
216
|
});
|
|
@@ -235,7 +231,7 @@ function lagIds(device) {
|
|
|
235
231
|
function ipv4Networks(device, iface) {
|
|
236
232
|
const settings = device.interfaces?.[iface] ?? device.ports?.[iface];
|
|
237
233
|
const addresses = (settings?.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
238
|
-
return addresses.filter((address) =>
|
|
234
|
+
return addresses.filter((address) => familyOf2(address) === "ipv4").map((address) => network(address));
|
|
239
235
|
}
|
|
240
236
|
var SWITCH_SERVICES = ["ssh", "telnet", "snmp"];
|
|
241
237
|
function switchUnsupported(device) {
|
|
@@ -312,12 +308,12 @@ function head(line) {
|
|
|
312
308
|
}
|
|
313
309
|
function compare(desired, current, dialect) {
|
|
314
310
|
const canon = dialect.canon ?? ((line) => line);
|
|
315
|
-
const
|
|
311
|
+
const keyOf = (entry) => [...entry.path, canon(entry.line)].join("\x00");
|
|
316
312
|
const pathKey = (path) => path.join("\x00");
|
|
317
313
|
const wanted = entries(desired, dialect);
|
|
318
314
|
const existing = entries(current, dialect);
|
|
319
|
-
const wantedKeys = new Set(wanted.map(
|
|
320
|
-
const existingKeys = new Set(existing.map(
|
|
315
|
+
const wantedKeys = new Set(wanted.map(keyOf));
|
|
316
|
+
const existingKeys = new Set(existing.map(keyOf));
|
|
321
317
|
const wantedBlocks = new Set(wanted.map((entry) => pathKey([...entry.path, entry.line])));
|
|
322
318
|
const groups = new Map;
|
|
323
319
|
const groupFor = (path) => {
|
|
@@ -327,7 +323,7 @@ function compare(desired, current, dialect) {
|
|
|
327
323
|
return groups.get(key);
|
|
328
324
|
};
|
|
329
325
|
for (const entry of wanted) {
|
|
330
|
-
if (existingKeys.has(
|
|
326
|
+
if (existingKeys.has(keyOf(entry)))
|
|
331
327
|
continue;
|
|
332
328
|
const ownBlock = pathKey([...entry.path, entry.line]);
|
|
333
329
|
const opensBlock = wanted.some((other) => {
|
|
@@ -339,7 +335,7 @@ function compare(desired, current, dialect) {
|
|
|
339
335
|
groupFor(entry.path).add.push(entry.line);
|
|
340
336
|
}
|
|
341
337
|
for (const entry of [...existing].reverse()) {
|
|
342
|
-
if (wantedKeys.has(
|
|
338
|
+
if (wantedKeys.has(keyOf(entry)))
|
|
343
339
|
continue;
|
|
344
340
|
const insideRemovedBlock = entry.path.some((_, depth) => {
|
|
345
341
|
const enclosing = pathKey(entry.path.slice(0, depth + 1));
|
|
@@ -349,7 +345,7 @@ function compare(desired, current, dialect) {
|
|
|
349
345
|
continue;
|
|
350
346
|
const replaced = wanted.some((other) => {
|
|
351
347
|
const sameBlock = pathKey(other.path) === pathKey(entry.path);
|
|
352
|
-
const isNew = !existingKeys.has(
|
|
348
|
+
const isNew = !existingKeys.has(keyOf(other));
|
|
353
349
|
return sameBlock && isNew && head(other.line) === head(entry.line);
|
|
354
350
|
});
|
|
355
351
|
if (!replaced)
|
|
@@ -504,7 +500,7 @@ function render(device) {
|
|
|
504
500
|
}
|
|
505
501
|
if (allow.length) {
|
|
506
502
|
lines.push(`access-list ${MANAGEMENT_ACL}`, " description management");
|
|
507
|
-
allow.forEach((cidr, index) => lines.push(` rule ${(index + 1) * 10} permit ${
|
|
503
|
+
allow.forEach((cidr, index) => lines.push(` rule ${(index + 1) * 10} permit ${host2(cidr)} ${netmask(split(cidr).len)}`));
|
|
508
504
|
lines.push(" rule 100 deny any", "!");
|
|
509
505
|
}
|
|
510
506
|
for (const [name, user] of Object.entries(device.users ?? {}))
|
|
@@ -529,8 +525,8 @@ function render(device) {
|
|
|
529
525
|
lines.push(`interface vlan ${vlanId(iface.vlan)}`);
|
|
530
526
|
for (const entry of iface.addresses ?? []) {
|
|
531
527
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
532
|
-
if (
|
|
533
|
-
lines.push(`ip address ${
|
|
528
|
+
if (familyOf2(address) === "ipv4")
|
|
529
|
+
lines.push(`ip address ${host2(address)} ${netmask(split(address).len)}`);
|
|
534
530
|
}
|
|
535
531
|
const ospfSettings = Object.values(ospf?.areas ?? {}).find((area) => area.interfaces[name])?.interfaces[name];
|
|
536
532
|
if (ospfSettings?.network === "point-to-point")
|
|
@@ -549,7 +545,7 @@ function render(device) {
|
|
|
549
545
|
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
550
546
|
for (const iface of Object.keys(area.interfaces)) {
|
|
551
547
|
for (const prefix of ipv4Networks(device, iface)) {
|
|
552
|
-
lines.push(`network ${
|
|
548
|
+
lines.push(`network ${host2(prefix)} ${wildcard(split(prefix).len)} area ${areaId}`);
|
|
553
549
|
}
|
|
554
550
|
}
|
|
555
551
|
}
|
|
@@ -584,7 +580,7 @@ function render(device) {
|
|
|
584
580
|
lines.push(`snmp-server access-list ${MANAGEMENT_ACL}`);
|
|
585
581
|
for (const route of device.routing?.static ?? []) {
|
|
586
582
|
const { addr, len } = split(route.prefix);
|
|
587
|
-
if (
|
|
583
|
+
if (familyOf2(addr) === "ipv4" && route.via)
|
|
588
584
|
lines.push(`ip route ${addr} ${netmask(len)} ${route.via}`);
|
|
589
585
|
}
|
|
590
586
|
if (management.telnet) {
|
|
@@ -726,11 +722,11 @@ function unsupported(device) {
|
|
|
726
722
|
}
|
|
727
723
|
for (const entry of iface.addresses ?? []) {
|
|
728
724
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
729
|
-
if (
|
|
725
|
+
if (familyOf2(address) === "ipv6")
|
|
730
726
|
found.push(`interfaces.${name}.addresses ${address}, only IPv4 here`);
|
|
731
727
|
}
|
|
732
728
|
for (const group of iface.vrrp ?? []) {
|
|
733
|
-
if (
|
|
729
|
+
if (familyOf2(group.address) === "ipv6")
|
|
734
730
|
found.push(`interfaces.${name}.vrrp for IPv6`);
|
|
735
731
|
}
|
|
736
732
|
}
|
|
@@ -751,7 +747,7 @@ function unsupported(device) {
|
|
|
751
747
|
}
|
|
752
748
|
}
|
|
753
749
|
for (const route of routing?.static ?? []) {
|
|
754
|
-
if (!route.via || route.vrf ||
|
|
750
|
+
if (!route.via || route.vrf || familyOf2(route.prefix) === "ipv6") {
|
|
755
751
|
found.push(`routing.static ${route.prefix}, only an IPv4 next hop in the default table`);
|
|
756
752
|
}
|
|
757
753
|
if (route.description || route.distance)
|
|
@@ -995,9 +991,9 @@ function parse(text) {
|
|
|
995
991
|
menu = line;
|
|
996
992
|
continue;
|
|
997
993
|
}
|
|
998
|
-
const
|
|
999
|
-
if (
|
|
1000
|
-
generated.add(
|
|
994
|
+
const marker = /^# generated (\S+)$/.exec(line);
|
|
995
|
+
if (marker)
|
|
996
|
+
generated.add(marker[1]);
|
|
1001
997
|
if (menu && /^(add|set)( |$)/.test(line))
|
|
1002
998
|
commands.push(parseLine(menu, line));
|
|
1003
999
|
}
|
|
@@ -1087,12 +1083,12 @@ function normalValue(command, property, value) {
|
|
|
1087
1083
|
if (property === "tagged" || property === "untagged" || property === "slaves")
|
|
1088
1084
|
return value.split(",").sort().join(",");
|
|
1089
1085
|
const onInterface = property === "address" && (command.menu === "/ip address" || command.menu === "/ipv6 address");
|
|
1090
|
-
const
|
|
1086
|
+
const isAddress = /(^|-)address$/.test(property) && !property.endsWith("mac-address");
|
|
1091
1087
|
if (onInterface && !value.includes("/"))
|
|
1092
1088
|
return `${value}/${value.includes(":") ? 64 : 32}`;
|
|
1093
|
-
if (!onInterface &&
|
|
1089
|
+
if (!onInterface && isAddress && /^[\d.]+\/32$/.test(value))
|
|
1094
1090
|
return value.slice(0, -3);
|
|
1095
|
-
if (!onInterface &&
|
|
1091
|
+
if (!onInterface && isAddress && value.includes(":") && !value.includes("/") && !value.includes(","))
|
|
1096
1092
|
return `${value}/128`;
|
|
1097
1093
|
return value;
|
|
1098
1094
|
}
|
|
@@ -1421,13 +1417,13 @@ function planOrdered(menu, wanted, existing) {
|
|
|
1421
1417
|
const inKeptChain = `[:typeof [:find $keep [${menu} get $id chain]]] != "nil"`;
|
|
1422
1418
|
removeOld = `:local keep {${keep}}; :foreach id in=$${OLD_RULES} do={ :if (${inKeptChain}) do={ ${menu} remove $id } }`;
|
|
1423
1419
|
}
|
|
1424
|
-
const
|
|
1420
|
+
const send = [
|
|
1425
1421
|
`:global ${OLD_RULES} [${menu} find${where}]`,
|
|
1426
1422
|
...wanted.map((item) => line(menu, "add", "", item.flags, item.attrs)),
|
|
1427
1423
|
`:global ${OLD_RULES}; ${removeOld}; /system script environment remove [find name="${OLD_RULES}"]`
|
|
1428
1424
|
];
|
|
1429
1425
|
const show = [...existing.map((item) => `- ${canonical(item)}`), ...wanted.map((item) => `+ ${canonical(item)}`)];
|
|
1430
|
-
result.first.push({ title: `${menu}, in the order of the config`, show, send
|
|
1426
|
+
result.first.push({ title: `${menu}, in the order of the config`, show, send });
|
|
1431
1427
|
if (hasChains) {
|
|
1432
1428
|
for (const chain of droppedChains) {
|
|
1433
1429
|
const selector = `[ find chain=${quote(chain)}${DYNAMIC.has(menu) ? " and !dynamic" : ""} ]`;
|
|
@@ -1483,22 +1479,22 @@ class Output {
|
|
|
1483
1479
|
`;
|
|
1484
1480
|
}
|
|
1485
1481
|
}
|
|
1486
|
-
function quote2(
|
|
1487
|
-
const plain = /^[\w.:/@-]+$/.test(
|
|
1482
|
+
function quote2(value) {
|
|
1483
|
+
const plain = /^[\w.:/@-]+$/.test(value);
|
|
1488
1484
|
if (plain)
|
|
1489
|
-
return
|
|
1490
|
-
const escaped =
|
|
1485
|
+
return value;
|
|
1486
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
|
|
1491
1487
|
return `"${escaped}"`;
|
|
1492
1488
|
}
|
|
1493
1489
|
function command(verb, properties) {
|
|
1494
|
-
const
|
|
1495
|
-
for (const [name,
|
|
1496
|
-
if (
|
|
1490
|
+
const words = [verb];
|
|
1491
|
+
for (const [name, value] of Object.entries(properties)) {
|
|
1492
|
+
if (value === undefined || value === false)
|
|
1497
1493
|
continue;
|
|
1498
|
-
const written = typeof
|
|
1499
|
-
|
|
1494
|
+
const written = typeof value === "number" ? String(value) : quote2(value);
|
|
1495
|
+
words.push(`${name}=${written}`);
|
|
1500
1496
|
}
|
|
1501
|
-
return
|
|
1497
|
+
return words.join(" ");
|
|
1502
1498
|
}
|
|
1503
1499
|
var yesNo = (on) => on ? "yes" : "no";
|
|
1504
1500
|
var familySuffix = (family) => family === "ipv4" ? "V4" : "V6";
|
|
@@ -1761,7 +1757,7 @@ function splitEndpoint(endpoint) {
|
|
|
1761
1757
|
function vrrp(out, ctx) {
|
|
1762
1758
|
for (const { name, settings } of ctx.routed()) {
|
|
1763
1759
|
for (const group of settings.vrrp ?? []) {
|
|
1764
|
-
const ipv6 =
|
|
1760
|
+
const ipv6 = familyOf2(group.address) === "ipv6";
|
|
1765
1761
|
out.add("/interface vrrp", command("add", {
|
|
1766
1762
|
interface: name,
|
|
1767
1763
|
interval: group.interval ? duration(group.interval) : undefined,
|
|
@@ -1887,7 +1883,7 @@ function addresses(out, ctx) {
|
|
|
1887
1883
|
for (const entry of settings.addresses ?? []) {
|
|
1888
1884
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
1889
1885
|
const peer = typeof entry === "string" ? undefined : entry.peer;
|
|
1890
|
-
if (
|
|
1886
|
+
if (familyOf2(address) === "ipv4") {
|
|
1891
1887
|
out.add("/ip address", command("add", { address, comment: settings.description, interface: name, network: peer }));
|
|
1892
1888
|
} else {
|
|
1893
1889
|
out.add("/ipv6 address", command("add", { address, advertise: yesNo(settings.ipv6_ra), comment: settings.description, interface: name }));
|
|
@@ -1896,7 +1892,7 @@ function addresses(out, ctx) {
|
|
|
1896
1892
|
for (const group of settings.vrrp ?? []) {
|
|
1897
1893
|
const vrrpInterface = vrrpName(name, group.id);
|
|
1898
1894
|
const comment = `VRRP ${group.id} on ${name}`;
|
|
1899
|
-
if (
|
|
1895
|
+
if (familyOf2(group.address) === "ipv4") {
|
|
1900
1896
|
out.add("/ip address", command("add", { address: `${group.address}/32`, comment, interface: vrrpInterface }));
|
|
1901
1897
|
} else {
|
|
1902
1898
|
out.add("/ipv6 address", command("add", {
|
|
@@ -1938,30 +1934,30 @@ function dhcp(out, ctx) {
|
|
|
1938
1934
|
}
|
|
1939
1935
|
}
|
|
1940
1936
|
function dns(out, ctx) {
|
|
1941
|
-
const
|
|
1942
|
-
out.add("/ip dns", command("set", { "allow-remote-requests": yesNo(
|
|
1937
|
+
const dns = ctx.device.system?.dns;
|
|
1938
|
+
out.add("/ip dns", command("set", { "allow-remote-requests": yesNo(dns?.serve), servers: dns?.servers.join(",") ?? "" }));
|
|
1943
1939
|
}
|
|
1944
1940
|
function addressLists(out, ctx) {
|
|
1945
1941
|
const { device } = ctx;
|
|
1946
|
-
const
|
|
1942
|
+
const entries = [];
|
|
1947
1943
|
for (const [list, members] of Object.entries(device.firewall?.address_sets ?? {})) {
|
|
1948
1944
|
for (const address of members)
|
|
1949
|
-
|
|
1945
|
+
entries.push({ list, address });
|
|
1950
1946
|
}
|
|
1951
1947
|
for (const [list, set] of Object.entries(device.prefix_sets ?? {})) {
|
|
1952
1948
|
if ("prefixes" in set)
|
|
1953
1949
|
for (const address of set.prefixes)
|
|
1954
|
-
|
|
1950
|
+
entries.push({ list, address });
|
|
1955
1951
|
else
|
|
1956
1952
|
out.add("/ip firewall address-list", `# generated ${list}`);
|
|
1957
1953
|
}
|
|
1958
1954
|
for (const prefix of device.routing?.bgp?.networks ?? []) {
|
|
1959
|
-
|
|
1955
|
+
entries.push({ list: NETWORKS[familyOf2(host2(prefix))], address: prefix });
|
|
1960
1956
|
}
|
|
1961
1957
|
for (const family of ["ipv4", "ipv6"]) {
|
|
1962
1958
|
const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
|
|
1963
|
-
for (const { list, address } of
|
|
1964
|
-
if (
|
|
1959
|
+
for (const { list, address } of entries) {
|
|
1960
|
+
if (familyOf2(host2(address)) !== family)
|
|
1965
1961
|
continue;
|
|
1966
1962
|
out.add(menu, command("add", { address, list }));
|
|
1967
1963
|
}
|
|
@@ -1981,7 +1977,7 @@ function renderFirewall(out, ctx) {
|
|
|
1981
1977
|
function untracked(out, ctx, family) {
|
|
1982
1978
|
const menu = family === "ipv4" ? "/ip firewall raw" : "/ipv6 firewall raw";
|
|
1983
1979
|
for (const prefix of ctx.device.firewall?.untracked ?? []) {
|
|
1984
|
-
if (
|
|
1980
|
+
if (familyOf2(prefix) !== family)
|
|
1985
1981
|
continue;
|
|
1986
1982
|
for (const [direction, property] of [
|
|
1987
1983
|
["to", "dst-address"],
|
|
@@ -1998,8 +1994,8 @@ function families(ctx, match) {
|
|
|
1998
1994
|
if (match?.family)
|
|
1999
1995
|
candidates = [match.family];
|
|
2000
1996
|
else if (address)
|
|
2001
|
-
candidates = [
|
|
2002
|
-
const setHas = (set, family) => !set || (sets[set] ?? []).some((member) =>
|
|
1997
|
+
candidates = [familyOf2(address)];
|
|
1998
|
+
const setHas = (set, family) => !set || (sets[set] ?? []).some((member) => familyOf2(member) === family);
|
|
2003
1999
|
return candidates.filter((family) => setHas(match?.src_set, family) && setHas(match?.dst_set, family));
|
|
2004
2000
|
}
|
|
2005
2001
|
function matchProperties(ctx, match, family) {
|
|
@@ -2050,7 +2046,7 @@ function nat(out, ctx, family) {
|
|
|
2050
2046
|
for (const rule of rules ?? []) {
|
|
2051
2047
|
if (!families(ctx, rule.match).includes(family))
|
|
2052
2048
|
continue;
|
|
2053
|
-
if (rule.action !== "masquerade" &&
|
|
2049
|
+
if (rule.action !== "masquerade" && familyOf2(rule.to) !== family)
|
|
2054
2050
|
continue;
|
|
2055
2051
|
const action = rule.action === "masquerade" ? {
|
|
2056
2052
|
action: "masquerade"
|
|
@@ -2072,7 +2068,7 @@ function switchAcls(out, ctx) {
|
|
|
2072
2068
|
continue;
|
|
2073
2069
|
for (const rule of ctx.device.acls?.[settings.acl] ?? []) {
|
|
2074
2070
|
const match = rule.match ?? {};
|
|
2075
|
-
const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((
|
|
2071
|
+
const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address) => address && familyOf2(address) === "ipv6");
|
|
2076
2072
|
const address = (prefix) => prefix && !ipv6 ? dottedMask(prefix) : prefix;
|
|
2077
2073
|
for (const srcPort of each(match.src_port)) {
|
|
2078
2074
|
for (const dstPort of each(match.dst_port)) {
|
|
@@ -2117,7 +2113,7 @@ function renderRouting(out, ctx) {
|
|
|
2117
2113
|
}
|
|
2118
2114
|
function staticRoutes(out, ctx) {
|
|
2119
2115
|
for (const route of ctx.device.routing?.static ?? []) {
|
|
2120
|
-
const menu =
|
|
2116
|
+
const menu = familyOf2(host2(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
2121
2117
|
const verb = route.blackhole ? "add blackhole" : "add";
|
|
2122
2118
|
const gateway = route.via ?? (route.interface ? ctx.name(route.interface) : undefined);
|
|
2123
2119
|
out.add(menu, command(verb, {
|
|
@@ -2148,21 +2144,21 @@ function settingsOf(ctx, neighbor) {
|
|
|
2148
2144
|
return { ...group, ...neighbor };
|
|
2149
2145
|
}
|
|
2150
2146
|
function familiesOf(ctx, neighbor) {
|
|
2151
|
-
return settingsOf(ctx, neighbor).families ?? [
|
|
2147
|
+
return settingsOf(ctx, neighbor).families ?? [familyOf2(neighbor.address)];
|
|
2152
2148
|
}
|
|
2153
2149
|
var chainName = (policy, family) => `${policy}-${familySuffix(family)}`;
|
|
2154
2150
|
function appliesTo(ctx, rule, family) {
|
|
2155
2151
|
const match = rule.match;
|
|
2156
2152
|
if (match?.family && match.family !== family)
|
|
2157
2153
|
return false;
|
|
2158
|
-
if (match?.prefix &&
|
|
2154
|
+
if (match?.prefix && familyOf2(host2(match.prefix)) !== family)
|
|
2159
2155
|
return false;
|
|
2160
2156
|
if (match?.prefix_set) {
|
|
2161
2157
|
const set = ctx.device.prefix_sets?.[match.prefix_set];
|
|
2162
2158
|
if (!set)
|
|
2163
2159
|
return false;
|
|
2164
2160
|
const firstPrefix = "source" in set ? undefined : set.prefixes[0];
|
|
2165
|
-
const setFamily = "source" in set ? set.source.family : firstPrefix &&
|
|
2161
|
+
const setFamily = "source" in set ? set.source.family : firstPrefix && familyOf2(host2(firstPrefix));
|
|
2166
2162
|
if (setFamily !== family)
|
|
2167
2163
|
return false;
|
|
2168
2164
|
}
|
|
@@ -2193,35 +2189,35 @@ function conditions(rule) {
|
|
|
2193
2189
|
}
|
|
2194
2190
|
function actions(rule, family) {
|
|
2195
2191
|
const set = rule.set ?? {};
|
|
2196
|
-
const
|
|
2192
|
+
const steps = [];
|
|
2197
2193
|
if (set.local_pref !== undefined)
|
|
2198
|
-
|
|
2194
|
+
steps.push(`set bgp-local-pref ${set.local_pref}`);
|
|
2199
2195
|
if (set.med !== undefined)
|
|
2200
|
-
|
|
2196
|
+
steps.push(`set bgp-med ${set.med}`);
|
|
2201
2197
|
if (set.prepend !== undefined)
|
|
2202
|
-
|
|
2198
|
+
steps.push(`set bgp-path-prepend ${set.prepend}`);
|
|
2203
2199
|
for (const pattern of set.remove_communities ?? [])
|
|
2204
|
-
|
|
2200
|
+
steps.push(`delete bgp-communities regexp "${communityPattern(pattern)}"`);
|
|
2205
2201
|
if (set.communities)
|
|
2206
|
-
|
|
2202
|
+
steps.push(`set bgp-communities ${set.communities.join(",")}`);
|
|
2207
2203
|
if (set.add_communities)
|
|
2208
|
-
|
|
2204
|
+
steps.push(`append bgp-communities ${set.add_communities.join(",")}`);
|
|
2209
2205
|
for (const pattern of set.remove_large_communities ?? []) {
|
|
2210
|
-
|
|
2206
|
+
steps.push(`delete bgp-large-communities regexp "${communityPattern(pattern)}"`);
|
|
2211
2207
|
}
|
|
2212
2208
|
if (set.large_communities)
|
|
2213
|
-
|
|
2209
|
+
steps.push(`set bgp-large-communities ${set.large_communities.join(",")}`);
|
|
2214
2210
|
if (set.add_large_communities)
|
|
2215
|
-
|
|
2216
|
-
if (set.next_hop &&
|
|
2217
|
-
|
|
2218
|
-
if (set.preferred_source &&
|
|
2219
|
-
|
|
2211
|
+
steps.push(`append bgp-large-communities ${set.add_large_communities.join(",")}`);
|
|
2212
|
+
if (set.next_hop && familyOf2(set.next_hop) === family)
|
|
2213
|
+
steps.push(`set gw ${set.next_hop}`);
|
|
2214
|
+
if (set.preferred_source && familyOf2(set.preferred_source) === family)
|
|
2215
|
+
steps.push(`set pref-src ${set.preferred_source}`);
|
|
2220
2216
|
if (set.blackhole)
|
|
2221
|
-
|
|
2217
|
+
steps.push("set blackhole yes");
|
|
2222
2218
|
if (rule.action)
|
|
2223
|
-
|
|
2224
|
-
return
|
|
2219
|
+
steps.push(rule.action);
|
|
2220
|
+
return steps;
|
|
2225
2221
|
}
|
|
2226
2222
|
function ruleText(rule, family) {
|
|
2227
2223
|
const parts = [];
|
|
@@ -2230,24 +2226,24 @@ function ruleText(rule, family) {
|
|
|
2230
2226
|
if (rule.match?.rpki)
|
|
2231
2227
|
parts.push(`rpki-verify ${RPKI_GROUP}`);
|
|
2232
2228
|
const tests = conditions(rule);
|
|
2233
|
-
const
|
|
2229
|
+
const steps = actions(rule, family);
|
|
2234
2230
|
if (tests.length)
|
|
2235
|
-
parts.push(`if (${tests.join(" && ")})${
|
|
2231
|
+
parts.push(`if (${tests.join(" && ")})${steps.length ? ` { ${steps.join("; ")} }` : ""}`);
|
|
2236
2232
|
else
|
|
2237
|
-
parts.push(...
|
|
2233
|
+
parts.push(...steps);
|
|
2238
2234
|
return parts.join("; ");
|
|
2239
2235
|
}
|
|
2240
2236
|
function policies(out, ctx) {
|
|
2241
2237
|
const bgp = ctx.device.routing.bgp;
|
|
2242
|
-
const
|
|
2238
|
+
const policies = ctx.device.policies ?? {};
|
|
2243
2239
|
const wanted = new Map;
|
|
2244
2240
|
const want = (policy, family) => {
|
|
2245
|
-
const
|
|
2246
|
-
if (
|
|
2241
|
+
const families = wanted.get(policy) ?? new Set;
|
|
2242
|
+
if (families.has(family))
|
|
2247
2243
|
return;
|
|
2248
|
-
|
|
2249
|
-
wanted.set(policy,
|
|
2250
|
-
for (const rule of
|
|
2244
|
+
families.add(family);
|
|
2245
|
+
wanted.set(policy, families);
|
|
2246
|
+
for (const rule of policies[policy] ?? [])
|
|
2251
2247
|
if (rule.call && appliesTo(ctx, rule, family))
|
|
2252
2248
|
want(rule.call, family);
|
|
2253
2249
|
};
|
|
@@ -2263,13 +2259,13 @@ function policies(out, ctx) {
|
|
|
2263
2259
|
}
|
|
2264
2260
|
}
|
|
2265
2261
|
}
|
|
2266
|
-
const called = new Set(Object.values(
|
|
2262
|
+
const called = new Set(Object.values(policies).flatMap((rules) => rules.flatMap((rule) => rule.call ? [rule.call] : [])));
|
|
2267
2263
|
for (const policy of [...wanted.keys()].sort()) {
|
|
2268
2264
|
for (const family of ["ipv4", "ipv6"]) {
|
|
2269
2265
|
if (!wanted.get(policy).has(family))
|
|
2270
2266
|
continue;
|
|
2271
2267
|
const chain = chainName(policy, family);
|
|
2272
|
-
const rules = (
|
|
2268
|
+
const rules = (policies[policy] ?? []).filter((rule) => appliesTo(ctx, rule, family));
|
|
2273
2269
|
for (const rule of rules) {
|
|
2274
2270
|
const text = ruleText(rule, family);
|
|
2275
2271
|
if (text)
|
|
@@ -2294,9 +2290,9 @@ function bgpConnections(out, ctx) {
|
|
|
2294
2290
|
for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
|
|
2295
2291
|
const settings = settingsOf(ctx, neighbor);
|
|
2296
2292
|
const [family] = familiesOf(ctx, neighbor);
|
|
2297
|
-
const addressFamily =
|
|
2293
|
+
const addressFamily = familyOf2(neighbor.address);
|
|
2298
2294
|
const internal = settings.remote_as === bgp.asn;
|
|
2299
|
-
const originates = bgp.networks?.some((prefix) =>
|
|
2295
|
+
const originates = bgp.networks?.some((prefix) => familyOf2(host2(prefix)) === family);
|
|
2300
2296
|
const role = settings.role ? ROLES[settings.role] : internal ? "ibgp" : "ebgp";
|
|
2301
2297
|
out.add("/routing bgp connection", command("add", {
|
|
2302
2298
|
afi: addressFamily === family ? undefined : family === "ipv6" ? "ipv6" : "ip",
|
|
@@ -2322,19 +2318,19 @@ function bgpConnections(out, ctx) {
|
|
|
2322
2318
|
}
|
|
2323
2319
|
}
|
|
2324
2320
|
function ospf(out, ctx) {
|
|
2325
|
-
const
|
|
2326
|
-
if (!
|
|
2321
|
+
const ospf = ctx.device.routing?.ospf;
|
|
2322
|
+
if (!ospf)
|
|
2327
2323
|
return;
|
|
2328
|
-
for (const family of
|
|
2324
|
+
for (const family of ospf.families ?? ["ipv4"]) {
|
|
2329
2325
|
const version = family === "ipv4" ? 2 : 3;
|
|
2330
2326
|
const instance = `ospf-v${version}`;
|
|
2331
2327
|
out.add("/routing ospf instance", command("add", {
|
|
2332
2328
|
name: instance,
|
|
2333
|
-
redistribute:
|
|
2329
|
+
redistribute: ospf.redistribute?.join(","),
|
|
2334
2330
|
"router-id": ctx.device.routing?.router_id,
|
|
2335
2331
|
version
|
|
2336
2332
|
}));
|
|
2337
|
-
for (const [areaId, area] of Object.entries(
|
|
2333
|
+
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
2338
2334
|
const areaName = `${instance}-${areaId}`;
|
|
2339
2335
|
out.add("/routing ospf area", command("add", { "area-id": areaId, instance, name: areaName }));
|
|
2340
2336
|
for (const [iface, settings] of Object.entries(area.interfaces)) {
|
|
@@ -2411,19 +2407,19 @@ function layerTwo(out, ctx) {
|
|
|
2411
2407
|
}
|
|
2412
2408
|
function snmp(out, ctx) {
|
|
2413
2409
|
const management = ctx.device.management ?? {};
|
|
2414
|
-
const
|
|
2415
|
-
if (!
|
|
2410
|
+
const snmp = management.snmp;
|
|
2411
|
+
if (!snmp) {
|
|
2416
2412
|
out.add("/snmp", command("set", { enabled: "no" }));
|
|
2417
2413
|
out.add("/snmp community", command("set [ find default=yes ]", { addresses: "::/0", name: "public" }));
|
|
2418
2414
|
return;
|
|
2419
2415
|
}
|
|
2420
|
-
const allow = (
|
|
2421
|
-
out.add("/snmp", command("set", { contact:
|
|
2422
|
-
if (
|
|
2423
|
-
out.add("/snmp community", command("set [ find default=yes ]", { addresses: allow, name: marker(
|
|
2416
|
+
const allow = (snmp.allow ?? management.allow ?? []).join(",");
|
|
2417
|
+
out.add("/snmp", command("set", { contact: snmp.contact ?? "", enabled: "yes", location: snmp.location ?? "" }));
|
|
2418
|
+
if (snmp.community)
|
|
2419
|
+
out.add("/snmp community", command("set [ find default=yes ]", { addresses: allow, name: marker(snmp.community) }));
|
|
2424
2420
|
else
|
|
2425
2421
|
out.add("/snmp community", command("set [ find default=yes ]", { addresses: "::/0", disabled: "yes", name: "public" }));
|
|
2426
|
-
for (const [name, user] of Object.entries(
|
|
2422
|
+
for (const [name, user] of Object.entries(snmp.users ?? {})) {
|
|
2427
2423
|
out.add("/snmp community", command("add", {
|
|
2428
2424
|
addresses: allow,
|
|
2429
2425
|
"authentication-password": marker(user.auth_password),
|
|
@@ -2490,15 +2486,15 @@ function renderSystem(out, ctx) {
|
|
|
2490
2486
|
flowExport(out, ctx);
|
|
2491
2487
|
}
|
|
2492
2488
|
function logging(out, ctx) {
|
|
2493
|
-
const
|
|
2494
|
-
const local =
|
|
2489
|
+
const logging = ctx.device.system?.logging;
|
|
2490
|
+
const local = logging?.local;
|
|
2495
2491
|
out.add("/system logging action", command("set 1", {
|
|
2496
2492
|
"disk-file-count": local?.files ?? 2,
|
|
2497
2493
|
"disk-lines-per-file": local?.lines_per_file ?? 1000
|
|
2498
2494
|
}));
|
|
2499
2495
|
for (const topic of local ? topicsFrom(local.level) : [])
|
|
2500
2496
|
out.add("/system logging", command("add", { action: "disk", topics: topic }));
|
|
2501
|
-
(
|
|
2497
|
+
(logging?.remote ?? []).forEach((collector, index) => {
|
|
2502
2498
|
const action = `remote${index + 1}`;
|
|
2503
2499
|
out.add("/system logging action", command("add", {
|
|
2504
2500
|
name: action,
|
|
@@ -2576,17 +2572,17 @@ async function read2(device) {
|
|
|
2576
2572
|
try {
|
|
2577
2573
|
await checkVersion(client, device);
|
|
2578
2574
|
const config = await exec(client, "/export", 300000);
|
|
2579
|
-
const
|
|
2580
|
-
const
|
|
2581
|
-
const listed =
|
|
2582
|
-
`).every((
|
|
2583
|
-
if (
|
|
2584
|
-
throw new Error(`${device.name}: certificates could not be listed: ${
|
|
2575
|
+
const users = await exec(client, "/user export");
|
|
2576
|
+
const certificates = (await exec(client, LIST_CERTIFICATES)).trim();
|
|
2577
|
+
const listed = certificates.split(`
|
|
2578
|
+
`).every((line) => line.trim().startsWith("add fingerprint="));
|
|
2579
|
+
if (certificates && !listed) {
|
|
2580
|
+
throw new Error(`${device.name}: certificates could not be listed: ${certificates.slice(0, 120)}`);
|
|
2585
2581
|
}
|
|
2586
|
-
const userMenu =
|
|
2582
|
+
const userMenu = users.slice(users.indexOf(`
|
|
2587
2583
|
/`)).trim();
|
|
2588
|
-
const certificateMenu =
|
|
2589
|
-
${
|
|
2584
|
+
const certificateMenu = certificates ? `/certificate
|
|
2585
|
+
${certificates}` : "";
|
|
2590
2586
|
const text = [config.trim(), userMenu, certificateMenu].filter(Boolean).join(`
|
|
2591
2587
|
`);
|
|
2592
2588
|
return withoutTimestamp(text) + `
|
|
@@ -2622,8 +2618,8 @@ function unsupported2(device) {
|
|
|
2622
2618
|
const bgp = device.routing?.bgp;
|
|
2623
2619
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
2624
2620
|
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
2625
|
-
const
|
|
2626
|
-
if (
|
|
2621
|
+
const families = neighbor.families ?? group?.families ?? [familyOf2(neighbor.address)];
|
|
2622
|
+
if (families.length > 1)
|
|
2627
2623
|
found.push(`routing.bgp.neighbors.${name}.families, one family per session`);
|
|
2628
2624
|
}
|
|
2629
2625
|
const management = device.management;
|
|
@@ -2649,8 +2645,8 @@ function unsupported2(device) {
|
|
|
2649
2645
|
}
|
|
2650
2646
|
var ROLLBACK_SCHEDULER = "apply-rollback";
|
|
2651
2647
|
var escapeForString = (text) => text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
|
|
2652
|
-
async function refusal(client,
|
|
2653
|
-
const compiled = (await exec(client, `:put [:parse "${escapeForString(
|
|
2648
|
+
async function refusal(client, command) {
|
|
2649
|
+
const compiled = (await exec(client, `:put [:parse "${escapeForString(command)}"]`)).trim();
|
|
2654
2650
|
const refused = !compiled.startsWith("(") || /syntax error|bad parameter|no such|expected/.test(compiled);
|
|
2655
2651
|
return refused ? compiled.slice(0, 200) : undefined;
|
|
2656
2652
|
}
|
|
@@ -2661,25 +2657,25 @@ async function installCertificate(client, device, name) {
|
|
|
2661
2657
|
await upload(client, `${name}.crt`, certificate.certificate);
|
|
2662
2658
|
await exec(client, `/certificate import file-name=${name}.crt name=${name} passphrase=""`);
|
|
2663
2659
|
if (certificate.private_key) {
|
|
2664
|
-
const
|
|
2665
|
-
const privateKey =
|
|
2666
|
-
` : Buffer.from(
|
|
2660
|
+
const value = resolve(certificate.private_key);
|
|
2661
|
+
const privateKey = value.includes("-----BEGIN") ? `${value}
|
|
2662
|
+
` : Buffer.from(value, "base64").toString("utf8");
|
|
2667
2663
|
await upload(client, `${name}.key`, privateKey);
|
|
2668
2664
|
await exec(client, `/certificate import file-name=${name}.key name=${name} passphrase=""`);
|
|
2669
2665
|
}
|
|
2670
2666
|
await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
|
|
2671
2667
|
}
|
|
2672
|
-
async function apply2(device, _network,
|
|
2668
|
+
async function apply2(device, _network, changes, options) {
|
|
2673
2669
|
const client = await login(device);
|
|
2674
2670
|
await checkVersion(client, device);
|
|
2675
|
-
const commands =
|
|
2676
|
-
for (const { title, command
|
|
2677
|
-
if (
|
|
2671
|
+
const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
|
|
2672
|
+
for (const { title, command } of commands) {
|
|
2673
|
+
if (command.startsWith(INSTALL_CERTIFICATE))
|
|
2678
2674
|
continue;
|
|
2679
|
-
const reason = await refusal(client, substitute(
|
|
2675
|
+
const reason = await refusal(client, substitute(command));
|
|
2680
2676
|
if (reason) {
|
|
2681
2677
|
client.end();
|
|
2682
|
-
throw new Error(`${title}: the device will not parse ${
|
|
2678
|
+
throw new Error(`${title}: the device will not parse ${command.slice(0, 200)}
|
|
2683
2679
|
${reason}`);
|
|
2684
2680
|
}
|
|
2685
2681
|
}
|
|
@@ -2696,15 +2692,15 @@ async function apply2(device, _network, changes2, options) {
|
|
|
2696
2692
|
await exec(client, `/system backup save name=${backup} dont-encrypt=yes`, 120000);
|
|
2697
2693
|
await exec(client, `/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
|
|
2698
2694
|
await exec(client, armRollback);
|
|
2699
|
-
for (const { title, command
|
|
2700
|
-
if (
|
|
2701
|
-
const certificateName =
|
|
2695
|
+
for (const { title, command } of commands) {
|
|
2696
|
+
if (command.startsWith(INSTALL_CERTIFICATE)) {
|
|
2697
|
+
const certificateName = command.slice(INSTALL_CERTIFICATE.length).trim();
|
|
2702
2698
|
await installCertificate(client, device, certificateName);
|
|
2703
2699
|
continue;
|
|
2704
2700
|
}
|
|
2705
|
-
const answer = (await exec(client, substitute(
|
|
2701
|
+
const answer = (await exec(client, substitute(command), 120000)).trim();
|
|
2706
2702
|
if (answer)
|
|
2707
|
-
throw new Error(`${title}: ${
|
|
2703
|
+
throw new Error(`${title}: ${command.slice(0, 160)}
|
|
2708
2704
|
device said: ${answer.slice(0, 300)}`);
|
|
2709
2705
|
}
|
|
2710
2706
|
} catch (error) {
|
|
@@ -2776,20 +2772,20 @@ var DEVICE_FACTS2 = [
|
|
|
2776
2772
|
"Info:"
|
|
2777
2773
|
];
|
|
2778
2774
|
var UNDO_BY_KEYWORD2 = /^(description|name|port default vlan|port link-type|port trunk pvid vlan|set authentication password|eth-trunk|ip binding vpn-instance)\b/;
|
|
2779
|
-
function undo2(
|
|
2780
|
-
if (
|
|
2781
|
-
return
|
|
2782
|
-
if (/^rule \d+/.test(
|
|
2783
|
-
return `undo ${
|
|
2784
|
-
const keyword = UNDO_BY_KEYWORD2.exec(
|
|
2775
|
+
function undo2(line) {
|
|
2776
|
+
if (line.startsWith("undo "))
|
|
2777
|
+
return line.slice("undo ".length);
|
|
2778
|
+
if (/^rule \d+/.test(line))
|
|
2779
|
+
return `undo ${line.split(" ").slice(0, 2).join(" ")}`;
|
|
2780
|
+
const keyword = UNDO_BY_KEYWORD2.exec(line);
|
|
2785
2781
|
if (keyword)
|
|
2786
2782
|
return `undo ${keyword[1]}`;
|
|
2787
|
-
if (
|
|
2788
|
-
return `undo ${
|
|
2789
|
-
return `undo ${
|
|
2783
|
+
if (line.startsWith("storm control "))
|
|
2784
|
+
return `undo ${line.replace(/ (min-rate|max-rate).*$/, "")}`;
|
|
2785
|
+
return `undo ${line}`;
|
|
2790
2786
|
}
|
|
2791
|
-
function canon(
|
|
2792
|
-
return
|
|
2787
|
+
function canon(line) {
|
|
2788
|
+
return line.replace(/(irreversible-cipher|cipher) \S+/g, "cipher <secret>").replace(/<secret:\w+>/g, "<secret>");
|
|
2793
2789
|
}
|
|
2794
2790
|
function order(path) {
|
|
2795
2791
|
const header = path[0] ?? "";
|
|
@@ -2811,7 +2807,7 @@ var dialect2 = {
|
|
|
2811
2807
|
block: /^(interface |vlan \d+$|acl number |aaa$|user-interface |ospf |ip vpn-instance |bfd$)/,
|
|
2812
2808
|
indented: true,
|
|
2813
2809
|
terminator: "#",
|
|
2814
|
-
ignore: (
|
|
2810
|
+
ignore: (line) => DEVICE_FACTS2.some((fact) => line.startsWith(fact)),
|
|
2815
2811
|
undo: undo2,
|
|
2816
2812
|
leave: "quit",
|
|
2817
2813
|
first: /^authentication-mode /,
|
|
@@ -2825,13 +2821,13 @@ var SNMP_GROUP2 = "circuit";
|
|
|
2825
2821
|
var USER_LEVEL = { admin: 3, operator: 2, "read-only": 1 };
|
|
2826
2822
|
var SNMP_AUTH3 = { sha1: "sha", sha256: "sha2-256" };
|
|
2827
2823
|
var SNMP_PRIVACY2 = { aes128: "aes128", des: "des56" };
|
|
2828
|
-
function batchIds(
|
|
2829
|
-
const
|
|
2824
|
+
function batchIds(line) {
|
|
2825
|
+
const words = line.replace(/^vlan batch /, "").split(" ");
|
|
2830
2826
|
const ids = [];
|
|
2831
|
-
for (let index = 0;index <
|
|
2832
|
-
const first = Number(
|
|
2833
|
-
if (
|
|
2834
|
-
const last = Number(
|
|
2827
|
+
for (let index = 0;index < words.length; index++) {
|
|
2828
|
+
const first = Number(words[index]);
|
|
2829
|
+
if (words[index + 1] === "to") {
|
|
2830
|
+
const last = Number(words[index + 2]);
|
|
2835
2831
|
for (let id = first;id <= last; id++)
|
|
2836
2832
|
ids.push(id);
|
|
2837
2833
|
index += 2;
|
|
@@ -2968,7 +2964,7 @@ function managementAcl(config, device) {
|
|
|
2968
2964
|
const allow = device.management?.allow ?? [];
|
|
2969
2965
|
if (!allow.length)
|
|
2970
2966
|
return;
|
|
2971
|
-
const rules = allow.map((cidr, index) => `rule ${(index + 1) * 5} permit source ${
|
|
2967
|
+
const rules = allow.map((cidr, index) => `rule ${(index + 1) * 5} permit source ${host2(cidr)} ${wildcard(split(cidr).len)}`);
|
|
2972
2968
|
config.block(`acl number ${MANAGEMENT_ACL2}`, ["description Management access", ...rules, `rule ${(allow.length + 1) * 5} deny`]);
|
|
2973
2969
|
}
|
|
2974
2970
|
function users2(config, device) {
|
|
@@ -3047,8 +3043,8 @@ function vlanInterface(device, name, iface) {
|
|
|
3047
3043
|
lines.push(`ip binding vpn-instance ${iface.vrf}`);
|
|
3048
3044
|
for (const entry of iface.addresses ?? []) {
|
|
3049
3045
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
3050
|
-
if (
|
|
3051
|
-
lines.push(`ip address ${
|
|
3046
|
+
if (familyOf2(address) === "ipv4")
|
|
3047
|
+
lines.push(`ip address ${host2(address)} ${netmask(split(address).len)}`);
|
|
3052
3048
|
else
|
|
3053
3049
|
lines.push(`ipv6 address ${address}`);
|
|
3054
3050
|
}
|
|
@@ -3103,7 +3099,7 @@ function staticRoutes2(config, device, nameOf) {
|
|
|
3103
3099
|
const vrf = route.vrf ? ` vpn-instance ${route.vrf}` : "";
|
|
3104
3100
|
const preference = route.distance ? ` preference ${route.distance}` : "";
|
|
3105
3101
|
const description = route.description ? ` description ${route.description}` : "";
|
|
3106
|
-
if (
|
|
3102
|
+
if (familyOf2(addr) === "ipv4")
|
|
3107
3103
|
config.line(`ip route-static${vrf} ${addr} ${netmask(len)} ${nextHop}${preference}${description}`);
|
|
3108
3104
|
else
|
|
3109
3105
|
config.line(`ipv6 route-static${vrf} ${addr} ${len} ${nextHop}${preference}`);
|
|
@@ -3111,21 +3107,21 @@ function staticRoutes2(config, device, nameOf) {
|
|
|
3111
3107
|
config.separator();
|
|
3112
3108
|
}
|
|
3113
3109
|
function ospf2(config, device, nameOf) {
|
|
3114
|
-
const
|
|
3115
|
-
if (!
|
|
3110
|
+
const ospf = device.routing?.ospf;
|
|
3111
|
+
if (!ospf)
|
|
3116
3112
|
return;
|
|
3117
3113
|
const routerId = device.routing?.router_id;
|
|
3118
3114
|
const lines = [];
|
|
3119
|
-
for (const source of
|
|
3115
|
+
for (const source of ospf.redistribute ?? [])
|
|
3120
3116
|
lines.push(`import-route ${source === "connected" ? "direct" : "static"}`);
|
|
3121
3117
|
const areaLines = [];
|
|
3122
|
-
for (const [areaId, area] of Object.entries(
|
|
3118
|
+
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
3123
3119
|
areaLines.push(`area ${areaId}`);
|
|
3124
3120
|
for (const [iface, settings] of Object.entries(area.interfaces)) {
|
|
3125
3121
|
if (settings?.passive)
|
|
3126
3122
|
lines.push(`silent-interface ${nameOf(iface)}`);
|
|
3127
3123
|
for (const prefix of ipv4Networks(device, iface))
|
|
3128
|
-
areaLines.push(` network ${
|
|
3124
|
+
areaLines.push(` network ${host2(prefix)} ${wildcard(split(prefix).len)}`);
|
|
3129
3125
|
}
|
|
3130
3126
|
}
|
|
3131
3127
|
config.block(`ospf ${OSPF_PROCESS2}${routerId ? ` router-id ${routerId}` : ""}`, [...lines, ...areaLines]);
|
|
@@ -3146,23 +3142,23 @@ function flowExport2(config, device) {
|
|
|
3146
3142
|
config.separator();
|
|
3147
3143
|
}
|
|
3148
3144
|
function snmp2(config, device) {
|
|
3149
|
-
const
|
|
3150
|
-
if (!
|
|
3145
|
+
const snmp = device.management?.snmp;
|
|
3146
|
+
if (!snmp)
|
|
3151
3147
|
return;
|
|
3152
|
-
const versions = [
|
|
3148
|
+
const versions = [snmp.community ? "v2c" : "", snmp.users ? "v3" : ""].filter(Boolean).join(" ");
|
|
3153
3149
|
const guarded = (device.management?.allow?.length ?? 0) > 0;
|
|
3154
3150
|
config.line("snmp-agent");
|
|
3155
3151
|
config.line(`snmp-agent sys-info version ${versions}`);
|
|
3156
|
-
if (
|
|
3157
|
-
config.line(`snmp-agent sys-info contact ${
|
|
3158
|
-
if (
|
|
3159
|
-
config.line(`snmp-agent sys-info location ${
|
|
3160
|
-
if (
|
|
3161
|
-
config.line(`snmp-agent community read cipher ${marker(
|
|
3162
|
-
if (
|
|
3152
|
+
if (snmp.contact)
|
|
3153
|
+
config.line(`snmp-agent sys-info contact ${snmp.contact}`);
|
|
3154
|
+
if (snmp.location)
|
|
3155
|
+
config.line(`snmp-agent sys-info location ${snmp.location}`);
|
|
3156
|
+
if (snmp.community)
|
|
3157
|
+
config.line(`snmp-agent community read cipher ${marker(snmp.community)}${guarded ? ` acl ${MANAGEMENT_ACL2}` : ""}`);
|
|
3158
|
+
if (snmp.users) {
|
|
3163
3159
|
config.line("snmp-agent mib-view included all iso");
|
|
3164
3160
|
config.line(`snmp-agent group v3 ${SNMP_GROUP2} privacy read-view all${guarded ? ` acl ${MANAGEMENT_ACL2}` : ""}`);
|
|
3165
|
-
for (const [name, user] of Object.entries(
|
|
3161
|
+
for (const [name, user] of Object.entries(snmp.users)) {
|
|
3166
3162
|
config.line(`snmp-agent usm-user v3 ${name} group ${SNMP_GROUP2}`);
|
|
3167
3163
|
config.line(`snmp-agent usm-user v3 ${name} authentication-mode ${SNMP_AUTH3[user.auth]} cipher ${marker(user.auth_password)}`);
|
|
3168
3164
|
config.line(`snmp-agent usm-user v3 ${name} privacy-mode ${SNMP_PRIVACY2[user.privacy]} cipher ${marker(user.privacy_password)}`);
|
|
@@ -3171,8 +3167,8 @@ function snmp2(config, device) {
|
|
|
3171
3167
|
config.separator();
|
|
3172
3168
|
}
|
|
3173
3169
|
function ssh(config, device) {
|
|
3174
|
-
const
|
|
3175
|
-
if (!
|
|
3170
|
+
const ssh = device.management?.ssh;
|
|
3171
|
+
if (!ssh)
|
|
3176
3172
|
return;
|
|
3177
3173
|
const guarded = (device.management?.allow?.length ?? 0) > 0;
|
|
3178
3174
|
config.line("stelnet server enable");
|
|
@@ -3185,7 +3181,7 @@ function ssh(config, device) {
|
|
|
3185
3181
|
config.line(`ssh server acl ${MANAGEMENT_ACL2}`);
|
|
3186
3182
|
config.line("ssh authorization-type default aaa");
|
|
3187
3183
|
config.separator();
|
|
3188
|
-
if (!
|
|
3184
|
+
if (!ssh.weak_crypto) {
|
|
3189
3185
|
config.line("ssh server cipher aes256_ctr aes128_ctr");
|
|
3190
3186
|
config.line("ssh server hmac sha2_256_96 sha2_256");
|
|
3191
3187
|
config.line("ssh server key-exchange dh_group_exchange_sha256 ecdh_sha2_nistp256 ecdh_sha2_nistp384 ecdh_sha2_nistp521");
|
|
@@ -3227,10 +3223,10 @@ async function read3(device) {
|
|
|
3227
3223
|
channel.end();
|
|
3228
3224
|
const lines = dump.replace(/\r/g, "").split(`
|
|
3229
3225
|
`);
|
|
3230
|
-
const start = lines.findIndex((
|
|
3226
|
+
const start = lines.findIndex((line) => line.startsWith("!Software Version"));
|
|
3231
3227
|
if (start < 0)
|
|
3232
3228
|
throw new Error(`${device.name} returned no configuration`);
|
|
3233
|
-
const body = lines.slice(start).filter((
|
|
3229
|
+
const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
|
|
3234
3230
|
return scrub(body.join(`
|
|
3235
3231
|
`).trim() + `
|
|
3236
3232
|
`);
|
|
@@ -3261,7 +3257,7 @@ function unsupported3(device) {
|
|
|
3261
3257
|
if (iface.mtu !== undefined)
|
|
3262
3258
|
found.push(`interfaces.${name}.mtu`);
|
|
3263
3259
|
for (const group of iface.vrrp ?? [])
|
|
3264
|
-
if (
|
|
3260
|
+
if (familyOf2(group.address) === "ipv6")
|
|
3265
3261
|
found.push(`interfaces.${name}.vrrp for IPv6`);
|
|
3266
3262
|
}
|
|
3267
3263
|
for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
|
|
@@ -3294,11 +3290,11 @@ function unsupported3(device) {
|
|
|
3294
3290
|
found.push("management.ssh.auth_timeout and auth_retries");
|
|
3295
3291
|
if (device.lldp && device.lldp !== true)
|
|
3296
3292
|
found.push("lldp.interfaces, use lldp on the ports instead");
|
|
3297
|
-
const
|
|
3298
|
-
if (
|
|
3293
|
+
const system = device.system ?? {};
|
|
3294
|
+
if (system.dns || system.logging || system.release_channel || system.banner) {
|
|
3299
3295
|
found.push("system.dns, logging, release_channel and banner");
|
|
3300
3296
|
}
|
|
3301
|
-
if (
|
|
3297
|
+
if (system.transceiver_monitoring !== undefined)
|
|
3302
3298
|
found.push("system.transceiver_monitoring");
|
|
3303
3299
|
found.push(...switchUnsupported(device));
|
|
3304
3300
|
const flow = device.flow_export;
|
|
@@ -3313,31 +3309,31 @@ function unsupported3(device) {
|
|
|
3313
3309
|
}
|
|
3314
3310
|
var REFUSED2 = /^\s*Error|Unrecognized command|Incomplete command|Wrong parameter|Too many parameters/m;
|
|
3315
3311
|
var INTERACTIVE = /^(snmp-agent usm-user v3 \S+ (?:authentication|privacy)-mode \S+) cipher <secret:([A-Za-z0-9_./:-]+)>$/;
|
|
3316
|
-
async function sendLine(channel,
|
|
3317
|
-
const interactive = INTERACTIVE.exec(
|
|
3312
|
+
async function sendLine(channel, line) {
|
|
3313
|
+
const interactive = INTERACTIVE.exec(line.trim());
|
|
3318
3314
|
if (interactive) {
|
|
3319
|
-
const [,
|
|
3315
|
+
const [, command, reference] = interactive;
|
|
3320
3316
|
const password = valueOfReference(reference);
|
|
3321
|
-
let
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
return
|
|
3317
|
+
let answer = await send(channel, command, 15000, 900);
|
|
3318
|
+
answer += await send(channel, password, 15000, 900);
|
|
3319
|
+
answer += await send(channel, password, 15000, 900);
|
|
3320
|
+
return answer;
|
|
3325
3321
|
}
|
|
3326
|
-
let answer = await send(channel, substitute(
|
|
3322
|
+
let answer = await send(channel, substitute(line), 20000, 800);
|
|
3327
3323
|
if (/\[Y\/N\]/i.test(answer))
|
|
3328
3324
|
answer += await send(channel, "Y", 20000, 800);
|
|
3329
3325
|
return answer;
|
|
3330
3326
|
}
|
|
3331
|
-
async function apply3(device, _network,
|
|
3327
|
+
async function apply3(device, _network, plan) {
|
|
3332
3328
|
const client = await login2(device);
|
|
3333
3329
|
const channel = await openShell(client);
|
|
3334
3330
|
try {
|
|
3335
3331
|
await send(channel, "system-view", 1e4, 1500);
|
|
3336
|
-
for (const step of
|
|
3337
|
-
for (const
|
|
3338
|
-
const answer = await sendLine(channel,
|
|
3332
|
+
for (const step of plan.steps) {
|
|
3333
|
+
for (const line of step.send) {
|
|
3334
|
+
const answer = await sendLine(channel, line);
|
|
3339
3335
|
if (REFUSED2.test(answer))
|
|
3340
|
-
throw new Error(`${step.title}: ${
|
|
3336
|
+
throw new Error(`${step.title}: ${line}
|
|
3341
3337
|
device said: ${answer.trim().slice(0, 300)}`);
|
|
3342
3338
|
}
|
|
3343
3339
|
}
|
|
@@ -3373,19 +3369,19 @@ function plan3(desired, current) {
|
|
|
3373
3369
|
const groups = compare(desired, current, dialect2);
|
|
3374
3370
|
vlanListChange(groups, "vlan batch ", batchIds, (ids) => [`vlan batch ${vlanBatch(ids)}`], (ids) => [`vlan batch ${vlanBatch(ids)}`]);
|
|
3375
3371
|
const top = groups.find((group) => group.path.length === 0);
|
|
3376
|
-
const removedBatch = top?.remove.find((
|
|
3372
|
+
const removedBatch = top?.remove.find((line) => line.startsWith("vlan batch ")) ?? "";
|
|
3377
3373
|
const removedIds = removedBatch ? batchIds(removedBatch) : [];
|
|
3378
3374
|
const firstRemovals = [
|
|
3379
|
-
...removedIds.map((id) => `interface Vlanif${id}`).filter((
|
|
3380
|
-
...removedIds.map((id) => `vlan ${id}`).filter((
|
|
3375
|
+
...removedIds.map((id) => `interface Vlanif${id}`).filter((line) => top?.remove.includes(line)),
|
|
3376
|
+
...removedIds.map((id) => `vlan ${id}`).filter((line) => top?.remove.includes(line)),
|
|
3381
3377
|
...removedBatch ? [removedBatch] : []
|
|
3382
3378
|
];
|
|
3383
3379
|
if (top)
|
|
3384
|
-
top.remove = top.remove.filter((
|
|
3380
|
+
top.remove = top.remove.filter((line) => !firstRemovals.includes(line));
|
|
3385
3381
|
const planned = steps(groups.filter((group) => group.add.length || group.remove.length), dialect2);
|
|
3386
3382
|
if (firstRemovals.length) {
|
|
3387
|
-
const sent = firstRemovals.filter((
|
|
3388
|
-
planned.unshift({ title: "VLANs no longer used", show: firstRemovals.map((
|
|
3383
|
+
const sent = firstRemovals.filter((line) => !/^vlan \d+$/.test(line)).map(undo2);
|
|
3384
|
+
planned.unshift({ title: "VLANs no longer used", show: firstRemovals.map((line) => `- ${line}`), send: sent });
|
|
3389
3385
|
}
|
|
3390
3386
|
return { steps: planned, problems: [] };
|
|
3391
3387
|
}
|
|
@@ -3420,17 +3416,17 @@ var registries = { "ripe-stat": ripeStat };
|
|
|
3420
3416
|
// src/adapters/registries/librenms.ts
|
|
3421
3417
|
var AUTH = { sha1: "SHA", sha256: "SHA-256" };
|
|
3422
3418
|
var PRIVACY = { aes128: "AES", des: "DES" };
|
|
3423
|
-
function settingsOf2(
|
|
3424
|
-
if (!
|
|
3419
|
+
function settingsOf2(network) {
|
|
3420
|
+
if (!network.librenms)
|
|
3425
3421
|
throw new Error("the network has no librenms settings");
|
|
3426
3422
|
return {
|
|
3427
|
-
base: `${
|
|
3428
|
-
headers: { "X-Auth-Token": resolve(
|
|
3423
|
+
base: `${network.librenms.url.replace(/\/+$/, "")}/api/v0`,
|
|
3424
|
+
headers: { "X-Auth-Token": resolve(network.librenms.api_token), "Content-Type": "application/json" }
|
|
3429
3425
|
};
|
|
3430
3426
|
}
|
|
3431
3427
|
function credentials(device) {
|
|
3432
|
-
const
|
|
3433
|
-
const [name, user] = Object.entries(
|
|
3428
|
+
const snmp = device.management?.snmp;
|
|
3429
|
+
const [name, user] = Object.entries(snmp?.users ?? {})[0] ?? [];
|
|
3434
3430
|
if (name && user) {
|
|
3435
3431
|
return {
|
|
3436
3432
|
snmpver: "v3",
|
|
@@ -3442,26 +3438,26 @@ function credentials(device) {
|
|
|
3442
3438
|
cryptoalgo: PRIVACY[user.privacy]
|
|
3443
3439
|
};
|
|
3444
3440
|
}
|
|
3445
|
-
if (
|
|
3446
|
-
return { snmpver: "v2c", community: resolve(
|
|
3441
|
+
if (snmp?.community)
|
|
3442
|
+
return { snmpver: "v2c", community: resolve(snmp.community) };
|
|
3447
3443
|
return;
|
|
3448
3444
|
}
|
|
3449
|
-
async function planLibreNms(
|
|
3450
|
-
const { base, headers } = settingsOf2(
|
|
3445
|
+
async function planLibreNms(network) {
|
|
3446
|
+
const { base, headers } = settingsOf2(network);
|
|
3451
3447
|
const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30000) });
|
|
3452
3448
|
if (!response.ok)
|
|
3453
3449
|
throw new Error(`LibreNMS answered ${response.status} to the device list`);
|
|
3454
3450
|
const monitored = new Set((await response.json()).devices.map((device) => device.hostname));
|
|
3455
|
-
const ours = new Set(
|
|
3456
|
-
const missing =
|
|
3451
|
+
const ours = new Set(network.devices.map((device) => device.connection.host));
|
|
3452
|
+
const missing = network.devices.filter((device) => !monitored.has(device.connection.host));
|
|
3457
3453
|
return {
|
|
3458
3454
|
additions: missing.filter((device) => device.management?.snmp),
|
|
3459
3455
|
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
3460
3456
|
unknown: [...monitored].filter((hostname) => !ours.has(hostname)).sort()
|
|
3461
3457
|
};
|
|
3462
3458
|
}
|
|
3463
|
-
async function sendLibreNms(
|
|
3464
|
-
const { base, headers } = settingsOf2(
|
|
3459
|
+
async function sendLibreNms(network, additions) {
|
|
3460
|
+
const { base, headers } = settingsOf2(network);
|
|
3465
3461
|
for (const device of additions) {
|
|
3466
3462
|
const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
|
|
3467
3463
|
const response = await fetch(`${base}/devices`, {
|
|
@@ -3487,48 +3483,48 @@ function peersWithRouteServer(device, iface) {
|
|
|
3487
3483
|
return Object.values(bgp?.neighbors ?? {}).some((neighbor) => {
|
|
3488
3484
|
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
3489
3485
|
const role = neighbor.role ?? group?.role;
|
|
3490
|
-
return role === "rs" && subnets.some((subnet) =>
|
|
3486
|
+
return role === "rs" && subnets.some((subnet) => contains2(subnet, neighbor.address));
|
|
3491
3487
|
});
|
|
3492
3488
|
}
|
|
3493
|
-
function wanted(
|
|
3489
|
+
function wanted(network) {
|
|
3494
3490
|
const records = [];
|
|
3495
|
-
for (const device of
|
|
3491
|
+
for (const device of network.devices) {
|
|
3496
3492
|
for (const iface of Object.values(device.interfaces ?? {})) {
|
|
3497
3493
|
const exchange = iface.exchange;
|
|
3498
3494
|
if (!exchange?.peeringdb_ixlan)
|
|
3499
3495
|
continue;
|
|
3500
|
-
const published = addressesOf2(iface).map(
|
|
3496
|
+
const published = addressesOf2(iface).map(host2).filter(isGlobal2);
|
|
3501
3497
|
records.push({
|
|
3502
3498
|
name: exchange.name,
|
|
3503
3499
|
ixlan_id: exchange.peeringdb_ixlan,
|
|
3504
3500
|
speed: exchange.speed,
|
|
3505
|
-
ipaddr4: published.find((address) =>
|
|
3506
|
-
ipaddr6: published.find((address) =>
|
|
3501
|
+
ipaddr4: published.find((address) => familyOf2(address) === "ipv4") ?? null,
|
|
3502
|
+
ipaddr6: published.find((address) => familyOf2(address) === "ipv6") ?? null,
|
|
3507
3503
|
is_rs_peer: peersWithRouteServer(device, iface)
|
|
3508
3504
|
});
|
|
3509
3505
|
}
|
|
3510
3506
|
}
|
|
3511
3507
|
return records;
|
|
3512
3508
|
}
|
|
3513
|
-
async function planPeeringDb(
|
|
3514
|
-
if (!
|
|
3509
|
+
async function planPeeringDb(network) {
|
|
3510
|
+
if (!network.asn)
|
|
3515
3511
|
throw new Error("the network declares no asn");
|
|
3516
|
-
const response = await fetch(`${API}/netixlan?asn=${
|
|
3512
|
+
const response = await fetch(`${API}/netixlan?asn=${network.asn}`, {
|
|
3517
3513
|
headers: { Accept: "application/json" },
|
|
3518
3514
|
signal: AbortSignal.timeout(30000)
|
|
3519
3515
|
});
|
|
3520
3516
|
if (!response.ok)
|
|
3521
3517
|
throw new Error(`PeeringDB answered ${response.status}`);
|
|
3522
3518
|
const existing = (await response.json()).data;
|
|
3523
|
-
const desired = wanted(
|
|
3519
|
+
const desired = wanted(network);
|
|
3524
3520
|
const netId = existing[0]?.net_id;
|
|
3525
|
-
const
|
|
3521
|
+
const changes = [];
|
|
3526
3522
|
for (const record of desired) {
|
|
3527
3523
|
const current = existing.find((candidate) => candidate.ixlan_id === record.ixlan_id);
|
|
3528
3524
|
const body = {
|
|
3529
3525
|
net_id: netId,
|
|
3530
3526
|
ixlan_id: record.ixlan_id,
|
|
3531
|
-
asn:
|
|
3527
|
+
asn: network.asn,
|
|
3532
3528
|
speed: record.speed,
|
|
3533
3529
|
ipaddr4: record.ipaddr4,
|
|
3534
3530
|
ipaddr6: record.ipaddr6,
|
|
@@ -3536,27 +3532,27 @@ async function planPeeringDb(network2) {
|
|
|
3536
3532
|
operational: current?.operational ?? true
|
|
3537
3533
|
};
|
|
3538
3534
|
if (!current) {
|
|
3539
|
-
|
|
3535
|
+
changes.push({ summary: `+ ${record.name}: ${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s`, body });
|
|
3540
3536
|
continue;
|
|
3541
3537
|
}
|
|
3542
3538
|
const differs = current.ipaddr4 !== record.ipaddr4 || current.ipaddr6 !== record.ipaddr6 || current.speed !== record.speed || current.is_rs_peer !== record.is_rs_peer;
|
|
3543
3539
|
if (differs) {
|
|
3544
3540
|
const before = `${current.ipaddr4}, ${current.ipaddr6}, ${current.speed} Mbit/s, route server ${current.is_rs_peer}`;
|
|
3545
3541
|
const after = `${record.ipaddr4}, ${record.ipaddr6}, ${record.speed} Mbit/s, route server ${record.is_rs_peer}`;
|
|
3546
|
-
|
|
3542
|
+
changes.push({ summary: `~ ${record.name}: ${before} to ${after}`, id: current.id, body });
|
|
3547
3543
|
}
|
|
3548
3544
|
}
|
|
3549
3545
|
const unknown = existing.filter((record) => !desired.some((candidate) => candidate.ixlan_id === record.ixlan_id)).map((record) => record.name);
|
|
3550
|
-
if (
|
|
3546
|
+
if (changes.some((change) => !change.id) && !netId) {
|
|
3551
3547
|
throw new Error("PeeringDB holds no presence for this network yet, so its record id is unknown; add the first one by hand");
|
|
3552
3548
|
}
|
|
3553
|
-
return { changes
|
|
3549
|
+
return { changes, unknown };
|
|
3554
3550
|
}
|
|
3555
|
-
async function sendPeeringDb(
|
|
3556
|
-
if (!
|
|
3551
|
+
async function sendPeeringDb(network, changes) {
|
|
3552
|
+
if (!network.peeringdb)
|
|
3557
3553
|
throw new Error("the network declares no peeringdb api_key");
|
|
3558
|
-
const apiKey = resolve(
|
|
3559
|
-
for (const change of
|
|
3554
|
+
const apiKey = resolve(network.peeringdb.api_key);
|
|
3555
|
+
for (const change of changes) {
|
|
3560
3556
|
const response = await fetch(`${API}/netixlan${change.id ? `/${change.id}` : ""}`, {
|
|
3561
3557
|
method: change.id ? "PUT" : "POST",
|
|
3562
3558
|
headers: { Authorization: `Api-Key ${apiKey}`, "Content-Type": "application/json" },
|
|
@@ -3574,13 +3570,13 @@ async function sendPeeringDb(network2, changes2) {
|
|
|
3574
3570
|
// src/adapters/registries/prometheus.ts
|
|
3575
3571
|
var AUTH2 = { sha1: "SHA", sha256: "SHA256" };
|
|
3576
3572
|
var PRIVACY2 = { aes128: "AES", des: "DES" };
|
|
3577
|
-
function prometheusTargets(
|
|
3573
|
+
function prometheusTargets(network) {
|
|
3578
3574
|
return {
|
|
3579
|
-
targets:
|
|
3575
|
+
targets: network.devices.filter((device) => device.management?.snmp).map((device) => ({
|
|
3580
3576
|
targets: [device.connection.host],
|
|
3581
3577
|
labels: { device: device.name, platform: device.platform, model: device.model, __param_auth: device.name }
|
|
3582
3578
|
})),
|
|
3583
|
-
withoutSnmp:
|
|
3579
|
+
withoutSnmp: network.devices.filter((device) => !device.management?.snmp)
|
|
3584
3580
|
};
|
|
3585
3581
|
}
|
|
3586
3582
|
function fromEnvironment(secret, notes, where) {
|
|
@@ -3589,17 +3585,17 @@ function fromEnvironment(secret, notes, where) {
|
|
|
3589
3585
|
notes.push(`${where} is read from a file; write its value in place of FILL_IN`);
|
|
3590
3586
|
return "FILL_IN";
|
|
3591
3587
|
}
|
|
3592
|
-
function snmpExporterAuths(
|
|
3588
|
+
function snmpExporterAuths(network) {
|
|
3593
3589
|
const notes = [];
|
|
3594
3590
|
const lines = [
|
|
3595
3591
|
"# Written by `circuit prometheus --auths`. Load it beside snmp.yml, with --config.expand-environment-variables.",
|
|
3596
3592
|
"auths:"
|
|
3597
3593
|
];
|
|
3598
|
-
for (const device of
|
|
3599
|
-
const
|
|
3600
|
-
if (!
|
|
3594
|
+
for (const device of network.devices) {
|
|
3595
|
+
const snmp = device.management?.snmp;
|
|
3596
|
+
if (!snmp)
|
|
3601
3597
|
continue;
|
|
3602
|
-
const [name, user] = Object.entries(
|
|
3598
|
+
const [name, user] = Object.entries(snmp.users ?? {})[0] ?? [];
|
|
3603
3599
|
lines.push(` ${device.name}:`);
|
|
3604
3600
|
if (name && user) {
|
|
3605
3601
|
lines.push(" version: 3", ` username: ${name}`, " security_level: authPriv", ` password: ${fromEnvironment(user.auth_password, notes, `${device.name}'s SNMP password`)}`, ` auth_protocol: ${AUTH2[user.auth]}`, ` priv_protocol: ${PRIVACY2[user.privacy]}`, ` priv_password: ${fromEnvironment(user.privacy_password, notes, `${device.name}'s SNMP privacy password`)}`);
|
|
@@ -3618,18 +3614,18 @@ import { X509Certificate as X509Certificate2 } from "node:crypto";
|
|
|
3618
3614
|
|
|
3619
3615
|
// src/core/across.ts
|
|
3620
3616
|
function checkAcross(devices) {
|
|
3621
|
-
const
|
|
3617
|
+
const holders = holdersOf(devices);
|
|
3622
3618
|
const owners = new Map;
|
|
3623
|
-
for (const holder of
|
|
3624
|
-
const key = keyOf(
|
|
3619
|
+
for (const holder of holders) {
|
|
3620
|
+
const key = keyOf(host2(holder.address));
|
|
3625
3621
|
if (!owners.has(key))
|
|
3626
3622
|
owners.set(key, holder);
|
|
3627
3623
|
}
|
|
3628
3624
|
return [
|
|
3629
3625
|
...checkLinks(devices),
|
|
3630
|
-
...checkAddresses(
|
|
3626
|
+
...checkAddresses(holders),
|
|
3631
3627
|
...checkSessions(devices, owners),
|
|
3632
|
-
...checkVrrp(
|
|
3628
|
+
...checkVrrp(holders),
|
|
3633
3629
|
...checkTunnels(devices, owners)
|
|
3634
3630
|
];
|
|
3635
3631
|
}
|
|
@@ -3651,7 +3647,7 @@ function carrierOf(device, portName, port) {
|
|
|
3651
3647
|
return { name: port.lag, settings: lag };
|
|
3652
3648
|
return { name: portName, settings: port };
|
|
3653
3649
|
}
|
|
3654
|
-
var plainAddresses = (settings, family) => (settings.addresses ?? []).filter((entry) => typeof entry === "string" &&
|
|
3650
|
+
var plainAddresses = (settings, family) => (settings.addresses ?? []).filter((entry) => typeof entry === "string" && isCidr2(entry) && familyOf2(entry) === family);
|
|
3655
3651
|
function addressDisagreement(here, there, far, farName) {
|
|
3656
3652
|
for (const family of ["ipv4", "ipv6"]) {
|
|
3657
3653
|
const ours = plainAddresses(here, family);
|
|
@@ -3663,7 +3659,7 @@ function addressDisagreement(here, there, far, farName) {
|
|
|
3663
3659
|
return `has ${ours.join(", ")}, and ${far.name} ${farName} has ${theirs.join(", ")}: not one prefix of one length`;
|
|
3664
3660
|
}
|
|
3665
3661
|
}
|
|
3666
|
-
const farHosts = new Set((there.addresses ?? []).map((entry) => keyOf(
|
|
3662
|
+
const farHosts = new Set((there.addresses ?? []).map((entry) => keyOf(host2(typeof entry === "string" ? entry : entry.address))));
|
|
3667
3663
|
for (const entry of here.addresses ?? []) {
|
|
3668
3664
|
if (typeof entry === "string" || !keyOf(entry.peer) || farHosts.has(keyOf(entry.peer)))
|
|
3669
3665
|
continue;
|
|
@@ -3725,12 +3721,12 @@ function checkLinks(devices) {
|
|
|
3725
3721
|
}
|
|
3726
3722
|
return findings;
|
|
3727
3723
|
}
|
|
3728
|
-
function checkAddresses(
|
|
3724
|
+
function checkAddresses(holders) {
|
|
3729
3725
|
const findings = [];
|
|
3730
3726
|
const first = new Map;
|
|
3731
3727
|
const isLoopback = (holder) => holder.device.interfaces?.[holder.name]?.type === "loopback";
|
|
3732
|
-
for (const holder of
|
|
3733
|
-
const key = keyOf(
|
|
3728
|
+
for (const holder of holders) {
|
|
3729
|
+
const key = keyOf(host2(holder.address));
|
|
3734
3730
|
const earlier = first.get(key);
|
|
3735
3731
|
if (!earlier) {
|
|
3736
3732
|
first.set(key, holder);
|
|
@@ -3742,7 +3738,7 @@ function checkAddresses(holders2) {
|
|
|
3742
3738
|
findings.push({
|
|
3743
3739
|
level: anycast ? "warning" : "error",
|
|
3744
3740
|
device: holder.device.name,
|
|
3745
|
-
message: `${
|
|
3741
|
+
message: `${host2(holder.address)} on ${holder.name} is also on ${earlier.device.name} ${earlier.name}${anycast ? ", which is right only for anycast" : ""}`
|
|
3746
3742
|
});
|
|
3747
3743
|
}
|
|
3748
3744
|
return findings;
|
|
@@ -3775,7 +3771,7 @@ function checkSessions(devices, owners) {
|
|
|
3775
3771
|
if (session.remoteAs !== undefined && session.remoteAs !== farBgp.asn) {
|
|
3776
3772
|
report(`expects AS${session.remoteAs}, and ${far.name} is AS${farBgp.asn}`);
|
|
3777
3773
|
}
|
|
3778
|
-
const back = sessionsOf(far).find((other) => owners.get(keyOf(other.address) ?? "")?.device === device &&
|
|
3774
|
+
const back = sessionsOf(far).find((other) => owners.get(keyOf(other.address) ?? "")?.device === device && familyOf2(other.address) === familyOf2(session.address));
|
|
3779
3775
|
if (!back) {
|
|
3780
3776
|
report(`${far.name} has no session back to ${device.name}`);
|
|
3781
3777
|
continue;
|
|
@@ -3787,15 +3783,15 @@ function checkSessions(devices, owners) {
|
|
|
3787
3783
|
}
|
|
3788
3784
|
return findings;
|
|
3789
3785
|
}
|
|
3790
|
-
function checkVrrp(
|
|
3786
|
+
function checkVrrp(holders) {
|
|
3791
3787
|
const findings = [];
|
|
3792
3788
|
const bySegment = new Map;
|
|
3793
|
-
for (const holder of
|
|
3789
|
+
for (const holder of holders) {
|
|
3794
3790
|
const segment = network(holder.address);
|
|
3795
3791
|
for (const group of holder.vrrp) {
|
|
3796
|
-
if (!keyOf(group.address) ||
|
|
3792
|
+
if (!keyOf(group.address) || familyOf2(group.address) !== familyOf2(holder.address))
|
|
3797
3793
|
continue;
|
|
3798
|
-
if (!
|
|
3794
|
+
if (!contains2(segment, group.address))
|
|
3799
3795
|
continue;
|
|
3800
3796
|
bySegment.set(segment, [...bySegment.get(segment) ?? [], { holder, id: group.id, address: group.address }]);
|
|
3801
3797
|
}
|
|
@@ -3852,15 +3848,15 @@ function checkTunnels(devices, owners) {
|
|
|
3852
3848
|
}
|
|
3853
3849
|
|
|
3854
3850
|
// src/core/validate.ts
|
|
3855
|
-
function validate(
|
|
3851
|
+
function validate(network) {
|
|
3856
3852
|
const findings = [];
|
|
3857
3853
|
const seen = new Set;
|
|
3858
|
-
for (const device of
|
|
3854
|
+
for (const device of network.devices) {
|
|
3859
3855
|
if (seen.has(device.name))
|
|
3860
3856
|
findings.push({ level: "error", device: device.name, message: "declared twice" });
|
|
3861
3857
|
seen.add(device.name);
|
|
3862
3858
|
}
|
|
3863
|
-
for (const device of
|
|
3859
|
+
for (const device of network.devices) {
|
|
3864
3860
|
const report = (message) => findings.push({ level: "error", device: device.name, message });
|
|
3865
3861
|
checkDevice(device, report);
|
|
3866
3862
|
const adapter = adapters[device.platform];
|
|
@@ -3870,9 +3866,9 @@ function validate(network2) {
|
|
|
3870
3866
|
for (const field of adapter.unsupported(device))
|
|
3871
3867
|
report(`${device.platform} cannot express ${field}`);
|
|
3872
3868
|
}
|
|
3873
|
-
findings.push(...checkAcross(
|
|
3874
|
-
for (const check of
|
|
3875
|
-
findings.push(...check(
|
|
3869
|
+
findings.push(...checkAcross(network.devices));
|
|
3870
|
+
for (const check of network.checks ?? [])
|
|
3871
|
+
findings.push(...check(network.devices));
|
|
3876
3872
|
return findings;
|
|
3877
3873
|
}
|
|
3878
3874
|
|
|
@@ -3904,13 +3900,13 @@ class Names {
|
|
|
3904
3900
|
}
|
|
3905
3901
|
function checkDevice(device, report) {
|
|
3906
3902
|
const names = new Names(device, report);
|
|
3907
|
-
const cidr = (where,
|
|
3908
|
-
if (!
|
|
3909
|
-
report(`${where}: '${
|
|
3903
|
+
const cidr = (where, value) => {
|
|
3904
|
+
if (!isCidr2(value))
|
|
3905
|
+
report(`${where}: '${value}' is not an address with a length`);
|
|
3910
3906
|
};
|
|
3911
|
-
const ip = (where,
|
|
3912
|
-
if (!
|
|
3913
|
-
report(`${where}: '${
|
|
3907
|
+
const ip = (where, value) => {
|
|
3908
|
+
if (!isAddress2(value))
|
|
3909
|
+
report(`${where}: '${value}' is not an address`);
|
|
3914
3910
|
};
|
|
3915
3911
|
if (!device.users?.[device.connection.user])
|
|
3916
3912
|
report(`connection user '${device.connection.user}' is not one of its users`);
|
|
@@ -3936,11 +3932,11 @@ function checkVlans(device, report) {
|
|
|
3936
3932
|
}
|
|
3937
3933
|
function checkAddresses2(device, report, cidr, ip) {
|
|
3938
3934
|
const holderOf = new Map;
|
|
3939
|
-
const
|
|
3935
|
+
const holders = [
|
|
3940
3936
|
...Object.entries(device.ports ?? {}).map(([name, settings]) => ({ where: `port ${name}`, settings })),
|
|
3941
3937
|
...Object.entries(device.interfaces ?? {}).map(([name, settings]) => ({ where: `interface ${name}`, settings }))
|
|
3942
3938
|
];
|
|
3943
|
-
for (const { where, settings } of
|
|
3939
|
+
for (const { where, settings } of holders) {
|
|
3944
3940
|
for (const entry of settings?.addresses ?? []) {
|
|
3945
3941
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
3946
3942
|
cidr(where, address);
|
|
@@ -4003,7 +3999,7 @@ function checkInterfaces(device, names, report, ip) {
|
|
|
4003
3999
|
if (iface.type === "wireguard") {
|
|
4004
4000
|
for (const peer of iface.peers ?? []) {
|
|
4005
4001
|
for (const address of peer.allowed_addresses) {
|
|
4006
|
-
if (!
|
|
4002
|
+
if (!isCidr2(address))
|
|
4007
4003
|
report(`interface ${name} peer ${peer.name}: '${address}' is not an address with a length`);
|
|
4008
4004
|
}
|
|
4009
4005
|
}
|
|
@@ -4012,7 +4008,7 @@ function checkInterfaces(device, names, report, ip) {
|
|
|
4012
4008
|
}
|
|
4013
4009
|
function checkRouting(device, names, report, cidr, ip) {
|
|
4014
4010
|
const routing = device.routing;
|
|
4015
|
-
const
|
|
4011
|
+
const policies = device.policies ?? {};
|
|
4016
4012
|
for (const route of routing?.static ?? []) {
|
|
4017
4013
|
cidr("static route", route.prefix);
|
|
4018
4014
|
if (!route.via && !route.interface && !route.blackhole)
|
|
@@ -4023,7 +4019,7 @@ function checkRouting(device, names, report, cidr, ip) {
|
|
|
4023
4019
|
names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
|
|
4024
4020
|
}
|
|
4025
4021
|
const bgp = routing?.bgp;
|
|
4026
|
-
const isPolicy = (policy) => policy !== undefined && (policy in
|
|
4022
|
+
const isPolicy = (policy) => policy !== undefined && (policy in policies);
|
|
4027
4023
|
for (const prefix of bgp?.networks ?? [])
|
|
4028
4024
|
cidr("BGP network", prefix);
|
|
4029
4025
|
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
@@ -4041,7 +4037,7 @@ function checkRouting(device, names, report, cidr, ip) {
|
|
|
4041
4037
|
names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
|
|
4042
4038
|
}
|
|
4043
4039
|
const local = neighbor.local_address ?? group?.local_address;
|
|
4044
|
-
if (local &&
|
|
4040
|
+
if (local && familyOf2(local) !== familyOf2(neighbor.address))
|
|
4045
4041
|
report(`neighbor ${name} local_address and address differ in family`);
|
|
4046
4042
|
}
|
|
4047
4043
|
for (const [areaId, area] of Object.entries(routing?.ospf?.areas ?? {})) {
|
|
@@ -4052,11 +4048,11 @@ function checkRouting(device, names, report, cidr, ip) {
|
|
|
4052
4048
|
}
|
|
4053
4049
|
}
|
|
4054
4050
|
function checkPolicies(device, names, report, cidr) {
|
|
4055
|
-
const
|
|
4051
|
+
const policies = device.policies ?? {};
|
|
4056
4052
|
const sets = device.prefix_sets ?? {};
|
|
4057
|
-
for (const [name, rules] of Object.entries(
|
|
4053
|
+
for (const [name, rules] of Object.entries(policies)) {
|
|
4058
4054
|
for (const rule of rules) {
|
|
4059
|
-
names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in
|
|
4055
|
+
names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies);
|
|
4060
4056
|
const set = rule.match?.prefix_set;
|
|
4061
4057
|
names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
|
|
4062
4058
|
if (rule.match?.prefix)
|
|
@@ -4066,11 +4062,11 @@ function checkPolicies(device, names, report, cidr) {
|
|
|
4066
4062
|
const visit = (policy, path) => {
|
|
4067
4063
|
if (path.includes(policy))
|
|
4068
4064
|
return report(`policy ${[...path, policy].join(" -> ")} calls itself`);
|
|
4069
|
-
for (const rule of
|
|
4065
|
+
for (const rule of policies[policy] ?? [])
|
|
4070
4066
|
if (rule.call)
|
|
4071
4067
|
visit(rule.call, [...path, policy]);
|
|
4072
4068
|
};
|
|
4073
|
-
for (const policy of Object.keys(
|
|
4069
|
+
for (const policy of Object.keys(policies))
|
|
4074
4070
|
visit(policy, []);
|
|
4075
4071
|
}
|
|
4076
4072
|
function checkFirewall(device, names, cidr) {
|
|
@@ -4078,7 +4074,7 @@ function checkFirewall(device, names, cidr) {
|
|
|
4078
4074
|
const sets = firewall?.address_sets ?? {};
|
|
4079
4075
|
for (const [name, members] of Object.entries(sets)) {
|
|
4080
4076
|
for (const member of members) {
|
|
4081
|
-
if (!
|
|
4077
|
+
if (!isAddress2(member))
|
|
4082
4078
|
cidr(`address set ${name}`, member);
|
|
4083
4079
|
}
|
|
4084
4080
|
}
|
|
@@ -4104,12 +4100,12 @@ function checkServices(device, names, report, cidr) {
|
|
|
4104
4100
|
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
4105
4101
|
names.expectInterface(`DHCP ${name} interface`, server.interface);
|
|
4106
4102
|
cidr(`DHCP ${name}`, server.network);
|
|
4107
|
-
if (!
|
|
4103
|
+
if (!isCidr2(server.network))
|
|
4108
4104
|
continue;
|
|
4109
4105
|
const reserved = (server.reservations ?? []).map((reservation) => reservation.address);
|
|
4110
|
-
const
|
|
4111
|
-
for (const address of
|
|
4112
|
-
if (!
|
|
4106
|
+
const addresses = [server.gateway, ...server.pool, ...reserved];
|
|
4107
|
+
for (const address of addresses) {
|
|
4108
|
+
if (!contains2(server.network, address))
|
|
4113
4109
|
report(`DHCP ${name}: ${address} is outside ${server.network}`);
|
|
4114
4110
|
}
|
|
4115
4111
|
}
|
|
@@ -4131,8 +4127,8 @@ function checkServices(device, names, report, cidr) {
|
|
|
4131
4127
|
report("SNMP is on with neither a community nor a user");
|
|
4132
4128
|
}
|
|
4133
4129
|
function checkCertificates(device, names, report) {
|
|
4134
|
-
const
|
|
4135
|
-
for (const [name, certificate] of Object.entries(
|
|
4130
|
+
const certificates = device.certificates ?? {};
|
|
4131
|
+
for (const [name, certificate] of Object.entries(certificates)) {
|
|
4136
4132
|
try {
|
|
4137
4133
|
new X509Certificate2(certificate.certificate);
|
|
4138
4134
|
} catch {
|
|
@@ -4141,8 +4137,8 @@ function checkCertificates(device, names, report) {
|
|
|
4141
4137
|
}
|
|
4142
4138
|
for (const service of ["https", "api_tls"]) {
|
|
4143
4139
|
const name = device.management?.[service]?.certificate;
|
|
4144
|
-
names.expect(`${service} certificate`, name, name !== undefined && name in
|
|
4145
|
-
if (name &&
|
|
4140
|
+
names.expect(`${service} certificate`, name, name !== undefined && name in certificates);
|
|
4141
|
+
if (name && certificates[name] && !certificates[name].private_key) {
|
|
4146
4142
|
report(`${service} certificate ${name} has no private key to serve with`);
|
|
4147
4143
|
}
|
|
4148
4144
|
}
|
|
@@ -4162,7 +4158,7 @@ function markOf(graph, node) {
|
|
|
4162
4158
|
}
|
|
4163
4159
|
function mermaid(graph) {
|
|
4164
4160
|
const ids = new Map(graph.nodes.map((node, index) => [node.id, `n${index}`]));
|
|
4165
|
-
const text = (...
|
|
4161
|
+
const text = (...lines) => `"${lines.map((line) => line.replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;")).join("<br/>")}"`;
|
|
4166
4162
|
const shapes = {
|
|
4167
4163
|
device: (label) => `[${label}]`,
|
|
4168
4164
|
segment: (label) => `([${label}])`,
|
|
@@ -4188,7 +4184,7 @@ function mermaid(graph) {
|
|
|
4188
4184
|
`;
|
|
4189
4185
|
}
|
|
4190
4186
|
function dot(graph) {
|
|
4191
|
-
const text = (
|
|
4187
|
+
const text = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
4192
4188
|
const colors = { error: "#d1242f", warning: "#bf8700" };
|
|
4193
4189
|
const shapes = {
|
|
4194
4190
|
device: "shape=box, style=rounded",
|
|
@@ -4226,42 +4222,41 @@ import { basename, dirname, extname, join as join3, resolve as resolve2 } from "
|
|
|
4226
4222
|
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
4227
4223
|
import { join as join2, relative } from "node:path";
|
|
4228
4224
|
var SKIPPED = new Set(["node_modules", ".git", ".circuit", "dist"]);
|
|
4229
|
-
function projectSources(
|
|
4230
|
-
const
|
|
4231
|
-
return
|
|
4225
|
+
function projectSources(root, directory = root) {
|
|
4226
|
+
const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
4227
|
+
return entries.flatMap((entry) => {
|
|
4232
4228
|
if (entry.isDirectory())
|
|
4233
|
-
return SKIPPED.has(entry.name) ? [] : projectSources(
|
|
4234
|
-
return /\.(ts|mts|cts)$/.test(entry.name) ? [relative(
|
|
4229
|
+
return SKIPPED.has(entry.name) ? [] : projectSources(root, join2(directory, entry.name));
|
|
4230
|
+
return /\.(ts|mts|cts)$/.test(entry.name) ? [relative(root, join2(directory, entry.name))] : [];
|
|
4235
4231
|
});
|
|
4236
4232
|
}
|
|
4237
|
-
var
|
|
4233
|
+
var FIRST_NAME = /^[\s\S]*?\bname\s*:\s*(["'`])(.*?)\1/;
|
|
4238
4234
|
function declaresDevice(text, name) {
|
|
4239
|
-
|
|
4240
|
-
return text.includes("defineDevice") && declares.test(text);
|
|
4235
|
+
return [...text.matchAll(/\bdefineDevice\s*\(/g)].some((call) => FIRST_NAME.exec(text.slice(call.index + call[0].length))?.[2] === name);
|
|
4241
4236
|
}
|
|
4242
|
-
function deviceFiles(
|
|
4243
|
-
return sources.filter((file) => declaresDevice(readFileSync2(join2(
|
|
4237
|
+
function deviceFiles(root, name, sources = projectSources(root)) {
|
|
4238
|
+
return sources.filter((file) => declaresDevice(readFileSync2(join2(root, file), "utf8"), name));
|
|
4244
4239
|
}
|
|
4245
4240
|
|
|
4246
4241
|
// src/cli/rename.ts
|
|
4247
|
-
var
|
|
4248
|
-
function planRename(
|
|
4249
|
-
const
|
|
4242
|
+
var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4243
|
+
function planRename(root, from, to, options) {
|
|
4244
|
+
const changes = [];
|
|
4250
4245
|
const moves = [];
|
|
4251
|
-
const exact = new RegExp(`(["'\`])${
|
|
4252
|
-
const anywhere = new RegExp(`(^|[^A-Za-z0-9_.-])${
|
|
4253
|
-
const sources = projectSources(
|
|
4246
|
+
const exact = new RegExp(`(["'\`])${escape(from)}\\1`, "g");
|
|
4247
|
+
const anywhere = new RegExp(`(^|[^A-Za-z0-9_.-])${escape(from)}(?![A-Za-z0-9_-])`);
|
|
4248
|
+
const sources = projectSources(root);
|
|
4254
4249
|
const declaring = new Set;
|
|
4255
4250
|
for (const file of sources) {
|
|
4256
|
-
const text = readFileSync3(join3(
|
|
4251
|
+
const text = readFileSync3(join3(root, file), "utf8");
|
|
4257
4252
|
const lines = text.split(`
|
|
4258
4253
|
`);
|
|
4259
4254
|
if (declaresDevice(text, from))
|
|
4260
4255
|
declaring.add(file);
|
|
4261
4256
|
for (const [index, before] of lines.entries()) {
|
|
4262
|
-
const after = before.replace(exact, (_match,
|
|
4257
|
+
const after = before.replace(exact, (_match, quote) => `${quote}${to}${quote}`);
|
|
4263
4258
|
if (after !== before)
|
|
4264
|
-
|
|
4259
|
+
changes.push({ file, line: index + 1, before, after });
|
|
4265
4260
|
}
|
|
4266
4261
|
}
|
|
4267
4262
|
if (!declaring.size) {
|
|
@@ -4275,58 +4270,58 @@ function planRename(root2, from, to, options) {
|
|
|
4275
4270
|
throw new Error(`${[...declaring].join(" and ")} both declare name: "${from}"; rename the file by hand`);
|
|
4276
4271
|
const [file] = [...declaring];
|
|
4277
4272
|
const target = join3(dirname(file), `${to}${extname(file)}`);
|
|
4278
|
-
if (existsSync2(join3(
|
|
4273
|
+
if (existsSync2(join3(root, target)))
|
|
4279
4274
|
throw new Error(`${target} already exists`);
|
|
4280
4275
|
moves.push({ from: file, to: target });
|
|
4281
|
-
|
|
4276
|
+
changes.push(...importChanges(root, sources, file, target, changes));
|
|
4282
4277
|
}
|
|
4283
|
-
const mentions = sources.flatMap((file) => readFileSync3(join3(
|
|
4278
|
+
const mentions = sources.flatMap((file) => readFileSync3(join3(root, file), "utf8").split(`
|
|
4284
4279
|
`).flatMap((original, index) => {
|
|
4285
|
-
const text =
|
|
4280
|
+
const text = changes.find((change) => change.file === file && change.line === index + 1)?.after ?? original;
|
|
4286
4281
|
return anywhere.test(text) ? [{ file, line: index + 1, text: text.trim() }] : [];
|
|
4287
4282
|
}));
|
|
4288
|
-
return { changes
|
|
4283
|
+
return { changes, moves, mentions };
|
|
4289
4284
|
}
|
|
4290
|
-
function importChanges(
|
|
4285
|
+
function importChanges(root, sources, file, target, earlier) {
|
|
4291
4286
|
const result = [];
|
|
4292
4287
|
const withoutExtension = (path) => path.replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "");
|
|
4293
|
-
const moved = withoutExtension(resolve2(
|
|
4288
|
+
const moved = withoutExtension(resolve2(root, file));
|
|
4294
4289
|
for (const source of sources) {
|
|
4295
|
-
const lines = readFileSync3(join3(
|
|
4290
|
+
const lines = readFileSync3(join3(root, source), "utf8").split(`
|
|
4296
4291
|
`);
|
|
4297
4292
|
for (const [index, original] of lines.entries()) {
|
|
4298
|
-
const
|
|
4299
|
-
const changed = earlier.find((change) => change.file === source && change.line ===
|
|
4293
|
+
const line = index + 1;
|
|
4294
|
+
const changed = earlier.find((change) => change.file === source && change.line === line);
|
|
4300
4295
|
const before = changed?.after ?? original;
|
|
4301
|
-
const after = before.replace(/(\bfrom\s+|\bimport\s*\(\s*)(["'])(\.{1,2}\/[^"']+)\2/g, (match, lead,
|
|
4302
|
-
if (withoutExtension(resolve2(
|
|
4296
|
+
const after = before.replace(/(\bfrom\s+|\bimport\s*\(\s*)(["'])(\.{1,2}\/[^"']+)\2/g, (match, lead, quote, specifier) => {
|
|
4297
|
+
if (withoutExtension(resolve2(root, dirname(source), specifier)) !== moved)
|
|
4303
4298
|
return match;
|
|
4304
4299
|
const extension = specifier.match(/\.(ts|mts|cts|js|mjs|cjs)$/)?.[0] ?? "";
|
|
4305
4300
|
const renamed = `${dirname(specifier)}/${basename(withoutExtension(target))}${extension}`;
|
|
4306
|
-
return `${lead}${
|
|
4301
|
+
return `${lead}${quote}${renamed}${quote}`;
|
|
4307
4302
|
});
|
|
4308
4303
|
if (after === before)
|
|
4309
4304
|
continue;
|
|
4310
4305
|
if (changed)
|
|
4311
4306
|
changed.after = after;
|
|
4312
4307
|
else
|
|
4313
|
-
result.push({ file: source, line
|
|
4308
|
+
result.push({ file: source, line, before: original, after });
|
|
4314
4309
|
}
|
|
4315
4310
|
}
|
|
4316
4311
|
return result;
|
|
4317
4312
|
}
|
|
4318
|
-
function applyRename(
|
|
4319
|
-
const files = new Set(
|
|
4313
|
+
function applyRename(root, plan) {
|
|
4314
|
+
const files = new Set(plan.changes.map((change) => change.file));
|
|
4320
4315
|
for (const file of files) {
|
|
4321
|
-
const lines = readFileSync3(join3(
|
|
4316
|
+
const lines = readFileSync3(join3(root, file), "utf8").split(`
|
|
4322
4317
|
`);
|
|
4323
|
-
for (const change of
|
|
4318
|
+
for (const change of plan.changes.filter((each) => each.file === file))
|
|
4324
4319
|
lines[change.line - 1] = change.after;
|
|
4325
|
-
writeFileSync(join3(
|
|
4320
|
+
writeFileSync(join3(root, file), lines.join(`
|
|
4326
4321
|
`));
|
|
4327
4322
|
}
|
|
4328
|
-
for (const move of
|
|
4329
|
-
renameSync(join3(
|
|
4323
|
+
for (const move of plan.moves)
|
|
4324
|
+
renameSync(join3(root, move.from), join3(root, move.to));
|
|
4330
4325
|
}
|
|
4331
4326
|
|
|
4332
4327
|
// src/cli/index.ts
|
|
@@ -4370,7 +4365,7 @@ var HELP = `usage: circuit <command> [device...] [options]
|
|
|
4370
4365
|
var DEFAULT_CONFIG = "circuit.config.ts";
|
|
4371
4366
|
var [command2, ...args] = process.argv.slice(2);
|
|
4372
4367
|
if (command2 === "new") {
|
|
4373
|
-
const { create } = await import("../chunks/new-
|
|
4368
|
+
const { create } = await import("../chunks/new-ny89s7y7.js");
|
|
4374
4369
|
await create(args);
|
|
4375
4370
|
process.exit(0);
|
|
4376
4371
|
}
|
|
@@ -4445,18 +4440,18 @@ function guard(devices) {
|
|
|
4445
4440
|
fail(`
|
|
4446
4441
|
${errors} error(s).`);
|
|
4447
4442
|
}
|
|
4448
|
-
function show(device,
|
|
4449
|
-
const commands =
|
|
4450
|
-
const summary = commands ? `${commands} command(s) in ${
|
|
4443
|
+
function show(device, plan) {
|
|
4444
|
+
const commands = plan.steps.reduce((count, step) => count + step.send.length, 0);
|
|
4445
|
+
const summary = commands ? `${commands} command(s) in ${plan.steps.length} step(s)` : "matches";
|
|
4451
4446
|
console.log(`${device.name}: ${summary}`);
|
|
4452
|
-
for (const step of
|
|
4447
|
+
for (const step of plan.steps) {
|
|
4453
4448
|
console.log(` ${step.title}`);
|
|
4454
|
-
for (const
|
|
4455
|
-
console.log(` ${
|
|
4449
|
+
for (const line of step.show)
|
|
4450
|
+
console.log(` ${line.slice(0, 220)}`);
|
|
4456
4451
|
}
|
|
4457
|
-
for (const problem of
|
|
4452
|
+
for (const problem of plan.problems)
|
|
4458
4453
|
console.log(` PROBLEM ${problem}`);
|
|
4459
|
-
return commands +
|
|
4454
|
+
return commands + plan.problems.length;
|
|
4460
4455
|
}
|
|
4461
4456
|
function validateCommand() {
|
|
4462
4457
|
guard([...network2.devices]);
|
|
@@ -4477,8 +4472,8 @@ function diffCommand() {
|
|
|
4477
4472
|
continue;
|
|
4478
4473
|
}
|
|
4479
4474
|
const adapter = adapterOf(device);
|
|
4480
|
-
const
|
|
4481
|
-
differences += show(device,
|
|
4475
|
+
const plan = adapter.plan(adapter.render(device), readFileSync4(path, "utf8"), options);
|
|
4476
|
+
differences += show(device, plan);
|
|
4482
4477
|
}
|
|
4483
4478
|
process.exit(differences ? 1 : 0);
|
|
4484
4479
|
}
|
|
@@ -4505,10 +4500,10 @@ async function applyCommand() {
|
|
|
4505
4500
|
const adapter = adapterOf(device);
|
|
4506
4501
|
const live = await adapter.read(device, network2.devices);
|
|
4507
4502
|
writeState(device, live);
|
|
4508
|
-
const
|
|
4509
|
-
if (!show(device,
|
|
4503
|
+
const plan = adapter.plan(adapter.render(device), live, options);
|
|
4504
|
+
if (!show(device, plan))
|
|
4510
4505
|
return;
|
|
4511
|
-
if (
|
|
4506
|
+
if (plan.problems.length)
|
|
4512
4507
|
fail(`
|
|
4513
4508
|
Not sent: resolve the problems first.`);
|
|
4514
4509
|
if (!confirmed) {
|
|
@@ -4516,13 +4511,13 @@ Not sent: resolve the problems first.`);
|
|
|
4516
4511
|
Nothing sent. Re-run with --confirm.`);
|
|
4517
4512
|
return;
|
|
4518
4513
|
}
|
|
4519
|
-
await adapter.apply(device, network2.devices,
|
|
4514
|
+
await adapter.apply(device, network2.devices, plan, options);
|
|
4520
4515
|
writeState(device, await adapter.read(device, network2.devices));
|
|
4521
4516
|
console.log("snapshot updated");
|
|
4522
4517
|
}
|
|
4523
4518
|
async function refreshCommand() {
|
|
4524
4519
|
const device = one();
|
|
4525
|
-
const
|
|
4520
|
+
const fill = adapterOf(device).fill ?? fail(`${device.platform} cannot hold a fetched prefix set`);
|
|
4526
4521
|
for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
|
|
4527
4522
|
if (!("source" in set))
|
|
4528
4523
|
continue;
|
|
@@ -4533,7 +4528,7 @@ async function refreshCommand() {
|
|
|
4533
4528
|
}
|
|
4534
4529
|
console.log(`${name}: ${prefixes.length} prefixes from ${registry} ${query}`);
|
|
4535
4530
|
if (confirmed)
|
|
4536
|
-
await
|
|
4531
|
+
await fill(device, network2.devices, name, family, prefixes);
|
|
4537
4532
|
}
|
|
4538
4533
|
if (!confirmed)
|
|
4539
4534
|
console.log(`
|
|
@@ -4581,13 +4576,13 @@ function communitiesCommand() {
|
|
|
4581
4576
|
}
|
|
4582
4577
|
}
|
|
4583
4578
|
async function peeringdbCommand() {
|
|
4584
|
-
const
|
|
4585
|
-
for (const name of
|
|
4579
|
+
const plan = await planPeeringDb(network2);
|
|
4580
|
+
for (const name of plan.unknown) {
|
|
4586
4581
|
console.log(` PeeringDB lists ${name}, which the config does not. Remove it by hand if the network left.`);
|
|
4587
4582
|
}
|
|
4588
|
-
for (const change of
|
|
4583
|
+
for (const change of plan.changes)
|
|
4589
4584
|
console.log(` ${change.summary}`);
|
|
4590
|
-
if (!
|
|
4585
|
+
if (!plan.changes.length) {
|
|
4591
4586
|
console.log("PeeringDB matches the config.");
|
|
4592
4587
|
return;
|
|
4593
4588
|
}
|
|
@@ -4596,17 +4591,17 @@ async function peeringdbCommand() {
|
|
|
4596
4591
|
Nothing sent. Re-run with --confirm.`);
|
|
4597
4592
|
return;
|
|
4598
4593
|
}
|
|
4599
|
-
await sendPeeringDb(network2,
|
|
4594
|
+
await sendPeeringDb(network2, plan.changes);
|
|
4600
4595
|
}
|
|
4601
4596
|
async function librenmsCommand() {
|
|
4602
|
-
const
|
|
4603
|
-
for (const hostname of
|
|
4597
|
+
const plan = await planLibreNms(network2);
|
|
4598
|
+
for (const hostname of plan.unknown)
|
|
4604
4599
|
console.log(` LibreNMS monitors ${hostname}, which the config does not. Remove it by hand if it is gone.`);
|
|
4605
|
-
for (const device of
|
|
4600
|
+
for (const device of plan.withoutSnmp)
|
|
4606
4601
|
console.log(` ${device.name} has no SNMP in its config, so LibreNMS cannot poll it. Add management.snmp first.`);
|
|
4607
|
-
for (const device of
|
|
4602
|
+
for (const device of plan.additions)
|
|
4608
4603
|
console.log(` add ${device.name}, ${device.connection.host}`);
|
|
4609
|
-
if (!
|
|
4604
|
+
if (!plan.additions.length) {
|
|
4610
4605
|
console.log("LibreNMS monitors every device it can.");
|
|
4611
4606
|
return;
|
|
4612
4607
|
}
|
|
@@ -4615,7 +4610,7 @@ async function librenmsCommand() {
|
|
|
4615
4610
|
Nothing sent. Re-run with --confirm.`);
|
|
4616
4611
|
return;
|
|
4617
4612
|
}
|
|
4618
|
-
await sendLibreNms(network2,
|
|
4613
|
+
await sendLibreNms(network2, plan.additions);
|
|
4619
4614
|
}
|
|
4620
4615
|
function prometheusCommand() {
|
|
4621
4616
|
if (flags.includes("--auths")) {
|
|
@@ -4630,14 +4625,14 @@ function prometheusCommand() {
|
|
|
4630
4625
|
console.error(`${device.name} has no SNMP in its config; left out`);
|
|
4631
4626
|
console.log(JSON.stringify(targets, null, 4));
|
|
4632
4627
|
}
|
|
4633
|
-
function secretsIn(
|
|
4634
|
-
if (isSecret(
|
|
4635
|
-
found.add(referenceOf(
|
|
4636
|
-
else if (Array.isArray(
|
|
4637
|
-
for (const item of
|
|
4628
|
+
function secretsIn(value, found = new Set) {
|
|
4629
|
+
if (isSecret(value))
|
|
4630
|
+
found.add(referenceOf(value));
|
|
4631
|
+
else if (Array.isArray(value))
|
|
4632
|
+
for (const item of value)
|
|
4638
4633
|
secretsIn(item, found);
|
|
4639
|
-
else if (typeof
|
|
4640
|
-
for (const item of Object.values(
|
|
4634
|
+
else if (typeof value === "object" && value !== null)
|
|
4635
|
+
for (const item of Object.values(value))
|
|
4641
4636
|
secretsIn(item, found);
|
|
4642
4637
|
return found;
|
|
4643
4638
|
}
|
|
@@ -4665,10 +4660,10 @@ function driftOf(device) {
|
|
|
4665
4660
|
return { status: "no snapshot", lines: [] };
|
|
4666
4661
|
try {
|
|
4667
4662
|
const adapter = adapterOf(device);
|
|
4668
|
-
const
|
|
4663
|
+
const plan = adapter.plan(adapter.render(device), readFileSync4(path, "utf8"), options);
|
|
4669
4664
|
const lines = [
|
|
4670
|
-
...
|
|
4671
|
-
...
|
|
4665
|
+
...plan.steps.flatMap((step) => [step.title, ...step.show.map((line) => ` ${line}`)]),
|
|
4666
|
+
...plan.problems.map((problem) => `PROBLEM ${problem}`)
|
|
4672
4667
|
];
|
|
4673
4668
|
return { status: lines.length ? "differs" : "matches", lines };
|
|
4674
4669
|
} catch (error) {
|
|
@@ -4711,7 +4706,7 @@ function printSegment(segment, indent) {
|
|
|
4711
4706
|
}
|
|
4712
4707
|
function addressesCommand() {
|
|
4713
4708
|
const [within] = names;
|
|
4714
|
-
if (within && !
|
|
4709
|
+
if (within && !isCidr2(within))
|
|
4715
4710
|
fail(`${within} is not a prefix, such as 10.0.0.0/16`);
|
|
4716
4711
|
const lengthText = flagValue("length");
|
|
4717
4712
|
const length = lengthText === undefined ? undefined : Number(lengthText);
|
|
@@ -4761,23 +4756,23 @@ function renameCommand() {
|
|
|
4761
4756
|
fail(`${to} is not a hostname: letters, digits, dots, dashes and underscores`);
|
|
4762
4757
|
const snapshot = statePath(device);
|
|
4763
4758
|
const snapshots = existsSync3(snapshot) ? [relative2(root2, snapshot)] : [];
|
|
4764
|
-
let
|
|
4759
|
+
let plan;
|
|
4765
4760
|
try {
|
|
4766
|
-
|
|
4761
|
+
plan = planRename(root2, from, to, { file: flags.includes("--file"), snapshots });
|
|
4767
4762
|
} catch (error) {
|
|
4768
4763
|
fail(error.message);
|
|
4769
4764
|
}
|
|
4770
|
-
for (const change of
|
|
4765
|
+
for (const change of plan.changes) {
|
|
4771
4766
|
console.log(`${change.file}:${change.line}`);
|
|
4772
4767
|
console.log(` - ${change.before.trim()}`);
|
|
4773
4768
|
console.log(` + ${change.after.trim()}`);
|
|
4774
4769
|
}
|
|
4775
|
-
for (const move of
|
|
4770
|
+
for (const move of plan.moves)
|
|
4776
4771
|
console.log(`move ${move.from} to ${move.to}`);
|
|
4777
|
-
if (
|
|
4772
|
+
if (plan.mentions.length) {
|
|
4778
4773
|
console.log(`
|
|
4779
4774
|
Still naming ${from}, left as they are: change them by hand if they mean the device.`);
|
|
4780
|
-
for (const mention of
|
|
4775
|
+
for (const mention of plan.mentions)
|
|
4781
4776
|
console.log(` ${mention.file}:${mention.line} ${mention.text.slice(0, 160)}`);
|
|
4782
4777
|
}
|
|
4783
4778
|
if (!confirmed) {
|
|
@@ -4785,7 +4780,7 @@ Still naming ${from}, left as they are: change them by hand if they mean the dev
|
|
|
4785
4780
|
Nothing written. Re-run with --confirm.`);
|
|
4786
4781
|
return;
|
|
4787
4782
|
}
|
|
4788
|
-
applyRename(root2,
|
|
4783
|
+
applyRename(root2, plan);
|
|
4789
4784
|
console.log(`
|
|
4790
4785
|
Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its old hostname until then.`);
|
|
4791
4786
|
}
|
|
@@ -4806,13 +4801,13 @@ async function studioCommand() {
|
|
|
4806
4801
|
const port = Number(flagValue("port") ?? 4600);
|
|
4807
4802
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
4808
4803
|
fail("--port takes a number from 1 to 65535");
|
|
4809
|
-
const { isLoopback, studio } = await import("../chunks/server-
|
|
4810
|
-
const
|
|
4804
|
+
const { isLoopback, studio } = await import("../chunks/server-ckdy25w2.js");
|
|
4805
|
+
const host = flagValue("host") ?? "127.0.0.1";
|
|
4811
4806
|
const allowHosts = (flagValue("allow-host") ?? "").split(",").filter(Boolean);
|
|
4812
|
-
if (!isLoopback(
|
|
4813
|
-
fail(`Studio on ${
|
|
4807
|
+
if (!isLoopback(host) && !allowHosts.length) {
|
|
4808
|
+
fail(`Studio on ${host} needs --allow-host=<name>, the name people reach it by, such as studio.example.com`);
|
|
4814
4809
|
}
|
|
4815
|
-
await studio({ configPath, root: root2, port, open: !flags.includes("--no-open"), host
|
|
4810
|
+
await studio({ configPath, root: root2, port, open: !flags.includes("--no-open"), host, allowHosts });
|
|
4816
4811
|
}
|
|
4817
4812
|
var commands = {
|
|
4818
4813
|
validate: validateCommand,
|