@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.
Files changed (44) hide show
  1. package/dist/chunks/index-6wb41q67.js +62 -0
  2. package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
  3. package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
  4. package/dist/cli/index.js +121 -47
  5. package/dist/helpers/index.js +14 -9
  6. package/dist/types/core/cabling.d.ts +0 -6
  7. package/dist/types/core/define.d.ts +1 -1
  8. package/dist/types/core/validator.d.ts +2 -0
  9. package/dist/types/helpers/cables.d.ts +14 -11
  10. package/dist/types/helpers/exchange.d.ts +7 -1
  11. package/dist/types/helpers/upstreams.d.ts +11 -3
  12. package/dist/types/platforms/derive.d.ts +1 -1
  13. package/dist/types/platforms/fortios/routing.d.ts +2 -2
  14. package/dist/types/platforms/junos/bgp.d.ts +2 -2
  15. package/dist/types/platforms/routeros/render.d.ts +4 -0
  16. package/dist/types/schema/device.d.ts +4 -4
  17. package/dist/types/schema/interfaces.d.ts +1 -1
  18. package/dist/types/schema/routing.d.ts +43 -10
  19. package/package.json +1 -1
  20. package/src/cli/index.ts +26 -29
  21. package/src/core/cabling.ts +0 -30
  22. package/src/core/define.ts +3 -1
  23. package/src/core/validator.ts +60 -0
  24. package/src/helpers/cables.ts +20 -15
  25. package/src/helpers/exchange.ts +9 -3
  26. package/src/helpers/upstreams.ts +9 -4
  27. package/src/platforms/derive.ts +23 -1
  28. package/src/platforms/fortios/routing.ts +2 -2
  29. package/src/platforms/junos/bgp.ts +2 -2
  30. package/src/platforms/routeros/TRAPS.md +4 -0
  31. package/src/platforms/routeros/export.ts +1 -0
  32. package/src/platforms/routeros/index.ts +11 -2
  33. package/src/platforms/routeros/plan.ts +3 -0
  34. package/src/platforms/routeros/render.ts +28 -3
  35. package/src/schema/device.ts +4 -3
  36. package/src/schema/interfaces.ts +1 -1
  37. package/src/schema/routing.ts +46 -10
  38. package/templates/colocation/checks.ts +14 -11
  39. package/templates/colocation/edge-01.ts +2 -2
  40. package/templates/colocation/tenancy.ts +50 -71
  41. package/templates/colocation/tenants/acme.ts +10 -10
  42. package/templates/colocation/tenants/index.ts +2 -1
  43. package/templates/colocation/tor-01.ts +2 -2
  44. 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 { Catalog, Topology, AddressSpace, StudioModel };
921
+ export { Topology, AddressSpace, StudioModel };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  StudioModel
3
- } from "./index-k38bby43.js";
3
+ } from "./index-yzw86rc8.js";
4
4
 
5
5
  // src/studio/server.ts
6
6
  import { execFile as execFile2, spawn } from "node:child_process";
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-k38bby43.js";
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, ["bfd-timers", "multihop-bfd", "static-route-bfd", "mac-limit", "ethertype-filter"]));
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} ${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`);
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
- let network;
11869
- if (process.versions.bun) {
11870
- network = (await import(path)).default;
11871
- } else {
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").argument("[name-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 (first, second, flags) => (await Cli.open(flags.config)).bgp(first, second, flags));
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(first, second, flags) {
12142
- const { device, pattern } = this.bgpTarget(first, second);
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
- bgpTarget(first, second) {
12203
- if (second !== undefined || first && this.network.devices.some((device) => device.name === first)) {
12204
- return { device: this.one(first), pattern: second };
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 { device: withBgp[0], pattern: first };
12283
+ return withBgp[0];
12210
12284
  if (!withBgp.length)
12211
- Cli.fail(`${named}no device has BGP neighbors`);
12212
- return Cli.fail(`${named}several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
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-ggertnce.js");
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) {
@@ -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.device, spec.b.port) },
242
- b: { [spec.b.port]: Cables.end(spec, spec.a.device, spec.a.port) }
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.device, far), lag: spec.a.lag }]));
255
- const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a.device, near), lag: spec.b.lag }]));
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, device, port) {
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 and its port by Circuit's name. */
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. When absent, Circuit writes the far device and the far port as printed on it, such as `firewall-1 port6`, from the far device's own file. */
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 device. */
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. A member port is in the peering VLAN and nothing else, takes no part in spanning tree or LLDP, and lets only a trickle of broadcast, multicast and unknown unicast through. With `mac_limit` and `ethertypes`, it also learns that many MAC addresses at most and takes in only those kinds of frame.
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.