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,448 @@
1
+ """
2
+ Site synchronization service using official Cloudflare library.
3
+
4
+ Handles synchronization between Django CloudflareSite models and actual Cloudflare zones.
5
+ """
6
+
7
+ import logging
8
+ from typing import List, Dict, Any, Optional, Tuple
9
+ from datetime import datetime
10
+ from django.utils import timezone
11
+ from django.db import transaction
12
+ from django.contrib.auth import get_user_model
13
+
14
+ from .cloudflare_client import CloudflareClient
15
+ from ..models import CloudflareSite
16
+ from django_cfg.models.cloudflare import CloudflareConfig
17
+
18
+ User = get_user_model()
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class SiteSyncService:
23
+ """
24
+ Service for synchronizing CloudflareSite models with Cloudflare zones.
25
+
26
+ Provides bidirectional sync between Django models and Cloudflare API.
27
+ """
28
+
29
+ def __init__(self, config: CloudflareConfig):
30
+ """Initialize sync service."""
31
+ self.config = config
32
+ self.client = CloudflareClient(config)
33
+
34
+ def sync_user_sites(
35
+ self,
36
+ user: User,
37
+ dry_run: bool = False,
38
+ force_update: bool = False,
39
+ environment: str = 'production',
40
+ project: str = '',
41
+ tags: List[str] = None
42
+ ) -> Dict[str, Any]:
43
+ """
44
+ Sync all sites for a user with Cloudflare zones.
45
+
46
+ Args:
47
+ user: User to sync sites for
48
+ dry_run: If True, only show what would be changed
49
+ force_update: If True, update existing sites
50
+ environment: Default environment for new sites
51
+ project: Default project for new sites
52
+ tags: Default tags for new sites
53
+
54
+ Returns:
55
+ Dict with sync statistics and results
56
+ """
57
+ logger.info(f"Starting site sync for user {user.username}")
58
+
59
+ stats = {
60
+ 'created': 0,
61
+ 'updated': 0,
62
+ 'skipped': 0,
63
+ 'errors': 0,
64
+ 'sites': []
65
+ }
66
+
67
+ try:
68
+ # Fetch zones from Cloudflare
69
+ cf_zones = self.client.list_zones()
70
+ logger.info(f"Found {len(cf_zones)} zones in Cloudflare")
71
+
72
+ # Get existing sites for user
73
+ existing_sites = {
74
+ site.domain: site
75
+ for site in CloudflareSite.objects.for_user(user)
76
+ }
77
+
78
+ # Process each Cloudflare zone
79
+ for zone in cf_zones:
80
+ try:
81
+ result = self._sync_single_zone(
82
+ user=user,
83
+ zone=zone,
84
+ existing_sites=existing_sites,
85
+ dry_run=dry_run,
86
+ force_update=force_update,
87
+ environment=environment,
88
+ project=project,
89
+ tags=tags or []
90
+ )
91
+
92
+ stats[result['action']] += 1
93
+ stats['sites'].append(result)
94
+
95
+ except Exception as e:
96
+ logger.error(f"Error syncing zone {zone.name}: {e}")
97
+ stats['errors'] += 1
98
+ stats['sites'].append({
99
+ 'domain': zone.name,
100
+ 'action': 'error',
101
+ 'error': str(e)
102
+ })
103
+
104
+ logger.info(f"Sync completed: {stats}")
105
+ return stats
106
+
107
+ except Exception as e:
108
+ logger.error(f"Site sync failed: {e}")
109
+ raise
110
+
111
+ def _sync_single_zone(
112
+ self,
113
+ user: User,
114
+ zone: Any, # Cloudflare Zone object
115
+ existing_sites: Dict[str, CloudflareSite],
116
+ dry_run: bool,
117
+ force_update: bool,
118
+ environment: str,
119
+ project: str,
120
+ tags: List[str]
121
+ ) -> Dict[str, Any]:
122
+ """Sync a single Cloudflare zone with Django model."""
123
+ domain = zone.name
124
+ zone_id = zone.id
125
+
126
+ existing_site = existing_sites.get(domain)
127
+
128
+ if existing_site:
129
+ if force_update:
130
+ # Update existing site
131
+ if not dry_run:
132
+ self._update_site_from_zone(existing_site, zone)
133
+
134
+ return {
135
+ 'domain': domain,
136
+ 'action': 'updated',
137
+ 'zone_id': zone_id,
138
+ 'site_id': existing_site.id if existing_site else None
139
+ }
140
+ else:
141
+ return {
142
+ 'domain': domain,
143
+ 'action': 'skipped',
144
+ 'zone_id': zone_id,
145
+ 'site_id': existing_site.id,
146
+ 'reason': 'already_exists'
147
+ }
148
+ else:
149
+ # Create new site
150
+ if not dry_run:
151
+ site = self._create_site_from_zone(
152
+ user=user,
153
+ zone=zone,
154
+ environment=environment,
155
+ project=project,
156
+ tags=tags
157
+ )
158
+ site_id = site.id
159
+ else:
160
+ site_id = None
161
+
162
+ return {
163
+ 'domain': domain,
164
+ 'action': 'created',
165
+ 'zone_id': zone_id,
166
+ 'site_id': site_id
167
+ }
168
+
169
+ def _create_site_from_zone(
170
+ self,
171
+ user: User,
172
+ zone: Any,
173
+ environment: str,
174
+ project: str,
175
+ tags: List[str]
176
+ ) -> CloudflareSite:
177
+ """Create CloudflareSite from Cloudflare zone."""
178
+ with transaction.atomic():
179
+ # Get account ID
180
+ account_id = getattr(zone.account, 'id', '') if hasattr(zone, 'account') else ''
181
+
182
+ site = CloudflareSite.objects.create_site(
183
+ name=zone.name,
184
+ domain=zone.name,
185
+ owner=user,
186
+ environment=environment,
187
+ project=project,
188
+ tags=tags,
189
+ # Cloudflare specific fields
190
+ zone_id=zone.id,
191
+ account_id=account_id,
192
+ current_status=self._map_zone_status(zone.status),
193
+ cloudflare_settings={
194
+ 'zone_data': self._serialize_zone(zone),
195
+ 'synced_at': timezone.now().isoformat(),
196
+ 'sync_source': 'cloudflare_api'
197
+ }
198
+ )
199
+
200
+ logger.info(f"Created site {site.name} from Cloudflare zone")
201
+ return site
202
+
203
+ def _update_site_from_zone(self, site: CloudflareSite, zone: Any) -> None:
204
+ """Update CloudflareSite with data from Cloudflare zone."""
205
+ with transaction.atomic():
206
+ # Update zone-specific fields
207
+ site.zone_id = zone.id
208
+ site.current_status = self._map_zone_status(zone.status)
209
+
210
+ # Update account ID if available
211
+ if hasattr(zone, 'account') and zone.account:
212
+ site.account_id = getattr(zone.account, 'id', site.account_id)
213
+
214
+ # Update settings
215
+ settings = site.cloudflare_settings or {}
216
+ settings.update({
217
+ 'zone_data': self._serialize_zone(zone),
218
+ 'synced_at': timezone.now().isoformat(),
219
+ 'sync_source': 'cloudflare_api'
220
+ })
221
+ site.cloudflare_settings = settings
222
+
223
+ site.save()
224
+ logger.info(f"Updated site {site.name} from Cloudflare zone")
225
+
226
+ def _map_zone_status(self, cf_status: str) -> str:
227
+ """Map Cloudflare zone status to CloudflareSite status."""
228
+ status_mapping = {
229
+ 'active': CloudflareSite.SiteStatus.ACTIVE,
230
+ 'pending': CloudflareSite.SiteStatus.UNKNOWN,
231
+ 'initializing': CloudflareSite.SiteStatus.UNKNOWN,
232
+ 'moved': CloudflareSite.SiteStatus.OFFLINE,
233
+ 'deleted': CloudflareSite.SiteStatus.OFFLINE,
234
+ 'deactivated': CloudflareSite.SiteStatus.OFFLINE,
235
+ }
236
+ return status_mapping.get(cf_status.lower(), CloudflareSite.SiteStatus.UNKNOWN)
237
+
238
+ def _serialize_zone(self, zone: Any) -> Dict[str, Any]:
239
+ """Serialize Cloudflare zone to standardized format."""
240
+ # Use the client's serialize method if available
241
+ if hasattr(self.client, 'serialize_zone'):
242
+ return self.client.serialize_zone(zone)
243
+
244
+ # Fallback to manual serialization
245
+ try:
246
+ if hasattr(zone, 'model_dump'):
247
+ # Pydantic model
248
+ return zone.model_dump()
249
+ elif isinstance(zone, dict):
250
+ # Already a dict
251
+ return zone
252
+ else:
253
+ # Try to extract common attributes
254
+ return {
255
+ 'id': getattr(zone, 'id', ''),
256
+ 'name': getattr(zone, 'name', ''),
257
+ 'status': getattr(zone, 'status', 'unknown'),
258
+ 'paused': getattr(zone, 'paused', False),
259
+ 'type': getattr(zone, 'type', 'full'),
260
+ 'development_mode': getattr(zone, 'development_mode', 0),
261
+ 'name_servers': getattr(zone, 'name_servers', []),
262
+ 'original_name_servers': getattr(zone, 'original_name_servers', []),
263
+ 'original_registrar': getattr(zone, 'original_registrar', ''),
264
+ 'original_dnshost': getattr(zone, 'original_dnshost', ''),
265
+ 'created_on': getattr(zone, 'created_on', ''),
266
+ 'modified_on': getattr(zone, 'modified_on', ''),
267
+ 'activated_on': getattr(zone, 'activated_on', ''),
268
+ 'account': getattr(zone, 'account', {})
269
+ }
270
+ except Exception as e:
271
+ zone_name = getattr(zone, 'name', 'unknown')
272
+ logger.warning(f"Failed to serialize zone {zone_name}: {e}")
273
+ return {
274
+ 'id': getattr(zone, 'id', ''),
275
+ 'name': zone_name,
276
+ 'status': getattr(zone, 'status', 'unknown'),
277
+ 'error': f"Serialization failed: {e}"
278
+ }
279
+
280
+ def sync_site_dns_records(self, site: CloudflareSite) -> Dict[str, Any]:
281
+ """
282
+ Sync DNS records for a specific site.
283
+
284
+ Args:
285
+ site: CloudflareSite to sync DNS records for
286
+
287
+ Returns:
288
+ Dict with sync results
289
+ """
290
+ logger.info(f"Syncing DNS records for site {site.domain}")
291
+
292
+ try:
293
+ # Fetch DNS records from Cloudflare
294
+ dns_records = self.client.list_dns_records(site.zone_id)
295
+
296
+ # Update site settings with DNS records
297
+ settings = site.cloudflare_settings or {}
298
+ settings['dns_records'] = [
299
+ {
300
+ 'id': record.id,
301
+ 'type': record.type,
302
+ 'name': record.name,
303
+ 'content': record.content,
304
+ 'ttl': getattr(record, 'ttl', 300),
305
+ 'proxied': getattr(record, 'proxied', False),
306
+ 'created_on': getattr(record, 'created_on', ''),
307
+ 'modified_on': getattr(record, 'modified_on', ''),
308
+ }
309
+ for record in dns_records
310
+ ]
311
+ settings['dns_synced_at'] = timezone.now().isoformat()
312
+
313
+ site.cloudflare_settings = settings
314
+ site.save()
315
+
316
+ logger.info(f"Synced {len(dns_records)} DNS records for {site.domain}")
317
+
318
+ return {
319
+ 'success': True,
320
+ 'records_count': len(dns_records),
321
+ 'synced_at': settings['dns_synced_at']
322
+ }
323
+
324
+ except Exception as e:
325
+ logger.error(f"Failed to sync DNS records for {site.domain}: {e}")
326
+ return {
327
+ 'success': False,
328
+ 'error': str(e)
329
+ }
330
+
331
+ def validate_site_configuration(self, site: CloudflareSite) -> Dict[str, Any]:
332
+ """
333
+ Validate site configuration against Cloudflare.
334
+
335
+ Args:
336
+ site: CloudflareSite to validate
337
+
338
+ Returns:
339
+ Dict with validation results
340
+ """
341
+ logger.info(f"Validating configuration for site {site.domain}")
342
+
343
+ validation_results = {
344
+ 'valid': True,
345
+ 'issues': [],
346
+ 'warnings': []
347
+ }
348
+
349
+ try:
350
+ # Check if zone exists in Cloudflare
351
+ cf_zone = self.client.get_zone(site.zone_id)
352
+ if not cf_zone:
353
+ validation_results['valid'] = False
354
+ validation_results['issues'].append(
355
+ f"Zone {site.zone_id} not found in Cloudflare"
356
+ )
357
+ return validation_results
358
+
359
+ # Check domain name consistency
360
+ if cf_zone.name != site.domain:
361
+ validation_results['warnings'].append(
362
+ f"Domain mismatch: Django={site.domain}, Cloudflare={cf_zone.name}"
363
+ )
364
+
365
+ # Check zone status
366
+ if cf_zone.status != 'active':
367
+ validation_results['warnings'].append(
368
+ f"Zone status is {cf_zone.status}, not active"
369
+ )
370
+
371
+ # Check if zone is paused
372
+ if getattr(cf_zone, 'paused', False):
373
+ validation_results['warnings'].append("Zone is paused in Cloudflare")
374
+
375
+ logger.info(f"Validation completed for {site.domain}")
376
+
377
+ except Exception as e:
378
+ logger.error(f"Validation failed for {site.domain}: {e}")
379
+ validation_results['valid'] = False
380
+ validation_results['issues'].append(f"Validation error: {e}")
381
+
382
+ return validation_results
383
+
384
+ def get_sync_status(self, user: User) -> Dict[str, Any]:
385
+ """
386
+ Get synchronization status for user's sites.
387
+
388
+ Args:
389
+ user: User to get sync status for
390
+
391
+ Returns:
392
+ Dict with sync status information
393
+ """
394
+ sites = CloudflareSite.objects.for_user(user).with_full_relations()
395
+
396
+ status = {
397
+ 'total_sites': sites.count(),
398
+ 'synced_sites': 0,
399
+ 'never_synced': 0,
400
+ 'outdated_sync': 0,
401
+ 'sync_errors': 0,
402
+ 'last_sync': None,
403
+ 'sites_status': []
404
+ }
405
+
406
+ now = timezone.now()
407
+ sync_threshold = now - timezone.timedelta(hours=24) # Consider outdated after 24h
408
+
409
+ for site in sites:
410
+ site_status = {
411
+ 'id': site.id,
412
+ 'domain': site.domain,
413
+ 'zone_id': site.zone_id,
414
+ 'status': 'unknown'
415
+ }
416
+
417
+ settings = site.cloudflare_settings or {}
418
+ synced_at_str = settings.get('synced_at')
419
+
420
+ if synced_at_str:
421
+ try:
422
+ synced_at = datetime.fromisoformat(synced_at_str.replace('Z', '+00:00'))
423
+ synced_at = timezone.make_aware(synced_at) if timezone.is_naive(synced_at) else synced_at
424
+
425
+ site_status['last_sync'] = synced_at.isoformat()
426
+
427
+ if synced_at > sync_threshold:
428
+ site_status['status'] = 'synced'
429
+ status['synced_sites'] += 1
430
+ else:
431
+ site_status['status'] = 'outdated'
432
+ status['outdated_sync'] += 1
433
+
434
+ # Update overall last sync
435
+ if not status['last_sync'] or synced_at.isoformat() > status['last_sync']:
436
+ status['last_sync'] = synced_at.isoformat()
437
+
438
+ except (ValueError, TypeError) as e:
439
+ logger.warning(f"Invalid sync timestamp for {site.domain}: {e}")
440
+ site_status['status'] = 'error'
441
+ status['sync_errors'] += 1
442
+ else:
443
+ site_status['status'] = 'never_synced'
444
+ status['never_synced'] += 1
445
+
446
+ status['sites_status'].append(site_status)
447
+
448
+ return status