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 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")