dsh-ensp 0.1.3 → 0.1.7
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 +86 -5
- package/docs/SCENARIOS.md +1410 -2
- package/docs/UI-ANCHORS.md +52 -0
- package/docs/plugin-detail.en.md +3 -2
- package/docs/plugin-detail.zh.md +3 -2
- package/package.json +5 -15
- package/scripts/gen-scenarios-doc.mjs +173 -0
- package/src/cli/ensp.mjs +462 -410
- package/src/core/build.mjs +218 -299
- package/src/core/compat.mjs +226 -0
- package/src/core/index.mjs +44 -4
- package/src/core/labs.mjs +1019 -0
- package/src/core/launcher.mjs +884 -465
- package/src/core/models.mjs +212 -0
- package/src/core/presets.mjs +583 -0
- package/src/core/provision.mjs +81 -0
- package/src/core/topology.mjs +259 -315
- package/src/core/version.mjs +22 -0
- package/src/dsh/index.mjs +470 -232
- package/src/mcp/server.mjs +438 -406
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoritative interface table for every eNSP model this project supports.
|
|
3
|
+
*
|
|
4
|
+
* This table used to be duplicated: `build.mjs` carried one copy for the topology
|
|
5
|
+
* builder and `topology.mjs` carried another for the serialiser and index maths.
|
|
6
|
+
* The two drifted, and the drift was not cosmetic — the serialiser's copy never
|
|
7
|
+
* learned that firewalls number their ports as `GigabitEthernet1/0/x`, so
|
|
8
|
+
* `ensp_build_topology` refused every `GigabitEthernet1/0/0` a caller passed in.
|
|
9
|
+
* A single table removes that whole class of bug.
|
|
10
|
+
*
|
|
11
|
+
* Group layout is **not** cosmetic either. eNSP rebuilds a device's port list from
|
|
12
|
+
* the `<interface>` elements verbatim; declaring one `count="3"` group on an
|
|
13
|
+
* AR2220 does not expand to GE0/0/0 + GE0/0/1..2, and the device silently fails to
|
|
14
|
+
* render (empty canvas, no error). The groupings below were counted across all 40
|
|
15
|
+
* bundled sample topologies — see `docs/ENSP-FORMAT.md`.
|
|
16
|
+
*
|
|
17
|
+
* Group tuple: `[sztype, interfacename, count, firstNumber, cliPrefix]`.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-ensp/models
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const MODEL_SPECS = {
|
|
23
|
+
// 24 GE ports in one group, numbering from 1.
|
|
24
|
+
S5700: {
|
|
25
|
+
groups: [['Ethernet', 'GE', 24, 1]],
|
|
26
|
+
base: 1,
|
|
27
|
+
vrp: 'GigabitEthernet0/0/',
|
|
28
|
+
console: true,
|
|
29
|
+
},
|
|
30
|
+
// 22 fast ports (Ethernet0/0/1..22) then 2 gigabit (GE0/0/1..2).
|
|
31
|
+
S3700: {
|
|
32
|
+
groups: [['Ethernet', 'Ethernet', 22, 1], ['Ethernet', 'GE', 2, 1]],
|
|
33
|
+
base: 1,
|
|
34
|
+
vrp: 'Ethernet0/0/',
|
|
35
|
+
console: true,
|
|
36
|
+
},
|
|
37
|
+
// GE0/0/0, then GE0/0/1..2 — the split is required, and the second group starts
|
|
38
|
+
// numbering at 1, not 0.
|
|
39
|
+
AR2220: {
|
|
40
|
+
groups: [['Ethernet', 'GE', 1, 0], ['Ethernet', 'GE', 2, 1]],
|
|
41
|
+
base: 0,
|
|
42
|
+
vrp: 'GigabitEthernet0/0/',
|
|
43
|
+
console: true,
|
|
44
|
+
},
|
|
45
|
+
AR2240: {
|
|
46
|
+
groups: [['Ethernet', 'GE', 4, 0]],
|
|
47
|
+
base: 0,
|
|
48
|
+
vrp: 'GigabitEthernet0/0/',
|
|
49
|
+
console: true,
|
|
50
|
+
},
|
|
51
|
+
// GE0/0/0..1 then Ethernet0/0/0..7 — two CLI families in one slot.
|
|
52
|
+
AR1220: {
|
|
53
|
+
groups: [['Ethernet', 'GE', 2, 0], ['Ethernet', 'Ethernet', 8, 0]],
|
|
54
|
+
base: 0,
|
|
55
|
+
vrp: 'GigabitEthernet0/0/',
|
|
56
|
+
console: true,
|
|
57
|
+
},
|
|
58
|
+
// Firewalls number their CLI ports from 1/0/0 while eNSP stores them from index
|
|
59
|
+
// 0, so the CLI prefix is stated explicitly. Guessing it from the eNSP prefix
|
|
60
|
+
// produces `GigabitEthernet0/0/x` and rejects every real firewall interface.
|
|
61
|
+
USG5500: {
|
|
62
|
+
groups: [['Ethernet', 'GE', 9, 0, 'GigabitEthernet1/0/']],
|
|
63
|
+
base: 0,
|
|
64
|
+
vrp: 'GigabitEthernet1/0/',
|
|
65
|
+
console: true,
|
|
66
|
+
},
|
|
67
|
+
USG6000V: {
|
|
68
|
+
groups: [['Ethernet', 'GE', 9, 0, 'GigabitEthernet1/0/']],
|
|
69
|
+
base: 0,
|
|
70
|
+
vrp: 'GigabitEthernet1/0/',
|
|
71
|
+
console: true,
|
|
72
|
+
},
|
|
73
|
+
AC6005: {
|
|
74
|
+
groups: [['Ethernet', 'GE', 8, 1]],
|
|
75
|
+
base: 1,
|
|
76
|
+
vrp: 'GigabitEthernet0/0/',
|
|
77
|
+
console: true,
|
|
78
|
+
},
|
|
79
|
+
// PC and Server declare `interfacename="GE"` in the file but expose
|
|
80
|
+
// `Ethernet0/0/1` on the CLI. eNSP's file vocabulary and VRP's differ here.
|
|
81
|
+
PC: {
|
|
82
|
+
groups: [['Ethernet', 'GE', 1, 1, 'Ethernet0/0/']],
|
|
83
|
+
base: 1,
|
|
84
|
+
vrp: 'Ethernet0/0/',
|
|
85
|
+
console: false,
|
|
86
|
+
},
|
|
87
|
+
Server: {
|
|
88
|
+
groups: [['Ethernet', 'GE', 1, 1, 'Ethernet0/0/']],
|
|
89
|
+
base: 1,
|
|
90
|
+
vrp: 'Ethernet0/0/',
|
|
91
|
+
console: false,
|
|
92
|
+
},
|
|
93
|
+
Cloud: {
|
|
94
|
+
groups: [['Ethernet', 'Ethernet', 3, 0]],
|
|
95
|
+
base: 0,
|
|
96
|
+
vrp: 'Ethernet0/0/',
|
|
97
|
+
console: false,
|
|
98
|
+
},
|
|
99
|
+
// Wireless endpoints: no console, one uplink.
|
|
100
|
+
AP6050: {
|
|
101
|
+
groups: [['Ethernet', 'GE', 2, 1]],
|
|
102
|
+
base: 1,
|
|
103
|
+
vrp: 'GigabitEthernet0/0/',
|
|
104
|
+
console: true,
|
|
105
|
+
},
|
|
106
|
+
STA: { groups: [], base: 0, vrp: '', console: false },
|
|
107
|
+
Laptop: {
|
|
108
|
+
groups: [['Ethernet', 'Ethernet', 1, 1, 'Ethernet0/0/']],
|
|
109
|
+
base: 1,
|
|
110
|
+
vrp: 'Ethernet0/0/',
|
|
111
|
+
console: false,
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Every model name this module knows. */
|
|
116
|
+
export const MODEL_NAMES = Object.keys(MODEL_SPECS)
|
|
117
|
+
|
|
118
|
+
/** Canvas geometry constants, taken from eNSP's own writer. */
|
|
119
|
+
export const GEOMETRY = { editLeftOffset: 27, editTopOffset: 54 }
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Icon bounding box, measured from every cable anchor in eNSP's own samples.
|
|
123
|
+
*
|
|
124
|
+
* The box is **not** centred on (cx, cy): it runs from `cx - 16.2` to
|
|
125
|
+
* `cx + 70.2`, so its centre is `cx + 27`, not `cx + 43`. Using `height / 2`
|
|
126
|
+
* puts the anchor outside the icon and eNSP then discards the whole `<lines>`
|
|
127
|
+
* block, which shows up as an empty canvas with no error.
|
|
128
|
+
*
|
|
129
|
+
* Frequencies across the bundled samples: (+70.2,+27.0) x35, (-16.2,+27.0) x35,
|
|
130
|
+
* (+27.0,+70.2) x29, (+27.0,-16.2) x27.
|
|
131
|
+
*/
|
|
132
|
+
export const ICON = { w: 86, h: 86, left: -16.2, right: 70.2, top: -16.2, bottom: 70.2 }
|
|
133
|
+
|
|
134
|
+
/** Total declared interface count for a model. */
|
|
135
|
+
export function portCount(model) {
|
|
136
|
+
const spec = MODEL_SPECS[model]
|
|
137
|
+
if (!spec) return 0
|
|
138
|
+
return spec.groups.reduce((sum, group) => sum + group[2], 0)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** CLI prefix for an eNSP interface-name group, when the group does not state one. */
|
|
142
|
+
export function cliPrefixFor(enspPrefix) {
|
|
143
|
+
return enspPrefix === 'GE' ? 'GigabitEthernet0/0/' : 'Ethernet0/0/'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Human-readable list of a model's ports, for error messages. */
|
|
147
|
+
export function describePorts(model) {
|
|
148
|
+
const spec = MODEL_SPECS[model]
|
|
149
|
+
if (!spec) return '(unknown model)'
|
|
150
|
+
if (!spec.groups.length) return '(no interfaces)'
|
|
151
|
+
return spec.groups
|
|
152
|
+
.map((group) => {
|
|
153
|
+
const [, enspPrefix, count, firstNumber, cliOverride] = group
|
|
154
|
+
const first = firstNumber ?? spec.base
|
|
155
|
+
const cli = cliOverride ?? cliPrefixFor(enspPrefix)
|
|
156
|
+
return `${cli}${first}..${first + count - 1}`
|
|
157
|
+
})
|
|
158
|
+
.join(', ')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a VRP interface name to the flat index eNSP stores.
|
|
163
|
+
*
|
|
164
|
+
* The flat index walks every declared group, so on an AR1220 index 2 is the first
|
|
165
|
+
* `Ethernet` port, not `GE0/0/2`.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} model
|
|
168
|
+
* @param {string} port e.g. `GigabitEthernet0/0/1`
|
|
169
|
+
* @returns {number|null}
|
|
170
|
+
*/
|
|
171
|
+
export function vrpToFlatIndex(model, port) {
|
|
172
|
+
const spec = MODEL_SPECS[model]
|
|
173
|
+
if (!spec) return null
|
|
174
|
+
|
|
175
|
+
let offset = 0
|
|
176
|
+
for (const group of spec.groups) {
|
|
177
|
+
const [, enspPrefix, count, firstNumber, cliOverride] = group
|
|
178
|
+
const first = firstNumber ?? spec.base
|
|
179
|
+
const cli = cliOverride ?? cliPrefixFor(enspPrefix)
|
|
180
|
+
if (port.startsWith(cli)) {
|
|
181
|
+
const tail = port.slice(cli.length)
|
|
182
|
+
if (/^\d+$/.test(tail)) {
|
|
183
|
+
const n = Number(tail) - first
|
|
184
|
+
if (n >= 0 && n < count) return offset + n
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
offset += count
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Inverse of {@link vrpToFlatIndex}.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} model
|
|
196
|
+
* @param {number} index
|
|
197
|
+
* @returns {string|null}
|
|
198
|
+
*/
|
|
199
|
+
export function flatIndexToVrp(model, index) {
|
|
200
|
+
const spec = MODEL_SPECS[model]
|
|
201
|
+
if (!spec) return null
|
|
202
|
+
|
|
203
|
+
let remaining = index
|
|
204
|
+
for (const group of spec.groups) {
|
|
205
|
+
const [, enspPrefix, count, firstNumber, cliOverride] = group
|
|
206
|
+
const first = firstNumber ?? spec.base
|
|
207
|
+
const cli = cliOverride ?? cliPrefixFor(enspPrefix)
|
|
208
|
+
if (remaining < count) return `${cli}${remaining + first}`
|
|
209
|
+
remaining -= count
|
|
210
|
+
}
|
|
211
|
+
return null
|
|
212
|
+
}
|