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.
Files changed (69) hide show
  1. netbox_toolkit_plugin/__init__.py +32 -0
  2. {netbox_toolkit → netbox_toolkit_plugin}/api/serializers.py +71 -35
  3. {netbox_toolkit → netbox_toolkit_plugin}/api/urls.py +3 -3
  4. {netbox_toolkit → netbox_toolkit_plugin}/connectors/factory.py +170 -111
  5. {netbox_toolkit → netbox_toolkit_plugin}/connectors/netmiko_connector.py +242 -179
  6. {netbox_toolkit → netbox_toolkit_plugin}/connectors/scrapli_connector.py +256 -172
  7. netbox_toolkit_plugin/migrations/0001_initial.py +108 -0
  8. netbox_toolkit_plugin/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +70 -0
  9. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0003_permission_system_update.py +26 -12
  10. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0004_remove_django_permissions.py +27 -29
  11. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0005_alter_command_options_and_more.py +7 -8
  12. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +7 -8
  13. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0007_alter_commandlog_parsing_template.py +6 -4
  14. {netbox_toolkit → netbox_toolkit_plugin}/models.py +31 -32
  15. {netbox_toolkit → netbox_toolkit_plugin}/navigation.py +6 -6
  16. {netbox_toolkit → netbox_toolkit_plugin}/services/command_service.py +188 -128
  17. {netbox_toolkit → netbox_toolkit_plugin}/services/rate_limiting_service.py +104 -97
  18. netbox_toolkit_plugin/settings.py +176 -0
  19. netbox_toolkit_plugin/tables.py +51 -0
  20. netbox_toolkit_plugin/templates/netbox_toolkit/command.html +108 -0
  21. netbox_toolkit_plugin/templates/netbox_toolkit/command_list.html +12 -0
  22. netbox_toolkit_plugin/templates/netbox_toolkit/commandlog.html +170 -0
  23. netbox_toolkit_plugin/templates/netbox_toolkit/device_toolkit.html +557 -0
  24. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command.html +5 -5
  25. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_list.html +2 -2
  26. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog.html +2 -2
  27. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/device_toolkit.html +6 -6
  28. netbox_toolkit_plugin/urls.py +38 -0
  29. {netbox_toolkit → netbox_toolkit_plugin}/utils/logging.py +20 -19
  30. {netbox_toolkit → netbox_toolkit_plugin}/views.py +251 -169
  31. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.2.dist-info}/METADATA +2 -2
  32. netbox_toolkit_plugin-0.1.2.dist-info/RECORD +60 -0
  33. netbox_toolkit_plugin-0.1.2.dist-info/entry_points.txt +2 -0
  34. netbox_toolkit_plugin-0.1.2.dist-info/top_level.txt +1 -0
  35. netbox_toolkit/__init__.py +0 -30
  36. netbox_toolkit/config.py +0 -159
  37. netbox_toolkit/migrations/0001_initial.py +0 -54
  38. netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +0 -66
  39. netbox_toolkit/tables.py +0 -37
  40. netbox_toolkit/urls.py +0 -22
  41. netbox_toolkit_plugin-0.1.0.dist-info/RECORD +0 -56
  42. netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +0 -2
  43. netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +0 -1
  44. {netbox_toolkit → netbox_toolkit_plugin}/admin.py +0 -0
  45. {netbox_toolkit → netbox_toolkit_plugin}/api/__init__.py +0 -0
  46. {netbox_toolkit → netbox_toolkit_plugin}/api/mixins.py +0 -0
  47. {netbox_toolkit → netbox_toolkit_plugin}/api/schemas.py +0 -0
  48. {netbox_toolkit → netbox_toolkit_plugin}/api/views/__init__.py +0 -0
  49. {netbox_toolkit → netbox_toolkit_plugin}/api/views/command_logs.py +0 -0
  50. {netbox_toolkit → netbox_toolkit_plugin}/api/views/commands.py +0 -0
  51. {netbox_toolkit → netbox_toolkit_plugin}/connectors/__init__.py +0 -0
  52. {netbox_toolkit → netbox_toolkit_plugin}/connectors/base.py +0 -0
  53. {netbox_toolkit → netbox_toolkit_plugin}/exceptions.py +0 -0
  54. {netbox_toolkit → netbox_toolkit_plugin}/filtersets.py +0 -0
  55. {netbox_toolkit → netbox_toolkit_plugin}/forms.py +0 -0
  56. {netbox_toolkit → netbox_toolkit_plugin}/migrations/__init__.py +0 -0
  57. {netbox_toolkit → netbox_toolkit_plugin}/search.py +0 -0
  58. {netbox_toolkit → netbox_toolkit_plugin}/services/__init__.py +0 -0
  59. {netbox_toolkit → netbox_toolkit_plugin}/services/device_service.py +0 -0
  60. {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/css/toolkit.css +0 -0
  61. {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/js/toolkit.js +0 -0
  62. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_edit.html +0 -0
  63. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog_list.html +0 -0
  64. {netbox_toolkit → netbox_toolkit_plugin}/utils/__init__.py +0 -0
  65. {netbox_toolkit → netbox_toolkit_plugin}/utils/connection.py +0 -0
  66. {netbox_toolkit → netbox_toolkit_plugin}/utils/error_parser.py +0 -0
  67. {netbox_toolkit → netbox_toolkit_plugin}/utils/network.py +0 -0
  68. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.2.dist-info}/WHEEL +0 -0
  69. {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 ObjectView, ObjectListView, ObjectEditView, ObjectDeleteView, ObjectChangeLogView
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
- @register_model_view(Device, name='toolkit', path='toolkit')
20
+
21
+ @register_model_view(Device, name="toolkit", path="toolkit")
15
22
  class DeviceToolkitView(ObjectView):
16
23
  queryset = Device.objects.all()
17
- template_name = 'netbox_toolkit/device_toolkit.html'
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('pk', kwargs.get('pk')))
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 = {'pk': pk} # Set kwargs for get_object
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 = self.device_service.validate_device_for_commands(device)
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(device, request.user)
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(request, self.template_name, {
57
- 'object': device,
58
- 'tab': self.tab,
59
- 'commands': commands,
60
- 'form': form,
61
- 'device_valid': is_valid,
62
- 'validation_message': error_message,
63
- 'validation_checks': validation_checks,
64
- 'connection_info': connection_info,
65
- 'rate_limit_status': rate_limit_status,
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 (permission.users.filter(id=user.id).exists() or
88
- permission.groups.filter(id__in=user_groups.values_list('id', flat=True)).exists()):
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(**permission.constraints)
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 == 'show':
127
+ if command.command_type == "show":
112
128
  # Check for 'execute_show' action permission
113
- if self._user_has_action_permission(user, command, 'execute_show'):
129
+ if self._user_has_action_permission(user, command, "execute_show"):
114
130
  commands.append(command)
115
- elif command.command_type == 'config':
116
- # Check for 'execute_config' action permission
117
- if self._user_has_action_permission(user, command, 'execute_config'):
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 = {'pk': pk} # Set kwargs for get_object
139
+ self.kwargs = {"pk": pk} # Set kwargs for get_object
124
140
  device = self.get_object()
125
- command_id = request.POST.get('command_id')
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 == 'config':
135
- if not self._user_has_action_permission(request.user, command, 'execute_config'):
136
- messages.error(request, "You don't have permission to execute configuration commands.")
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 == 'show':
139
- if not self._user_has_action_permission(request.user, command, 'execute_show'):
140
- messages.error(request, "You don't have permission to execute show commands.")
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
- 'username': request.POST.get('username', ''),
146
- 'password': request.POST.get('password', '')
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['username']
153
- password = form.cleaned_data['password']
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(device, request.user)
157
-
158
- if not rate_limit_check['allowed']:
159
- messages.error(request, f"Rate limit exceeded: {rate_limit_check['reason']}")
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(device, request.user)
163
- is_valid, error_message, validation_checks = self.device_service.validate_device_for_commands(device)
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(request, self.template_name, {
167
- 'object': device,
168
- 'tab': self.tab,
169
- 'commands': commands,
170
- 'form': form,
171
- 'device_valid': is_valid,
172
- 'validation_message': error_message,
173
- 'validation_checks': validation_checks,
174
- 'connection_info': connection_info,
175
- 'rate_limit_status': rate_limit_status,
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(request, f"Command '{command.name}' executed successfully.")
226
+ messages.success(
227
+ request, f"Command '{command.name}' executed successfully."
228
+ )
190
229
  elif result.has_syntax_error:
191
- messages.warning(request, f"Command '{command.name}' executed but syntax error detected: {result.syntax_error_type}")
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(request, f"Command execution failed: {result.error_message}")
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 = self.device_service.validate_device_for_commands(device)
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(device, request.user)
202
-
203
- return render(request, self.template_name, {
204
- 'object': device,
205
- 'tab': self.tab,
206
- 'commands': commands,
207
- 'form': empty_form, # Use an empty form for the next command
208
- 'command_output': result.output,
209
- 'executed_command': command,
210
- 'execution_success': overall_success,
211
- 'execution_time': result.execution_time,
212
- 'has_syntax_error': result.has_syntax_error,
213
- 'syntax_error_type': result.syntax_error_type,
214
- 'syntax_error_vendor': result.syntax_error_vendor,
215
- 'parsed_data': result.parsed_output,
216
- 'parsing_success': result.parsing_success,
217
- 'parsing_template': result.parsing_method,
218
- 'device_valid': is_valid,
219
- 'validation_message': error_message,
220
- 'validation_checks': validation_checks,
221
- 'connection_info': connection_info,
222
- 'rate_limit_status': rate_limit_status,
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 = self.device_service.validate_device_for_commands(device)
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(device, request.user)
230
- return render(request, self.template_name, {
231
- 'object': device,
232
- 'tab': self.tab,
233
- 'commands': commands,
234
- 'form': form,
235
- 'device_valid': is_valid,
236
- 'validation_message': error_message,
237
- 'validation_checks': validation_checks,
238
- 'connection_info': connection_info,
239
- 'rate_limit_status': rate_limit_status,
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 = 'netbox_toolkit/command_list.html'
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 = 'netbox_toolkit/command_edit.html'
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'/plugins/toolkit/commands/{self.object.pk}/'
266
- return '/plugins/toolkit/commands/'
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('return_url')
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 '/plugins/toolkit/commands/'
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['base_template'] = 'generic/object_edit.html'
283
- context['return_url'] = self.get_return_url(request, instance)
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 = 'netbox_toolkit/command.html'
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['can_execute'] = False
304
- if instance.command_type == 'show':
305
- context['can_execute'] = self._user_has_action_permission(request.user, instance, 'execute_show')
306
- elif instance.command_type == 'config':
307
- context['can_execute'] = self._user_has_action_permission(request.user, instance, 'execute_config')
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['can_edit'] = self._user_has_action_permission(request.user, instance, 'change')
311
- context['can_delete'] = self._user_has_action_permission(request.user, instance, 'delete')
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 (permission.users.filter(id=user.id).exists() or
335
- permission.groups.filter(id__in=user_groups.values_list('id', flat=True)).exists()):
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(**permission.constraints)
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
- return reverse('plugins:netbox_toolkit:command_list')
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 = 'netbox_toolkit/commandlog_list.html'
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['add_button_url'] = None # Disable the add button
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 = 'netbox_toolkit/commandlog.html'
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.0
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/PERMISSIONS_SETUP_GUIDE.md) - Configure granular access control
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