@takodotid/circuit 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{index-yzw86rc8.js → index-k38bby43.js} +62 -4
- package/dist/chunks/{server-cxr9ynxc.js → server-ggertnce.js} +1 -1
- package/dist/cli/index.js +33 -9
- package/dist/helpers/index.js +71 -29
- package/dist/types/core/cabling.d.ts +6 -0
- package/dist/types/helpers/cables.d.ts +16 -20
- package/dist/types/helpers/index.d.ts +1 -1
- package/dist/types/helpers/upstreams.d.ts +102 -19
- package/dist/types/schema/interfaces.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +12 -4
- package/src/core/cabling.ts +30 -0
- package/src/helpers/cables.ts +21 -43
- package/src/helpers/index.ts +1 -1
- package/src/helpers/upstreams.ts +171 -38
- package/src/schema/interfaces.ts +1 -1
- package/dist/chunks/index-6wb41q67.js +0 -62
|
@@ -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. Without a `description`, the port is described from it: the far device and the far port as printed on it, such as `firewall-1 port6`. */
|
|
113
113
|
link?: Link;
|
|
114
114
|
};
|
|
115
115
|
/** Settings every interface that is not a physical port shares. */
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -103,12 +103,20 @@ class Cli {
|
|
|
103
103
|
return new Cli(configPath, network, options);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
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
|
+
*/
|
|
107
109
|
private static async load(path: string): Promise<Network> {
|
|
108
|
-
|
|
110
|
+
let network: Network;
|
|
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
|
+
}
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
return createJiti(import.meta.url).import(path, { default: true });
|
|
119
|
+
return { ...network, devices: Cabling.describeLinks(network.devices) };
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
private static fail(message: string): never {
|
package/src/core/cabling.ts
CHANGED
|
@@ -121,6 +121,36 @@ export class Cabling {
|
|
|
121
121
|
return findings;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* 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.
|
|
126
|
+
*/
|
|
127
|
+
static describeLinks(devices: readonly Device[]): Device[] {
|
|
128
|
+
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
129
|
+
|
|
130
|
+
return devices.map((device) => {
|
|
131
|
+
const ports = Object.entries(device.ports ?? {});
|
|
132
|
+
if (!ports.some(([, port]) => port?.link && port.description === undefined)) return device;
|
|
133
|
+
|
|
134
|
+
const described = ports.map(([name, port]) => {
|
|
135
|
+
const far = port?.link ? byName.get(port.link.device) : undefined;
|
|
136
|
+
if (!port?.link || port.description !== undefined || !far) return [name, port];
|
|
137
|
+
|
|
138
|
+
return [name, { ...port, description: `${far.name} ${Cabling.labelOf(far, port.link.port)}` }];
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return { ...device, ports: Object.fromEntries(described) } as Device;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
|
|
146
|
+
private static labelOf(device: Device, port: string): string {
|
|
147
|
+
try {
|
|
148
|
+
return Catalog.spell(device.platform, device.model, port);
|
|
149
|
+
} catch {
|
|
150
|
+
return port;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
/** A system name is the device's name, alone or with a domain after it. */
|
|
125
155
|
private static isDevice(system: string, name: string): boolean {
|
|
126
156
|
return system === name || system.startsWith(`${name}.`);
|
package/src/helpers/cables.ts
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/** One end of a cable: the device, and its platform and model, which name its ports. */
|
|
4
|
-
export type CableEnd<P extends PlatformName, M extends ModelName<P>> = {
|
|
5
|
-
device: string;
|
|
6
|
-
platform: P;
|
|
7
|
-
model: M;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/** The far end of a port, for its description: the device, the port by Circuit's name, and by the name printed on the device. */
|
|
11
|
-
export type FarEnd = { device: string; port: string; label: string };
|
|
1
|
+
/** The far end of a port, for a description of your own: the device and its port by Circuit's name. */
|
|
2
|
+
export type FarEnd = { device: string; port: string };
|
|
12
3
|
|
|
13
4
|
/** What every cable of one entry shares. */
|
|
14
5
|
type Shared = {
|
|
15
6
|
/** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
|
|
16
7
|
through?: string;
|
|
17
|
-
/** A port's description from its far end.
|
|
8
|
+
/** A port's description from its far end. When absent, Circuit writes the far device and the far port as printed on it, such as `firewall-1 port6`, from the far device's own file. */
|
|
18
9
|
describe?: (far: FarEnd) => string;
|
|
19
10
|
};
|
|
20
11
|
|
|
21
|
-
/** A port a cable builds:
|
|
12
|
+
/** A port a cable builds: the other end, the LAG it is in, and a description when `describe` gives one. */
|
|
22
13
|
type CablePort<L extends string> = {
|
|
23
|
-
description: string;
|
|
24
14
|
link: { device: string; port: string; through?: string };
|
|
25
15
|
lag?: L;
|
|
16
|
+
description?: string;
|
|
26
17
|
};
|
|
27
18
|
|
|
28
19
|
/** Ports for both ends of the cables between two of our devices, each cable written once. */
|
|
@@ -31,37 +22,26 @@ export class Cables {
|
|
|
31
22
|
* One cable, both ends. Spread each end into its device's `ports`:
|
|
32
23
|
*
|
|
33
24
|
* ```ts
|
|
34
|
-
* const uplink = Cables.link({ a: { device: "router-1",
|
|
25
|
+
* const uplink = Cables.link({ a: { device: "router-1", port: "25g-1" }, b: { device: "switch-1", port: "10g-48" } });
|
|
35
26
|
* // router-1: ports: { ...uplink.a }
|
|
36
27
|
* ```
|
|
37
28
|
*/
|
|
38
|
-
static link<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const NA extends PortName<PA, MA>,
|
|
42
|
-
const PB extends PlatformName,
|
|
43
|
-
const MB extends ModelName<PB>,
|
|
44
|
-
const NB extends PortName<PB, MB>,
|
|
45
|
-
>(spec: Shared & { a: CableEnd<PA, MA> & { port: NA }; b: CableEnd<PB, MB> & { port: NB } }) {
|
|
29
|
+
static link<const NA extends string, const NB extends string>(
|
|
30
|
+
spec: Shared & { a: { device: string; port: NA }; b: { device: string; port: NB } }
|
|
31
|
+
) {
|
|
46
32
|
return {
|
|
47
|
-
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) } as Record<NA, CablePort<never>>,
|
|
48
|
-
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) } as Record<NB, CablePort<never>>,
|
|
33
|
+
a: { [spec.a.port]: Cables.end(spec, spec.b.device, spec.b.port) } as Record<NA, CablePort<never>>,
|
|
34
|
+
b: { [spec.b.port]: Cables.end(spec, spec.a.device, spec.a.port) } as Record<NB, CablePort<never>>,
|
|
49
35
|
};
|
|
50
36
|
}
|
|
51
37
|
|
|
52
38
|
/**
|
|
53
39
|
* 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.
|
|
54
40
|
*/
|
|
55
|
-
static lag<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
const PB extends PlatformName,
|
|
60
|
-
const MB extends ModelName<PB>,
|
|
61
|
-
const LB extends string,
|
|
62
|
-
const C extends Partial<Record<PortName<PA, MA>, PortName<PB, MB>>>,
|
|
63
|
-
>(spec: Shared & { a: CableEnd<PA, MA> & { lag: LA }; b: CableEnd<PB, MB> & { lag: LB }; cables: C }) {
|
|
64
|
-
const pairs = Object.entries(spec.cables) as [string, string][];
|
|
41
|
+
static lag<const LA extends string, const LB extends string, const C extends Readonly<Record<string, string>>>(
|
|
42
|
+
spec: Shared & { a: { device: string; lag: LA }; b: { device: string; lag: LB }; cables: C }
|
|
43
|
+
) {
|
|
44
|
+
const pairs = Object.entries(spec.cables);
|
|
65
45
|
|
|
66
46
|
const taken = new Map<string, string>();
|
|
67
47
|
for (const [near, far] of pairs) {
|
|
@@ -70,22 +50,20 @@ export class Cables {
|
|
|
70
50
|
taken.set(far, near);
|
|
71
51
|
}
|
|
72
52
|
|
|
73
|
-
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
74
|
-
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
53
|
+
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b.device, far), lag: spec.a.lag }]));
|
|
54
|
+
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a.device, near), lag: spec.b.lag }]));
|
|
75
55
|
|
|
76
56
|
return {
|
|
77
57
|
a: a as Record<keyof C & string, CablePort<LA>>,
|
|
78
|
-
b: b as Record<
|
|
58
|
+
b: b as Record<C[keyof C], CablePort<LB>>,
|
|
79
59
|
};
|
|
80
60
|
}
|
|
81
61
|
|
|
82
62
|
/** A port facing `port` on the far device. */
|
|
83
|
-
private static end(shared: Shared,
|
|
84
|
-
const farEnd = { device: far.device, port, label: Catalog.spell(far.platform, far.model, port) };
|
|
85
|
-
|
|
63
|
+
private static end(shared: Shared, device: string, port: string): CablePort<never> {
|
|
86
64
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
link: { device, port, ...(shared.through ? { through: shared.through } : {}) },
|
|
66
|
+
...(shared.describe ? { description: shared.describe({ device, port }) } : {}),
|
|
89
67
|
};
|
|
90
68
|
}
|
|
91
69
|
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export { Bgp } from "./bgp";
|
|
4
4
|
export type { Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp";
|
|
5
5
|
export { Cables } from "./cables";
|
|
6
|
-
export type {
|
|
6
|
+
export type { FarEnd } from "./cables";
|
|
7
7
|
export { Checks } from "./checks";
|
|
8
8
|
export type { TrustBoundary } from "./checks";
|
|
9
9
|
export { Exchange } from "./exchange";
|
package/src/helpers/upstreams.ts
CHANGED
|
@@ -32,6 +32,8 @@ export type Upstream = {
|
|
|
32
32
|
vlan: number;
|
|
33
33
|
/** It sends its VLAN tagged: its port gets a native VLAN of its own with this ID, which nothing else is in. Untagged when absent. */
|
|
34
34
|
native?: number;
|
|
35
|
+
/** Its number in names with `{number}`, such as `core-ipt01-transita`. Written here, so removing another upstream renumbers nothing. */
|
|
36
|
+
number?: number;
|
|
35
37
|
/** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
|
|
36
38
|
addresses: readonly (InterfaceAddress | { address: InterfaceAddress; peer: IP })[];
|
|
37
39
|
/** The switch port it arrives on. */
|
|
@@ -39,8 +41,8 @@ export type Upstream = {
|
|
|
39
41
|
/** An exchange's LAN, for `circuit peeringdb`. */
|
|
40
42
|
exchange?: ExchangeLan;
|
|
41
43
|
};
|
|
42
|
-
/** A GRE tunnel to it. */
|
|
43
|
-
tunnel?: { local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number };
|
|
44
|
+
/** A GRE tunnel to it. `number` is its number in names with `{number}`. */
|
|
45
|
+
tunnel?: { local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number; number?: number };
|
|
44
46
|
/** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
|
|
45
47
|
sessions: Readonly<Record<string, { address: IP; local_address?: IP }>>;
|
|
46
48
|
/** The sessions are more than one hop away, from this address of ours. They share a group. */
|
|
@@ -65,6 +67,29 @@ export type Upstream = {
|
|
|
65
67
|
/** Upstreams by key. The key names everything built from it. */
|
|
66
68
|
export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
|
|
67
69
|
|
|
70
|
+
/** A name made from an upstream: `{key}` is its key, `{KEY}` the key in capitals, `{number}` its `handoff.number` or `tunnel.number`. */
|
|
71
|
+
type Pattern = string;
|
|
72
|
+
/** A pattern for every upstream, or one per role. A role left out takes the default. */
|
|
73
|
+
type ByRole = Pattern | { readonly provider?: Pattern; readonly rs?: Pattern; readonly peer?: Pattern };
|
|
74
|
+
|
|
75
|
+
/** How the names built from each upstream are made. Each one left out takes its default. */
|
|
76
|
+
export type UpstreamNames = {
|
|
77
|
+
/** The VLAN a handoff is on. `{key}` when absent. */
|
|
78
|
+
vlan?: ByRole;
|
|
79
|
+
/** The native VLAN of a handoff sent tagged. `{key}-native` when absent. */
|
|
80
|
+
native?: Pattern;
|
|
81
|
+
/** The router's interface: per role, and `tunnel` for one reached over a tunnel. `{key}` when absent. */
|
|
82
|
+
interface?: Pattern | { readonly provider?: Pattern; readonly rs?: Pattern; readonly peer?: Pattern; readonly tunnel?: Pattern };
|
|
83
|
+
/** The import policy. `{KEY}-IN` when absent. */
|
|
84
|
+
import?: Pattern;
|
|
85
|
+
/** The export policy. `{KEY}-OUT` when absent. */
|
|
86
|
+
export?: Pattern;
|
|
87
|
+
/** The BGP group of an exchange's route servers or of multihop sessions. `{key}` when absent. */
|
|
88
|
+
group?: Pattern;
|
|
89
|
+
/** The description of a handoff's switch port. Its `name` when absent. No device names it, so it may be any function. */
|
|
90
|
+
port?: (key: string, upstream: Upstream) => string;
|
|
91
|
+
};
|
|
92
|
+
|
|
68
93
|
/** How each upstream's policies are written: shared by every upstream of a catalog. */
|
|
69
94
|
export type UpstreamPolicies<P extends string> = {
|
|
70
95
|
/** A policy holding `Bgp.sanity()` that each import calls. The sanity rules are written in each import when absent. */
|
|
@@ -77,6 +102,19 @@ export type UpstreamPolicies<P extends string> = {
|
|
|
77
102
|
sources?: readonly IP[];
|
|
78
103
|
};
|
|
79
104
|
|
|
105
|
+
/** The names a device can refer to, and their patterns when the project gives none. */
|
|
106
|
+
const DEFAULTS = {
|
|
107
|
+
vlan: "{key}",
|
|
108
|
+
native: "{key}-native",
|
|
109
|
+
interface: "{key}",
|
|
110
|
+
import: "{KEY}-IN",
|
|
111
|
+
export: "{KEY}-OUT",
|
|
112
|
+
group: "{key}",
|
|
113
|
+
} as const;
|
|
114
|
+
|
|
115
|
+
type Defaults = typeof DEFAULTS;
|
|
116
|
+
type Field = keyof Defaults;
|
|
117
|
+
|
|
80
118
|
type Key<C> = keyof C & string;
|
|
81
119
|
/** The upstreams that land on a VLAN of ours. */
|
|
82
120
|
type Landed<C> = { [K in Key<C>]: C[K] extends { handoff: object } ? K : never }[Key<C>];
|
|
@@ -89,35 +127,120 @@ type Grouped<C> = { [K in Key<C>]: C[K] extends { role: "rs" } | { multihop: obj
|
|
|
89
127
|
/** The upstreams we take routes from. */
|
|
90
128
|
type Importing<C> = { [K in Key<C>]: C[K] extends { import: object } ? K : never }[Key<C>];
|
|
91
129
|
|
|
92
|
-
/**
|
|
93
|
-
type
|
|
94
|
-
/**
|
|
95
|
-
type
|
|
130
|
+
/** The project's pattern for a name, or the default. */
|
|
131
|
+
type PatternOf<N, F extends Field> = N extends { [X in F]: infer P } ? P : Defaults[F];
|
|
132
|
+
/** The pattern an upstream takes: the only one, its tunnel's, its role's, or the default. */
|
|
133
|
+
type Chosen<P, U, F extends Field> = P extends string
|
|
134
|
+
? P
|
|
135
|
+
: U extends { tunnel: object }
|
|
136
|
+
? P extends { tunnel: infer T extends string }
|
|
137
|
+
? T
|
|
138
|
+
: Defaults[F]
|
|
139
|
+
: U extends { role: infer R extends string }
|
|
140
|
+
? P extends { [X in R]: infer T extends string }
|
|
141
|
+
? T
|
|
142
|
+
: Defaults[F]
|
|
143
|
+
: Defaults[F];
|
|
144
|
+
/** Every `From` in `S` replaced. */
|
|
145
|
+
type Replace<S extends string, From extends string, To extends string> = S extends `${infer A}${From}${infer B}`
|
|
146
|
+
? `${A}${To}${Replace<B, From, To>}`
|
|
147
|
+
: S;
|
|
148
|
+
/** An upstream's `handoff.number` or `tunnel.number`. */
|
|
149
|
+
type NumberOf<U> = U extends { handoff: { number: infer N extends number } }
|
|
150
|
+
? N
|
|
151
|
+
: U extends { tunnel: { number: infer N extends number } }
|
|
152
|
+
? N
|
|
153
|
+
: number;
|
|
154
|
+
/** The name built for upstream `K`: its pattern with the key, the key in capitals and its number filled in. */
|
|
155
|
+
type Named<C, N, F extends Field, K extends Key<C>> = Replace<
|
|
156
|
+
Replace<Replace<Chosen<PatternOf<N, F>, C[K], F>, "{key}", K>, "{KEY}", Uppercase<K>>,
|
|
157
|
+
"{number}",
|
|
158
|
+
`${NumberOf<C[K]>}`
|
|
159
|
+
>;
|
|
160
|
+
/** Each of these upstreams' names. */
|
|
161
|
+
type Names<C, N, F extends Field, Ks extends Key<C>> = { [K in Ks]: Named<C, N, F, K> }[Ks];
|
|
96
162
|
|
|
97
163
|
/** The catalog as written, with any key the shape does not have refused, at every level. */
|
|
98
|
-
type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret
|
|
164
|
+
type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown)
|
|
99
165
|
? T
|
|
100
166
|
: { [K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never };
|
|
101
167
|
|
|
168
|
+
/** The names as written, with a name or a role Upstreams does not make refused. */
|
|
169
|
+
type ExactNames<N> = {
|
|
170
|
+
[F in keyof N]: F extends keyof UpstreamNames
|
|
171
|
+
? N[F] extends string | ((...args: never[]) => unknown)
|
|
172
|
+
? N[F]
|
|
173
|
+
: { [R in keyof N[F]]: R extends "provider" | "rs" | "peer" | "tunnel" ? N[F][R] : never }
|
|
174
|
+
: never;
|
|
175
|
+
};
|
|
176
|
+
|
|
102
177
|
/** A network's upstreams written once, and each device's part built from them. */
|
|
103
178
|
export class Upstreams {
|
|
104
179
|
/**
|
|
105
|
-
* Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked:
|
|
180
|
+
* Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked. By default:
|
|
106
181
|
*
|
|
107
182
|
* - VLAN `<key>`, and `<key>-native` for one that sends its VLAN tagged. The router's interface is `<key>` too, a VLAN interface or a GRE tunnel.
|
|
108
183
|
* - Policies `<KEY>-IN` and `<KEY>-OUT`; a BGP group `<key>` for an exchange's route servers or multihop sessions.
|
|
109
184
|
*
|
|
110
|
-
*
|
|
185
|
+
* `names` gives patterns of your own, such as `{ interface: { provider: "core-ipt0{number}-{key}" } }`; see `UpstreamNames`.
|
|
186
|
+
*
|
|
187
|
+
* On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies({ ... })`, `tunnelRules()`, `addresses()`. On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`. For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`. Lists follow the catalog's order, where order counts on a device.
|
|
111
188
|
*/
|
|
112
|
-
static define<const C extends UpstreamCatalog
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
189
|
+
static define<const C extends UpstreamCatalog, const N extends UpstreamNames = {}>(
|
|
190
|
+
catalog: C & { [K in keyof C]: Exact<Upstream, C[K]> },
|
|
191
|
+
options: { names?: N & ExactNames<N> } = {}
|
|
192
|
+
) {
|
|
193
|
+
type V = Names<C, N, "vlan", Landed<C>>;
|
|
194
|
+
type T = Names<C, N, "native", Tagged<C>>;
|
|
195
|
+
type I = Names<C, N, "interface", Landed<C>>;
|
|
196
|
+
type G = Names<C, N, "interface", Tunneled<C>>;
|
|
197
|
+
type PolicyName = Names<C, N, "import", Importing<C>> | Names<C, N, "export", Key<C>>;
|
|
198
|
+
type Group = Names<C, N, "group", Grouped<C>>;
|
|
199
|
+
|
|
200
|
+
const names: UpstreamNames = options.names ?? {};
|
|
116
201
|
const all = Object.entries(catalog) as [Key<C>, Upstream][];
|
|
117
202
|
const nameOf = (key: string, upstream: Upstream) => upstream.name ?? key;
|
|
118
|
-
const policy = (key: string) => key.toUpperCase();
|
|
119
203
|
const grouped = (upstream: Upstream) => upstream.role === "rs" || Boolean(upstream.multihop);
|
|
120
204
|
|
|
205
|
+
/** The name built for one upstream: its pattern with the key, the key in capitals and its number filled in. */
|
|
206
|
+
const named = (field: Field, key: string, upstream: Upstream): string => {
|
|
207
|
+
const given = names[field];
|
|
208
|
+
const pattern =
|
|
209
|
+
given === undefined || typeof given === "string"
|
|
210
|
+
? (given ?? DEFAULTS[field])
|
|
211
|
+
: ((upstream.tunnel && "tunnel" in given ? given.tunnel : given[upstream.role]) ?? DEFAULTS[field]);
|
|
212
|
+
|
|
213
|
+
return pattern
|
|
214
|
+
.replaceAll("{key}", key)
|
|
215
|
+
.replaceAll("{KEY}", key.toUpperCase())
|
|
216
|
+
.replaceAll("{number}", () => {
|
|
217
|
+
const number = upstream.handoff?.number ?? upstream.tunnel?.number;
|
|
218
|
+
if (number === undefined) throw new Error(`Upstreams: ${key} needs a number for ${field} name ${pattern}`);
|
|
219
|
+
return String(number);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Two upstreams under one name would replace each other on the device.
|
|
224
|
+
for (const field of Object.keys(DEFAULTS) as Field[]) {
|
|
225
|
+
const taken = new Map<string, string>();
|
|
226
|
+
|
|
227
|
+
for (const [key, upstream] of all) {
|
|
228
|
+
if (field === "native" && upstream.handoff?.native === undefined) continue;
|
|
229
|
+
if (field === "import" && !upstream.import) continue;
|
|
230
|
+
if (field === "group" && !grouped(upstream)) continue;
|
|
231
|
+
if ((field === "vlan" && !upstream.handoff) || (field === "interface" && !upstream.handoff && !upstream.tunnel)) continue;
|
|
232
|
+
|
|
233
|
+
const name = named(field, key, upstream);
|
|
234
|
+
const other = taken.get(name);
|
|
235
|
+
if (other) throw new Error(`Upstreams: ${other} and ${key} both have the ${field} name ${name}`);
|
|
236
|
+
taken.set(name, key);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const vlan = (key: string, upstream: Upstream) => named("vlan", key, upstream) as V;
|
|
241
|
+
const native = (key: string, upstream: Upstream) => named("native", key, upstream) as T;
|
|
242
|
+
const policy = (direction: "import" | "export", key: string, upstream: Upstream) => named(direction, key, upstream) as PolicyName;
|
|
243
|
+
|
|
121
244
|
/** Each landed upstream's VLAN; with a device, the VLANs and natives of those arriving on its ports. */
|
|
122
245
|
const vlans = <const D extends string | undefined = undefined>(device?: D) => {
|
|
123
246
|
const result: Record<string, Vlan> = {};
|
|
@@ -126,33 +249,33 @@ export class Upstreams {
|
|
|
126
249
|
const handoff = upstream.handoff;
|
|
127
250
|
if (!handoff || (device !== undefined && handoff.port?.device !== device)) continue;
|
|
128
251
|
|
|
129
|
-
result[key] = { id: handoff.vlan, description: nameOf(key, upstream) };
|
|
252
|
+
result[vlan(key, upstream)] = { id: handoff.vlan, description: nameOf(key, upstream) };
|
|
130
253
|
if (device !== undefined && handoff.native !== undefined) {
|
|
131
|
-
result[
|
|
254
|
+
result[native(key, upstream)] = {
|
|
132
255
|
id: handoff.native,
|
|
133
256
|
description: `Native toward ${nameOf(key, upstream)}, nothing else in it`,
|
|
134
257
|
};
|
|
135
258
|
}
|
|
136
259
|
}
|
|
137
260
|
|
|
138
|
-
return result as D extends string ? { [K in V]: Vlan } & { [K in T
|
|
261
|
+
return result as D extends string ? { [K in V]: Vlan } & { [K in T]: Vlan } : { [K in V]: Vlan };
|
|
139
262
|
};
|
|
140
263
|
|
|
141
264
|
const vlansOn = (device: string) =>
|
|
142
|
-
all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key]) => key
|
|
265
|
+
all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key, upstream]) => vlan(key, upstream));
|
|
143
266
|
|
|
144
267
|
const ports = (device: string) => {
|
|
145
|
-
const result: Record<string, Port<V |
|
|
268
|
+
const result: Record<string, Port<V | T, never, never, never>> = {};
|
|
146
269
|
|
|
147
270
|
for (const [key, upstream] of all) {
|
|
148
271
|
const handoff = upstream.handoff;
|
|
149
272
|
if (!handoff?.port || handoff.port.device !== device) continue;
|
|
150
273
|
|
|
151
274
|
result[handoff.port.port] = {
|
|
152
|
-
description: nameOf(key, upstream),
|
|
275
|
+
description: names.port ? names.port(key, upstream) : nameOf(key, upstream),
|
|
153
276
|
...(handoff.native !== undefined
|
|
154
|
-
? { trunk_vlans: [key
|
|
155
|
-
: { access_vlan: key
|
|
277
|
+
? { trunk_vlans: [vlan(key, upstream)], native_vlan: native(key, upstream) }
|
|
278
|
+
: { access_vlan: vlan(key, upstream) }),
|
|
156
279
|
lldp: false,
|
|
157
280
|
stp: false,
|
|
158
281
|
...(handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}),
|
|
@@ -167,26 +290,31 @@ export class Upstreams {
|
|
|
167
290
|
|
|
168
291
|
for (const [key, upstream] of all) {
|
|
169
292
|
const description = nameOf(key, upstream);
|
|
293
|
+
const name = named("interface", key, upstream);
|
|
170
294
|
|
|
171
295
|
if (upstream.tunnel) {
|
|
172
296
|
const { local, remote, addresses, mtu } = upstream.tunnel;
|
|
173
|
-
result[
|
|
297
|
+
result[name] = { type: "gre", description, local, remote, addresses, ...(mtu ? { mtu } : {}) };
|
|
174
298
|
} else if (upstream.handoff) {
|
|
175
299
|
const { addresses, exchange } = upstream.handoff;
|
|
176
|
-
result[
|
|
300
|
+
result[name] = { type: "vlan", vlan: vlan(key, upstream), description, addresses, ...(exchange ? { exchange } : {}) };
|
|
177
301
|
}
|
|
178
302
|
}
|
|
179
303
|
|
|
180
|
-
return result as { [K in
|
|
304
|
+
return result as { [K in I]: VlanInterface<V, never> } & { [K in G]: Gre<never> };
|
|
181
305
|
};
|
|
182
306
|
|
|
307
|
+
/** The router's interfaces toward the internet: every landed upstream's, for NAT and lists like it. */
|
|
308
|
+
const uplinks = () =>
|
|
309
|
+
all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => named("interface", key, upstream) as I);
|
|
310
|
+
|
|
183
311
|
/** What a session or its group takes from its upstream. */
|
|
184
|
-
const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName
|
|
312
|
+
const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName> => ({
|
|
185
313
|
remote_as: upstream.asn,
|
|
186
314
|
description: nameOf(key, upstream),
|
|
187
315
|
role: upstream.role,
|
|
188
|
-
...(upstream.import ? { import:
|
|
189
|
-
export:
|
|
316
|
+
...(upstream.import ? { import: policy("import", key, upstream) } : {}),
|
|
317
|
+
export: policy("export", key, upstream),
|
|
190
318
|
});
|
|
191
319
|
|
|
192
320
|
const groups = () =>
|
|
@@ -196,7 +324,7 @@ export class Upstreams {
|
|
|
196
324
|
.map(([key, upstream]) => {
|
|
197
325
|
const multihop = upstream.multihop;
|
|
198
326
|
return [
|
|
199
|
-
key,
|
|
327
|
+
named("group", key, upstream),
|
|
200
328
|
{
|
|
201
329
|
...settings(key, upstream),
|
|
202
330
|
...(multihop
|
|
@@ -210,7 +338,7 @@ export class Upstreams {
|
|
|
210
338
|
},
|
|
211
339
|
];
|
|
212
340
|
})
|
|
213
|
-
) as Record<
|
|
341
|
+
) as Record<Group, NeighborSettings<PolicyName>>;
|
|
214
342
|
|
|
215
343
|
const neighbors = () =>
|
|
216
344
|
Object.fromEntries(
|
|
@@ -221,19 +349,19 @@ export class Upstreams {
|
|
|
221
349
|
name,
|
|
222
350
|
{
|
|
223
351
|
...session,
|
|
224
|
-
...(grouped(upstream) ? { group: key } : settings(key, upstream)),
|
|
352
|
+
...(grouped(upstream) ? { group: named("group", key, upstream) } : settings(key, upstream)),
|
|
225
353
|
...(limit ? { max_prefixes: limit } : {}),
|
|
226
354
|
},
|
|
227
355
|
];
|
|
228
356
|
})
|
|
229
357
|
)
|
|
230
|
-
) as Record<string, Neighbor<PolicyName
|
|
358
|
+
) as Record<string, Neighbor<PolicyName, Group>>;
|
|
231
359
|
|
|
232
360
|
const policies = <const P extends string = never>(options: UpstreamPolicies<P> = {}) => {
|
|
233
361
|
const result: Record<string, PolicyRule<P, never>[]> = {};
|
|
234
362
|
|
|
235
363
|
for (const [key, upstream] of all) {
|
|
236
|
-
result[
|
|
364
|
+
result[policy("export", key, upstream)] = [
|
|
237
365
|
...(options.actions?.(key, upstream) ?? []),
|
|
238
366
|
...(upstream.export ?? []),
|
|
239
367
|
...Bgp.announce(upstream.announce),
|
|
@@ -243,7 +371,7 @@ export class Upstreams {
|
|
|
243
371
|
if (!taken) continue;
|
|
244
372
|
|
|
245
373
|
const tag = options.tag?.(key, upstream);
|
|
246
|
-
result[
|
|
374
|
+
result[policy("import", key, upstream)] = [
|
|
247
375
|
...(taken.default
|
|
248
376
|
? Bgp.defaultOnly({ ...taken.default, ...(options.sources ? { sources: options.sources } : {}) })
|
|
249
377
|
: []),
|
|
@@ -259,16 +387,20 @@ export class Upstreams {
|
|
|
259
387
|
];
|
|
260
388
|
}
|
|
261
389
|
|
|
262
|
-
return result as Record<PolicyName
|
|
390
|
+
return result as Record<PolicyName, PolicyRule<P, never>[]>;
|
|
263
391
|
};
|
|
264
392
|
|
|
265
393
|
/** The VLANs straight off an upstream, natives included: `untrusted` for `Checks.trustBoundary`. */
|
|
266
|
-
const untrusted = () => [
|
|
394
|
+
const untrusted = () => [
|
|
395
|
+
...all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => vlan(key, upstream)),
|
|
396
|
+
...natives(),
|
|
397
|
+
];
|
|
267
398
|
|
|
268
399
|
const natives = () =>
|
|
269
|
-
all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key]) =>
|
|
400
|
+
all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key, upstream]) => native(key, upstream));
|
|
270
401
|
|
|
271
|
-
const privateDrops = () =>
|
|
402
|
+
const privateDrops = () =>
|
|
403
|
+
all.filter(([, upstream]) => upstream.handoff && upstream.drop_private).map(([key, upstream]) => vlan(key, upstream));
|
|
272
404
|
|
|
273
405
|
const tunnelRules = (): FilterRule<never, never>[] =>
|
|
274
406
|
all.flatMap(([key, upstream]) =>
|
|
@@ -293,6 +425,7 @@ export class Upstreams {
|
|
|
293
425
|
vlansOn,
|
|
294
426
|
ports,
|
|
295
427
|
interfaces,
|
|
428
|
+
uplinks,
|
|
296
429
|
groups,
|
|
297
430
|
neighbors,
|
|
298
431
|
policies,
|
package/src/schema/interfaces.ts
CHANGED
|
@@ -113,7 +113,7 @@ export type Port<V extends string, A extends string, L extends string, F extends
|
|
|
113
113
|
acl?: A;
|
|
114
114
|
/** The LAG this port is in. Every other setting goes on the LAG. */
|
|
115
115
|
lag?: L;
|
|
116
|
-
/** Another of our devices at the other end of the cable. */
|
|
116
|
+
/** Another of our devices at the other end of the cable. Without a `description`, the port is described from it: the far device and the far port as printed on it, such as `firewall-1 port6`. */
|
|
117
117
|
link?: Link;
|
|
118
118
|
};
|
|
119
119
|
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// src/platforms/fortios/models.ts
|
|
2
|
-
var models = {
|
|
3
|
-
"FG-3700D": {
|
|
4
|
-
ports: { "40g": 4, "10g": 28, "1g": 2 },
|
|
5
|
-
spelling: { "40g": "port{n}", "10g": "port{n+4}", "1g": "mgmt{n}" }
|
|
6
|
-
}
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
// src/platforms/junos/models.ts
|
|
10
|
-
var models2 = {
|
|
11
|
-
"QFX5100-48S-6Q": {
|
|
12
|
-
ports: { "40g": 6, "10g": 48, "1g": 2 },
|
|
13
|
-
spelling: { "40g": "et-0/0/{n+47}", "10g": "xe-0/0/{n-1}", "1g": "em{n-1}" }
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// src/platforms/raisecom-ros/models.ts
|
|
18
|
-
var models3 = {
|
|
19
|
-
"RAX721-C-6C48": {
|
|
20
|
-
ports: { "100g": 6, "25g": 24, "10g": 24 },
|
|
21
|
-
spelling: { "100g": "hundredgige 1/1/{n}", "25g": "twenty-fivegige 1/2/{n}", "10g": "tengigabitethernet 1/3/{n}" }
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// src/platforms/routeros/models.ts
|
|
26
|
-
var models4 = {
|
|
27
|
-
"CCR2216-1G-12XS-2XQ": {
|
|
28
|
-
ports: { "100g": 2, "25g": 12, "1g": 1 },
|
|
29
|
-
spelling: { "100g": "qsfp28-{n}-1", "25g": "sfp28-{n}", "1g": "ether{n}" },
|
|
30
|
-
limits: { lag_members: 8, switch_port_ranges: 5 }
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// src/platforms/vrp/models.ts
|
|
35
|
-
var models5 = {
|
|
36
|
-
"CE6855-48S6Q-HI": { ports: { "10g": 48, "40g": 6 }, spelling: { "10g": "10GE1/0/{n}", "40g": "40GE1/0/{n}" } }
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// src/platforms/catalog.ts
|
|
40
|
-
class Catalog {
|
|
41
|
-
static MODELS = { routeros: models4, vrp: models5, "raisecom-ros": models3, junos: models2, fortios: models };
|
|
42
|
-
static ports(platform, model) {
|
|
43
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
44
|
-
if (!spec)
|
|
45
|
-
throw new Error(`${platform} has no model ${model}`);
|
|
46
|
-
return Object.entries(spec.ports).flatMap(([speed, count]) => Array.from({ length: count }, (_, i) => `${speed}-${i + 1}`));
|
|
47
|
-
}
|
|
48
|
-
static limits(platform, model) {
|
|
49
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
50
|
-
return spec?.limits ?? {};
|
|
51
|
-
}
|
|
52
|
-
static spell(platform, model, port) {
|
|
53
|
-
const spec = Catalog.MODELS[platform]?.[model];
|
|
54
|
-
const parts = /^(\d+[gm])-(\d+)$/.exec(port);
|
|
55
|
-
const template = parts && spec?.spelling[parts[1]];
|
|
56
|
-
if (!template)
|
|
57
|
-
throw new Error(`${model} has no port ${port}`);
|
|
58
|
-
return template.replace(/\{n([+-]\d+)?\}/, (_, shift) => String(Number(parts[2]) + Number(shift ?? 0)));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export { Catalog };
|