@takodotid/circuit 0.41.0 → 0.42.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/dist/chunks/index-6wb41q67.js +62 -0
- package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
- package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
- package/dist/cli/index.js +121 -47
- package/dist/helpers/index.js +14 -9
- package/dist/types/core/cabling.d.ts +0 -6
- package/dist/types/core/define.d.ts +1 -1
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/cables.d.ts +14 -11
- package/dist/types/helpers/exchange.d.ts +7 -1
- package/dist/types/helpers/upstreams.d.ts +11 -3
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/routing.d.ts +2 -2
- package/dist/types/platforms/junos/bgp.d.ts +2 -2
- package/dist/types/platforms/routeros/render.d.ts +4 -0
- package/dist/types/schema/device.d.ts +4 -4
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/dist/types/schema/routing.d.ts +43 -10
- package/package.json +1 -1
- package/src/cli/index.ts +26 -29
- package/src/core/cabling.ts +0 -30
- package/src/core/define.ts +3 -1
- package/src/core/validator.ts +60 -0
- package/src/helpers/cables.ts +20 -15
- package/src/helpers/exchange.ts +9 -3
- package/src/helpers/upstreams.ts +9 -4
- package/src/platforms/derive.ts +23 -1
- package/src/platforms/fortios/routing.ts +2 -2
- package/src/platforms/junos/bgp.ts +2 -2
- package/src/platforms/routeros/TRAPS.md +4 -0
- package/src/platforms/routeros/export.ts +1 -0
- package/src/platforms/routeros/index.ts +11 -2
- package/src/platforms/routeros/plan.ts +3 -0
- package/src/platforms/routeros/render.ts +28 -3
- package/src/schema/device.ts +4 -3
- package/src/schema/interfaces.ts +1 -1
- package/src/schema/routing.ts +46 -10
- package/templates/colocation/checks.ts +14 -11
- package/templates/colocation/edge-01.ts +2 -2
- package/templates/colocation/tenancy.ts +50 -71
- package/templates/colocation/tenants/acme.ts +10 -10
- package/templates/colocation/tenants/index.ts +2 -1
- package/templates/colocation/tor-01.ts +2 -2
- package/templates/project/AGENTS.md +15 -15
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/platforms/fortios/models.ts
|
|
2
|
+
var models = {
|
|
3
|
+
"FG-3700D": {
|
|
4
|
+
ports: { "40g": 4, "10g": 28, "1g": 2 },
|
|
5
|
+
spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/platforms/junos/models.ts
|
|
10
|
+
var models2 = {
|
|
11
|
+
"QFX5100-48S-6Q": {
|
|
12
|
+
ports: { "40g": 6, "10g": 48, "1g": 2 },
|
|
13
|
+
spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/platforms/raisecom-ros/models.ts
|
|
18
|
+
var models3 = {
|
|
19
|
+
"RAX721-C-6C48": {
|
|
20
|
+
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
21
|
+
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/platforms/routeros/models.ts
|
|
26
|
+
var models4 = {
|
|
27
|
+
"CCR2216-1G-12XS-2XQ": {
|
|
28
|
+
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
29
|
+
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
|
|
30
|
+
limits: { lag_members: 8, switch_port_ranges: 5 }
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/platforms/vrp/models.ts
|
|
35
|
+
var models5 = {
|
|
36
|
+
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/platforms/catalog.ts
|
|
40
|
+
class Catalog {
|
|
41
|
+
static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
|
|
42
|
+
static ports(platform, model) {
|
|
43
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
44
|
+
if (!spec)
|
|
45
|
+
throw new Error(`${platform} has no model ${model}`);
|
|
46
|
+
return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
|
|
47
|
+
}
|
|
48
|
+
static limits(platform, model) {
|
|
49
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
50
|
+
return spec?.limits ?? {};
|
|
51
|
+
}
|
|
52
|
+
static spell(platform, model, port) {
|
|
53
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
54
|
+
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
55
|
+
const template = parts && spec?.spelling[parts[1]];
|
|
56
|
+
if (!template)
|
|
57
|
+
throw new Error(`${model} has no port ${port}`);
|
|
58
|
+
return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Catalog };
|
|
@@ -1,68 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Catalog
|
|
3
|
+
} from "./index-6wb41q67.js";
|
|
1
4
|
import {
|
|
2
5
|
Address2
|
|
3
6
|
} from "./index-n7w3tg5y.js";
|
|
4
7
|
|
|
5
|
-
// src/platforms/fortios/models.ts
|
|
6
|
-
var models = {
|
|
7
|
-
"FG-3700D": {
|
|
8
|
-
ports: { "40g": 4, "10g": 28, "1g": 2 },
|
|
9
|
-
spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// src/platforms/junos/models.ts
|
|
14
|
-
var models2 = {
|
|
15
|
-
"QFX5100-48S-6Q": {
|
|
16
|
-
ports: { "40g": 6, "10g": 48, "1g": 2 },
|
|
17
|
-
spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// src/platforms/raisecom-ros/models.ts
|
|
22
|
-
var models3 = {
|
|
23
|
-
"RAX721-C-6C48": {
|
|
24
|
-
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
25
|
-
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// src/platforms/routeros/models.ts
|
|
30
|
-
var models4 = {
|
|
31
|
-
"CCR2216-1G-12XS-2XQ": {
|
|
32
|
-
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
33
|
-
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
|
|
34
|
-
limits: { lag_members: 8, switch_port_ranges: 5 }
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// src/platforms/vrp/models.ts
|
|
39
|
-
var models5 = {
|
|
40
|
-
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// src/platforms/catalog.ts
|
|
44
|
-
class Catalog {
|
|
45
|
-
static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
|
|
46
|
-
static ports(platform, model) {
|
|
47
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
48
|
-
if (!spec)
|
|
49
|
-
throw new Error(`${platform} has no model ${model}`);
|
|
50
|
-
return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
|
|
51
|
-
}
|
|
52
|
-
static limits(platform, model) {
|
|
53
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
54
|
-
return spec?.limits ?? {};
|
|
55
|
-
}
|
|
56
|
-
static spell(platform, model, port) {
|
|
57
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
58
|
-
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
59
|
-
const template = parts && spec?.spelling[parts[1]];
|
|
60
|
-
if (!template)
|
|
61
|
-
throw new Error(`${model} has no port ${port}`);
|
|
62
|
-
return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
8
|
// src/core/topology.ts
|
|
67
9
|
class Builder {
|
|
68
10
|
nodes = new Map;
|
|
@@ -976,4 +918,4 @@ class StudioModel {
|
|
|
976
918
|
}
|
|
977
919
|
}
|
|
978
920
|
|
|
979
|
-
export {
|
|
921
|
+
export { Topology, AddressSpace, StudioModel };
|
package/dist/cli/index.js
CHANGED
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
Secrets2
|
|
5
5
|
} from "../chunks/index-20kcjxjg.js";
|
|
6
6
|
import {
|
|
7
|
-
Catalog,
|
|
8
7
|
Topology,
|
|
9
8
|
AddressSpace,
|
|
10
9
|
StudioModel
|
|
11
|
-
} from "../chunks/index-
|
|
10
|
+
} from "../chunks/index-yzw86rc8.js";
|
|
11
|
+
import {
|
|
12
|
+
Catalog
|
|
13
|
+
} from "../chunks/index-6wb41q67.js";
|
|
12
14
|
import {
|
|
13
15
|
Address2
|
|
14
16
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -547,9 +549,23 @@ class Derive {
|
|
|
547
549
|
for (const route of routing?.static ?? []) {
|
|
548
550
|
if (route.bfd && !has("static-route-bfd"))
|
|
549
551
|
found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
|
|
552
|
+
if (route.table && !has("routing-tables"))
|
|
553
|
+
found.push(`routing.static ${route.prefix} table, a routing table beside the main one is ${unproved}`);
|
|
554
|
+
}
|
|
555
|
+
if (!has("routing-tables")) {
|
|
556
|
+
if (routing?.tables)
|
|
557
|
+
found.push(`routing.tables, a routing table beside the main one is ${unproved}`);
|
|
558
|
+
if (routing?.rules)
|
|
559
|
+
found.push(`routing.rules, routing rules are ${unproved}`);
|
|
550
560
|
}
|
|
551
561
|
const bgp = routing?.bgp;
|
|
562
|
+
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
563
|
+
if (group.table && !has("routing-tables"))
|
|
564
|
+
found.push(`routing.bgp.groups.${name}.table, a routing table beside the main one is ${unproved}`);
|
|
565
|
+
}
|
|
552
566
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
567
|
+
if (neighbor.table && !has("routing-tables"))
|
|
568
|
+
found.push(`routing.bgp.neighbors.${name}.table, a routing table beside the main one is ${unproved}`);
|
|
553
569
|
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
554
570
|
const bfd = neighbor.bfd ?? group?.bfd;
|
|
555
571
|
const multihop = neighbor.multihop ?? group?.multihop;
|
|
@@ -3969,28 +3985,6 @@ class Cabling {
|
|
|
3969
3985
|
}
|
|
3970
3986
|
return findings;
|
|
3971
3987
|
}
|
|
3972
|
-
static describeLinks(devices) {
|
|
3973
|
-
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
3974
|
-
return devices.map((device) => {
|
|
3975
|
-
const ports = Object.entries(device.ports ?? {});
|
|
3976
|
-
if (!ports.some(([, port]) => port?.link && port.description === undefined))
|
|
3977
|
-
return device;
|
|
3978
|
-
const described = ports.map(([name, port]) => {
|
|
3979
|
-
const far = port?.link ? byName.get(port.link.device) : undefined;
|
|
3980
|
-
if (!port?.link || port.description !== undefined || !far)
|
|
3981
|
-
return [name, port];
|
|
3982
|
-
return [name, { ...port, description: `${far.name} ${Cabling.labelOf(far, port.link.port)}` }];
|
|
3983
|
-
});
|
|
3984
|
-
return { ...device, ports: Object.fromEntries(described) };
|
|
3985
|
-
});
|
|
3986
|
-
}
|
|
3987
|
-
static labelOf(device, port) {
|
|
3988
|
-
try {
|
|
3989
|
-
return Catalog.spell(device.platform, device.model, port);
|
|
3990
|
-
} catch {
|
|
3991
|
-
return port;
|
|
3992
|
-
}
|
|
3993
|
-
}
|
|
3994
3988
|
static isDevice(system, name) {
|
|
3995
3989
|
return system === name || system.startsWith(`${name}.`);
|
|
3996
3990
|
}
|
|
@@ -6954,6 +6948,7 @@ class RouterOsExport {
|
|
|
6954
6948
|
};
|
|
6955
6949
|
static ORDERED = new Set([
|
|
6956
6950
|
"/routing filter rule",
|
|
6951
|
+
"/routing rule",
|
|
6957
6952
|
"/ip firewall filter",
|
|
6958
6953
|
"/ipv6 firewall filter",
|
|
6959
6954
|
"/ip firewall nat",
|
|
@@ -7135,6 +7130,8 @@ class RouterOsPlan {
|
|
|
7135
7130
|
"/interface bridge filter",
|
|
7136
7131
|
"/interface list",
|
|
7137
7132
|
"/ip vrf",
|
|
7133
|
+
"/routing table",
|
|
7134
|
+
"/routing rule",
|
|
7138
7135
|
"/ip address",
|
|
7139
7136
|
"/ipv6 address",
|
|
7140
7137
|
"/ip pool",
|
|
@@ -8297,6 +8294,8 @@ class RouterOsRender {
|
|
|
8297
8294
|
return rules;
|
|
8298
8295
|
}
|
|
8299
8296
|
routing() {
|
|
8297
|
+
this.routingTables();
|
|
8298
|
+
this.routingRules();
|
|
8300
8299
|
this.staticRoutes();
|
|
8301
8300
|
this.rpki();
|
|
8302
8301
|
if (this.device.routing?.bgp) {
|
|
@@ -8307,6 +8306,24 @@ class RouterOsRender {
|
|
|
8307
8306
|
this.ospf();
|
|
8308
8307
|
this.bfd();
|
|
8309
8308
|
}
|
|
8309
|
+
routingTables() {
|
|
8310
|
+
for (const [name, table] of Object.entries(this.device.routing?.tables ?? {})) {
|
|
8311
|
+
this.add("/routing table", "add fib", { comment: table.description, name });
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
routingRules() {
|
|
8315
|
+
for (const rule of this.device.routing?.rules ?? []) {
|
|
8316
|
+
const onlyInTable = rule.table !== "main" && (rule.fallback ?? "none") === "none";
|
|
8317
|
+
this.add("/routing rule", "add", {
|
|
8318
|
+
action: onlyInTable ? "lookup-only-in-table" : "lookup",
|
|
8319
|
+
comment: rule.description,
|
|
8320
|
+
"dst-address": rule.match?.dst,
|
|
8321
|
+
interface: rule.match?.in_interface && this.name(rule.match.in_interface),
|
|
8322
|
+
"src-address": rule.match?.src,
|
|
8323
|
+
table: rule.table
|
|
8324
|
+
});
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8310
8327
|
staticRoutes() {
|
|
8311
8328
|
for (const route of this.device.routing?.static ?? []) {
|
|
8312
8329
|
const menu = Address2.family(Address2.host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
@@ -8317,7 +8334,7 @@ class RouterOsRender {
|
|
|
8317
8334
|
distance: route.distance,
|
|
8318
8335
|
"dst-address": route.prefix,
|
|
8319
8336
|
gateway,
|
|
8320
|
-
"routing-table": route.vrf
|
|
8337
|
+
"routing-table": route.vrf ?? route.table
|
|
8321
8338
|
});
|
|
8322
8339
|
}
|
|
8323
8340
|
}
|
|
@@ -8438,6 +8455,7 @@ class RouterOsRender {
|
|
|
8438
8455
|
"output.network": originates ? RouterOsRender.NETWORKS[family] : undefined,
|
|
8439
8456
|
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
8440
8457
|
"remote.as": settings.remote_as,
|
|
8458
|
+
"routing-table": settings.table,
|
|
8441
8459
|
"tcp-md5-key": settings.password && Secrets2.marker(settings.password),
|
|
8442
8460
|
"use-bfd": settings.bfd ? "yes" : undefined
|
|
8443
8461
|
});
|
|
@@ -8761,7 +8779,14 @@ class RouterOs extends Platform {
|
|
|
8761
8779
|
if (route.bfd)
|
|
8762
8780
|
found.push(`routing.static ${route.prefix} bfd, a static route's check-gateway takes arp, none or ping on 7.23`);
|
|
8763
8781
|
}
|
|
8764
|
-
found.push(...Derive.unproved(device, [
|
|
8782
|
+
found.push(...Derive.unproved(device, [
|
|
8783
|
+
"bfd-timers",
|
|
8784
|
+
"multihop-bfd",
|
|
8785
|
+
"static-route-bfd",
|
|
8786
|
+
"mac-limit",
|
|
8787
|
+
"ethertype-filter",
|
|
8788
|
+
"routing-tables"
|
|
8789
|
+
]));
|
|
8765
8790
|
return found;
|
|
8766
8791
|
}
|
|
8767
8792
|
render(device) {
|
|
@@ -9003,7 +9028,7 @@ ${certificates}` : "";
|
|
|
9003
9028
|
console.log(` BGP: all ${neighbors.length} established again`);
|
|
9004
9029
|
return [];
|
|
9005
9030
|
}
|
|
9006
|
-
return down.map((bgp) => bgp.limit ? `BGP session ${bgp.neighbor} was established before the apply and closed on its limit${bgp.limit.max !== undefined ? ` of ${bgp.limit.max} prefixes` : ""}: the neighbor sends more than it may. Find out why, then: circuit bgp ${device.name}
|
|
9031
|
+
return down.map((bgp) => bgp.limit ? `BGP session ${bgp.neighbor} was established before the apply and closed on its limit${bgp.limit.max !== undefined ? ` of ${bgp.limit.max} prefixes` : ""}: the neighbor sends more than it may. Find out why, then: circuit bgp ${device.name} --neighbors '^${bgp.neighbor}$' --clear-limit` : `BGP session ${bgp.neighbor} was established before the apply and is not ${limits.seconds} s after it (${bgp.reason ?? (bgp.uptime === "no session" ? "no session" : "not established")}). Look with: circuit bgp ${device.name} --log`);
|
|
9007
9032
|
}
|
|
9008
9033
|
static async invalidSwitchRules(session, device) {
|
|
9009
9034
|
const answer = (await session.run(RouterOs.COUNT_INVALID_SWITCH_RULES)).trim();
|
|
@@ -11433,15 +11458,21 @@ class Validator {
|
|
|
11433
11458
|
report(`static route ${route.prefix} has bfd without via, the address BFD watches`);
|
|
11434
11459
|
names.expectInterface(`static route ${route.prefix} interface`, route.interface);
|
|
11435
11460
|
names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
|
|
11461
|
+
names.expect(`static route ${route.prefix} table`, route.table, route.table !== undefined && route.table in (routing?.tables ?? {}));
|
|
11462
|
+
if (route.vrf && route.table)
|
|
11463
|
+
report(`static route ${route.prefix} has both vrf and table; a route is in one of them`);
|
|
11436
11464
|
}
|
|
11465
|
+
Validator.checkTables(device, names, report, cidr);
|
|
11437
11466
|
const bgp = routing?.bgp;
|
|
11438
11467
|
const isPolicy = (policy) => policy !== undefined && (policy in policies);
|
|
11439
11468
|
for (const prefix of bgp?.networks ?? [])
|
|
11440
11469
|
cidr("BGP network", prefix);
|
|
11470
|
+
const isTable = (table) => table !== undefined && (table in (routing?.tables ?? {}));
|
|
11441
11471
|
for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
|
|
11442
11472
|
for (const policy of [group.import, group.export]) {
|
|
11443
11473
|
names.expect(`BGP group ${name} policy`, policy, isPolicy(policy));
|
|
11444
11474
|
}
|
|
11475
|
+
names.expect(`BGP group ${name} table`, group.table, isTable(group.table));
|
|
11445
11476
|
}
|
|
11446
11477
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
11447
11478
|
ip(`neighbor ${name}`, neighbor.address);
|
|
@@ -11452,6 +11483,7 @@ class Validator {
|
|
|
11452
11483
|
for (const policy of [neighbor.import, neighbor.export]) {
|
|
11453
11484
|
names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
|
|
11454
11485
|
}
|
|
11486
|
+
names.expect(`neighbor ${name} table`, neighbor.table, isTable(neighbor.table));
|
|
11455
11487
|
const local = neighbor.local_address ?? group?.local_address;
|
|
11456
11488
|
if (local && Address2.family(local) !== Address2.family(neighbor.address))
|
|
11457
11489
|
report(`neighbor ${name} local_address and address differ in family`);
|
|
@@ -11463,6 +11495,48 @@ class Validator {
|
|
|
11463
11495
|
names.expectInterface(`OSPF area ${areaId} interface`, iface);
|
|
11464
11496
|
}
|
|
11465
11497
|
}
|
|
11498
|
+
static checkTables(device, names, report, cidr) {
|
|
11499
|
+
const routing = device.routing;
|
|
11500
|
+
const tables = routing?.tables ?? {};
|
|
11501
|
+
const bgp = routing?.bgp;
|
|
11502
|
+
const installed = new Set;
|
|
11503
|
+
for (const route of routing?.static ?? [])
|
|
11504
|
+
if (route.table)
|
|
11505
|
+
installed.add(route.table);
|
|
11506
|
+
for (const neighbor of Object.values(bgp?.neighbors ?? {})) {
|
|
11507
|
+
const table = neighbor.table ?? (neighbor.group ? bgp?.groups?.[neighbor.group]?.table : undefined);
|
|
11508
|
+
if (table)
|
|
11509
|
+
installed.add(table);
|
|
11510
|
+
}
|
|
11511
|
+
for (const name of Object.keys(tables)) {
|
|
11512
|
+
if (name === "main")
|
|
11513
|
+
report("routing.tables has main, the main table every device has; give the table another name");
|
|
11514
|
+
if (name in (device.vrfs ?? {}))
|
|
11515
|
+
report(`routing.tables ${name} has the name of a VRF; give the table another name`);
|
|
11516
|
+
if (name !== "main" && !installed.has(name))
|
|
11517
|
+
report(`routing.tables ${name}: nothing installs routes in it; give a static route or a BGP neighbor table: "${name}", or remove the table`);
|
|
11518
|
+
}
|
|
11519
|
+
const rules = routing?.rules ?? [];
|
|
11520
|
+
rules.forEach((rule, index) => {
|
|
11521
|
+
const where = `routing.rules ${index + 1}${rule.description ? ` (${rule.description})` : ""}`;
|
|
11522
|
+
const match = rule.match ?? {};
|
|
11523
|
+
names.expect(`${where} table`, rule.table, rule.table === "main" || rule.table in tables);
|
|
11524
|
+
names.expectInterface(`${where} in_interface`, match.in_interface);
|
|
11525
|
+
if (match.src)
|
|
11526
|
+
cidr(where, match.src);
|
|
11527
|
+
if (match.dst)
|
|
11528
|
+
cidr(where, match.dst);
|
|
11529
|
+
if (match.src && match.dst && Address2.isCidr(match.src) && Address2.isCidr(match.dst)) {
|
|
11530
|
+
if (Address2.family(Address2.host(match.src)) !== Address2.family(Address2.host(match.dst)))
|
|
11531
|
+
report(`${where}: src and dst differ in family, so no packet matches both`);
|
|
11532
|
+
}
|
|
11533
|
+
if (rule.table === "main" && rule.fallback)
|
|
11534
|
+
report(`${where} looks up main and has fallback; fallback is for another table, so remove it`);
|
|
11535
|
+
const matchesAll = !match.src && !match.dst && !match.in_interface;
|
|
11536
|
+
if (matchesAll && index < rules.length - 1)
|
|
11537
|
+
report(`${where} matches every packet, so the rules after it never run; give it a match, or move it last`);
|
|
11538
|
+
});
|
|
11539
|
+
}
|
|
11466
11540
|
static checkPolicies(device, names, report, cidr) {
|
|
11467
11541
|
const policies = device.policies ?? {};
|
|
11468
11542
|
const sets = device.prefix_sets ?? {};
|
|
@@ -11865,14 +11939,10 @@ class Cli {
|
|
|
11865
11939
|
return new Cli(configPath, network, options);
|
|
11866
11940
|
}
|
|
11867
11941
|
static async load(path) {
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
const { createJiti } = await import("jiti");
|
|
11873
|
-
network = await createJiti(import.meta.url).import(path, { default: true });
|
|
11874
|
-
}
|
|
11875
|
-
return { ...network, devices: Cabling.describeLinks(network.devices) };
|
|
11942
|
+
if (process.versions.bun)
|
|
11943
|
+
return (await import(path)).default;
|
|
11944
|
+
const { createJiti } = await import("jiti");
|
|
11945
|
+
return createJiti(import.meta.url).import(path, { default: true });
|
|
11876
11946
|
}
|
|
11877
11947
|
static fail(message) {
|
|
11878
11948
|
console.error(message);
|
|
@@ -11916,7 +11986,12 @@ class Cli {
|
|
|
11916
11986
|
program.command("snapshot").description("read what each device runs into the state directory").argument("[device...]", "every device when none is named").option("--console", "through the device's serial console on its jump host, instead of SSH").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (devices, flags) => (await Cli.open(flags.config, flags)).snapshot(devices));
|
|
11917
11987
|
program.command("apply").description("read the device, plan against what it runs, and send with --confirm").argument("<device>").option("--confirm", "send the plan; without it, nothing is sent").option("--secrets", "also send every secret, to rotate one").option("--rollback <minutes>", "minutes before a device that can restore itself does", Cli.minutes, 10).option("--console", "through the device's serial console on its jump host, instead of SSH").option("--force", "send even when the pings before --confirm find a path that loses too much or is too slow").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, flags) => (await Cli.open(flags.config, flags)).apply(device, flags.confirm ?? false, flags.force ?? false));
|
|
11918
11988
|
program.command("show").description("run commands that only read on a device and print what it answers, secrets hidden").argument("<device>").argument("[command...]", "each command in quotes, such as '/ip route print'").option("--file <path>", "also each line of this file; blank lines and lines starting with # are skipped").option("--timeout <seconds>", "how long each command may take", Cli.seconds, 90).option("--console", "through the device's serial console, instead of SSH").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, commands, flags) => (await Cli.open(flags.config, flags)).show(device, commands, flags.file, flags.timeout));
|
|
11919
|
-
program.command("bgp").description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime").argument("[device]", "may be left out when only one device has BGP neighbors").
|
|
11989
|
+
program.command("bgp").description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime").argument("[device]", "may be left out when only one device has BGP neighbors").allowExcessArguments().option("--neighbors <regex>", "only the neighbors whose name matches").option("--wait [count]", `read again every ${BgpWait.COMMAND.every} seconds until every neighbor shown, or this many, is up`, Cli.count).option("--max <seconds>", "how long --wait waits at most", Cli.seconds, BgpWait.COMMAND.seconds).option("--log [minutes]", "also print the device's BGP log lines from the last minutes, 5 by default", Cli.minutes).option("--clear-limit", "open again the neighbors shown that closed on their prefix limit; any other is left alone").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, flags, command) => {
|
|
11990
|
+
const [, filter] = command.args;
|
|
11991
|
+
if (filter !== undefined)
|
|
11992
|
+
Cli.fail(`circuit bgp takes one device; filter the neighbors with --neighbors: circuit bgp ${device} --neighbors '${filter}'`);
|
|
11993
|
+
(await Cli.open(flags.config)).bgp(device, flags);
|
|
11994
|
+
});
|
|
11920
11995
|
program.command("console").description("open a device's serial console here, logged in; or send it lines and print what it answers").argument("<device>").argument("[line...]", "lines to send, each in quotes; none joins the console to this terminal, Ctrl-] leaves").option("--raw", "do not log in or out: only join the console, or send the lines").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, lines, flags) => (await Cli.open(flags.config)).console(device, lines, flags.raw ?? false));
|
|
11921
11996
|
program.command("refresh").description("fetch prefix sets that come from a registry onto the device, with --confirm").argument("<device>").option("--confirm", "put them on the device; without it, nothing is sent").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, flags) => (await Cli.open(flags.config)).refresh(device, flags.confirm ?? false));
|
|
11922
11997
|
program.command("wireguard").description("a client config for one peer, its private key left for the peer to fill").argument("<device>").argument("<interface>").argument("<peer>").option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP).action(async (device, iface, peer, flags) => (await Cli.open(flags.config)).wireguard(device, iface, peer));
|
|
@@ -12138,8 +12213,9 @@ ${output.replace(/\r/g, "").trimEnd()}${stoppedAtLimit}
|
|
|
12138
12213
|
if (failed)
|
|
12139
12214
|
process.exitCode = 1;
|
|
12140
12215
|
}
|
|
12141
|
-
async bgp(
|
|
12142
|
-
const
|
|
12216
|
+
async bgp(deviceName, flags) {
|
|
12217
|
+
const device = this.bgpDevice(deviceName);
|
|
12218
|
+
const pattern = flags.neighbors;
|
|
12143
12219
|
const adapter = this.adapterOf(device);
|
|
12144
12220
|
const supported = Platforms.all().filter((platform) => platform.bgpSessions).map((platform) => platform.name).join(", ");
|
|
12145
12221
|
if (!adapter.bgpSessions)
|
|
@@ -12199,17 +12275,15 @@ no neighbor shown is closed on its prefix limit, so nothing was cleared`);
|
|
|
12199
12275
|
`);
|
|
12200
12276
|
Cli.printBgp(await read());
|
|
12201
12277
|
}
|
|
12202
|
-
|
|
12203
|
-
if (
|
|
12204
|
-
return
|
|
12205
|
-
}
|
|
12278
|
+
bgpDevice(name) {
|
|
12279
|
+
if (name !== undefined)
|
|
12280
|
+
return this.one(name);
|
|
12206
12281
|
const withBgp = this.network.devices.filter((device) => Object.keys(device.routing?.bgp?.neighbors ?? {}).length);
|
|
12207
|
-
const named = first ? `no device ${first}, and ` : "";
|
|
12208
12282
|
if (withBgp.length === 1)
|
|
12209
|
-
return
|
|
12283
|
+
return withBgp[0];
|
|
12210
12284
|
if (!withBgp.length)
|
|
12211
|
-
Cli.fail(
|
|
12212
|
-
return Cli.fail(
|
|
12285
|
+
Cli.fail("no device has BGP neighbors");
|
|
12286
|
+
return Cli.fail(`several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
|
|
12213
12287
|
}
|
|
12214
12288
|
static printBgp(sessions) {
|
|
12215
12289
|
const width = Math.max(...sessions.map((session) => session.neighbor.length));
|
|
@@ -12677,7 +12751,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
|
|
|
12677
12751
|
const port = Number(flags.port);
|
|
12678
12752
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
12679
12753
|
Cli.fail("--port takes a number from 1 to 65535");
|
|
12680
|
-
const { StudioServer } = await import("../chunks/server-
|
|
12754
|
+
const { StudioServer } = await import("../chunks/server-cxr9ynxc.js");
|
|
12681
12755
|
const host = flags.host;
|
|
12682
12756
|
const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
|
|
12683
12757
|
if (!StudioServer.isLoopback(host) && !allowHosts.length) {
|
package/dist/helpers/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Catalog
|
|
3
|
+
} from "../chunks/index-6wb41q67.js";
|
|
1
4
|
import {
|
|
2
5
|
Address2
|
|
3
6
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -238,8 +241,8 @@ class Bgp {
|
|
|
238
241
|
class Cables {
|
|
239
242
|
static link(spec) {
|
|
240
243
|
return {
|
|
241
|
-
a: { [spec.a.port]: Cables.end(spec, spec.b
|
|
242
|
-
b: { [spec.b.port]: Cables.end(spec, spec.a
|
|
244
|
+
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
|
|
245
|
+
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
|
|
243
246
|
};
|
|
244
247
|
}
|
|
245
248
|
static lag(spec) {
|
|
@@ -251,17 +254,18 @@ class Cables {
|
|
|
251
254
|
throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
|
|
252
255
|
taken.set(far, near);
|
|
253
256
|
}
|
|
254
|
-
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b
|
|
255
|
-
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a
|
|
257
|
+
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
258
|
+
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
256
259
|
return {
|
|
257
260
|
a,
|
|
258
261
|
b
|
|
259
262
|
};
|
|
260
263
|
}
|
|
261
|
-
static end(shared,
|
|
264
|
+
static end(shared, far, port) {
|
|
265
|
+
const label = far.platform && far.model ? Catalog.spell(far.platform, far.model, port) : undefined;
|
|
262
266
|
return {
|
|
263
|
-
link: { device, port, ...shared.through ? { through: shared.through } : {} },
|
|
264
|
-
...shared.describe ? { description: shared.describe({ device, port }) } : {}
|
|
267
|
+
link: { device: far.device, port, ...shared.through ? { through: shared.through } : {} },
|
|
268
|
+
...shared.describe ? { description: shared.describe({ device: far.device, port, ...label ? { label } : {} }) } : {}
|
|
265
269
|
};
|
|
266
270
|
}
|
|
267
271
|
}
|
|
@@ -560,8 +564,8 @@ class Exchange {
|
|
|
560
564
|
result[port] = {
|
|
561
565
|
description: `${member.name}, AS${member.asn}`,
|
|
562
566
|
access_vlan: exchange.vlan,
|
|
563
|
-
stp: false,
|
|
564
|
-
lldp: false,
|
|
567
|
+
stp: exchange.stp ?? false,
|
|
568
|
+
lldp: exchange.lldp ?? false,
|
|
565
569
|
storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
|
|
566
570
|
...exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {},
|
|
567
571
|
...exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}
|
|
@@ -767,6 +771,7 @@ class Upstreams {
|
|
|
767
771
|
role: upstream.role,
|
|
768
772
|
...upstream.import ? { import: upstream.import.name } : {},
|
|
769
773
|
export: upstream.export.name,
|
|
774
|
+
...upstream.table ? { table: upstream.table } : {},
|
|
770
775
|
...multihop ? {
|
|
771
776
|
local_address: multihop.local_address,
|
|
772
777
|
multihop: true,
|
|
@@ -31,12 +31,6 @@ export declare class Cabling {
|
|
|
31
31
|
* Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. A link `through` a carrier is not compared. LLDP can be off at the far end, so each is a warning.
|
|
32
32
|
*/
|
|
33
33
|
static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[];
|
|
34
|
-
/**
|
|
35
|
-
* Each linked port without a description gets one from its far end: the far device and the far port as printed on it, such as `firewall-1 port6`. The far device's own file has its platform and model, so the label is never written twice. A port whose far device is not in the network keeps none.
|
|
36
|
-
*/
|
|
37
|
-
static describeLinks(devices: readonly Device[]): Device[];
|
|
38
|
-
/** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
|
|
39
|
-
private static labelOf;
|
|
40
34
|
/** A system name is the device's name, alone or with a domain after it. */
|
|
41
35
|
private static isDevice;
|
|
42
36
|
private static deviceOf;
|
|
@@ -9,7 +9,7 @@ export declare const secretFile: (path: string) => Secret;
|
|
|
9
9
|
*
|
|
10
10
|
* The names it declares (VLANs, interfaces, policies and the rest) are the only names its other fields accept, and the editor suggests them. The port names come from the model.
|
|
11
11
|
*/
|
|
12
|
-
export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates>): Device;
|
|
12
|
+
export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never, const Tables extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates, Tables>): Device;
|
|
13
13
|
/** Something a check found. */
|
|
14
14
|
export type Finding = {
|
|
15
15
|
/** An error stops `diff` and `apply`; a warning is reported only. */
|
|
@@ -14,6 +14,8 @@ export declare class Validator {
|
|
|
14
14
|
private static checkFrames;
|
|
15
15
|
private static checkInterfaces;
|
|
16
16
|
private static checkRouting;
|
|
17
|
+
/** Routing tables have names of their own and something installing routes in them, and rules name a table and can each be reached. */
|
|
18
|
+
private static checkTables;
|
|
17
19
|
private static checkPolicies;
|
|
18
20
|
private static checkFirewall;
|
|
19
21
|
private static checkServices;
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
/** The far end of a port, for a description of your own: the device
|
|
1
|
+
/** The far end of a port, for a description of your own: the device, its port by Circuit's name, and the port as printed on the device when that end names its platform and model. */
|
|
2
2
|
export type FarEnd = {
|
|
3
3
|
device: string;
|
|
4
4
|
port: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
/** One end of a cable entry: the device, and its platform and model if a description should name its ports as printed on it. */
|
|
8
|
+
type End = {
|
|
9
|
+
device: string;
|
|
10
|
+
platform?: string;
|
|
11
|
+
model?: string;
|
|
5
12
|
};
|
|
6
13
|
/** What every cable of one entry shares. */
|
|
7
14
|
type Shared = {
|
|
8
15
|
/** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
|
|
9
16
|
through?: string;
|
|
10
|
-
/** A port's description from its far end
|
|
17
|
+
/** A port's description from its far end, such as `(far) => \`${far.device} ${far.label}\``. No description when absent. */
|
|
11
18
|
describe?: (far: FarEnd) => string;
|
|
12
19
|
};
|
|
13
20
|
/** A port a cable builds: the other end, the LAG it is in, and a description when `describe` gives one. */
|
|
@@ -31,12 +38,10 @@ export declare class Cables {
|
|
|
31
38
|
* ```
|
|
32
39
|
*/
|
|
33
40
|
static link<const NA extends string, const NB extends string>(spec: Shared & {
|
|
34
|
-
a: {
|
|
35
|
-
device: string;
|
|
41
|
+
a: End & {
|
|
36
42
|
port: NA;
|
|
37
43
|
};
|
|
38
|
-
b: {
|
|
39
|
-
device: string;
|
|
44
|
+
b: End & {
|
|
40
45
|
port: NB;
|
|
41
46
|
};
|
|
42
47
|
}): {
|
|
@@ -47,12 +52,10 @@ export declare class Cables {
|
|
|
47
52
|
* The cables of a LAG, both ends, from one list read from end A: `{ "25g-1": "10g-2" }` is a cable from A's 25g-1 to B's 10g-2. Each port is a member of its end's LAG. Spread each end into its device's `ports`; the LAG itself stays on each device.
|
|
48
53
|
*/
|
|
49
54
|
static lag<const LA extends string, const LB extends string, const C extends Readonly<Record<string, string>>>(spec: Shared & {
|
|
50
|
-
a: {
|
|
51
|
-
device: string;
|
|
55
|
+
a: End & {
|
|
52
56
|
lag: LA;
|
|
53
57
|
};
|
|
54
|
-
b: {
|
|
55
|
-
device: string;
|
|
58
|
+
b: End & {
|
|
56
59
|
lag: LB;
|
|
57
60
|
};
|
|
58
61
|
cables: C;
|
|
@@ -60,7 +63,7 @@ export declare class Cables {
|
|
|
60
63
|
a: Record<keyof C & string, CablePort<LA>>;
|
|
61
64
|
b: Record<C[keyof C], CablePort<LB>>;
|
|
62
65
|
};
|
|
63
|
-
/** A port facing `port` on the far
|
|
66
|
+
/** A port facing `port` on the far end. */
|
|
64
67
|
private static end;
|
|
65
68
|
}
|
|
66
69
|
export {};
|
|
@@ -29,13 +29,19 @@ export type InternetExchange<V extends string> = {
|
|
|
29
29
|
mac_limit?: number;
|
|
30
30
|
/** The only kinds of frame a member port takes in, usually `ipv4`, `ipv6` and `arp`. Every kind when absent. No platform renders it yet, so `validate` refuses it. */
|
|
31
31
|
ethertypes?: readonly Ethertype[];
|
|
32
|
+
/** LLDP on member ports. Off when absent: a member's router has no business learning the fabric. */
|
|
33
|
+
lldp?: boolean;
|
|
34
|
+
/** Spanning tree on member ports. Off when absent: a member's port faces a router, which must not join the fabric's spanning tree. */
|
|
35
|
+
stp?: boolean;
|
|
32
36
|
};
|
|
33
37
|
/** An internet exchange's fabric: its member ports, a check of its members, and its route servers' config. */
|
|
34
38
|
export declare class Exchange {
|
|
35
39
|
/**
|
|
36
40
|
* Member ports and a check for an internet exchange's fabric.
|
|
37
41
|
*
|
|
38
|
-
* - `ports(device)`: every member's port on one switch
|
|
42
|
+
* - `ports(device)`: every member's port on one switch, each written as
|
|
43
|
+
* `{ description: "<name>, AS<asn>", access_vlan: vlan, stp: false, lldp: false, storm_control: { broadcast, multicast, unknown_unicast: { percent: storm_percent } } }`,
|
|
44
|
+
* plus `mac_limit` and `ethertypes` when the exchange sets them. `stp` and `lldp` are the exchange's own when it sets them, and storm control is 1 % when `storm_percent` is absent.
|
|
39
45
|
* - `check`: members never share a name, an AS, an address or a port; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
|
|
40
46
|
* - `bird(server)`: one route server's BIRD 2 config, with a session to every member. Write it to a file for the server; Circuit never logs in to a route server.
|
|
41
47
|
* - `openbgpd(server)`: the same route server's config for OpenBGPD, its `bgpd.conf`, with the same sessions and the same rules.
|