seaf-netconf-parser 0.1.0__py3-none-any.whl
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.
- lib/device_analyzer.py +2554 -0
- lib/network_visualizer.py +1667 -0
- lib/pattern_validator.py +274 -0
- lib/seaf_converter.py +874 -0
- main.py +111 -0
- patterns/devices/arista.json +54 -0
- patterns/devices/aruba.json +54 -0
- patterns/devices/b4com.json +104 -0
- patterns/devices/cisco.json +476 -0
- patterns/devices/cisco_asa.json +134 -0
- patterns/devices/dell.json +55 -0
- patterns/devices/dlink.json +105 -0
- patterns/devices/f5.json +61 -0
- patterns/devices/fortinet.json +52 -0
- patterns/devices/hpe_aruba.json +125 -0
- patterns/devices/huawei.json +108 -0
- patterns/devices/juniper.json +82 -0
- patterns/devices/mikrotik.json +129 -0
- patterns/devices/paloalto.json +52 -0
- patterns/devices/qtech.json +108 -0
- patterns/devices/tfortis.json +102 -0
- patterns/devices/unknown.json +99 -0
- patterns/drawio/base.drawio +12 -0
- patterns/drawio/templates/index.yaml +56 -0
- patterns/drawio/templates/stencils.yaml +32 -0
- patterns/schema.json +340 -0
- patterns/seaf/base_entity.yaml +40 -0
- patterns/seaf/device.yaml +84 -0
- patterns/seaf/logical_link.yaml +82 -0
- patterns/seaf/network.yaml +117 -0
- seaf_netconf_parser-0.1.0.dist-info/METADATA +175 -0
- seaf_netconf_parser-0.1.0.dist-info/RECORD +35 -0
- seaf_netconf_parser-0.1.0.dist-info/WHEEL +4 -0
- seaf_netconf_parser-0.1.0.dist-info/entry_points.txt +2 -0
- seaf_netconf_parser-0.1.0.dist-info/licenses/LICENSE +21 -0
lib/device_analyzer.py
ADDED
|
@@ -0,0 +1,2554 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль анализа сетевого оборудования и топологии
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict, Any, Tuple, Set, Union
|
|
9
|
+
import ipaddress
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from .pattern_validator import PatternValidator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VendorPatternLoader:
|
|
15
|
+
"""Загрузчик шаблонов распознавания вендоров из JSON-файлов."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, patterns_dir: Union[str, Path], validate: bool = True):
|
|
18
|
+
self.patterns_dir = Path(patterns_dir).resolve()
|
|
19
|
+
self.patterns: List[Dict[str, Any]] = []
|
|
20
|
+
self.validator: PatternValidator = None
|
|
21
|
+
|
|
22
|
+
# Инициализация валидатора если включена валидация
|
|
23
|
+
if validate:
|
|
24
|
+
schema_path = self.patterns_dir.parent / "schema.json"
|
|
25
|
+
if schema_path.exists():
|
|
26
|
+
self.validator = PatternValidator(str(schema_path))
|
|
27
|
+
|
|
28
|
+
def load_patterns(self) -> List[Dict[str, Any]]:
|
|
29
|
+
"""Загружает все шаблоны из каталога с опциональной валидацией."""
|
|
30
|
+
if not self.patterns_dir.exists():
|
|
31
|
+
sys.stderr.write(f"❌ Каталог шаблонов не найден: {self.patterns_dir}\n")
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
self.patterns = []
|
|
35
|
+
errors_found = False
|
|
36
|
+
|
|
37
|
+
for filepath in self.patterns_dir.glob("*.json"):
|
|
38
|
+
try:
|
|
39
|
+
with open(filepath, 'r', encoding='utf-8-sig') as f:
|
|
40
|
+
pattern = json.load(f)
|
|
41
|
+
|
|
42
|
+
# Валидация шаблона
|
|
43
|
+
if self.validator:
|
|
44
|
+
is_valid, validation_errors = self.validator.validate(pattern)
|
|
45
|
+
if not is_valid:
|
|
46
|
+
sys.stderr.write(f"❌ Ошибки валидации в {filepath.name}:\n")
|
|
47
|
+
for error in validation_errors:
|
|
48
|
+
sys.stderr.write(f" • {error}\n")
|
|
49
|
+
errors_found = True
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
pattern['_source_file'] = filepath.name
|
|
53
|
+
self.patterns.append(pattern)
|
|
54
|
+
status = "✅" if not self.validator else "✅✓"
|
|
55
|
+
print(
|
|
56
|
+
f"{status} Загружен шаблон: {filepath.name} "
|
|
57
|
+
f"(версия {pattern.get('version', 'unknown')})"
|
|
58
|
+
)
|
|
59
|
+
except json.JSONDecodeError as e:
|
|
60
|
+
sys.stderr.write(f"❌ Ошибка JSON в файле {filepath.name}: {e}\n")
|
|
61
|
+
errors_found = True
|
|
62
|
+
except Exception as e:
|
|
63
|
+
sys.stderr.write(f"❌ Ошибка в файле {filepath.name}: {e}\n")
|
|
64
|
+
errors_found = True
|
|
65
|
+
|
|
66
|
+
if not self.patterns:
|
|
67
|
+
sys.stderr.write(f"⚠️ В каталоге {self.patterns_dir} не найдено корректных шаблонов (*.json)\n")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
if errors_found:
|
|
71
|
+
sys.stderr.write("\n⚠️ Некоторые шаблоны не загружены из-за ошибок\n")
|
|
72
|
+
|
|
73
|
+
return self.patterns
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class NetworkDevice:
|
|
77
|
+
"""Представление сетевого устройства с методами анализа конфигурации."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, filepath: Union[str, Path], vendor_patterns: List[Dict[str, Any]]):
|
|
80
|
+
self.filepath = Path(filepath).resolve()
|
|
81
|
+
self.filename = self.filepath.name
|
|
82
|
+
self.vendor_patterns = vendor_patterns
|
|
83
|
+
self.content: str = ""
|
|
84
|
+
self.content_lines: List[str] = []
|
|
85
|
+
self.content_lower: str = ""
|
|
86
|
+
self.vendor: str = "unknown"
|
|
87
|
+
self.device_name: str = "unknown"
|
|
88
|
+
self.model: str = "unknown"
|
|
89
|
+
self.device_type: str = "unknown"
|
|
90
|
+
self.routing_networks: List[Dict[str, str]] = []
|
|
91
|
+
self.total_vlans: int = 0
|
|
92
|
+
self.active_vlans: List[int] = []
|
|
93
|
+
self.all_vlans: List[int] = []
|
|
94
|
+
|
|
95
|
+
def load_content(self) -> bool:
|
|
96
|
+
"""Загружает содержимое конфигурационного файла."""
|
|
97
|
+
try:
|
|
98
|
+
with open(self.filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
|
99
|
+
self.content = f.read()
|
|
100
|
+
self.content_lines = [line.rstrip() for line in self.content.splitlines() if line.strip()]
|
|
101
|
+
self.content_lower = self.content.lower()
|
|
102
|
+
return True
|
|
103
|
+
except Exception as e:
|
|
104
|
+
sys.stderr.write(f"❌ Ошибка чтения файла {self.filename}: {e}\n")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def _match_patterns(self, patterns: List[str], case_insensitive: bool = True) -> bool:
|
|
108
|
+
"""Проверяет совпадение любого паттерна с содержимым файла."""
|
|
109
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
110
|
+
for line in self.content_lines:
|
|
111
|
+
for pattern in patterns:
|
|
112
|
+
if re.search(pattern, line, flags):
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _extract_with_pattern(self, patterns: List[Dict]) -> str:
|
|
117
|
+
"""Извлекает значение по списку паттернов."""
|
|
118
|
+
for p in patterns:
|
|
119
|
+
if p.get("multiline", False):
|
|
120
|
+
match = re.search(p["pattern"], self.content, re.IGNORECASE | re.DOTALL)
|
|
121
|
+
else:
|
|
122
|
+
match = None
|
|
123
|
+
for line in self.content_lines:
|
|
124
|
+
match = re.search(p["pattern"], line, re.IGNORECASE)
|
|
125
|
+
if match:
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if match:
|
|
129
|
+
value = match.group(p.get("group", 1)).strip()
|
|
130
|
+
if p.get("clean", True):
|
|
131
|
+
value = re.sub(r'[^\w.\-_]', '', value).strip()
|
|
132
|
+
if not value and p.get("fallback", False):
|
|
133
|
+
value = match.group(p.get("group", 1)).strip()
|
|
134
|
+
return value
|
|
135
|
+
return "unknown"
|
|
136
|
+
|
|
137
|
+
def _extract_model_with_fallback(self, patterns: List[Dict], vendor: str) -> str:
|
|
138
|
+
"""Извлекает модель с fallback-логикой из шаблона JSON."""
|
|
139
|
+
model = self._extract_with_pattern(patterns)
|
|
140
|
+
|
|
141
|
+
# Если модель извлечена паттернами, нормализуем и возвращаем её
|
|
142
|
+
if model != "unknown":
|
|
143
|
+
normalized_model = self._normalize_model_name(model)
|
|
144
|
+
return normalized_model
|
|
145
|
+
|
|
146
|
+
# Fallback-логика применяется только если модель не была извлечена паттернами
|
|
147
|
+
# Ищем шаблон для текущего вендора и применяем fallback-правила
|
|
148
|
+
vendor_pattern = next((p for p in self.vendor_patterns if p["vendor"] == vendor), None)
|
|
149
|
+
if vendor_pattern and "model_fallback_rules" in vendor_pattern:
|
|
150
|
+
for rule in vendor_pattern["model_fallback_rules"]:
|
|
151
|
+
conditions = rule.get("conditions", {})
|
|
152
|
+
matched = False
|
|
153
|
+
|
|
154
|
+
# Проверка условия "all" (все паттерны должны совпасть)
|
|
155
|
+
if "all" in conditions:
|
|
156
|
+
matched = all(self._check_condition_pattern(pat) for pat in conditions["all"])
|
|
157
|
+
|
|
158
|
+
# Проверка условия "any" (хотя бы один паттерн должен совпасть)
|
|
159
|
+
if "any" in conditions:
|
|
160
|
+
any_matched = any(self._check_condition_pattern(pat) for pat in conditions["any"])
|
|
161
|
+
matched = matched and any_matched if "all" in conditions else any_matched
|
|
162
|
+
|
|
163
|
+
# Если условий нет, считаем правило неприменимым
|
|
164
|
+
if not conditions:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if matched:
|
|
168
|
+
return rule.get("model", "unknown")
|
|
169
|
+
|
|
170
|
+
return "unknown"
|
|
171
|
+
|
|
172
|
+
def _check_condition_pattern(self, pattern: str) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Проверяет условие паттерна.
|
|
175
|
+
|
|
176
|
+
Если паттерн содержит regex-символы (.*, +, ?, ^, $), используется regex-поиск.
|
|
177
|
+
Иначе используется простой поиск подстроки.
|
|
178
|
+
"""
|
|
179
|
+
# Символы, указывающие на regex-паттерн
|
|
180
|
+
regex_chars = ['.*', '.+', '+', '?', '^', '$', '\\d', '\\w', '\\s', '[', ']', '(', ')']
|
|
181
|
+
|
|
182
|
+
is_regex = any(rc in pattern for rc in regex_chars)
|
|
183
|
+
|
|
184
|
+
if is_regex:
|
|
185
|
+
# Используем regex-поиск по всему содержимому
|
|
186
|
+
try:
|
|
187
|
+
return bool(re.search(pattern, self.content, re.IGNORECASE))
|
|
188
|
+
except re.error:
|
|
189
|
+
# Если паттерн некорректен, пробуем простой поиск
|
|
190
|
+
return pattern.lower() in self.content_lower
|
|
191
|
+
else:
|
|
192
|
+
# Простой поиск подстроки
|
|
193
|
+
return pattern.lower() in self.content_lower
|
|
194
|
+
|
|
195
|
+
def _normalize_model_name(self, model: str) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Нормализует имя модели, преобразуя имена образов IOS в названия моделей.
|
|
198
|
+
|
|
199
|
+
Например:
|
|
200
|
+
- cat4500e-lanbase-mz → Catalyst 4500-E
|
|
201
|
+
- cat3750-lanbase → Catalyst 3750
|
|
202
|
+
- CISCO2951/K9 → Cisco 2951
|
|
203
|
+
- CISCO1941/K9 → Cisco 1941
|
|
204
|
+
"""
|
|
205
|
+
if model == "unknown":
|
|
206
|
+
return model
|
|
207
|
+
|
|
208
|
+
model_lower = model.lower()
|
|
209
|
+
|
|
210
|
+
# Словарь преобразований имен образов в модели
|
|
211
|
+
ios_image_to_model = {
|
|
212
|
+
"cat4500e": "Catalyst 4500-E",
|
|
213
|
+
"cat4500": "Catalyst 4500",
|
|
214
|
+
"cat3750": "Catalyst 3750",
|
|
215
|
+
"cat3650": "Catalyst 3650",
|
|
216
|
+
"cat3560": "Catalyst 3560",
|
|
217
|
+
"cat2960": "Catalyst 2960",
|
|
218
|
+
"cat9300": "Catalyst 9300",
|
|
219
|
+
"cat9200": "Catalyst 9200",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Проверяем, начинается ли имя модели с известного префикса
|
|
223
|
+
for image_prefix, model_name in ios_image_to_model.items():
|
|
224
|
+
if model_lower.startswith(image_prefix):
|
|
225
|
+
return model_name
|
|
226
|
+
|
|
227
|
+
# Нормализация моделей ISR (CISCO2951/K9 → Cisco 2951, CISCO2951K → Cisco 2951, CISCO861-PCI-K9 → Cisco 861)
|
|
228
|
+
isar_match = re.match(r'^cisco(\d+)(?:[/-]?k9|[a-z\-]*-k9|k)?$', model_lower)
|
|
229
|
+
if isar_match:
|
|
230
|
+
return f"Cisco {isar_match.group(1)}"
|
|
231
|
+
|
|
232
|
+
# Нормализация моделей из boot system (c2800, c2801, c3800 и т.д.)
|
|
233
|
+
boot_match = re.match(r'^c(\d+)$', model_lower)
|
|
234
|
+
if boot_match:
|
|
235
|
+
num = boot_match.group(1)
|
|
236
|
+
if num.startswith('28') or num.startswith('38'):
|
|
237
|
+
return f"Cisco {num[0]}800 ISR"
|
|
238
|
+
elif num.startswith('29') or num.startswith('39'):
|
|
239
|
+
return f"Cisco {num[0]}900 ISR"
|
|
240
|
+
elif num.startswith('8'):
|
|
241
|
+
return f"Cisco {num} Series"
|
|
242
|
+
elif num.startswith('19') or num.startswith('29') or num.startswith('39'):
|
|
243
|
+
return f"Cisco {num} ISR"
|
|
244
|
+
|
|
245
|
+
# Нормализация моделей ASA (Ethernet0/0 → ASA 5505, GigabitEthernet0/0 → ASA 5500-X)
|
|
246
|
+
if model_lower.startswith('ethernet'):
|
|
247
|
+
return "ASA 5505"
|
|
248
|
+
if model_lower.startswith('gigabitethernet'):
|
|
249
|
+
return "ASA 5500-X"
|
|
250
|
+
|
|
251
|
+
return model
|
|
252
|
+
|
|
253
|
+
def _infer_type_by_features(self, type_rules: List[Dict]) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Определяет тип устройства по наличию функций.
|
|
256
|
+
|
|
257
|
+
Поддерживает два формата правил:
|
|
258
|
+
1. Старый формат (type_inference): {"any": [...], "type": "...", "score": N}
|
|
259
|
+
2. Новый формат (type_rules): {"any"/"all"/"not": [...], "type": "...", "score": N}
|
|
260
|
+
"""
|
|
261
|
+
best_type = "unknown"
|
|
262
|
+
best_score = -1
|
|
263
|
+
|
|
264
|
+
for rule in type_rules:
|
|
265
|
+
score = rule.get("score", 1)
|
|
266
|
+
matched = self._check_type_conditions(rule)
|
|
267
|
+
|
|
268
|
+
if matched and score > best_score:
|
|
269
|
+
best_score = score
|
|
270
|
+
best_type = rule["type"]
|
|
271
|
+
|
|
272
|
+
return best_type
|
|
273
|
+
|
|
274
|
+
def _check_type_conditions(self, rule: Dict) -> bool:
|
|
275
|
+
"""
|
|
276
|
+
Проверяет условия правила для определения типа устройства.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
rule: Правило с условиями (any, all, not)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True если все условия выполнены
|
|
283
|
+
"""
|
|
284
|
+
matched = False
|
|
285
|
+
|
|
286
|
+
# Проверка условия "any" (хотя бы один паттерн)
|
|
287
|
+
if "any" in rule:
|
|
288
|
+
matched = any(pat.lower() in self.content_lower for pat in rule["any"])
|
|
289
|
+
|
|
290
|
+
# Проверка условия "all" (все паттерны)
|
|
291
|
+
if "all" in rule:
|
|
292
|
+
all_matched = all(pat.lower() in self.content_lower for pat in rule["all"])
|
|
293
|
+
matched = matched and all_matched if "any" in rule else all_matched
|
|
294
|
+
|
|
295
|
+
# Проверка условия "not" (ни один паттерн не должен совпасть)
|
|
296
|
+
if "not" in rule:
|
|
297
|
+
not_matched = not any(pat.lower() in self.content_lower for pat in rule["not"])
|
|
298
|
+
matched = matched and not_matched
|
|
299
|
+
|
|
300
|
+
return matched
|
|
301
|
+
|
|
302
|
+
def _extract_networks_and_vlans(self, rules: Dict) -> Dict[str, Any]:
|
|
303
|
+
"""Извлекает сети и VLAN согласно правилам шаблона.
|
|
304
|
+
|
|
305
|
+
Использует универсальный метод _extract_interfaces() для извлечения сетей,
|
|
306
|
+
что исключает дублирование логики с _extract_all_ip_interfaces().
|
|
307
|
+
"""
|
|
308
|
+
result = {
|
|
309
|
+
"routing_networks": [],
|
|
310
|
+
"total_vlans": 0,
|
|
311
|
+
"active_vlans": [],
|
|
312
|
+
"all_vlans": []
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Извлечение интерфейсов с IP через универсальный метод
|
|
316
|
+
# Это заменяет сложную логику с определением формата (MikroTik/однострочный/многострочный)
|
|
317
|
+
routing_networks, _ = self._extract_interfaces(extract_all=False)
|
|
318
|
+
result["routing_networks"] = routing_networks
|
|
319
|
+
|
|
320
|
+
# Извлечение VLAN
|
|
321
|
+
if "vlans" in rules:
|
|
322
|
+
vlan_set = set()
|
|
323
|
+
active_set = set()
|
|
324
|
+
|
|
325
|
+
for line in self.content_lines:
|
|
326
|
+
if "all_pattern" in rules["vlans"]:
|
|
327
|
+
for match in re.finditer(rules["vlans"]["all_pattern"], line, re.IGNORECASE):
|
|
328
|
+
try:
|
|
329
|
+
vid = int(match.group(1))
|
|
330
|
+
vlan_set.add(vid)
|
|
331
|
+
except (ValueError, IndexError):
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
if "active_pattern" in rules["vlans"]:
|
|
335
|
+
for match in re.finditer(rules["vlans"]["active_pattern"], line, re.IGNORECASE):
|
|
336
|
+
try:
|
|
337
|
+
vid = int(match.group(1))
|
|
338
|
+
active_set.add(vid)
|
|
339
|
+
vlan_set.add(vid)
|
|
340
|
+
except (ValueError, IndexError):
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
result["all_vlans"] = sorted(vlan_set)
|
|
344
|
+
result["active_vlans"] = sorted(active_set)
|
|
345
|
+
result["total_vlans"] = len(vlan_set)
|
|
346
|
+
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
def analyze(self) -> bool:
|
|
350
|
+
"""Выполняет полный анализ конфигурации устройства."""
|
|
351
|
+
if not self.load_content():
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
# Этап 1: Определение вендора
|
|
355
|
+
vendor_scores = Counter()
|
|
356
|
+
for pattern in self.vendor_patterns:
|
|
357
|
+
vendor = pattern["vendor"]
|
|
358
|
+
signatures = pattern.get("vendor_signatures", [])
|
|
359
|
+
if signatures:
|
|
360
|
+
score = sum(
|
|
361
|
+
any(re.search(sig, line, re.IGNORECASE) for line in self.content_lines)
|
|
362
|
+
for sig in signatures
|
|
363
|
+
)
|
|
364
|
+
if score > 0:
|
|
365
|
+
vendor_scores[vendor] = score
|
|
366
|
+
|
|
367
|
+
if not vendor_scores:
|
|
368
|
+
for pattern in self.vendor_patterns:
|
|
369
|
+
if self._match_patterns(pattern.get("detect_patterns", [])):
|
|
370
|
+
vendor_scores[pattern["vendor"]] = 1
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
if not vendor_scores:
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
self.vendor = vendor_scores.most_common(1)[0][0]
|
|
377
|
+
pattern = next(p for p in self.vendor_patterns if p["vendor"] == self.vendor)
|
|
378
|
+
|
|
379
|
+
# Этап 2: Извлечение данных
|
|
380
|
+
self.device_name = self._extract_with_pattern(pattern.get("name_patterns", []))
|
|
381
|
+
|
|
382
|
+
# Fallback: если имя не извлечено, используем имя файла без расширения
|
|
383
|
+
if self.device_name == "unknown":
|
|
384
|
+
self.device_name = self.filename.rsplit('.', 1)[0]
|
|
385
|
+
|
|
386
|
+
self.model = self._extract_model_with_fallback(pattern.get("model_patterns", []), self.vendor)
|
|
387
|
+
|
|
388
|
+
# Этап 3: Определение типа
|
|
389
|
+
# Приоритет: type_rules (новый формат) > type_inference (старый формат)
|
|
390
|
+
type_rules = pattern.get("type_rules") or pattern.get("type_inference")
|
|
391
|
+
if type_rules:
|
|
392
|
+
self.device_type = self._infer_type_by_features(type_rules)
|
|
393
|
+
if self.device_type == "unknown":
|
|
394
|
+
self.device_type = pattern.get("default_device_type", "unknown")
|
|
395
|
+
|
|
396
|
+
# Этап 4: Извлечение сетей и VLAN
|
|
397
|
+
rules = pattern.get("network_extraction_rules", {})
|
|
398
|
+
network_info = self._extract_networks_and_vlans(rules)
|
|
399
|
+
self.routing_networks = network_info["routing_networks"]
|
|
400
|
+
self.total_vlans = network_info["total_vlans"]
|
|
401
|
+
self.active_vlans = network_info["active_vlans"]
|
|
402
|
+
self.all_vlans = network_info["all_vlans"]
|
|
403
|
+
|
|
404
|
+
# Дополнительное извлечение всех IP интерфейсов для b4com
|
|
405
|
+
self.all_ip_interfaces = self._extract_all_ip_interfaces()
|
|
406
|
+
|
|
407
|
+
# Этап 5: Извлечение дополнительной информации (BGP, Port-Channel, VXLAN, Management)
|
|
408
|
+
bgp_rules = pattern.get("bgp_extraction_rules", {})
|
|
409
|
+
if bgp_rules.get("enabled"):
|
|
410
|
+
self.bgp_info = self._extract_bgp_info(bgp_rules)
|
|
411
|
+
else:
|
|
412
|
+
self.bgp_info = {}
|
|
413
|
+
|
|
414
|
+
pc_rules = pattern.get("port_channel_extraction_rules", {})
|
|
415
|
+
if pc_rules.get("enabled"):
|
|
416
|
+
self.port_channels = self._extract_port_channels(pc_rules)
|
|
417
|
+
else:
|
|
418
|
+
self.port_channels = []
|
|
419
|
+
|
|
420
|
+
vxlan_rules = pattern.get("vxlan_extraction_rules", {})
|
|
421
|
+
if vxlan_rules.get("enabled"):
|
|
422
|
+
self.vxlan_info = self._extract_vxlan_info(vxlan_rules)
|
|
423
|
+
else:
|
|
424
|
+
self.vxlan_info = {}
|
|
425
|
+
|
|
426
|
+
mgmt_rules = pattern.get("management_extraction_rules", {})
|
|
427
|
+
if mgmt_rules.get("enabled"):
|
|
428
|
+
self.management_info = self._extract_management_info(mgmt_rules)
|
|
429
|
+
else:
|
|
430
|
+
self.management_info = {}
|
|
431
|
+
|
|
432
|
+
# Этап 6: Извлечение маршрутов (routing extraction)
|
|
433
|
+
routing_rules = pattern.get("routing_extraction_rules", {})
|
|
434
|
+
if routing_rules.get("enabled"):
|
|
435
|
+
self.routing_paths = self._extract_routing_paths(routing_rules)
|
|
436
|
+
else:
|
|
437
|
+
self.routing_paths = []
|
|
438
|
+
|
|
439
|
+
# Этап 7: Извлечение VRF информации
|
|
440
|
+
vrf_rules = pattern.get("vrf_extraction_rules", {})
|
|
441
|
+
if vrf_rules.get("enabled"):
|
|
442
|
+
self.vrf_info = self._extract_vrf_info(vrf_rules)
|
|
443
|
+
else:
|
|
444
|
+
self.vrf_info = {}
|
|
445
|
+
|
|
446
|
+
# Этап 8: Извлечение OSPF информации
|
|
447
|
+
ospf_rules = pattern.get("ospf_extraction_rules", {})
|
|
448
|
+
if ospf_rules.get("enabled"):
|
|
449
|
+
self.ospf_info = self._extract_ospf_info(ospf_rules)
|
|
450
|
+
else:
|
|
451
|
+
self.ospf_info = {}
|
|
452
|
+
|
|
453
|
+
# Этап 9: Извлечение LLDP информации
|
|
454
|
+
lldp_rules = pattern.get("lldp_extraction_rules", {})
|
|
455
|
+
if lldp_rules.get("enabled"):
|
|
456
|
+
self.lldp_info = self._extract_lldp_info(lldp_rules)
|
|
457
|
+
else:
|
|
458
|
+
self.lldp_info = {}
|
|
459
|
+
|
|
460
|
+
# Этап 10: Извлечение статуса интерфейсов
|
|
461
|
+
intf_status_rules = pattern.get("interface_status_rules", {})
|
|
462
|
+
if intf_status_rules.get("enabled"):
|
|
463
|
+
self.interface_status = self._extract_interface_status(intf_status_rules)
|
|
464
|
+
else:
|
|
465
|
+
self.interface_status = {}
|
|
466
|
+
|
|
467
|
+
# Этап 11: Извлечение StackWise информации
|
|
468
|
+
stackwise_rules = pattern.get("stackwise_extraction_rules", {})
|
|
469
|
+
if stackwise_rules.get("enabled"):
|
|
470
|
+
self.stackwise_info = self._extract_stackwise_info(stackwise_rules)
|
|
471
|
+
else:
|
|
472
|
+
self.stackwise_info = {}
|
|
473
|
+
|
|
474
|
+
# Этап 12: Извлечение QoS информации
|
|
475
|
+
qos_rules = pattern.get("qos_extraction_rules", {})
|
|
476
|
+
if qos_rules.get("enabled"):
|
|
477
|
+
self.qos_info = self._extract_qos_info(qos_rules)
|
|
478
|
+
else:
|
|
479
|
+
self.qos_info = {}
|
|
480
|
+
|
|
481
|
+
# Этап 13: Извлечение Power информации
|
|
482
|
+
power_rules = pattern.get("power_extraction_rules", {})
|
|
483
|
+
if power_rules.get("enabled"):
|
|
484
|
+
self.power_info = self._extract_power_info(power_rules)
|
|
485
|
+
else:
|
|
486
|
+
self.power_info = {}
|
|
487
|
+
|
|
488
|
+
# Этап 14: Извлечение UDLD информации
|
|
489
|
+
udld_rules = pattern.get("udld_extraction_rules", {})
|
|
490
|
+
if udld_rules.get("enabled"):
|
|
491
|
+
self.udld_info = self._extract_udld_info(udld_rules)
|
|
492
|
+
else:
|
|
493
|
+
self.udld_info = {}
|
|
494
|
+
|
|
495
|
+
# Этап 15: Добавление BGP local адресов в routing_networks
|
|
496
|
+
if hasattr(self, 'bgp_info') and self.bgp_info.get('local_addresses'):
|
|
497
|
+
for local_ip in self.bgp_info['local_addresses']:
|
|
498
|
+
# Добавляем как /32 сеть
|
|
499
|
+
self.routing_networks.append({
|
|
500
|
+
"interface": "BGP",
|
|
501
|
+
"network": f"{local_ip}/32"
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
return True
|
|
505
|
+
|
|
506
|
+
def _extract_bgp_info(self, rules: Dict) -> Dict[str, Any]:
|
|
507
|
+
"""Извлекает информацию о BGP конфигурации."""
|
|
508
|
+
result = {
|
|
509
|
+
"enabled": False,
|
|
510
|
+
"asn": None,
|
|
511
|
+
"router_id": None,
|
|
512
|
+
"neighbors": [],
|
|
513
|
+
"evpn_neighbors": [],
|
|
514
|
+
"address_families": [],
|
|
515
|
+
"local_addresses": []
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
neighbor_descriptions = {}
|
|
519
|
+
|
|
520
|
+
for line in self.content_lines:
|
|
521
|
+
# ASN
|
|
522
|
+
if rules.get("asn_pattern"):
|
|
523
|
+
match = re.search(rules["asn_pattern"], line, re.IGNORECASE)
|
|
524
|
+
if match:
|
|
525
|
+
result["enabled"] = True
|
|
526
|
+
result["asn"] = match.group(1)
|
|
527
|
+
|
|
528
|
+
# Router ID
|
|
529
|
+
if rules.get("router_id_pattern"):
|
|
530
|
+
match = re.search(rules["router_id_pattern"], line, re.IGNORECASE)
|
|
531
|
+
if match:
|
|
532
|
+
result["router_id"] = match.group(1)
|
|
533
|
+
|
|
534
|
+
# Local address (MikroTik BGP local.address)
|
|
535
|
+
if rules.get("local_address_pattern"):
|
|
536
|
+
match = re.search(rules["local_address_pattern"], line, re.IGNORECASE)
|
|
537
|
+
if match:
|
|
538
|
+
local_addr = match.group(1)
|
|
539
|
+
if local_addr not in result["local_addresses"]:
|
|
540
|
+
result["local_addresses"].append(local_addr)
|
|
541
|
+
|
|
542
|
+
# Neighbor с remote-as
|
|
543
|
+
if rules.get("neighbor_pattern"):
|
|
544
|
+
match = re.search(rules["neighbor_pattern"], line, re.IGNORECASE)
|
|
545
|
+
if match:
|
|
546
|
+
neighbor_ip = match.group(1)
|
|
547
|
+
neighbor_as = match.group(2)
|
|
548
|
+
# Проверяем, есть ли уже такой сосед
|
|
549
|
+
existing = next((n for n in result["neighbors"] if n["ip"] == neighbor_ip), None)
|
|
550
|
+
if existing:
|
|
551
|
+
existing["remote_as"] = neighbor_as
|
|
552
|
+
else:
|
|
553
|
+
result["neighbors"].append({
|
|
554
|
+
"ip": neighbor_ip,
|
|
555
|
+
"remote_as": neighbor_as,
|
|
556
|
+
"description": neighbor_descriptions.get(neighbor_ip, ""),
|
|
557
|
+
"evpn_enabled": False
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
# Neighbor description
|
|
561
|
+
if rules.get("neighbor_desc_pattern"):
|
|
562
|
+
match = re.search(rules["neighbor_desc_pattern"], line, re.IGNORECASE)
|
|
563
|
+
if match:
|
|
564
|
+
neighbor_ip = match.group(1)
|
|
565
|
+
description = match.group(2)
|
|
566
|
+
neighbor_descriptions[neighbor_ip] = description
|
|
567
|
+
# Обновляем существующего соседа
|
|
568
|
+
existing = next((n for n in result["neighbors"] if n["ip"] == neighbor_ip), None)
|
|
569
|
+
if existing:
|
|
570
|
+
existing["description"] = description
|
|
571
|
+
|
|
572
|
+
# Address-family
|
|
573
|
+
if rules.get("address_family_pattern"):
|
|
574
|
+
match = re.search(rules["address_family_pattern"], line, re.IGNORECASE)
|
|
575
|
+
if match:
|
|
576
|
+
af = f"{match.group(1)} {match.group(2)}"
|
|
577
|
+
if af not in result["address_families"]:
|
|
578
|
+
result["address_families"].append(af)
|
|
579
|
+
|
|
580
|
+
# EVPN neighbor activate
|
|
581
|
+
if rules.get("evpn_neighbors_pattern"):
|
|
582
|
+
match = re.search(rules["evpn_neighbors_pattern"], line, re.IGNORECASE)
|
|
583
|
+
if match:
|
|
584
|
+
neighbor_ip = match.group(1)
|
|
585
|
+
existing = next((n for n in result["neighbors"] if n["ip"] == neighbor_ip), None)
|
|
586
|
+
if existing:
|
|
587
|
+
existing["evpn_enabled"] = True
|
|
588
|
+
result["evpn_neighbors"].append(neighbor_ip)
|
|
589
|
+
|
|
590
|
+
# Применяем описания ко всем соседям
|
|
591
|
+
for neighbor in result["neighbors"]:
|
|
592
|
+
if not neighbor["description"]:
|
|
593
|
+
neighbor["description"] = neighbor_descriptions.get(neighbor["ip"], "")
|
|
594
|
+
|
|
595
|
+
return result
|
|
596
|
+
|
|
597
|
+
def _extract_port_channels(self, rules: Dict) -> List[Dict[str, Any]]:
|
|
598
|
+
"""Извлекает информацию о Port-Channel интерфейсах."""
|
|
599
|
+
port_channels = []
|
|
600
|
+
current_pc = None
|
|
601
|
+
|
|
602
|
+
for line in self.content_lines:
|
|
603
|
+
# Новый Port-Channel
|
|
604
|
+
if rules.get("port_channel_pattern"):
|
|
605
|
+
match = re.search(rules["port_channel_pattern"], line, re.IGNORECASE)
|
|
606
|
+
if match:
|
|
607
|
+
if current_pc:
|
|
608
|
+
port_channels.append(current_pc)
|
|
609
|
+
current_pc = {
|
|
610
|
+
"name": match.group(1),
|
|
611
|
+
"description": "",
|
|
612
|
+
"members": [],
|
|
613
|
+
"mode": "",
|
|
614
|
+
"vlans": "",
|
|
615
|
+
"shutdown": False
|
|
616
|
+
}
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
if current_pc:
|
|
620
|
+
# Description
|
|
621
|
+
if rules.get("port_channel_desc_pattern"):
|
|
622
|
+
match = re.search(rules["port_channel_desc_pattern"], line, re.IGNORECASE)
|
|
623
|
+
if match:
|
|
624
|
+
current_pc["description"] = match.group(1).strip()
|
|
625
|
+
|
|
626
|
+
# Channel-group members
|
|
627
|
+
if rules.get("port_channel_members_pattern"):
|
|
628
|
+
match = re.search(rules["port_channel_members_pattern"], line, re.IGNORECASE)
|
|
629
|
+
if match:
|
|
630
|
+
current_pc["members"].append({
|
|
631
|
+
"group": match.group(1),
|
|
632
|
+
"mode": match.group(2)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
# VLANs
|
|
636
|
+
if rules.get("port_channel_vlans_pattern"):
|
|
637
|
+
match = re.search(rules["port_channel_vlans_pattern"], line, re.IGNORECASE)
|
|
638
|
+
if match:
|
|
639
|
+
current_pc["vlans"] = match.group(1)
|
|
640
|
+
|
|
641
|
+
# Shutdown status
|
|
642
|
+
if re.search(r"^\s*shutdown\s*$", line, re.IGNORECASE):
|
|
643
|
+
current_pc["shutdown"] = True
|
|
644
|
+
|
|
645
|
+
if current_pc:
|
|
646
|
+
port_channels.append(current_pc)
|
|
647
|
+
|
|
648
|
+
return port_channels
|
|
649
|
+
|
|
650
|
+
def _extract_vxlan_info(self, rules: Dict) -> Dict[str, Any]:
|
|
651
|
+
"""Извлекает информацию о VXLAN конфигурации."""
|
|
652
|
+
result = {
|
|
653
|
+
"enabled": False,
|
|
654
|
+
"vtep_ip": None,
|
|
655
|
+
"vnis": [],
|
|
656
|
+
"anycast_mac": None,
|
|
657
|
+
"mac_vrfs": []
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
current_mac_vrf = None
|
|
661
|
+
in_mac_vrf = False
|
|
662
|
+
|
|
663
|
+
for line in self.content_lines:
|
|
664
|
+
# VTEP IP
|
|
665
|
+
if rules.get("vtep_ip_pattern"):
|
|
666
|
+
match = re.search(rules["vtep_ip_pattern"], line, re.IGNORECASE)
|
|
667
|
+
if match:
|
|
668
|
+
result["enabled"] = True
|
|
669
|
+
result["vtep_ip"] = match.group(1)
|
|
670
|
+
|
|
671
|
+
# VNI
|
|
672
|
+
if rules.get("vni_pattern"):
|
|
673
|
+
match = re.search(rules["vni_pattern"], line, re.IGNORECASE)
|
|
674
|
+
if match:
|
|
675
|
+
vni_id = match.group(1)
|
|
676
|
+
bridge_vlan = match.group(2)
|
|
677
|
+
result["vnis"].append({
|
|
678
|
+
"vni": vni_id,
|
|
679
|
+
"bridge_vlan": bridge_vlan,
|
|
680
|
+
"name": ""
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
# VNI name
|
|
684
|
+
if rules.get("vni_name_pattern"):
|
|
685
|
+
match = re.search(rules["vni_name_pattern"], line, re.IGNORECASE)
|
|
686
|
+
if match:
|
|
687
|
+
if result["vnis"]:
|
|
688
|
+
result["vnis"][-1]["name"] = match.group(1)
|
|
689
|
+
|
|
690
|
+
# Anycast gateway MAC
|
|
691
|
+
if rules.get("evpn_irb_mac_pattern"):
|
|
692
|
+
match = re.search(rules["evpn_irb_mac_pattern"], line, re.IGNORECASE)
|
|
693
|
+
if match:
|
|
694
|
+
result["anycast_mac"] = match.group(1)
|
|
695
|
+
|
|
696
|
+
# MAC VRF start - проверяем начало секции
|
|
697
|
+
if rules.get("mac_vrf_pattern"):
|
|
698
|
+
match = re.search(rules["mac_vrf_pattern"], line, re.IGNORECASE)
|
|
699
|
+
if match:
|
|
700
|
+
if current_mac_vrf:
|
|
701
|
+
result["mac_vrfs"].append(current_mac_vrf)
|
|
702
|
+
current_mac_vrf = {
|
|
703
|
+
"name": match.group(1),
|
|
704
|
+
"rd": "",
|
|
705
|
+
"route_target": "",
|
|
706
|
+
"description": ""
|
|
707
|
+
}
|
|
708
|
+
in_mac_vrf = True
|
|
709
|
+
continue
|
|
710
|
+
|
|
711
|
+
if in_mac_vrf and current_mac_vrf:
|
|
712
|
+
# RD
|
|
713
|
+
if rules.get("mac_vrf_rd_pattern"):
|
|
714
|
+
match = re.search(rules["mac_vrf_rd_pattern"], line, re.IGNORECASE)
|
|
715
|
+
if match:
|
|
716
|
+
current_mac_vrf["rd"] = match.group(1)
|
|
717
|
+
|
|
718
|
+
# Route-target
|
|
719
|
+
if rules.get("mac_vrf_rt_pattern"):
|
|
720
|
+
match = re.search(rules["mac_vrf_rt_pattern"], line, re.IGNORECASE)
|
|
721
|
+
if match:
|
|
722
|
+
current_mac_vrf["route_target"] = match.group(1)
|
|
723
|
+
|
|
724
|
+
# Description
|
|
725
|
+
if rules.get("mac_vrf_desc_pattern"):
|
|
726
|
+
match = re.search(rules["mac_vrf_desc_pattern"], line, re.IGNORECASE)
|
|
727
|
+
if match:
|
|
728
|
+
current_mac_vrf["description"] = match.group(1)
|
|
729
|
+
|
|
730
|
+
# Выход из секции MAC VRF - новая секция или конец
|
|
731
|
+
if re.search(r"^mac vrf\s+\S+", line, re.IGNORECASE) and current_mac_vrf["name"] != line.split()[-1]:
|
|
732
|
+
pass # Обработано выше
|
|
733
|
+
elif re.search(r"^evpn irb-forwarding", line, re.IGNORECASE):
|
|
734
|
+
in_mac_vrf = False
|
|
735
|
+
|
|
736
|
+
if current_mac_vrf:
|
|
737
|
+
result["mac_vrfs"].append(current_mac_vrf)
|
|
738
|
+
|
|
739
|
+
return result
|
|
740
|
+
|
|
741
|
+
def _extract_management_info(self, rules: Dict) -> Dict[str, Any]:
|
|
742
|
+
"""Извлекает информацию об управлении."""
|
|
743
|
+
result = {
|
|
744
|
+
"mgmt_interface": None,
|
|
745
|
+
"mgmt_ip": None,
|
|
746
|
+
"mgmt_mask": None,
|
|
747
|
+
"mgmt_vrf": None,
|
|
748
|
+
"default_gateway": None,
|
|
749
|
+
"default_gateway_iface": None
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
in_mgmt_interface = False
|
|
753
|
+
|
|
754
|
+
for line in self.content_lines:
|
|
755
|
+
# Management interface
|
|
756
|
+
if rules.get("mgmt_interface_pattern"):
|
|
757
|
+
match = re.search(rules["mgmt_interface_pattern"], line, re.IGNORECASE)
|
|
758
|
+
if match:
|
|
759
|
+
result["mgmt_interface"] = match.group(1)
|
|
760
|
+
in_mgmt_interface = True
|
|
761
|
+
continue
|
|
762
|
+
|
|
763
|
+
if in_mgmt_interface:
|
|
764
|
+
# Management VRF
|
|
765
|
+
if rules.get("mgmt_vrf_pattern"):
|
|
766
|
+
match = re.search(rules["mgmt_vrf_pattern"], line, re.IGNORECASE)
|
|
767
|
+
if match:
|
|
768
|
+
result["mgmt_vrf"] = match.group(1)
|
|
769
|
+
|
|
770
|
+
# Management IP - CIDR формат (10.7.8.1/24) или с маской
|
|
771
|
+
if rules.get("mgmt_ip_pattern"):
|
|
772
|
+
# CIDR формат
|
|
773
|
+
cidr_match = re.search(r"ip address\s+(\S+)/(\d+)", line, re.IGNORECASE)
|
|
774
|
+
if cidr_match:
|
|
775
|
+
result["mgmt_ip"] = cidr_match.group(1)
|
|
776
|
+
result["mgmt_mask"] = cidr_match.group(2)
|
|
777
|
+
else:
|
|
778
|
+
match = re.search(rules["mgmt_ip_pattern"], line, re.IGNORECASE)
|
|
779
|
+
if match:
|
|
780
|
+
result["mgmt_ip"] = match.group(1)
|
|
781
|
+
result["mgmt_mask"] = match.group(2)
|
|
782
|
+
|
|
783
|
+
# Выход из секции интерфейса
|
|
784
|
+
if re.search(r"^interface\s+", line, re.IGNORECASE) and not re.search(r"^interface\s+(eth0|mgmt)", line, re.IGNORECASE):
|
|
785
|
+
in_mgmt_interface = False
|
|
786
|
+
|
|
787
|
+
# Default route
|
|
788
|
+
if rules.get("default_route_pattern"):
|
|
789
|
+
match = re.search(rules["default_route_pattern"], line, re.IGNORECASE)
|
|
790
|
+
if match:
|
|
791
|
+
result["mgmt_vrf"] = match.group(1)
|
|
792
|
+
result["default_gateway"] = match.group(2)
|
|
793
|
+
# Пытаемся извлечь интерфейс из следующей строки или из gateway
|
|
794
|
+
gw_parts = result["default_gateway"].split()
|
|
795
|
+
if len(gw_parts) > 1:
|
|
796
|
+
result["default_gateway"] = gw_parts[0]
|
|
797
|
+
result["default_gateway_iface"] = gw_parts[1]
|
|
798
|
+
|
|
799
|
+
return result
|
|
800
|
+
|
|
801
|
+
def _extract_vrf_info(self, rules: Dict) -> Dict[str, Any]:
|
|
802
|
+
"""Извлекает информацию о VRF (Virtual Routing and Forwarding)."""
|
|
803
|
+
result = {
|
|
804
|
+
"enabled": False,
|
|
805
|
+
"vrfs": []
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
current_vrf = None
|
|
809
|
+
|
|
810
|
+
for line in self.content_lines:
|
|
811
|
+
# Определение VRF
|
|
812
|
+
if rules.get("vrf_definition_pattern"):
|
|
813
|
+
match = re.search(rules["vrf_definition_pattern"], line, re.IGNORECASE)
|
|
814
|
+
if match:
|
|
815
|
+
vrf_name = match.group(1)
|
|
816
|
+
if vrf_name.lower() != "management": # Исключаем management VRF
|
|
817
|
+
current_vrf = {
|
|
818
|
+
"name": vrf_name,
|
|
819
|
+
"description": None,
|
|
820
|
+
"interfaces": []
|
|
821
|
+
}
|
|
822
|
+
result["enabled"] = True
|
|
823
|
+
result["vrfs"].append(current_vrf)
|
|
824
|
+
continue
|
|
825
|
+
|
|
826
|
+
# Описание VRF
|
|
827
|
+
if current_vrf and rules.get("vrf_description_pattern"):
|
|
828
|
+
match = re.search(rules["vrf_description_pattern"], line, re.IGNORECASE)
|
|
829
|
+
if match:
|
|
830
|
+
current_vrf["description"] = match.group(1).strip()
|
|
831
|
+
|
|
832
|
+
# Интерфейсы в VRF
|
|
833
|
+
if rules.get("vrf_forwarding_pattern"):
|
|
834
|
+
match = re.search(rules["vrf_forwarding_pattern"], line, re.IGNORECASE)
|
|
835
|
+
if match:
|
|
836
|
+
vrf_name = match.group(1)
|
|
837
|
+
if vrf_name.lower() != "management":
|
|
838
|
+
# Ищем существующий VRF или создаём новый
|
|
839
|
+
existing_vrf = next((v for v in result["vrfs"] if v["name"] == vrf_name), None)
|
|
840
|
+
if not existing_vrf:
|
|
841
|
+
existing_vrf = {
|
|
842
|
+
"name": vrf_name,
|
|
843
|
+
"description": None,
|
|
844
|
+
"interfaces": []
|
|
845
|
+
}
|
|
846
|
+
result["vrfs"].append(existing_vrf)
|
|
847
|
+
result["enabled"] = True
|
|
848
|
+
|
|
849
|
+
return result
|
|
850
|
+
|
|
851
|
+
def _extract_ospf_info(self, rules: Dict) -> Dict[str, Any]:
|
|
852
|
+
"""Извлекает информацию об OSPF конфигурации."""
|
|
853
|
+
result = {
|
|
854
|
+
"enabled": False,
|
|
855
|
+
"process_id": None,
|
|
856
|
+
"vrf": None,
|
|
857
|
+
"router_id": None,
|
|
858
|
+
"areas": [],
|
|
859
|
+
"networks": []
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
current_area = None
|
|
863
|
+
|
|
864
|
+
for line in self.content_lines:
|
|
865
|
+
# OSPF процесс
|
|
866
|
+
if rules.get("process_pattern"):
|
|
867
|
+
match = re.search(rules["process_pattern"], line, re.IGNORECASE)
|
|
868
|
+
if match:
|
|
869
|
+
result["enabled"] = True
|
|
870
|
+
result["process_id"] = match.group(1)
|
|
871
|
+
if match.lastindex >= 2 and match.group(2):
|
|
872
|
+
result["vrf"] = match.group(2)
|
|
873
|
+
continue
|
|
874
|
+
|
|
875
|
+
# Router ID
|
|
876
|
+
if result["enabled"] and rules.get("router_id_pattern"):
|
|
877
|
+
match = re.search(rules["router_id_pattern"], line, re.IGNORECASE)
|
|
878
|
+
if match:
|
|
879
|
+
result["router_id"] = match.group(1)
|
|
880
|
+
|
|
881
|
+
# OSPF Area с authentication
|
|
882
|
+
if result["enabled"] and rules.get("area_pattern"):
|
|
883
|
+
match = re.search(rules["area_pattern"], line, re.IGNORECASE)
|
|
884
|
+
if match:
|
|
885
|
+
current_area = match.group(1)
|
|
886
|
+
auth_type = match.group(2)
|
|
887
|
+
if current_area not in result["areas"]:
|
|
888
|
+
result["areas"].append({
|
|
889
|
+
"area_id": current_area,
|
|
890
|
+
"authentication": auth_type
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
# OSPF Network
|
|
894
|
+
if result["enabled"] and rules.get("network_pattern"):
|
|
895
|
+
match = re.search(rules["network_pattern"], line, re.IGNORECASE)
|
|
896
|
+
if match:
|
|
897
|
+
network = match.group(1)
|
|
898
|
+
area = match.group(2)
|
|
899
|
+
result["networks"].append({
|
|
900
|
+
"network": network,
|
|
901
|
+
"area": area
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
return result
|
|
905
|
+
|
|
906
|
+
def _extract_lldp_info(self, rules: Dict) -> Dict[str, Any]:
|
|
907
|
+
"""Извлекает информацию о LLDP конфигурации и соседях."""
|
|
908
|
+
result = {
|
|
909
|
+
"enabled": False,
|
|
910
|
+
"lldp_run": False,
|
|
911
|
+
"neighbors": []
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
current_interface = None
|
|
915
|
+
in_lldp_agent = False
|
|
916
|
+
|
|
917
|
+
for line in self.content_lines:
|
|
918
|
+
# Проверка глобального включения LLDP
|
|
919
|
+
if rules.get("lldp_run_pattern"):
|
|
920
|
+
match = re.search(rules["lldp_run_pattern"], line, re.IGNORECASE)
|
|
921
|
+
if match:
|
|
922
|
+
result["lldp_run"] = True
|
|
923
|
+
result["enabled"] = True
|
|
924
|
+
|
|
925
|
+
# Определение интерфейса
|
|
926
|
+
intf_match = re.search(r"^interface\s+(\S+)", line, re.IGNORECASE)
|
|
927
|
+
if intf_match:
|
|
928
|
+
current_interface = intf_match.group(1)
|
|
929
|
+
in_lldp_agent = False
|
|
930
|
+
continue
|
|
931
|
+
|
|
932
|
+
# LLDP agent секция
|
|
933
|
+
if current_interface and rules.get("lldp_agent_pattern"):
|
|
934
|
+
match = re.search(rules["lldp_agent_pattern"], line, re.IGNORECASE)
|
|
935
|
+
if match:
|
|
936
|
+
in_lldp_agent = True
|
|
937
|
+
continue
|
|
938
|
+
|
|
939
|
+
if in_lldp_agent and current_interface:
|
|
940
|
+
# Chassis ID
|
|
941
|
+
if rules.get("chassis_id_pattern"):
|
|
942
|
+
match = re.search(rules["chassis_id_pattern"], line, re.IGNORECASE)
|
|
943
|
+
if match:
|
|
944
|
+
# Ищем существующего соседа или создаём нового
|
|
945
|
+
neighbor = next((n for n in result["neighbors"] if n["interface"] == current_interface), None)
|
|
946
|
+
if not neighbor:
|
|
947
|
+
neighbor = {
|
|
948
|
+
"interface": current_interface,
|
|
949
|
+
"chassis_id": None,
|
|
950
|
+
"port_id": None,
|
|
951
|
+
"description": None
|
|
952
|
+
}
|
|
953
|
+
result["neighbors"].append(neighbor)
|
|
954
|
+
neighbor["chassis_id"] = match.group(1)
|
|
955
|
+
|
|
956
|
+
# Port ID
|
|
957
|
+
if rules.get("port_id_pattern"):
|
|
958
|
+
match = re.search(rules["port_id_pattern"], line, re.IGNORECASE)
|
|
959
|
+
if match:
|
|
960
|
+
neighbor = next((n for n in result["neighbors"] if n["interface"] == current_interface), None)
|
|
961
|
+
if not neighbor:
|
|
962
|
+
neighbor = {
|
|
963
|
+
"interface": current_interface,
|
|
964
|
+
"chassis_id": None,
|
|
965
|
+
"port_id": None,
|
|
966
|
+
"description": None
|
|
967
|
+
}
|
|
968
|
+
result["neighbors"].append(neighbor)
|
|
969
|
+
neighbor["port_id"] = match.group(1)
|
|
970
|
+
|
|
971
|
+
# Выход из LLDP agent секции
|
|
972
|
+
if re.search(r"^\s*exit\s*$", line, re.IGNORECASE):
|
|
973
|
+
in_lldp_agent = False
|
|
974
|
+
|
|
975
|
+
# Description (сосед)
|
|
976
|
+
if current_interface and rules.get("neighbor_description_pattern"):
|
|
977
|
+
match = re.search(rules["neighbor_description_pattern"], line, re.IGNORECASE)
|
|
978
|
+
if match:
|
|
979
|
+
neighbor = next((n for n in result["neighbors"] if n["interface"] == current_interface), None)
|
|
980
|
+
if not neighbor:
|
|
981
|
+
neighbor = {
|
|
982
|
+
"interface": current_interface,
|
|
983
|
+
"chassis_id": None,
|
|
984
|
+
"port_id": None,
|
|
985
|
+
"description": None
|
|
986
|
+
}
|
|
987
|
+
result["neighbors"].append(neighbor)
|
|
988
|
+
neighbor["description"] = match.group(1).strip()
|
|
989
|
+
|
|
990
|
+
return result
|
|
991
|
+
|
|
992
|
+
def _extract_interface_status(self, rules: Dict) -> Dict[str, str]:
|
|
993
|
+
"""Извлекает статус интерфейсов (up/down)."""
|
|
994
|
+
result = {}
|
|
995
|
+
current_interface = None
|
|
996
|
+
is_shutdown = False
|
|
997
|
+
|
|
998
|
+
for line in self.content_lines:
|
|
999
|
+
# Определение интерфейса
|
|
1000
|
+
intf_match = re.search(r"^interface\s+(\S+)", line, re.IGNORECASE)
|
|
1001
|
+
if intf_match:
|
|
1002
|
+
# Сохраняем статус предыдущего интерфейса
|
|
1003
|
+
if current_interface:
|
|
1004
|
+
result[current_interface] = "down" if is_shutdown else "up"
|
|
1005
|
+
|
|
1006
|
+
current_interface = intf_match.group(1)
|
|
1007
|
+
is_shutdown = False
|
|
1008
|
+
continue
|
|
1009
|
+
|
|
1010
|
+
if current_interface:
|
|
1011
|
+
# Shutdown
|
|
1012
|
+
if rules.get("shutdown_pattern"):
|
|
1013
|
+
match = re.search(rules["shutdown_pattern"], line, re.IGNORECASE)
|
|
1014
|
+
if match:
|
|
1015
|
+
is_shutdown = True
|
|
1016
|
+
|
|
1017
|
+
# No shutdown
|
|
1018
|
+
if rules.get("no_shutdown_pattern"):
|
|
1019
|
+
match = re.search(rules["no_shutdown_pattern"], line, re.IGNORECASE)
|
|
1020
|
+
if match:
|
|
1021
|
+
is_shutdown = False
|
|
1022
|
+
|
|
1023
|
+
# Последний интерфейс
|
|
1024
|
+
if current_interface:
|
|
1025
|
+
result[current_interface] = "down" if is_shutdown else "up"
|
|
1026
|
+
|
|
1027
|
+
return result
|
|
1028
|
+
|
|
1029
|
+
def _extract_stackwise_info(self, rules: Dict) -> Dict[str, Any]:
|
|
1030
|
+
"""Извлекает информацию о StackWise конфигурации."""
|
|
1031
|
+
result = {
|
|
1032
|
+
"enabled": False,
|
|
1033
|
+
"members": [],
|
|
1034
|
+
"system_mtu": None
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
for line in self.content_lines:
|
|
1038
|
+
# Switch provision
|
|
1039
|
+
if rules.get("switch_provision_pattern"):
|
|
1040
|
+
match = re.search(rules["switch_provision_pattern"], line, re.IGNORECASE)
|
|
1041
|
+
if match:
|
|
1042
|
+
result["enabled"] = True
|
|
1043
|
+
switch_num = match.group(1)
|
|
1044
|
+
model = match.group(2)
|
|
1045
|
+
result["members"].append({
|
|
1046
|
+
"switch_number": switch_num,
|
|
1047
|
+
"model": model
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
# System MTU
|
|
1051
|
+
if rules.get("system_mtu_pattern"):
|
|
1052
|
+
match = re.search(rules["system_mtu_pattern"], line, re.IGNORECASE)
|
|
1053
|
+
if match:
|
|
1054
|
+
result["system_mtu"] = match.group(1)
|
|
1055
|
+
|
|
1056
|
+
return result
|
|
1057
|
+
|
|
1058
|
+
def _extract_qos_info(self, rules: Dict) -> Dict[str, Any]:
|
|
1059
|
+
"""Извлекает информацию о QoS конфигурации."""
|
|
1060
|
+
result = {
|
|
1061
|
+
"enabled": False,
|
|
1062
|
+
"mls_qos": False,
|
|
1063
|
+
"control_packets": False,
|
|
1064
|
+
"queue_config": []
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
for line in self.content_lines:
|
|
1068
|
+
# MLS QoS
|
|
1069
|
+
if rules.get("mls_qos_pattern"):
|
|
1070
|
+
match = re.search(rules["mls_qos_pattern"], line, re.IGNORECASE)
|
|
1071
|
+
if match:
|
|
1072
|
+
result["enabled"] = True
|
|
1073
|
+
result["mls_qos"] = True
|
|
1074
|
+
|
|
1075
|
+
# QoS control packets
|
|
1076
|
+
if rules.get("qos_control_packets_pattern"):
|
|
1077
|
+
match = re.search(rules["qos_control_packets_pattern"], line, re.IGNORECASE)
|
|
1078
|
+
if match:
|
|
1079
|
+
result["enabled"] = True
|
|
1080
|
+
result["control_packets"] = True
|
|
1081
|
+
|
|
1082
|
+
return result
|
|
1083
|
+
|
|
1084
|
+
def _extract_power_info(self, rules: Dict) -> Dict[str, Any]:
|
|
1085
|
+
"""Извлекает информацию о питании (PoE, redundancy)."""
|
|
1086
|
+
result = {
|
|
1087
|
+
"enabled": False,
|
|
1088
|
+
"redundancy_mode": None,
|
|
1089
|
+
"inline_power": []
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
for line in self.content_lines:
|
|
1093
|
+
# Power redundancy
|
|
1094
|
+
if rules.get("power_redundancy_pattern"):
|
|
1095
|
+
match = re.search(rules["power_redundancy_pattern"], line, re.IGNORECASE)
|
|
1096
|
+
if match:
|
|
1097
|
+
result["enabled"] = True
|
|
1098
|
+
result["redundancy_mode"] = match.group(1)
|
|
1099
|
+
|
|
1100
|
+
return result
|
|
1101
|
+
|
|
1102
|
+
def _extract_udld_info(self, rules: Dict) -> Dict[str, Any]:
|
|
1103
|
+
"""Извлекает информацию о UDLD конфигурации."""
|
|
1104
|
+
result = {
|
|
1105
|
+
"enabled": False,
|
|
1106
|
+
"aggressive": False,
|
|
1107
|
+
"errdisable_recovery": False
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
for line in self.content_lines:
|
|
1111
|
+
# UDLD aggressive
|
|
1112
|
+
if rules.get("udld_aggressive_pattern"):
|
|
1113
|
+
match = re.search(rules["udld_aggressive_pattern"], line, re.IGNORECASE)
|
|
1114
|
+
if match:
|
|
1115
|
+
result["enabled"] = True
|
|
1116
|
+
result["aggressive"] = True
|
|
1117
|
+
|
|
1118
|
+
# Errdisable recovery UDLD
|
|
1119
|
+
if rules.get("errdisable_udld_pattern"):
|
|
1120
|
+
match = re.search(rules["errdisable_udld_pattern"], line, re.IGNORECASE)
|
|
1121
|
+
if match:
|
|
1122
|
+
result["enabled"] = True
|
|
1123
|
+
result["errdisable_recovery"] = True
|
|
1124
|
+
|
|
1125
|
+
return result
|
|
1126
|
+
|
|
1127
|
+
def _extract_routing_paths(self, rules: Dict) -> List[Dict[str, Any]]:
|
|
1128
|
+
"""Извлекает маршруты из конфигурации на основе правил из шаблона.
|
|
1129
|
+
|
|
1130
|
+
Поддерживает форматы:
|
|
1131
|
+
- MikroTik: секционный формат с /ip route и add dst-address=X gateway=Y
|
|
1132
|
+
- Cisco/Huawei: статические маршруты в формате ip route X Y Z
|
|
1133
|
+
"""
|
|
1134
|
+
routes = []
|
|
1135
|
+
|
|
1136
|
+
# Получаем правила из шаблона
|
|
1137
|
+
section_pattern = rules.get("section_pattern")
|
|
1138
|
+
route_pattern = rules.get("route_pattern")
|
|
1139
|
+
dst_pattern = rules.get("dst_pattern", r"dst-address=([^\s]+)")
|
|
1140
|
+
gateway_pattern = rules.get("gateway_pattern", r"(?<!check-)gateway=\s*([^\s]+)")
|
|
1141
|
+
comment_pattern = rules.get("comment_pattern", r"comment=([^\s]+)")
|
|
1142
|
+
disabled_pattern = rules.get("disabled_pattern", r"disabled=(yes|no)")
|
|
1143
|
+
static_route_pattern = rules.get("static_route_pattern") # Для Cisco/Huawei
|
|
1144
|
+
|
|
1145
|
+
# Определяем формат по наличию section_pattern (MikroTik) или static_route_pattern (Cisco/Huawei)
|
|
1146
|
+
is_mikrotik_format = section_pattern is not None
|
|
1147
|
+
is_static_route_format = static_route_pattern is not None
|
|
1148
|
+
|
|
1149
|
+
if is_mikrotik_format:
|
|
1150
|
+
# MikroTik формат: /ip route ... add dst-address=X gateway=Y
|
|
1151
|
+
routes.extend(self._extract_mikrotik_routes(
|
|
1152
|
+
section_pattern=section_pattern,
|
|
1153
|
+
dst_pattern=dst_pattern,
|
|
1154
|
+
gateway_pattern=gateway_pattern,
|
|
1155
|
+
comment_pattern=comment_pattern,
|
|
1156
|
+
disabled_pattern=disabled_pattern
|
|
1157
|
+
))
|
|
1158
|
+
elif is_static_route_format:
|
|
1159
|
+
# Cisco/Huawei формат: ip route X Y Z
|
|
1160
|
+
routes.extend(self._extract_static_routes(static_route_pattern))
|
|
1161
|
+
else:
|
|
1162
|
+
# Fallback: пытаемся определить формат автоматически
|
|
1163
|
+
if any(line.strip() == "/ip route" for line in self.content_lines):
|
|
1164
|
+
routes.extend(self._extract_mikrotik_routes(
|
|
1165
|
+
section_pattern=r"^/ip route$",
|
|
1166
|
+
dst_pattern=dst_pattern,
|
|
1167
|
+
gateway_pattern=gateway_pattern,
|
|
1168
|
+
comment_pattern=comment_pattern,
|
|
1169
|
+
disabled_pattern=disabled_pattern
|
|
1170
|
+
))
|
|
1171
|
+
|
|
1172
|
+
return routes
|
|
1173
|
+
|
|
1174
|
+
def _extract_mikrotik_routes(self, section_pattern: str, dst_pattern: str,
|
|
1175
|
+
gateway_pattern: str, comment_pattern: str,
|
|
1176
|
+
disabled_pattern: str) -> List[Dict[str, Any]]:
|
|
1177
|
+
"""Извлекает маршруты в формате MikroTik."""
|
|
1178
|
+
routes = []
|
|
1179
|
+
in_route_section = False
|
|
1180
|
+
|
|
1181
|
+
# Объединяем многострочные команды (с продолжением \)
|
|
1182
|
+
joined_lines = []
|
|
1183
|
+
current_line = ""
|
|
1184
|
+
|
|
1185
|
+
for line in self.content_lines:
|
|
1186
|
+
if current_line:
|
|
1187
|
+
current_line += " " + line.lstrip()
|
|
1188
|
+
else:
|
|
1189
|
+
current_line = line
|
|
1190
|
+
|
|
1191
|
+
if current_line.rstrip().endswith("\\"):
|
|
1192
|
+
current_line = current_line.rstrip()[:-1]
|
|
1193
|
+
else:
|
|
1194
|
+
joined_lines.append(current_line)
|
|
1195
|
+
current_line = ""
|
|
1196
|
+
|
|
1197
|
+
if current_line:
|
|
1198
|
+
joined_lines.append(current_line)
|
|
1199
|
+
|
|
1200
|
+
for line in joined_lines:
|
|
1201
|
+
if re.search(section_pattern, line, re.IGNORECASE):
|
|
1202
|
+
in_route_section = True
|
|
1203
|
+
continue
|
|
1204
|
+
|
|
1205
|
+
if in_route_section and line.startswith("/"):
|
|
1206
|
+
in_route_section = False
|
|
1207
|
+
continue
|
|
1208
|
+
|
|
1209
|
+
if not in_route_section:
|
|
1210
|
+
continue
|
|
1211
|
+
|
|
1212
|
+
if not line.strip().startswith("add"):
|
|
1213
|
+
continue
|
|
1214
|
+
|
|
1215
|
+
dst_match = re.search(dst_pattern, line)
|
|
1216
|
+
gateway_match = re.search(gateway_pattern, line)
|
|
1217
|
+
comment_match = re.search(comment_pattern, line) if comment_pattern else None
|
|
1218
|
+
disabled_match = re.search(disabled_pattern, line) if disabled_pattern else None
|
|
1219
|
+
|
|
1220
|
+
if dst_match and gateway_match:
|
|
1221
|
+
dst_address = dst_match.group(1)
|
|
1222
|
+
gateway = gateway_match.group(1)
|
|
1223
|
+
comment = comment_match.group(1).strip('"') if comment_match and comment_match.group(1) else ""
|
|
1224
|
+
disabled = disabled_match.group(1) == "yes" if disabled_match and disabled_match.group(1) else False
|
|
1225
|
+
|
|
1226
|
+
interface = None
|
|
1227
|
+
if not re.match(r"^\d+\.\d+\.\d+\.\d+$", gateway):
|
|
1228
|
+
interface = gateway
|
|
1229
|
+
else:
|
|
1230
|
+
vrf_intf_match = re.search(r"vrf-interface=([^\s]+)", line)
|
|
1231
|
+
if vrf_intf_match:
|
|
1232
|
+
interface = vrf_intf_match.group(1)
|
|
1233
|
+
|
|
1234
|
+
routes.append({
|
|
1235
|
+
"dst_address": dst_address,
|
|
1236
|
+
"gateway": gateway,
|
|
1237
|
+
"interface": interface,
|
|
1238
|
+
"comment": comment,
|
|
1239
|
+
"disabled": disabled
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
return routes
|
|
1243
|
+
|
|
1244
|
+
def _extract_static_routes(self, static_route_pattern: str) -> List[Dict[str, Any]]:
|
|
1245
|
+
"""Извлекает статические маршруты в формате Cisco/Huawei."""
|
|
1246
|
+
routes = []
|
|
1247
|
+
|
|
1248
|
+
for line in self.content_lines:
|
|
1249
|
+
match = re.search(static_route_pattern, line, re.IGNORECASE)
|
|
1250
|
+
if match:
|
|
1251
|
+
groups = match.groups()
|
|
1252
|
+
route_data = {
|
|
1253
|
+
"dst_address": groups[0] if len(groups) > 0 else "",
|
|
1254
|
+
"gateway": groups[1] if len(groups) > 1 else "",
|
|
1255
|
+
"interface": groups[2] if len(groups) > 2 else None,
|
|
1256
|
+
"comment": "",
|
|
1257
|
+
"disabled": False
|
|
1258
|
+
}
|
|
1259
|
+
routes.append(route_data)
|
|
1260
|
+
|
|
1261
|
+
return routes
|
|
1262
|
+
|
|
1263
|
+
def _extract_interfaces(self, extract_all: bool = True) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
|
|
1264
|
+
"""
|
|
1265
|
+
Универсальный метод извлечения интерфейсов с IP адресами.
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
extract_all: Если True, извлекает все интерфейсы для all_ip_interfaces.
|
|
1269
|
+
Если False, извлекает только routing networks.
|
|
1270
|
+
|
|
1271
|
+
Returns:
|
|
1272
|
+
Кортеж (routing_networks, all_ip_interfaces)
|
|
1273
|
+
"""
|
|
1274
|
+
routing_networks = []
|
|
1275
|
+
all_interfaces = []
|
|
1276
|
+
current_interface = None
|
|
1277
|
+
is_shutdown = False
|
|
1278
|
+
|
|
1279
|
+
# Определение формата конфигурации
|
|
1280
|
+
is_mikrotik = any(line.startswith("/ip address") for line in self.content_lines)
|
|
1281
|
+
|
|
1282
|
+
if is_mikrotik:
|
|
1283
|
+
# Обработка формата MikroTik (секционный: /ip address ... add address=X interface=Y)
|
|
1284
|
+
in_section = False
|
|
1285
|
+
|
|
1286
|
+
for line in self.content_lines:
|
|
1287
|
+
# Проверка на заголовок секции
|
|
1288
|
+
if line.strip() == "/ip address":
|
|
1289
|
+
in_section = True
|
|
1290
|
+
continue
|
|
1291
|
+
|
|
1292
|
+
# Если в другой секции - пропускаем
|
|
1293
|
+
if in_section and line.startswith("/"):
|
|
1294
|
+
in_section = False
|
|
1295
|
+
continue
|
|
1296
|
+
|
|
1297
|
+
if not in_section:
|
|
1298
|
+
continue
|
|
1299
|
+
|
|
1300
|
+
# Проверка на строку добавления адреса
|
|
1301
|
+
if not line.startswith("add"):
|
|
1302
|
+
continue
|
|
1303
|
+
|
|
1304
|
+
# Проверка на отключённый интерфейс
|
|
1305
|
+
if "disabled=yes" in line:
|
|
1306
|
+
continue
|
|
1307
|
+
|
|
1308
|
+
# Извлечение address=X и interface=Y
|
|
1309
|
+
addr_match = re.search(r"address=([^\s]+)", line)
|
|
1310
|
+
intf_match = re.search(r"interface=([^\s]+)", line)
|
|
1311
|
+
|
|
1312
|
+
if addr_match and intf_match:
|
|
1313
|
+
address = addr_match.group(1)
|
|
1314
|
+
interface = intf_match.group(1)
|
|
1315
|
+
|
|
1316
|
+
# Разбор address=IP/mask
|
|
1317
|
+
if "/" in address:
|
|
1318
|
+
ip, mask = address.split("/", 1)
|
|
1319
|
+
else:
|
|
1320
|
+
ip = address
|
|
1321
|
+
mask = "32"
|
|
1322
|
+
|
|
1323
|
+
# Извлечение description если есть
|
|
1324
|
+
desc_match = re.search(r"comment=([^\s]+)", line)
|
|
1325
|
+
description = desc_match.group(1).strip('"') if desc_match else ""
|
|
1326
|
+
|
|
1327
|
+
intf_data = {
|
|
1328
|
+
'interface': interface,
|
|
1329
|
+
'ip': ip,
|
|
1330
|
+
'mask': mask,
|
|
1331
|
+
'description': description
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if extract_all:
|
|
1335
|
+
all_interfaces.append(intf_data)
|
|
1336
|
+
|
|
1337
|
+
# Для routing networks используем те же данные
|
|
1338
|
+
routing_networks.append({
|
|
1339
|
+
'interface': interface,
|
|
1340
|
+
'network': f"{ip}/{mask}"
|
|
1341
|
+
})
|
|
1342
|
+
else:
|
|
1343
|
+
# Стандартный формат (Cisco/Huawei: interface X ... ip address Y)
|
|
1344
|
+
for line in self.content_lines:
|
|
1345
|
+
# Проверка на интерфейс
|
|
1346
|
+
intf_match = re.search(r"^interface\s+(\S+)", line, re.IGNORECASE)
|
|
1347
|
+
if intf_match:
|
|
1348
|
+
# Сохраняем предыдущий интерфейс если был IP
|
|
1349
|
+
if current_interface and not is_shutdown and current_interface.get('ip'):
|
|
1350
|
+
intf_data = {
|
|
1351
|
+
'interface': current_interface['name'],
|
|
1352
|
+
'ip': current_interface['ip'],
|
|
1353
|
+
'mask': current_interface['mask'],
|
|
1354
|
+
'description': current_interface.get('description', '')
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if extract_all:
|
|
1358
|
+
all_interfaces.append(intf_data)
|
|
1359
|
+
|
|
1360
|
+
routing_networks.append({
|
|
1361
|
+
'interface': current_interface['name'],
|
|
1362
|
+
'network': f"{current_interface['ip']}/{current_interface['mask']}"
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
current_interface = {
|
|
1366
|
+
'name': intf_match.group(1),
|
|
1367
|
+
'ip': None,
|
|
1368
|
+
'mask': None,
|
|
1369
|
+
'description': ''
|
|
1370
|
+
}
|
|
1371
|
+
is_shutdown = False
|
|
1372
|
+
continue
|
|
1373
|
+
|
|
1374
|
+
if current_interface:
|
|
1375
|
+
# IP адрес в формате CIDR (10.7.0.0/31) или с маской (10.7.0.0 255.255.255.254)
|
|
1376
|
+
ip_cidr_match = re.search(r"ip address\s+(\S+)/(\d+)", line, re.IGNORECASE)
|
|
1377
|
+
if ip_cidr_match:
|
|
1378
|
+
current_interface['ip'] = ip_cidr_match.group(1)
|
|
1379
|
+
current_interface['mask'] = ip_cidr_match.group(2) # CIDR префикс
|
|
1380
|
+
else:
|
|
1381
|
+
ip_mask_match = re.search(r"ip address\s+(\S+)\s+(\S+)", line, re.IGNORECASE)
|
|
1382
|
+
if ip_mask_match:
|
|
1383
|
+
current_interface['ip'] = ip_mask_match.group(1)
|
|
1384
|
+
current_interface['mask'] = ip_mask_match.group(2)
|
|
1385
|
+
|
|
1386
|
+
# Description
|
|
1387
|
+
desc_match = re.search(r"description\s+(.+)", line, re.IGNORECASE)
|
|
1388
|
+
if desc_match:
|
|
1389
|
+
current_interface['description'] = desc_match.group(1).strip()
|
|
1390
|
+
|
|
1391
|
+
# Shutdown
|
|
1392
|
+
if re.search(r"^\s*shutdown\s*$", line, re.IGNORECASE):
|
|
1393
|
+
is_shutdown = True
|
|
1394
|
+
|
|
1395
|
+
# Последний интерфейс
|
|
1396
|
+
if current_interface and not is_shutdown and current_interface.get('ip'):
|
|
1397
|
+
intf_data = {
|
|
1398
|
+
'interface': current_interface['name'],
|
|
1399
|
+
'ip': current_interface['ip'],
|
|
1400
|
+
'mask': current_interface['mask'],
|
|
1401
|
+
'description': current_interface.get('description', '')
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if extract_all:
|
|
1405
|
+
all_interfaces.append(intf_data)
|
|
1406
|
+
|
|
1407
|
+
routing_networks.append({
|
|
1408
|
+
'interface': current_interface['name'],
|
|
1409
|
+
'network': f"{current_interface['ip']}/{current_interface['mask']}"
|
|
1410
|
+
})
|
|
1411
|
+
|
|
1412
|
+
return routing_networks, all_interfaces
|
|
1413
|
+
|
|
1414
|
+
def _extract_all_ip_interfaces(self) -> List[Dict[str, str]]:
|
|
1415
|
+
"""Извлекает все интерфейсы с IP адресами из конфигурации (обратная совместимость)."""
|
|
1416
|
+
_, all_interfaces = self._extract_interfaces(extract_all=True)
|
|
1417
|
+
return all_interfaces
|
|
1418
|
+
|
|
1419
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1420
|
+
"""Возвращает результаты анализа в виде словаря."""
|
|
1421
|
+
return {
|
|
1422
|
+
"filename": self.filename,
|
|
1423
|
+
"vendor": self.vendor,
|
|
1424
|
+
"device_name": self.device_name,
|
|
1425
|
+
"model": self.model,
|
|
1426
|
+
"device_type": self.device_type,
|
|
1427
|
+
"routing_networks": self.routing_networks,
|
|
1428
|
+
"total_vlans": self.total_vlans,
|
|
1429
|
+
"active_vlans": self.active_vlans,
|
|
1430
|
+
"all_vlans": self.all_vlans,
|
|
1431
|
+
"bgp_info": getattr(self, 'bgp_info', {}),
|
|
1432
|
+
"port_channels": getattr(self, 'port_channels', []),
|
|
1433
|
+
"vxlan_info": getattr(self, 'vxlan_info', {}),
|
|
1434
|
+
"management_info": getattr(self, 'management_info', {}),
|
|
1435
|
+
"all_ip_interfaces": getattr(self, 'all_ip_interfaces', []),
|
|
1436
|
+
"routing_paths": getattr(self, 'routing_paths', []),
|
|
1437
|
+
"vrf_info": getattr(self, 'vrf_info', {}),
|
|
1438
|
+
"ospf_info": getattr(self, 'ospf_info', {}),
|
|
1439
|
+
"lldp_info": getattr(self, 'lldp_info', {}),
|
|
1440
|
+
"interface_status": getattr(self, 'interface_status', {}),
|
|
1441
|
+
"stackwise_info": getattr(self, 'stackwise_info', {}),
|
|
1442
|
+
"qos_info": getattr(self, 'qos_info', {}),
|
|
1443
|
+
"power_info": getattr(self, 'power_info', {}),
|
|
1444
|
+
"udld_info": getattr(self, 'udld_info', {})
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
class NetworkTopologyAnalyzer:
|
|
1449
|
+
"""Анализатор сетевой топологии на основе данных об устройствах."""
|
|
1450
|
+
|
|
1451
|
+
@staticmethod
|
|
1452
|
+
def netmask_to_prefix(netmask: str) -> int:
|
|
1453
|
+
"""Преобразует маску из dotted-decimal в префикс."""
|
|
1454
|
+
try:
|
|
1455
|
+
# Обработка CIDR нотации (например, "31", "30")
|
|
1456
|
+
if netmask_str := netmask.strip():
|
|
1457
|
+
if netmask_str.isdigit():
|
|
1458
|
+
return int(netmask_str)
|
|
1459
|
+
return ipaddress.IPv4Network(f"0.0.0.0/{netmask}").prefixlen
|
|
1460
|
+
except ValueError as e:
|
|
1461
|
+
raise ValueError(f"Некорректная маска '{netmask}': {e}")
|
|
1462
|
+
|
|
1463
|
+
@staticmethod
|
|
1464
|
+
def calculate_network_address(ip_str: str, netmask_str: str) -> str:
|
|
1465
|
+
"""Вычисляет сетевой адрес в CIDR формате."""
|
|
1466
|
+
prefix = NetworkTopologyAnalyzer.netmask_to_prefix(netmask_str)
|
|
1467
|
+
network = ipaddress.IPv4Network(f"{ip_str}/{prefix}", strict=False)
|
|
1468
|
+
return str(network)
|
|
1469
|
+
|
|
1470
|
+
@staticmethod
|
|
1471
|
+
def parse_interface_network(network_entry: str) -> Dict[str, Any]:
|
|
1472
|
+
"""Парсит запись сети интерфейса."""
|
|
1473
|
+
# Обработка формата "ip/mask" или "ip mask" или "ip/secondary"
|
|
1474
|
+
parts = network_entry.replace('secondary', '').strip().split()
|
|
1475
|
+
if len(parts) >= 1:
|
|
1476
|
+
network_str = parts[0]
|
|
1477
|
+
if '/' in network_str:
|
|
1478
|
+
ip_str, netmask_str = network_str.split('/', 1)
|
|
1479
|
+
else:
|
|
1480
|
+
ip_str = network_str
|
|
1481
|
+
netmask_str = '32' # Default to host route
|
|
1482
|
+
|
|
1483
|
+
# Обработка CIDR нотации
|
|
1484
|
+
if netmask_str.isdigit():
|
|
1485
|
+
prefix = int(netmask_str)
|
|
1486
|
+
else:
|
|
1487
|
+
try:
|
|
1488
|
+
prefix = NetworkTopologyAnalyzer.netmask_to_prefix(netmask_str)
|
|
1489
|
+
except ValueError:
|
|
1490
|
+
prefix = 32
|
|
1491
|
+
|
|
1492
|
+
# Вычисляем реальный адрес сети (network address)
|
|
1493
|
+
try:
|
|
1494
|
+
network = ipaddress.IPv4Network(f'{ip_str}/{prefix}', strict=False)
|
|
1495
|
+
network_cidr = str(network)
|
|
1496
|
+
except ValueError:
|
|
1497
|
+
network_cidr = f"{ip_str}/{prefix}"
|
|
1498
|
+
|
|
1499
|
+
return {
|
|
1500
|
+
'ip': ip_str,
|
|
1501
|
+
'prefix': prefix,
|
|
1502
|
+
'network_cidr': network_cidr,
|
|
1503
|
+
'is_loopback': prefix == 32,
|
|
1504
|
+
'is_mgmt_network': prefix in (24, 23, 22),
|
|
1505
|
+
'is_p2p': prefix in (31, 30)
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
return {
|
|
1509
|
+
'ip': network_entry,
|
|
1510
|
+
'prefix': 32,
|
|
1511
|
+
'network_cidr': f"{network_entry}/32",
|
|
1512
|
+
'is_loopback': False,
|
|
1513
|
+
'is_mgmt_network': False,
|
|
1514
|
+
'is_p2p': False
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
@staticmethod
|
|
1518
|
+
def is_physical_interface(interface_name: str) -> bool:
|
|
1519
|
+
"""Определяет физический интерфейс."""
|
|
1520
|
+
non_physical = ('MEth', 'Vbdif', 'Vlanif', 'LoopBack', 'NULL')
|
|
1521
|
+
return not any(interface_name.startswith(prefix) for prefix in non_physical)
|
|
1522
|
+
|
|
1523
|
+
@staticmethod
|
|
1524
|
+
def is_mgmt_interface(interface_name: str, is_mgmt_network: bool) -> bool:
|
|
1525
|
+
"""Определяет управленческий интерфейс."""
|
|
1526
|
+
mgmt_indicators = ('MEth', 'Vbdif1360837')
|
|
1527
|
+
return (any(interface_name.startswith(prefix) for prefix in mgmt_indicators) or
|
|
1528
|
+
(is_mgmt_network and interface_name.startswith('Vbdif')))
|
|
1529
|
+
|
|
1530
|
+
@staticmethod
|
|
1531
|
+
def extract_interface_number(interface_name: str) -> Tuple[str, List[int]]:
|
|
1532
|
+
"""Извлекает базовое имя интерфейса и номера подынтерфейсов."""
|
|
1533
|
+
match = re.match(r'^([^\d]*[\d/]+)(?:\.(\d+))?$', interface_name)
|
|
1534
|
+
if not match:
|
|
1535
|
+
return interface_name, []
|
|
1536
|
+
base = match.group(1)
|
|
1537
|
+
subif = [int(match.group(2))] if match.group(2) else []
|
|
1538
|
+
return base, subif
|
|
1539
|
+
|
|
1540
|
+
@staticmethod
|
|
1541
|
+
def extract_device_interfaces(device: Dict[str, Any], filter_type: str = 'all') -> List[Dict[str, Any]]:
|
|
1542
|
+
"""Извлекает интерфейсы устройства с фильтрацией по типу."""
|
|
1543
|
+
interfaces = []
|
|
1544
|
+
device_name = device.get('device_name', 'unknown')
|
|
1545
|
+
processed_networks = set()
|
|
1546
|
+
|
|
1547
|
+
# 1. Все IP интерфейсы из all_ip_interfaces (для b4com)
|
|
1548
|
+
for intf_entry in device.get('all_ip_interfaces', []):
|
|
1549
|
+
interface_name = intf_entry.get('interface', '')
|
|
1550
|
+
ip = intf_entry.get('ip')
|
|
1551
|
+
mask = intf_entry.get('mask', '32')
|
|
1552
|
+
description = intf_entry.get('description', '')
|
|
1553
|
+
|
|
1554
|
+
if interface_name and ip:
|
|
1555
|
+
# Преобразуем маску
|
|
1556
|
+
if mask.isdigit():
|
|
1557
|
+
prefix = int(mask)
|
|
1558
|
+
else:
|
|
1559
|
+
try:
|
|
1560
|
+
prefix = NetworkTopologyAnalyzer.netmask_to_prefix(mask)
|
|
1561
|
+
except ValueError:
|
|
1562
|
+
prefix = 32
|
|
1563
|
+
|
|
1564
|
+
# Вычисляем реальный адрес сети
|
|
1565
|
+
try:
|
|
1566
|
+
network = ipaddress.IPv4Network(f'{ip}/{prefix}', strict=False)
|
|
1567
|
+
network_cidr = str(network)
|
|
1568
|
+
except ValueError:
|
|
1569
|
+
network_cidr = f"{ip}/{prefix}"
|
|
1570
|
+
|
|
1571
|
+
# Пропускаем дубликаты и loopback
|
|
1572
|
+
if network_cidr in processed_networks:
|
|
1573
|
+
continue
|
|
1574
|
+
processed_networks.add(network_cidr)
|
|
1575
|
+
|
|
1576
|
+
base_intf, subif_numbers = NetworkTopologyAnalyzer.extract_interface_number(interface_name)
|
|
1577
|
+
|
|
1578
|
+
intf_data = {
|
|
1579
|
+
'interface': interface_name,
|
|
1580
|
+
'base_interface': base_intf,
|
|
1581
|
+
'subif_numbers': subif_numbers,
|
|
1582
|
+
'ip': ip,
|
|
1583
|
+
'prefix': prefix,
|
|
1584
|
+
'network_cidr': network_cidr,
|
|
1585
|
+
'description': description,
|
|
1586
|
+
'is_physical': NetworkTopologyAnalyzer.is_physical_interface(interface_name),
|
|
1587
|
+
'is_mgmt': NetworkTopologyAnalyzer.is_mgmt_interface(interface_name, prefix in (24, 23, 22)),
|
|
1588
|
+
'is_loopback': interface_name.lower().startswith('lo'),
|
|
1589
|
+
'is_p2p': prefix in (31, 30),
|
|
1590
|
+
'source': 'all_ip'
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
# Фильтрация
|
|
1594
|
+
if filter_type == 'physical':
|
|
1595
|
+
if not (intf_data['is_physical'] and intf_data['is_p2p'] and not intf_data['is_loopback']):
|
|
1596
|
+
continue
|
|
1597
|
+
elif filter_type == 'mgmt':
|
|
1598
|
+
if not (intf_data['is_mgmt'] and not intf_data['is_loopback']):
|
|
1599
|
+
continue
|
|
1600
|
+
elif filter_type == 'logical':
|
|
1601
|
+
if (intf_data['is_loopback'] or
|
|
1602
|
+
intf_data['is_mgmt'] or
|
|
1603
|
+
(intf_data['is_physical'] and intf_data['is_p2p'])):
|
|
1604
|
+
continue
|
|
1605
|
+
|
|
1606
|
+
interfaces.append(intf_data)
|
|
1607
|
+
|
|
1608
|
+
# 2. Management interface из management_info (если ещё не добавлен)
|
|
1609
|
+
if filter_type in ('all', 'mgmt'):
|
|
1610
|
+
mgmt_info = device.get('management_info', {})
|
|
1611
|
+
if mgmt_info.get('mgmt_interface') and mgmt_info.get('mgmt_ip'):
|
|
1612
|
+
mgmt_ip = mgmt_info['mgmt_ip']
|
|
1613
|
+
mgmt_mask = mgmt_info.get('mgmt_mask', '24')
|
|
1614
|
+
mgmt_intf = mgmt_info['mgmt_interface']
|
|
1615
|
+
|
|
1616
|
+
# Проверяем, не добавлен ли уже
|
|
1617
|
+
already_added = any(
|
|
1618
|
+
intf['interface'] == mgmt_intf and intf['ip'] == mgmt_ip
|
|
1619
|
+
for intf in interfaces
|
|
1620
|
+
)
|
|
1621
|
+
|
|
1622
|
+
if not already_added:
|
|
1623
|
+
# Преобразуем маску если нужно
|
|
1624
|
+
if mgmt_mask.isdigit():
|
|
1625
|
+
prefix = int(mgmt_mask)
|
|
1626
|
+
else:
|
|
1627
|
+
try:
|
|
1628
|
+
prefix = NetworkTopologyAnalyzer.netmask_to_prefix(mgmt_mask)
|
|
1629
|
+
except ValueError:
|
|
1630
|
+
prefix = 24
|
|
1631
|
+
|
|
1632
|
+
interfaces.append({
|
|
1633
|
+
'interface': mgmt_intf,
|
|
1634
|
+
'base_interface': mgmt_intf,
|
|
1635
|
+
'subif_numbers': [],
|
|
1636
|
+
'ip': mgmt_ip,
|
|
1637
|
+
'prefix': prefix,
|
|
1638
|
+
'network_cidr': f"{mgmt_ip}/{prefix}",
|
|
1639
|
+
'is_physical': True,
|
|
1640
|
+
'is_mgmt': True,
|
|
1641
|
+
'is_loopback': False,
|
|
1642
|
+
'is_p2p': False,
|
|
1643
|
+
'source': 'management'
|
|
1644
|
+
})
|
|
1645
|
+
|
|
1646
|
+
return interfaces
|
|
1647
|
+
|
|
1648
|
+
@staticmethod
|
|
1649
|
+
def find_physical_links(devices_data: List[Dict[str, Any]]) -> List[List[Any]]:
|
|
1650
|
+
"""Выявляет физические P2P связи через /31 и /30 сети с указанием вендора и типа."""
|
|
1651
|
+
# Маппинг имени → метаданные
|
|
1652
|
+
device_metadata: Dict[str, Dict[str, str]] = {
|
|
1653
|
+
device['device_name']: {
|
|
1654
|
+
'vendor': device['vendor'],
|
|
1655
|
+
'device_type': device['device_type']
|
|
1656
|
+
}
|
|
1657
|
+
for device in devices_data if device['device_name'] != 'unknown'
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
# Сбор физических интерфейсов
|
|
1661
|
+
device_interfaces: Dict[str, List[Dict[str, Any]]] = {}
|
|
1662
|
+
for device in devices_data:
|
|
1663
|
+
device_name = device['device_name']
|
|
1664
|
+
if device_name != 'unknown':
|
|
1665
|
+
device_interfaces[device_name] = NetworkTopologyAnalyzer.extract_device_interfaces(
|
|
1666
|
+
device, filter_type='physical'
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
# Индексация сетей
|
|
1670
|
+
network_index: Dict[str, List[Tuple[str, Dict[str, Any]]]] = {}
|
|
1671
|
+
for device_name, interfaces in device_interfaces.items():
|
|
1672
|
+
for intf in interfaces:
|
|
1673
|
+
net = intf['network_cidr']
|
|
1674
|
+
network_index.setdefault(net, []).append((device_name, intf))
|
|
1675
|
+
|
|
1676
|
+
# Формирование связей
|
|
1677
|
+
links = []
|
|
1678
|
+
processed_pairs: Set[Tuple[str, str, str]] = set()
|
|
1679
|
+
|
|
1680
|
+
for network_cidr, endpoints in network_index.items():
|
|
1681
|
+
if len(endpoints) != 2:
|
|
1682
|
+
continue
|
|
1683
|
+
|
|
1684
|
+
dev1_name, intf1 = endpoints[0]
|
|
1685
|
+
dev2_name, intf2 = endpoints[1]
|
|
1686
|
+
|
|
1687
|
+
pair_key = tuple(sorted([dev1_name, dev2_name]) + [network_cidr])
|
|
1688
|
+
if pair_key in processed_pairs:
|
|
1689
|
+
continue
|
|
1690
|
+
|
|
1691
|
+
processed_pairs.add(pair_key)
|
|
1692
|
+
|
|
1693
|
+
dev1_meta = device_metadata.get(dev1_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1694
|
+
dev2_meta = device_metadata.get(dev2_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1695
|
+
|
|
1696
|
+
links.append([
|
|
1697
|
+
dev1_name,
|
|
1698
|
+
dev1_meta['vendor'],
|
|
1699
|
+
dev1_meta['device_type'],
|
|
1700
|
+
intf1['interface'],
|
|
1701
|
+
intf1['ip'],
|
|
1702
|
+
dev2_name,
|
|
1703
|
+
dev2_meta['vendor'],
|
|
1704
|
+
dev2_meta['device_type'],
|
|
1705
|
+
intf2['interface'],
|
|
1706
|
+
intf2['ip'],
|
|
1707
|
+
network_cidr
|
|
1708
|
+
])
|
|
1709
|
+
|
|
1710
|
+
return links
|
|
1711
|
+
|
|
1712
|
+
@staticmethod
|
|
1713
|
+
def find_mgmt_interfaces(devices_data: List[Dict[str, Any]]) -> List[List[str]]:
|
|
1714
|
+
"""Извлекает управленческие интерфейсы."""
|
|
1715
|
+
mgmt_interfaces = []
|
|
1716
|
+
for device in devices_data:
|
|
1717
|
+
device_name = device['device_name']
|
|
1718
|
+
if device_name == 'unknown':
|
|
1719
|
+
continue
|
|
1720
|
+
mgmt_ifs = NetworkTopologyAnalyzer.extract_device_interfaces(device, filter_type='mgmt')
|
|
1721
|
+
for intf in mgmt_ifs:
|
|
1722
|
+
mgmt_interfaces.append([
|
|
1723
|
+
device_name,
|
|
1724
|
+
device.get('vendor', 'unknown'), # Add vendor
|
|
1725
|
+
device.get('device_type', 'unknown'), # Add type
|
|
1726
|
+
intf['interface'],
|
|
1727
|
+
intf['ip'],
|
|
1728
|
+
intf['network_cidr']
|
|
1729
|
+
])
|
|
1730
|
+
|
|
1731
|
+
# Если mgmt интерфейсы не найдены, но есть routing_networks, добавляем их как mgmt
|
|
1732
|
+
# Это нужно для одиночных устройств типа MikroTik
|
|
1733
|
+
if not mgmt_ifs and device.get('routing_networks'):
|
|
1734
|
+
for routing_net in device['routing_networks']:
|
|
1735
|
+
interface = routing_net.get('interface', '')
|
|
1736
|
+
network = routing_net.get('network', '')
|
|
1737
|
+
|
|
1738
|
+
# Разбор network=IP/mask
|
|
1739
|
+
if "/" in network:
|
|
1740
|
+
ip, mask = network.split("/", 1)
|
|
1741
|
+
prefix = int(mask) if mask.isdigit() else 24
|
|
1742
|
+
else:
|
|
1743
|
+
ip = network
|
|
1744
|
+
prefix = 24
|
|
1745
|
+
|
|
1746
|
+
# Вычисляем network_cidr
|
|
1747
|
+
try:
|
|
1748
|
+
network_obj = ipaddress.IPv4Network(f"{ip}/{prefix}", strict=False)
|
|
1749
|
+
network_cidr = str(network_obj)
|
|
1750
|
+
except ValueError:
|
|
1751
|
+
network_cidr = f"{ip}/{prefix}"
|
|
1752
|
+
|
|
1753
|
+
mgmt_interfaces.append([
|
|
1754
|
+
device_name,
|
|
1755
|
+
device.get('vendor', 'unknown'),
|
|
1756
|
+
device.get('device_type', 'unknown'),
|
|
1757
|
+
interface,
|
|
1758
|
+
ip,
|
|
1759
|
+
network_cidr
|
|
1760
|
+
])
|
|
1761
|
+
|
|
1762
|
+
mgmt_interfaces.sort(key=lambda x: (x[5], x[0])) # Sort by network_cidr then device_name
|
|
1763
|
+
return mgmt_interfaces
|
|
1764
|
+
|
|
1765
|
+
@staticmethod
|
|
1766
|
+
def find_logical_links(devices_data: List[Dict[str, Any]]) -> List[List[str]]:
|
|
1767
|
+
"""Выявляет логические связи (сервисные сети, VXLAN, логические P2P)."""
|
|
1768
|
+
logical_links = []
|
|
1769
|
+
processed_networks: Set[str] = set()
|
|
1770
|
+
processed_vni_pairs: Set[Tuple[str, str, int]] = set()
|
|
1771
|
+
|
|
1772
|
+
# Создание маппинга имени устройства к его вендору и типу
|
|
1773
|
+
device_metadata: Dict[str, Dict[str, str]] = {
|
|
1774
|
+
device['device_name']: {
|
|
1775
|
+
'vendor': device['vendor'],
|
|
1776
|
+
'device_type': device['device_type']
|
|
1777
|
+
}
|
|
1778
|
+
for device in devices_data
|
|
1779
|
+
if device['device_name'] != 'unknown'
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
# Сбор всех интерфейсов
|
|
1783
|
+
all_interfaces: Dict[str, List[Dict[str, Any]]] = {}
|
|
1784
|
+
for device in devices_data:
|
|
1785
|
+
device_name = device['device_name']
|
|
1786
|
+
if device_name != 'unknown':
|
|
1787
|
+
all_interfaces[device_name] = NetworkTopologyAnalyzer.extract_device_interfaces(
|
|
1788
|
+
device, filter_type='all'
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
# 1. Сервисные сети (VBDIF/Vlanif)
|
|
1792
|
+
network_to_devices: Dict[str, List[Tuple[str, Dict[str, Any]]]] = {}
|
|
1793
|
+
for device_name, interfaces in all_interfaces.items():
|
|
1794
|
+
for intf in interfaces:
|
|
1795
|
+
if (intf['interface'].startswith(('Vbdif', 'Vlanif')) and
|
|
1796
|
+
24 <= intf['prefix'] <= 28 and
|
|
1797
|
+
not intf['is_loopback']):
|
|
1798
|
+
net = intf['network_cidr']
|
|
1799
|
+
network_to_devices.setdefault(net, []).append((device_name, intf))
|
|
1800
|
+
|
|
1801
|
+
for network_cidr, endpoints in network_to_devices.items():
|
|
1802
|
+
if len(endpoints) < 2 or network_cidr in processed_networks:
|
|
1803
|
+
continue
|
|
1804
|
+
processed_networks.add(network_cidr)
|
|
1805
|
+
for i in range(len(endpoints)):
|
|
1806
|
+
for j in range(i + 1, len(endpoints)):
|
|
1807
|
+
dev1_name, intf1 = endpoints[i]
|
|
1808
|
+
dev2_name, intf2 = endpoints[j]
|
|
1809
|
+
|
|
1810
|
+
# Получаем метаданные устройств
|
|
1811
|
+
dev1_meta = device_metadata.get(dev1_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1812
|
+
dev2_meta = device_metadata.get(dev2_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1813
|
+
|
|
1814
|
+
logical_links.append([
|
|
1815
|
+
dev1_name,
|
|
1816
|
+
dev1_meta['vendor'],
|
|
1817
|
+
dev1_meta['device_type'],
|
|
1818
|
+
f"{intf1['interface']}/{intf1['ip']}",
|
|
1819
|
+
dev2_name,
|
|
1820
|
+
dev2_meta['vendor'],
|
|
1821
|
+
dev2_meta['device_type'],
|
|
1822
|
+
f"{intf2['interface']}/{intf2['ip']}",
|
|
1823
|
+
f"Service Network: {network_cidr}"
|
|
1824
|
+
])
|
|
1825
|
+
|
|
1826
|
+
# 2. VXLAN overlay (подынтерфейсы с номерами VNI)
|
|
1827
|
+
vni_map: Dict[int, List[Tuple[str, Dict[str, Any]]]] = {}
|
|
1828
|
+
for device_name, interfaces in all_interfaces.items():
|
|
1829
|
+
for intf in interfaces:
|
|
1830
|
+
if intf['subif_numbers'] and intf['base_interface'].startswith(('100GE', '40GE', '10GE')):
|
|
1831
|
+
vni = intf['subif_numbers'][0]
|
|
1832
|
+
if 1000 <= vni <= 16777215:
|
|
1833
|
+
vni_map.setdefault(vni, []).append((device_name, intf))
|
|
1834
|
+
|
|
1835
|
+
for vni, endpoints in vni_map.items():
|
|
1836
|
+
if len(endpoints) < 2:
|
|
1837
|
+
continue
|
|
1838
|
+
base_intf_groups: Dict[str, List[Tuple[str, Dict[str, Any]]]] = {}
|
|
1839
|
+
for dev_name, intf in endpoints:
|
|
1840
|
+
base_intf_groups.setdefault(intf['base_interface'], []).append((dev_name, intf))
|
|
1841
|
+
for group_endpoints in base_intf_groups.values():
|
|
1842
|
+
if len(group_endpoints) < 2:
|
|
1843
|
+
continue
|
|
1844
|
+
for i in range(len(group_endpoints)):
|
|
1845
|
+
for j in range(i + 1, len(group_endpoints)):
|
|
1846
|
+
dev1_name, intf1 = group_endpoints[i]
|
|
1847
|
+
dev2_name, intf2 = group_endpoints[j]
|
|
1848
|
+
pair_key = (min(dev1_name, dev2_name), max(dev1_name, dev2_name), vni)
|
|
1849
|
+
if pair_key in processed_vni_pairs:
|
|
1850
|
+
continue
|
|
1851
|
+
processed_vni_pairs.add(pair_key)
|
|
1852
|
+
# Получаем метаданные устройств
|
|
1853
|
+
dev1_meta = device_metadata.get(dev1_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1854
|
+
dev2_meta = device_metadata.get(dev2_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1855
|
+
|
|
1856
|
+
logical_links.append([
|
|
1857
|
+
dev1_name,
|
|
1858
|
+
dev1_meta['vendor'],
|
|
1859
|
+
dev1_meta['device_type'],
|
|
1860
|
+
f"{intf1['interface']}/{intf1['ip']}",
|
|
1861
|
+
dev2_name,
|
|
1862
|
+
dev2_meta['vendor'],
|
|
1863
|
+
dev2_meta['device_type'],
|
|
1864
|
+
f"{intf2['interface']}/{intf2['ip']}",
|
|
1865
|
+
f"VXLAN VNI {vni} (Overlay)"
|
|
1866
|
+
])
|
|
1867
|
+
|
|
1868
|
+
# 3. Логические P2P через /30
|
|
1869
|
+
p2p30_networks: Dict[str, List[Tuple[str, Dict[str, Any]]]] = {}
|
|
1870
|
+
for device_name, interfaces in all_interfaces.items():
|
|
1871
|
+
for intf in interfaces:
|
|
1872
|
+
if (intf['prefix'] == 30 and
|
|
1873
|
+
not intf['is_loopback'] and
|
|
1874
|
+
not (intf['is_physical'] and intf['interface'].startswith(('100GE', '40GE')))):
|
|
1875
|
+
net = intf['network_cidr']
|
|
1876
|
+
p2p30_networks.setdefault(net, []).append((device_name, intf))
|
|
1877
|
+
|
|
1878
|
+
for network_cidr, endpoints in p2p30_networks.items():
|
|
1879
|
+
if len(endpoints) != 2 or network_cidr in processed_networks:
|
|
1880
|
+
continue
|
|
1881
|
+
processed_networks.add(network_cidr)
|
|
1882
|
+
dev1_name, intf1 = endpoints[0]
|
|
1883
|
+
dev2_name, intf2 = endpoints[1]
|
|
1884
|
+
# Получаем метаданные устройств
|
|
1885
|
+
dev1_meta = device_metadata.get(dev1_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1886
|
+
dev2_meta = device_metadata.get(dev2_name, {'vendor': 'N/A', 'device_type': 'N/A'})
|
|
1887
|
+
|
|
1888
|
+
logical_links.append([
|
|
1889
|
+
dev1_name,
|
|
1890
|
+
dev1_meta['vendor'],
|
|
1891
|
+
dev1_meta['device_type'],
|
|
1892
|
+
f"{intf1['interface']}/{intf1['ip']}",
|
|
1893
|
+
dev2_name,
|
|
1894
|
+
dev2_meta['vendor'],
|
|
1895
|
+
dev2_meta['device_type'],
|
|
1896
|
+
f"{intf2['interface']}/{intf2['ip']}",
|
|
1897
|
+
f"Logical P2P: {network_cidr}"
|
|
1898
|
+
])
|
|
1899
|
+
|
|
1900
|
+
return logical_links
|
|
1901
|
+
|
|
1902
|
+
@staticmethod
|
|
1903
|
+
def analyze_topology(devices_data: List[Dict[str, Any]]) -> Dict[str, List[List[str]]]:
|
|
1904
|
+
"""Полный анализ сетевой топологии."""
|
|
1905
|
+
return {
|
|
1906
|
+
"physical_links": NetworkTopologyAnalyzer.find_physical_links(devices_data),
|
|
1907
|
+
"mgmt_networks": NetworkTopologyAnalyzer.find_mgmt_interfaces(devices_data),
|
|
1908
|
+
"logical_links": NetworkTopologyAnalyzer.find_logical_links(devices_data)
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
class ReportGenerator:
|
|
1913
|
+
"""Генератор отчётов по результатам анализа."""
|
|
1914
|
+
|
|
1915
|
+
@staticmethod
|
|
1916
|
+
def print_short_report(results: List[Dict[str, Any]]) -> None:
|
|
1917
|
+
"""Печатает краткий отчёт в виде таблицы."""
|
|
1918
|
+
headers = ["Файл", "Вендор", "Имя", "Модель", "Тип", "VLAN", "Сети"]
|
|
1919
|
+
rows = []
|
|
1920
|
+
|
|
1921
|
+
for r in results:
|
|
1922
|
+
filename = r["filename"]
|
|
1923
|
+
if len(filename) > 35:
|
|
1924
|
+
filename = filename[:32] + "..."
|
|
1925
|
+
|
|
1926
|
+
rows.append([
|
|
1927
|
+
filename,
|
|
1928
|
+
r["vendor"],
|
|
1929
|
+
r["device_name"] if r["device_name"] != "unknown" else "—",
|
|
1930
|
+
r["model"] if r["model"] != "unknown" else "—",
|
|
1931
|
+
r["device_type"],
|
|
1932
|
+
str(r["total_vlans"]),
|
|
1933
|
+
str(len(r["routing_networks"]))
|
|
1934
|
+
])
|
|
1935
|
+
|
|
1936
|
+
col_widths = [
|
|
1937
|
+
max(len(str(row[i])) for row in [headers] + rows)
|
|
1938
|
+
for i in range(len(headers))
|
|
1939
|
+
]
|
|
1940
|
+
|
|
1941
|
+
def format_row(row_data):
|
|
1942
|
+
return " ".join(str(item).ljust(col_widths[i]) for i, item in enumerate(row_data))
|
|
1943
|
+
|
|
1944
|
+
total_width = sum(col_widths) + 3 * (len(col_widths) - 1)
|
|
1945
|
+
print("\n" + "=" * total_width)
|
|
1946
|
+
print(format_row(headers))
|
|
1947
|
+
print("-" * total_width)
|
|
1948
|
+
for row in rows:
|
|
1949
|
+
print(format_row(row))
|
|
1950
|
+
print("=" * total_width + "\n")
|
|
1951
|
+
|
|
1952
|
+
@staticmethod
|
|
1953
|
+
def print_topology_analysis(result: Dict[str, List[List[str]]]) -> None:
|
|
1954
|
+
"""Печатает анализ топологии."""
|
|
1955
|
+
# Физические связи
|
|
1956
|
+
links = result.get("physical_links", [])
|
|
1957
|
+
print("\n" + "=" * 150)
|
|
1958
|
+
print("🔗 ФИЗИЧЕСКИЕ СВЯЗИ (Physical P2P Links)")
|
|
1959
|
+
print("=" * 150)
|
|
1960
|
+
if links:
|
|
1961
|
+
print(f"{'Устройство 1':<25} | {'Интерфейс':<18} | {'IP':<16} | "
|
|
1962
|
+
f"{'Устройство 2':<25} | {'Интерфейс':<18} | {'IP':<16} | {'Сеть':<20}")
|
|
1963
|
+
print("-" * 150)
|
|
1964
|
+
for link in links:
|
|
1965
|
+
dev1, vendor1, type1, intf1, ip1, dev2, vendor2, type2, intf2, ip2, net = link
|
|
1966
|
+
print(f"{dev1:<25} | {intf1:<18} | {ip1:<16} | "
|
|
1967
|
+
f"{dev2:<25} | {intf2:<18} | {ip2:<16} | {net:<20}")
|
|
1968
|
+
print(f"\n✅ Всего физических связей: {len(links)}")
|
|
1969
|
+
else:
|
|
1970
|
+
print("⚠️ Физические связи не обнаружены")
|
|
1971
|
+
|
|
1972
|
+
# Управленческие сети
|
|
1973
|
+
mgmt = result.get("mgmt_networks", [])
|
|
1974
|
+
print("\n" + "=" * 110)
|
|
1975
|
+
print("🖥️ УПРАВЛЕНЧЕСКИЕ ИНТЕРФЕЙСЫ (Management Networks)")
|
|
1976
|
+
print("=" * 110)
|
|
1977
|
+
if mgmt:
|
|
1978
|
+
print(f"{'Устройство':<25} | {'Интерфейс':<18} | {'IP адрес':<16} | {'Сеть':<20}")
|
|
1979
|
+
print("-" * 110)
|
|
1980
|
+
for entry in mgmt:
|
|
1981
|
+
if len(entry) >= 6:
|
|
1982
|
+
dev, vendor, dev_type, intf, ip, net = entry
|
|
1983
|
+
print(f"{dev:<25} | {intf:<18} | {ip:<16} | {net:<20}")
|
|
1984
|
+
else:
|
|
1985
|
+
# Fallback for backward compatibility
|
|
1986
|
+
dev, intf, ip, net = entry
|
|
1987
|
+
print(f"{dev:<25} | {intf:<18} | {ip:<16} | {net:<20}")
|
|
1988
|
+
print(f"\n✅ Всего управленческих интерфейсов: {len(mgmt)}")
|
|
1989
|
+
|
|
1990
|
+
networks = {}
|
|
1991
|
+
for entry in mgmt:
|
|
1992
|
+
if len(entry) >= 6:
|
|
1993
|
+
net = entry[5]
|
|
1994
|
+
networks.setdefault(net, []).append(f"{entry[0]} ({entry[4]})")
|
|
1995
|
+
else:
|
|
1996
|
+
net = entry[3]
|
|
1997
|
+
networks.setdefault(net, []).append(f"{entry[0]} ({entry[2]})")
|
|
1998
|
+
|
|
1999
|
+
print("\nГруппировка по сетям управления:")
|
|
2000
|
+
for net, devices in sorted(networks.items()):
|
|
2001
|
+
print(f" • {net}: {', '.join(devices)}")
|
|
2002
|
+
else:
|
|
2003
|
+
print("⚠️ Управленческие интерфейсы не обнаружены")
|
|
2004
|
+
|
|
2005
|
+
# Логические связи
|
|
2006
|
+
logical = result.get("logical_links", [])
|
|
2007
|
+
print("\n" + "=" * 160)
|
|
2008
|
+
print("🌐 ЛОГИЧЕСКИЕ СВЯЗИ (Logical Links: VXLAN Overlay, Service Networks)")
|
|
2009
|
+
print("=" * 160)
|
|
2010
|
+
if logical:
|
|
2011
|
+
print(f"{'Устройство 1':<25} | {'Интерфейс/IP':<25} | {'Устройство 2':<25} | {'Интерфейс/IP':<25} | {'Тип связи':<35}")
|
|
2012
|
+
print("-" * 160)
|
|
2013
|
+
for link in logical:
|
|
2014
|
+
if len(link) >= 9:
|
|
2015
|
+
dev1, vendor1, type1, intf_ip1, dev2, vendor2, type2, intf_ip2, desc = link
|
|
2016
|
+
print(f"{dev1:<25} | {intf_ip1:<25} | {dev2:<25} | {intf_ip2:<25} | {desc:<35}")
|
|
2017
|
+
else:
|
|
2018
|
+
# Fallback for backward compatibility
|
|
2019
|
+
dev1, intf_ip1, dev2, intf_ip2, desc = link
|
|
2020
|
+
print(f"{dev1:<25} | {intf_ip1:<25} | {dev2:<25} | {intf_ip2:<25} | {desc:<35}")
|
|
2021
|
+
print(f"\n✅ Всего логических связей: {len(logical)}")
|
|
2022
|
+
|
|
2023
|
+
# Calculate statistics considering the new structure
|
|
2024
|
+
vxlan_count = 0
|
|
2025
|
+
service_count = 0
|
|
2026
|
+
p2p_count = 0
|
|
2027
|
+
for l in logical:
|
|
2028
|
+
if len(l) >= 9:
|
|
2029
|
+
desc = l[8] # Description is at index 8 in new structure
|
|
2030
|
+
else:
|
|
2031
|
+
desc = l[4] # Description is at index 4 in old structure
|
|
2032
|
+
if 'VXLAN' in desc:
|
|
2033
|
+
vxlan_count += 1
|
|
2034
|
+
if 'Service Network' in desc:
|
|
2035
|
+
service_count += 1
|
|
2036
|
+
if 'Logical P2P' in desc:
|
|
2037
|
+
p2p_count += 1
|
|
2038
|
+
|
|
2039
|
+
print("\nСтатистика логических связей:")
|
|
2040
|
+
if vxlan_count:
|
|
2041
|
+
print(f" • VXLAN Overlay (VNI): {vxlan_count}")
|
|
2042
|
+
if service_count:
|
|
2043
|
+
print(f" • Сервисные сети (L3): {service_count}")
|
|
2044
|
+
if p2p_count:
|
|
2045
|
+
print(f" • Логические P2P (/30): {p2p_count}")
|
|
2046
|
+
else:
|
|
2047
|
+
print("ℹ️ Логические связи не обнаружены (требуется дополнительная информация о конфигурации тоннелей)")
|
|
2048
|
+
|
|
2049
|
+
print("=" * 130 + "\n")
|
|
2050
|
+
|
|
2051
|
+
@staticmethod
|
|
2052
|
+
def write_detailed_report(results: List[Dict[str, Any]],
|
|
2053
|
+
output_file: str,
|
|
2054
|
+
links_result: Dict[str, List[List[str]]],
|
|
2055
|
+
conf_dir: str) -> None:
|
|
2056
|
+
"""Записывает подробный отчёт в файл."""
|
|
2057
|
+
from datetime import datetime
|
|
2058
|
+
|
|
2059
|
+
with open(output_file, "w", encoding='utf-8') as f:
|
|
2060
|
+
f.write(f"Анализ сетевого оборудования - {len(results)} устройств\n")
|
|
2061
|
+
f.write(f"Дата: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
2062
|
+
f.write("=" * 80 + "\n\n")
|
|
2063
|
+
|
|
2064
|
+
for r in results:
|
|
2065
|
+
f.write(f"{'=' * 40}\n")
|
|
2066
|
+
f.write(f"Устройство: {r['filename']}\n")
|
|
2067
|
+
f.write(f"{'=' * 40}\n")
|
|
2068
|
+
f.write(f"Vendor: {r['vendor']}\n")
|
|
2069
|
+
f.write(f"Device Name: {r['device_name']}\n")
|
|
2070
|
+
f.write(f"Model: {r['model']}\n")
|
|
2071
|
+
f.write(f"Type: {r['device_type']}\n")
|
|
2072
|
+
f.write(f"Total VLANs: {r['total_vlans']}\n")
|
|
2073
|
+
f.write(
|
|
2074
|
+
f"Active VLANs: {', '.join(str(vlan) for vlan in r['active_vlans']) if r['active_vlans'] else 'None'}\n")
|
|
2075
|
+
f.write(f"Routing Networks Count: {len(r['routing_networks'])}\n")
|
|
2076
|
+
|
|
2077
|
+
if r['routing_networks']:
|
|
2078
|
+
f.write("\nRouting Networks:\n")
|
|
2079
|
+
for i, net in enumerate(r["routing_networks"], 1):
|
|
2080
|
+
if 'interface' in net:
|
|
2081
|
+
f.write(f" {i}. Interface: {net['interface']}, Network: {net['network']}\n")
|
|
2082
|
+
elif 'route' in net:
|
|
2083
|
+
f.write(f" {i}. Static Route: {net['route']}\n")
|
|
2084
|
+
|
|
2085
|
+
f.write("\nConfiguration snippet:\n")
|
|
2086
|
+
try:
|
|
2087
|
+
with open(Path(conf_dir) / r['filename'], 'r', encoding='utf-8', errors='ignore') as config_file:
|
|
2088
|
+
lines = config_file.readlines()
|
|
2089
|
+
for line in lines[:10]:
|
|
2090
|
+
f.write(f" {line.rstrip()}\n")
|
|
2091
|
+
except Exception as e:
|
|
2092
|
+
f.write(f" ⚠️ Не удалось прочитать конфигурацию: {str(e)}\n")
|
|
2093
|
+
|
|
2094
|
+
# VRF информация
|
|
2095
|
+
vrf_info = r.get('vrf_info', {})
|
|
2096
|
+
if vrf_info and vrf_info.get('enabled') and vrf_info.get('vrfs'):
|
|
2097
|
+
f.write("\nVRF (Virtual Routing and Forwarding):\n")
|
|
2098
|
+
for vrf in vrf_info['vrfs']:
|
|
2099
|
+
f.write(f" • VRF: {vrf['name']}\n")
|
|
2100
|
+
if vrf.get('description'):
|
|
2101
|
+
f.write(f" Description: {vrf['description']}\n")
|
|
2102
|
+
|
|
2103
|
+
# OSPF информация
|
|
2104
|
+
ospf_info = r.get('ospf_info', {})
|
|
2105
|
+
if ospf_info and ospf_info.get('enabled'):
|
|
2106
|
+
f.write("\nOSPF Configuration:\n")
|
|
2107
|
+
f.write(f" • Process ID: {ospf_info.get('process_id', 'N/A')}\n")
|
|
2108
|
+
if ospf_info.get('vrf'):
|
|
2109
|
+
f.write(f" • VRF: {ospf_info['vrf']}\n")
|
|
2110
|
+
if ospf_info.get('router_id'):
|
|
2111
|
+
f.write(f" • Router ID: {ospf_info['router_id']}\n")
|
|
2112
|
+
if ospf_info.get('areas'):
|
|
2113
|
+
f.write(" • Areas:\n")
|
|
2114
|
+
for area in ospf_info['areas']:
|
|
2115
|
+
area_id = area.get('area_id', 'N/A')
|
|
2116
|
+
auth = area.get('authentication', 'N/A')
|
|
2117
|
+
f.write(f" - Area {area_id}: Authentication = {auth}\n")
|
|
2118
|
+
if ospf_info.get('networks'):
|
|
2119
|
+
f.write(" • Networks:\n")
|
|
2120
|
+
for net in ospf_info['networks'][:10]: # Ограничим вывод
|
|
2121
|
+
f.write(f" - {net['network']} → Area {net['area']}\n")
|
|
2122
|
+
if len(ospf_info['networks']) > 10:
|
|
2123
|
+
f.write(f" ... и ещё {len(ospf_info['networks']) - 10} сетей\n")
|
|
2124
|
+
|
|
2125
|
+
# LLDP информация
|
|
2126
|
+
lldp_info = r.get('lldp_info', {})
|
|
2127
|
+
if lldp_info and lldp_info.get('enabled') and lldp_info.get('lldp_run'):
|
|
2128
|
+
f.write("\nLLDP Configuration:\n")
|
|
2129
|
+
f.write(" • LLDP: Enabled\n")
|
|
2130
|
+
if lldp_info.get('neighbors'):
|
|
2131
|
+
f.write(f" • Neighbors: {len(lldp_info['neighbors'])} обнаружено\n")
|
|
2132
|
+
for neighbor in lldp_info['neighbors'][:10]: # Ограничим вывод
|
|
2133
|
+
intf = neighbor.get('interface', 'N/A')
|
|
2134
|
+
desc = neighbor.get('description', 'N/A')
|
|
2135
|
+
chassis = neighbor.get('chassis_id', 'N/A')
|
|
2136
|
+
port = neighbor.get('port_id', 'N/A')
|
|
2137
|
+
f.write(f" - {intf}: {desc}")
|
|
2138
|
+
if chassis != 'N/A' or port != 'N/A':
|
|
2139
|
+
f.write(f" (Chassis: {chassis}, Port: {port})")
|
|
2140
|
+
f.write("\n")
|
|
2141
|
+
if len(lldp_info['neighbors']) > 10:
|
|
2142
|
+
f.write(f" ... и ещё {len(lldp_info['neighbors']) - 10} соседей\n")
|
|
2143
|
+
|
|
2144
|
+
# Статус интерфейсов
|
|
2145
|
+
interface_status = r.get('interface_status', {})
|
|
2146
|
+
if interface_status:
|
|
2147
|
+
up_count = sum(1 for s in interface_status.values() if s == 'up')
|
|
2148
|
+
down_count = sum(1 for s in interface_status.values() if s == 'down')
|
|
2149
|
+
f.write(f"\nInterface Status: {up_count} up, {down_count} down\n")
|
|
2150
|
+
# Показываем только интерфейсы в состоянии down
|
|
2151
|
+
down_interfaces = [intf for intf, status in interface_status.items() if status == 'down']
|
|
2152
|
+
if down_interfaces:
|
|
2153
|
+
f.write(" • Down interfaces:\n")
|
|
2154
|
+
for intf in down_interfaces[:10]:
|
|
2155
|
+
f.write(f" - {intf}: DOWN\n")
|
|
2156
|
+
if len(down_interfaces) > 10:
|
|
2157
|
+
f.write(f" ... и ещё {len(down_interfaces) - 10} интерфейсов\n")
|
|
2158
|
+
|
|
2159
|
+
f.write("\n")
|
|
2160
|
+
|
|
2161
|
+
links = links_result.get("physical_links", [])
|
|
2162
|
+
if not links:
|
|
2163
|
+
f.write("⚠️ Физические связи не обнаружены\n")
|
|
2164
|
+
else:
|
|
2165
|
+
f.write("### Таблица связей между устройствами\n")
|
|
2166
|
+
f.write("\n" + "=" * 150 + "\n")
|
|
2167
|
+
f.write(f"{'Устройство 1':<25} | {'Интерфейс':<18} | {'IP':<16} | "
|
|
2168
|
+
f"{'Устройство 2':<25} | {'Интерфейс':<18} | {'IP':<16} | {'Сеть':<20}\n")
|
|
2169
|
+
f.write("=" * 150 + "\n")
|
|
2170
|
+
|
|
2171
|
+
for link in links:
|
|
2172
|
+
dev1, vendor1, type1, intf1, ip1, dev2, vendor2, type2, intf2, ip2, net = link
|
|
2173
|
+
f.write(f"{dev1:<25} | {intf1:<18} | {ip1:<16} | "
|
|
2174
|
+
f"{dev2:<25} | {intf2:<18} | {ip2:<16} | {net:<20}\n")
|
|
2175
|
+
|
|
2176
|
+
f.write("=" * 150 + "\n")
|
|
2177
|
+
f.write(f"Всего обнаружено физических связей: {len(links)}\n")
|
|
2178
|
+
|
|
2179
|
+
# Управленческие сети
|
|
2180
|
+
mgmt = links_result.get("mgmt_networks", [])
|
|
2181
|
+
f.write("\n" + "=" * 130 + "\n")
|
|
2182
|
+
f.write(" 🖥️ УПРАВЛЕНЧЕСКИЕ ИНТЕРФЕЙСЫ (Management Networks)\n")
|
|
2183
|
+
f.write("=" * 130 + "\n")
|
|
2184
|
+
if mgmt:
|
|
2185
|
+
f.write(f"{'Устройство':<25} | {'Вендор':<15} | {'Тип':<15} | {'Интерфейс':<18} | {'IP адрес':<16} | {'Сеть':<20}\n")
|
|
2186
|
+
f.write("-" * 130 + "\n")
|
|
2187
|
+
for entry in mgmt:
|
|
2188
|
+
if len(entry) >= 6:
|
|
2189
|
+
dev, vendor, dev_type, intf, ip, net = entry
|
|
2190
|
+
f.write(f"{dev:<25} | {vendor:<15} | {dev_type:<15} | {intf:<18} | {ip:<16} | {net:<20}\n")
|
|
2191
|
+
else:
|
|
2192
|
+
# Fallback for backward compatibility
|
|
2193
|
+
dev, intf, ip, net = entry
|
|
2194
|
+
f.write(f"{dev:<25} | {'':<15} | {'':<15} | {intf:<18} | {ip:<16} | {net:<20}\n")
|
|
2195
|
+
f.write(f"\n✅ Всего управленческих интерфейсов: {len(mgmt)}\n")
|
|
2196
|
+
|
|
2197
|
+
networks = {}
|
|
2198
|
+
for entry in mgmt:
|
|
2199
|
+
if len(entry) >= 6:
|
|
2200
|
+
net = entry[5]
|
|
2201
|
+
networks.setdefault(net, []).append(f"{entry[0]} ({entry[4]})")
|
|
2202
|
+
else:
|
|
2203
|
+
net = entry[3]
|
|
2204
|
+
networks.setdefault(net, []).append(f"{entry[0]} ({entry[2]})")
|
|
2205
|
+
|
|
2206
|
+
f.write("\nГруппировка по сетям управления:\n")
|
|
2207
|
+
for net, devices in sorted(networks.items()):
|
|
2208
|
+
f.write(f" • {net}: {', '.join(devices)}\n")
|
|
2209
|
+
else:
|
|
2210
|
+
f.write("⚠️ Управленческие интерфейсы не обнаружены\n")
|
|
2211
|
+
|
|
2212
|
+
# Логические связи
|
|
2213
|
+
logical = links_result.get("logical_links", [])
|
|
2214
|
+
f.write("\n" + "=" * 160 + "\n")
|
|
2215
|
+
f.write(" 🌐 ЛОГИЧЕСКИЕ СВЯЗИ (Logical Links: VXLAN Overlay, Service Networks)\n")
|
|
2216
|
+
f.write("=" * 160 + "\n")
|
|
2217
|
+
if logical:
|
|
2218
|
+
f.write(f"{'Устройство 1':<25} | {'Вендор':<12} | {'Тип':<15} | {'Интерфейс/IP':<25} | {'Устройство 2':<25} | {'Вендор':<12} | {'Тип':<15} | {'Интерфейс/IP':<25} | {'Тип связи':<35}\n")
|
|
2219
|
+
f.write("-" * 160 + "\n")
|
|
2220
|
+
for link in logical:
|
|
2221
|
+
if len(link) >= 9:
|
|
2222
|
+
dev1, vendor1, type1, intf_ip1, dev2, vendor2, type2, intf_ip2, desc = link
|
|
2223
|
+
f.write(f"{dev1:<25} | {vendor1:<12} | {type1:<15} | {intf_ip1:<25} | {dev2:<25} | {vendor2:<12} | {type2:<15} | {intf_ip2:<25} | {desc:<35}\n")
|
|
2224
|
+
else:
|
|
2225
|
+
# Fallback for backward compatibility
|
|
2226
|
+
dev1, intf_ip1, dev2, intf_ip2, desc = link
|
|
2227
|
+
f.write(f"{dev1:<25} | {'':<12} | {'':<15} | {intf_ip1:<25} | {dev2:<25} | {'':<12} | {'':<15} | {intf_ip2:<25} | {desc:<35}\n")
|
|
2228
|
+
f.write(f"\n✅ Всего логических связей: {len(logical)}\n")
|
|
2229
|
+
|
|
2230
|
+
# Calculate statistics considering the new structure
|
|
2231
|
+
vxlan_count = 0
|
|
2232
|
+
service_count = 0
|
|
2233
|
+
p2p_count = 0
|
|
2234
|
+
for l in logical:
|
|
2235
|
+
if len(l) >= 9:
|
|
2236
|
+
desc = l[8] # Description is at index 8 in new structure
|
|
2237
|
+
else:
|
|
2238
|
+
desc = l[4] # Description is at index 4 in old structure
|
|
2239
|
+
if 'VXLAN' in desc:
|
|
2240
|
+
vxlan_count += 1
|
|
2241
|
+
if 'Service Network' in desc:
|
|
2242
|
+
service_count += 1
|
|
2243
|
+
if 'Logical P2P' in desc:
|
|
2244
|
+
p2p_count += 1
|
|
2245
|
+
|
|
2246
|
+
f.write("\nСтатистика логических связей:\n")
|
|
2247
|
+
if vxlan_count:
|
|
2248
|
+
f.write(f" • VXLAN Overlay (VNI): {vxlan_count}\n")
|
|
2249
|
+
if service_count:
|
|
2250
|
+
f.write(f" • Сервисные сети (L3): {service_count}\n")
|
|
2251
|
+
if p2p_count:
|
|
2252
|
+
f.write(f" • Логические P2P (/30): {p2p_count}\n")
|
|
2253
|
+
else:
|
|
2254
|
+
f.write("ℹ️ Логические связи не обнаружены (требуется дополнительная информация о конфигурации тоннелей)\n")
|
|
2255
|
+
|
|
2256
|
+
f.write(f"\n✅ Детальная информация сохранена в файл: {output_file}\n")
|
|
2257
|
+
|
|
2258
|
+
print(f"✅ Детальная информация сохранена в файл: \033[32m{output_file}\033[0m\n\n")
|
|
2259
|
+
|
|
2260
|
+
@staticmethod
|
|
2261
|
+
def draw_topology_ascii(results: List[Dict[str, Any]],
|
|
2262
|
+
links_result: Dict[str, List[List[str]]],
|
|
2263
|
+
output_file: str) -> None:
|
|
2264
|
+
"""Генерирует текстовую ASCII-диаграмму топологии и расширенную информацию."""
|
|
2265
|
+
from datetime import datetime
|
|
2266
|
+
|
|
2267
|
+
with open(output_file, "a", encoding='utf-8') as f:
|
|
2268
|
+
# Заголовок секции топологии
|
|
2269
|
+
f.write("\n" + "=" * 130 + "\n")
|
|
2270
|
+
f.write(" 📊 ТЕКСТОВАЯ КАРТА ТОПОЛОГИИ СЕТИ\n")
|
|
2271
|
+
f.write("=" * 130 + "\n\n")
|
|
2272
|
+
|
|
2273
|
+
# === СПИСЕК УСТРОЙСТВ ПО РОЛЯМ ===
|
|
2274
|
+
f.write("┌" + "─" * 128 + "┐\n")
|
|
2275
|
+
f.write("│" + " СПИСОК УСТРОЙСТВ ПО РОЛЯМ ".center(128) + "│\n")
|
|
2276
|
+
f.write("└" + "─" * 128 + "┘\n\n")
|
|
2277
|
+
|
|
2278
|
+
spine_devices = [r for r in results if 'spn' in r['device_name'].lower()]
|
|
2279
|
+
leaf_devices = [r for r in results if 'lf' in r['device_name'].lower() and 'brl' not in r['device_name'].lower()]
|
|
2280
|
+
border_devices = [r for r in results if 'brl' in r['device_name'].lower()]
|
|
2281
|
+
|
|
2282
|
+
f.write(" Spine (Ядро):\n")
|
|
2283
|
+
for dev in spine_devices:
|
|
2284
|
+
vxlan_ip = dev.get('vxlan_info', {}).get('vtep_ip', 'N/A')
|
|
2285
|
+
bgp_asn = dev.get('bgp_info', {}).get('asn', 'N/A')
|
|
2286
|
+
f.write(f" ├── {dev['device_name']:<25} VTEP: {vxlan_ip:<15} ASN: {bgp_asn}\n")
|
|
2287
|
+
f.write("\n")
|
|
2288
|
+
|
|
2289
|
+
f.write(" Leaf (Доступ):\n")
|
|
2290
|
+
for dev in leaf_devices:
|
|
2291
|
+
vxlan_ip = dev.get('vxlan_info', {}).get('vtep_ip', 'N/A')
|
|
2292
|
+
bgp_asn = dev.get('bgp_info', {}).get('asn', 'N/A')
|
|
2293
|
+
vlan_count = dev.get('total_vlans', 0)
|
|
2294
|
+
f.write(f" ├── {dev['device_name']:<25} VTEP: {vxlan_ip:<15} ASN: {bgp_asn} VLANs: {vlan_count}\n")
|
|
2295
|
+
f.write("\n")
|
|
2296
|
+
|
|
2297
|
+
f.write(" Border Leaf (Граница):\n")
|
|
2298
|
+
for dev in border_devices:
|
|
2299
|
+
vxlan_ip = dev.get('vxlan_info', {}).get('vtep_ip', 'N/A')
|
|
2300
|
+
bgp_asn = dev.get('bgp_info', {}).get('asn', 'N/A')
|
|
2301
|
+
vlan_count = dev.get('total_vlans', 0)
|
|
2302
|
+
f.write(f" ├── {dev['device_name']:<25} VTEP: {vxlan_ip:<15} ASN: {bgp_asn} VLANs: {vlan_count}\n")
|
|
2303
|
+
f.write("\n")
|
|
2304
|
+
|
|
2305
|
+
# === BGP ТОПОЛОГИЯ ===
|
|
2306
|
+
f.write("┌" + "─" * 128 + "┐\n")
|
|
2307
|
+
f.write("│" + " BGP ТОПОЛОГИЯ (EVPN) ".center(128) + "│\n")
|
|
2308
|
+
f.write("└" + "─" * 128 + "┘\n\n")
|
|
2309
|
+
|
|
2310
|
+
# ASCII схема BGP
|
|
2311
|
+
f.write(" ASN 65100 (Spine)\n")
|
|
2312
|
+
f.write(" ┌────────────┬────────────┬────────────┐\n")
|
|
2313
|
+
for dev in spine_devices:
|
|
2314
|
+
bgp_info = dev.get('bgp_info', {})
|
|
2315
|
+
router_id = bgp_info.get('router_id', 'N/A')
|
|
2316
|
+
f.write(f" {dev['device_name']:<18} (RID: {router_id})\n")
|
|
2317
|
+
f.write(" │ │ │\n")
|
|
2318
|
+
f.write(" ─────────┴────────────┴────────────┴─────────\n")
|
|
2319
|
+
f.write(" │ │ │ │\n")
|
|
2320
|
+
|
|
2321
|
+
for dev in leaf_devices:
|
|
2322
|
+
bgp_info = dev.get('bgp_info', {})
|
|
2323
|
+
asn = bgp_info.get('asn', 'N/A')
|
|
2324
|
+
f.write(f" ASN {asn:<5} ASN {asn:<5}\n")
|
|
2325
|
+
f.write(f" {dev['device_name']:<18}\n")
|
|
2326
|
+
|
|
2327
|
+
for dev in border_devices:
|
|
2328
|
+
bgp_info = dev.get('bgp_info', {})
|
|
2329
|
+
asn = bgp_info.get('asn', 'N/A')
|
|
2330
|
+
f.write(f" ASN {asn:<5} ASN {asn:<5}\n")
|
|
2331
|
+
f.write(f" {dev['device_name']:<18}\n")
|
|
2332
|
+
f.write("\n")
|
|
2333
|
+
|
|
2334
|
+
# Детали BGP сессий
|
|
2335
|
+
f.write(" BGP Соседи:\n")
|
|
2336
|
+
for dev in results:
|
|
2337
|
+
bgp_info = dev.get('bgp_info', {})
|
|
2338
|
+
if bgp_info.get('enabled'):
|
|
2339
|
+
f.write(f"\n {dev['device_name']} (ASN {bgp_info.get('asn', 'N/A')}):\n")
|
|
2340
|
+
neighbors = bgp_info.get('neighbors', [])[:5] # Первые 5 соседей
|
|
2341
|
+
for n in neighbors:
|
|
2342
|
+
evpn_status = "✓ EVPN" if n.get('evpn_enabled') else ""
|
|
2343
|
+
f.write(f" ├── {n['ip']:<15} → AS {n['remote_as']:<6} {n.get('description', ''):<20} {evpn_status}\n")
|
|
2344
|
+
if len(bgp_info.get('neighbors', [])) > 5:
|
|
2345
|
+
f.write(f" ... и ещё {len(bgp_info.get('neighbors', [])) - 5} соседей\n")
|
|
2346
|
+
f.write("\n")
|
|
2347
|
+
|
|
2348
|
+
# === VXLAN ИНФОРМАЦИЯ ===
|
|
2349
|
+
f.write("┌" + "─" * 128 + "┐\n")
|
|
2350
|
+
f.write("│" + " VXLAN / EVPN КОНФИГУРАЦИЯ ".center(128) + "│\n")
|
|
2351
|
+
f.write("└" + "─" * 128 + "┘\n\n")
|
|
2352
|
+
|
|
2353
|
+
f.write(" VTEP IP адреса:\n")
|
|
2354
|
+
for dev in results:
|
|
2355
|
+
vxlan_info = dev.get('vxlan_info', {})
|
|
2356
|
+
if vxlan_info.get('enabled'):
|
|
2357
|
+
f.write(f" ├── {dev['device_name']:<25} → {vxlan_info.get('vtep_ip', 'N/A')}\n")
|
|
2358
|
+
f.write("\n")
|
|
2359
|
+
|
|
2360
|
+
anycast_mac = None
|
|
2361
|
+
for dev in results:
|
|
2362
|
+
vxlan_info = dev.get('vxlan_info', {})
|
|
2363
|
+
if vxlan_info.get('anycast_mac'):
|
|
2364
|
+
anycast_mac = vxlan_info['anycast_mac']
|
|
2365
|
+
break
|
|
2366
|
+
if anycast_mac:
|
|
2367
|
+
f.write(f" Anycast Gateway MAC: {anycast_mac}\n\n")
|
|
2368
|
+
|
|
2369
|
+
# VNI список - все устройства
|
|
2370
|
+
f.write(" VNI (VXLAN Network Identifier):\n")
|
|
2371
|
+
# Ширина колонок: VNI=12, Bridge VLAN=13, VNI Name=12, Device=25
|
|
2372
|
+
col1, col2, col3, col4 = 12, 13, 12, 25
|
|
2373
|
+
f.write(" ┌" + "─" * col1 + "┬" + "─" * col2 + "┬" + "─" * col3 + "┬" + "─" * col4 + "┐\n")
|
|
2374
|
+
f.write(" │" + "VNI".center(col1) + "│" + "Bridge VLAN".center(col2) + "│" + "VNI Name".center(col3) + "│" + "Device".center(col4) + "│\n")
|
|
2375
|
+
f.write(" ├" + "─" * col1 + "┼" + "─" * col2 + "┼" + "─" * col3 + "┼" + "─" * col4 + "┤\n")
|
|
2376
|
+
|
|
2377
|
+
vni_count = 0
|
|
2378
|
+
for dev in results:
|
|
2379
|
+
vxlan_info = dev.get('vxlan_info', {})
|
|
2380
|
+
vnis = vxlan_info.get('vnis', [])
|
|
2381
|
+
device_name = dev.get('device_name', 'unknown')
|
|
2382
|
+
for vni in vnis:
|
|
2383
|
+
vni_id = str(vni.get('vni', 'N/A'))[:col1]
|
|
2384
|
+
bridge_vlan = str(vni.get('bridge_vlan', 'N/A'))[:col2]
|
|
2385
|
+
vni_name = str(vni.get('name', 'N/A'))[:col3]
|
|
2386
|
+
dev_name = device_name[:col4]
|
|
2387
|
+
f.write(f" │{vni_id:^{col1}}│{bridge_vlan:^{col2}}│{vni_name:^{col3}}│{dev_name:^{col4}}│\n")
|
|
2388
|
+
vni_count += 1
|
|
2389
|
+
|
|
2390
|
+
if vni_count == 0:
|
|
2391
|
+
total_width = col1 + col2 + col3 + col4 + 5 # +5 для рамок ┌┬┬┬┐
|
|
2392
|
+
f.write(" │" + "Нет данных".center(total_width) + "│\n")
|
|
2393
|
+
f.write(" └" + "─" * col1 + "┴" + "─" * col2 + "┴" + "─" * col3 + "┴" + "─" * col4 + "┘\n")
|
|
2394
|
+
f.write(f"\n Всего VNI: {vni_count}\n")
|
|
2395
|
+
f.write("\n")
|
|
2396
|
+
|
|
2397
|
+
# MAC VRF (EVPN Route Targets) - все устройства
|
|
2398
|
+
f.write(" MAC VRF (EVPN Route Targets):\n")
|
|
2399
|
+
# Ширина колонок: VRF Name=12, RD=14, Route Target=16, Desc=12, Device=25
|
|
2400
|
+
col1, col2, col3, col4, col5 = 12, 14, 16, 12, 25
|
|
2401
|
+
f.write(" ┌" + "─" * col1 + "┬" + "─" * col2 + "┬" + "─" * col3 + "┬" + "─" * col4 + "┬" + "─" * col5 + "┐\n")
|
|
2402
|
+
f.write(" │" + "VRF Name".center(col1) + "│" + "RD".center(col2) + "│" + "Route Target".center(col3) + "│" + "Desc".center(col4) + "│" + "Device".center(col5) + "│\n")
|
|
2403
|
+
f.write(" ├" + "─" * col1 + "┼" + "─" * col2 + "┼" + "─" * col3 + "┼" + "─" * col4 + "┼" + "─" * col5 + "┤\n")
|
|
2404
|
+
|
|
2405
|
+
mac_vrf_count = 0
|
|
2406
|
+
for dev in results:
|
|
2407
|
+
vxlan_info = dev.get('vxlan_info', {})
|
|
2408
|
+
mac_vrfs = vxlan_info.get('mac_vrfs', [])
|
|
2409
|
+
device_name = dev.get('device_name', 'unknown')
|
|
2410
|
+
for vrf in mac_vrfs:
|
|
2411
|
+
name = str(vrf.get('name', 'N/A'))[:col1]
|
|
2412
|
+
rd = str(vrf.get('rd', 'N/A'))[:col2]
|
|
2413
|
+
rt = str(vrf.get('route_target', 'N/A'))[:col3]
|
|
2414
|
+
desc = str(vrf.get('description', 'N/A'))[:col4]
|
|
2415
|
+
dev_name = device_name[:col5]
|
|
2416
|
+
f.write(f" │{name:^{col1}}│{rd:^{col2}}│{rt:^{col3}}│{desc:^{col4}}│{dev_name:^{col5}}│\n")
|
|
2417
|
+
mac_vrf_count += 1
|
|
2418
|
+
|
|
2419
|
+
if mac_vrf_count == 0:
|
|
2420
|
+
total_width = col1 + col2 + col3 + col4 + col5 + 7 # +7 для рамок ┌┬┬┬┬┐
|
|
2421
|
+
f.write(" │" + "Нет данных".center(total_width) + "│\n")
|
|
2422
|
+
f.write(" └" + "─" * col1 + "┴" + "─" * col2 + "┴" + "─" * col3 + "┴" + "─" * col4 + "┴" + "─" * col5 + "┘\n")
|
|
2423
|
+
f.write(f"\n Всего MAC VRF: {mac_vrf_count}\n")
|
|
2424
|
+
f.write("\n")
|
|
2425
|
+
|
|
2426
|
+
# === PORT-CHANNEL (LACP) ===
|
|
2427
|
+
f.write("┌" + "─" * 128 + "┐\n")
|
|
2428
|
+
f.write("│" + " PORT-CHANNEL (LACP) ".center(128) + "│\n")
|
|
2429
|
+
f.write("└" + "─" * 128 + "┘\n\n")
|
|
2430
|
+
|
|
2431
|
+
for dev in results:
|
|
2432
|
+
port_channels = dev.get('port_channels', [])
|
|
2433
|
+
if port_channels:
|
|
2434
|
+
f.write(f" {dev['device_name']}:\n")
|
|
2435
|
+
for pc in port_channels:
|
|
2436
|
+
status = "▼ DOWN" if pc.get('shutdown') else "▲ UP"
|
|
2437
|
+
members = ", ".join([f"grp{m['group']}({m['mode']})" for m in pc.get('members', [])])
|
|
2438
|
+
f.write(f" ├── {pc['name']:<10} {pc.get('description', ''):<35} VLANs: {pc.get('vlans', 'N/A'):<20} {status}\n")
|
|
2439
|
+
if members:
|
|
2440
|
+
f.write(f" │ Members: {members}\n")
|
|
2441
|
+
f.write("\n")
|
|
2442
|
+
|
|
2443
|
+
# === СЕТЬ УПРАВЛЕНИЯ ===
|
|
2444
|
+
f.write("┌" + "─" * 128 + "┐\n")
|
|
2445
|
+
f.write("│" + " СЕТЬ УПРАВЛЕНИЯ (Management OOB) ".center(128) + "│\n")
|
|
2446
|
+
f.write("└" + "─" * 128 + "┘\n\n")
|
|
2447
|
+
|
|
2448
|
+
mgmt_network = None
|
|
2449
|
+
for dev in results:
|
|
2450
|
+
mgmt_info = dev.get('management_info', {})
|
|
2451
|
+
if mgmt_info.get('mgmt_ip'):
|
|
2452
|
+
if not mgmt_network:
|
|
2453
|
+
mgmt_network = f"10.7.8.0/{mgmt_info.get('mgmt_mask', '24')}"
|
|
2454
|
+
f.write(f" ├── {dev['device_name']:<25} → {mgmt_info.get('mgmt_interface', 'eth0')}: "
|
|
2455
|
+
f"{mgmt_info.get('mgmt_ip')}/{mgmt_info.get('mgmt_mask', '24')} "
|
|
2456
|
+
f"(GW: {mgmt_info.get('default_gateway', 'N/A')})\n")
|
|
2457
|
+
if mgmt_network:
|
|
2458
|
+
f.write(f"\n Management Network: {mgmt_network}\n")
|
|
2459
|
+
f.write("\n")
|
|
2460
|
+
|
|
2461
|
+
# === ASCII СХЕМА ТОПОЛОГИИ ===
|
|
2462
|
+
f.write("┌" + "─" * 128 + "┐\n")
|
|
2463
|
+
f.write("│" + " ФИЗИЧЕСКАЯ ТОПОЛОГИЯ (CLOS Architecture) ".center(128) + "│\n")
|
|
2464
|
+
f.write("└" + "─" * 128 + "┘\n\n")
|
|
2465
|
+
|
|
2466
|
+
# Рисуем схему CLOS
|
|
2467
|
+
f.write(" ╔════════════════════════════════════════╗\n")
|
|
2468
|
+
f.write(" ║ SPINE LAYER (ASN 65100) ║\n")
|
|
2469
|
+
f.write(" ╚════════════════════════════════════════╝\n")
|
|
2470
|
+
f.write(" │ │ │\n")
|
|
2471
|
+
|
|
2472
|
+
# Spine устройства
|
|
2473
|
+
spine_names = [d['device_name'] for d in spine_devices]
|
|
2474
|
+
f.write(f" {' '.join([f'{s:<15}' for s in spine_names])}\n")
|
|
2475
|
+
f.write(f" {' '.join(['│' * len(spine_names)])}\n")
|
|
2476
|
+
|
|
2477
|
+
# Листья
|
|
2478
|
+
f.write("\n")
|
|
2479
|
+
f.write(" ╔════════════════════════════════════════════════════════════════════════════╗\n")
|
|
2480
|
+
f.write(" ║ LEAF LAYER (Доступ/Граница) ║\n")
|
|
2481
|
+
f.write(" ╚════════════════════════════════════════════════════════════════════════════╝\n")
|
|
2482
|
+
|
|
2483
|
+
all_leaf = leaf_devices + border_devices
|
|
2484
|
+
for dev in all_leaf:
|
|
2485
|
+
bgp_info = dev.get('bgp_info', {})
|
|
2486
|
+
vxlan_info = dev.get('vxlan_info', {})
|
|
2487
|
+
f.write(f"\n {dev['device_name']:<20} ASN:{bgp_info.get('asn', 'N/A'):<6} VTEP:{vxlan_info.get('vtep_ip', 'N/A'):<15}\n")
|
|
2488
|
+
f.write(f" │\\\n ├─────────── подключено ко всем Spine (ECMP)\n │/\n")
|
|
2489
|
+
|
|
2490
|
+
f.write("\n")
|
|
2491
|
+
f.write(" Условные обозначения:\n")
|
|
2492
|
+
f.write(" ├── VTEP: VXLAN Tunnel End Point IP\n")
|
|
2493
|
+
f.write(" ├── ASN: BGP Autonomous System Number\n")
|
|
2494
|
+
f.write(" ├── ECMP: Equal-Cost Multi-Path routing\n")
|
|
2495
|
+
f.write(" └── EVPN: Ethernet VPN (BGP control plane)\n")
|
|
2496
|
+
f.write("\n")
|
|
2497
|
+
|
|
2498
|
+
# Итоговая статистика
|
|
2499
|
+
f.write("┌" + "─" * 128 + "┐\n")
|
|
2500
|
+
f.write("│" + " ИТОГОВАЯ СТАТИСТИКА ".center(128) + "│\n")
|
|
2501
|
+
f.write("└" + "─" * 128 + "┘\n\n")
|
|
2502
|
+
|
|
2503
|
+
total_devices = len(results)
|
|
2504
|
+
total_spine = len(spine_devices)
|
|
2505
|
+
total_leaf = len(leaf_devices)
|
|
2506
|
+
total_border = len(border_devices)
|
|
2507
|
+
total_vlans = sum(r.get('total_vlans', 0) for r in results)
|
|
2508
|
+
total_vnis = sum(len(r.get('vxlan_info', {}).get('vnis', [])) for r in results)
|
|
2509
|
+
total_port_channels = sum(len(r.get('port_channels', [])) for r in results)
|
|
2510
|
+
total_bgp_sessions = sum(len(r.get('bgp_info', {}).get('neighbors', [])) for r in results)
|
|
2511
|
+
|
|
2512
|
+
# LLDP статистика
|
|
2513
|
+
total_lldp_neighbors = sum(len(r.get('lldp_info', {}).get('neighbors', [])) for r in results)
|
|
2514
|
+
lldp_enabled_devices = sum(1 for r in results if r.get('lldp_info', {}).get('lldp_run'))
|
|
2515
|
+
|
|
2516
|
+
# Статус интерфейсов
|
|
2517
|
+
total_interfaces_up = sum(
|
|
2518
|
+
sum(1 for s in r.get('interface_status', {}).values() if s == 'up')
|
|
2519
|
+
for r in results
|
|
2520
|
+
)
|
|
2521
|
+
total_interfaces_down = sum(
|
|
2522
|
+
sum(1 for s in r.get('interface_status', {}).values() if s == 'down')
|
|
2523
|
+
for r in results
|
|
2524
|
+
)
|
|
2525
|
+
|
|
2526
|
+
f.write(f" Общее количество устройств: {total_devices}\n")
|
|
2527
|
+
f.write(f" ├── Spine: {total_spine}\n")
|
|
2528
|
+
f.write(f" ├── Leaf: {total_leaf}\n")
|
|
2529
|
+
f.write(f" └── Border Leaf: {total_border}\n")
|
|
2530
|
+
f.write(f"\n")
|
|
2531
|
+
f.write(f" VLAN (всего): {total_vlans}\n")
|
|
2532
|
+
f.write(f" VXLAN VNI (всего): {total_vnis}\n")
|
|
2533
|
+
f.write(f" Port-Channel интерфейсов: {total_port_channels}\n")
|
|
2534
|
+
f.write(f" BGP сессий (всего): {total_bgp_sessions}\n")
|
|
2535
|
+
f.write(f" LLDP соседей (всего): {total_lldp_neighbors} (на {lldp_enabled_devices} устройствах)\n")
|
|
2536
|
+
f.write(f" Интерфейсов: {total_interfaces_up} up, {total_interfaces_down} down\n")
|
|
2537
|
+
f.write(f"\n")
|
|
2538
|
+
|
|
2539
|
+
# Физические связи из links_result
|
|
2540
|
+
physical_links = links_result.get("physical_links", [])
|
|
2541
|
+
if physical_links:
|
|
2542
|
+
f.write(f" Физических связей (P2P /31): {len(physical_links)}\n")
|
|
2543
|
+
|
|
2544
|
+
mgmt_networks = links_result.get("mgmt_networks", [])
|
|
2545
|
+
if mgmt_networks:
|
|
2546
|
+
f.write(f" Управленческих интерфейсов: {len(mgmt_networks)}\n")
|
|
2547
|
+
|
|
2548
|
+
logical_links = links_result.get("logical_links", [])
|
|
2549
|
+
if logical_links:
|
|
2550
|
+
f.write(f" Логических связей (Overlay): {len(logical_links)}\n")
|
|
2551
|
+
|
|
2552
|
+
f.write("\n" + "=" * 130 + "\n")
|
|
2553
|
+
f.write(f" Дата генерации отчёта: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
2554
|
+
f.write("=" * 130 + "\n")
|