django-cfg 1.2.14__py3-none-any.whl → 1.2.16__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/maintenance/README.md +305 -0
- django_cfg/apps/maintenance/__init__.py +27 -0
- django_cfg/apps/maintenance/admin/__init__.py +28 -0
- django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
- django_cfg/apps/maintenance/admin/events_admin.py +374 -0
- django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
- django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
- django_cfg/apps/maintenance/apps.py +105 -0
- django_cfg/apps/maintenance/management/__init__.py +0 -0
- django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
- django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
- django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
- django_cfg/apps/maintenance/managers/__init__.py +20 -0
- django_cfg/apps/maintenance/managers/deployments.py +287 -0
- django_cfg/apps/maintenance/managers/events.py +374 -0
- django_cfg/apps/maintenance/managers/monitoring.py +301 -0
- django_cfg/apps/maintenance/managers/sites.py +335 -0
- django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
- django_cfg/apps/maintenance/migrations/__init__.py +0 -0
- django_cfg/apps/maintenance/models/__init__.py +27 -0
- django_cfg/apps/maintenance/models/cloudflare.py +316 -0
- django_cfg/apps/maintenance/models/maintenance.py +334 -0
- django_cfg/apps/maintenance/models/monitoring.py +393 -0
- django_cfg/apps/maintenance/models/sites.py +419 -0
- django_cfg/apps/maintenance/serializers/__init__.py +60 -0
- django_cfg/apps/maintenance/serializers/actions.py +310 -0
- django_cfg/apps/maintenance/serializers/base.py +44 -0
- django_cfg/apps/maintenance/serializers/deployments.py +209 -0
- django_cfg/apps/maintenance/serializers/events.py +210 -0
- django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
- django_cfg/apps/maintenance/serializers/sites.py +213 -0
- django_cfg/apps/maintenance/services/README.md +168 -0
- django_cfg/apps/maintenance/services/__init__.py +21 -0
- django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
- django_cfg/apps/maintenance/services/dns_manager.py +497 -0
- django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
- django_cfg/apps/maintenance/services/site_sync.py +448 -0
- django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
- django_cfg/apps/maintenance/services/worker_manager.py +264 -0
- django_cfg/apps/maintenance/signals.py +38 -0
- django_cfg/apps/maintenance/urls.py +36 -0
- django_cfg/apps/maintenance/views/__init__.py +18 -0
- django_cfg/apps/maintenance/views/base.py +61 -0
- django_cfg/apps/maintenance/views/deployments.py +175 -0
- django_cfg/apps/maintenance/views/events.py +204 -0
- django_cfg/apps/maintenance/views/monitoring.py +213 -0
- django_cfg/apps/maintenance/views/sites.py +338 -0
- django_cfg/apps/urls.py +5 -1
- django_cfg/core/config.py +42 -3
- django_cfg/core/generation.py +16 -5
- django_cfg/models/cloudflare.py +316 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +55 -1
- django_cfg/modules/base.py +12 -5
- django_cfg/modules/django_tasks.py +41 -3
- django_cfg/modules/django_unfold/dashboard.py +16 -1
- {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/METADATA +2 -1
- {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/RECORD +62 -14
- {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
"""
|
2
|
+
Maintenance event serializers.
|
3
|
+
|
4
|
+
Serializers for MaintenanceEvent and MaintenanceLog models.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework import serializers
|
8
|
+
from drf_spectacular.utils import extend_schema_serializer, OpenApiExample
|
9
|
+
|
10
|
+
from ..models import MaintenanceEvent, MaintenanceLog, CloudflareSite
|
11
|
+
from .base import UserSerializer
|
12
|
+
from .sites import CloudflareSiteListSerializer
|
13
|
+
|
14
|
+
|
15
|
+
class MaintenanceLogSerializer(serializers.ModelSerializer):
|
16
|
+
"""Serializer for MaintenanceLog model."""
|
17
|
+
|
18
|
+
user = UserSerializer(read_only=True)
|
19
|
+
|
20
|
+
class Meta:
|
21
|
+
model = MaintenanceLog
|
22
|
+
fields = [
|
23
|
+
'id', 'event', 'level', 'message', 'component', 'operation',
|
24
|
+
'user', 'metadata', 'timestamp'
|
25
|
+
]
|
26
|
+
read_only_fields = fields
|
27
|
+
extra_kwargs = {
|
28
|
+
'level': {
|
29
|
+
'help_text': 'Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)'
|
30
|
+
},
|
31
|
+
'message': {
|
32
|
+
'help_text': 'Log message content'
|
33
|
+
},
|
34
|
+
'component': {
|
35
|
+
'help_text': 'Component that generated the log (e.g., cloudflare, monitoring)'
|
36
|
+
},
|
37
|
+
'operation': {
|
38
|
+
'help_text': 'Operation being performed (e.g., deploy_worker, health_check)'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
@extend_schema_serializer(
|
44
|
+
examples=[
|
45
|
+
OpenApiExample(
|
46
|
+
'Scheduled Maintenance',
|
47
|
+
summary='A scheduled maintenance event',
|
48
|
+
description='Example of a scheduled maintenance event for multiple sites',
|
49
|
+
value={
|
50
|
+
'title': 'Database Migration',
|
51
|
+
'description': 'Upgrading database to PostgreSQL 14',
|
52
|
+
'reason': 'scheduled',
|
53
|
+
'estimated_duration': '02:00:00',
|
54
|
+
'maintenance_message': 'We are performing scheduled maintenance. Please try again in 2 hours.',
|
55
|
+
'sites': [1, 2, 3]
|
56
|
+
}
|
57
|
+
),
|
58
|
+
OpenApiExample(
|
59
|
+
'Emergency Maintenance',
|
60
|
+
summary='An emergency maintenance event',
|
61
|
+
description='Example of an emergency maintenance event',
|
62
|
+
value={
|
63
|
+
'title': 'Security Patch',
|
64
|
+
'description': 'Applying critical security updates',
|
65
|
+
'reason': 'emergency',
|
66
|
+
'estimated_duration': '00:30:00',
|
67
|
+
'maintenance_message': 'Emergency maintenance in progress. Service will be restored shortly.',
|
68
|
+
'sites': [1]
|
69
|
+
}
|
70
|
+
)
|
71
|
+
]
|
72
|
+
)
|
73
|
+
class MaintenanceEventSerializer(serializers.ModelSerializer):
|
74
|
+
"""Serializer for MaintenanceEvent model with full details."""
|
75
|
+
|
76
|
+
initiated_by = UserSerializer(read_only=True)
|
77
|
+
completed_by = UserSerializer(read_only=True)
|
78
|
+
sites = CloudflareSiteListSerializer(many=True, read_only=True)
|
79
|
+
logs = MaintenanceLogSerializer(many=True, read_only=True)
|
80
|
+
duration = serializers.ReadOnlyField()
|
81
|
+
is_active = serializers.ReadOnlyField()
|
82
|
+
is_scheduled = serializers.ReadOnlyField()
|
83
|
+
affected_sites_count = serializers.ReadOnlyField()
|
84
|
+
|
85
|
+
class Meta:
|
86
|
+
model = MaintenanceEvent
|
87
|
+
fields = [
|
88
|
+
'id', 'title', 'description', 'reason', 'status', 'initiated_by',
|
89
|
+
'completed_by', 'sites', 'logs', 'started_at', 'ended_at',
|
90
|
+
'estimated_duration', 'maintenance_message', 'duration',
|
91
|
+
'is_active', 'is_scheduled', 'affected_sites_count',
|
92
|
+
'success_count', 'error_count_before', 'error_count_during',
|
93
|
+
'created_at', 'updated_at'
|
94
|
+
]
|
95
|
+
read_only_fields = [
|
96
|
+
'id', 'initiated_by', 'completed_by', 'sites', 'logs', 'duration',
|
97
|
+
'is_active', 'is_scheduled', 'affected_sites_count', 'success_count',
|
98
|
+
'error_count_before', 'error_count_during', 'created_at', 'updated_at'
|
99
|
+
]
|
100
|
+
extra_kwargs = {
|
101
|
+
'title': {
|
102
|
+
'help_text': 'Brief title describing the maintenance'
|
103
|
+
},
|
104
|
+
'description': {
|
105
|
+
'help_text': 'Detailed description of the maintenance work'
|
106
|
+
},
|
107
|
+
'reason': {
|
108
|
+
'help_text': 'Reason for maintenance (manual, scheduled, automatic, emergency)'
|
109
|
+
},
|
110
|
+
'maintenance_message': {
|
111
|
+
'help_text': 'Message to display to users during maintenance'
|
112
|
+
},
|
113
|
+
'estimated_duration': {
|
114
|
+
'help_text': 'Estimated duration in HH:MM:SS format'
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
|
119
|
+
class MaintenanceEventCreateSerializer(serializers.ModelSerializer):
|
120
|
+
"""Serializer for creating MaintenanceEvent."""
|
121
|
+
|
122
|
+
sites = serializers.PrimaryKeyRelatedField(
|
123
|
+
queryset=CloudflareSite.objects.all(),
|
124
|
+
many=True,
|
125
|
+
help_text="List of site IDs to include in maintenance"
|
126
|
+
)
|
127
|
+
|
128
|
+
class Meta:
|
129
|
+
model = MaintenanceEvent
|
130
|
+
fields = [
|
131
|
+
'title', 'description', 'reason', 'sites', 'started_at',
|
132
|
+
'estimated_duration', 'maintenance_message'
|
133
|
+
]
|
134
|
+
extra_kwargs = {
|
135
|
+
'title': {
|
136
|
+
'help_text': 'Brief title describing the maintenance'
|
137
|
+
},
|
138
|
+
'description': {
|
139
|
+
'help_text': 'Detailed description of the maintenance work'
|
140
|
+
},
|
141
|
+
'maintenance_message': {
|
142
|
+
'help_text': 'Custom message to display during maintenance (optional)'
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
def validate_sites(self, value):
|
147
|
+
"""Validate that user has access to selected sites."""
|
148
|
+
user = self.context['request'].user
|
149
|
+
if not user.is_staff:
|
150
|
+
# Non-staff users can only select their own sites
|
151
|
+
user_sites = CloudflareSite.objects.filter(owner=user)
|
152
|
+
invalid_sites = [site for site in value if site not in user_sites]
|
153
|
+
if invalid_sites:
|
154
|
+
raise serializers.ValidationError(
|
155
|
+
f"You don't have access to sites: {[s.domain for s in invalid_sites]}"
|
156
|
+
)
|
157
|
+
return value
|
158
|
+
|
159
|
+
def validate_estimated_duration(self, value):
|
160
|
+
"""Validate estimated duration is reasonable."""
|
161
|
+
if value and value.total_seconds() > 24 * 3600: # 24 hours
|
162
|
+
raise serializers.ValidationError(
|
163
|
+
"Estimated duration cannot exceed 24 hours"
|
164
|
+
)
|
165
|
+
if value and value.total_seconds() < 60: # 1 minute
|
166
|
+
raise serializers.ValidationError(
|
167
|
+
"Estimated duration must be at least 1 minute"
|
168
|
+
)
|
169
|
+
return value
|
170
|
+
|
171
|
+
|
172
|
+
class MaintenanceEventListSerializer(serializers.ModelSerializer):
|
173
|
+
"""Lightweight serializer for event lists."""
|
174
|
+
|
175
|
+
initiated_by = serializers.StringRelatedField(read_only=True)
|
176
|
+
affected_sites_count = serializers.ReadOnlyField()
|
177
|
+
duration = serializers.ReadOnlyField()
|
178
|
+
is_active = serializers.ReadOnlyField()
|
179
|
+
|
180
|
+
class Meta:
|
181
|
+
model = MaintenanceEvent
|
182
|
+
fields = [
|
183
|
+
'id', 'title', 'reason', 'status', 'initiated_by',
|
184
|
+
'affected_sites_count', 'duration', 'is_active',
|
185
|
+
'started_at', 'ended_at', 'created_at'
|
186
|
+
]
|
187
|
+
read_only_fields = fields
|
188
|
+
|
189
|
+
|
190
|
+
class MaintenanceEventUpdateSerializer(serializers.ModelSerializer):
|
191
|
+
"""Serializer for updating MaintenanceEvent."""
|
192
|
+
|
193
|
+
class Meta:
|
194
|
+
model = MaintenanceEvent
|
195
|
+
fields = [
|
196
|
+
'title', 'description', 'maintenance_message', 'estimated_duration'
|
197
|
+
]
|
198
|
+
|
199
|
+
def validate(self, data):
|
200
|
+
"""Validate that event can be updated."""
|
201
|
+
instance = self.instance
|
202
|
+
if instance and instance.status in [
|
203
|
+
MaintenanceEvent.Status.COMPLETED,
|
204
|
+
MaintenanceEvent.Status.CANCELLED,
|
205
|
+
MaintenanceEvent.Status.FAILED
|
206
|
+
]:
|
207
|
+
raise serializers.ValidationError(
|
208
|
+
"Cannot update completed, cancelled, or failed maintenance events"
|
209
|
+
)
|
210
|
+
return data
|
@@ -0,0 +1,278 @@
|
|
1
|
+
"""
|
2
|
+
Monitoring serializers.
|
3
|
+
|
4
|
+
Serializers for MonitoringTarget and HealthCheckResult models.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework import serializers
|
8
|
+
from drf_spectacular.utils import extend_schema_serializer, OpenApiExample
|
9
|
+
|
10
|
+
from ..models import MonitoringTarget, HealthCheckResult
|
11
|
+
from .sites import CloudflareSiteListSerializer
|
12
|
+
|
13
|
+
|
14
|
+
class HealthCheckResultSerializer(serializers.ModelSerializer):
|
15
|
+
"""Serializer for HealthCheckResult model."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
model = HealthCheckResult
|
19
|
+
fields = [
|
20
|
+
'id', 'target', 'success', 'status_code', 'response_time_ms',
|
21
|
+
'error_message', 'response_headers', 'timestamp'
|
22
|
+
]
|
23
|
+
read_only_fields = fields
|
24
|
+
extra_kwargs = {
|
25
|
+
'success': {
|
26
|
+
'help_text': 'Whether the health check was successful'
|
27
|
+
},
|
28
|
+
'status_code': {
|
29
|
+
'help_text': 'HTTP status code returned'
|
30
|
+
},
|
31
|
+
'response_time_ms': {
|
32
|
+
'help_text': 'Response time in milliseconds'
|
33
|
+
},
|
34
|
+
'error_message': {
|
35
|
+
'help_text': 'Error message if check failed'
|
36
|
+
},
|
37
|
+
'response_headers': {
|
38
|
+
'help_text': 'Response headers as JSON'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
class HealthCheckResultListSerializer(serializers.ModelSerializer):
|
44
|
+
"""Lightweight serializer for health check result lists."""
|
45
|
+
|
46
|
+
class Meta:
|
47
|
+
model = HealthCheckResult
|
48
|
+
fields = [
|
49
|
+
'id', 'success', 'status_code', 'response_time_ms', 'timestamp'
|
50
|
+
]
|
51
|
+
read_only_fields = fields
|
52
|
+
|
53
|
+
|
54
|
+
@extend_schema_serializer(
|
55
|
+
examples=[
|
56
|
+
OpenApiExample(
|
57
|
+
'Basic Monitoring Target',
|
58
|
+
summary='A basic monitoring configuration',
|
59
|
+
description='Example of a basic monitoring target for a production site',
|
60
|
+
value={
|
61
|
+
'check_url': 'https://example.com/health/',
|
62
|
+
'expected_status_codes': [200, 201],
|
63
|
+
'timeout_seconds': 10,
|
64
|
+
'check_interval_seconds': 60,
|
65
|
+
'failure_threshold': 3,
|
66
|
+
'recovery_threshold': 2
|
67
|
+
}
|
68
|
+
),
|
69
|
+
OpenApiExample(
|
70
|
+
'Advanced Monitoring Target',
|
71
|
+
summary='An advanced monitoring configuration',
|
72
|
+
description='Example with custom headers and SSL verification',
|
73
|
+
value={
|
74
|
+
'check_url': 'https://api.example.com/status',
|
75
|
+
'expected_status_codes': [200],
|
76
|
+
'timeout_seconds': 15,
|
77
|
+
'check_interval_seconds': 30,
|
78
|
+
'failure_threshold': 5,
|
79
|
+
'recovery_threshold': 3,
|
80
|
+
'custom_headers': {
|
81
|
+
'Authorization': 'Bearer token123',
|
82
|
+
'User-Agent': 'Maintenance-Monitor/1.0'
|
83
|
+
},
|
84
|
+
'follow_redirects': True,
|
85
|
+
'verify_ssl': True,
|
86
|
+
'expected_response_time_ms': 2000
|
87
|
+
}
|
88
|
+
)
|
89
|
+
]
|
90
|
+
)
|
91
|
+
class MonitoringTargetSerializer(serializers.ModelSerializer):
|
92
|
+
"""Serializer for MonitoringTarget model."""
|
93
|
+
|
94
|
+
site = CloudflareSiteListSerializer(read_only=True)
|
95
|
+
results = HealthCheckResultListSerializer(many=True, read_only=True)
|
96
|
+
recent_results = serializers.SerializerMethodField()
|
97
|
+
uptime_percentage = serializers.SerializerMethodField()
|
98
|
+
average_response_time = serializers.SerializerMethodField()
|
99
|
+
|
100
|
+
class Meta:
|
101
|
+
model = MonitoringTarget
|
102
|
+
fields = [
|
103
|
+
'id', 'site', 'status', 'check_url', 'expected_status_codes',
|
104
|
+
'timeout_seconds', 'check_interval_seconds', 'failure_threshold',
|
105
|
+
'recovery_threshold', 'custom_headers', 'follow_redirects',
|
106
|
+
'verify_ssl', 'expected_response_time_ms', 'results',
|
107
|
+
'recent_results', 'uptime_percentage', 'average_response_time',
|
108
|
+
'last_check_at', 'consecutive_failures', 'consecutive_successes',
|
109
|
+
'created_at', 'updated_at'
|
110
|
+
]
|
111
|
+
read_only_fields = [
|
112
|
+
'id', 'site', 'results', 'recent_results', 'uptime_percentage',
|
113
|
+
'average_response_time', 'last_check_at', 'consecutive_failures',
|
114
|
+
'consecutive_successes', 'created_at', 'updated_at'
|
115
|
+
]
|
116
|
+
extra_kwargs = {
|
117
|
+
'check_url': {
|
118
|
+
'help_text': 'URL to monitor for health checks'
|
119
|
+
},
|
120
|
+
'expected_status_codes': {
|
121
|
+
'help_text': 'List of HTTP status codes considered successful'
|
122
|
+
},
|
123
|
+
'timeout_seconds': {
|
124
|
+
'help_text': 'Request timeout in seconds'
|
125
|
+
},
|
126
|
+
'check_interval_seconds': {
|
127
|
+
'help_text': 'Interval between checks in seconds'
|
128
|
+
},
|
129
|
+
'failure_threshold': {
|
130
|
+
'help_text': 'Number of consecutive failures before marking as down'
|
131
|
+
},
|
132
|
+
'recovery_threshold': {
|
133
|
+
'help_text': 'Number of consecutive successes before marking as up'
|
134
|
+
},
|
135
|
+
'custom_headers': {
|
136
|
+
'help_text': 'Custom HTTP headers to send with requests'
|
137
|
+
},
|
138
|
+
'expected_response_time_ms': {
|
139
|
+
'help_text': 'Expected response time threshold in milliseconds'
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
def get_recent_results(self, obj):
|
144
|
+
"""Get recent health check results (last 10)."""
|
145
|
+
recent = obj.results.order_by('-timestamp')[:10]
|
146
|
+
return HealthCheckResultListSerializer(recent, many=True).data
|
147
|
+
|
148
|
+
def get_uptime_percentage(self, obj):
|
149
|
+
"""Calculate uptime percentage over last 24 hours."""
|
150
|
+
from django.utils import timezone
|
151
|
+
from datetime import timedelta
|
152
|
+
|
153
|
+
# Get results from last 24 hours
|
154
|
+
since = timezone.now() - timedelta(hours=24)
|
155
|
+
recent_results = obj.results.filter(timestamp__gte=since)
|
156
|
+
|
157
|
+
if not recent_results.exists():
|
158
|
+
return None
|
159
|
+
|
160
|
+
total_checks = recent_results.count()
|
161
|
+
successful_checks = recent_results.filter(success=True).count()
|
162
|
+
|
163
|
+
return round((successful_checks / total_checks) * 100, 2)
|
164
|
+
|
165
|
+
def get_average_response_time(self, obj):
|
166
|
+
"""Calculate average response time over last 24 hours."""
|
167
|
+
from django.utils import timezone
|
168
|
+
from datetime import timedelta
|
169
|
+
from django.db.models import Avg
|
170
|
+
|
171
|
+
# Get results from last 24 hours
|
172
|
+
since = timezone.now() - timedelta(hours=24)
|
173
|
+
recent_results = obj.results.filter(
|
174
|
+
timestamp__gte=since,
|
175
|
+
success=True,
|
176
|
+
response_time_ms__isnull=False
|
177
|
+
)
|
178
|
+
|
179
|
+
if not recent_results.exists():
|
180
|
+
return None
|
181
|
+
|
182
|
+
avg_time = recent_results.aggregate(
|
183
|
+
avg=Avg('response_time_ms')
|
184
|
+
)['avg']
|
185
|
+
|
186
|
+
return round(avg_time, 2) if avg_time else None
|
187
|
+
|
188
|
+
|
189
|
+
class MonitoringTargetCreateSerializer(serializers.ModelSerializer):
|
190
|
+
"""Serializer for creating MonitoringTarget."""
|
191
|
+
|
192
|
+
class Meta:
|
193
|
+
model = MonitoringTarget
|
194
|
+
fields = [
|
195
|
+
'check_url', 'expected_status_codes', 'timeout_seconds',
|
196
|
+
'check_interval_seconds', 'failure_threshold', 'recovery_threshold',
|
197
|
+
'custom_headers', 'follow_redirects', 'verify_ssl',
|
198
|
+
'expected_response_time_ms'
|
199
|
+
]
|
200
|
+
|
201
|
+
def validate_check_url(self, value):
|
202
|
+
"""Validate check URL format."""
|
203
|
+
if not value.startswith(('http://', 'https://')):
|
204
|
+
raise serializers.ValidationError(
|
205
|
+
"Check URL must start with http:// or https://"
|
206
|
+
)
|
207
|
+
return value
|
208
|
+
|
209
|
+
def validate_expected_status_codes(self, value):
|
210
|
+
"""Validate status codes."""
|
211
|
+
if not value:
|
212
|
+
raise serializers.ValidationError(
|
213
|
+
"At least one expected status code is required"
|
214
|
+
)
|
215
|
+
|
216
|
+
for code in value:
|
217
|
+
if not (100 <= code <= 599):
|
218
|
+
raise serializers.ValidationError(
|
219
|
+
f"Invalid HTTP status code: {code}"
|
220
|
+
)
|
221
|
+
|
222
|
+
return value
|
223
|
+
|
224
|
+
def validate_timeout_seconds(self, value):
|
225
|
+
"""Validate timeout is reasonable."""
|
226
|
+
if value < 1:
|
227
|
+
raise serializers.ValidationError(
|
228
|
+
"Timeout must be at least 1 second"
|
229
|
+
)
|
230
|
+
if value > 300:
|
231
|
+
raise serializers.ValidationError(
|
232
|
+
"Timeout cannot exceed 300 seconds"
|
233
|
+
)
|
234
|
+
return value
|
235
|
+
|
236
|
+
def validate_check_interval_seconds(self, value):
|
237
|
+
"""Validate check interval is reasonable."""
|
238
|
+
if value < 10:
|
239
|
+
raise serializers.ValidationError(
|
240
|
+
"Check interval must be at least 10 seconds"
|
241
|
+
)
|
242
|
+
if value > 3600:
|
243
|
+
raise serializers.ValidationError(
|
244
|
+
"Check interval cannot exceed 3600 seconds (1 hour)"
|
245
|
+
)
|
246
|
+
return value
|
247
|
+
|
248
|
+
|
249
|
+
class MonitoringTargetListSerializer(serializers.ModelSerializer):
|
250
|
+
"""Lightweight serializer for monitoring target lists."""
|
251
|
+
|
252
|
+
site = serializers.StringRelatedField(read_only=True)
|
253
|
+
uptime_percentage = serializers.SerializerMethodField()
|
254
|
+
|
255
|
+
class Meta:
|
256
|
+
model = MonitoringTarget
|
257
|
+
fields = [
|
258
|
+
'id', 'site', 'status', 'check_url', 'uptime_percentage',
|
259
|
+
'last_check_at', 'consecutive_failures', 'consecutive_successes'
|
260
|
+
]
|
261
|
+
read_only_fields = fields
|
262
|
+
|
263
|
+
def get_uptime_percentage(self, obj):
|
264
|
+
"""Calculate uptime percentage over last 24 hours."""
|
265
|
+
from django.utils import timezone
|
266
|
+
from datetime import timedelta
|
267
|
+
|
268
|
+
# Get results from last 24 hours
|
269
|
+
since = timezone.now() - timedelta(hours=24)
|
270
|
+
recent_results = obj.results.filter(timestamp__gte=since)
|
271
|
+
|
272
|
+
if not recent_results.exists():
|
273
|
+
return None
|
274
|
+
|
275
|
+
total_checks = recent_results.count()
|
276
|
+
successful_checks = recent_results.filter(success=True).count()
|
277
|
+
|
278
|
+
return round((successful_checks / total_checks) * 100, 2)
|