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.
- netbox_toolkit/__init__.py +30 -0
- netbox_toolkit/admin.py +16 -0
- netbox_toolkit/api/__init__.py +0 -0
- netbox_toolkit/api/mixins.py +54 -0
- netbox_toolkit/api/schemas.py +234 -0
- netbox_toolkit/api/serializers.py +158 -0
- netbox_toolkit/api/urls.py +10 -0
- netbox_toolkit/api/views/__init__.py +10 -0
- netbox_toolkit/api/views/command_logs.py +170 -0
- netbox_toolkit/api/views/commands.py +267 -0
- netbox_toolkit/config.py +159 -0
- netbox_toolkit/connectors/__init__.py +15 -0
- netbox_toolkit/connectors/base.py +97 -0
- netbox_toolkit/connectors/factory.py +301 -0
- netbox_toolkit/connectors/netmiko_connector.py +443 -0
- netbox_toolkit/connectors/scrapli_connector.py +486 -0
- netbox_toolkit/exceptions.py +36 -0
- netbox_toolkit/filtersets.py +85 -0
- netbox_toolkit/forms.py +31 -0
- netbox_toolkit/migrations/0001_initial.py +54 -0
- netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +66 -0
- netbox_toolkit/migrations/0003_permission_system_update.py +48 -0
- netbox_toolkit/migrations/0004_remove_django_permissions.py +77 -0
- netbox_toolkit/migrations/0005_alter_command_options_and_more.py +25 -0
- netbox_toolkit/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +28 -0
- netbox_toolkit/migrations/0007_alter_commandlog_parsing_template.py +18 -0
- netbox_toolkit/migrations/__init__.py +0 -0
- netbox_toolkit/models.py +89 -0
- netbox_toolkit/navigation.py +30 -0
- netbox_toolkit/search.py +21 -0
- netbox_toolkit/services/__init__.py +7 -0
- netbox_toolkit/services/command_service.py +357 -0
- netbox_toolkit/services/device_service.py +87 -0
- netbox_toolkit/services/rate_limiting_service.py +228 -0
- netbox_toolkit/static/netbox_toolkit/css/toolkit.css +143 -0
- netbox_toolkit/static/netbox_toolkit/js/toolkit.js +657 -0
- netbox_toolkit/tables.py +37 -0
- netbox_toolkit/templates/netbox_toolkit/command.html +108 -0
- netbox_toolkit/templates/netbox_toolkit/command_edit.html +10 -0
- netbox_toolkit/templates/netbox_toolkit/command_list.html +12 -0
- netbox_toolkit/templates/netbox_toolkit/commandlog.html +170 -0
- netbox_toolkit/templates/netbox_toolkit/commandlog_list.html +4 -0
- netbox_toolkit/templates/netbox_toolkit/device_toolkit.html +536 -0
- netbox_toolkit/urls.py +22 -0
- netbox_toolkit/utils/__init__.py +1 -0
- netbox_toolkit/utils/connection.py +125 -0
- netbox_toolkit/utils/error_parser.py +428 -0
- netbox_toolkit/utils/logging.py +58 -0
- netbox_toolkit/utils/network.py +157 -0
- netbox_toolkit/views.py +385 -0
- netbox_toolkit_plugin-0.1.0.dist-info/METADATA +76 -0
- netbox_toolkit_plugin-0.1.0.dist-info/RECORD +56 -0
- netbox_toolkit_plugin-0.1.0.dist-info/WHEEL +5 -0
- netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- netbox_toolkit_plugin-0.1.0.dist-info/licenses/LICENSE +200 -0
- netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,657 @@
|
|
1
|
+
/**
|
2
|
+
* NetBox Toolkit Plugin - Consolidated JavaScript
|
3
|
+
*
|
4
|
+
* This file contains all the JavaScript functionality for the NetBox Toolkit plugin
|
5
|
+
* to avoid duplication across templates and improve maintainability.
|
6
|
+
*/
|
7
|
+
|
8
|
+
// Namespace for the toolkit functionality
|
9
|
+
window.NetBoxToolkit = window.NetBoxToolkit || {};
|
10
|
+
|
11
|
+
(function(Toolkit) {
|
12
|
+
'use strict';
|
13
|
+
|
14
|
+
// Prevent multiple initialization
|
15
|
+
if (Toolkit.initialized) {
|
16
|
+
console.log('NetBox Toolkit already initialized, skipping');
|
17
|
+
return;
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Utility functions
|
22
|
+
*/
|
23
|
+
Toolkit.Utils = {
|
24
|
+
/**
|
25
|
+
* Show success state on a button temporarily
|
26
|
+
*/
|
27
|
+
showButtonSuccess: function(btn, successText = '<i class="mdi mdi-check me-1"></i>Copied!', duration = 2000) {
|
28
|
+
const originalText = btn.innerHTML;
|
29
|
+
const originalClass = btn.className;
|
30
|
+
|
31
|
+
btn.classList.add('copied');
|
32
|
+
btn.innerHTML = successText;
|
33
|
+
btn.style.backgroundColor = 'var(--tblr-success)';
|
34
|
+
btn.style.borderColor = 'var(--tblr-success)';
|
35
|
+
btn.style.color = 'white';
|
36
|
+
|
37
|
+
setTimeout(() => {
|
38
|
+
btn.className = originalClass;
|
39
|
+
btn.innerHTML = originalText;
|
40
|
+
btn.style.backgroundColor = '';
|
41
|
+
btn.style.borderColor = '';
|
42
|
+
btn.style.color = '';
|
43
|
+
}, duration);
|
44
|
+
},
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Fallback text copy using document.execCommand (legacy browsers)
|
48
|
+
*/
|
49
|
+
fallbackCopyText: function(text, btn) {
|
50
|
+
const textArea = document.createElement('textarea');
|
51
|
+
textArea.value = text;
|
52
|
+
textArea.style.position = 'fixed';
|
53
|
+
textArea.style.left = '-999999px';
|
54
|
+
textArea.style.top = '-999999px';
|
55
|
+
document.body.appendChild(textArea);
|
56
|
+
|
57
|
+
try {
|
58
|
+
textArea.focus();
|
59
|
+
textArea.select();
|
60
|
+
const successful = document.execCommand('copy');
|
61
|
+
if (successful) {
|
62
|
+
this.showButtonSuccess(btn);
|
63
|
+
} else {
|
64
|
+
console.error('Fallback copy command failed');
|
65
|
+
alert('Failed to copy to clipboard');
|
66
|
+
}
|
67
|
+
} catch (err) {
|
68
|
+
console.error('Fallback copy failed:', err);
|
69
|
+
alert('Failed to copy to clipboard');
|
70
|
+
} finally {
|
71
|
+
document.body.removeChild(textArea);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
};
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Copy functionality for both parsed data and raw output
|
78
|
+
* Works with multiple element ID patterns for flexibility
|
79
|
+
*/
|
80
|
+
Toolkit.CopyManager = {
|
81
|
+
/**
|
82
|
+
* Initialize copy functionality for both parsed data and raw output buttons
|
83
|
+
*/
|
84
|
+
init: function() {
|
85
|
+
// Initialize parsed data copy buttons
|
86
|
+
const copyParsedBtns = document.querySelectorAll('.copy-parsed-btn');
|
87
|
+
copyParsedBtns.forEach(btn => {
|
88
|
+
btn.addEventListener('click', this.handleCopyParsedData.bind(this));
|
89
|
+
});
|
90
|
+
|
91
|
+
// Initialize raw output copy buttons
|
92
|
+
const copyOutputBtns = document.querySelectorAll('.copy-output-btn');
|
93
|
+
copyOutputBtns.forEach(btn => {
|
94
|
+
btn.addEventListener('click', this.handleCopyRawOutput.bind(this));
|
95
|
+
});
|
96
|
+
},
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Handle copying raw command output from pre elements
|
100
|
+
*/
|
101
|
+
handleCopyRawOutput: function(event) {
|
102
|
+
const btn = event.target.closest('.copy-output-btn');
|
103
|
+
if (!btn) return;
|
104
|
+
|
105
|
+
// Find the command output element
|
106
|
+
// Look for .command-output within the same tab pane or nearby
|
107
|
+
const tabPane = btn.closest('.tab-pane') || btn.closest('.card-body') || document;
|
108
|
+
const outputElement = tabPane.querySelector('.command-output');
|
109
|
+
|
110
|
+
if (!outputElement) {
|
111
|
+
console.error('No command output element found');
|
112
|
+
alert('No command output found to copy');
|
113
|
+
return;
|
114
|
+
}
|
115
|
+
|
116
|
+
const outputText = outputElement.textContent || outputElement.innerText;
|
117
|
+
if (!outputText || !outputText.trim()) {
|
118
|
+
console.error('No command output text found');
|
119
|
+
alert('No command output available to copy');
|
120
|
+
return;
|
121
|
+
}
|
122
|
+
|
123
|
+
// Use modern Clipboard API if available
|
124
|
+
if (navigator.clipboard && window.isSecureContext) {
|
125
|
+
navigator.clipboard.writeText(outputText.trim()).then(() => {
|
126
|
+
Toolkit.Utils.showButtonSuccess(btn);
|
127
|
+
}).catch(err => {
|
128
|
+
console.error('Failed to copy using Clipboard API:', err);
|
129
|
+
Toolkit.Utils.fallbackCopyText(outputText.trim(), btn);
|
130
|
+
});
|
131
|
+
} else {
|
132
|
+
// Fallback for older browsers or non-secure contexts
|
133
|
+
Toolkit.Utils.fallbackCopyText(outputText.trim(), btn);
|
134
|
+
}
|
135
|
+
},
|
136
|
+
|
137
|
+
/**
|
138
|
+
* Handle copying parsed data from JSON script elements
|
139
|
+
*/
|
140
|
+
handleCopyParsedData: function(event) {
|
141
|
+
const btn = event.target.closest('.copy-parsed-btn');
|
142
|
+
if (!btn) return;
|
143
|
+
|
144
|
+
// Try multiple possible element IDs for parsed data
|
145
|
+
const possibleIds = [
|
146
|
+
'parsed-data-json', // device_toolkit.html
|
147
|
+
'commandlog-parsed-data-json' // commandlog.html
|
148
|
+
];
|
149
|
+
|
150
|
+
let parsedDataElement = null;
|
151
|
+
for (const id of possibleIds) {
|
152
|
+
parsedDataElement = document.getElementById(id);
|
153
|
+
if (parsedDataElement) break;
|
154
|
+
}
|
155
|
+
|
156
|
+
if (!parsedDataElement) {
|
157
|
+
console.error('No parsed data script element found with IDs:', possibleIds);
|
158
|
+
alert('No parsed data found to copy');
|
159
|
+
return;
|
160
|
+
}
|
161
|
+
|
162
|
+
const parsedDataStr = parsedDataElement.textContent;
|
163
|
+
if (!parsedDataStr) {
|
164
|
+
console.error('No parsed data found to copy');
|
165
|
+
alert('No parsed data available');
|
166
|
+
return;
|
167
|
+
}
|
168
|
+
|
169
|
+
try {
|
170
|
+
// Parse and re-stringify for clean formatting
|
171
|
+
const parsedData = JSON.parse(parsedDataStr);
|
172
|
+
const formattedJson = JSON.stringify(parsedData, null, 2);
|
173
|
+
|
174
|
+
// Use modern Clipboard API if available
|
175
|
+
if (navigator.clipboard && window.isSecureContext) {
|
176
|
+
navigator.clipboard.writeText(formattedJson).then(() => {
|
177
|
+
Toolkit.Utils.showButtonSuccess(btn);
|
178
|
+
}).catch(err => {
|
179
|
+
console.error('Failed to copy using Clipboard API:', err);
|
180
|
+
Toolkit.Utils.fallbackCopyText(formattedJson, btn);
|
181
|
+
});
|
182
|
+
} else {
|
183
|
+
// Fallback for older browsers or non-secure contexts
|
184
|
+
Toolkit.Utils.fallbackCopyText(formattedJson, btn);
|
185
|
+
}
|
186
|
+
} catch (err) {
|
187
|
+
console.error('Error processing parsed data:', err);
|
188
|
+
alert('Failed to process parsed data for copying: ' + err.message);
|
189
|
+
}
|
190
|
+
}
|
191
|
+
};
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Modal management for device toolkit
|
195
|
+
*/
|
196
|
+
Toolkit.ModalManager = {
|
197
|
+
instance: null,
|
198
|
+
|
199
|
+
/**
|
200
|
+
* Initialize modal functionality
|
201
|
+
*/
|
202
|
+
init: function() {
|
203
|
+
const credentialModal = document.getElementById('credentialModal');
|
204
|
+
if (!credentialModal) return;
|
205
|
+
|
206
|
+
// Try Bootstrap modal first, fallback to manual control
|
207
|
+
try {
|
208
|
+
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
209
|
+
this.instance = new bootstrap.Modal(credentialModal);
|
210
|
+
console.log('Bootstrap modal initialized successfully');
|
211
|
+
} else {
|
212
|
+
console.log('Bootstrap not available, using manual modal control');
|
213
|
+
this.instance = this.createManualModal(credentialModal);
|
214
|
+
}
|
215
|
+
} catch (error) {
|
216
|
+
console.error('Bootstrap modal initialization failed:', error);
|
217
|
+
this.instance = this.createManualModal(credentialModal);
|
218
|
+
}
|
219
|
+
|
220
|
+
this.setupModalEvents(credentialModal);
|
221
|
+
},
|
222
|
+
|
223
|
+
/**
|
224
|
+
* Create manual modal controls for fallback
|
225
|
+
*/
|
226
|
+
createManualModal: function(modalElement) {
|
227
|
+
return {
|
228
|
+
show: function() {
|
229
|
+
modalElement.style.display = 'block';
|
230
|
+
modalElement.classList.add('show');
|
231
|
+
document.body.classList.add('modal-open');
|
232
|
+
|
233
|
+
// Create backdrop
|
234
|
+
const backdrop = document.createElement('div');
|
235
|
+
backdrop.className = 'modal-backdrop fade show';
|
236
|
+
backdrop.id = 'credentialModalBackdrop';
|
237
|
+
document.body.appendChild(backdrop);
|
238
|
+
|
239
|
+
// Trigger shown event
|
240
|
+
const shownEvent = new Event('shown.bs.modal');
|
241
|
+
modalElement.dispatchEvent(shownEvent);
|
242
|
+
},
|
243
|
+
hide: function() {
|
244
|
+
modalElement.style.display = 'none';
|
245
|
+
modalElement.classList.remove('show');
|
246
|
+
document.body.classList.remove('modal-open');
|
247
|
+
|
248
|
+
// Remove backdrop
|
249
|
+
const backdrop = document.getElementById('credentialModalBackdrop');
|
250
|
+
if (backdrop) {
|
251
|
+
backdrop.remove();
|
252
|
+
}
|
253
|
+
|
254
|
+
// Trigger hidden event
|
255
|
+
const hiddenEvent = new Event('hidden.bs.modal');
|
256
|
+
modalElement.dispatchEvent(hiddenEvent);
|
257
|
+
}
|
258
|
+
};
|
259
|
+
},
|
260
|
+
|
261
|
+
/**
|
262
|
+
* Setup modal event handlers
|
263
|
+
*/
|
264
|
+
setupModalEvents: function(credentialModal) {
|
265
|
+
// Handle close button clicks
|
266
|
+
const modalCloseButton = credentialModal.querySelector('.btn-close');
|
267
|
+
const modalCancelButton = credentialModal.querySelector('.btn-secondary');
|
268
|
+
|
269
|
+
if (modalCloseButton) {
|
270
|
+
modalCloseButton.addEventListener('click', (event) => {
|
271
|
+
event.preventDefault();
|
272
|
+
if (this.instance) {
|
273
|
+
this.instance.hide();
|
274
|
+
}
|
275
|
+
});
|
276
|
+
}
|
277
|
+
|
278
|
+
if (modalCancelButton) {
|
279
|
+
modalCancelButton.addEventListener('click', (event) => {
|
280
|
+
event.preventDefault();
|
281
|
+
if (this.instance) {
|
282
|
+
this.instance.hide();
|
283
|
+
}
|
284
|
+
});
|
285
|
+
}
|
286
|
+
|
287
|
+
// Handle backdrop clicks for manual modal
|
288
|
+
credentialModal.addEventListener('click', (event) => {
|
289
|
+
if (event.target === credentialModal && this.instance) {
|
290
|
+
this.instance.hide();
|
291
|
+
}
|
292
|
+
});
|
293
|
+
},
|
294
|
+
|
295
|
+
/**
|
296
|
+
* Show the modal
|
297
|
+
*/
|
298
|
+
show: function() {
|
299
|
+
if (this.instance) {
|
300
|
+
this.instance.show();
|
301
|
+
}
|
302
|
+
},
|
303
|
+
|
304
|
+
/**
|
305
|
+
* Hide the modal
|
306
|
+
*/
|
307
|
+
hide: function() {
|
308
|
+
if (this.instance) {
|
309
|
+
this.instance.hide();
|
310
|
+
}
|
311
|
+
}
|
312
|
+
};
|
313
|
+
|
314
|
+
/**
|
315
|
+
* Command execution functionality for device toolkit
|
316
|
+
*/
|
317
|
+
Toolkit.CommandManager = {
|
318
|
+
currentCommandData: null,
|
319
|
+
|
320
|
+
/**
|
321
|
+
* Initialize command execution functionality
|
322
|
+
*/
|
323
|
+
init: function() {
|
324
|
+
// Initialize modal first
|
325
|
+
Toolkit.ModalManager.init();
|
326
|
+
|
327
|
+
// Setup collapse toggle for connection info
|
328
|
+
this.setupConnectionInfoToggle();
|
329
|
+
|
330
|
+
// Setup command execution
|
331
|
+
this.setupCommandExecution();
|
332
|
+
|
333
|
+
// Setup modal form handlers
|
334
|
+
this.setupModalForm();
|
335
|
+
},
|
336
|
+
|
337
|
+
/**
|
338
|
+
* Setup connection info collapse toggle
|
339
|
+
*/
|
340
|
+
setupConnectionInfoToggle: function() {
|
341
|
+
const connectionInfoCollapse = document.getElementById('connectionInfoCollapse');
|
342
|
+
const connectionInfoToggleButton = document.querySelector('[data-bs-target="#connectionInfoCollapse"]');
|
343
|
+
|
344
|
+
if (connectionInfoCollapse && connectionInfoToggleButton) {
|
345
|
+
connectionInfoCollapse.addEventListener('hidden.bs.collapse', function () {
|
346
|
+
connectionInfoToggleButton.classList.add('collapsed');
|
347
|
+
});
|
348
|
+
|
349
|
+
connectionInfoCollapse.addEventListener('shown.bs.collapse', function () {
|
350
|
+
connectionInfoToggleButton.classList.remove('collapsed');
|
351
|
+
});
|
352
|
+
}
|
353
|
+
},
|
354
|
+
|
355
|
+
/**
|
356
|
+
* Setup command execution event handlers
|
357
|
+
*/
|
358
|
+
setupCommandExecution: function() {
|
359
|
+
const commandContainer = document.querySelector('.card-commands');
|
360
|
+
if (!commandContainer) {
|
361
|
+
console.error('Command container not found');
|
362
|
+
return;
|
363
|
+
}
|
364
|
+
|
365
|
+
// Use event delegation to avoid duplicate listeners
|
366
|
+
commandContainer.removeEventListener('click', this.handleRunButtonClick.bind(this));
|
367
|
+
commandContainer.addEventListener('click', this.handleRunButtonClick.bind(this));
|
368
|
+
},
|
369
|
+
|
370
|
+
/**
|
371
|
+
* Handle run button clicks
|
372
|
+
*/
|
373
|
+
handleRunButtonClick: function(event) {
|
374
|
+
// Only handle clicks on run buttons
|
375
|
+
const runButton = event.target.closest('.command-run-btn');
|
376
|
+
if (!runButton) return;
|
377
|
+
|
378
|
+
// Prevent any other event handlers from running
|
379
|
+
event.preventDefault();
|
380
|
+
event.stopImmediatePropagation();
|
381
|
+
|
382
|
+
console.log('Run button clicked:', runButton);
|
383
|
+
|
384
|
+
// Prevent double-clicks by checking if already processing
|
385
|
+
if (runButton.dataset.processing === 'true') {
|
386
|
+
console.log('Command already processing, ignoring click');
|
387
|
+
return;
|
388
|
+
}
|
389
|
+
|
390
|
+
// Get the command item and set active state
|
391
|
+
const commandItem = runButton.closest('.command-item');
|
392
|
+
const allCommandItems = document.querySelectorAll('.command-item');
|
393
|
+
allCommandItems.forEach(ci => ci.classList.remove('active'));
|
394
|
+
commandItem.classList.add('active');
|
395
|
+
|
396
|
+
// Store command data for modal
|
397
|
+
const commandId = runButton.getAttribute('data-command-id');
|
398
|
+
const commandName = runButton.getAttribute('data-command-name');
|
399
|
+
|
400
|
+
this.currentCommandData = {
|
401
|
+
id: commandId,
|
402
|
+
name: commandName,
|
403
|
+
element: commandItem,
|
404
|
+
button: runButton,
|
405
|
+
originalIconClass: runButton.querySelector('i').className
|
406
|
+
};
|
407
|
+
|
408
|
+
// Update modal content
|
409
|
+
const commandToExecuteElement = document.getElementById('commandToExecute');
|
410
|
+
if (commandToExecuteElement) {
|
411
|
+
commandToExecuteElement.textContent = commandName;
|
412
|
+
}
|
413
|
+
|
414
|
+
// Clear previous credentials and show modal
|
415
|
+
const modalUsernameField = document.getElementById('modalUsername');
|
416
|
+
const modalPasswordField = document.getElementById('modalPassword');
|
417
|
+
if (modalUsernameField) modalUsernameField.value = '';
|
418
|
+
if (modalPasswordField) modalPasswordField.value = '';
|
419
|
+
|
420
|
+
// Show the modal
|
421
|
+
Toolkit.ModalManager.show();
|
422
|
+
|
423
|
+
// Focus on username field when modal is shown
|
424
|
+
const credentialModal = document.getElementById('credentialModal');
|
425
|
+
if (credentialModal && modalUsernameField) {
|
426
|
+
credentialModal.addEventListener('shown.bs.modal', function () {
|
427
|
+
modalUsernameField.focus();
|
428
|
+
}, { once: true });
|
429
|
+
}
|
430
|
+
},
|
431
|
+
|
432
|
+
/**
|
433
|
+
* Setup modal form handlers
|
434
|
+
*/
|
435
|
+
setupModalForm: function() {
|
436
|
+
const executeCommandBtn = document.getElementById('executeCommandBtn');
|
437
|
+
const modalUsernameField = document.getElementById('modalUsername');
|
438
|
+
const modalPasswordField = document.getElementById('modalPassword');
|
439
|
+
const credentialModal = document.getElementById('credentialModal');
|
440
|
+
|
441
|
+
if (!executeCommandBtn) return;
|
442
|
+
|
443
|
+
// Handle execute button click in modal
|
444
|
+
executeCommandBtn.addEventListener('click', this.executeCommand.bind(this));
|
445
|
+
|
446
|
+
// Handle Enter key in modal form
|
447
|
+
if (modalPasswordField) {
|
448
|
+
modalPasswordField.addEventListener('keypress', (event) => {
|
449
|
+
if (event.key === 'Enter') {
|
450
|
+
event.preventDefault();
|
451
|
+
executeCommandBtn.click();
|
452
|
+
}
|
453
|
+
});
|
454
|
+
}
|
455
|
+
|
456
|
+
if (modalUsernameField) {
|
457
|
+
modalUsernameField.addEventListener('keypress', (event) => {
|
458
|
+
if (event.key === 'Enter') {
|
459
|
+
event.preventDefault();
|
460
|
+
if (modalPasswordField) {
|
461
|
+
modalPasswordField.focus();
|
462
|
+
}
|
463
|
+
}
|
464
|
+
});
|
465
|
+
}
|
466
|
+
|
467
|
+
// Clean up when modal is hidden
|
468
|
+
if (credentialModal) {
|
469
|
+
credentialModal.addEventListener('hidden.bs.modal', () => {
|
470
|
+
if (this.currentCommandData && this.currentCommandData.button.dataset.processing !== 'true') {
|
471
|
+
this.currentCommandData.element.classList.remove('active');
|
472
|
+
}
|
473
|
+
this.currentCommandData = null;
|
474
|
+
if (modalUsernameField) modalUsernameField.value = '';
|
475
|
+
if (modalPasswordField) modalPasswordField.value = '';
|
476
|
+
});
|
477
|
+
}
|
478
|
+
},
|
479
|
+
|
480
|
+
/**
|
481
|
+
* Execute the selected command
|
482
|
+
*/
|
483
|
+
executeCommand: function() {
|
484
|
+
const modalUsernameField = document.getElementById('modalUsername');
|
485
|
+
const modalPasswordField = document.getElementById('modalPassword');
|
486
|
+
|
487
|
+
// Validate credentials
|
488
|
+
if (!modalUsernameField?.value || !modalPasswordField?.value) {
|
489
|
+
alert('Please enter both username and password');
|
490
|
+
return;
|
491
|
+
}
|
492
|
+
|
493
|
+
if (!this.currentCommandData) {
|
494
|
+
alert('No command selected');
|
495
|
+
return;
|
496
|
+
}
|
497
|
+
|
498
|
+
console.log('Executing command:', this.currentCommandData);
|
499
|
+
|
500
|
+
// Set processing flag and update UI
|
501
|
+
this.setCommandProcessing(true);
|
502
|
+
|
503
|
+
// Update output area
|
504
|
+
this.showCommandRunning();
|
505
|
+
|
506
|
+
// Prepare and submit form
|
507
|
+
this.submitCommandForm();
|
508
|
+
},
|
509
|
+
|
510
|
+
/**
|
511
|
+
* Set command processing state
|
512
|
+
*/
|
513
|
+
setCommandProcessing: function(processing) {
|
514
|
+
if (!this.currentCommandData) return;
|
515
|
+
|
516
|
+
const button = this.currentCommandData.button;
|
517
|
+
const buttonIcon = button.querySelector('i');
|
518
|
+
|
519
|
+
button.dataset.processing = processing.toString();
|
520
|
+
button.disabled = processing;
|
521
|
+
|
522
|
+
if (processing) {
|
523
|
+
buttonIcon.className = 'mdi mdi-loading mdi-spin';
|
524
|
+
} else {
|
525
|
+
buttonIcon.className = this.currentCommandData.originalIconClass;
|
526
|
+
}
|
527
|
+
},
|
528
|
+
|
529
|
+
/**
|
530
|
+
* Show command running state in output area
|
531
|
+
*/
|
532
|
+
showCommandRunning: function() {
|
533
|
+
const outputContainer = document.getElementById('commandOutputContainer');
|
534
|
+
const commandHeader = document.querySelector('.output-card .card-header span.text-muted');
|
535
|
+
|
536
|
+
if (outputContainer) {
|
537
|
+
outputContainer.innerHTML = `
|
538
|
+
<div class="alert alert-primary d-flex align-items-center mb-0">
|
539
|
+
<div class="spinner-border spinner-border-sm me-3" role="status" aria-hidden="true" style="width: 1rem; height: 1rem; border-width: 0.125em;"></div>
|
540
|
+
<div>
|
541
|
+
<strong>Running command:</strong> ${this.currentCommandData.name}<br>
|
542
|
+
<small class="text-muted">Please wait while the command executes...</small>
|
543
|
+
</div>
|
544
|
+
</div>
|
545
|
+
`;
|
546
|
+
}
|
547
|
+
|
548
|
+
// Update command header to show executing command
|
549
|
+
if (commandHeader) {
|
550
|
+
commandHeader.textContent = `Executing: ${this.currentCommandData.name}`;
|
551
|
+
}
|
552
|
+
},
|
553
|
+
|
554
|
+
/**
|
555
|
+
* Submit the command execution form
|
556
|
+
*/
|
557
|
+
submitCommandForm: function() {
|
558
|
+
const commandForm = document.getElementById('commandExecutionForm');
|
559
|
+
const selectedCommandIdField = document.getElementById('selectedCommandId');
|
560
|
+
const formUsernameField = document.getElementById('formUsername');
|
561
|
+
const formPasswordField = document.getElementById('formPassword');
|
562
|
+
const modalUsernameField = document.getElementById('modalUsername');
|
563
|
+
const modalPasswordField = document.getElementById('modalPassword');
|
564
|
+
|
565
|
+
if (!commandForm) {
|
566
|
+
console.error('Command form not found');
|
567
|
+
this.handleSubmissionError('Form not found');
|
568
|
+
return;
|
569
|
+
}
|
570
|
+
|
571
|
+
// Prepare form data
|
572
|
+
if (selectedCommandIdField) selectedCommandIdField.value = this.currentCommandData.id;
|
573
|
+
if (formUsernameField) formUsernameField.value = modalUsernameField.value;
|
574
|
+
if (formPasswordField) formPasswordField.value = modalPasswordField.value;
|
575
|
+
|
576
|
+
console.log('Form data:', {
|
577
|
+
commandId: selectedCommandIdField?.value,
|
578
|
+
username: formUsernameField?.value,
|
579
|
+
hasPassword: !!formPasswordField?.value
|
580
|
+
});
|
581
|
+
|
582
|
+
// Close modal
|
583
|
+
Toolkit.ModalManager.hide();
|
584
|
+
|
585
|
+
try {
|
586
|
+
commandForm.submit();
|
587
|
+
} catch (error) {
|
588
|
+
console.error('Error submitting form:', error);
|
589
|
+
this.handleSubmissionError('Error submitting form. Please check the console for details.');
|
590
|
+
}
|
591
|
+
},
|
592
|
+
|
593
|
+
/**
|
594
|
+
* Handle form submission errors
|
595
|
+
*/
|
596
|
+
handleSubmissionError: function(message) {
|
597
|
+
alert(message);
|
598
|
+
|
599
|
+
// Reset processing state
|
600
|
+
this.setCommandProcessing(false);
|
601
|
+
|
602
|
+
// Show error in output container
|
603
|
+
const outputContainer = document.getElementById('commandOutputContainer');
|
604
|
+
const commandHeader = document.querySelector('.output-card .card-header span.text-muted');
|
605
|
+
|
606
|
+
if (outputContainer) {
|
607
|
+
outputContainer.innerHTML = `
|
608
|
+
<div class="alert alert-danger d-flex align-items-start mb-3">
|
609
|
+
<i class="mdi mdi-alert-circle me-2 mt-1"></i>
|
610
|
+
<div>
|
611
|
+
<strong>Form submission error</strong>
|
612
|
+
<br><small class="text-muted">${message}</small>
|
613
|
+
</div>
|
614
|
+
</div>
|
615
|
+
<div class="alert alert-info" id="defaultMessage">
|
616
|
+
Select a command to execute from the list on the left.
|
617
|
+
</div>
|
618
|
+
`;
|
619
|
+
}
|
620
|
+
|
621
|
+
// Clear command header
|
622
|
+
if (commandHeader) {
|
623
|
+
commandHeader.textContent = '';
|
624
|
+
}
|
625
|
+
}
|
626
|
+
};
|
627
|
+
|
628
|
+
/**
|
629
|
+
* Main initialization function
|
630
|
+
*/
|
631
|
+
Toolkit.init = function() {
|
632
|
+
console.log('Initializing NetBox Toolkit JavaScript');
|
633
|
+
|
634
|
+
// Initialize copy functionality (available on all pages)
|
635
|
+
this.CopyManager.init();
|
636
|
+
|
637
|
+
// Initialize command functionality only if elements exist (device toolkit page)
|
638
|
+
const commandForm = document.getElementById('commandExecutionForm');
|
639
|
+
if (commandForm) {
|
640
|
+
this.CommandManager.init();
|
641
|
+
}
|
642
|
+
|
643
|
+
this.initialized = true;
|
644
|
+
console.log('NetBox Toolkit JavaScript initialized successfully');
|
645
|
+
};
|
646
|
+
|
647
|
+
// Auto-initialize when DOM is ready
|
648
|
+
if (document.readyState === 'loading') {
|
649
|
+
document.addEventListener('DOMContentLoaded', function() {
|
650
|
+
Toolkit.init();
|
651
|
+
});
|
652
|
+
} else {
|
653
|
+
// DOM is already ready
|
654
|
+
Toolkit.init();
|
655
|
+
}
|
656
|
+
|
657
|
+
})(window.NetBoxToolkit);
|
netbox_toolkit/tables.py
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
import django_tables2 as tables
|
2
|
+
from netbox.tables import NetBoxTable, columns
|
3
|
+
from .models import Command, CommandLog
|
4
|
+
|
5
|
+
class CommandTable(NetBoxTable):
|
6
|
+
name = tables.Column(
|
7
|
+
linkify=('plugins:netbox_toolkit:command_detail', [tables.A('pk')])
|
8
|
+
)
|
9
|
+
platform = tables.Column(
|
10
|
+
linkify=True
|
11
|
+
)
|
12
|
+
command_type = tables.Column()
|
13
|
+
|
14
|
+
class Meta(NetBoxTable.Meta):
|
15
|
+
model = Command
|
16
|
+
fields = ('pk', 'id', 'name', 'platform', 'command_type', 'description')
|
17
|
+
default_columns = ('pk', 'name', 'platform', 'command_type', 'description')
|
18
|
+
|
19
|
+
class CommandLogTable(NetBoxTable):
|
20
|
+
command = tables.Column(
|
21
|
+
linkify=('plugins:netbox_toolkit:command_detail', [tables.A('command.pk')])
|
22
|
+
)
|
23
|
+
device = tables.Column(
|
24
|
+
linkify=True
|
25
|
+
)
|
26
|
+
success = tables.BooleanColumn(
|
27
|
+
verbose_name='Status',
|
28
|
+
yesno=('Success', 'Failed')
|
29
|
+
)
|
30
|
+
|
31
|
+
# Remove actions column entirely
|
32
|
+
actions = False
|
33
|
+
|
34
|
+
class Meta(NetBoxTable.Meta):
|
35
|
+
model = CommandLog
|
36
|
+
fields = ('pk', 'id', 'command', 'device', 'username', 'execution_time', 'success', 'execution_duration')
|
37
|
+
default_columns = ('pk', 'command', 'device', 'username', 'execution_time', 'success')
|