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
package/src/core/build.mjs
CHANGED
|
@@ -1,299 +1,218 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Topology construction helpers.
|
|
3
|
-
*
|
|
4
|
-
* Building a `.topo` by hand is error-prone because eNSP validates a surprising
|
|
5
|
-
* amount of derived state on load. The rules below were reverse-engineered from
|
|
6
|
-
* files eNSP itself wrote and confirmed by opening generated topologies
|
|
7
|
-
* (`docs/UI-ANCHORS.md` records the surrounding experiments):
|
|
8
|
-
*
|
|
9
|
-
* | Rule | Value |
|
|
10
|
-
* |---|---|
|
|
11
|
-
* | XML declaration | says `encoding="UNICODE"`, file is actually UTF-8 |
|
|
12
|
-
* | `edit_left` / `edit_top` | `cx + 27` / `cy + 54` |
|
|
13
|
-
* | attribute order | `id name model settings com_port bootmode cx cy edit_left edit_top` |
|
|
14
|
-
* | switch interfaces | one slot, `interfacename="GE"`, index base 1 |
|
|
15
|
-
* | PC / Server interfaces | `interfacename="Ethernet"`, index base 1 |
|
|
16
|
-
* | router / firewall interfaces | `interfacename="GE"`, index base 0 |
|
|
17
|
-
* | cable anchors | on the icon's bounding box: `x-16.2` / `x+70.2`, `y-16.2` / `y+70.2` |
|
|
18
|
-
*
|
|
19
|
-
* Two mistakes cost real debugging time and are called out in the code:
|
|
20
|
-
* declaring PC interfaces as `GE` (eNSP silently renders an empty canvas) and
|
|
21
|
-
* omitting `system_mac` handling (the attribute is absent from native files).
|
|
22
|
-
*
|
|
23
|
-
* @module dsh-ensp/build
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { randomUUID } from 'node:crypto'
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const all = spec.groups
|
|
220
|
-
.map((g) => {
|
|
221
|
-
const first = g[3] ?? spec.base
|
|
222
|
-
return `${cliPrefixFor(g[1])}${first}..${first + g[2] - 1}`
|
|
223
|
-
})
|
|
224
|
-
.join(', ')
|
|
225
|
-
throw new Error(
|
|
226
|
-
`${device.name} (${device.model}) has no interface ${port}; expected one of: ${all}`,
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
return index
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
finish() {
|
|
233
|
-
return {
|
|
234
|
-
devices: this.#devices.map((d) => ({ ...d })),
|
|
235
|
-
links: this.#links.map((l) => ({ ...l })),
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
toXml() {
|
|
240
|
-
return serialiseTopology(this.finish())
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Start a builder. */
|
|
245
|
-
export function buildTopology(options) {
|
|
246
|
-
return new TopologyBuilder(options)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Deterministic grid layout.
|
|
251
|
-
* @param {object} topo
|
|
252
|
-
*/
|
|
253
|
-
export function autoLayout(topo, { columns = 4, paddingX = 220, paddingY = 200, originX = 120, originY = 80 } = {}) {
|
|
254
|
-
topo.devices.forEach((device, i) => {
|
|
255
|
-
device.x = originX + (i % columns) * paddingX
|
|
256
|
-
device.y = originY + Math.floor(i / columns) * paddingY
|
|
257
|
-
})
|
|
258
|
-
return topo
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Two PCs on one Layer-2 switch — the smallest lab that proves the whole chain
|
|
263
|
-
* (generate → open → boot → configure → verify).
|
|
264
|
-
*
|
|
265
|
-
* @param {{vlan?: number, subnet?: string}} [options]
|
|
266
|
-
*/
|
|
267
|
-
export function twoPcLab({ vlan = 10, subnet = '192.168.10' } = {}) {
|
|
268
|
-
const topo = buildTopology({ firstComPort: 2000 })
|
|
269
|
-
.add('SW1', 'S5700', { x: 300, y: 120 })
|
|
270
|
-
.add('PC1', 'PC', { x: 180, y: 320, pc: { ip: `${subnet}.1`, gateway: `${subnet}.254` } })
|
|
271
|
-
.add('PC2', 'PC', { x: 420, y: 320, pc: { ip: `${subnet}.2`, gateway: `${subnet}.254` } })
|
|
272
|
-
.link('PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1')
|
|
273
|
-
.link('PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2')
|
|
274
|
-
.finish()
|
|
275
|
-
|
|
276
|
-
topo.meta = { vlan, subnet, pc1: `${subnet}.1`, pc2: `${subnet}.2` }
|
|
277
|
-
return topo
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Switch configuration for {@link twoPcLab}: both ports into one VLAN.
|
|
282
|
-
* @param {{vlan?: number}} [options]
|
|
283
|
-
*/
|
|
284
|
-
export function twoPcLabConfig({ vlan = 10 } = {}) {
|
|
285
|
-
return [
|
|
286
|
-
'system-view',
|
|
287
|
-
`vlan ${vlan}`,
|
|
288
|
-
' quit',
|
|
289
|
-
'interface GigabitEthernet0/0/1',
|
|
290
|
-
' port link-type access',
|
|
291
|
-
` port default vlan ${vlan}`,
|
|
292
|
-
' quit',
|
|
293
|
-
'interface GigabitEthernet0/0/2',
|
|
294
|
-
' port link-type access',
|
|
295
|
-
` port default vlan ${vlan}`,
|
|
296
|
-
' quit',
|
|
297
|
-
'return',
|
|
298
|
-
].join('\n')
|
|
299
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Topology construction helpers.
|
|
3
|
+
*
|
|
4
|
+
* Building a `.topo` by hand is error-prone because eNSP validates a surprising
|
|
5
|
+
* amount of derived state on load. The rules below were reverse-engineered from
|
|
6
|
+
* files eNSP itself wrote and confirmed by opening generated topologies
|
|
7
|
+
* (`docs/UI-ANCHORS.md` records the surrounding experiments):
|
|
8
|
+
*
|
|
9
|
+
* | Rule | Value |
|
|
10
|
+
* |---|---|
|
|
11
|
+
* | XML declaration | says `encoding="UNICODE"`, file is actually UTF-8 |
|
|
12
|
+
* | `edit_left` / `edit_top` | `cx + 27` / `cy + 54` |
|
|
13
|
+
* | attribute order | `id name model settings com_port bootmode cx cy edit_left edit_top` |
|
|
14
|
+
* | switch interfaces | one slot, `interfacename="GE"`, index base 1 |
|
|
15
|
+
* | PC / Server interfaces | `interfacename="Ethernet"`, index base 1 |
|
|
16
|
+
* | router / firewall interfaces | `interfacename="GE"`, index base 0 |
|
|
17
|
+
* | cable anchors | on the icon's bounding box: `x-16.2` / `x+70.2`, `y-16.2` / `y+70.2` |
|
|
18
|
+
*
|
|
19
|
+
* Two mistakes cost real debugging time and are called out in the code:
|
|
20
|
+
* declaring PC interfaces as `GE` (eNSP silently renders an empty canvas) and
|
|
21
|
+
* omitting `system_mac` handling (the attribute is absent from native files).
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-ensp/build
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { randomUUID } from 'node:crypto'
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
MODEL_SPECS,
|
|
30
|
+
ICON,
|
|
31
|
+
describePorts,
|
|
32
|
+
vrpToFlatIndex,
|
|
33
|
+
} from './models.mjs'
|
|
34
|
+
import { serialiseTopology } from './topology.mjs'
|
|
35
|
+
|
|
36
|
+
// Re-exported so callers that only need the table plus the builder keep one
|
|
37
|
+
// import. The authoritative definitions live in models.mjs.
|
|
38
|
+
export { MODEL_SPECS, ICON, vrpToFlatIndex, flatIndexToVrp } from './models.mjs'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `settings` blob eNSP writes for a simulated PC. Empty for every other model.
|
|
42
|
+
* Without it eNSP still shows the PC but with no address, which breaks the
|
|
43
|
+
* "ping between the two PCs" step.
|
|
44
|
+
*/
|
|
45
|
+
function pcSettings({ ip = '192.168.10.1', mask = '255.255.255.0', gateway = '192.168.10.254', mac, dhcp = false } = {}) {
|
|
46
|
+
const macAddr = mac ?? `54-89-98-${Array.from({ length: 3 }, () =>
|
|
47
|
+
Math.floor(Math.random() * 256).toString(16).toUpperCase().padStart(2, '0')).join('-')}`
|
|
48
|
+
// A DHCP client must not carry a static address, or eNSP keeps using it and the
|
|
49
|
+
// pool is never exercised. -simpc_dhcp_state 1 is what makes the PC ask.
|
|
50
|
+
const addressing = dhcp
|
|
51
|
+
? ' -simpc_ip 0.0.0.0 -simpc_mask 0.0.0.0 -simpc_gateway 0.0.0.0'
|
|
52
|
+
: ` -simpc_ip ${ip} -simpc_mask ${mask} -simpc_gateway ${gateway}`
|
|
53
|
+
return (
|
|
54
|
+
`${addressing} -simpc_mac ${macAddr}` +
|
|
55
|
+
' -simpc_mc_dstip 0.0.0.0 -simpc_mc_dstmac 00-00-00-00-00-00' +
|
|
56
|
+
' -simpc_dns1 0.0.0.0 -simpc_dns2 0.0.0.0' +
|
|
57
|
+
' -simpc_ipv6 :: -simpc_prefix 128 -simpc_gatewayv6 ::' +
|
|
58
|
+
` -simpc_dhcp_state ${dhcp ? 1 : 0} -simpc_dhcpv6_state 0 -simpc_dns_auto_state 0 `
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fluent topology builder.
|
|
64
|
+
*/
|
|
65
|
+
export class TopologyBuilder {
|
|
66
|
+
#devices = []
|
|
67
|
+
#links = []
|
|
68
|
+
#nextComPort
|
|
69
|
+
|
|
70
|
+
constructor({ firstComPort = 2000 } = {}) {
|
|
71
|
+
this.#nextComPort = firstComPort
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add a device.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @param {string} model - key of {@link MODEL_SPECS}
|
|
79
|
+
* @param {{x?: number, y?: number, comPort?: number|null, bootmode?: number,
|
|
80
|
+
* pc?: {ip?: string, mask?: string, gateway?: string, mac?: string}}} [options]
|
|
81
|
+
*/
|
|
82
|
+
add(name, model, options = {}) {
|
|
83
|
+
const spec = MODEL_SPECS[model]
|
|
84
|
+
if (!spec) throw new Error(`unknown model "${model}"; known: ${Object.keys(MODEL_SPECS).join(', ')}`)
|
|
85
|
+
if (this.#devices.some((d) => d.name === name)) throw new Error(`duplicate device name "${name}"`)
|
|
86
|
+
|
|
87
|
+
const comPort =
|
|
88
|
+
options.comPort !== undefined
|
|
89
|
+
? options.comPort
|
|
90
|
+
: spec.console
|
|
91
|
+
? this.#nextComPort++
|
|
92
|
+
: 0
|
|
93
|
+
|
|
94
|
+
this.#devices.push({
|
|
95
|
+
id: randomUUID(),
|
|
96
|
+
name,
|
|
97
|
+
model,
|
|
98
|
+
settings: model === 'PC' ? pcSettings(options.pc).trim() : '',
|
|
99
|
+
comPort,
|
|
100
|
+
bootmode: options.bootmode ?? 1,
|
|
101
|
+
x: options.x ?? 100,
|
|
102
|
+
y: options.y ?? 100,
|
|
103
|
+
})
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Connect two devices using **VRP interface names** (what you type on the CLI).
|
|
109
|
+
*
|
|
110
|
+
* @param {string} aName
|
|
111
|
+
* @param {string} aPort e.g. `GigabitEthernet0/0/1` or `Ethernet0/0/1`
|
|
112
|
+
* @param {string} bName
|
|
113
|
+
* @param {string} bPort
|
|
114
|
+
* @param {{media?: string}} [options]
|
|
115
|
+
*/
|
|
116
|
+
link(aName, aPort, bName, bPort, options = {}) {
|
|
117
|
+
const a = this.#devices.find((d) => d.name === aName)
|
|
118
|
+
const b = this.#devices.find((d) => d.name === bName)
|
|
119
|
+
if (!a) throw new Error(`no device "${aName}"`)
|
|
120
|
+
if (!b) throw new Error(`no device "${bName}"`)
|
|
121
|
+
|
|
122
|
+
const ai = this.#vrpToIndex(a, aPort)
|
|
123
|
+
const bi = this.#vrpToIndex(b, bPort)
|
|
124
|
+
|
|
125
|
+
this.#links.push({
|
|
126
|
+
srcId: a.id,
|
|
127
|
+
dstId: b.id,
|
|
128
|
+
srcIndex: ai,
|
|
129
|
+
dstIndex: bi,
|
|
130
|
+
media: options.media ?? 'Copper',
|
|
131
|
+
})
|
|
132
|
+
return this
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Map a VRP interface name to the flat slot index eNSP stores. */
|
|
136
|
+
#vrpToIndex(device, port) {
|
|
137
|
+
const spec = MODEL_SPECS[device.model]
|
|
138
|
+
if (!spec) throw new Error(`unknown model "${device.model}"`)
|
|
139
|
+
const index = vrpToFlatIndex(device.model, port)
|
|
140
|
+
if (index === null) {
|
|
141
|
+
// Firewalls and PCs override the CLI family, so the shared helper is the
|
|
142
|
+
// only reliable way to describe what this model actually accepts.
|
|
143
|
+
const all = describePorts(device.model)
|
|
144
|
+
throw new Error(
|
|
145
|
+
`${device.name} (${device.model}) has no interface ${port}; expected one of: ${all}`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
return index
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
finish() {
|
|
152
|
+
return {
|
|
153
|
+
devices: this.#devices.map((d) => ({ ...d })),
|
|
154
|
+
links: this.#links.map((l) => ({ ...l })),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
toXml() {
|
|
159
|
+
return serialiseTopology(this.finish())
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Start a builder. */
|
|
164
|
+
export function buildTopology(options) {
|
|
165
|
+
return new TopologyBuilder(options)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deterministic grid layout.
|
|
170
|
+
* @param {object} topo
|
|
171
|
+
*/
|
|
172
|
+
export function autoLayout(topo, { columns = 4, paddingX = 220, paddingY = 200, originX = 120, originY = 80 } = {}) {
|
|
173
|
+
topo.devices.forEach((device, i) => {
|
|
174
|
+
device.x = originX + (i % columns) * paddingX
|
|
175
|
+
device.y = originY + Math.floor(i / columns) * paddingY
|
|
176
|
+
})
|
|
177
|
+
return topo
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Two PCs on one Layer-2 switch — the smallest lab that proves the whole chain
|
|
182
|
+
* (generate → open → boot → configure → verify).
|
|
183
|
+
*
|
|
184
|
+
* @param {{vlan?: number, subnet?: string}} [options]
|
|
185
|
+
*/
|
|
186
|
+
export function twoPcLab({ vlan = 10, subnet = '192.168.10' } = {}) {
|
|
187
|
+
const topo = buildTopology({ firstComPort: 2000 })
|
|
188
|
+
.add('SW1', 'S5700', { x: 300, y: 120 })
|
|
189
|
+
.add('PC1', 'PC', { x: 180, y: 320, pc: { ip: `${subnet}.1`, gateway: `${subnet}.254` } })
|
|
190
|
+
.add('PC2', 'PC', { x: 420, y: 320, pc: { ip: `${subnet}.2`, gateway: `${subnet}.254` } })
|
|
191
|
+
.link('PC1', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/1')
|
|
192
|
+
.link('PC2', 'Ethernet0/0/1', 'SW1', 'GigabitEthernet0/0/2')
|
|
193
|
+
.finish()
|
|
194
|
+
|
|
195
|
+
topo.meta = { vlan, subnet, pc1: `${subnet}.1`, pc2: `${subnet}.2` }
|
|
196
|
+
return topo
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Switch configuration for {@link twoPcLab}: both ports into one VLAN.
|
|
201
|
+
* @param {{vlan?: number}} [options]
|
|
202
|
+
*/
|
|
203
|
+
export function twoPcLabConfig({ vlan = 10 } = {}) {
|
|
204
|
+
return [
|
|
205
|
+
'system-view',
|
|
206
|
+
`vlan ${vlan}`,
|
|
207
|
+
' quit',
|
|
208
|
+
'interface GigabitEthernet0/0/1',
|
|
209
|
+
' port link-type access',
|
|
210
|
+
` port default vlan ${vlan}`,
|
|
211
|
+
' quit',
|
|
212
|
+
'interface GigabitEthernet0/0/2',
|
|
213
|
+
' port link-type access',
|
|
214
|
+
` port default vlan ${vlan}`,
|
|
215
|
+
' quit',
|
|
216
|
+
'return',
|
|
217
|
+
].join('\n')
|
|
218
|
+
}
|