netbox-toolkit-plugin 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.
- netbox_toolkit/__init__.py +30 -0
- netbox_toolkit/admin.py +16 -0
- netbox_toolkit/api/__init__.py +0 -0
- netbox_toolkit/api/mixins.py +54 -0
- netbox_toolkit/api/schemas.py +234 -0
- netbox_toolkit/api/serializers.py +158 -0
- netbox_toolkit/api/urls.py +10 -0
- netbox_toolkit/api/views/__init__.py +10 -0
- netbox_toolkit/api/views/command_logs.py +170 -0
- netbox_toolkit/api/views/commands.py +267 -0
- netbox_toolkit/config.py +159 -0
- netbox_toolkit/connectors/__init__.py +15 -0
- netbox_toolkit/connectors/base.py +97 -0
- netbox_toolkit/connectors/factory.py +301 -0
- netbox_toolkit/connectors/netmiko_connector.py +443 -0
- netbox_toolkit/connectors/scrapli_connector.py +486 -0
- netbox_toolkit/exceptions.py +36 -0
- netbox_toolkit/filtersets.py +85 -0
- netbox_toolkit/forms.py +31 -0
- netbox_toolkit/migrations/0001_initial.py +54 -0
- netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +66 -0
- netbox_toolkit/migrations/0003_permission_system_update.py +48 -0
- netbox_toolkit/migrations/0004_remove_django_permissions.py +77 -0
- netbox_toolkit/migrations/0005_alter_command_options_and_more.py +25 -0
- netbox_toolkit/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +28 -0
- netbox_toolkit/migrations/0007_alter_commandlog_parsing_template.py +18 -0
- netbox_toolkit/migrations/__init__.py +0 -0
- netbox_toolkit/models.py +89 -0
- netbox_toolkit/navigation.py +30 -0
- netbox_toolkit/search.py +21 -0
- netbox_toolkit/services/__init__.py +7 -0
- netbox_toolkit/services/command_service.py +357 -0
- netbox_toolkit/services/device_service.py +87 -0
- netbox_toolkit/services/rate_limiting_service.py +228 -0
- netbox_toolkit/static/netbox_toolkit/css/toolkit.css +143 -0
- netbox_toolkit/static/netbox_toolkit/js/toolkit.js +657 -0
- netbox_toolkit/tables.py +37 -0
- netbox_toolkit/templates/netbox_toolkit/command.html +108 -0
- netbox_toolkit/templates/netbox_toolkit/command_edit.html +10 -0
- netbox_toolkit/templates/netbox_toolkit/command_list.html +12 -0
- netbox_toolkit/templates/netbox_toolkit/commandlog.html +170 -0
- netbox_toolkit/templates/netbox_toolkit/commandlog_list.html +4 -0
- netbox_toolkit/templates/netbox_toolkit/device_toolkit.html +536 -0
- netbox_toolkit/urls.py +22 -0
- netbox_toolkit/utils/__init__.py +1 -0
- netbox_toolkit/utils/connection.py +125 -0
- netbox_toolkit/utils/error_parser.py +428 -0
- netbox_toolkit/utils/logging.py +58 -0
- netbox_toolkit/utils/network.py +157 -0
- netbox_toolkit/views.py +385 -0
- netbox_toolkit_plugin-0.1.0.dist-info/METADATA +76 -0
- netbox_toolkit_plugin-0.1.0.dist-info/RECORD +56 -0
- netbox_toolkit_plugin-0.1.0.dist-info/WHEEL +5 -0
- netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- netbox_toolkit_plugin-0.1.0.dist-info/licenses/LICENSE +200 -0
- netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,443 @@
|
|
1
|
+
"""Netmiko-based device connector implementation."""
|
2
|
+
import time
|
3
|
+
from typing import Dict, Any, Optional, Tuple
|
4
|
+
|
5
|
+
from netmiko import ConnectHandler, SSHDetect
|
6
|
+
from netmiko.exceptions import (
|
7
|
+
NetmikoTimeoutException,
|
8
|
+
NetmikoAuthenticationException,
|
9
|
+
NetmikoBaseException,
|
10
|
+
ConnectionException,
|
11
|
+
ConfigInvalidException
|
12
|
+
)
|
13
|
+
|
14
|
+
from .base import BaseDeviceConnector, ConnectionConfig, CommandResult
|
15
|
+
from ..config import ToolkitConfig
|
16
|
+
from ..exceptions import DeviceConnectionError, CommandExecutionError, UnsupportedPlatformError
|
17
|
+
from ..utils.network import validate_device_connectivity
|
18
|
+
from ..utils.error_parser import VendorErrorParser
|
19
|
+
from ..utils.logging import get_toolkit_logger
|
20
|
+
|
21
|
+
logger = get_toolkit_logger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class NetmikoConnector(BaseDeviceConnector):
|
25
|
+
"""Netmiko-based implementation of device connector for legacy/fallback support."""
|
26
|
+
|
27
|
+
# NetBox platform to Netmiko device_type mapping
|
28
|
+
DEVICE_TYPE_MAP = {
|
29
|
+
'cisco_ios': 'cisco_ios',
|
30
|
+
'cisco_nxos': 'cisco_nxos',
|
31
|
+
'cisco_iosxr': 'cisco_xr',
|
32
|
+
'cisco_xe': 'cisco_ios',
|
33
|
+
'cisco_asa': 'cisco_asa',
|
34
|
+
'arista_eos': 'arista_eos',
|
35
|
+
'juniper_junos': 'juniper_junos',
|
36
|
+
'hp_procurve': 'hp_procurve',
|
37
|
+
'hp_comware': 'hp_comware',
|
38
|
+
'dell_os10': 'dell_os10',
|
39
|
+
'dell_powerconnect': 'dell_powerconnect',
|
40
|
+
'linux': 'linux',
|
41
|
+
'paloalto_panos': 'paloalto_panos',
|
42
|
+
'fortinet': 'fortinet',
|
43
|
+
'mikrotik_routeros': 'mikrotik_routeros',
|
44
|
+
'ubiquiti_edge': 'ubiquiti_edge',
|
45
|
+
# Generic fallback
|
46
|
+
'generic': 'generic_termserver',
|
47
|
+
'autodetect': 'autodetect'
|
48
|
+
}
|
49
|
+
|
50
|
+
def __init__(self, config: ConnectionConfig):
|
51
|
+
super().__init__(config)
|
52
|
+
self._error_parser = VendorErrorParser()
|
53
|
+
self._retry_config = ToolkitConfig.get_retry_config()
|
54
|
+
|
55
|
+
# Use config from extra_options if available, otherwise get from ToolkitConfig
|
56
|
+
if config.extra_options:
|
57
|
+
self._netmiko_config = config.extra_options.copy()
|
58
|
+
logger.debug(f"Using Netmiko config from extra_options: {list(self._netmiko_config.keys())}")
|
59
|
+
else:
|
60
|
+
self._netmiko_config = ToolkitConfig.get_netmiko_config()
|
61
|
+
logger.debug("Using default Netmiko config from ToolkitConfig")
|
62
|
+
|
63
|
+
logger.debug(f"Initialized NetmikoConnector for {config.hostname} with platform '{config.platform}'")
|
64
|
+
|
65
|
+
def _filter_valid_netmiko_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
66
|
+
"""Filter parameters to only include those supported by Netmiko."""
|
67
|
+
# Define valid Netmiko connection parameters
|
68
|
+
valid_netmiko_params = {
|
69
|
+
'device_type', 'host', 'username', 'password', 'port', 'timeout',
|
70
|
+
'banner_timeout', 'auth_timeout', 'global_delay_factor', 'use_keys',
|
71
|
+
'key_file', 'allow_agent', 'session_log', 'session_log_record_writes',
|
72
|
+
'session_log_file_mode', 'fast_cli', 'secret', 'blocking_timeout',
|
73
|
+
'verbose', 'conn_timeout', 'read_timeout', 'keepalive'
|
74
|
+
}
|
75
|
+
|
76
|
+
filtered_params = {}
|
77
|
+
for key, value in params.items():
|
78
|
+
if key in valid_netmiko_params:
|
79
|
+
filtered_params[key] = value
|
80
|
+
else:
|
81
|
+
logger.debug(f"Filtering out unsupported Netmiko parameter: {key}")
|
82
|
+
|
83
|
+
return filtered_params
|
84
|
+
|
85
|
+
@classmethod
|
86
|
+
def get_supported_platforms(cls) -> list:
|
87
|
+
"""Get list of supported platform names."""
|
88
|
+
return list(cls.DEVICE_TYPE_MAP.keys())
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def normalize_platform_name(cls, platform_name: str) -> str:
|
92
|
+
"""Normalize platform name for consistent mapping."""
|
93
|
+
if not platform_name:
|
94
|
+
return 'autodetect'
|
95
|
+
|
96
|
+
normalized = platform_name.lower().strip()
|
97
|
+
|
98
|
+
# Handle common variations
|
99
|
+
platform_mappings = {
|
100
|
+
'ios': 'cisco_ios',
|
101
|
+
'nxos': 'cisco_nxos',
|
102
|
+
'nexus': 'cisco_nxos',
|
103
|
+
'iosxr': 'cisco_iosxr',
|
104
|
+
'ios-xr': 'cisco_iosxr',
|
105
|
+
'ios-xe': 'cisco_xe',
|
106
|
+
'eos': 'arista_eos',
|
107
|
+
'junos': 'juniper_junos',
|
108
|
+
'asa': 'cisco_asa',
|
109
|
+
}
|
110
|
+
|
111
|
+
return platform_mappings.get(normalized, normalized)
|
112
|
+
|
113
|
+
def _get_device_type(self) -> str:
|
114
|
+
"""Get the appropriate Netmiko device_type for the platform."""
|
115
|
+
if not self.config.platform:
|
116
|
+
logger.debug("No platform specified, attempting auto-detection")
|
117
|
+
return 'autodetect'
|
118
|
+
|
119
|
+
normalized_platform = self.normalize_platform_name(self.config.platform)
|
120
|
+
device_type = self.DEVICE_TYPE_MAP.get(normalized_platform, 'autodetect')
|
121
|
+
|
122
|
+
logger.debug(f"Platform '{normalized_platform}' mapped to device_type '{device_type}'")
|
123
|
+
return device_type
|
124
|
+
|
125
|
+
def _auto_detect_device_type(self) -> str:
|
126
|
+
"""Use Netmiko's auto-detection for unknown platforms."""
|
127
|
+
try:
|
128
|
+
logger.debug(f"Attempting auto-detection for {self.config.hostname}")
|
129
|
+
|
130
|
+
device_dict = {
|
131
|
+
'device_type': 'autodetect',
|
132
|
+
'host': self.config.hostname,
|
133
|
+
'username': self.config.username,
|
134
|
+
'password': self.config.password,
|
135
|
+
'port': self.config.port,
|
136
|
+
'timeout': self.config.timeout_socket,
|
137
|
+
}
|
138
|
+
|
139
|
+
guesser = SSHDetect(**device_dict)
|
140
|
+
best_match = guesser.autodetect()
|
141
|
+
|
142
|
+
if best_match:
|
143
|
+
logger.info(f"Auto-detected device type '{best_match}' for {self.config.hostname}")
|
144
|
+
return best_match
|
145
|
+
else:
|
146
|
+
logger.warning(f"Auto-detection failed for {self.config.hostname}, using generic")
|
147
|
+
return 'generic_termserver'
|
148
|
+
|
149
|
+
except Exception as e:
|
150
|
+
logger.warning(f"Auto-detection error for {self.config.hostname}: {str(e)}")
|
151
|
+
return 'generic_termserver'
|
152
|
+
|
153
|
+
def _build_connection_params(self) -> Dict[str, Any]:
|
154
|
+
"""Build connection parameters for Netmiko."""
|
155
|
+
device_type = self._get_device_type()
|
156
|
+
|
157
|
+
# Handle auto-detection
|
158
|
+
if device_type == 'autodetect':
|
159
|
+
device_type = self._auto_detect_device_type()
|
160
|
+
|
161
|
+
params = {
|
162
|
+
'device_type': device_type,
|
163
|
+
'host': self.config.hostname,
|
164
|
+
'username': self.config.username,
|
165
|
+
'password': self.config.password,
|
166
|
+
'port': self.config.port,
|
167
|
+
'timeout': self.config.timeout_socket,
|
168
|
+
'banner_timeout': self._netmiko_config.get('banner_timeout', 15),
|
169
|
+
'auth_timeout': self._netmiko_config.get('auth_timeout', 15),
|
170
|
+
'blocking_timeout': self.config.timeout_ops,
|
171
|
+
}
|
172
|
+
|
173
|
+
# Add advanced options from Netmiko config
|
174
|
+
if 'global_delay_factor' in self._netmiko_config:
|
175
|
+
params['global_delay_factor'] = self._netmiko_config['global_delay_factor']
|
176
|
+
|
177
|
+
if 'session_log' in self._netmiko_config and self._netmiko_config['session_log']:
|
178
|
+
params['session_log'] = self._netmiko_config['session_log']
|
179
|
+
|
180
|
+
# SSH key options - only set if explicitly enabled
|
181
|
+
if self._netmiko_config.get('use_keys', False):
|
182
|
+
params['use_keys'] = True
|
183
|
+
if 'key_file' in self._netmiko_config:
|
184
|
+
params['key_file'] = self._netmiko_config['key_file']
|
185
|
+
else:
|
186
|
+
# Explicitly disable key authentication for faster connections
|
187
|
+
params['use_keys'] = False
|
188
|
+
|
189
|
+
# SSH agent options
|
190
|
+
if not self._netmiko_config.get('allow_agent', True):
|
191
|
+
params['allow_agent'] = False
|
192
|
+
|
193
|
+
# Add any other valid Netmiko options from extra_options
|
194
|
+
if self.config.extra_options:
|
195
|
+
# Filter to only include valid Netmiko parameters
|
196
|
+
valid_params = self._filter_valid_netmiko_params(self.config.extra_options)
|
197
|
+
params.update(valid_params)
|
198
|
+
|
199
|
+
logger.debug(f"Netmiko connection params: device_type={device_type}, host={params['host']}")
|
200
|
+
return params
|
201
|
+
|
202
|
+
def connect(self) -> None:
|
203
|
+
"""Establish connection to the device using Netmiko with retry logic."""
|
204
|
+
if self._connection:
|
205
|
+
logger.debug(f"Already connected to {self.config.hostname}")
|
206
|
+
return
|
207
|
+
|
208
|
+
# Validate connectivity first
|
209
|
+
validate_device_connectivity(self.config.hostname, self.config.port)
|
210
|
+
|
211
|
+
max_retries = self._retry_config['max_retries']
|
212
|
+
retry_delay = self._retry_config['retry_delay']
|
213
|
+
last_error = None
|
214
|
+
|
215
|
+
for attempt in range(max_retries + 1):
|
216
|
+
try:
|
217
|
+
if attempt > 0:
|
218
|
+
logger.debug(f"Connection attempt {attempt + 1}/{max_retries + 1} after {retry_delay}s delay")
|
219
|
+
time.sleep(retry_delay)
|
220
|
+
retry_delay *= self._retry_config['backoff_multiplier']
|
221
|
+
else:
|
222
|
+
logger.debug(f"Initial connection attempt to {self.config.hostname}")
|
223
|
+
|
224
|
+
# Build connection parameters
|
225
|
+
conn_params = self._build_connection_params()
|
226
|
+
|
227
|
+
# Create and establish connection
|
228
|
+
logger.debug(f"Creating Netmiko ConnectHandler for {self.config.hostname}")
|
229
|
+
self._connection = ConnectHandler(**conn_params)
|
230
|
+
|
231
|
+
logger.info(f"Successfully connected to {self.config.hostname} using Netmiko")
|
232
|
+
return
|
233
|
+
|
234
|
+
except NetmikoAuthenticationException as e:
|
235
|
+
logger.error(f"Authentication failed for {self.config.hostname}: {str(e)}")
|
236
|
+
raise DeviceConnectionError(f"Authentication failed: {str(e)}")
|
237
|
+
|
238
|
+
except NetmikoTimeoutException as e:
|
239
|
+
last_error = e
|
240
|
+
logger.warning(f"Connection timeout for {self.config.hostname}: {str(e)}")
|
241
|
+
|
242
|
+
if attempt >= max_retries:
|
243
|
+
raise DeviceConnectionError(f"Connection timeout after {max_retries + 1} attempts: {str(e)}")
|
244
|
+
|
245
|
+
except NetmikoBaseException as e:
|
246
|
+
last_error = e
|
247
|
+
logger.warning(f"Netmiko error for {self.config.hostname}: {str(e)}")
|
248
|
+
|
249
|
+
if attempt >= max_retries:
|
250
|
+
raise DeviceConnectionError(f"Netmiko connection failed: {str(e)}")
|
251
|
+
|
252
|
+
except Exception as e:
|
253
|
+
last_error = e
|
254
|
+
logger.warning(f"Unexpected error for {self.config.hostname}: {str(e)}")
|
255
|
+
|
256
|
+
if attempt >= max_retries:
|
257
|
+
raise DeviceConnectionError(f"Connection failed: {str(e)}")
|
258
|
+
|
259
|
+
def disconnect(self) -> None:
|
260
|
+
"""Close connection to the device."""
|
261
|
+
if self._connection:
|
262
|
+
logger.debug(f"Disconnecting from {self.config.hostname}")
|
263
|
+
try:
|
264
|
+
self._connection.disconnect()
|
265
|
+
logger.debug("Successfully disconnected")
|
266
|
+
except Exception as e:
|
267
|
+
logger.warning(f"Error during disconnect: {str(e)}")
|
268
|
+
finally:
|
269
|
+
self._connection = None
|
270
|
+
|
271
|
+
def execute_command(self, command: str, command_type: str = 'show') -> CommandResult:
|
272
|
+
"""Execute a command on the device.
|
273
|
+
|
274
|
+
Args:
|
275
|
+
command: The command string to execute
|
276
|
+
command_type: Type of command ('show' or 'config') for proper handling
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
CommandResult with execution details
|
280
|
+
"""
|
281
|
+
if not self._connection:
|
282
|
+
raise CommandExecutionError("Not connected to device")
|
283
|
+
|
284
|
+
logger.debug(f"Executing {command_type} command on {self.config.hostname}: {command}")
|
285
|
+
start_time = time.time()
|
286
|
+
|
287
|
+
try:
|
288
|
+
# Use command_type parameter to determine execution method
|
289
|
+
if command_type == 'config':
|
290
|
+
output = self._execute_config_command(command)
|
291
|
+
parsed_data = None # Config commands don't get parsed
|
292
|
+
else:
|
293
|
+
output, parsed_data = self._execute_show_command(command)
|
294
|
+
|
295
|
+
execution_time = time.time() - start_time
|
296
|
+
|
297
|
+
# Create initial result
|
298
|
+
result = CommandResult(
|
299
|
+
command=command,
|
300
|
+
output=output,
|
301
|
+
success=True,
|
302
|
+
execution_time=execution_time
|
303
|
+
)
|
304
|
+
|
305
|
+
# Add parsed data if available
|
306
|
+
if parsed_data:
|
307
|
+
result.parsed_output = parsed_data
|
308
|
+
result.parsing_success = True
|
309
|
+
result.parsing_method = 'textfsm'
|
310
|
+
|
311
|
+
# Check for syntax errors in the output even if command executed successfully
|
312
|
+
parsed_error = self._error_parser.parse_command_output(output, self.config.platform)
|
313
|
+
if parsed_error:
|
314
|
+
logger.warning(f"Syntax error detected in command output: {parsed_error.error_type.value}")
|
315
|
+
# Update result with syntax error information
|
316
|
+
result.has_syntax_error = True
|
317
|
+
result.syntax_error_type = parsed_error.error_type.value
|
318
|
+
result.syntax_error_vendor = parsed_error.vendor
|
319
|
+
result.syntax_error_guidance = parsed_error.guidance
|
320
|
+
|
321
|
+
# Enhance the output with error information
|
322
|
+
enhanced_output = output + "\n\n" + "="*50 + "\n"
|
323
|
+
enhanced_output += "SYNTAX ERROR DETECTED\n" + "="*50 + "\n"
|
324
|
+
enhanced_output += f"Error Type: {parsed_error.error_type.value.replace('_', ' ').title()}\n"
|
325
|
+
enhanced_output += f"Vendor: {self._error_parser._get_vendor_display_name(parsed_error.vendor)}\n"
|
326
|
+
enhanced_output += f"Confidence: {parsed_error.confidence:.0%}\n\n"
|
327
|
+
enhanced_output += parsed_error.enhanced_message + "\n\n"
|
328
|
+
enhanced_output += parsed_error.guidance
|
329
|
+
|
330
|
+
result.output = enhanced_output
|
331
|
+
|
332
|
+
# Log final result summary
|
333
|
+
if parsed_data:
|
334
|
+
logger.debug(f"Command completed in {execution_time:.2f}s with {len(parsed_data)} parsed records")
|
335
|
+
else:
|
336
|
+
logger.debug(f"Command completed in {execution_time:.2f}s")
|
337
|
+
return result
|
338
|
+
|
339
|
+
except NetmikoBaseException as e:
|
340
|
+
execution_time = time.time() - start_time
|
341
|
+
error_msg = str(e)
|
342
|
+
logger.error(f"Command execution failed: {error_msg}")
|
343
|
+
|
344
|
+
return CommandResult(
|
345
|
+
command=command,
|
346
|
+
output="",
|
347
|
+
success=False,
|
348
|
+
error_message=error_msg,
|
349
|
+
execution_time=execution_time
|
350
|
+
)
|
351
|
+
|
352
|
+
except Exception as e:
|
353
|
+
execution_time = time.time() - start_time
|
354
|
+
error_msg = f"Unexpected error: {str(e)}"
|
355
|
+
logger.error(f"Command execution failed: {error_msg}")
|
356
|
+
|
357
|
+
return CommandResult(
|
358
|
+
command=command,
|
359
|
+
output="",
|
360
|
+
success=False,
|
361
|
+
error_message=error_msg,
|
362
|
+
execution_time=execution_time
|
363
|
+
)
|
364
|
+
|
365
|
+
def _execute_show_command(self, command: str) -> Tuple[str, Optional[list]]:
|
366
|
+
"""Execute a show/display command and return both raw output and parsed data.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
command: The command to execute
|
370
|
+
|
371
|
+
Returns:
|
372
|
+
tuple: (raw_output, parsed_data) where parsed_data is None if parsing failed
|
373
|
+
"""
|
374
|
+
try:
|
375
|
+
# Execute command once and get raw output
|
376
|
+
raw_output = self._connection.send_command(command)
|
377
|
+
|
378
|
+
# Now attempt TextFSM parsing using the textfsm library directly
|
379
|
+
# This avoids re-executing the command on the device
|
380
|
+
parsed_data = None
|
381
|
+
try:
|
382
|
+
# Import textfsm here to avoid dependency issues if not installed
|
383
|
+
import textfsm
|
384
|
+
import os
|
385
|
+
from ntc_templates.parse import parse_output
|
386
|
+
|
387
|
+
# Try to parse using ntc-templates (which is what Netmiko uses)
|
388
|
+
try:
|
389
|
+
parsed_result = parse_output(
|
390
|
+
platform=self._connection.device_type,
|
391
|
+
command=command,
|
392
|
+
data=raw_output
|
393
|
+
)
|
394
|
+
|
395
|
+
if (isinstance(parsed_result, list) and
|
396
|
+
len(parsed_result) > 0 and
|
397
|
+
isinstance(parsed_result[0], dict)):
|
398
|
+
parsed_data = parsed_result
|
399
|
+
logger.debug(f"TextFSM parsed {len(parsed_data)} records")
|
400
|
+
else:
|
401
|
+
logger.debug("No TextFSM template found")
|
402
|
+
|
403
|
+
except Exception as ntc_error:
|
404
|
+
logger.debug(f"TextFSM parsing failed: {str(ntc_error)}")
|
405
|
+
|
406
|
+
except ImportError:
|
407
|
+
logger.debug("TextFSM or ntc-templates not available, skipping parsing")
|
408
|
+
except Exception as parse_error:
|
409
|
+
logger.debug(f"TextFSM parsing failed: {str(parse_error)}")
|
410
|
+
|
411
|
+
return raw_output, parsed_data
|
412
|
+
|
413
|
+
except Exception as e:
|
414
|
+
logger.error(f"Show command failed: {str(e)}")
|
415
|
+
raise CommandExecutionError(f"Show command failed: {str(e)}")
|
416
|
+
|
417
|
+
def _execute_config_command(self, command: str) -> str:
|
418
|
+
"""Execute a configuration command."""
|
419
|
+
try:
|
420
|
+
if isinstance(command, str):
|
421
|
+
commands = [command]
|
422
|
+
else:
|
423
|
+
commands = command
|
424
|
+
|
425
|
+
return self._connection.send_config_set(commands)
|
426
|
+
except Exception as e:
|
427
|
+
logger.error(f"Config command failed: {str(e)}")
|
428
|
+
raise CommandExecutionError(f"Config command failed: {str(e)}")
|
429
|
+
|
430
|
+
# Note: The _attempt_parsing method has been removed as parsing is now handled
|
431
|
+
# directly in _execute_show_command to avoid re-executing commands on the device
|
432
|
+
|
433
|
+
def is_connected(self) -> bool:
|
434
|
+
"""Check if device is connected."""
|
435
|
+
if not self._connection:
|
436
|
+
return False
|
437
|
+
|
438
|
+
try:
|
439
|
+
# Simple connectivity test
|
440
|
+
self._connection.find_prompt()
|
441
|
+
return True
|
442
|
+
except Exception:
|
443
|
+
return False
|