@takodotid/circuit 0.38.0 → 0.40.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/dist/chunks/{index-yzw86rc8.js → index-k38bby43.js} +62 -4
- package/dist/chunks/{server-cxr9ynxc.js → server-ggertnce.js} +1 -1
- package/dist/cli/index.js +332 -36
- package/dist/helpers/index.js +71 -29
- package/dist/types/core/cabling.d.ts +6 -0
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/cables.d.ts +16 -20
- package/dist/types/helpers/index.d.ts +1 -1
- package/dist/types/helpers/upstreams.d.ts +102 -19
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/firewall.d.ts +2 -0
- package/dist/types/platforms/fortios/index.d.ts +7 -1
- package/dist/types/platforms/fortios/render.d.ts +2 -0
- package/dist/types/platforms/fortios/vdoms.d.ts +50 -0
- package/dist/types/platforms/routeros/render.d.ts +7 -1
- package/dist/types/schema/device.d.ts +21 -1
- package/dist/types/schema/firewall.d.ts +1 -1
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +12 -4
- package/src/core/cabling.ts +30 -0
- package/src/core/validator.ts +25 -0
- package/src/helpers/cables.ts +21 -43
- package/src/helpers/index.ts +1 -1
- package/src/helpers/upstreams.ts +175 -39
- package/src/platforms/derive.ts +3 -1
- package/src/platforms/fortios/firewall.ts +9 -0
- package/src/platforms/fortios/index.ts +90 -28
- package/src/platforms/fortios/render.ts +27 -5
- package/src/platforms/fortios/vdoms.ts +217 -0
- package/src/platforms/routeros/TRAPS.md +1 -0
- package/src/platforms/routeros/render.ts +36 -3
- package/src/schema/device.ts +22 -1
- package/src/schema/firewall.ts +1 -1
- package/src/schema/interfaces.ts +1 -1
- package/dist/chunks/index-6wb41q67.js +0 -62
|
@@ -1,10 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Catalog
|
|
3
|
-
} from "./index-6wb41q67.js";
|
|
4
1
|
import {
|
|
5
2
|
Address2
|
|
6
3
|
} from "./index-n7w3tg5y.js";
|
|
7
4
|
|
|
5
|
+
// src/platforms/fortios/models.ts
|
|
6
|
+
var models = {
|
|
7
|
+
"FG-3700D": {
|
|
8
|
+
ports: { "40g": 4, "10g": 28, "1g": 2 },
|
|
9
|
+
spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/platforms/junos/models.ts
|
|
14
|
+
var models2 = {
|
|
15
|
+
"QFX5100-48S-6Q": {
|
|
16
|
+
ports: { "40g": 6, "10g": 48, "1g": 2 },
|
|
17
|
+
spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/platforms/raisecom-ros/models.ts
|
|
22
|
+
var models3 = {
|
|
23
|
+
"RAX721-C-6C48": {
|
|
24
|
+
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
25
|
+
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/platforms/routeros/models.ts
|
|
30
|
+
var models4 = {
|
|
31
|
+
"CCR2216-1G-12XS-2XQ": {
|
|
32
|
+
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
33
|
+
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
|
|
34
|
+
limits: { lag_members: 8, switch_port_ranges: 5 }
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/platforms/vrp/models.ts
|
|
39
|
+
var models5 = {
|
|
40
|
+
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/platforms/catalog.ts
|
|
44
|
+
class Catalog {
|
|
45
|
+
static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
|
|
46
|
+
static ports(platform, model) {
|
|
47
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
48
|
+
if (!spec)
|
|
49
|
+
throw new Error(`${platform} has no model ${model}`);
|
|
50
|
+
return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
|
|
51
|
+
}
|
|
52
|
+
static limits(platform, model) {
|
|
53
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
54
|
+
return spec?.limits ?? {};
|
|
55
|
+
}
|
|
56
|
+
static spell(platform, model, port) {
|
|
57
|
+
const spec = Catalog.MODELS[platform]?.[model];
|
|
58
|
+
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
59
|
+
const template = parts && spec?.spelling[parts[1]];
|
|
60
|
+
if (!template)
|
|
61
|
+
throw new Error(`${model} has no port ${port}`);
|
|
62
|
+
return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
8
66
|
// src/core/topology.ts
|
|
9
67
|
class Builder {
|
|
10
68
|
nodes = new Map;
|
|
@@ -918,4 +976,4 @@ class StudioModel {
|
|
|
918
976
|
}
|
|
919
977
|
}
|
|
920
978
|
|
|
921
|
-
export { Topology, AddressSpace, StudioModel };
|
|
979
|
+
export { Catalog, Topology, AddressSpace, StudioModel };
|
package/dist/cli/index.js
CHANGED
|
@@ -4,13 +4,11 @@ import {
|
|
|
4
4
|
Secrets2
|
|
5
5
|
} from "../chunks/index-20kcjxjg.js";
|
|
6
6
|
import {
|
|
7
|
+
Catalog,
|
|
7
8
|
Topology,
|
|
8
9
|
AddressSpace,
|
|
9
10
|
StudioModel
|
|
10
|
-
} from "../chunks/index-
|
|
11
|
-
import {
|
|
12
|
-
Catalog
|
|
13
|
-
} from "../chunks/index-6wb41q67.js";
|
|
11
|
+
} from "../chunks/index-k38bby43.js";
|
|
14
12
|
import {
|
|
15
13
|
Address2
|
|
16
14
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -544,6 +542,8 @@ class Derive {
|
|
|
544
542
|
const has = (feature) => proved.includes(feature);
|
|
545
543
|
if (routing?.bfd && !has("bfd-timers"))
|
|
546
544
|
found.push(`routing.bfd, BFD timers are ${unproved}`);
|
|
545
|
+
if (device.vdoms && !has("management-vdom"))
|
|
546
|
+
found.push(`vdoms, a management VDOM is ${unproved}`);
|
|
547
547
|
for (const route of routing?.static ?? []) {
|
|
548
548
|
if (route.bfd && !has("static-route-bfd"))
|
|
549
549
|
found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
|
|
@@ -909,6 +909,11 @@ class FortiOsFirewall {
|
|
|
909
909
|
const state = rule.match?.state;
|
|
910
910
|
return rule.action === "accept" && state !== undefined && state.length > 0 && state.every((one) => one === "established" || one === "related");
|
|
911
911
|
}
|
|
912
|
+
static managementOnly(root, device) {
|
|
913
|
+
const localIns = { ipv4: 0, ipv6: 0 };
|
|
914
|
+
const localInEntry = (family) => root.block(family === "ipv4" ? "firewall local-in-policy" : "firewall local-in-policy6").entry(++localIns[family]);
|
|
915
|
+
FortiOsFirewall.management(localInEntry, new Objects(root, device), device);
|
|
916
|
+
}
|
|
912
917
|
static render(root, device) {
|
|
913
918
|
const fw = device.firewall;
|
|
914
919
|
const objects = new Objects(root, device);
|
|
@@ -2168,6 +2173,155 @@ class FortiOsPlan {
|
|
|
2168
2173
|
}
|
|
2169
2174
|
}
|
|
2170
2175
|
|
|
2176
|
+
// src/platforms/fortios/vdoms.ts
|
|
2177
|
+
class FortiOsVdoms {
|
|
2178
|
+
static ROOT = "root";
|
|
2179
|
+
static GLOBAL = new Set([
|
|
2180
|
+
"vpn certificate local",
|
|
2181
|
+
"vpn certificate ca",
|
|
2182
|
+
"system global",
|
|
2183
|
+
'system replacemsg admin "pre_admin-disclaimer-text"',
|
|
2184
|
+
"system accprofile",
|
|
2185
|
+
"system admin",
|
|
2186
|
+
"system interface",
|
|
2187
|
+
"system dns",
|
|
2188
|
+
"system ntp",
|
|
2189
|
+
"system snmp sysinfo",
|
|
2190
|
+
"system snmp community",
|
|
2191
|
+
"system snmp user",
|
|
2192
|
+
"log disk setting",
|
|
2193
|
+
...["", "2", "3", "4"].flatMap((number) => [`log syslogd${number} setting`, `log syslogd${number} filter`]),
|
|
2194
|
+
"system sflow",
|
|
2195
|
+
"system netflow",
|
|
2196
|
+
"system session-helper"
|
|
2197
|
+
]);
|
|
2198
|
+
static MANAGEMENT_SECTIONS = [
|
|
2199
|
+
"firewall address",
|
|
2200
|
+
"firewall address6",
|
|
2201
|
+
"firewall addrgrp",
|
|
2202
|
+
"firewall addrgrp6",
|
|
2203
|
+
"firewall service custom",
|
|
2204
|
+
"router static",
|
|
2205
|
+
"router static6",
|
|
2206
|
+
"firewall local-in-policy",
|
|
2207
|
+
"firewall local-in-policy6"
|
|
2208
|
+
];
|
|
2209
|
+
static managementOf(device) {
|
|
2210
|
+
return device.vdoms?.management.name;
|
|
2211
|
+
}
|
|
2212
|
+
static vdomOf(device, name) {
|
|
2213
|
+
const management = device.vdoms?.management;
|
|
2214
|
+
if (!management)
|
|
2215
|
+
return FortiOsVdoms.ROOT;
|
|
2216
|
+
const lag = device.ports?.[name]?.lag;
|
|
2217
|
+
const holder = lag ?? name;
|
|
2218
|
+
return management.interfaces.includes(holder) ? management.name : FortiOsVdoms.ROOT;
|
|
2219
|
+
}
|
|
2220
|
+
static key(vdom, section) {
|
|
2221
|
+
return `@${vdom} ${section}`;
|
|
2222
|
+
}
|
|
2223
|
+
static scopeOf(key) {
|
|
2224
|
+
const named = /^@(\S+) (.+)$/.exec(key);
|
|
2225
|
+
if (named)
|
|
2226
|
+
return { scope: { kind: "vdom", vdom: named[1] }, section: named[2] };
|
|
2227
|
+
return FortiOsVdoms.GLOBAL.has(key) ? { scope: { kind: "global" }, section: key } : { scope: { kind: "vdom", vdom: FortiOsVdoms.ROOT }, section: key };
|
|
2228
|
+
}
|
|
2229
|
+
static isMulti(tree) {
|
|
2230
|
+
return tree.blocks.get("system global")?.settings.get("vdom-mode") === "multi-vdom";
|
|
2231
|
+
}
|
|
2232
|
+
static wrap(scope, lines) {
|
|
2233
|
+
return scope.kind === "global" ? ["config global", ...lines, "end"] : ["config vdom", `edit ${scope.vdom}`, ...lines, "next", "end"];
|
|
2234
|
+
}
|
|
2235
|
+
static named(vdom, text) {
|
|
2236
|
+
return text.replace(/^config /gm, `config @${vdom} `);
|
|
2237
|
+
}
|
|
2238
|
+
static listed(text) {
|
|
2239
|
+
return [...text.matchAll(/^name=([^/\s]+)\//gm)].map((match) => match[1]);
|
|
2240
|
+
}
|
|
2241
|
+
static plan(desired, current, planOne) {
|
|
2242
|
+
const want = FortiOsTree.parse(desired);
|
|
2243
|
+
const have = FortiOsTree.parse(current);
|
|
2244
|
+
const wantMulti = FortiOsVdoms.isMulti(want);
|
|
2245
|
+
const haveMulti = FortiOsVdoms.isMulti(have);
|
|
2246
|
+
const refused = (problem) => ({ steps: [], problems: [problem] });
|
|
2247
|
+
if (wantMulti && !haveMulti) {
|
|
2248
|
+
return refused("the config has vdoms and the device runs one VDOM. Turn multi-VDOM mode on by hand, on the console: `config system global`, `set vdom-mode multi-vdom`, `end`. It moves everything into root and ends every session");
|
|
2249
|
+
}
|
|
2250
|
+
if (!wantMulti && haveMulti) {
|
|
2251
|
+
return refused("the device runs several VDOMs and the config has no vdoms. Write vdoms.management, or turn multi-VDOM mode off by hand");
|
|
2252
|
+
}
|
|
2253
|
+
if (!wantMulti)
|
|
2254
|
+
return planOne(desired, current);
|
|
2255
|
+
const moved = FortiOsVdoms.moved(want, have);
|
|
2256
|
+
if (moved.length) {
|
|
2257
|
+
return refused(`${moved.map(({ name, from, to }) => `${name} from ${from} to ${to}`).join(", ")}: move an interface into another VDOM by hand, on the console, since it may carry the way in`);
|
|
2258
|
+
}
|
|
2259
|
+
const scopes = new Map;
|
|
2260
|
+
const scopeFor = (scope) => {
|
|
2261
|
+
const name = scope.kind === "global" ? "" : scope.vdom;
|
|
2262
|
+
if (!scopes.has(name))
|
|
2263
|
+
scopes.set(name, { scope, want: new FortiOsBlock, have: new FortiOsBlock });
|
|
2264
|
+
return scopes.get(name);
|
|
2265
|
+
};
|
|
2266
|
+
for (const [side, tree] of [
|
|
2267
|
+
["want", want],
|
|
2268
|
+
["have", have]
|
|
2269
|
+
]) {
|
|
2270
|
+
for (const [key, block] of tree.blocks) {
|
|
2271
|
+
const { scope, section } = FortiOsVdoms.scopeOf(key);
|
|
2272
|
+
scopeFor(scope)[side].blocks.set(section, block);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const global = scopes.get("");
|
|
2276
|
+
const globalPlan = global ? planOne(FortiOsTree.print(global.want), FortiOsTree.print(global.have)) : { steps: [], problems: [] };
|
|
2277
|
+
const inScope = (scope, step) => ({ ...step, send: FortiOsVdoms.wrap(scope, step.send) });
|
|
2278
|
+
const removal = (step) => step.title.endsWith(", removed");
|
|
2279
|
+
const vdomPlans = [...scopes.values()].filter(({ scope }) => scope.kind === "vdom").map(({ scope, want: wanted, have: had }) => ({ scope, plan: planOne(FortiOsTree.print(wanted), FortiOsTree.print(had)) }));
|
|
2280
|
+
const vdomSteps = vdomPlans.flatMap(({ scope, plan }) => plan.steps.map((step) => inScope(scope, { ...step, title: `${scope.kind === "vdom" ? scope.vdom : ""}: ${step.title}` })));
|
|
2281
|
+
return {
|
|
2282
|
+
problems: [...globalPlan.problems, ...vdomPlans.flatMap(({ plan }) => plan.problems)],
|
|
2283
|
+
steps: [
|
|
2284
|
+
...globalPlan.steps.filter((step) => !removal(step)).map((step) => inScope({ kind: "global" }, step)),
|
|
2285
|
+
...vdomSteps,
|
|
2286
|
+
...globalPlan.steps.filter(removal).map((step) => inScope({ kind: "global" }, step))
|
|
2287
|
+
]
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
static moved(want, have) {
|
|
2291
|
+
const wanted = want.blocks.get("system interface");
|
|
2292
|
+
const had = have.blocks.get("system interface");
|
|
2293
|
+
if (!wanted || !had)
|
|
2294
|
+
return [];
|
|
2295
|
+
return [...wanted.entries.entries()].flatMap(([name, { block }]) => {
|
|
2296
|
+
const from = had.entries.get(name)?.block.settings.get("vdom");
|
|
2297
|
+
const to = block.settings.get("vdom");
|
|
2298
|
+
return from && to && FortiOsTree.bare(from) !== FortiOsTree.bare(to) ? [{ name, from: FortiOsTree.bare(from), to: FortiOsTree.bare(to) }] : [];
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
static gaps(device) {
|
|
2302
|
+
if (!device.vdoms)
|
|
2303
|
+
return [];
|
|
2304
|
+
const found = [];
|
|
2305
|
+
const tunnels = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "gre" || iface.type === "vxlan");
|
|
2306
|
+
if (tunnels.length)
|
|
2307
|
+
found.push(`vdoms with GRE or VXLAN tunnels (${tunnels.map(([name]) => name).join(", ")}), whose tables belong to a VDOM`);
|
|
2308
|
+
if (Object.values(device.interfaces ?? {}).some((iface) => iface.type === "bridge"))
|
|
2309
|
+
found.push("vdoms with a bridge, which needs transparent mode");
|
|
2310
|
+
const management = device.vdoms.management;
|
|
2311
|
+
const inManagement = (name) => name !== undefined && management.interfaces.includes(name);
|
|
2312
|
+
for (const route of device.routing?.static ?? []) {
|
|
2313
|
+
if (inManagement(route.interface))
|
|
2314
|
+
found.push(`routing.static ${route.prefix} leaves through ${route.interface}, in vdoms.management; write it there`);
|
|
2315
|
+
}
|
|
2316
|
+
for (const name of management.interfaces) {
|
|
2317
|
+
const settings = device.interfaces?.[name] ?? device.ports?.[name];
|
|
2318
|
+
if (settings && "vrf" in settings && settings.vrf)
|
|
2319
|
+
found.push(`vdoms.management interface ${name} in VRF ${settings.vrf}`);
|
|
2320
|
+
}
|
|
2321
|
+
return found;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2171
2325
|
// src/platforms/fortios/render.ts
|
|
2172
2326
|
class FortiOsRender {
|
|
2173
2327
|
static BANNER = 'system replacemsg admin "pre_admin-disclaimer-text"';
|
|
@@ -2410,8 +2564,24 @@ class FortiOsRender {
|
|
|
2410
2564
|
FortiOsRouting.render(root, device);
|
|
2411
2565
|
FortiOsRender.helpers(section("system session-helper"), device);
|
|
2412
2566
|
FortiOsFirewall.render(root, device);
|
|
2567
|
+
FortiOsRender.managementVdom(root, device);
|
|
2413
2568
|
return FortiOsTree.print(root);
|
|
2414
2569
|
}
|
|
2570
|
+
static managementVdom(root, device) {
|
|
2571
|
+
const management = device.vdoms?.management;
|
|
2572
|
+
if (!management)
|
|
2573
|
+
return;
|
|
2574
|
+
const vdom = new FortiOsBlock;
|
|
2575
|
+
for (const section of FortiOsVdoms.MANAGEMENT_SECTIONS)
|
|
2576
|
+
vdom.block(section);
|
|
2577
|
+
FortiOsRender.staticRoutes(vdom.block("router static"), vdom.block("router static6"), {
|
|
2578
|
+
...device,
|
|
2579
|
+
routing: { static: management.static ?? [] }
|
|
2580
|
+
});
|
|
2581
|
+
FortiOsFirewall.managementOnly(vdom, device);
|
|
2582
|
+
for (const [section, block] of vdom.blocks)
|
|
2583
|
+
root.blocks.set(FortiOsVdoms.key(management.name, section), block);
|
|
2584
|
+
}
|
|
2415
2585
|
static certificates(local, trusted, device) {
|
|
2416
2586
|
for (const [name, certificate] of Object.entries(device.certificates ?? {})) {
|
|
2417
2587
|
const text = FortiOsTree.quote(certificate.certificate.trim());
|
|
@@ -2447,6 +2617,9 @@ class FortiOsRender {
|
|
|
2447
2617
|
block.set("pre-login-banner", "enable");
|
|
2448
2618
|
if (device.lldp === true)
|
|
2449
2619
|
block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
|
|
2620
|
+
const vdom = FortiOsVdoms.managementOf(device);
|
|
2621
|
+
if (vdom)
|
|
2622
|
+
block.set("vdom-mode", "multi-vdom").set("management-vdom", FortiOsTree.quote(vdom));
|
|
2450
2623
|
}
|
|
2451
2624
|
static users(profiles, admins, device) {
|
|
2452
2625
|
const roles = new Set(Object.values(device.users ?? {}).map((user) => user.role));
|
|
@@ -2473,7 +2646,7 @@ class FortiOsRender {
|
|
|
2473
2646
|
for (const port of Catalog.ports(device.platform, device.model)) {
|
|
2474
2647
|
const settings = device.ports?.[port];
|
|
2475
2648
|
const entry = block.entry(Catalog.spell(device.platform, device.model, port));
|
|
2476
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2649
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, port)));
|
|
2477
2650
|
entry.set("type", "physical");
|
|
2478
2651
|
if (!settings) {
|
|
2479
2652
|
FortiOsRender.redirects(entry, device);
|
|
@@ -2497,7 +2670,7 @@ class FortiOsRender {
|
|
|
2497
2670
|
continue;
|
|
2498
2671
|
const members = Object.entries(device.ports ?? {}).filter(([, port]) => port?.lag === name).map(([port]) => Catalog.spell(device.platform, device.model, port));
|
|
2499
2672
|
const entry = block.entry(name);
|
|
2500
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2673
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2501
2674
|
entry.set("type", "aggregate");
|
|
2502
2675
|
if (members.length)
|
|
2503
2676
|
entry.set("member", FortiOsTree.quoteAll(members));
|
|
@@ -2515,7 +2688,7 @@ class FortiOsRender {
|
|
|
2515
2688
|
continue;
|
|
2516
2689
|
const parent = FortiOsRender.vlanParent(device, iface.vlan);
|
|
2517
2690
|
const entry = block.entry(name);
|
|
2518
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2691
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2519
2692
|
if (parent)
|
|
2520
2693
|
entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, parent)));
|
|
2521
2694
|
entry.set("vlanid", device.vlans?.[iface.vlan]?.id ?? 0);
|
|
@@ -2533,7 +2706,7 @@ class FortiOsRender {
|
|
|
2533
2706
|
if (iface.type !== "loopback")
|
|
2534
2707
|
continue;
|
|
2535
2708
|
const entry = block.entry(name);
|
|
2536
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2709
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2537
2710
|
entry.set("type", "loopback");
|
|
2538
2711
|
if (iface.description)
|
|
2539
2712
|
entry.set("description", FortiOsTree.quote(iface.description));
|
|
@@ -2562,7 +2735,7 @@ class FortiOsRender {
|
|
|
2562
2735
|
tunnel.set("dstport", iface.port);
|
|
2563
2736
|
}
|
|
2564
2737
|
const entry = block.entry(name);
|
|
2565
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2738
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2566
2739
|
entry.set("type", iface.type === "gre" ? "tunnel" : "vxlan");
|
|
2567
2740
|
entry.set("interface", via);
|
|
2568
2741
|
if (iface.type === "vxlan" && iface.mac)
|
|
@@ -2951,6 +3124,7 @@ class FortiOs extends Platform {
|
|
|
2951
3124
|
if (device.stp)
|
|
2952
3125
|
found.push("stp, ports are routed");
|
|
2953
3126
|
found.push(...FortiOs.transparentGaps(device));
|
|
3127
|
+
found.push(...FortiOsVdoms.gaps(device));
|
|
2954
3128
|
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
2955
3129
|
if (iface.type === "vlan" && iface.mac)
|
|
2956
3130
|
found.push(`interfaces.${name}.mac, which only RouterOS sets for now`);
|
|
@@ -2973,7 +3147,8 @@ class FortiOs extends Platform {
|
|
|
2973
3147
|
neighbors(device, _network = [], options = {}) {
|
|
2974
3148
|
return FortiOs.withCli(device, async (cli) => {
|
|
2975
3149
|
await FortiOs.checkVersion(cli, device);
|
|
2976
|
-
|
|
3150
|
+
const details = await FortiOs.inGlobal(cli, device, () => cli.run("diagnose lldprx neighbor details"));
|
|
3151
|
+
return FortiOs.neighborsOf(details, device);
|
|
2977
3152
|
}, options);
|
|
2978
3153
|
}
|
|
2979
3154
|
static neighborsOf(text, device) {
|
|
@@ -2996,14 +3171,14 @@ class FortiOs extends Platform {
|
|
|
2996
3171
|
return found;
|
|
2997
3172
|
}
|
|
2998
3173
|
plan(desired, current, options) {
|
|
2999
|
-
return
|
|
3174
|
+
return FortiOsVdoms.plan(desired, current, (want, have) => FortiOsPlan.of(want, have, options));
|
|
3000
3175
|
}
|
|
3001
3176
|
async apply(device, network, changes, options) {
|
|
3002
3177
|
await FortiOs.applyOnce(device, changes, options);
|
|
3003
3178
|
const switched = changes.steps.some((step) => step.send.some((line) => line.startsWith("set opmode ")));
|
|
3004
3179
|
if (!switched)
|
|
3005
3180
|
return;
|
|
3006
|
-
const again =
|
|
3181
|
+
const again = this.plan(FortiOsRender.of(device), await this.read(device, network, options), options);
|
|
3007
3182
|
if (!again.steps.length)
|
|
3008
3183
|
return;
|
|
3009
3184
|
console.log(" the operation mode changed; applying what it added on its own");
|
|
@@ -3012,7 +3187,8 @@ class FortiOs extends Platform {
|
|
|
3012
3187
|
async fill(device, _network, set, family, prefixes) {
|
|
3013
3188
|
await FortiOs.withCli(device, async (cli) => {
|
|
3014
3189
|
await FortiOs.checkVersion(cli, device);
|
|
3015
|
-
|
|
3190
|
+
const lines = FortiOs.fillLines(set, family, prefixes);
|
|
3191
|
+
for (const line of device.vdoms ? FortiOsVdoms.wrap({ kind: "vdom", vdom: FortiOsVdoms.ROOT }, lines) : lines) {
|
|
3016
3192
|
let answer = await cli.run(line);
|
|
3017
3193
|
if (FortiOsCli.QUESTION.test(cli.prompt) && line === "purge")
|
|
3018
3194
|
answer = await cli.answer("y");
|
|
@@ -3145,6 +3321,18 @@ class FortiOs extends Platform {
|
|
|
3145
3321
|
if (`${version[1]}.${version[2]}` !== FortiOs.SUPPORTED) {
|
|
3146
3322
|
throw new Error(`${device.name} runs FortiOS ${version[1]}.${version[2]}.${version[3]}; this adapter renders for FortiOS ${FortiOs.SUPPORTED}`);
|
|
3147
3323
|
}
|
|
3324
|
+
return status;
|
|
3325
|
+
}
|
|
3326
|
+
static async inGlobal(cli, device, work) {
|
|
3327
|
+
const multi = typeof device === "boolean" ? device : Boolean(device.vdoms);
|
|
3328
|
+
if (!multi)
|
|
3329
|
+
return work();
|
|
3330
|
+
await cli.run("config global");
|
|
3331
|
+
try {
|
|
3332
|
+
return await work();
|
|
3333
|
+
} finally {
|
|
3334
|
+
await FortiOs.leave(cli);
|
|
3335
|
+
}
|
|
3148
3336
|
}
|
|
3149
3337
|
static scrub(text) {
|
|
3150
3338
|
const root = FortiOsTree.parse(text);
|
|
@@ -3168,11 +3356,14 @@ class FortiOs extends Platform {
|
|
|
3168
3356
|
return FortiOsTree.print(root);
|
|
3169
3357
|
}
|
|
3170
3358
|
static async readConfig(cli, device) {
|
|
3171
|
-
await FortiOs.checkVersion(cli, device);
|
|
3359
|
+
const status = await FortiOs.checkVersion(cli, device);
|
|
3360
|
+
const show = (section) => cli.run(`${FortiOsRender.FULL.includes(section) ? "show full-configuration" : "show"} ${section}`, 120000);
|
|
3172
3361
|
const parts = [];
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3362
|
+
if (/^Virtual domain configuration: (multiple|split-task)/m.test(status)) {
|
|
3363
|
+
parts.push(...await FortiOs.readVdoms(cli, device, show));
|
|
3364
|
+
} else {
|
|
3365
|
+
for (const section of FortiOsRender.SECTIONS)
|
|
3366
|
+
parts.push(await show(section));
|
|
3176
3367
|
}
|
|
3177
3368
|
const text = parts.join(`
|
|
3178
3369
|
`);
|
|
@@ -3180,26 +3371,53 @@ class FortiOs extends Platform {
|
|
|
3180
3371
|
throw new Error(`${device.name} returned no configuration`);
|
|
3181
3372
|
return FortiOs.scrub(FortiOsPlan.clean(text));
|
|
3182
3373
|
}
|
|
3374
|
+
static async readVdoms(cli, device, show) {
|
|
3375
|
+
const management = FortiOsVdoms.managementOf(device);
|
|
3376
|
+
if (!management)
|
|
3377
|
+
throw new Error(`${device.name} runs several VDOMs and its config has no vdoms; write vdoms.management`);
|
|
3378
|
+
const parts = [];
|
|
3379
|
+
const inVdom = async (vdom, sections) => {
|
|
3380
|
+
await cli.run("config vdom");
|
|
3381
|
+
await cli.run(`edit ${vdom}`);
|
|
3382
|
+
try {
|
|
3383
|
+
for (const section of sections) {
|
|
3384
|
+
const text = await show(section);
|
|
3385
|
+
parts.push(vdom === FortiOsVdoms.ROOT ? text : FortiOsVdoms.named(vdom, text));
|
|
3386
|
+
}
|
|
3387
|
+
} finally {
|
|
3388
|
+
await FortiOs.leave(cli);
|
|
3389
|
+
}
|
|
3390
|
+
};
|
|
3391
|
+
const listed = await FortiOs.inGlobal(cli, true, async () => {
|
|
3392
|
+
for (const section of FortiOsRender.SECTIONS.filter((one) => FortiOsVdoms.GLOBAL.has(one)))
|
|
3393
|
+
parts.push(await show(section));
|
|
3394
|
+
return FortiOsVdoms.listed(await cli.run("diagnose sys vd list"));
|
|
3395
|
+
});
|
|
3396
|
+
await inVdom(FortiOsVdoms.ROOT, FortiOsRender.SECTIONS.filter((one) => !FortiOsVdoms.GLOBAL.has(one)));
|
|
3397
|
+
if (listed.includes(management))
|
|
3398
|
+
await inVdom(management, FortiOsVdoms.MANAGEMENT_SECTIONS);
|
|
3399
|
+
return parts;
|
|
3400
|
+
}
|
|
3183
3401
|
static async leave(cli) {
|
|
3184
3402
|
for (let level = 0;level < 10 && /\(.*\) [#$] $/.test(cli.prompt); level++)
|
|
3185
3403
|
await cli.run("end");
|
|
3186
3404
|
}
|
|
3187
|
-
static async saving(cli, mode, seconds) {
|
|
3405
|
+
static async saving(cli, mode, multi, seconds) {
|
|
3188
3406
|
const lines = ["config system global", `set cfg-save ${mode}`, ...seconds ? [`set cfg-revert-timeout ${seconds}`] : [], "end"];
|
|
3189
|
-
for (const line of lines) {
|
|
3407
|
+
for (const line of multi ? FortiOsVdoms.wrap({ kind: "global" }, lines) : lines) {
|
|
3190
3408
|
const answer = await cli.run(line);
|
|
3191
3409
|
if (FortiOs.REFUSED.test(answer))
|
|
3192
3410
|
throw new Error(`the device refused ${line}: ${answer.trim().slice(0, 300)}`);
|
|
3193
3411
|
}
|
|
3194
3412
|
}
|
|
3195
|
-
static async configure(cli, plan, guard) {
|
|
3413
|
+
static async configure(cli, plan, guard, multi) {
|
|
3196
3414
|
const minutes = guard.seconds / 60;
|
|
3197
|
-
await FortiOs.saving(cli, "revert", guard.seconds);
|
|
3415
|
+
await FortiOs.saving(cli, "revert", multi, guard.seconds);
|
|
3198
3416
|
const restore = async (reason) => {
|
|
3199
3417
|
await FortiOs.leave(cli).catch(() => {
|
|
3200
3418
|
return;
|
|
3201
3419
|
});
|
|
3202
|
-
await cli.run("execute cfg reload").catch(() => {
|
|
3420
|
+
await FortiOs.inGlobal(cli, multi, () => cli.run("execute cfg reload")).catch(() => {
|
|
3203
3421
|
return;
|
|
3204
3422
|
});
|
|
3205
3423
|
return new Reloaded(`${reason}
|
|
@@ -3233,27 +3451,28 @@ class FortiOs extends Platform {
|
|
|
3233
3451
|
} catch (error) {
|
|
3234
3452
|
throw await restore(`a fresh login failed: ${error.message}`);
|
|
3235
3453
|
}
|
|
3236
|
-
const saved = await cli.run("execute cfg save", 120000);
|
|
3454
|
+
const saved = await FortiOs.inGlobal(cli, multi, () => cli.run("execute cfg save", 120000));
|
|
3237
3455
|
if (!/config saved|no config to be saved/.test(saved)) {
|
|
3238
3456
|
throw new Error(`access was proved but the save failed; the device reverts in ${minutes} minutes:
|
|
3239
3457
|
${saved.trim().slice(0, 300)}`);
|
|
3240
3458
|
}
|
|
3241
|
-
await FortiOs.saving(cli, "automatic");
|
|
3459
|
+
await FortiOs.saving(cli, "automatic", multi);
|
|
3242
3460
|
console.log(" logged in again, saved");
|
|
3243
3461
|
}
|
|
3244
3462
|
static async settle(device, options) {
|
|
3245
3463
|
for (let attempt = 0;attempt < FortiOs.RELOAD.attempts; attempt++) {
|
|
3246
3464
|
await new Promise((done) => setTimeout(done, FortiOs.RELOAD.interval));
|
|
3247
3465
|
const settled = await FortiOs.withCli(device, async (cli) => {
|
|
3248
|
-
|
|
3466
|
+
const reloaded = await FortiOs.inGlobal(cli, device, () => cli.run("execute cfg reload"));
|
|
3467
|
+
if (!/no config to be reloaded/.test(reloaded))
|
|
3249
3468
|
return false;
|
|
3250
|
-
await FortiOs.saving(cli, "automatic");
|
|
3469
|
+
await FortiOs.saving(cli, "automatic", Boolean(device.vdoms));
|
|
3251
3470
|
return true;
|
|
3252
3471
|
}, options).catch(() => false);
|
|
3253
3472
|
if (settled)
|
|
3254
3473
|
return "the device is back and saves changes on its own again";
|
|
3255
3474
|
}
|
|
3256
|
-
return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global`";
|
|
3475
|
+
return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global` (in `config global` on a device with VDOMs)";
|
|
3257
3476
|
}
|
|
3258
3477
|
static async applyOnce(device, plan, options) {
|
|
3259
3478
|
const seconds = options.rollback * 60;
|
|
@@ -3261,7 +3480,7 @@ class FortiOs extends Platform {
|
|
|
3261
3480
|
if (options.console) {
|
|
3262
3481
|
await FortiOs.consoleSession(device, async (cli, relogin) => {
|
|
3263
3482
|
await FortiOs.checkVersion(cli, device);
|
|
3264
|
-
await FortiOs.configure(cli, plan, { seconds, prove: relogin });
|
|
3483
|
+
await FortiOs.configure(cli, plan, { seconds, prove: relogin }, Boolean(device.vdoms));
|
|
3265
3484
|
});
|
|
3266
3485
|
return;
|
|
3267
3486
|
}
|
|
@@ -3273,7 +3492,7 @@ class FortiOs extends Platform {
|
|
|
3273
3492
|
const proof = await FortiOs.login(device);
|
|
3274
3493
|
proof.end();
|
|
3275
3494
|
}
|
|
3276
|
-
});
|
|
3495
|
+
}, Boolean(device.vdoms));
|
|
3277
3496
|
});
|
|
3278
3497
|
} catch (error) {
|
|
3279
3498
|
if (!(error instanceof Reloaded))
|
|
@@ -3750,6 +3969,28 @@ class Cabling {
|
|
|
3750
3969
|
}
|
|
3751
3970
|
return findings;
|
|
3752
3971
|
}
|
|
3972
|
+
static describeLinks(devices) {
|
|
3973
|
+
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
3974
|
+
return devices.map((device) => {
|
|
3975
|
+
const ports = Object.entries(device.ports ?? {});
|
|
3976
|
+
if (!ports.some(([, port]) => port?.link && port.description === undefined))
|
|
3977
|
+
return device;
|
|
3978
|
+
const described = ports.map(([name, port]) => {
|
|
3979
|
+
const far = port?.link ? byName.get(port.link.device) : undefined;
|
|
3980
|
+
if (!port?.link || port.description !== undefined || !far)
|
|
3981
|
+
return [name, port];
|
|
3982
|
+
return [name, { ...port, description: `${far.name} ${Cabling.labelOf(far, port.link.port)}` }];
|
|
3983
|
+
});
|
|
3984
|
+
return { ...device, ports: Object.fromEntries(described) };
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
static labelOf(device, port) {
|
|
3988
|
+
try {
|
|
3989
|
+
return Catalog.spell(device.platform, device.model, port);
|
|
3990
|
+
} catch {
|
|
3991
|
+
return port;
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3753
3994
|
static isDevice(system, name) {
|
|
3754
3995
|
return system === name || system.startsWith(`${name}.`);
|
|
3755
3996
|
}
|
|
@@ -7881,9 +8122,23 @@ class RouterOsRender {
|
|
|
7881
8122
|
}
|
|
7882
8123
|
untracked(family) {
|
|
7883
8124
|
const menu = family === "ipv4" ? "/ip firewall raw" : "/ipv6 firewall raw";
|
|
7884
|
-
|
|
7885
|
-
|
|
8125
|
+
const untracked = (this.device.firewall?.untracked ?? []).filter((prefix) => Address2.family(prefix) === family);
|
|
8126
|
+
for (const address of RouterOsRender.natAddresses(this.device)) {
|
|
8127
|
+
if (Address2.family(address) !== family || !untracked.some((prefix) => Address2.contains(prefix, address)))
|
|
7886
8128
|
continue;
|
|
8129
|
+
for (const [direction, property] of [
|
|
8130
|
+
["to", "dst-address"],
|
|
8131
|
+
["from", "src-address"]
|
|
8132
|
+
]) {
|
|
8133
|
+
this.add(menu, "add", {
|
|
8134
|
+
action: "accept",
|
|
8135
|
+
chain: "prerouting",
|
|
8136
|
+
comment: `Tracked for NAT, ${direction} ${address}`,
|
|
8137
|
+
[property]: address
|
|
8138
|
+
});
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
for (const prefix of untracked) {
|
|
7887
8142
|
for (const [direction, property] of [
|
|
7888
8143
|
["to", "dst-address"],
|
|
7889
8144
|
["from", "src-address"]
|
|
@@ -7897,6 +8152,17 @@ class RouterOsRender {
|
|
|
7897
8152
|
}
|
|
7898
8153
|
}
|
|
7899
8154
|
}
|
|
8155
|
+
static natAddresses(device) {
|
|
8156
|
+
const nat = device.firewall?.nat;
|
|
8157
|
+
const addresses = [
|
|
8158
|
+
...(nat?.source ?? []).flatMap((rule) => rule.action === "masquerade" ? [] : [rule.to]),
|
|
8159
|
+
...(nat?.destination ?? []).flatMap((rule) => [
|
|
8160
|
+
...rule.match?.dst ? [rule.match.dst] : [],
|
|
8161
|
+
...rule.action === "masquerade" ? [] : [rule.to]
|
|
8162
|
+
])
|
|
8163
|
+
];
|
|
8164
|
+
return [...new Set(addresses)];
|
|
8165
|
+
}
|
|
7900
8166
|
families(match) {
|
|
7901
8167
|
const sets = this.device.firewall?.address_sets ?? {};
|
|
7902
8168
|
const address = match?.src ?? match?.dst;
|
|
@@ -10986,6 +11252,32 @@ class Validator {
|
|
|
10986
11252
|
Validator.checkFirewall(device, names, cidr);
|
|
10987
11253
|
Validator.checkServices(device, names, report, cidr);
|
|
10988
11254
|
Validator.checkCertificates(device, names, report);
|
|
11255
|
+
Validator.checkVdoms(device, names, report, cidr, ip);
|
|
11256
|
+
}
|
|
11257
|
+
static checkVdoms(device, names, report, cidr, ip) {
|
|
11258
|
+
const management = device.vdoms?.management;
|
|
11259
|
+
if (!management)
|
|
11260
|
+
return;
|
|
11261
|
+
if (device.platform !== "fortios")
|
|
11262
|
+
report("vdoms are a FortiOS setting");
|
|
11263
|
+
if (management.name === "root")
|
|
11264
|
+
report("vdoms.management.name is root, the VDOM everything else is in; give it a name of its own");
|
|
11265
|
+
for (const name of management.interfaces) {
|
|
11266
|
+
names.expectInterface("vdoms.management interface", name);
|
|
11267
|
+
const port = device.ports?.[name];
|
|
11268
|
+
if (port?.lag)
|
|
11269
|
+
report(`vdoms.management interface ${name} is in LAG ${port.lag}; list the LAG`);
|
|
11270
|
+
}
|
|
11271
|
+
for (const route of management.static ?? []) {
|
|
11272
|
+
const where = `vdoms.management static route ${route.prefix}`;
|
|
11273
|
+
cidr(where, route.prefix);
|
|
11274
|
+
if (route.via)
|
|
11275
|
+
ip(where, route.via);
|
|
11276
|
+
if (!route.via && !route.interface && !route.blackhole)
|
|
11277
|
+
report(`${where} has no next hop`);
|
|
11278
|
+
if (route.interface && !management.interfaces.includes(route.interface))
|
|
11279
|
+
report(`${where} leaves through ${route.interface}, which is not in the VDOM`);
|
|
11280
|
+
}
|
|
10989
11281
|
}
|
|
10990
11282
|
static checkLimits(device) {
|
|
10991
11283
|
const limits = Catalog.limits(device.platform, device.model);
|
|
@@ -11569,10 +11861,14 @@ class Cli {
|
|
|
11569
11861
|
return new Cli(configPath, network, options);
|
|
11570
11862
|
}
|
|
11571
11863
|
static async load(path) {
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11864
|
+
let network;
|
|
11865
|
+
if (process.versions.bun) {
|
|
11866
|
+
network = (await import(path)).default;
|
|
11867
|
+
} else {
|
|
11868
|
+
const { createJiti } = await import("jiti");
|
|
11869
|
+
network = await createJiti(import.meta.url).import(path, { default: true });
|
|
11870
|
+
}
|
|
11871
|
+
return { ...network, devices: Cabling.describeLinks(network.devices) };
|
|
11576
11872
|
}
|
|
11577
11873
|
static fail(message) {
|
|
11578
11874
|
console.error(message);
|
|
@@ -12377,7 +12673,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
|
|
|
12377
12673
|
const port = Number(flags.port);
|
|
12378
12674
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
12379
12675
|
Cli.fail("--port takes a number from 1 to 65535");
|
|
12380
|
-
const { StudioServer } = await import("../chunks/server-
|
|
12676
|
+
const { StudioServer } = await import("../chunks/server-ggertnce.js");
|
|
12381
12677
|
const host = flags.host;
|
|
12382
12678
|
const allowHosts = (flags.allowHost ?? "").split(",").filter(Boolean);
|
|
12383
12679
|
if (!StudioServer.isLoopback(host) && !allowHosts.length) {
|