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.
- netbox_toolkit_plugin/__init__.py +32 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/serializers.py +71 -35
- {netbox_toolkit → netbox_toolkit_plugin}/api/urls.py +3 -3
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/factory.py +170 -111
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/netmiko_connector.py +242 -179
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/scrapli_connector.py +256 -172
- netbox_toolkit_plugin/migrations/0001_initial.py +108 -0
- netbox_toolkit_plugin/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +70 -0
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0003_permission_system_update.py +26 -12
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0004_remove_django_permissions.py +27 -29
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0005_alter_command_options_and_more.py +7 -8
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +7 -8
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0007_alter_commandlog_parsing_template.py +6 -4
- {netbox_toolkit → netbox_toolkit_plugin}/models.py +31 -32
- {netbox_toolkit → netbox_toolkit_plugin}/navigation.py +6 -6
- {netbox_toolkit → netbox_toolkit_plugin}/services/command_service.py +188 -128
- {netbox_toolkit → netbox_toolkit_plugin}/services/rate_limiting_service.py +104 -97
- netbox_toolkit_plugin/settings.py +176 -0
- netbox_toolkit_plugin/tables.py +51 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/command.html +108 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/command_list.html +12 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/commandlog.html +170 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/device_toolkit.html +557 -0
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command.html +5 -5
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_list.html +2 -2
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog.html +2 -2
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/device_toolkit.html +6 -6
- netbox_toolkit_plugin/urls.py +38 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/logging.py +20 -19
- {netbox_toolkit → netbox_toolkit_plugin}/views.py +251 -169
- {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.2.dist-info}/METADATA +2 -2
- netbox_toolkit_plugin-0.1.2.dist-info/RECORD +60 -0
- netbox_toolkit_plugin-0.1.2.dist-info/entry_points.txt +2 -0
- netbox_toolkit_plugin-0.1.2.dist-info/top_level.txt +1 -0
- netbox_toolkit/__init__.py +0 -30
- netbox_toolkit/config.py +0 -159
- netbox_toolkit/migrations/0001_initial.py +0 -54
- netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +0 -66
- netbox_toolkit/tables.py +0 -37
- netbox_toolkit/urls.py +0 -22
- netbox_toolkit_plugin-0.1.0.dist-info/RECORD +0 -56
- netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +0 -2
- netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +0 -1
- {netbox_toolkit → netbox_toolkit_plugin}/admin.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/mixins.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/schemas.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/views/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/views/command_logs.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/views/commands.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/base.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/exceptions.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/filtersets.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/forms.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/search.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/services/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/services/device_service.py +0 -0
- {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/css/toolkit.css +0 -0
- {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/js/toolkit.js +0 -0
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_edit.html +0 -0
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog_list.html +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/connection.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/error_parser.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/network.py +0 -0
- {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.2.dist-info}/WHEEL +0 -0
- {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
|
"""Service for handling command execution on devices."""
|
2
|
+
|
2
3
|
import traceback
|
3
4
|
from typing import Optional, Any
|
4
5
|
|
@@ -9,7 +10,7 @@ from ..connectors.factory import ConnectorFactory
|
|
9
10
|
from ..connectors.base import CommandResult
|
10
11
|
from ..connectors.netmiko_connector import NetmikoConnector
|
11
12
|
from ..exceptions import DeviceConnectionError, CommandExecutionError
|
12
|
-
from ..
|
13
|
+
from ..settings import ToolkitSettings
|
13
14
|
from ..utils.logging import get_toolkit_logger
|
14
15
|
|
15
16
|
logger = get_toolkit_logger(__name__)
|
@@ -17,166 +18,201 @@ logger = get_toolkit_logger(__name__)
|
|
17
18
|
|
18
19
|
class CommandExecutionService:
|
19
20
|
"""Service for executing commands on devices."""
|
20
|
-
|
21
|
+
|
21
22
|
def __init__(self):
|
22
23
|
self.connector_factory = ConnectorFactory()
|
23
|
-
|
24
|
+
|
24
25
|
def execute_command_with_retry(
|
25
26
|
self,
|
26
27
|
command: "PredefinedCommand",
|
27
28
|
device: Any,
|
28
29
|
username: str,
|
29
30
|
password: str,
|
30
|
-
max_retries: int = 1
|
31
|
+
max_retries: int = 1,
|
31
32
|
) -> "CommandResult":
|
32
33
|
"""
|
33
34
|
Execute a command with connection retry capability.
|
34
|
-
|
35
|
+
|
35
36
|
Args:
|
36
37
|
command: Command to execute
|
37
38
|
device: Target device
|
38
39
|
username: Authentication username
|
39
40
|
password: Authentication password
|
40
41
|
max_retries: Maximum number of retry attempts
|
41
|
-
|
42
|
+
|
42
43
|
Returns:
|
43
44
|
CommandResult with execution details
|
44
45
|
"""
|
45
46
|
last_error = None
|
46
|
-
|
47
|
-
logger.info(
|
48
|
-
|
49
|
-
|
47
|
+
|
48
|
+
logger.info(
|
49
|
+
"Executing command '%s' on device %s (max_retries=%d)",
|
50
|
+
command.name,
|
51
|
+
device.name,
|
52
|
+
max_retries,
|
53
|
+
)
|
54
|
+
|
50
55
|
for attempt in range(max_retries + 1):
|
51
56
|
try:
|
52
|
-
logger.debug(
|
53
|
-
|
57
|
+
logger.debug(
|
58
|
+
"Attempt %d/%d for command execution", attempt + 1, max_retries + 1
|
59
|
+
)
|
60
|
+
|
54
61
|
# Create appropriate connector for the device
|
55
|
-
connector = self.connector_factory.create_connector(
|
56
|
-
|
57
|
-
|
58
|
-
|
62
|
+
connector = self.connector_factory.create_connector(
|
63
|
+
device, username, password
|
64
|
+
)
|
65
|
+
logger.debug(
|
66
|
+
"Created %s connector for device %s",
|
67
|
+
type(connector).__name__,
|
68
|
+
device.name,
|
69
|
+
)
|
70
|
+
|
59
71
|
# Execute command using context manager for proper cleanup
|
60
72
|
with connector:
|
61
|
-
result = connector.execute_command(
|
62
|
-
|
63
|
-
|
64
|
-
|
73
|
+
result = connector.execute_command(
|
74
|
+
command.command, command.command_type
|
75
|
+
)
|
76
|
+
logger.debug(
|
77
|
+
"Command executed successfully, output length: %d chars",
|
78
|
+
len(result.output) if result.output else 0,
|
79
|
+
)
|
80
|
+
|
65
81
|
# If successful, log and return
|
66
|
-
logger.info(
|
82
|
+
logger.info(
|
83
|
+
"Command execution completed successfully on %s", device.name
|
84
|
+
)
|
67
85
|
self._log_command_execution(command, device, result, username)
|
68
86
|
return result
|
69
|
-
|
87
|
+
|
70
88
|
except Exception as e:
|
71
89
|
last_error = e
|
72
90
|
error_msg = str(e)
|
73
|
-
logger.warning(
|
74
|
-
|
91
|
+
logger.warning(
|
92
|
+
"Command execution attempt %d failed: %s", attempt + 1, error_msg
|
93
|
+
)
|
94
|
+
|
75
95
|
# Check for fast-fail scenario and automatically retry with Netmiko
|
76
|
-
if (
|
77
|
-
|
78
|
-
|
96
|
+
if (
|
97
|
+
"Fast-fail to Netmiko" in error_msg
|
98
|
+
or ToolkitSettings.should_fast_fail_to_netmiko(error_msg)
|
99
|
+
):
|
100
|
+
logger.info(
|
101
|
+
"Fast-fail pattern detected, attempting fallback to Netmiko for device %s",
|
102
|
+
device.name,
|
103
|
+
)
|
79
104
|
try:
|
80
105
|
# Create Netmiko connector directly for fallback
|
81
|
-
base_config = self.connector_factory._build_connection_config(
|
82
|
-
|
106
|
+
base_config = self.connector_factory._build_connection_config(
|
107
|
+
device, username, password
|
108
|
+
)
|
109
|
+
netmiko_config = (
|
110
|
+
self.connector_factory._prepare_connector_config(
|
111
|
+
base_config, NetmikoConnector
|
112
|
+
)
|
113
|
+
)
|
83
114
|
fallback_connector = NetmikoConnector(netmiko_config)
|
84
|
-
|
115
|
+
|
85
116
|
# Execute command using Netmiko fallback connector
|
86
117
|
with fallback_connector:
|
87
|
-
result = fallback_connector.execute_command(
|
88
|
-
|
89
|
-
|
118
|
+
result = fallback_connector.execute_command(
|
119
|
+
command.command, command.command_type
|
120
|
+
)
|
121
|
+
logger.info(
|
122
|
+
"Command executed successfully using Netmiko fallback on %s",
|
123
|
+
device.name,
|
124
|
+
)
|
125
|
+
self._log_command_execution(
|
126
|
+
command, device, result, username
|
127
|
+
)
|
90
128
|
return result
|
91
|
-
|
129
|
+
|
92
130
|
except Exception as fallback_error:
|
93
|
-
logger.warning(
|
131
|
+
logger.warning(
|
132
|
+
"Netmiko fallback also failed for device %s: %s",
|
133
|
+
device.name,
|
134
|
+
str(fallback_error),
|
135
|
+
)
|
94
136
|
last_error = fallback_error
|
95
137
|
break # Don't retry after fallback failure
|
96
|
-
|
138
|
+
|
97
139
|
# If this was a socket/connection error and we have retries left, continue
|
98
|
-
elif
|
99
|
-
|
100
|
-
|
101
|
-
|
140
|
+
elif attempt < max_retries and (
|
141
|
+
"socket" in error_msg.lower()
|
142
|
+
or "connection" in error_msg.lower()
|
143
|
+
or "Bad file descriptor" in error_msg
|
144
|
+
):
|
102
145
|
logger.debug("Connection error detected, will retry")
|
103
146
|
continue
|
104
147
|
else:
|
105
148
|
logger.error("Max retries reached or non-retryable error")
|
106
149
|
break
|
107
|
-
|
150
|
+
|
108
151
|
# All attempts failed, create error result
|
109
152
|
logger.error("All command execution attempts failed for device %s", device.name)
|
110
153
|
error_result = CommandResult(
|
111
154
|
command=command.command,
|
112
155
|
output="",
|
113
156
|
success=False,
|
114
|
-
error_message=str(last_error)
|
157
|
+
error_message=str(last_error),
|
115
158
|
)
|
116
|
-
|
159
|
+
|
117
160
|
# Add detailed error information
|
118
161
|
error_result = self._enhance_error_result(error_result, last_error, device)
|
119
|
-
|
162
|
+
|
120
163
|
# Log the failed execution
|
121
164
|
self._log_command_execution(command, device, error_result, username)
|
122
|
-
|
165
|
+
|
123
166
|
return error_result
|
124
167
|
|
125
168
|
def execute_command(
|
126
|
-
self,
|
127
|
-
command: Command,
|
128
|
-
device: Device,
|
129
|
-
username: str,
|
130
|
-
password: str
|
169
|
+
self, command: Command, device: Device, username: str, password: str
|
131
170
|
) -> CommandResult:
|
132
171
|
"""
|
133
172
|
Execute a command on a device and log the result.
|
134
|
-
|
173
|
+
|
135
174
|
Args:
|
136
175
|
command: Command to execute
|
137
176
|
device: Target device
|
138
177
|
username: Authentication username
|
139
178
|
password: Authentication password
|
140
|
-
|
179
|
+
|
141
180
|
Returns:
|
142
181
|
CommandResult with execution details
|
143
182
|
"""
|
144
183
|
try:
|
145
184
|
# Create appropriate connector for the device
|
146
|
-
connector = self.connector_factory.create_connector(
|
147
|
-
|
185
|
+
connector = self.connector_factory.create_connector(
|
186
|
+
device, username, password
|
187
|
+
)
|
188
|
+
|
148
189
|
# Execute command using context manager for proper cleanup
|
149
190
|
with connector:
|
150
|
-
result = connector.execute_command(
|
151
|
-
|
191
|
+
result = connector.execute_command(
|
192
|
+
command.command, command.command_type
|
193
|
+
)
|
194
|
+
|
152
195
|
# Log the execution
|
153
196
|
self._log_command_execution(command, device, result, username)
|
154
|
-
|
197
|
+
|
155
198
|
return result
|
156
|
-
|
199
|
+
|
157
200
|
except Exception as e:
|
158
201
|
# Create error result
|
159
202
|
error_result = CommandResult(
|
160
|
-
command=command.command,
|
161
|
-
output="",
|
162
|
-
success=False,
|
163
|
-
error_message=str(e)
|
203
|
+
command=command.command, output="", success=False, error_message=str(e)
|
164
204
|
)
|
165
|
-
|
205
|
+
|
166
206
|
# Add detailed error information
|
167
207
|
error_result = self._enhance_error_result(error_result, e, device)
|
168
|
-
|
208
|
+
|
169
209
|
# Log the failed execution
|
170
210
|
self._log_command_execution(command, device, error_result, username)
|
171
|
-
|
211
|
+
|
172
212
|
return error_result
|
173
|
-
|
213
|
+
|
174
214
|
def _log_command_execution(
|
175
|
-
self,
|
176
|
-
command: Command,
|
177
|
-
device: Device,
|
178
|
-
result: CommandResult,
|
179
|
-
username: str
|
215
|
+
self, command: Command, device: Device, result: CommandResult, username: str
|
180
216
|
) -> CommandLog:
|
181
217
|
"""Log command execution to database."""
|
182
218
|
if result.success:
|
@@ -193,8 +229,8 @@ class CommandExecutionService:
|
|
193
229
|
if result.output:
|
194
230
|
output += f"\n\nOutput: {result.output}"
|
195
231
|
success = False
|
196
|
-
error_message = result.error_message or
|
197
|
-
|
232
|
+
error_message = result.error_message or ""
|
233
|
+
|
198
234
|
# Create log entry with execution details
|
199
235
|
command_log = CommandLog.objects.create(
|
200
236
|
command=command,
|
@@ -206,28 +242,32 @@ class CommandExecutionService:
|
|
206
242
|
execution_duration=result.execution_time,
|
207
243
|
parsed_data=result.parsed_output,
|
208
244
|
parsing_success=result.parsing_success,
|
209
|
-
parsing_template=result.parsing_method
|
245
|
+
parsing_template=result.parsing_method,
|
210
246
|
)
|
211
|
-
|
247
|
+
|
212
248
|
if result.has_syntax_error:
|
213
249
|
pass # Syntax error detected but not logging
|
214
250
|
else:
|
215
251
|
pass # Command executed successfully but not logging
|
216
|
-
|
252
|
+
|
217
253
|
return command_log
|
218
|
-
|
219
|
-
def _enhance_error_result(
|
254
|
+
|
255
|
+
def _enhance_error_result(
|
256
|
+
self, result: CommandResult, error: Exception, device: Device
|
257
|
+
) -> CommandResult:
|
220
258
|
"""Enhance error result with detailed troubleshooting information."""
|
221
259
|
error_message = str(error)
|
222
260
|
error_details = traceback.format_exc()
|
223
|
-
|
261
|
+
|
224
262
|
enhanced_output = f"Error executing command: {error_message}"
|
225
|
-
|
263
|
+
|
226
264
|
# Add specific guidance for common errors
|
227
265
|
guidance_added = False
|
228
|
-
|
266
|
+
|
229
267
|
if isinstance(error, DeviceConnectionError):
|
230
|
-
enhanced_output += self._get_connection_error_guidance(
|
268
|
+
enhanced_output += self._get_connection_error_guidance(
|
269
|
+
error_message, device
|
270
|
+
)
|
231
271
|
guidance_added = True
|
232
272
|
elif "Bad file descriptor" in error_details:
|
233
273
|
enhanced_output += self._get_bad_descriptor_guidance(device)
|
@@ -235,17 +275,28 @@ class CommandExecutionService:
|
|
235
275
|
elif "Error reading SSH protocol banner" in error_details:
|
236
276
|
enhanced_output += self._get_banner_error_guidance(device)
|
237
277
|
guidance_added = True
|
238
|
-
|
278
|
+
|
239
279
|
# Check for connection/authentication errors in the error message even if not DeviceConnectionError
|
240
280
|
if not guidance_added:
|
241
281
|
error_lower = error_message.lower()
|
242
|
-
if any(
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
282
|
+
if any(
|
283
|
+
error_term in error_lower
|
284
|
+
for error_term in [
|
285
|
+
"connect",
|
286
|
+
"connection",
|
287
|
+
"authentication",
|
288
|
+
"failed to connect",
|
289
|
+
"ssh",
|
290
|
+
"timeout",
|
291
|
+
"unreachable",
|
292
|
+
"refused",
|
293
|
+
]
|
294
|
+
):
|
295
|
+
enhanced_output += self._get_connection_error_guidance(
|
296
|
+
error_message, device
|
297
|
+
)
|
247
298
|
guidance_added = True
|
248
|
-
|
299
|
+
|
249
300
|
# Add general troubleshooting if no specific guidance was provided
|
250
301
|
if not guidance_added:
|
251
302
|
enhanced_output += (
|
@@ -254,61 +305,66 @@ class CommandExecutionService:
|
|
254
305
|
"\n- Check credentials and device configuration"
|
255
306
|
"\n- Review the debug information below for more details"
|
256
307
|
)
|
257
|
-
|
308
|
+
|
258
309
|
enhanced_output += f"\n\nDebug information:\n{error_details}"
|
259
|
-
|
310
|
+
|
260
311
|
return CommandResult(
|
261
312
|
command=result.command,
|
262
313
|
output=enhanced_output,
|
263
314
|
success=False,
|
264
315
|
error_message=result.error_message,
|
265
|
-
execution_time=result.execution_time
|
316
|
+
execution_time=result.execution_time,
|
266
317
|
)
|
267
|
-
|
318
|
+
|
268
319
|
def _get_connection_error_guidance(self, error_message: str, device: Device) -> str:
|
269
320
|
"""Get guidance for connection errors."""
|
270
|
-
hostname =
|
271
|
-
|
321
|
+
hostname = (
|
322
|
+
str(device.primary_ip.address.ip) if device.primary_ip else device.name
|
323
|
+
)
|
324
|
+
|
272
325
|
guidance = "\n\nConnection Error Troubleshooting:"
|
273
|
-
|
326
|
+
|
274
327
|
# Convert to lowercase for case-insensitive matching
|
275
328
|
error_lower = error_message.lower()
|
276
|
-
|
329
|
+
|
277
330
|
if "no matching key exchange" in error_lower:
|
278
|
-
guidance +=
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
331
|
+
guidance += "\n- This is an SSH key exchange error"
|
332
|
+
elif any(
|
333
|
+
conn_error in error_lower
|
334
|
+
for conn_error in [
|
335
|
+
"connection not opened",
|
336
|
+
"connection refused",
|
337
|
+
"connection timed out",
|
338
|
+
"network is unreachable",
|
339
|
+
"no route to host",
|
340
|
+
]
|
341
|
+
):
|
288
342
|
guidance += (
|
289
343
|
"\n- Verify the device is reachable on the network"
|
290
344
|
"\n- Check that SSH service is running on the device"
|
291
345
|
"\n- Verify there's no firewall blocking the connection"
|
292
346
|
"\n- Ensure the correct NetBox has correct device details (IP, Hostname)"
|
293
347
|
)
|
294
|
-
elif any(
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
348
|
+
elif any(
|
349
|
+
auth_error in error_lower
|
350
|
+
for auth_error in [
|
351
|
+
"authentication failed",
|
352
|
+
"all authentication methods failed",
|
353
|
+
"permission denied",
|
354
|
+
"invalid user",
|
355
|
+
"login incorrect",
|
356
|
+
"authentication error",
|
357
|
+
]
|
358
|
+
):
|
302
359
|
guidance += (
|
303
360
|
"\n- Verify username and password are correct"
|
304
361
|
"\n- Ensure the user has SSH access permissions on the device"
|
305
362
|
"\n- Check if the device requires specific authentication methods"
|
306
363
|
)
|
307
|
-
elif any(
|
308
|
-
|
309
|
-
"timed out",
|
310
|
-
|
311
|
-
]):
|
364
|
+
elif any(
|
365
|
+
timeout_error in error_lower
|
366
|
+
for timeout_error in ["timeout", "timed out", "operation timed out"]
|
367
|
+
):
|
312
368
|
guidance += (
|
313
369
|
"\n- The connection or operation timed out"
|
314
370
|
"\n- Check network connectivity to the device"
|
@@ -322,15 +378,17 @@ class CommandExecutionService:
|
|
322
378
|
"\n- Verify network connectivity and firewall settings"
|
323
379
|
"\n- Ensure your credentials are correct"
|
324
380
|
)
|
325
|
-
|
381
|
+
|
326
382
|
guidance += f"\n- Try connecting manually: ssh {hostname}"
|
327
|
-
|
383
|
+
|
328
384
|
return guidance
|
329
|
-
|
385
|
+
|
330
386
|
def _get_bad_descriptor_guidance(self, device: Device) -> str:
|
331
387
|
"""Get guidance for 'Bad file descriptor' errors."""
|
332
|
-
hostname =
|
333
|
-
|
388
|
+
hostname = (
|
389
|
+
str(device.primary_ip.address.ip) if device.primary_ip else device.name
|
390
|
+
)
|
391
|
+
|
334
392
|
return (
|
335
393
|
"\n\n'Bad file descriptor' Error Guidance:"
|
336
394
|
"\n- This often indicates network connectivity issues"
|
@@ -339,11 +397,13 @@ class CommandExecutionService:
|
|
339
397
|
"\n- Confirm SSH service is running on the device"
|
340
398
|
f"\n- Try connecting manually: ssh {hostname}"
|
341
399
|
)
|
342
|
-
|
400
|
+
|
343
401
|
def _get_banner_error_guidance(self, device: Device) -> str:
|
344
402
|
"""Get guidance for SSH banner errors."""
|
345
|
-
hostname =
|
346
|
-
|
403
|
+
hostname = (
|
404
|
+
str(device.primary_ip.address.ip) if device.primary_ip else device.name
|
405
|
+
)
|
406
|
+
|
347
407
|
return (
|
348
408
|
"\n\nSSH Banner Error Guidance:"
|
349
409
|
"\n- The device accepts connections but doesn't provide an SSH banner"
|