netbox-toolkit-plugin 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. netbox_toolkit/__init__.py +30 -0
  2. netbox_toolkit/admin.py +16 -0
  3. netbox_toolkit/api/__init__.py +0 -0
  4. netbox_toolkit/api/mixins.py +54 -0
  5. netbox_toolkit/api/schemas.py +234 -0
  6. netbox_toolkit/api/serializers.py +158 -0
  7. netbox_toolkit/api/urls.py +10 -0
  8. netbox_toolkit/api/views/__init__.py +10 -0
  9. netbox_toolkit/api/views/command_logs.py +170 -0
  10. netbox_toolkit/api/views/commands.py +267 -0
  11. netbox_toolkit/config.py +159 -0
  12. netbox_toolkit/connectors/__init__.py +15 -0
  13. netbox_toolkit/connectors/base.py +97 -0
  14. netbox_toolkit/connectors/factory.py +301 -0
  15. netbox_toolkit/connectors/netmiko_connector.py +443 -0
  16. netbox_toolkit/connectors/scrapli_connector.py +486 -0
  17. netbox_toolkit/exceptions.py +36 -0
  18. netbox_toolkit/filtersets.py +85 -0
  19. netbox_toolkit/forms.py +31 -0
  20. netbox_toolkit/migrations/0001_initial.py +54 -0
  21. netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +66 -0
  22. netbox_toolkit/migrations/0003_permission_system_update.py +48 -0
  23. netbox_toolkit/migrations/0004_remove_django_permissions.py +77 -0
  24. netbox_toolkit/migrations/0005_alter_command_options_and_more.py +25 -0
  25. netbox_toolkit/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +28 -0
  26. netbox_toolkit/migrations/0007_alter_commandlog_parsing_template.py +18 -0
  27. netbox_toolkit/migrations/__init__.py +0 -0
  28. netbox_toolkit/models.py +89 -0
  29. netbox_toolkit/navigation.py +30 -0
  30. netbox_toolkit/search.py +21 -0
  31. netbox_toolkit/services/__init__.py +7 -0
  32. netbox_toolkit/services/command_service.py +357 -0
  33. netbox_toolkit/services/device_service.py +87 -0
  34. netbox_toolkit/services/rate_limiting_service.py +228 -0
  35. netbox_toolkit/static/netbox_toolkit/css/toolkit.css +143 -0
  36. netbox_toolkit/static/netbox_toolkit/js/toolkit.js +657 -0
  37. netbox_toolkit/tables.py +37 -0
  38. netbox_toolkit/templates/netbox_toolkit/command.html +108 -0
  39. netbox_toolkit/templates/netbox_toolkit/command_edit.html +10 -0
  40. netbox_toolkit/templates/netbox_toolkit/command_list.html +12 -0
  41. netbox_toolkit/templates/netbox_toolkit/commandlog.html +170 -0
  42. netbox_toolkit/templates/netbox_toolkit/commandlog_list.html +4 -0
  43. netbox_toolkit/templates/netbox_toolkit/device_toolkit.html +536 -0
  44. netbox_toolkit/urls.py +22 -0
  45. netbox_toolkit/utils/__init__.py +1 -0
  46. netbox_toolkit/utils/connection.py +125 -0
  47. netbox_toolkit/utils/error_parser.py +428 -0
  48. netbox_toolkit/utils/logging.py +58 -0
  49. netbox_toolkit/utils/network.py +157 -0
  50. netbox_toolkit/views.py +385 -0
  51. netbox_toolkit_plugin-0.1.0.dist-info/METADATA +76 -0
  52. netbox_toolkit_plugin-0.1.0.dist-info/RECORD +56 -0
  53. netbox_toolkit_plugin-0.1.0.dist-info/WHEEL +5 -0
  54. netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +2 -0
  55. netbox_toolkit_plugin-0.1.0.dist-info/licenses/LICENSE +200 -0
  56. netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ from netbox.plugins import PluginConfig
2
+
3
+ class ToolkitConfig(PluginConfig):
4
+ name = 'netbox_toolkit_plugin'
5
+ verbose_name = 'Netbox Command Toolkit Plugin'
6
+ description = 'NetBox plugin for running pre-defined commands on network devices'
7
+ version = '0.1.0'
8
+ author = 'Andy Norwood'
9
+ base_url = 'toolkit'
10
+
11
+ # Database migrations
12
+ required_settings = []
13
+
14
+ # Default plugin settings
15
+ default_settings = {
16
+ 'rate_limiting_enabled': True,
17
+ 'device_command_limit': 10,
18
+ 'time_window_minutes': 5,
19
+ 'bypass_users': [],
20
+ 'bypass_groups': [],
21
+ 'debug_logging': False, # Enable debug logging for this plugin
22
+ }
23
+
24
+ # Middleware
25
+ middleware = []
26
+
27
+ # Django apps to load when plugin is activated
28
+ django_apps = []
29
+
30
+ config = ToolkitConfig
@@ -0,0 +1,16 @@
1
+ from django.contrib import admin
2
+ from netbox.admin import NetBoxModelAdmin
3
+ from .models import Command, CommandLog
4
+
5
+ @admin.register(Command)
6
+ class CommandAdmin(NetBoxModelAdmin):
7
+ list_display = ('name', 'platform', 'command_type', 'description')
8
+ list_filter = ('platform', 'command_type')
9
+ search_fields = ('name', 'command', 'description')
10
+
11
+ @admin.register(CommandLog)
12
+ class CommandLogAdmin(NetBoxModelAdmin):
13
+ list_display = ('command', 'device', 'username', 'execution_time')
14
+ list_filter = ('command', 'device', 'username', 'execution_time')
15
+ search_fields = ('command__name', 'device__name', 'username', 'output')
16
+ readonly_fields = ('output', 'execution_time')
File without changes
@@ -0,0 +1,54 @@
1
+ """
2
+ Common mixins and utilities for NetBox Toolkit API views
3
+ """
4
+ from rest_framework.response import Response
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from users.models import ObjectPermission
7
+
8
+
9
+ class APIResponseMixin:
10
+ """Mixin to add consistent API response headers"""
11
+
12
+ def finalize_response(self, request, response, *args, **kwargs):
13
+ """Add custom headers to all responses"""
14
+ response = super().finalize_response(request, response, *args, **kwargs)
15
+
16
+ # Add API version header
17
+ response['X-NetBox-Toolkit-API-Version'] = '1.0'
18
+
19
+ return response
20
+
21
+
22
+ class PermissionCheckMixin:
23
+ """Mixin for NetBox ObjectPermission checking"""
24
+
25
+ def _user_has_action_permission(self, user, obj, action):
26
+ """Check if user has permission for a specific action on an object using NetBox's ObjectPermission system"""
27
+ # Get content type for the object
28
+ content_type = ContentType.objects.get_for_model(obj)
29
+
30
+ # Check if user has any ObjectPermissions with the required action
31
+ user_permissions = ObjectPermission.objects.filter(
32
+ object_types__in=[content_type],
33
+ actions__contains=[action],
34
+ enabled=True
35
+ )
36
+
37
+ # Check if user is assigned to any groups with this permission
38
+ user_groups = user.groups.all()
39
+ for permission in user_permissions:
40
+ # Check if permission applies to user or user's groups
41
+ if (permission.users.filter(id=user.id).exists() or
42
+ permission.groups.filter(id__in=user_groups.values_list('id', flat=True)).exists()):
43
+
44
+ # If there are constraints, evaluate them
45
+ if permission.constraints:
46
+ # Create a queryset with the constraints and check if the object matches
47
+ queryset = content_type.model_class().objects.filter(**permission.constraints)
48
+ if queryset.filter(id=obj.id).exists():
49
+ return True
50
+ else:
51
+ # No constraints means permission applies to all objects of this type
52
+ return True
53
+
54
+ return False
@@ -0,0 +1,234 @@
1
+ """
2
+ OpenAPI schema definitions for NetBox Toolkit API
3
+ """
4
+ from drf_spectacular.utils import extend_schema, OpenApiResponse
5
+
6
+
7
+ # Command ViewSet Schemas
8
+ COMMAND_LIST_SCHEMA = extend_schema(
9
+ summary="List commands",
10
+ description="Retrieve a list of all commands available in the system.",
11
+ tags=["Commands"]
12
+ )
13
+
14
+ COMMAND_RETRIEVE_SCHEMA = extend_schema(
15
+ summary="Retrieve command",
16
+ description="Retrieve details of a specific command.",
17
+ tags=["Commands"]
18
+ )
19
+
20
+ COMMAND_CREATE_SCHEMA = extend_schema(
21
+ summary="Create command",
22
+ description="Create a new command.",
23
+ tags=["Commands"]
24
+ )
25
+
26
+ COMMAND_UPDATE_SCHEMA = extend_schema(
27
+ summary="Update command",
28
+ description="Update an existing command.",
29
+ tags=["Commands"]
30
+ )
31
+
32
+ COMMAND_PARTIAL_UPDATE_SCHEMA = extend_schema(
33
+ summary="Partial update command",
34
+ description="Partially update an existing command.",
35
+ tags=["Commands"]
36
+ )
37
+
38
+ COMMAND_DESTROY_SCHEMA = extend_schema(
39
+ summary="Delete command",
40
+ description="Delete a command.",
41
+ tags=["Commands"]
42
+ )
43
+
44
+ COMMAND_EXECUTE_SCHEMA = extend_schema(
45
+ summary="Execute command on device",
46
+ description="Execute a specific command on a target device with authentication credentials.",
47
+ tags=["Commands"],
48
+ responses={
49
+ 200: OpenApiResponse(
50
+ description="Command executed successfully",
51
+ examples=[
52
+ {
53
+ "success": True,
54
+ "output": "interface status output...",
55
+ "error_message": None,
56
+ "execution_time": 1.23,
57
+ "command": {
58
+ "id": 1,
59
+ "name": "show interfaces",
60
+ "command_type": "show"
61
+ },
62
+ "device": {
63
+ "id": 1,
64
+ "name": "switch01.example.com"
65
+ },
66
+ "syntax_error": {
67
+ "detected": False
68
+ },
69
+ "parsed_output": {
70
+ "success": True,
71
+ "method": "textfsm",
72
+ "data": [{"interface": "GigabitEthernet1/0/1", "status": "up"}]
73
+ }
74
+ }
75
+ ]
76
+ ),
77
+ 400: OpenApiResponse(
78
+ description="Bad request - validation errors or command execution failed"
79
+ ),
80
+ 403: OpenApiResponse(
81
+ description="Forbidden - insufficient permissions"
82
+ ),
83
+ 404: OpenApiResponse(
84
+ description="Not found - command or device not found"
85
+ ),
86
+ 429: OpenApiResponse(
87
+ description="Too many requests - rate limit exceeded"
88
+ )
89
+ }
90
+ )
91
+
92
+ COMMAND_BULK_EXECUTE_SCHEMA = extend_schema(
93
+ summary="Bulk execute commands",
94
+ description="Execute multiple commands on multiple devices in a single API call.",
95
+ tags=["Commands"],
96
+ request={
97
+ "type": "object",
98
+ "properties": {
99
+ "executions": {
100
+ "type": "array",
101
+ "items": {
102
+ "type": "object",
103
+ "properties": {
104
+ "command_id": {"type": "integer"},
105
+ "device_id": {"type": "integer"},
106
+ "username": {"type": "string"},
107
+ "password": {"type": "string", "format": "password"}
108
+ },
109
+ "required": ["command_id", "device_id", "username", "password"]
110
+ }
111
+ }
112
+ },
113
+ "required": ["executions"]
114
+ },
115
+ responses={
116
+ 200: OpenApiResponse(
117
+ description="Bulk execution completed",
118
+ examples=[
119
+ {
120
+ "results": [
121
+ {"execution_id": 1, "success": True, "command_log_id": 123},
122
+ {"execution_id": 2, "success": False, "error": "Permission denied"}
123
+ ],
124
+ "summary": {
125
+ "total": 2,
126
+ "successful": 1,
127
+ "failed": 1
128
+ }
129
+ }
130
+ ]
131
+ )
132
+ }
133
+ )
134
+
135
+ # Command Log ViewSet Schemas
136
+ COMMAND_LOG_LIST_SCHEMA = extend_schema(
137
+ summary="List command logs",
138
+ description="Retrieve a list of command execution logs with filtering and search capabilities.",
139
+ tags=["Command Logs"]
140
+ )
141
+
142
+ COMMAND_LOG_RETRIEVE_SCHEMA = extend_schema(
143
+ summary="Retrieve command log",
144
+ description="Retrieve details of a specific command execution log.",
145
+ tags=["Command Logs"]
146
+ )
147
+
148
+ COMMAND_LOG_CREATE_SCHEMA = extend_schema(
149
+ summary="Create command log",
150
+ description="Create a new command execution log entry.",
151
+ tags=["Command Logs"]
152
+ )
153
+
154
+ COMMAND_LOG_UPDATE_SCHEMA = extend_schema(
155
+ summary="Update command log",
156
+ description="Update an existing command log entry.",
157
+ tags=["Command Logs"]
158
+ )
159
+
160
+ COMMAND_LOG_PARTIAL_UPDATE_SCHEMA = extend_schema(
161
+ summary="Partial update command log",
162
+ description="Partially update an existing command log entry.",
163
+ tags=["Command Logs"]
164
+ )
165
+
166
+ COMMAND_LOG_DESTROY_SCHEMA = extend_schema(
167
+ summary="Delete command log",
168
+ description="Delete a command log entry.",
169
+ tags=["Command Logs"]
170
+ )
171
+
172
+ COMMAND_LOG_STATISTICS_SCHEMA = extend_schema(
173
+ summary="Get command log statistics",
174
+ description="Retrieve statistics about command execution logs including success rates and common errors.",
175
+ tags=["Command Logs"],
176
+ responses={
177
+ 200: OpenApiResponse(
178
+ description="Statistics retrieved successfully",
179
+ examples=[
180
+ {
181
+ "total_logs": 1000,
182
+ "success_rate": 85.5,
183
+ "last_24h": {
184
+ "total": 50,
185
+ "successful": 45,
186
+ "failed": 5
187
+ },
188
+ "top_commands": [
189
+ {"command_name": "show interfaces", "count": 150},
190
+ {"command_name": "show version", "count": 120}
191
+ ],
192
+ "common_errors": [
193
+ {"error": "Connection timeout", "count": 10},
194
+ {"error": "Invalid command", "count": 5}
195
+ ]
196
+ }
197
+ ]
198
+ )
199
+ }
200
+ )
201
+
202
+ from drf_spectacular.utils import OpenApiParameter
203
+
204
+ COMMAND_LOG_EXPORT_SCHEMA = extend_schema(
205
+ summary="Export command logs",
206
+ description="Export command logs in various formats (CSV, JSON).",
207
+ tags=["Command Logs"],
208
+ parameters=[
209
+ OpenApiParameter(
210
+ name='format',
211
+ description='Export format',
212
+ required=False,
213
+ type=str,
214
+ enum=['csv', 'json'],
215
+ default='json'
216
+ ),
217
+ OpenApiParameter(
218
+ name='start_date',
219
+ description='Start date for export (YYYY-MM-DD)',
220
+ required=False,
221
+ type=str
222
+ ),
223
+ OpenApiParameter(
224
+ name='end_date',
225
+ description='End date for export (YYYY-MM-DD)',
226
+ required=False,
227
+ type=str
228
+ ),
229
+ ],
230
+ responses={
231
+ 200: OpenApiResponse(description="Export data"),
232
+ 400: OpenApiResponse(description="Invalid parameters")
233
+ }
234
+ )
@@ -0,0 +1,158 @@
1
+ from rest_framework import serializers
2
+ from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
3
+ from dcim.api.serializers import PlatformSerializer, DeviceSerializer
4
+ from ..models import Command, CommandLog
5
+
6
+ class CommandExecutionSerializer(serializers.Serializer):
7
+ """Serializer for command execution input validation"""
8
+ device_id = serializers.IntegerField(
9
+ help_text="ID of the device to execute the command on"
10
+ )
11
+ username = serializers.CharField(
12
+ max_length=100,
13
+ help_text="Username for device authentication",
14
+ trim_whitespace=True
15
+ )
16
+ password = serializers.CharField(
17
+ max_length=255,
18
+ style={'input_type': 'password'},
19
+ help_text="Password for device authentication",
20
+ trim_whitespace=False
21
+ )
22
+ timeout = serializers.IntegerField(
23
+ required=False,
24
+ default=30,
25
+ min_value=5,
26
+ max_value=300,
27
+ help_text="Command execution timeout in seconds (5-300)"
28
+ )
29
+
30
+ def validate_device_id(self, value):
31
+ """Validate that the device exists and has required attributes"""
32
+ from dcim.models import Device
33
+ try:
34
+ device = Device.objects.get(id=value)
35
+ if not device.platform:
36
+ raise serializers.ValidationError(
37
+ "Device must have a platform assigned for command execution"
38
+ )
39
+ if not device.primary_ip:
40
+ raise serializers.ValidationError(
41
+ "Device must have a primary IP address for command execution"
42
+ )
43
+ return value
44
+ except Device.DoesNotExist:
45
+ raise serializers.ValidationError("Device not found")
46
+
47
+ def validate(self, data):
48
+ """Cross-field validation and object retrieval"""
49
+ from dcim.models import Device
50
+
51
+ # Get the actual device object for use in views
52
+ device = Device.objects.get(id=data['device_id'])
53
+ data['device'] = device
54
+
55
+ return data
56
+
57
+ def validate_username(self, value):
58
+ """Validate username format"""
59
+ if not value.strip():
60
+ raise serializers.ValidationError("Username cannot be empty")
61
+ if len(value.strip()) < 2:
62
+ raise serializers.ValidationError("Username must be at least 2 characters")
63
+ return value.strip()
64
+
65
+ def validate_password(self, value):
66
+ """Validate password"""
67
+ if not value:
68
+ raise serializers.ValidationError("Password cannot be empty")
69
+ if len(value) < 3:
70
+ raise serializers.ValidationError("Password must be at least 3 characters")
71
+ return value
72
+
73
+ class NestedCommandSerializer(WritableNestedSerializer):
74
+ url = serializers.HyperlinkedIdentityField(
75
+ view_name='plugins-api:netbox_toolkit-api:command-detail'
76
+ )
77
+
78
+ class Meta:
79
+ model = Command
80
+ fields = ('id', 'url', 'name', 'display')
81
+
82
+ class CommandSerializer(NetBoxModelSerializer):
83
+ url = serializers.HyperlinkedIdentityField(
84
+ view_name='plugins-api:netbox_toolkit-api:command-detail'
85
+ )
86
+ platform = PlatformSerializer(nested=True)
87
+
88
+ class Meta:
89
+ model = Command
90
+ fields = (
91
+ 'id', 'url', 'display', 'name', 'command', 'description',
92
+ 'platform', 'command_type',
93
+ 'tags', 'custom_fields', 'created', 'last_updated'
94
+ )
95
+ brief_fields = ('id', 'url', 'display', 'name', 'command_type', 'platform')
96
+
97
+ class CommandLogSerializer(NetBoxModelSerializer):
98
+ url = serializers.HyperlinkedIdentityField(
99
+ view_name='plugins-api:netbox_toolkit-api:commandlog-detail'
100
+ )
101
+ command = NestedCommandSerializer()
102
+ device = DeviceSerializer(nested=True)
103
+
104
+ class Meta:
105
+ model = CommandLog
106
+ fields = (
107
+ 'id', 'url', 'display', 'command', 'device', 'output',
108
+ 'username', 'execution_time', 'success', 'error_message', 'execution_duration',
109
+ 'parsed_data', 'parsing_success', 'parsing_template',
110
+ 'created', 'last_updated'
111
+ )
112
+ brief_fields = ('id', 'url', 'display', 'command', 'device', 'username', 'execution_time', 'success')
113
+
114
+ class BulkCommandExecutionSerializer(serializers.Serializer):
115
+ """Serializer for bulk command execution validation"""
116
+ command_id = serializers.IntegerField(
117
+ help_text="ID of the command to execute"
118
+ )
119
+ device_id = serializers.IntegerField(
120
+ help_text="ID of the device to execute the command on"
121
+ )
122
+ username = serializers.CharField(
123
+ max_length=100,
124
+ help_text="Username for device authentication"
125
+ )
126
+ password = serializers.CharField(
127
+ max_length=255,
128
+ style={'input_type': 'password'},
129
+ help_text="Password for device authentication"
130
+ )
131
+ timeout = serializers.IntegerField(
132
+ required=False,
133
+ default=30,
134
+ min_value=5,
135
+ max_value=300,
136
+ help_text="Command execution timeout in seconds"
137
+ )
138
+
139
+ def validate_command_id(self, value):
140
+ """Validate that the command exists"""
141
+ try:
142
+ Command.objects.get(id=value)
143
+ return value
144
+ except Command.DoesNotExist:
145
+ raise serializers.ValidationError("Command not found")
146
+
147
+ def validate_device_id(self, value):
148
+ """Validate that the device exists"""
149
+ from dcim.models import Device
150
+ try:
151
+ device = Device.objects.get(id=value)
152
+ if not device.platform:
153
+ raise serializers.ValidationError(
154
+ "Device must have a platform assigned"
155
+ )
156
+ return value
157
+ except Device.DoesNotExist:
158
+ raise serializers.ValidationError("Device not found")
@@ -0,0 +1,10 @@
1
+ from netbox.api.routers import NetBoxRouter
2
+ from .views import CommandViewSet, CommandLogViewSet
3
+
4
+ app_name = 'netbox_toolkit'
5
+
6
+ router = NetBoxRouter()
7
+ router.register('commands', CommandViewSet)
8
+ router.register('command-logs', CommandLogViewSet)
9
+
10
+ urlpatterns = router.urls
@@ -0,0 +1,10 @@
1
+ """
2
+ Import all viewsets for easier access
3
+ """
4
+ from .commands import CommandViewSet
5
+ from .command_logs import CommandLogViewSet
6
+
7
+ __all__ = [
8
+ 'CommandViewSet',
9
+ 'CommandLogViewSet',
10
+ ]
@@ -0,0 +1,170 @@
1
+ """
2
+ API ViewSet for CommandLog resources
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
8
+ from datetime import timedelta
9
+ from django.db.models import Count, Q
10
+ from django.http import HttpResponse
11
+ from drf_spectacular.utils import extend_schema_view
12
+ from netbox.api.viewsets import NetBoxModelViewSet
13
+
14
+ from ... import models, filtersets
15
+ from ..serializers import CommandLogSerializer
16
+ from ..mixins import APIResponseMixin
17
+ from ..schemas import (
18
+ COMMAND_LOG_LIST_SCHEMA,
19
+ COMMAND_LOG_RETRIEVE_SCHEMA,
20
+ COMMAND_LOG_CREATE_SCHEMA,
21
+ COMMAND_LOG_UPDATE_SCHEMA,
22
+ COMMAND_LOG_PARTIAL_UPDATE_SCHEMA,
23
+ COMMAND_LOG_DESTROY_SCHEMA,
24
+ COMMAND_LOG_STATISTICS_SCHEMA,
25
+ COMMAND_LOG_EXPORT_SCHEMA,
26
+ )
27
+
28
+
29
+ @extend_schema_view(
30
+ list=COMMAND_LOG_LIST_SCHEMA,
31
+ retrieve=COMMAND_LOG_RETRIEVE_SCHEMA,
32
+ create=COMMAND_LOG_CREATE_SCHEMA,
33
+ update=COMMAND_LOG_UPDATE_SCHEMA,
34
+ partial_update=COMMAND_LOG_PARTIAL_UPDATE_SCHEMA,
35
+ destroy=COMMAND_LOG_DESTROY_SCHEMA,
36
+ )
37
+ class CommandLogViewSet(NetBoxModelViewSet, APIResponseMixin):
38
+ queryset = models.CommandLog.objects.all()
39
+ serializer_class = CommandLogSerializer
40
+ filterset_class = filtersets.CommandLogFilterSet
41
+ # NetBox automatically handles object-based permissions - no need for explicit permission_classes
42
+
43
+ def get_queryset(self):
44
+ """NetBox will automatically filter based on user's ObjectPermissions"""
45
+ return super().get_queryset()
46
+
47
+ @COMMAND_LOG_STATISTICS_SCHEMA
48
+ @action(detail=False, methods=['get'], url_path='statistics')
49
+ def statistics(self, request):
50
+ """Get command execution statistics"""
51
+ queryset = self.get_queryset()
52
+
53
+ # Basic stats
54
+ total_logs = queryset.count()
55
+ successful_logs = queryset.filter(success=True).count()
56
+ success_rate = (successful_logs / total_logs * 100) if total_logs > 0 else 0
57
+
58
+ # Last 24 hours stats
59
+ last_24h = timezone.now() - timedelta(hours=24)
60
+ recent_logs = queryset.filter(created__gte=last_24h)
61
+ recent_total = recent_logs.count()
62
+ recent_successful = recent_logs.filter(success=True).count()
63
+ recent_failed = recent_total - recent_successful
64
+
65
+ # Top commands
66
+ top_commands = (
67
+ queryset.values('command__name')
68
+ .annotate(count=Count('command'))
69
+ .order_by('-count')[:10]
70
+ )
71
+
72
+ # Common errors (non-empty error messages)
73
+ 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
+ )
79
+
80
+ 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
87
+ },
88
+ 'top_commands': [
89
+ {'command_name': item['command__name'], 'count': item['count']}
90
+ for item in top_commands
91
+ ],
92
+ 'common_errors': [
93
+ {'error': item['error_message'][:100], 'count': item['count']}
94
+ for item in common_errors
95
+ ]
96
+ })
97
+
98
+ @COMMAND_LOG_EXPORT_SCHEMA
99
+ @action(detail=False, methods=['get'], url_path='export')
100
+ def export(self, request):
101
+ """Export command logs"""
102
+ import csv
103
+ 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
+
109
+ queryset = self.get_queryset()
110
+
111
+ # Apply date filters if provided
112
+ if start_date:
113
+ try:
114
+ start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
115
+ queryset = queryset.filter(created__date__gte=start_date)
116
+ except ValueError:
117
+ return Response(
118
+ {'error': 'Invalid start_date format. Use YYYY-MM-DD.'},
119
+ status=status.HTTP_400_BAD_REQUEST
120
+ )
121
+
122
+ if end_date:
123
+ try:
124
+ end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
125
+ queryset = queryset.filter(created__date__lte=end_date)
126
+ except ValueError:
127
+ return Response(
128
+ {'error': 'Invalid end_date format. Use YYYY-MM-DD.'},
129
+ status=status.HTTP_400_BAD_REQUEST
130
+ )
131
+
132
+ # Limit export size for performance
133
+ if queryset.count() > 10000:
134
+ return Response(
135
+ {'error': 'Export too large. Please use date filters to reduce size.'},
136
+ status=status.HTTP_400_BAD_REQUEST
137
+ )
138
+
139
+ if export_format == 'csv':
140
+ # CSV export
141
+ response = HttpResponse(content_type='text/csv')
142
+ response['Content-Disposition'] = 'attachment; filename="command_logs.csv"'
143
+
144
+ writer = csv.writer(response)
145
+ writer.writerow([
146
+ 'ID', 'Command', 'Device', 'User', 'Success',
147
+ 'Created', 'Execution Time', 'Error Message'
148
+ ])
149
+
150
+ for log in queryset.select_related('command', 'device', 'user'):
151
+ writer.writerow([
152
+ log.id,
153
+ log.command.name,
154
+ log.device.name,
155
+ log.user.username if log.user else 'Unknown',
156
+ log.success,
157
+ log.created.isoformat(),
158
+ log.execution_time,
159
+ log.error_message or ''
160
+ ])
161
+
162
+ return response
163
+
164
+ else:
165
+ # JSON export
166
+ serializer = self.get_serializer(queryset, many=True)
167
+ return Response({
168
+ 'count': queryset.count(),
169
+ 'results': serializer.data
170
+ })