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.
Files changed (56) hide show
  1. netbox_toolkit/__init__.py +30 -0
  2. netbox_toolkit/admin.py +16 -0
  3. netbox_toolkit/api/__init__.py +0 -0
  4. netbox_toolkit/api/mixins.py +54 -0
  5. netbox_toolkit/api/schemas.py +234 -0
  6. netbox_toolkit/api/serializers.py +158 -0
  7. netbox_toolkit/api/urls.py +10 -0
  8. netbox_toolkit/api/views/__init__.py +10 -0
  9. netbox_toolkit/api/views/command_logs.py +170 -0
  10. netbox_toolkit/api/views/commands.py +267 -0
  11. netbox_toolkit/config.py +159 -0
  12. netbox_toolkit/connectors/__init__.py +15 -0
  13. netbox_toolkit/connectors/base.py +97 -0
  14. netbox_toolkit/connectors/factory.py +301 -0
  15. netbox_toolkit/connectors/netmiko_connector.py +443 -0
  16. netbox_toolkit/connectors/scrapli_connector.py +486 -0
  17. netbox_toolkit/exceptions.py +36 -0
  18. netbox_toolkit/filtersets.py +85 -0
  19. netbox_toolkit/forms.py +31 -0
  20. netbox_toolkit/migrations/0001_initial.py +54 -0
  21. netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +66 -0
  22. netbox_toolkit/migrations/0003_permission_system_update.py +48 -0
  23. netbox_toolkit/migrations/0004_remove_django_permissions.py +77 -0
  24. netbox_toolkit/migrations/0005_alter_command_options_and_more.py +25 -0
  25. netbox_toolkit/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +28 -0
  26. netbox_toolkit/migrations/0007_alter_commandlog_parsing_template.py +18 -0
  27. netbox_toolkit/migrations/__init__.py +0 -0
  28. netbox_toolkit/models.py +89 -0
  29. netbox_toolkit/navigation.py +30 -0
  30. netbox_toolkit/search.py +21 -0
  31. netbox_toolkit/services/__init__.py +7 -0
  32. netbox_toolkit/services/command_service.py +357 -0
  33. netbox_toolkit/services/device_service.py +87 -0
  34. netbox_toolkit/services/rate_limiting_service.py +228 -0
  35. netbox_toolkit/static/netbox_toolkit/css/toolkit.css +143 -0
  36. netbox_toolkit/static/netbox_toolkit/js/toolkit.js +657 -0
  37. netbox_toolkit/tables.py +37 -0
  38. netbox_toolkit/templates/netbox_toolkit/command.html +108 -0
  39. netbox_toolkit/templates/netbox_toolkit/command_edit.html +10 -0
  40. netbox_toolkit/templates/netbox_toolkit/command_list.html +12 -0
  41. netbox_toolkit/templates/netbox_toolkit/commandlog.html +170 -0
  42. netbox_toolkit/templates/netbox_toolkit/commandlog_list.html +4 -0
  43. netbox_toolkit/templates/netbox_toolkit/device_toolkit.html +536 -0
  44. netbox_toolkit/urls.py +22 -0
  45. netbox_toolkit/utils/__init__.py +1 -0
  46. netbox_toolkit/utils/connection.py +125 -0
  47. netbox_toolkit/utils/error_parser.py +428 -0
  48. netbox_toolkit/utils/logging.py +58 -0
  49. netbox_toolkit/utils/network.py +157 -0
  50. netbox_toolkit/views.py +385 -0
  51. netbox_toolkit_plugin-0.1.0.dist-info/METADATA +76 -0
  52. netbox_toolkit_plugin-0.1.0.dist-info/RECORD +56 -0
  53. netbox_toolkit_plugin-0.1.0.dist-info/WHEEL +5 -0
  54. netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +2 -0
  55. netbox_toolkit_plugin-0.1.0.dist-info/licenses/LICENSE +200 -0
  56. 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