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,125 @@
1
+ """Connection management utilities for robust socket handling."""
2
+ import socket
3
+ import time
4
+ from typing import Any, Optional
5
+
6
+
7
+ def safe_socket_close(sock: Optional[socket.socket]) -> None:
8
+ """Safely close a socket with proper error handling."""
9
+ if sock is None:
10
+ return
11
+
12
+ try:
13
+ # Try graceful shutdown first
14
+ try:
15
+ sock.shutdown(socket.SHUT_RDWR)
16
+ except (OSError, AttributeError):
17
+ # Socket might already be closed or not connected
18
+ pass
19
+
20
+ # Then close the socket
21
+ sock.close()
22
+
23
+ except Exception as e:
24
+ pass # Socket cleanup error ignored
25
+
26
+
27
+ def is_socket_valid(sock: Optional[socket.socket]) -> bool:
28
+ """Check if a socket is valid and usable."""
29
+ if sock is None:
30
+ return False
31
+
32
+ try:
33
+ # Try to get socket peer name - this will fail if socket is closed
34
+ sock.getpeername()
35
+ return True
36
+ except (OSError, AttributeError):
37
+ return False
38
+
39
+
40
+ def cleanup_connection_resources(connection: Any) -> None:
41
+ """Clean up all resources associated with a connection object."""
42
+ if not connection:
43
+ return
44
+
45
+ try:
46
+ # Handle Scrapli-specific cleanup
47
+ if hasattr(connection, 'channel') and connection.channel:
48
+ try:
49
+ # Close the channel
50
+ if hasattr(connection.channel, 'close'):
51
+ connection.channel.close()
52
+
53
+ # Clean up the underlying socket if accessible
54
+ if hasattr(connection.channel, 'socket'):
55
+ safe_socket_close(connection.channel.socket)
56
+ elif hasattr(connection.channel, '_socket'):
57
+ safe_socket_close(connection.channel._socket)
58
+
59
+ except Exception as e:
60
+ pass # Channel cleanup error ignored
61
+
62
+ # Handle transport cleanup
63
+ if hasattr(connection, 'transport') and connection.transport:
64
+ try:
65
+ if hasattr(connection.transport, 'close'):
66
+ connection.transport.close()
67
+
68
+ # Clean up transport socket if accessible
69
+ if hasattr(connection.transport, 'sock'):
70
+ safe_socket_close(connection.transport.sock)
71
+ elif hasattr(connection.transport, '_socket'):
72
+ safe_socket_close(connection.transport._socket)
73
+
74
+ except Exception as e:
75
+ pass # Transport cleanup error ignored
76
+
77
+ # Try the main close method
78
+ if hasattr(connection, 'close'):
79
+ try:
80
+ connection.close()
81
+ except Exception as e:
82
+ pass # Connection close error ignored
83
+
84
+ except Exception as e:
85
+ pass # Connection cleanup error ignored
86
+
87
+
88
+ def validate_connection_health(connection: Any) -> bool:
89
+ """Validate that a connection is healthy and usable."""
90
+ if not connection:
91
+ return False
92
+
93
+ try:
94
+ # Check if connection reports as alive
95
+ if hasattr(connection, 'isalive'):
96
+ if not connection.isalive():
97
+ return False
98
+
99
+ # Check underlying socket health if available
100
+ if hasattr(connection, 'channel') and connection.channel:
101
+ if hasattr(connection.channel, 'socket'):
102
+ if not is_socket_valid(connection.channel.socket):
103
+ return False
104
+ elif hasattr(connection.channel, '_socket'):
105
+ if not is_socket_valid(connection.channel._socket):
106
+ return False
107
+
108
+ # Check transport socket health if available
109
+ if hasattr(connection, 'transport') and connection.transport:
110
+ if hasattr(connection.transport, 'sock'):
111
+ if not is_socket_valid(connection.transport.sock):
112
+ return False
113
+ elif hasattr(connection.transport, '_socket'):
114
+ if not is_socket_valid(connection.transport._socket):
115
+ return False
116
+
117
+ return True
118
+
119
+ except Exception as e:
120
+ return False
121
+
122
+
123
+ def wait_for_socket_cleanup(timeout: float = 2.0) -> None:
124
+ """Wait for socket cleanup to complete."""
125
+ time.sleep(min(timeout, 2.0))
@@ -0,0 +1,428 @@
1
+ """Utility for parsing and detecting vendor-specific error messages in command output."""
2
+ import re
3
+ from typing import Dict, List, Optional, Tuple
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+
7
+ class ErrorType(Enum):
8
+ """Types of errors that can be detected in command output."""
9
+ SYNTAX_ERROR = "syntax_error"
10
+ PERMISSION_ERROR = "permission_error"
11
+ COMMAND_NOT_FOUND = "command_not_found"
12
+ CONFIGURATION_ERROR = "configuration_error"
13
+ TIMEOUT_ERROR = "timeout_error"
14
+ UNKNOWN_ERROR = "unknown_error"
15
+
16
+ @dataclass
17
+ class ErrorPattern:
18
+ """Represents an error pattern for a specific vendor."""
19
+ pattern: str
20
+ error_type: ErrorType
21
+ vendor: str
22
+ case_sensitive: bool = False
23
+ description: str = ""
24
+
25
+ @dataclass
26
+ class ParsedError:
27
+ """Result of error parsing."""
28
+ error_type: ErrorType
29
+ vendor: str
30
+ original_message: str
31
+ enhanced_message: str
32
+ guidance: str
33
+ confidence: float # 0.0 to 1.0, how confident we are this is an error
34
+
35
+ class VendorErrorParser:
36
+ """Parser for detecting vendor-specific error messages."""
37
+
38
+ def __init__(self):
39
+ self.error_patterns = self._initialize_error_patterns()
40
+
41
+ def _initialize_error_patterns(self) -> List[ErrorPattern]:
42
+ """Initialize patterns for various vendor error messages."""
43
+ patterns = [
44
+ # Cisco IOS/IOS-XE Syntax Errors
45
+ ErrorPattern(
46
+ pattern=r"% Invalid input detected at '\^' marker\.",
47
+ error_type=ErrorType.SYNTAX_ERROR,
48
+ vendor="cisco_ios",
49
+ description="Invalid command syntax - caret indicates error position"
50
+ ),
51
+ ErrorPattern(
52
+ pattern=r"% Ambiguous command:",
53
+ error_type=ErrorType.SYNTAX_ERROR,
54
+ vendor="cisco_ios",
55
+ description="Command is ambiguous - need more specific input"
56
+ ),
57
+ ErrorPattern(
58
+ pattern=r"% Incomplete command\.",
59
+ error_type=ErrorType.SYNTAX_ERROR,
60
+ vendor="cisco_ios",
61
+ description="Command is incomplete - missing parameters"
62
+ ),
63
+ ErrorPattern(
64
+ pattern=r"% Unknown command or computer name, or unable to find computer address",
65
+ error_type=ErrorType.COMMAND_NOT_FOUND,
66
+ vendor="cisco_ios",
67
+ description="Command not recognized"
68
+ ),
69
+
70
+ # Cisco NX-OS Syntax Errors
71
+ ErrorPattern(
72
+ pattern=r"% Invalid command at '\^' marker\.",
73
+ error_type=ErrorType.SYNTAX_ERROR,
74
+ vendor="cisco_nxos",
75
+ description="Invalid command syntax"
76
+ ),
77
+ ErrorPattern(
78
+ pattern=r"% Invalid parameter detected at '\^' marker\.",
79
+ error_type=ErrorType.SYNTAX_ERROR,
80
+ vendor="cisco_nxos",
81
+ description="Invalid parameter in command"
82
+ ),
83
+ ErrorPattern(
84
+ pattern=r"% Command incomplete\.",
85
+ error_type=ErrorType.SYNTAX_ERROR,
86
+ vendor="cisco_nxos",
87
+ description="Command requires additional parameters"
88
+ ),
89
+
90
+ # Cisco IOS-XR Syntax Errors
91
+ ErrorPattern(
92
+ pattern=r"% Invalid input detected at '\^' marker\.",
93
+ error_type=ErrorType.SYNTAX_ERROR,
94
+ vendor="cisco_iosxr",
95
+ description="Invalid command syntax"
96
+ ),
97
+ ErrorPattern(
98
+ pattern=r"% Incomplete command\.",
99
+ error_type=ErrorType.SYNTAX_ERROR,
100
+ vendor="cisco_iosxr",
101
+ description="Command is incomplete"
102
+ ),
103
+
104
+ # Juniper Junos Syntax Errors
105
+ ErrorPattern(
106
+ pattern=r"syntax error, expecting <[\w\-]+>",
107
+ error_type=ErrorType.SYNTAX_ERROR,
108
+ vendor="juniper_junos",
109
+ description="Syntax error - expected different keyword"
110
+ ),
111
+ ErrorPattern(
112
+ pattern=r"syntax error\.",
113
+ error_type=ErrorType.SYNTAX_ERROR,
114
+ vendor="juniper_junos",
115
+ description="General syntax error"
116
+ ),
117
+ ErrorPattern(
118
+ pattern=r"unknown command\.",
119
+ error_type=ErrorType.COMMAND_NOT_FOUND,
120
+ vendor="juniper_junos",
121
+ description="Command not recognized"
122
+ ),
123
+ ErrorPattern(
124
+ pattern=r"error: .*",
125
+ error_type=ErrorType.UNKNOWN_ERROR,
126
+ vendor="juniper_junos",
127
+ description="General error from device"
128
+ ),
129
+
130
+ # Arista EOS Syntax Errors
131
+ ErrorPattern(
132
+ pattern=r"% Invalid input",
133
+ error_type=ErrorType.SYNTAX_ERROR,
134
+ vendor="arista_eos",
135
+ description="Invalid command input"
136
+ ),
137
+ ErrorPattern(
138
+ pattern=r"% Incomplete command",
139
+ error_type=ErrorType.SYNTAX_ERROR,
140
+ vendor="arista_eos",
141
+ description="Command requires additional parameters"
142
+ ),
143
+ ErrorPattern(
144
+ pattern=r"% Ambiguous command",
145
+ error_type=ErrorType.SYNTAX_ERROR,
146
+ vendor="arista_eos",
147
+ description="Command is ambiguous"
148
+ ),
149
+
150
+ # HPE/Aruba Syntax Errors
151
+ ErrorPattern(
152
+ pattern=r"Invalid input:",
153
+ error_type=ErrorType.SYNTAX_ERROR,
154
+ vendor="aruba",
155
+ description="Invalid command syntax"
156
+ ),
157
+ ErrorPattern(
158
+ pattern=r"Unknown command\.",
159
+ error_type=ErrorType.COMMAND_NOT_FOUND,
160
+ vendor="aruba",
161
+ description="Command not recognized"
162
+ ),
163
+
164
+ # Huawei Syntax Errors
165
+ ErrorPattern(
166
+ pattern=r"Error: Unrecognized command found at '\^' position\.",
167
+ error_type=ErrorType.SYNTAX_ERROR,
168
+ vendor="huawei",
169
+ description="Unrecognized command syntax"
170
+ ),
171
+ ErrorPattern(
172
+ pattern=r"Error: Incomplete command found at '\^' position\.",
173
+ error_type=ErrorType.SYNTAX_ERROR,
174
+ vendor="huawei",
175
+ description="Incomplete command"
176
+ ),
177
+
178
+ # Fortinet FortiOS Syntax Errors
179
+ ErrorPattern(
180
+ pattern=r"command parse error before",
181
+ error_type=ErrorType.SYNTAX_ERROR,
182
+ vendor="fortinet",
183
+ description="Command parsing error"
184
+ ),
185
+ ErrorPattern(
186
+ pattern=r"Unknown action",
187
+ error_type=ErrorType.COMMAND_NOT_FOUND,
188
+ vendor="fortinet",
189
+ description="Unknown command or action"
190
+ ),
191
+
192
+ # Palo Alto PAN-OS Syntax Errors
193
+ ErrorPattern(
194
+ pattern=r"Invalid syntax\.",
195
+ error_type=ErrorType.SYNTAX_ERROR,
196
+ vendor="paloalto",
197
+ description="Invalid command syntax"
198
+ ),
199
+ ErrorPattern(
200
+ pattern=r"Unknown command:",
201
+ error_type=ErrorType.COMMAND_NOT_FOUND,
202
+ vendor="paloalto",
203
+ description="Command not recognized"
204
+ ),
205
+
206
+ # Generic Permission Errors (across vendors)
207
+ ErrorPattern(
208
+ pattern=r"Permission denied",
209
+ error_type=ErrorType.PERMISSION_ERROR,
210
+ vendor="generic",
211
+ description="Insufficient permissions for command"
212
+ ),
213
+ ErrorPattern(
214
+ pattern=r"Access denied",
215
+ error_type=ErrorType.PERMISSION_ERROR,
216
+ vendor="generic",
217
+ description="Access denied for command"
218
+ ),
219
+ ErrorPattern(
220
+ pattern=r"Insufficient privileges",
221
+ error_type=ErrorType.PERMISSION_ERROR,
222
+ vendor="generic",
223
+ description="User lacks required privileges"
224
+ ),
225
+
226
+ # Generic Command Not Found Errors
227
+ ErrorPattern(
228
+ pattern=r"command not found",
229
+ error_type=ErrorType.COMMAND_NOT_FOUND,
230
+ vendor="generic",
231
+ case_sensitive=False,
232
+ description="Command not found on system"
233
+ ),
234
+ ErrorPattern(
235
+ pattern=r"bad command or file name",
236
+ error_type=ErrorType.COMMAND_NOT_FOUND,
237
+ vendor="generic",
238
+ case_sensitive=False,
239
+ description="Command not recognized"
240
+ ),
241
+ ]
242
+
243
+ return patterns
244
+
245
+ def parse_command_output(self, output: str, device_platform: Optional[str] = None) -> Optional[ParsedError]:
246
+ """
247
+ Parse command output to detect vendor-specific errors.
248
+
249
+ Args:
250
+ output: The command output to analyze
251
+ device_platform: Optional platform hint to prioritize certain patterns
252
+
253
+ Returns:
254
+ ParsedError if an error is detected, None otherwise
255
+ """
256
+ if not output or not output.strip():
257
+ return None
258
+
259
+ # Normalize the output for consistent parsing
260
+ output_lines = output.strip().split('\n')
261
+
262
+ # Check each line for error patterns
263
+ best_match = None
264
+ highest_confidence = 0.0
265
+
266
+ for line in output_lines:
267
+ line = line.strip()
268
+ if not line:
269
+ continue
270
+
271
+ for pattern in self.error_patterns:
272
+ match = self._match_pattern(line, pattern, device_platform)
273
+ if match and match.confidence > highest_confidence:
274
+ best_match = match
275
+ highest_confidence = match.confidence
276
+
277
+ return best_match
278
+
279
+ def _match_pattern(self, line: str, pattern: ErrorPattern, device_platform: Optional[str] = None) -> Optional[ParsedError]:
280
+ """Check if a line matches an error pattern."""
281
+ flags = 0 if pattern.case_sensitive else re.IGNORECASE
282
+
283
+ if re.search(pattern.pattern, line, flags):
284
+ # Calculate confidence based on vendor match and pattern specificity
285
+ confidence = 0.7 # Base confidence
286
+
287
+ # Boost confidence if vendor matches device platform
288
+ if device_platform and self._platforms_match(pattern.vendor, device_platform):
289
+ confidence += 0.2
290
+
291
+ # Boost confidence for more specific patterns
292
+ if len(pattern.pattern) > 20: # Longer patterns are typically more specific
293
+ confidence += 0.1
294
+
295
+ # Cap confidence at 1.0
296
+ confidence = min(confidence, 1.0)
297
+
298
+ enhanced_message = self._enhance_error_message(line, pattern)
299
+ guidance = self._get_error_guidance(pattern, line)
300
+
301
+ return ParsedError(
302
+ error_type=pattern.error_type,
303
+ vendor=pattern.vendor,
304
+ original_message=line,
305
+ enhanced_message=enhanced_message,
306
+ guidance=guidance,
307
+ confidence=confidence
308
+ )
309
+
310
+ return None
311
+
312
+ def _platforms_match(self, pattern_vendor: str, device_platform: str) -> bool:
313
+ """Check if pattern vendor matches device platform."""
314
+ if not device_platform:
315
+ return False
316
+
317
+ device_platform = device_platform.lower().strip()
318
+ pattern_vendor = pattern_vendor.lower().strip()
319
+
320
+ # Direct match
321
+ if pattern_vendor == device_platform:
322
+ return True
323
+
324
+ # Check for platform family matches
325
+ cisco_platforms = ['cisco_ios', 'cisco_nxos', 'cisco_iosxr', 'ios', 'nxos', 'iosxr']
326
+ juniper_platforms = ['juniper_junos', 'junos']
327
+ arista_platforms = ['arista_eos', 'eos']
328
+
329
+ if pattern_vendor in cisco_platforms and device_platform in cisco_platforms:
330
+ return True
331
+ if pattern_vendor in juniper_platforms and device_platform in juniper_platforms:
332
+ return True
333
+ if pattern_vendor in arista_platforms and device_platform in arista_platforms:
334
+ return True
335
+
336
+ return False
337
+
338
+ def _enhance_error_message(self, original_message: str, pattern: ErrorPattern) -> str:
339
+ """Enhance the original error message with additional context."""
340
+ vendor_name = self._get_vendor_display_name(pattern.vendor)
341
+
342
+ enhanced = f"[{vendor_name}] {original_message}"
343
+
344
+ if pattern.description:
345
+ enhanced += f"\n\nDescription: {pattern.description}"
346
+
347
+ return enhanced
348
+
349
+ def _get_vendor_display_name(self, vendor: str) -> str:
350
+ """Get a human-readable vendor name."""
351
+ vendor_map = {
352
+ 'cisco_ios': 'Cisco IOS/IOS-XE',
353
+ 'cisco_nxos': 'Cisco NX-OS',
354
+ 'cisco_iosxr': 'Cisco IOS-XR',
355
+ 'juniper_junos': 'Juniper Junos',
356
+ 'arista_eos': 'Arista EOS',
357
+ 'aruba': 'HPE Aruba',
358
+ 'huawei': 'Huawei',
359
+ 'fortinet': 'Fortinet FortiOS',
360
+ 'paloalto': 'Palo Alto PAN-OS',
361
+ 'generic': 'Generic'
362
+ }
363
+
364
+ return vendor_map.get(vendor, vendor.title())
365
+
366
+ def _get_error_guidance(self, pattern: ErrorPattern, original_message: str) -> str:
367
+ """Get vendor-specific guidance for resolving the error."""
368
+ base_guidance = {
369
+ ErrorType.SYNTAX_ERROR: self._get_syntax_error_guidance(pattern.vendor, original_message),
370
+ ErrorType.PERMISSION_ERROR: self._get_permission_error_guidance(pattern.vendor),
371
+ ErrorType.COMMAND_NOT_FOUND: self._get_command_not_found_guidance(pattern.vendor),
372
+ ErrorType.CONFIGURATION_ERROR: self._get_configuration_error_guidance(pattern.vendor),
373
+ ErrorType.TIMEOUT_ERROR: self._get_timeout_error_guidance(pattern.vendor),
374
+ ErrorType.UNKNOWN_ERROR: self._get_unknown_error_guidance(pattern.vendor)
375
+ }
376
+
377
+ return base_guidance.get(pattern.error_type, "Check device documentation for error details.")
378
+
379
+ def _get_syntax_error_guidance(self, vendor: str, message: str) -> str:
380
+ """Get syntax error guidance based on vendor."""
381
+ common_guidance = (
382
+ "Command Syntax Error Troubleshooting:\n"
383
+ "• Verify the command syntax is correct for this device platform\n"
384
+ "• Ensure command has correct mode set (e.g., show or config)\n"
385
+ )
386
+
387
+ return common_guidance
388
+
389
+ def _get_permission_error_guidance(self, vendor: str) -> str:
390
+ """Get permission error guidance."""
391
+ return (
392
+ "Permission Error Troubleshooting:\n"
393
+ "• Verify your user account has the necessary privileges\n"
394
+ "• Review device AAA configuration for command authorization\n"
395
+ )
396
+
397
+ def _get_command_not_found_guidance(self, vendor: str) -> str:
398
+ """Get command not found guidance."""
399
+ return (
400
+ "Command Not Found Troubleshooting:\n"
401
+ "• Verify the command exists on this device/platform\n"
402
+ "• Check the command spelling and syntax\n"
403
+ "• Try the command manually on device\n"
404
+ )
405
+
406
+ def _get_configuration_error_guidance(self, vendor: str) -> str:
407
+ """Get configuration error guidance."""
408
+ return (
409
+ "Configuration Error Troubleshooting:\n"
410
+ "• Review the configuration syntax for this platform\n"
411
+ )
412
+
413
+ def _get_timeout_error_guidance(self, vendor: str) -> str:
414
+ """Get timeout error guidance."""
415
+ return (
416
+ "Timeout Error Troubleshooting:\n"
417
+ "• The command may be taking longer than expected\n"
418
+ )
419
+
420
+ def _get_unknown_error_guidance(self, vendor: str) -> str:
421
+ """Get unknown error guidance."""
422
+ return (
423
+ "General Error Troubleshooting:\n"
424
+ "• Check device logs for more details\n"
425
+ "• Verify device configuration and status\n"
426
+ "• Review command syntax and parameters\n"
427
+ "• Try the command manually on device\n"
428
+ )
@@ -0,0 +1,58 @@
1
+ """Logging utilities for NetBox Toolkit plugin."""
2
+ import logging
3
+ from django.conf import settings
4
+
5
+
6
+ class RequireToolkitDebug(logging.Filter):
7
+ """
8
+ Custom logging filter that only allows log records when the plugin's
9
+ debug_logging setting is enabled.
10
+
11
+ This allows plugin-specific debug logging without requiring Django's
12
+ DEBUG=True, making it safe for production environments.
13
+ """
14
+
15
+ def filter(self, record):
16
+ """
17
+ Check if toolkit debug logging is enabled.
18
+
19
+ Returns:
20
+ bool: True if debug logging is enabled for this plugin
21
+ """
22
+ try:
23
+ # Get plugin configuration from Django settings
24
+ plugins_config = getattr(settings, 'PLUGINS_CONFIG', {})
25
+ toolkit_config = plugins_config.get('netbox_toolkit', {})
26
+
27
+ # Check if debug_logging is enabled (default: False)
28
+ return toolkit_config.get('debug_logging', False)
29
+ except (AttributeError, KeyError):
30
+ # If configuration is not available, don't log
31
+ return False
32
+
33
+
34
+ def get_toolkit_logger(name: str) -> logging.Logger:
35
+ """
36
+ Get a logger for the toolkit plugin with the proper namespace.
37
+
38
+ Args:
39
+ name: Logger name (typically __name__)
40
+
41
+ Returns:
42
+ Logger instance for the toolkit plugin
43
+ """
44
+ # Ensure we're using the netbox_toolkit namespace
45
+ if not name.startswith('netbox_toolkit'):
46
+ if name == '__main__':
47
+ name = 'netbox_toolkit'
48
+ else:
49
+ # Extract module name and add to toolkit namespace
50
+ module_parts = name.split('.')
51
+ if 'netbox_toolkit' in module_parts:
52
+ # Already in our namespace, use as-is
53
+ pass
54
+ else:
55
+ # Add to our namespace
56
+ name = f'netbox_toolkit.{name.split(".")[-1]}'
57
+
58
+ return logging.getLogger(name)