django-cfg 1.2.16__py3-none-any.whl → 1.2.18__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/accounts/models/__init__.py +68 -0
- django_cfg/apps/accounts/models/activity.py +34 -0
- django_cfg/apps/accounts/models/auth.py +50 -0
- django_cfg/apps/accounts/models/base.py +8 -0
- django_cfg/apps/accounts/models/choices.py +32 -0
- django_cfg/apps/accounts/models/integrations.py +75 -0
- django_cfg/apps/accounts/models/registration.py +52 -0
- django_cfg/apps/accounts/models/user.py +80 -0
- django_cfg/apps/maintenance/__init__.py +53 -24
- django_cfg/apps/maintenance/admin/__init__.py +7 -18
- django_cfg/apps/maintenance/admin/api_key_admin.py +185 -0
- django_cfg/apps/maintenance/admin/log_admin.py +156 -0
- django_cfg/apps/maintenance/admin/scheduled_admin.py +390 -0
- django_cfg/apps/maintenance/admin/site_admin.py +448 -0
- django_cfg/apps/maintenance/apps.py +9 -96
- django_cfg/apps/maintenance/management/commands/maintenance.py +193 -307
- django_cfg/apps/maintenance/management/commands/process_scheduled_maintenance.py +241 -0
- django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +152 -111
- django_cfg/apps/maintenance/managers/__init__.py +7 -12
- django_cfg/apps/maintenance/managers/cloudflare_site_manager.py +192 -0
- django_cfg/apps/maintenance/managers/maintenance_log_manager.py +151 -0
- django_cfg/apps/maintenance/migrations/0001_initial.py +145 -705
- django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py +21 -0
- django_cfg/apps/maintenance/models/__init__.py +23 -21
- django_cfg/apps/maintenance/models/cloudflare_api_key.py +109 -0
- django_cfg/apps/maintenance/models/cloudflare_site.py +125 -0
- django_cfg/apps/maintenance/models/maintenance_log.py +131 -0
- django_cfg/apps/maintenance/models/scheduled_maintenance.py +307 -0
- django_cfg/apps/maintenance/services/__init__.py +37 -16
- django_cfg/apps/maintenance/services/bulk_operations_service.py +400 -0
- django_cfg/apps/maintenance/services/maintenance_service.py +230 -0
- django_cfg/apps/maintenance/services/scheduled_maintenance_service.py +381 -0
- django_cfg/apps/maintenance/services/site_sync_service.py +390 -0
- django_cfg/apps/maintenance/utils/__init__.py +12 -0
- django_cfg/apps/maintenance/utils/retry_utils.py +109 -0
- django_cfg/config.py +3 -0
- django_cfg/core/config.py +4 -6
- django_cfg/modules/django_unfold/dashboard.py +4 -5
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/METADATA +52 -1
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/RECORD +45 -55
- django_cfg/apps/maintenance/README.md +0 -305
- django_cfg/apps/maintenance/admin/deployments_admin.py +0 -251
- django_cfg/apps/maintenance/admin/events_admin.py +0 -374
- django_cfg/apps/maintenance/admin/monitoring_admin.py +0 -215
- django_cfg/apps/maintenance/admin/sites_admin.py +0 -464
- django_cfg/apps/maintenance/managers/deployments.py +0 -287
- django_cfg/apps/maintenance/managers/events.py +0 -374
- django_cfg/apps/maintenance/managers/monitoring.py +0 -301
- django_cfg/apps/maintenance/managers/sites.py +0 -335
- django_cfg/apps/maintenance/models/cloudflare.py +0 -316
- django_cfg/apps/maintenance/models/maintenance.py +0 -334
- django_cfg/apps/maintenance/models/monitoring.py +0 -393
- django_cfg/apps/maintenance/models/sites.py +0 -419
- django_cfg/apps/maintenance/serializers/__init__.py +0 -60
- django_cfg/apps/maintenance/serializers/actions.py +0 -310
- django_cfg/apps/maintenance/serializers/base.py +0 -44
- django_cfg/apps/maintenance/serializers/deployments.py +0 -209
- django_cfg/apps/maintenance/serializers/events.py +0 -210
- django_cfg/apps/maintenance/serializers/monitoring.py +0 -278
- django_cfg/apps/maintenance/serializers/sites.py +0 -213
- django_cfg/apps/maintenance/services/README.md +0 -168
- django_cfg/apps/maintenance/services/cloudflare_client.py +0 -441
- django_cfg/apps/maintenance/services/dns_manager.py +0 -497
- django_cfg/apps/maintenance/services/maintenance_manager.py +0 -504
- django_cfg/apps/maintenance/services/site_sync.py +0 -448
- django_cfg/apps/maintenance/services/sync_command_service.py +0 -330
- django_cfg/apps/maintenance/services/worker_manager.py +0 -264
- django_cfg/apps/maintenance/signals.py +0 -38
- django_cfg/apps/maintenance/urls.py +0 -36
- django_cfg/apps/maintenance/views/__init__.py +0 -18
- django_cfg/apps/maintenance/views/base.py +0 -61
- django_cfg/apps/maintenance/views/deployments.py +0 -175
- django_cfg/apps/maintenance/views/events.py +0 -204
- django_cfg/apps/maintenance/views/monitoring.py +0 -213
- django_cfg/apps/maintenance/views/sites.py +0 -338
- django_cfg/models/cloudflare.py +0 -316
- /django_cfg/apps/accounts/{models.py → __models.py} +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/licenses/LICENSE +0 -0
@@ -1,316 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Cloudflare integration models.
|
3
|
-
|
4
|
-
Models for tracking Cloudflare deployments and configurations.
|
5
|
-
Following CRITICAL_REQUIREMENTS - proper typing, no raw Dict usage.
|
6
|
-
"""
|
7
|
-
|
8
|
-
from django.db import models
|
9
|
-
from django.utils import timezone
|
10
|
-
from django.core.exceptions import ValidationError
|
11
|
-
from typing import Optional, Dict, Any
|
12
|
-
from datetime import timedelta
|
13
|
-
import json
|
14
|
-
|
15
|
-
|
16
|
-
class CloudflareDeployment(models.Model):
|
17
|
-
"""
|
18
|
-
Cloudflare Worker deployment tracking.
|
19
|
-
|
20
|
-
Tracks deployment of maintenance mode Workers to Cloudflare
|
21
|
-
with full audit trail and rollback capabilities.
|
22
|
-
"""
|
23
|
-
|
24
|
-
class Status(models.TextChoices):
|
25
|
-
"""Deployment status choices."""
|
26
|
-
PENDING = "pending", "Pending"
|
27
|
-
DEPLOYING = "deploying", "Deploying"
|
28
|
-
DEPLOYED = "deployed", "Deployed"
|
29
|
-
FAILED = "failed", "Failed"
|
30
|
-
ROLLED_BACK = "rolled_back", "Rolled Back"
|
31
|
-
|
32
|
-
class DeploymentType(models.TextChoices):
|
33
|
-
"""Type of deployment."""
|
34
|
-
WORKER = "worker", "Cloudflare Worker"
|
35
|
-
PAGE_RULE = "page_rule", "Page Rule"
|
36
|
-
DNS_RECORD = "dns_record", "DNS Record"
|
37
|
-
SSL_SETTING = "ssl_setting", "SSL Setting"
|
38
|
-
CUSTOM_ERROR_PAGE = "custom_error_page", "Custom Error Page"
|
39
|
-
|
40
|
-
# === Basic Information ===
|
41
|
-
site = models.ForeignKey(
|
42
|
-
'django_cfg_maintenance.CloudflareSite',
|
43
|
-
on_delete=models.CASCADE,
|
44
|
-
related_name='deployments',
|
45
|
-
help_text="Site this deployment belongs to"
|
46
|
-
)
|
47
|
-
deployment_type = models.CharField(
|
48
|
-
max_length=50,
|
49
|
-
choices=DeploymentType.choices,
|
50
|
-
default=DeploymentType.WORKER,
|
51
|
-
help_text="Type of Cloudflare resource being deployed"
|
52
|
-
)
|
53
|
-
|
54
|
-
# === Deployment Details ===
|
55
|
-
resource_name = models.CharField(
|
56
|
-
max_length=200,
|
57
|
-
help_text="Name of the deployed resource (Worker name, Page Rule, etc.)"
|
58
|
-
)
|
59
|
-
resource_id = models.CharField(
|
60
|
-
max_length=100,
|
61
|
-
blank=True,
|
62
|
-
help_text="Cloudflare resource ID after deployment"
|
63
|
-
)
|
64
|
-
|
65
|
-
# === Status Tracking ===
|
66
|
-
status = models.CharField(
|
67
|
-
max_length=20,
|
68
|
-
choices=Status.choices,
|
69
|
-
default=Status.PENDING,
|
70
|
-
help_text="Current deployment status"
|
71
|
-
)
|
72
|
-
|
73
|
-
# === Timing ===
|
74
|
-
created_at = models.DateTimeField(
|
75
|
-
auto_now_add=True,
|
76
|
-
help_text="When deployment was initiated"
|
77
|
-
)
|
78
|
-
deployed_at = models.DateTimeField(
|
79
|
-
null=True,
|
80
|
-
blank=True,
|
81
|
-
help_text="When deployment completed successfully"
|
82
|
-
)
|
83
|
-
failed_at = models.DateTimeField(
|
84
|
-
null=True,
|
85
|
-
blank=True,
|
86
|
-
help_text="When deployment failed"
|
87
|
-
)
|
88
|
-
|
89
|
-
# === Configuration ===
|
90
|
-
configuration = models.JSONField(
|
91
|
-
default=dict,
|
92
|
-
help_text="Deployment configuration (Worker script, Page Rule settings, etc.)"
|
93
|
-
)
|
94
|
-
|
95
|
-
# === Results ===
|
96
|
-
cloudflare_response = models.JSONField(
|
97
|
-
default=dict,
|
98
|
-
blank=True,
|
99
|
-
help_text="Full response from Cloudflare API"
|
100
|
-
)
|
101
|
-
error_message = models.TextField(
|
102
|
-
blank=True,
|
103
|
-
help_text="Error message if deployment failed"
|
104
|
-
)
|
105
|
-
|
106
|
-
# === Rollback Support ===
|
107
|
-
previous_deployment = models.ForeignKey(
|
108
|
-
'self',
|
109
|
-
on_delete=models.SET_NULL,
|
110
|
-
null=True,
|
111
|
-
blank=True,
|
112
|
-
related_name='rollback_deployments',
|
113
|
-
help_text="Previous deployment for rollback"
|
114
|
-
)
|
115
|
-
rollback_data = models.JSONField(
|
116
|
-
default=dict,
|
117
|
-
blank=True,
|
118
|
-
help_text="Data needed for rollback (previous configuration, etc.)"
|
119
|
-
)
|
120
|
-
|
121
|
-
# === Maintenance Event Link ===
|
122
|
-
maintenance_event = models.ForeignKey(
|
123
|
-
'django_cfg_maintenance.MaintenanceEvent',
|
124
|
-
on_delete=models.SET_NULL,
|
125
|
-
null=True,
|
126
|
-
blank=True,
|
127
|
-
related_name='cloudflare_deployments',
|
128
|
-
help_text="Related maintenance event"
|
129
|
-
)
|
130
|
-
|
131
|
-
# === Custom Manager ===
|
132
|
-
from ..managers.deployments import CloudflareDeploymentManager
|
133
|
-
objects = CloudflareDeploymentManager()
|
134
|
-
|
135
|
-
class Meta:
|
136
|
-
ordering = ['-created_at']
|
137
|
-
verbose_name = "Cloudflare Deployment"
|
138
|
-
verbose_name_plural = "Cloudflare Deployments"
|
139
|
-
indexes = [
|
140
|
-
models.Index(fields=['site', '-created_at']),
|
141
|
-
models.Index(fields=['status', '-created_at']),
|
142
|
-
models.Index(fields=['deployment_type', '-created_at']),
|
143
|
-
models.Index(fields=['resource_id']),
|
144
|
-
]
|
145
|
-
|
146
|
-
def __str__(self) -> str:
|
147
|
-
return f"{self.get_deployment_type_display()}: {self.resource_name} ({self.get_status_display()})"
|
148
|
-
|
149
|
-
@property
|
150
|
-
def is_active(self) -> bool:
|
151
|
-
"""Check if deployment is currently active."""
|
152
|
-
return self.status == self.Status.DEPLOYED
|
153
|
-
|
154
|
-
@property
|
155
|
-
def is_pending(self) -> bool:
|
156
|
-
"""Check if deployment is pending."""
|
157
|
-
return self.status in [self.Status.PENDING, self.Status.DEPLOYING]
|
158
|
-
|
159
|
-
@property
|
160
|
-
def deployment_duration(self) -> Optional[timedelta]:
|
161
|
-
"""Calculate deployment duration."""
|
162
|
-
if self.deployed_at:
|
163
|
-
return self.deployed_at - self.created_at
|
164
|
-
elif self.failed_at:
|
165
|
-
return self.failed_at - self.created_at
|
166
|
-
elif self.status == self.Status.DEPLOYING:
|
167
|
-
return timezone.now() - self.created_at
|
168
|
-
return None
|
169
|
-
|
170
|
-
@property
|
171
|
-
def can_rollback(self) -> bool:
|
172
|
-
"""Check if deployment can be rolled back."""
|
173
|
-
return (
|
174
|
-
self.status == self.Status.DEPLOYED and
|
175
|
-
(self.previous_deployment is not None or self.rollback_data)
|
176
|
-
)
|
177
|
-
|
178
|
-
def mark_deploying(self) -> None:
|
179
|
-
"""Mark deployment as in progress."""
|
180
|
-
self.status = self.Status.DEPLOYING
|
181
|
-
self.save(update_fields=['status'])
|
182
|
-
|
183
|
-
def mark_deployed(self, resource_id: str, cloudflare_response: Dict[str, Any]) -> None:
|
184
|
-
"""Mark deployment as successful."""
|
185
|
-
self.status = self.Status.DEPLOYED
|
186
|
-
self.resource_id = resource_id
|
187
|
-
self.deployed_at = timezone.now()
|
188
|
-
self.cloudflare_response = cloudflare_response
|
189
|
-
self.save(update_fields=[
|
190
|
-
'status',
|
191
|
-
'resource_id',
|
192
|
-
'deployed_at',
|
193
|
-
'cloudflare_response'
|
194
|
-
])
|
195
|
-
|
196
|
-
def mark_failed(self, error_message: str, cloudflare_response: Optional[Dict[str, Any]] = None) -> None:
|
197
|
-
"""Mark deployment as failed."""
|
198
|
-
self.status = self.Status.FAILED
|
199
|
-
self.failed_at = timezone.now()
|
200
|
-
self.error_message = error_message
|
201
|
-
if cloudflare_response:
|
202
|
-
self.cloudflare_response = cloudflare_response
|
203
|
-
|
204
|
-
self.save(update_fields=[
|
205
|
-
'status',
|
206
|
-
'failed_at',
|
207
|
-
'error_message',
|
208
|
-
'cloudflare_response'
|
209
|
-
])
|
210
|
-
|
211
|
-
def rollback(self) -> bool:
|
212
|
-
"""Attempt to rollback this deployment."""
|
213
|
-
if not self.can_rollback:
|
214
|
-
return False
|
215
|
-
|
216
|
-
try:
|
217
|
-
# Create rollback deployment record
|
218
|
-
rollback_deployment = CloudflareDeployment.objects.create(
|
219
|
-
site=self.site,
|
220
|
-
deployment_type=self.deployment_type,
|
221
|
-
resource_name=f"rollback-{self.resource_name}",
|
222
|
-
configuration=self.rollback_data,
|
223
|
-
previous_deployment=self,
|
224
|
-
maintenance_event=self.maintenance_event
|
225
|
-
)
|
226
|
-
|
227
|
-
# Mark this deployment as rolled back
|
228
|
-
self.status = self.Status.ROLLED_BACK
|
229
|
-
self.save(update_fields=['status'])
|
230
|
-
|
231
|
-
return True
|
232
|
-
|
233
|
-
except Exception:
|
234
|
-
return False
|
235
|
-
|
236
|
-
def get_configuration_summary(self) -> str:
|
237
|
-
"""Get human-readable configuration summary."""
|
238
|
-
if not self.configuration:
|
239
|
-
return "No configuration"
|
240
|
-
|
241
|
-
if self.deployment_type == self.DeploymentType.WORKER:
|
242
|
-
return f"Worker script ({len(str(self.configuration.get('script', '')))} chars)"
|
243
|
-
elif self.deployment_type == self.DeploymentType.PAGE_RULE:
|
244
|
-
actions = self.configuration.get('actions', [])
|
245
|
-
return f"Page Rule with {len(actions)} actions"
|
246
|
-
elif self.deployment_type == self.DeploymentType.DNS_RECORD:
|
247
|
-
record_type = self.configuration.get('type', 'Unknown')
|
248
|
-
content = self.configuration.get('content', 'Unknown')
|
249
|
-
return f"{record_type} record: {content}"
|
250
|
-
else:
|
251
|
-
return f"{len(self.configuration)} configuration items"
|
252
|
-
|
253
|
-
def get_error_summary(self) -> str:
|
254
|
-
"""Get concise error summary."""
|
255
|
-
if not self.error_message:
|
256
|
-
return "No error"
|
257
|
-
|
258
|
-
# Extract first line of error message
|
259
|
-
first_line = self.error_message.split('\n')[0]
|
260
|
-
return first_line[:200] if len(first_line) > 200 else first_line
|
261
|
-
|
262
|
-
def clean(self) -> None:
|
263
|
-
"""Validate model data."""
|
264
|
-
super().clean()
|
265
|
-
|
266
|
-
# Validate resource name
|
267
|
-
if not self.resource_name.strip():
|
268
|
-
raise ValidationError({'resource_name': 'Resource name cannot be empty'})
|
269
|
-
|
270
|
-
# Validate timing
|
271
|
-
if self.deployed_at and self.failed_at:
|
272
|
-
raise ValidationError("Deployment cannot be both successful and failed")
|
273
|
-
|
274
|
-
if self.deployed_at and self.deployed_at < self.created_at:
|
275
|
-
raise ValidationError({'deployed_at': 'Deployed time cannot be before created time'})
|
276
|
-
|
277
|
-
if self.failed_at and self.failed_at < self.created_at:
|
278
|
-
raise ValidationError({'failed_at': 'Failed time cannot be before created time'})
|
279
|
-
|
280
|
-
@classmethod
|
281
|
-
def create_worker_deployment(cls,
|
282
|
-
site: 'CloudflareSite',
|
283
|
-
worker_name: str,
|
284
|
-
script_content: str,
|
285
|
-
maintenance_event: Optional['MaintenanceEvent'] = None) -> 'CloudflareDeployment':
|
286
|
-
"""Create Worker deployment record."""
|
287
|
-
return cls.objects.create(
|
288
|
-
site=site,
|
289
|
-
deployment_type=cls.DeploymentType.WORKER,
|
290
|
-
resource_name=worker_name,
|
291
|
-
configuration={
|
292
|
-
'script': script_content,
|
293
|
-
'name': worker_name
|
294
|
-
},
|
295
|
-
maintenance_event=maintenance_event
|
296
|
-
)
|
297
|
-
|
298
|
-
@classmethod
|
299
|
-
def create_page_rule_deployment(cls,
|
300
|
-
site: 'CloudflareSite',
|
301
|
-
rule_name: str,
|
302
|
-
pattern: str,
|
303
|
-
actions: Dict[str, Any],
|
304
|
-
maintenance_event: Optional['MaintenanceEvent'] = None) -> 'CloudflareDeployment':
|
305
|
-
"""Create Page Rule deployment record."""
|
306
|
-
return cls.objects.create(
|
307
|
-
site=site,
|
308
|
-
deployment_type=cls.DeploymentType.PAGE_RULE,
|
309
|
-
resource_name=rule_name,
|
310
|
-
configuration={
|
311
|
-
'targets': [{'target': 'url', 'constraint': {'operator': 'matches', 'value': pattern}}],
|
312
|
-
'actions': actions,
|
313
|
-
'status': 'active'
|
314
|
-
},
|
315
|
-
maintenance_event=maintenance_event
|
316
|
-
)
|
@@ -1,334 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Maintenance event models.
|
3
|
-
|
4
|
-
Core models for tracking maintenance events and logs.
|
5
|
-
Following CRITICAL_REQUIREMENTS - no raw Dict usage, proper typing.
|
6
|
-
"""
|
7
|
-
|
8
|
-
from django.db import models
|
9
|
-
from django.contrib.auth import get_user_model
|
10
|
-
from django.utils import timezone
|
11
|
-
from django.core.exceptions import ValidationError
|
12
|
-
from typing import Optional
|
13
|
-
from datetime import timedelta
|
14
|
-
|
15
|
-
User = get_user_model()
|
16
|
-
|
17
|
-
|
18
|
-
class MaintenanceEvent(models.Model):
|
19
|
-
"""
|
20
|
-
Main maintenance event tracking model.
|
21
|
-
|
22
|
-
Tracks maintenance events with full audit trail and Cloudflare integration.
|
23
|
-
"""
|
24
|
-
|
25
|
-
class Status(models.TextChoices):
|
26
|
-
"""Maintenance event status choices."""
|
27
|
-
SCHEDULED = "scheduled", "Scheduled"
|
28
|
-
ACTIVE = "active", "Active"
|
29
|
-
COMPLETED = "completed", "Completed"
|
30
|
-
FAILED = "failed", "Failed"
|
31
|
-
CANCELLED = "cancelled", "Cancelled"
|
32
|
-
|
33
|
-
class Reason(models.TextChoices):
|
34
|
-
"""Maintenance reason choices."""
|
35
|
-
MANUAL = "manual", "Manual Activation"
|
36
|
-
SERVER_DOWN = "server_down", "Server Unreachable"
|
37
|
-
HIGH_ERROR_RATE = "high_error_rate", "High Error Rate"
|
38
|
-
DATABASE_ISSUES = "database_issues", "Database Issues"
|
39
|
-
DEPLOYMENT = "deployment", "Deployment"
|
40
|
-
SECURITY_UPDATE = "security_update", "Security Update"
|
41
|
-
SCHEDULED = "scheduled", "Scheduled Maintenance"
|
42
|
-
EMERGENCY = "emergency", "Emergency Maintenance"
|
43
|
-
|
44
|
-
# === Basic Information ===
|
45
|
-
title = models.CharField(
|
46
|
-
max_length=200,
|
47
|
-
help_text="Human-readable maintenance event title"
|
48
|
-
)
|
49
|
-
description = models.TextField(
|
50
|
-
blank=True,
|
51
|
-
help_text="Detailed description of maintenance work"
|
52
|
-
)
|
53
|
-
reason = models.CharField(
|
54
|
-
max_length=50,
|
55
|
-
choices=Reason.choices,
|
56
|
-
default=Reason.MANUAL,
|
57
|
-
help_text="Reason for maintenance activation"
|
58
|
-
)
|
59
|
-
status = models.CharField(
|
60
|
-
max_length=20,
|
61
|
-
choices=Status.choices,
|
62
|
-
default=Status.ACTIVE,
|
63
|
-
help_text="Current maintenance status"
|
64
|
-
)
|
65
|
-
|
66
|
-
# === Timing ===
|
67
|
-
started_at = models.DateTimeField(
|
68
|
-
default=timezone.now,
|
69
|
-
help_text="When maintenance was started"
|
70
|
-
)
|
71
|
-
ended_at = models.DateTimeField(
|
72
|
-
null=True,
|
73
|
-
blank=True,
|
74
|
-
help_text="When maintenance was completed"
|
75
|
-
)
|
76
|
-
estimated_duration = models.DurationField(
|
77
|
-
null=True,
|
78
|
-
blank=True,
|
79
|
-
help_text="Estimated maintenance duration"
|
80
|
-
)
|
81
|
-
|
82
|
-
# === User Tracking ===
|
83
|
-
initiated_by = models.ForeignKey(
|
84
|
-
User,
|
85
|
-
on_delete=models.SET_NULL,
|
86
|
-
null=True,
|
87
|
-
blank=True,
|
88
|
-
related_name='initiated_maintenance_events',
|
89
|
-
help_text="User who initiated maintenance"
|
90
|
-
)
|
91
|
-
completed_by = models.ForeignKey(
|
92
|
-
User,
|
93
|
-
on_delete=models.SET_NULL,
|
94
|
-
null=True,
|
95
|
-
blank=True,
|
96
|
-
related_name='completed_maintenance_events',
|
97
|
-
help_text="User who completed maintenance"
|
98
|
-
)
|
99
|
-
|
100
|
-
# === Cloudflare Integration ===
|
101
|
-
cloudflare_worker_deployed = models.BooleanField(
|
102
|
-
default=False,
|
103
|
-
help_text="Whether Cloudflare Worker was deployed"
|
104
|
-
)
|
105
|
-
cloudflare_deployment_id = models.CharField(
|
106
|
-
max_length=100,
|
107
|
-
blank=True,
|
108
|
-
help_text="Cloudflare deployment identifier"
|
109
|
-
)
|
110
|
-
worker_deployment_success = models.BooleanField(
|
111
|
-
default=False,
|
112
|
-
help_text="Whether Worker deployment was successful"
|
113
|
-
)
|
114
|
-
|
115
|
-
# === Metrics ===
|
116
|
-
affected_requests = models.PositiveIntegerField(
|
117
|
-
default=0,
|
118
|
-
help_text="Number of requests affected during maintenance"
|
119
|
-
)
|
120
|
-
error_count_before = models.PositiveIntegerField(
|
121
|
-
default=0,
|
122
|
-
help_text="Error count before maintenance"
|
123
|
-
)
|
124
|
-
error_count_during = models.PositiveIntegerField(
|
125
|
-
default=0,
|
126
|
-
help_text="Error count during maintenance"
|
127
|
-
)
|
128
|
-
|
129
|
-
# === Sites (Many-to-Many relationship) ===
|
130
|
-
sites = models.ManyToManyField(
|
131
|
-
'django_cfg_maintenance.CloudflareSite',
|
132
|
-
blank=True,
|
133
|
-
related_name='maintenance_events',
|
134
|
-
help_text="Sites affected by this maintenance event"
|
135
|
-
)
|
136
|
-
|
137
|
-
# === Metadata ===
|
138
|
-
created_at = models.DateTimeField(auto_now_add=True)
|
139
|
-
updated_at = models.DateTimeField(auto_now=True)
|
140
|
-
|
141
|
-
# === Custom Manager ===
|
142
|
-
from ..managers.events import MaintenanceEventManager
|
143
|
-
objects = MaintenanceEventManager()
|
144
|
-
|
145
|
-
class Meta:
|
146
|
-
ordering = ['-started_at']
|
147
|
-
verbose_name = "Maintenance Event"
|
148
|
-
verbose_name_plural = "Maintenance Events"
|
149
|
-
indexes = [
|
150
|
-
models.Index(fields=['status', '-started_at']),
|
151
|
-
models.Index(fields=['reason', '-started_at']),
|
152
|
-
models.Index(fields=['initiated_by', '-started_at']),
|
153
|
-
]
|
154
|
-
|
155
|
-
def __str__(self) -> str:
|
156
|
-
return f"{self.title} ({self.get_status_display()})"
|
157
|
-
|
158
|
-
@property
|
159
|
-
def duration(self) -> Optional[timedelta]:
|
160
|
-
"""Calculate actual maintenance duration."""
|
161
|
-
if self.ended_at:
|
162
|
-
return self.ended_at - self.started_at
|
163
|
-
elif self.status == self.Status.ACTIVE:
|
164
|
-
return timezone.now() - self.started_at
|
165
|
-
return None
|
166
|
-
|
167
|
-
@property
|
168
|
-
def is_active(self) -> bool:
|
169
|
-
"""Check if maintenance is currently active."""
|
170
|
-
return self.status == self.Status.ACTIVE
|
171
|
-
|
172
|
-
@property
|
173
|
-
def is_scheduled(self) -> bool:
|
174
|
-
"""Check if maintenance is scheduled for future."""
|
175
|
-
return self.status == self.Status.SCHEDULED
|
176
|
-
|
177
|
-
@property
|
178
|
-
def affected_sites_count(self) -> int:
|
179
|
-
"""Count of affected sites."""
|
180
|
-
return self.sites.count()
|
181
|
-
|
182
|
-
def complete(self, user: Optional[User] = None) -> None:
|
183
|
-
"""Mark maintenance as completed."""
|
184
|
-
self.status = self.Status.COMPLETED
|
185
|
-
self.ended_at = timezone.now()
|
186
|
-
self.completed_by = user
|
187
|
-
self.save(update_fields=['status', 'ended_at', 'completed_by', 'updated_at'])
|
188
|
-
|
189
|
-
def cancel(self, user: Optional[User] = None) -> None:
|
190
|
-
"""Cancel maintenance event."""
|
191
|
-
self.status = self.Status.CANCELLED
|
192
|
-
self.ended_at = timezone.now()
|
193
|
-
self.completed_by = user
|
194
|
-
self.save(update_fields=['status', 'ended_at', 'completed_by', 'updated_at'])
|
195
|
-
|
196
|
-
def fail(self, error_message: str = "") -> None:
|
197
|
-
"""Mark maintenance as failed."""
|
198
|
-
self.status = self.Status.FAILED
|
199
|
-
self.ended_at = timezone.now()
|
200
|
-
self.save(update_fields=['status', 'ended_at', 'updated_at'])
|
201
|
-
|
202
|
-
# Create failure log
|
203
|
-
MaintenanceLog.objects.create(
|
204
|
-
event=self,
|
205
|
-
level=MaintenanceLog.Level.ERROR,
|
206
|
-
message=f"Maintenance failed: {error_message}",
|
207
|
-
details={'error': error_message}
|
208
|
-
)
|
209
|
-
|
210
|
-
def clean(self) -> None:
|
211
|
-
"""Validate model data."""
|
212
|
-
super().clean()
|
213
|
-
|
214
|
-
# Validate timing
|
215
|
-
if self.ended_at and self.ended_at < self.started_at:
|
216
|
-
raise ValidationError("End time cannot be before start time")
|
217
|
-
|
218
|
-
# Validate estimated duration
|
219
|
-
if self.estimated_duration and self.estimated_duration.total_seconds() <= 0:
|
220
|
-
raise ValidationError("Estimated duration must be positive")
|
221
|
-
|
222
|
-
|
223
|
-
class MaintenanceLog(models.Model):
|
224
|
-
"""
|
225
|
-
Detailed logging for maintenance events.
|
226
|
-
|
227
|
-
Provides audit trail and debugging information for maintenance operations.
|
228
|
-
"""
|
229
|
-
|
230
|
-
class Level(models.TextChoices):
|
231
|
-
"""Log level choices."""
|
232
|
-
DEBUG = "debug", "Debug"
|
233
|
-
INFO = "info", "Info"
|
234
|
-
WARNING = "warning", "Warning"
|
235
|
-
ERROR = "error", "Error"
|
236
|
-
CRITICAL = "critical", "Critical"
|
237
|
-
|
238
|
-
# === Relationships ===
|
239
|
-
event = models.ForeignKey(
|
240
|
-
MaintenanceEvent,
|
241
|
-
on_delete=models.CASCADE,
|
242
|
-
related_name='logs',
|
243
|
-
help_text="Related maintenance event"
|
244
|
-
)
|
245
|
-
|
246
|
-
# === Log Data ===
|
247
|
-
level = models.CharField(
|
248
|
-
max_length=20,
|
249
|
-
choices=Level.choices,
|
250
|
-
default=Level.INFO,
|
251
|
-
help_text="Log level"
|
252
|
-
)
|
253
|
-
message = models.TextField(
|
254
|
-
help_text="Log message"
|
255
|
-
)
|
256
|
-
details = models.JSONField(
|
257
|
-
default=dict,
|
258
|
-
blank=True,
|
259
|
-
help_text="Additional log details (JSON)"
|
260
|
-
)
|
261
|
-
|
262
|
-
# === Context ===
|
263
|
-
component = models.CharField(
|
264
|
-
max_length=100,
|
265
|
-
blank=True,
|
266
|
-
help_text="Component that generated the log (e.g., 'cloudflare', 'monitoring')"
|
267
|
-
)
|
268
|
-
operation = models.CharField(
|
269
|
-
max_length=100,
|
270
|
-
blank=True,
|
271
|
-
help_text="Operation being performed (e.g., 'deploy_worker', 'health_check')"
|
272
|
-
)
|
273
|
-
|
274
|
-
# === User Context ===
|
275
|
-
user = models.ForeignKey(
|
276
|
-
User,
|
277
|
-
on_delete=models.SET_NULL,
|
278
|
-
null=True,
|
279
|
-
blank=True,
|
280
|
-
help_text="User associated with this log entry"
|
281
|
-
)
|
282
|
-
|
283
|
-
# === Metadata ===
|
284
|
-
timestamp = models.DateTimeField(
|
285
|
-
default=timezone.now,
|
286
|
-
help_text="When the log entry was created"
|
287
|
-
)
|
288
|
-
|
289
|
-
# === Custom Manager ===
|
290
|
-
from ..managers.events import MaintenanceLogManager
|
291
|
-
objects = MaintenanceLogManager()
|
292
|
-
|
293
|
-
class Meta:
|
294
|
-
ordering = ['-timestamp']
|
295
|
-
verbose_name = "Maintenance Log"
|
296
|
-
verbose_name_plural = "Maintenance Logs"
|
297
|
-
indexes = [
|
298
|
-
models.Index(fields=['event', '-timestamp']),
|
299
|
-
models.Index(fields=['level', '-timestamp']),
|
300
|
-
models.Index(fields=['component', '-timestamp']),
|
301
|
-
]
|
302
|
-
|
303
|
-
def __str__(self) -> str:
|
304
|
-
return f"{self.get_level_display()}: {self.message[:100]}"
|
305
|
-
|
306
|
-
@classmethod
|
307
|
-
def log_info(cls, event: MaintenanceEvent, message: str, **kwargs) -> 'MaintenanceLog':
|
308
|
-
"""Create info log entry."""
|
309
|
-
return cls.objects.create(
|
310
|
-
event=event,
|
311
|
-
level=cls.Level.INFO,
|
312
|
-
message=message,
|
313
|
-
**kwargs
|
314
|
-
)
|
315
|
-
|
316
|
-
@classmethod
|
317
|
-
def log_error(cls, event: MaintenanceEvent, message: str, **kwargs) -> 'MaintenanceLog':
|
318
|
-
"""Create error log entry."""
|
319
|
-
return cls.objects.create(
|
320
|
-
event=event,
|
321
|
-
level=cls.Level.ERROR,
|
322
|
-
message=message,
|
323
|
-
**kwargs
|
324
|
-
)
|
325
|
-
|
326
|
-
@classmethod
|
327
|
-
def log_warning(cls, event: MaintenanceEvent, message: str, **kwargs) -> 'MaintenanceLog':
|
328
|
-
"""Create warning log entry."""
|
329
|
-
return cls.objects.create(
|
330
|
-
event=event,
|
331
|
-
level=cls.Level.WARNING,
|
332
|
-
message=message,
|
333
|
-
**kwargs
|
334
|
-
)
|