@takodotid/circuit 0.41.0 → 0.42.1
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-6wb41q67.js +62 -0
- package/dist/chunks/{index-k38bby43.js → index-yzw86rc8.js} +4 -62
- package/dist/chunks/{server-ggertnce.js → server-cxr9ynxc.js} +1 -1
- package/dist/cli/index.js +226 -55
- package/dist/helpers/index.js +14 -9
- package/dist/types/core/cabling.d.ts +0 -6
- package/dist/types/core/define.d.ts +1 -1
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/cables.d.ts +14 -11
- package/dist/types/helpers/exchange.d.ts +7 -1
- package/dist/types/helpers/upstreams.d.ts +11 -3
- package/dist/types/platforms/bgp-wait.d.ts +2 -2
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/routing.d.ts +2 -2
- package/dist/types/platforms/junos/bgp.d.ts +2 -2
- package/dist/types/platforms/platform.d.ts +3 -0
- package/dist/types/platforms/routeros/index.d.ts +14 -1
- package/dist/types/platforms/routeros/plan.d.ts +4 -0
- package/dist/types/platforms/routeros/render.d.ts +4 -0
- package/dist/types/schema/device.d.ts +4 -4
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/dist/types/schema/routing.d.ts +43 -10
- package/package.json +1 -1
- package/src/cli/index.ts +26 -29
- package/src/core/cabling.ts +0 -30
- package/src/core/define.ts +3 -1
- package/src/core/validator.ts +64 -1
- package/src/helpers/cables.ts +20 -15
- package/src/helpers/exchange.ts +9 -3
- package/src/helpers/upstreams.ts +9 -4
- package/src/platforms/bgp-wait.ts +11 -11
- package/src/platforms/derive.ts +23 -1
- package/src/platforms/fortios/routing.ts +2 -2
- package/src/platforms/junos/bgp.ts +2 -2
- package/src/platforms/platform.ts +3 -0
- package/src/platforms/routeros/TRAPS.md +6 -0
- package/src/platforms/routeros/export.ts +1 -0
- package/src/platforms/routeros/index.ts +133 -4
- package/src/platforms/routeros/plan.ts +20 -1
- package/src/platforms/routeros/render.ts +28 -3
- package/src/schema/device.ts +4 -3
- package/src/schema/interfaces.ts +1 -1
- package/src/schema/routing.ts +46 -10
- package/templates/colocation/checks.ts +14 -11
- package/templates/colocation/edge-01.ts +2 -2
- package/templates/colocation/tenancy.ts +50 -71
- package/templates/colocation/tenants/acme.ts +10 -10
- package/templates/colocation/tenants/index.ts +2 -1
- package/templates/colocation/tor-01.ts +2 -2
- package/templates/project/AGENTS.md +15 -15
package/dist/helpers/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Catalog
|
|
3
|
+
} from "../chunks/index-6wb41q67.js";
|
|
1
4
|
import {
|
|
2
5
|
Address2
|
|
3
6
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -238,8 +241,8 @@ class Bgp {
|
|
|
238
241
|
class Cables {
|
|
239
242
|
static link(spec) {
|
|
240
243
|
return {
|
|
241
|
-
a: { [spec.a.port]: Cables.end(spec, spec.b
|
|
242
|
-
b: { [spec.b.port]: Cables.end(spec, spec.a
|
|
244
|
+
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
|
|
245
|
+
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
|
|
243
246
|
};
|
|
244
247
|
}
|
|
245
248
|
static lag(spec) {
|
|
@@ -251,17 +254,18 @@ class Cables {
|
|
|
251
254
|
throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
|
|
252
255
|
taken.set(far, near);
|
|
253
256
|
}
|
|
254
|
-
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b
|
|
255
|
-
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a
|
|
257
|
+
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
258
|
+
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
256
259
|
return {
|
|
257
260
|
a,
|
|
258
261
|
b
|
|
259
262
|
};
|
|
260
263
|
}
|
|
261
|
-
static end(shared,
|
|
264
|
+
static end(shared, far, port) {
|
|
265
|
+
const label = far.platform && far.model ? Catalog.spell(far.platform, far.model, port) : undefined;
|
|
262
266
|
return {
|
|
263
|
-
link: { device, port, ...shared.through ? { through: shared.through } : {} },
|
|
264
|
-
...shared.describe ? { description: shared.describe({ device, port }) } : {}
|
|
267
|
+
link: { device: far.device, port, ...shared.through ? { through: shared.through } : {} },
|
|
268
|
+
...shared.describe ? { description: shared.describe({ device: far.device, port, ...label ? { label } : {} }) } : {}
|
|
265
269
|
};
|
|
266
270
|
}
|
|
267
271
|
}
|
|
@@ -560,8 +564,8 @@ class Exchange {
|
|
|
560
564
|
result[port] = {
|
|
561
565
|
description: `${member.name}, AS${member.asn}`,
|
|
562
566
|
access_vlan: exchange.vlan,
|
|
563
|
-
stp: false,
|
|
564
|
-
lldp: false,
|
|
567
|
+
stp: exchange.stp ?? false,
|
|
568
|
+
lldp: exchange.lldp ?? false,
|
|
565
569
|
storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
|
|
566
570
|
...exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {},
|
|
567
571
|
...exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}
|
|
@@ -767,6 +771,7 @@ class Upstreams {
|
|
|
767
771
|
role: upstream.role,
|
|
768
772
|
...upstream.import ? { import: upstream.import.name } : {},
|
|
769
773
|
export: upstream.export.name,
|
|
774
|
+
...upstream.table ? { table: upstream.table } : {},
|
|
770
775
|
...multihop ? {
|
|
771
776
|
local_address: multihop.local_address,
|
|
772
777
|
multihop: true,
|
|
@@ -31,12 +31,6 @@ export declare class Cabling {
|
|
|
31
31
|
* Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. A link `through` a carrier is not compared. LLDP can be off at the far end, so each is a warning.
|
|
32
32
|
*/
|
|
33
33
|
static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[];
|
|
34
|
-
/**
|
|
35
|
-
* Each linked port without a description gets one from its far end: the far device and the far port as printed on it, such as `firewall-1 port6`. The far device's own file has its platform and model, so the label is never written twice. A port whose far device is not in the network keeps none.
|
|
36
|
-
*/
|
|
37
|
-
static describeLinks(devices: readonly Device[]): Device[];
|
|
38
|
-
/** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
|
|
39
|
-
private static labelOf;
|
|
40
34
|
/** A system name is the device's name, alone or with a domain after it. */
|
|
41
35
|
private static isDevice;
|
|
42
36
|
private static deviceOf;
|
|
@@ -9,7 +9,7 @@ export declare const secretFile: (path: string) => Secret;
|
|
|
9
9
|
*
|
|
10
10
|
* The names it declares (VLANs, interfaces, policies and the rest) are the only names its other fields accept, and the editor suggests them. The port names come from the model.
|
|
11
11
|
*/
|
|
12
|
-
export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates>): Device;
|
|
12
|
+
export declare function defineDevice<const Platform extends PlatformName, const Model extends ModelName<Platform>, const Vlans extends string = never, const Interfaces extends string = never, const Policies extends string = never, const PrefixSets extends string = never, const BgpGroups extends string = never, const Acls extends string = never, const AddressSets extends string = never, const Users extends string = never, const Vrfs extends string = never, const Certificates extends string = never, const Tables extends string = never>(device: Device<Platform, Model, PortName<Platform, Model>, Vlans, Interfaces, Policies, PrefixSets, BgpGroups, Acls, AddressSets, Users, Vrfs, Certificates, Tables>): Device;
|
|
13
13
|
/** Something a check found. */
|
|
14
14
|
export type Finding = {
|
|
15
15
|
/** An error stops `diff` and `apply`; a warning is reported only. */
|
|
@@ -14,6 +14,8 @@ export declare class Validator {
|
|
|
14
14
|
private static checkFrames;
|
|
15
15
|
private static checkInterfaces;
|
|
16
16
|
private static checkRouting;
|
|
17
|
+
/** Routing tables have names of their own and something installing routes in them, and rules name a table and can each be reached. */
|
|
18
|
+
private static checkTables;
|
|
17
19
|
private static checkPolicies;
|
|
18
20
|
private static checkFirewall;
|
|
19
21
|
private static checkServices;
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
/** The far end of a port, for a description of your own: the device
|
|
1
|
+
/** The far end of a port, for a description of your own: the device, its port by Circuit's name, and the port as printed on the device when that end names its platform and model. */
|
|
2
2
|
export type FarEnd = {
|
|
3
3
|
device: string;
|
|
4
4
|
port: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
/** One end of a cable entry: the device, and its platform and model if a description should name its ports as printed on it. */
|
|
8
|
+
type End = {
|
|
9
|
+
device: string;
|
|
10
|
+
platform?: string;
|
|
11
|
+
model?: string;
|
|
5
12
|
};
|
|
6
13
|
/** What every cable of one entry shares. */
|
|
7
14
|
type Shared = {
|
|
8
15
|
/** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
|
|
9
16
|
through?: string;
|
|
10
|
-
/** A port's description from its far end
|
|
17
|
+
/** A port's description from its far end, such as `(far) => \`${far.device} ${far.label}\``. No description when absent. */
|
|
11
18
|
describe?: (far: FarEnd) => string;
|
|
12
19
|
};
|
|
13
20
|
/** A port a cable builds: the other end, the LAG it is in, and a description when `describe` gives one. */
|
|
@@ -31,12 +38,10 @@ export declare class Cables {
|
|
|
31
38
|
* ```
|
|
32
39
|
*/
|
|
33
40
|
static link<const NA extends string, const NB extends string>(spec: Shared & {
|
|
34
|
-
a: {
|
|
35
|
-
device: string;
|
|
41
|
+
a: End & {
|
|
36
42
|
port: NA;
|
|
37
43
|
};
|
|
38
|
-
b: {
|
|
39
|
-
device: string;
|
|
44
|
+
b: End & {
|
|
40
45
|
port: NB;
|
|
41
46
|
};
|
|
42
47
|
}): {
|
|
@@ -47,12 +52,10 @@ export declare class Cables {
|
|
|
47
52
|
* The cables of a LAG, both ends, from one list read from end A: `{ "25g-1": "10g-2" }` is a cable from A's 25g-1 to B's 10g-2. Each port is a member of its end's LAG. Spread each end into its device's `ports`; the LAG itself stays on each device.
|
|
48
53
|
*/
|
|
49
54
|
static lag<const LA extends string, const LB extends string, const C extends Readonly<Record<string, string>>>(spec: Shared & {
|
|
50
|
-
a: {
|
|
51
|
-
device: string;
|
|
55
|
+
a: End & {
|
|
52
56
|
lag: LA;
|
|
53
57
|
};
|
|
54
|
-
b: {
|
|
55
|
-
device: string;
|
|
58
|
+
b: End & {
|
|
56
59
|
lag: LB;
|
|
57
60
|
};
|
|
58
61
|
cables: C;
|
|
@@ -60,7 +63,7 @@ export declare class Cables {
|
|
|
60
63
|
a: Record<keyof C & string, CablePort<LA>>;
|
|
61
64
|
b: Record<C[keyof C], CablePort<LB>>;
|
|
62
65
|
};
|
|
63
|
-
/** A port facing `port` on the far
|
|
66
|
+
/** A port facing `port` on the far end. */
|
|
64
67
|
private static end;
|
|
65
68
|
}
|
|
66
69
|
export {};
|
|
@@ -29,13 +29,19 @@ export type InternetExchange<V extends string> = {
|
|
|
29
29
|
mac_limit?: number;
|
|
30
30
|
/** The only kinds of frame a member port takes in, usually `ipv4`, `ipv6` and `arp`. Every kind when absent. No platform renders it yet, so `validate` refuses it. */
|
|
31
31
|
ethertypes?: readonly Ethertype[];
|
|
32
|
+
/** LLDP on member ports. Off when absent: a member's router has no business learning the fabric. */
|
|
33
|
+
lldp?: boolean;
|
|
34
|
+
/** Spanning tree on member ports. Off when absent: a member's port faces a router, which must not join the fabric's spanning tree. */
|
|
35
|
+
stp?: boolean;
|
|
32
36
|
};
|
|
33
37
|
/** An internet exchange's fabric: its member ports, a check of its members, and its route servers' config. */
|
|
34
38
|
export declare class Exchange {
|
|
35
39
|
/**
|
|
36
40
|
* Member ports and a check for an internet exchange's fabric.
|
|
37
41
|
*
|
|
38
|
-
* - `ports(device)`: every member's port on one switch
|
|
42
|
+
* - `ports(device)`: every member's port on one switch, each written as
|
|
43
|
+
* `{ description: "<name>, AS<asn>", access_vlan: vlan, stp: false, lldp: false, storm_control: { broadcast, multicast, unknown_unicast: { percent: storm_percent } } }`,
|
|
44
|
+
* plus `mac_limit` and `ethertypes` when the exchange sets them. `stp` and `lldp` are the exchange's own when it sets them, and storm control is 1 % when `storm_percent` is absent.
|
|
39
45
|
* - `check`: members never share a name, an AS, an address or a port; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
|
|
40
46
|
* - `bird(server)`: one route server's BIRD 2 config, with a session to every member. Write it to a file for the server; Circuit never logs in to a route server.
|
|
41
47
|
* - `openbgpd(server)`: the same route server's config for OpenBGPD, its `bgpd.conf`, with the same sessions and the same rules.
|
|
@@ -59,7 +59,7 @@ export type Upstream = {
|
|
|
59
59
|
address: IP;
|
|
60
60
|
local_address?: IP;
|
|
61
61
|
}>>;
|
|
62
|
-
/** A BGP group its sessions share, by this name: the group holds the AS, role, policies and multihop settings, and each session names it. Each session holds them itself when absent. */
|
|
62
|
+
/** A BGP group its sessions share, by this name: the group holds the AS, role, policies, table and multihop settings, and each session names it. Each session holds them itself when absent. */
|
|
63
63
|
group?: string;
|
|
64
64
|
/** The sessions are more than one hop away, from this address of ours. */
|
|
65
65
|
multihop?: {
|
|
@@ -67,6 +67,8 @@ export type Upstream = {
|
|
|
67
67
|
families?: readonly Family[];
|
|
68
68
|
password?: Secret;
|
|
69
69
|
};
|
|
70
|
+
/** The routing table its sessions install the routes they learn in, one of the router's `routing.tables`: a transit that carries only one block's traffic out. The main table when absent. On RouterOS the sessions also announce from that table, so what they announce needs a route there too. */
|
|
71
|
+
table?: string;
|
|
70
72
|
/** The most prefixes a session may send, by its family: about twice what it sends. Over it, the session closes until it is cleared. */
|
|
71
73
|
limits?: {
|
|
72
74
|
ipv4?: number;
|
|
@@ -143,6 +145,12 @@ type GroupName<C> = Values<{
|
|
|
143
145
|
group: infer N extends string;
|
|
144
146
|
} ? N : never;
|
|
145
147
|
}>;
|
|
148
|
+
/** Each routing table name. */
|
|
149
|
+
type TableName<C> = Values<{
|
|
150
|
+
[K in keyof C]: C[K] extends {
|
|
151
|
+
table: infer N extends string;
|
|
152
|
+
} ? N : never;
|
|
153
|
+
}>;
|
|
146
154
|
/** A network's upstreams written once, and each device's part built from them. */
|
|
147
155
|
export declare class Upstreams {
|
|
148
156
|
/**
|
|
@@ -160,8 +168,8 @@ export declare class Upstreams {
|
|
|
160
168
|
ports: (device: string) => Record<string, Port<NativeName<C> | VlanName<C>, never, never, never>>;
|
|
161
169
|
interfaces: () => { [K in HandoffInterface<C>]: VlanInterface<VlanName<C>, never>; } & { [K in TunnelInterface<C>]: Gre<never>; };
|
|
162
170
|
uplinks: () => HandoffInterface<C>[];
|
|
163
|
-
groups: () => Record<GroupName<C>, NeighborSettings<PolicyName<C>>>;
|
|
164
|
-
neighbors: () => Record<string, Neighbor<PolicyName<C>, GroupName<C>>>;
|
|
171
|
+
groups: () => Record<GroupName<C>, NeighborSettings<PolicyName<C>, TableName<C>>>;
|
|
172
|
+
neighbors: () => Record<string, Neighbor<PolicyName<C>, GroupName<C>, TableName<C>>>;
|
|
165
173
|
policies: () => Record<PolicyName<C>, PolicyRule<never, never>[]>;
|
|
166
174
|
untrusted: () => (NativeName<C> | VlanName<C>)[];
|
|
167
175
|
natives: () => NativeName<C>[];
|
|
@@ -11,8 +11,8 @@ export declare class BgpWait {
|
|
|
11
11
|
* After an apply, the sessions established before have 180 seconds to be established again, read every 10 seconds. A session reset by a change opens again when one side retries; three minutes leaves room for a slow peer without holding the apply up for long. A session that never went down is seen at the first read, so a healthy apply does not wait.
|
|
12
12
|
*/
|
|
13
13
|
static readonly AFTER_APPLY: WaitLimits;
|
|
14
|
-
/** Read
|
|
15
|
-
static until(read: () => Promise<
|
|
14
|
+
/** Read until `enough` holds or time runs out, and give the last read: the sessions, or what they announce. `waiting` is told of each read that fell short, before the pause. */
|
|
15
|
+
static until<T>(read: () => Promise<T>, enough: (value: T) => boolean, limits: WaitLimits, waiting?: (value: T) => void): Promise<T>;
|
|
16
16
|
/** How many of these sessions are established. */
|
|
17
17
|
static upCount(sessions: readonly BgpSession[]): number;
|
|
18
18
|
/** Sessions closed on their limit within this many minutes of each other are one burst. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Device } from "../schema/index.js";
|
|
2
2
|
/** A feature a platform renders once proved on one of its devices. */
|
|
3
|
-
export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom";
|
|
3
|
+
export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom" | "routing-tables";
|
|
4
4
|
export declare class Derive {
|
|
5
5
|
/** Each LAG's number: its own `id`, or its position among the device's LAGs. */
|
|
6
6
|
static lagIds(device: Device): Map<string, number>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../schema/index.js";
|
|
2
2
|
import { FortiOsBlock } from "./tree.js";
|
|
3
3
|
type AnyRule = PolicyRule<string, string>;
|
|
4
|
-
type AnyNeighbor = Neighbor<string, string>;
|
|
4
|
+
type AnyNeighbor = Neighbor<string, string, string>;
|
|
5
5
|
/** The lengths a prefix list's rule takes within its prefix. */
|
|
6
6
|
type Lengths = {
|
|
7
7
|
ge?: number;
|
|
@@ -23,7 +23,7 @@ export declare class FortiOsRouting {
|
|
|
23
23
|
readonly ipv6: 128;
|
|
24
24
|
};
|
|
25
25
|
/** A neighbor's settings with its group's beneath them. */
|
|
26
|
-
static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
|
|
26
|
+
static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string>;
|
|
27
27
|
/** The families a session carries: its own, or the family of its address. */
|
|
28
28
|
static familiesOfNeighbor(device: Device, neighbor: AnyNeighbor): Family[];
|
|
29
29
|
/** Whether a static route, OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Device, Neighbor, NeighborSettings } from "../../schema/index.js";
|
|
2
2
|
import { type JunosStatements } from "./render.js";
|
|
3
|
-
type AnyNeighbor = Neighbor<string, string>;
|
|
3
|
+
type AnyNeighbor = Neighbor<string, string, string>;
|
|
4
4
|
/** BGP sessions, the policies they use, prefix sets and RPKI servers, added to one device's statements. */
|
|
5
5
|
export declare class JunosBgp {
|
|
6
6
|
private readonly out;
|
|
@@ -19,7 +19,7 @@ export declare class JunosBgp {
|
|
|
19
19
|
/** Add the device's prefix sets, RPKI servers, BGP sessions and the policies they use. */
|
|
20
20
|
static render(out: JunosStatements, device: Device): void;
|
|
21
21
|
/** A neighbor's settings with its group's beneath them. */
|
|
22
|
-
static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string>;
|
|
22
|
+
static settingsOf(device: Device, neighbor: AnyNeighbor): NeighborSettings<string, string>;
|
|
23
23
|
/** A name made of a value's words, for a community or a path-length list: `64500:*` becomes `64500-any`. */
|
|
24
24
|
private static slug;
|
|
25
25
|
/** A community as the platform writes a member: a large community carries the `large:` prefix. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Neighbor } from "../core/cabling.js";
|
|
2
|
+
import type { Finding } from "../core/define.js";
|
|
2
3
|
import type { Device, Family } from "../schema/index.js";
|
|
3
4
|
import type { ConsoleDialect } from "../transport/console.js";
|
|
4
5
|
import type { WaitLimits } from "./bgp-wait.js";
|
|
@@ -101,6 +102,8 @@ export declare abstract class Platform {
|
|
|
101
102
|
readonly console: ConsoleDialect | undefined;
|
|
102
103
|
/** Fields this platform cannot express. Validation fails on any of them, so nothing is skipped silently. */
|
|
103
104
|
abstract unsupported(device: Device): string[];
|
|
105
|
+
/** What the platform does with a config that it may not do elsewhere, each as `validate` reports it: an error stops `diff` and `apply`, a warning is reported only. Absent on a platform with none. */
|
|
106
|
+
checks?(device: Device): Finding[];
|
|
104
107
|
/** The switch rules the render makes from the device's hardware ACLs, so `validate` can hold them to the model's limits. Absent on a platform whose rules are not counted. */
|
|
105
108
|
switchRules?(device: Device): SwitchRules;
|
|
106
109
|
/** The whole configuration the device should run. Secrets appear as markers, never values. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Neighbor } from "../../core/cabling.js";
|
|
2
|
+
import type { Finding } from "../../core/define.js";
|
|
2
3
|
import type { Device, Family } from "../../schema/index.js";
|
|
3
4
|
import { type ConsoleDialect } from "../../transport/console.js";
|
|
4
5
|
import { type ApplyOptions, type BgpLogLine, type BgpSession, type HealthCheck, type HealthOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, type SwitchRules, Platform } from "../platform.js";
|
|
@@ -18,6 +19,8 @@ export declare class RouterOs extends Platform {
|
|
|
18
19
|
*/
|
|
19
20
|
private static readonly LIST_BGP_SESSIONS;
|
|
20
21
|
/** Open again the session from one address, only while it is closed on its limit. */
|
|
22
|
+
/** Each advertisement of these prefixes, one line each: `ADV|<session>|<prefix>`. One line, because a script sent over exec runs as one. */
|
|
23
|
+
private static listAdvertisements;
|
|
21
24
|
private static clearLimit;
|
|
22
25
|
/**
|
|
23
26
|
* Every LLDP neighbor, one line each: our interfaces, the port first and then the LAG and bridge it is in, the far system name, and the far port as LLDP reports it. `:tostr` keeps the interface list one value; joined to a string as it is, a list repeats the whole line for each of its items. One line, because a script sent over exec runs as one.
|
|
@@ -41,6 +44,10 @@ export declare class RouterOs extends Platform {
|
|
|
41
44
|
readonly os: OsSupport;
|
|
42
45
|
readonly extension = ".rsc";
|
|
43
46
|
readonly console: ConsoleDialect;
|
|
47
|
+
/**
|
|
48
|
+
* A BGP session with a `table` announces from that table, not from main: each prefix of `networks` 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.
|
|
49
|
+
*/
|
|
50
|
+
checks(device: Device): Finding[];
|
|
44
51
|
unsupported(device: Device): string[];
|
|
45
52
|
render(device: Device): string;
|
|
46
53
|
switchRules(device: Device): SwitchRules;
|
|
@@ -63,7 +70,7 @@ export declare class RouterOs extends Platform {
|
|
|
63
70
|
/** A `time>` filter in `/log print` does not filter, so the lines are kept here by comparing each one's time with the device's clock. */
|
|
64
71
|
bgpLog(session: ReadSession, minutes: number): Promise<BgpLogLine[]>;
|
|
65
72
|
/**
|
|
66
|
-
*
|
|
73
|
+
* 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 `networks` 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.
|
|
67
74
|
*/
|
|
68
75
|
health(device: Device, plan: Plan, options?: HealthOptions): Promise<HealthCheck>;
|
|
69
76
|
/** The LLDP neighbors the device sees, by Circuit's port names. */
|
|
@@ -80,6 +87,12 @@ export declare class RouterOs extends Platform {
|
|
|
80
87
|
private establishedSessions;
|
|
81
88
|
/** Wait until every one of these neighbors is established again, and give a problem for each that is not when time runs out. */
|
|
82
89
|
private bgpBack;
|
|
90
|
+
/** What each session announces of the device's own `networks`, as `neighbor prefix`. None, without logging in, on a device without networks. */
|
|
91
|
+
private announcedNow;
|
|
92
|
+
/** The `networks` each session announces, as `neighbor prefix`, sorted. An advertisement names its session, which is matched to its neighbor by address first, as in `bgpSessions`. */
|
|
93
|
+
private announced;
|
|
94
|
+
/** Wait until each session announces again what it announced before, and give a problem for each prefix a session no longer announces when time runs out. */
|
|
95
|
+
private announcedBack;
|
|
83
96
|
/** A problem when any switch rule is invalid, saying what to do. */
|
|
84
97
|
private static invalidSwitchRules;
|
|
85
98
|
/** Log in over SSH. `flags` go after the user, as RouterOS reads them: `+ct300w`. */
|
|
@@ -9,6 +9,10 @@ export declare class RouterOsPlan {
|
|
|
9
9
|
private static readonly DYNAMIC;
|
|
10
10
|
/** Menus where an entry that keeps its name but changes what it is known by is changed with a `set`, so what points at it by name, such as an address on a VLAN interface, stays attached. In every other menu it is removed and added again. */
|
|
11
11
|
private static readonly CHANGED_IN_PLACE;
|
|
12
|
+
/**
|
|
13
|
+
* Properties a running entry takes only when it restarts. A BGP session given a new `routing-table` stops announcing at once and keeps its routes in the old table until it restarts (TRAPS.md), so the plan disables the connection, changes it and enables it again: one restart, written in the plan.
|
|
14
|
+
*/
|
|
15
|
+
private static readonly RESTART_ON;
|
|
12
16
|
/** The variable holding the old rules of an ordered menu while the new ones are added. Removed when the rewrite ends. */
|
|
13
17
|
private static readonly OLD_RULES;
|
|
14
18
|
/** What blocks an apply: something this plan cannot converge. */
|
|
@@ -199,6 +199,10 @@ export declare class RouterOsRender {
|
|
|
199
199
|
/** Every switch rule the device's hardware ACLs make, in order: one for each port with an ACL, each rule of it, and each source and destination port or range of the rule. */
|
|
200
200
|
static switchRules(device: Device): SwitchRule[];
|
|
201
201
|
private routing;
|
|
202
|
+
/** A table beside the main one. `fib` puts its routes in the forwarding table, so traffic can be routed by them. */
|
|
203
|
+
private routingTables;
|
|
204
|
+
/** Rules in the order written. `lookup-only-in-table` drops what the table has no route for, and `lookup` falls back to the main table. */
|
|
205
|
+
private routingRules;
|
|
202
206
|
private staticRoutes;
|
|
203
207
|
private rpki;
|
|
204
208
|
/** The default instance exists on every device; it is stated in the form the export prints it. */
|
|
@@ -65,9 +65,9 @@ export type Vrf = {
|
|
|
65
65
|
/**
|
|
66
66
|
* Everything one device runs. What is not written here is removed from the device or returned to its default.
|
|
67
67
|
*
|
|
68
|
-
* Type parameters are the names the device declares, so a reference to a name that does not exist fails to compile: N ports, V VLANs, I interfaces, P policies, S prefix sets, G BGP groups, A ACLs, AS address sets, U users, F VRFs, C certificates.
|
|
68
|
+
* Type parameters are the names the device declares, so a reference to a name that does not exist fails to compile: N ports, V VLANs, I interfaces, P policies, S prefix sets, G BGP groups, A ACLs, AS address sets, U users, F VRFs, C certificates, T routing tables.
|
|
69
69
|
*/
|
|
70
|
-
export type Device<Pl extends string = string, M extends string = string, N extends string = string, V extends string = string, I extends string = string, P extends string = string, S extends string = string, G extends string = string, A extends string = string, AS extends string = string, U extends string = string, F extends string = string, C extends string = string> = {
|
|
70
|
+
export type Device<Pl extends string = string, M extends string = string, N extends string = string, V extends string = string, I extends string = string, P extends string = string, S extends string = string, G extends string = string, A extends string = string, AS extends string = string, U extends string = string, F extends string = string, C extends string = string, T extends string = string> = {
|
|
71
71
|
/** Hostname. */
|
|
72
72
|
name: string;
|
|
73
73
|
/** Operating system. It decides which adapter renders and applies the device. */
|
|
@@ -92,8 +92,8 @@ export type Device<Pl extends string = string, M extends string = string, N exte
|
|
|
92
92
|
interfaces?: Record<I, Interface<NoInfer<V>, NoInfer<F>>>;
|
|
93
93
|
/** Separate routing tables, keyed by name. */
|
|
94
94
|
vrfs?: Record<F, Vrf>;
|
|
95
|
-
/**
|
|
96
|
-
routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F
|
|
95
|
+
/** Routing tables and rules, static routes, BGP, OSPF and route validation. */
|
|
96
|
+
routing?: Routing<NoInfer<P>, G, NoInfer<I | N>, NoInfer<F>, T>;
|
|
97
97
|
/** Route policies, keyed by name. */
|
|
98
98
|
policies?: Record<P, RoutePolicy<NoInfer<P>, NoInfer<S>>>;
|
|
99
99
|
/** Prefix sets that policies match on, keyed by name. */
|
|
@@ -109,7 +109,7 @@ export type Port<V extends string, A extends string, L extends string, F extends
|
|
|
109
109
|
acl?: A;
|
|
110
110
|
/** The LAG this port is in. Every other setting goes on the LAG. */
|
|
111
111
|
lag?: L;
|
|
112
|
-
/** Another of our devices at the other end of the cable.
|
|
112
|
+
/** Another of our devices at the other end of the cable. */
|
|
113
113
|
link?: Link;
|
|
114
114
|
};
|
|
115
115
|
/** Settings every interface that is not a physical port shares. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Family, IP, Prefix, Secret } from "./common.js";
|
|
2
2
|
/** A route the device has without learning it. */
|
|
3
|
-
export type StaticRoute<I extends string, F extends string> = {
|
|
3
|
+
export type StaticRoute<I extends string, F extends string, T extends string = never> = {
|
|
4
4
|
/** Destination. */
|
|
5
5
|
prefix: Prefix;
|
|
6
6
|
/** Next-hop address. */
|
|
@@ -13,8 +13,10 @@ export type StaticRoute<I extends string, F extends string> = {
|
|
|
13
13
|
bfd?: true;
|
|
14
14
|
/** Preference against other routes to the same prefix. Lower wins. 1 when absent. */
|
|
15
15
|
distance?: number;
|
|
16
|
-
/** The VRF the route is in. The
|
|
16
|
+
/** The VRF the route is in. The main table when absent. */
|
|
17
17
|
vrf?: F;
|
|
18
|
+
/** The table the route is in, one of `routing.tables`. The main table when absent. A route has a `vrf` or a `table`, not both. */
|
|
19
|
+
table?: T;
|
|
18
20
|
/** Why the route exists. */
|
|
19
21
|
description?: string;
|
|
20
22
|
};
|
|
@@ -23,7 +25,7 @@ export type StaticRoute<I extends string, F extends string> = {
|
|
|
23
25
|
*/
|
|
24
26
|
export type BgpRole = "provider" | "customer" | "peer" | "rs" | "rs-client";
|
|
25
27
|
/** Settings a BGP neighbor can set itself or take from its group. */
|
|
26
|
-
export type NeighborSettings<P extends string> = {
|
|
28
|
+
export type NeighborSettings<P extends string, T extends string = never> = {
|
|
27
29
|
/** The neighbor's AS. Our own AS makes the session internal. */
|
|
28
30
|
remote_as?: number;
|
|
29
31
|
/** Who the neighbor is. */
|
|
@@ -52,24 +54,26 @@ export type NeighborSettings<P extends string> = {
|
|
|
52
54
|
passive?: boolean;
|
|
53
55
|
/** Detect a dead link in under a second with BFD. */
|
|
54
56
|
bfd?: boolean;
|
|
57
|
+
/** The table the routes learned are installed in, one of `routing.tables`. The main table when absent. On RouterOS the session also announces from that table, so each prefix of `networks` it announces needs a route in the table too, and a running session takes a new table only when it restarts. */
|
|
58
|
+
table?: T;
|
|
55
59
|
};
|
|
56
60
|
/** One BGP session. Settings it does not set come from its group. */
|
|
57
|
-
export type Neighbor<P extends string, G extends string> = NeighborSettings<P> & {
|
|
61
|
+
export type Neighbor<P extends string, G extends string, T extends string = never> = NeighborSettings<P, T> & {
|
|
58
62
|
/** The neighbor's address. */
|
|
59
63
|
address: IP;
|
|
60
64
|
/** The group this neighbor takes a setting from when it does not set that setting itself. */
|
|
61
65
|
group?: NoInfer<G>;
|
|
62
66
|
};
|
|
63
67
|
/** BGP on this device. */
|
|
64
|
-
export type Bgp<P extends string, G extends string> = {
|
|
68
|
+
export type Bgp<P extends string, G extends string, T extends string = never> = {
|
|
65
69
|
/** Our AS number. */
|
|
66
70
|
asn: number;
|
|
67
71
|
/** Prefixes this device originates. Each needs a route, for example a static blackhole. Export policy still decides who receives them. */
|
|
68
72
|
networks?: readonly Prefix[];
|
|
69
73
|
/** Settings shared by several neighbors, keyed by group name. */
|
|
70
|
-
groups?: Record<G, NeighborSettings<P>>;
|
|
74
|
+
groups?: Record<G, NeighborSettings<P, T>>;
|
|
71
75
|
/** Sessions keyed by name. */
|
|
72
|
-
neighbors?: Record<string, Neighbor<P, G>>;
|
|
76
|
+
neighbors?: Record<string, Neighbor<P, G, T>>;
|
|
73
77
|
};
|
|
74
78
|
/** How fast BFD notices a dead neighbor: after `multiplier` packets in a row are missed, so in `interval` times `multiplier` milliseconds. */
|
|
75
79
|
export type Bfd = {
|
|
@@ -111,14 +115,43 @@ export type Ospf<I extends string> = {
|
|
|
111
115
|
/** Routes from other sources announced into OSPF. */
|
|
112
116
|
redistribute?: readonly ("connected" | "static")[];
|
|
113
117
|
};
|
|
118
|
+
/** A routing table beside the main one. It holds no interfaces: routes go in from static routes and BGP neighbors that name it in `table`, and `routing.rules` send traffic to it. */
|
|
119
|
+
export type RoutingTable = {
|
|
120
|
+
/** What the table is for. */
|
|
121
|
+
description?: string;
|
|
122
|
+
};
|
|
123
|
+
/** Traffic a routing rule matches: all of what is given. Every packet when absent. */
|
|
124
|
+
export type RoutingRuleMatch<I extends string> = {
|
|
125
|
+
/** Source prefix. */
|
|
126
|
+
src?: Prefix;
|
|
127
|
+
/** Destination prefix. */
|
|
128
|
+
dst?: Prefix;
|
|
129
|
+
/** The interface the traffic arrives on. */
|
|
130
|
+
in_interface?: I;
|
|
131
|
+
};
|
|
132
|
+
/** One routing rule: traffic it matches is routed by `table` instead of the main table. */
|
|
133
|
+
export type RoutingRule<I extends string, T extends string> = {
|
|
134
|
+
/** Why the rule exists. */
|
|
135
|
+
description?: string;
|
|
136
|
+
/** What the traffic must match. Every packet when absent, so such a rule is the last one. */
|
|
137
|
+
match?: RoutingRuleMatch<I>;
|
|
138
|
+
/** The table that routes the traffic: one of `routing.tables`, or `main`. */
|
|
139
|
+
table: T | "main";
|
|
140
|
+
/** When `table` has no route for a packet: `none` drops it as unroutable, `main` routes it by the main table instead. `none` when absent. Not for a rule whose table is `main`. */
|
|
141
|
+
fallback?: "none" | "main";
|
|
142
|
+
};
|
|
114
143
|
/** Routing on this device. */
|
|
115
|
-
export type Routing<P extends string, G extends string, I extends string, F extends string> = {
|
|
144
|
+
export type Routing<P extends string, G extends string, I extends string, F extends string, T extends string = never> = {
|
|
116
145
|
/** Identifies this router to its neighbors. */
|
|
117
146
|
router_id?: IP;
|
|
147
|
+
/** Routing tables beside the main one, keyed by name. Not `main`, and not the name of a VRF. */
|
|
148
|
+
tables?: Record<T, RoutingTable>;
|
|
149
|
+
/** Rules that route matching traffic by another table, such as traffic from one block by its own transits' default. In order: the first that matches decides, and traffic no rule matches is routed by the main table. */
|
|
150
|
+
rules?: readonly RoutingRule<I, NoInfer<T>>[];
|
|
118
151
|
/** Routes the device has without learning them. */
|
|
119
|
-
static?: readonly StaticRoute<I, F
|
|
152
|
+
static?: readonly StaticRoute<I, F, NoInfer<T>>[];
|
|
120
153
|
/** BGP sessions and what they exchange. */
|
|
121
|
-
bgp?: Bgp<P, G
|
|
154
|
+
bgp?: Bgp<P, G, NoInfer<T>>;
|
|
122
155
|
/** OSPF areas and interfaces. */
|
|
123
156
|
ospf?: Ospf<I>;
|
|
124
157
|
/** Validators answering route origin validation. */
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -103,20 +103,12 @@ class Cli {
|
|
|
103
103
|
return new Cli(configPath, network, options);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
* The config, a TypeScript file. Bun imports it as it is; Node needs jiti, which also resolves imports written without an extension. A linked port without a description is given one from its far end.
|
|
108
|
-
*/
|
|
106
|
+
/** The config, a TypeScript file. Bun imports it as it is; Node needs jiti, which also resolves imports written without an extension. */
|
|
109
107
|
private static async load(path: string): Promise<Network> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (process.versions.bun) {
|
|
113
|
-
network = (await import(path)).default;
|
|
114
|
-
} else {
|
|
115
|
-
const { createJiti } = await import("jiti");
|
|
116
|
-
network = await createJiti(import.meta.url).import<Network>(path, { default: true });
|
|
117
|
-
}
|
|
108
|
+
if (process.versions.bun) return (await import(path)).default;
|
|
118
109
|
|
|
119
|
-
|
|
110
|
+
const { createJiti } = await import("jiti");
|
|
111
|
+
return createJiti(import.meta.url).import(path, { default: true });
|
|
120
112
|
}
|
|
121
113
|
|
|
122
114
|
private static fail(message: string): never {
|
|
@@ -238,7 +230,9 @@ class Cli {
|
|
|
238
230
|
.command("bgp")
|
|
239
231
|
.description("each BGP neighbor in the config next to its live session: up or down and why, prefixes, uptime")
|
|
240
232
|
.argument("[device]", "may be left out when only one device has BGP neighbors")
|
|
241
|
-
|
|
233
|
+
// A second argument was the name filter before 0.42; it is refused below with the way to write it now.
|
|
234
|
+
.allowExcessArguments()
|
|
235
|
+
.option("--neighbors <regex>", "only the neighbors whose name matches")
|
|
242
236
|
.option(
|
|
243
237
|
"--wait [count]",
|
|
244
238
|
`read again every ${BgpWait.COMMAND.every} seconds until every neighbor shown, or this many, is up`,
|
|
@@ -248,7 +242,14 @@ class Cli {
|
|
|
248
242
|
.option("--log [minutes]", "also print the device's BGP log lines from the last minutes, 5 by default", Cli.minutes)
|
|
249
243
|
.option("--clear-limit", "open again the neighbors shown that closed on their prefix limit; any other is left alone")
|
|
250
244
|
.option(Cli.CONFIG_FLAGS, Cli.CONFIG_HELP)
|
|
251
|
-
.action(async (
|
|
245
|
+
.action(async (device, flags, command) => {
|
|
246
|
+
const [, filter] = command.args;
|
|
247
|
+
if (filter !== undefined)
|
|
248
|
+
Cli.fail(
|
|
249
|
+
`circuit bgp takes one device; filter the neighbors with --neighbors: circuit bgp ${device} --neighbors '${filter}'`
|
|
250
|
+
);
|
|
251
|
+
(await Cli.open(flags.config)).bgp(device, flags);
|
|
252
|
+
});
|
|
252
253
|
|
|
253
254
|
program
|
|
254
255
|
.command("console")
|
|
@@ -613,14 +614,14 @@ class Cli {
|
|
|
613
614
|
}
|
|
614
615
|
|
|
615
616
|
/**
|
|
616
|
-
* Each BGP neighbor in the config next to its live session
|
|
617
|
+
* Each BGP neighbor in the config next to its live session, on the device named or the only one with BGP neighbors, and only those whose name matches `neighbors` when given. With `wait`, reads again until every neighbor shown, or that many, is up, and fails when time runs out. With `clearLimit`, opens again the neighbors shown that closed on their prefix limit, and fails when none did.
|
|
617
618
|
*/
|
|
618
619
|
private async bgp(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
flags: { wait?: true | number; max: number; log?: true | number; clearLimit?: true }
|
|
620
|
+
deviceName: string | undefined,
|
|
621
|
+
flags: { neighbors?: string; wait?: true | number; max: number; log?: true | number; clearLimit?: true }
|
|
622
622
|
): Promise<void> {
|
|
623
|
-
const
|
|
623
|
+
const device = this.bgpDevice(deviceName);
|
|
624
|
+
const pattern = flags.neighbors;
|
|
624
625
|
const adapter = this.adapterOf(device);
|
|
625
626
|
|
|
626
627
|
const supported = Platforms.all()
|
|
@@ -697,19 +698,15 @@ class Cli {
|
|
|
697
698
|
Cli.printBgp(await read());
|
|
698
699
|
}
|
|
699
700
|
|
|
700
|
-
/** The device `circuit bgp` reads
|
|
701
|
-
private
|
|
702
|
-
if (
|
|
703
|
-
return { device: this.one(first!), pattern: second };
|
|
704
|
-
}
|
|
701
|
+
/** The device `circuit bgp` reads: the one named, or the only device with BGP neighbors. */
|
|
702
|
+
private bgpDevice(name: string | undefined): Device {
|
|
703
|
+
if (name !== undefined) return this.one(name);
|
|
705
704
|
|
|
706
705
|
const withBgp = this.network.devices.filter((device) => Object.keys(device.routing?.bgp?.neighbors ?? {}).length);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (withBgp.length === 1) return { device: withBgp[0]!, pattern: first };
|
|
710
|
-
if (!withBgp.length) Cli.fail(`${named}no device has BGP neighbors`);
|
|
706
|
+
if (withBgp.length === 1) return withBgp[0]!;
|
|
707
|
+
if (!withBgp.length) Cli.fail("no device has BGP neighbors");
|
|
711
708
|
|
|
712
|
-
return Cli.fail(
|
|
709
|
+
return Cli.fail(`several devices have BGP neighbors, so name one: ${withBgp.map((device) => device.name).join(", ")}`);
|
|
713
710
|
}
|
|
714
711
|
|
|
715
712
|
/** One line per neighbor: name, up or down, prefixes received, uptime, remote address. Then how many are up. */
|