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,310 @@
|
|
1
|
+
"""
|
2
|
+
Action serializers.
|
3
|
+
|
4
|
+
Serializers for bulk operations and filtering.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework import serializers
|
8
|
+
from drf_spectacular.utils import extend_schema_serializer, OpenApiExample
|
9
|
+
|
10
|
+
from ..models import CloudflareSite
|
11
|
+
|
12
|
+
|
13
|
+
@extend_schema_serializer(
|
14
|
+
examples=[
|
15
|
+
OpenApiExample(
|
16
|
+
'Enable Maintenance',
|
17
|
+
summary='Enable maintenance for multiple sites',
|
18
|
+
description='Example of enabling maintenance mode for selected sites',
|
19
|
+
value={
|
20
|
+
'action': 'enable',
|
21
|
+
'site_ids': [1, 2, 3],
|
22
|
+
'reason': 'Database migration',
|
23
|
+
'maintenance_message': 'We are performing scheduled maintenance. Please try again in 2 hours.',
|
24
|
+
'estimated_duration': '02:00:00',
|
25
|
+
'dry_run': False
|
26
|
+
}
|
27
|
+
),
|
28
|
+
OpenApiExample(
|
29
|
+
'Disable Maintenance',
|
30
|
+
summary='Disable maintenance for multiple sites',
|
31
|
+
description='Example of disabling maintenance mode',
|
32
|
+
value={
|
33
|
+
'action': 'disable',
|
34
|
+
'site_ids': [1, 2, 3],
|
35
|
+
'dry_run': False
|
36
|
+
}
|
37
|
+
),
|
38
|
+
OpenApiExample(
|
39
|
+
'Status Check',
|
40
|
+
summary='Check status of multiple sites',
|
41
|
+
description='Example of checking status for multiple sites',
|
42
|
+
value={
|
43
|
+
'action': 'status_check',
|
44
|
+
'site_ids': [1, 2, 3, 4, 5]
|
45
|
+
}
|
46
|
+
),
|
47
|
+
OpenApiExample(
|
48
|
+
'Dry Run',
|
49
|
+
summary='Dry run operation',
|
50
|
+
description='Example of a dry run to see what would be affected',
|
51
|
+
value={
|
52
|
+
'action': 'enable',
|
53
|
+
'site_ids': [1, 2, 3],
|
54
|
+
'reason': 'Test maintenance',
|
55
|
+
'dry_run': True
|
56
|
+
}
|
57
|
+
)
|
58
|
+
]
|
59
|
+
)
|
60
|
+
class BulkMaintenanceActionSerializer(serializers.Serializer):
|
61
|
+
"""Serializer for bulk maintenance actions."""
|
62
|
+
|
63
|
+
action = serializers.ChoiceField(
|
64
|
+
choices=['enable', 'disable', 'status_check'],
|
65
|
+
help_text="Action to perform on selected sites"
|
66
|
+
)
|
67
|
+
site_ids = serializers.ListField(
|
68
|
+
child=serializers.IntegerField(min_value=1),
|
69
|
+
min_length=1,
|
70
|
+
max_length=100,
|
71
|
+
help_text="List of site IDs to perform action on (max 100)"
|
72
|
+
)
|
73
|
+
reason = serializers.CharField(
|
74
|
+
max_length=500,
|
75
|
+
required=False,
|
76
|
+
help_text="Reason for maintenance (required for enable action)"
|
77
|
+
)
|
78
|
+
maintenance_message = serializers.CharField(
|
79
|
+
max_length=1000,
|
80
|
+
required=False,
|
81
|
+
help_text="Custom maintenance message to display"
|
82
|
+
)
|
83
|
+
estimated_duration = serializers.DurationField(
|
84
|
+
required=False,
|
85
|
+
help_text="Estimated maintenance duration (HH:MM:SS format)"
|
86
|
+
)
|
87
|
+
dry_run = serializers.BooleanField(
|
88
|
+
default=False,
|
89
|
+
help_text="Perform a dry run without making actual changes"
|
90
|
+
)
|
91
|
+
|
92
|
+
def validate(self, data):
|
93
|
+
"""Validate bulk action data."""
|
94
|
+
action = data['action']
|
95
|
+
|
96
|
+
# Reason is required for enable action
|
97
|
+
if action == 'enable' and not data.get('reason'):
|
98
|
+
raise serializers.ValidationError({
|
99
|
+
'reason': 'Reason is required when enabling maintenance'
|
100
|
+
})
|
101
|
+
|
102
|
+
# Validate estimated duration
|
103
|
+
if data.get('estimated_duration'):
|
104
|
+
duration = data['estimated_duration']
|
105
|
+
if duration.total_seconds() > 24 * 3600: # 24 hours
|
106
|
+
raise serializers.ValidationError({
|
107
|
+
'estimated_duration': 'Duration cannot exceed 24 hours'
|
108
|
+
})
|
109
|
+
if duration.total_seconds() < 60: # 1 minute
|
110
|
+
raise serializers.ValidationError({
|
111
|
+
'estimated_duration': 'Duration must be at least 1 minute'
|
112
|
+
})
|
113
|
+
|
114
|
+
return data
|
115
|
+
|
116
|
+
def validate_site_ids(self, value):
|
117
|
+
"""Validate site IDs exist and are accessible."""
|
118
|
+
# Remove duplicates while preserving order
|
119
|
+
unique_ids = list(dict.fromkeys(value))
|
120
|
+
|
121
|
+
if len(unique_ids) != len(value):
|
122
|
+
raise serializers.ValidationError(
|
123
|
+
"Duplicate site IDs are not allowed"
|
124
|
+
)
|
125
|
+
|
126
|
+
# Check if sites exist (will be further filtered by permissions in view)
|
127
|
+
existing_sites = CloudflareSite.objects.filter(id__in=unique_ids)
|
128
|
+
existing_ids = set(existing_sites.values_list('id', flat=True))
|
129
|
+
missing_ids = set(unique_ids) - existing_ids
|
130
|
+
|
131
|
+
if missing_ids:
|
132
|
+
raise serializers.ValidationError(
|
133
|
+
f"Sites not found: {sorted(missing_ids)}"
|
134
|
+
)
|
135
|
+
|
136
|
+
return unique_ids
|
137
|
+
|
138
|
+
|
139
|
+
class SiteFilterSerializer(serializers.Serializer):
|
140
|
+
"""Serializer for site filtering parameters."""
|
141
|
+
|
142
|
+
environment = serializers.ChoiceField(
|
143
|
+
choices=CloudflareSite.SiteEnvironment.choices,
|
144
|
+
required=False,
|
145
|
+
help_text="Filter by environment"
|
146
|
+
)
|
147
|
+
project = serializers.CharField(
|
148
|
+
max_length=100,
|
149
|
+
required=False,
|
150
|
+
help_text="Filter by project name"
|
151
|
+
)
|
152
|
+
tags = serializers.ListField(
|
153
|
+
child=serializers.CharField(max_length=50),
|
154
|
+
required=False,
|
155
|
+
help_text="Filter by tags (sites must have ALL specified tags)"
|
156
|
+
)
|
157
|
+
status = serializers.ChoiceField(
|
158
|
+
choices=CloudflareSite.SiteStatus.choices,
|
159
|
+
required=False,
|
160
|
+
help_text="Filter by current status"
|
161
|
+
)
|
162
|
+
maintenance_active = serializers.BooleanField(
|
163
|
+
required=False,
|
164
|
+
help_text="Filter by maintenance status"
|
165
|
+
)
|
166
|
+
monitoring_enabled = serializers.BooleanField(
|
167
|
+
required=False,
|
168
|
+
help_text="Filter by monitoring status"
|
169
|
+
)
|
170
|
+
owner_id = serializers.IntegerField(
|
171
|
+
required=False,
|
172
|
+
help_text="Filter by owner ID (staff only)"
|
173
|
+
)
|
174
|
+
|
175
|
+
def validate_tags(self, value):
|
176
|
+
"""Validate tags format."""
|
177
|
+
if value:
|
178
|
+
# Remove duplicates and empty strings
|
179
|
+
cleaned_tags = [tag.strip() for tag in value if tag.strip()]
|
180
|
+
if len(cleaned_tags) != len(value):
|
181
|
+
raise serializers.ValidationError(
|
182
|
+
"Tags cannot be empty or contain only whitespace"
|
183
|
+
)
|
184
|
+
return cleaned_tags
|
185
|
+
return value
|
186
|
+
|
187
|
+
|
188
|
+
class BulkOperationResultSerializer(serializers.Serializer):
|
189
|
+
"""Serializer for bulk operation results."""
|
190
|
+
|
191
|
+
total = serializers.IntegerField(help_text="Total number of sites processed")
|
192
|
+
successful = serializers.ListField(
|
193
|
+
child=serializers.CharField(),
|
194
|
+
help_text="List of successfully processed site domains"
|
195
|
+
)
|
196
|
+
failed = serializers.ListField(
|
197
|
+
child=serializers.DictField(),
|
198
|
+
help_text="List of failed operations with details"
|
199
|
+
)
|
200
|
+
skipped = serializers.ListField(
|
201
|
+
child=serializers.DictField(),
|
202
|
+
required=False,
|
203
|
+
help_text="List of skipped operations with reasons"
|
204
|
+
)
|
205
|
+
dry_run = serializers.BooleanField(
|
206
|
+
default=False,
|
207
|
+
help_text="Whether this was a dry run"
|
208
|
+
)
|
209
|
+
would_affect = serializers.ListField(
|
210
|
+
child=serializers.CharField(),
|
211
|
+
required=False,
|
212
|
+
help_text="Sites that would be affected (dry run only)"
|
213
|
+
)
|
214
|
+
execution_time = serializers.FloatField(
|
215
|
+
required=False,
|
216
|
+
help_text="Execution time in seconds"
|
217
|
+
)
|
218
|
+
|
219
|
+
|
220
|
+
class SiteGroupActionSerializer(serializers.Serializer):
|
221
|
+
"""Serializer for site group actions."""
|
222
|
+
|
223
|
+
action = serializers.ChoiceField(
|
224
|
+
choices=['add_sites', 'remove_sites', 'enable_maintenance', 'disable_maintenance'],
|
225
|
+
help_text="Action to perform on the group"
|
226
|
+
)
|
227
|
+
site_ids = serializers.ListField(
|
228
|
+
child=serializers.IntegerField(min_value=1),
|
229
|
+
required=False,
|
230
|
+
help_text="Site IDs for add/remove actions"
|
231
|
+
)
|
232
|
+
reason = serializers.CharField(
|
233
|
+
max_length=500,
|
234
|
+
required=False,
|
235
|
+
help_text="Reason for maintenance actions"
|
236
|
+
)
|
237
|
+
maintenance_message = serializers.CharField(
|
238
|
+
max_length=1000,
|
239
|
+
required=False,
|
240
|
+
help_text="Custom maintenance message"
|
241
|
+
)
|
242
|
+
|
243
|
+
def validate(self, data):
|
244
|
+
"""Validate group action data."""
|
245
|
+
action = data['action']
|
246
|
+
|
247
|
+
# Site IDs required for add/remove actions
|
248
|
+
if action in ['add_sites', 'remove_sites'] and not data.get('site_ids'):
|
249
|
+
raise serializers.ValidationError({
|
250
|
+
'site_ids': f'Site IDs are required for {action} action'
|
251
|
+
})
|
252
|
+
|
253
|
+
# Reason required for enable maintenance
|
254
|
+
if action == 'enable_maintenance' and not data.get('reason'):
|
255
|
+
raise serializers.ValidationError({
|
256
|
+
'reason': 'Reason is required for enable maintenance action'
|
257
|
+
})
|
258
|
+
|
259
|
+
return data
|
260
|
+
|
261
|
+
|
262
|
+
class MaintenanceScheduleSerializer(serializers.Serializer):
|
263
|
+
"""Serializer for scheduling maintenance."""
|
264
|
+
|
265
|
+
scheduled_at = serializers.DateTimeField(
|
266
|
+
help_text="When to start the maintenance"
|
267
|
+
)
|
268
|
+
site_ids = serializers.ListField(
|
269
|
+
child=serializers.IntegerField(min_value=1),
|
270
|
+
help_text="Sites to include in scheduled maintenance"
|
271
|
+
)
|
272
|
+
title = serializers.CharField(
|
273
|
+
max_length=200,
|
274
|
+
help_text="Maintenance title"
|
275
|
+
)
|
276
|
+
description = serializers.CharField(
|
277
|
+
max_length=1000,
|
278
|
+
required=False,
|
279
|
+
help_text="Detailed description"
|
280
|
+
)
|
281
|
+
estimated_duration = serializers.DurationField(
|
282
|
+
help_text="Estimated maintenance duration"
|
283
|
+
)
|
284
|
+
maintenance_message = serializers.CharField(
|
285
|
+
max_length=1000,
|
286
|
+
required=False,
|
287
|
+
help_text="Message to display during maintenance"
|
288
|
+
)
|
289
|
+
notify_users = serializers.BooleanField(
|
290
|
+
default=True,
|
291
|
+
help_text="Whether to notify users about scheduled maintenance"
|
292
|
+
)
|
293
|
+
|
294
|
+
def validate_scheduled_at(self, value):
|
295
|
+
"""Validate scheduled time is in the future."""
|
296
|
+
from django.utils import timezone
|
297
|
+
|
298
|
+
if value <= timezone.now():
|
299
|
+
raise serializers.ValidationError(
|
300
|
+
"Scheduled time must be in the future"
|
301
|
+
)
|
302
|
+
|
303
|
+
# Don't allow scheduling too far in the future (1 year)
|
304
|
+
max_future = timezone.now() + timezone.timedelta(days=365)
|
305
|
+
if value > max_future:
|
306
|
+
raise serializers.ValidationError(
|
307
|
+
"Cannot schedule maintenance more than 1 year in advance"
|
308
|
+
)
|
309
|
+
|
310
|
+
return value
|
@@ -0,0 +1,44 @@
|
|
1
|
+
"""
|
2
|
+
Base serializers for maintenance app.
|
3
|
+
|
4
|
+
Common serializers used across the maintenance application.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework import serializers
|
8
|
+
from django.contrib.auth import get_user_model
|
9
|
+
|
10
|
+
User = get_user_model()
|
11
|
+
|
12
|
+
|
13
|
+
class UserSerializer(serializers.ModelSerializer):
|
14
|
+
"""Serializer for User model."""
|
15
|
+
|
16
|
+
class Meta:
|
17
|
+
model = User
|
18
|
+
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'is_staff']
|
19
|
+
read_only_fields = fields
|
20
|
+
|
21
|
+
|
22
|
+
class APIResponseSerializer(serializers.Serializer):
|
23
|
+
"""Generic API response serializer for OpenAPI documentation."""
|
24
|
+
|
25
|
+
success = serializers.BooleanField(
|
26
|
+
help_text="Whether the operation was successful"
|
27
|
+
)
|
28
|
+
message = serializers.CharField(
|
29
|
+
required=False,
|
30
|
+
help_text="Response message"
|
31
|
+
)
|
32
|
+
data = serializers.JSONField(
|
33
|
+
required=False,
|
34
|
+
help_text="Response data"
|
35
|
+
)
|
36
|
+
error = serializers.CharField(
|
37
|
+
required=False,
|
38
|
+
help_text="Error message if failed"
|
39
|
+
)
|
40
|
+
errors = serializers.JSONField(
|
41
|
+
required=False,
|
42
|
+
help_text="Detailed error information"
|
43
|
+
)
|
44
|
+
|
@@ -0,0 +1,209 @@
|
|
1
|
+
"""
|
2
|
+
Deployment serializers.
|
3
|
+
|
4
|
+
Serializers for CloudflareDeployment model.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework import serializers
|
8
|
+
from drf_spectacular.utils import extend_schema_serializer, OpenApiExample
|
9
|
+
|
10
|
+
from ..models import CloudflareDeployment
|
11
|
+
from .sites import CloudflareSiteListSerializer
|
12
|
+
from .events import MaintenanceEventListSerializer
|
13
|
+
|
14
|
+
|
15
|
+
@extend_schema_serializer(
|
16
|
+
examples=[
|
17
|
+
OpenApiExample(
|
18
|
+
'Worker Deployment',
|
19
|
+
summary='A Cloudflare Worker deployment',
|
20
|
+
description='Example of a successful Worker deployment',
|
21
|
+
value={
|
22
|
+
'id': 1,
|
23
|
+
'deployment_type': 'worker',
|
24
|
+
'resource_name': 'maintenance-mode',
|
25
|
+
'resource_id': 'worker123',
|
26
|
+
'status': 'deployed',
|
27
|
+
'script_content': 'addEventListener("fetch", event => { ... })',
|
28
|
+
'is_active': True,
|
29
|
+
'can_rollback': True
|
30
|
+
}
|
31
|
+
),
|
32
|
+
OpenApiExample(
|
33
|
+
'Failed Deployment',
|
34
|
+
summary='A failed deployment',
|
35
|
+
description='Example of a deployment that failed',
|
36
|
+
value={
|
37
|
+
'id': 2,
|
38
|
+
'deployment_type': 'worker',
|
39
|
+
'resource_name': 'maintenance-mode',
|
40
|
+
'status': 'failed',
|
41
|
+
'error_message': 'Script validation failed: Invalid syntax',
|
42
|
+
'is_active': False,
|
43
|
+
'can_rollback': False
|
44
|
+
}
|
45
|
+
)
|
46
|
+
]
|
47
|
+
)
|
48
|
+
class CloudflareDeploymentSerializer(serializers.ModelSerializer):
|
49
|
+
"""Serializer for CloudflareDeployment model with full details."""
|
50
|
+
|
51
|
+
site = CloudflareSiteListSerializer(read_only=True)
|
52
|
+
maintenance_event = MaintenanceEventListSerializer(read_only=True)
|
53
|
+
is_active = serializers.ReadOnlyField()
|
54
|
+
can_rollback = serializers.ReadOnlyField()
|
55
|
+
deployment_duration = serializers.SerializerMethodField()
|
56
|
+
|
57
|
+
class Meta:
|
58
|
+
model = CloudflareDeployment
|
59
|
+
fields = [
|
60
|
+
'id', 'site', 'deployment_type', 'resource_name', 'resource_id',
|
61
|
+
'status', 'maintenance_event', 'script_content', 'configuration_data',
|
62
|
+
'rollback_data', 'error_message', 'is_active', 'can_rollback',
|
63
|
+
'deployment_duration', 'created_at', 'deployed_at', 'failed_at', 'rolled_back_at'
|
64
|
+
]
|
65
|
+
read_only_fields = [
|
66
|
+
'id', 'site', 'maintenance_event', 'resource_id', 'status',
|
67
|
+
'error_message', 'is_active', 'can_rollback', 'deployment_duration',
|
68
|
+
'created_at', 'deployed_at', 'failed_at', 'rolled_back_at'
|
69
|
+
]
|
70
|
+
extra_kwargs = {
|
71
|
+
'deployment_type': {
|
72
|
+
'help_text': 'Type of Cloudflare resource (worker, page_rule, dns_record, etc.)'
|
73
|
+
},
|
74
|
+
'resource_name': {
|
75
|
+
'help_text': 'Name of the deployed resource'
|
76
|
+
},
|
77
|
+
'resource_id': {
|
78
|
+
'help_text': 'Cloudflare resource ID (set after deployment)'
|
79
|
+
},
|
80
|
+
'script_content': {
|
81
|
+
'help_text': 'Worker script content (for worker deployments)'
|
82
|
+
},
|
83
|
+
'configuration_data': {
|
84
|
+
'help_text': 'Configuration data as JSON'
|
85
|
+
},
|
86
|
+
'rollback_data': {
|
87
|
+
'help_text': 'Data needed for rollback operations'
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
def get_deployment_duration(self, obj):
|
92
|
+
"""Calculate deployment duration if available."""
|
93
|
+
if obj.deployed_at and obj.created_at:
|
94
|
+
duration = obj.deployed_at - obj.created_at
|
95
|
+
return duration.total_seconds()
|
96
|
+
return None
|
97
|
+
|
98
|
+
|
99
|
+
class CloudflareDeploymentListSerializer(serializers.ModelSerializer):
|
100
|
+
"""Lightweight serializer for deployment lists."""
|
101
|
+
|
102
|
+
site = serializers.StringRelatedField(read_only=True)
|
103
|
+
maintenance_event = serializers.StringRelatedField(read_only=True)
|
104
|
+
is_active = serializers.ReadOnlyField()
|
105
|
+
can_rollback = serializers.ReadOnlyField()
|
106
|
+
|
107
|
+
class Meta:
|
108
|
+
model = CloudflareDeployment
|
109
|
+
fields = [
|
110
|
+
'id', 'site', 'deployment_type', 'resource_name', 'status',
|
111
|
+
'maintenance_event', 'is_active', 'can_rollback',
|
112
|
+
'created_at', 'deployed_at'
|
113
|
+
]
|
114
|
+
read_only_fields = fields
|
115
|
+
|
116
|
+
|
117
|
+
class CloudflareDeploymentCreateSerializer(serializers.ModelSerializer):
|
118
|
+
"""Serializer for creating CloudflareDeployment."""
|
119
|
+
|
120
|
+
class Meta:
|
121
|
+
model = CloudflareDeployment
|
122
|
+
fields = [
|
123
|
+
'deployment_type', 'resource_name', 'script_content',
|
124
|
+
'configuration_data', 'rollback_data'
|
125
|
+
]
|
126
|
+
extra_kwargs = {
|
127
|
+
'script_content': {
|
128
|
+
'help_text': 'JavaScript code for Worker deployments'
|
129
|
+
},
|
130
|
+
'configuration_data': {
|
131
|
+
'help_text': 'Configuration as JSON object'
|
132
|
+
},
|
133
|
+
'rollback_data': {
|
134
|
+
'help_text': 'Rollback configuration as JSON object'
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
def validate_script_content(self, value):
|
139
|
+
"""Validate Worker script content."""
|
140
|
+
if self.initial_data.get('deployment_type') == 'worker' and not value:
|
141
|
+
raise serializers.ValidationError(
|
142
|
+
"Script content is required for Worker deployments"
|
143
|
+
)
|
144
|
+
|
145
|
+
if value and len(value) > 1024 * 1024: # 1MB limit
|
146
|
+
raise serializers.ValidationError(
|
147
|
+
"Script content cannot exceed 1MB"
|
148
|
+
)
|
149
|
+
|
150
|
+
return value
|
151
|
+
|
152
|
+
def validate_resource_name(self, value):
|
153
|
+
"""Validate resource name format."""
|
154
|
+
import re
|
155
|
+
|
156
|
+
if not re.match(r'^[a-zA-Z0-9\-_]+$', value):
|
157
|
+
raise serializers.ValidationError(
|
158
|
+
"Resource name can only contain letters, numbers, hyphens, and underscores"
|
159
|
+
)
|
160
|
+
|
161
|
+
if len(value) > 100:
|
162
|
+
raise serializers.ValidationError(
|
163
|
+
"Resource name cannot exceed 100 characters"
|
164
|
+
)
|
165
|
+
|
166
|
+
return value
|
167
|
+
|
168
|
+
|
169
|
+
class DeploymentStatusSerializer(serializers.Serializer):
|
170
|
+
"""Serializer for deployment status responses."""
|
171
|
+
|
172
|
+
deployment_id = serializers.IntegerField(help_text="Deployment ID")
|
173
|
+
status = serializers.ChoiceField(
|
174
|
+
choices=CloudflareDeployment.Status.choices,
|
175
|
+
help_text="Current deployment status"
|
176
|
+
)
|
177
|
+
is_active = serializers.BooleanField(help_text="Whether deployment is active")
|
178
|
+
can_rollback = serializers.BooleanField(help_text="Whether deployment can be rolled back")
|
179
|
+
resource_id = serializers.CharField(
|
180
|
+
required=False,
|
181
|
+
help_text="Cloudflare resource ID (if deployed)"
|
182
|
+
)
|
183
|
+
error_message = serializers.CharField(
|
184
|
+
required=False,
|
185
|
+
help_text="Error message (if failed)"
|
186
|
+
)
|
187
|
+
deployed_at = serializers.DateTimeField(
|
188
|
+
required=False,
|
189
|
+
help_text="Deployment timestamp"
|
190
|
+
)
|
191
|
+
|
192
|
+
|
193
|
+
class RollbackRequestSerializer(serializers.Serializer):
|
194
|
+
"""Serializer for rollback requests."""
|
195
|
+
|
196
|
+
reason = serializers.CharField(
|
197
|
+
max_length=500,
|
198
|
+
required=False,
|
199
|
+
help_text="Reason for rollback"
|
200
|
+
)
|
201
|
+
force = serializers.BooleanField(
|
202
|
+
default=False,
|
203
|
+
help_text="Force rollback even if risky"
|
204
|
+
)
|
205
|
+
|
206
|
+
def validate(self, data):
|
207
|
+
"""Validate rollback request."""
|
208
|
+
# Additional validation can be added here
|
209
|
+
return data
|