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