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,301 @@
|
|
1
|
+
"""Factory for creating device connectors."""
|
2
|
+
import logging
|
3
|
+
from typing import Type, Optional
|
4
|
+
|
5
|
+
from django.db.models import Model
|
6
|
+
from dcim.models import Device
|
7
|
+
|
8
|
+
from ..exceptions import UnsupportedPlatformError, DeviceConnectionError
|
9
|
+
from ..config import ToolkitConfig
|
10
|
+
from ..utils.logging import get_toolkit_logger
|
11
|
+
from .base import BaseDeviceConnector, ConnectionConfig
|
12
|
+
from .scrapli_connector import ScrapliConnector
|
13
|
+
from .netmiko_connector import NetmikoConnector
|
14
|
+
|
15
|
+
logger = get_toolkit_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class ConnectorFactory:
|
19
|
+
"""Factory for creating device connectors with Scrapli primary and Netmiko fallback."""
|
20
|
+
|
21
|
+
# Primary connector (high-performance)
|
22
|
+
PRIMARY_CONNECTOR = ScrapliConnector
|
23
|
+
|
24
|
+
# Fallback connector (legacy device support)
|
25
|
+
FALLBACK_CONNECTOR = NetmikoConnector
|
26
|
+
|
27
|
+
# Platform-specific connector mappings
|
28
|
+
CONNECTOR_MAP = {
|
29
|
+
# Scrapli-optimized platforms (use primary connector)
|
30
|
+
'cisco_ios': ScrapliConnector,
|
31
|
+
'cisco_nxos': ScrapliConnector,
|
32
|
+
'cisco_iosxr': ScrapliConnector,
|
33
|
+
'cisco_xe': ScrapliConnector,
|
34
|
+
'ios': ScrapliConnector,
|
35
|
+
'nxos': ScrapliConnector,
|
36
|
+
'iosxr': ScrapliConnector,
|
37
|
+
'ios-xe': ScrapliConnector,
|
38
|
+
'ios-xr': ScrapliConnector,
|
39
|
+
|
40
|
+
# Platforms that may work better with Netmiko fallback
|
41
|
+
'juniper_junos': ScrapliConnector, # Try Scrapli first
|
42
|
+
'arista_eos': ScrapliConnector, # Try Scrapli first
|
43
|
+
'linux': ScrapliConnector, # Try Scrapli first
|
44
|
+
|
45
|
+
# Legacy/specialized platforms - direct to Netmiko
|
46
|
+
'hp_procurve': NetmikoConnector,
|
47
|
+
'hp_comware': NetmikoConnector,
|
48
|
+
'dell_os10': NetmikoConnector,
|
49
|
+
'dell_powerconnect': NetmikoConnector,
|
50
|
+
'cisco_asa': NetmikoConnector,
|
51
|
+
'paloalto_panos': NetmikoConnector,
|
52
|
+
'fortinet': NetmikoConnector,
|
53
|
+
'mikrotik_routeros': NetmikoConnector,
|
54
|
+
'ubiquiti_edge': NetmikoConnector,
|
55
|
+
}
|
56
|
+
|
57
|
+
@classmethod
|
58
|
+
def create_connector(
|
59
|
+
cls,
|
60
|
+
device: Device,
|
61
|
+
username: str,
|
62
|
+
password: str,
|
63
|
+
connector_type: Optional[str] = None,
|
64
|
+
use_fallback: bool = True
|
65
|
+
) -> BaseDeviceConnector:
|
66
|
+
"""
|
67
|
+
Create a device connector with Scrapli primary and Netmiko fallback strategy.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
device: NetBox Device instance
|
71
|
+
username: Authentication username
|
72
|
+
password: Authentication password
|
73
|
+
connector_type: Override connector type (optional)
|
74
|
+
use_fallback: Whether to attempt fallback to Netmiko on Scrapli failure
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
Device connector instance
|
78
|
+
|
79
|
+
Raises:
|
80
|
+
UnsupportedPlatformError: If platform is not supported by any connector
|
81
|
+
DeviceConnectionError: If both primary and fallback connectors fail
|
82
|
+
"""
|
83
|
+
logger.debug("Creating connector for device %s (platform: %s)",
|
84
|
+
device.name, device.platform)
|
85
|
+
|
86
|
+
config = cls._build_connection_config(device, username, password)
|
87
|
+
|
88
|
+
# Determine connector class
|
89
|
+
if connector_type:
|
90
|
+
logger.debug("Using override connector type: %s", connector_type)
|
91
|
+
connector_class = cls._get_connector_by_type(connector_type)
|
92
|
+
logger.info("Created %s connector for device %s",
|
93
|
+
connector_class.__name__, device.name)
|
94
|
+
return connector_class(config)
|
95
|
+
|
96
|
+
# Try platform-specific connector first
|
97
|
+
primary_connector_class = cls._get_primary_connector_by_platform(config.platform)
|
98
|
+
|
99
|
+
try:
|
100
|
+
logger.debug("Attempting primary connector: %s", primary_connector_class.__name__)
|
101
|
+
|
102
|
+
# Create a clean config for the primary connector
|
103
|
+
primary_config = cls._prepare_connector_config(config, primary_connector_class)
|
104
|
+
connector = primary_connector_class(primary_config)
|
105
|
+
|
106
|
+
# For ScrapliConnector, rely on fast-fail logic in connect() method
|
107
|
+
# instead of pre-testing connection. This avoids double connection attempts.
|
108
|
+
logger.info("Created %s connector for device %s",
|
109
|
+
primary_connector_class.__name__, device.name)
|
110
|
+
return connector
|
111
|
+
|
112
|
+
except Exception as e:
|
113
|
+
# Check if this is a fast-fail scenario for immediate Netmiko fallback
|
114
|
+
error_msg = str(e)
|
115
|
+
if (use_fallback and
|
116
|
+
primary_connector_class == ScrapliConnector and
|
117
|
+
("Fast-fail to Netmiko" in error_msg or
|
118
|
+
ToolkitConfig.should_fast_fail_to_netmiko(error_msg))):
|
119
|
+
logger.info("Fast-fail pattern detected during connector creation for %s", device.name)
|
120
|
+
return cls._create_fallback_connector(config, device.name, error_msg)
|
121
|
+
elif use_fallback and primary_connector_class != NetmikoConnector:
|
122
|
+
logger.warning("Primary connector failed for %s: %s", device.name, error_msg)
|
123
|
+
return cls._create_fallback_connector(config, device.name, error_msg)
|
124
|
+
else:
|
125
|
+
raise DeviceConnectionError(f"Connector creation failed: {error_msg}")
|
126
|
+
|
127
|
+
@classmethod
|
128
|
+
def _create_fallback_connector(cls, config: ConnectionConfig, device_name: str, primary_error: str) -> BaseDeviceConnector:
|
129
|
+
"""Create fallback Netmiko connector when primary fails."""
|
130
|
+
logger.info("Falling back to Netmiko connector for device %s", device_name)
|
131
|
+
try:
|
132
|
+
# Create a clean config specifically for Netmiko
|
133
|
+
fallback_config = cls._prepare_connector_config(config, NetmikoConnector)
|
134
|
+
connector = NetmikoConnector(fallback_config)
|
135
|
+
logger.info("Successfully created Netmiko fallback connector for device %s", device_name)
|
136
|
+
return connector
|
137
|
+
except Exception as fallback_error:
|
138
|
+
logger.error("Both primary and fallback connectors failed for %s", device_name)
|
139
|
+
raise DeviceConnectionError(
|
140
|
+
f"Both connectors failed. Primary error: {primary_error}. "
|
141
|
+
f"Fallback error: {str(fallback_error)}"
|
142
|
+
)
|
143
|
+
|
144
|
+
@classmethod
|
145
|
+
def _prepare_connector_config(cls, base_config: ConnectionConfig, connector_class: Type[BaseDeviceConnector]) -> ConnectionConfig:
|
146
|
+
"""Prepare a clean configuration for a specific connector type."""
|
147
|
+
# Create a copy of the base config
|
148
|
+
config = ConnectionConfig(
|
149
|
+
hostname=base_config.hostname,
|
150
|
+
username=base_config.username,
|
151
|
+
password=base_config.password,
|
152
|
+
port=base_config.port,
|
153
|
+
timeout_socket=base_config.timeout_socket,
|
154
|
+
timeout_transport=base_config.timeout_transport,
|
155
|
+
timeout_ops=base_config.timeout_ops,
|
156
|
+
auth_strict_key=base_config.auth_strict_key,
|
157
|
+
transport=base_config.transport,
|
158
|
+
platform=base_config.platform,
|
159
|
+
extra_options=None # Start with clean extra_options
|
160
|
+
)
|
161
|
+
|
162
|
+
# Add connector-specific configurations
|
163
|
+
if connector_class == NetmikoConnector:
|
164
|
+
# For Netmiko, add Netmiko-specific config to extra_options
|
165
|
+
netmiko_config = ToolkitConfig.get_netmiko_config()
|
166
|
+
config.extra_options = netmiko_config.copy()
|
167
|
+
logger.debug("Prepared Netmiko config with options: %s", list(netmiko_config.keys()))
|
168
|
+
|
169
|
+
elif connector_class == ScrapliConnector:
|
170
|
+
# For Scrapli, keep extra_options clean (transport options are handled separately)
|
171
|
+
# Any Scrapli-specific config would go here
|
172
|
+
config.extra_options = None
|
173
|
+
logger.debug("Prepared clean Scrapli config")
|
174
|
+
|
175
|
+
return config
|
176
|
+
|
177
|
+
@classmethod
|
178
|
+
def _build_connection_config(cls, device: Device, username: str, password: str) -> ConnectionConfig:
|
179
|
+
"""Build connection configuration from device properties."""
|
180
|
+
logger.debug("Building connection config for device %s", device.name)
|
181
|
+
|
182
|
+
# Get device connection details
|
183
|
+
hostname = str(device.primary_ip.address.ip) if device.primary_ip else device.name
|
184
|
+
platform = str(device.platform).lower() if device.platform else None
|
185
|
+
|
186
|
+
logger.debug("Connection details - hostname: %s, platform: %s", hostname, platform)
|
187
|
+
|
188
|
+
# Normalize platform using config
|
189
|
+
normalized_platform = ToolkitConfig.normalize_platform(platform)
|
190
|
+
logger.debug("Normalized platform: %s -> %s", platform, normalized_platform)
|
191
|
+
|
192
|
+
# Get timeouts based on device type
|
193
|
+
device_model = str(device.device_type.model) if device.device_type else None
|
194
|
+
timeouts = ToolkitConfig.get_timeouts_for_device(device_model)
|
195
|
+
|
196
|
+
# Build configuration
|
197
|
+
config = ConnectionConfig(
|
198
|
+
hostname=hostname,
|
199
|
+
username=username,
|
200
|
+
password=password,
|
201
|
+
platform=normalized_platform,
|
202
|
+
timeout_socket=timeouts['socket'],
|
203
|
+
timeout_transport=timeouts['transport'],
|
204
|
+
timeout_ops=timeouts['ops'],
|
205
|
+
)
|
206
|
+
|
207
|
+
# Add device-specific customizations if needed
|
208
|
+
config = cls._customize_config_for_device(config, device)
|
209
|
+
|
210
|
+
return config
|
211
|
+
|
212
|
+
@classmethod
|
213
|
+
def _customize_config_for_device(cls, config: ConnectionConfig, device: Device) -> ConnectionConfig:
|
214
|
+
"""Customize configuration based on device properties."""
|
215
|
+
# Add custom port if specified in device custom fields
|
216
|
+
# (This would require custom fields to be defined in NetBox)
|
217
|
+
# if hasattr(device, 'cf') and device.cf.get('ssh_port'):
|
218
|
+
# config.port = int(device.cf['ssh_port'])
|
219
|
+
|
220
|
+
return config
|
221
|
+
|
222
|
+
@classmethod
|
223
|
+
def _get_connector_by_type(cls, connector_type: str) -> Type[BaseDeviceConnector]:
|
224
|
+
"""Get connector class by explicit type."""
|
225
|
+
connector_type_lower = connector_type.lower()
|
226
|
+
if connector_type_lower == 'scrapli':
|
227
|
+
return ScrapliConnector
|
228
|
+
elif connector_type_lower == 'netmiko':
|
229
|
+
return NetmikoConnector
|
230
|
+
else:
|
231
|
+
raise UnsupportedPlatformError(f"Unsupported connector type: {connector_type}")
|
232
|
+
|
233
|
+
@classmethod
|
234
|
+
def _get_primary_connector_by_platform(cls, platform: Optional[str]) -> Type[BaseDeviceConnector]:
|
235
|
+
"""Get primary connector class by device platform."""
|
236
|
+
if not platform:
|
237
|
+
return cls.PRIMARY_CONNECTOR
|
238
|
+
|
239
|
+
platform_lower = platform.lower()
|
240
|
+
|
241
|
+
# Check for exact match first
|
242
|
+
if platform_lower in cls.CONNECTOR_MAP:
|
243
|
+
return cls.CONNECTOR_MAP[platform_lower]
|
244
|
+
|
245
|
+
# Check for partial matches (e.g., 'ios' in 'cisco_ios')
|
246
|
+
for supported_platform, connector_class in cls.CONNECTOR_MAP.items():
|
247
|
+
if platform_lower in supported_platform or supported_platform in platform_lower:
|
248
|
+
return connector_class
|
249
|
+
|
250
|
+
# If no specific match, use primary connector
|
251
|
+
return cls.PRIMARY_CONNECTOR
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
def _get_connector_by_platform(cls, platform: Optional[str]) -> Type[BaseDeviceConnector]:
|
255
|
+
"""Legacy method for backward compatibility."""
|
256
|
+
return cls._get_primary_connector_by_platform(platform)
|
257
|
+
|
258
|
+
@classmethod
|
259
|
+
def get_supported_platforms(cls) -> list[str]:
|
260
|
+
"""Get list of supported platforms from both connectors."""
|
261
|
+
scrapli_platforms = ScrapliConnector.get_supported_platforms()
|
262
|
+
netmiko_platforms = NetmikoConnector.get_supported_platforms()
|
263
|
+
|
264
|
+
# Combine and deduplicate
|
265
|
+
all_platforms = list(set(scrapli_platforms + netmiko_platforms + list(cls.CONNECTOR_MAP.keys())))
|
266
|
+
return sorted(all_platforms)
|
267
|
+
|
268
|
+
@classmethod
|
269
|
+
def is_platform_supported(cls, platform: str) -> bool:
|
270
|
+
"""Check if a platform is supported by any connector."""
|
271
|
+
if not platform:
|
272
|
+
return True # Default connectors can handle unknown platforms
|
273
|
+
|
274
|
+
platform_lower = platform.lower()
|
275
|
+
|
276
|
+
# Check exact match in our mapping
|
277
|
+
if platform_lower in cls.CONNECTOR_MAP:
|
278
|
+
return True
|
279
|
+
|
280
|
+
# Check partial matches
|
281
|
+
for supported_platform in cls.CONNECTOR_MAP.keys():
|
282
|
+
if platform_lower in supported_platform or supported_platform in platform_lower:
|
283
|
+
return True
|
284
|
+
|
285
|
+
# Check if either connector supports it
|
286
|
+
if (platform_lower in [p.lower() for p in ScrapliConnector.get_supported_platforms()] or
|
287
|
+
platform_lower in [p.lower() for p in NetmikoConnector.get_supported_platforms()]):
|
288
|
+
return True
|
289
|
+
|
290
|
+
return False
|
291
|
+
|
292
|
+
@classmethod
|
293
|
+
def get_recommended_connector(cls, platform: Optional[str]) -> str:
|
294
|
+
"""Get recommended connector type for a platform."""
|
295
|
+
connector_class = cls._get_primary_connector_by_platform(platform)
|
296
|
+
if connector_class == ScrapliConnector:
|
297
|
+
return "scrapli"
|
298
|
+
elif connector_class == NetmikoConnector:
|
299
|
+
return "netmiko"
|
300
|
+
else:
|
301
|
+
return "scrapli" # Default recommendation
|