@takodotid/circuit 0.34.2 → 0.35.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/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11347 -9018
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
- package/src/adapters/devices/raisecom-ros/index.ts +0 -693
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Every address the config uses, by prefix, and what is still free: what `circuit addresses` prints. It reads the config only, so an address a device gave out by itself, such as a DHCP lease, is not known.
|
|
2
|
+
|
|
3
|
+
import type { Device } from "../schema";
|
|
4
|
+
import { Address } from "./address";
|
|
5
|
+
import { Topology } from "./topology";
|
|
6
|
+
|
|
7
|
+
/** One address or range in a segment, and what holds it. */
|
|
8
|
+
export type Use = {
|
|
9
|
+
/** An address, or `first-last` for a range. */
|
|
10
|
+
address: string;
|
|
11
|
+
what: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** A prefix our devices have addresses in. */
|
|
15
|
+
export type Segment = {
|
|
16
|
+
prefix: string;
|
|
17
|
+
uses: Use[];
|
|
18
|
+
/** How many addresses no one uses, as a number written out, since an IPv6 count does not fit a JavaScript number. */
|
|
19
|
+
free: string;
|
|
20
|
+
/** The lowest free address, when there is one. */
|
|
21
|
+
next?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** A prefix in use without being a segment: a loopback's address, or a block routed elsewhere. */
|
|
25
|
+
export type Block = {
|
|
26
|
+
prefix: string;
|
|
27
|
+
what: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Space the network owns: a prefix it originates in BGP, or one asked for. Everything in use inside it, and what is free. */
|
|
31
|
+
export type Space = {
|
|
32
|
+
prefix: string;
|
|
33
|
+
/** Why it is listed, such as `originated by edge-01`. */
|
|
34
|
+
owner: string;
|
|
35
|
+
segments: Segment[];
|
|
36
|
+
blocks: Block[];
|
|
37
|
+
/** The fewest prefixes that cover what nothing uses. */
|
|
38
|
+
free: string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type AddressReport = {
|
|
42
|
+
spaces: Space[];
|
|
43
|
+
/** What is in use outside every space. Left out when a prefix is asked for. */
|
|
44
|
+
elsewhere: { segments: Segment[]; blocks: Block[] };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** A use as a range of numbers, for counting. */
|
|
48
|
+
type Taken = { first: bigint; last: bigint };
|
|
49
|
+
|
|
50
|
+
export class AddressSpace {
|
|
51
|
+
private static isHostRoute(cidr: string): boolean {
|
|
52
|
+
return Address.split(cidr).len === Address.bits(Address.split(cidr).family);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private static numberOf(address: string): bigint {
|
|
56
|
+
return Address.toNumber(address)!;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Addresses a segment can give out: IPv4 leaves out the network and broadcast addresses below /31, and IPv6 the subnet-router anycast address. */
|
|
60
|
+
private static usable(prefix: string): Taken {
|
|
61
|
+
const { first, last, family } = Address.range(prefix);
|
|
62
|
+
const { len } = Address.split(prefix);
|
|
63
|
+
|
|
64
|
+
if (family === "ipv4" && len <= 30) return { first: first + 1n, last: last - 1n };
|
|
65
|
+
if (family === "ipv6" && len < 127) return { first: first + 1n, last };
|
|
66
|
+
|
|
67
|
+
return { first, last };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Ranges merged, so overlapping ones count once. */
|
|
71
|
+
private static merged(ranges: Taken[]): Taken[] {
|
|
72
|
+
const sorted = [...ranges].sort((a, b) => (a.first < b.first ? -1 : a.first > b.first ? 1 : 0));
|
|
73
|
+
const result: Taken[] = [];
|
|
74
|
+
|
|
75
|
+
for (const range of sorted) {
|
|
76
|
+
const previous = result.at(-1);
|
|
77
|
+
if (previous && range.first <= previous.last + 1n) previous.last = range.last > previous.last ? range.last : previous.last;
|
|
78
|
+
else result.push({ ...range });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What is inside `within` and not in any of the ranges. */
|
|
85
|
+
private static gaps(within: Taken, taken: Taken[]): Taken[] {
|
|
86
|
+
const result: Taken[] = [];
|
|
87
|
+
let cursor = within.first;
|
|
88
|
+
|
|
89
|
+
for (const range of AddressSpace.merged(taken)) {
|
|
90
|
+
if (range.last < within.first || range.first > within.last) continue;
|
|
91
|
+
if (range.first > cursor) result.push({ first: cursor, last: range.first - 1n });
|
|
92
|
+
if (range.last + 1n > cursor) cursor = range.last + 1n;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (cursor <= within.last) result.push({ first: cursor, last: within.last });
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `12` or, for a count too long to read, `about 2^64`, to the nearest power of two. */
|
|
100
|
+
private static count(value: bigint): string {
|
|
101
|
+
if (value <= 1n << 20n) return String(value);
|
|
102
|
+
|
|
103
|
+
const power = value.toString(2).length - 1;
|
|
104
|
+
const nearer = value - (1n << BigInt(power)) >= 1n << BigInt(power - 1) ? power + 1 : power;
|
|
105
|
+
return `about 2^${nearer}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private static byAddress(a: Use, b: Use): number {
|
|
109
|
+
const first = (use: Use) => AddressSpace.numberOf(use.address.split("-")[0]!);
|
|
110
|
+
return first(a) < first(b) ? -1 : first(a) > first(b) ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** IPv4 before IPv6, then by first address. */
|
|
114
|
+
private static comparePrefixes(a: string, b: string): number {
|
|
115
|
+
const [ra, rb] = [Address.range(a), Address.range(b)];
|
|
116
|
+
if (ra.family !== rb.family) return ra.family === "ipv4" ? -1 : 1;
|
|
117
|
+
return ra.first < rb.first ? -1 : ra.first > rb.first ? 1 : 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private static byPrefix(a: { prefix: string }, b: { prefix: string }): number {
|
|
121
|
+
return AddressSpace.comparePrefixes(a.prefix, b.prefix);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Every address in use, by segment, under the space the network owns. With `within`, only what falls inside that prefix, as the one space. */
|
|
125
|
+
static report(devices: readonly Device[], within?: string): AddressReport {
|
|
126
|
+
const segments = new Map<string, { uses: Use[]; taken: Taken[] }>();
|
|
127
|
+
const blocks: Block[] = [];
|
|
128
|
+
|
|
129
|
+
const segmentOf = (address: string) => [...segments.keys()].find((prefix) => Address.contains(prefix, address));
|
|
130
|
+
|
|
131
|
+
// Segments come from the addresses on interfaces; a host route, such as a loopback's, is a block of its own.
|
|
132
|
+
for (const holder of Topology.holders(devices)) {
|
|
133
|
+
const vrf = holder.device.ports?.[holder.name]?.vrf ?? holder.device.interfaces?.[holder.name]?.vrf;
|
|
134
|
+
const what = `${holder.device.name} ${holder.name}${vrf ? `, VRF ${vrf}` : ""}`;
|
|
135
|
+
|
|
136
|
+
if (AddressSpace.isHostRoute(holder.address) || holder.peer) {
|
|
137
|
+
blocks.push({ prefix: `${Address.host(holder.address)}/${Address.bits(Address.family(holder.address))}`, what });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const prefix = Address.prefixOf(holder.address);
|
|
142
|
+
const segment = segments.get(prefix) ?? { uses: [], taken: [] };
|
|
143
|
+
const value = AddressSpace.numberOf(Address.host(holder.address));
|
|
144
|
+
|
|
145
|
+
segment.uses.push({ address: Address.host(holder.address), what });
|
|
146
|
+
segment.taken.push({ first: value, last: value });
|
|
147
|
+
segments.set(prefix, segment);
|
|
148
|
+
|
|
149
|
+
for (const group of holder.vrrp) {
|
|
150
|
+
if (!Address.isValid(group.address) || !Address.contains(prefix, group.address)) continue;
|
|
151
|
+
if (segment.uses.some((use) => use.address === group.address)) continue;
|
|
152
|
+
|
|
153
|
+
const holders = devices.filter((device) =>
|
|
154
|
+
Object.values({ ...device.ports, ...device.interfaces }).some((settings) =>
|
|
155
|
+
settings?.vrrp?.some((other) => other.id === group.id && other.address === group.address)
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
segment.uses.push({
|
|
160
|
+
address: group.address,
|
|
161
|
+
what: `VRRP ${group.id} on ${holders.map((device) => device.name).join(", ")}`,
|
|
162
|
+
});
|
|
163
|
+
segment.taken.push({ first: AddressSpace.numberOf(group.address), last: AddressSpace.numberOf(group.address) });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// A use inside a segment is an address taken; outside every segment, a block of its own. An address one of our interfaces holds is listed once, as that interface.
|
|
168
|
+
const take = (address: string, what: string, first = address, last = address) => {
|
|
169
|
+
const prefix = segmentOf(first);
|
|
170
|
+
if (!prefix) return false;
|
|
171
|
+
|
|
172
|
+
const segment = segments.get(prefix)!;
|
|
173
|
+
if (segment.uses.some((use) => use.address === address)) return true;
|
|
174
|
+
|
|
175
|
+
segment.uses.push({ address, what });
|
|
176
|
+
segment.taken.push({ first: AddressSpace.numberOf(first), last: AddressSpace.numberOf(last) });
|
|
177
|
+
return true;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
for (const device of devices) {
|
|
181
|
+
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
182
|
+
const [start, end] = server.pool;
|
|
183
|
+
if (Address.isValid(start) && Address.isValid(end))
|
|
184
|
+
take(`${start}-${end}`, `DHCP pool ${name} on ${device.name}`, start, end);
|
|
185
|
+
|
|
186
|
+
for (const reservation of server.reservations ?? []) {
|
|
187
|
+
if (Address.isValid(reservation.address))
|
|
188
|
+
take(reservation.address, `reserved for ${reservation.description ?? reservation.mac} by ${device.name}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const route of device.routing?.static ?? []) {
|
|
193
|
+
if (route.via && Address.isValid(route.via)) take(route.via, `next hop for ${route.prefix} on ${device.name}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const [name, neighbor] of Object.entries(device.routing?.bgp?.neighbors ?? {})) {
|
|
197
|
+
if (Address.isValid(neighbor.address)) take(neighbor.address, `BGP neighbor ${name} of ${device.name}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
201
|
+
if (iface.type !== "wireguard") continue;
|
|
202
|
+
|
|
203
|
+
for (const peer of iface.peers ?? []) {
|
|
204
|
+
for (const allowed of peer.allowed_addresses) {
|
|
205
|
+
if (!Address.isCidr(allowed)) continue;
|
|
206
|
+
|
|
207
|
+
const what = `WireGuard peer ${peer.name} on ${device.name} ${name}`;
|
|
208
|
+
if (AddressSpace.isHostRoute(allowed) && take(Address.host(allowed), what)) continue;
|
|
209
|
+
if (!segmentOf(Address.host(allowed))) blocks.push({ prefix: Address.prefixOf(allowed), what });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Space the network owns: what it originates in BGP, or the prefix asked for.
|
|
216
|
+
const owners = new Map<string, string[]>();
|
|
217
|
+
for (const device of devices) {
|
|
218
|
+
for (const prefix of device.routing?.bgp?.networks ?? []) {
|
|
219
|
+
if (!Address.isCidr(prefix)) continue;
|
|
220
|
+
owners.set(Address.prefixOf(prefix), [...(owners.get(Address.prefixOf(prefix)) ?? []), device.name]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// A static route to a prefix no segment is in hands that block to whoever is at the next hop. A route covering segments or owned space, such as an aggregate, is not a use.
|
|
225
|
+
for (const device of devices) {
|
|
226
|
+
for (const route of device.routing?.static ?? []) {
|
|
227
|
+
if (!Address.isCidr(route.prefix) || Address.split(route.prefix).len === 0) continue;
|
|
228
|
+
|
|
229
|
+
const overlapsSegment = [...segments.keys()].some(
|
|
230
|
+
(prefix) => Address.contains(route.prefix, prefix) || Address.contains(prefix, route.prefix)
|
|
231
|
+
);
|
|
232
|
+
const coversOwned = [...owners.keys()].some((owned) => Address.contains(route.prefix, owned));
|
|
233
|
+
if (overlapsSegment || coversOwned) continue;
|
|
234
|
+
|
|
235
|
+
const how = route.blackhole ? "discarded" : route.via ? `routed via ${route.via}` : `routed out ${route.interface}`;
|
|
236
|
+
blocks.push({ prefix: Address.prefixOf(route.prefix), what: `${how} by ${device.name}` });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const asked = within ? Address.prefixOf(within) : undefined;
|
|
241
|
+
const spacePrefixes = asked ? [asked] : [...owners.keys()];
|
|
242
|
+
|
|
243
|
+
// A segment or block belongs to the smallest space it is inside.
|
|
244
|
+
const spaceOf = (prefix: string) =>
|
|
245
|
+
spacePrefixes.filter((space) => Address.contains(space, prefix)).sort((a, b) => Address.split(b).len - Address.split(a).len)[0];
|
|
246
|
+
|
|
247
|
+
const toSegment = (prefix: string): Segment => {
|
|
248
|
+
const { uses, taken } = segments.get(prefix)!;
|
|
249
|
+
const free = AddressSpace.gaps(AddressSpace.usable(prefix), taken);
|
|
250
|
+
const total = free.reduce((sum, range) => sum + range.last - range.first + 1n, 0n);
|
|
251
|
+
const family = Address.family(prefix);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
prefix,
|
|
255
|
+
uses: uses.sort(AddressSpace.byAddress),
|
|
256
|
+
free: AddressSpace.count(total),
|
|
257
|
+
...(free[0] ? { next: Address.fromNumber(free[0].first, family) } : {}),
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const uniqueBlocks = blocks.filter(
|
|
262
|
+
(block, index) => blocks.findIndex((other) => other.prefix === block.prefix && other.what === block.what) === index
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const spaces: Space[] = spacePrefixes.sort(AddressSpace.comparePrefixes).map((space) => {
|
|
266
|
+
const inside = (prefix: string) => spaceOf(prefix) === space;
|
|
267
|
+
const spaceSegments = [...segments.keys()].filter(inside).sort(AddressSpace.comparePrefixes);
|
|
268
|
+
const spaceBlocks = uniqueBlocks.filter((block) => inside(block.prefix)).sort(AddressSpace.byPrefix);
|
|
269
|
+
|
|
270
|
+
// Smaller spaces inside this one are in use too.
|
|
271
|
+
const nested = spacePrefixes.filter((other) => other !== space && Address.contains(space, other));
|
|
272
|
+
const taken = [...spaceSegments, ...spaceBlocks.map((block) => block.prefix), ...nested].map((prefix) => Address.range(prefix));
|
|
273
|
+
const family = Address.family(space);
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
prefix: space,
|
|
277
|
+
owner: asked && !owners.has(space) ? "asked for" : `originated by ${(owners.get(space) ?? []).join(", ")}`,
|
|
278
|
+
segments: spaceSegments.map(toSegment),
|
|
279
|
+
blocks: spaceBlocks,
|
|
280
|
+
free: AddressSpace.gaps(Address.range(space), taken).flatMap((range) =>
|
|
281
|
+
Address.prefixesBetween(range.first, range.last, family)
|
|
282
|
+
),
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const outside = (prefix: string) => !spaceOf(prefix);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
spaces,
|
|
290
|
+
elsewhere: asked
|
|
291
|
+
? { segments: [], blocks: [] }
|
|
292
|
+
: {
|
|
293
|
+
segments: [...segments.keys()].filter(outside).sort(AddressSpace.comparePrefixes).map(toSegment),
|
|
294
|
+
blocks: uniqueBlocks.filter((block) => outside(block.prefix)).sort(AddressSpace.byPrefix),
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Free prefixes of one length inside the free space, up to `limit`, and how many there are in all. */
|
|
300
|
+
static freeOfLength(free: readonly string[], length: number, limit: number): { prefixes: string[]; total: bigint } {
|
|
301
|
+
const prefixes: string[] = [];
|
|
302
|
+
let total = 0n;
|
|
303
|
+
|
|
304
|
+
for (const prefix of free) {
|
|
305
|
+
const { len, family } = Address.split(prefix);
|
|
306
|
+
if (len > length || length > Address.bits(family)) continue;
|
|
307
|
+
|
|
308
|
+
const { first } = Address.range(prefix);
|
|
309
|
+
const step = 1n << BigInt(Address.bits(family) - length);
|
|
310
|
+
const fits = 1n << BigInt(length - len);
|
|
311
|
+
total += fits;
|
|
312
|
+
|
|
313
|
+
for (let index = 0n; index < fits && prefixes.length < limit; index++) {
|
|
314
|
+
prefixes.push(`${Address.fromNumber(first + index * step, family)}/${length}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { prefixes, total };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Address arithmetic for both families. Addresses are parsed to bigint so v4 and v6 share one code path.
|
|
2
|
+
|
|
3
|
+
import type { Family, IP, IPv4, IPv6, Prefix } from "../schema";
|
|
4
|
+
|
|
5
|
+
/** Address helpers, for checks of your own. */
|
|
6
|
+
export class Address {
|
|
7
|
+
/** IPv4 space another network cannot reach: this host, private, shared, loopback, link-local and documentation, after RFC 6890. */
|
|
8
|
+
private static readonly NOT_GLOBAL_V4 = [
|
|
9
|
+
"0.0.0.0/8",
|
|
10
|
+
"10.0.0.0/8",
|
|
11
|
+
"100.64.0.0/10",
|
|
12
|
+
"127.0.0.0/8",
|
|
13
|
+
"169.254.0.0/16",
|
|
14
|
+
"172.16.0.0/12",
|
|
15
|
+
"192.0.2.0/24",
|
|
16
|
+
"192.168.0.0/16",
|
|
17
|
+
"198.18.0.0/15",
|
|
18
|
+
"198.51.100.0/24",
|
|
19
|
+
"203.0.113.0/24",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
static family(address: string): Family {
|
|
23
|
+
return address.includes(":") ? "ipv6" : "ipv4";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** How many bits an address of the family has: 32 or 128. */
|
|
27
|
+
static bits(family: Family): number {
|
|
28
|
+
return family === "ipv4" ? 32 : 128;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An address as a number, or null when it is not one. */
|
|
32
|
+
static toNumber(address: string): bigint | null {
|
|
33
|
+
if (!address.includes(":")) {
|
|
34
|
+
const octets = address.split(".");
|
|
35
|
+
const valid = octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
|
|
36
|
+
if (!valid) return null;
|
|
37
|
+
|
|
38
|
+
return octets.reduce((value, octet) => (value << 8n) | BigInt(octet), 0n);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [before, after, extra] = address.split("::");
|
|
42
|
+
if (extra !== undefined) return null;
|
|
43
|
+
|
|
44
|
+
const groupsOf = (text: string | undefined) => (text ? text.split(":") : []);
|
|
45
|
+
const leading = groupsOf(before);
|
|
46
|
+
const trailing = groupsOf(after);
|
|
47
|
+
|
|
48
|
+
// `::` stands for as many zero groups as make eight.
|
|
49
|
+
const zeros = after === undefined ? 0 : 8 - leading.length - trailing.length;
|
|
50
|
+
const groups = [...leading, ...Array(zeros).fill("0"), ...trailing];
|
|
51
|
+
|
|
52
|
+
const valid = groups.length === 8 && groups.every((group) => /^[0-9a-fA-F]{1,4}$/.test(group));
|
|
53
|
+
if (!valid) return null;
|
|
54
|
+
|
|
55
|
+
return groups.reduce((value, group) => (value << 16n) | BigInt(parseInt(group, 16)), 0n);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `10.0.0.1/24` to its parts. Throws when it is not an address with a length. */
|
|
59
|
+
static split(cidr: string): { addr: string; len: number; family: Family } {
|
|
60
|
+
const [address, lengthText] = cidr.split("/");
|
|
61
|
+
const family = Address.family(address!);
|
|
62
|
+
const length = Number(lengthText);
|
|
63
|
+
|
|
64
|
+
const valid =
|
|
65
|
+
Address.toNumber(address!) !== null &&
|
|
66
|
+
lengthText !== undefined &&
|
|
67
|
+
Number.isInteger(length) &&
|
|
68
|
+
length >= 0 &&
|
|
69
|
+
length <= Address.bits(family);
|
|
70
|
+
if (!valid) throw new Error(`'${cidr}' is not an address with a length`);
|
|
71
|
+
|
|
72
|
+
return { addr: address!, len: length, family };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static isValid(text: string): boolean {
|
|
76
|
+
return Address.toNumber(text) !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static isCidr(text: string): boolean {
|
|
80
|
+
try {
|
|
81
|
+
Address.split(text);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private static mask(length: number, family: Family): bigint {
|
|
89
|
+
const all = (1n << BigInt(Address.bits(family))) - 1n;
|
|
90
|
+
const hostBits = (1n << BigInt(Address.bits(family) - length)) - 1n;
|
|
91
|
+
return all ^ hostBits;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A number back to the address it stands for. */
|
|
95
|
+
static fromNumber(value: bigint, family: Family): IP {
|
|
96
|
+
if (family === "ipv4") {
|
|
97
|
+
return [24n, 16n, 8n, 0n].map((shift) => String((value >> shift) & 255n)).join(".") as IPv4;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const groups = Array.from({ length: 8 }, (_, index) => ((value >> BigInt(112 - index * 16)) & 0xffffn).toString(16));
|
|
101
|
+
|
|
102
|
+
// Compress the longest run of zero groups, as RFC 5952 asks.
|
|
103
|
+
let longestStart = -1;
|
|
104
|
+
let longestLength = 0;
|
|
105
|
+
|
|
106
|
+
for (let start = 0; start < 8;) {
|
|
107
|
+
let end = start;
|
|
108
|
+
while (end < 8 && groups[end] === "0") end++;
|
|
109
|
+
|
|
110
|
+
const runLength = end - start;
|
|
111
|
+
if (runLength > longestLength && runLength > 1) {
|
|
112
|
+
longestStart = start;
|
|
113
|
+
longestLength = runLength;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
start = end === start ? start + 1 : end;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (longestStart < 0) return groups.join(":") as IPv6;
|
|
120
|
+
|
|
121
|
+
const head = groups.slice(0, longestStart).join(":");
|
|
122
|
+
const tail = groups.slice(longestStart + longestLength).join(":");
|
|
123
|
+
return `${head}::${tail}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** An address, with or without its length, in one spelling: IPv6 as RFC 5952 writes it, so `2001:DB8:0:0::1` and `2001:db8::1` compare equal. Text that is not an address comes back as it is. */
|
|
127
|
+
static canonical(text: string): string {
|
|
128
|
+
const [address, length] = text.split("/");
|
|
129
|
+
const value = Address.toNumber(address!);
|
|
130
|
+
if (value === null) return text;
|
|
131
|
+
|
|
132
|
+
const spelled = Address.fromNumber(value, Address.family(address!));
|
|
133
|
+
return length === undefined ? spelled : `${spelled}/${length}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `10.0.0.1/24` to `10.0.0.0/24`. */
|
|
137
|
+
static prefixOf(cidr: string): Prefix {
|
|
138
|
+
const { addr, len, family } = Address.split(cidr);
|
|
139
|
+
return `${Address.fromNumber(Address.toNumber(addr)! & Address.mask(len, family), family)}/${len}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Is the address or prefix `inner` inside `outer`? */
|
|
143
|
+
static contains(outer: string, inner: string): boolean {
|
|
144
|
+
const outerParts = Address.split(outer);
|
|
145
|
+
const [innerAddress, innerLengthText] = inner.split("/");
|
|
146
|
+
if (Address.family(innerAddress!) !== outerParts.family) return false;
|
|
147
|
+
|
|
148
|
+
const innerLength = innerLengthText === undefined ? Address.bits(outerParts.family) : Number(innerLengthText);
|
|
149
|
+
if (innerLength < outerParts.len) return false;
|
|
150
|
+
|
|
151
|
+
const outerMask = Address.mask(outerParts.len, outerParts.family);
|
|
152
|
+
return (Address.toNumber(innerAddress!)! & outerMask) === (Address.toNumber(outerParts.addr)! & outerMask);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The first and last address of a prefix, as numbers. */
|
|
156
|
+
static range(cidr: string): { first: bigint; last: bigint; family: Family } {
|
|
157
|
+
const { addr, len, family } = Address.split(cidr);
|
|
158
|
+
const hostBits = Address.mask(len, family) ^ ((1n << BigInt(Address.bits(family))) - 1n);
|
|
159
|
+
const first = Address.toNumber(addr)! & Address.mask(len, family);
|
|
160
|
+
|
|
161
|
+
return { first, last: first | hostBits, family };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The fewest prefixes that cover every address from `first` to `last` and nothing else. */
|
|
165
|
+
static prefixesBetween(first: bigint, last: bigint, family: Family): Prefix[] {
|
|
166
|
+
const bits = Address.bits(family);
|
|
167
|
+
const prefixes: Prefix[] = [];
|
|
168
|
+
|
|
169
|
+
for (let start = first; start <= last;) {
|
|
170
|
+
// The largest block that starts here on its own boundary and does not pass `last`.
|
|
171
|
+
let size = 0;
|
|
172
|
+
while (size < bits && start % (1n << BigInt(size + 1)) === 0n && start + (1n << BigInt(size + 1)) - 1n <= last) size++;
|
|
173
|
+
|
|
174
|
+
prefixes.push(`${Address.fromNumber(start, family)}/${bits - size}`);
|
|
175
|
+
start += 1n << BigInt(size);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return prefixes;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** `/25` to `255.255.255.128`. */
|
|
182
|
+
static netmask(length: number): IPv4 {
|
|
183
|
+
return Address.fromNumber(Address.mask(length, "ipv4"), "ipv4") as IPv4;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** `/24` to `0.0.0.255`, the inverted mask some ACLs take. */
|
|
187
|
+
static wildcard(length: number): IPv4 {
|
|
188
|
+
return Address.fromNumber(Address.mask(length, "ipv4") ^ 0xffffffffn, "ipv4") as IPv4;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The address with its length dropped. */
|
|
192
|
+
static host(cidr: string): IP {
|
|
193
|
+
return cidr.split("/")[0]! as IP;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Whether another network can reach the address. IPv6 is global only inside 2000::/3, less documentation space. */
|
|
197
|
+
static isGlobal(address: string): boolean {
|
|
198
|
+
if (Address.family(address) === "ipv6") return Address.contains("2000::/3", address) && !Address.contains("2001:db8::/32", address);
|
|
199
|
+
|
|
200
|
+
return !Address.NOT_GLOBAL_V4.some((prefix) => Address.contains(prefix, address));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// The cabling as LLDP sees it, compared with what `link` says. `snapshot` saves each device's LLDP neighbors beside its config; `validate` warns where they disagree.
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Catalog } from "../platforms/catalog";
|
|
6
|
+
import type { Device } from "../schema";
|
|
7
|
+
import type { Finding } from "./define";
|
|
8
|
+
|
|
9
|
+
/** A device one of our ports sees at the far end of its cable, over LLDP. */
|
|
10
|
+
export type Neighbor = {
|
|
11
|
+
/** Our port, by Circuit's name, such as `10g-1`. */
|
|
12
|
+
port: string;
|
|
13
|
+
/** The system name the far device sends, its hostname. */
|
|
14
|
+
system: string;
|
|
15
|
+
/** The far device's port, as that device spells it, such as `XGigabitEthernet0/0/1`. */
|
|
16
|
+
remote_port: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class Cabling {
|
|
20
|
+
/** The end of a state file that holds a device's LLDP neighbors, beside its config. */
|
|
21
|
+
static readonly SUFFIX = ".lldp.json";
|
|
22
|
+
|
|
23
|
+
/** The file a device's neighbors are saved in. */
|
|
24
|
+
static path(stateDirectory: string, device: string): string {
|
|
25
|
+
return join(stateDirectory, device + Cabling.SUFFIX);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Save what a device sees, sorted, so a file changes only when a neighbor does. */
|
|
29
|
+
static save(stateDirectory: string, device: string, neighbors: readonly Neighbor[]): void {
|
|
30
|
+
const sorted = [...neighbors].sort((a, b) => Cabling.order(a).localeCompare(Cabling.order(b)));
|
|
31
|
+
const tidy = sorted.map(({ port, system, remote_port }) => ({ port, system, remote_port }));
|
|
32
|
+
|
|
33
|
+
writeFileSync(Cabling.path(stateDirectory, device), JSON.stringify(tidy, null, 4) + "\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Forget what a device saw, when its neighbors could not be read this time. */
|
|
37
|
+
static forget(stateDirectory: string, device: string): void {
|
|
38
|
+
rmSync(Cabling.path(stateDirectory, device), { force: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What a device saw at its last snapshot, or undefined when its neighbors were never read. */
|
|
42
|
+
static load(stateDirectory: string, device: string): Neighbor[] | undefined {
|
|
43
|
+
const path = Cabling.path(stateDirectory, device);
|
|
44
|
+
if (!existsSync(path)) return undefined;
|
|
45
|
+
|
|
46
|
+
return JSON.parse(readFileSync(path, "utf8")) as Neighbor[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static order(neighbor: Neighbor): string {
|
|
50
|
+
return `${neighbor.port} ${neighbor.system} ${neighbor.remote_port}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The device runs LLDP somewhere: on every port, on the interfaces it lists, or on a port that turns it on. */
|
|
54
|
+
static runsAnywhere(device: Device): boolean {
|
|
55
|
+
return Boolean(device.lldp) || Object.values(device.ports ?? {}).some((port) => port?.lldp === true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The port runs LLDP, by the port's own `lldp`, or else the device's. */
|
|
59
|
+
static runsOn(device: Device, port: string): boolean {
|
|
60
|
+
const own = device.ports?.[port as keyof typeof device.ports]?.lldp;
|
|
61
|
+
if (own !== undefined) return own;
|
|
62
|
+
|
|
63
|
+
const lldp = device.lldp;
|
|
64
|
+
return lldp === true || (typeof lldp === "object" && (lldp.interfaces as readonly string[]).includes(port));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Warnings where LLDP and `link` disagree, from each device's own view: a cable in another port than `link` names, a cable `link` names that LLDP does not see, and another of our devices on a port without `link`.
|
|
69
|
+
*
|
|
70
|
+
* Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. LLDP can be off at the far end, so each is a warning.
|
|
71
|
+
*/
|
|
72
|
+
static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[] {
|
|
73
|
+
const findings: Finding[] = [];
|
|
74
|
+
|
|
75
|
+
for (const device of devices) {
|
|
76
|
+
const seen = neighbors.get(device.name);
|
|
77
|
+
if (!seen) continue;
|
|
78
|
+
|
|
79
|
+
const warn = (message: string) => findings.push({ level: "warning", device: device.name, message });
|
|
80
|
+
const linked = Object.entries(device.ports ?? {}).filter(([, port]) => port?.link);
|
|
81
|
+
const ports = new Set([...linked.map(([name]) => name), ...seen.map((neighbor) => neighbor.port)]);
|
|
82
|
+
|
|
83
|
+
for (const port of ports) {
|
|
84
|
+
if (!Cabling.runsOn(device, port)) continue;
|
|
85
|
+
|
|
86
|
+
const here = seen.filter((neighbor) => neighbor.port === port);
|
|
87
|
+
const link = device.ports?.[port as keyof typeof device.ports]?.link;
|
|
88
|
+
|
|
89
|
+
if (!link) {
|
|
90
|
+
for (const neighbor of here) {
|
|
91
|
+
const far = Cabling.deviceOf(devices, neighbor.system);
|
|
92
|
+
if (!far) continue;
|
|
93
|
+
|
|
94
|
+
const farPort = Cabling.portOf(far, neighbor.remote_port);
|
|
95
|
+
if (Cabling.runsOn(far, farPort)) warn(`port ${port}: LLDP sees ${far.name} ${farPort}, but the port has no link`);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const far = devices.find((candidate) => candidate.name === link.device);
|
|
101
|
+
if (!far || !Cabling.runsOn(far, link.port)) continue;
|
|
102
|
+
|
|
103
|
+
const named = `${link.device} ${link.port}`;
|
|
104
|
+
|
|
105
|
+
if (!here.length) {
|
|
106
|
+
warn(`port ${port}: LLDP sees nothing, but link names ${named}: the cable is missing or down`);
|
|
107
|
+
} else if (
|
|
108
|
+
!here.some(
|
|
109
|
+
(neighbor) => Cabling.isDevice(neighbor.system, far.name) && Cabling.isPort(far, link.port, neighbor.remote_port)
|
|
110
|
+
)
|
|
111
|
+
) {
|
|
112
|
+
const what = here.map((neighbor) => `${neighbor.system} ${neighbor.remote_port}`).join(" and ");
|
|
113
|
+
warn(`port ${port}: LLDP sees ${what}, but link names ${named}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A system name is the device's name, alone or with a domain after it. */
|
|
122
|
+
private static isDevice(system: string, name: string): boolean {
|
|
123
|
+
return system === name || system.startsWith(`${name}.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private static deviceOf(devices: readonly Device[], system: string): Device | undefined {
|
|
127
|
+
return devices.find((device) => Cabling.isDevice(system, device.name));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A port LLDP reports, as the far device spells it, is that device's port by Circuit's name. */
|
|
131
|
+
private static isPort(device: Device, port: string, reported: string): boolean {
|
|
132
|
+
const tidy = (text: string) => text.replace(/\s+/g, "").toLowerCase();
|
|
133
|
+
if (tidy(reported) === tidy(port)) return true;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
return tidy(Catalog.spell(device.platform, device.model, port)) === tidy(reported);
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Circuit's name for a port LLDP reports, or the report itself when no port of the device is spelled that way. */
|
|
143
|
+
private static portOf(device: Device, reported: string): string {
|
|
144
|
+
let ports: string[] = [];
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
ports = Catalog.ports(device.platform, device.model);
|
|
148
|
+
} catch {
|
|
149
|
+
return reported;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ports.find((port) => Cabling.isPort(device, port, reported)) ?? reported;
|
|
153
|
+
}
|
|
154
|
+
}
|