netbox-toolkit-plugin 0.1.0__py3-none-any.whl

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