netbox-toolkit-plugin 0.1.0__py3-none-any.whl → 0.1.2__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}/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/settings.py +176 -0
- 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.2.dist-info}/METADATA +2 -2
- netbox_toolkit_plugin-0.1.2.dist-info/RECORD +60 -0
- netbox_toolkit_plugin-0.1.2.dist-info/entry_points.txt +2 -0
- netbox_toolkit_plugin-0.1.2.dist-info/top_level.txt +1 -0
- netbox_toolkit/__init__.py +0 -30
- netbox_toolkit/config.py +0 -159
- 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.2.dist-info}/WHEEL +0 -0
- {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -5,21 +5,26 @@ from django.core.exceptions import PermissionDenied
|
|
5
5
|
from dcim.models import Device
|
6
6
|
from .models import Command, CommandLog
|
7
7
|
from .forms import CommandForm, CommandLogForm, CommandExecutionForm
|
8
|
-
from netbox.views.generic import
|
8
|
+
from netbox.views.generic import (
|
9
|
+
ObjectView,
|
10
|
+
ObjectListView,
|
11
|
+
ObjectEditView,
|
12
|
+
ObjectDeleteView,
|
13
|
+
ObjectChangeLogView,
|
14
|
+
)
|
9
15
|
from utilities.views import ViewTab, register_model_view
|
10
16
|
from .services.command_service import CommandExecutionService
|
11
17
|
from .services.device_service import DeviceService
|
12
18
|
from .services.rate_limiting_service import RateLimitingService
|
13
19
|
|
14
|
-
|
20
|
+
|
21
|
+
@register_model_view(Device, name="toolkit", path="toolkit")
|
15
22
|
class DeviceToolkitView(ObjectView):
|
16
23
|
queryset = Device.objects.all()
|
17
|
-
template_name =
|
18
|
-
|
24
|
+
template_name = "netbox_toolkit_plugin/device_toolkit.html"
|
25
|
+
|
19
26
|
# Define tab without a badge counter
|
20
|
-
tab = ViewTab(
|
21
|
-
label='Toolkit'
|
22
|
-
)
|
27
|
+
tab = ViewTab(label="Toolkit")
|
23
28
|
|
24
29
|
def __init__(self, *args, **kwargs):
|
25
30
|
super().__init__(*args, **kwargs)
|
@@ -29,261 +34,319 @@ class DeviceToolkitView(ObjectView):
|
|
29
34
|
|
30
35
|
def get_object(self, **kwargs):
|
31
36
|
"""Override get_object to properly filter by pk"""
|
32
|
-
return Device.objects.get(pk=self.kwargs.get(
|
37
|
+
return Device.objects.get(pk=self.kwargs.get("pk", kwargs.get("pk")))
|
33
38
|
|
34
39
|
def get(self, request, pk):
|
35
|
-
self.kwargs = {
|
40
|
+
self.kwargs = {"pk": pk} # Set kwargs for get_object
|
36
41
|
device = self.get_object()
|
37
|
-
|
42
|
+
|
38
43
|
# Validate device is ready for commands
|
39
|
-
is_valid, error_message, validation_checks =
|
44
|
+
is_valid, error_message, validation_checks = (
|
45
|
+
self.device_service.validate_device_for_commands(device)
|
46
|
+
)
|
40
47
|
if not is_valid:
|
41
48
|
messages.warning(request, f"Device validation warning: {error_message}")
|
42
|
-
|
49
|
+
|
43
50
|
# Get connection info for the device
|
44
51
|
connection_info = self.device_service.get_device_connection_info(device)
|
45
|
-
|
52
|
+
|
46
53
|
# Get available commands for the device with permission filtering
|
47
54
|
commands = self._get_filtered_commands(request.user, device)
|
48
|
-
|
55
|
+
|
49
56
|
# Get rate limit status for UI display
|
50
|
-
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
51
|
-
|
57
|
+
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
58
|
+
device, request.user
|
59
|
+
)
|
60
|
+
|
52
61
|
form = CommandExecutionForm()
|
53
62
|
|
54
63
|
# No credential storage - credentials required for each command execution
|
55
64
|
|
56
|
-
return render(
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
65
|
+
return render(
|
66
|
+
request,
|
67
|
+
self.template_name,
|
68
|
+
{
|
69
|
+
"object": device,
|
70
|
+
"tab": self.tab,
|
71
|
+
"commands": commands,
|
72
|
+
"form": form,
|
73
|
+
"device_valid": is_valid,
|
74
|
+
"validation_message": error_message,
|
75
|
+
"validation_checks": validation_checks,
|
76
|
+
"connection_info": connection_info,
|
77
|
+
"rate_limit_status": rate_limit_status,
|
78
|
+
},
|
79
|
+
)
|
67
80
|
|
68
81
|
def _user_has_action_permission(self, user, obj, action):
|
69
82
|
"""Check if user has permission for a specific action on an object using NetBox's ObjectPermission system"""
|
70
83
|
from django.contrib.contenttypes.models import ContentType
|
71
84
|
from users.models import ObjectPermission
|
72
|
-
|
85
|
+
|
73
86
|
# Get content type for the object
|
74
87
|
content_type = ContentType.objects.get_for_model(obj)
|
75
|
-
|
88
|
+
|
76
89
|
# Check if user has any ObjectPermissions with the required action
|
77
90
|
user_permissions = ObjectPermission.objects.filter(
|
78
|
-
object_types__in=[content_type],
|
79
|
-
actions__contains=[action],
|
80
|
-
enabled=True
|
91
|
+
object_types__in=[content_type], actions__contains=[action], enabled=True
|
81
92
|
)
|
82
|
-
|
93
|
+
|
83
94
|
# Check if user is assigned to any groups with this permission
|
84
95
|
user_groups = user.groups.all()
|
85
96
|
for permission in user_permissions:
|
86
97
|
# Check if permission applies to user or user's groups
|
87
|
-
if (
|
88
|
-
permission.
|
89
|
-
|
98
|
+
if (
|
99
|
+
permission.users.filter(id=user.id).exists()
|
100
|
+
or permission.groups.filter(
|
101
|
+
id__in=user_groups.values_list("id", flat=True)
|
102
|
+
).exists()
|
103
|
+
):
|
90
104
|
# If there are constraints, evaluate them
|
91
105
|
if permission.constraints:
|
92
106
|
# Create a queryset with the constraints and check if the object matches
|
93
|
-
queryset = content_type.model_class().objects.filter(
|
107
|
+
queryset = content_type.model_class().objects.filter(
|
108
|
+
**permission.constraints
|
109
|
+
)
|
94
110
|
if queryset.filter(id=obj.id).exists():
|
95
111
|
return True
|
96
112
|
else:
|
97
113
|
# No constraints means permission applies to all objects of this type
|
98
114
|
return True
|
99
|
-
|
115
|
+
|
100
116
|
return False
|
101
117
|
|
102
118
|
def _get_filtered_commands(self, user, device):
|
103
119
|
"""Get commands for a device filtered by user permissions"""
|
104
120
|
# Get all available commands for the device
|
105
121
|
all_commands = self.device_service.get_available_commands(device)
|
106
|
-
|
122
|
+
|
107
123
|
# Filter commands based on user permissions for custom actions
|
108
124
|
commands = []
|
109
125
|
for command in all_commands:
|
110
126
|
# Check if user has permission for the specific action on this command
|
111
|
-
if command.command_type ==
|
127
|
+
if command.command_type == "show":
|
112
128
|
# Check for 'execute_show' action permission
|
113
|
-
if self._user_has_action_permission(user, command,
|
129
|
+
if self._user_has_action_permission(user, command, "execute_show"):
|
114
130
|
commands.append(command)
|
115
|
-
elif command.command_type ==
|
116
|
-
# Check for 'execute_config' action permission
|
117
|
-
if self._user_has_action_permission(user, command,
|
131
|
+
elif command.command_type == "config":
|
132
|
+
# Check for 'execute_config' action permission
|
133
|
+
if self._user_has_action_permission(user, command, "execute_config"):
|
118
134
|
commands.append(command)
|
119
|
-
|
135
|
+
|
120
136
|
return commands
|
121
137
|
|
122
138
|
def post(self, request, pk):
|
123
|
-
self.kwargs = {
|
139
|
+
self.kwargs = {"pk": pk} # Set kwargs for get_object
|
124
140
|
device = self.get_object()
|
125
|
-
command_id = request.POST.get(
|
126
|
-
|
141
|
+
command_id = request.POST.get("command_id")
|
142
|
+
|
127
143
|
try:
|
128
144
|
command = Command.objects.get(id=command_id)
|
129
145
|
except Command.DoesNotExist:
|
130
146
|
messages.error(request, "Selected command not found.")
|
131
147
|
return self.get(request, pk)
|
132
|
-
|
148
|
+
|
133
149
|
# Check permissions based on command type using NetBox's object-based permissions
|
134
|
-
if command.command_type ==
|
135
|
-
if not self._user_has_action_permission(
|
136
|
-
|
150
|
+
if command.command_type == "config":
|
151
|
+
if not self._user_has_action_permission(
|
152
|
+
request.user, command, "execute_config"
|
153
|
+
):
|
154
|
+
messages.error(
|
155
|
+
request,
|
156
|
+
"You don't have permission to execute configuration commands.",
|
157
|
+
)
|
137
158
|
return self.get(request, pk)
|
138
|
-
elif command.command_type ==
|
139
|
-
if not self._user_has_action_permission(
|
140
|
-
|
159
|
+
elif command.command_type == "show":
|
160
|
+
if not self._user_has_action_permission(
|
161
|
+
request.user, command, "execute_show"
|
162
|
+
):
|
163
|
+
messages.error(
|
164
|
+
request, "You don't have permission to execute show commands."
|
165
|
+
)
|
141
166
|
return self.get(request, pk)
|
142
|
-
|
167
|
+
|
143
168
|
# Create a form with the POST data
|
144
169
|
form_data = {
|
145
|
-
|
146
|
-
|
170
|
+
"username": request.POST.get("username", ""),
|
171
|
+
"password": request.POST.get("password", ""),
|
147
172
|
}
|
148
173
|
form = CommandExecutionForm(form_data)
|
149
174
|
commands = self._get_filtered_commands(request.user, device)
|
150
175
|
|
151
176
|
if form.is_valid():
|
152
|
-
username = form.cleaned_data[
|
153
|
-
password = form.cleaned_data[
|
177
|
+
username = form.cleaned_data["username"]
|
178
|
+
password = form.cleaned_data["password"]
|
154
179
|
|
155
180
|
# Check rate limiting before command execution
|
156
|
-
rate_limit_check = self.rate_limiting_service.check_rate_limit(
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
181
|
+
rate_limit_check = self.rate_limiting_service.check_rate_limit(
|
182
|
+
device, request.user
|
183
|
+
)
|
184
|
+
|
185
|
+
if not rate_limit_check["allowed"]:
|
186
|
+
messages.error(
|
187
|
+
request, f"Rate limit exceeded: {rate_limit_check['reason']}"
|
188
|
+
)
|
189
|
+
|
161
190
|
# Get rate limit status and other context for display
|
162
|
-
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
163
|
-
|
191
|
+
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
192
|
+
device, request.user
|
193
|
+
)
|
194
|
+
is_valid, error_message, validation_checks = (
|
195
|
+
self.device_service.validate_device_for_commands(device)
|
196
|
+
)
|
164
197
|
connection_info = self.device_service.get_device_connection_info(device)
|
165
|
-
|
166
|
-
return render(
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
198
|
+
|
199
|
+
return render(
|
200
|
+
request,
|
201
|
+
self.template_name,
|
202
|
+
{
|
203
|
+
"object": device,
|
204
|
+
"tab": self.tab,
|
205
|
+
"commands": commands,
|
206
|
+
"form": form,
|
207
|
+
"device_valid": is_valid,
|
208
|
+
"validation_message": error_message,
|
209
|
+
"validation_checks": validation_checks,
|
210
|
+
"connection_info": connection_info,
|
211
|
+
"rate_limit_status": rate_limit_status,
|
212
|
+
},
|
213
|
+
)
|
177
214
|
|
178
215
|
# Execute command using the service with retry for socket error recovery
|
179
216
|
result = self.command_service.execute_command_with_retry(
|
180
217
|
command, device, username, password, max_retries=1
|
181
218
|
)
|
182
|
-
|
219
|
+
|
183
220
|
# No credential storage for security
|
184
|
-
|
221
|
+
|
185
222
|
# Determine overall success and appropriate message
|
186
223
|
overall_success = result.success and not result.has_syntax_error
|
187
|
-
|
224
|
+
|
188
225
|
if overall_success:
|
189
|
-
messages.success(
|
226
|
+
messages.success(
|
227
|
+
request, f"Command '{command.name}' executed successfully."
|
228
|
+
)
|
190
229
|
elif result.has_syntax_error:
|
191
|
-
messages.warning(
|
230
|
+
messages.warning(
|
231
|
+
request,
|
232
|
+
f"Command '{command.name}' executed but syntax error detected: {result.syntax_error_type}",
|
233
|
+
)
|
192
234
|
else:
|
193
|
-
messages.error(
|
235
|
+
messages.error(
|
236
|
+
request, f"Command execution failed: {result.error_message}"
|
237
|
+
)
|
194
238
|
|
195
239
|
# Return a new empty form after execution
|
196
240
|
empty_form = CommandExecutionForm()
|
197
|
-
|
241
|
+
|
198
242
|
# Get validation checks and rate limit status for display
|
199
|
-
is_valid, error_message, validation_checks =
|
243
|
+
is_valid, error_message, validation_checks = (
|
244
|
+
self.device_service.validate_device_for_commands(device)
|
245
|
+
)
|
200
246
|
connection_info = self.device_service.get_device_connection_info(device)
|
201
|
-
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
247
|
+
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
248
|
+
device, request.user
|
249
|
+
)
|
250
|
+
|
251
|
+
return render(
|
252
|
+
request,
|
253
|
+
self.template_name,
|
254
|
+
{
|
255
|
+
"object": device,
|
256
|
+
"tab": self.tab,
|
257
|
+
"commands": commands,
|
258
|
+
"form": empty_form, # Use an empty form for the next command
|
259
|
+
"command_output": result.output,
|
260
|
+
"executed_command": command,
|
261
|
+
"execution_success": overall_success,
|
262
|
+
"execution_time": result.execution_time,
|
263
|
+
"has_syntax_error": result.has_syntax_error,
|
264
|
+
"syntax_error_type": result.syntax_error_type,
|
265
|
+
"syntax_error_vendor": result.syntax_error_vendor,
|
266
|
+
"parsed_data": result.parsed_output,
|
267
|
+
"parsing_success": result.parsing_success,
|
268
|
+
"parsing_template": result.parsing_method,
|
269
|
+
"device_valid": is_valid,
|
270
|
+
"validation_message": error_message,
|
271
|
+
"validation_checks": validation_checks,
|
272
|
+
"connection_info": connection_info,
|
273
|
+
"rate_limit_status": rate_limit_status,
|
274
|
+
},
|
275
|
+
)
|
224
276
|
else:
|
225
277
|
messages.error(request, "Please correct the form errors.")
|
226
278
|
# Get validation checks and rate limit status for display
|
227
|
-
is_valid, error_message, validation_checks =
|
279
|
+
is_valid, error_message, validation_checks = (
|
280
|
+
self.device_service.validate_device_for_commands(device)
|
281
|
+
)
|
228
282
|
connection_info = self.device_service.get_device_connection_info(device)
|
229
|
-
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
283
|
+
rate_limit_status = self.rate_limiting_service.get_rate_limit_status(
|
284
|
+
device, request.user
|
285
|
+
)
|
286
|
+
return render(
|
287
|
+
request,
|
288
|
+
self.template_name,
|
289
|
+
{
|
290
|
+
"object": device,
|
291
|
+
"tab": self.tab,
|
292
|
+
"commands": commands,
|
293
|
+
"form": form,
|
294
|
+
"device_valid": is_valid,
|
295
|
+
"validation_message": error_message,
|
296
|
+
"validation_checks": validation_checks,
|
297
|
+
"connection_info": connection_info,
|
298
|
+
"rate_limit_status": rate_limit_status,
|
299
|
+
},
|
300
|
+
)
|
301
|
+
|
241
302
|
|
242
303
|
# Command views
|
243
304
|
class CommandListView(ObjectListView):
|
244
305
|
queryset = Command.objects.all()
|
245
306
|
filterset = None # Will update this after import
|
246
307
|
table = None # Will update this after import
|
247
|
-
template_name =
|
248
|
-
|
308
|
+
template_name = "netbox_toolkit_plugin/command_list.html"
|
309
|
+
|
249
310
|
def __init__(self, *args, **kwargs):
|
250
311
|
super().__init__(*args, **kwargs)
|
251
312
|
from .filtersets import CommandFilterSet
|
252
313
|
from .tables import CommandTable
|
314
|
+
|
253
315
|
self.filterset = CommandFilterSet
|
254
316
|
self.table = CommandTable
|
255
317
|
|
318
|
+
|
256
319
|
class CommandEditView(ObjectEditView):
|
257
320
|
queryset = Command.objects.all()
|
258
321
|
form = CommandForm
|
259
|
-
template_name =
|
260
|
-
|
322
|
+
template_name = "netbox_toolkit_plugin/command_edit.html"
|
323
|
+
|
261
324
|
def get_success_url(self):
|
262
325
|
"""Override to use correct plugin namespace"""
|
263
326
|
# Try hardcoded URL first to see if the issue is with reverse()
|
264
327
|
if self.object and self.object.pk:
|
265
|
-
return f
|
266
|
-
return
|
267
|
-
|
328
|
+
return f"/plugins/toolkit/commands/{self.object.pk}/"
|
329
|
+
return "/plugins/toolkit/commands/"
|
330
|
+
|
268
331
|
def get_return_url(self, request, instance):
|
269
332
|
"""Override to use correct plugin namespace for cancel/return links"""
|
270
333
|
# Check if there's a return URL in the request
|
271
|
-
return_url = request.GET.get(
|
334
|
+
return_url = request.GET.get("return_url")
|
272
335
|
if return_url:
|
273
336
|
return return_url
|
274
337
|
# Return hardcoded URL
|
275
|
-
return
|
276
|
-
|
338
|
+
return "/plugins/toolkit/commands/"
|
339
|
+
|
277
340
|
def get_extra_context(self, request, instance):
|
278
341
|
"""Override to provide additional context with correct URLs"""
|
279
342
|
context = super().get_extra_context(request, instance)
|
280
|
-
|
343
|
+
|
281
344
|
# Override any auto-generated URLs that might be using wrong namespace
|
282
|
-
context[
|
283
|
-
context[
|
284
|
-
|
345
|
+
context["base_template"] = "generic/object_edit.html"
|
346
|
+
context["return_url"] = self.get_return_url(request, instance)
|
347
|
+
|
285
348
|
return context
|
286
|
-
|
349
|
+
|
287
350
|
def form_valid(self, form):
|
288
351
|
"""Override form_valid to ensure correct URL handling"""
|
289
352
|
# Let the parent handle the form saving
|
@@ -291,95 +354,114 @@ class CommandEditView(ObjectEditView):
|
|
291
354
|
# The parent should redirect to get_success_url()
|
292
355
|
return response
|
293
356
|
|
357
|
+
|
294
358
|
class CommandView(ObjectView):
|
295
359
|
queryset = Command.objects.all()
|
296
|
-
template_name =
|
297
|
-
|
360
|
+
template_name = "netbox_toolkit_plugin/command.html"
|
361
|
+
|
298
362
|
def get_extra_context(self, request, instance):
|
299
363
|
"""Add permission context to the template"""
|
300
364
|
context = super().get_extra_context(request, instance)
|
301
|
-
|
365
|
+
|
302
366
|
# Add permission information for the template using NetBox's object-based permissions
|
303
|
-
context[
|
304
|
-
if instance.command_type ==
|
305
|
-
context[
|
306
|
-
|
307
|
-
|
308
|
-
|
367
|
+
context["can_execute"] = False
|
368
|
+
if instance.command_type == "show":
|
369
|
+
context["can_execute"] = self._user_has_action_permission(
|
370
|
+
request.user, instance, "execute_show"
|
371
|
+
)
|
372
|
+
elif instance.command_type == "config":
|
373
|
+
context["can_execute"] = self._user_has_action_permission(
|
374
|
+
request.user, instance, "execute_config"
|
375
|
+
)
|
376
|
+
|
309
377
|
# NetBox will automatically handle 'change' and 'delete' permissions through standard actions
|
310
|
-
context[
|
311
|
-
|
312
|
-
|
378
|
+
context["can_edit"] = self._user_has_action_permission(
|
379
|
+
request.user, instance, "change"
|
380
|
+
)
|
381
|
+
context["can_delete"] = self._user_has_action_permission(
|
382
|
+
request.user, instance, "delete"
|
383
|
+
)
|
384
|
+
|
313
385
|
return context
|
314
386
|
|
315
387
|
def _user_has_action_permission(self, user, obj, action):
|
316
388
|
"""Check if user has permission for a specific action on an object using NetBox's ObjectPermission system"""
|
317
389
|
from django.contrib.contenttypes.models import ContentType
|
318
390
|
from users.models import ObjectPermission
|
319
|
-
|
391
|
+
|
320
392
|
# Get content type for the object
|
321
393
|
content_type = ContentType.objects.get_for_model(obj)
|
322
|
-
|
394
|
+
|
323
395
|
# Check if user has any ObjectPermissions with the required action
|
324
396
|
user_permissions = ObjectPermission.objects.filter(
|
325
|
-
object_types__in=[content_type],
|
326
|
-
actions__contains=[action],
|
327
|
-
enabled=True
|
397
|
+
object_types__in=[content_type], actions__contains=[action], enabled=True
|
328
398
|
)
|
329
|
-
|
399
|
+
|
330
400
|
# Check if user is assigned to any groups with this permission
|
331
401
|
user_groups = user.groups.all()
|
332
402
|
for permission in user_permissions:
|
333
403
|
# Check if permission applies to user or user's groups
|
334
|
-
if (
|
335
|
-
permission.
|
336
|
-
|
404
|
+
if (
|
405
|
+
permission.users.filter(id=user.id).exists()
|
406
|
+
or permission.groups.filter(
|
407
|
+
id__in=user_groups.values_list("id", flat=True)
|
408
|
+
).exists()
|
409
|
+
):
|
337
410
|
# If there are constraints, evaluate them
|
338
411
|
if permission.constraints:
|
339
412
|
# Create a queryset with the constraints and check if the object matches
|
340
|
-
queryset = content_type.model_class().objects.filter(
|
413
|
+
queryset = content_type.model_class().objects.filter(
|
414
|
+
**permission.constraints
|
415
|
+
)
|
341
416
|
if queryset.filter(id=obj.id).exists():
|
342
417
|
return True
|
343
418
|
else:
|
344
419
|
# No constraints means permission applies to all objects of this type
|
345
420
|
return True
|
346
|
-
|
421
|
+
|
347
422
|
return False
|
348
423
|
|
424
|
+
|
349
425
|
class CommandDeleteView(ObjectDeleteView):
|
350
426
|
queryset = Command.objects.all()
|
351
|
-
|
427
|
+
|
352
428
|
def get_success_url(self):
|
353
429
|
"""Override to use correct plugin namespace"""
|
354
430
|
from django.urls import reverse
|
355
|
-
|
431
|
+
|
432
|
+
return reverse("plugins:netbox_toolkit_plugin:command_list")
|
433
|
+
|
356
434
|
|
357
435
|
class CommandChangeLogView(ObjectChangeLogView):
|
358
436
|
queryset = Command.objects.all()
|
359
437
|
|
438
|
+
|
360
439
|
# CommandLog views
|
361
440
|
class CommandLogListView(ObjectListView):
|
362
441
|
queryset = CommandLog.objects.all()
|
363
442
|
filterset = None # Will update this after import
|
364
443
|
table = None # Will update this after import
|
365
|
-
template_name =
|
366
|
-
|
444
|
+
template_name = "netbox_toolkit_plugin/commandlog_list.html"
|
445
|
+
|
367
446
|
def __init__(self, *args, **kwargs):
|
368
447
|
super().__init__(*args, **kwargs)
|
369
448
|
from .filtersets import CommandLogFilterSet
|
370
449
|
from .tables import CommandLogTable
|
450
|
+
|
371
451
|
self.filterset = CommandLogFilterSet
|
372
452
|
self.table = CommandLogTable
|
373
|
-
|
453
|
+
|
374
454
|
def get_extra_context(self, request):
|
375
455
|
"""Override to disable 'Add' button since logs are created automatically"""
|
376
456
|
context = super().get_extra_context(request)
|
377
|
-
context[
|
457
|
+
context["add_button_url"] = None # Disable the add button
|
378
458
|
return context
|
379
459
|
|
460
|
+
|
380
461
|
class CommandLogView(ObjectView):
|
381
462
|
queryset = CommandLog.objects.all()
|
382
|
-
template_name =
|
463
|
+
template_name = "netbox_toolkit_plugin/commandlog.html"
|
464
|
+
|
383
465
|
|
384
466
|
class CommandLogChangeLogView(ObjectChangeLogView):
|
385
|
-
queryset = CommandLog.objects.all()
|
467
|
+
queryset = CommandLog.objects.all()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: netbox-toolkit-plugin
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.2
|
4
4
|
Summary: NetBox plugin for running pre-defined commands on network devices
|
5
5
|
Author: Andy Norwood
|
6
6
|
Classifier: Development Status :: 3 - Alpha
|
@@ -58,7 +58,7 @@ A NetBox plugin that allows you to run commands on network devices directly from
|
|
58
58
|
|
59
59
|
#### 📋 Command Management
|
60
60
|
- [📋 Command Creation](./docs/user/command-creation.md) - Create platform-specific commands
|
61
|
-
- [🔐 Permissions Setup](./docs/user/
|
61
|
+
- [🔐 Permissions Setup](./docs/user/permissions-setup-guide.md) - Configure granular access control
|
62
62
|
- [📝 Permission Examples](./docs/user/permission-examples.md) - Example permission configuration
|
63
63
|
|
64
64
|
#### 🔧 Troubleshooting
|