@takodotid/circuit 0.34.3 → 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.
- package/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11345 -9024
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
// What it takes to make a RouterOS device run exactly what was rendered. Everything the device holds that the render does not is removed or reset.
|
|
2
|
-
//
|
|
3
|
-
// Two phases. Additions and changes go first, in dependency order, so nothing ever points at something that does not exist yet. Removals go last, in reverse order, so nothing is removed while something still uses it.
|
|
4
|
-
|
|
5
|
-
import type { ApplyOptions, Plan, Step } from "../types";
|
|
6
|
-
import { canonical, CLEARED_WITH, type Command, defaultOf, identity, isSecretMarker, normal, ORDERED, parse, SECRET_KEYS } from "./export";
|
|
7
|
-
|
|
8
|
-
/** A plan line the apply handles itself: upload a certificate and its key, and import them. */
|
|
9
|
-
export const INSTALL_CERTIFICATE = "#install-certificate ";
|
|
10
|
-
|
|
11
|
-
/** Dependency order. A menu not listed sorts after all of these. The longest matching prefix decides. */
|
|
12
|
-
const ORDER = [
|
|
13
|
-
// Certificates depend on nothing, and services name them.
|
|
14
|
-
"/certificate",
|
|
15
|
-
"/interface bridge",
|
|
16
|
-
"/interface ethernet",
|
|
17
|
-
"/interface bonding",
|
|
18
|
-
"/interface vlan",
|
|
19
|
-
"/interface macvlan",
|
|
20
|
-
"/interface gre",
|
|
21
|
-
"/interface vxlan",
|
|
22
|
-
"/interface wireguard",
|
|
23
|
-
"/interface vrrp",
|
|
24
|
-
"/interface bridge port",
|
|
25
|
-
"/interface bridge vlan",
|
|
26
|
-
"/interface bridge filter",
|
|
27
|
-
"/interface list",
|
|
28
|
-
"/ip vrf",
|
|
29
|
-
"/ip address",
|
|
30
|
-
"/ipv6 address",
|
|
31
|
-
"/ip pool",
|
|
32
|
-
"/ip dhcp-server",
|
|
33
|
-
"/ip firewall address-list",
|
|
34
|
-
"/ipv6 firewall address-list",
|
|
35
|
-
"/ip firewall",
|
|
36
|
-
"/ipv6 firewall",
|
|
37
|
-
"/routing rpki",
|
|
38
|
-
"/routing filter rule",
|
|
39
|
-
"/routing bgp instance",
|
|
40
|
-
"/routing bgp connection",
|
|
41
|
-
"/routing ospf instance",
|
|
42
|
-
"/routing ospf area",
|
|
43
|
-
"/routing ospf interface-template",
|
|
44
|
-
"/ip",
|
|
45
|
-
"/ipv6",
|
|
46
|
-
"/routing",
|
|
47
|
-
"/interface ethernet switch",
|
|
48
|
-
"/snmp",
|
|
49
|
-
"/tool",
|
|
50
|
-
"/user",
|
|
51
|
-
// A logging rule names its action, so the action exists first.
|
|
52
|
-
"/system logging action",
|
|
53
|
-
"/system",
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
function rank(menu: string): number {
|
|
57
|
-
let best = ORDER.length;
|
|
58
|
-
let bestLength = 0;
|
|
59
|
-
ORDER.forEach((prefix, index) => {
|
|
60
|
-
const matches = menu === prefix || menu.startsWith(prefix + " ");
|
|
61
|
-
if (matches && prefix.length > bestLength) {
|
|
62
|
-
best = index;
|
|
63
|
-
bestLength = prefix.length;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
return best;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Menus that also hold entries the device learned rather than was told. A selector must not match those. */
|
|
70
|
-
const DYNAMIC = new Set([
|
|
71
|
-
"/ip route",
|
|
72
|
-
"/ipv6 route",
|
|
73
|
-
"/ip address",
|
|
74
|
-
"/ipv6 address",
|
|
75
|
-
"/ip firewall address-list",
|
|
76
|
-
"/ipv6 firewall address-list",
|
|
77
|
-
"/ip firewall filter",
|
|
78
|
-
"/ipv6 firewall filter",
|
|
79
|
-
"/ip firewall nat",
|
|
80
|
-
"/ipv6 firewall nat",
|
|
81
|
-
"/ip firewall mangle",
|
|
82
|
-
"/ipv6 firewall mangle",
|
|
83
|
-
"/ip firewall raw",
|
|
84
|
-
"/ipv6 firewall raw",
|
|
85
|
-
"/ip dhcp-server lease",
|
|
86
|
-
"/interface bridge vlan",
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
/** Quote a value for a command. */
|
|
90
|
-
const quote = (value: string) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$")}"`;
|
|
91
|
-
|
|
92
|
-
/** A value as a command takes it: bare when plain, quoted otherwise. */
|
|
93
|
-
const value = (raw: string) => (/^[\w.:/@,!-]+$/.test(raw) ? raw : quote(raw));
|
|
94
|
-
|
|
95
|
-
/** In a `find`, a quoted `yes` is a string and matches no boolean, while an unquoted slash matches nothing. Plain words go bare, anything else quoted. */
|
|
96
|
-
const findValue = (raw: string) => (/^[\w.:-]+$/.test(raw) ? raw : quote(raw));
|
|
97
|
-
|
|
98
|
-
/** A selector for the entry with this identity. */
|
|
99
|
-
function find(menu: string, id: Record<string, string>): string {
|
|
100
|
-
const conditions = Object.entries(id).map(([property, raw]) => `${property}=${findValue(raw)}`);
|
|
101
|
-
if (DYNAMIC.has(menu)) conditions.push("!dynamic");
|
|
102
|
-
return `[ find ${conditions.join(" and ")} ]`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** A whole command as sent: menu, verb, selector, flags, then properties. */
|
|
106
|
-
function line(menu: string, verb: string, selector: string, flags: string[], attrs: Record<string, string>): string {
|
|
107
|
-
const properties = Object.entries(attrs).map(([property, raw]) => `${property}=${value(raw)}`);
|
|
108
|
-
return [menu, verb, selector, ...flags, ...properties].filter(Boolean).join(" ");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const describe = (attrs: Record<string, string>) =>
|
|
112
|
-
Object.entries(attrs)
|
|
113
|
-
.map(([property, raw]) => `${property}=${raw}`)
|
|
114
|
-
.join(" ");
|
|
115
|
-
|
|
116
|
-
/** Group commands by menu. */
|
|
117
|
-
function byMenu(commands: Command[]): Map<string, Command[]> {
|
|
118
|
-
const groups = new Map<string, Command[]>();
|
|
119
|
-
|
|
120
|
-
for (const command of commands) {
|
|
121
|
-
const group = groups.get(command.menu) ?? [];
|
|
122
|
-
group.push(command);
|
|
123
|
-
groups.set(command.menu, group);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return groups;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** What is planned for one menu: changes for the first phase, removals for the last. */
|
|
130
|
-
type MenuPlan = { first: Step[]; last: Step[] };
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* The properties to send so that the device's entry matches the rendered one. A property the device holds and the render does not state is reset to its default.
|
|
134
|
-
*/
|
|
135
|
-
function changes(
|
|
136
|
-
wanted: Command,
|
|
137
|
-
existing: Command | undefined,
|
|
138
|
-
sendSecrets: boolean,
|
|
139
|
-
problems: string[],
|
|
140
|
-
label: string
|
|
141
|
-
): Record<string, string> {
|
|
142
|
-
const want = normal(wanted);
|
|
143
|
-
const have = existing ? normal(existing) : {};
|
|
144
|
-
const result: Record<string, string> = {};
|
|
145
|
-
|
|
146
|
-
for (const [property, desired] of Object.entries(want)) {
|
|
147
|
-
if (have[property] !== desired) result[property] = desired;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
for (const property of Object.keys(have)) {
|
|
151
|
-
if (property in want) continue;
|
|
152
|
-
|
|
153
|
-
const clearedWith = CLEARED_WITH[wanted.menu]?.[property];
|
|
154
|
-
if (clearedWith && !(clearedWith in want)) continue;
|
|
155
|
-
|
|
156
|
-
const fallback = defaultOf(wanted, property);
|
|
157
|
-
if (fallback === undefined) {
|
|
158
|
-
problems.push(`${label}: ${property}=${have[property]} is set on the device and its default is unknown; add it to DEFAULTS`);
|
|
159
|
-
} else {
|
|
160
|
-
result[property] = fallback;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (sendSecrets) {
|
|
165
|
-
for (const [property, raw] of Object.entries(wanted.attrs)) {
|
|
166
|
-
if (SECRET_KEYS.has(property) || isSecretMarker(raw)) result[property] = raw;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Settings addressed by a selector: `set ftp ...`, `set [ find default=yes ] ...`. They are changed, never added or removed. */
|
|
174
|
-
function planSettings(menu: string, wanted: Command[], existing: Command[], sendSecrets: boolean, problems: string[]): Step | undefined {
|
|
175
|
-
const step: Step = { title: menu, show: [], send: [] };
|
|
176
|
-
const wantedBySelector = new Map(wanted.filter((item) => item.verb === "set").map((item) => [item.selector, item]));
|
|
177
|
-
const existingBySelector = new Map(existing.filter((item) => item.verb === "set").map((item) => [item.selector, item]));
|
|
178
|
-
|
|
179
|
-
for (const selector of new Set([...wantedBySelector.keys(), ...existingBySelector.keys()])) {
|
|
180
|
-
const want = wantedBySelector.get(selector);
|
|
181
|
-
const have = existingBySelector.get(selector);
|
|
182
|
-
// A setting only the device states is reset by comparing it with nothing.
|
|
183
|
-
const target = want ?? { ...have!, attrs: {} };
|
|
184
|
-
const update = changes(target, have, sendSecrets, problems, `${menu} ${selector}`);
|
|
185
|
-
if (!Object.keys(update).length) continue;
|
|
186
|
-
|
|
187
|
-
step.show.push(`~ set ${selector} ${describe(update)}`.replace(/\s+/g, " "));
|
|
188
|
-
step.send.push(line(menu, "set", selector, [], update));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return step.send.length ? step : undefined;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Entries, told apart by identity. New ones are added, changed ones set, and ones the render does not hold removed in the last phase. */
|
|
195
|
-
function planEntries(menu: string, wanted: Command[], existing: Command[], sendSecrets: boolean, problems: string[]): MenuPlan {
|
|
196
|
-
const result: MenuPlan = { first: [], last: [] };
|
|
197
|
-
const changeStep: Step = { title: menu, show: [], send: [] };
|
|
198
|
-
const removeStep: Step = { title: menu, show: [], send: [] };
|
|
199
|
-
const key = (c: Command) => JSON.stringify(identity(c));
|
|
200
|
-
|
|
201
|
-
const wantedEntries = wanted.filter((item) => item.verb === "add");
|
|
202
|
-
const existingEntries = existing.filter((item) => item.verb === "add");
|
|
203
|
-
|
|
204
|
-
const existingByKey = new Map<string, Command>();
|
|
205
|
-
|
|
206
|
-
for (const entry of existingEntries) {
|
|
207
|
-
const entryKey = key(entry);
|
|
208
|
-
if (existingByKey.has(entryKey)) problems.push(`${menu}: two entries share ${entryKey}, so neither can be addressed alone`);
|
|
209
|
-
existingByKey.set(entryKey, entry);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const kept = new Set<string>();
|
|
213
|
-
|
|
214
|
-
for (const entry of wantedEntries) {
|
|
215
|
-
const entryKey = key(entry);
|
|
216
|
-
kept.add(entryKey);
|
|
217
|
-
const current = existingByKey.get(entryKey);
|
|
218
|
-
|
|
219
|
-
if (!current) {
|
|
220
|
-
changeStep.show.push(`+ ${canonical(entry)}`);
|
|
221
|
-
changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// A flag such as `blackhole` cannot be changed on an existing entry. It is replaced.
|
|
226
|
-
const flagsDiffer = [...entry.flags].sort().join() !== [...current.flags].sort().join();
|
|
227
|
-
if (flagsDiffer) {
|
|
228
|
-
changeStep.show.push(`~ replace ${canonical(entry)}`);
|
|
229
|
-
removeStep.send.push(`${menu} remove ${find(menu, identity(current))}`);
|
|
230
|
-
changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const update = changes(entry, current, sendSecrets, problems, `${menu} ${entryKey}`);
|
|
235
|
-
if (!Object.keys(update).length) continue;
|
|
236
|
-
|
|
237
|
-
changeStep.show.push(`~ ${entryKey} ${describe(update)}`);
|
|
238
|
-
changeStep.send.push(line(menu, "set", find(menu, identity(current)), [], update));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const removed = existingEntries.filter((entry) => !kept.has(key(entry)));
|
|
242
|
-
// An address list that goes entirely goes in one command, not one per entry.
|
|
243
|
-
const wholeLists = new Set<string>();
|
|
244
|
-
if (menu.endsWith("address-list")) {
|
|
245
|
-
for (const entry of removed) {
|
|
246
|
-
const list = entry.attrs.list!;
|
|
247
|
-
if (!wantedEntries.some((candidate) => candidate.attrs.list === list)) wholeLists.add(list);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
for (const list of wholeLists) {
|
|
252
|
-
const count = removed.filter((entry) => entry.attrs.list === list).length;
|
|
253
|
-
removeStep.show.push(`- list ${list}, ${count} entr${count === 1 ? "y" : "ies"}`);
|
|
254
|
-
removeStep.send.push(`${menu} remove [ find list=${quote(list)} and !dynamic ]`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
for (const entry of removed) {
|
|
258
|
-
if (wholeLists.has(entry.attrs.list ?? "")) continue;
|
|
259
|
-
|
|
260
|
-
removeStep.show.push(`- ${canonical(entry)}`);
|
|
261
|
-
removeStep.send.push(`${menu} remove ${find(menu, identity(entry))}`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (changeStep.send.length) result.first.push(changeStep);
|
|
265
|
-
if (removeStep.send.length) result.last.push(removeStep);
|
|
266
|
-
return result;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Certificates are installed from the config when missing or different, after the old one of that name is removed. One the device holds under another name, with the same fingerprint and key, is renamed rather than imported a second time. One the config does not hold is removed in the last phase.
|
|
271
|
-
*/
|
|
272
|
-
function planCertificates(wanted: Command[], existing: Command[]): MenuPlan {
|
|
273
|
-
const menu = "/certificate";
|
|
274
|
-
const result: MenuPlan = { first: [], last: [] };
|
|
275
|
-
const install: Step = { title: menu, show: [], send: [] };
|
|
276
|
-
const remove: Step = { title: menu, show: [], send: [] };
|
|
277
|
-
const existingByName = new Map(existing.map((item) => [item.attrs.name, item]));
|
|
278
|
-
const wantedNames = new Set(wanted.map((item) => item.attrs.name));
|
|
279
|
-
const renamed = new Set<string>();
|
|
280
|
-
|
|
281
|
-
for (const entry of wanted) {
|
|
282
|
-
const name = entry.attrs.name!;
|
|
283
|
-
const current = existingByName.get(name);
|
|
284
|
-
if (current && canonical(current) === canonical(entry)) continue;
|
|
285
|
-
|
|
286
|
-
const sameCertificate = existing.find(
|
|
287
|
-
(item) =>
|
|
288
|
-
!wantedNames.has(item.attrs.name) &&
|
|
289
|
-
!renamed.has(item.attrs.name!) &&
|
|
290
|
-
item.attrs.fingerprint === entry.attrs.fingerprint &&
|
|
291
|
-
item.attrs["private-key"] === entry.attrs["private-key"]
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
if (!current && sameCertificate) {
|
|
295
|
-
const oldName = sameCertificate.attrs.name!;
|
|
296
|
-
renamed.add(oldName);
|
|
297
|
-
install.show.push(`~ rename ${oldName} to ${name}`);
|
|
298
|
-
install.send.push(`${menu} set [ find name=${quote(oldName)} ] name=${quote(name)}`);
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
install.show.push(`${current ? "~" : "+"} install ${name}`);
|
|
303
|
-
if (current) install.send.push(`${menu} remove [ find name=${quote(name)} ]`);
|
|
304
|
-
install.send.push(INSTALL_CERTIFICATE + name);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
for (const entry of existing) {
|
|
308
|
-
if (wantedNames.has(entry.attrs.name) || renamed.has(entry.attrs.name!)) continue;
|
|
309
|
-
|
|
310
|
-
remove.show.push(`- ${entry.attrs.name}`);
|
|
311
|
-
remove.send.push(`${menu} remove [ find name=${quote(entry.attrs.name!)} ]`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (install.send.length) result.first.push(install);
|
|
315
|
-
if (remove.send.length) result.last.push(remove);
|
|
316
|
-
return result;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** The variable holding the old rules of an ordered menu while the new ones are added. Removed when the rewrite ends. */
|
|
320
|
-
const OLD_RULES = "frameworkOldRules";
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* An ordered menu is rewritten whole whenever it differs, so the device lists its rules in the order the render gives.
|
|
324
|
-
*
|
|
325
|
-
* The old rules are captured, the new ones added after them, then the captured ones removed. Until that moment the old rules keep deciding, so no chain is ever empty or half new.
|
|
326
|
-
*
|
|
327
|
-
* In a menu with chains, an old rule is removed in this step only if its chain is still wanted. A chain no longer wanted is removed in the last phase, after whatever pointed at it has been changed.
|
|
328
|
-
*/
|
|
329
|
-
function planOrdered(menu: string, wanted: Command[], existing: Command[]): MenuPlan {
|
|
330
|
-
const result: MenuPlan = { first: [], last: [] };
|
|
331
|
-
const same = wanted.map((item) => canonical(item)).join("\n") === existing.map((item) => canonical(item)).join("\n");
|
|
332
|
-
if (same) return result;
|
|
333
|
-
|
|
334
|
-
const where = DYNAMIC.has(menu) ? " where !dynamic" : "";
|
|
335
|
-
const hasChains = [...wanted, ...existing].some((item) => item.attrs.chain !== undefined);
|
|
336
|
-
const keptChains = new Set(wanted.map((item) => item.attrs.chain ?? ""));
|
|
337
|
-
const droppedChains = [...new Set(existing.map((item) => item.attrs.chain ?? ""))].filter((chain) => !keptChains.has(chain));
|
|
338
|
-
|
|
339
|
-
let removeOld = `${menu} remove $${OLD_RULES}`;
|
|
340
|
-
if (hasChains) {
|
|
341
|
-
const keep = [...keptChains].map((chain) => quote(chain)).join(";");
|
|
342
|
-
const inKeptChain = `[:typeof [:find $keep [${menu} get $id chain]]] != "nil"`;
|
|
343
|
-
removeOld = `:local keep {${keep}}; :foreach id in=$${OLD_RULES} do={ :if (${inKeptChain}) do={ ${menu} remove $id } }`;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const send = [
|
|
347
|
-
// A global survives between the commands of one run, so the rules captured here are the ones removed below.
|
|
348
|
-
`:global ${OLD_RULES} [${menu} find${where}]`,
|
|
349
|
-
...wanted.map((item) => line(menu, "add", "", item.flags, item.attrs)),
|
|
350
|
-
`:global ${OLD_RULES}; ${removeOld}; /system script environment remove [find name="${OLD_RULES}"]`,
|
|
351
|
-
];
|
|
352
|
-
const show = [...existing.map((item) => `- ${canonical(item)}`), ...wanted.map((item) => `+ ${canonical(item)}`)];
|
|
353
|
-
result.first.push({ title: `${menu}, in the order of the config`, show, send });
|
|
354
|
-
|
|
355
|
-
if (hasChains) {
|
|
356
|
-
for (const chain of droppedChains) {
|
|
357
|
-
const selector = `[ find chain=${quote(chain)}${DYNAMIC.has(menu) ? " and !dynamic" : ""} ]`;
|
|
358
|
-
result.last.push({ title: `${menu} chain ${chain}`, show: [`- chain ${chain}`], send: [`${menu} remove ${selector}`] });
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return result;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export function plan(desired: string, current: string, options: Pick<ApplyOptions, "secrets">): Plan {
|
|
366
|
-
const want = parse(desired);
|
|
367
|
-
const have = parse(current);
|
|
368
|
-
|
|
369
|
-
// Entries of lists filled by `refresh` belong to it, not to the render.
|
|
370
|
-
const owned = (command: Command) => !(command.menu.endsWith("address-list") && want.generated.has(command.attrs.list ?? ""));
|
|
371
|
-
const wantedByMenu = byMenu(want.commands.filter(owned));
|
|
372
|
-
const existingByMenu = byMenu(have.commands.filter(owned));
|
|
373
|
-
|
|
374
|
-
const menus = [...new Set([...wantedByMenu.keys(), ...existingByMenu.keys()])].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
375
|
-
const first: Step[] = [];
|
|
376
|
-
const last: Step[] = [];
|
|
377
|
-
const problems: string[] = [];
|
|
378
|
-
|
|
379
|
-
for (const menu of menus) {
|
|
380
|
-
const wanted = wantedByMenu.get(menu) ?? [];
|
|
381
|
-
const existing = existingByMenu.get(menu) ?? [];
|
|
382
|
-
|
|
383
|
-
let menuPlan: MenuPlan;
|
|
384
|
-
if (menu === "/certificate") menuPlan = planCertificates(wanted, existing);
|
|
385
|
-
else if (ORDERED.has(menu)) menuPlan = planOrdered(menu, wanted, existing);
|
|
386
|
-
else {
|
|
387
|
-
menuPlan = planEntries(menu, wanted, existing, options.secrets, problems);
|
|
388
|
-
const settings = planSettings(menu, wanted, existing, options.secrets, problems);
|
|
389
|
-
if (settings) menuPlan.first.unshift(settings);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
first.push(...menuPlan.first);
|
|
393
|
-
// Removals run in reverse dependency order, so a menu's are prepended.
|
|
394
|
-
last.unshift(...menuPlan.last);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return { steps: [...first, ...last], problems };
|
|
398
|
-
}
|