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.
- netbox_toolkit_plugin/__init__.py +1 -1
- netbox_toolkit_plugin/admin.py +11 -7
- netbox_toolkit_plugin/api/mixins.py +20 -16
- netbox_toolkit_plugin/api/schemas.py +53 -74
- netbox_toolkit_plugin/api/serializers.py +10 -11
- netbox_toolkit_plugin/api/urls.py +2 -1
- netbox_toolkit_plugin/api/views/__init__.py +4 -3
- netbox_toolkit_plugin/api/views/command_logs.py +80 -73
- netbox_toolkit_plugin/api/views/commands.py +140 -134
- netbox_toolkit_plugin/connectors/__init__.py +9 -9
- netbox_toolkit_plugin/connectors/base.py +30 -31
- netbox_toolkit_plugin/connectors/factory.py +22 -26
- netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
- netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
- netbox_toolkit_plugin/exceptions.py +0 -7
- netbox_toolkit_plugin/filtersets.py +26 -42
- netbox_toolkit_plugin/forms.py +13 -11
- netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
- netbox_toolkit_plugin/models.py +2 -17
- netbox_toolkit_plugin/navigation.py +3 -0
- netbox_toolkit_plugin/search.py +12 -9
- netbox_toolkit_plugin/services/__init__.py +1 -1
- netbox_toolkit_plugin/services/command_service.py +7 -10
- netbox_toolkit_plugin/services/device_service.py +40 -32
- netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
- netbox_toolkit_plugin/{config.py → settings.py} +17 -7
- netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
- netbox_toolkit_plugin/tables.py +10 -1
- netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
- netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
- netbox_toolkit_plugin/urls.py +10 -3
- netbox_toolkit_plugin/utils/connection.py +54 -54
- netbox_toolkit_plugin/utils/error_parser.py +128 -109
- netbox_toolkit_plugin/utils/logging.py +1 -0
- netbox_toolkit_plugin/utils/network.py +74 -47
- netbox_toolkit_plugin/views.py +51 -22
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
- netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
- netbox_toolkit_plugin-0.1.1.dist-info/RECORD +0 -60
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
netbox_toolkit_plugin/tables.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
from netbox.tables import NetBoxTable
|
2
|
+
|
1
3
|
import django_tables2 as tables
|
2
|
-
|
4
|
+
|
3
5
|
from .models import Command, CommandLog
|
4
6
|
|
5
7
|
|
@@ -17,6 +19,13 @@ class CommandTable(NetBoxTable):
|
|
17
19
|
|
18
20
|
|
19
21
|
class CommandLogTable(NetBoxTable):
|
22
|
+
pk = tables.Column(
|
23
|
+
linkify=(
|
24
|
+
"plugins:netbox_toolkit_plugin:commandlog_view",
|
25
|
+
[tables.A("pk")],
|
26
|
+
),
|
27
|
+
verbose_name="ID",
|
28
|
+
)
|
20
29
|
command = tables.Column(
|
21
30
|
linkify=(
|
22
31
|
"plugins:netbox_toolkit_plugin:command_detail",
|
@@ -35,7 +35,7 @@
|
|
35
35
|
<th scope="row">Status</th>
|
36
36
|
<td>
|
37
37
|
{% if object.success %}
|
38
|
-
<span class="badge bg-success">Success</span>
|
38
|
+
<span class="badge bg-success-lt text-success-lt-fg">Success</span>
|
39
39
|
{% else %}
|
40
40
|
<span class="badge bg-danger">Failed</span>
|
41
41
|
{% endif %}
|
@@ -47,7 +47,9 @@
|
|
47
47
|
<td>{{ object.execution_duration|floatformat:3 }}s</td>
|
48
48
|
</tr>
|
49
49
|
{% endif %}
|
50
|
-
{%
|
50
|
+
{% comment %}
|
51
|
+
<!-- Parsing status removed - focus on execution history -->
|
52
|
+
{% if object.get_fresh_parsed_data %}
|
51
53
|
<tr>
|
52
54
|
<th scope="row">Parsing Status</th>
|
53
55
|
<td>
|
@@ -55,22 +57,11 @@
|
|
55
57
|
<i class="mdi mdi-check-circle me-1"></i>
|
56
58
|
Successfully Parsed
|
57
59
|
</span>
|
58
|
-
|
59
|
-
<br><small class="text-muted">Template: {{ object.parsing_template }}</small>
|
60
|
-
{% endif %}
|
61
|
-
</td>
|
62
|
-
</tr>
|
63
|
-
{% elif object.parsed_data is not None %}
|
64
|
-
<tr>
|
65
|
-
<th scope="row">Parsing Status</th>
|
66
|
-
<td>
|
67
|
-
<span class="badge bg-warning">
|
68
|
-
<i class="mdi mdi-alert-circle me-1"></i>
|
69
|
-
Parsing Attempted
|
70
|
-
</span>
|
60
|
+
<br><small class="text-muted">Parsed from raw output</small>
|
71
61
|
</td>
|
72
62
|
</tr>
|
73
63
|
{% endif %}
|
64
|
+
{% endcomment %}
|
74
65
|
{% if not object.success and object.error_message %}
|
75
66
|
<tr>
|
76
67
|
<th scope="row">Error</th>
|
@@ -86,82 +77,23 @@
|
|
86
77
|
<div class="col col-md-12">
|
87
78
|
<div class="card">
|
88
79
|
<div class="card-header">
|
89
|
-
<
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
</div>
|
97
|
-
|
98
|
-
{% if object.parsed_data %}
|
99
|
-
<div class="row mt-3">
|
100
|
-
<div class="col col-md-12">
|
101
|
-
<div class="card">
|
102
|
-
<div class="card-header">
|
103
|
-
<h3 class="card-title">
|
104
|
-
<i class="mdi mdi-table me-1"></i>
|
105
|
-
Parsed Data
|
106
|
-
</h3>
|
107
|
-
{% if object.parsing_template %}
|
108
|
-
<div class="card-subtitle">
|
109
|
-
<small class="text-muted">Template: {{ object.parsing_template }}</small>
|
80
|
+
<div class="d-flex justify-content-between align-items-start">
|
81
|
+
<h3 class="card-title mb-0 mt-1 me-3">Command Output</h3>
|
82
|
+
<div class="btn-list">
|
83
|
+
<button type="button" class="btn btn-sm btn-outline-primary copy-output-btn"
|
84
|
+
title="Copy output to clipboard">
|
85
|
+
<i class="mdi mdi-content-copy me-1"></i>Copy
|
86
|
+
</button>
|
110
87
|
</div>
|
111
|
-
|
88
|
+
</div>
|
112
89
|
</div>
|
113
90
|
<div class="card-body">
|
114
|
-
|
115
|
-
{% if object.parsed_data.0 %}
|
116
|
-
<!-- Table format for structured data -->
|
117
|
-
<div class="table-responsive">
|
118
|
-
<table class="table table-sm table-striped">
|
119
|
-
<thead>
|
120
|
-
<tr>
|
121
|
-
{% for key in object.parsed_data.0.keys %}
|
122
|
-
<th>{{ key|title }}</th>
|
123
|
-
{% endfor %}
|
124
|
-
</tr>
|
125
|
-
</thead>
|
126
|
-
<tbody>
|
127
|
-
{% for row in object.parsed_data %}
|
128
|
-
<tr>
|
129
|
-
{% for value in row.values %}
|
130
|
-
<td>{{ value }}</td>
|
131
|
-
{% endfor %}
|
132
|
-
</tr>
|
133
|
-
{% endfor %}
|
134
|
-
</tbody>
|
135
|
-
</table>
|
136
|
-
</div>
|
137
|
-
{% else %}
|
138
|
-
<!-- JSON format for other data types -->
|
139
|
-
<pre class="bg-light p-3 rounded">{{ object.parsed_data|pprint }}</pre>
|
140
|
-
{% endif %}
|
141
|
-
{% else %}
|
142
|
-
<div class="alert alert-info mb-0">
|
143
|
-
<i class="mdi mdi-information-outline me-1"></i>
|
144
|
-
No structured data found in the output.
|
145
|
-
</div>
|
146
|
-
{% endif %}
|
147
|
-
|
148
|
-
<!-- Copy parsed data button -->
|
149
|
-
{% if object.parsed_data|length > 0 %}
|
150
|
-
{{ object.parsed_data|json_script:"commandlog-parsed-data-json" }}
|
151
|
-
<div class="mt-3">
|
152
|
-
<div class="btn-list">
|
153
|
-
<button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
|
154
|
-
title="Copy parsed data as JSON">
|
155
|
-
<i class="mdi mdi-content-copy me-1"></i>Copy Parsed Data
|
156
|
-
</button>
|
157
|
-
</div>
|
158
|
-
</div>
|
159
|
-
{% endif %}
|
91
|
+
<pre class="command-output bg-surface p-3 rounded font-monospace">{{ object.output }}</pre>
|
160
92
|
</div>
|
161
93
|
</div>
|
162
94
|
</div>
|
163
95
|
</div>
|
164
|
-
|
96
|
+
|
165
97
|
{% endblock %}
|
166
98
|
|
167
99
|
{% block javascript %}
|
@@ -15,8 +15,8 @@
|
|
15
15
|
<div class="card-header">
|
16
16
|
<h3 class="card-title">Connection Info</h3>
|
17
17
|
<div class="card-actions">
|
18
|
-
<button class="btn btn-icon" type="button" data-bs-toggle="collapse"
|
19
|
-
data-bs-target="#connectionInfoCollapse" aria-expanded="true"
|
18
|
+
<button class="btn btn-icon" type="button" data-bs-toggle="collapse"
|
19
|
+
data-bs-target="#connectionInfoCollapse" aria-expanded="true"
|
20
20
|
aria-controls="connectionInfoCollapse">
|
21
21
|
<i class="mdi mdi-chevron-up collapse-icon"></i>
|
22
22
|
</button>
|
@@ -77,15 +77,15 @@
|
|
77
77
|
</div>
|
78
78
|
</div>
|
79
79
|
</div>
|
80
|
-
|
80
|
+
|
81
81
|
<!-- Rate Limiting Status -->
|
82
82
|
{% if rate_limit_status.enabled %}
|
83
83
|
<div class="card mt-3">
|
84
84
|
<div class="card-header">
|
85
85
|
<h3 class="card-title">Rate Limiting</h3>
|
86
86
|
<div class="card-actions">
|
87
|
-
<button class="btn btn-icon" type="button" data-bs-toggle="collapse"
|
88
|
-
data-bs-target="#rateLimitCollapse" aria-expanded="true"
|
87
|
+
<button class="btn btn-icon" type="button" data-bs-toggle="collapse"
|
88
|
+
data-bs-target="#rateLimitCollapse" aria-expanded="true"
|
89
89
|
aria-controls="rateLimitCollapse">
|
90
90
|
<i class="mdi mdi-chevron-up collapse-icon"></i>
|
91
91
|
</button>
|
@@ -132,7 +132,7 @@
|
|
132
132
|
{{ rate_limit_status.message }}
|
133
133
|
</small>
|
134
134
|
</div>
|
135
|
-
|
135
|
+
|
136
136
|
{% if rate_limit_status.is_exceeded %}
|
137
137
|
<div class="alert alert-danger mt-3 mb-0">
|
138
138
|
<i class="mdi mdi-block-helper me-2"></i>
|
@@ -151,7 +151,7 @@
|
|
151
151
|
</div>
|
152
152
|
</div>
|
153
153
|
{% endif %}
|
154
|
-
|
154
|
+
|
155
155
|
<!-- Commands List -->
|
156
156
|
<div class="card flex-grow-1">
|
157
157
|
<div class="card-header">
|
@@ -169,27 +169,27 @@
|
|
169
169
|
{% if commands %}
|
170
170
|
<div class="list-group list-group-flush">
|
171
171
|
{% for command in commands %}
|
172
|
-
<div class="list-group-item list-group-item-action command-item"
|
173
|
-
data-command-id="{{ command.id }}" data-command-name="{{ command.name }}"
|
172
|
+
<div class="list-group-item list-group-item-action command-item"
|
173
|
+
data-command-id="{{ command.id }}" data-command-name="{{ command.name }}"
|
174
174
|
title="{{ command.description }}">
|
175
175
|
<div class="d-flex justify-content-between align-items-center">
|
176
176
|
<div class="d-flex align-items-center">
|
177
177
|
{% if command.command_type == 'config' %}
|
178
|
-
<i class="mdi mdi-alert-outline text-danger me-2 opacity-75"
|
178
|
+
<i class="mdi mdi-alert-outline text-danger me-2 opacity-75"
|
179
179
|
style="font-size: 1.25rem;"
|
180
180
|
title="Configuration command - use with caution"></i>
|
181
181
|
{% endif %}
|
182
|
-
<a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=command.pk %}"
|
182
|
+
<a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=command.pk %}"
|
183
183
|
class="text-decoration-none text-body">
|
184
184
|
{{ command.name }}
|
185
185
|
</a>
|
186
186
|
</div>
|
187
187
|
<div class="d-flex align-items-center">
|
188
188
|
<!-- Run button (hidden by default, shown on hover) -->
|
189
|
-
<button type="button"
|
190
|
-
class="btn btn-sm btn-success command-run-btn"
|
189
|
+
<button type="button"
|
190
|
+
class="btn btn-sm btn-success command-run-btn"
|
191
191
|
title="Execute command"
|
192
|
-
data-command-id="{{ command.id }}"
|
192
|
+
data-command-id="{{ command.id }}"
|
193
193
|
data-command-name="{{ command.name }}">
|
194
194
|
<i class="mdi mdi-play me-1"></i>Run
|
195
195
|
</button>
|
@@ -218,7 +218,7 @@
|
|
218
218
|
{% endif %}
|
219
219
|
</div>
|
220
220
|
</div>
|
221
|
-
|
221
|
+
|
222
222
|
<!-- Recent Command History -->
|
223
223
|
<div class="card mt-3">
|
224
224
|
<div class="card-header">
|
@@ -252,7 +252,7 @@
|
|
252
252
|
</div>
|
253
253
|
</div>
|
254
254
|
</div>
|
255
|
-
|
255
|
+
|
256
256
|
<!-- Right Column with Output -->
|
257
257
|
<div class="col-md-9">
|
258
258
|
<div class="card h-100">
|
@@ -260,8 +260,8 @@
|
|
260
260
|
<h3 class="card-title">Command Output</h3>
|
261
261
|
{% if executed_command %}
|
262
262
|
<div class="card-actions">
|
263
|
-
<a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=executed_command.pk %}"
|
264
|
-
class="text-decoration-none text-muted"
|
263
|
+
<a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=executed_command.pk %}"
|
264
|
+
class="text-decoration-none text-muted"
|
265
265
|
title="View command details">
|
266
266
|
{{ executed_command.name }}
|
267
267
|
<i class="mdi mdi-open-in-new ms-1" style="font-size: 0.8rem;"></i>
|
@@ -283,14 +283,14 @@
|
|
283
283
|
{% endif %}
|
284
284
|
</div>
|
285
285
|
</div>
|
286
|
-
|
286
|
+
|
287
287
|
<!-- Tabbed output interface -->
|
288
288
|
<div class="card">
|
289
289
|
<div class="card-header">
|
290
290
|
<ul class="nav nav-tabs nav-fill" id="outputTabs" role="tablist">
|
291
291
|
<li class="nav-item" role="presentation">
|
292
|
-
<button class="nav-link active" id="raw-output-tab" data-bs-toggle="tab"
|
293
|
-
data-bs-target="#raw-output" type="button" role="tab"
|
292
|
+
<button class="nav-link active" id="raw-output-tab" data-bs-toggle="tab"
|
293
|
+
data-bs-target="#raw-output" type="button" role="tab"
|
294
294
|
aria-controls="raw-output" aria-selected="true">
|
295
295
|
<i class="mdi mdi-console me-1"></i>
|
296
296
|
Raw Output
|
@@ -298,8 +298,8 @@
|
|
298
298
|
</li>
|
299
299
|
{% if parsed_data %}
|
300
300
|
<li class="nav-item" role="presentation">
|
301
|
-
<button class="nav-link" id="parsed-data-tab" data-bs-toggle="tab"
|
302
|
-
data-bs-target="#parsed-data" type="button" role="tab"
|
301
|
+
<button class="nav-link" id="parsed-data-tab" data-bs-toggle="tab"
|
302
|
+
data-bs-target="#parsed-data" type="button" role="tab"
|
303
303
|
aria-controls="parsed-data" aria-selected="false">
|
304
304
|
<i class="mdi mdi-table me-1"></i>
|
305
305
|
Parsed Data
|
@@ -311,12 +311,12 @@
|
|
311
311
|
<div class="card-body p-0">
|
312
312
|
<div class="tab-content" id="outputTabContent">
|
313
313
|
<!-- Raw Output Tab -->
|
314
|
-
<div class="tab-pane fade show active" id="raw-output" role="tabpanel"
|
314
|
+
<div class="tab-pane fade show active" id="raw-output" role="tabpanel"
|
315
315
|
aria-labelledby="raw-output-tab">
|
316
316
|
<div class="d-flex justify-content-between align-items-start py-3 px-3 border-bottom">
|
317
317
|
<h6 class="mb-0 mt-1">Command Output</h6>
|
318
318
|
<div class="btn-list">
|
319
|
-
<button type="button" class="btn btn-sm btn-outline-primary copy-output-btn"
|
319
|
+
<button type="button" class="btn btn-sm btn-outline-primary copy-output-btn"
|
320
320
|
title="Copy raw output to clipboard">
|
321
321
|
<i class="mdi mdi-content-copy me-1"></i>Copy
|
322
322
|
</button>
|
@@ -325,10 +325,10 @@
|
|
325
325
|
<pre class="command-output bg-surface p-3 rounded font-monospace">{{ command_output }}</pre>
|
326
326
|
</div>
|
327
327
|
</div>
|
328
|
-
|
328
|
+
|
329
329
|
<!-- Parsed Data Tab -->
|
330
330
|
{% if parsed_data %}
|
331
|
-
<div class="tab-pane fade" id="parsed-data" role="tabpanel"
|
331
|
+
<div class="tab-pane fade" id="parsed-data" role="tabpanel"
|
332
332
|
aria-labelledby="parsed-data-tab">
|
333
333
|
<div class="d-flex justify-content-between align-items-start py-3 px-3 border-bottom">
|
334
334
|
<div class="mt-1">
|
@@ -340,10 +340,14 @@
|
|
340
340
|
{% if parsed_data|length > 0 %}
|
341
341
|
{{ parsed_data|json_script:"parsed-data-json" }}
|
342
342
|
<div class="btn-list">
|
343
|
-
<button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
|
343
|
+
<button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
|
344
344
|
title="Copy parsed data as JSON">
|
345
345
|
<i class="mdi mdi-content-copy me-1"></i>Copy JSON
|
346
346
|
</button>
|
347
|
+
<button type="button" class="btn btn-sm btn-outline-info download-csv-btn"
|
348
|
+
title="Download parsed data as CSV">
|
349
|
+
<i class="mdi mdi-file-table me-1"></i>Download CSV
|
350
|
+
</button>
|
347
351
|
</div>
|
348
352
|
{% endif %}
|
349
353
|
</div>
|
@@ -355,7 +359,7 @@
|
|
355
359
|
<table class="table table-sm table-striped mb-0">
|
356
360
|
<thead>
|
357
361
|
<tr>
|
358
|
-
{% for key in parsed_data.0.
|
362
|
+
{% for key, value in parsed_data.0.items %}
|
359
363
|
<th>{{ key|title }}</th>
|
360
364
|
{% endfor %}
|
361
365
|
</tr>
|
@@ -363,7 +367,7 @@
|
|
363
367
|
<tbody>
|
364
368
|
{% for row in parsed_data %}
|
365
369
|
<tr>
|
366
|
-
{% for value in row.
|
370
|
+
{% for key, value in row.items %}
|
367
371
|
<td>{{ value }}</td>
|
368
372
|
{% endfor %}
|
369
373
|
</tr>
|
@@ -468,7 +472,7 @@
|
|
468
472
|
Enter your credentials to execute: <strong id="commandToExecute"></strong>
|
469
473
|
</p>
|
470
474
|
</div>
|
471
|
-
|
475
|
+
|
472
476
|
<!-- Rate Limiting Information in Modal -->
|
473
477
|
{% if rate_limit_status.enabled and not rate_limit_status.bypassed %}
|
474
478
|
<div class="alert {% if rate_limit_status.is_exceeded %}alert-danger{% elif rate_limit_status.is_warning %}alert-warning{% else %}alert-info{% endif %} mb-3">
|
@@ -476,7 +480,7 @@
|
|
476
480
|
<i class="mdi {% if rate_limit_status.is_exceeded %}mdi-block-helper{% elif rate_limit_status.is_warning %}mdi-alert-outline{% else %}mdi-information-outline{% endif %} me-2 mt-1"></i>
|
477
481
|
<div>
|
478
482
|
<small>
|
479
|
-
<strong>Rate Limiting:</strong> {{ rate_limit_status.remaining }} commands remaining
|
483
|
+
<strong>Rate Limiting:</strong> {{ rate_limit_status.remaining }} commands remaining
|
480
484
|
({{ rate_limit_status.current_count }}/{{ rate_limit_status.limit }} successful commands used)
|
481
485
|
{% if rate_limit_status.is_exceeded %}
|
482
486
|
<br><span class="text-danger">Error: Rate limit exceeded! Commands are blocked.</span>
|
@@ -488,7 +492,7 @@
|
|
488
492
|
</div>
|
489
493
|
</div>
|
490
494
|
{% endif %}
|
491
|
-
|
495
|
+
|
492
496
|
<form id="modalCredentialsForm">
|
493
497
|
<div class="mb-3">
|
494
498
|
<label for="modalUsername" class="form-label">Username</label>
|
netbox_toolkit_plugin/urls.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from django.urls import path
|
2
|
+
|
2
3
|
from . import views
|
3
4
|
|
4
5
|
app_name = "netbox_toolkit_plugin"
|
@@ -23,11 +24,17 @@ urlpatterns = [
|
|
23
24
|
),
|
24
25
|
# Command Log views
|
25
26
|
path("logs/", views.CommandLogListView.as_view(), name="commandlog_list"),
|
27
|
+
path("logs/add/", views.CommandLogEditView.as_view(), name="commandlog_add"),
|
26
28
|
path("logs/<int:pk>/", views.CommandLogView.as_view(), name="commandlog_view"),
|
27
29
|
path(
|
28
|
-
"logs/<int:pk>/
|
29
|
-
views.
|
30
|
-
name="
|
30
|
+
"logs/<int:pk>/edit/",
|
31
|
+
views.CommandLogEditView.as_view(),
|
32
|
+
name="commandlog_edit",
|
33
|
+
),
|
34
|
+
path(
|
35
|
+
"logs/<int:pk>/delete/",
|
36
|
+
views.CommandLogDeleteView.as_view(),
|
37
|
+
name="commandlog_delete",
|
31
38
|
),
|
32
39
|
# Device toolkit view
|
33
40
|
path(
|
@@ -1,34 +1,34 @@
|
|
1
1
|
"""Connection management utilities for robust socket handling."""
|
2
|
+
|
3
|
+
import contextlib
|
2
4
|
import socket
|
3
5
|
import time
|
4
|
-
from typing import Any
|
6
|
+
from typing import Any
|
5
7
|
|
6
8
|
|
7
|
-
def safe_socket_close(sock:
|
9
|
+
def safe_socket_close(sock: socket.socket | None) -> None:
|
8
10
|
"""Safely close a socket with proper error handling."""
|
9
11
|
if sock is None:
|
10
12
|
return
|
11
|
-
|
13
|
+
|
12
14
|
try:
|
13
15
|
# Try graceful shutdown first
|
14
|
-
|
15
|
-
sock.shutdown(socket.SHUT_RDWR)
|
16
|
-
except (OSError, AttributeError):
|
16
|
+
with contextlib.suppress(OSError, AttributeError):
|
17
17
|
# Socket might already be closed or not connected
|
18
|
-
|
19
|
-
|
18
|
+
sock.shutdown(socket.SHUT_RDWR)
|
19
|
+
|
20
20
|
# Then close the socket
|
21
21
|
sock.close()
|
22
|
-
|
23
|
-
except Exception
|
22
|
+
|
23
|
+
except Exception:
|
24
24
|
pass # Socket cleanup error ignored
|
25
25
|
|
26
26
|
|
27
|
-
def is_socket_valid(sock:
|
27
|
+
def is_socket_valid(sock: socket.socket | None) -> bool:
|
28
28
|
"""Check if a socket is valid and usable."""
|
29
29
|
if sock is None:
|
30
30
|
return False
|
31
|
-
|
31
|
+
|
32
32
|
try:
|
33
33
|
# Try to get socket peer name - this will fail if socket is closed
|
34
34
|
sock.getpeername()
|
@@ -41,47 +41,46 @@ def cleanup_connection_resources(connection: Any) -> None:
|
|
41
41
|
"""Clean up all resources associated with a connection object."""
|
42
42
|
if not connection:
|
43
43
|
return
|
44
|
-
|
44
|
+
|
45
45
|
try:
|
46
46
|
# Handle Scrapli-specific cleanup
|
47
|
-
if hasattr(connection,
|
47
|
+
if hasattr(connection, "channel") and connection.channel:
|
48
48
|
try:
|
49
49
|
# Close the channel
|
50
|
-
if hasattr(connection.channel,
|
50
|
+
if hasattr(connection.channel, "close"):
|
51
51
|
connection.channel.close()
|
52
|
-
|
52
|
+
|
53
53
|
# Clean up the underlying socket if accessible
|
54
|
-
if hasattr(connection.channel,
|
54
|
+
if hasattr(connection.channel, "socket"):
|
55
55
|
safe_socket_close(connection.channel.socket)
|
56
|
-
elif hasattr(connection.channel,
|
56
|
+
elif hasattr(connection.channel, "_socket"):
|
57
57
|
safe_socket_close(connection.channel._socket)
|
58
|
-
|
59
|
-
except Exception
|
58
|
+
|
59
|
+
except Exception:
|
60
60
|
pass # Channel cleanup error ignored
|
61
|
-
|
61
|
+
|
62
62
|
# Handle transport cleanup
|
63
|
-
if hasattr(connection,
|
63
|
+
if hasattr(connection, "transport") and connection.transport:
|
64
64
|
try:
|
65
|
-
if hasattr(connection.transport,
|
65
|
+
if hasattr(connection.transport, "close"):
|
66
66
|
connection.transport.close()
|
67
|
-
|
67
|
+
|
68
68
|
# Clean up transport socket if accessible
|
69
|
-
if hasattr(connection.transport,
|
69
|
+
if hasattr(connection.transport, "sock"):
|
70
70
|
safe_socket_close(connection.transport.sock)
|
71
|
-
elif hasattr(connection.transport,
|
71
|
+
elif hasattr(connection.transport, "_socket"):
|
72
72
|
safe_socket_close(connection.transport._socket)
|
73
|
-
|
74
|
-
except Exception
|
73
|
+
|
74
|
+
except Exception:
|
75
75
|
pass # Transport cleanup error ignored
|
76
|
-
|
76
|
+
|
77
77
|
# Try the main close method
|
78
|
-
if hasattr(connection,
|
79
|
-
|
78
|
+
if hasattr(connection, "close"):
|
79
|
+
with contextlib.suppress(Exception):
|
80
|
+
# Connection close error ignored
|
80
81
|
connection.close()
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
except Exception as e:
|
82
|
+
|
83
|
+
except Exception:
|
85
84
|
pass # Connection cleanup error ignored
|
86
85
|
|
87
86
|
|
@@ -89,34 +88,35 @@ def validate_connection_health(connection: Any) -> bool:
|
|
89
88
|
"""Validate that a connection is healthy and usable."""
|
90
89
|
if not connection:
|
91
90
|
return False
|
92
|
-
|
91
|
+
|
93
92
|
try:
|
94
93
|
# Check if connection reports as alive
|
95
|
-
if hasattr(connection,
|
96
|
-
|
97
|
-
|
98
|
-
|
94
|
+
if hasattr(connection, "isalive") and not connection.isalive():
|
95
|
+
return False
|
96
|
+
|
99
97
|
# Check underlying socket health if available
|
100
|
-
if hasattr(connection,
|
101
|
-
if hasattr(connection.channel,
|
98
|
+
if hasattr(connection, "channel") and connection.channel:
|
99
|
+
if hasattr(connection.channel, "socket"):
|
102
100
|
if not is_socket_valid(connection.channel.socket):
|
103
101
|
return False
|
104
|
-
elif hasattr(connection.channel,
|
105
|
-
|
106
|
-
|
107
|
-
|
102
|
+
elif hasattr(connection.channel, "_socket") and not is_socket_valid(
|
103
|
+
connection.channel._socket
|
104
|
+
):
|
105
|
+
return False
|
106
|
+
|
108
107
|
# Check transport socket health if available
|
109
|
-
if hasattr(connection,
|
110
|
-
if hasattr(connection.transport,
|
108
|
+
if hasattr(connection, "transport") and connection.transport:
|
109
|
+
if hasattr(connection.transport, "sock"):
|
111
110
|
if not is_socket_valid(connection.transport.sock):
|
112
111
|
return False
|
113
|
-
elif hasattr(connection.transport,
|
114
|
-
|
115
|
-
|
116
|
-
|
112
|
+
elif hasattr(connection.transport, "_socket") and not is_socket_valid(
|
113
|
+
connection.transport._socket
|
114
|
+
):
|
115
|
+
return False
|
116
|
+
|
117
117
|
return True
|
118
|
-
|
119
|
-
except Exception
|
118
|
+
|
119
|
+
except Exception:
|
120
120
|
return False
|
121
121
|
|
122
122
|
|