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.
- netbox_toolkit_plugin/__init__.py +1 -1
- netbox_toolkit_plugin/admin.py +11 -7
- netbox_toolkit_plugin/api/mixins.py +20 -16
- netbox_toolkit_plugin/api/schemas.py +53 -74
- netbox_toolkit_plugin/api/serializers.py +10 -11
- netbox_toolkit_plugin/api/urls.py +2 -1
- netbox_toolkit_plugin/api/views/__init__.py +4 -3
- netbox_toolkit_plugin/api/views/command_logs.py +80 -73
- netbox_toolkit_plugin/api/views/commands.py +140 -134
- netbox_toolkit_plugin/connectors/__init__.py +9 -9
- netbox_toolkit_plugin/connectors/base.py +30 -31
- netbox_toolkit_plugin/connectors/factory.py +21 -25
- netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
- netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
- netbox_toolkit_plugin/exceptions.py +0 -7
- netbox_toolkit_plugin/filtersets.py +26 -42
- netbox_toolkit_plugin/forms.py +13 -11
- netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
- netbox_toolkit_plugin/models.py +2 -17
- netbox_toolkit_plugin/navigation.py +3 -0
- netbox_toolkit_plugin/search.py +12 -9
- netbox_toolkit_plugin/services/__init__.py +1 -1
- netbox_toolkit_plugin/services/command_service.py +6 -9
- netbox_toolkit_plugin/services/device_service.py +40 -32
- netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
- netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
- netbox_toolkit_plugin/tables.py +10 -1
- netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
- netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
- netbox_toolkit_plugin/urls.py +10 -3
- netbox_toolkit_plugin/utils/connection.py +54 -54
- netbox_toolkit_plugin/utils/error_parser.py +128 -109
- netbox_toolkit_plugin/utils/logging.py +1 -0
- netbox_toolkit_plugin/utils/network.py +74 -47
- netbox_toolkit_plugin/views.py +51 -22
- {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
- netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
- netbox_toolkit_plugin-0.1.2.dist-info/RECORD +0 -60
- {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
- {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
- {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {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,
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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 =
|
21
|
-
platform:
|
22
|
-
extra_options:
|
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:
|
32
|
-
execution_time:
|
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:
|
36
|
-
syntax_error_vendor:
|
37
|
-
syntax_error_guidance:
|
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:
|
40
|
+
parsed_output: dict[str, Any] | None = None
|
40
41
|
parsing_success: bool = False
|
41
|
-
parsing_method:
|
42
|
-
parsing_error:
|
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
|
-
|
56
|
-
|
56
|
+
|
57
57
|
@abstractmethod
|
58
58
|
def disconnect(self) -> None:
|
59
59
|
"""Close connection to the device."""
|
60
|
-
|
61
|
-
|
60
|
+
|
62
61
|
@abstractmethod
|
63
|
-
def execute_command(
|
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
|
-
|
74
|
-
|
74
|
+
|
75
75
|
@abstractmethod
|
76
76
|
def is_connected(self) -> bool:
|
77
77
|
"""Check if connection is active."""
|
78
|
-
|
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) ->
|
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
|
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:
|
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(
|
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:
|
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) ->
|
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:
|
280
|
-
) ->
|
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:
|
305
|
-
) ->
|
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
|
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
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
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:
|
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
|
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
|
-
|
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
|
-
|
15
|
+
DeviceConnectionError,
|
21
16
|
)
|
22
|
-
from ..
|
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:
|
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) ->
|
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(
|
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) ->
|
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
|
4
|
+
from typing import Any
|
5
5
|
|
6
|
-
from scrapli.driver.core import IOSXEDriver,
|
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 ..
|
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) ->
|
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) ->
|
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) ->
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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
|
1
|
+
from dcim.models import Device, Platform
|
2
2
|
from netbox.filtersets import NetBoxModelFilterSet
|
3
|
-
|
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=
|
14
|
+
label="Platform (ID)",
|
11
15
|
)
|
12
16
|
platform_slug = django_filters.CharFilter(
|
13
|
-
field_name=
|
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=[(
|
20
|
+
choices=[("show", "Show Command"), ("config", "Configuration Command")]
|
19
21
|
)
|
20
22
|
created_after = django_filters.DateTimeFilter(
|
21
|
-
field_name=
|
22
|
-
lookup_expr='gte'
|
23
|
+
field_name="created", lookup_expr="gte"
|
23
24
|
)
|
24
25
|
created_before = django_filters.DateTimeFilter(
|
25
|
-
field_name=
|
26
|
-
lookup_expr='lte'
|
26
|
+
field_name="created", lookup_expr="lte"
|
27
27
|
)
|
28
28
|
name_icontains = django_filters.CharFilter(
|
29
|
-
field_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=
|
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 = (
|
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=
|
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=
|
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=
|
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=
|
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=
|
79
|
-
lookup_expr=
|
80
|
-
label=
|
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 = (
|
69
|
+
fields = ("command", "device", "username", "success")
|