@takodotid/circuit 0.34.2 → 0.35.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 (240) hide show
  1. package/README.md +13 -14
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11347 -9018
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +17 -11
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -693
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -1,444 +0,0 @@
1
- // OSPF, OSPFv3 and BGP, with the route maps, prefix lists and community lists BGP's policies become.
2
-
3
- import { familyOf, host, split } from "../../../core/addr";
4
- import { marker } from "../../../core/secrets";
5
- import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../../schema";
6
- import { addressesOf, familyOfPrefix, holderOf, nameOf, network, settingsOf, subnet } from "./names";
7
- import { Block, quote, quoteAll, UNSET } from "./tree";
8
-
9
- type AnyRule = PolicyRule<string, string>;
10
- type AnyNeighbor = Neighbor<string, string>;
11
-
12
- /** The route map a session takes when its config states none, so it learns or announces nothing. */
13
- export const REJECT_ALL = "circuit-reject-all";
14
-
15
- /** The start of the comment on a prefix list `refresh` fills. The plan keeps the entries the device holds in one. */
16
- export const FILLED = "Filled by circuit refresh";
17
-
18
- /** The table a prefix list of one family sits in. */
19
- export const PREFIX_LIST = { ipv4: "router prefix-list", ipv6: "router prefix-list6" } as const;
20
-
21
- /** The longest prefix of each family. */
22
- export const BITS = { ipv4: 32, ipv6: 128 } as const;
23
-
24
- /** A neighbor's settings with its group's beneath them. */
25
- export function neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string> {
26
- const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
27
- return { ...group, ...neighbor };
28
- }
29
-
30
- /** The families a session carries: its own, or the family of its address. */
31
- export function familiesOfNeighbor(device: Device, neighbor: AnyNeighbor): Family[] {
32
- return [...(neighborSettings(device, neighbor).families ?? [familyOf(neighbor.address)])];
33
- }
34
-
35
- /** Whether OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
36
- export function usesBfd(device: Device): boolean {
37
- const areas = Object.values(device.routing?.ospf?.areas ?? {});
38
- const ospf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
39
- const bgp = Object.values(device.routing?.bgp?.neighbors ?? {}).some((neighbor) => neighborSettings(device, neighbor).bfd);
40
- return ospf || bgp;
41
- }
42
-
43
- export function routing(root: Block, device: Device): void {
44
- prefixLists(root, device);
45
- ospf(root, device);
46
- ospf6(root, device);
47
- bgp(root, device);
48
- }
49
-
50
- /** A prefix as the platform writes it in a prefix list of its family. */
51
- const listPrefix = (prefix: string, family: Family) => (family === "ipv4" ? subnet(network(prefix)) : network(prefix));
52
-
53
- /** One rule of a prefix list: a prefix, and the lengths it takes within it. The device prints a bound the rule does not hold as `unset`. */
54
- function listRule(rule: Block, family: Family, prefix: string, lengths: { ge?: number; le?: number } = {}): void {
55
- rule.set(family === "ipv4" ? "prefix" : "prefix6", listPrefix(prefix, family));
56
- rule.set("ge", lengths.ge ?? UNSET);
57
- rule.set("le", lengths.le ?? UNSET);
58
- }
59
-
60
- /** The lengths a rule takes within a prefix to match lengths from `min` to `max`, or none when no length fits. A bound equal to the prefix's own length is left out. */
61
- function bounds(prefix: string, family: Family, min = 0, max: number = BITS[family]): { ge?: number; le?: number } | undefined {
62
- const own = split(network(prefix)).len;
63
- const low = Math.max(min, own);
64
- if (max < low) return undefined;
65
- if (low === own && max === own) return {};
66
-
67
- return { ...(low > own ? { ge: low } : {}), le: max };
68
- }
69
-
70
- /** Each prefix set as a prefix list matching its prefixes and any more specific. One a registry fills starts with a rule that denies every prefix, so it matches nothing until `refresh` fills it. */
71
- function prefixLists(root: Block, device: Device): void {
72
- for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
73
- if ("source" in set) {
74
- const { registry, query, family } = set.source;
75
- const entry = root
76
- .block(PREFIX_LIST[family])
77
- .entry(name)
78
- .set("comments", quote(`${FILLED} from ${registry} ${query}`));
79
- // Until `refresh` fills it, the list refuses every prefix, so a rule matching it matches nothing.
80
- const rule = entry.block("rule").entry(1);
81
- listRule(rule, family, family === "ipv4" ? "0.0.0.0/0" : "::/0", { le: BITS[family] });
82
- rule.set("action", "deny");
83
- continue;
84
- }
85
-
86
- const family = set.prefixes[0] ? familyOfPrefix(set.prefixes[0]) : "ipv4";
87
- const entry = root.block(PREFIX_LIST[family]).entry(name);
88
- set.prefixes.forEach((prefix, index) => listRule(entry.block("rule").entry(index + 1), family, prefix, bounds(prefix, family)));
89
- }
90
- }
91
-
92
- /** The networks of an interface's addresses of one family, for an OSPF area to announce. */
93
- function networksOf(device: Device, name: string, family: Family): string[] {
94
- return addressesOf(settingsOf(device, name))
95
- .filter((address) => familyOfPrefix(address) === family)
96
- .map(network);
97
- }
98
-
99
- /** OSPF for IPv4. An area runs on the networks of its interfaces; an interface named with a cost, a link type or BFD has settings of its own; a passive one forms no adjacency. */
100
- function ospf(root: Block, device: Device): void {
101
- const ospf = device.routing?.ospf;
102
- if (!ospf || !(ospf.families ?? ["ipv4"]).includes("ipv4")) return;
103
-
104
- const block = root.block("router ospf");
105
- if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
106
-
107
- const passive: string[] = [];
108
- let networks = 0;
109
-
110
- for (const [areaId, area] of Object.entries(ospf.areas)) {
111
- block.block("area").entry(areaId, true);
112
-
113
- for (const [name, settings] of Object.entries(area.interfaces)) {
114
- const iface = nameOf(device, name);
115
- if (settings?.passive) passive.push(iface);
116
-
117
- if (settings?.cost !== undefined || settings?.network === "point-to-point" || settings?.bfd) {
118
- const entry = block.block("ospf-interface").entry(iface);
119
- entry.set("interface", quote(iface));
120
- if (settings.cost !== undefined) entry.set("cost", settings.cost);
121
- if (settings.network === "point-to-point") entry.set("network-type", "point-to-point");
122
- if (settings.bfd) entry.set("bfd", "enable");
123
- }
124
-
125
- for (const prefix of networksOf(device, name, "ipv4")) {
126
- const entry = block.block("network").entry(++networks);
127
- entry.set("prefix", subnet(prefix));
128
- // The backbone is the platform's default area and is not printed.
129
- if (areaId !== "0.0.0.0") entry.set("area", areaId);
130
- }
131
- }
132
- }
133
-
134
- if (passive.length) block.set("passive-interface", quoteAll(passive));
135
- for (const source of ospf.redistribute ?? []) block.block(`redistribute "${source}"`).set("status", "enable");
136
- }
137
-
138
- /** OSPFv3 for IPv6. Every interface of an area runs it in that area. */
139
- function ospf6(root: Block, device: Device): void {
140
- const ospf = device.routing?.ospf;
141
- if (!ospf?.families?.includes("ipv6")) return;
142
-
143
- const block = root.block("router ospf6");
144
- if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
145
-
146
- const passive: string[] = [];
147
-
148
- for (const [areaId, area] of Object.entries(ospf.areas)) {
149
- block.block("area").entry(areaId, true);
150
-
151
- for (const [name, settings] of Object.entries(area.interfaces)) {
152
- const iface = nameOf(device, name);
153
- if (settings?.passive) passive.push(iface);
154
-
155
- const entry = block.block("ospf6-interface").entry(iface);
156
- entry.set("interface", quote(iface));
157
- if (areaId !== "0.0.0.0") entry.set("area-id", areaId);
158
- if (settings?.cost !== undefined) entry.set("cost", settings.cost);
159
- if (settings?.network === "point-to-point") entry.set("network-type", "point-to-point");
160
- if (settings?.bfd) entry.set("bfd", "enable");
161
- }
162
- }
163
-
164
- if (passive.length) block.set("passive-interface", quoteAll(passive));
165
- for (const source of ospf.redistribute ?? []) block.block(`redistribute "${source}"`).set("status", "enable");
166
- }
167
-
168
- /** BGP: each session with its settings and group's, the route maps its policies become for each family it carries, and the prefixes the device originates. */
169
- function bgp(root: Block, device: Device): void {
170
- const bgp = device.routing?.bgp;
171
- if (!bgp) return;
172
-
173
- const block = root.block("router bgp");
174
- block.set("as", bgp.asn);
175
- if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
176
-
177
- const maps = new RouteMaps(root, device);
178
-
179
- for (const neighbor of Object.values(bgp.neighbors ?? {})) {
180
- const settings = neighborSettings(device, neighbor);
181
- const families = familiesOfNeighbor(device, neighbor);
182
- const entry = block.block("neighbor").entry(neighbor.address);
183
-
184
- // Both families are active unless told otherwise.
185
- if (!families.includes("ipv4")) entry.set("activate", "disable");
186
- if (!families.includes("ipv6")) entry.set("activate6", "disable");
187
- if (settings.bfd) entry.set("bfd", "enable");
188
- if (settings.multihop) entry.set("ebgp-enforce-multihop", "enable");
189
- if (settings.passive) entry.set("passive", "enable");
190
- if (settings.description) entry.set("description", quote(settings.description));
191
- if (settings.remote_as !== undefined) entry.set("remote-as", settings.remote_as);
192
-
193
- for (const family of families) {
194
- const suffix = family === "ipv4" ? "" : "6";
195
- if (settings.max_prefixes !== undefined) entry.set(`maximum-prefix${suffix}`, settings.max_prefixes);
196
- entry.set(`route-map-in${suffix}`, quote(maps.of(settings.import, family)));
197
- entry.set(`route-map-out${suffix}`, quote(maps.of(settings.export, family)));
198
- }
199
-
200
- if (settings.keepalive !== undefined) entry.set("keep-alive-timer", settings.keepalive);
201
- if (settings.hold_time !== undefined) entry.set("holdtime-timer", settings.hold_time);
202
-
203
- const source = settings.local_address ? holderOf(device, settings.local_address) : undefined;
204
- if (source) entry.set("update-source", quote(nameOf(device, source)));
205
-
206
- if (settings.password) entry.set("password", quote(marker(settings.password)));
207
- }
208
-
209
- const networks = { ipv4: 0, ipv6: 0 };
210
-
211
- for (const prefix of bgp.networks ?? []) {
212
- const family = familyOfPrefix(prefix);
213
- const entry = block.block(family === "ipv4" ? "network" : "network6").entry(++networks[family]);
214
- entry.set(family === "ipv4" ? "prefix" : "prefix6", listPrefix(prefix, family));
215
- }
216
- }
217
-
218
- /** The family a rule is limited to, by its match: a family, a prefix, or a prefix set of one family. */
219
- export function familyOfRule(device: Device, rule: AnyRule): Family | undefined {
220
- const match = rule.match;
221
- if (match?.family) return match.family;
222
- if (match?.prefix) return familyOfPrefix(match.prefix);
223
-
224
- const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
225
- if (set && "source" in set) return set.source.family;
226
- if (set?.prefixes[0]) return familyOfPrefix(set.prefixes[0]);
227
-
228
- return undefined;
229
- }
230
-
231
- /** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller. */
232
- export function flatten(device: Device, policy: string, seen: string[] = [policy]): AnyRule[] {
233
- const flat: AnyRule[] = [];
234
-
235
- for (const rule of device.policies?.[policy] ?? []) {
236
- if (rule.call && !seen.includes(rule.call)) flat.push(...flatten(device, rule.call, [...seen, rule.call]));
237
- if (rule.match || rule.set || rule.action) flat.push(rule);
238
- }
239
-
240
- return flat;
241
- }
242
-
243
- /** A name made of a value's words, for a list named after what it matches: `64500:*` becomes `64500-any`. */
244
- const slug = (value: string) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
245
-
246
- /** A community pattern as a regular expression, `*` standing for any value of its field. */
247
- const patternOf = (pattern: string) => `^${pattern.replace(/\*/g, "[0-9]+")}$`;
248
-
249
- /** Each policy as a route map for each family, built once, with the lists its rules match. */
250
- class RouteMaps {
251
- private readonly built = new Set<string>();
252
-
253
- constructor(
254
- private readonly root: Block,
255
- private readonly device: Device
256
- ) {}
257
-
258
- /** The route map for a policy in one family; one that rejects everything when there is no policy. */
259
- of(policy: string | undefined, family: Family): string {
260
- if (!policy) {
261
- if (!this.built.has(REJECT_ALL)) {
262
- this.built.add(REJECT_ALL);
263
- this.root.block("router route-map").entry(REJECT_ALL).block("rule").entry(1).set("action", "deny");
264
- }
265
-
266
- return REJECT_ALL;
267
- }
268
-
269
- const name = family === "ipv4" ? policy : `${policy}-v6`;
270
- if (this.built.has(name)) return name;
271
- this.built.add(name);
272
-
273
- const map = this.root.block("router route-map").entry(name);
274
- let number = 0;
275
-
276
- for (const rule of flatten(this.device, policy)) {
277
- const only = familyOfRule(this.device, rule);
278
- if (only && only !== family) continue;
279
-
280
- this.rule(map.block("rule").entry(++number), rule, family, `${name}-${number}`);
281
- }
282
-
283
- return name;
284
- }
285
-
286
- private rule(entry: Block, rule: AnyRule, family: Family, label: string): void {
287
- const match = rule.match ?? {};
288
- const set = rule.set ?? {};
289
- const address = family === "ipv4" ? "match-ip-address" : "match-ip6-address";
290
-
291
- if (match.prefix) {
292
- this.list(label, family, [[match.prefix, {}]]);
293
- entry.set(address, quote(label));
294
- } else if (match.prefix_set && match.prefix_length) {
295
- const name = `${match.prefix_set}-${match.prefix_length.min ?? 0}-${match.prefix_length.max ?? BITS[family]}`;
296
- const prefixes = this.prefixesOf(match.prefix_set);
297
- this.list(
298
- name,
299
- family,
300
- prefixes.map((prefix) => [prefix, bounds(prefix, family, match.prefix_length?.min, match.prefix_length?.max)])
301
- );
302
- entry.set(address, quote(name));
303
- } else if (match.prefix_set) {
304
- entry.set(address, quote(match.prefix_set));
305
- } else if (match.prefix_length) {
306
- const { min = 0, max = BITS[family] } = match.prefix_length;
307
- const name = `circuit-length-${min}-${max}`;
308
- const everything = family === "ipv4" ? "0.0.0.0/0" : "::/0";
309
- this.list(name, family, [[everything, bounds(everything, family, min, max)]]);
310
- entry.set(address, quote(name));
311
- }
312
-
313
- if (match.community)
314
- entry.set("match-community", quote(this.communities(`circuit-match-${slug(match.community)}`, [match.community], false)));
315
-
316
- if (set.local_pref !== undefined) entry.set("set-local-preference", set.local_pref);
317
- if (set.med !== undefined) entry.set("set-metric", set.med);
318
- if (set.prepend !== undefined) entry.set("set-aspath", quote(Array(set.prepend).fill(this.device.routing!.bgp!.asn).join(" ")));
319
-
320
- // Replacing the communities makes removing some needless; added ones join the new ones.
321
- if (set.communities) {
322
- entry.set("set-community", quoteAll([...set.communities, ...(set.add_communities ?? [])]));
323
- } else {
324
- if (set.add_communities) entry.set("set-community", quoteAll(set.add_communities)).set("set-community-additive", "enable");
325
-
326
- if (set.remove_communities) {
327
- const name = `circuit-remove-${slug(set.remove_communities.join(" "))}`;
328
- entry.set("set-community-delete", quote(this.communities(name, set.remove_communities, true)));
329
- }
330
- }
331
-
332
- if (set.next_hop && familyOf(set.next_hop) === family)
333
- entry.set(family === "ipv4" ? "set-ip-nexthop" : "set-ip6-nexthop", set.next_hop);
334
-
335
- // Permit is the default and is not printed.
336
- if (rule.action === "reject") entry.set("action", "deny");
337
- }
338
-
339
- /** The prefixes of a set the config lists. */
340
- private prefixesOf(set: string): string[] {
341
- const prefixes = this.device.prefix_sets?.[set];
342
- return prefixes && "prefixes" in prefixes ? [...prefixes.prefixes] : [];
343
- }
344
-
345
- /** A prefix list of its own, rule by rule; a prefix no length fits is left out. */
346
- private list(name: string, family: Family, rules: [string, { ge?: number; le?: number } | undefined][]): void {
347
- const rulesBlock = this.root.block(PREFIX_LIST[family]).entry(name).block("rule");
348
- let number = 0;
349
-
350
- for (const [prefix, lengths] of rules) if (lengths) listRule(rulesBlock.entry(++number), family, prefix, lengths);
351
- }
352
-
353
- /** A community list matching these communities, or, expanded, these patterns. */
354
- private communities(name: string, values: readonly string[], patterns: boolean): string {
355
- const list = this.root.block("router community-list").entry(name);
356
- if (patterns) list.set("type", "expanded");
357
-
358
- values.forEach((value, index) => {
359
- const rule = list
360
- .block("rule")
361
- .entry(index + 1)
362
- .set("action", "permit");
363
- if (patterns) rule.set("regexp", quote(patternOf(value)));
364
- else rule.set("match", quote(value));
365
- });
366
-
367
- return name;
368
- }
369
- }
370
-
371
- /** Routing the platform cannot express, or that this adapter does not yet write. */
372
- export function routingGaps(device: Device): string[] {
373
- const found: string[] = [];
374
- const routing = device.routing;
375
-
376
- if (routing?.rpki) found.push("routing.rpki, the platform has no route origin validation");
377
- if (routing?.router_id && !routing.ospf && !routing.bgp) found.push("routing.router_id without OSPF or BGP, which each hold it");
378
-
379
- const ospf = routing?.ospf;
380
- if (ospf?.families?.includes("ipv6") && !routing?.router_id) found.push("routing.ospf for IPv6 without routing.router_id");
381
-
382
- for (const area of Object.values(ospf?.areas ?? {})) {
383
- for (const name of Object.keys(area.interfaces)) {
384
- if (settingsOf(device, name)?.vrf) found.push(`routing.ospf on ${name}, which routes in a VRF: OSPF runs in the default table`);
385
-
386
- const ipv4 = addressesOf(settingsOf(device, name)).some((address) => familyOf(host(address)) === "ipv4");
387
- if ((ospf?.families ?? ["ipv4"]).includes("ipv4") && !ipv4)
388
- found.push(`routing.ospf on ${name}, which has no IPv4 address for OSPF to run on`);
389
- }
390
- }
391
-
392
- for (const [name, neighbor] of Object.entries(routing?.bgp?.neighbors ?? {})) {
393
- const where = `routing.bgp.neighbors.${name}`;
394
- const settings = neighborSettings(device, neighbor);
395
-
396
- if (settings.role) found.push(`${where}.role, the platform has no BGP roles`);
397
- if (settings.local_address && !holderOf(device, settings.local_address)) {
398
- found.push(`${where}.local_address, no interface holds ${settings.local_address}`);
399
- }
400
-
401
- if (familyOf(neighbor.address) === "ipv6" && familiesOfNeighbor(device, neighbor).includes("ipv4")) {
402
- found.push(`${where}.families ipv4 over an IPv6 session`);
403
- }
404
- }
405
-
406
- for (const [name, rules] of Object.entries(device.policies ?? {})) {
407
- rules.forEach((rule, index) => found.push(...ruleGaps(device, `policies.${name}[${index}]`, rule)));
408
- }
409
-
410
- return found;
411
- }
412
-
413
- function ruleGaps(device: Device, where: string, rule: AnyRule): string[] {
414
- const found: string[] = [];
415
- const match = rule.match ?? {};
416
- const set = rule.set ?? {};
417
-
418
- if ((rule.match || rule.set) && !rule.action)
419
- found.push(`${where}, a rule without action: the platform stops at the first rule that matches`);
420
-
421
- for (const field of ["as_path_length", "through_as", "rpki", "large_community"] as const) {
422
- if (match[field] !== undefined) found.push(`${where}.match.${field}`);
423
- }
424
-
425
- for (const field of [
426
- "large_communities",
427
- "add_large_communities",
428
- "remove_large_communities",
429
- "preferred_source",
430
- "blackhole",
431
- ] as const) {
432
- if (set[field] !== undefined) found.push(`${where}.set.${field}`);
433
- }
434
-
435
- if (match.prefix && match.prefix_length) found.push(`${where}.match.prefix with prefix_length`);
436
- if (match.prefix && match.prefix_set) found.push(`${where}.match.prefix with prefix_set`);
437
-
438
- const prefixSet = match.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
439
- if (prefixSet && "source" in prefixSet && match.prefix_length) {
440
- found.push(`${where}.match.prefix_length with a prefix set a registry fills`);
441
- }
442
-
443
- return found;
444
- }
@@ -1,121 +0,0 @@
1
- // FortiOS configuration as a tree. A `config` block holds settings, nested blocks and, in a table, entries opened with `edit`. The render builds a tree and prints it in the form `show` prints, and the read parses what `show` printed, so the plan compares two trees.
2
-
3
- /** A setting the device prints as `unset <key>`, a key it holds empty on purpose, such as an ICMP service's type. */
4
- export const UNSET = "\u0000unset";
5
-
6
- /** One `edit` of a table: its name as written, and what it holds. */
7
- export type Entry = { id: string; block: Block };
8
-
9
- export class Block {
10
- readonly settings = new Map<string, string>();
11
- readonly blocks = new Map<string, Block>();
12
- readonly entries = new Map<string, Entry>();
13
-
14
- /** Set a key. A value of several words is written as given. */
15
- set(key: string, value: string | number): this {
16
- this.settings.set(key, String(value));
17
- return this;
18
- }
19
-
20
- /** The nested `config` block with this header, created when absent. */
21
- block(header: string): Block {
22
- if (!this.blocks.has(header)) this.blocks.set(header, new Block());
23
- return this.blocks.get(header)!;
24
- }
25
-
26
- /** The table entry with this name, created when absent. A name is quoted unless it is a number, or the table prints it bare, such as an OSPF area or an IPv6 prefix. */
27
- entry(id: string | number, plain = false): Block {
28
- const written = typeof id === "number" || plain ? String(id) : quote(id);
29
- const key = bare(written);
30
- if (!this.entries.has(key)) this.entries.set(key, { id: written, block: new Block() });
31
- return this.entries.get(key)!.block;
32
- }
33
-
34
- isEmpty(): boolean {
35
- return !this.settings.size && !this.blocks.size && !this.entries.size;
36
- }
37
- }
38
-
39
- /** A string as the CLI takes it, in double quotes. */
40
- export function quote(text: string): string {
41
- return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
42
- }
43
-
44
- /** Each of several strings quoted, as a list setting takes them. */
45
- export const quoteAll = (texts: readonly string[]) => texts.map(quote).join(" ");
46
-
47
- /** A value's words; a quoted string is one word with its quotes. */
48
- export function wordsOf(value: string): string[] {
49
- return value.match(/"(?:[^"\\]|\\.)*"|'[^']*'|\S+/g) ?? [];
50
- }
51
-
52
- /** A word as the device means it, without quotes or escapes. */
53
- export function bare(word: string): string {
54
- if (word.startsWith('"')) return word.slice(1, -1).replace(/\\(.)/g, "$1");
55
- if (word.startsWith("'")) return word.slice(1, -1);
56
- return word;
57
- }
58
-
59
- /** The lines that build a block, at an indent, in the form `show` prints and the CLI takes. */
60
- export function linesOf(block: Block, indent = ""): string[] {
61
- const lines: string[] = [];
62
-
63
- for (const [key, value] of block.settings) lines.push(value === UNSET ? `${indent}unset ${key}` : `${indent}set ${key} ${value}`);
64
-
65
- for (const [header, child] of block.blocks) lines.push(`${indent}config ${header}`, ...linesOf(child, indent + " "), `${indent}end`);
66
-
67
- for (const { id, block: entry } of block.entries.values()) {
68
- lines.push(`${indent}edit ${id}`, ...linesOf(entry, indent + " "), `${indent}next`);
69
- }
70
-
71
- return lines;
72
- }
73
-
74
- /** The whole configuration: each top-level block as `config <section>` to `end`. */
75
- export function print(root: Block): string {
76
- const lines: string[] = [];
77
- for (const [section, block] of root.blocks) lines.push(`config ${section}`, ...linesOf(block, " "), "end");
78
- return lines.join("\n") + "\n";
79
- }
80
-
81
- /** Whether a line opens a quoted value it does not close, so the value goes on over the next lines. */
82
- const unclosed = (line: string) => (line.replace(/\\./g, "").match(/"/g)?.length ?? 0) % 2 === 1;
83
-
84
- /** The tree of what `show` printed. Lines that are not configuration, such as the version header, are skipped. */
85
- export function parse(text: string): Block {
86
- const root = new Block();
87
- const stack: { block: Block; isEntry: boolean }[] = [{ block: root, isEntry: false }];
88
- const lines = text.replace(/\r/g, "").split("\n");
89
-
90
- for (let index = 0; index < lines.length; index++) {
91
- let line = lines[index]!.trim();
92
- if (!line || line.startsWith("#")) continue;
93
-
94
- while (unclosed(line) && index + 1 < lines.length) line += "\n" + lines[++index];
95
-
96
- const current = stack.at(-1)!.block;
97
- const [verb] = line.split(/\s+/, 1);
98
- const rest = line.slice(verb!.length).trim();
99
-
100
- if (verb === "config") {
101
- const child = current.block(rest);
102
- stack.push({ block: child, isEntry: false });
103
- } else if (verb === "edit") {
104
- const key = bare(rest);
105
- if (!current.entries.has(key)) current.entries.set(key, { id: rest, block: new Block() });
106
- stack.push({ block: current.entries.get(key)!.block, isEntry: true });
107
- } else if (verb === "next") {
108
- if (stack.at(-1)!.isEntry) stack.pop();
109
- } else if (verb === "end") {
110
- if (stack.at(-1)!.isEntry) stack.pop();
111
- if (stack.length > 1) stack.pop();
112
- } else if (verb === "set") {
113
- const [key] = rest.split(/\s+/, 1);
114
- current.settings.set(key!, rest.slice(key!.length).trim());
115
- } else if (verb === "unset") {
116
- current.settings.set(rest, UNSET);
117
- }
118
- }
119
-
120
- return root;
121
- }
@@ -1,9 +0,0 @@
1
- import { fortios } from "./fortios";
2
- import { junos } from "./junos";
3
- import { raisecom } from "./raisecom-ros";
4
- import { routeros } from "./routeros";
5
- import type { DeviceAdapter } from "./types";
6
- import { vrp } from "./vrp";
7
-
8
- /** Every platform the framework can render and apply, by name. */
9
- export const adapters: Record<string, DeviceAdapter> = { routeros, vrp, "raisecom-ros": raisecom, junos, fortios };