netbox-toolkit-plugin 0.1.1__py3-none-any.whl → 0.1.3__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 (43) hide show
  1. netbox_toolkit_plugin/__init__.py +1 -1
  2. netbox_toolkit_plugin/admin.py +11 -7
  3. netbox_toolkit_plugin/api/mixins.py +20 -16
  4. netbox_toolkit_plugin/api/schemas.py +53 -74
  5. netbox_toolkit_plugin/api/serializers.py +10 -11
  6. netbox_toolkit_plugin/api/urls.py +2 -1
  7. netbox_toolkit_plugin/api/views/__init__.py +4 -3
  8. netbox_toolkit_plugin/api/views/command_logs.py +80 -73
  9. netbox_toolkit_plugin/api/views/commands.py +140 -134
  10. netbox_toolkit_plugin/connectors/__init__.py +9 -9
  11. netbox_toolkit_plugin/connectors/base.py +30 -31
  12. netbox_toolkit_plugin/connectors/factory.py +22 -26
  13. netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
  14. netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
  15. netbox_toolkit_plugin/exceptions.py +0 -7
  16. netbox_toolkit_plugin/filtersets.py +26 -42
  17. netbox_toolkit_plugin/forms.py +13 -11
  18. netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
  19. netbox_toolkit_plugin/models.py +2 -17
  20. netbox_toolkit_plugin/navigation.py +3 -0
  21. netbox_toolkit_plugin/search.py +12 -9
  22. netbox_toolkit_plugin/services/__init__.py +1 -1
  23. netbox_toolkit_plugin/services/command_service.py +7 -10
  24. netbox_toolkit_plugin/services/device_service.py +40 -32
  25. netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
  26. netbox_toolkit_plugin/{config.py → settings.py} +17 -7
  27. netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
  28. netbox_toolkit_plugin/tables.py +10 -1
  29. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
  30. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
  31. netbox_toolkit_plugin/urls.py +10 -3
  32. netbox_toolkit_plugin/utils/connection.py +54 -54
  33. netbox_toolkit_plugin/utils/error_parser.py +128 -109
  34. netbox_toolkit_plugin/utils/logging.py +1 -0
  35. netbox_toolkit_plugin/utils/network.py +74 -47
  36. netbox_toolkit_plugin/views.py +51 -22
  37. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
  38. netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
  39. netbox_toolkit_plugin-0.1.1.dist-info/RECORD +0 -60
  40. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
  41. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
  42. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
  43. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,13 @@
1
1
  """Utility for parsing and detecting vendor-specific error messages in command output."""
2
+
2
3
  import re
3
- from typing import Dict, List, Optional, Tuple
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
6
 
7
+
7
8
  class ErrorType(Enum):
8
9
  """Types of errors that can be detected in command output."""
10
+
9
11
  SYNTAX_ERROR = "syntax_error"
10
12
  PERMISSION_ERROR = "permission_error"
11
13
  COMMAND_NOT_FOUND = "command_not_found"
@@ -13,18 +15,22 @@ class ErrorType(Enum):
13
15
  TIMEOUT_ERROR = "timeout_error"
14
16
  UNKNOWN_ERROR = "unknown_error"
15
17
 
18
+
16
19
  @dataclass
17
20
  class ErrorPattern:
18
21
  """Represents an error pattern for a specific vendor."""
22
+
19
23
  pattern: str
20
24
  error_type: ErrorType
21
25
  vendor: str
22
26
  case_sensitive: bool = False
23
27
  description: str = ""
24
28
 
29
+
25
30
  @dataclass
26
31
  class ParsedError:
27
32
  """Result of error parsing."""
33
+
28
34
  error_type: ErrorType
29
35
  vendor: str
30
36
  original_message: str
@@ -32,13 +38,14 @@ class ParsedError:
32
38
  guidance: str
33
39
  confidence: float # 0.0 to 1.0, how confident we are this is an error
34
40
 
41
+
35
42
  class VendorErrorParser:
36
43
  """Parser for detecting vendor-specific error messages."""
37
-
44
+
38
45
  def __init__(self):
39
46
  self.error_patterns = self._initialize_error_patterns()
40
-
41
- def _initialize_error_patterns(self) -> List[ErrorPattern]:
47
+
48
+ def _initialize_error_patterns(self) -> list[ErrorPattern]:
42
49
  """Initialize patterns for various vendor error messages."""
43
50
  patterns = [
44
51
  # Cisco IOS/IOS-XE Syntax Errors
@@ -46,336 +53,348 @@ class VendorErrorParser:
46
53
  pattern=r"% Invalid input detected at '\^' marker\.",
47
54
  error_type=ErrorType.SYNTAX_ERROR,
48
55
  vendor="cisco_ios",
49
- description="Invalid command syntax - caret indicates error position"
56
+ description="Invalid command syntax - caret indicates error position",
50
57
  ),
51
58
  ErrorPattern(
52
59
  pattern=r"% Ambiguous command:",
53
60
  error_type=ErrorType.SYNTAX_ERROR,
54
61
  vendor="cisco_ios",
55
- description="Command is ambiguous - need more specific input"
62
+ description="Command is ambiguous - need more specific input",
56
63
  ),
57
64
  ErrorPattern(
58
65
  pattern=r"% Incomplete command\.",
59
66
  error_type=ErrorType.SYNTAX_ERROR,
60
67
  vendor="cisco_ios",
61
- description="Command is incomplete - missing parameters"
68
+ description="Command is incomplete - missing parameters",
62
69
  ),
63
70
  ErrorPattern(
64
71
  pattern=r"% Unknown command or computer name, or unable to find computer address",
65
72
  error_type=ErrorType.COMMAND_NOT_FOUND,
66
73
  vendor="cisco_ios",
67
- description="Command not recognized"
74
+ description="Command not recognized",
68
75
  ),
69
-
70
76
  # Cisco NX-OS Syntax Errors
71
77
  ErrorPattern(
72
78
  pattern=r"% Invalid command at '\^' marker\.",
73
79
  error_type=ErrorType.SYNTAX_ERROR,
74
80
  vendor="cisco_nxos",
75
- description="Invalid command syntax"
81
+ description="Invalid command syntax",
76
82
  ),
77
83
  ErrorPattern(
78
84
  pattern=r"% Invalid parameter detected at '\^' marker\.",
79
85
  error_type=ErrorType.SYNTAX_ERROR,
80
86
  vendor="cisco_nxos",
81
- description="Invalid parameter in command"
87
+ description="Invalid parameter in command",
82
88
  ),
83
89
  ErrorPattern(
84
90
  pattern=r"% Command incomplete\.",
85
91
  error_type=ErrorType.SYNTAX_ERROR,
86
92
  vendor="cisco_nxos",
87
- description="Command requires additional parameters"
93
+ description="Command requires additional parameters",
88
94
  ),
89
-
90
95
  # Cisco IOS-XR Syntax Errors
91
96
  ErrorPattern(
92
97
  pattern=r"% Invalid input detected at '\^' marker\.",
93
98
  error_type=ErrorType.SYNTAX_ERROR,
94
99
  vendor="cisco_iosxr",
95
- description="Invalid command syntax"
100
+ description="Invalid command syntax",
96
101
  ),
97
102
  ErrorPattern(
98
103
  pattern=r"% Incomplete command\.",
99
104
  error_type=ErrorType.SYNTAX_ERROR,
100
105
  vendor="cisco_iosxr",
101
- description="Command is incomplete"
106
+ description="Command is incomplete",
102
107
  ),
103
-
104
108
  # Juniper Junos Syntax Errors
105
109
  ErrorPattern(
106
110
  pattern=r"syntax error, expecting <[\w\-]+>",
107
111
  error_type=ErrorType.SYNTAX_ERROR,
108
112
  vendor="juniper_junos",
109
- description="Syntax error - expected different keyword"
113
+ description="Syntax error - expected different keyword",
110
114
  ),
111
115
  ErrorPattern(
112
116
  pattern=r"syntax error\.",
113
117
  error_type=ErrorType.SYNTAX_ERROR,
114
118
  vendor="juniper_junos",
115
- description="General syntax error"
119
+ description="General syntax error",
116
120
  ),
117
121
  ErrorPattern(
118
122
  pattern=r"unknown command\.",
119
123
  error_type=ErrorType.COMMAND_NOT_FOUND,
120
124
  vendor="juniper_junos",
121
- description="Command not recognized"
125
+ description="Command not recognized",
122
126
  ),
123
127
  ErrorPattern(
124
128
  pattern=r"error: .*",
125
129
  error_type=ErrorType.UNKNOWN_ERROR,
126
130
  vendor="juniper_junos",
127
- description="General error from device"
131
+ description="General error from device",
128
132
  ),
129
-
130
133
  # Arista EOS Syntax Errors
131
134
  ErrorPattern(
132
135
  pattern=r"% Invalid input",
133
136
  error_type=ErrorType.SYNTAX_ERROR,
134
137
  vendor="arista_eos",
135
- description="Invalid command input"
138
+ description="Invalid command input",
136
139
  ),
137
140
  ErrorPattern(
138
141
  pattern=r"% Incomplete command",
139
142
  error_type=ErrorType.SYNTAX_ERROR,
140
143
  vendor="arista_eos",
141
- description="Command requires additional parameters"
144
+ description="Command requires additional parameters",
142
145
  ),
143
146
  ErrorPattern(
144
147
  pattern=r"% Ambiguous command",
145
148
  error_type=ErrorType.SYNTAX_ERROR,
146
149
  vendor="arista_eos",
147
- description="Command is ambiguous"
150
+ description="Command is ambiguous",
148
151
  ),
149
-
150
152
  # HPE/Aruba Syntax Errors
151
153
  ErrorPattern(
152
154
  pattern=r"Invalid input:",
153
155
  error_type=ErrorType.SYNTAX_ERROR,
154
156
  vendor="aruba",
155
- description="Invalid command syntax"
157
+ description="Invalid command syntax",
156
158
  ),
157
159
  ErrorPattern(
158
160
  pattern=r"Unknown command\.",
159
161
  error_type=ErrorType.COMMAND_NOT_FOUND,
160
162
  vendor="aruba",
161
- description="Command not recognized"
163
+ description="Command not recognized",
162
164
  ),
163
-
164
165
  # Huawei Syntax Errors
165
166
  ErrorPattern(
166
167
  pattern=r"Error: Unrecognized command found at '\^' position\.",
167
168
  error_type=ErrorType.SYNTAX_ERROR,
168
169
  vendor="huawei",
169
- description="Unrecognized command syntax"
170
+ description="Unrecognized command syntax",
170
171
  ),
171
172
  ErrorPattern(
172
173
  pattern=r"Error: Incomplete command found at '\^' position\.",
173
174
  error_type=ErrorType.SYNTAX_ERROR,
174
175
  vendor="huawei",
175
- description="Incomplete command"
176
+ description="Incomplete command",
176
177
  ),
177
-
178
178
  # Fortinet FortiOS Syntax Errors
179
179
  ErrorPattern(
180
180
  pattern=r"command parse error before",
181
181
  error_type=ErrorType.SYNTAX_ERROR,
182
182
  vendor="fortinet",
183
- description="Command parsing error"
183
+ description="Command parsing error",
184
184
  ),
185
185
  ErrorPattern(
186
186
  pattern=r"Unknown action",
187
187
  error_type=ErrorType.COMMAND_NOT_FOUND,
188
188
  vendor="fortinet",
189
- description="Unknown command or action"
189
+ description="Unknown command or action",
190
190
  ),
191
-
192
191
  # Palo Alto PAN-OS Syntax Errors
193
192
  ErrorPattern(
194
193
  pattern=r"Invalid syntax\.",
195
194
  error_type=ErrorType.SYNTAX_ERROR,
196
195
  vendor="paloalto",
197
- description="Invalid command syntax"
196
+ description="Invalid command syntax",
198
197
  ),
199
198
  ErrorPattern(
200
199
  pattern=r"Unknown command:",
201
200
  error_type=ErrorType.COMMAND_NOT_FOUND,
202
201
  vendor="paloalto",
203
- description="Command not recognized"
202
+ description="Command not recognized",
204
203
  ),
205
-
206
204
  # Generic Permission Errors (across vendors)
207
205
  ErrorPattern(
208
206
  pattern=r"Permission denied",
209
207
  error_type=ErrorType.PERMISSION_ERROR,
210
208
  vendor="generic",
211
- description="Insufficient permissions for command"
209
+ description="Insufficient permissions for command",
212
210
  ),
213
211
  ErrorPattern(
214
212
  pattern=r"Access denied",
215
213
  error_type=ErrorType.PERMISSION_ERROR,
216
214
  vendor="generic",
217
- description="Access denied for command"
215
+ description="Access denied for command",
218
216
  ),
219
217
  ErrorPattern(
220
218
  pattern=r"Insufficient privileges",
221
219
  error_type=ErrorType.PERMISSION_ERROR,
222
220
  vendor="generic",
223
- description="User lacks required privileges"
221
+ description="User lacks required privileges",
224
222
  ),
225
-
226
223
  # Generic Command Not Found Errors
227
224
  ErrorPattern(
228
225
  pattern=r"command not found",
229
226
  error_type=ErrorType.COMMAND_NOT_FOUND,
230
227
  vendor="generic",
231
228
  case_sensitive=False,
232
- description="Command not found on system"
229
+ description="Command not found on system",
233
230
  ),
234
231
  ErrorPattern(
235
232
  pattern=r"bad command or file name",
236
233
  error_type=ErrorType.COMMAND_NOT_FOUND,
237
234
  vendor="generic",
238
235
  case_sensitive=False,
239
- description="Command not recognized"
236
+ description="Command not recognized",
240
237
  ),
241
238
  ]
242
-
239
+
243
240
  return patterns
244
-
245
- def parse_command_output(self, output: str, device_platform: Optional[str] = None) -> Optional[ParsedError]:
241
+
242
+ def parse_command_output(
243
+ self, output: str, device_platform: str | None = None
244
+ ) -> ParsedError | None:
246
245
  """
247
246
  Parse command output to detect vendor-specific errors.
248
-
247
+
249
248
  Args:
250
249
  output: The command output to analyze
251
250
  device_platform: Optional platform hint to prioritize certain patterns
252
-
251
+
253
252
  Returns:
254
253
  ParsedError if an error is detected, None otherwise
255
254
  """
256
255
  if not output or not output.strip():
257
256
  return None
258
-
257
+
259
258
  # Normalize the output for consistent parsing
260
- output_lines = output.strip().split('\n')
261
-
259
+ output_lines = output.strip().split("\n")
260
+
262
261
  # Check each line for error patterns
263
262
  best_match = None
264
263
  highest_confidence = 0.0
265
-
264
+
266
265
  for line in output_lines:
267
266
  line = line.strip()
268
267
  if not line:
269
268
  continue
270
-
269
+
271
270
  for pattern in self.error_patterns:
272
271
  match = self._match_pattern(line, pattern, device_platform)
273
272
  if match and match.confidence > highest_confidence:
274
273
  best_match = match
275
274
  highest_confidence = match.confidence
276
-
275
+
277
276
  return best_match
278
-
279
- def _match_pattern(self, line: str, pattern: ErrorPattern, device_platform: Optional[str] = None) -> Optional[ParsedError]:
277
+
278
+ def _match_pattern(
279
+ self, line: str, pattern: ErrorPattern, device_platform: str | None = None
280
+ ) -> ParsedError | None:
280
281
  """Check if a line matches an error pattern."""
281
282
  flags = 0 if pattern.case_sensitive else re.IGNORECASE
282
-
283
+
283
284
  if re.search(pattern.pattern, line, flags):
284
285
  # Calculate confidence based on vendor match and pattern specificity
285
286
  confidence = 0.7 # Base confidence
286
-
287
+
287
288
  # Boost confidence if vendor matches device platform
288
- if device_platform and self._platforms_match(pattern.vendor, device_platform):
289
+ if device_platform and self._platforms_match(
290
+ pattern.vendor, device_platform
291
+ ):
289
292
  confidence += 0.2
290
-
293
+
291
294
  # Boost confidence for more specific patterns
292
295
  if len(pattern.pattern) > 20: # Longer patterns are typically more specific
293
296
  confidence += 0.1
294
-
297
+
295
298
  # Cap confidence at 1.0
296
299
  confidence = min(confidence, 1.0)
297
-
300
+
298
301
  enhanced_message = self._enhance_error_message(line, pattern)
299
302
  guidance = self._get_error_guidance(pattern, line)
300
-
303
+
301
304
  return ParsedError(
302
305
  error_type=pattern.error_type,
303
306
  vendor=pattern.vendor,
304
307
  original_message=line,
305
308
  enhanced_message=enhanced_message,
306
309
  guidance=guidance,
307
- confidence=confidence
310
+ confidence=confidence,
308
311
  )
309
-
312
+
310
313
  return None
311
-
314
+
312
315
  def _platforms_match(self, pattern_vendor: str, device_platform: str) -> bool:
313
316
  """Check if pattern vendor matches device platform."""
314
317
  if not device_platform:
315
318
  return False
316
-
319
+
317
320
  device_platform = device_platform.lower().strip()
318
321
  pattern_vendor = pattern_vendor.lower().strip()
319
-
322
+
320
323
  # Direct match
321
324
  if pattern_vendor == device_platform:
322
325
  return True
323
-
326
+
324
327
  # 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
-
328
+ cisco_platforms = [
329
+ "cisco_ios",
330
+ "cisco_nxos",
331
+ "cisco_iosxr",
332
+ "ios",
333
+ "nxos",
334
+ "iosxr",
335
+ ]
336
+ juniper_platforms = ["juniper_junos", "junos"]
337
+ arista_platforms = ["arista_eos", "eos"]
338
+
329
339
  if pattern_vendor in cisco_platforms and device_platform in cisco_platforms:
330
340
  return True
331
341
  if pattern_vendor in juniper_platforms and device_platform in juniper_platforms:
332
342
  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:
343
+ return bool(pattern_vendor in arista_platforms and device_platform in arista_platforms)
344
+
345
+ def _enhance_error_message(
346
+ self, original_message: str, pattern: ErrorPattern
347
+ ) -> str:
339
348
  """Enhance the original error message with additional context."""
340
349
  vendor_name = self._get_vendor_display_name(pattern.vendor)
341
-
350
+
342
351
  enhanced = f"[{vendor_name}] {original_message}"
343
-
352
+
344
353
  if pattern.description:
345
354
  enhanced += f"\n\nDescription: {pattern.description}"
346
-
355
+
347
356
  return enhanced
348
-
357
+
349
358
  def _get_vendor_display_name(self, vendor: str) -> str:
350
359
  """Get a human-readable vendor name."""
351
360
  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'
361
+ "cisco_ios": "Cisco IOS/IOS-XE",
362
+ "cisco_nxos": "Cisco NX-OS",
363
+ "cisco_iosxr": "Cisco IOS-XR",
364
+ "juniper_junos": "Juniper Junos",
365
+ "arista_eos": "Arista EOS",
366
+ "aruba": "HPE Aruba",
367
+ "huawei": "Huawei",
368
+ "fortinet": "Fortinet FortiOS",
369
+ "paloalto": "Palo Alto PAN-OS",
370
+ "generic": "Generic",
362
371
  }
363
-
372
+
364
373
  return vendor_map.get(vendor, vendor.title())
365
-
374
+
366
375
  def _get_error_guidance(self, pattern: ErrorPattern, original_message: str) -> str:
367
376
  """Get vendor-specific guidance for resolving the error."""
368
377
  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),
378
+ ErrorType.SYNTAX_ERROR: self._get_syntax_error_guidance(
379
+ pattern.vendor, original_message
380
+ ),
381
+ ErrorType.PERMISSION_ERROR: self._get_permission_error_guidance(
382
+ pattern.vendor
383
+ ),
384
+ ErrorType.COMMAND_NOT_FOUND: self._get_command_not_found_guidance(
385
+ pattern.vendor
386
+ ),
387
+ ErrorType.CONFIGURATION_ERROR: self._get_configuration_error_guidance(
388
+ pattern.vendor
389
+ ),
373
390
  ErrorType.TIMEOUT_ERROR: self._get_timeout_error_guidance(pattern.vendor),
374
- ErrorType.UNKNOWN_ERROR: self._get_unknown_error_guidance(pattern.vendor)
391
+ ErrorType.UNKNOWN_ERROR: self._get_unknown_error_guidance(pattern.vendor),
375
392
  }
376
-
377
- return base_guidance.get(pattern.error_type, "Check device documentation for error details.")
378
-
393
+
394
+ return base_guidance.get(
395
+ pattern.error_type, "Check device documentation for error details."
396
+ )
397
+
379
398
  def _get_syntax_error_guidance(self, vendor: str, message: str) -> str:
380
399
  """Get syntax error guidance based on vendor."""
381
400
  common_guidance = (
@@ -383,9 +402,9 @@ class VendorErrorParser:
383
402
  "• Verify the command syntax is correct for this device platform\n"
384
403
  "• Ensure command has correct mode set (e.g., show or config)\n"
385
404
  )
386
-
405
+
387
406
  return common_guidance
388
-
407
+
389
408
  def _get_permission_error_guidance(self, vendor: str) -> str:
390
409
  """Get permission error guidance."""
391
410
  return (
@@ -393,7 +412,7 @@ class VendorErrorParser:
393
412
  "• Verify your user account has the necessary privileges\n"
394
413
  "• Review device AAA configuration for command authorization\n"
395
414
  )
396
-
415
+
397
416
  def _get_command_not_found_guidance(self, vendor: str) -> str:
398
417
  """Get command not found guidance."""
399
418
  return (
@@ -402,21 +421,21 @@ class VendorErrorParser:
402
421
  "• Check the command spelling and syntax\n"
403
422
  "• Try the command manually on device\n"
404
423
  )
405
-
424
+
406
425
  def _get_configuration_error_guidance(self, vendor: str) -> str:
407
426
  """Get configuration error guidance."""
408
427
  return (
409
428
  "Configuration Error Troubleshooting:\n"
410
429
  "• Review the configuration syntax for this platform\n"
411
430
  )
412
-
431
+
413
432
  def _get_timeout_error_guidance(self, vendor: str) -> str:
414
433
  """Get timeout error guidance."""
415
434
  return (
416
435
  "Timeout Error Troubleshooting:\n"
417
436
  "• The command may be taking longer than expected\n"
418
437
  )
419
-
438
+
420
439
  def _get_unknown_error_guidance(self, vendor: str) -> str:
421
440
  """Get unknown error guidance."""
422
441
  return (
@@ -1,6 +1,7 @@
1
1
  """Logging utilities for NetBox Toolkit plugin."""
2
2
 
3
3
  import logging
4
+
4
5
  from django.conf import settings
5
6
 
6
7