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,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)
|