netbox-toolkit-plugin 0.1.1__py3-none-any.whl → 0.1.3__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 (43) hide show
  1. netbox_toolkit_plugin/__init__.py +1 -1
  2. netbox_toolkit_plugin/admin.py +11 -7
  3. netbox_toolkit_plugin/api/mixins.py +20 -16
  4. netbox_toolkit_plugin/api/schemas.py +53 -74
  5. netbox_toolkit_plugin/api/serializers.py +10 -11
  6. netbox_toolkit_plugin/api/urls.py +2 -1
  7. netbox_toolkit_plugin/api/views/__init__.py +4 -3
  8. netbox_toolkit_plugin/api/views/command_logs.py +80 -73
  9. netbox_toolkit_plugin/api/views/commands.py +140 -134
  10. netbox_toolkit_plugin/connectors/__init__.py +9 -9
  11. netbox_toolkit_plugin/connectors/base.py +30 -31
  12. netbox_toolkit_plugin/connectors/factory.py +22 -26
  13. netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
  14. netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
  15. netbox_toolkit_plugin/exceptions.py +0 -7
  16. netbox_toolkit_plugin/filtersets.py +26 -42
  17. netbox_toolkit_plugin/forms.py +13 -11
  18. netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
  19. netbox_toolkit_plugin/models.py +2 -17
  20. netbox_toolkit_plugin/navigation.py +3 -0
  21. netbox_toolkit_plugin/search.py +12 -9
  22. netbox_toolkit_plugin/services/__init__.py +1 -1
  23. netbox_toolkit_plugin/services/command_service.py +7 -10
  24. netbox_toolkit_plugin/services/device_service.py +40 -32
  25. netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
  26. netbox_toolkit_plugin/{config.py → settings.py} +17 -7
  27. netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
  28. netbox_toolkit_plugin/tables.py +10 -1
  29. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
  30. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
  31. netbox_toolkit_plugin/urls.py +10 -3
  32. netbox_toolkit_plugin/utils/connection.py +54 -54
  33. netbox_toolkit_plugin/utils/error_parser.py +128 -109
  34. netbox_toolkit_plugin/utils/logging.py +1 -0
  35. netbox_toolkit_plugin/utils/network.py +74 -47
  36. netbox_toolkit_plugin/views.py +51 -22
  37. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
  38. netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
  39. netbox_toolkit_plugin-0.1.1.dist-info/RECORD +0 -60
  40. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
  41. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
  42. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
  43. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,29 +1,33 @@
1
1
  """
2
2
  API ViewSet for CommandLog resources
3
3
  """
4
- from rest_framework import status
5
- from rest_framework.decorators import action
6
- from rest_framework.response import Response
7
- from django.utils import timezone
4
+
8
5
  from datetime import timedelta
6
+
9
7
  from django.db.models import Count, Q
10
8
  from django.http import HttpResponse
11
- from drf_spectacular.utils import extend_schema_view
9
+ from django.utils import timezone
10
+
12
11
  from netbox.api.viewsets import NetBoxModelViewSet
13
12
 
14
- from ... import models, filtersets
15
- from ..serializers import CommandLogSerializer
13
+ from drf_spectacular.utils import extend_schema_view
14
+ from rest_framework import status
15
+ from rest_framework.decorators import action
16
+ from rest_framework.response import Response
17
+
18
+ from ... import filtersets, models
16
19
  from ..mixins import APIResponseMixin
17
20
  from ..schemas import (
18
- COMMAND_LOG_LIST_SCHEMA,
19
- COMMAND_LOG_RETRIEVE_SCHEMA,
20
21
  COMMAND_LOG_CREATE_SCHEMA,
21
- COMMAND_LOG_UPDATE_SCHEMA,
22
- COMMAND_LOG_PARTIAL_UPDATE_SCHEMA,
23
22
  COMMAND_LOG_DESTROY_SCHEMA,
24
- COMMAND_LOG_STATISTICS_SCHEMA,
25
23
  COMMAND_LOG_EXPORT_SCHEMA,
24
+ COMMAND_LOG_LIST_SCHEMA,
25
+ COMMAND_LOG_PARTIAL_UPDATE_SCHEMA,
26
+ COMMAND_LOG_RETRIEVE_SCHEMA,
27
+ COMMAND_LOG_STATISTICS_SCHEMA,
28
+ COMMAND_LOG_UPDATE_SCHEMA,
26
29
  )
30
+ from ..serializers import CommandLogSerializer
27
31
 
28
32
 
29
33
  @extend_schema_view(
@@ -39,132 +43,135 @@ class CommandLogViewSet(NetBoxModelViewSet, APIResponseMixin):
39
43
  serializer_class = CommandLogSerializer
40
44
  filterset_class = filtersets.CommandLogFilterSet
41
45
  # NetBox automatically handles object-based permissions - no need for explicit permission_classes
42
-
46
+
43
47
  def get_queryset(self):
44
48
  """NetBox will automatically filter based on user's ObjectPermissions"""
45
49
  return super().get_queryset()
46
-
50
+
47
51
  @COMMAND_LOG_STATISTICS_SCHEMA
48
- @action(detail=False, methods=['get'], url_path='statistics')
52
+ @action(detail=False, methods=["get"], url_path="statistics")
49
53
  def statistics(self, request):
50
54
  """Get command execution statistics"""
51
55
  queryset = self.get_queryset()
52
-
56
+
53
57
  # Basic stats
54
58
  total_logs = queryset.count()
55
59
  successful_logs = queryset.filter(success=True).count()
56
60
  success_rate = (successful_logs / total_logs * 100) if total_logs > 0 else 0
57
-
61
+
58
62
  # Last 24 hours stats
59
63
  last_24h = timezone.now() - timedelta(hours=24)
60
64
  recent_logs = queryset.filter(created__gte=last_24h)
61
65
  recent_total = recent_logs.count()
62
66
  recent_successful = recent_logs.filter(success=True).count()
63
67
  recent_failed = recent_total - recent_successful
64
-
68
+
65
69
  # Top commands
66
70
  top_commands = (
67
- queryset.values('command__name')
68
- .annotate(count=Count('command'))
69
- .order_by('-count')[:10]
71
+ queryset.values("command__name")
72
+ .annotate(count=Count("command"))
73
+ .order_by("-count")[:10]
70
74
  )
71
-
75
+
72
76
  # Common errors (non-empty error messages)
73
77
  common_errors = (
74
- queryset.filter(~Q(error_message=''), ~Q(error_message__isnull=True))
75
- .values('error_message')
76
- .annotate(count=Count('error_message'))
77
- .order_by('-count')[:10]
78
+ queryset.filter(~Q(error_message=""), ~Q(error_message__isnull=True))
79
+ .values("error_message")
80
+ .annotate(count=Count("error_message"))
81
+ .order_by("-count")[:10]
78
82
  )
79
-
83
+
80
84
  return Response({
81
- 'total_logs': total_logs,
82
- 'success_rate': round(success_rate, 2),
83
- 'last_24h': {
84
- 'total': recent_total,
85
- 'successful': recent_successful,
86
- 'failed': recent_failed
85
+ "total_logs": total_logs,
86
+ "success_rate": round(success_rate, 2),
87
+ "last_24h": {
88
+ "total": recent_total,
89
+ "successful": recent_successful,
90
+ "failed": recent_failed,
87
91
  },
88
- 'top_commands': [
89
- {'command_name': item['command__name'], 'count': item['count']}
92
+ "top_commands": [
93
+ {"command_name": item["command__name"], "count": item["count"]}
90
94
  for item in top_commands
91
95
  ],
92
- 'common_errors': [
93
- {'error': item['error_message'][:100], 'count': item['count']}
96
+ "common_errors": [
97
+ {"error": item["error_message"][:100], "count": item["count"]}
94
98
  for item in common_errors
95
- ]
99
+ ],
96
100
  })
97
-
101
+
98
102
  @COMMAND_LOG_EXPORT_SCHEMA
99
- @action(detail=False, methods=['get'], url_path='export')
103
+ @action(detail=False, methods=["get"], url_path="export")
100
104
  def export(self, request):
101
105
  """Export command logs"""
102
106
  import csv
103
107
  from datetime import datetime
104
-
105
- export_format = request.query_params.get('format', 'json')
106
- start_date = request.query_params.get('start_date')
107
- end_date = request.query_params.get('end_date')
108
-
108
+
109
+ export_format = request.query_params.get("format", "json")
110
+ start_date = request.query_params.get("start_date")
111
+ end_date = request.query_params.get("end_date")
112
+
109
113
  queryset = self.get_queryset()
110
-
114
+
111
115
  # Apply date filters if provided
112
116
  if start_date:
113
117
  try:
114
- start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
118
+ start_date = datetime.strptime(start_date, "%Y-%m-%d").date()
115
119
  queryset = queryset.filter(created__date__gte=start_date)
116
120
  except ValueError:
117
121
  return Response(
118
- {'error': 'Invalid start_date format. Use YYYY-MM-DD.'},
119
- status=status.HTTP_400_BAD_REQUEST
122
+ {"error": "Invalid start_date format. Use YYYY-MM-DD."},
123
+ status=status.HTTP_400_BAD_REQUEST,
120
124
  )
121
-
125
+
122
126
  if end_date:
123
127
  try:
124
- end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
128
+ end_date = datetime.strptime(end_date, "%Y-%m-%d").date()
125
129
  queryset = queryset.filter(created__date__lte=end_date)
126
130
  except ValueError:
127
131
  return Response(
128
- {'error': 'Invalid end_date format. Use YYYY-MM-DD.'},
129
- status=status.HTTP_400_BAD_REQUEST
132
+ {"error": "Invalid end_date format. Use YYYY-MM-DD."},
133
+ status=status.HTTP_400_BAD_REQUEST,
130
134
  )
131
-
135
+
132
136
  # Limit export size for performance
133
137
  if queryset.count() > 10000:
134
138
  return Response(
135
- {'error': 'Export too large. Please use date filters to reduce size.'},
136
- status=status.HTTP_400_BAD_REQUEST
139
+ {"error": "Export too large. Please use date filters to reduce size."},
140
+ status=status.HTTP_400_BAD_REQUEST,
137
141
  )
138
-
139
- if export_format == 'csv':
142
+
143
+ if export_format == "csv":
140
144
  # CSV export
141
- response = HttpResponse(content_type='text/csv')
142
- response['Content-Disposition'] = 'attachment; filename="command_logs.csv"'
143
-
145
+ response = HttpResponse(content_type="text/csv")
146
+ response["Content-Disposition"] = 'attachment; filename="command_logs.csv"'
147
+
144
148
  writer = csv.writer(response)
145
149
  writer.writerow([
146
- 'ID', 'Command', 'Device', 'User', 'Success',
147
- 'Created', 'Execution Time', 'Error Message'
150
+ "ID",
151
+ "Command",
152
+ "Device",
153
+ "User",
154
+ "Success",
155
+ "Created",
156
+ "Execution Time",
157
+ "Error Message",
148
158
  ])
149
-
150
- for log in queryset.select_related('command', 'device', 'user'):
159
+
160
+ for log in queryset.select_related("command", "device", "user"):
151
161
  writer.writerow([
152
162
  log.id,
153
163
  log.command.name,
154
164
  log.device.name,
155
- log.user.username if log.user else 'Unknown',
165
+ log.user.username if log.user else "Unknown",
156
166
  log.success,
157
167
  log.created.isoformat(),
158
168
  log.execution_time,
159
- log.error_message or ''
169
+ log.error_message or "",
160
170
  ])
161
-
171
+
162
172
  return response
163
-
173
+
164
174
  else:
165
175
  # JSON export
166
176
  serializer = self.get_serializer(queryset, many=True)
167
- return Response({
168
- 'count': queryset.count(),
169
- 'results': serializer.data
170
- })
177
+ return Response({"count": queryset.count(), "results": serializer.data})
@@ -1,29 +1,32 @@
1
1
  """
2
2
  API ViewSet for Command resources
3
3
  """
4
- from rest_framework import status
5
- from rest_framework.decorators import action
6
- from rest_framework.response import Response
4
+
7
5
  from django.db import transaction
8
- from drf_spectacular.utils import extend_schema_view
6
+
9
7
  from dcim.models import Device
10
8
  from netbox.api.viewsets import NetBoxModelViewSet
11
9
 
12
- from ... import models, filtersets
10
+ from drf_spectacular.utils import extend_schema_view
11
+ from rest_framework import status
12
+ from rest_framework.decorators import action
13
+ from rest_framework.response import Response
14
+
15
+ from ... import filtersets, models
13
16
  from ...services.command_service import CommandExecutionService
14
17
  from ...services.rate_limiting_service import RateLimitingService
15
- from ..serializers import CommandSerializer, CommandExecutionSerializer
16
18
  from ..mixins import APIResponseMixin, PermissionCheckMixin
17
19
  from ..schemas import (
18
- COMMAND_LIST_SCHEMA,
19
- COMMAND_RETRIEVE_SCHEMA,
20
+ COMMAND_BULK_EXECUTE_SCHEMA,
20
21
  COMMAND_CREATE_SCHEMA,
21
- COMMAND_UPDATE_SCHEMA,
22
- COMMAND_PARTIAL_UPDATE_SCHEMA,
23
22
  COMMAND_DESTROY_SCHEMA,
24
23
  COMMAND_EXECUTE_SCHEMA,
25
- COMMAND_BULK_EXECUTE_SCHEMA,
24
+ COMMAND_LIST_SCHEMA,
25
+ COMMAND_PARTIAL_UPDATE_SCHEMA,
26
+ COMMAND_RETRIEVE_SCHEMA,
27
+ COMMAND_UPDATE_SCHEMA,
26
28
  )
29
+ from ..serializers import CommandExecutionSerializer, CommandSerializer
27
30
 
28
31
 
29
32
  @extend_schema_view(
@@ -40,193 +43,197 @@ class CommandViewSet(NetBoxModelViewSet, APIResponseMixin, PermissionCheckMixin)
40
43
  filterset_class = filtersets.CommandFilterSet
41
44
  # Using custom RateLimitingService instead of generic API throttling
42
45
  # NetBox automatically handles object-based permissions - no need for explicit permission_classes
43
-
46
+
44
47
  def get_queryset(self):
45
48
  """NetBox will automatically filter based on user's ObjectPermissions"""
46
49
  # NetBox's object-based permission system will automatically filter this queryset
47
50
  # based on the user's ObjectPermissions for 'view' action on Command objects
48
51
  return super().get_queryset()
49
-
52
+
50
53
  @COMMAND_EXECUTE_SCHEMA
51
- @action(detail=True, methods=['post'], url_path='execute')
54
+ @action(detail=True, methods=["post"], url_path="execute")
52
55
  def execute_command(self, request, pk=None):
53
56
  """Execute a command on a device via API"""
54
57
  command = self.get_object()
55
-
58
+
56
59
  # Validate input using serializer
57
60
  execution_serializer = CommandExecutionSerializer(data=request.data)
58
61
  if not execution_serializer.is_valid():
59
62
  return Response(
60
- {
61
- 'error': 'Invalid input data',
62
- 'details': execution_serializer.errors
63
- },
64
- status=status.HTTP_400_BAD_REQUEST
63
+ {"error": "Invalid input data", "details": execution_serializer.errors},
64
+ status=status.HTTP_400_BAD_REQUEST,
65
65
  )
66
-
66
+
67
67
  validated_data = execution_serializer.validated_data
68
- device_id = validated_data['device_id']
69
- username = validated_data['username']
70
- password = validated_data['password']
71
-
68
+ device_id = validated_data["device_id"]
69
+ username = validated_data["username"]
70
+ password = validated_data["password"]
71
+
72
72
  # Get device object
73
73
  try:
74
74
  device = Device.objects.get(id=device_id)
75
75
  except Device.DoesNotExist:
76
76
  return Response(
77
- {'error': f'Device with ID {device_id} not found'},
78
- status=status.HTTP_404_NOT_FOUND
77
+ {"error": f"Device with ID {device_id} not found"},
78
+ status=status.HTTP_404_NOT_FOUND,
79
79
  )
80
-
80
+
81
81
  # Check permissions based on command type using NetBox's object-based permissions
82
- if command.command_type == 'config':
83
- if not self._user_has_action_permission(request.user, command, 'execute_config'):
84
- return Response(
85
- {'error': 'You do not have permission to execute configuration commands'},
86
- status=status.HTTP_403_FORBIDDEN
87
- )
88
- elif command.command_type == 'show':
89
- if not self._user_has_action_permission(request.user, command, 'execute_show'):
82
+ if command.command_type == "config":
83
+ if not self._user_has_action_permission(
84
+ request.user, command, "execute_config"
85
+ ):
90
86
  return Response(
91
- {'error': 'You do not have permission to execute show commands'},
92
- status=status.HTTP_403_FORBIDDEN
87
+ {
88
+ "error": "You do not have permission to execute configuration commands"
89
+ },
90
+ status=status.HTTP_403_FORBIDDEN,
93
91
  )
94
-
92
+ elif command.command_type == "show" and not self._user_has_action_permission(
93
+ request.user, command, "execute_show"
94
+ ):
95
+ return Response(
96
+ {"error": "You do not have permission to execute show commands"},
97
+ status=status.HTTP_403_FORBIDDEN,
98
+ )
99
+
95
100
  # Check custom rate limiting (device-specific with bypass rules)
96
101
  rate_limiting_service = RateLimitingService()
97
102
  rate_limit_check = rate_limiting_service.check_rate_limit(device, request.user)
98
-
99
- if not rate_limit_check['allowed']:
103
+
104
+ if not rate_limit_check["allowed"]:
100
105
  return Response(
101
106
  {
102
- 'error': 'Rate limit exceeded',
103
- 'details': {
104
- 'reason': rate_limit_check['reason'],
105
- 'current_count': rate_limit_check['current_count'],
106
- 'limit': rate_limit_check['limit'],
107
- 'time_window_minutes': rate_limit_check['time_window_minutes']
108
- }
107
+ "error": "Rate limit exceeded",
108
+ "details": {
109
+ "reason": rate_limit_check["reason"],
110
+ "current_count": rate_limit_check["current_count"],
111
+ "limit": rate_limit_check["limit"],
112
+ "time_window_minutes": rate_limit_check["time_window_minutes"],
113
+ },
109
114
  },
110
- status=status.HTTP_429_TOO_MANY_REQUESTS
115
+ status=status.HTTP_429_TOO_MANY_REQUESTS,
111
116
  )
112
-
117
+
113
118
  # Execute command using the service
114
119
  command_service = CommandExecutionService()
115
120
  result = command_service.execute_command_with_retry(
116
121
  command, device, username, password, max_retries=1
117
122
  )
118
-
123
+
119
124
  # Determine overall success - failed if either execution failed or syntax error detected
120
125
  overall_success = result.success and not result.has_syntax_error
121
-
126
+
122
127
  # Prepare response data
123
128
  response_data = {
124
- 'success': overall_success,
125
- 'output': result.output,
126
- 'error_message': result.error_message,
127
- 'execution_time': result.execution_time,
128
- 'command': {
129
- 'id': command.id,
130
- 'name': command.name,
131
- 'command_type': command.command_type
129
+ "success": overall_success,
130
+ "output": result.output,
131
+ "error_message": result.error_message,
132
+ "execution_time": result.execution_time,
133
+ "command": {
134
+ "id": command.id,
135
+ "name": command.name,
136
+ "command_type": command.command_type,
132
137
  },
133
- 'device': {
134
- 'id': device.id,
135
- 'name': device.name
136
- }
138
+ "device": {"id": device.id, "name": device.name},
137
139
  }
138
-
140
+
139
141
  # Add syntax error information if detected
140
142
  if result.has_syntax_error:
141
- response_data['syntax_error'] = {
142
- 'detected': True,
143
- 'type': result.syntax_error_type,
144
- 'vendor': result.syntax_error_vendor,
145
- 'guidance_provided': True
143
+ response_data["syntax_error"] = {
144
+ "detected": True,
145
+ "type": result.syntax_error_type,
146
+ "vendor": result.syntax_error_vendor,
147
+ "guidance_provided": True,
146
148
  }
147
149
  else:
148
- response_data['syntax_error'] = {
149
- 'detected': False
150
- }
151
-
150
+ response_data["syntax_error"] = {"detected": False}
151
+
152
152
  # Add parsing information if available
153
153
  if result.parsing_success and result.parsed_output:
154
- response_data['parsed_output'] = {
155
- 'success': True,
156
- 'method': result.parsing_method,
157
- 'data': result.parsed_output
154
+ response_data["parsed_output"] = {
155
+ "success": True,
156
+ "method": result.parsing_method,
157
+ "data": result.parsed_output,
158
158
  }
159
159
  else:
160
- response_data['parsed_output'] = {
161
- 'success': False,
162
- 'method': None,
163
- 'error': result.parsing_error
160
+ response_data["parsed_output"] = {
161
+ "success": False,
162
+ "method": None,
163
+ "error": result.parsing_error,
164
164
  }
165
-
165
+
166
166
  # Return appropriate status code
167
- status_code = status.HTTP_200_OK if overall_success else status.HTTP_400_BAD_REQUEST
168
-
167
+ status_code = (
168
+ status.HTTP_200_OK if overall_success else status.HTTP_400_BAD_REQUEST
169
+ )
170
+
169
171
  return Response(response_data, status=status_code)
170
-
172
+
171
173
  @COMMAND_BULK_EXECUTE_SCHEMA
172
- @action(detail=False, methods=['post'], url_path='bulk-execute')
174
+ @action(detail=False, methods=["post"], url_path="bulk-execute")
173
175
  def bulk_execute(self, request):
174
176
  """Execute multiple commands on multiple devices"""
175
- executions = request.data.get('executions', [])
176
-
177
+ executions = request.data.get("executions", [])
178
+
177
179
  if not executions:
178
180
  return Response(
179
- {'error': 'No executions provided'},
180
- status=status.HTTP_400_BAD_REQUEST
181
+ {"error": "No executions provided"}, status=status.HTTP_400_BAD_REQUEST
181
182
  )
182
-
183
+
183
184
  results = []
184
-
185
+
185
186
  with transaction.atomic():
186
187
  for i, execution_data in enumerate(executions):
187
188
  try:
188
189
  # Validate each execution
189
- command_id = execution_data.get('command_id')
190
- device_id = execution_data.get('device_id')
191
- username = execution_data.get('username')
192
- password = execution_data.get('password')
193
-
190
+ command_id = execution_data.get("command_id")
191
+ device_id = execution_data.get("device_id")
192
+ username = execution_data.get("username")
193
+ password = execution_data.get("password")
194
+
194
195
  if not all([command_id, device_id, username, password]):
195
196
  results.append({
196
- 'execution_id': i + 1,
197
- 'success': False,
198
- 'error': 'Missing required fields'
197
+ "execution_id": i + 1,
198
+ "success": False,
199
+ "error": "Missing required fields",
199
200
  })
200
201
  continue
201
-
202
+
202
203
  # Get command and device objects
203
204
  try:
204
205
  command = models.Command.objects.get(id=command_id)
205
206
  device = Device.objects.get(id=device_id)
206
207
  except (models.Command.DoesNotExist, Device.DoesNotExist) as e:
207
208
  results.append({
208
- 'execution_id': i + 1,
209
- 'success': False,
210
- 'error': f'Object not found: {str(e)}'
209
+ "execution_id": i + 1,
210
+ "success": False,
211
+ "error": f"Object not found: {str(e)}",
211
212
  })
212
213
  continue
213
-
214
+
214
215
  # Check permissions
215
- action = 'execute_config' if command.command_type == 'config' else 'execute_show'
216
- if not self._user_has_action_permission(request.user, command, action):
216
+ action = (
217
+ "execute_config"
218
+ if command.command_type == "config"
219
+ else "execute_show"
220
+ )
221
+ if not self._user_has_action_permission(
222
+ request.user, command, action
223
+ ):
217
224
  results.append({
218
- 'execution_id': i + 1,
219
- 'success': False,
220
- 'error': 'Insufficient permissions'
225
+ "execution_id": i + 1,
226
+ "success": False,
227
+ "error": "Insufficient permissions",
221
228
  })
222
229
  continue
223
-
230
+
224
231
  # Execute command
225
232
  command_service = CommandExecutionService()
226
233
  result = command_service.execute_command_with_retry(
227
234
  command, device, username, password, max_retries=1
228
235
  )
229
-
236
+
230
237
  # Create command log entry (this would typically be done by the service)
231
238
  log_entry = models.CommandLog.objects.create(
232
239
  command=command,
@@ -235,33 +242,32 @@ class CommandViewSet(NetBoxModelViewSet, APIResponseMixin, PermissionCheckMixin)
235
242
  output=result.output,
236
243
  error_message=result.error_message,
237
244
  execution_time=result.execution_time,
238
- success=result.success and not result.has_syntax_error
245
+ success=result.success and not result.has_syntax_error,
239
246
  )
240
-
247
+
241
248
  results.append({
242
- 'execution_id': i + 1,
243
- 'success': result.success and not result.has_syntax_error,
244
- 'command_log_id': log_entry.id,
245
- 'execution_time': result.execution_time
249
+ "execution_id": i + 1,
250
+ "success": result.success and not result.has_syntax_error,
251
+ "command_log_id": log_entry.id,
252
+ "execution_time": result.execution_time,
246
253
  })
247
-
254
+
248
255
  except Exception as e:
249
256
  results.append({
250
- 'execution_id': i + 1,
251
- 'success': False,
252
- 'error': f'Unexpected error: {str(e)}'
257
+ "execution_id": i + 1,
258
+ "success": False,
259
+ "error": f"Unexpected error: {str(e)}",
253
260
  })
254
-
261
+
255
262
  # Generate summary
256
263
  total = len(results)
257
- successful = sum(1 for r in results if r.get('success', False))
264
+ successful = sum(1 for r in results if r.get("success", False))
258
265
  failed = total - successful
259
-
260
- return Response({
261
- 'results': results,
262
- 'summary': {
263
- 'total': total,
264
- 'successful': successful,
265
- 'failed': failed
266
- }
267
- }, status=status.HTTP_200_OK)
266
+
267
+ return Response(
268
+ {
269
+ "results": results,
270
+ "summary": {"total": total, "successful": successful, "failed": failed},
271
+ },
272
+ status=status.HTTP_200_OK,
273
+ )