@takodotid/circuit 0.34.3 → 0.36.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 +14 -15
- 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 +11345 -9024
- 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 +18 -12
- 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 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- 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
package/src/studio/data.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { type AddressReport
|
|
9
|
-
import { type Annotated, type Holder
|
|
10
|
-
import {
|
|
6
|
+
import { Catalog } from "../platforms/catalog";
|
|
7
|
+
import { Address } from "../core/address";
|
|
8
|
+
import { AddressSpace, type AddressReport } from "../core/address-space";
|
|
9
|
+
import { Topology, type Annotated, type Holder } from "../core/topology";
|
|
10
|
+
import { SchemaDocs } from "../core/schema-docs";
|
|
11
11
|
import type { Device } from "../schema";
|
|
12
12
|
|
|
13
13
|
/** One BGP session, with its group's settings filled in, and what is at its far end. */
|
|
@@ -68,188 +68,192 @@ export type StudioData = {
|
|
|
68
68
|
docs: Record<string, TypeDocs>;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const bgp = device.routing?.bgp;
|
|
74
|
-
|
|
75
|
-
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]): Session & { deviceObject: Device } => {
|
|
76
|
-
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
77
|
-
const settings = { ...group, ...neighbor };
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
deviceObject: device,
|
|
81
|
-
device: device.name,
|
|
82
|
-
name,
|
|
83
|
-
address: neighbor.address,
|
|
84
|
-
group: neighbor.group,
|
|
85
|
-
remoteAs: settings.remote_as,
|
|
86
|
-
role: settings.role,
|
|
87
|
-
import: settings.import,
|
|
88
|
-
export: settings.export,
|
|
89
|
-
description: settings.description,
|
|
90
|
-
};
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const ownerOf = (address: string) => owners.get(keyOf(address) ?? "")?.device;
|
|
71
|
+
/** Where a device is kept, relative to the config: the file that declares it, and its snapshot. Either is absent when it cannot be found. */
|
|
72
|
+
export type DeviceFiles = { source?: string; snapshot?: string };
|
|
95
73
|
|
|
96
|
-
|
|
97
|
-
|
|
74
|
+
/** How Studio learns what a device would run and what it ran, from the caller, which knows the adapters and the state directory. */
|
|
75
|
+
export type DeviceTexts = {
|
|
76
|
+
/** What the device would run, as `circuit build` prints it. */
|
|
77
|
+
render: (device: Device) => string;
|
|
78
|
+
/** Its last snapshot, or undefined when it has none. */
|
|
79
|
+
snapshot: (device: Device) => string | undefined;
|
|
80
|
+
/** Its file and its snapshot's file. */
|
|
81
|
+
files: (device: Device) => DeviceFiles;
|
|
82
|
+
};
|
|
98
83
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
return { ...session, far: far.name, ...(back ? {} : { oneSided: true as const }) };
|
|
84
|
+
export class StudioModel {
|
|
85
|
+
/** Everything Studio shows. `graph` comes ready from the caller, with each device compared with its snapshot. */
|
|
86
|
+
static build(devices: readonly Device[], graph: Annotated, texts: DeviceTexts): StudioData {
|
|
87
|
+
const owners = new Map<string, Holder>();
|
|
88
|
+
for (const holder of Topology.holders(devices)) {
|
|
89
|
+
const key = Topology.keyOf(Address.host(holder.address))!;
|
|
90
|
+
if (!owners.has(key)) owners.set(key, holder);
|
|
108
91
|
}
|
|
109
92
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
93
|
+
const attempt = <T>(work: () => T, otherwise: (error: Error) => T) => {
|
|
94
|
+
try {
|
|
95
|
+
return work();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return otherwise(error as Error);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
113
100
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
...(farPort?.lag ? { farLag: farPort.lag } : {}),
|
|
148
|
-
...(port.description ? { description: port.description } : {}),
|
|
149
|
-
vlans: vlansOf(device, portName),
|
|
150
|
-
...(linksBack ? {} : { oneSided: true as const }),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
101
|
+
return {
|
|
102
|
+
graph,
|
|
103
|
+
devices,
|
|
104
|
+
ports: Object.fromEntries(
|
|
105
|
+
devices.map((device) => [
|
|
106
|
+
device.name,
|
|
107
|
+
attempt(
|
|
108
|
+
() => Catalog.ports(device.platform, device.model),
|
|
109
|
+
() => []
|
|
110
|
+
),
|
|
111
|
+
])
|
|
112
|
+
),
|
|
113
|
+
rendered: Object.fromEntries(
|
|
114
|
+
devices.map((device) => [
|
|
115
|
+
device.name,
|
|
116
|
+
attempt(
|
|
117
|
+
() => texts.render(device),
|
|
118
|
+
(error) => `Cannot be rendered: ${error.message}`
|
|
119
|
+
),
|
|
120
|
+
])
|
|
121
|
+
),
|
|
122
|
+
snapshots: Object.fromEntries(
|
|
123
|
+
devices.flatMap((device) => {
|
|
124
|
+
const text = texts.snapshot(device);
|
|
125
|
+
return text === undefined ? [] : [[device.name, text]];
|
|
126
|
+
})
|
|
127
|
+
),
|
|
128
|
+
files: Object.fromEntries(devices.map((device) => [device.name, texts.files(device)])),
|
|
129
|
+
addresses: AddressSpace.report(devices),
|
|
130
|
+
sessions: StudioModel.sessionsOf(devices, owners),
|
|
131
|
+
cables: StudioModel.cablesOf(devices),
|
|
132
|
+
docs: StudioModel.schemaDocs(),
|
|
133
|
+
};
|
|
153
134
|
}
|
|
154
135
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
/** The directory Circuit is installed in: the nearest one up with its package.json. Its sources ship with it, in src: the schema, and the files of Studio's page. */
|
|
159
|
-
export function packageRoot(): string | undefined {
|
|
160
|
-
let directory = dirname(fileURLToPath(import.meta.url));
|
|
136
|
+
/** The directory Circuit is installed in: the nearest one up with its package.json. Its sources ship with it, in src: the schema, and the files of Studio's page. */
|
|
137
|
+
static packageRoot(): string | undefined {
|
|
138
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
161
139
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
140
|
+
while (true) {
|
|
141
|
+
const manifest = join(directory, "package.json");
|
|
142
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@takodotid/circuit") return directory;
|
|
165
143
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
144
|
+
const parent = dirname(directory);
|
|
145
|
+
if (parent === directory) return undefined;
|
|
146
|
+
directory = parent;
|
|
147
|
+
}
|
|
169
148
|
}
|
|
170
|
-
}
|
|
171
149
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
150
|
+
private static sessionsOf(devices: readonly Device[], owners: Map<string, Holder>): Session[] {
|
|
151
|
+
const all = devices.flatMap((device) => {
|
|
152
|
+
const bgp = device.routing?.bgp;
|
|
153
|
+
|
|
154
|
+
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]): Session & { deviceObject: Device } => {
|
|
155
|
+
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
156
|
+
const settings = { ...group, ...neighbor };
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
deviceObject: device,
|
|
160
|
+
device: device.name,
|
|
161
|
+
name,
|
|
162
|
+
address: neighbor.address,
|
|
163
|
+
group: neighbor.group,
|
|
164
|
+
remoteAs: settings.remote_as,
|
|
165
|
+
role: settings.role,
|
|
166
|
+
import: settings.import,
|
|
167
|
+
export: settings.export,
|
|
168
|
+
description: settings.description,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
});
|
|
176
172
|
|
|
177
|
-
|
|
178
|
-
const docs: Record<string, TypeDocs> = {};
|
|
173
|
+
const ownerOf = (address: string) => owners.get(Topology.keyOf(address) ?? "")?.device;
|
|
179
174
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
175
|
+
return all.map(({ deviceObject, ...session }) => {
|
|
176
|
+
const far = ownerOf(session.address);
|
|
177
|
+
|
|
178
|
+
if (far && far !== deviceObject) {
|
|
179
|
+
// The session back is one of the same family: a v4 session back does not answer a v6 session.
|
|
180
|
+
const back = all.some(
|
|
181
|
+
(other) =>
|
|
182
|
+
other.deviceObject === far &&
|
|
183
|
+
ownerOf(other.address) === deviceObject &&
|
|
184
|
+
Address.family(other.address) === Address.family(session.address)
|
|
185
|
+
);
|
|
186
|
+
return { ...session, far: far.name, ...(back ? {} : { oneSided: true as const }) };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { ...session, ...(session.remoteAs ? { far: `AS${session.remoteAs}` } : {}) };
|
|
190
|
+
});
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
private static cablesOf(devices: readonly Device[]): Cable[] {
|
|
194
|
+
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
195
|
+
const drawn = new Set<string>();
|
|
196
|
+
const cables: Cable[] = [];
|
|
197
|
+
|
|
198
|
+
const vlansOf = (device: Device, portName: string) => {
|
|
199
|
+
const port = device.ports?.[portName];
|
|
200
|
+
const lag = port?.lag ? device.interfaces?.[port.lag] : undefined;
|
|
201
|
+
const carrier = lag?.type === "lag" ? lag : port;
|
|
202
|
+
const names = [carrier?.access_vlan, carrier?.native_vlan, ...(carrier?.trunk_vlans ?? [])].filter(
|
|
203
|
+
(name) => name !== undefined
|
|
204
|
+
);
|
|
192
205
|
|
|
193
|
-
|
|
194
|
-
|
|
206
|
+
return [...new Set(names)].map((name) => `${name} (${device.vlans?.[name]?.id ?? "?"})`);
|
|
207
|
+
};
|
|
195
208
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
};
|
|
209
|
+
for (const device of devices) {
|
|
210
|
+
for (const [portName, port] of Object.entries(device.ports ?? {})) {
|
|
211
|
+
const link = port?.link;
|
|
212
|
+
if (!port || !link) continue;
|
|
213
|
+
|
|
214
|
+
const here = `${device.name} ${portName}`;
|
|
215
|
+
const there = `${link.device} ${link.port}`;
|
|
216
|
+
if (drawn.has(`${there}|${here}`)) continue;
|
|
217
|
+
drawn.add(`${here}|${there}`);
|
|
218
|
+
|
|
219
|
+
const farPort = byName.get(link.device)?.ports?.[link.port];
|
|
220
|
+
const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
|
|
221
|
+
|
|
222
|
+
cables.push({
|
|
223
|
+
device: device.name,
|
|
224
|
+
port: portName,
|
|
225
|
+
farDevice: link.device,
|
|
226
|
+
farPort: link.port,
|
|
227
|
+
...(port.lag ? { lag: port.lag } : {}),
|
|
228
|
+
...(farPort?.lag ? { farLag: farPort.lag } : {}),
|
|
229
|
+
...(port.description ? { description: port.description } : {}),
|
|
230
|
+
vlans: vlansOf(device, portName),
|
|
231
|
+
...(linksBack ? {} : { oneSided: true as const }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
205
235
|
|
|
206
|
-
|
|
207
|
-
export function studioData(devices: readonly Device[], graph: Annotated, texts: DeviceTexts): StudioData {
|
|
208
|
-
const owners = new Map<string, Holder>();
|
|
209
|
-
for (const holder of holdersOf(devices)) {
|
|
210
|
-
const key = keyOf(host(holder.address))!;
|
|
211
|
-
if (!owners.has(key)) owners.set(key, holder);
|
|
236
|
+
return cables;
|
|
212
237
|
}
|
|
213
238
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
attempt(
|
|
229
|
-
() => portsOf(device.platform, device.model),
|
|
230
|
-
() => []
|
|
231
|
-
),
|
|
232
|
-
])
|
|
233
|
-
),
|
|
234
|
-
rendered: Object.fromEntries(
|
|
235
|
-
devices.map((device) => [
|
|
236
|
-
device.name,
|
|
237
|
-
attempt(
|
|
238
|
-
() => texts.render(device),
|
|
239
|
-
(error) => `Cannot be rendered: ${error.message}`
|
|
239
|
+
/** The schema's descriptions, or none when its sources cannot be found. */
|
|
240
|
+
private static schemaDocs(): Record<string, TypeDocs> {
|
|
241
|
+
const root = StudioModel.packageRoot();
|
|
242
|
+
if (!root) return {};
|
|
243
|
+
|
|
244
|
+
const sources = SchemaDocs.FILES.map(([file]) => readFileSync(join(root, "src", "schema", file), "utf8"));
|
|
245
|
+
const docs: Record<string, TypeDocs> = {};
|
|
246
|
+
|
|
247
|
+
for (const type of SchemaDocs.read(sources).flat()) {
|
|
248
|
+
docs[type.name] = {
|
|
249
|
+
description: type.description,
|
|
250
|
+
includes: (type.includes ?? []).map((name) => name.replace(/<.*$/, "")),
|
|
251
|
+
fields: Object.fromEntries(
|
|
252
|
+
type.fields.map((field) => [field.path.replace(/\?$/, ""), { type: field.type, description: field.description }])
|
|
240
253
|
),
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return text === undefined ? [] : [[device.name, text]];
|
|
247
|
-
})
|
|
248
|
-
),
|
|
249
|
-
files: Object.fromEntries(devices.map((device) => [device.name, texts.files(device)])),
|
|
250
|
-
addresses: addressesOf(devices),
|
|
251
|
-
sessions: sessionsOf(devices, owners),
|
|
252
|
-
cables: cablesOf(devices),
|
|
253
|
-
docs: schemaDocs(),
|
|
254
|
-
};
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return docs;
|
|
258
|
+
}
|
|
255
259
|
}
|
package/src/studio/git.ts
CHANGED
|
@@ -14,106 +14,111 @@ export type Commit = {
|
|
|
14
14
|
subject: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** Run git in a directory, and give what it printed, or fail with what it said. */
|
|
21
|
-
function git(cwd: string, args: string[]): Promise<string> {
|
|
22
|
-
return new Promise((done, fail) => {
|
|
23
|
-
execFile("git", args, { cwd, env: environment(), maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
24
|
-
if (error) fail(new Error((stderr || error.message).trim()));
|
|
25
|
-
else done(stdout);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
17
|
+
export class Git {
|
|
18
|
+
private static readonly FORMAT = "--format=%H%x1f%h%x1f%an%x1f%aI%x1f%s";
|
|
29
19
|
|
|
30
|
-
|
|
20
|
+
/** The environment without git's own variables, such as GIT_DIR in a hook, which would point git at another repository than the directory's. */
|
|
21
|
+
private static environment(): Record<string, string | undefined> {
|
|
22
|
+
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_")));
|
|
23
|
+
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
/** Run git in a directory, and give what it printed, or fail with what it said. */
|
|
26
|
+
private static run(cwd: string, args: string[]): Promise<string> {
|
|
27
|
+
return new Promise((done, fail) => {
|
|
28
|
+
execFile("git", args, { cwd, env: Git.environment(), maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
29
|
+
if (error) fail(new Error((stderr || error.message).trim()));
|
|
30
|
+
else done(stdout);
|
|
31
|
+
});
|
|
39
32
|
});
|
|
40
|
-
}
|
|
33
|
+
}
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
private static commitsOf(text: string): Commit[] {
|
|
36
|
+
return text
|
|
37
|
+
.split("\n")
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((line) => {
|
|
40
|
+
const [commit = "", short = "", author = "", date = "", subject = ""] = line.split("\x1f");
|
|
41
|
+
return { commit, short, author, date, subject };
|
|
42
|
+
});
|
|
48
43
|
}
|
|
49
|
-
}
|
|
50
44
|
|
|
51
|
-
/** The
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
45
|
+
/** The top of the git repository the directory is in, or undefined when it is in none, or git is missing. */
|
|
46
|
+
static async top(directory: string): Promise<string | undefined> {
|
|
47
|
+
try {
|
|
48
|
+
return (await Git.run(directory, ["rev-parse", "--show-toplevel"])).trim();
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
56
53
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
"--count=100",
|
|
63
|
-
"--sort=-committerdate",
|
|
64
|
-
"--format=%(refname:short)",
|
|
65
|
-
"refs/heads",
|
|
66
|
-
"refs/remotes",
|
|
67
|
-
"refs/tags",
|
|
68
|
-
])
|
|
69
|
-
)
|
|
70
|
-
.split("\n")
|
|
71
|
-
.filter((name) => name && !name.endsWith("/HEAD"));
|
|
72
|
-
|
|
73
|
-
return { names, commits: commitsOf(await git(directory, ["log", "-n30", FORMAT])) };
|
|
74
|
-
}
|
|
54
|
+
/** The latest commits that changed any of these files, relative to the directory, newest first. */
|
|
55
|
+
static async history(directory: string, files: string[], limit = 50): Promise<Commit[]> {
|
|
56
|
+
if (!files.length) return [];
|
|
57
|
+
return Git.commitsOf(await Git.run(directory, ["log", `-n${limit}`, Git.FORMAT, "--", ...files]));
|
|
58
|
+
}
|
|
75
59
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
/** Branches and tags to compare with, and the latest commits. */
|
|
61
|
+
static async references(directory: string): Promise<{ names: string[]; commits: Commit[] }> {
|
|
62
|
+
const names = (
|
|
63
|
+
await Git.run(directory, [
|
|
64
|
+
"for-each-ref",
|
|
65
|
+
"--count=100",
|
|
66
|
+
"--sort=-committerdate",
|
|
67
|
+
"--format=%(refname:short)",
|
|
68
|
+
"refs/heads",
|
|
69
|
+
"refs/remotes",
|
|
70
|
+
"refs/tags",
|
|
71
|
+
])
|
|
72
|
+
)
|
|
73
|
+
.split("\n")
|
|
74
|
+
.filter((name) => name && !name.endsWith("/HEAD"));
|
|
75
|
+
|
|
76
|
+
return { names, commits: Git.commitsOf(await Git.run(directory, ["log", "-n30", Git.FORMAT])) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A branch, tag or commit, as the commit it names, or undefined when it names none. Only the characters a git name can hold are accepted. */
|
|
80
|
+
static async commitOf(directory: string, name: string): Promise<Commit | undefined> {
|
|
81
|
+
if (!/^[A-Za-z0-9][\w./@^~-]*$/.test(name)) return undefined;
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
try {
|
|
84
|
+
const [commit] = Git.commitsOf(await Git.run(directory, ["log", "-n1", Git.FORMAT, `${name}^{commit}`, "--"]));
|
|
85
|
+
return commit;
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
85
89
|
}
|
|
86
|
-
}
|
|
87
90
|
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
): Promise<string> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Read the network as it was at a commit: check that commit out beside the project, in `.circuit/cache/`, which git ignores, gather it there with `gather`, then remove the checkout. The config is the file of that name in the same directory, `configDirectory`.
|
|
93
|
+
*
|
|
94
|
+
* The checkout sits inside the project, so its imports of Circuit resolve to the version installed now.
|
|
95
|
+
*/
|
|
96
|
+
static async networkAt(
|
|
97
|
+
configDirectory: string,
|
|
98
|
+
configPath: string,
|
|
99
|
+
commit: string,
|
|
100
|
+
gather: (configPath: string) => Promise<string>
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
const top = await Git.top(configDirectory);
|
|
103
|
+
if (!top) throw new Error("the config is not in a git repository");
|
|
104
|
+
|
|
105
|
+
const cache = join(configDirectory, ".circuit", "cache");
|
|
106
|
+
mkdirSync(cache, { recursive: true });
|
|
107
|
+
writeFileSync(join(cache, ".gitignore"), "# Written by Circuit Studio: checkouts of other commits, removed once read.\n*\n");
|
|
108
|
+
|
|
109
|
+
// A checkout left behind by a run that stopped is forgotten first.
|
|
110
|
+
await Git.run(top, ["worktree", "prune"]);
|
|
111
|
+
|
|
112
|
+
const checkout = join(cache, commit.slice(0, 12));
|
|
113
|
+
// The checkout sits deep inside the project, so a long path in the repository passes the 260 characters Windows allows unless git is told otherwise.
|
|
114
|
+
await Git.run(top, ["-c", "core.longpaths=true", "worktree", "add", "--detach", "--force", checkout, commit]);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Where the config sits inside the repository, as git says: comparing paths would trip on two spellings of one directory, such as Windows' short names.
|
|
118
|
+
const prefix = (await Git.run(configDirectory, ["rev-parse", "--show-prefix"])).trim();
|
|
119
|
+
return await gather(join(checkout, prefix, basename(configPath)));
|
|
120
|
+
} finally {
|
|
121
|
+
await Git.run(top, ["-c", "core.longpaths=true", "worktree", "remove", "--force", checkout]).catch(() => {});
|
|
122
|
+
}
|
|
118
123
|
}
|
|
119
124
|
}
|