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