@takodotid/circuit 0.43.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
package/src/helpers/upstreams.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import { Address } from "../core/address";
|
|
2
|
-
import type {
|
|
3
|
-
Exchange as ExchangeLan,
|
|
4
|
-
Family,
|
|
5
|
-
FilterRule,
|
|
6
|
-
Gre,
|
|
7
|
-
InterfaceAddress,
|
|
8
|
-
StandardCommunity,
|
|
9
|
-
LargeCommunity,
|
|
10
|
-
IP,
|
|
11
|
-
Neighbor,
|
|
12
|
-
NeighborSettings,
|
|
13
|
-
PolicyRule,
|
|
14
|
-
Port,
|
|
15
|
-
Prefix,
|
|
16
|
-
Secret,
|
|
17
|
-
Vlan,
|
|
18
|
-
VlanInterface,
|
|
19
|
-
} from "../schema";
|
|
20
|
-
import type { NeighborCommunities } from "./bgp";
|
|
21
|
-
|
|
22
|
-
/** A route policy an upstream's sessions use: its name on the device, and its rules, in order. */
|
|
23
|
-
export type UpstreamPolicy = {
|
|
24
|
-
/** The policy's name, as the device carries it. */
|
|
25
|
-
name: string;
|
|
26
|
-
/** Its rules, in order. Nothing is added to them unless `mapImports` or `mapExports` adds it. */
|
|
27
|
-
rules: readonly PolicyRule<string, string>[];
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** A VLAN by its name and its ID. */
|
|
31
|
-
export type NamedVlan = { name: string; id: number };
|
|
32
|
-
|
|
33
|
-
/** One network we exchange routes with: a transit we buy, an exchange's route servers, or a peer. Every name it puts on a device is written here. */
|
|
34
|
-
export type Upstream = {
|
|
35
|
-
/** Its name as people write it, for descriptions: `Transit A`. The key when absent. */
|
|
36
|
-
name?: string;
|
|
37
|
-
asn: number;
|
|
38
|
-
/** What it is to us: `provider` for a transit, `rs` for an exchange's route servers, `peer` for a direct session. */
|
|
39
|
-
role: "provider" | "rs" | "peer";
|
|
40
|
-
/** Where it lands on our side: a VLAN, the router's interface on it, and the switch port it arrives on. A peer over a tunnel, or over an exchange's VLAN, has none. */
|
|
41
|
-
handoff?: {
|
|
42
|
-
/** The VLAN it arrives on. */
|
|
43
|
-
vlan: NamedVlan;
|
|
44
|
-
/** It sends its VLAN tagged: its switch port gets this native VLAN, which nothing else is in. Untagged when absent. */
|
|
45
|
-
native?: NamedVlan;
|
|
46
|
-
/** The router's VLAN interface on it. */
|
|
47
|
-
interface: string;
|
|
48
|
-
/** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
|
|
49
|
-
addresses: readonly (InterfaceAddress | { address: InterfaceAddress; peer: IP })[];
|
|
50
|
-
/**
|
|
51
|
-
* The switch port it arrives on. LLDP and spanning tree are off there unless `lldp` or `stp` says otherwise: an upstream's port faces someone else's network, which should neither learn ours nor join our spanning tree.
|
|
52
|
-
*/
|
|
53
|
-
port?: {
|
|
54
|
-
device: string;
|
|
55
|
-
port: string;
|
|
56
|
-
storm_control?: Port<string, string, string, string>["storm_control"];
|
|
57
|
-
/** LLDP on the port. Off when absent. */
|
|
58
|
-
lldp?: boolean;
|
|
59
|
-
/** Spanning tree on the port. Off when absent. */
|
|
60
|
-
stp?: boolean;
|
|
61
|
-
};
|
|
62
|
-
/** An exchange's LAN, for `circuit peeringdb`. */
|
|
63
|
-
exchange?: ExchangeLan;
|
|
64
|
-
};
|
|
65
|
-
/** A GRE tunnel to it, and the router's interface for it. */
|
|
66
|
-
tunnel?: { interface: string; local: IP; remote: IP; addresses: readonly InterfaceAddress[]; mtu?: number };
|
|
67
|
-
/** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
|
|
68
|
-
sessions: Readonly<Record<string, { address: IP; local_address?: IP }>>;
|
|
69
|
-
/** 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. */
|
|
70
|
-
group?: string;
|
|
71
|
-
/** The sessions are more than one hop away, from this address of ours. */
|
|
72
|
-
multihop?: { local_address: IP; families?: readonly Family[]; password?: Secret };
|
|
73
|
-
/** The routing table its sessions install the routes they learn in: `main`, or one of the router's `routing.tables`, for a transit that carries only one block's traffic out. The main table when absent; write `main` to say so. On RouterOS the sessions also announce from that table, so what they announce needs a route there too. */
|
|
74
|
-
table?: string;
|
|
75
|
-
/** The most prefixes a session may send, by its family: about twice what it sends. Over it, the session closes until it is cleared. */
|
|
76
|
-
limits?: { ipv4?: number; ipv6?: number };
|
|
77
|
-
/** What we take, as its policy. Nothing when absent. */
|
|
78
|
-
import?: UpstreamPolicy;
|
|
79
|
-
/** What we send it, as its policy. */
|
|
80
|
-
export: UpstreamPolicy;
|
|
81
|
-
/** Its VLAN is listed by `privateDrops()`, for an ACL that drops RFC 1918 sources arriving from it. Nothing else is done with it. */
|
|
82
|
-
drop_private?: true;
|
|
83
|
-
/** The communities it offers, as its NOC gives them: its blackhole community, and how to keep a route from one AS. `communityScheme().actions(upstream)` passes blackholes on with it, and `Upstreams.doNotAnnounceTo` uses the other. */
|
|
84
|
-
communities?: NeighborCommunities;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/** Upstreams by key. */
|
|
88
|
-
export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
|
|
89
|
-
|
|
90
|
-
/** What every upstream's policies share, written once: plain functions over each policy's rules. */
|
|
91
|
-
export type UpstreamOptions = {
|
|
92
|
-
/** Each import's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
|
|
93
|
-
mapImports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
|
|
94
|
-
/** Each export's rules as the device gets them, from the rules the upstream writes. The rules as written when absent. */
|
|
95
|
-
mapExports?: (rules: readonly PolicyRule<string, string>[], key: string, upstream: Upstream) => readonly PolicyRule<string, string>[];
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
type Values<T> = T[keyof T];
|
|
99
|
-
/** Each VLAN name the catalog writes. */
|
|
100
|
-
type VlanName<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { vlan: { name: infer N extends string } } } ? N : never }>;
|
|
101
|
-
/** Each native VLAN name. */
|
|
102
|
-
type NativeName<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { native: { name: infer N extends string } } } ? N : never }>;
|
|
103
|
-
/** Each router interface on a handoff. */
|
|
104
|
-
type HandoffInterface<C> = Values<{ [K in keyof C]: C[K] extends { handoff: { interface: infer N extends string } } ? N : never }>;
|
|
105
|
-
/** Each router interface for a tunnel. */
|
|
106
|
-
type TunnelInterface<C> = Values<{ [K in keyof C]: C[K] extends { tunnel: { interface: infer N extends string } } ? N : never }>;
|
|
107
|
-
/** Each policy name. */
|
|
108
|
-
type PolicyName<C> = Values<{
|
|
109
|
-
[K in keyof C]:
|
|
110
|
-
| (C[K] extends { import: { name: infer N extends string } } ? N : never)
|
|
111
|
-
| (C[K] extends { export: { name: infer N extends string } } ? N : never);
|
|
112
|
-
}>;
|
|
113
|
-
/** Each BGP group name. */
|
|
114
|
-
type GroupName<C> = Values<{ [K in keyof C]: C[K] extends { group: infer N extends string } ? N : never }>;
|
|
115
|
-
/** Each routing table name, other than main. */
|
|
116
|
-
type TableName<C> = Exclude<Values<{ [K in keyof C]: C[K] extends { table: infer N extends string } ? N : never }>, "main">;
|
|
117
|
-
|
|
118
|
-
/** A network's upstreams written once, and each device's part built from them. */
|
|
119
|
-
export class Upstreams {
|
|
120
|
-
/**
|
|
121
|
-
* A catalog of upstreams, by key, written once and checked: a key the shape doesn't have is refused by its name. It gives the catalog back as written, so its keys and entries can be read, and is what `define` takes.
|
|
122
|
-
*/
|
|
123
|
-
static catalog<const C extends UpstreamCatalog>(catalog: C & UpstreamCatalog): C {
|
|
124
|
-
return catalog;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Export rules asking an upstream not to pass our routes to these ASes, with its own `communities.do_not_announce_to`: one rule, `description`, adding one community per AS. Nothing for no AS. An upstream without `do_not_announce_to` is an error, since it can't be asked.
|
|
129
|
-
*/
|
|
130
|
-
static doNotAnnounceTo(upstream: Upstream, asns: readonly number[], description: string): PolicyRule<never, never>[] {
|
|
131
|
-
if (!asns.length) return [];
|
|
132
|
-
|
|
133
|
-
const community = upstream.communities?.do_not_announce_to;
|
|
134
|
-
if (!community)
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Upstreams: ${upstream.name ?? `AS${upstream.asn}`} has no communities.do_not_announce_to, so it can't be asked to keep our routes from AS${asns.join(", AS")}`
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const communities = asns.map(community);
|
|
140
|
-
const large = communities.filter((value) => value.split(":").length === 3) as LargeCommunity[];
|
|
141
|
-
const standard = communities.filter((value) => value.split(":").length === 2) as StandardCommunity[];
|
|
142
|
-
|
|
143
|
-
return [
|
|
144
|
-
{
|
|
145
|
-
description,
|
|
146
|
-
set: {
|
|
147
|
-
...(standard.length ? { add_communities: standard } : {}),
|
|
148
|
-
...(large.length ? { add_large_communities: large } : {}),
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Each upstream written once: its AS, where it lands, its sessions, limits and policies, and every name it puts on a device. Everything a device needs is built from it:
|
|
156
|
-
*
|
|
157
|
-
* - On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies()`, `tunnelRules()`, `addresses()`.
|
|
158
|
-
* - On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`.
|
|
159
|
-
* - For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`.
|
|
160
|
-
*
|
|
161
|
-
* An import or export is the rules the upstream writes, in order, passed through `mapImports` or `mapExports` when given: what every upstream shares, such as a call to a sanity policy or the community actions, goes there. Nothing else is added. Lists follow the catalog's order, where order counts on a device. A key the shape doesn't have is refused by the type, by its name.
|
|
162
|
-
*/
|
|
163
|
-
static define<const C extends UpstreamCatalog>(catalog: C & UpstreamCatalog, options: UpstreamOptions = {}) {
|
|
164
|
-
type V = VlanName<C>;
|
|
165
|
-
type T = NativeName<C>;
|
|
166
|
-
|
|
167
|
-
const all = Object.entries(catalog) as [string, Upstream][];
|
|
168
|
-
const nameOf = (key: string, upstream: Upstream) => upstream.name ?? key;
|
|
169
|
-
|
|
170
|
-
// Two upstreams under one name would replace each other on the device.
|
|
171
|
-
const unique = (what: string, names: [string, string | undefined][]) => {
|
|
172
|
-
const taken = new Map<string, string>();
|
|
173
|
-
for (const [key, name] of names) {
|
|
174
|
-
if (name === undefined) continue;
|
|
175
|
-
const other = taken.get(name);
|
|
176
|
-
if (other) throw new Error(`Upstreams: ${other} and ${key} both have the ${what} ${name}`);
|
|
177
|
-
taken.set(name, key);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
unique("VLAN", [
|
|
181
|
-
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.handoff?.vlan.name]),
|
|
182
|
-
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.handoff?.native?.name]),
|
|
183
|
-
]);
|
|
184
|
-
unique(
|
|
185
|
-
"interface",
|
|
186
|
-
all.map(([key, upstream]) => [key, upstream.handoff?.interface ?? upstream.tunnel?.interface])
|
|
187
|
-
);
|
|
188
|
-
unique("policy", [
|
|
189
|
-
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.import?.name]),
|
|
190
|
-
...all.map(([key, upstream]): [string, string | undefined] => [key, upstream.export.name]),
|
|
191
|
-
]);
|
|
192
|
-
unique(
|
|
193
|
-
"BGP group",
|
|
194
|
-
all.map(([key, upstream]) => [key, upstream.group])
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
/** Each landed upstream's VLAN and native, or with a device, those of the upstreams arriving on its ports. */
|
|
198
|
-
const vlans = (device?: string) => {
|
|
199
|
-
const result: Record<string, Vlan> = {};
|
|
200
|
-
|
|
201
|
-
for (const [key, upstream] of all) {
|
|
202
|
-
const handoff = upstream.handoff;
|
|
203
|
-
if (!handoff || (device !== undefined && handoff.port?.device !== device)) continue;
|
|
204
|
-
|
|
205
|
-
result[handoff.vlan.name] = { id: handoff.vlan.id, description: nameOf(key, upstream) };
|
|
206
|
-
if (handoff.native) {
|
|
207
|
-
result[handoff.native.name] = {
|
|
208
|
-
id: handoff.native.id,
|
|
209
|
-
description: `Native toward ${nameOf(key, upstream)}, nothing else in it`,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return result as { [K in V | T]: Vlan };
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const vlansOn = (device: string) =>
|
|
218
|
-
all.flatMap(([, upstream]) => (upstream.handoff?.port?.device === device ? [upstream.handoff.vlan.name as V] : []));
|
|
219
|
-
|
|
220
|
-
const ports = (device: string) => {
|
|
221
|
-
const result: Record<string, Port<V | T, never, never, never>> = {};
|
|
222
|
-
|
|
223
|
-
for (const [key, upstream] of all) {
|
|
224
|
-
const handoff = upstream.handoff;
|
|
225
|
-
if (!handoff?.port || handoff.port.device !== device) continue;
|
|
226
|
-
|
|
227
|
-
result[handoff.port.port] = {
|
|
228
|
-
description: nameOf(key, upstream),
|
|
229
|
-
...(handoff.native
|
|
230
|
-
? { trunk_vlans: [handoff.vlan.name as V], native_vlan: handoff.native.name as T }
|
|
231
|
-
: { access_vlan: handoff.vlan.name as V }),
|
|
232
|
-
lldp: handoff.port.lldp ?? false,
|
|
233
|
-
stp: handoff.port.stp ?? false,
|
|
234
|
-
...(handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return result;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const interfaces = () => {
|
|
242
|
-
const result: Record<string, VlanInterface<V, never> | Gre<never>> = {};
|
|
243
|
-
|
|
244
|
-
for (const [key, upstream] of all) {
|
|
245
|
-
const description = nameOf(key, upstream);
|
|
246
|
-
|
|
247
|
-
if (upstream.tunnel) {
|
|
248
|
-
const { interface: name, local, remote, addresses, mtu } = upstream.tunnel;
|
|
249
|
-
result[name] = { type: "gre", description, local, remote, addresses, ...(mtu ? { mtu } : {}) };
|
|
250
|
-
} else if (upstream.handoff) {
|
|
251
|
-
const { interface: name, vlan, addresses, exchange } = upstream.handoff;
|
|
252
|
-
result[name] = { type: "vlan", vlan: vlan.name as V, description, addresses, ...(exchange ? { exchange } : {}) };
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return result as { [K in HandoffInterface<C>]: VlanInterface<V, never> } & { [K in TunnelInterface<C>]: Gre<never> };
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
/** The router's interface toward each handoff, for NAT and lists like it. */
|
|
260
|
-
const uplinks = () => all.flatMap(([, upstream]) => (upstream.handoff ? [upstream.handoff.interface as HandoffInterface<C>] : []));
|
|
261
|
-
|
|
262
|
-
/** What a session, or the group it is in, takes from its upstream. */
|
|
263
|
-
const settings = (key: string, upstream: Upstream): NeighborSettings<PolicyName<C>, TableName<C>> => {
|
|
264
|
-
const multihop = upstream.multihop;
|
|
265
|
-
return {
|
|
266
|
-
remote_as: upstream.asn,
|
|
267
|
-
description: nameOf(key, upstream),
|
|
268
|
-
role: upstream.role,
|
|
269
|
-
...(upstream.import ? { import: upstream.import.name as PolicyName<C> } : {}),
|
|
270
|
-
export: upstream.export.name as PolicyName<C>,
|
|
271
|
-
...(upstream.table ? { table: upstream.table as TableName<C> | "main" } : {}),
|
|
272
|
-
...(multihop
|
|
273
|
-
? {
|
|
274
|
-
local_address: multihop.local_address,
|
|
275
|
-
multihop: true,
|
|
276
|
-
...(multihop.families ? { families: multihop.families } : {}),
|
|
277
|
-
...(multihop.password ? { password: multihop.password } : {}),
|
|
278
|
-
}
|
|
279
|
-
: {}),
|
|
280
|
-
};
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const groups = () =>
|
|
284
|
-
Object.fromEntries(
|
|
285
|
-
all.flatMap(([key, upstream]) => (upstream.group ? [[upstream.group, settings(key, upstream)]] : []))
|
|
286
|
-
) as Record<GroupName<C>, NeighborSettings<PolicyName<C>, TableName<C>>>;
|
|
287
|
-
|
|
288
|
-
const neighbors = () =>
|
|
289
|
-
Object.fromEntries(
|
|
290
|
-
all.flatMap(([key, upstream]) =>
|
|
291
|
-
Object.entries(upstream.sessions).map(([name, session]) => {
|
|
292
|
-
const limit = upstream.limits?.[Address.family(session.address)];
|
|
293
|
-
return [
|
|
294
|
-
name,
|
|
295
|
-
{
|
|
296
|
-
...(upstream.group ? { group: upstream.group } : settings(key, upstream)),
|
|
297
|
-
...session,
|
|
298
|
-
...(limit ? { max_prefixes: limit } : {}),
|
|
299
|
-
},
|
|
300
|
-
];
|
|
301
|
-
})
|
|
302
|
-
)
|
|
303
|
-
) as Record<string, Neighbor<PolicyName<C>, GroupName<C>, TableName<C>>>;
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Each upstream's import and export, by name. The policies and prefix sets their rules name are checked by `validate`, not by the editor, so the rules are typed to fit any device.
|
|
307
|
-
*/
|
|
308
|
-
const policies = () => {
|
|
309
|
-
const result: Record<string, readonly PolicyRule<string, string>[]> = {};
|
|
310
|
-
|
|
311
|
-
for (const [key, upstream] of all) {
|
|
312
|
-
if (upstream.import) {
|
|
313
|
-
const rules = upstream.import.rules;
|
|
314
|
-
result[upstream.import.name] = options.mapImports ? options.mapImports(rules, key, upstream) : rules;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const rules = upstream.export.rules;
|
|
318
|
-
result[upstream.export.name] = options.mapExports ? options.mapExports(rules, key, upstream) : rules;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return result as Record<PolicyName<C>, PolicyRule<never, never>[]>;
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
/** The VLANs straight off an upstream, natives included: `untrusted` for `Checks.trustBoundary`. */
|
|
325
|
-
const untrusted = () => [
|
|
326
|
-
...all.flatMap(([, upstream]) => (upstream.handoff ? [upstream.handoff.vlan.name as V] : [])),
|
|
327
|
-
...natives(),
|
|
328
|
-
];
|
|
329
|
-
|
|
330
|
-
const natives = () => all.flatMap(([, upstream]) => (upstream.handoff?.native ? [upstream.handoff.native.name as T] : []));
|
|
331
|
-
|
|
332
|
-
const privateDrops = () =>
|
|
333
|
-
all.flatMap(([, upstream]) => (upstream.handoff && upstream.drop_private ? [upstream.handoff.vlan.name as V] : []));
|
|
334
|
-
|
|
335
|
-
const tunnelRules = (): FilterRule<never, never>[] =>
|
|
336
|
-
all.flatMap(([key, upstream]) =>
|
|
337
|
-
upstream.tunnel
|
|
338
|
-
? [
|
|
339
|
-
{
|
|
340
|
-
description: `GRE from ${nameOf(key, upstream)}`,
|
|
341
|
-
match: {
|
|
342
|
-
protocol: "gre" as const,
|
|
343
|
-
src: `${upstream.tunnel.remote}/${Address.bits(Address.family(upstream.tunnel.remote))}` as Prefix,
|
|
344
|
-
},
|
|
345
|
-
action: "accept" as const,
|
|
346
|
-
},
|
|
347
|
-
]
|
|
348
|
-
: []
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
const addresses = () => all.flatMap(([, upstream]) => Object.values(upstream.sessions).map((session) => session.address));
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
vlans,
|
|
355
|
-
vlansOn,
|
|
356
|
-
ports,
|
|
357
|
-
interfaces,
|
|
358
|
-
uplinks,
|
|
359
|
-
groups,
|
|
360
|
-
neighbors,
|
|
361
|
-
policies,
|
|
362
|
-
untrusted,
|
|
363
|
-
natives,
|
|
364
|
-
privateDrops,
|
|
365
|
-
tunnelRules,
|
|
366
|
-
addresses,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export { defineDevice, defineNetwork, secret, secretFile } from "./core/define";
|
|
2
|
-
export { Utils } from "./core/utils";
|
|
3
|
-
export type { Check, Community, Finding, NetBoxDevice, Network } from "./core/define";
|
|
4
|
-
export type * from "./schema";
|
|
5
|
-
|
|
6
|
-
/** The platforms, their models and each model's ports, for types of your own: `PortName<"junos", "QFX5100-48S-6Q">` is each port of that model, such as `"10g-1"` or `"40g-6"`. */
|
|
7
|
-
export type { ModelName, PlatformName, PortName } from "./platforms/catalog";
|
|
8
|
-
|
|
9
|
-
/** Address helpers, for checks of your own. */
|
|
10
|
-
export { Address } from "./core/address";
|
|
11
|
-
|
|
12
|
-
/** Secrets, for tools of your own: a secret's value, text with every secret hidden, and how Circuit logs in to a device. */
|
|
13
|
-
export { Secrets } from "./core/secrets";
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
// Keeps LibreNMS in step with the config: every device Circuit manages is monitored there, under its name, polled with the SNMP credentials its config gives it, at the location it reports, and in a group for its site. A device LibreNMS holds that the config does not is reported, never deleted, and so is a group.
|
|
2
|
-
//
|
|
3
|
-
// Written against LibreNMS's API v0 as LibreNMS 26.10 serves it: a device's name is its `display_template`, which LibreNMS 26.5 and later take.
|
|
4
|
-
|
|
5
|
-
import type { Network } from "../core/define";
|
|
6
|
-
import { Secrets } from "../core/secrets";
|
|
7
|
-
import type { Device } from "../schema";
|
|
8
|
-
|
|
9
|
-
/** A device as LibreNMS lists it, with the fields Circuit compares. */
|
|
10
|
-
type Monitored = {
|
|
11
|
-
device_id: number | string;
|
|
12
|
-
hostname: string;
|
|
13
|
-
display?: string | null;
|
|
14
|
-
location?: string | null;
|
|
15
|
-
[field: string]: unknown;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/** A device LibreNMS monitors whose settings differ from its config. */
|
|
19
|
-
export type LibreNmsChange = {
|
|
20
|
-
device: Device;
|
|
21
|
-
/** LibreNMS's id for it. */
|
|
22
|
-
id: number;
|
|
23
|
-
/** What differs, in words: `name`, `SNMP credentials` or `location`. Never a secret's value. */
|
|
24
|
-
what: string[];
|
|
25
|
-
/** What is sent: LibreNMS's field names and their values. */
|
|
26
|
-
fields: Record<string, string>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/** A device group for one site, to make or whose devices change. */
|
|
30
|
-
export type LibreNmsGroup = {
|
|
31
|
-
/** The site's name, which is the group's. */
|
|
32
|
-
name: string;
|
|
33
|
-
/** LibreNMS has no group of this name yet. */
|
|
34
|
-
create: boolean;
|
|
35
|
-
/** Devices at the site that are not in the group. */
|
|
36
|
-
add: Device[];
|
|
37
|
-
/** Devices in the group that the config puts at another site. */
|
|
38
|
-
remove: Device[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/** What `librenms` would do. */
|
|
42
|
-
export type LibreNmsPlan = {
|
|
43
|
-
/** Devices to add. */
|
|
44
|
-
additions: Device[];
|
|
45
|
-
/** Devices to change. */
|
|
46
|
-
changes: LibreNmsChange[];
|
|
47
|
-
/** Site groups to make or change. Empty when the network has no `site`. */
|
|
48
|
-
groups: LibreNmsGroup[];
|
|
49
|
-
/** Devices that cannot be added: their config turns SNMP off. */
|
|
50
|
-
withoutSnmp: Device[];
|
|
51
|
-
/** Hostnames LibreNMS holds that no device in the config has. */
|
|
52
|
-
unknown: string[];
|
|
53
|
-
/** Site groups LibreNMS holds as dynamic groups, which Circuit cannot fill, so leaves alone. */
|
|
54
|
-
dynamicGroups: string[];
|
|
55
|
-
/** LibreNMS's id for each device it monitors, by hostname. */
|
|
56
|
-
ids: ReadonlyMap<string, number>;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export class LibreNms {
|
|
60
|
-
/** LibreNMS's names for the algorithms. */
|
|
61
|
-
private static readonly AUTH = { sha1: "SHA", sha256: "SHA-256" } as const;
|
|
62
|
-
private static readonly PRIVACY = { aes128: "AES", des: "DES" } as const;
|
|
63
|
-
|
|
64
|
-
/** Compare the config with what LibreNMS monitors. A device is known to LibreNMS by its management address. */
|
|
65
|
-
static async plan(network: Network): Promise<LibreNmsPlan> {
|
|
66
|
-
const settings = LibreNms.settingsOf(network);
|
|
67
|
-
|
|
68
|
-
const listed = await LibreNms.call<{ devices: Monitored[] }>(settings, "GET", "devices?type=all");
|
|
69
|
-
const monitored = new Map(listed.devices.map((device) => [device.hostname, device]));
|
|
70
|
-
const ids = new Map(listed.devices.map((device) => [device.hostname, Number(device.device_id)]));
|
|
71
|
-
|
|
72
|
-
// A device reached only through its console has no address to poll.
|
|
73
|
-
const polled = network.devices.filter((device) => device.connection.host);
|
|
74
|
-
const ours = new Set<string | undefined>(polled.map((device) => device.connection.host));
|
|
75
|
-
const missing = polled.filter((device) => !monitored.has(device.connection.host!));
|
|
76
|
-
const additions = missing.filter((device) => device.management?.snmp);
|
|
77
|
-
|
|
78
|
-
const changes = polled.flatMap((device) => {
|
|
79
|
-
const current = monitored.get(device.connection.host!);
|
|
80
|
-
return current ? LibreNms.changeOf(device, current) : [];
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const atSites = polled.filter((device) => monitored.has(device.connection.host!) || additions.includes(device));
|
|
84
|
-
const { groups, dynamicGroups } = await LibreNms.groupsOf(network, settings, atSites, ids);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
additions,
|
|
88
|
-
changes,
|
|
89
|
-
groups,
|
|
90
|
-
withoutSnmp: missing.filter((device) => !device.management?.snmp),
|
|
91
|
-
unknown: [...monitored.keys()].filter((hostname) => !ours.has(hostname)).sort(),
|
|
92
|
-
dynamicGroups,
|
|
93
|
-
ids,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** How many changes a plan holds. */
|
|
98
|
-
static count(plan: LibreNmsPlan): number {
|
|
99
|
-
return plan.additions.length + plan.changes.length + plan.groups.length;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Add each device, change each that differs, then make and fill the site groups. */
|
|
103
|
-
static async send(network: Network, plan: LibreNmsPlan): Promise<void> {
|
|
104
|
-
const settings = LibreNms.settingsOf(network);
|
|
105
|
-
const ids = new Map(plan.ids);
|
|
106
|
-
|
|
107
|
-
for (const device of plan.additions) {
|
|
108
|
-
const location = device.management?.snmp?.location;
|
|
109
|
-
const body = {
|
|
110
|
-
hostname: device.connection.host,
|
|
111
|
-
display_template: device.name,
|
|
112
|
-
...LibreNms.credentials(device),
|
|
113
|
-
...(location ? { location } : {}),
|
|
114
|
-
};
|
|
115
|
-
const answer = await LibreNms.call<{ message?: string; devices?: Monitored[] }>(settings, "POST", "devices", body);
|
|
116
|
-
|
|
117
|
-
const added = answer.devices?.[0];
|
|
118
|
-
if (added) ids.set(device.connection.host!, Number(added.device_id));
|
|
119
|
-
|
|
120
|
-
console.log(` ${device.name}: ${answer.message}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (const change of plan.changes) {
|
|
124
|
-
const fields = Object.keys(change.fields);
|
|
125
|
-
await LibreNms.call(settings, "PATCH", `devices/${change.id}`, {
|
|
126
|
-
field: fields,
|
|
127
|
-
data: fields.map((field) => change.fields[field]),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
console.log(` ${change.device.name}: changed its ${change.what.join(", ")}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// A device LibreNMS did not give an id for is left for the next run.
|
|
134
|
-
const idsOf = (devices: readonly Device[]) => devices.flatMap((device) => ids.get(device.connection.host!) ?? []);
|
|
135
|
-
|
|
136
|
-
for (const group of plan.groups) {
|
|
137
|
-
const path = `devicegroups/${encodeURIComponent(group.name)}`;
|
|
138
|
-
|
|
139
|
-
if (group.create) {
|
|
140
|
-
await LibreNms.call(settings, "POST", "devicegroups", {
|
|
141
|
-
name: group.name,
|
|
142
|
-
desc: `The devices at ${group.name}, kept by Circuit`,
|
|
143
|
-
type: "static",
|
|
144
|
-
devices: idsOf(group.add),
|
|
145
|
-
});
|
|
146
|
-
console.log(` made the group ${group.name}`);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (group.add.length) await LibreNms.call(settings, "POST", `${path}/devices`, { devices: idsOf(group.add) });
|
|
151
|
-
if (group.remove.length) await LibreNms.call(settings, "DELETE", `${path}/devices`, { devices: idsOf(group.remove) });
|
|
152
|
-
|
|
153
|
-
console.log(` changed the group ${group.name}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** What differs between a device's config and LibreNMS's record of it: its name, SNMP credentials and location. */
|
|
158
|
-
private static changeOf(device: Device, current: Monitored): LibreNmsChange[] {
|
|
159
|
-
const what: string[] = [];
|
|
160
|
-
let fields: Record<string, string> = {};
|
|
161
|
-
|
|
162
|
-
if (current.display !== device.name) {
|
|
163
|
-
what.push("name");
|
|
164
|
-
fields.display_template = device.name;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const credentials = LibreNms.credentials(device);
|
|
168
|
-
if (credentials && Object.entries(credentials).some(([field, value]) => String(current[field] ?? "") !== value)) {
|
|
169
|
-
what.push("SNMP credentials");
|
|
170
|
-
fields = { ...fields, ...credentials };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// LibreNMS reads the location from the device too; setting it now shows it before the next poll.
|
|
174
|
-
const location = device.management?.snmp?.location;
|
|
175
|
-
if (location && current.location !== location) {
|
|
176
|
-
what.push("location");
|
|
177
|
-
fields.location = location;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return what.length ? [{ device, id: Number(current.device_id), what, fields }] : [];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** A static group for each site, with the devices at it. Only groups named after a site are read or changed. */
|
|
184
|
-
private static async groupsOf(
|
|
185
|
-
network: Network,
|
|
186
|
-
settings: LibreNmsSettings,
|
|
187
|
-
devices: readonly Device[],
|
|
188
|
-
ids: ReadonlyMap<string, number>
|
|
189
|
-
): Promise<{ groups: LibreNmsGroup[]; dynamicGroups: string[] }> {
|
|
190
|
-
const siteOf = network.site;
|
|
191
|
-
if (!siteOf) return { groups: [], dynamicGroups: [] };
|
|
192
|
-
|
|
193
|
-
const existing =
|
|
194
|
-
(await LibreNms.call<{ groups?: { name: string; type: string }[] }>(settings, "GET", "devicegroups", undefined, true)).groups ??
|
|
195
|
-
[];
|
|
196
|
-
const sites = [...new Set(devices.map(siteOf))].sort();
|
|
197
|
-
|
|
198
|
-
const groups: LibreNmsGroup[] = [];
|
|
199
|
-
const dynamicGroups: string[] = [];
|
|
200
|
-
|
|
201
|
-
for (const site of sites) {
|
|
202
|
-
const atSite = devices.filter((device) => siteOf(device) === site);
|
|
203
|
-
const found = existing.find((group) => group.name === site);
|
|
204
|
-
|
|
205
|
-
if (!found) {
|
|
206
|
-
groups.push({ name: site, create: true, add: atSite, remove: [] });
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (found.type !== "static") {
|
|
211
|
-
dynamicGroups.push(site);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const listed = await LibreNms.call<{ devices?: { device_id: number | string }[] }>(
|
|
216
|
-
settings,
|
|
217
|
-
"GET",
|
|
218
|
-
`devicegroups/${encodeURIComponent(site)}`,
|
|
219
|
-
undefined,
|
|
220
|
-
true
|
|
221
|
-
);
|
|
222
|
-
const members = new Set((listed.devices ?? []).map((member) => Number(member.device_id)));
|
|
223
|
-
const isMember = (device: Device) => members.has(ids.get(device.connection.host!) ?? -1);
|
|
224
|
-
|
|
225
|
-
const add = atSite.filter((device) => !isMember(device));
|
|
226
|
-
const remove = devices.filter((device) => siteOf(device) !== site && isMember(device));
|
|
227
|
-
if (add.length || remove.length) groups.push({ name: site, create: false, add, remove });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return { groups, dynamicGroups };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
private static settingsOf(network: Network): LibreNmsSettings {
|
|
234
|
-
if (!network.librenms) throw new Error("the network has no librenms settings");
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
base: `${network.librenms.url.replace(/\/+$/, "")}/api/v0`,
|
|
238
|
-
// The header every version of API v0 accepts.
|
|
239
|
-
headers: { "X-Auth-Token": Secrets.reveal(network.librenms.api_token), "Content-Type": "application/json" },
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** One request. LibreNMS answers a list with nothing in it with 404, so `emptyOn404` reads that as an empty answer. */
|
|
244
|
-
private static async call<T>(
|
|
245
|
-
settings: LibreNmsSettings,
|
|
246
|
-
method: "GET" | "POST" | "PATCH" | "DELETE",
|
|
247
|
-
path: string,
|
|
248
|
-
body?: unknown,
|
|
249
|
-
emptyOn404 = false
|
|
250
|
-
): Promise<T> {
|
|
251
|
-
const response = await fetch(`${settings.base}/${path}`, {
|
|
252
|
-
method,
|
|
253
|
-
headers: settings.headers,
|
|
254
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
255
|
-
signal: AbortSignal.timeout(60_000),
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
if (response.status === 404 && emptyOn404) return {} as T;
|
|
259
|
-
|
|
260
|
-
const answer = (await response.json().catch(() => ({}))) as T & { status?: string; message?: string };
|
|
261
|
-
if (!response.ok || answer.status === "error") {
|
|
262
|
-
throw new Error(`LibreNMS refused ${method} ${path}: ${answer.message ?? response.status}`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return answer;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** How LibreNMS polls a device: its first SNMP version 3 user, or else its version 2c community. */
|
|
269
|
-
private static credentials(device: Device): Record<string, string> | undefined {
|
|
270
|
-
const snmp = device.management?.snmp;
|
|
271
|
-
const [name, user] = Object.entries(snmp?.users ?? {})[0] ?? [];
|
|
272
|
-
|
|
273
|
-
if (name && user) {
|
|
274
|
-
return {
|
|
275
|
-
snmpver: "v3",
|
|
276
|
-
authlevel: "authPriv",
|
|
277
|
-
authname: name,
|
|
278
|
-
authpass: Secrets.reveal(user.auth_password),
|
|
279
|
-
authalgo: LibreNms.AUTH[user.auth],
|
|
280
|
-
cryptopass: Secrets.reveal(user.privacy_password),
|
|
281
|
-
cryptoalgo: LibreNms.PRIVACY[user.privacy],
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (snmp?.community) return { snmpver: "v2c", community: Secrets.reveal(snmp.community) };
|
|
286
|
-
return undefined;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Where LibreNMS's API is, and the headers every request sends. */
|
|
291
|
-
type LibreNmsSettings = { base: string; headers: Record<string, string> };
|