django-cfg 1.2.15__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 (61) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/README.md +305 -0
  3. django_cfg/apps/maintenance/__init__.py +27 -0
  4. django_cfg/apps/maintenance/admin/__init__.py +28 -0
  5. django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
  6. django_cfg/apps/maintenance/admin/events_admin.py +374 -0
  7. django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
  8. django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
  9. django_cfg/apps/maintenance/apps.py +105 -0
  10. django_cfg/apps/maintenance/management/__init__.py +0 -0
  11. django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
  12. django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
  13. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
  14. django_cfg/apps/maintenance/managers/__init__.py +20 -0
  15. django_cfg/apps/maintenance/managers/deployments.py +287 -0
  16. django_cfg/apps/maintenance/managers/events.py +374 -0
  17. django_cfg/apps/maintenance/managers/monitoring.py +301 -0
  18. django_cfg/apps/maintenance/managers/sites.py +335 -0
  19. django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
  20. django_cfg/apps/maintenance/migrations/__init__.py +0 -0
  21. django_cfg/apps/maintenance/models/__init__.py +27 -0
  22. django_cfg/apps/maintenance/models/cloudflare.py +316 -0
  23. django_cfg/apps/maintenance/models/maintenance.py +334 -0
  24. django_cfg/apps/maintenance/models/monitoring.py +393 -0
  25. django_cfg/apps/maintenance/models/sites.py +419 -0
  26. django_cfg/apps/maintenance/serializers/__init__.py +60 -0
  27. django_cfg/apps/maintenance/serializers/actions.py +310 -0
  28. django_cfg/apps/maintenance/serializers/base.py +44 -0
  29. django_cfg/apps/maintenance/serializers/deployments.py +209 -0
  30. django_cfg/apps/maintenance/serializers/events.py +210 -0
  31. django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
  32. django_cfg/apps/maintenance/serializers/sites.py +213 -0
  33. django_cfg/apps/maintenance/services/README.md +168 -0
  34. django_cfg/apps/maintenance/services/__init__.py +21 -0
  35. django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
  36. django_cfg/apps/maintenance/services/dns_manager.py +497 -0
  37. django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
  38. django_cfg/apps/maintenance/services/site_sync.py +448 -0
  39. django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
  40. django_cfg/apps/maintenance/services/worker_manager.py +264 -0
  41. django_cfg/apps/maintenance/signals.py +38 -0
  42. django_cfg/apps/maintenance/urls.py +36 -0
  43. django_cfg/apps/maintenance/views/__init__.py +18 -0
  44. django_cfg/apps/maintenance/views/base.py +61 -0
  45. django_cfg/apps/maintenance/views/deployments.py +175 -0
  46. django_cfg/apps/maintenance/views/events.py +204 -0
  47. django_cfg/apps/maintenance/views/monitoring.py +213 -0
  48. django_cfg/apps/maintenance/views/sites.py +338 -0
  49. django_cfg/apps/urls.py +5 -1
  50. django_cfg/core/config.py +34 -3
  51. django_cfg/core/generation.py +15 -10
  52. django_cfg/models/cloudflare.py +316 -0
  53. django_cfg/models/revolution.py +1 -1
  54. django_cfg/models/tasks.py +1 -1
  55. django_cfg/modules/base.py +12 -5
  56. django_cfg/modules/django_unfold/dashboard.py +16 -1
  57. {django_cfg-1.2.15.dist-info → django_cfg-1.2.16.dist-info}/METADATA +2 -1
  58. {django_cfg-1.2.15.dist-info → django_cfg-1.2.16.dist-info}/RECORD +61 -13
  59. {django_cfg-1.2.15.dist-info → django_cfg-1.2.16.dist-info}/WHEEL +0 -0
  60. {django_cfg-1.2.15.dist-info → django_cfg-1.2.16.dist-info}/entry_points.txt +0 -0
  61. {django_cfg-1.2.15.dist-info → django_cfg-1.2.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,419 @@
1
+ """
2
+ Multi-site management models.
3
+
4
+ Models for managing multiple Cloudflare sites with ORM-like interface.
5
+ Following CRITICAL_REQUIREMENTS - proper typing, no raw Dict usage.
6
+ """
7
+
8
+ from django.db import models
9
+ from django.contrib.auth import get_user_model
10
+ from django.core.validators import URLValidator
11
+ from django.core.exceptions import ValidationError
12
+ from django.utils import timezone
13
+ from typing import Optional, List, Dict, Any
14
+ from datetime import timedelta
15
+ import re
16
+
17
+ User = get_user_model()
18
+
19
+
20
+ class CloudflareSite(models.Model):
21
+ """
22
+ Individual Cloudflare site configuration.
23
+
24
+ Represents a single site/domain managed through Cloudflare with
25
+ maintenance mode capabilities and monitoring.
26
+ """
27
+
28
+ class SiteEnvironment(models.TextChoices):
29
+ """Site environment types."""
30
+ PRODUCTION = "production", "Production"
31
+ STAGING = "staging", "Staging"
32
+ DEVELOPMENT = "development", "Development"
33
+ TESTING = "testing", "Testing"
34
+
35
+ class SiteStatus(models.TextChoices):
36
+ """Site operational status."""
37
+ ACTIVE = "active", "Active"
38
+ MAINTENANCE = "maintenance", "Under Maintenance"
39
+ OFFLINE = "offline", "Offline"
40
+ UNKNOWN = "unknown", "Unknown"
41
+
42
+ # === Basic Information ===
43
+ name = models.CharField(
44
+ max_length=100,
45
+ help_text="Friendly site name for identification"
46
+ )
47
+ domain = models.CharField(
48
+ max_length=253,
49
+ unique=True,
50
+ help_text="Domain name (e.g., example.com)"
51
+ )
52
+ description = models.TextField(
53
+ blank=True,
54
+ help_text="Site description or notes"
55
+ )
56
+
57
+ # === Cloudflare Configuration ===
58
+ zone_id = models.CharField(
59
+ max_length=32,
60
+ unique=True,
61
+ help_text="Cloudflare Zone ID"
62
+ )
63
+ account_id = models.CharField(
64
+ max_length=32,
65
+ help_text="Cloudflare Account ID"
66
+ )
67
+ api_token = models.CharField(
68
+ max_length=200,
69
+ help_text="Site-specific or account API token"
70
+ )
71
+
72
+ # === Site Classification ===
73
+ environment = models.CharField(
74
+ max_length=20,
75
+ choices=SiteEnvironment.choices,
76
+ default=SiteEnvironment.PRODUCTION,
77
+ help_text="Site environment type"
78
+ )
79
+ project = models.CharField(
80
+ max_length=100,
81
+ blank=True,
82
+ help_text="Project or client name"
83
+ )
84
+ tags = models.JSONField(
85
+ default=list,
86
+ blank=True,
87
+ help_text="Custom tags for filtering and organization"
88
+ )
89
+
90
+ # === Current State ===
91
+ current_status = models.CharField(
92
+ max_length=20,
93
+ choices=SiteStatus.choices,
94
+ default=SiteStatus.UNKNOWN,
95
+ help_text="Current operational status"
96
+ )
97
+ maintenance_active = models.BooleanField(
98
+ default=False,
99
+ help_text="Whether maintenance mode is currently active"
100
+ )
101
+ last_status_check = models.DateTimeField(
102
+ null=True,
103
+ blank=True,
104
+ help_text="When status was last checked"
105
+ )
106
+
107
+ # === Maintenance Configuration ===
108
+ worker_name = models.CharField(
109
+ max_length=100,
110
+ default="maintenance-mode",
111
+ help_text="Cloudflare Worker name for maintenance mode"
112
+ )
113
+ maintenance_template = models.CharField(
114
+ max_length=50,
115
+ default="modern",
116
+ help_text="Maintenance page template"
117
+ )
118
+ custom_maintenance_message = models.TextField(
119
+ blank=True,
120
+ help_text="Custom maintenance message for this site"
121
+ )
122
+
123
+ # === Monitoring Settings ===
124
+ monitoring_enabled = models.BooleanField(
125
+ default=True,
126
+ help_text="Enable health monitoring for this site"
127
+ )
128
+ health_check_url = models.URLField(
129
+ blank=True,
130
+ help_text="Custom health check URL (defaults to domain/health/)"
131
+ )
132
+ check_interval = models.PositiveIntegerField(
133
+ default=300, # 5 minutes
134
+ help_text="Health check interval in seconds"
135
+ )
136
+
137
+ # === Access Control ===
138
+ owner = models.ForeignKey(
139
+ User,
140
+ on_delete=models.CASCADE,
141
+ related_name='owned_sites',
142
+ help_text="Site owner"
143
+ )
144
+ allowed_users = models.ManyToManyField(
145
+ User,
146
+ blank=True,
147
+ related_name='accessible_sites',
148
+ help_text="Users with access to manage this site"
149
+ )
150
+
151
+ # === Metadata ===
152
+ created_at = models.DateTimeField(auto_now_add=True)
153
+ updated_at = models.DateTimeField(auto_now=True)
154
+ last_maintenance_at = models.DateTimeField(
155
+ null=True,
156
+ blank=True,
157
+ help_text="When maintenance was last activated"
158
+ )
159
+
160
+ # === Custom Manager ===
161
+ from ..managers.sites import CloudflareSiteManager
162
+ objects = CloudflareSiteManager()
163
+
164
+ class Meta:
165
+ ordering = ['name']
166
+ verbose_name = "Cloudflare Site"
167
+ verbose_name_plural = "Cloudflare Sites"
168
+ indexes = [
169
+ models.Index(fields=['owner', 'environment']),
170
+ models.Index(fields=['project', 'environment']),
171
+ models.Index(fields=['current_status']),
172
+ models.Index(fields=['maintenance_active']),
173
+ models.Index(fields=['domain']),
174
+ ]
175
+
176
+ def __str__(self) -> str:
177
+ status_emoji = {
178
+ self.SiteStatus.ACTIVE: "🟢",
179
+ self.SiteStatus.MAINTENANCE: "🔧",
180
+ self.SiteStatus.OFFLINE: "🔴",
181
+ self.SiteStatus.UNKNOWN: "❓"
182
+ }.get(self.current_status, "❓")
183
+
184
+ return f"{status_emoji} {self.name} ({self.domain})"
185
+
186
+ @property
187
+ def is_production(self) -> bool:
188
+ """Check if this is a production site."""
189
+ return self.environment == self.SiteEnvironment.PRODUCTION
190
+
191
+ @property
192
+ def maintenance_duration(self) -> Optional[timedelta]:
193
+ """Calculate current maintenance duration."""
194
+ if self.maintenance_active and self.last_maintenance_at:
195
+ return timezone.now() - self.last_maintenance_at
196
+ return None
197
+
198
+ @property
199
+ def health_check_endpoint(self) -> str:
200
+ """Get health check URL for this site."""
201
+ if self.health_check_url:
202
+ return self.health_check_url
203
+
204
+ # Default health check endpoint
205
+ protocol = "https" if self.is_production else "http"
206
+ return f"{protocol}://{self.domain}/health/"
207
+
208
+ def has_tag(self, tag: str) -> bool:
209
+ """Check if site has specific tag."""
210
+ return tag in (self.tags or [])
211
+
212
+ def add_tag(self, tag: str) -> None:
213
+ """Add tag to site."""
214
+ if not self.tags:
215
+ self.tags = []
216
+ if tag not in self.tags:
217
+ self.tags.append(tag)
218
+ self.save(update_fields=['tags', 'updated_at'])
219
+
220
+ def remove_tag(self, tag: str) -> None:
221
+ """Remove tag from site."""
222
+ if self.tags and tag in self.tags:
223
+ self.tags.remove(tag)
224
+ self.save(update_fields=['tags', 'updated_at'])
225
+
226
+ def enable_maintenance(self, user: Optional[User] = None) -> None:
227
+ """Enable maintenance mode for this site."""
228
+ self.maintenance_active = True
229
+ self.current_status = self.SiteStatus.MAINTENANCE
230
+ self.last_maintenance_at = timezone.now()
231
+ self.save(update_fields=[
232
+ 'maintenance_active',
233
+ 'current_status',
234
+ 'last_maintenance_at',
235
+ 'updated_at'
236
+ ])
237
+
238
+ def disable_maintenance(self) -> None:
239
+ """Disable maintenance mode for this site."""
240
+ self.maintenance_active = False
241
+ self.current_status = self.SiteStatus.ACTIVE
242
+ self.save(update_fields=[
243
+ 'maintenance_active',
244
+ 'current_status',
245
+ 'updated_at'
246
+ ])
247
+
248
+ def update_status(self, status: str) -> None:
249
+ """Update site status."""
250
+ self.current_status = status
251
+ self.last_status_check = timezone.now()
252
+ self.save(update_fields=[
253
+ 'current_status',
254
+ 'last_status_check',
255
+ 'updated_at'
256
+ ])
257
+
258
+ def clean(self) -> None:
259
+ """Validate model data."""
260
+ super().clean()
261
+
262
+ # Validate domain format
263
+ domain_pattern = re.compile(
264
+ r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?'
265
+ r'(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
266
+ )
267
+ if not domain_pattern.match(self.domain):
268
+ raise ValidationError({'domain': 'Invalid domain format'})
269
+
270
+ # Validate zone_id format (Cloudflare zone IDs are 32 chars)
271
+ if len(self.zone_id) != 32:
272
+ raise ValidationError({'zone_id': 'Zone ID must be 32 characters'})
273
+
274
+ # Validate worker name
275
+ worker_pattern = re.compile(r'^[a-zA-Z0-9\-_]+$')
276
+ if not worker_pattern.match(self.worker_name):
277
+ raise ValidationError({
278
+ 'worker_name': 'Worker name can only contain letters, numbers, hyphens, and underscores'
279
+ })
280
+
281
+ # Validate check interval
282
+ if self.check_interval < 30:
283
+ raise ValidationError({'check_interval': 'Check interval must be at least 30 seconds'})
284
+
285
+
286
+ class SiteGroup(models.Model):
287
+ """
288
+ Logical grouping of sites for bulk operations.
289
+
290
+ Allows organizing sites by project, client, environment, or any other
291
+ criteria for efficient bulk maintenance operations.
292
+ """
293
+
294
+ # === Basic Information ===
295
+ name = models.CharField(
296
+ max_length=100,
297
+ help_text="Group name"
298
+ )
299
+ description = models.TextField(
300
+ blank=True,
301
+ help_text="Group description"
302
+ )
303
+
304
+ # === Relationships ===
305
+ sites = models.ManyToManyField(
306
+ CloudflareSite,
307
+ related_name='groups',
308
+ help_text="Sites in this group"
309
+ )
310
+ owner = models.ForeignKey(
311
+ User,
312
+ on_delete=models.CASCADE,
313
+ help_text="Group owner"
314
+ )
315
+
316
+ # === Group Settings ===
317
+ auto_maintenance_rules = models.JSONField(
318
+ default=dict,
319
+ blank=True,
320
+ help_text="Automatic maintenance rules for this group"
321
+ )
322
+ notification_settings = models.JSONField(
323
+ default=dict,
324
+ blank=True,
325
+ help_text="Notification preferences for group events"
326
+ )
327
+
328
+ # === Metadata ===
329
+ created_at = models.DateTimeField(auto_now_add=True)
330
+ updated_at = models.DateTimeField(auto_now=True)
331
+
332
+ # === Custom Manager ===
333
+ from ..managers.sites import SiteGroupManager
334
+ objects = SiteGroupManager()
335
+
336
+ class Meta:
337
+ ordering = ['name']
338
+ unique_together = ['name', 'owner']
339
+ verbose_name = "Site Group"
340
+ verbose_name_plural = "Site Groups"
341
+ indexes = [
342
+ models.Index(fields=['owner', 'name']),
343
+ ]
344
+
345
+ def __str__(self) -> str:
346
+ return f"{self.name} ({self.sites.count()} sites)"
347
+
348
+ @property
349
+ def sites_count(self) -> int:
350
+ """Count of sites in this group."""
351
+ return self.sites.count()
352
+
353
+ @property
354
+ def active_sites_count(self) -> int:
355
+ """Count of active sites in this group."""
356
+ return self.sites.filter(current_status=CloudflareSite.SiteStatus.ACTIVE).count()
357
+
358
+ @property
359
+ def maintenance_sites_count(self) -> int:
360
+ """Count of sites in maintenance in this group."""
361
+ return self.sites.filter(maintenance_active=True).count()
362
+
363
+ def get_sites_by_environment(self, environment: str) -> models.QuerySet:
364
+ """Get sites in this group by environment."""
365
+ return self.sites.filter(environment=environment)
366
+
367
+ def get_sites_by_status(self, status: str) -> models.QuerySet:
368
+ """Get sites in this group by status."""
369
+ return self.sites.filter(current_status=status)
370
+
371
+ def add_sites(self, sites: List[CloudflareSite]) -> None:
372
+ """Add multiple sites to this group."""
373
+ self.sites.add(*sites)
374
+
375
+ def remove_sites(self, sites: List[CloudflareSite]) -> None:
376
+ """Remove multiple sites from this group."""
377
+ self.sites.remove(*sites)
378
+
379
+ def enable_maintenance_for_all(self, user: Optional[User] = None) -> Dict[str, Any]:
380
+ """Enable maintenance for all sites in group."""
381
+ results = {
382
+ 'total': 0,
383
+ 'successful': [],
384
+ 'failed': []
385
+ }
386
+
387
+ for site in self.sites.all():
388
+ results['total'] += 1
389
+ try:
390
+ site.enable_maintenance(user)
391
+ results['successful'].append(site.domain)
392
+ except Exception as e:
393
+ results['failed'].append({
394
+ 'site': site.domain,
395
+ 'error': str(e)
396
+ })
397
+
398
+ return results
399
+
400
+ def disable_maintenance_for_all(self) -> Dict[str, Any]:
401
+ """Disable maintenance for all sites in group."""
402
+ results = {
403
+ 'total': 0,
404
+ 'successful': [],
405
+ 'failed': []
406
+ }
407
+
408
+ for site in self.sites.filter(maintenance_active=True):
409
+ results['total'] += 1
410
+ try:
411
+ site.disable_maintenance()
412
+ results['successful'].append(site.domain)
413
+ except Exception as e:
414
+ results['failed'].append({
415
+ 'site': site.domain,
416
+ 'error': str(e)
417
+ })
418
+
419
+ return results
@@ -0,0 +1,60 @@
1
+ """
2
+ Maintenance app serializers.
3
+
4
+ Decomposed serializers for better organization and maintainability.
5
+ """
6
+
7
+ from .base import UserSerializer, APIResponseSerializer
8
+ from .sites import (
9
+ CloudflareSiteSerializer, CloudflareSiteCreateSerializer, CloudflareSiteListSerializer,
10
+ SiteGroupSerializer, SiteGroupCreateSerializer
11
+ )
12
+ from .events import (
13
+ MaintenanceEventSerializer, MaintenanceEventCreateSerializer, MaintenanceEventListSerializer,
14
+ MaintenanceEventUpdateSerializer, MaintenanceLogSerializer
15
+ )
16
+ from .monitoring import (
17
+ MonitoringTargetSerializer, MonitoringTargetCreateSerializer, HealthCheckResultSerializer
18
+ )
19
+ from .deployments import (
20
+ CloudflareDeploymentSerializer, CloudflareDeploymentListSerializer
21
+ )
22
+ from .actions import (
23
+ BulkMaintenanceActionSerializer, SiteFilterSerializer, SiteGroupActionSerializer,
24
+ BulkOperationResultSerializer
25
+ )
26
+
27
+ __all__ = [
28
+ # Base
29
+ 'UserSerializer',
30
+ 'APIResponseSerializer',
31
+ 'BulkOperationResultSerializer',
32
+
33
+ # Sites
34
+ 'CloudflareSiteSerializer',
35
+ 'CloudflareSiteCreateSerializer',
36
+ 'CloudflareSiteListSerializer',
37
+ 'SiteGroupSerializer',
38
+ 'SiteGroupCreateSerializer',
39
+
40
+ # Events
41
+ 'MaintenanceEventSerializer',
42
+ 'MaintenanceEventCreateSerializer',
43
+ 'MaintenanceEventListSerializer',
44
+ 'MaintenanceEventUpdateSerializer',
45
+ 'MaintenanceLogSerializer',
46
+
47
+ # Monitoring
48
+ 'MonitoringTargetSerializer',
49
+ 'MonitoringTargetCreateSerializer',
50
+ 'HealthCheckResultSerializer',
51
+
52
+ # Deployments
53
+ 'CloudflareDeploymentSerializer',
54
+ 'CloudflareDeploymentListSerializer',
55
+
56
+ # Actions
57
+ 'BulkMaintenanceActionSerializer',
58
+ 'SiteFilterSerializer',
59
+ 'SiteGroupActionSerializer',
60
+ ]