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.
Files changed (62) 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 +42 -3
  51. django_cfg/core/generation.py +16 -5
  52. django_cfg/models/cloudflare.py +316 -0
  53. django_cfg/models/revolution.py +1 -1
  54. django_cfg/models/tasks.py +55 -1
  55. django_cfg/modules/base.py +12 -5
  56. django_cfg/modules/django_tasks.py +41 -3
  57. django_cfg/modules/django_unfold/dashboard.py +16 -1
  58. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/METADATA +2 -1
  59. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/RECORD +62 -14
  60. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/WHEEL +0 -0
  61. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/entry_points.txt +0 -0
  62. {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)