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.
- netbox_toolkit_plugin/__init__.py +32 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/serializers.py +71 -35
- {netbox_toolkit → netbox_toolkit_plugin}/api/urls.py +3 -3
- {netbox_toolkit → netbox_toolkit_plugin}/config.py +80 -73
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/factory.py +170 -111
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/netmiko_connector.py +242 -179
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/scrapli_connector.py +256 -172
- netbox_toolkit_plugin/migrations/0001_initial.py +108 -0
- netbox_toolkit_plugin/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +70 -0
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0003_permission_system_update.py +26 -12
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0004_remove_django_permissions.py +27 -29
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0005_alter_command_options_and_more.py +7 -8
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +7 -8
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/0007_alter_commandlog_parsing_template.py +6 -4
- {netbox_toolkit → netbox_toolkit_plugin}/models.py +31 -32
- {netbox_toolkit → netbox_toolkit_plugin}/navigation.py +6 -6
- {netbox_toolkit → netbox_toolkit_plugin}/services/command_service.py +188 -128
- {netbox_toolkit → netbox_toolkit_plugin}/services/rate_limiting_service.py +104 -97
- netbox_toolkit_plugin/tables.py +51 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/command.html +108 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/command_list.html +12 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/commandlog.html +170 -0
- netbox_toolkit_plugin/templates/netbox_toolkit/device_toolkit.html +557 -0
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command.html +5 -5
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_list.html +2 -2
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog.html +2 -2
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/device_toolkit.html +6 -6
- netbox_toolkit_plugin/urls.py +38 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/logging.py +20 -19
- {netbox_toolkit → netbox_toolkit_plugin}/views.py +251 -169
- {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/METADATA +2 -2
- netbox_toolkit_plugin-0.1.1.dist-info/RECORD +60 -0
- netbox_toolkit_plugin-0.1.1.dist-info/entry_points.txt +2 -0
- netbox_toolkit_plugin-0.1.1.dist-info/top_level.txt +1 -0
- netbox_toolkit/__init__.py +0 -30
- netbox_toolkit/migrations/0001_initial.py +0 -54
- netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +0 -66
- netbox_toolkit/tables.py +0 -37
- netbox_toolkit/urls.py +0 -22
- netbox_toolkit_plugin-0.1.0.dist-info/RECORD +0 -56
- netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +0 -2
- netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +0 -1
- {netbox_toolkit → netbox_toolkit_plugin}/admin.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/mixins.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/schemas.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/views/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/views/command_logs.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/api/views/commands.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/connectors/base.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/exceptions.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/filtersets.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/forms.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/migrations/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/search.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/services/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/services/device_service.py +0 -0
- {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/css/toolkit.css +0 -0
- {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/js/toolkit.js +0 -0
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_edit.html +0 -0
- {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog_list.html +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/__init__.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/connection.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/error_parser.py +0 -0
- {netbox_toolkit → netbox_toolkit_plugin}/utils/network.py +0 -0
- {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/WHEEL +0 -0
- {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -7,38 +7,40 @@ from ..models import CommandLog
|
|
7
7
|
|
8
8
|
class RateLimitingService:
|
9
9
|
"""Service for managing command execution rate limiting"""
|
10
|
-
|
10
|
+
|
11
11
|
def __init__(self):
|
12
12
|
"""Initialize the rate limiting service with plugin settings"""
|
13
|
-
self.plugin_settings = getattr(settings,
|
14
|
-
|
13
|
+
self.plugin_settings = getattr(settings, "PLUGINS_CONFIG", {}).get(
|
14
|
+
"netbox_toolkit_plugin", {}
|
15
|
+
)
|
16
|
+
|
15
17
|
def is_rate_limiting_enabled(self):
|
16
18
|
"""Check if rate limiting is enabled in plugin settings"""
|
17
|
-
return self.plugin_settings.get(
|
18
|
-
|
19
|
+
return self.plugin_settings.get("rate_limiting_enabled", False)
|
20
|
+
|
19
21
|
def get_device_command_limit(self):
|
20
22
|
"""Get the maximum number of commands allowed per device within the time window"""
|
21
|
-
return self.plugin_settings.get(
|
22
|
-
|
23
|
+
return self.plugin_settings.get("device_command_limit", 10)
|
24
|
+
|
23
25
|
def get_time_window_minutes(self):
|
24
26
|
"""Get the time window in minutes for rate limiting"""
|
25
|
-
return self.plugin_settings.get(
|
26
|
-
|
27
|
+
return self.plugin_settings.get("time_window_minutes", 5)
|
28
|
+
|
27
29
|
def get_bypass_users(self):
|
28
30
|
"""Get list of usernames that bypass rate limiting"""
|
29
|
-
return self.plugin_settings.get(
|
30
|
-
|
31
|
+
return self.plugin_settings.get("bypass_users", [])
|
32
|
+
|
31
33
|
def get_bypass_groups(self):
|
32
34
|
"""Get list of group names that bypass rate limiting"""
|
33
|
-
return self.plugin_settings.get(
|
34
|
-
|
35
|
+
return self.plugin_settings.get("bypass_groups", [])
|
36
|
+
|
35
37
|
def user_bypasses_rate_limiting(self, user):
|
36
38
|
"""
|
37
39
|
Check if a user bypasses rate limiting based on username or group membership
|
38
|
-
|
40
|
+
|
39
41
|
Args:
|
40
42
|
user: Django User object
|
41
|
-
|
43
|
+
|
42
44
|
Returns:
|
43
45
|
bool: True if user bypasses rate limiting, False otherwise
|
44
46
|
"""
|
@@ -46,49 +48,49 @@ class RateLimitingService:
|
|
46
48
|
bypass_users = self.get_bypass_users()
|
47
49
|
if user.username in bypass_users:
|
48
50
|
return True
|
49
|
-
|
51
|
+
|
50
52
|
# Check if user is in any bypass groups
|
51
53
|
bypass_groups = self.get_bypass_groups()
|
52
54
|
if bypass_groups:
|
53
|
-
user_groups = user.groups.values_list(
|
55
|
+
user_groups = user.groups.values_list("name", flat=True)
|
54
56
|
if any(group in bypass_groups for group in user_groups):
|
55
57
|
return True
|
56
|
-
|
58
|
+
|
57
59
|
return False
|
58
|
-
|
60
|
+
|
59
61
|
def get_recent_command_count(self, device, user=None):
|
60
62
|
"""
|
61
63
|
Get the number of successful commands executed on a device within the time window
|
62
|
-
|
64
|
+
|
63
65
|
Args:
|
64
66
|
device: Device object
|
65
67
|
user: Optional User object to count only commands by this user
|
66
|
-
|
68
|
+
|
67
69
|
Returns:
|
68
70
|
int: Number of recent successful commands
|
69
71
|
"""
|
70
72
|
time_window = self.get_time_window_minutes()
|
71
73
|
cutoff_time = timezone.now() - timedelta(minutes=time_window)
|
72
|
-
|
74
|
+
|
73
75
|
query = CommandLog.objects.filter(
|
74
76
|
device=device,
|
75
77
|
execution_time__gte=cutoff_time,
|
76
|
-
success=True # Only count successful commands
|
78
|
+
success=True, # Only count successful commands
|
77
79
|
)
|
78
|
-
|
80
|
+
|
79
81
|
if user:
|
80
82
|
query = query.filter(username=user.username)
|
81
|
-
|
83
|
+
|
82
84
|
return query.count()
|
83
|
-
|
85
|
+
|
84
86
|
def check_rate_limit(self, device, user):
|
85
87
|
"""
|
86
88
|
Check if a command execution would exceed rate limits
|
87
|
-
|
89
|
+
|
88
90
|
Args:
|
89
91
|
device: Device object to check rate limits for
|
90
92
|
user: User object attempting to execute the command
|
91
|
-
|
93
|
+
|
92
94
|
Returns:
|
93
95
|
dict: {
|
94
96
|
'allowed': bool,
|
@@ -101,128 +103,133 @@ class RateLimitingService:
|
|
101
103
|
# If rate limiting is disabled, always allow
|
102
104
|
if not self.is_rate_limiting_enabled():
|
103
105
|
return {
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
"allowed": True,
|
107
|
+
"current_count": 0,
|
108
|
+
"limit": self.get_device_command_limit(),
|
109
|
+
"time_window_minutes": self.get_time_window_minutes(),
|
110
|
+
"reason": "Rate limiting disabled",
|
109
111
|
}
|
110
|
-
|
112
|
+
|
111
113
|
# If user bypasses rate limiting, always allow
|
112
114
|
if self.user_bypasses_rate_limiting(user):
|
113
115
|
return {
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
116
|
+
"allowed": True,
|
117
|
+
"current_count": 0,
|
118
|
+
"limit": self.get_device_command_limit(),
|
119
|
+
"time_window_minutes": self.get_time_window_minutes(),
|
120
|
+
"reason": "User bypasses rate limiting",
|
119
121
|
}
|
120
|
-
|
122
|
+
|
121
123
|
# Check current command count
|
122
124
|
current_count = self.get_recent_command_count(device)
|
123
125
|
limit = self.get_device_command_limit()
|
124
126
|
time_window = self.get_time_window_minutes()
|
125
|
-
|
127
|
+
|
126
128
|
if current_count >= limit:
|
127
129
|
return {
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
130
|
+
"allowed": False,
|
131
|
+
"current_count": current_count,
|
132
|
+
"limit": limit,
|
133
|
+
"time_window_minutes": time_window,
|
134
|
+
"reason": f"Rate limit exceeded: {current_count}/{limit} successful commands in last {time_window} minutes",
|
133
135
|
}
|
134
|
-
|
136
|
+
|
135
137
|
return {
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
138
|
+
"allowed": True,
|
139
|
+
"current_count": current_count,
|
140
|
+
"limit": limit,
|
141
|
+
"time_window_minutes": time_window,
|
142
|
+
"reason": "Within rate limits",
|
141
143
|
}
|
142
|
-
|
144
|
+
|
143
145
|
def get_rate_limit_status(self, device, user):
|
144
146
|
"""
|
145
147
|
Get rate limit status for display in UI
|
146
|
-
|
148
|
+
|
147
149
|
Args:
|
148
150
|
device: Device object
|
149
151
|
user: User object
|
150
|
-
|
152
|
+
|
151
153
|
Returns:
|
152
154
|
dict: Rate limit status information for UI display
|
153
155
|
"""
|
154
156
|
if not self.is_rate_limiting_enabled():
|
155
|
-
return {
|
156
|
-
|
157
|
-
'message': 'Rate limiting is disabled'
|
158
|
-
}
|
159
|
-
|
157
|
+
return {"enabled": False, "message": "Rate limiting is disabled"}
|
158
|
+
|
160
159
|
if self.user_bypasses_rate_limiting(user):
|
161
160
|
return {
|
162
|
-
|
163
|
-
|
164
|
-
|
161
|
+
"enabled": True,
|
162
|
+
"bypassed": True,
|
163
|
+
"message": "You have unlimited command execution (bypass enabled)",
|
165
164
|
}
|
166
|
-
|
165
|
+
|
167
166
|
current_count = self.get_recent_command_count(device)
|
168
167
|
limit = self.get_device_command_limit()
|
169
168
|
time_window = self.get_time_window_minutes()
|
170
169
|
remaining = max(0, limit - current_count)
|
171
|
-
|
170
|
+
|
172
171
|
# Determine status and appropriate message
|
173
172
|
if current_count >= limit:
|
174
|
-
status =
|
173
|
+
status = "exceeded"
|
175
174
|
# Get time until reset for exceeded status
|
176
175
|
time_until_reset = self.get_time_until_reset(device)
|
177
176
|
if time_until_reset:
|
178
177
|
minutes_until_reset = int(time_until_reset.total_seconds() / 60) + 1
|
179
|
-
message = f
|
178
|
+
message = f"Rate limit exceeded! ({current_count}/{limit} successful commands) - Try again in {minutes_until_reset} minutes"
|
180
179
|
else:
|
181
|
-
message = f
|
182
|
-
elif
|
183
|
-
|
184
|
-
|
180
|
+
message = f"Rate limit exceeded! ({current_count}/{limit} successful commands in last {time_window} minutes)"
|
181
|
+
elif (
|
182
|
+
remaining <= 2 and current_count < limit
|
183
|
+
): # Only warning if we haven't exceeded the limit
|
184
|
+
status = "warning"
|
185
|
+
message = f"{remaining} commands remaining ({current_count}/{limit} successful commands in last {time_window} minutes)"
|
185
186
|
else:
|
186
|
-
status =
|
187
|
-
message = f
|
188
|
-
|
187
|
+
status = "normal"
|
188
|
+
message = f"{remaining} commands remaining ({current_count}/{limit} successful commands in last {time_window} minutes)"
|
189
|
+
|
189
190
|
return {
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
191
|
+
"enabled": True,
|
192
|
+
"bypassed": False,
|
193
|
+
"current_count": current_count,
|
194
|
+
"limit": limit,
|
195
|
+
"remaining": remaining,
|
196
|
+
"time_window_minutes": time_window,
|
197
|
+
"status": status,
|
198
|
+
"message": message,
|
199
|
+
"is_exceeded": current_count >= limit,
|
200
|
+
"is_warning": remaining <= 2 and current_count < limit,
|
201
|
+
"time_until_reset": self.get_time_until_reset(device)
|
202
|
+
if current_count >= limit
|
203
|
+
else None,
|
201
204
|
}
|
202
|
-
|
205
|
+
|
203
206
|
def get_time_until_reset(self, device):
|
204
207
|
"""
|
205
208
|
Get the time until the rate limit resets (oldest successful command in window expires)
|
206
|
-
|
209
|
+
|
207
210
|
Args:
|
208
211
|
device: Device object
|
209
|
-
|
212
|
+
|
210
213
|
Returns:
|
211
214
|
timedelta or None: Time until oldest successful command expires, None if no recent successful commands
|
212
215
|
"""
|
213
216
|
time_window = self.get_time_window_minutes()
|
214
217
|
cutoff_time = timezone.now() - timedelta(minutes=time_window)
|
215
|
-
|
216
|
-
oldest_command =
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
218
|
+
|
219
|
+
oldest_command = (
|
220
|
+
CommandLog.objects.filter(
|
221
|
+
device=device,
|
222
|
+
execution_time__gte=cutoff_time,
|
223
|
+
success=True, # Only consider successful commands
|
224
|
+
)
|
225
|
+
.order_by("execution_time")
|
226
|
+
.first()
|
227
|
+
)
|
228
|
+
|
222
229
|
if not oldest_command:
|
223
230
|
return None
|
224
|
-
|
231
|
+
|
225
232
|
reset_time = oldest_command.execution_time + timedelta(minutes=time_window)
|
226
233
|
time_until_reset = reset_time - timezone.now()
|
227
|
-
|
234
|
+
|
228
235
|
return time_until_reset if time_until_reset > timedelta(0) else None
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import django_tables2 as tables
|
2
|
+
from netbox.tables import NetBoxTable, columns
|
3
|
+
from .models import Command, CommandLog
|
4
|
+
|
5
|
+
|
6
|
+
class CommandTable(NetBoxTable):
|
7
|
+
name = tables.Column(
|
8
|
+
linkify=("plugins:netbox_toolkit_plugin:command_detail", [tables.A("pk")])
|
9
|
+
)
|
10
|
+
platform = tables.Column(linkify=True)
|
11
|
+
command_type = tables.Column()
|
12
|
+
|
13
|
+
class Meta(NetBoxTable.Meta):
|
14
|
+
model = Command
|
15
|
+
fields = ("pk", "id", "name", "platform", "command_type", "description")
|
16
|
+
default_columns = ("pk", "name", "platform", "command_type", "description")
|
17
|
+
|
18
|
+
|
19
|
+
class CommandLogTable(NetBoxTable):
|
20
|
+
command = tables.Column(
|
21
|
+
linkify=(
|
22
|
+
"plugins:netbox_toolkit_plugin:command_detail",
|
23
|
+
[tables.A("command.pk")],
|
24
|
+
)
|
25
|
+
)
|
26
|
+
device = tables.Column(linkify=True)
|
27
|
+
success = tables.BooleanColumn(verbose_name="Status", yesno=("Success", "Failed"))
|
28
|
+
|
29
|
+
# Remove actions column entirely
|
30
|
+
actions = False
|
31
|
+
|
32
|
+
class Meta(NetBoxTable.Meta):
|
33
|
+
model = CommandLog
|
34
|
+
fields = (
|
35
|
+
"pk",
|
36
|
+
"id",
|
37
|
+
"command",
|
38
|
+
"device",
|
39
|
+
"username",
|
40
|
+
"execution_time",
|
41
|
+
"success",
|
42
|
+
"execution_duration",
|
43
|
+
)
|
44
|
+
default_columns = (
|
45
|
+
"pk",
|
46
|
+
"command",
|
47
|
+
"device",
|
48
|
+
"username",
|
49
|
+
"execution_time",
|
50
|
+
"success",
|
51
|
+
)
|
@@ -0,0 +1,108 @@
|
|
1
|
+
{% extends 'generic/object.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 buttons %}
|
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
|
+
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
13
|
+
</a>
|
14
|
+
{% endif %}
|
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
|
+
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
|
18
|
+
</a>
|
19
|
+
{% endif %}
|
20
|
+
{% endblock %}
|
21
|
+
|
22
|
+
{% block content %}
|
23
|
+
<div class="row mb-3">
|
24
|
+
<div class="col col-md-6">
|
25
|
+
<div class="card">
|
26
|
+
<div class="card-header">
|
27
|
+
<h3 class="card-title">Command</h3>
|
28
|
+
</div>
|
29
|
+
<div class="card-body">
|
30
|
+
<table class="table table-hover attr-table">
|
31
|
+
<tr>
|
32
|
+
<th scope="row">Name</th>
|
33
|
+
<td>{{ object.name }}</td>
|
34
|
+
</tr>
|
35
|
+
<tr>
|
36
|
+
<th scope="row">Platform</th>
|
37
|
+
<td>{{ object.platform|linkify }}</td>
|
38
|
+
</tr>
|
39
|
+
<tr>
|
40
|
+
<th scope="row">Command Type</th>
|
41
|
+
<td>
|
42
|
+
{{ object.get_command_type_display }}
|
43
|
+
{% if can_execute %}
|
44
|
+
<span class="badge bg-success ms-2">
|
45
|
+
<i class="mdi mdi-check"></i> Executable
|
46
|
+
</span>
|
47
|
+
{% else %}
|
48
|
+
<span class="badge bg-secondary ms-2">
|
49
|
+
<i class="mdi mdi-lock"></i> No Execute Permission
|
50
|
+
</span>
|
51
|
+
{% endif %}
|
52
|
+
</td>
|
53
|
+
</tr>
|
54
|
+
<tr>
|
55
|
+
<th scope="row">Description</th>
|
56
|
+
<td>{{ object.description|placeholder }}</td>
|
57
|
+
</tr>
|
58
|
+
</table>
|
59
|
+
</div>
|
60
|
+
</div>
|
61
|
+
{% include 'inc/panels/custom_fields.html' %}
|
62
|
+
{% include 'inc/panels/tags.html' %}
|
63
|
+
</div>
|
64
|
+
<div class="col col-md-6">
|
65
|
+
<div class="card">
|
66
|
+
<div class="card-header">
|
67
|
+
<h3 class="card-title">Command Detail</h3>
|
68
|
+
</div>
|
69
|
+
<div class="card-body">
|
70
|
+
<pre>{{ object.command }}</pre>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
<div class="row">
|
76
|
+
<div class="col col-md-12">
|
77
|
+
<div class="card">
|
78
|
+
<div class="card-header">
|
79
|
+
<h3 class="card-title">Recent Command Logs</h3>
|
80
|
+
</div>
|
81
|
+
<div class="card-body">
|
82
|
+
<table class="table table-hover table-headings">
|
83
|
+
<thead>
|
84
|
+
<tr>
|
85
|
+
<th>Device</th>
|
86
|
+
<th>Username</th>
|
87
|
+
<th>Execution Time</th>
|
88
|
+
</tr>
|
89
|
+
</thead>
|
90
|
+
<tbody>
|
91
|
+
{% for log in object.logs.all|slice:":5" %}
|
92
|
+
<tr>
|
93
|
+
<td>{{ log.device|linkify }}</td>
|
94
|
+
<td>{{ log.username }}</td>
|
95
|
+
<td>{{ log.execution_time }}</td>
|
96
|
+
</tr>
|
97
|
+
{% empty %}
|
98
|
+
<tr>
|
99
|
+
<td colspan="3" class="text-muted">No command executions recorded</td>
|
100
|
+
</tr>
|
101
|
+
{% endfor %}
|
102
|
+
</tbody>
|
103
|
+
</table>
|
104
|
+
</div>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
</div>
|
108
|
+
{% endblock %}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
{% extends 'generic/object_list.html' %}
|
2
|
+
{% load helpers %}
|
3
|
+
|
4
|
+
{% block title %}Commands{% endblock %}
|
5
|
+
|
6
|
+
{% block buttons %}
|
7
|
+
{% if perms.netbox_toolkit_plugin.add_command %}
|
8
|
+
<a href="{% url 'plugins:netbox_toolkit_plugin:command_add' %}" class="btn btn-primary">
|
9
|
+
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Command
|
10
|
+
</a>
|
11
|
+
{% endif %}
|
12
|
+
{% endblock %}
|
@@ -0,0 +1,170 @@
|
|
1
|
+
{% extends 'generic/object.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 mb-3">
|
11
|
+
<div class="col col-md-6">
|
12
|
+
<div class="card">
|
13
|
+
<div class="card-header">
|
14
|
+
<h3 class="card-title">Command Log</h3>
|
15
|
+
</div>
|
16
|
+
<div class="card-body">
|
17
|
+
<table class="table table-hover attr-table">
|
18
|
+
<tr>
|
19
|
+
<th scope="row">Command</th>
|
20
|
+
<td>{{ object.command|linkify }}</td>
|
21
|
+
</tr>
|
22
|
+
<tr>
|
23
|
+
<th scope="row">Device</th>
|
24
|
+
<td>{{ object.device|linkify }}</td>
|
25
|
+
</tr>
|
26
|
+
<tr>
|
27
|
+
<th scope="row">Username</th>
|
28
|
+
<td>{{ object.username }}</td>
|
29
|
+
</tr>
|
30
|
+
<tr>
|
31
|
+
<th scope="row">Execution Time</th>
|
32
|
+
<td>{{ object.execution_time }}</td>
|
33
|
+
</tr>
|
34
|
+
<tr>
|
35
|
+
<th scope="row">Status</th>
|
36
|
+
<td>
|
37
|
+
{% if object.success %}
|
38
|
+
<span class="badge bg-success">Success</span>
|
39
|
+
{% else %}
|
40
|
+
<span class="badge bg-danger">Failed</span>
|
41
|
+
{% endif %}
|
42
|
+
</td>
|
43
|
+
</tr>
|
44
|
+
{% if object.execution_duration %}
|
45
|
+
<tr>
|
46
|
+
<th scope="row">Duration</th>
|
47
|
+
<td>{{ object.execution_duration|floatformat:3 }}s</td>
|
48
|
+
</tr>
|
49
|
+
{% endif %}
|
50
|
+
{% if object.parsing_success %}
|
51
|
+
<tr>
|
52
|
+
<th scope="row">Parsing Status</th>
|
53
|
+
<td>
|
54
|
+
<span class="badge bg-success">
|
55
|
+
<i class="mdi mdi-check-circle me-1"></i>
|
56
|
+
Successfully Parsed
|
57
|
+
</span>
|
58
|
+
{% if object.parsing_template %}
|
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>
|
71
|
+
</td>
|
72
|
+
</tr>
|
73
|
+
{% endif %}
|
74
|
+
{% if not object.success and object.error_message %}
|
75
|
+
<tr>
|
76
|
+
<th scope="row">Error</th>
|
77
|
+
<td><code>{{ object.error_message }}</code></td>
|
78
|
+
</tr>
|
79
|
+
{% endif %}
|
80
|
+
</table>
|
81
|
+
</div>
|
82
|
+
</div>
|
83
|
+
</div>
|
84
|
+
</div>
|
85
|
+
<div class="row">
|
86
|
+
<div class="col col-md-12">
|
87
|
+
<div class="card">
|
88
|
+
<div class="card-header">
|
89
|
+
<h3 class="card-title">Command Output</h3>
|
90
|
+
</div>
|
91
|
+
<div class="card-body">
|
92
|
+
<pre>{{ object.output }}</pre>
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
</div>
|
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>
|
110
|
+
</div>
|
111
|
+
{% endif %}
|
112
|
+
</div>
|
113
|
+
<div class="card-body">
|
114
|
+
{% if object.parsed_data|length > 0 %}
|
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 %}
|
160
|
+
</div>
|
161
|
+
</div>
|
162
|
+
</div>
|
163
|
+
</div>
|
164
|
+
{% endif %}
|
165
|
+
{% endblock %}
|
166
|
+
|
167
|
+
{% block javascript %}
|
168
|
+
<!-- Load NetBox Toolkit consolidated JavaScript -->
|
169
|
+
<script src="{% static 'netbox_toolkit_plugin/js/toolkit.js' %}"></script>
|
170
|
+
{% endblock %}
|