@takodotid/circuit 0.34.3 → 0.36.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 +14 -15
  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 +11345 -9024
  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 +18 -12
  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 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  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,888 +0,0 @@
1
- // What it takes to turn the configuration a FortiGate prints into the one the render holds.
2
- //
3
- // A setting the render holds is set; one only the device holds is unset, which returns it to its default. A table entry only the device holds is deleted, except one the factory put there. Deletions of whole entries run last, in the reverse of the render's order, so an address goes only after the policies that named it.
4
-
5
- import type { ApplyOptions, Plan, Step } from "../types";
6
- import { FILLED, PREFIX_LIST } from "./routing";
7
- import { Block, bare, type Entry, linesOf, parse, print, quoteAll, UNSET, wordsOf } from "./tree";
8
-
9
- /** How the plan treats the entries of one table. */
10
- type Table = {
11
- /** Entries the factory holds. The plan neither reads nor removes them. */
12
- factory?: readonly string[];
13
- /** The device keeps the entries in an order of their own, which `move` changes. */
14
- ordered?: true;
15
- /** An entry is known by what it holds, not by its number. */
16
- byContent?: true;
17
- /** Settings the device gives an entry it creates. They are unset when the render does not hold them. */
18
- created?: (entry: Block) => readonly string[];
19
- /** Settings an entry keeps for life. A change deletes the entry and creates it again. */
20
- fixed?: readonly string[];
21
- /** The device refuses an entry that holds the same as another, `Duplicated entry`, even for a moment while entries are renumbered. */
22
- distinct?: true;
23
- };
24
-
25
- /** Objects every FortiGate running 6.2 is shipped with, which policies and profiles of the factory name. */
26
- const FACTORY_ADDRESSES = [
27
- "none",
28
- "login.microsoftonline.com",
29
- "login.microsoft.com",
30
- "login.windows.net",
31
- "gmail.com",
32
- "wildcard.google.com",
33
- "wildcard.dropbox.com",
34
- "all",
35
- "FIREWALL_AUTH_PORTAL_ADDRESS",
36
- "FABRIC_DEVICE",
37
- "SSLVPN_TUNNEL_ADDR1",
38
- ];
39
-
40
- const FACTORY_SERVICES = [
41
- "ALL",
42
- "ALL_TCP",
43
- "ALL_UDP",
44
- "ALL_ICMP",
45
- "ALL_ICMP6",
46
- "GRE",
47
- "AH",
48
- "ESP",
49
- "AOL",
50
- "BGP",
51
- "DHCP",
52
- "DNS",
53
- "FINGER",
54
- "FTP",
55
- "FTP_GET",
56
- "FTP_PUT",
57
- "GOPHER",
58
- "H323",
59
- "HTTP",
60
- "HTTPS",
61
- "IKE",
62
- "IMAP",
63
- "IMAPS",
64
- "Internet-Locator-Service",
65
- "IRC",
66
- "L2TP",
67
- "LDAP",
68
- "NetMeeting",
69
- "NFS",
70
- "NNTP",
71
- "NTP",
72
- "OSPF",
73
- "PC-Anywhere",
74
- "PING",
75
- "TIMESTAMP",
76
- "INFO_REQUEST",
77
- "INFO_ADDRESS",
78
- "ONC-RPC",
79
- "DCE-RPC",
80
- "POP3",
81
- "POP3S",
82
- "PPTP",
83
- "QUAKE",
84
- "RAUDIO",
85
- "REXEC",
86
- "RIP",
87
- "RLOGIN",
88
- "RSH",
89
- "SCCP",
90
- "SIP",
91
- "SIP-MSNmessenger",
92
- "SAMBA",
93
- "SMTP",
94
- "SMTPS",
95
- "SNMP",
96
- "SSH",
97
- "SYSLOG",
98
- "TALK",
99
- "TELNET",
100
- "TFTP",
101
- "MGCP",
102
- "UUCP",
103
- "VDOLIVE",
104
- "WAIS",
105
- "WINFRAME",
106
- "X-WINDOWS",
107
- "PING6",
108
- "MS-SQL",
109
- "MYSQL",
110
- "RDP",
111
- "VNC",
112
- "DHCP6",
113
- "SQUID",
114
- "SOCKS",
115
- "WINS",
116
- "RADIUS",
117
- "RADIUS-OLD",
118
- "CVSPSERVER",
119
- "AFS3",
120
- "TRACEROUTE",
121
- "RTSP",
122
- "MMS",
123
- "KERBEROS",
124
- "LDAP_UDP",
125
- "SMB",
126
- "NONE",
127
- "webproxy",
128
- ];
129
-
130
- /** The device's own certificates, which the factory makes for each device. */
131
- const FACTORY_CERTIFICATES = [
132
- "Fortinet_CA_SSL",
133
- "Fortinet_CA_Untrusted",
134
- "Fortinet_SSL",
135
- "Fortinet_SSL_RSA1024",
136
- "Fortinet_SSL_RSA2048",
137
- "Fortinet_SSL_RSA4096",
138
- "Fortinet_SSL_DSA1024",
139
- "Fortinet_SSL_DSA2048",
140
- "Fortinet_SSL_ECDSA256",
141
- "Fortinet_SSL_ECDSA384",
142
- "Fortinet_SSL_ECDSA521",
143
- "Fortinet_SSL_ED25519",
144
- "Fortinet_SSL_ED448",
145
- ];
146
-
147
- const ORDERED: Table = { ordered: true };
148
-
149
- /** Tables whose entries need more than a plain comparison, by section. */
150
- export const TABLES: Record<string, Table> = {
151
- // A new aggregate comes with device identification and LLDP of its own, a new VLAN interface, which names no type, with device identification and a role.
152
- "system interface": {
153
- factory: ["modem", "ssl.root"],
154
- created: (entry) => {
155
- if (entry.settings.get("type") === "aggregate") return ["device-identification", "lldp-transmission"];
156
- if (entry.settings.has("interface") && !entry.settings.has("type")) return ["device-identification", "role"];
157
- return [];
158
- },
159
- fixed: ["type", "interface"],
160
- },
161
- "system accprofile": { factory: ["prof_admin"] },
162
- "vpn certificate local": { factory: FACTORY_CERTIFICATES },
163
- "system session-helper": { byContent: true },
164
- "firewall address": { factory: FACTORY_ADDRESSES },
165
- "firewall address6": { factory: ["SSLVPN_TUNNEL_IPv6_ADDR1", "all", "none"] },
166
- "firewall addrgrp": { factory: ["G Suite", "Microsoft Office 365"] },
167
- "firewall service custom": { factory: FACTORY_SERVICES },
168
- "firewall policy": ORDERED,
169
- "firewall policy6": ORDERED,
170
- "firewall local-in-policy": ORDERED,
171
- "firewall local-in-policy6": ORDERED,
172
- "firewall central-snat-map": ORDERED,
173
- "router static": { distinct: true },
174
- "router static6": { distinct: true },
175
- };
176
-
177
- /** Settings the device keeps for itself: an object's UUID and an interface's SNMP index. */
178
- const IGNORED = new Set(["uuid", "snmp-index"]);
179
-
180
- /** Settings of one section the device keeps for itself: how changes are saved, which `apply` sets while it works, and the alias it gives itself at every start. */
181
- const IGNORED_IN: Record<string, readonly string[]> = { "system global": ["cfg-save", "cfg-revert-timeout", "alias"] };
182
-
183
- /** In a certificate's entry, the settings that matter: its text and its key. The device prints others it decides for itself. */
184
- const KEPT: Record<string, readonly string[]> = { "vpn certificate local": ["certificate", "private-key"], "vpn certificate ca": ["ca"] };
185
-
186
- /** A policy denies unless told otherwise. */
187
- const DENY = { action: "deny" };
188
-
189
- /** Defaults the device does not print, by section. A config may state one, such as SSH on port 22, and the render states some because other settings appear only once they hold: a DHCP server's resolvers show only while it names them itself, and a policy answers a denied sender only while it denies. */
190
- const DEFAULTS: Record<string, Record<string, string>> = {
191
- "system global": {
192
- "admin-ssh-port": "22",
193
- "admin-sport": "443",
194
- "admin-port": "80",
195
- "admin-telnet-port": "23",
196
- "admin-ssh-grace-time": "120",
197
- },
198
- "system interface": { mtu: "1500" },
199
- "system snmp community": { "query-v2c-port": "161" },
200
- "system snmp user": { "query-port": "161" },
201
- "system dhcp server": { "dns-service": "specify" },
202
- "firewall policy": DENY,
203
- "firewall policy6": DENY,
204
- "firewall local-in-policy": DENY,
205
- "firewall local-in-policy6": DENY,
206
- };
207
-
208
- /** Settings whose value is a secret wherever they appear, such as a certificate's key, which the device prints encrypted. */
209
- const SECRET_KEYS = new Set(["private-key"]);
210
-
211
- /** Settings holding a certificate's text, compared without its line breaks. */
212
- const CERTIFICATES = new Set(["certificate", "ca"]);
213
-
214
- /** Settings undone before a nested list is deleted. Any other goes after it. */
215
- const FIRST = new Set(["ntpsync"]);
216
-
217
- /** Settings holding a list in no particular order. */
218
- const LISTS = new Set([
219
- "allowaccess",
220
- "ip6-allowaccess",
221
- "member",
222
- "srcintf",
223
- "dstintf",
224
- "srcaddr",
225
- "dstaddr",
226
- "service",
227
- "src-filter",
228
- "dhcp-relay-ip",
229
- ]);
230
-
231
- /** A value that is a secret: a marker in the render, a cipher or a redaction in the read. */
232
- const isSecret = (value: string) => /<secret:|<redacted>/.test(value) || value.startsWith("ENC ");
233
-
234
- /** A value in the form both sides compare in: words without quotes, a secret as one word, a list sorted. */
235
- function canonical(key: string, value: string): string {
236
- if (value === UNSET) return UNSET;
237
- if (isSecret(value) || SECRET_KEYS.has(key)) return "<secret>";
238
- if (CERTIFICATES.has(key)) return bare(value).replace(/\s+/g, "");
239
-
240
- const words = wordsOf(value).map(bare);
241
- return (LISTS.has(key) ? words.sort() : words).join(" ");
242
- }
243
-
244
- /** Whether an entry changes a setting it keeps for life, so it must be created again. */
245
- function changesFixed(table: Table | undefined, before: Block, after: Block): boolean {
246
- const value = (block: Block, key: string) => canonical(key, block.settings.get(key) ?? "");
247
- return (table?.fixed ?? []).some((key) => value(before, key) !== value(after, key));
248
- }
249
-
250
- /** Whether the device holds an entry the plan leaves alone. */
251
- function isFactory(table: Table | undefined, entry: Entry): boolean {
252
- return table?.factory?.includes(bare(entry.id)) ?? false;
253
- }
254
-
255
- /** The configuration as the plan reads it: factory entries and the device's own settings left out. */
256
- export function clean(text: string): string {
257
- const root = parse(text);
258
-
259
- for (const [section, block] of root.blocks) {
260
- const table = TABLES[section];
261
-
262
- for (const [key, entry] of block.entries) if (isFactory(table, entry)) block.entries.delete(key);
263
- for (const key of IGNORED_IN[section] ?? []) block.settings.delete(key);
264
- dropIgnored(block);
265
-
266
- const kept = KEPT[section];
267
- if (!kept) continue;
268
-
269
- for (const { block: entry } of block.entries.values()) {
270
- for (const key of entry.settings.keys()) if (!kept.includes(key)) entry.settings.delete(key);
271
- entry.blocks.clear();
272
- }
273
- }
274
-
275
- return print(root);
276
- }
277
-
278
- function dropIgnored(block: Block): void {
279
- for (const key of block.settings.keys()) if (IGNORED.has(key)) block.settings.delete(key);
280
- for (const child of block.blocks.values()) dropIgnored(child);
281
- for (const { block: entry } of block.entries.values()) dropIgnored(entry);
282
- }
283
-
284
- /** What a comparison needs to know beyond the blocks: the section it is in, and whether to send every secret. */
285
- type Context = { section: string; secrets: boolean };
286
-
287
- /** What one block's comparison sends and what a reviewer reads, then the entries it deletes and their lines for the reviewer. */
288
- type Diff = { send: string[]; show: string[]; removals: string[]; removed: string[] };
289
-
290
- /** The words before a line a reviewer reads, naming the entry and block it sits in. */
291
- const labelOf = (path: string[]) => (path.length ? `${path.join(" ")}: ` : "");
292
-
293
- /** A block's lines as reviewer lines, each with its place. An entry that holds nothing, such as an extra IPv6 address, is its own line. */
294
- function shown(sign: "+" | "-", path: string[], block: Block): string[] {
295
- const label = labelOf(path);
296
- const lines = linesOf(block).map((line) => `${sign} ${label}${line.trim()}`);
297
- return lines.length ? lines : [`${sign} ${path.join(" ")}`];
298
- }
299
-
300
- /** The lines that create an entry, with the settings the device adds unset when the render does not hold them. */
301
- function creation(entry: Entry, table: Table | undefined, id = entry.id): string[] {
302
- const unset = (table?.created?.(entry.block) ?? []).filter((key) => !entry.block.settings.has(key)).map((key) => `unset ${key}`);
303
- return [`edit ${id}`, ...linesOf(entry.block).map((line) => line.trim()), ...unset, "next"];
304
- }
305
-
306
- /**
307
- * Compare one block. `table` applies to its entries; `top` sends their removal to the end of the plan.
308
- *
309
- * What goes is undone before what comes is set, since a setting can hide another: `send-deny-packet` exists only while a policy denies. A nested list that goes is deleted before the settings, since one can hide the list and keep its entries hidden, such as `secondary-IP`; NTP refuses to delete its last server while it synchronises, so `ntpsync` goes before its list.
310
- */
311
- function compare(path: string[], want: Block, have: Block, table: Table | undefined, top: boolean, context: Context): Diff {
312
- const send: string[] = [];
313
- const show: string[] = [];
314
- const removals: string[] = [];
315
- const removed: string[] = [];
316
- const label = labelOf(path);
317
-
318
- const nested = (header: string, wanted: Block, held: Block) => {
319
- const diff = compare([...path, header], wanted, held, undefined, false, context);
320
- if (!diff.send.length) return;
321
-
322
- send.push(`config ${header}`, ...diff.send, "end");
323
- show.push(...diff.show);
324
- };
325
-
326
- const unset = (key: string, old: string) => {
327
- send.push(`unset ${key}`);
328
- show.push(`- ${label}set ${key} ${old}`);
329
- };
330
-
331
- const gone = [...have.settings].filter(([key]) => !want.settings.has(key) && !IGNORED.has(key));
332
- for (const [key, old] of gone) if (FIRST.has(key)) unset(key, old);
333
-
334
- // In reverse, so a list that names another, such as OSPF's networks naming its areas, goes first.
335
- for (const [header, child] of [...have.blocks].reverse()) if (!want.blocks.has(header)) nested(header, new Block(), child);
336
-
337
- // In reverse, so a setting that shows others, such as port forwarding, goes after them.
338
- for (const [key, old] of gone.reverse()) if (!FIRST.has(key)) unset(key, old);
339
-
340
- for (const [key, value] of want.settings) {
341
- const old = have.settings.get(key);
342
- const current = old ?? DEFAULTS[context.section]?.[key];
343
- const same = current !== undefined && canonical(key, current) === canonical(key, value);
344
- if (same && !(context.secrets && value.includes("<secret:"))) continue;
345
-
346
- send.push(value === UNSET ? `unset ${key}` : `set ${key} ${value}`);
347
- if (old !== undefined) show.push(`- ${label}set ${key} ${old}`);
348
- show.push(`+ ${label}set ${key} ${value}`);
349
- }
350
-
351
- for (const [header, child] of want.blocks) nested(header, child, have.blocks.get(header) ?? new Block());
352
-
353
- const entries = table?.byContent
354
- ? byContent(path, want, have)
355
- : table?.distinct
356
- ? distinctById(path, want, have, table, context)
357
- : byId(path, want, have, table, context);
358
- send.push(...entries.send);
359
- show.push(...entries.show);
360
-
361
- if (top) {
362
- removals.push(...entries.removals);
363
- removed.push(...entries.removed);
364
- } else {
365
- send.push(...entries.removals);
366
- show.push(...entries.removed);
367
- }
368
-
369
- return { send, show, removals, removed };
370
- }
371
-
372
- /** Entries matched by name: new ones created, changed ones edited, gone ones deleted, and an ordered table put in order. */
373
- function byId(path: string[], want: Block, have: Block, table: Table | undefined, context: Context): Diff {
374
- const send: string[] = [];
375
- const show: string[] = [];
376
- const removals: string[] = [];
377
- const removed: string[] = [];
378
-
379
- for (const [key, entry] of want.entries) {
380
- const old = have.entries.get(key);
381
- const where = [...path, bare(entry.id)];
382
-
383
- const replaced = old && changesFixed(table, old.block, entry.block);
384
-
385
- if (!old || replaced) {
386
- if (old) {
387
- send.push(`delete ${old.id}`);
388
- show.push(...shown("-", where, old.block));
389
- }
390
-
391
- send.push(...creation(entry, table));
392
- show.push(...shown("+", where, entry.block));
393
- continue;
394
- }
395
-
396
- const diff = compare(where, entry.block, old.block, undefined, false, context);
397
- if (!diff.send.length) continue;
398
-
399
- send.push(`edit ${entry.id}`, ...diff.send, "next");
400
- show.push(...diff.show);
401
- }
402
-
403
- // An interface on another goes before the one it sits on.
404
- const gone = [...have.entries].filter(([key, entry]) => !want.entries.has(key) && !isFactory(table, entry));
405
- gone.sort(([, a], [, b]) => Number(b.block.settings.has("interface")) - Number(a.block.settings.has("interface")));
406
-
407
- for (const [, entry] of gone) {
408
- removals.push(`delete ${entry.id}`);
409
- removed.push(...shown("-", [...path, bare(entry.id)], entry.block));
410
- }
411
-
412
- if (table?.ordered) {
413
- const moved = moves(want, have);
414
- send.push(...moved);
415
- show.push(...moved.map((line) => `+ ${labelOf(path)}${line}`));
416
- }
417
-
418
- return { send, show, removals, removed };
419
- }
420
-
421
- /** Entries matched by name in a table that refuses two alike: every entry that changes or goes is deleted before any is created, so none is ever briefly the same as another. */
422
- function distinctById(path: string[], want: Block, have: Block, table: Table, context: Context): Diff {
423
- const deletes: string[] = [];
424
- const creates: string[] = [];
425
- const show: string[] = [];
426
-
427
- for (const [key, entry] of want.entries) {
428
- const old = have.entries.get(key);
429
- const where = [...path, bare(entry.id)];
430
-
431
- if (old) {
432
- if (!compare(where, entry.block, old.block, undefined, false, context).send.length) continue;
433
-
434
- deletes.push(`delete ${old.id}`);
435
- show.push(...shown("-", where, old.block));
436
- }
437
-
438
- creates.push(...creation(entry, table));
439
- show.push(...shown("+", where, entry.block));
440
- }
441
-
442
- for (const [key, entry] of have.entries) {
443
- if (want.entries.has(key) || isFactory(table, entry)) continue;
444
-
445
- deletes.push(`delete ${entry.id}`);
446
- show.push(...shown("-", [...path, bare(entry.id)], entry.block));
447
- }
448
-
449
- return { send: [...deletes, ...creates], show, removals: [], removed: [] };
450
- }
451
-
452
- /** The `move` commands that put a table's entries in the render's order. Kept entries hold their place on the device and new ones are added at the end. */
453
- function moves(want: Block, have: Block): string[] {
454
- const wanted = [...want.entries.keys()];
455
- const order = [...have.entries.keys()].filter((key) => want.entries.has(key));
456
- order.push(...wanted.filter((key) => !have.entries.has(key)));
457
-
458
- const send: string[] = [];
459
- const idOf = (key: string) => want.entries.get(key)!.id;
460
-
461
- for (let index = 0; index < wanted.length; index++) {
462
- if (order[index] === wanted[index]) continue;
463
-
464
- const key = wanted[index]!;
465
- send.push(index === 0 ? `move ${idOf(key)} before ${idOf(order[0]!)}` : `move ${idOf(key)} after ${idOf(wanted[index - 1]!)}`);
466
- order.splice(order.indexOf(key), 1);
467
- order.splice(index, 0, key);
468
- }
469
-
470
- return send;
471
- }
472
-
473
- /** An entry's settings in the form two entries compare in, whatever their numbers. */
474
- function signature(block: Block): string {
475
- return [...block.settings]
476
- .map(([key, value]) => `${key} ${canonical(key, value)}`)
477
- .sort()
478
- .join("\n");
479
- }
480
-
481
- /** Entries matched by what they hold. One the device holds and the render does not is deleted; one the render adds takes a number the device does not use. */
482
- function byContent(path: string[], want: Block, have: Block): Diff {
483
- const send: string[] = [];
484
- const show: string[] = [];
485
- const removals: string[] = [];
486
- const removed: string[] = [];
487
-
488
- const unmatched = [...have.entries.values()];
489
- const added: Entry[] = [];
490
-
491
- for (const entry of want.entries.values()) {
492
- const index = unmatched.findIndex((old) => signature(old.block) === signature(entry.block));
493
- if (index >= 0) unmatched.splice(index, 1);
494
- else added.push(entry);
495
- }
496
-
497
- let next = Math.max(0, ...[...have.entries.keys()].map(Number).filter(Number.isInteger)) + 1;
498
-
499
- for (const entry of added) {
500
- const id = String(next++);
501
- send.push(...creation(entry, undefined, id));
502
- show.push(...shown("+", [...path, id], entry.block));
503
- }
504
-
505
- for (const entry of unmatched) {
506
- removals.push(`delete ${entry.id}`);
507
- removed.push(...shown("-", [...path, bare(entry.id)], entry.block));
508
- }
509
-
510
- return { send, show, removals, removed };
511
- }
512
-
513
- /** Settings that name an interface: an entry's own, or a list a section holds, such as OSPF's passive interfaces or the interfaces NTP answers on. */
514
- const NAMES_INTERFACE = new Set(["device", "interface", "srcintf", "dstintf", "intf", "extintf", "update-source", "passive-interface"]);
515
-
516
- /** The interface names a block's settings hold. */
517
- function interfacesNamed(block: Block): string[] {
518
- return [...block.settings].filter(([key]) => NAMES_INTERFACE.has(key)).flatMap(([, value]) => wordsOf(value).map(bare));
519
- }
520
-
521
- /** Tables whose entries are named after an interface, so that releasing the interface deletes its entry: `The vxlan device is currently used` otherwise. */
522
- const KEYED_BY_INTERFACE = new Set(["system dns-server"]);
523
-
524
- /** The tables whose entries are interfaces, so that releasing one frees what names it. */
525
- const INTERFACE_TABLES = new Set(["system interface", "system gre-tunnel", "system vxlan"]);
526
-
527
- /** What changes before anything else: interfaces freed, LAGs that let some ports go, and the ports of LAGs that go, changed after everything else. */
528
- type Release = { freed: Set<string>; gone: Set<string>; trims: { id: string; members: string[] }[]; deferred: Set<string> };
529
-
530
- /**
531
- * What goes before anything else changes.
532
- *
533
- * A port joins a LAG only while nothing names it, and its speed cannot change while it is in one. So a LAG that keeps some of its ports lets the others go first, a port whose speed changes leaves its LAG and joins again later, a LAG whose ports join another goes first, and an interface whose type or parent changes is deleted and created again. Whatever names such an interface on the device is deleted first, or unset when a list names it, and set again later if the render holds it; an interface or tunnel deleted this way frees what names it in turn.
534
- *
535
- * A LAG that goes otherwise is deleted with everything else that goes, after what names it; its ports are changed after that.
536
- */
537
- function released(want: Block, have: Block): Release {
538
- const interfaces = have.blocks.get("system interface") ?? new Block();
539
- const wanted = want.blocks.get("system interface") ?? new Block();
540
- const members = (block: Block | undefined) => wordsOf(block?.settings.get("member") ?? "").map(bare);
541
-
542
- const freed = new Set<string>();
543
- const gone = new Set<string>();
544
- const trims: Release["trims"] = [];
545
- const deferred = new Set<string>();
546
-
547
- // A port in a LAG keeps its speed until it leaves.
548
- const changesSpeed = (port: string) => {
549
- const speed = (block: Block | undefined) => canonical("speed", block?.settings.get("speed") ?? "");
550
- return speed(interfaces.entries.get(port)?.block) !== speed(wanted.entries.get(port)?.block);
551
- };
552
-
553
- for (const [key, entry] of wanted.entries) {
554
- const before = interfaces.entries.get(key)?.block;
555
- for (const member of members(entry.block)) if (!members(before).includes(member)) freed.add(member);
556
-
557
- if (before && changesFixed(TABLES["system interface"], before, entry.block)) freed.add(key);
558
- }
559
-
560
- for (const [key, entry] of interfaces.entries) {
561
- if (entry.block.settings.get("type") !== "aggregate") continue;
562
-
563
- const kept = members(entry.block).filter(
564
- (member) => members(wanted.entries.get(key)?.block).includes(member) && !changesSpeed(member)
565
- );
566
- if (kept.length === members(entry.block).length) continue;
567
-
568
- if (kept.length && wanted.entries.has(key)) {
569
- trims.push({ id: entry.id, members: kept });
570
- continue;
571
- }
572
-
573
- // A LAG that goes, and whose ports join no other, goes with the rest.
574
- if (!wanted.entries.has(key) && !members(entry.block).some((member) => freed.has(member))) {
575
- for (const member of members(entry.block)) deferred.add(member);
576
- continue;
577
- }
578
-
579
- freed.add(key);
580
- gone.add(key);
581
- }
582
-
583
- let grew = true;
584
-
585
- while (grew) {
586
- grew = false;
587
-
588
- for (const [section, block] of have.blocks) {
589
- if (!INTERFACE_TABLES.has(section)) continue;
590
-
591
- for (const [key, entry] of block.entries) {
592
- if (freed.has(key) || !interfacesNamed(entry.block).some((name) => freed.has(name))) continue;
593
-
594
- freed.add(key);
595
- grew = true;
596
- }
597
- }
598
- }
599
-
600
- return { freed, gone, trims, deferred };
601
- }
602
-
603
- /**
604
- * The lines, inside one block, that release what names a freed interface: an entry that names one is deleted, and a list a block holds that names one is unset. The device's tree loses them too, so the plan creates or sets them again. An interface the release deletes itself is in `gone`.
605
- */
606
- /**
607
- * Lists of interfaces that exist only while a setting is on, by the list. NTP serves only on interfaces it names, so its list cannot be emptied while it serves, `There aren't any interfaces with NTP server enabled`; turning it off hides the list and lets go of them.
608
- */
609
- const HELD_BY: Record<string, string> = { interface: "server-mode" };
610
-
611
- function release(
612
- block: Block,
613
- path: string[],
614
- freed: Set<string>,
615
- gone: Set<string>,
616
- isEntry: boolean
617
- ): { send: string[]; show: string[] } {
618
- const send: string[] = [];
619
- const show: string[] = [];
620
- const label = labelOf(path);
621
-
622
- if (!isEntry) {
623
- for (const [key, value] of [...block.settings]) {
624
- if (!NAMES_INTERFACE.has(key) || !wordsOf(value).some((word) => freed.has(bare(word)))) continue;
625
-
626
- // A list that exists only while a setting is on goes with it, and lets go of what it names.
627
- const holder = HELD_BY[key];
628
- if (holder && block.settings.has(holder)) {
629
- send.push(`unset ${holder}`);
630
- show.push(`- ${label}set ${holder} ${block.settings.get(holder)}`, `- ${label}set ${key} ${value}`);
631
- block.settings.delete(holder);
632
- block.settings.delete(key);
633
- continue;
634
- }
635
-
636
- send.push(`unset ${key}`);
637
- show.push(`- ${label}set ${key} ${value}`);
638
- block.settings.delete(key);
639
- }
640
- }
641
-
642
- for (const [header, child] of block.blocks) {
643
- const inner = release(child, [...path, header], freed, gone, false);
644
- if (inner.send.length) send.push(`config ${header}`, ...inner.send, "end");
645
- show.push(...inner.show);
646
- }
647
-
648
- // An interface on another goes before the one it sits on.
649
- const entries = [...block.entries].sort(
650
- ([, a], [, b]) => Number(b.block.settings.has("interface")) - Number(a.block.settings.has("interface"))
651
- );
652
-
653
- for (const [key, entry] of entries) {
654
- const where = [...path, bare(entry.id)];
655
- const named = interfacesNamed(entry.block).some((name) => freed.has(name));
656
-
657
- if (named || gone.has(key)) {
658
- send.push(`delete ${entry.id}`);
659
- show.push(...shown("-", where, entry.block));
660
- block.entries.delete(key);
661
- continue;
662
- }
663
-
664
- const inner = release(entry.block, where, freed, gone, true);
665
- if (inner.send.length) send.push(`edit ${entry.id}`, ...inner.send, "next");
666
- show.push(...inner.show);
667
- }
668
-
669
- return { send, show };
670
- }
671
-
672
- /**
673
- * Addresses that leave an interface, taken off before any is set: the device refuses an address in a subnet another interface still holds, `Subnets overlap`. A primary address that changes takes the secondary ones with it, since they hang on it.
674
- */
675
- function movedAddresses(want: Block, have: Block): Step | undefined {
676
- const wanted = want.blocks.get("system interface");
677
- const held = have.blocks.get("system interface");
678
- if (!wanted || !held) return undefined;
679
-
680
- const send: string[] = [];
681
- const show: string[] = [];
682
- const same = (key: string, a: string | undefined, b: string | undefined) => canonical(key, a ?? "") === canonical(key, b ?? "");
683
-
684
- for (const [key, entry] of held.entries) {
685
- const after = wanted.entries.get(key)?.block;
686
- const before = entry.block;
687
- const lines: string[] = [];
688
- const label = labelOf([bare(entry.id)]);
689
-
690
- const primaryMoves = before.settings.has("ip") && !same("ip", before.settings.get("ip"), after?.settings.get("ip"));
691
- const secondaries = before.blocks.get("secondaryip");
692
- const keptSecondaries = after?.blocks.get("secondaryip");
693
- const goneSecondaries = [...(secondaries?.entries ?? [])].filter(
694
- ([id, secondary]) =>
695
- primaryMoves || !same("ip", secondary.block.settings.get("ip"), keptSecondaries?.entries.get(id)?.block.settings.get("ip"))
696
- );
697
-
698
- if (goneSecondaries.length) {
699
- lines.push("config secondaryip", ...goneSecondaries.map(([, secondary]) => `delete ${secondary.id}`), "end");
700
- for (const [id, secondary] of goneSecondaries) {
701
- show.push(`- ${label}secondaryip ${bare(secondary.id)}: set ip ${secondary.block.settings.get("ip")}`);
702
- secondaries!.entries.delete(id);
703
- }
704
- }
705
-
706
- if (primaryMoves) {
707
- lines.push("unset ip");
708
- show.push(`- ${label}set ip ${before.settings.get("ip")}`);
709
- before.settings.delete("ip");
710
- }
711
-
712
- const ipv6 = before.blocks.get("ipv6");
713
- const keptIpv6 = after?.blocks.get("ipv6");
714
- const extra = ipv6?.blocks.get("ip6-extra-addr");
715
- const goneExtra = [...(extra?.entries ?? [])].filter(([id]) => !keptIpv6?.blocks.get("ip6-extra-addr")?.entries.has(id));
716
- const v6Lines: string[] = [];
717
-
718
- if (goneExtra.length) {
719
- v6Lines.push("config ip6-extra-addr", ...goneExtra.map(([, address]) => `delete ${address.id}`), "end");
720
- for (const [id, address] of goneExtra) {
721
- show.push(`- ${label}ipv6 ip6-extra-addr ${bare(address.id)}`);
722
- extra!.entries.delete(id);
723
- }
724
- }
725
-
726
- if (
727
- ipv6?.settings.has("ip6-address") &&
728
- !same("ip6-address", ipv6.settings.get("ip6-address"), keptIpv6?.settings.get("ip6-address"))
729
- ) {
730
- v6Lines.push("unset ip6-address");
731
- show.push(`- ${label}ipv6: set ip6-address ${ipv6.settings.get("ip6-address")}`);
732
- ipv6.settings.delete("ip6-address");
733
- }
734
-
735
- if (v6Lines.length) lines.push("config ipv6", ...v6Lines, "end");
736
- if (lines.length) send.push(`edit ${entry.id}`, ...lines, "next");
737
- }
738
-
739
- return send.length
740
- ? { title: "system interface, addresses that move", show, send: ["config system interface", ...send, "end"] }
741
- : undefined;
742
- }
743
-
744
- /**
745
- * What changes, by section, in the order the render lists them. Entries that go are deleted after everything else, in the reverse order, so nothing is deleted while another section still names it.
746
- *
747
- * A secret is sent when its entry is new, or with `secrets` set; it cannot be read back to compare.
748
- */
749
- export function plan(desired: string, current: string, options: Pick<ApplyOptions, "secrets">): Plan {
750
- const want = separateTunnels(parse(desired));
751
- const have = separateTunnels(parse(clean(current)));
752
- keepFilled(want, have);
753
-
754
- const first: Step[] = [];
755
- const steps: Step[] = [];
756
- const removals: Step[] = [];
757
-
758
- const { freed, gone, trims, deferred } = released(want, have);
759
- const late = deferredPorts(want, have, deferred, options.secrets);
760
-
761
- if (freed.size) {
762
- for (const section of [...have.blocks.keys()].reverse()) {
763
- const block = have.blocks.get(section)!;
764
-
765
- // A tunnel's interface goes with its tunnel, which the release deletes.
766
- if (section === TUNNELS) {
767
- for (const key of [...block.entries.keys()]) if (freed.has(key)) block.entries.delete(key);
768
- continue;
769
- }
770
-
771
- const { send, show } = release(
772
- block,
773
- [],
774
- freed,
775
- KEYED_BY_INTERFACE.has(section) ? freed : section === "system interface" ? gone : new Set(),
776
- false
777
- );
778
- if (send.length) first.push({ title: `${section}, released`, show, send: [`config ${section}`, ...send, "end"] });
779
- }
780
- }
781
-
782
- if (trims.length) {
783
- const interfaces = have.blocks.get("system interface")!;
784
- const send = trims.flatMap(({ id, members }) => [`edit ${id}`, `set member ${quoteAll(members)}`, "next"]);
785
- const show = trims.map(({ id, members }) => `+ ${bare(id)}: set member ${quoteAll(members)}`);
786
- first.push({ title: "system interface, ports leaving a LAG", show, send: ["config system interface", ...send, "end"] });
787
-
788
- for (const { id, members } of trims) interfaces.entries.get(bare(id))!.block.settings.set("member", quoteAll(members));
789
- }
790
-
791
- const moved = movedAddresses(want, have);
792
- if (moved) first.push(moved);
793
-
794
- for (const [section, block] of want.blocks) {
795
- const header = section === TUNNELS ? "system interface" : section;
796
- const context = { section: header, secrets: options.secrets };
797
- const table = section === TUNNELS ? undefined : TABLES[header];
798
- const diff = compare([], block, have.blocks.get(section) ?? new Block(), table, true, context);
799
-
800
- if (diff.send.length) steps.push({ title: section, show: diff.show, send: [`config ${header}`, ...diff.send, "end"] });
801
-
802
- // A tunnel's interface goes with its tunnel.
803
- if (diff.removals.length && section !== TUNNELS) {
804
- removals.unshift({ title: `${section}, removed`, show: diff.removed, send: [`config ${header}`, ...diff.removals, "end"] });
805
- }
806
- }
807
-
808
- // Leaving transparent mode needs an address and an interface for it, which the config of a device without a bridge does not say.
809
- const opmode = (root: Block) => root.blocks.get("system settings")?.settings.get("opmode");
810
- const problems =
811
- opmode(have) === "transparent" && opmode(want) !== "transparent"
812
- ? [
813
- "the device runs in transparent mode and the config has no bridge; leave transparent mode by hand, `set opmode nat` with an address and an interface under `config system settings`, then plan again",
814
- ]
815
- : [];
816
-
817
- return { steps: [...first, ...steps, ...removals, ...late], problems };
818
- }
819
-
820
- /** The interfaces of tunnels, which their own tables create, compared after those tables as a section of their own. */
821
- const TUNNELS = "system interface, tunnels";
822
-
823
- /** The tree with each tunnel's interface moved from `system interface` to a section of its own, just after the tunnels' tables. The interface it leaves through is its tunnel's, and is left to the tunnel's table. */
824
- function separateTunnels(root: Block): Block {
825
- const interfaces = root.blocks.get("system interface");
826
- if (!interfaces) return root;
827
-
828
- const tunnels = new Block();
829
-
830
- for (const [key, entry] of interfaces.entries) {
831
- const type = entry.block.settings.get("type");
832
- if (type !== "tunnel" && type !== "vxlan") continue;
833
-
834
- entry.block.settings.delete("interface");
835
- tunnels.entries.set(key, entry);
836
- interfaces.entries.delete(key);
837
- }
838
-
839
- const sections = [...root.blocks];
840
- root.blocks.clear();
841
-
842
- for (const [section, block] of sections) {
843
- root.blocks.set(section, block);
844
- if (section === "system vxlan") root.blocks.set(TUNNELS, tunnels);
845
- }
846
-
847
- if (!root.blocks.has(TUNNELS)) root.blocks.set(TUNNELS, tunnels);
848
- return root;
849
- }
850
-
851
- /** A prefix list `refresh` fills keeps the entries the device holds. */
852
- function keepFilled(want: Block, have: Block): void {
853
- for (const table of Object.values(PREFIX_LIST)) {
854
- for (const [key, entry] of want.blocks.get(table)?.entries ?? []) {
855
- if (!bare(entry.block.settings.get("comments") ?? "").startsWith(FILLED)) continue;
856
-
857
- const held = have.blocks.get(table)?.entries.get(key)?.block.blocks.get("rule");
858
- if (held) entry.block.blocks.set("rule", held);
859
- }
860
- }
861
- }
862
-
863
- /** The ports of a LAG that goes, taken out of the main comparison and changed in a step of their own once the LAG is deleted. */
864
- function deferredPorts(want: Block, have: Block, ports: Set<string>, secrets: boolean): Step[] {
865
- const wanted = want.blocks.get("system interface");
866
- const held = have.blocks.get("system interface");
867
- if (!ports.size || !wanted || !held) return [];
868
-
869
- const later = { want: new Block(), have: new Block() };
870
-
871
- for (const port of ports) {
872
- const after = wanted.entries.get(port);
873
- const before = held.entries.get(port);
874
- if (!after || !before) continue;
875
-
876
- later.want.entries.set(port, after);
877
- later.have.entries.set(port, before);
878
- wanted.entries.delete(port);
879
- held.entries.delete(port);
880
- }
881
-
882
- const diff = compare([], later.want, later.have, TABLES["system interface"], true, { section: "system interface", secrets });
883
- if (!diff.send.length) return [];
884
-
885
- return [
886
- { title: "system interface, ports of a LAG that went", show: diff.show, send: ["config system interface", ...diff.send, "end"] },
887
- ];
888
- }