@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
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { Address } from "../../core/address";
|
|
4
4
|
import { Secrets } from "../../core/secrets";
|
|
5
|
-
import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../
|
|
5
|
+
import type { Device, Family, Neighbor, NeighborSettings, PolicyRule, PrefixSet, RouteRule } from "../../model";
|
|
6
6
|
import { FortiOsNames } from "./names";
|
|
7
7
|
import { FortiOsBlock, FortiOsTree } from "./tree";
|
|
8
8
|
|
|
9
|
-
type AnyRule = PolicyRule<string, string>;
|
|
10
|
-
type AnyNeighbor = Neighbor<string, string, string>;
|
|
11
|
-
|
|
12
9
|
/** The lengths a prefix list's rule takes within its prefix. */
|
|
13
10
|
type Lengths = { ge?: number; le?: number };
|
|
14
11
|
|
|
@@ -59,8 +56,8 @@ class RouteMaps {
|
|
|
59
56
|
return name;
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
private rule(entry: FortiOsBlock, rule:
|
|
63
|
-
const match = rule.
|
|
59
|
+
private rule(entry: FortiOsBlock, rule: RouteRule, family: Family, label: string): void {
|
|
60
|
+
const match = rule.if ?? {};
|
|
64
61
|
const set = rule.set ?? {};
|
|
65
62
|
const address = family === "ipv4" ? "match-ip-address" : "match-ip6-address";
|
|
66
63
|
const bits = FortiOsRouting.BITS[family];
|
|
@@ -90,27 +87,29 @@ class RouteMaps {
|
|
|
90
87
|
entry.set(address, FortiOsTree.quote(name));
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
if (match.
|
|
90
|
+
if (match.standard_community)
|
|
94
91
|
entry.set(
|
|
95
92
|
"match-community",
|
|
96
|
-
FortiOsTree.quote(
|
|
93
|
+
FortiOsTree.quote(
|
|
94
|
+
this.communities(`circuit-match-${RouteMaps.slug(match.standard_community)}`, [match.standard_community], false)
|
|
95
|
+
)
|
|
97
96
|
);
|
|
98
97
|
|
|
99
98
|
if (set.local_pref !== undefined) entry.set("set-local-preference", set.local_pref);
|
|
100
99
|
if (set.med !== undefined) entry.set("set-metric", set.med);
|
|
101
100
|
if (set.prepend !== undefined)
|
|
102
|
-
entry.set("set-aspath", FortiOsTree.quote(Array(set.prepend).fill(this.device.routing
|
|
101
|
+
entry.set("set-aspath", FortiOsTree.quote(Array(set.prepend).fill(this.device.routing?.bgp?.asn).join(" ")));
|
|
103
102
|
|
|
104
103
|
// Replacing the communities makes removing some needless; added ones join the new ones.
|
|
105
|
-
if (set.
|
|
106
|
-
entry.set("set-community", FortiOsTree.quoteAll([...set.
|
|
104
|
+
if (set.standard_communities) {
|
|
105
|
+
entry.set("set-community", FortiOsTree.quoteAll([...set.standard_communities, ...(set.add_standard_communities ?? [])]));
|
|
107
106
|
} else {
|
|
108
|
-
if (set.
|
|
109
|
-
entry.set("set-community", FortiOsTree.quoteAll(set.
|
|
107
|
+
if (set.add_standard_communities)
|
|
108
|
+
entry.set("set-community", FortiOsTree.quoteAll(set.add_standard_communities)).set("set-community-additive", "enable");
|
|
110
109
|
|
|
111
|
-
if (set.
|
|
112
|
-
const name = `circuit-remove-${RouteMaps.slug(set.
|
|
113
|
-
entry.set("set-community-delete", FortiOsTree.quote(this.communities(name, set.
|
|
110
|
+
if (set.remove_standard_communities) {
|
|
111
|
+
const name = `circuit-remove-${RouteMaps.slug(set.remove_standard_communities.join(" "))}`;
|
|
112
|
+
entry.set("set-community-delete", FortiOsTree.quote(this.communities(name, set.remove_standard_communities, true)));
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
|
|
@@ -118,13 +117,12 @@ class RouteMaps {
|
|
|
118
117
|
entry.set(family === "ipv4" ? "set-ip-nexthop" : "set-ip6-nexthop", set.next_hop);
|
|
119
118
|
|
|
120
119
|
// Permit is the default and is not printed.
|
|
121
|
-
if (rule.
|
|
120
|
+
if (rule.then === "reject") entry.set("action", "deny");
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
/** The prefixes of a set the config lists. */
|
|
125
124
|
private prefixesOf(set: string): string[] {
|
|
126
|
-
|
|
127
|
-
return prefixes && "prefixes" in prefixes ? [...prefixes.prefixes] : [];
|
|
125
|
+
return FortiOsRouting.listed(this.device.routing?.prefix_sets?.[set]) ?? [];
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
/** A prefix list of its own, rule by rule; a prefix no length fits is left out. */
|
|
@@ -167,19 +165,25 @@ export class FortiOsRouting {
|
|
|
167
165
|
static readonly BITS = { ipv4: 32, ipv6: 128 } as const;
|
|
168
166
|
|
|
169
167
|
/** A neighbor's settings with its group's beneath them. */
|
|
170
|
-
static neighborSettings(device: Device, neighbor:
|
|
171
|
-
const group = neighbor.
|
|
168
|
+
static neighborSettings(device: Device, neighbor: Neighbor): NeighborSettings {
|
|
169
|
+
const group = neighbor.neighbor_group ? device.routing?.bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
|
|
172
170
|
return { ...group, ...neighbor };
|
|
173
171
|
}
|
|
174
172
|
|
|
175
173
|
/** The families a session carries: its own, or the family of its address. */
|
|
176
|
-
static familiesOfNeighbor(device: Device, neighbor:
|
|
177
|
-
return [...(FortiOsRouting.neighborSettings(device, neighbor).
|
|
174
|
+
static familiesOfNeighbor(device: Device, neighbor: Neighbor): Family[] {
|
|
175
|
+
return [...(FortiOsRouting.neighborSettings(device, neighbor).address_families ?? [Address.family(neighbor.address)])];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The prefixes a set lists, from a list or a map of named prefixes; undefined for a set a registry fills. */
|
|
179
|
+
static listed(set: PrefixSet | undefined): string[] | undefined {
|
|
180
|
+
if (!set || !("prefixes" in set)) return undefined;
|
|
181
|
+
return Array.isArray(set.prefixes) ? [...set.prefixes] : Object.values(set.prefixes);
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
/** Whether a static route, OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
|
|
181
185
|
static usesBfd(device: Device): boolean {
|
|
182
|
-
const routes = (device.routing?.
|
|
186
|
+
const routes = (device.routing?.static_routes ?? []).some((route) => route.bfd);
|
|
183
187
|
const areas = Object.values(device.routing?.ospf?.areas ?? {});
|
|
184
188
|
const ospf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
|
|
185
189
|
const bgp = Object.values(device.routing?.bgp?.neighbors ?? {}).some(
|
|
@@ -219,7 +223,9 @@ export class FortiOsRouting {
|
|
|
219
223
|
|
|
220
224
|
/** Each prefix set as a prefix list matching its prefixes and any more specific. One a registry fills starts with a rule that denies every prefix, so it matches nothing until `refresh` fills it. */
|
|
221
225
|
private static prefixLists(root: FortiOsBlock, device: Device): void {
|
|
222
|
-
for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
|
|
226
|
+
for (const [name, set] of Object.entries(device.routing?.prefix_sets ?? {})) {
|
|
227
|
+
if (!set) continue;
|
|
228
|
+
|
|
223
229
|
if ("source" in set) {
|
|
224
230
|
const { registry, query, family } = set.source;
|
|
225
231
|
const entry = root
|
|
@@ -233,9 +239,10 @@ export class FortiOsRouting {
|
|
|
233
239
|
continue;
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
const
|
|
242
|
+
const prefixes = FortiOsRouting.listed(set) ?? [];
|
|
243
|
+
const family = prefixes[0] ? FortiOsNames.familyOfPrefix(prefixes[0]) : "ipv4";
|
|
237
244
|
const entry = root.block(FortiOsRouting.PREFIX_LIST[family]).entry(name);
|
|
238
|
-
|
|
245
|
+
prefixes.forEach((prefix, index) =>
|
|
239
246
|
FortiOsRouting.listRule(entry.block("rule").entry(index + 1), family, prefix, FortiOsRouting.bounds(prefix, family))
|
|
240
247
|
);
|
|
241
248
|
}
|
|
@@ -251,7 +258,7 @@ export class FortiOsRouting {
|
|
|
251
258
|
/** OSPF for IPv4. An area runs on the networks of its interfaces; an interface named with a cost, a link type or BFD has settings of its own; a passive one forms no adjacency. */
|
|
252
259
|
private static ospf(root: FortiOsBlock, device: Device): void {
|
|
253
260
|
const ospf = device.routing?.ospf;
|
|
254
|
-
if (!ospf || !(ospf.
|
|
261
|
+
if (!ospf || !(ospf.address_families ?? ["ipv4"]).includes("ipv4")) return;
|
|
255
262
|
|
|
256
263
|
const block = root.block("router ospf");
|
|
257
264
|
if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
|
|
@@ -290,7 +297,7 @@ export class FortiOsRouting {
|
|
|
290
297
|
/** OSPFv3 for IPv6. Every interface of an area runs it in that area. */
|
|
291
298
|
private static ospf6(root: FortiOsBlock, device: Device): void {
|
|
292
299
|
const ospf = device.routing?.ospf;
|
|
293
|
-
if (!ospf?.
|
|
300
|
+
if (!ospf?.address_families?.includes("ipv6")) return;
|
|
294
301
|
|
|
295
302
|
const block = root.block("router ospf6");
|
|
296
303
|
if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
|
|
@@ -323,12 +330,14 @@ export class FortiOsRouting {
|
|
|
323
330
|
if (!bgp) return;
|
|
324
331
|
|
|
325
332
|
const block = root.block("router bgp");
|
|
326
|
-
block.set("as", bgp.asn);
|
|
333
|
+
if (bgp.asn !== undefined) block.set("as", bgp.asn);
|
|
327
334
|
if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
|
|
328
335
|
|
|
329
336
|
const maps = new RouteMaps(root, device);
|
|
330
337
|
|
|
331
338
|
for (const neighbor of Object.values(bgp.neighbors ?? {})) {
|
|
339
|
+
if (!neighbor) continue;
|
|
340
|
+
|
|
332
341
|
const settings = FortiOsRouting.neighborSettings(device, neighbor);
|
|
333
342
|
const families = FortiOsRouting.familiesOfNeighbor(device, neighbor);
|
|
334
343
|
const entry = block.block("neighbor").entry(neighbor.address);
|
|
@@ -345,8 +354,8 @@ export class FortiOsRouting {
|
|
|
345
354
|
for (const family of families) {
|
|
346
355
|
const suffix = family === "ipv4" ? "" : "6";
|
|
347
356
|
if (settings.max_prefixes !== undefined) entry.set(`maximum-prefix${suffix}`, settings.max_prefixes);
|
|
348
|
-
entry.set(`route-map-in${suffix}`, FortiOsTree.quote(maps.of(settings.
|
|
349
|
-
entry.set(`route-map-out${suffix}`, FortiOsTree.quote(maps.of(settings.
|
|
357
|
+
entry.set(`route-map-in${suffix}`, FortiOsTree.quote(maps.of(settings.import_policy, family)));
|
|
358
|
+
entry.set(`route-map-out${suffix}`, FortiOsTree.quote(maps.of(settings.export_policy, family)));
|
|
350
359
|
}
|
|
351
360
|
|
|
352
361
|
if (settings.keepalive !== undefined) entry.set("keep-alive-timer", settings.keepalive);
|
|
@@ -360,7 +369,7 @@ export class FortiOsRouting {
|
|
|
360
369
|
|
|
361
370
|
const networks = { ipv4: 0, ipv6: 0 };
|
|
362
371
|
|
|
363
|
-
for (const prefix of bgp.
|
|
372
|
+
for (const prefix of bgp.originated_prefixes ?? []) {
|
|
364
373
|
const family = FortiOsNames.familyOfPrefix(prefix);
|
|
365
374
|
const entry = block.block(family === "ipv4" ? "network" : "network6").entry(++networks[family]);
|
|
366
375
|
entry.set(family === "ipv4" ? "prefix" : "prefix6", FortiOsRouting.listPrefix(prefix, family));
|
|
@@ -368,30 +377,41 @@ export class FortiOsRouting {
|
|
|
368
377
|
}
|
|
369
378
|
|
|
370
379
|
/** The family a rule is limited to, by its match: a family, a prefix, or a prefix set of one family. */
|
|
371
|
-
static familyOfRule(device: Device, rule:
|
|
372
|
-
const match = rule.
|
|
380
|
+
static familyOfRule(device: Device, rule: RouteRule): Family | undefined {
|
|
381
|
+
const match = rule.if;
|
|
373
382
|
if (match?.family) return match.family;
|
|
374
383
|
if (match?.prefix) return FortiOsNames.familyOfPrefix(match.prefix);
|
|
375
384
|
|
|
376
|
-
const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
|
|
385
|
+
const set = match?.prefix_set ? device.routing?.prefix_sets?.[match.prefix_set] : undefined;
|
|
377
386
|
if (set && "source" in set) return set.source.family;
|
|
378
|
-
|
|
387
|
+
|
|
388
|
+
const first = FortiOsRouting.listed(set)?.[0];
|
|
389
|
+
if (first) return FortiOsNames.familyOfPrefix(first);
|
|
379
390
|
|
|
380
391
|
return undefined;
|
|
381
392
|
}
|
|
382
393
|
|
|
383
|
-
/** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller. */
|
|
384
|
-
static flatten(device: Device, policy: string, seen: string[] = [policy]):
|
|
385
|
-
const flat:
|
|
394
|
+
/** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller. A `{ communities }` rule, which Circuit writes out before the platform reads it, is left out; `unsupported()` refuses one. */
|
|
395
|
+
static flatten(device: Device, policy: string, seen: string[] = [policy]): RouteRule[] {
|
|
396
|
+
const flat: RouteRule[] = [];
|
|
386
397
|
|
|
387
|
-
for (const rule of device.policies?.[policy] ?? []) {
|
|
388
|
-
if (
|
|
389
|
-
|
|
398
|
+
for (const rule of device.routing?.policies?.[policy] ?? []) {
|
|
399
|
+
if ("call" in rule && rule.call !== undefined) {
|
|
400
|
+
if (!seen.includes(rule.call)) flat.push(...FortiOsRouting.flatten(device, rule.call, [...seen, rule.call]));
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (FortiOsRouting.isRouteRule(rule) && (rule.if || rule.set || rule.then)) flat.push(rule);
|
|
390
405
|
}
|
|
391
406
|
|
|
392
407
|
return flat;
|
|
393
408
|
}
|
|
394
409
|
|
|
410
|
+
/** Whether a policy rule is a rule on routes, not a call or a `{ communities }` rule. */
|
|
411
|
+
static isRouteRule(rule: PolicyRule): rule is RouteRule {
|
|
412
|
+
return !("call" in rule && rule.call !== undefined) && !("communities" in rule && rule.communities !== undefined);
|
|
413
|
+
}
|
|
414
|
+
|
|
395
415
|
/** Routing the platform cannot express, or that this adapter does not yet write. */
|
|
396
416
|
static gaps(device: Device): string[] {
|
|
397
417
|
const found: string[] = [];
|
|
@@ -399,9 +419,10 @@ export class FortiOsRouting {
|
|
|
399
419
|
|
|
400
420
|
if (routing?.rpki) found.push("routing.rpki, the platform has no route origin validation");
|
|
401
421
|
if (routing?.router_id && !routing.ospf && !routing.bgp) found.push("routing.router_id without OSPF or BGP, which each hold it");
|
|
422
|
+
if (routing?.bgp && routing.bgp.asn === undefined) found.push("routing.bgp without asn, which the platform's BGP needs");
|
|
402
423
|
|
|
403
424
|
const ospf = routing?.ospf;
|
|
404
|
-
if (ospf?.
|
|
425
|
+
if (ospf?.address_families?.includes("ipv6") && !routing?.router_id) found.push("routing.ospf for IPv6 without routing.router_id");
|
|
405
426
|
|
|
406
427
|
for (const area of Object.values(ospf?.areas ?? {})) {
|
|
407
428
|
for (const name of Object.keys(area.interfaces)) {
|
|
@@ -411,12 +432,14 @@ export class FortiOsRouting {
|
|
|
411
432
|
const ipv4 = FortiOsNames.addressesOf(FortiOsNames.settingsOf(device, name)).some(
|
|
412
433
|
(address) => Address.family(Address.host(address)) === "ipv4"
|
|
413
434
|
);
|
|
414
|
-
if ((ospf?.
|
|
435
|
+
if ((ospf?.address_families ?? ["ipv4"]).includes("ipv4") && !ipv4)
|
|
415
436
|
found.push(`routing.ospf on ${name}, which has no IPv4 address for OSPF to run on`);
|
|
416
437
|
}
|
|
417
438
|
}
|
|
418
439
|
|
|
419
440
|
for (const [name, neighbor] of Object.entries(routing?.bgp?.neighbors ?? {})) {
|
|
441
|
+
if (!neighbor) continue;
|
|
442
|
+
|
|
420
443
|
const where = `routing.bgp.neighbors.${name}`;
|
|
421
444
|
const settings = FortiOsRouting.neighborSettings(device, neighbor);
|
|
422
445
|
|
|
@@ -430,27 +453,35 @@ export class FortiOsRouting {
|
|
|
430
453
|
}
|
|
431
454
|
|
|
432
455
|
if (Address.family(neighbor.address) === "ipv6" && FortiOsRouting.familiesOfNeighbor(device, neighbor).includes("ipv4")) {
|
|
433
|
-
found.push(`${where}.
|
|
456
|
+
found.push(`${where}.address_families ipv4 over an IPv6 session`);
|
|
434
457
|
}
|
|
435
458
|
}
|
|
436
459
|
|
|
437
|
-
for (const [name, rules] of Object.entries(
|
|
438
|
-
rules.forEach((rule, index) =>
|
|
460
|
+
for (const [name, rules] of Object.entries(routing?.policies ?? {})) {
|
|
461
|
+
(rules ?? []).forEach((rule, index) =>
|
|
462
|
+
found.push(...FortiOsRouting.ruleGaps(device, `routing.policies.${name}[${index}]`, rule))
|
|
463
|
+
);
|
|
439
464
|
}
|
|
440
465
|
|
|
441
466
|
return found;
|
|
442
467
|
}
|
|
443
468
|
|
|
444
|
-
private static ruleGaps(device: Device, where: string, rule:
|
|
469
|
+
private static ruleGaps(device: Device, where: string, rule: PolicyRule): string[] {
|
|
470
|
+
if (!FortiOsRouting.isRouteRule(rule)) {
|
|
471
|
+
return "communities" in rule && rule.communities !== undefined
|
|
472
|
+
? [`${where}.communities, a rule Circuit writes out as route rules before the platform reads it`]
|
|
473
|
+
: [];
|
|
474
|
+
}
|
|
475
|
+
|
|
445
476
|
const found: string[] = [];
|
|
446
|
-
const match = rule.
|
|
477
|
+
const match = rule.if ?? {};
|
|
447
478
|
const set = rule.set ?? {};
|
|
448
479
|
|
|
449
|
-
if ((rule.
|
|
450
|
-
found.push(`${where}, a rule without
|
|
480
|
+
if ((rule.if || rule.set) && !rule.then)
|
|
481
|
+
found.push(`${where}, a rule without then: the platform stops at the first rule that matches`);
|
|
451
482
|
|
|
452
|
-
for (const field of ["as_path_length", "
|
|
453
|
-
if (match[field] !== undefined) found.push(`${where}.
|
|
483
|
+
for (const field of ["as_path_length", "as_path_contains", "rpki", "large_community"] as const) {
|
|
484
|
+
if (match[field] !== undefined) found.push(`${where}.if.${field}`);
|
|
454
485
|
}
|
|
455
486
|
|
|
456
487
|
for (const field of [
|
|
@@ -463,12 +494,12 @@ export class FortiOsRouting {
|
|
|
463
494
|
if (set[field] !== undefined) found.push(`${where}.set.${field}`);
|
|
464
495
|
}
|
|
465
496
|
|
|
466
|
-
if (match.prefix && match.prefix_length) found.push(`${where}.
|
|
467
|
-
if (match.prefix && match.prefix_set) found.push(`${where}.
|
|
497
|
+
if (match.prefix && match.prefix_length) found.push(`${where}.if.prefix with prefix_length`);
|
|
498
|
+
if (match.prefix && match.prefix_set) found.push(`${where}.if.prefix with prefix_set`);
|
|
468
499
|
|
|
469
|
-
const prefixSet = match.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
|
|
500
|
+
const prefixSet = match.prefix_set ? device.routing?.prefix_sets?.[match.prefix_set] : undefined;
|
|
470
501
|
if (prefixSet && "source" in prefixSet && match.prefix_length) {
|
|
471
|
-
found.push(`${where}.
|
|
502
|
+
found.push(`${where}.if.prefix_length with a prefix set a registry fills`);
|
|
472
503
|
}
|
|
473
504
|
|
|
474
505
|
return found;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Virtual domains: a management VDOM beside `root`. In multi-VDOM mode some sections are the device's own, under `config global`, and the rest belong to one VDOM, under `config vdom` and `edit <name>`. The render and the read write a management VDOM's sections as `@<name> <section>`, so both sides compare like any other section; a plan is made for each scope on its own, and each of its steps sent in that scope.
|
|
2
2
|
|
|
3
|
-
import type { Device } from "../../
|
|
3
|
+
import type { Device } from "../../model";
|
|
4
|
+
import { Derive } from "../derive";
|
|
4
5
|
import type { Plan, Step } from "../platform";
|
|
6
|
+
import { FortiOsNames } from "./names";
|
|
5
7
|
import { FortiOsBlock, FortiOsTree } from "./tree";
|
|
6
8
|
|
|
7
9
|
/** Where a section lives in multi-VDOM mode: the device's own, or one VDOM's. */
|
|
@@ -49,15 +51,15 @@ export class FortiOsVdoms {
|
|
|
49
51
|
|
|
50
52
|
/** The management VDOM's name, when the device has one. */
|
|
51
53
|
static managementOf(device: Device): string | undefined {
|
|
52
|
-
return device.vdoms?.management.name;
|
|
54
|
+
return device.system?.vdoms?.management.name;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** The VDOM a port or interface is in, by Circuit's name. A port in a LAG is where its LAG is. */
|
|
56
58
|
static vdomOf(device: Device, name: string): string {
|
|
57
|
-
const management = device.vdoms?.management;
|
|
59
|
+
const management = device.system?.vdoms?.management;
|
|
58
60
|
if (!management) return FortiOsVdoms.ROOT;
|
|
59
61
|
|
|
60
|
-
const lag = device.
|
|
62
|
+
const lag = device.interfaces?.ethernet?.[name]?.in_lag;
|
|
61
63
|
const holder = lag ?? name;
|
|
62
64
|
return (management.interfaces as readonly string[]).includes(holder) ? management.name : FortiOsVdoms.ROOT;
|
|
63
65
|
}
|
|
@@ -119,7 +121,7 @@ export class FortiOsVdoms {
|
|
|
119
121
|
}
|
|
120
122
|
if (!wantMulti && haveMulti) {
|
|
121
123
|
return refused(
|
|
122
|
-
"the device runs several VDOMs and the config has no vdoms. Write vdoms.management, or turn multi-VDOM mode off by hand"
|
|
124
|
+
"the device runs several VDOMs and the config has no vdoms. Write system.vdoms.management, or turn multi-VDOM mode off by hand"
|
|
123
125
|
);
|
|
124
126
|
}
|
|
125
127
|
if (!wantMulti) return planOne(desired, current);
|
|
@@ -187,29 +189,58 @@ export class FortiOsVdoms {
|
|
|
187
189
|
});
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
/** What makes a management VDOM wrong, whatever the platform can express: it is not `root`, it lists a LAG rather than its ports, and each of its routes has a next hop and leaves through one of its interfaces. */
|
|
193
|
+
static problems(device: Device): string[] {
|
|
194
|
+
const management = device.system?.vdoms?.management;
|
|
195
|
+
if (!management) return [];
|
|
196
|
+
|
|
197
|
+
const found: string[] = [];
|
|
198
|
+
const where = "system.vdoms.management";
|
|
199
|
+
|
|
200
|
+
if (management.name === FortiOsVdoms.ROOT)
|
|
201
|
+
found.push(`${where}.name is root, the VDOM everything else is in; give it a name of its own`);
|
|
202
|
+
|
|
203
|
+
for (const name of management.interfaces) {
|
|
204
|
+
const lag = device.interfaces?.ethernet?.[name]?.in_lag;
|
|
205
|
+
if (lag) found.push(`${where}.interfaces ${name} is in LAG ${lag}; list the LAG`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const route of management.static_routes ?? []) {
|
|
209
|
+
const at = `${where}.static_routes ${route.prefix}`;
|
|
210
|
+
if (!route.next_hop && !route.interface && !route.blackhole) found.push(`${at} has no next hop`);
|
|
211
|
+
if (route.interface && !(management.interfaces as readonly string[]).includes(route.interface))
|
|
212
|
+
found.push(`${at} leaves through ${route.interface}, which is not in the VDOM`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return found;
|
|
216
|
+
}
|
|
217
|
+
|
|
190
218
|
/** What a management VDOM can't hold yet, or a config with one can't have. */
|
|
219
|
+
|
|
191
220
|
static gaps(device: Device): string[] {
|
|
192
|
-
|
|
221
|
+
const management = device.system?.vdoms?.management;
|
|
222
|
+
if (!management) return [];
|
|
193
223
|
|
|
194
224
|
const found: string[] = [];
|
|
195
|
-
const tunnels =
|
|
225
|
+
const tunnels = FortiOsNames.interfacesOf(device).filter(({ kind }) => kind === "gre" || kind === "vxlan");
|
|
196
226
|
if (tunnels.length)
|
|
197
|
-
found.push(
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
found.push(
|
|
228
|
+
`system.vdoms with GRE or VXLAN tunnels (${tunnels.map(({ name }) => name).join(", ")}), whose tables belong to a VDOM`
|
|
229
|
+
);
|
|
230
|
+
if (FortiOsNames.isTransparent(device)) found.push("system.vdoms with a bridge, which needs transparent mode");
|
|
200
231
|
|
|
201
|
-
const management = device.vdoms.management;
|
|
202
232
|
const inManagement = (name: string | undefined) =>
|
|
203
233
|
name !== undefined && (management.interfaces as readonly string[]).includes(name);
|
|
204
234
|
|
|
205
|
-
for (const route of device.routing?.
|
|
235
|
+
for (const route of device.routing?.static_routes ?? []) {
|
|
206
236
|
if (inManagement(route.interface))
|
|
207
|
-
found.push(
|
|
237
|
+
found.push(
|
|
238
|
+
`routing.static_routes ${route.prefix} leaves through ${route.interface}, in system.vdoms.management; write it in its static_routes`
|
|
239
|
+
);
|
|
208
240
|
}
|
|
209
241
|
for (const name of management.interfaces) {
|
|
210
|
-
const settings
|
|
211
|
-
|
|
212
|
-
if (settings && "vrf" in settings && settings.vrf) found.push(`vdoms.management interface ${name} in VRF ${settings.vrf}`);
|
|
242
|
+
const vrf = Derive.settings(device, name)?.vrf;
|
|
243
|
+
if (vrf) found.push(`system.vdoms.management interface ${name} in VRF ${vrf}`);
|
|
213
244
|
}
|
|
214
245
|
|
|
215
246
|
return found;
|
|
@@ -22,7 +22,7 @@ Behaviour that looks like something else. Each one has been hit on a QFX5100-48S
|
|
|
22
22
|
- The factory VLAN `default` can be deleted.
|
|
23
23
|
- BGP without a licence commits with only a warning, `requires 'bgp' license`, and sessions establish. The same is true for the other licensed features.
|
|
24
24
|
- Junos accepts and announces every route over eBGP unless a policy says otherwise: `Default eBGP mode: advertise - accept, receive - accept`. A session without a policy is given `circuit-reject-all`.
|
|
25
|
-
- An export policy sees every active route, not only BGP's. Every export starts with `circuit-bgp-origin`, which passes on BGP's routes and the prefixes in `routing.bgp.
|
|
25
|
+
- An export policy sees every active route, not only BGP's. Every export starts with `circuit-bgp-origin`, which passes on BGP's routes and the prefixes in `routing.bgp.originated_prefixes` and rejects the rest.
|
|
26
26
|
- A policy's or a filter's terms are kept in order, and `set` adds a new term at the end. A change to one rewrites it whole.
|
|
27
27
|
- One policy term cannot filter IPv4 and IPv6 prefixes together: `invalid prefix ::/0 for family inet`. A rule for both families becomes a term for each.
|
|
28
28
|
- A port filter cannot match the VLAN a frame is switched in: `learn-vlan-id not supported on ingress interface`.
|
|
@@ -38,5 +38,5 @@ Behaviour that looks like something else. Each one has been hit on a QFX5100-48S
|
|
|
38
38
|
- A static route with BFD is withdrawn while its next hop does not answer BFD, so `show route` shows nothing for it. A session that is down sends every 2 seconds, `Transmit Interval 2.000`, whatever the interval.
|
|
39
39
|
- `bfd-liveness-detection` on a static route also watches a `qualified-next-hop`: a session to that address starts.
|
|
40
40
|
- `set switch-options interface xe-0/0/1 interface-mac-limit 1 packet-action drop` is stored on the logical interface and in two statements: `... interface xe-0/0/1.0 interface-mac-limit 1` and `... interface-mac-limit packet-action drop`. The render writes those two.
|
|
41
|
-
- An `ethernet-switching` filter takes `from ether-type ipv4`, `ipv6` and `arp` by name. Ethertypes and an `
|
|
41
|
+
- An `ethernet-switching` filter takes `from ether-type ipv4`, `ipv6` and `arp` by name. Ethertypes and an `ingress_acl` would both be the port's `filter input`, so Circuit refuses the two on one port rather than merge them.
|
|
42
42
|
- LLDP sends a port's SNMP index as its port ID, `PortIdSubtype: local`, `PortId: 520`, so a neighbor cannot tell which port it is. `set protocols lldp port-id-subtype interface-name` makes it send `et-0/0/52`, and the render sets it wherever LLDP runs.
|