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.
@@ -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)