netbox-toolkit-plugin 0.1.0__py3-none-any.whl → 0.1.1__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 (68) hide show
  1. netbox_toolkit_plugin/__init__.py +32 -0
  2. {netbox_toolkit → netbox_toolkit_plugin}/api/serializers.py +71 -35
  3. {netbox_toolkit → netbox_toolkit_plugin}/api/urls.py +3 -3
  4. {netbox_toolkit → netbox_toolkit_plugin}/config.py +80 -73
  5. {netbox_toolkit → netbox_toolkit_plugin}/connectors/factory.py +170 -111
  6. {netbox_toolkit → netbox_toolkit_plugin}/connectors/netmiko_connector.py +242 -179
  7. {netbox_toolkit → netbox_toolkit_plugin}/connectors/scrapli_connector.py +256 -172
  8. netbox_toolkit_plugin/migrations/0001_initial.py +108 -0
  9. netbox_toolkit_plugin/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +70 -0
  10. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0003_permission_system_update.py +26 -12
  11. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0004_remove_django_permissions.py +27 -29
  12. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0005_alter_command_options_and_more.py +7 -8
  13. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +7 -8
  14. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0007_alter_commandlog_parsing_template.py +6 -4
  15. {netbox_toolkit → netbox_toolkit_plugin}/models.py +31 -32
  16. {netbox_toolkit → netbox_toolkit_plugin}/navigation.py +6 -6
  17. {netbox_toolkit → netbox_toolkit_plugin}/services/command_service.py +188 -128
  18. {netbox_toolkit → netbox_toolkit_plugin}/services/rate_limiting_service.py +104 -97
  19. netbox_toolkit_plugin/tables.py +51 -0
  20. netbox_toolkit_plugin/templates/netbox_toolkit/command.html +108 -0
  21. netbox_toolkit_plugin/templates/netbox_toolkit/command_list.html +12 -0
  22. netbox_toolkit_plugin/templates/netbox_toolkit/commandlog.html +170 -0
  23. netbox_toolkit_plugin/templates/netbox_toolkit/device_toolkit.html +557 -0
  24. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command.html +5 -5
  25. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_list.html +2 -2
  26. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog.html +2 -2
  27. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/device_toolkit.html +6 -6
  28. netbox_toolkit_plugin/urls.py +38 -0
  29. {netbox_toolkit → netbox_toolkit_plugin}/utils/logging.py +20 -19
  30. {netbox_toolkit → netbox_toolkit_plugin}/views.py +251 -169
  31. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/METADATA +2 -2
  32. netbox_toolkit_plugin-0.1.1.dist-info/RECORD +60 -0
  33. netbox_toolkit_plugin-0.1.1.dist-info/entry_points.txt +2 -0
  34. netbox_toolkit_plugin-0.1.1.dist-info/top_level.txt +1 -0
  35. netbox_toolkit/__init__.py +0 -30
  36. netbox_toolkit/migrations/0001_initial.py +0 -54
  37. netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +0 -66
  38. netbox_toolkit/tables.py +0 -37
  39. netbox_toolkit/urls.py +0 -22
  40. netbox_toolkit_plugin-0.1.0.dist-info/RECORD +0 -56
  41. netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +0 -2
  42. netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +0 -1
  43. {netbox_toolkit → netbox_toolkit_plugin}/admin.py +0 -0
  44. {netbox_toolkit → netbox_toolkit_plugin}/api/__init__.py +0 -0
  45. {netbox_toolkit → netbox_toolkit_plugin}/api/mixins.py +0 -0
  46. {netbox_toolkit → netbox_toolkit_plugin}/api/schemas.py +0 -0
  47. {netbox_toolkit → netbox_toolkit_plugin}/api/views/__init__.py +0 -0
  48. {netbox_toolkit → netbox_toolkit_plugin}/api/views/command_logs.py +0 -0
  49. {netbox_toolkit → netbox_toolkit_plugin}/api/views/commands.py +0 -0
  50. {netbox_toolkit → netbox_toolkit_plugin}/connectors/__init__.py +0 -0
  51. {netbox_toolkit → netbox_toolkit_plugin}/connectors/base.py +0 -0
  52. {netbox_toolkit → netbox_toolkit_plugin}/exceptions.py +0 -0
  53. {netbox_toolkit → netbox_toolkit_plugin}/filtersets.py +0 -0
  54. {netbox_toolkit → netbox_toolkit_plugin}/forms.py +0 -0
  55. {netbox_toolkit → netbox_toolkit_plugin}/migrations/__init__.py +0 -0
  56. {netbox_toolkit → netbox_toolkit_plugin}/search.py +0 -0
  57. {netbox_toolkit → netbox_toolkit_plugin}/services/__init__.py +0 -0
  58. {netbox_toolkit → netbox_toolkit_plugin}/services/device_service.py +0 -0
  59. {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/css/toolkit.css +0 -0
  60. {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/js/toolkit.js +0 -0
  61. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_edit.html +0 -0
  62. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog_list.html +0 -0
  63. {netbox_toolkit → netbox_toolkit_plugin}/utils/__init__.py +0 -0
  64. {netbox_toolkit → netbox_toolkit_plugin}/utils/connection.py +0 -0
  65. {netbox_toolkit → netbox_toolkit_plugin}/utils/error_parser.py +0 -0
  66. {netbox_toolkit → netbox_toolkit_plugin}/utils/network.py +0 -0
  67. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/WHEEL +0 -0
  68. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,557 @@
1
+ {% extends 'dcim/device.html' %}
2
+ {% load helpers %}
3
+ {% load static %}
4
+
5
+ {% block style %}
6
+ <link href="{% static 'netbox_toolkit_plugin/css/toolkit.css' %}" rel="stylesheet">
7
+ {% endblock %}
8
+
9
+ {% block content %}
10
+ <div class="row">
11
+ <!-- Left Column with Commands -->
12
+ <div class="col-md-3 d-flex flex-column">
13
+ <!-- Device Connection Info -->
14
+ <div class="card mb-3">
15
+ <div class="card-header">
16
+ <h3 class="card-title">Connection Info</h3>
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"
20
+ aria-controls="connectionInfoCollapse">
21
+ <i class="mdi mdi-chevron-up collapse-icon"></i>
22
+ </button>
23
+ </div>
24
+ </div>
25
+ <div class="collapse show" id="connectionInfoCollapse">
26
+ <div class="card-body">
27
+ <table class="table table-sm">
28
+ <tbody>
29
+ <tr>
30
+ <td class="align-middle border-0 fw-semibold text-muted"
31
+ style="width: 40%; padding: 0.5rem 0;">Hostname / IP</td>
32
+ <td class="align-middle border-0" style="width: 60%; padding: 0.5rem 0;">
33
+ <div class="font-monospace text-xs">
34
+ {% if connection_info.hostname %}
35
+ {{ connection_info.hostname }}
36
+ {% else %}
37
+ <span class="text-danger fst-italic">Missing</span>
38
+ {% endif %}
39
+ </div>
40
+ </td>
41
+ </tr>
42
+ <tr>
43
+ <td class="align-middle border-0 fw-semibold text-muted"
44
+ style="width: 40%; padding: 0.5rem 0;">Platform</td>
45
+ <td class="align-middle border-0" style="width: 60%; padding: 0.5rem 0;">
46
+ <div class="font-monospace text-xs">
47
+ {% if connection_info.platform %}
48
+ {{ connection_info.platform }}
49
+ {% else %}
50
+ <span class="text-danger fst-italic">Missing</span>
51
+ {% endif %}
52
+ </div>
53
+ </td>
54
+ </tr>
55
+ <tr>
56
+ <td class="align-middle border-0 fw-semibold text-muted"
57
+ style="width: 40%; padding: 0.5rem 0;">Connection</td>
58
+ <td class="align-middle border-0" style="width: 60%; padding: 0.5rem 0;">
59
+ <div class="font-monospace text-xs">
60
+ {% if device_valid %}
61
+ <span class="text-success">Ready</span>
62
+ {% else %}
63
+ <span class="text-danger">Not Ready</span>
64
+ {% endif %}
65
+ </div>
66
+ </td>
67
+ </tr>
68
+ </tbody>
69
+ </table>
70
+ <div class="pt-3 border-top mt-2">
71
+ <small class="text-muted">
72
+ <i class="mdi mdi-information-outline me-1"></i>
73
+ {% if device_valid %}
74
+ Device is ready for command execution.
75
+ {% else %}
76
+ {{ validation_message }}
77
+ {% endif %}
78
+ </small>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Rate Limiting Status -->
85
+ {% if rate_limit_status.enabled %}
86
+ <div class="card mt-3">
87
+ <div class="card-header">
88
+ <h3 class="card-title">Rate Limiting</h3>
89
+ <div class="card-actions">
90
+ <button class="btn btn-icon" type="button" data-bs-toggle="collapse"
91
+ data-bs-target="#rateLimitCollapse" aria-expanded="true" aria-controls="rateLimitCollapse">
92
+ <i class="mdi mdi-chevron-up collapse-icon"></i>
93
+ </button>
94
+ </div>
95
+ </div>
96
+ <div class="collapse show" id="rateLimitCollapse">
97
+ <div class="card-body">
98
+ {% if rate_limit_status.bypassed %}
99
+ <div class="alert alert-info mb-3">
100
+ <i class="mdi mdi-shield-check me-2"></i>
101
+ <strong>Rate Limiting Bypassed</strong><br>
102
+ <small>You have unlimited command execution privileges.</small>
103
+ </div>
104
+ {% else %}
105
+ <table class="table table-sm">
106
+ <tbody>
107
+ <tr>
108
+ <td class="align-middle border-0 fw-semibold text-muted"
109
+ style="width: 40%; padding: 0.5rem 0;">Commands Remaining</td>
110
+ <td class="align-middle border-0" style="width: 60%; padding: 0.5rem 0;">
111
+ <div class="font-monospace text-xs">
112
+ <span
113
+ class="{% if rate_limit_status.is_exceeded %}text-danger{% elif rate_limit_status.is_warning %}text-warning{% else %}text-success{% endif %}">
114
+ {{ rate_limit_status.remaining }}
115
+ </span>
116
+ </div>
117
+ </td>
118
+ </tr>
119
+ <tr>
120
+ <td class="align-middle border-0 fw-semibold text-muted"
121
+ style="width: 40%; padding: 0.5rem 0;">Successful Commands Used</td>
122
+ <td class="align-middle border-0" style="width: 60%; padding: 0.5rem 0;">
123
+ <div class="font-monospace text-xs">{{ rate_limit_status.current_count }} / {{
124
+ rate_limit_status.limit }}</div>
125
+ </td>
126
+ </tr>
127
+ <tr>
128
+ <td class="align-middle border-0 fw-semibold text-muted"
129
+ style="width: 40%; padding: 0.5rem 0;">Time Window</td>
130
+ <td class="align-middle border-0" style="width: 60%; padding: 0.5rem 0;">
131
+ <div class="font-monospace text-xs">{{ rate_limit_status.time_window_minutes }}
132
+ minutes</div>
133
+ </td>
134
+ </tr>
135
+ </tbody>
136
+ </table>
137
+ <div class="pt-3 border-top mt-2">
138
+ <small class="text-muted">
139
+ <i class="mdi mdi-clock-outline me-1"></i>
140
+ {{ rate_limit_status.message }}
141
+ </small>
142
+ </div>
143
+
144
+ {% if rate_limit_status.is_exceeded %}
145
+ <div class="alert alert-danger mt-3 mb-0">
146
+ <i class="mdi mdi-block-helper me-2"></i>
147
+ <strong>Rate Limit Exceeded</strong><br>
148
+ <small>Command execution is blocked. Wait for the time window to reset or contact an
149
+ administrator.</small>
150
+ </div>
151
+ {% elif rate_limit_status.is_warning %}
152
+ <div class="alert alert-warning mt-3 mb-0">
153
+ <i class="mdi mdi-alert-outline me-2"></i>
154
+ <strong>Rate Limit Warning</strong><br>
155
+ <small>You are approaching the command execution limit. Commands will be blocked if the limit is
156
+ exceeded.</small>
157
+ </div>
158
+ {% endif %}
159
+ {% endif %}
160
+ </div>
161
+ </div>
162
+ </div>
163
+ {% endif %}
164
+
165
+ <!-- Commands List -->
166
+ <div class="card flex-grow-1">
167
+ <div class="card-header">
168
+ <h3 class="card-title">Available Commands</h3>
169
+ {% if command_count is not None and total_command_count != command_count %}
170
+ <div class="card-subtitle">
171
+ <small class="text-muted">
172
+ <i class="mdi mdi-information-outline"></i>
173
+ Showing {{ command_count }} of {{ total_command_count }} based on your permissions
174
+ </small>
175
+ </div>
176
+ {% endif %}
177
+ </div>
178
+ <div class="card-body card-commands p-1">
179
+ {% if commands %}
180
+ <div class="list-group list-group-flush">
181
+ {% for command in commands %}
182
+ <div class="list-group-item list-group-item-action command-item" data-command-id="{{ command.id }}"
183
+ data-command-name="{{ command.name }}" title="{{ command.description }}">
184
+ <div class="d-flex justify-content-between align-items-center">
185
+ <div class="d-flex align-items-center">
186
+ {% if command.command_type == 'config' %}
187
+ <i class="mdi mdi-alert-outline text-danger me-2 opacity-75" style="font-size: 1.25rem;"
188
+ title="Configuration command - use with caution"></i>
189
+ {% endif %}
190
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=command.pk %}"
191
+ class="text-decoration-none text-body">
192
+ {{ command.name }}
193
+ </a>
194
+ </div>
195
+ <div class="d-flex align-items-center">
196
+ <!-- Run button (hidden by default, shown on hover) -->
197
+ <button type="button" class="btn btn-sm btn-success command-run-btn"
198
+ title="Execute command" data-command-id="{{ command.id }}"
199
+ data-command-name="{{ command.name }}">
200
+ <i class="mdi mdi-play me-1"></i>Run
201
+ </button>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ {% endfor %}
206
+ </div>
207
+ {% else %}
208
+ <div class="alert alert-info">
209
+ {% if object.platform %}
210
+ {% if not user.is_authenticated %}
211
+ You must be logged in to see commands.
212
+ {% else %}
213
+ No commands available for platform "{{ object.platform }}" with your current permissions.
214
+ {% endif %}
215
+ {% else %}
216
+ No platform assigned to this device.
217
+ {% endif %}
218
+ {% if perms.netbox_toolkit_plugin.add_command %}
219
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_add' %}" class="alert-link">
220
+ Add a command
221
+ </a>
222
+ {% endif %}
223
+ </div>
224
+ {% endif %}
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Recent Command History -->
229
+ <div class="card mt-3">
230
+ <div class="card-header">
231
+ <h3 class="card-title">Recent History</h3>
232
+ <div class="card-actions">
233
+ <small class="text-muted">Last 3</small>
234
+ </div>
235
+ </div>
236
+ <div class="card-body p-2">
237
+ {% if object.command_logs.all %}
238
+ <div class="list-group list-group-flush">
239
+ {% for log in object.command_logs.all|dictsortreversed:"execution_time"|slice:":3" %}
240
+ <div class="list-group-item px-0 py-1 border-0">
241
+ <div class="d-flex flex-column">
242
+ <div class="text-truncate" style="font-size: 0.85rem; line-height: 1.2;"
243
+ title="{{ log.command }}">
244
+ <strong>{{ log.command|truncatechars:40 }}</strong>
245
+ </div>
246
+ <div class="d-flex justify-content-between align-items-center mt-1">
247
+ <small class="text-muted text-truncate" style="flex-shrink: 1;">{{ log.username
248
+ }}</small>
249
+ <small class="text-muted" style="flex-shrink: 0; margin-left: 8px;">{{
250
+ log.execution_time|timesince }} ago</small>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ {% endfor %}
255
+ </div>
256
+ {% else %}
257
+ <div class="text-center text-muted py-2">
258
+ <small>No history available</small>
259
+ </div>
260
+ {% endif %}
261
+ </div>
262
+ </div>
263
+ </div>
264
+
265
+ <!-- Right Column with Output -->
266
+ <div class="col-md-9">
267
+ <div class="card h-100">
268
+ <div class="card-header">
269
+ <h3 class="card-title">Command Output</h3>
270
+ {% if executed_command %}
271
+ <div class="card-actions">
272
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=executed_command.pk %}"
273
+ class="text-decoration-none text-muted" title="View command details">
274
+ {{ executed_command.name }}
275
+ <i class="mdi mdi-open-in-new ms-1" style="font-size: 0.8rem;"></i>
276
+ </a>
277
+ </div>
278
+ {% endif %}
279
+ </div>
280
+ <div class="card-body">
281
+ <div id="commandOutputContainer">
282
+ {% if command_output %}
283
+ {% if execution_success %}
284
+ <!-- Successful command output -->
285
+ <div class="alert alert-success d-flex align-items-start mb-3">
286
+ <i class="mdi mdi-check-circle me-2 mt-1"></i>
287
+ <div>
288
+ <strong>Command executed successfully</strong>
289
+ {% if execution_time %}
290
+ <br><small class="text-muted">Execution time: {{ execution_time|floatformat:3 }}s</small>
291
+ {% endif %}
292
+ </div>
293
+ </div>
294
+
295
+ <!-- Tabbed output interface -->
296
+ <div class="card">
297
+ <div class="card-header">
298
+ <ul class="nav nav-tabs nav-fill" id="outputTabs" role="tablist">
299
+ <li class="nav-item" role="presentation">
300
+ <button class="nav-link active" id="raw-output-tab" data-bs-toggle="tab"
301
+ data-bs-target="#raw-output" type="button" role="tab" aria-controls="raw-output"
302
+ aria-selected="true">
303
+ <i class="mdi mdi-console me-1"></i>
304
+ Raw Output
305
+ </button>
306
+ </li>
307
+ {% if parsed_data %}
308
+ <li class="nav-item" role="presentation">
309
+ <button class="nav-link" id="parsed-data-tab" data-bs-toggle="tab"
310
+ data-bs-target="#parsed-data" type="button" role="tab"
311
+ aria-controls="parsed-data" aria-selected="false">
312
+ <i class="mdi mdi-table me-1"></i>
313
+ Parsed Data
314
+ </button>
315
+ </li>
316
+ {% endif %}
317
+ </ul>
318
+ </div>
319
+ <div class="card-body p-0">
320
+ <div class="tab-content" id="outputTabContent">
321
+ <!-- Raw Output Tab -->
322
+ <div class="tab-pane fade show active" id="raw-output" role="tabpanel"
323
+ aria-labelledby="raw-output-tab">
324
+ <div
325
+ class="d-flex justify-content-between align-items-start py-3 px-3 border-bottom">
326
+ <h6 class="mb-0 mt-1">Command Output</h6>
327
+ <div class="btn-list">
328
+ <button type="button" class="btn btn-sm btn-outline-primary copy-output-btn"
329
+ title="Copy raw output to clipboard">
330
+ <i class="mdi mdi-content-copy me-1"></i>Copy
331
+ </button>
332
+ </div>
333
+ </div>
334
+ <div class="p-3">
335
+ <pre
336
+ class="command-output bg-surface p-3 rounded font-monospace">{{ command_output }}</pre>
337
+ </div>
338
+ </div>
339
+
340
+ <!-- Parsed Data Tab -->
341
+ {% if parsed_data %}
342
+ <div class="tab-pane fade" id="parsed-data" role="tabpanel"
343
+ aria-labelledby="parsed-data-tab">
344
+ <div
345
+ class="d-flex justify-content-between align-items-start py-3 px-3 border-bottom">
346
+ <div class="mt-1">
347
+ <h6 class="mb-0">Structured Data</h6>
348
+ {% if parsing_template %}
349
+ <small class="text-muted">Template: {{ parsing_template }}</small>
350
+ {% endif %}
351
+ </div>
352
+ {% if parsed_data|length > 0 %}
353
+ {{ parsed_data|json_script:"parsed-data-json" }}
354
+ <div class="btn-list">
355
+ <button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
356
+ title="Copy parsed data as JSON">
357
+ <i class="mdi mdi-content-copy me-1"></i>Copy JSON
358
+ </button>
359
+ </div>
360
+ {% endif %}
361
+ </div>
362
+ <div class="p-3">
363
+ {% if parsed_data|length > 0 %}
364
+ {% if parsed_data.0 %}
365
+ <!-- Table format for structured data -->
366
+ <div class="table-responsive">
367
+ <table class="table table-sm table-striped mb-0">
368
+ <thead>
369
+ <tr>
370
+ {% for key in parsed_data.0.keys %}
371
+ <th>{{ key|title }}</th>
372
+ {% endfor %}
373
+ </tr>
374
+ </thead>
375
+ <tbody>
376
+ {% for row in parsed_data %}
377
+ <tr>
378
+ {% for value in row.values %}
379
+ <td>{{ value }}</td>
380
+ {% endfor %}
381
+ </tr>
382
+ {% endfor %}
383
+ </tbody>
384
+ </table>
385
+ </div>
386
+ {% else %}
387
+ <!-- JSON format for other data types -->
388
+ <pre class="bg-light p-3 rounded mb-0">{{ parsed_data|pprint }}</pre>
389
+ {% endif %}
390
+ {% else %}
391
+ <div class="alert alert-info mb-0">
392
+ <i class="mdi mdi-information-outline me-1"></i>
393
+ No structured data found in the output.
394
+ </div>
395
+ {% endif %}
396
+ </div>
397
+ </div>
398
+ {% endif %}
399
+ </div>
400
+ </div>
401
+ </div>
402
+ {% elif has_syntax_error %}
403
+ <!-- Syntax error output -->
404
+ <div class="alert alert-warning alert-dismissible" role="alert">
405
+ <div class="d-flex">
406
+ <div>
407
+ <svg class="icon alert-icon" width="24" height="24">
408
+ <use xlink:href="#tabler-alert-triangle"></use>
409
+ </svg>
410
+ </div>
411
+ <div>
412
+ <h4 class="alert-title">Command executed with syntax error detected</h4>
413
+ {% if syntax_error_type and syntax_error_vendor %}
414
+ <div class="text-secondary">Error Type: {{ syntax_error_type|title }} | Vendor: {{
415
+ syntax_error_vendor|title }}</div>
416
+ {% endif %}
417
+ </div>
418
+ </div>
419
+ </div>
420
+ <div class="card">
421
+ <div class="card-header bg-warning-subtle">
422
+ <h6 class="card-title mb-0">
423
+ <i class="mdi mdi-alert-outline me-1"></i>
424
+ Syntax Error Details and Guidance
425
+ </h6>
426
+ </div>
427
+ <div class="card-body">
428
+ <pre class="command-output font-monospace">{{ command_output }}</pre>
429
+ </div>
430
+ </div>
431
+ {% else %}
432
+ <!-- Error output -->
433
+ <div class="alert alert-danger alert-dismissible" role="alert">
434
+ <div class="d-flex">
435
+ <div>
436
+ <svg class="icon alert-icon" width="24" height="24">
437
+ <use xlink:href="#tabler-alert-circle"></use>
438
+ </svg>
439
+ </div>
440
+ <div>
441
+ <h4 class="alert-title">Command execution failed</h4>
442
+ <div class="text-secondary">Check the error details below for troubleshooting
443
+ information.</div>
444
+ </div>
445
+ </div>
446
+ </div>
447
+ <div class="card">
448
+ <div class="card-header bg-danger-subtle">
449
+ <h6 class="card-title mb-0">
450
+ <i class="mdi mdi-information-outline me-1"></i>
451
+ Error Details and Troubleshooting
452
+ </h6>
453
+ </div>
454
+ <div class="card-body">
455
+ <pre class="command-output font-monospace">{{ command_output }}</pre>
456
+ </div>
457
+ </div>
458
+ {% endif %}
459
+ {% else %}
460
+ <div class="alert alert-info" id="defaultMessage">
461
+ Select a command to execute from the list on the left.
462
+ </div>
463
+ {% endif %}
464
+ </div>
465
+ </div>
466
+ </div>
467
+ </div>
468
+ </div>
469
+
470
+ <!-- Credential and Confirmation Modal -->
471
+ <div class="modal fade credential-modal" id="credentialModal" tabindex="-1" aria-labelledby="credentialModalLabel"
472
+ aria-hidden="true">
473
+ <div class="modal-dialog">
474
+ <div class="modal-content">
475
+ <div class="modal-header">
476
+ <h5 class="modal-title" id="credentialModalLabel">Execute Command</h5>
477
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
478
+ </div>
479
+ <div class="modal-body">
480
+ <div class="mb-3">
481
+ <p class="text-muted mb-3">
482
+ <i class="mdi mdi-information-outline me-1"></i>
483
+ Enter your credentials to execute: <strong id="commandToExecute"></strong>
484
+ </p>
485
+ </div>
486
+
487
+ <!-- Rate Limiting Information in Modal -->
488
+ {% if rate_limit_status.enabled and not rate_limit_status.bypassed %}
489
+ <div
490
+ class="alert {% if rate_limit_status.is_exceeded %}alert-danger{% elif rate_limit_status.is_warning %}alert-warning{% else %}alert-info{% endif %} mb-3">
491
+ <div class="d-flex align-items-start">
492
+ <i
493
+ 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>
494
+ <div>
495
+ <small>
496
+ <strong>Rate Limiting:</strong> {{ rate_limit_status.remaining }} commands remaining
497
+ ({{ rate_limit_status.current_count }}/{{ rate_limit_status.limit }} successful commands
498
+ used)
499
+ {% if rate_limit_status.is_exceeded %}
500
+ <br><span class="text-danger">Error: Rate limit exceeded! Commands are blocked.</span>
501
+ {% elif rate_limit_status.is_warning %}
502
+ <br><span class="text-warning">Warning: Approaching command limit!</span>
503
+ {% endif %}
504
+ </small>
505
+ </div>
506
+ </div>
507
+ </div>
508
+ {% endif %}
509
+
510
+ <form id="modalCredentialsForm">
511
+ <div class="mb-3">
512
+ <label for="modalUsername" class="form-label">Username</label>
513
+ <input type="text" id="modalUsername" class="form-control" required autocomplete="username">
514
+ </div>
515
+ <div class="mb-3">
516
+ <label for="modalPassword" class="form-label">Password</label>
517
+ <input type="password" id="modalPassword" class="form-control" required
518
+ autocomplete="current-password">
519
+ </div>
520
+ <div class="alert alert-warning">
521
+ <i class="mdi mdi-shield-lock-outline me-1"></i>
522
+ <small>Credentials are not stored and are required for each command execution.</small>
523
+ </div>
524
+ </form>
525
+ </div>
526
+ <div class="modal-footer">
527
+ <div class="btn-list w-100 justify-content-end">
528
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
529
+ <button type="button" class="btn btn-primary" id="executeCommandBtn" {% if rate_limit_status.enabled
530
+ and not rate_limit_status.bypassed and rate_limit_status.is_exceeded %}disabled{% endif %}>
531
+ <i class="mdi mdi-play me-1"></i>
532
+ {% if rate_limit_status.enabled and not rate_limit_status.bypassed and
533
+ rate_limit_status.is_exceeded %}
534
+ Rate Limited
535
+ {% else %}
536
+ Execute Command
537
+ {% endif %}
538
+ </button>
539
+ </div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ </div>
544
+
545
+ <form id="commandExecutionForm" method="post" style="display: none;">
546
+ {% csrf_token %}
547
+ <input type="hidden" name="command_id" id="selectedCommandId">
548
+ <input type="hidden" name="username" id="formUsername">
549
+ <input type="hidden" name="password" id="formPassword">
550
+ </form>
551
+
552
+ {% endblock %}
553
+
554
+ {% block javascript %}
555
+ <!-- Load NetBox Toolkit consolidated JavaScript -->
556
+ <script src="{% static 'netbox_toolkit_plugin/js/toolkit.js' %}"></script>
557
+ {% endblock %}
@@ -3,17 +3,17 @@
3
3
  {% load static %}
4
4
 
5
5
  {% block style %}
6
- <link href="{% static 'netbox_toolkit/css/toolkit.css' %}" rel="stylesheet">
6
+ <link href="{% static 'netbox_toolkit_plugin/css/toolkit.css' %}" rel="stylesheet">
7
7
  {% endblock %}
8
8
 
9
9
  {% block buttons %}
10
- {% if perms.netbox_toolkit.change_command %}
11
- <a href="{% url 'plugins:netbox_toolkit:command_edit' pk=object.pk %}" class="btn btn-warning">
10
+ {% if perms.netbox_toolkit_plugin.change_command %}
11
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_edit' pk=object.pk %}" class="btn btn-warning">
12
12
  <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
13
13
  </a>
14
14
  {% endif %}
15
- {% if perms.netbox_toolkit.delete_command %}
16
- <a href="{% url 'plugins:netbox_toolkit:command_delete' pk=object.pk %}" class="btn btn-danger">
15
+ {% if perms.netbox_toolkit_plugin.delete_command %}
16
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_delete' pk=object.pk %}" class="btn btn-danger">
17
17
  <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
18
18
  </a>
19
19
  {% endif %}
@@ -4,8 +4,8 @@
4
4
  {% block title %}Commands{% endblock %}
5
5
 
6
6
  {% block buttons %}
7
- {% if perms.netbox_toolkit.add_command %}
8
- <a href="{% url 'plugins:netbox_toolkit:command_add' %}" class="btn btn-primary">
7
+ {% if perms.netbox_toolkit_plugin.add_command %}
8
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_add' %}" class="btn btn-primary">
9
9
  <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Command
10
10
  </a>
11
11
  {% endif %}
@@ -3,7 +3,7 @@
3
3
  {% load static %}
4
4
 
5
5
  {% block style %}
6
- <link href="{% static 'netbox_toolkit/css/toolkit.css' %}" rel="stylesheet">
6
+ <link href="{% static 'netbox_toolkit_plugin/css/toolkit.css' %}" rel="stylesheet">
7
7
  {% endblock %}
8
8
 
9
9
  {% block content %}
@@ -166,5 +166,5 @@
166
166
 
167
167
  {% block javascript %}
168
168
  <!-- Load NetBox Toolkit consolidated JavaScript -->
169
- <script src="{% static 'netbox_toolkit/js/toolkit.js' %}"></script>
169
+ <script src="{% static 'netbox_toolkit_plugin/js/toolkit.js' %}"></script>
170
170
  {% endblock %}
@@ -3,7 +3,7 @@
3
3
  {% load static %}
4
4
 
5
5
  {% block style %}
6
- <link href="{% static 'netbox_toolkit/css/toolkit.css' %}" rel="stylesheet">
6
+ <link href="{% static 'netbox_toolkit_plugin/css/toolkit.css' %}" rel="stylesheet">
7
7
  {% endblock %}
8
8
 
9
9
  {% block content %}
@@ -179,7 +179,7 @@
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: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>
@@ -209,8 +209,8 @@
209
209
  {% else %}
210
210
  No platform assigned to this device.
211
211
  {% endif %}
212
- {% if perms.netbox_toolkit.add_command %}
213
- <a href="{% url 'plugins:netbox_toolkit:command_add' %}" class="alert-link">
212
+ {% if perms.netbox_toolkit_plugin.add_command %}
213
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_add' %}" class="alert-link">
214
214
  Add a command
215
215
  </a>
216
216
  {% endif %}
@@ -260,7 +260,7 @@
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:command_detail' pk=executed_command.pk %}"
263
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=executed_command.pk %}"
264
264
  class="text-decoration-none text-muted"
265
265
  title="View command details">
266
266
  {{ executed_command.name }}
@@ -532,5 +532,5 @@
532
532
 
533
533
  {% block javascript %}
534
534
  <!-- Load NetBox Toolkit consolidated JavaScript -->
535
- <script src="{% static 'netbox_toolkit/js/toolkit.js' %}"></script>
535
+ <script src="{% static 'netbox_toolkit_plugin/js/toolkit.js' %}"></script>
536
536
  {% endblock %}