netbox-toolkit-plugin 0.1.2__py3-none-any.whl → 0.1.3__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 (42) hide show
  1. netbox_toolkit_plugin/__init__.py +1 -1
  2. netbox_toolkit_plugin/admin.py +11 -7
  3. netbox_toolkit_plugin/api/mixins.py +20 -16
  4. netbox_toolkit_plugin/api/schemas.py +53 -74
  5. netbox_toolkit_plugin/api/serializers.py +10 -11
  6. netbox_toolkit_plugin/api/urls.py +2 -1
  7. netbox_toolkit_plugin/api/views/__init__.py +4 -3
  8. netbox_toolkit_plugin/api/views/command_logs.py +80 -73
  9. netbox_toolkit_plugin/api/views/commands.py +140 -134
  10. netbox_toolkit_plugin/connectors/__init__.py +9 -9
  11. netbox_toolkit_plugin/connectors/base.py +30 -31
  12. netbox_toolkit_plugin/connectors/factory.py +21 -25
  13. netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
  14. netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
  15. netbox_toolkit_plugin/exceptions.py +0 -7
  16. netbox_toolkit_plugin/filtersets.py +26 -42
  17. netbox_toolkit_plugin/forms.py +13 -11
  18. netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
  19. netbox_toolkit_plugin/models.py +2 -17
  20. netbox_toolkit_plugin/navigation.py +3 -0
  21. netbox_toolkit_plugin/search.py +12 -9
  22. netbox_toolkit_plugin/services/__init__.py +1 -1
  23. netbox_toolkit_plugin/services/command_service.py +6 -9
  24. netbox_toolkit_plugin/services/device_service.py +40 -32
  25. netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
  26. netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
  27. netbox_toolkit_plugin/tables.py +10 -1
  28. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
  29. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
  30. netbox_toolkit_plugin/urls.py +10 -3
  31. netbox_toolkit_plugin/utils/connection.py +54 -54
  32. netbox_toolkit_plugin/utils/error_parser.py +128 -109
  33. netbox_toolkit_plugin/utils/logging.py +1 -0
  34. netbox_toolkit_plugin/utils/network.py +74 -47
  35. netbox_toolkit_plugin/views.py +51 -22
  36. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
  37. netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
  38. netbox_toolkit_plugin-0.1.2.dist-info/RECORD +0 -60
  39. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
  40. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
  41. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
  42. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,15 @@
1
1
  """Connectors package for device connection logic."""
2
2
 
3
- from .base import BaseDeviceConnector, ConnectionConfig, CommandResult
4
- from .scrapli_connector import ScrapliConnector
5
- from .netmiko_connector import NetmikoConnector
3
+ from .base import BaseDeviceConnector, CommandResult, ConnectionConfig
6
4
  from .factory import ConnectorFactory
5
+ from .netmiko_connector import NetmikoConnector
6
+ from .scrapli_connector import ScrapliConnector
7
7
 
8
8
  __all__ = [
9
- 'BaseDeviceConnector',
10
- 'ConnectionConfig',
11
- 'CommandResult',
12
- 'ScrapliConnector',
13
- 'NetmikoConnector',
14
- 'ConnectorFactory',
9
+ "BaseDeviceConnector",
10
+ "ConnectionConfig",
11
+ "CommandResult",
12
+ "ScrapliConnector",
13
+ "NetmikoConnector",
14
+ "ConnectorFactory",
15
15
  ]
@@ -1,14 +1,14 @@
1
1
  """Base connector interface for device connections."""
2
+
2
3
  from abc import ABC, abstractmethod
3
- from typing import Dict, Any, Optional
4
4
  from dataclasses import dataclass
5
-
6
- from ..exceptions import DeviceConnectionError, CommandExecutionError
5
+ from typing import Any
7
6
 
8
7
 
9
8
  @dataclass
10
9
  class ConnectionConfig:
11
10
  """Configuration for device connections."""
11
+
12
12
  hostname: str
13
13
  username: str
14
14
  password: str
@@ -17,81 +17,80 @@ class ConnectionConfig:
17
17
  timeout_transport: int = 15
18
18
  timeout_ops: int = 30
19
19
  auth_strict_key: bool = False
20
- transport: str = 'system' # Default to system transport for Scrapli
21
- platform: Optional[str] = None
22
- extra_options: Optional[Dict[str, Any]] = None
20
+ transport: str = "system" # Default to system transport for Scrapli
21
+ platform: str | None = None
22
+ extra_options: dict[str, Any] | None = None
23
23
 
24
24
 
25
25
  @dataclass
26
26
  class CommandResult:
27
27
  """Result of command execution."""
28
+
28
29
  command: str
29
30
  output: str
30
31
  success: bool
31
- error_message: Optional[str] = None
32
- execution_time: Optional[float] = None
32
+ error_message: str | None = None
33
+ execution_time: float | None = None
33
34
  # New fields for syntax error detection
34
35
  has_syntax_error: bool = False
35
- syntax_error_type: Optional[str] = None
36
- syntax_error_vendor: Optional[str] = None
37
- syntax_error_guidance: Optional[str] = None
36
+ syntax_error_type: str | None = None
37
+ syntax_error_vendor: str | None = None
38
+ syntax_error_guidance: str | None = None
38
39
  # New fields for command output parsing
39
- parsed_output: Optional[Dict[str, Any]] = None
40
+ parsed_output: dict[str, Any] | None = None
40
41
  parsing_success: bool = False
41
- parsing_method: Optional[str] = None # 'textfsm', 'genie', 'ttp'
42
- parsing_error: Optional[str] = None
42
+ parsing_method: str | None = None # 'textfsm', 'genie', 'ttp'
43
+ parsing_error: str | None = None
43
44
 
44
45
 
45
46
  class BaseDeviceConnector(ABC):
46
47
  """Abstract base class for device connectors."""
47
-
48
+
48
49
  def __init__(self, config: ConnectionConfig):
49
50
  self.config = config
50
51
  self._connection = None
51
-
52
+
52
53
  @abstractmethod
53
54
  def connect(self) -> None:
54
55
  """Establish connection to the device."""
55
- pass
56
-
56
+
57
57
  @abstractmethod
58
58
  def disconnect(self) -> None:
59
59
  """Close connection to the device."""
60
- pass
61
-
60
+
62
61
  @abstractmethod
63
- def execute_command(self, command: str, command_type: str = 'show') -> CommandResult:
62
+ def execute_command(
63
+ self, command: str, command_type: str = "show"
64
+ ) -> CommandResult:
64
65
  """Execute a command on the device.
65
-
66
+
66
67
  Args:
67
68
  command: The command string to execute
68
69
  command_type: Type of command ('show' or 'config') for proper handling
69
-
70
+
70
71
  Returns:
71
72
  CommandResult with execution details
72
73
  """
73
- pass
74
-
74
+
75
75
  @abstractmethod
76
76
  def is_connected(self) -> bool:
77
77
  """Check if connection is active."""
78
- pass
79
-
78
+
80
79
  def __enter__(self):
81
80
  """Context manager entry."""
82
81
  self.connect()
83
82
  return self
84
-
83
+
85
84
  def __exit__(self, exc_type, exc_val, exc_tb):
86
85
  """Context manager exit."""
87
86
  self.disconnect()
88
-
87
+
89
88
  @property
90
89
  def hostname(self) -> str:
91
90
  """Get the hostname for this connection."""
92
91
  return self.config.hostname
93
-
92
+
94
93
  @property
95
- def platform(self) -> Optional[str]:
94
+ def platform(self) -> str | None:
96
95
  """Get the platform for this connection."""
97
96
  return self.config.platform
@@ -1,17 +1,13 @@
1
1
  """Factory for creating device connectors."""
2
2
 
3
- import logging
4
- from typing import Type, Optional
5
-
6
- from django.db.models import Model
7
3
  from dcim.models import Device
8
4
 
9
- from ..exceptions import UnsupportedPlatformError, DeviceConnectionError
5
+ from ..exceptions import DeviceConnectionError, UnsupportedPlatformError
10
6
  from ..settings import ToolkitSettings
11
7
  from ..utils.logging import get_toolkit_logger
12
8
  from .base import BaseDeviceConnector, ConnectionConfig
13
- from .scrapli_connector import ScrapliConnector
14
9
  from .netmiko_connector import NetmikoConnector
10
+ from .scrapli_connector import ScrapliConnector
15
11
 
16
12
  logger = get_toolkit_logger(__name__)
17
13
 
@@ -59,7 +55,7 @@ class ConnectorFactory:
59
55
  device: Device,
60
56
  username: str,
61
57
  password: str,
62
- connector_type: Optional[str] = None,
58
+ connector_type: str | None = None,
63
59
  use_fallback: bool = True,
64
60
  ) -> BaseDeviceConnector:
65
61
  """
@@ -145,7 +141,9 @@ class ConnectorFactory:
145
141
  )
146
142
  return cls._create_fallback_connector(config, device.name, error_msg)
147
143
  else:
148
- raise DeviceConnectionError(f"Connector creation failed: {error_msg}")
144
+ raise DeviceConnectionError(
145
+ f"Connector creation failed: {error_msg}"
146
+ ) from e
149
147
 
150
148
  @classmethod
151
149
  def _create_fallback_connector(
@@ -169,11 +167,11 @@ class ConnectorFactory:
169
167
  raise DeviceConnectionError(
170
168
  f"Both connectors failed. Primary error: {primary_error}. "
171
169
  f"Fallback error: {str(fallback_error)}"
172
- )
170
+ ) from fallback_error
173
171
 
174
172
  @classmethod
175
173
  def _prepare_connector_config(
176
- cls, base_config: ConnectionConfig, connector_class: Type[BaseDeviceConnector]
174
+ cls, base_config: ConnectionConfig, connector_class: type[BaseDeviceConnector]
177
175
  ) -> ConnectionConfig:
178
176
  """Prepare a clean configuration for a specific connector type."""
179
177
  # Create a copy of the base config
@@ -262,7 +260,7 @@ class ConnectorFactory:
262
260
  return config
263
261
 
264
262
  @classmethod
265
- def _get_connector_by_type(cls, connector_type: str) -> Type[BaseDeviceConnector]:
263
+ def _get_connector_by_type(cls, connector_type: str) -> type[BaseDeviceConnector]:
266
264
  """Get connector class by explicit type."""
267
265
  connector_type_lower = connector_type.lower()
268
266
  if connector_type_lower == "scrapli":
@@ -276,8 +274,8 @@ class ConnectorFactory:
276
274
 
277
275
  @classmethod
278
276
  def _get_primary_connector_by_platform(
279
- cls, platform: Optional[str]
280
- ) -> Type[BaseDeviceConnector]:
277
+ cls, platform: str | None
278
+ ) -> type[BaseDeviceConnector]:
281
279
  """Get primary connector class by device platform."""
282
280
  if not platform:
283
281
  return cls.PRIMARY_CONNECTOR
@@ -301,8 +299,8 @@ class ConnectorFactory:
301
299
 
302
300
  @classmethod
303
301
  def _get_connector_by_platform(
304
- cls, platform: Optional[str]
305
- ) -> Type[BaseDeviceConnector]:
302
+ cls, platform: str | None
303
+ ) -> type[BaseDeviceConnector]:
306
304
  """Legacy method for backward compatibility."""
307
305
  return cls._get_primary_connector_by_platform(platform)
308
306
 
@@ -331,7 +329,7 @@ class ConnectorFactory:
331
329
  return True
332
330
 
333
331
  # Check partial matches
334
- for supported_platform in cls.CONNECTOR_MAP.keys():
332
+ for supported_platform in cls.CONNECTOR_MAP:
335
333
  if (
336
334
  platform_lower in supported_platform
337
335
  or supported_platform in platform_lower
@@ -339,17 +337,15 @@ class ConnectorFactory:
339
337
  return True
340
338
 
341
339
  # Check if either connector supports it
342
- if platform_lower in [
343
- p.lower() for p in ScrapliConnector.get_supported_platforms()
344
- ] or platform_lower in [
345
- p.lower() for p in NetmikoConnector.get_supported_platforms()
346
- ]:
347
- return True
348
-
349
- return False
340
+ return bool(
341
+ platform_lower
342
+ in [p.lower() for p in ScrapliConnector.get_supported_platforms()]
343
+ or platform_lower
344
+ in [p.lower() for p in NetmikoConnector.get_supported_platforms()]
345
+ )
350
346
 
351
347
  @classmethod
352
- def get_recommended_connector(cls, platform: Optional[str]) -> str:
348
+ def get_recommended_connector(cls, platform: str | None) -> str:
353
349
  """Get recommended connector type for a platform."""
354
350
  connector_class = cls._get_primary_connector_by_platform(platform)
355
351
  if connector_class == ScrapliConnector:
@@ -1,27 +1,24 @@
1
1
  """Netmiko-based device connector implementation."""
2
2
 
3
3
  import time
4
- from typing import Dict, Any, Optional, Tuple
4
+ from typing import Any
5
5
 
6
6
  from netmiko import ConnectHandler, SSHDetect
7
7
  from netmiko.exceptions import (
8
- NetmikoTimeoutException,
9
8
  NetmikoAuthenticationException,
10
9
  NetmikoBaseException,
11
- ConnectionException,
12
- ConfigInvalidException,
10
+ NetmikoTimeoutException,
13
11
  )
14
12
 
15
- from .base import BaseDeviceConnector, ConnectionConfig, CommandResult
16
- from ..settings import ToolkitSettings
17
13
  from ..exceptions import (
18
- DeviceConnectionError,
19
14
  CommandExecutionError,
20
- UnsupportedPlatformError,
15
+ DeviceConnectionError,
21
16
  )
22
- from ..utils.network import validate_device_connectivity
17
+ from ..settings import ToolkitSettings
23
18
  from ..utils.error_parser import VendorErrorParser
24
19
  from ..utils.logging import get_toolkit_logger
20
+ from ..utils.network import validate_device_connectivity
21
+ from .base import BaseDeviceConnector, CommandResult, ConnectionConfig
25
22
 
26
23
  logger = get_toolkit_logger(__name__)
27
24
 
@@ -71,7 +68,7 @@ class NetmikoConnector(BaseDeviceConnector):
71
68
  f"Initialized NetmikoConnector for {config.hostname} with platform '{config.platform}'"
72
69
  )
73
70
 
74
- def _filter_valid_netmiko_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
71
+ def _filter_valid_netmiko_params(self, params: dict[str, Any]) -> dict[str, Any]:
75
72
  """Filter parameters to only include those supported by Netmiko."""
76
73
  # Define valid Netmiko connection parameters
77
74
  valid_netmiko_params = {
@@ -182,7 +179,7 @@ class NetmikoConnector(BaseDeviceConnector):
182
179
  logger.warning(f"Auto-detection error for {self.config.hostname}: {str(e)}")
183
180
  return "generic_termserver"
184
181
 
185
- def _build_connection_params(self) -> Dict[str, Any]:
182
+ def _build_connection_params(self) -> dict[str, Any]:
186
183
  """Build connection parameters for Netmiko."""
187
184
  device_type = self._get_device_type()
188
185
 
@@ -247,7 +244,6 @@ class NetmikoConnector(BaseDeviceConnector):
247
244
 
248
245
  max_retries = self._retry_config["max_retries"]
249
246
  retry_delay = self._retry_config["retry_delay"]
250
- last_error = None
251
247
 
252
248
  for attempt in range(max_retries + 1):
253
249
  try:
@@ -280,10 +276,9 @@ class NetmikoConnector(BaseDeviceConnector):
280
276
  logger.error(
281
277
  f"Authentication failed for {self.config.hostname}: {str(e)}"
282
278
  )
283
- raise DeviceConnectionError(f"Authentication failed: {str(e)}")
279
+ raise DeviceConnectionError(f"Authentication failed: {str(e)}") from e
284
280
 
285
281
  except NetmikoTimeoutException as e:
286
- last_error = e
287
282
  logger.warning(
288
283
  f"Connection timeout for {self.config.hostname}: {str(e)}"
289
284
  )
@@ -291,21 +286,21 @@ class NetmikoConnector(BaseDeviceConnector):
291
286
  if attempt >= max_retries:
292
287
  raise DeviceConnectionError(
293
288
  f"Connection timeout after {max_retries + 1} attempts: {str(e)}"
294
- )
289
+ ) from e
295
290
 
296
291
  except NetmikoBaseException as e:
297
- last_error = e
298
292
  logger.warning(f"Netmiko error for {self.config.hostname}: {str(e)}")
299
293
 
300
294
  if attempt >= max_retries:
301
- raise DeviceConnectionError(f"Netmiko connection failed: {str(e)}")
295
+ raise DeviceConnectionError(
296
+ f"Netmiko connection failed: {str(e)}"
297
+ ) from e
302
298
 
303
299
  except Exception as e:
304
- last_error = e
305
300
  logger.warning(f"Unexpected error for {self.config.hostname}: {str(e)}")
306
301
 
307
302
  if attempt >= max_retries:
308
- raise DeviceConnectionError(f"Connection failed: {str(e)}")
303
+ raise DeviceConnectionError(f"Connection failed: {str(e)}") from e
309
304
 
310
305
  def disconnect(self) -> None:
311
306
  """Close connection to the device."""
@@ -423,7 +418,7 @@ class NetmikoConnector(BaseDeviceConnector):
423
418
  execution_time=execution_time,
424
419
  )
425
420
 
426
- def _execute_show_command(self, command: str) -> Tuple[str, Optional[list]]:
421
+ def _execute_show_command(self, command: str) -> tuple[str, list | None]:
427
422
  """Execute a show/display command and return both raw output and parsed data.
428
423
 
429
424
  Args:
@@ -441,8 +436,6 @@ class NetmikoConnector(BaseDeviceConnector):
441
436
  parsed_data = None
442
437
  try:
443
438
  # Import textfsm here to avoid dependency issues if not installed
444
- import textfsm
445
- import os
446
439
  from ntc_templates.parse import parse_output
447
440
 
448
441
  # Try to parse using ntc-templates (which is what Netmiko uses)
@@ -475,20 +468,17 @@ class NetmikoConnector(BaseDeviceConnector):
475
468
 
476
469
  except Exception as e:
477
470
  logger.error(f"Show command failed: {str(e)}")
478
- raise CommandExecutionError(f"Show command failed: {str(e)}")
471
+ raise CommandExecutionError(f"Show command failed: {str(e)}") from e
479
472
 
480
473
  def _execute_config_command(self, command: str) -> str:
481
474
  """Execute a configuration command."""
482
475
  try:
483
- if isinstance(command, str):
484
- commands = [command]
485
- else:
486
- commands = command
476
+ commands = [command] if isinstance(command, str) else command
487
477
 
488
478
  return self._connection.send_config_set(commands)
489
479
  except Exception as e:
490
480
  logger.error(f"Config command failed: {str(e)}")
491
- raise CommandExecutionError(f"Config command failed: {str(e)}")
481
+ raise CommandExecutionError(f"Config command failed: {str(e)}") from e
492
482
 
493
483
  # Note: The _attempt_parsing method has been removed as parsing is now handled
494
484
  # directly in _execute_show_command to avoid re-executing commands on the device
@@ -1,20 +1,15 @@
1
1
  """Scrapli-based device connector implementation."""
2
2
 
3
3
  import time
4
- from typing import Dict, Any, Type, Optional
4
+ from typing import Any
5
5
 
6
- from scrapli.driver.core import IOSXEDriver, NXOSDriver, IOSXRDriver
6
+ from scrapli.driver.core import IOSXEDriver, IOSXRDriver, NXOSDriver
7
7
  from scrapli.driver.generic import GenericDriver
8
- from scrapli.exceptions import ScrapliException
9
8
 
10
- from .base import BaseDeviceConnector, ConnectionConfig, CommandResult
11
- from ..settings import ToolkitSettings
12
9
  from ..exceptions import (
13
10
  DeviceConnectionError,
14
- CommandExecutionError,
15
- UnsupportedPlatformError,
16
11
  )
17
- from ..utils.network import validate_device_connectivity
12
+ from ..settings import ToolkitSettings
18
13
  from ..utils.connection import (
19
14
  cleanup_connection_resources,
20
15
  validate_connection_health,
@@ -22,6 +17,8 @@ from ..utils.connection import (
22
17
  )
23
18
  from ..utils.error_parser import VendorErrorParser
24
19
  from ..utils.logging import get_toolkit_logger
20
+ from ..utils.network import validate_device_connectivity
21
+ from .base import BaseDeviceConnector, CommandResult, ConnectionConfig
25
22
 
26
23
  logger = get_toolkit_logger(__name__)
27
24
 
@@ -80,7 +77,7 @@ class ScrapliConnector(BaseDeviceConnector):
80
77
 
81
78
  return normalized
82
79
 
83
- def _get_driver_class(self) -> Type:
80
+ def _get_driver_class(self) -> type:
84
81
  """Get the appropriate Scrapli driver class for the platform."""
85
82
  if not self.config.platform:
86
83
  logger.debug("No platform specified, using GenericDriver")
@@ -100,7 +97,7 @@ class ScrapliConnector(BaseDeviceConnector):
100
97
 
101
98
  return driver_class
102
99
 
103
- def _build_connection_params(self) -> Dict[str, Any]:
100
+ def _build_connection_params(self) -> dict[str, Any]:
104
101
  """Build connection parameters for Scrapli."""
105
102
  # Use fast test timeouts for initial attempts if in fast fail mode
106
103
  if self._fast_fail_mode:
@@ -171,7 +168,7 @@ class ScrapliConnector(BaseDeviceConnector):
171
168
 
172
169
  return params
173
170
 
174
- def _get_transport_options(self) -> Dict[str, Any]:
171
+ def _get_transport_options(self) -> dict[str, Any]:
175
172
  """Get transport-specific options for Scrapli."""
176
173
  ssh_options = ToolkitSettings.get_ssh_transport_options()
177
174
 
@@ -219,7 +216,9 @@ class ScrapliConnector(BaseDeviceConnector):
219
216
  logger.error(
220
217
  f"Pre-connection validation failed for {self.config.hostname}: {str(e)}"
221
218
  )
222
- raise DeviceConnectionError(f"Pre-connection validation failed: {str(e)}")
219
+ raise DeviceConnectionError(
220
+ f"Pre-connection validation failed: {str(e)}"
221
+ ) from e
223
222
 
224
223
  # Use fast-fail mode for first attempt to quickly detect incompatible scenarios
225
224
  self._fast_fail_mode = True
@@ -292,7 +291,9 @@ class ScrapliConnector(BaseDeviceConnector):
292
291
  ):
293
292
  logger.info(f"Fast-fail pattern detected: {error_msg}")
294
293
  logger.info("Triggering immediate fallback to Netmiko")
295
- raise DeviceConnectionError(f"Fast-fail to Netmiko: {error_msg}")
294
+ raise DeviceConnectionError(
295
+ f"Fast-fail to Netmiko: {error_msg}"
296
+ ) from None
296
297
 
297
298
  # Clean up failed connection attempt
298
299
  if self._connection:
@@ -308,7 +309,7 @@ class ScrapliConnector(BaseDeviceConnector):
308
309
  logger.error(
309
310
  f"All connection attempts failed for {self.config.hostname}: {error_msg}"
310
311
  )
311
- raise DeviceConnectionError(error_msg)
312
+ raise DeviceConnectionError(error_msg) from e
312
313
 
313
314
  def disconnect(self) -> None:
314
315
  """Close connection to the device with proper socket cleanup."""
@@ -320,7 +321,7 @@ class ScrapliConnector(BaseDeviceConnector):
320
321
  logger.debug("Connection cleanup completed successfully")
321
322
  except Exception as e:
322
323
  logger.warning(f"Error during connection cleanup: {str(e)}")
323
- pass # Cleanup error ignored
324
+ # Cleanup error ignored
324
325
  finally:
325
326
  self._connection = None
326
327
  # Give time for socket cleanup to complete
@@ -515,7 +516,7 @@ class ScrapliConnector(BaseDeviceConnector):
515
516
  logger.debug(
516
517
  "TextFSM parsing returned empty result (no matching template)"
517
518
  )
518
- pass # TextFSM parsing returned empty result
519
+ # TextFSM parsing returned empty result
519
520
 
520
521
  except Exception as e:
521
522
  # TextFSM parsing failed - this is common for commands without templates
@@ -3,34 +3,27 @@
3
3
 
4
4
  class ToolkitError(Exception):
5
5
  """Base exception for toolkit-related errors."""
6
- pass
7
6
 
8
7
 
9
8
  class DeviceConnectionError(ToolkitError):
10
9
  """Raised when device connection fails."""
11
- pass
12
10
 
13
11
 
14
12
  class DeviceReachabilityError(DeviceConnectionError):
15
13
  """Raised when device is not reachable."""
16
- pass
17
14
 
18
15
 
19
16
  class SSHBannerError(DeviceConnectionError):
20
17
  """Raised when SSH banner issues occur."""
21
- pass
22
18
 
23
19
 
24
20
  class AuthenticationError(DeviceConnectionError):
25
21
  """Raised when authentication fails."""
26
- pass
27
22
 
28
23
 
29
24
  class CommandExecutionError(ToolkitError):
30
25
  """Raised when command execution fails."""
31
- pass
32
26
 
33
27
 
34
28
  class UnsupportedPlatformError(ToolkitError):
35
29
  """Raised when device platform is not supported."""
36
- pass
@@ -1,85 +1,69 @@
1
- import django_filters
1
+ from dcim.models import Device, Platform
2
2
  from netbox.filtersets import NetBoxModelFilterSet
3
- from dcim.models import Platform, Device
3
+
4
+ import django_filters
5
+
4
6
  from .models import Command, CommandLog
5
7
 
8
+
6
9
  class CommandFilterSet(NetBoxModelFilterSet):
7
10
  """Enhanced filtering for commands"""
11
+
8
12
  platform_id = django_filters.ModelMultipleChoiceFilter(
9
13
  queryset=Platform.objects.all(),
10
- label='Platform (ID)',
14
+ label="Platform (ID)",
11
15
  )
12
16
  platform_slug = django_filters.CharFilter(
13
- field_name='platform__slug',
14
- lookup_expr='icontains',
15
- label='Platform slug'
17
+ field_name="platform__slug", lookup_expr="icontains", label="Platform slug"
16
18
  )
17
19
  command_type = django_filters.ChoiceFilter(
18
- choices=[('show', 'Show Command'), ('config', 'Configuration Command')]
20
+ choices=[("show", "Show Command"), ("config", "Configuration Command")]
19
21
  )
20
22
  created_after = django_filters.DateTimeFilter(
21
- field_name='created',
22
- lookup_expr='gte'
23
+ field_name="created", lookup_expr="gte"
23
24
  )
24
25
  created_before = django_filters.DateTimeFilter(
25
- field_name='created',
26
- lookup_expr='lte'
26
+ field_name="created", lookup_expr="lte"
27
27
  )
28
28
  name_icontains = django_filters.CharFilter(
29
- field_name='name',
30
- lookup_expr='icontains',
31
- label='Name contains'
29
+ field_name="name", lookup_expr="icontains", label="Name contains"
32
30
  )
33
31
  description_icontains = django_filters.CharFilter(
34
- field_name='description',
35
- lookup_expr='icontains',
36
- label='Description contains'
32
+ field_name="description", lookup_expr="icontains", label="Description contains"
37
33
  )
38
34
 
39
35
  class Meta:
40
36
  model = Command
41
- fields = ('name', 'platform', 'command_type', 'description')
37
+ fields = ("name", "platform", "command_type", "description")
38
+
42
39
 
43
40
  class CommandLogFilterSet(NetBoxModelFilterSet):
44
41
  """Enhanced filtering for command logs"""
42
+
45
43
  device_id = django_filters.ModelMultipleChoiceFilter(
46
- queryset=Device.objects.all(),
47
- label='Device (ID)'
44
+ queryset=Device.objects.all(), label="Device (ID)"
48
45
  )
49
46
  command_id = django_filters.ModelMultipleChoiceFilter(
50
- queryset=Command.objects.all(),
51
- label='Command (ID)'
47
+ queryset=Command.objects.all(), label="Command (ID)"
52
48
  )
53
49
  execution_time_after = django_filters.DateTimeFilter(
54
- field_name='execution_time',
55
- lookup_expr='gte'
50
+ field_name="execution_time", lookup_expr="gte"
56
51
  )
57
52
  execution_time_before = django_filters.DateTimeFilter(
58
- field_name='execution_time',
59
- lookup_expr='lte'
60
- )
61
- has_parsed_data = django_filters.BooleanFilter(
62
- field_name='parsed_data',
63
- lookup_expr='isnull',
64
- exclude=True,
65
- label='Has parsed data'
53
+ field_name="execution_time", lookup_expr="lte"
66
54
  )
67
55
  username_icontains = django_filters.CharFilter(
68
- field_name='username',
69
- lookup_expr='icontains',
70
- label='Username contains'
56
+ field_name="username", lookup_expr="icontains", label="Username contains"
71
57
  )
72
58
  device_name_icontains = django_filters.CharFilter(
73
- field_name='device__name',
74
- lookup_expr='icontains',
75
- label='Device name contains'
59
+ field_name="device__name", lookup_expr="icontains", label="Device name contains"
76
60
  )
77
61
  command_name_icontains = django_filters.CharFilter(
78
- field_name='command__name',
79
- lookup_expr='icontains',
80
- label='Command name contains'
62
+ field_name="command__name",
63
+ lookup_expr="icontains",
64
+ label="Command name contains",
81
65
  )
82
66
 
83
67
  class Meta:
84
68
  model = CommandLog
85
- fields = ('command', 'device', 'username', 'success', 'parsing_success')
69
+ fields = ("command", "device", "username", "success")