ntermqt 0.1.7__py3-none-any.whl → 0.1.9__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.
- nterm/parser/api_help_dialog.py +426 -223
- nterm/scripting/api.py +421 -701
- nterm/scripting/models.py +195 -0
- nterm/scripting/platform_data.py +272 -0
- nterm/scripting/platform_utils.py +596 -0
- nterm/scripting/repl.py +527 -131
- nterm/scripting/repl_interactive.py +356 -213
- nterm/scripting/ssh_connection.py +632 -0
- nterm/scripting/test_api_repl.py +290 -0
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/METADATA +89 -29
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/RECORD +14 -9
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/entry_points.txt +0 -0
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nterm/scripting/platform_utils.py
|
|
3
|
+
|
|
4
|
+
Platform detection and field normalization utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional, List, Dict, Any, Union
|
|
9
|
+
|
|
10
|
+
from .platform_data import (
|
|
11
|
+
PLATFORM_PATTERNS,
|
|
12
|
+
PLATFORM_COMMANDS,
|
|
13
|
+
DEFAULT_COMMANDS,
|
|
14
|
+
INTERFACE_DETAIL_FIELD_MAP,
|
|
15
|
+
DEFAULT_FIELD_MAP,
|
|
16
|
+
VERSION_FIELD_MAP,
|
|
17
|
+
DEFAULT_VERSION_FIELD_MAP,
|
|
18
|
+
NEIGHBOR_FIELD_MAP,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detect_platform(version_output: str, tfsm_engine=None) -> Optional[str]:
|
|
23
|
+
"""
|
|
24
|
+
Detect device platform from 'show version' output.
|
|
25
|
+
|
|
26
|
+
Uses TextFSM template matching as primary method (more accurate),
|
|
27
|
+
falls back to regex patterns if no template matches.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
version_output: Raw output from 'show version' command
|
|
31
|
+
tfsm_engine: Optional TextFSMAutoEngine instance for template-based detection
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Platform string (e.g., 'cisco_ios', 'arista_eos') or None if not detected
|
|
35
|
+
"""
|
|
36
|
+
# Primary method: Use TextFSM template matching
|
|
37
|
+
if tfsm_engine is not None:
|
|
38
|
+
platform = detect_platform_from_template(version_output, tfsm_engine)
|
|
39
|
+
if platform:
|
|
40
|
+
return platform
|
|
41
|
+
|
|
42
|
+
# Fallback: Regex pattern matching
|
|
43
|
+
for platform, patterns in PLATFORM_PATTERNS.items():
|
|
44
|
+
for pattern in patterns:
|
|
45
|
+
if re.search(pattern, version_output, re.IGNORECASE):
|
|
46
|
+
return platform
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def detect_platform_from_template(
|
|
51
|
+
version_output: str,
|
|
52
|
+
tfsm_engine,
|
|
53
|
+
min_score: float = 50.0,
|
|
54
|
+
) -> Optional[str]:
|
|
55
|
+
"""
|
|
56
|
+
Detect platform by finding the best matching TextFSM template.
|
|
57
|
+
|
|
58
|
+
This is more accurate than regex because TextFSM templates are designed
|
|
59
|
+
to parse specific platform outputs structurally.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
version_output: Raw output from 'show version' or similar command
|
|
63
|
+
tfsm_engine: TextFSMAutoEngine instance
|
|
64
|
+
min_score: Minimum match score to accept (default 50.0)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Platform string extracted from template name, or None
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
Template 'cisco_ios_show_version' → returns 'cisco_ios'
|
|
71
|
+
Template 'arista_eos_show_version' → returns 'arista_eos'
|
|
72
|
+
"""
|
|
73
|
+
if not tfsm_engine or not version_output:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Try with show_version filter first
|
|
78
|
+
best_template, parsed_data, best_score, all_scores = tfsm_engine.find_best_template(
|
|
79
|
+
device_output=version_output,
|
|
80
|
+
filter_string="show_version",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if best_template and best_score >= min_score:
|
|
84
|
+
platform = extract_platform_from_template_name(best_template)
|
|
85
|
+
if platform:
|
|
86
|
+
return platform
|
|
87
|
+
|
|
88
|
+
# If show_version didn't match well, try without filter
|
|
89
|
+
# This catches show system info, show version detail, etc.
|
|
90
|
+
if not best_template or best_score < min_score:
|
|
91
|
+
best_template, parsed_data, best_score, all_scores = tfsm_engine.find_best_template(
|
|
92
|
+
device_output=version_output,
|
|
93
|
+
filter_string="", # No filter - let it find best match
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if best_template and best_score >= min_score:
|
|
97
|
+
platform = extract_platform_from_template_name(best_template)
|
|
98
|
+
if platform:
|
|
99
|
+
return platform
|
|
100
|
+
|
|
101
|
+
except Exception:
|
|
102
|
+
pass # Fall through to return None
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def extract_platform_from_template_name(template_name: str) -> Optional[str]:
|
|
108
|
+
"""
|
|
109
|
+
Extract platform identifier from a TextFSM template name.
|
|
110
|
+
|
|
111
|
+
Template names follow the pattern: {platform}_{command}
|
|
112
|
+
Examples:
|
|
113
|
+
'cisco_ios_show_version' → 'cisco_ios'
|
|
114
|
+
'arista_eos_show_interfaces' → 'arista_eos'
|
|
115
|
+
'juniper_junos_show_version' → 'juniper_junos'
|
|
116
|
+
'cisco_nxos_show_version' → 'cisco_nxos'
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
template_name: Full template name from TextFSM database
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Platform string or None if pattern not recognized
|
|
123
|
+
"""
|
|
124
|
+
if not template_name:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Known platform prefixes (ordered by specificity - longer matches first)
|
|
128
|
+
known_platforms = [
|
|
129
|
+
'cisco_iosxr',
|
|
130
|
+
'cisco_iosxe',
|
|
131
|
+
'cisco_nxos',
|
|
132
|
+
'cisco_ios',
|
|
133
|
+
'cisco_asa',
|
|
134
|
+
'cisco_wlc',
|
|
135
|
+
'arista_eos',
|
|
136
|
+
'juniper_junos',
|
|
137
|
+
'juniper_screenos',
|
|
138
|
+
'hp_procurve',
|
|
139
|
+
'hp_comware',
|
|
140
|
+
'huawei_vrp',
|
|
141
|
+
'linux',
|
|
142
|
+
'paloalto_panos',
|
|
143
|
+
'fortinet_fortios',
|
|
144
|
+
'dell_force10',
|
|
145
|
+
'dell_os10',
|
|
146
|
+
'extreme_exos',
|
|
147
|
+
'extreme_nos',
|
|
148
|
+
'brocade_fastiron',
|
|
149
|
+
'brocade_netiron',
|
|
150
|
+
'ubiquiti_edgeswitch',
|
|
151
|
+
'mikrotik_routeros',
|
|
152
|
+
'vyos',
|
|
153
|
+
'alcatel_aos',
|
|
154
|
+
'alcatel_sros',
|
|
155
|
+
'checkpoint_gaia',
|
|
156
|
+
'enterasys',
|
|
157
|
+
'ruckus_fastiron',
|
|
158
|
+
'yamaha',
|
|
159
|
+
'zyxel_os',
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
template_lower = template_name.lower()
|
|
163
|
+
|
|
164
|
+
for platform in known_platforms:
|
|
165
|
+
if template_lower.startswith(platform + '_'):
|
|
166
|
+
return platform
|
|
167
|
+
|
|
168
|
+
# Fallback: try to extract platform from template name pattern
|
|
169
|
+
# Assumes format: platform_command or platform_subplatform_command
|
|
170
|
+
parts = template_name.split('_')
|
|
171
|
+
if len(parts) >= 2:
|
|
172
|
+
# Try first two parts (handles cisco_ios, arista_eos, etc.)
|
|
173
|
+
potential_platform = f"{parts[0]}_{parts[1]}"
|
|
174
|
+
if potential_platform.lower() in [p.lower() for p in known_platforms]:
|
|
175
|
+
return potential_platform.lower()
|
|
176
|
+
|
|
177
|
+
# Try just first part for single-word platforms (linux, vyos)
|
|
178
|
+
if parts[0].lower() in [p.lower() for p in known_platforms]:
|
|
179
|
+
return parts[0].lower()
|
|
180
|
+
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_platform_command(
|
|
185
|
+
platform: Optional[str],
|
|
186
|
+
command_type: str,
|
|
187
|
+
**kwargs
|
|
188
|
+
) -> Optional[str]:
|
|
189
|
+
"""
|
|
190
|
+
Get platform-specific command for a given operation.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
platform: Platform string (e.g., 'cisco_ios') or None
|
|
194
|
+
command_type: Command type (e.g., 'config', 'version', 'interfaces')
|
|
195
|
+
**kwargs: Format arguments for commands with placeholders (e.g., name='Gi0/1')
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Command string or None if command not available for platform
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> get_platform_command('cisco_ios', 'config')
|
|
202
|
+
'show running-config'
|
|
203
|
+
>>> get_platform_command('juniper_junos', 'config')
|
|
204
|
+
'show configuration'
|
|
205
|
+
>>> get_platform_command('cisco_ios', 'interface_detail', name='Gi0/1')
|
|
206
|
+
'show interfaces Gi0/1'
|
|
207
|
+
"""
|
|
208
|
+
# Get platform-specific commands or fall back to defaults
|
|
209
|
+
platform_cmds = PLATFORM_COMMANDS.get(platform, {}) if platform else {}
|
|
210
|
+
|
|
211
|
+
# Look up command type
|
|
212
|
+
cmd = platform_cmds.get(command_type)
|
|
213
|
+
if cmd is None:
|
|
214
|
+
cmd = DEFAULT_COMMANDS.get(command_type)
|
|
215
|
+
|
|
216
|
+
if cmd is None:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
# Apply format arguments if provided
|
|
220
|
+
if kwargs:
|
|
221
|
+
try:
|
|
222
|
+
cmd = cmd.format(**kwargs)
|
|
223
|
+
except KeyError:
|
|
224
|
+
pass # Return unformatted if missing keys
|
|
225
|
+
|
|
226
|
+
return cmd
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_command_alternatives(command_type: str) -> List[str]:
|
|
230
|
+
"""
|
|
231
|
+
Get all platform variants of a command for try-until-success patterns.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
command_type: Command type (e.g., 'neighbors')
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of unique command strings across all platforms
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
>>> get_command_alternatives('neighbors')
|
|
241
|
+
['show cdp neighbors detail', 'show lldp neighbors detail', 'show lldp neighbors']
|
|
242
|
+
"""
|
|
243
|
+
commands = set()
|
|
244
|
+
|
|
245
|
+
# Gather from all platforms
|
|
246
|
+
for platform_cmds in PLATFORM_COMMANDS.values():
|
|
247
|
+
cmd = platform_cmds.get(command_type)
|
|
248
|
+
if cmd:
|
|
249
|
+
commands.add(cmd)
|
|
250
|
+
|
|
251
|
+
# Add default
|
|
252
|
+
default = DEFAULT_COMMANDS.get(command_type)
|
|
253
|
+
if default:
|
|
254
|
+
commands.add(default)
|
|
255
|
+
|
|
256
|
+
return list(commands)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def extract_field(
|
|
260
|
+
data: Dict[str, Any],
|
|
261
|
+
field_names: List[str],
|
|
262
|
+
default: Any = None,
|
|
263
|
+
first_element: bool = True,
|
|
264
|
+
) -> Any:
|
|
265
|
+
"""
|
|
266
|
+
Extract a field value trying multiple possible field names.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
data: Dictionary to extract from (e.g., parsed command output row)
|
|
270
|
+
field_names: List of possible field names in priority order
|
|
271
|
+
default: Value to return if no field found
|
|
272
|
+
first_element: If value is a list, return first element
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Field value or default
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
>>> data = {'VERSION': '15.2(4)M', 'HARDWARE': ['ISR4451']}
|
|
279
|
+
>>> extract_field(data, ['version', 'VERSION', 'SOFTWARE'])
|
|
280
|
+
'15.2(4)M'
|
|
281
|
+
>>> extract_field(data, ['HARDWARE', 'MODEL'])
|
|
282
|
+
'ISR4451' # first_element=True extracts from list
|
|
283
|
+
"""
|
|
284
|
+
for name in field_names:
|
|
285
|
+
if name in data and data[name]:
|
|
286
|
+
value = data[name]
|
|
287
|
+
# Handle list values
|
|
288
|
+
if first_element and isinstance(value, list):
|
|
289
|
+
return value[0] if value else default
|
|
290
|
+
return value
|
|
291
|
+
return default
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def extract_fields(
|
|
295
|
+
data: Dict[str, Any],
|
|
296
|
+
field_map: Dict[str, List[str]],
|
|
297
|
+
defaults: Optional[Dict[str, Any]] = None,
|
|
298
|
+
) -> Dict[str, Any]:
|
|
299
|
+
"""
|
|
300
|
+
Extract multiple fields using a field mapping.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
data: Dictionary to extract from
|
|
304
|
+
field_map: Mapping of canonical names to possible field names
|
|
305
|
+
defaults: Default values for each canonical field
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Dictionary with canonical field names and extracted values
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> data = {'VERSION': '15.2', 'HARDWARE': 'ISR4451'}
|
|
312
|
+
>>> field_map = {'version': ['VERSION', 'SOFTWARE'], 'hardware': ['HARDWARE', 'MODEL']}
|
|
313
|
+
>>> extract_fields(data, field_map)
|
|
314
|
+
{'version': '15.2', 'hardware': 'ISR4451'}
|
|
315
|
+
"""
|
|
316
|
+
defaults = defaults or {}
|
|
317
|
+
result = {}
|
|
318
|
+
|
|
319
|
+
for canonical_name, possible_names in field_map.items():
|
|
320
|
+
default = defaults.get(canonical_name, 'unknown')
|
|
321
|
+
result[canonical_name] = extract_field(data, possible_names, default)
|
|
322
|
+
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def extract_version_info(
|
|
327
|
+
parsed_data: List[Dict[str, Any]],
|
|
328
|
+
platform: Optional[str] = None,
|
|
329
|
+
) -> Dict[str, Any]:
|
|
330
|
+
"""
|
|
331
|
+
Extract version information from parsed 'show version' output.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
parsed_data: Parsed output from show version command
|
|
335
|
+
platform: Platform string for platform-specific field mapping
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Dictionary with version, hardware, serial, uptime, hostname
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> result = api.send(session, "show version")
|
|
342
|
+
>>> extract_version_info(result.parsed_data, session.platform)
|
|
343
|
+
{'version': '15.2(4)M', 'hardware': 'ISR4451', 'serial': 'FDO2134', ...}
|
|
344
|
+
"""
|
|
345
|
+
if not parsed_data:
|
|
346
|
+
return {
|
|
347
|
+
'version': 'unknown',
|
|
348
|
+
'hardware': 'unknown',
|
|
349
|
+
'serial': 'unknown',
|
|
350
|
+
'uptime': 'unknown',
|
|
351
|
+
'hostname': 'unknown',
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
data = parsed_data[0] # show version typically returns single row
|
|
355
|
+
|
|
356
|
+
# Platform-specific field mappings
|
|
357
|
+
# These override the generic mappings in platform_data.py when needed
|
|
358
|
+
PLATFORM_VERSION_FIELDS = {
|
|
359
|
+
'arista_eos': {
|
|
360
|
+
'version': ['IMAGE', 'VERSION', 'SOFTWARE_IMAGE', 'EOS_VERSION'],
|
|
361
|
+
'hardware': ['MODEL', 'HARDWARE', 'PLATFORM'],
|
|
362
|
+
'serial': ['SERIAL_NUMBER', 'SERIAL', 'SN'],
|
|
363
|
+
'uptime': ['UPTIME'],
|
|
364
|
+
'hostname': ['HOSTNAME'],
|
|
365
|
+
},
|
|
366
|
+
'cisco_ios': {
|
|
367
|
+
'version': ['VERSION', 'SOFTWARE_VERSION', 'ROMMON'],
|
|
368
|
+
'hardware': ['HARDWARE', 'MODEL', 'PLATFORM'],
|
|
369
|
+
'serial': ['SERIAL', 'SERIAL_NUMBER'],
|
|
370
|
+
'uptime': ['UPTIME'],
|
|
371
|
+
'hostname': ['HOSTNAME'],
|
|
372
|
+
},
|
|
373
|
+
'cisco_nxos': {
|
|
374
|
+
'version': ['OS', 'VERSION', 'NXOS_VERSION', 'KICKSTART_VERSION'],
|
|
375
|
+
'hardware': ['PLATFORM', 'HARDWARE', 'CHASSIS'],
|
|
376
|
+
'serial': ['SERIAL', 'SERIAL_NUMBER'],
|
|
377
|
+
'uptime': ['UPTIME'],
|
|
378
|
+
'hostname': ['HOSTNAME', 'DEVICE_NAME'],
|
|
379
|
+
},
|
|
380
|
+
'cisco_iosxe': {
|
|
381
|
+
'version': ['VERSION', 'ROMMON'],
|
|
382
|
+
'hardware': ['HARDWARE', 'MODEL', 'PLATFORM'],
|
|
383
|
+
'serial': ['SERIAL', 'SERIAL_NUMBER'],
|
|
384
|
+
'uptime': ['UPTIME'],
|
|
385
|
+
'hostname': ['HOSTNAME'],
|
|
386
|
+
},
|
|
387
|
+
'juniper_junos': {
|
|
388
|
+
'version': ['JUNOS_VERSION', 'VERSION'],
|
|
389
|
+
'hardware': ['MODEL', 'HARDWARE'],
|
|
390
|
+
'serial': ['SERIAL_NUMBER', 'SERIAL'],
|
|
391
|
+
'uptime': ['UPTIME', 'RE_UPTIME'],
|
|
392
|
+
'hostname': ['HOSTNAME'],
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# Default field mapping (tries common field names)
|
|
397
|
+
DEFAULT_FIELDS = {
|
|
398
|
+
'version': ['VERSION', 'IMAGE', 'SOFTWARE_VERSION', 'SOFTWARE_IMAGE', 'OS', 'ROMMON'],
|
|
399
|
+
'hardware': ['HARDWARE', 'MODEL', 'PLATFORM', 'CHASSIS'],
|
|
400
|
+
'serial': ['SERIAL', 'SERIAL_NUMBER', 'SN'],
|
|
401
|
+
'uptime': ['UPTIME'],
|
|
402
|
+
'hostname': ['HOSTNAME', 'DEVICE_NAME'],
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Get platform-specific mapping or fall back to defaults
|
|
406
|
+
if platform and platform in PLATFORM_VERSION_FIELDS:
|
|
407
|
+
field_map = PLATFORM_VERSION_FIELDS[platform]
|
|
408
|
+
else:
|
|
409
|
+
# Try to load from platform_data if available
|
|
410
|
+
try:
|
|
411
|
+
loaded_map = VERSION_FIELD_MAP.get(platform, DEFAULT_VERSION_FIELD_MAP) if platform else DEFAULT_VERSION_FIELD_MAP
|
|
412
|
+
field_map = loaded_map if loaded_map else DEFAULT_FIELDS
|
|
413
|
+
except (NameError, TypeError):
|
|
414
|
+
field_map = DEFAULT_FIELDS
|
|
415
|
+
|
|
416
|
+
return extract_fields(data, field_map, defaults={
|
|
417
|
+
'version': 'unknown',
|
|
418
|
+
'hardware': 'unknown',
|
|
419
|
+
'serial': 'unknown',
|
|
420
|
+
'uptime': 'unknown',
|
|
421
|
+
'hostname': 'unknown',
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def extract_neighbor_info(
|
|
426
|
+
parsed_data: List[Dict[str, Any]],
|
|
427
|
+
) -> List[Dict[str, Any]]:
|
|
428
|
+
"""
|
|
429
|
+
Extract neighbor information from parsed CDP/LLDP output.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
parsed_data: Parsed output from show cdp/lldp neighbors command
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
List of neighbor dictionaries with normalized field names
|
|
436
|
+
"""
|
|
437
|
+
if not parsed_data:
|
|
438
|
+
return []
|
|
439
|
+
|
|
440
|
+
neighbors = []
|
|
441
|
+
for row in parsed_data:
|
|
442
|
+
neighbor = extract_fields(row, NEIGHBOR_FIELD_MAP, defaults={
|
|
443
|
+
'local_interface': 'unknown',
|
|
444
|
+
'neighbor_device': 'unknown',
|
|
445
|
+
'neighbor_interface': 'unknown',
|
|
446
|
+
'platform': 'unknown',
|
|
447
|
+
'ip_address': '',
|
|
448
|
+
})
|
|
449
|
+
neighbors.append(neighbor)
|
|
450
|
+
|
|
451
|
+
return neighbors
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def normalize_fields(
|
|
455
|
+
parsed_data: List[Dict[str, Any]],
|
|
456
|
+
platform: str,
|
|
457
|
+
field_map_dict: Dict[str, Dict[str, List[str]]],
|
|
458
|
+
) -> List[Dict[str, Any]]:
|
|
459
|
+
"""
|
|
460
|
+
Normalize vendor-specific field names to canonical names.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
parsed_data: Raw parsed data from TextFSM
|
|
464
|
+
platform: Detected platform (e.g., 'cisco_ios')
|
|
465
|
+
field_map_dict: Mapping dict (e.g., INTERFACE_DETAIL_FIELD_MAP)
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
List of dicts with normalized field names
|
|
469
|
+
|
|
470
|
+
Example:
|
|
471
|
+
>>> data = [{'LINK_STATUS': 'up', 'PROTOCOL_STATUS': 'up'}]
|
|
472
|
+
>>> normalize_fields(data, 'cisco_ios', INTERFACE_DETAIL_FIELD_MAP)
|
|
473
|
+
[{'admin_state': 'up', 'oper_state': 'up'}]
|
|
474
|
+
"""
|
|
475
|
+
field_map = field_map_dict.get(platform, DEFAULT_FIELD_MAP)
|
|
476
|
+
normalized = []
|
|
477
|
+
|
|
478
|
+
for row in parsed_data:
|
|
479
|
+
norm_row = {}
|
|
480
|
+
|
|
481
|
+
# Map vendor-specific fields to canonical names
|
|
482
|
+
for canonical_name, vendor_names in field_map.items():
|
|
483
|
+
for vendor_name in vendor_names:
|
|
484
|
+
if vendor_name in row:
|
|
485
|
+
norm_row[canonical_name] = row[vendor_name]
|
|
486
|
+
break
|
|
487
|
+
|
|
488
|
+
# Keep any fields that weren't in the mapping
|
|
489
|
+
mapped_vendor_fields = {vn for vnames in field_map.values() for vn in vnames}
|
|
490
|
+
for key, value in row.items():
|
|
491
|
+
if key not in mapped_vendor_fields:
|
|
492
|
+
norm_row[key] = value
|
|
493
|
+
|
|
494
|
+
normalized.append(norm_row)
|
|
495
|
+
|
|
496
|
+
return normalized
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def get_paging_disable_command(platform: Optional[str]) -> Optional[str]:
|
|
500
|
+
"""
|
|
501
|
+
Get the appropriate command to disable terminal paging for a platform.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
platform: Detected platform string
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Command string or None if platform doesn't require paging disable
|
|
508
|
+
"""
|
|
509
|
+
if not platform:
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
if 'cisco' in platform:
|
|
513
|
+
return "terminal length 0"
|
|
514
|
+
elif platform == 'juniper_junos':
|
|
515
|
+
return "set cli screen-length 0"
|
|
516
|
+
elif platform == 'arista_eos':
|
|
517
|
+
return "terminal length 0"
|
|
518
|
+
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def get_paging_disable_commands_to_try() -> List[str]:
|
|
523
|
+
"""
|
|
524
|
+
Get list of paging disable commands to try when platform is unknown.
|
|
525
|
+
|
|
526
|
+
Returns a prioritized list of commands that cover most network platforms.
|
|
527
|
+
Commands are ordered by likelihood of success across vendors.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
List of paging disable commands to try in order
|
|
531
|
+
"""
|
|
532
|
+
return [
|
|
533
|
+
# Cisco IOS/IOS-XE/NX-OS, Arista EOS (most common)
|
|
534
|
+
"terminal length 0",
|
|
535
|
+
# Cisco alternative
|
|
536
|
+
"terminal pager 0",
|
|
537
|
+
# Juniper JUNOS
|
|
538
|
+
"set cli screen-length 0",
|
|
539
|
+
# Some Cisco platforms
|
|
540
|
+
"screen-length 0",
|
|
541
|
+
# HP/Aruba
|
|
542
|
+
"no page",
|
|
543
|
+
# Huawei
|
|
544
|
+
"screen-length 0 temporary",
|
|
545
|
+
# Extreme
|
|
546
|
+
"disable clipaging",
|
|
547
|
+
# Dell/Force10
|
|
548
|
+
"terminal length 0",
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def try_disable_paging(shell, prompt, send_command_func, debug: bool = False) -> bool:
|
|
553
|
+
"""
|
|
554
|
+
Try multiple commands to disable terminal paging.
|
|
555
|
+
|
|
556
|
+
Used for auto-recovery when PagingNotDisabledError is raised.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
shell: Active SSH channel
|
|
560
|
+
prompt: Device prompt
|
|
561
|
+
send_command_func: Function to send commands (ssh_connection.send_command)
|
|
562
|
+
debug: Print debug info
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
True if any command succeeded (didn't raise exception), False if all failed
|
|
566
|
+
"""
|
|
567
|
+
commands = get_paging_disable_commands_to_try()
|
|
568
|
+
|
|
569
|
+
for cmd in commands:
|
|
570
|
+
try:
|
|
571
|
+
if debug:
|
|
572
|
+
print(f"[PAGING] Trying: {cmd}")
|
|
573
|
+
# Short timeout - these commands should return quickly
|
|
574
|
+
send_command_func(shell, cmd, prompt, timeout=5)
|
|
575
|
+
if debug:
|
|
576
|
+
print(f"[PAGING] Success: {cmd}")
|
|
577
|
+
return True
|
|
578
|
+
except Exception as e:
|
|
579
|
+
if debug:
|
|
580
|
+
print(f"[PAGING] Failed: {cmd} - {e}")
|
|
581
|
+
continue
|
|
582
|
+
|
|
583
|
+
return False
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def sanitize_filename(name: str) -> str:
|
|
587
|
+
"""
|
|
588
|
+
Make a device name safe for use in filenames.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
name: Device name or identifier
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
Sanitized string safe for filesystem use
|
|
595
|
+
"""
|
|
596
|
+
return "".join(c if c.isalnum() or c in ('-', '_') else '_' for c in name)
|