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,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
|