@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.
- package/README.md +14 -15
- 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 +18 -12
- 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,321 +0,0 @@
|
|
|
1
|
-
// Reading an export into commands, and normalising both sides so that only a real difference survives a comparison.
|
|
2
|
-
|
|
3
|
-
import { network } from "../../../core/addr";
|
|
4
|
-
|
|
5
|
-
export type Command = {
|
|
6
|
-
/** The menu the command belongs to, such as `/ip address`. */
|
|
7
|
-
menu: string;
|
|
8
|
-
verb: "add" | "set";
|
|
9
|
-
/** What a `set` addresses: `[ find default-name=ether1 ]`, `ftp`, `1`, or nothing for a menu of one. */
|
|
10
|
-
selector: string;
|
|
11
|
-
/** Words without a value, such as `blackhole`. */
|
|
12
|
-
flags: string[];
|
|
13
|
-
/** Properties, unquoted and unescaped. */
|
|
14
|
-
attrs: Record<string, string>;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type Parsed = {
|
|
18
|
-
commands: Command[];
|
|
19
|
-
/** Address lists whose entries are filled by `refresh`, not by the render. */
|
|
20
|
-
generated: Set<string>;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** Undo the line wrapping an export applies: a backslash at the end of a line continues it. */
|
|
24
|
-
const unwrap = (text: string) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
|
|
25
|
-
|
|
26
|
-
/** A quoted value without its quotes and escapes. `\_` is an escaped space. */
|
|
27
|
-
function unquote(value: string): string {
|
|
28
|
-
if (!value.startsWith('"')) return value;
|
|
29
|
-
|
|
30
|
-
return value.slice(1, -1).replace(/\\(.)/g, (_, escaped: string) => (escaped === "_" ? " " : escaped));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Split a command into words, keeping quoted values and bracketed selectors whole. */
|
|
34
|
-
function words(line: string): string[] {
|
|
35
|
-
const result: string[] = [];
|
|
36
|
-
let current = "";
|
|
37
|
-
let inQuotes = false;
|
|
38
|
-
let bracketDepth = 0;
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < line.length; i++) {
|
|
41
|
-
const char = line[i]!;
|
|
42
|
-
if (inQuotes) {
|
|
43
|
-
current += char;
|
|
44
|
-
if (char === "\\") current += line[++i] ?? "";
|
|
45
|
-
else if (char === '"') inQuotes = false;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (char === '"') inQuotes = true;
|
|
50
|
-
if (char === "[") bracketDepth++;
|
|
51
|
-
if (char === "]") bracketDepth--;
|
|
52
|
-
if (char === " " && bracketDepth === 0) {
|
|
53
|
-
if (current) result.push(current);
|
|
54
|
-
current = "";
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
current += char;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (current) result.push(current);
|
|
62
|
-
return result;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** One `add` or `set` line into a command. */
|
|
66
|
-
function parseLine(menu: string, line: string): Command {
|
|
67
|
-
const [verb, ...rest] = words(line) as ["add" | "set", ...string[]];
|
|
68
|
-
|
|
69
|
-
let selector = "";
|
|
70
|
-
const first = rest[0];
|
|
71
|
-
if (verb === "set" && first && (first.startsWith("[") || !first.includes("="))) {
|
|
72
|
-
selector = rest.shift()!.replace(/\s+/g, " ");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const flags: string[] = [];
|
|
76
|
-
const attrs: Record<string, string> = {};
|
|
77
|
-
// A property of the same group as the one before it is written `.name`, which only reads back inside its own command, so it is expanded here.
|
|
78
|
-
let group = "";
|
|
79
|
-
|
|
80
|
-
for (const word of rest) {
|
|
81
|
-
const equals = word.indexOf("=");
|
|
82
|
-
if (equals < 0) {
|
|
83
|
-
flags.push(word);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let name = word.slice(0, equals);
|
|
88
|
-
if (name.startsWith(".")) name = group + name;
|
|
89
|
-
else if (name.includes(".")) group = name.split(".")[0]!;
|
|
90
|
-
attrs[name] = unquote(word.slice(equals + 1));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { menu, verb, selector, flags, attrs };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function parse(text: string): Parsed {
|
|
97
|
-
const commands: Command[] = [];
|
|
98
|
-
const generated = new Set<string>();
|
|
99
|
-
let menu = "";
|
|
100
|
-
|
|
101
|
-
for (const raw of unwrap(text).split("\n")) {
|
|
102
|
-
const line = raw.trim();
|
|
103
|
-
if (line.startsWith("/")) {
|
|
104
|
-
menu = line;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const marker = /^# generated (\S+)$/.exec(line);
|
|
109
|
-
if (marker) generated.add(marker[1]!);
|
|
110
|
-
if (menu && /^(add|set)( |$)/.test(line)) commands.push(parseLine(menu, line));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return { commands, generated };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Values the platform holds when nothing was set, per menu, and under `*` for every menu.
|
|
118
|
-
*
|
|
119
|
-
* A property at its default is dropped from both sides of a comparison, and this is the value a property the config does not state is reset to. A property the device holds that is not here cannot be reset, which the plan reports as a problem.
|
|
120
|
-
*/
|
|
121
|
-
export const DEFAULTS: Record<string, Record<string, string>> = {
|
|
122
|
-
"*": { disabled: "no", comment: "", instance: "default", "routing-table": "main", vrf: "main" },
|
|
123
|
-
"/interface bridge": { "protocol-mode": "rstp", priority: "0x8000", "vlan-filtering": "no", pvid: "1" },
|
|
124
|
-
"/interface bridge port": { pvid: "1", "frame-types": "admit-all", edge: "auto", "path-cost": "10", priority: "0x80" },
|
|
125
|
-
"/interface bonding": { mode: "balance-rr", "transmit-hash-policy": "layer-2", mtu: "1500" },
|
|
126
|
-
"/interface ethernet": { mtu: "1500", "auto-negotiation": "yes" },
|
|
127
|
-
"/interface ethernet switch port": {
|
|
128
|
-
"storm-rate": "100",
|
|
129
|
-
"limit-broadcasts": "yes",
|
|
130
|
-
"limit-unknown-multicasts": "no",
|
|
131
|
-
"limit-unknown-unicasts": "no",
|
|
132
|
-
},
|
|
133
|
-
"/interface vlan": { "l3-hw-offloading": "yes", mtu: "1500" },
|
|
134
|
-
"/interface macvlan": { mode: "bridge", mtu: "1500" },
|
|
135
|
-
"/interface vrrp": { priority: "100", "preemption-mode": "yes", interval: "1s", version: "3", "v3-protocol": "ipv4" },
|
|
136
|
-
"/interface vxlan": { port: "4789" },
|
|
137
|
-
"/interface wireguard peers": {
|
|
138
|
-
"client-allowed-address": "",
|
|
139
|
-
"endpoint-address": "",
|
|
140
|
-
"endpoint-port": "0",
|
|
141
|
-
"persistent-keepalive": "0s",
|
|
142
|
-
},
|
|
143
|
-
"/ipv6 address": { advertise: "yes" },
|
|
144
|
-
"/ip dhcp-server": { "lease-time": "30m" },
|
|
145
|
-
"/ip dns": { servers: "", "allow-remote-requests": "no" },
|
|
146
|
-
"/ip service": { address: "", certificate: "none" },
|
|
147
|
-
"/ip ssh": { "strong-crypto": "no" },
|
|
148
|
-
"/ip traffic-flow": { enabled: "no", interfaces: "all", "packet-sampling": "no" },
|
|
149
|
-
"/tool mac-server": { "allowed-interface-list": "all" },
|
|
150
|
-
"/tool mac-server mac-winbox": { "allowed-interface-list": "all" },
|
|
151
|
-
"/tool mac-server ping": { enabled: "yes" },
|
|
152
|
-
"/tool bandwidth-server": { enabled: "yes" },
|
|
153
|
-
"/ip neighbor discovery-settings": { "discover-interface-list": "!dynamic", protocol: "cdp,lldp,mndp" },
|
|
154
|
-
"/snmp": { enabled: "no", contact: "", location: "" },
|
|
155
|
-
"/snmp community": {
|
|
156
|
-
addresses: "::/0",
|
|
157
|
-
name: "public",
|
|
158
|
-
security: "none",
|
|
159
|
-
"authentication-protocol": "MD5",
|
|
160
|
-
"encryption-protocol": "DES",
|
|
161
|
-
"read-access": "yes",
|
|
162
|
-
"write-access": "no",
|
|
163
|
-
},
|
|
164
|
-
"/ip settings": { "send-redirects": "yes", "tcp-syncookies": "no" },
|
|
165
|
-
"/system clock": { "time-zone-autodetect": "yes", "time-zone-name": "manual" },
|
|
166
|
-
"/system note": { note: "", "show-at-login": "yes" },
|
|
167
|
-
"/system ntp client": { enabled: "no" },
|
|
168
|
-
"/system ntp server": { enabled: "no" },
|
|
169
|
-
"/system logging action": { "disk-file-count": "2", "disk-lines-per-file": "1000" },
|
|
170
|
-
"/system package update": { channel: "stable" },
|
|
171
|
-
"/system routerboard settings": { "enter-setup-on": "any-key" },
|
|
172
|
-
"/interface ethernet switch": { "l3-hw-offloading": "no" },
|
|
173
|
-
"/interface ethernet switch l3hw-settings": { autorestart: "no", "ipv6-hw": "no" },
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
/** Whether each `/ip service` ships disabled. */
|
|
177
|
-
const SERVICE_DISABLED: Record<string, string> = {
|
|
178
|
-
ftp: "no",
|
|
179
|
-
ssh: "no",
|
|
180
|
-
telnet: "no",
|
|
181
|
-
www: "no",
|
|
182
|
-
"www-ssl": "yes",
|
|
183
|
-
"reverse-proxy": "no",
|
|
184
|
-
winbox: "no",
|
|
185
|
-
api: "no",
|
|
186
|
-
"api-ssl": "no",
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
/** Helpers of `/ip firewall service-port` that ship disabled. The rest ship enabled. */
|
|
190
|
-
const HELPER_DISABLED: Record<string, string> = { irc: "yes", rtsp: "yes" };
|
|
191
|
-
|
|
192
|
-
/** The default of one property of a command, or undefined when it is not known. */
|
|
193
|
-
export function defaultOf(command: Command, property: string): string | undefined {
|
|
194
|
-
if (command.menu === "/ip service" && property === "disabled") return SERVICE_DISABLED[command.selector];
|
|
195
|
-
|
|
196
|
-
if (command.menu === "/ip firewall service-port" && property === "disabled") return HELPER_DISABLED[command.selector] ?? "no";
|
|
197
|
-
|
|
198
|
-
return DEFAULTS[command.menu]?.[property] ?? DEFAULTS["*"]![property];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** A property the device stops printing once another returns to its default. Resetting that other one clears it, so it is never reset on its own. */
|
|
202
|
-
export const CLEARED_WITH: Record<string, Record<string, string>> = {
|
|
203
|
-
"/interface ethernet": { speed: "auto-negotiation" },
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/** Properties whose value cannot be read back from the device. They are compared only by the presence of what holds them. */
|
|
207
|
-
export const SECRET_KEYS = new Set(["private-key", "password", "tcp-md5-key", "authentication-password", "encryption-password"]);
|
|
208
|
-
|
|
209
|
-
/** How the renderer writes a secret. */
|
|
210
|
-
export const isSecretMarker = (value: string) => value.startsWith("<secret:");
|
|
211
|
-
|
|
212
|
-
/** A filter rule as the platform prints it, with its own spacing, squeezed so that only a real change survives. */
|
|
213
|
-
const canonicalRule = (rule: string) =>
|
|
214
|
-
rule
|
|
215
|
-
.replace(/\s*([=&|;{}(),<>!]+)\s*/g, "$1")
|
|
216
|
-
.replace(/\s+/g, " ")
|
|
217
|
-
.trim();
|
|
218
|
-
|
|
219
|
-
/** One property value in the form both sides compare in. */
|
|
220
|
-
function normalValue(command: Command, property: string, value: string): string {
|
|
221
|
-
if (property === "rule") return canonicalRule(value);
|
|
222
|
-
// A list whose order means nothing.
|
|
223
|
-
if (property === "tagged" || property === "untagged" || property === "slaves") return value.split(",").sort().join(",");
|
|
224
|
-
|
|
225
|
-
const onInterface = property === "address" && (command.menu === "/ip address" || command.menu === "/ipv6 address");
|
|
226
|
-
// An IP address property, which a MAC address is not.
|
|
227
|
-
const isAddress = /(^|-)address$/.test(property) && !property.endsWith("mac-address");
|
|
228
|
-
// On an interface, a bare address is shorthand for /32 on IPv4 and /64 on IPv6.
|
|
229
|
-
if (onInterface && !value.includes("/")) return `${value}/${value.includes(":") ? 64 : 32}`;
|
|
230
|
-
// Elsewhere a host is printed bare on IPv4 and with /128 on IPv6.
|
|
231
|
-
if (!onInterface && isAddress && /^[\d.]+\/32$/.test(value)) return value.slice(0, -3);
|
|
232
|
-
|
|
233
|
-
if (!onInterface && isAddress && value.includes(":") && !value.includes("/") && !value.includes(",")) return `${value}/128`;
|
|
234
|
-
|
|
235
|
-
return value;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** A command's properties without defaults or secrets, in the form both sides compare in. */
|
|
239
|
-
export function normal(command: Command): Record<string, string> {
|
|
240
|
-
const result: Record<string, string> = {};
|
|
241
|
-
|
|
242
|
-
for (const [property, raw] of Object.entries(command.attrs)) {
|
|
243
|
-
if (SECRET_KEYS.has(property) || isSecretMarker(raw)) continue;
|
|
244
|
-
|
|
245
|
-
const value = normalValue(command, property, raw);
|
|
246
|
-
if (value === defaultOf(command, property)) continue;
|
|
247
|
-
|
|
248
|
-
result[property] = value;
|
|
249
|
-
}
|
|
250
|
-
// The platform prints the network it derived from an address. Only a network it could not derive says something.
|
|
251
|
-
if (command.menu === "/ip address" && result.network && result.address && network(result.address).split("/")[0] === result.network)
|
|
252
|
-
delete result.network;
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Properties that tell two `add` entries of a menu apart. `name` where the menu is not listed. */
|
|
257
|
-
const IDENTITY: Record<string, string[]> = {
|
|
258
|
-
"/interface list member": ["list", "interface"],
|
|
259
|
-
"/ip firewall address-list": ["list", "address"],
|
|
260
|
-
"/ipv6 firewall address-list": ["list", "address"],
|
|
261
|
-
"/interface bridge port": ["interface"],
|
|
262
|
-
"/interface bridge vlan": ["vlan-ids"],
|
|
263
|
-
// One VLAN ID per interface: the device refuses a second, so a rename, such as one a MAC of its own brings, is a rename.
|
|
264
|
-
"/interface vlan": ["interface", "vlan-id"],
|
|
265
|
-
"/interface vxlan vteps": ["interface", "remote-ip"],
|
|
266
|
-
"/routing bgp connection": ["remote.address"],
|
|
267
|
-
"/routing rpki": ["address"],
|
|
268
|
-
"/routing ospf interface-template": ["area", "interfaces"],
|
|
269
|
-
"/routing bfd configuration": ["interfaces"],
|
|
270
|
-
"/ip route": ["dst-address", "routing-table"],
|
|
271
|
-
"/ipv6 route": ["dst-address", "routing-table"],
|
|
272
|
-
"/ip address": ["address"],
|
|
273
|
-
"/ipv6 address": ["address"],
|
|
274
|
-
"/ip dhcp-server network": ["address"],
|
|
275
|
-
"/ip dhcp-server lease": ["address"],
|
|
276
|
-
"/ip traffic-flow target": ["dst-address", "port"],
|
|
277
|
-
"/system ntp client servers": ["address"],
|
|
278
|
-
"/system logging": ["action", "topics"],
|
|
279
|
-
"/user ssh-keys": ["user", "key"],
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
/** What identifies an `add` entry, so that the same entry is found on both sides. */
|
|
283
|
-
export function identity(command: Command): Record<string, string> {
|
|
284
|
-
// The switching bridge is the one that filters VLANs, whatever it is called, so a rename is a rename and not a rebuild of every port.
|
|
285
|
-
if (command.menu === "/interface bridge" && command.attrs["vlan-filtering"] === "yes") return { "vlan-filtering": "yes" };
|
|
286
|
-
|
|
287
|
-
const normalized = normal(command);
|
|
288
|
-
const properties = IDENTITY[command.menu] ?? ["name"];
|
|
289
|
-
const result: Record<string, string> = {};
|
|
290
|
-
|
|
291
|
-
for (const property of properties) {
|
|
292
|
-
const value = normalized[property] ?? command.attrs[property] ?? defaultOf(command, property) ?? "";
|
|
293
|
-
result[property] = value;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Menus whose entries are an ordered list without names. They are rewritten whole, in the order the render gives. */
|
|
300
|
-
export const ORDERED = new Set([
|
|
301
|
-
"/routing filter rule",
|
|
302
|
-
"/ip firewall filter",
|
|
303
|
-
"/ipv6 firewall filter",
|
|
304
|
-
"/ip firewall nat",
|
|
305
|
-
"/ipv6 firewall nat",
|
|
306
|
-
"/ip firewall mangle",
|
|
307
|
-
"/ipv6 firewall mangle",
|
|
308
|
-
"/ip firewall raw",
|
|
309
|
-
"/ipv6 firewall raw",
|
|
310
|
-
"/interface bridge filter",
|
|
311
|
-
"/interface ethernet switch rule",
|
|
312
|
-
]);
|
|
313
|
-
|
|
314
|
-
/** One command as a string that is equal for equal commands. */
|
|
315
|
-
export function canonical(command: Command): string {
|
|
316
|
-
const properties = Object.entries(normal(command))
|
|
317
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
318
|
-
.map(([name, value]) => `${name}=${value}`);
|
|
319
|
-
const flags = [...command.flags].sort();
|
|
320
|
-
return [command.verb, command.selector, ...flags, ...properties].filter(Boolean).join(" ");
|
|
321
|
-
}
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
// RouterOS 7: logging in, reading, the fields it cannot express, and sending a plan.
|
|
2
|
-
//
|
|
3
|
-
// The platform applies every command the moment it arrives and has no candidate configuration, so a run is guarded three ways. Every command is first compiled on the device without running, so a syntax error or an unknown property costs nothing. A backup is saved and a scheduler armed to restore it, and the run must log in again in a fresh session before the scheduler is removed. A command the device refuses restores the backup at once, rather than leaving the device half applied.
|
|
4
|
-
|
|
5
|
-
import type { Client } from "ssh2";
|
|
6
|
-
import { familyOf } from "../../../core/addr";
|
|
7
|
-
import { resolve, substitute } from "../../../core/secrets";
|
|
8
|
-
import type { Device, Family } from "../../../schema";
|
|
9
|
-
import { type ConsoleDialect, consoleLogin, consoleOf, ConsoleShell, openConsole } from "../../../transport/console";
|
|
10
|
-
import { connect, exec, managementHost, upload } from "../../../transport/ssh";
|
|
11
|
-
import type { ApplyOptions, DeviceAdapter, Plan, ReadOptions } from "../types";
|
|
12
|
-
import { INSTALL_CERTIFICATE, plan } from "./plan";
|
|
13
|
-
import { spell } from "../catalog";
|
|
14
|
-
import { mediumOf, render } from "./render";
|
|
15
|
-
|
|
16
|
-
/** The major version this adapter renders for. Version 6 has a different BGP and filter language altogether. */
|
|
17
|
-
export const SUPPORTED_MAJOR = 7;
|
|
18
|
-
|
|
19
|
-
export async function login(device: Device): Promise<Client> {
|
|
20
|
-
const user = device.users?.[device.connection.user];
|
|
21
|
-
if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
|
|
22
|
-
|
|
23
|
-
return connect({
|
|
24
|
-
host: managementHost(device),
|
|
25
|
-
port: device.connection.port,
|
|
26
|
-
user: device.connection.user,
|
|
27
|
-
password: resolve(user.password),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** How the RouterOS console asks for a login. The flags after the user turn off colours and terminal detection, without which the console redraws every line. */
|
|
32
|
-
export const ROUTEROS_CONSOLE: ConsoleDialect = {
|
|
33
|
-
login: /Login: $/,
|
|
34
|
-
password: /Password: $/,
|
|
35
|
-
prompt: /\] > $/,
|
|
36
|
-
logout: "/quit",
|
|
37
|
-
others: [
|
|
38
|
-
[/-- \[Q quit\|D dump\|(?:C-z pause\|)?down\]$/, "D"],
|
|
39
|
-
[/\[Y\/n\]:? ?$/, "n"],
|
|
40
|
-
],
|
|
41
|
-
user: (user) => `${user}+cwt`,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** Where commands run, one at a time, each returning what it printed: an SSH session, or the console. */
|
|
45
|
-
type Session = {
|
|
46
|
-
run(command: string, timeout?: number): Promise<string>;
|
|
47
|
-
/** Write a file to the device. Only SSH can. */
|
|
48
|
-
upload?(path: string, content: string): Promise<void>;
|
|
49
|
-
/** A fresh login, which proves access survived: this session ends and a new SSH session opens, or the console logs out and in again. */
|
|
50
|
-
fresh(): Promise<Session>;
|
|
51
|
-
end(): void;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
function sshSession(client: Client, device: Device): Session {
|
|
55
|
-
return {
|
|
56
|
-
run: (command, timeout) => exec(client, command, timeout),
|
|
57
|
-
upload: (path, content) => upload(client, path, content),
|
|
58
|
-
fresh: async () => {
|
|
59
|
-
client.end();
|
|
60
|
-
return sshSession(await login(device), device);
|
|
61
|
-
},
|
|
62
|
-
end: () => client.end(),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function consoleSession(device: Device): Promise<Session> {
|
|
67
|
-
const { console, user, password } = consoleOf(device);
|
|
68
|
-
const opened = await openConsole(console);
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password, console.passwordOnly);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
opened.close();
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const shell = new ConsoleShell(opened.channel, ROUTEROS_CONSOLE);
|
|
78
|
-
|
|
79
|
-
const session: Session = {
|
|
80
|
-
run: (command, timeout) => shell.run(command, timeout),
|
|
81
|
-
fresh: async () => {
|
|
82
|
-
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password, console.passwordOnly);
|
|
83
|
-
return session;
|
|
84
|
-
},
|
|
85
|
-
end: () => {
|
|
86
|
-
shell.logout();
|
|
87
|
-
opened.close();
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
return session;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Log in over SSH, or on the console when asked. */
|
|
95
|
-
async function open(device: Device, options: ReadOptions = {}): Promise<Session> {
|
|
96
|
-
return options.console ? consoleSession(device) : sshSession(await login(device), device);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Refuse a device on a version this adapter does not render for, before anything is read or sent. */
|
|
100
|
-
export async function checkVersion(session: Session, device: Device): Promise<void> {
|
|
101
|
-
const version = (await session.run(":put [/system resource get version]")).trim();
|
|
102
|
-
const major = Number(version.split(".")[0]);
|
|
103
|
-
|
|
104
|
-
if (major !== SUPPORTED_MAJOR) {
|
|
105
|
-
throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${SUPPORTED_MAJOR}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Both dumps stamp the time they were taken. Without the stamp, an unchanged device reads the same twice. */
|
|
110
|
-
const EXPORT_TIMESTAMP = /^# \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} by RouterOS/m;
|
|
111
|
-
|
|
112
|
-
const withoutTimestamp = (text: string) => text.replace(/\r/g, "").replace(EXPORT_TIMESTAMP, "# by RouterOS");
|
|
113
|
-
|
|
114
|
-
/** Lists certificates in the form the renderer writes them, since an export does not include them. One line, because a script sent over exec runs as one. */
|
|
115
|
-
const LIST_CERTIFICATES = [
|
|
116
|
-
":foreach c in=[/certificate find] do={",
|
|
117
|
-
':local key "no";',
|
|
118
|
-
':if ([/certificate get $c private-key]) do={ :set key "yes" };',
|
|
119
|
-
':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
|
|
120
|
-
"}",
|
|
121
|
-
].join(" ");
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* What the device runs: its export, its users, and its certificates. Secrets are not part of an export.
|
|
125
|
-
*/
|
|
126
|
-
export async function read(device: Device, _network: readonly Device[] = [], options: ReadOptions = {}): Promise<string> {
|
|
127
|
-
const session = await open(device, options);
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await checkVersion(session, device);
|
|
131
|
-
|
|
132
|
-
const config = await session.run("/export", 300_000);
|
|
133
|
-
const users = await session.run("/user export");
|
|
134
|
-
const certificates = (await session.run(LIST_CERTIFICATES)).trim();
|
|
135
|
-
|
|
136
|
-
const listed = certificates.split("\n").every((line) => line.trim().startsWith("add fingerprint="));
|
|
137
|
-
if (certificates && !listed) {
|
|
138
|
-
throw new Error(`${device.name}: certificates could not be listed: ${certificates.slice(0, 120)}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// The user dump repeats the export's header; only its menu is kept.
|
|
142
|
-
const userMenu = users.slice(users.indexOf("\n/")).trim();
|
|
143
|
-
const certificateMenu = certificates ? `/certificate\n${certificates}` : "";
|
|
144
|
-
const text = [config.trim(), userMenu, certificateMenu].filter(Boolean).join("\n");
|
|
145
|
-
return withoutTimestamp(text) + "\n";
|
|
146
|
-
} finally {
|
|
147
|
-
session.end();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function unsupported(device: Device): string[] {
|
|
152
|
-
const found: string[] = [];
|
|
153
|
-
|
|
154
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
155
|
-
if (iface.type === "bridge") found.push(`interfaces.${name}, a bridge, which only FortiOS builds for now`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
159
|
-
if (!port) continue;
|
|
160
|
-
|
|
161
|
-
if (port.speed && !mediumOf(spell(device.platform, device.model, name), port.speed)) {
|
|
162
|
-
found.push(`ports.${name}.speed ${port.speed}, the port has no fixed speed by that name`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const thresholds = Object.values(port.storm_control ?? {});
|
|
166
|
-
const percents = new Set(thresholds.map((threshold) => ("percent" in threshold ? threshold.percent : undefined)));
|
|
167
|
-
|
|
168
|
-
if (thresholds.some((threshold) => "pps" in threshold)) {
|
|
169
|
-
found.push(`ports.${name}.storm_control in pps, the platform takes percent`);
|
|
170
|
-
} else if (percents.size > 1) {
|
|
171
|
-
found.push(`ports.${name}.storm_control with a different percent per kind, the platform takes one`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
176
|
-
if (iface.type !== "lag") continue;
|
|
177
|
-
|
|
178
|
-
for (const field of ["storm_control", "stp"] as const) {
|
|
179
|
-
if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}, on a LAG`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const bgp = device.routing?.bgp;
|
|
184
|
-
|
|
185
|
-
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
186
|
-
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
187
|
-
const families = neighbor.families ?? group?.families ?? [familyOf(neighbor.address)];
|
|
188
|
-
|
|
189
|
-
if (families.length > 1) found.push(`routing.bgp.neighbors.${name}.families, one family per session`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const management = device.management;
|
|
193
|
-
|
|
194
|
-
if (management?.console) found.push("management.console, the console uses the device's users");
|
|
195
|
-
if (management?.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
|
|
196
|
-
if (management?.privilege_password) found.push("management.privilege_password, the platform has no privileged mode");
|
|
197
|
-
|
|
198
|
-
if (management?.snmp?.port) found.push("management.snmp.port");
|
|
199
|
-
|
|
200
|
-
if (management?.ssh?.auth_timeout || management?.ssh?.auth_retries) {
|
|
201
|
-
found.push("management.ssh.auth_timeout and auth_retries");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
for (const [name, user] of Object.entries(management?.snmp?.users ?? {})) {
|
|
205
|
-
if (user.auth !== "sha1") {
|
|
206
|
-
found.push(`management.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (device.system?.transceiver_monitoring === false) found.push("system.transceiver_monitoring false, it is always on");
|
|
211
|
-
if (device.flow_export?.protocol === "sflow") found.push("flow_export.protocol sflow, the platform exports NetFlow and IPFIX");
|
|
212
|
-
|
|
213
|
-
return found;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const ROLLBACK_SCHEDULER = "apply-rollback";
|
|
217
|
-
|
|
218
|
-
/** Escape a command so it can sit inside a quoted string on the device. */
|
|
219
|
-
const escapeForString = (text: string) => text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$");
|
|
220
|
-
|
|
221
|
-
/** Compile a command on the device without running it. Returns why it was refused, or undefined. */
|
|
222
|
-
async function refusal(session: Session, command: string): Promise<string | undefined> {
|
|
223
|
-
const compiled = (await session.run(`:put [:parse "${escapeForString(command)}"]`)).trim();
|
|
224
|
-
const refused = !compiled.startsWith("(") || /syntax error|bad parameter|no such|expected/.test(compiled);
|
|
225
|
-
return refused ? compiled.slice(0, 200) : undefined;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Upload a certificate and its key, import both under one name, and delete the files. */
|
|
229
|
-
async function installCertificate(session: Session, device: Device, name: string): Promise<void> {
|
|
230
|
-
const certificate = device.certificates?.[name];
|
|
231
|
-
if (!certificate) throw new Error(`certificate ${name} is not declared`);
|
|
232
|
-
if (!session.upload)
|
|
233
|
-
throw new Error(`certificate ${name} is installed from a file, which only SSH can write; apply it without --console`);
|
|
234
|
-
|
|
235
|
-
await session.upload(`${name}.crt`, certificate.certificate);
|
|
236
|
-
await session.run(`/certificate import file-name=${name}.crt name=${name} passphrase=""`);
|
|
237
|
-
|
|
238
|
-
if (certificate.private_key) {
|
|
239
|
-
// A key from a file is PEM as it is; one from a variable is PEM encoded as one line of base64.
|
|
240
|
-
const value = resolve(certificate.private_key);
|
|
241
|
-
const privateKey = value.includes("-----BEGIN") ? `${value}\n` : Buffer.from(value, "base64").toString("utf8");
|
|
242
|
-
|
|
243
|
-
await session.upload(`${name}.key`, privateKey);
|
|
244
|
-
await session.run(`/certificate import file-name=${name}.key name=${name} passphrase=""`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
await session.run(`/file remove [find name="${name}.crt" or name="${name}.key"]`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export async function apply(device: Device, _network: readonly Device[], changes: Plan, options: ApplyOptions): Promise<void> {
|
|
251
|
-
const session = await open(device, options);
|
|
252
|
-
await checkVersion(session, device);
|
|
253
|
-
|
|
254
|
-
const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
|
|
255
|
-
|
|
256
|
-
for (const { title, command } of commands) {
|
|
257
|
-
if (command.startsWith(INSTALL_CERTIFICATE)) continue;
|
|
258
|
-
|
|
259
|
-
const reason = await refusal(session, substitute(command));
|
|
260
|
-
if (reason) {
|
|
261
|
-
session.end();
|
|
262
|
-
throw new Error(`${title}: the device will not parse ${command.slice(0, 200)}\n ${reason}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const stamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 12);
|
|
267
|
-
const backup = `pre-apply-${stamp}`;
|
|
268
|
-
|
|
269
|
-
// `interval` counts from now; `start-time=startup` would count from boot.
|
|
270
|
-
const armRollback = [
|
|
271
|
-
`/system scheduler add name="${ROLLBACK_SCHEDULER}"`,
|
|
272
|
-
`interval=${options.rollback}m`,
|
|
273
|
-
`on-event="/system backup load name=${backup} password=\\"\\""`,
|
|
274
|
-
"policy=read,write,policy,test,reboot,ftp",
|
|
275
|
-
].join(" ");
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
console.log(` saving ${backup}, arming a ${options.rollback} minute rollback`);
|
|
279
|
-
|
|
280
|
-
await session.run(`/system backup save name=${backup} dont-encrypt=yes`, 120_000);
|
|
281
|
-
await session.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
|
|
282
|
-
await session.run(armRollback);
|
|
283
|
-
|
|
284
|
-
for (const { title, command } of commands) {
|
|
285
|
-
if (command.startsWith(INSTALL_CERTIFICATE)) {
|
|
286
|
-
const certificateName = command.slice(INSTALL_CERTIFICATE.length).trim();
|
|
287
|
-
await installCertificate(session, device, certificateName);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const answer = (await session.run(substitute(command), 120_000)).trim();
|
|
292
|
-
if (answer) throw new Error(`${title}: ${command.slice(0, 160)}\n device said: ${answer.slice(0, 300)}`);
|
|
293
|
-
}
|
|
294
|
-
} catch (error) {
|
|
295
|
-
console.log(` FAILED: ${(error as Error).message}`);
|
|
296
|
-
console.log(` restoring ${backup} now; the rollback stays armed in case this does not get through`);
|
|
297
|
-
|
|
298
|
-
await session.run(`/system backup load name=${backup} password=""`, 30_000).catch(() => undefined);
|
|
299
|
-
session.end();
|
|
300
|
-
|
|
301
|
-
throw new Error("apply failed and the backup is being restored");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// A fresh login is the proof that access survived. Only then is the rollback removed, with the backup, which holds every credential.
|
|
305
|
-
const proof = await session.fresh().catch(() => undefined);
|
|
306
|
-
if (!proof) throw new Error(`could not log in again; the device restores ${backup} within ${options.rollback} minutes`);
|
|
307
|
-
|
|
308
|
-
await proof.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
|
|
309
|
-
await proof.run(`/file remove [find name="${backup}.backup"]`);
|
|
310
|
-
proof.end();
|
|
311
|
-
console.log(" logged in again, rollback disarmed");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Replace a list's entries in batches, so each command stays a sane length. */
|
|
315
|
-
export async function fill(device: Device, _network: readonly Device[], set: string, family: Family, prefixes: string[]): Promise<void> {
|
|
316
|
-
const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
|
|
317
|
-
const batchSize = 60;
|
|
318
|
-
const client = await login(device);
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
await exec(client, `${menu} remove [find list="${set}" and !dynamic]`);
|
|
322
|
-
|
|
323
|
-
for (let start = 0; start < prefixes.length; start += batchSize) {
|
|
324
|
-
const batch = prefixes.slice(start, start + batchSize).map((prefix) => `${menu} add list="${set}" address=${prefix}`);
|
|
325
|
-
|
|
326
|
-
const answer = (await exec(client, batch.join("; "), 90_000)).trim();
|
|
327
|
-
if (answer) throw new Error(`${set}: ${answer.slice(0, 200)}`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const count = (await exec(client, `:put [:len [${menu} find list="${set}"]]`)).trim();
|
|
331
|
-
console.log(` ${set}: ${count} entries on the device`);
|
|
332
|
-
} finally {
|
|
333
|
-
client.end();
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export const routeros: DeviceAdapter = {
|
|
338
|
-
extension: ".rsc",
|
|
339
|
-
console: true,
|
|
340
|
-
unsupported,
|
|
341
|
-
render,
|
|
342
|
-
read,
|
|
343
|
-
plan,
|
|
344
|
-
apply,
|
|
345
|
-
fill,
|
|
346
|
-
};
|