ntermqt 0.1.6__py3-none-any.whl → 0.1.8__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/parser/tfsm_fire_tester.py +561 -731
- nterm/scripting/api.py +249 -641
- nterm/scripting/models.py +195 -0
- nterm/scripting/platform_data.py +272 -0
- nterm/scripting/platform_utils.py +330 -0
- nterm/scripting/repl.py +344 -103
- nterm/scripting/repl_interactive.py +331 -213
- nterm/scripting/ssh_connection.py +632 -0
- nterm/scripting/test_api_repl.py +290 -0
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/METADATA +88 -28
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/RECORD +15 -10
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/entry_points.txt +0 -0
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
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) -> Optional[str]:
|
|
23
|
+
"""
|
|
24
|
+
Detect device platform from 'show version' output.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
version_output: Raw output from 'show version' command
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Platform string (e.g., 'cisco_ios', 'arista_eos') or None if not detected
|
|
31
|
+
"""
|
|
32
|
+
for platform, patterns in PLATFORM_PATTERNS.items():
|
|
33
|
+
for pattern in patterns:
|
|
34
|
+
if re.search(pattern, version_output, re.IGNORECASE):
|
|
35
|
+
return platform
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_platform_command(
|
|
40
|
+
platform: Optional[str],
|
|
41
|
+
command_type: str,
|
|
42
|
+
**kwargs
|
|
43
|
+
) -> Optional[str]:
|
|
44
|
+
"""
|
|
45
|
+
Get platform-specific command for a given operation.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
platform: Platform string (e.g., 'cisco_ios') or None
|
|
49
|
+
command_type: Command type (e.g., 'config', 'version', 'interfaces')
|
|
50
|
+
**kwargs: Format arguments for commands with placeholders (e.g., name='Gi0/1')
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Command string or None if command not available for platform
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> get_platform_command('cisco_ios', 'config')
|
|
57
|
+
'show running-config'
|
|
58
|
+
>>> get_platform_command('juniper_junos', 'config')
|
|
59
|
+
'show configuration'
|
|
60
|
+
>>> get_platform_command('cisco_ios', 'interface_detail', name='Gi0/1')
|
|
61
|
+
'show interfaces Gi0/1'
|
|
62
|
+
"""
|
|
63
|
+
# Get platform-specific commands or fall back to defaults
|
|
64
|
+
platform_cmds = PLATFORM_COMMANDS.get(platform, {}) if platform else {}
|
|
65
|
+
|
|
66
|
+
# Look up command type
|
|
67
|
+
cmd = platform_cmds.get(command_type)
|
|
68
|
+
if cmd is None:
|
|
69
|
+
cmd = DEFAULT_COMMANDS.get(command_type)
|
|
70
|
+
|
|
71
|
+
if cmd is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# Apply format arguments if provided
|
|
75
|
+
if kwargs:
|
|
76
|
+
try:
|
|
77
|
+
cmd = cmd.format(**kwargs)
|
|
78
|
+
except KeyError:
|
|
79
|
+
pass # Return unformatted if missing keys
|
|
80
|
+
|
|
81
|
+
return cmd
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_command_alternatives(command_type: str) -> List[str]:
|
|
85
|
+
"""
|
|
86
|
+
Get all platform variants of a command for try-until-success patterns.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
command_type: Command type (e.g., 'neighbors')
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of unique command strings across all platforms
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> get_command_alternatives('neighbors')
|
|
96
|
+
['show cdp neighbors detail', 'show lldp neighbors detail', 'show lldp neighbors']
|
|
97
|
+
"""
|
|
98
|
+
commands = set()
|
|
99
|
+
|
|
100
|
+
# Gather from all platforms
|
|
101
|
+
for platform_cmds in PLATFORM_COMMANDS.values():
|
|
102
|
+
cmd = platform_cmds.get(command_type)
|
|
103
|
+
if cmd:
|
|
104
|
+
commands.add(cmd)
|
|
105
|
+
|
|
106
|
+
# Add default
|
|
107
|
+
default = DEFAULT_COMMANDS.get(command_type)
|
|
108
|
+
if default:
|
|
109
|
+
commands.add(default)
|
|
110
|
+
|
|
111
|
+
return list(commands)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extract_field(
|
|
115
|
+
data: Dict[str, Any],
|
|
116
|
+
field_names: List[str],
|
|
117
|
+
default: Any = None,
|
|
118
|
+
first_element: bool = True,
|
|
119
|
+
) -> Any:
|
|
120
|
+
"""
|
|
121
|
+
Extract a field value trying multiple possible field names.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
data: Dictionary to extract from (e.g., parsed command output row)
|
|
125
|
+
field_names: List of possible field names in priority order
|
|
126
|
+
default: Value to return if no field found
|
|
127
|
+
first_element: If value is a list, return first element
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Field value or default
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> data = {'VERSION': '15.2(4)M', 'HARDWARE': ['ISR4451']}
|
|
134
|
+
>>> extract_field(data, ['version', 'VERSION', 'SOFTWARE'])
|
|
135
|
+
'15.2(4)M'
|
|
136
|
+
>>> extract_field(data, ['HARDWARE', 'MODEL'])
|
|
137
|
+
'ISR4451' # first_element=True extracts from list
|
|
138
|
+
"""
|
|
139
|
+
for name in field_names:
|
|
140
|
+
if name in data and data[name]:
|
|
141
|
+
value = data[name]
|
|
142
|
+
# Handle list values
|
|
143
|
+
if first_element and isinstance(value, list):
|
|
144
|
+
return value[0] if value else default
|
|
145
|
+
return value
|
|
146
|
+
return default
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def extract_fields(
|
|
150
|
+
data: Dict[str, Any],
|
|
151
|
+
field_map: Dict[str, List[str]],
|
|
152
|
+
defaults: Optional[Dict[str, Any]] = None,
|
|
153
|
+
) -> Dict[str, Any]:
|
|
154
|
+
"""
|
|
155
|
+
Extract multiple fields using a field mapping.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
data: Dictionary to extract from
|
|
159
|
+
field_map: Mapping of canonical names to possible field names
|
|
160
|
+
defaults: Default values for each canonical field
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary with canonical field names and extracted values
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> data = {'VERSION': '15.2', 'HARDWARE': 'ISR4451'}
|
|
167
|
+
>>> field_map = {'version': ['VERSION', 'SOFTWARE'], 'hardware': ['HARDWARE', 'MODEL']}
|
|
168
|
+
>>> extract_fields(data, field_map)
|
|
169
|
+
{'version': '15.2', 'hardware': 'ISR4451'}
|
|
170
|
+
"""
|
|
171
|
+
defaults = defaults or {}
|
|
172
|
+
result = {}
|
|
173
|
+
|
|
174
|
+
for canonical_name, possible_names in field_map.items():
|
|
175
|
+
default = defaults.get(canonical_name, 'unknown')
|
|
176
|
+
result[canonical_name] = extract_field(data, possible_names, default)
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def extract_version_info(
|
|
182
|
+
parsed_data: List[Dict[str, Any]],
|
|
183
|
+
platform: Optional[str] = None,
|
|
184
|
+
) -> Dict[str, Any]:
|
|
185
|
+
"""
|
|
186
|
+
Extract version information from parsed 'show version' output.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
parsed_data: Parsed output from show version command
|
|
190
|
+
platform: Platform string for platform-specific field mapping
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dictionary with version, hardware, serial, uptime, hostname
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> result = api.send(session, "show version")
|
|
197
|
+
>>> extract_version_info(result.parsed_data, session.platform)
|
|
198
|
+
{'version': '15.2(4)M', 'hardware': 'ISR4451', 'serial': 'FDO2134', ...}
|
|
199
|
+
"""
|
|
200
|
+
if not parsed_data:
|
|
201
|
+
return {
|
|
202
|
+
'version': 'unknown',
|
|
203
|
+
'hardware': 'unknown',
|
|
204
|
+
'serial': 'unknown',
|
|
205
|
+
'uptime': 'unknown',
|
|
206
|
+
'hostname': 'unknown',
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
data = parsed_data[0] # show version typically returns single row
|
|
210
|
+
|
|
211
|
+
# Get platform-specific or default field map
|
|
212
|
+
field_map = VERSION_FIELD_MAP.get(platform, DEFAULT_VERSION_FIELD_MAP) if platform else DEFAULT_VERSION_FIELD_MAP
|
|
213
|
+
|
|
214
|
+
return extract_fields(data, field_map, defaults={
|
|
215
|
+
'version': 'unknown',
|
|
216
|
+
'hardware': 'unknown',
|
|
217
|
+
'serial': 'unknown',
|
|
218
|
+
'uptime': 'unknown',
|
|
219
|
+
'hostname': 'unknown',
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def extract_neighbor_info(
|
|
224
|
+
parsed_data: List[Dict[str, Any]],
|
|
225
|
+
) -> List[Dict[str, Any]]:
|
|
226
|
+
"""
|
|
227
|
+
Extract neighbor information from parsed CDP/LLDP output.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
parsed_data: Parsed output from show cdp/lldp neighbors command
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of neighbor dictionaries with normalized field names
|
|
234
|
+
"""
|
|
235
|
+
if not parsed_data:
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
neighbors = []
|
|
239
|
+
for row in parsed_data:
|
|
240
|
+
neighbor = extract_fields(row, NEIGHBOR_FIELD_MAP, defaults={
|
|
241
|
+
'local_interface': 'unknown',
|
|
242
|
+
'neighbor_device': 'unknown',
|
|
243
|
+
'neighbor_interface': 'unknown',
|
|
244
|
+
'platform': 'unknown',
|
|
245
|
+
'ip_address': '',
|
|
246
|
+
})
|
|
247
|
+
neighbors.append(neighbor)
|
|
248
|
+
|
|
249
|
+
return neighbors
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def normalize_fields(
|
|
253
|
+
parsed_data: List[Dict[str, Any]],
|
|
254
|
+
platform: str,
|
|
255
|
+
field_map_dict: Dict[str, Dict[str, List[str]]],
|
|
256
|
+
) -> List[Dict[str, Any]]:
|
|
257
|
+
"""
|
|
258
|
+
Normalize vendor-specific field names to canonical names.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
parsed_data: Raw parsed data from TextFSM
|
|
262
|
+
platform: Detected platform (e.g., 'cisco_ios')
|
|
263
|
+
field_map_dict: Mapping dict (e.g., INTERFACE_DETAIL_FIELD_MAP)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of dicts with normalized field names
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
>>> data = [{'LINK_STATUS': 'up', 'PROTOCOL_STATUS': 'up'}]
|
|
270
|
+
>>> normalize_fields(data, 'cisco_ios', INTERFACE_DETAIL_FIELD_MAP)
|
|
271
|
+
[{'admin_state': 'up', 'oper_state': 'up'}]
|
|
272
|
+
"""
|
|
273
|
+
field_map = field_map_dict.get(platform, DEFAULT_FIELD_MAP)
|
|
274
|
+
normalized = []
|
|
275
|
+
|
|
276
|
+
for row in parsed_data:
|
|
277
|
+
norm_row = {}
|
|
278
|
+
|
|
279
|
+
# Map vendor-specific fields to canonical names
|
|
280
|
+
for canonical_name, vendor_names in field_map.items():
|
|
281
|
+
for vendor_name in vendor_names:
|
|
282
|
+
if vendor_name in row:
|
|
283
|
+
norm_row[canonical_name] = row[vendor_name]
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
# Keep any fields that weren't in the mapping
|
|
287
|
+
mapped_vendor_fields = {vn for vnames in field_map.values() for vn in vnames}
|
|
288
|
+
for key, value in row.items():
|
|
289
|
+
if key not in mapped_vendor_fields:
|
|
290
|
+
norm_row[key] = value
|
|
291
|
+
|
|
292
|
+
normalized.append(norm_row)
|
|
293
|
+
|
|
294
|
+
return normalized
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_paging_disable_command(platform: Optional[str]) -> Optional[str]:
|
|
298
|
+
"""
|
|
299
|
+
Get the appropriate command to disable terminal paging for a platform.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
platform: Detected platform string
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Command string or None if platform doesn't require paging disable
|
|
306
|
+
"""
|
|
307
|
+
if not platform:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
if 'cisco' in platform:
|
|
311
|
+
return "terminal length 0"
|
|
312
|
+
elif platform == 'juniper_junos':
|
|
313
|
+
return "set cli screen-length 0"
|
|
314
|
+
elif platform == 'arista_eos':
|
|
315
|
+
return "terminal length 0"
|
|
316
|
+
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def sanitize_filename(name: str) -> str:
|
|
321
|
+
"""
|
|
322
|
+
Make a device name safe for use in filenames.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
name: Device name or identifier
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Sanitized string safe for filesystem use
|
|
329
|
+
"""
|
|
330
|
+
return "".join(c if c.isalnum() or c in ('-', '_') else '_' for c in name)
|