dsh-ensp 0.4.0 → 0.5.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.en.md +12 -1
- package/README.md +12 -1
- package/package.json +3 -2
- package/scripts/gen-knowledge.mjs +161 -0
- package/src/cli/ensp.mjs +22 -1
- package/src/core/console.mjs +23 -1
- package/src/core/index.mjs +7 -0
- package/src/core/knowledge.json +412 -0
- package/src/core/knowledge.mjs +104 -0
- package/src/dsh/index.mjs +44 -1
- package/src/mcp/server.mjs +23 -0
package/README.en.md
CHANGED
|
@@ -128,6 +128,17 @@ npm run docs # regenerate docs/SCENARIOS.md
|
|
|
128
128
|
npm run docs:check # check whether it is stale
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
### Knowledge points
|
|
132
|
+
|
|
133
|
+
A scenario gives direction, not commands, so the vocabulary of the lab has to come from somewhere. `src/core/knowledge.json` holds knowledge points filed by topic: each scenario ends with a few question-and-answer pairs on the same subject, covering things like which LSA belongs to which area, which attribute decides a BGP path, and which port role only MSTP has.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
ensp preset ospf-lsa-types # the detail ends with a knowledge section
|
|
137
|
+
ensp preset --knowledge LSA # search the index by keyword
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The points come from a certification question bank, re-filed by topic. The bank's own tags are unreliable, so a question about OSPF LSA types can also carry a security tag; each question is scored against topic keywords and kept only under the best match. Questions phrased as "which of the following is wrong" are dropped, because once the distractors are stripped what remains is a false statement. The bank itself is not shipped: `knowledge.json` keeps only the stem and the correct option text.
|
|
141
|
+
|
|
131
142
|
### Device image check
|
|
132
143
|
|
|
133
144
|
Some models have a catalogue entry without a shipped image, most notably `USG6000V` (its component has to be selected during eNSP installation). Such models can be placed on the canvas and saved to a `.topo`, but eNSP renders the topology and then quits, which shows up as consoles that never answer.
|
|
@@ -254,7 +265,7 @@ ensp status
|
|
|
254
265
|
}
|
|
255
266
|
```
|
|
256
267
|
|
|
257
|
-
|
|
268
|
+
15 tools are provided: `ensp_status`, `ensp_build_topology`, `ensp_open`, `ensp_start`, `ensp_stop`, `ensp_apply`, `ensp_apply_dir`, `ensp_query`, `ensp_probe`, `ensp_doctor`, `ensp_verify`, `ensp_scenarios`, `ensp_scenario`, `ensp_knowledge` and `ensp_preset_config`.
|
|
258
269
|
|
|
259
270
|
## Requirements
|
|
260
271
|
|
package/README.md
CHANGED
|
@@ -123,6 +123,17 @@ ensp up ospf-single.topo
|
|
|
123
123
|
|
|
124
124
|
场景清单与蓝图均可生成、可校验。`docs/SCENARIOS.md` 由 `src/core/presets.mjs` 与 `src/core/labs.mjs` 渲染,`npm run docs:check` 在两者不一致时返回非零,CI 亦执行该检查。
|
|
125
125
|
|
|
126
|
+
### 知识点
|
|
127
|
+
|
|
128
|
+
场景说明只给方向,命令由使用者补全,因此需要知道该场景涉及的术语。`src/core/knowledge.json` 收录按主题归类的知识点:每个场景在末尾附上同主题的若干「问题 + 答案」,涵盖 LSA 类型归属、BGP 选路属性、MSTP 端口角色等。
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
ensp preset ospf-lsa-types # 详情末尾带知识点小节
|
|
132
|
+
ensp preset --knowledge LSA # 按关键词检索知识点
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
知识点来自认证题库,按主题重新归类:题目的原始标签并不可靠,同一道 OSPF 题可能同时被标为安全,因此按正文关键词评分后只归入最匹配的主题,并剔除「以下错误的是」这类去掉干扰项后只剩错误陈述的题目。题库原文不随包分发,`knowledge.json` 只保留题干与正确选项文本。
|
|
136
|
+
|
|
126
137
|
```bash
|
|
127
138
|
npm run docs # 重新生成 docs/SCENARIOS.md
|
|
128
139
|
npm run docs:check # 只检查是否过期
|
|
@@ -254,7 +265,7 @@ ensp status
|
|
|
254
265
|
}
|
|
255
266
|
```
|
|
256
267
|
|
|
257
|
-
提供
|
|
268
|
+
提供 15 个工具:`ensp_status`、`ensp_build_topology`、`ensp_open`、`ensp_start`、`ensp_stop`、`ensp_apply`、`ensp_apply_dir`、`ensp_query`、`ensp_probe`、`ensp_doctor`、`ensp_verify`、`ensp_scenarios`、`ensp_scenario`、`ensp_knowledge`、`ensp_preset_config`。
|
|
258
269
|
|
|
259
270
|
## 环境要求
|
|
260
271
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ensp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Automate Huawei eNSP labs: generate .topo files, start and stop devices, and push configuration one command at a time with per-command error reporting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"test": "node --test tests/core.test.mjs",
|
|
48
48
|
"probe": "node src/cli/ensp.mjs probe",
|
|
49
49
|
"docs": "node scripts/gen-scenarios-doc.mjs",
|
|
50
|
-
"docs:check": "node scripts/gen-scenarios-doc.mjs --check"
|
|
50
|
+
"docs:check": "node scripts/gen-scenarios-doc.mjs --check",
|
|
51
|
+
"knowledge": "node scripts/gen-knowledge.mjs"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
53
54
|
"node": ">=18"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerate `src/core/knowledge.json` from a certification question bank.
|
|
3
|
+
*
|
|
4
|
+
* The bank is not part of this package: it is copyrighted material, and most of
|
|
5
|
+
* its questions are three-sentence reading exercises rather than facts. What the
|
|
6
|
+
* package does ship is the index this script produces, so the transformation has
|
|
7
|
+
* to be reproducible and reviewable by anyone who has the bank.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/gen-knowledge.mjs <bank.json>
|
|
11
|
+
*
|
|
12
|
+
* The bank is expected as an array of
|
|
13
|
+
* `{ n, type, stem, options: [{key, text}], answer, analysis, topics }`.
|
|
14
|
+
*
|
|
15
|
+
* Two rules shape the output, and both exist because the obvious version of this
|
|
16
|
+
* script produced a worse index:
|
|
17
|
+
*
|
|
18
|
+
* 1. **One topic per question.** The bank's own tags are unreliable: a question
|
|
19
|
+
* about OSPF LSA types carries `ospf` and `security` both, and honouring every
|
|
20
|
+
* tag files the same fact under two headings. Score the text against topic
|
|
21
|
+
* keywords and keep the best match.
|
|
22
|
+
* 2. **Drop "which is wrong" questions.** Once the distractors are removed, the
|
|
23
|
+
* surviving string is a false statement, which is worse than no material.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
import path from 'node:path'
|
|
29
|
+
|
|
30
|
+
/** Topic tag -> the scenarios that teach it. */
|
|
31
|
+
const TOPIC_SCENARIOS = {
|
|
32
|
+
ospf: ['ospf-single', 'ospf-multi-area', 'ospf-stub-nssa', 'ospf-lsa-types', 'ospf-virtual-link', 'ospf-troubleshooting'],
|
|
33
|
+
bgp: ['bgp-ebgp-ibgp', 'bgp-route-reflector', 'bgp-attributes', 'bgp-aggregation', 'bgp-troubleshooting'],
|
|
34
|
+
isis: ['isis-multi-level'],
|
|
35
|
+
switching: [
|
|
36
|
+
'vlan-access',
|
|
37
|
+
'trunk-interconnect',
|
|
38
|
+
'stp-rstp',
|
|
39
|
+
'mstp-load-balance',
|
|
40
|
+
'link-aggregation',
|
|
41
|
+
'super-vlan',
|
|
42
|
+
'port-security',
|
|
43
|
+
'port-isolation',
|
|
44
|
+
'stack-istack',
|
|
45
|
+
],
|
|
46
|
+
multicast: ['multicast-pim-sm'],
|
|
47
|
+
mpls: ['mpls-basic', 'mpls-l3vpn'],
|
|
48
|
+
ipv6: ['ipv6', 'ospfv3-ipv6'],
|
|
49
|
+
qos: ['qos-basics'],
|
|
50
|
+
reliability: ['vrrp-basics', 'bfd-fast-convergence', 'firewall-hrp', 'dual-homed-isp'],
|
|
51
|
+
security: ['acl-filter', 'aaa-local', 'ipsec-vpn', 'firewall-zones', 'firewall-hrp', 'arp-security', 'dhcp-snooping'],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Keywords that decide which topic a question belongs to. */
|
|
55
|
+
const TOPIC_TERMS = {
|
|
56
|
+
ospf: ['ospf', 'lsa', 'abr', 'asbr', 'nssa', 'stub', 'router-id'],
|
|
57
|
+
bgp: ['bgp', 'as-path', 'ibgp', 'ebgp', 'community', 'aggregator', 'med', 'local-preference'],
|
|
58
|
+
isis: ['is-is', 'isis', 'level-1', 'level-2', 'dis', 'lsp', 'att'],
|
|
59
|
+
switching: ['vlan', 'mstp', 'rstp', 'stp', 'eth-trunk', '端口隔离', '端口安全', '堆叠', '根桥', 'bpdu'],
|
|
60
|
+
multicast: ['pim', 'igmp', '组播', 'rpf', 'spt'],
|
|
61
|
+
mpls: ['mpls', 'ldp', 'lsr', '标签', 'vpn-instance'],
|
|
62
|
+
ipv6: ['ipv6', 'ospfv3', 'slaac'],
|
|
63
|
+
qos: ['qos', '队列', '拥塞', '流量监管', '优先级映射'],
|
|
64
|
+
reliability: ['vrrp', 'bfd', 'hrp', '双机热备', '主备'],
|
|
65
|
+
security: ['acl', '安全区域', 'security-policy', 'ipsec', 'ike', 'aaa', 'radius', '防火墙', 'nat'],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A question phrased this way keeps only a false statement once stripped. */
|
|
69
|
+
const NEGATIVE = /错误的是|不正确的是|不属于|不支持|不能|无需/
|
|
70
|
+
|
|
71
|
+
/** Points kept per topic. Enough to cover the vocabulary, short enough to read. */
|
|
72
|
+
const MAX_POINTS = 8
|
|
73
|
+
|
|
74
|
+
/** A question must score at least this many keyword hits to be filed at all. */
|
|
75
|
+
const MIN_SCORE = 2
|
|
76
|
+
|
|
77
|
+
const clean = (s) => String(s ?? '').replace(/\s+/g, ' ').trim()
|
|
78
|
+
const clip = (s, n) => (s.length <= n ? s : s.slice(0, n - 1).replace(/[,,、;;:: .]*$/, '') + '…')
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The answer as readable text.
|
|
82
|
+
*
|
|
83
|
+
* A bare key list (`A,C`) teaches nothing, so every chosen option is spelled out.
|
|
84
|
+
* Returns '' when the answer is not machine-readable, which is the case for the
|
|
85
|
+
* drag-and-drop and fill-in-the-blank questions.
|
|
86
|
+
*/
|
|
87
|
+
function answerText(q) {
|
|
88
|
+
const keys = clean(q.answer).toUpperCase().match(/[A-Z]/g)
|
|
89
|
+
if (!keys || !q.options?.length) return q.type === '判断题' ? clean(q.answer) : ''
|
|
90
|
+
const picked = q.options.filter((o) => keys.includes(String(o.key).toUpperCase()))
|
|
91
|
+
if (!picked.length) return ''
|
|
92
|
+
return picked.map((o) => `${o.key}. ${clip(clean(o.text), 70)}`).join(' / ')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the index.
|
|
97
|
+
*
|
|
98
|
+
* @param {Array<object>} bank
|
|
99
|
+
* @returns {object}
|
|
100
|
+
*/
|
|
101
|
+
export function buildIndex(bank) {
|
|
102
|
+
const perTopic = {}
|
|
103
|
+
for (const q of bank) {
|
|
104
|
+
const answer = answerText(q)
|
|
105
|
+
if (!answer) continue
|
|
106
|
+
const stem = clean(q.stem)
|
|
107
|
+
if (NEGATIVE.test(stem)) continue
|
|
108
|
+
const hay = (stem + ' ' + clean(q.analysis)).toLowerCase()
|
|
109
|
+
let best = null
|
|
110
|
+
let bestScore = 0
|
|
111
|
+
for (const [topic, terms] of Object.entries(TOPIC_TERMS)) {
|
|
112
|
+
const score = terms.reduce((sum, t) => sum + (hay.split(t).length - 1), 0)
|
|
113
|
+
if (score > bestScore) {
|
|
114
|
+
bestScore = score
|
|
115
|
+
best = topic
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!best || bestScore < MIN_SCORE) continue
|
|
119
|
+
;(perTopic[best] ||= []).push({ n: q.n, q: clip(stem, 90), a: answer, score: bestScore })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const topics = {}
|
|
123
|
+
for (const [topic, rows] of Object.entries(perTopic)) {
|
|
124
|
+
// Highest keyword score first, then the shortest stem: a one-line question
|
|
125
|
+
// states a fact, while a long one describes a scene.
|
|
126
|
+
const ranked = rows.slice().sort((a, b) => b.score - a.score || a.q.length - b.q.length)
|
|
127
|
+
const seen = new Set()
|
|
128
|
+
const points = []
|
|
129
|
+
for (const r of ranked) {
|
|
130
|
+
const key = r.q.slice(0, 40) + '|' + r.a.slice(0, 30)
|
|
131
|
+
if (seen.has(key)) continue
|
|
132
|
+
seen.add(key)
|
|
133
|
+
points.push({ n: r.n, q: r.q, a: r.a })
|
|
134
|
+
if (points.length >= MAX_POINTS) break
|
|
135
|
+
}
|
|
136
|
+
topics[topic] = { scenarios: TOPIC_SCENARIOS[topic], points }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
generatedFrom: 'HCIP H12-821 单选/多选/判断题',
|
|
141
|
+
sourceQuestions: bank.length,
|
|
142
|
+
topics,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const invokedDirectly =
|
|
147
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
148
|
+
|
|
149
|
+
if (invokedDirectly) {
|
|
150
|
+
const bankPath = process.argv[2]
|
|
151
|
+
if (!bankPath) {
|
|
152
|
+
console.error('usage: node scripts/gen-knowledge.mjs <bank.json>')
|
|
153
|
+
process.exit(2)
|
|
154
|
+
}
|
|
155
|
+
const bank = JSON.parse(readFileSync(bankPath, 'utf8'))
|
|
156
|
+
const index = buildIndex(bank)
|
|
157
|
+
const target = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'core', 'knowledge.json')
|
|
158
|
+
writeFileSync(target, JSON.stringify(index, null, 1) + '\n', 'utf8')
|
|
159
|
+
const total = Object.values(index.topics).reduce((sum, t) => sum + t.points.length, 0)
|
|
160
|
+
console.log(`wrote src/core/knowledge.json (${total} points from ${bank.length} questions)`)
|
|
161
|
+
}
|
package/src/cli/ensp.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { doctor, formatDoctor, modelsOf } from '../core/doctor.mjs'
|
|
|
35
35
|
import { runAcceptance, formatAcceptance, hasChecks, planFor } from '../core/acceptance.mjs'
|
|
36
36
|
import { pushConfig } from '../core/provision.mjs'
|
|
37
37
|
import { SCENARIOS, SCENARIO_IDS, byLevel, renderScenario, searchScenarios } from '../core/presets.mjs'
|
|
38
|
+
import { knowledgeFor, renderKnowledge, searchKnowledge } from '../core/knowledge.mjs'
|
|
38
39
|
import {
|
|
39
40
|
LAB_IDS,
|
|
40
41
|
buildScenarioTopology,
|
|
@@ -320,6 +321,23 @@ async function cmdStatus({ flags }) {
|
|
|
320
321
|
async function cmdPreset({ positional, flags }) {
|
|
321
322
|
const id = positional[0]
|
|
322
323
|
|
|
324
|
+
if (flags.knowledge) {
|
|
325
|
+
const hits = searchKnowledge(String(flags.knowledge))
|
|
326
|
+
if (flags.json) {
|
|
327
|
+
console.log(JSON.stringify(hits, null, 2))
|
|
328
|
+
return hits.length ? 0 : 1
|
|
329
|
+
}
|
|
330
|
+
if (!hits.length) {
|
|
331
|
+
console.log(C.yellow('no knowledge point matched') + ` "${flags.knowledge}"`)
|
|
332
|
+
return 1
|
|
333
|
+
}
|
|
334
|
+
for (const h of hits) {
|
|
335
|
+
console.log(`${C.bold('#' + h.n)} ${C.dim('[' + h.topic + ']')} ${h.q}`)
|
|
336
|
+
console.log(` 答: ${h.a}`)
|
|
337
|
+
}
|
|
338
|
+
return 0
|
|
339
|
+
}
|
|
340
|
+
|
|
323
341
|
if (flags.search) {
|
|
324
342
|
const hits = searchScenarios(String(flags.search))
|
|
325
343
|
if (flags.json) {
|
|
@@ -392,7 +410,10 @@ async function cmdPreset({ positional, flags }) {
|
|
|
392
410
|
}
|
|
393
411
|
|
|
394
412
|
const text = renderScenario(id)
|
|
395
|
-
|
|
413
|
+
const points = knowledgeFor(id)
|
|
414
|
+
console.log(flags.json ? JSON.stringify({ ...SCENARIOS[id], knowledge: points }, null, 2) : text)
|
|
415
|
+
const knowledge = renderKnowledge(id)
|
|
416
|
+
if (knowledge) console.log(knowledge)
|
|
396
417
|
if (hasLab(id)) {
|
|
397
418
|
console.log(C.dim(`\n可生成拓扑: ensp preset ${id} --topo ${id}.topo`))
|
|
398
419
|
}
|
package/src/core/console.mjs
CHANGED
|
@@ -166,6 +166,7 @@ export class ConsoleSession {
|
|
|
166
166
|
#prompt = null
|
|
167
167
|
#hostname = null
|
|
168
168
|
#closed = false
|
|
169
|
+
#closeReason = null
|
|
169
170
|
#waiters = new Set()
|
|
170
171
|
|
|
171
172
|
/**
|
|
@@ -182,10 +183,12 @@ export class ConsoleSession {
|
|
|
182
183
|
})
|
|
183
184
|
socket.on('close', () => {
|
|
184
185
|
this.#closed = true
|
|
186
|
+
this.#closeReason ??= 'the console connection was closed by the device'
|
|
185
187
|
for (const waiter of this.#waiters) waiter()
|
|
186
188
|
})
|
|
187
|
-
socket.on('error', () => {
|
|
189
|
+
socket.on('error', (err) => {
|
|
188
190
|
this.#closed = true
|
|
191
|
+
this.#closeReason ??= `console error: ${err?.message ?? err}`
|
|
189
192
|
for (const waiter of this.#waiters) waiter()
|
|
190
193
|
})
|
|
191
194
|
}
|
|
@@ -218,6 +221,11 @@ export class ConsoleSession {
|
|
|
218
221
|
return this.#closed
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
/** @returns {string|null} why the console closed, when it closed unexpectedly. */
|
|
225
|
+
get closeReason() {
|
|
226
|
+
return this.#closeReason
|
|
227
|
+
}
|
|
228
|
+
|
|
221
229
|
/**
|
|
222
230
|
* Wait until `predicate()` is true, a new chunk arrives, or the timeout fires.
|
|
223
231
|
* @param {() => boolean} predicate
|
|
@@ -350,6 +358,20 @@ export class ConsoleSession {
|
|
|
350
358
|
// the previous line so the caller sees what paging disabled would have shown.
|
|
351
359
|
if (pages > 0) output = stitchPages(output)
|
|
352
360
|
|
|
361
|
+
// A console that closed mid-command produced no output, and no output looks
|
|
362
|
+
// exactly like a command that succeeded and printed nothing. Report the
|
|
363
|
+
// closed socket instead of handing the caller a silent empty success.
|
|
364
|
+
if (this.#closed) {
|
|
365
|
+
return {
|
|
366
|
+
command,
|
|
367
|
+
ok: false,
|
|
368
|
+
error: this.#closeReason || 'the console connection closed before the reply arrived',
|
|
369
|
+
prompt: this.#prompt,
|
|
370
|
+
view: this.view,
|
|
371
|
+
output,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
353
375
|
const errorMatch = output.match(ERROR_RE)
|
|
354
376
|
const prompt = this.#prompt
|
|
355
377
|
return {
|
package/src/core/index.mjs
CHANGED
|
@@ -123,6 +123,13 @@ export {
|
|
|
123
123
|
searchScenarios,
|
|
124
124
|
} from './presets.mjs'
|
|
125
125
|
|
|
126
|
+
export {
|
|
127
|
+
knowledgeTopics,
|
|
128
|
+
knowledgeFor,
|
|
129
|
+
searchKnowledge,
|
|
130
|
+
renderKnowledge,
|
|
131
|
+
} from './knowledge.mjs'
|
|
132
|
+
|
|
126
133
|
export {
|
|
127
134
|
resolveEnspExe,
|
|
128
135
|
resolveVBoxManage,
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedFrom": "HCIP H12-821 单选/多选/判断题",
|
|
3
|
+
"sourceQuestions": 486,
|
|
4
|
+
"topics": {
|
|
5
|
+
"bgp": {
|
|
6
|
+
"scenarios": [
|
|
7
|
+
"bgp-ebgp-ibgp",
|
|
8
|
+
"bgp-route-reflector",
|
|
9
|
+
"bgp-attributes",
|
|
10
|
+
"bgp-aggregation",
|
|
11
|
+
"bgp-troubleshooting"
|
|
12
|
+
],
|
|
13
|
+
"points": [
|
|
14
|
+
{
|
|
15
|
+
"n": 313,
|
|
16
|
+
"q": "在BGP中非客户机角色既不是RR也不是客户机的IBGP设备,在AS内部非客户机与RR之间,以及所有的非客户机之间必须建立全连接关系",
|
|
17
|
+
"a": "A. 正确"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"n": 163,
|
|
21
|
+
"q": "在BGP中MED属性是一种度量值,用于向外部对等体指出进入本AS的首选路径。以下关于该属性的描述,正确的是哪些项?",
|
|
22
|
+
"a": "A. MED属性值越小则BGP路由越优 / C. 在IBGP对等体之间传递路由时,MED值会被保留并传递,除非部署了策略,否则MED值在传递过程中不发生改变也不会丢失 / D. MED被传递给EBGP对等体后,对等体在其AS内传递路由时,携带该MED值"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"n": 262,
|
|
26
|
+
"q": "在配置EBGP时,缺省情况下EBGP连接允许的最大跳数为1,即只能在物理直连链路上建立EBGP连接。",
|
|
27
|
+
"a": "A. 正确"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"n": 380,
|
|
31
|
+
"q": "BGP设备与对等体建立邻居关系后,会通告路由给邻居。以下关于BGP通告路由的描述,正确的是哪些项?",
|
|
32
|
+
"a": "A. 当存在多条到达同一目的地址的有效路由时,BGP设备只将最优路由发布给对等体 / C. 从EBGP对等体获取的路由,会发布给所有对等体 / D. 从IBGP对等体获取的路由,不会发送给IBGP对等体"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"n": 340,
|
|
36
|
+
"q": "在BGP中MED属性仅在相邻两个AS之间传递,收到此属性的AS一方不会再将其通告给任何其他第三方AS,同时,缺省情况下路由器只比较来自同一相邻AS的BGP路由的MED值,也就说如…",
|
|
37
|
+
"a": "A. 正确"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"n": 224,
|
|
41
|
+
"q": "如图所示,R1通告BGP路由给EBGP对等体,以下关于该过程的描述,正确的是哪些项?",
|
|
42
|
+
"a": "A. R2收到该路由传递给R3时,会保持路由的Next_Hop属性值不变 / B. 该路由中包含了这条路由的Origin属性 / C. R1只发送增量更新路由"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"n": 176,
|
|
46
|
+
"q": "在BGP网络中,建立EBGP邻居关系时,要使非直连的EBGP邻居正常建立,则必须包含以下哪些配置?",
|
|
47
|
+
"a": "A. peer connectinterface / B. peer as-number / C. peer ebgp-max-hop"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"n": 383,
|
|
51
|
+
"q": "以下哪些项属于BGP可选过渡属性?",
|
|
52
|
+
"a": "A. Aggregator / B. Community"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"security": {
|
|
57
|
+
"scenarios": [
|
|
58
|
+
"acl-filter",
|
|
59
|
+
"aaa-local",
|
|
60
|
+
"ipsec-vpn",
|
|
61
|
+
"firewall-zones",
|
|
62
|
+
"firewall-hrp",
|
|
63
|
+
"arp-security",
|
|
64
|
+
"dhcp-snooping"
|
|
65
|
+
],
|
|
66
|
+
"points": [
|
|
67
|
+
{
|
|
68
|
+
"n": 147,
|
|
69
|
+
"q": "ACL是路由常用匹配工具之一,并根据ACL规则功能的不同将ACL划分成多种类型,且每类ACL编号的取值范围不同。其中,编号范围为4000~4999,表示的是以下哪—类型ACL?",
|
|
70
|
+
"a": "C. 二层ACL"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"n": 195,
|
|
74
|
+
"q": "一台路由器收到报文后按照ACL匹配流程进行匹配时,匹配结果只有两种:“匹配”或“不匹配”。那么以下场景中,其返回匹配结果为“匹配”的是哪些项?",
|
|
75
|
+
"a": "B. 匹配了ACL,但匹配动作为“permit\" / C. 匹配了ACL,但匹配动作为“deny\""
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"n": 247,
|
|
79
|
+
"q": "路由器配置访问控制列表时,可以通过ACL编号标识ACL,也可以通过名称来标识ACL。其中,配置命名型ACL时,需手工指定ACL的名字和ACL的编号。",
|
|
80
|
+
"a": "B. 错误"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"n": 235,
|
|
84
|
+
"q": "在路由器上配置ACL时,可通过指定一个唯一的数字标识该ACL,或通过名称代替编号来标识ACL。其中,命名型ACI一旦创建成功,用户便无法修改,只能删除该ACL,重新配置",
|
|
85
|
+
"a": "B. 错误"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"n": 93,
|
|
89
|
+
"q": "ACL是路由常用匹配工具之一,并根据ACL规则功能的不同将ACL划分成多种类型,且每类ACL编号的取值范围不同。其中,编号范围为4000~4999,表示的是以下哪一类型ACL?",
|
|
90
|
+
"a": "B. 二层ACL"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"n": 2,
|
|
94
|
+
"q": "在华为路由器上,若未配置ACL或配置了ACL但没有配置规则,那么它们返回ACL的匹配结果均为不匹配.",
|
|
95
|
+
"a": "A. TURE"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"n": 53,
|
|
99
|
+
"q": "使用ACL进行路由匹配时,不管是基本ACL还是高级ACL,或者是用户ACL,缺省情况下它们的ACL规则步长均为10。",
|
|
100
|
+
"a": "B. FALSE"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"n": 213,
|
|
104
|
+
"q": "某企业部署完IPSec后发现业务不通,经管理员排查发现没有数据流触发IKE协商,那么以下哪些原因可能导致该情况发生?",
|
|
105
|
+
"a": "A. 路由不可达 / B. ACL与保护数据流不匹配 / C. 域间安全策略配置错误 / D. IPSec策略未正确应用到相关接口"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"ospf": {
|
|
110
|
+
"scenarios": [
|
|
111
|
+
"ospf-single",
|
|
112
|
+
"ospf-multi-area",
|
|
113
|
+
"ospf-stub-nssa",
|
|
114
|
+
"ospf-lsa-types",
|
|
115
|
+
"ospf-virtual-link",
|
|
116
|
+
"ospf-troubleshooting"
|
|
117
|
+
],
|
|
118
|
+
"points": [
|
|
119
|
+
{
|
|
120
|
+
"n": 186,
|
|
121
|
+
"q": "某企业网络中,运行OSPF路由协议实现网络通信,已知某路由器角色仅为ABR,那么该设备可能产生以下哪些类型的LSA?",
|
|
122
|
+
"a": "A. Network Summary LSA / D. ASBR Summary LSA"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"n": 470,
|
|
126
|
+
"q": "以下关于配置OSPFStub区域注意事项的描述,正确的是哪一项?",
|
|
127
|
+
"a": "D. 如果将一个区域配置成为Stub区域,则该区域中的所有路由器都要配置Stub区域属性"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"n": 55,
|
|
131
|
+
"q": "某企业OSPF网络的NSSA区域引入了大量外部路由,现用户要求减少非特殊区域的LSA数量,那么该企业工程呵在NSSA区域中的ABR上,对由7类LSA转化成的5类LSA进行路由汇总。",
|
|
132
|
+
"a": "A. TURE"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"n": 382,
|
|
136
|
+
"q": "以下哪些方法能够相对降低OSPF路由计算负荷?",
|
|
137
|
+
"a": "A. 对普通区域路由手动汇总 / C. 部署Stub区域 / E. 划分OSPF非骨干区域"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"n": 439,
|
|
141
|
+
"q": "以下关于OSPFv3类LSA、4类LSA和5类LSA的描述,正确的是哪一项?",
|
|
142
|
+
"a": "C. 5类LSA在穿越不同区域后,不会发生改变,而3类LSA会发生变化"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"n": 339,
|
|
146
|
+
"q": "NSSA区域的ABR不会向NSSA泛洪4类和5类LSA,会将7类LSA转换成5类LSA泛洪给其他区域。",
|
|
147
|
+
"a": "A. 正确"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"n": 157,
|
|
151
|
+
"q": "在OSPF网络中,为减少某些区域的LSA数量,定义了多种特殊区域,如STUB、NSSA等。那么以下关于STUB区域的描述,正确的是哪些项?",
|
|
152
|
+
"a": "A. 虚连接不能穿越STUB区域 / C. STUB区域中的所有路由器都必须配置STUB区域属性"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"n": 266,
|
|
156
|
+
"q": "某企业办公网络中,运行了OSPF路由协议且划分了多个OSPF区域,为减少路由表规模和优化设备资源利用率,该企业网络工程师可在ASBR上执行路由汇总,减少区域间的三类LSA。",
|
|
157
|
+
"a": "B. 错误"
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
"reliability": {
|
|
162
|
+
"scenarios": [
|
|
163
|
+
"vrrp-basics",
|
|
164
|
+
"bfd-fast-convergence",
|
|
165
|
+
"firewall-hrp",
|
|
166
|
+
"dual-homed-isp"
|
|
167
|
+
],
|
|
168
|
+
"points": [
|
|
169
|
+
{
|
|
170
|
+
"n": 50,
|
|
171
|
+
"q": "如图所示,通过配置VRRP与BFD联动,当Backup设备通过BFD感知故障发生之后,等待Master Domn_Timer计时器超时后Backup设备会立即切换VRRP状态。",
|
|
172
|
+
"a": "B. 错误"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"n": 404,
|
|
176
|
+
"q": "VRRP协议包括两个版本VRRPV2和VRRPV3,以下关于VRRPV3版本的描述,正确的是哪些项?",
|
|
177
|
+
"a": "A. VRRPV3适用于IPv4和IPv6两种网络 / C. 设备的缺省优先级为100 / D. 缺省情况下,VRRP通告报文的发送时间间隔为1秒"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"n": 189,
|
|
181
|
+
"q": "某企业WLAN网络,为保证业务的可靠性,部署了VRRP双机热备。用户配置完成后,发现主备通道无法建立,导致双机热备份功能异常。那么造成上述故障的原因,可能是以下哪些?",
|
|
182
|
+
"a": "A. 主备服务报文的重传次数和重传间隔不一致 / C. 主备通道参数配置不匹配 / D. 本端的源IP地址、源端口号和对端的目的IP地址、目的端口号不一致"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"n": 412,
|
|
186
|
+
"q": "以下关于VRRP协议版本的描述,正确的是哪些项?",
|
|
187
|
+
"a": "A. VRRPv2仅适用于IPv4 / C. VRRPv3不支持认证 / D. VRRPv2支持认证"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"n": 324,
|
|
191
|
+
"q": "VRRP目前有两个版本,其中VRRPV2仅适用于IPV4网络,VRRPV3仅适用于IPv6网络。",
|
|
192
|
+
"a": "B. 错误"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"n": 181,
|
|
196
|
+
"q": "BFD作为一个快速故障检测机制,可以和多种协议进行联动。那么以下哪些协议支持和BFD进行联动?",
|
|
197
|
+
"a": "A. 静态路由 / B. BGP / C. OSPF / D. PIM"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"n": 342,
|
|
201
|
+
"q": "BFD提供了一个通用的、标准化的、介质无关的、协议无关的快速故障检测机制,如果想要通过该机制检测链路是否连通,两端设备必须都需要支持该特性。",
|
|
202
|
+
"a": "B. 错误"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"n": 182,
|
|
206
|
+
"q": "BFD的单臂回声功能是指通过BFD报文的环回操作检测链路的连通性,那么以下关于该功能的描述,正确的是哪些项?",
|
|
207
|
+
"a": "A. 单臂回声功能只适用于单跳BFD会话中 / B. 在对端使能了URPF特性的情况下,可能会导致BFD报文在对端被错误地丢弃 / C. 在配置单臂回声功能时,设备缺省使用出接口IP为源IP"
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
"isis": {
|
|
212
|
+
"scenarios": [
|
|
213
|
+
"isis-multi-level"
|
|
214
|
+
],
|
|
215
|
+
"points": [
|
|
216
|
+
{
|
|
217
|
+
"n": 14,
|
|
218
|
+
"q": "四台路由器运行is-is且已经建立邻接关系,区域号和路由器的等级如图中标记,下列说法中正确的有?",
|
|
219
|
+
"a": "A. R2和R3都会产生ATT置位的Level-1的LSP"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"n": 264,
|
|
223
|
+
"q": "在IS-IS网络中,Level-1路由器只能通过Level-1-2路由器接入IS-IS骨干区域从而访问其他区域。",
|
|
224
|
+
"a": "A. 正确"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"n": 234,
|
|
228
|
+
"q": "在IS-IS网络中,区域是根据路由器来划分的,一台路由器只能属于一个区域,因此IS-IS路由器只需要维护当前所在区域的LSDB即可。",
|
|
229
|
+
"a": "B. 错"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"n": 199,
|
|
233
|
+
"q": "在IS-IS网络中,路由器发送LSP报文交换链路状态信息,该报文分为Leve1-1LSP和Leve1-2LSP两种,且有着相同的报文格式。其中,LSP报文中的LSP ID由以下哪…",
|
|
234
|
+
"a": "A. System ID / B. LSP分片后的编号 / C. 伪节点JD"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"n": 375,
|
|
238
|
+
"q": "在广播型网络中,IS-IS协议会选举出一个DIS,而OSPF协议会选举出一个DR。那么以下关于DIS和DR区别的描述,正确的是哪些项?",
|
|
239
|
+
"a": "B. DIS和DR形成邻接关系的条件不同,IS-IS中的非DIS路由器之间也会建立邻接关系 / C. DIS和DR负责的功能不同,DIS主要用于创建和更新伪节点 / D. DIS和DR选举时间不同,DIS在建立完邻接关系之后选举"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"n": 173,
|
|
243
|
+
"q": "某园区网络中,全网运行IS-IS路由协议,现工程师在某一台设备上查看LSDB的详细信息如下所示。那么根据回显信息,以下描述正确的是哪些项?",
|
|
244
|
+
"a": "B. 该LSP来源的区域号为49.0123 / D. 该LSP源节点的系统lD为0100.0000.1001.00"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"n": 208,
|
|
248
|
+
"q": "在lS-IS网络中,每台路由器都可以产生LSP,那么当遇到以下哪些事件时,会触发生成一个新的LSP?",
|
|
249
|
+
"a": "A. 周期性更新 / B. IS-IS相关接口Up或Down / C. 区域间的IP路由发生变化 / D. 调大IS-IS接口开销值"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"n": 19,
|
|
253
|
+
"q": "在广播型IS-IS网络中,需要选举一个DIS来创建和更新伪节点。如图所示为某广播型IS-IS网络,在该场最下会被选举为DIS的是哪一台路由器?",
|
|
254
|
+
"a": "C. R3"
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
},
|
|
258
|
+
"switching": {
|
|
259
|
+
"scenarios": [
|
|
260
|
+
"vlan-access",
|
|
261
|
+
"trunk-interconnect",
|
|
262
|
+
"stp-rstp",
|
|
263
|
+
"mstp-load-balance",
|
|
264
|
+
"link-aggregation",
|
|
265
|
+
"super-vlan",
|
|
266
|
+
"port-security",
|
|
267
|
+
"port-isolation",
|
|
268
|
+
"stack-istack"
|
|
269
|
+
],
|
|
270
|
+
"points": [
|
|
271
|
+
{
|
|
272
|
+
"n": 250,
|
|
273
|
+
"q": "在RSTP网络中,如果一个接口收到一个RSTBPDU,发现自身缓存的RSTBPDU优于收到的RSTBPDU,则会直接丢弃收到的RSTBPDU,不做其他回应。",
|
|
274
|
+
"a": "B. 错误"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"n": 216,
|
|
278
|
+
"q": "在RSTP网络中,针对STP的不足,对端口角色进行了改进。那么RSTP可以包含以下哪些角色?",
|
|
279
|
+
"a": "A. Backup端 / B. 指定端口 / C. Alternate端 / D. 根端口"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"n": 137,
|
|
283
|
+
"q": "针对STP的不足,RSTP改变了配置BPDU的格式,充分利用了STP报文中的以下哪一字段,明确了端口角色?",
|
|
284
|
+
"a": "A. Flag"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"n": 167,
|
|
288
|
+
"q": "在RSTP网络中,新增了多种保护功能,如:根保护、环路保护、防TC-BPDU攻击等。那么以下关于RSTP新增保护功能的描述,正确的是哪些项?",
|
|
289
|
+
"a": "A. 某开启了根保护功能的指定接口收到优先级更高的RST BPDU时,一段时间内将无法转发报文 / C. 若设备开启BPDU保护功能后,会影响边缘端口的属性 / D. 启用防TC-BPDU攻击功能后,对于其他超出阈值的TC BPDU报文,定时器到期后设备只对其统一处理一次"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"n": 405,
|
|
293
|
+
"q": "某大型商场为防止广播域过大,导致网络性能变差,配置了VLAN Pool。网络工程师执行命令display vlan pool name STA的回显如下。那么以下关于该配置信息的…",
|
|
294
|
+
"a": "B. VLAN Pool的名称为STA / D. 加入到VLAN Pool的VLAN ID为2和4"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"n": 325,
|
|
298
|
+
"q": "在STP网络中,为了避免环路,指定端口选举完成后必须等待足够长的时间,使全网的端口状态全部确定后才能进行转发,针对STP上述问题,RSTP通过P/A机制加快了下游端口进人Forw…",
|
|
299
|
+
"a": "A. 正确"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"n": 236,
|
|
303
|
+
"q": "在STP网络中,若拓扑发生变更,必须先将拓扑变化信息传递给根桥,再由根桥向下泛洪拓扑变更信息。",
|
|
304
|
+
"a": "A. 对"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"n": 193,
|
|
308
|
+
"q": "若一个MST域内有多台交换机,那么在部署MSTP时,需要确保以下哪些参数必须一致?",
|
|
309
|
+
"a": "A. 域名 / B. VLAN与生成树实例的映射关系 / C. 都运行了MSTP功能 / D. MSTP修订级别"
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
"multicast": {
|
|
314
|
+
"scenarios": [
|
|
315
|
+
"multicast-pim-sm"
|
|
316
|
+
],
|
|
317
|
+
"points": [
|
|
318
|
+
{
|
|
319
|
+
"n": 345,
|
|
320
|
+
"q": "工程师配置组播协议之前,必须先使能IP组播路由功能,使能IP组播路由功能是配置一切组播功能的前提",
|
|
321
|
+
"a": "A. 正确"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"n": 223,
|
|
325
|
+
"q": "RPF检查是组播中一个极为重要的机制,可以确保转发路径的正确性和唯一性。正常情况下,RPF检查会从以下哪些路由表项中选出最优路由?",
|
|
326
|
+
"a": "A. MBGP路由表 / B. 单播路由表 / D. 组播静态路由表"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"n": 393,
|
|
330
|
+
"q": "作为IP传输三种方式之一,IP组播通信指的是IP报文从一个源发出,被转发到一组特定的接收者。以下关于IP组播技术的描述,正确的是哪些项?",
|
|
331
|
+
"a": "B. 相比广播,可以防止信息泛滥 / C. 相比广播,IP组播可以有效地节约网络带宽"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"n": 427,
|
|
335
|
+
"q": "以下关于IGMP SSM Mapping的描述,正确的是哪些项?",
|
|
336
|
+
"a": "A. SSM组播组的地址是232.0.0.0个232.255.255.255 / C. IGMP SSM Mapping通过静态的将组播源与组播组进行绑定,使得IGMPv1与IGMPv2的组成员也能接入SSM组播网络"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"n": 120,
|
|
340
|
+
"q": "IGMP有三个版本,不同版本支持的特性有所区别。以下哪一项是所有版本均支持的特性?",
|
|
341
|
+
"a": "C. 成员报告报文"
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"n": 142,
|
|
345
|
+
"q": "在IPv4地址空间中,D类地址被用于组播。在D类地址范围内,以下哪一项属于为路由协议预留的永久组播地址?",
|
|
346
|
+
"a": "C. 224.0.0.0-224.0.0.255"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"n": 168,
|
|
350
|
+
"q": "IGMP组表项在组播转发中起到了重要的作用,某管理员通过命令查看一个组播表项信息如下。以下关于该表项的描述,正确的是哪些项?",
|
|
351
|
+
"a": "B. Expires代表了该组的老化时间 / C. 该表项是由用户主机发送的IGMP加入报文触发创建的 / D. 225.1.1.2是用户加入的组地址"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"n": 122,
|
|
355
|
+
"q": "某工程师在配置组播协议时未经过版本比对,结果设备配置的IGMP Snooping版本比用户主机的IGMP版本低。此时会发生以下哪一种情况?",
|
|
356
|
+
"a": "B. 用户无法收到组播数据,因为设备在收到IGMP Report报文后,只会向路由器端口转发,不会生成成员端口和转发表项"
|
|
357
|
+
}
|
|
358
|
+
]
|
|
359
|
+
},
|
|
360
|
+
"ipv6": {
|
|
361
|
+
"scenarios": [
|
|
362
|
+
"ipv6",
|
|
363
|
+
"ospfv3-ipv6"
|
|
364
|
+
],
|
|
365
|
+
"points": [
|
|
366
|
+
{
|
|
367
|
+
"n": 169,
|
|
368
|
+
"q": "IPv6在设计的时候考虑到IPv4的弊端,进行了改良和更新。相较于IPv4协议,IPv6协议具有以下哪些优势?",
|
|
369
|
+
"a": "A. IPv6协议内置支持通过地址自动配置方式使主机自动发现网络并获取IPv6地址 / B. 可实现QoS / C. 支持IPSec的认证和加密"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"n": 255,
|
|
373
|
+
"q": "在IPv6网络中,为了减少中间转发设备的处理压力,中间转发设备不对IPv6报文进行分片,报文的分片只在源节点进行",
|
|
374
|
+
"a": "A. 正确"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"n": 253,
|
|
378
|
+
"q": "某WLAN网络中,AC与AP为三层组网,且AC与AP均采用IPv6地址。此时AP可通过DHCPv6服务器响应报文中所携带的Option43字段来获取AC的IPv6地址fcO0:1…",
|
|
379
|
+
"a": "B. 错误"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"n": 220,
|
|
383
|
+
"q": "以下关于IPv6地址表示方法的描述,正确的是哪些项?",
|
|
384
|
+
"a": "A. 在一个IPv6地址中只能使用一次双冒号\"….\" / B. 每组中的前导“O”都可以省略"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"n": 374,
|
|
388
|
+
"q": "IPv6是网络层协议的第二代标准协议,也被称为lpng。以下关于它和IPv4比较的描述,正确的是哪些项?",
|
|
389
|
+
"a": "A. IPv6地址长度比IPV4大得多,拥有的地址规模也更为庞大 / D. IPv4没有安全件设计,而IPv6中支持到的安全"
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
"mpls": {
|
|
394
|
+
"scenarios": [
|
|
395
|
+
"mpls-basic",
|
|
396
|
+
"mpls-l3vpn"
|
|
397
|
+
],
|
|
398
|
+
"points": [
|
|
399
|
+
{
|
|
400
|
+
"n": 424,
|
|
401
|
+
"q": "作为网络管理员,可以使用下列哪些协议用于网络设备配置?",
|
|
402
|
+
"a": "B. SNMP / C. NETCONF / D. SSH"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"n": 436,
|
|
406
|
+
"q": "MPLS VPN网络一般由运营商搭建)VPN用户购买VPN服务来实现用户网络之间的数据互通。在MPLS VPN网络中存在诸多设备角色,其中以下哪一项设备角色是运营商网络中的骨干路…",
|
|
407
|
+
"a": "D. Provider"
|
|
408
|
+
}
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curriculum knowledge attached to scenarios.
|
|
3
|
+
*
|
|
4
|
+
* The scenario presets say what to build and in what order. They deliberately do
|
|
5
|
+
* not carry commands, because the whole point of the tool is to catch a copied
|
|
6
|
+
* command that does not fit. What they were missing is the vocabulary the lab is
|
|
7
|
+
* teaching: which LSA belongs to which area, which attribute wins a BGP decision,
|
|
8
|
+
* which port role only MSTP has. That is exactly the material a certification
|
|
9
|
+
* question bank covers.
|
|
10
|
+
*
|
|
11
|
+
* This module reads a small generated index instead of the question bank itself:
|
|
12
|
+
* one topic, the scenarios that teach it, and a list of short question/answer
|
|
13
|
+
* pairs. Keeping the bank out of the package matters for two reasons. Most of its
|
|
14
|
+
* questions are three-sentence reading exercises that say nothing, and shipping
|
|
15
|
+
* a copyrighted question bank inside a GPL package is not a reasonable default.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-ensp/knowledge
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'node:fs'
|
|
21
|
+
|
|
22
|
+
let cache = null
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load the generated index, once.
|
|
26
|
+
*
|
|
27
|
+
* Read through `new URL` so the path resolves from this module rather than from
|
|
28
|
+
* whatever directory the caller happens to run in.
|
|
29
|
+
*
|
|
30
|
+
* @returns {object}
|
|
31
|
+
*/
|
|
32
|
+
function catalogue() {
|
|
33
|
+
if (cache) return cache
|
|
34
|
+
const text = readFileSync(new URL('./knowledge.json', import.meta.url), 'utf8')
|
|
35
|
+
cache = JSON.parse(text)
|
|
36
|
+
return cache
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @returns {string[]} the topics the index covers. */
|
|
40
|
+
export function knowledgeTopics() {
|
|
41
|
+
return Object.keys(catalogue().topics).sort()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Knowledge points for one scenario.
|
|
46
|
+
*
|
|
47
|
+
* A topic can feed several scenarios: `ospf` covers six of them, and each gets the
|
|
48
|
+
* same points. That repetition is intentional. A learner running the multi-area
|
|
49
|
+
* lab should not have to know that the material also sits under another scene.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} scenarioId
|
|
52
|
+
* @returns {Array<{topic: string, n: number, q: string, a: string}>}
|
|
53
|
+
*/
|
|
54
|
+
export function knowledgeFor(scenarioId) {
|
|
55
|
+
const out = []
|
|
56
|
+
for (const [topic, entry] of Object.entries(catalogue().topics)) {
|
|
57
|
+
if (!entry.scenarios?.includes(scenarioId)) continue
|
|
58
|
+
for (const point of entry.points || []) out.push({ topic, ...point })
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Search the index by question or answer text.
|
|
65
|
+
*
|
|
66
|
+
* Case-insensitive substring match, same contract as {@link searchScenarios} so the
|
|
67
|
+
* two can be chained in a CLI command without surprising the user.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} query
|
|
70
|
+
* @returns {Array<{topic: string, n: number, q: string, a: string}>}
|
|
71
|
+
*/
|
|
72
|
+
export function searchKnowledge(query) {
|
|
73
|
+
const needle = String(query ?? '').trim().toLowerCase()
|
|
74
|
+
if (!needle) return []
|
|
75
|
+
const out = []
|
|
76
|
+
for (const [topic, entry] of Object.entries(catalogue().topics)) {
|
|
77
|
+
for (const point of entry.points || []) {
|
|
78
|
+
if (`${point.q} ${point.a}`.toLowerCase().includes(needle)) out.push({ topic, ...point })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render the knowledge section for a scenario brief.
|
|
86
|
+
*
|
|
87
|
+
* Returns an empty string when the scenario has none, so callers can append it
|
|
88
|
+
* unconditionally.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} scenarioId
|
|
91
|
+
* @param {object} [options]
|
|
92
|
+
* @param {number} [options.limit=10]
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function renderKnowledge(scenarioId, { limit = 10 } = {}) {
|
|
96
|
+
const points = knowledgeFor(scenarioId).slice(0, limit)
|
|
97
|
+
if (!points.length) return ''
|
|
98
|
+
const lines = ['', '## 知识点(来自认证题库,按主题归类)']
|
|
99
|
+
for (const p of points) {
|
|
100
|
+
lines.push(`- ${p.q}`)
|
|
101
|
+
lines.push(` 答: ${p.a}`)
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n')
|
|
104
|
+
}
|
package/src/dsh/index.mjs
CHANGED
|
@@ -48,6 +48,7 @@ import { pushConfig, pushConfigMany, queryDevice } from '../core/provision.mjs'
|
|
|
48
48
|
import { SCENARIOS, SCENARIO_IDS, renderScenario, searchScenarios, byLevel } from '../core/presets.mjs'
|
|
49
49
|
import { doctor, modelsOf } from '../core/doctor.mjs'
|
|
50
50
|
import { runAcceptance, hasChecks } from '../core/acceptance.mjs'
|
|
51
|
+
import { knowledgeFor, searchKnowledge, knowledgeTopics, renderKnowledge } from '../core/knowledge.mjs'
|
|
51
52
|
import { VERSION } from '../core/version.mjs'
|
|
52
53
|
|
|
53
54
|
/** Declared hard dependency: the tool registry. */
|
|
@@ -598,13 +599,55 @@ export function apply(ctx) {
|
|
|
598
599
|
brief +
|
|
599
600
|
(hasChecks(id)
|
|
600
601
|
? `\n\n验收: ensp_verify { scenario: "${id}", topology: "${id}.topo" }`
|
|
601
|
-
: '\n\n验收: 无专门断言,ensp_verify 会逐条跑场景里的验证命令')
|
|
602
|
+
: '\n\n验收: 无专门断言,ensp_verify 会逐条跑场景里的验证命令') +
|
|
603
|
+
renderKnowledge(id),
|
|
602
604
|
}
|
|
603
605
|
},
|
|
604
606
|
})
|
|
605
607
|
|
|
606
608
|
/* ---------------------------------------------------------- preset config */
|
|
607
609
|
|
|
610
|
+
add(tools, {
|
|
611
|
+
name: 'ensp_knowledge',
|
|
612
|
+
description:
|
|
613
|
+
'Look up curriculum knowledge derived from a certification question bank: short ' +
|
|
614
|
+
'question/answer pairs filed by topic, and the scenarios that teach each topic. ' +
|
|
615
|
+
'Pass scenario to list what one lab teaches, or query to search the index.',
|
|
616
|
+
parameters: {
|
|
617
|
+
query: str('Keyword, e.g. "LSA" or "MED".'),
|
|
618
|
+
scenario: str('Scenario id, e.g. "ospf-lsa-types".'),
|
|
619
|
+
},
|
|
620
|
+
output: {
|
|
621
|
+
schema: obj({
|
|
622
|
+
scope: req(str()),
|
|
623
|
+
count: req(num()),
|
|
624
|
+
points: req(str()),
|
|
625
|
+
error: req(str()),
|
|
626
|
+
}),
|
|
627
|
+
render(args, value) {
|
|
628
|
+
return text(value.points || '(no knowledge point matched)')
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
async execute({ query, scenario }) {
|
|
632
|
+
if (scenario) {
|
|
633
|
+
if (!SCENARIOS[scenario]) throw new Error(`unknown scenario "${scenario}"`)
|
|
634
|
+
const points = knowledgeFor(scenario)
|
|
635
|
+
return {
|
|
636
|
+
scope: scenario,
|
|
637
|
+
count: points.length,
|
|
638
|
+
points: points.map((p) => `#${p.n} [${p.topic}] ${p.q}\n 答: ${p.a}`).join('\n'),
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (!query) throw new Error('pass query or scenario')
|
|
642
|
+
const points = searchKnowledge(query)
|
|
643
|
+
return {
|
|
644
|
+
scope: String(query),
|
|
645
|
+
count: points.length,
|
|
646
|
+
points: points.map((p) => `#${p.n} [${p.topic}] ${p.q}\n 答: ${p.a}`).join('\n'),
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
})
|
|
650
|
+
|
|
608
651
|
add(tools, {
|
|
609
652
|
name: 'ensp_preset_config',
|
|
610
653
|
description: 'Return the configuration text for the built-in two-PC lab.',
|
package/src/mcp/server.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import { calibrateAnchors, inspectAnchors, DEFAULT_ANCHORS } from '../core/calibrate.mjs'
|
|
41
41
|
import { doctor, modelsOf } from '../core/doctor.mjs'
|
|
42
42
|
import { runAcceptance, hasChecks, EXPECTATIONS } from '../core/acceptance.mjs'
|
|
43
|
+
import { knowledgeFor, searchKnowledge, knowledgeTopics } from '../core/knowledge.mjs'
|
|
43
44
|
import { pushConfig, pushConfigMany, queryDevice } from '../core/provision.mjs'
|
|
44
45
|
import { SCENARIOS, SCENARIO_IDS, byLevel, renderScenario, searchScenarios } from '../core/presets.mjs'
|
|
45
46
|
import { PACKAGE_NAME, VERSION } from '../core/version.mjs'
|
|
@@ -331,6 +332,7 @@ const TOOLS = [
|
|
|
331
332
|
hasTopology: hasLab(id),
|
|
332
333
|
hasCuratedChecks: hasChecks(id),
|
|
333
334
|
...(hasLab(id) ? { devices: labDevices(id), addressing: labAddressing(id) } : {}),
|
|
335
|
+
knowledge: knowledgeFor(id),
|
|
334
336
|
}
|
|
335
337
|
},
|
|
336
338
|
},
|
|
@@ -373,6 +375,27 @@ const TOOLS = [
|
|
|
373
375
|
return runAcceptance({ id: scenario, topology, timeoutMs: timeout, waitMs: wait })
|
|
374
376
|
},
|
|
375
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: 'ensp_knowledge',
|
|
380
|
+
description:
|
|
381
|
+
'Search the curriculum knowledge index derived from a certification question bank. ' +
|
|
382
|
+
'Use it to look up the vocabulary a scenario teaches (which LSA belongs to which area, ' +
|
|
383
|
+
'which BGP attribute wins, which port role is MSTP only) without leaving the lab. ' +
|
|
384
|
+
'Returns short question/answer pairs plus the scenarios that teach each topic.',
|
|
385
|
+
inputSchema: obj({
|
|
386
|
+
query: { type: 'string', description: 'keyword, e.g. "LSA" or "MED"' },
|
|
387
|
+
scenario: { type: 'string', description: 'scenario id, to list what that lab teaches' },
|
|
388
|
+
}),
|
|
389
|
+
async run({ query, scenario }) {
|
|
390
|
+
if (scenario) {
|
|
391
|
+
if (!SCENARIOS[scenario]) throw new Error(`unknown scenario "${scenario}"`)
|
|
392
|
+
return { scenario, topics: knowledgeTopics(), points: knowledgeFor(scenario) }
|
|
393
|
+
}
|
|
394
|
+
if (!query) throw new Error('pass query or scenario')
|
|
395
|
+
return { query, points: searchKnowledge(query) }
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
|
|
376
399
|
{
|
|
377
400
|
name: 'ensp_preset_config',
|
|
378
401
|
description: 'Return the configuration that makes the built-in two-PC lab pass traffic.',
|