@takodotid/circuit 0.43.1 → 1.0.0-rc.2
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 +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
|
@@ -6,9 +6,9 @@ import type { Client } from "ssh2";
|
|
|
6
6
|
import { Utils } from "../../core/utils";
|
|
7
7
|
import { Address } from "../../core/address";
|
|
8
8
|
import type { Neighbor } from "../../core/cabling";
|
|
9
|
-
import type { Finding } from "
|
|
9
|
+
import type { Finding } from "../finding";
|
|
10
10
|
import { Secrets } from "../../core/secrets";
|
|
11
|
-
import type { Device, Family } from "../../
|
|
11
|
+
import type { Device, Family } from "../../model";
|
|
12
12
|
import { SerialConsole, type ConsoleDialect, ConsoleShell } from "../../transport/console";
|
|
13
13
|
import { Ssh } from "../../transport/ssh";
|
|
14
14
|
import {
|
|
@@ -144,41 +144,41 @@ export class RouterOs extends Platform {
|
|
|
144
144
|
override readonly console = RouterOs.CONSOLE;
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* A BGP session with a `
|
|
147
|
+
* A BGP session with a `routing_table` announces from that table, not from main: each prefix of `originated_prefixes` it announces needs a route in the table too, or it is not announced (TRAPS.md). A session left with nothing to announce is an error; a prefix missing from its table is a warning, since its export may not announce it anyway.
|
|
148
148
|
*/
|
|
149
149
|
checks(device: Device): Finding[] {
|
|
150
150
|
const bgp = device.routing?.bgp;
|
|
151
|
-
const statics = device.routing?.
|
|
151
|
+
const statics = device.routing?.static_routes ?? [];
|
|
152
152
|
const findings: Finding[] = [];
|
|
153
153
|
|
|
154
154
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
155
|
-
const group = neighbor.
|
|
156
|
-
const table = Utils.otherTable(neighbor.
|
|
155
|
+
const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
|
|
156
|
+
const table = Utils.otherTable(neighbor.routing_table ?? group?.routing_table);
|
|
157
157
|
if (!table) continue;
|
|
158
158
|
|
|
159
|
-
const families = neighbor.
|
|
160
|
-
const
|
|
159
|
+
const families = neighbor.address_families ?? group?.address_families ?? [Address.family(neighbor.address)];
|
|
160
|
+
const originated = (bgp?.originated_prefixes ?? []).filter((prefix) => families.includes(Address.family(Address.host(prefix))));
|
|
161
161
|
const inTable = (prefix: string) =>
|
|
162
|
-
statics.some((route) => route.
|
|
163
|
-
// What its export accepts by prefix: a
|
|
164
|
-
const exportPolicy = neighbor.
|
|
165
|
-
const accepted = (exportPolicy ? (device.policies?.[exportPolicy] ?? []) : [])
|
|
166
|
-
.
|
|
167
|
-
.map((
|
|
168
|
-
const announced = accepted.length ?
|
|
162
|
+
statics.some((route) => route.routing_table === table && Address.canonical(route.prefix) === Address.canonical(prefix));
|
|
163
|
+
// What its export accepts by prefix: a prefix it would reject anyway needs no route in the table. Every prefix when the export names none.
|
|
164
|
+
const exportPolicy = neighbor.export_policy ?? group?.export_policy;
|
|
165
|
+
const accepted = (exportPolicy ? (device.routing?.policies?.[exportPolicy] ?? []) : [])
|
|
166
|
+
.flatMap((rule) => (RouterOsRender.onRoutes(rule) && rule.then === "accept" && rule.if?.prefix ? [rule.if.prefix] : []))
|
|
167
|
+
.map((prefix) => Address.canonical(prefix));
|
|
168
|
+
const announced = accepted.length ? originated.filter((prefix) => accepted.includes(Address.canonical(prefix))) : originated;
|
|
169
169
|
|
|
170
170
|
const missing = announced.filter((prefix) => !inTable(prefix));
|
|
171
171
|
if (!missing.length) continue;
|
|
172
172
|
|
|
173
173
|
const where = `routing.bgp.neighbors.${name}`;
|
|
174
|
-
const route = (prefix: string) => `{ prefix: "${prefix}", blackhole: true,
|
|
174
|
+
const route = (prefix: string) => `{ prefix: "${prefix}", blackhole: true, routing_table: "${table}" }`;
|
|
175
175
|
const fix = `On RouterOS a session announces from its own table, ${table}`;
|
|
176
176
|
|
|
177
177
|
if (missing.length === announced.length) {
|
|
178
178
|
findings.push({
|
|
179
179
|
level: "error",
|
|
180
180
|
device: device.name,
|
|
181
|
-
message: `${where} would announce nothing: ${fix}, which has no route for any of
|
|
181
|
+
message: `${where} would announce nothing: ${fix}, which has no route for any of originated_prefixes. Add the routes it announces to ${table} too, such as ${route(missing[0]!)} in routing.static_routes`,
|
|
182
182
|
});
|
|
183
183
|
continue;
|
|
184
184
|
}
|
|
@@ -187,7 +187,7 @@ export class RouterOs extends Platform {
|
|
|
187
187
|
findings.push({
|
|
188
188
|
level: "warning",
|
|
189
189
|
device: device.name,
|
|
190
|
-
message: `${where} does not announce ${prefix}: ${fix}, which has no route for it. If its export should announce it, add ${route(prefix)} to routing.
|
|
190
|
+
message: `${where} does not announce ${prefix}: ${fix}, which has no route for it. If its export should announce it, add ${route(prefix)} to routing.static_routes`,
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -198,74 +198,89 @@ export class RouterOs extends Platform {
|
|
|
198
198
|
unsupported(device: Device): string[] {
|
|
199
199
|
const found: string[] = [];
|
|
200
200
|
|
|
201
|
-
for (const [name
|
|
202
|
-
|
|
201
|
+
for (const [name] of RouterOsRender.entries(device.interfaces?.bridge)) {
|
|
202
|
+
found.push(`interfaces.bridge.${name}, a bridge, which only FortiOS builds for now`);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
for (const [name, port] of
|
|
206
|
-
if (!port) continue;
|
|
207
|
-
|
|
205
|
+
for (const [name, port] of Derive.ports(device)) {
|
|
208
206
|
if (port.speed && !RouterOsRender.medium(Catalog.spell(device.platform, device.model, name), port.speed)) {
|
|
209
|
-
found.push(`
|
|
207
|
+
found.push(`interfaces.ethernet.${name}.speed ${port.speed}, the port has no fixed speed by that name`);
|
|
210
208
|
}
|
|
211
209
|
|
|
212
|
-
const thresholds = Object.values(port.storm_control ?? {});
|
|
210
|
+
const thresholds = Object.values(port.storm_control ?? {}).filter((threshold) => threshold !== undefined);
|
|
213
211
|
const percents = new Set(thresholds.map((threshold) => ("percent" in threshold ? threshold.percent : undefined)));
|
|
214
212
|
|
|
215
|
-
if (port.mac_limit !== undefined) found.push(`
|
|
213
|
+
if (port.mac_limit !== undefined) found.push(`interfaces.ethernet.${name}.mac_limit, ${RouterOs.NO_MAC_LIMIT}`);
|
|
216
214
|
|
|
217
215
|
if (thresholds.some((threshold) => "pps" in threshold)) {
|
|
218
|
-
found.push(`
|
|
216
|
+
found.push(`interfaces.ethernet.${name}.storm_control in pps, the platform takes percent`);
|
|
219
217
|
} else if (percents.size > 1) {
|
|
220
|
-
found.push(`
|
|
218
|
+
found.push(`interfaces.ethernet.${name}.storm_control with a different percent per kind, the platform takes one`);
|
|
221
219
|
}
|
|
222
220
|
}
|
|
223
221
|
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
// Routing in software is set per VLAN interface, on the switch chip that offloads.
|
|
223
|
+
for (const [name, port] of Derive.ports(device))
|
|
224
|
+
if (port.hardware_offload !== undefined) found.push(`interfaces.ethernet.${name}.hardware_offload, set on a VLAN interface`);
|
|
225
|
+
for (const { name, kind, settings } of Derive.interfaces(device)) {
|
|
226
|
+
if (settings.hardware_offload === undefined) continue;
|
|
227
|
+
if (kind !== "vlan_interface") found.push(`interfaces.${kind}.${name}.hardware_offload, set on a VLAN interface`);
|
|
228
|
+
else if (settings.hardware_offload && !device.system?.hardware_offload)
|
|
229
|
+
found.push(`interfaces.vlan_interface.${name}.hardware_offload true while system.hardware_offload is off`);
|
|
230
|
+
}
|
|
226
231
|
|
|
227
|
-
|
|
228
|
-
if (
|
|
232
|
+
for (const [name, lag] of RouterOsRender.entries(device.interfaces?.lag)) {
|
|
233
|
+
if (lag.mac_limit !== undefined) found.push(`interfaces.lag.${name}.mac_limit, ${RouterOs.NO_MAC_LIMIT}`);
|
|
234
|
+
if (lag.ethertypes !== undefined)
|
|
235
|
+
found.push(`interfaces.lag.${name}.ethertypes, on a LAG, which is not proved on a device yet`);
|
|
229
236
|
|
|
230
237
|
for (const field of ["storm_control", "stp"] as const) {
|
|
231
|
-
if (
|
|
238
|
+
if (lag[field] !== undefined) found.push(`interfaces.lag.${name}.${field}, on a LAG`);
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
const bgp = device.routing?.bgp;
|
|
236
243
|
|
|
237
244
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
238
|
-
const group = neighbor.
|
|
239
|
-
const families = neighbor.
|
|
245
|
+
const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
|
|
246
|
+
const families = neighbor.address_families ?? group?.address_families ?? [Address.family(neighbor.address)];
|
|
240
247
|
|
|
241
|
-
if (families.length > 1) found.push(`routing.bgp.neighbors.${name}.
|
|
248
|
+
if (families.length > 1) found.push(`routing.bgp.neighbors.${name}.address_families, one family per session`);
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
const management = device.management;
|
|
252
|
+
const snmp = device.monitoring?.snmp;
|
|
245
253
|
|
|
246
254
|
if (management?.console) found.push("management.console, the console uses the device's users");
|
|
247
255
|
if (management?.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
|
|
248
256
|
if (management?.privilege_password) found.push("management.privilege_password, the platform has no privileged mode");
|
|
249
257
|
|
|
250
|
-
if (
|
|
258
|
+
if (snmp?.port) found.push("monitoring.snmp.port");
|
|
251
259
|
|
|
252
260
|
if (management?.ssh?.auth_timeout || management?.ssh?.auth_retries) {
|
|
253
261
|
found.push("management.ssh.auth_timeout and auth_retries");
|
|
254
262
|
}
|
|
255
263
|
|
|
256
|
-
for (const [name, user] of Object.entries(
|
|
264
|
+
for (const [name, user] of Object.entries(snmp?.users ?? {})) {
|
|
257
265
|
if (user.auth !== "sha1") {
|
|
258
|
-
found.push(
|
|
266
|
+
found.push(
|
|
267
|
+
`monitoring.snmp.users.${name}.auth ${user.auth}: Circuit writes SHA1 only on RouterOS (SHA-2 comes with RouterOS 7.26, not proved yet)`
|
|
268
|
+
);
|
|
259
269
|
}
|
|
260
270
|
}
|
|
261
271
|
|
|
262
|
-
if (device.
|
|
263
|
-
if (device.flow_export?.protocol === "sflow")
|
|
272
|
+
if (device.monitoring?.transceivers === false) found.push("monitoring.transceivers false, it is always on");
|
|
273
|
+
if (device.monitoring?.flow_export?.protocol === "sflow") {
|
|
274
|
+
found.push("monitoring.flow_export.protocol sflow, the platform exports NetFlow and IPFIX");
|
|
275
|
+
}
|
|
264
276
|
|
|
265
|
-
for (const route of device.routing?.
|
|
266
|
-
if (route.bfd)
|
|
277
|
+
for (const route of device.routing?.static_routes ?? []) {
|
|
278
|
+
if (route.bfd)
|
|
279
|
+
found.push(`routing.static_routes ${route.prefix} bfd, a static route's check-gateway takes arp, none or ping on 7.23`);
|
|
267
280
|
}
|
|
268
281
|
|
|
282
|
+
found.push(...RouterOs.unsupportedShapes(device));
|
|
283
|
+
|
|
269
284
|
// BFD on a static route and a MAC limit are refused above, for the reason the device gave, and an ethertype filter on a LAG as unproved.
|
|
270
285
|
found.push(
|
|
271
286
|
...Derive.unproved(device, [
|
|
@@ -281,6 +296,54 @@ export class RouterOs extends Platform {
|
|
|
281
296
|
return found;
|
|
282
297
|
}
|
|
283
298
|
|
|
299
|
+
/** What the v1 model can write that the platform does not render: a list of values in one rule's match, an interface set, an address set named in `management.allow`, and a `{ communities }` rule. */
|
|
300
|
+
private static unsupportedShapes(device: Device): string[] {
|
|
301
|
+
const found: string[] = [];
|
|
302
|
+
const firewall = device.firewall;
|
|
303
|
+
const list = (value: unknown) => Array.isArray(value) && value.length > 1;
|
|
304
|
+
|
|
305
|
+
if (firewall?.interface_sets) found.push("firewall.interface_sets, which the platform does not build yet");
|
|
306
|
+
|
|
307
|
+
const chains = (["input", "forward", "output"] as const).map(
|
|
308
|
+
(chain) => [`firewall.filter.${chain}.rules`, firewall?.filter?.[chain]?.rules] as const
|
|
309
|
+
);
|
|
310
|
+
const nat = [
|
|
311
|
+
["firewall.nat.source", firewall?.nat?.source],
|
|
312
|
+
["firewall.nat.destination", firewall?.nat?.destination],
|
|
313
|
+
] as const;
|
|
314
|
+
|
|
315
|
+
for (const [where, rules] of [...chains, ...nat]) {
|
|
316
|
+
(rules ?? []).forEach((rule, index) => {
|
|
317
|
+
for (const field of ["src", "dst", "in_interface", "out_interface"] as const) {
|
|
318
|
+
if (list(rule.if?.[field])) found.push(`${where}[${index}].if.${field}, a list: write one rule for each`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const field of ["in_interface_set", "out_interface_set"] as const) {
|
|
322
|
+
if (rule.if?.[field] !== undefined) found.push(`${where}[${index}].if.${field}, which the platform does not build yet`);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const [name, rules] of RouterOsRender.entries(device.switching?.acls)) {
|
|
328
|
+
rules.forEach((rule, index) => {
|
|
329
|
+
for (const field of ["src", "dst", "vlan"] as const) {
|
|
330
|
+
if (list(rule.if?.[field])) found.push(`switching.acls.${name}[${index}].if.${field}, a list: write one rule for each`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const allow = device.management?.allow;
|
|
336
|
+
if (typeof allow === "string") found.push(`management.allow ${allow}, an address set by name: write its prefixes`);
|
|
337
|
+
|
|
338
|
+
for (const [name, rules] of RouterOsRender.entries(device.routing?.policies)) {
|
|
339
|
+
rules.forEach((rule, index) => {
|
|
340
|
+
if (rule.communities !== undefined) found.push(`routing.policies.${name}[${index}].communities ${rule.communities}`);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return found;
|
|
345
|
+
}
|
|
346
|
+
|
|
284
347
|
render(device: Device): string {
|
|
285
348
|
return RouterOsRender.of(device);
|
|
286
349
|
}
|
|
@@ -487,7 +550,7 @@ export class RouterOs extends Platform {
|
|
|
487
550
|
}
|
|
488
551
|
|
|
489
552
|
/**
|
|
490
|
-
* Three checks after an apply. When the plan touched switch rules, none may be invalid. BGP sessions established before the apply must be established again, and announce again each of the device's `
|
|
553
|
+
* Three checks after an apply. When the plan touched switch rules, none may be invalid. BGP sessions established before the apply must be established again, and announce again each of the device's `originated_prefixes` they announced before, within `BgpWait.AFTER_APPLY`. A session can stay up and stop announcing (TRAPS.md), which the session check alone would not see.
|
|
491
554
|
*/
|
|
492
555
|
async health(device: Device, plan: Plan, options: HealthOptions = {}): Promise<HealthCheck> {
|
|
493
556
|
const switchRules = plan.steps.some((step) => step.send.some((command) => command.startsWith(RouterOs.SWITCH_RULES)));
|
|
@@ -551,9 +614,7 @@ export class RouterOs extends Platform {
|
|
|
551
614
|
*/
|
|
552
615
|
private static farPort(network: readonly Device[], system: string, reported: string): string {
|
|
553
616
|
const far = network.find((candidate) => system === candidate.name || system.startsWith(`${candidate.name}.`));
|
|
554
|
-
const described =
|
|
555
|
-
.filter(([, port]) => port?.description === reported)
|
|
556
|
-
.map(([name]) => name);
|
|
617
|
+
const described = (far ? Derive.ports(far) : []).filter(([, port]) => port.description === reported).map(([name]) => name);
|
|
557
618
|
|
|
558
619
|
return described.length === 1 ? described[0]! : reported;
|
|
559
620
|
}
|
|
@@ -625,9 +686,9 @@ export class RouterOs extends Platform {
|
|
|
625
686
|
);
|
|
626
687
|
}
|
|
627
688
|
|
|
628
|
-
/** What each session announces of the device's own `
|
|
689
|
+
/** What each session announces of the device's own `originated_prefixes`, as `neighbor prefix`. None, without logging in, on a device that originates none. */
|
|
629
690
|
private async announcedNow(device: Device, options: ReadOptions): Promise<string[]> {
|
|
630
|
-
if (!device.routing?.bgp?.
|
|
691
|
+
if (!device.routing?.bgp?.originated_prefixes?.length) return [];
|
|
631
692
|
|
|
632
693
|
const session = await this.session(device, options);
|
|
633
694
|
|
|
@@ -638,11 +699,11 @@ export class RouterOs extends Platform {
|
|
|
638
699
|
}
|
|
639
700
|
}
|
|
640
701
|
|
|
641
|
-
/** The `
|
|
702
|
+
/** The `originated_prefixes` each session announces, as `neighbor prefix`, sorted. An advertisement names its session, which is matched to its neighbor by address first, as in `bgpSessions`. */
|
|
642
703
|
private async announced(device: Device, session: ReadSession): Promise<string[]> {
|
|
643
704
|
const bgp = device.routing?.bgp;
|
|
644
|
-
const
|
|
645
|
-
if (!
|
|
705
|
+
const originated = bgp?.originated_prefixes ?? [];
|
|
706
|
+
if (!originated.length) return [];
|
|
646
707
|
|
|
647
708
|
const sessions = (await session.run(RouterOs.LIST_BGP_SESSIONS, 60_000))
|
|
648
709
|
.split(/\r?\n/)
|
|
@@ -656,7 +717,7 @@ export class RouterOs extends Platform {
|
|
|
656
717
|
return (byAddress ?? byName)?.[0];
|
|
657
718
|
};
|
|
658
719
|
|
|
659
|
-
const output = await session.run(RouterOs.listAdvertisements(
|
|
720
|
+
const output = await session.run(RouterOs.listAdvertisements(originated), 60_000);
|
|
660
721
|
return output
|
|
661
722
|
.split(/\r?\n/)
|
|
662
723
|
.map((line) => line.trim().split("|"))
|
|
@@ -687,7 +748,7 @@ export class RouterOs extends Platform {
|
|
|
687
748
|
|
|
688
749
|
return gone.map((pair) => {
|
|
689
750
|
const [neighbor, prefix] = pair.split(" ");
|
|
690
|
-
return `BGP session ${neighbor} announced ${prefix} before the apply and does not ${limits.seconds} s after it, though it may still be established. A RouterOS session announces from its routing table: that table needs a route for ${prefix}, and a session whose table changed must restart. Look with: circuit
|
|
751
|
+
return `BGP session ${neighbor} announced ${prefix} before the apply and does not ${limits.seconds} s after it, though it may still be established. A RouterOS session announces from its routing table: that table needs a route for ${prefix}, and a session whose table changed must restart. Look with: circuit exec ${device.name} '/routing bgp advertisements print'`;
|
|
691
752
|
});
|
|
692
753
|
}
|
|
693
754
|
|
|
@@ -703,7 +764,7 @@ export class RouterOs extends Platform {
|
|
|
703
764
|
}
|
|
704
765
|
|
|
705
766
|
return [
|
|
706
|
-
`${invalid} switch rule(s) are invalid: the switch chip cannot hold them, and every rule after the first invalid one is invalid too, so those rules do nothing. Use fewer distinct port ranges in this device's ACLs, then apply again. See them with: circuit
|
|
767
|
+
`${invalid} switch rule(s) are invalid: the switch chip cannot hold them, and every rule after the first invalid one is invalid too, so those rules do nothing. Use fewer distinct port ranges in this device's ACLs, then apply again. See them with: circuit exec ${device.name} '${RouterOs.SWITCH_RULES} print where invalid'`,
|
|
707
768
|
];
|
|
708
769
|
}
|
|
709
770
|
|
|
@@ -737,7 +798,7 @@ export class RouterOs extends Platform {
|
|
|
737
798
|
const opened = await SerialConsole.open(console);
|
|
738
799
|
|
|
739
800
|
try {
|
|
740
|
-
await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.
|
|
801
|
+
await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.password_only);
|
|
741
802
|
} catch (error) {
|
|
742
803
|
opened.close();
|
|
743
804
|
throw error;
|
|
@@ -748,7 +809,7 @@ export class RouterOs extends Platform {
|
|
|
748
809
|
const session: Session = {
|
|
749
810
|
run: (command, timeout) => shell.run(command, timeout),
|
|
750
811
|
fresh: async () => {
|
|
751
|
-
await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.
|
|
812
|
+
await SerialConsole.login(opened.channel, RouterOs.CONSOLE, user, password, console.password_only);
|
|
752
813
|
return session;
|
|
753
814
|
},
|
|
754
815
|
end: () => {
|
|
@@ -798,12 +859,12 @@ export class RouterOs extends Platform {
|
|
|
798
859
|
|
|
799
860
|
/** Upload a certificate and its key, import both under one name, and delete the files. */
|
|
800
861
|
private static async installCertificate(session: Session, device: Device, name: string): Promise<void> {
|
|
801
|
-
const certificate = device.certificates?.[name];
|
|
802
|
-
if (!certificate) throw new Error(`certificate ${name} is not declared`);
|
|
862
|
+
const certificate = device.system?.certificates?.[name];
|
|
863
|
+
if (!certificate) throw new Error(`certificate ${name} is not declared in system.certificates`);
|
|
803
864
|
if (!session.upload)
|
|
804
865
|
throw new Error(`certificate ${name} is installed from a file, which only SSH can write; apply it without --console`);
|
|
805
866
|
|
|
806
|
-
await session.upload(`${name}.crt`, certificate.
|
|
867
|
+
await session.upload(`${name}.crt`, Utils.projectFile(certificate.certificate_file));
|
|
807
868
|
await session.run(`/certificate import file-name=${name}.crt name=${name} passphrase=""`);
|
|
808
869
|
|
|
809
870
|
if (certificate.private_key) {
|