django-cfg 1.2.15__py3-none-any.whl → 1.2.17__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 (61) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/README.md +305 -0
  3. django_cfg/apps/maintenance/__init__.py +27 -0
  4. django_cfg/apps/maintenance/admin/__init__.py +28 -0
  5. django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
  6. django_cfg/apps/maintenance/admin/events_admin.py +374 -0
  7. django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
  8. django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
  9. django_cfg/apps/maintenance/apps.py +105 -0
  10. django_cfg/apps/maintenance/management/__init__.py +0 -0
  11. django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
  12. django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
  13. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
  14. django_cfg/apps/maintenance/managers/__init__.py +20 -0
  15. django_cfg/apps/maintenance/managers/deployments.py +287 -0
  16. django_cfg/apps/maintenance/managers/events.py +374 -0
  17. django_cfg/apps/maintenance/managers/monitoring.py +301 -0
  18. django_cfg/apps/maintenance/managers/sites.py +335 -0
  19. django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
  20. django_cfg/apps/maintenance/migrations/__init__.py +0 -0
  21. django_cfg/apps/maintenance/models/__init__.py +27 -0
  22. django_cfg/apps/maintenance/models/cloudflare.py +316 -0
  23. django_cfg/apps/maintenance/models/maintenance.py +334 -0
  24. django_cfg/apps/maintenance/models/monitoring.py +393 -0
  25. django_cfg/apps/maintenance/models/sites.py +419 -0
  26. django_cfg/apps/maintenance/serializers/__init__.py +60 -0
  27. django_cfg/apps/maintenance/serializers/actions.py +310 -0
  28. django_cfg/apps/maintenance/serializers/base.py +44 -0
  29. django_cfg/apps/maintenance/serializers/deployments.py +209 -0
  30. django_cfg/apps/maintenance/serializers/events.py +210 -0
  31. django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
  32. django_cfg/apps/maintenance/serializers/sites.py +213 -0
  33. django_cfg/apps/maintenance/services/README.md +168 -0
  34. django_cfg/apps/maintenance/services/__init__.py +21 -0
  35. django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
  36. django_cfg/apps/maintenance/services/dns_manager.py +497 -0
  37. django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
  38. django_cfg/apps/maintenance/services/site_sync.py +448 -0
  39. django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
  40. django_cfg/apps/maintenance/services/worker_manager.py +264 -0
  41. django_cfg/apps/maintenance/signals.py +38 -0
  42. django_cfg/apps/maintenance/urls.py +36 -0
  43. django_cfg/apps/maintenance/views/__init__.py +18 -0
  44. django_cfg/apps/maintenance/views/base.py +61 -0
  45. django_cfg/apps/maintenance/views/deployments.py +175 -0
  46. django_cfg/apps/maintenance/views/events.py +204 -0
  47. django_cfg/apps/maintenance/views/monitoring.py +213 -0
  48. django_cfg/apps/maintenance/views/sites.py +338 -0
  49. django_cfg/apps/urls.py +5 -1
  50. django_cfg/core/config.py +34 -3
  51. django_cfg/core/generation.py +15 -10
  52. django_cfg/models/cloudflare.py +316 -0
  53. django_cfg/models/revolution.py +1 -1
  54. django_cfg/models/tasks.py +1 -1
  55. django_cfg/modules/base.py +12 -5
  56. django_cfg/modules/django_unfold/dashboard.py +16 -1
  57. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/METADATA +2 -1
  58. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/RECORD +61 -13
  59. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/WHEEL +0 -0
  60. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/entry_points.txt +0 -0
  61. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,61 @@
1
+ """
2
+ Base views for maintenance app.
3
+
4
+ Common functionality and mixins for maintenance views.
5
+ """
6
+
7
+ import logging
8
+ from rest_framework import permissions
9
+ from rest_framework.response import Response
10
+ from rest_framework import status
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class MaintenancePermissionMixin:
16
+ """Mixin for maintenance app permissions."""
17
+
18
+ permission_classes = [permissions.IsAuthenticated]
19
+
20
+ def get_user_queryset(self, model_class, owner_field='owner'):
21
+ """Get queryset filtered by user permissions."""
22
+ if getattr(self, 'swagger_fake_view', False):
23
+ return model_class.objects.none()
24
+
25
+ user = self.request.user
26
+ if user.is_staff:
27
+ return model_class.objects.all()
28
+
29
+ # Use dynamic field lookup
30
+ filter_kwargs = {owner_field: user}
31
+ return model_class.objects.filter(**filter_kwargs)
32
+
33
+
34
+ class MaintenanceResponseMixin:
35
+ """Mixin for standardized API responses."""
36
+
37
+ def success_response(self, message, data=None):
38
+ """Return standardized success response."""
39
+ response_data = {
40
+ 'success': True,
41
+ 'message': message
42
+ }
43
+ if data is not None:
44
+ response_data['data'] = data
45
+ return Response(response_data)
46
+
47
+ def error_response(self, error, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR):
48
+ """Return standardized error response."""
49
+ logger.error(f"API Error: {error}")
50
+ return Response({
51
+ 'success': False,
52
+ 'error': str(error)
53
+ }, status=status_code)
54
+
55
+ def validation_error_response(self, errors):
56
+ """Return standardized validation error response."""
57
+ return Response({
58
+ 'success': False,
59
+ 'error': 'Validation failed',
60
+ 'errors': errors
61
+ }, status=status.HTTP_400_BAD_REQUEST)
@@ -0,0 +1,175 @@
1
+ """
2
+ Cloudflare deployment views.
3
+
4
+ ViewSets for CloudflareDeployment management.
5
+ """
6
+
7
+ import logging
8
+ from rest_framework import viewsets
9
+ from rest_framework.decorators import action
10
+ from drf_spectacular.utils import extend_schema, extend_schema_view
11
+
12
+ from ..models import CloudflareDeployment
13
+ from ..serializers import (
14
+ CloudflareDeploymentSerializer, CloudflareDeploymentListSerializer
15
+ )
16
+ from .base import MaintenancePermissionMixin, MaintenanceResponseMixin
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @extend_schema_view(
22
+ list=extend_schema(
23
+ summary="List Cloudflare deployments",
24
+ description="Get list of Cloudflare deployments"
25
+ ),
26
+ retrieve=extend_schema(
27
+ summary="Get Cloudflare deployment",
28
+ description="Get detailed information about a Cloudflare deployment"
29
+ ),
30
+ destroy=extend_schema(
31
+ summary="Delete Cloudflare deployment",
32
+ description="Delete a Cloudflare deployment record"
33
+ )
34
+ )
35
+ class CloudflareDeploymentViewSet(MaintenancePermissionMixin, MaintenanceResponseMixin, viewsets.ReadOnlyModelViewSet):
36
+ """ViewSet for managing Cloudflare deployments (read-only)."""
37
+
38
+ serializer_class = CloudflareDeploymentSerializer
39
+ lookup_field = 'id'
40
+ filterset_fields = ['deployment_type', 'status']
41
+ ordering = ['-deployed_at']
42
+
43
+ def get_queryset(self):
44
+ """Get queryset filtered by user permissions."""
45
+ # CloudflareDeployment is related to CloudflareSite via site.owner
46
+ if getattr(self, 'swagger_fake_view', False):
47
+ return CloudflareDeployment.objects.none()
48
+
49
+ user = self.request.user
50
+ if user.is_staff:
51
+ return CloudflareDeployment.objects.all()
52
+
53
+ return CloudflareDeployment.objects.filter(site__owner=user)
54
+
55
+ def get_serializer_class(self):
56
+ """Return appropriate serializer based on action."""
57
+ if self.action == 'list':
58
+ return CloudflareDeploymentListSerializer
59
+ return CloudflareDeploymentSerializer
60
+
61
+ @action(detail=True, methods=['post'])
62
+ @extend_schema(
63
+ summary="Rollback deployment",
64
+ description="Rollback this Cloudflare deployment"
65
+ )
66
+ def rollback(self, request, id=None):
67
+ """Rollback a deployment."""
68
+ try:
69
+ deployment = self.get_object()
70
+
71
+ if deployment.status != CloudflareDeployment.Status.ACTIVE:
72
+ return self.error_response(
73
+ f'Cannot rollback deployment with status: {deployment.status}',
74
+ status_code=400
75
+ )
76
+
77
+ # Import Cloudflare service
78
+ from ..services.cloudflare_service import CloudflareService
79
+
80
+ cloudflare_service = CloudflareService()
81
+ success = cloudflare_service.rollback_deployment(deployment)
82
+
83
+ if success:
84
+ deployment.status = CloudflareDeployment.Status.ROLLED_BACK
85
+ deployment.save()
86
+
87
+ return self.success_response(f'Deployment {deployment.id} rolled back successfully')
88
+ else:
89
+ return self.error_response('Failed to rollback deployment')
90
+
91
+ except Exception as e:
92
+ return self.error_response(f"Rollback deployment error: {e}")
93
+
94
+ @action(detail=True, methods=['get'])
95
+ @extend_schema(
96
+ summary="Get deployment logs",
97
+ description="Get logs for this deployment"
98
+ )
99
+ def logs(self, request, id=None):
100
+ """Get logs for a deployment."""
101
+ try:
102
+ deployment = self.get_object()
103
+
104
+ # Import Cloudflare service
105
+ from ..services.cloudflare_service import CloudflareService
106
+
107
+ cloudflare_service = CloudflareService()
108
+ logs = cloudflare_service.get_deployment_logs(deployment)
109
+
110
+ return self.success_response(
111
+ f'Retrieved logs for deployment {deployment.id}',
112
+ data={'logs': logs}
113
+ )
114
+
115
+ except Exception as e:
116
+ return self.error_response(f"Get deployment logs error: {e}")
117
+
118
+ @action(detail=True, methods=['get'])
119
+ @extend_schema(
120
+ summary="Get deployment status",
121
+ description="Get current status of this deployment from Cloudflare"
122
+ )
123
+ def status(self, request, id=None):
124
+ """Get current deployment status from Cloudflare."""
125
+ try:
126
+ deployment = self.get_object()
127
+
128
+ # Import Cloudflare service
129
+ from ..services.cloudflare_service import CloudflareService
130
+
131
+ cloudflare_service = CloudflareService()
132
+ status_info = cloudflare_service.get_deployment_status(deployment)
133
+
134
+ return self.success_response(
135
+ f'Retrieved status for deployment {deployment.id}',
136
+ data=status_info
137
+ )
138
+
139
+ except Exception as e:
140
+ return self.error_response(f"Get deployment status error: {e}")
141
+
142
+ @action(detail=False, methods=['get'])
143
+ @extend_schema(
144
+ summary="Get deployment statistics",
145
+ description="Get deployment statistics for user's sites"
146
+ )
147
+ def statistics(self, request):
148
+ """Get deployment statistics."""
149
+ try:
150
+ deployments = self.get_queryset()
151
+
152
+ stats = {
153
+ 'total_deployments': deployments.count(),
154
+ 'active_deployments': deployments.filter(status=CloudflareDeployment.Status.ACTIVE).count(),
155
+ 'failed_deployments': deployments.filter(status=CloudflareDeployment.Status.FAILED).count(),
156
+ 'rolled_back_deployments': deployments.filter(status=CloudflareDeployment.Status.ROLLED_BACK).count(),
157
+ 'by_type': {},
158
+ 'recent_deployments': []
159
+ }
160
+
161
+ # Count by deployment type
162
+ for deployment_type in CloudflareDeployment.DeploymentType.choices:
163
+ type_value = deployment_type[0]
164
+ count = deployments.filter(deployment_type=type_value).count()
165
+ stats['by_type'][type_value] = count
166
+
167
+ # Get recent deployments
168
+ recent = deployments.order_by('-deployed_at')[:10]
169
+ serializer = CloudflareDeploymentListSerializer(recent, many=True)
170
+ stats['recent_deployments'] = serializer.data
171
+
172
+ return self.success_response('Deployment statistics retrieved', data=stats)
173
+
174
+ except Exception as e:
175
+ return self.error_response(f"Get deployment statistics error: {e}")
@@ -0,0 +1,204 @@
1
+ """
2
+ Maintenance event views.
3
+
4
+ ViewSets for MaintenanceEvent management.
5
+ """
6
+
7
+ import logging
8
+ from rest_framework import viewsets
9
+ from rest_framework.decorators import action
10
+ from drf_spectacular.utils import extend_schema, extend_schema_view
11
+
12
+ from ..models import MaintenanceEvent
13
+ from ..serializers import (
14
+ MaintenanceEventSerializer, MaintenanceEventCreateSerializer, MaintenanceEventListSerializer,
15
+ MaintenanceEventUpdateSerializer
16
+ )
17
+ from .base import MaintenancePermissionMixin, MaintenanceResponseMixin
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @extend_schema_view(
23
+ list=extend_schema(
24
+ summary="List maintenance events",
25
+ description="Get list of maintenance events"
26
+ ),
27
+ create=extend_schema(
28
+ summary="Create maintenance event",
29
+ description="Create a new maintenance event"
30
+ ),
31
+ retrieve=extend_schema(
32
+ summary="Get maintenance event",
33
+ description="Get detailed information about a maintenance event"
34
+ ),
35
+ update=extend_schema(
36
+ summary="Update maintenance event",
37
+ description="Update maintenance event details"
38
+ ),
39
+ destroy=extend_schema(
40
+ summary="Delete maintenance event",
41
+ description="Delete a maintenance event"
42
+ )
43
+ )
44
+ class MaintenanceEventViewSet(MaintenancePermissionMixin, MaintenanceResponseMixin, viewsets.ModelViewSet):
45
+ """ViewSet for managing maintenance events."""
46
+
47
+ serializer_class = MaintenanceEventSerializer
48
+ lookup_field = 'id'
49
+ filterset_fields = ['status', 'reason']
50
+ ordering = ['-created_at']
51
+
52
+ def get_queryset(self):
53
+ """Get queryset filtered by user permissions."""
54
+ return self.get_user_queryset(MaintenanceEvent, 'initiated_by')
55
+
56
+ def get_serializer_class(self):
57
+ """Return appropriate serializer based on action."""
58
+ if self.action == 'create':
59
+ return MaintenanceEventCreateSerializer
60
+ elif self.action == 'list':
61
+ return MaintenanceEventListSerializer
62
+ elif self.action in ['update', 'partial_update']:
63
+ return MaintenanceEventUpdateSerializer
64
+ return MaintenanceEventSerializer
65
+
66
+ def perform_create(self, serializer):
67
+ """Set initiated_by when creating event."""
68
+ serializer.save(initiated_by=self.request.user)
69
+
70
+ @action(detail=True, methods=['post'])
71
+ @extend_schema(
72
+ summary="Complete maintenance event",
73
+ description="Mark maintenance event as completed"
74
+ )
75
+ def complete(self, request, id=None):
76
+ """Complete a maintenance event."""
77
+ try:
78
+ event = self.get_object()
79
+
80
+ if event.status != MaintenanceEvent.Status.ACTIVE:
81
+ return self.error_response(
82
+ f'Cannot complete event with status: {event.status}',
83
+ status_code=400
84
+ )
85
+
86
+ event.complete(request.user)
87
+
88
+ return self.success_response(f'Maintenance event "{event.title}" completed')
89
+
90
+ except Exception as e:
91
+ return self.error_response(f"Complete maintenance event error: {e}")
92
+
93
+ @action(detail=True, methods=['post'])
94
+ @extend_schema(
95
+ summary="Cancel maintenance event",
96
+ description="Cancel maintenance event"
97
+ )
98
+ def cancel(self, request, id=None):
99
+ """Cancel a maintenance event."""
100
+ try:
101
+ event = self.get_object()
102
+
103
+ if event.status not in [MaintenanceEvent.Status.ACTIVE, MaintenanceEvent.Status.SCHEDULED]:
104
+ return self.error_response(
105
+ f'Cannot cancel event with status: {event.status}',
106
+ status_code=400
107
+ )
108
+
109
+ event.cancel(request.user)
110
+
111
+ return self.success_response(f'Maintenance event "{event.title}" cancelled')
112
+
113
+ except Exception as e:
114
+ return self.error_response(f"Cancel maintenance event error: {e}")
115
+
116
+ @action(detail=True, methods=['post'])
117
+ @extend_schema(
118
+ summary="Fail maintenance event",
119
+ description="Mark maintenance event as failed"
120
+ )
121
+ def fail(self, request, id=None):
122
+ """Mark a maintenance event as failed."""
123
+ try:
124
+ event = self.get_object()
125
+ error_message = request.data.get('error_message', 'Maintenance failed')
126
+
127
+ if event.status != MaintenanceEvent.Status.ACTIVE:
128
+ return self.error_response(
129
+ f'Cannot fail event with status: {event.status}',
130
+ status_code=400
131
+ )
132
+
133
+ event.fail(error_message)
134
+
135
+ return self.success_response(f'Maintenance event "{event.title}" marked as failed')
136
+
137
+ except Exception as e:
138
+ return self.error_response(f"Fail maintenance event error: {e}")
139
+
140
+ @action(detail=True, methods=['get'])
141
+ @extend_schema(
142
+ summary="Get event logs",
143
+ description="Get logs for this maintenance event"
144
+ )
145
+ def logs(self, request, id=None):
146
+ """Get logs for a maintenance event."""
147
+ try:
148
+ event = self.get_object()
149
+ logs = event.logs.order_by('-timestamp')
150
+
151
+ # Simple pagination
152
+ page_size = int(request.query_params.get('page_size', 50))
153
+ page = int(request.query_params.get('page', 1))
154
+ start = (page - 1) * page_size
155
+ end = start + page_size
156
+
157
+ paginated_logs = logs[start:end]
158
+
159
+ from ..serializers import MaintenanceLogSerializer
160
+ serializer = MaintenanceLogSerializer(paginated_logs, many=True)
161
+
162
+ return self.success_response(
163
+ f'Retrieved {len(serializer.data)} logs',
164
+ data={
165
+ 'logs': serializer.data,
166
+ 'total': logs.count(),
167
+ 'page': page,
168
+ 'page_size': page_size
169
+ }
170
+ )
171
+
172
+ except Exception as e:
173
+ return self.error_response(f"Get event logs error: {e}")
174
+
175
+ @action(detail=True, methods=['get'])
176
+ @extend_schema(
177
+ summary="Get event statistics",
178
+ description="Get statistics for this maintenance event"
179
+ )
180
+ def statistics(self, request, id=None):
181
+ """Get statistics for a maintenance event."""
182
+ try:
183
+ event = self.get_object()
184
+
185
+ stats = {
186
+ 'event_id': event.id,
187
+ 'title': event.title,
188
+ 'status': event.status,
189
+ 'duration': event.duration.total_seconds() if event.duration else None,
190
+ 'affected_sites': event.affected_sites_count,
191
+ 'success_count': event.success_count,
192
+ 'error_count_before': event.error_count_before,
193
+ 'error_count_during': event.error_count_during,
194
+ 'is_active': event.is_active,
195
+ 'is_scheduled': event.is_scheduled,
196
+ 'started_at': event.started_at.isoformat() if event.started_at else None,
197
+ 'ended_at': event.ended_at.isoformat() if event.ended_at else None,
198
+ 'logs_count': event.logs.count()
199
+ }
200
+
201
+ return self.success_response('Event statistics retrieved', data=stats)
202
+
203
+ except Exception as e:
204
+ return self.error_response(f"Get event statistics error: {e}")
@@ -0,0 +1,213 @@
1
+ """
2
+ Monitoring views.
3
+
4
+ ViewSets for MonitoringTarget management.
5
+ """
6
+
7
+ import logging
8
+ from rest_framework import viewsets
9
+ from rest_framework.decorators import action
10
+ from drf_spectacular.utils import extend_schema, extend_schema_view
11
+
12
+ from ..models import MonitoringTarget
13
+ from ..serializers import (
14
+ MonitoringTargetSerializer, MonitoringTargetCreateSerializer,
15
+ HealthCheckResultSerializer
16
+ )
17
+ from .base import MaintenancePermissionMixin, MaintenanceResponseMixin
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @extend_schema_view(
23
+ list=extend_schema(
24
+ summary="List monitoring targets",
25
+ description="Get list of monitoring targets"
26
+ ),
27
+ create=extend_schema(
28
+ summary="Create monitoring target",
29
+ description="Create a new monitoring target"
30
+ ),
31
+ retrieve=extend_schema(
32
+ summary="Get monitoring target",
33
+ description="Get detailed information about a monitoring target"
34
+ ),
35
+ update=extend_schema(
36
+ summary="Update monitoring target",
37
+ description="Update monitoring target configuration"
38
+ ),
39
+ destroy=extend_schema(
40
+ summary="Delete monitoring target",
41
+ description="Delete a monitoring target"
42
+ )
43
+ )
44
+ class MonitoringTargetViewSet(MaintenancePermissionMixin, MaintenanceResponseMixin, viewsets.ModelViewSet):
45
+ """ViewSet for managing monitoring targets."""
46
+
47
+ serializer_class = MonitoringTargetSerializer
48
+ lookup_field = 'id'
49
+ filterset_fields = ['is_active', 'check_type']
50
+ ordering = ['-created_at']
51
+
52
+ def get_queryset(self):
53
+ """Get queryset filtered by user permissions."""
54
+ # MonitoringTarget is related to CloudflareSite via site.owner
55
+ if getattr(self, 'swagger_fake_view', False):
56
+ return MonitoringTarget.objects.none()
57
+
58
+ user = self.request.user
59
+ if user.is_staff:
60
+ return MonitoringTarget.objects.all()
61
+
62
+ return MonitoringTarget.objects.filter(site__owner=user)
63
+
64
+ def get_serializer_class(self):
65
+ """Return appropriate serializer based on action."""
66
+ if self.action == 'create':
67
+ return MonitoringTargetCreateSerializer
68
+ return MonitoringTargetSerializer
69
+
70
+ @action(detail=True, methods=['post'])
71
+ @extend_schema(
72
+ summary="Run health check",
73
+ description="Run health check for this monitoring target",
74
+ responses={
75
+ 200: HealthCheckResultSerializer
76
+ }
77
+ )
78
+ def health_check(self, request, id=None):
79
+ """Run health check for a monitoring target."""
80
+ try:
81
+ target = self.get_object()
82
+
83
+ # Import monitoring service
84
+ from ..services.monitoring_service import MonitoringService
85
+
86
+ monitoring_service = MonitoringService()
87
+ result = monitoring_service.check_target_health(target)
88
+
89
+ # Serialize the result
90
+ serializer = HealthCheckResultSerializer(result)
91
+
92
+ return self.success_response(
93
+ f'Health check completed for {target.site.domain}',
94
+ data=serializer.data
95
+ )
96
+
97
+ except Exception as e:
98
+ return self.error_response(f"Health check error: {e}")
99
+
100
+ @action(detail=True, methods=['post'])
101
+ @extend_schema(
102
+ summary="Enable monitoring",
103
+ description="Enable monitoring for this target"
104
+ )
105
+ def enable(self, request, id=None):
106
+ """Enable monitoring for a target."""
107
+ try:
108
+ target = self.get_object()
109
+ target.is_active = True
110
+ target.save()
111
+
112
+ return self.success_response(f'Monitoring enabled for {target.site.domain}')
113
+
114
+ except Exception as e:
115
+ return self.error_response(f"Enable monitoring error: {e}")
116
+
117
+ @action(detail=True, methods=['post'])
118
+ @extend_schema(
119
+ summary="Disable monitoring",
120
+ description="Disable monitoring for this target"
121
+ )
122
+ def disable(self, request, id=None):
123
+ """Disable monitoring for a target."""
124
+ try:
125
+ target = self.get_object()
126
+ target.is_active = False
127
+ target.save()
128
+
129
+ return self.success_response(f'Monitoring disabled for {target.site.domain}')
130
+
131
+ except Exception as e:
132
+ return self.error_response(f"Disable monitoring error: {e}")
133
+
134
+ @action(detail=True, methods=['get'])
135
+ @extend_schema(
136
+ summary="Get monitoring statistics",
137
+ description="Get monitoring statistics for this target"
138
+ )
139
+ def statistics(self, request, id=None):
140
+ """Get monitoring statistics for a target."""
141
+ try:
142
+ target = self.get_object()
143
+
144
+ stats = {
145
+ 'target_id': target.id,
146
+ 'site_domain': target.site.domain,
147
+ 'is_active': target.is_active,
148
+ 'check_type': target.check_type,
149
+ 'check_interval': target.check_interval,
150
+ 'timeout': target.timeout,
151
+ 'retry_count': target.retry_count,
152
+ 'last_check': target.last_check.isoformat() if target.last_check else None,
153
+ 'last_status': target.last_status,
154
+ 'consecutive_failures': target.consecutive_failures,
155
+ 'total_checks': target.total_checks,
156
+ 'total_failures': target.total_failures,
157
+ 'uptime_percentage': target.uptime_percentage,
158
+ 'created_at': target.created_at.isoformat(),
159
+ 'updated_at': target.updated_at.isoformat()
160
+ }
161
+
162
+ return self.success_response('Monitoring statistics retrieved', data=stats)
163
+
164
+ except Exception as e:
165
+ return self.error_response(f"Get monitoring statistics error: {e}")
166
+
167
+ @action(detail=False, methods=['post'])
168
+ @extend_schema(
169
+ summary="Run bulk health checks",
170
+ description="Run health checks for multiple monitoring targets"
171
+ )
172
+ def bulk_health_check(self, request):
173
+ """Run health checks for multiple targets."""
174
+ try:
175
+ target_ids = request.data.get('target_ids', [])
176
+
177
+ if not target_ids:
178
+ return self.error_response('No target IDs provided', status_code=400)
179
+
180
+ # Get targets (filtered by user permissions)
181
+ targets = self.get_queryset().filter(id__in=target_ids)
182
+
183
+ # Import monitoring service
184
+ from ..services.monitoring_service import MonitoringService
185
+
186
+ monitoring_service = MonitoringService()
187
+ results = []
188
+
189
+ for target in targets:
190
+ try:
191
+ result = monitoring_service.check_target_health(target)
192
+ serializer = HealthCheckResultSerializer(result)
193
+ results.append({
194
+ 'target_id': target.id,
195
+ 'domain': target.site.domain,
196
+ 'success': True,
197
+ 'result': serializer.data
198
+ })
199
+ except Exception as e:
200
+ results.append({
201
+ 'target_id': target.id,
202
+ 'domain': target.site.domain,
203
+ 'success': False,
204
+ 'error': str(e)
205
+ })
206
+
207
+ return self.success_response(
208
+ f'Bulk health check completed for {len(results)} targets',
209
+ data={'results': results}
210
+ )
211
+
212
+ except Exception as e:
213
+ return self.error_response(f"Bulk health check error: {e}")