django-cfg 1.2.17__py3-none-any.whl → 1.2.19__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 (81) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/models/__init__.py +68 -0
  3. django_cfg/apps/accounts/models/activity.py +34 -0
  4. django_cfg/apps/accounts/models/auth.py +50 -0
  5. django_cfg/apps/accounts/models/base.py +8 -0
  6. django_cfg/apps/accounts/models/choices.py +32 -0
  7. django_cfg/apps/accounts/models/integrations.py +75 -0
  8. django_cfg/apps/accounts/models/registration.py +52 -0
  9. django_cfg/apps/accounts/models/user.py +80 -0
  10. django_cfg/apps/maintenance/__init__.py +53 -24
  11. django_cfg/apps/maintenance/admin/__init__.py +7 -18
  12. django_cfg/apps/maintenance/admin/api_key_admin.py +185 -0
  13. django_cfg/apps/maintenance/admin/log_admin.py +156 -0
  14. django_cfg/apps/maintenance/admin/scheduled_admin.py +390 -0
  15. django_cfg/apps/maintenance/admin/site_admin.py +448 -0
  16. django_cfg/apps/maintenance/apps.py +9 -96
  17. django_cfg/apps/maintenance/management/commands/maintenance.py +193 -307
  18. django_cfg/apps/maintenance/management/commands/process_scheduled_maintenance.py +241 -0
  19. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +152 -111
  20. django_cfg/apps/maintenance/managers/__init__.py +7 -12
  21. django_cfg/apps/maintenance/managers/cloudflare_site_manager.py +192 -0
  22. django_cfg/apps/maintenance/managers/maintenance_log_manager.py +151 -0
  23. django_cfg/apps/maintenance/migrations/0001_initial.py +145 -705
  24. django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py +21 -0
  25. django_cfg/apps/maintenance/models/__init__.py +23 -21
  26. django_cfg/apps/maintenance/models/cloudflare_api_key.py +109 -0
  27. django_cfg/apps/maintenance/models/cloudflare_site.py +125 -0
  28. django_cfg/apps/maintenance/models/maintenance_log.py +131 -0
  29. django_cfg/apps/maintenance/models/scheduled_maintenance.py +307 -0
  30. django_cfg/apps/maintenance/services/__init__.py +37 -16
  31. django_cfg/apps/maintenance/services/bulk_operations_service.py +400 -0
  32. django_cfg/apps/maintenance/services/maintenance_service.py +230 -0
  33. django_cfg/apps/maintenance/services/scheduled_maintenance_service.py +381 -0
  34. django_cfg/apps/maintenance/services/site_sync_service.py +390 -0
  35. django_cfg/apps/maintenance/utils/__init__.py +12 -0
  36. django_cfg/apps/maintenance/utils/retry_utils.py +109 -0
  37. django_cfg/config.py +4 -0
  38. django_cfg/core/config.py +4 -6
  39. django_cfg/modules/django_unfold/dashboard.py +4 -5
  40. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/METADATA +52 -1
  41. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/RECORD +45 -55
  42. django_cfg/apps/maintenance/README.md +0 -305
  43. django_cfg/apps/maintenance/admin/deployments_admin.py +0 -251
  44. django_cfg/apps/maintenance/admin/events_admin.py +0 -374
  45. django_cfg/apps/maintenance/admin/monitoring_admin.py +0 -215
  46. django_cfg/apps/maintenance/admin/sites_admin.py +0 -464
  47. django_cfg/apps/maintenance/managers/deployments.py +0 -287
  48. django_cfg/apps/maintenance/managers/events.py +0 -374
  49. django_cfg/apps/maintenance/managers/monitoring.py +0 -301
  50. django_cfg/apps/maintenance/managers/sites.py +0 -335
  51. django_cfg/apps/maintenance/models/cloudflare.py +0 -316
  52. django_cfg/apps/maintenance/models/maintenance.py +0 -334
  53. django_cfg/apps/maintenance/models/monitoring.py +0 -393
  54. django_cfg/apps/maintenance/models/sites.py +0 -419
  55. django_cfg/apps/maintenance/serializers/__init__.py +0 -60
  56. django_cfg/apps/maintenance/serializers/actions.py +0 -310
  57. django_cfg/apps/maintenance/serializers/base.py +0 -44
  58. django_cfg/apps/maintenance/serializers/deployments.py +0 -209
  59. django_cfg/apps/maintenance/serializers/events.py +0 -210
  60. django_cfg/apps/maintenance/serializers/monitoring.py +0 -278
  61. django_cfg/apps/maintenance/serializers/sites.py +0 -213
  62. django_cfg/apps/maintenance/services/README.md +0 -168
  63. django_cfg/apps/maintenance/services/cloudflare_client.py +0 -441
  64. django_cfg/apps/maintenance/services/dns_manager.py +0 -497
  65. django_cfg/apps/maintenance/services/maintenance_manager.py +0 -504
  66. django_cfg/apps/maintenance/services/site_sync.py +0 -448
  67. django_cfg/apps/maintenance/services/sync_command_service.py +0 -330
  68. django_cfg/apps/maintenance/services/worker_manager.py +0 -264
  69. django_cfg/apps/maintenance/signals.py +0 -38
  70. django_cfg/apps/maintenance/urls.py +0 -36
  71. django_cfg/apps/maintenance/views/__init__.py +0 -18
  72. django_cfg/apps/maintenance/views/base.py +0 -61
  73. django_cfg/apps/maintenance/views/deployments.py +0 -175
  74. django_cfg/apps/maintenance/views/events.py +0 -204
  75. django_cfg/apps/maintenance/views/monitoring.py +0 -213
  76. django_cfg/apps/maintenance/views/sites.py +0 -338
  77. django_cfg/models/cloudflare.py +0 -316
  78. /django_cfg/apps/accounts/{models.py → __models.py} +0 -0
  79. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/WHEEL +0 -0
  80. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/entry_points.txt +0 -0
  81. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,381 @@
1
+ """
2
+ Scheduled maintenance service.
3
+
4
+ Handles automatic execution of scheduled maintenance events.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, List, Any, Optional
9
+ from datetime import datetime, timedelta
10
+ from django.utils import timezone
11
+ from django.db import transaction
12
+
13
+ from ..models import ScheduledMaintenance, CloudflareSite
14
+ from .bulk_operations_service import bulk_operations
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ScheduledMaintenanceService:
20
+ """Service for managing scheduled maintenance events."""
21
+
22
+ def __init__(self):
23
+ """Initialize scheduled maintenance service."""
24
+ pass
25
+
26
+ def create_scheduled_maintenance(self,
27
+ title: str,
28
+ scheduled_start: datetime,
29
+ estimated_duration: timedelta,
30
+ sites: List[CloudflareSite],
31
+ description: str = "",
32
+ maintenance_message: str = "",
33
+ template: str = "modern",
34
+ priority: str = "normal",
35
+ auto_enable: bool = True,
36
+ auto_disable: bool = True,
37
+ notify_before: timedelta = timedelta(hours=1),
38
+ created_by: str = "") -> ScheduledMaintenance:
39
+ """
40
+ Create a new scheduled maintenance event.
41
+
42
+ Args:
43
+ title: Maintenance event title
44
+ scheduled_start: When maintenance should start
45
+ estimated_duration: Expected duration
46
+ sites: List of sites to affect
47
+ description: Detailed description
48
+ maintenance_message: Message to display during maintenance
49
+ template: Template to use for maintenance page
50
+ priority: Priority level
51
+ auto_enable: Automatically enable maintenance
52
+ auto_disable: Automatically disable maintenance
53
+ notify_before: When to send notification before start
54
+ created_by: Who created this maintenance
55
+
56
+ Returns:
57
+ Created ScheduledMaintenance instance
58
+ """
59
+ with transaction.atomic():
60
+ scheduled_maintenance = ScheduledMaintenance.objects.create(
61
+ title=title,
62
+ description=description,
63
+ scheduled_start=scheduled_start,
64
+ estimated_duration=estimated_duration,
65
+ maintenance_message=maintenance_message,
66
+ template=template,
67
+ priority=priority,
68
+ auto_enable=auto_enable,
69
+ auto_disable=auto_disable,
70
+ notify_before=notify_before,
71
+ created_by=created_by
72
+ )
73
+
74
+ # Add sites
75
+ scheduled_maintenance.sites.set(sites)
76
+
77
+ logger.info(f"Created scheduled maintenance: {scheduled_maintenance}")
78
+ return scheduled_maintenance
79
+
80
+ def process_due_maintenances(self) -> Dict[str, Any]:
81
+ """
82
+ Process all maintenance events that are due to start.
83
+
84
+ Returns:
85
+ Dict with processing results
86
+ """
87
+ due_maintenances = ScheduledMaintenance.get_due_maintenances()
88
+
89
+ results = {
90
+ 'processed': 0,
91
+ 'successful': 0,
92
+ 'failed': 0,
93
+ 'details': []
94
+ }
95
+
96
+ for maintenance in due_maintenances:
97
+ if maintenance.auto_enable:
98
+ try:
99
+ result = maintenance.start_maintenance()
100
+
101
+ if result['success']:
102
+ results['successful'] += 1
103
+ logger.info(f"Started scheduled maintenance: {maintenance.title}")
104
+ else:
105
+ results['failed'] += 1
106
+ logger.error(f"Failed to start maintenance {maintenance.title}: {result.get('error')}")
107
+
108
+ results['details'].append({
109
+ 'maintenance_id': maintenance.id,
110
+ 'title': maintenance.title,
111
+ 'success': result['success'],
112
+ 'sites_affected': result.get('sites_affected', 0),
113
+ 'error': result.get('error')
114
+ })
115
+
116
+ except Exception as e:
117
+ results['failed'] += 1
118
+ logger.error(f"Exception starting maintenance {maintenance.title}: {e}")
119
+
120
+ results['details'].append({
121
+ 'maintenance_id': maintenance.id,
122
+ 'title': maintenance.title,
123
+ 'success': False,
124
+ 'error': str(e)
125
+ })
126
+
127
+ results['processed'] += 1
128
+
129
+ if results['processed'] > 0:
130
+ logger.info(f"Processed {results['processed']} due maintenances: {results['successful']} successful, {results['failed']} failed")
131
+
132
+ return results
133
+
134
+ def process_overdue_maintenances(self) -> Dict[str, Any]:
135
+ """
136
+ Process all maintenance events that should have ended.
137
+
138
+ Returns:
139
+ Dict with processing results
140
+ """
141
+ overdue_maintenances = ScheduledMaintenance.get_overdue_maintenances()
142
+
143
+ results = {
144
+ 'processed': 0,
145
+ 'successful': 0,
146
+ 'failed': 0,
147
+ 'details': []
148
+ }
149
+
150
+ for maintenance in overdue_maintenances:
151
+ if maintenance.auto_disable:
152
+ try:
153
+ result = maintenance.complete_maintenance()
154
+
155
+ if result['success']:
156
+ results['successful'] += 1
157
+ logger.info(f"Completed overdue maintenance: {maintenance.title}")
158
+ else:
159
+ results['failed'] += 1
160
+ logger.error(f"Failed to complete maintenance {maintenance.title}: {result.get('error')}")
161
+
162
+ results['details'].append({
163
+ 'maintenance_id': maintenance.id,
164
+ 'title': maintenance.title,
165
+ 'success': result['success'],
166
+ 'sites_affected': result.get('sites_affected', 0),
167
+ 'actual_duration': result.get('actual_duration'),
168
+ 'error': result.get('error')
169
+ })
170
+
171
+ except Exception as e:
172
+ results['failed'] += 1
173
+ logger.error(f"Exception completing maintenance {maintenance.title}: {e}")
174
+
175
+ results['details'].append({
176
+ 'maintenance_id': maintenance.id,
177
+ 'title': maintenance.title,
178
+ 'success': False,
179
+ 'error': str(e)
180
+ })
181
+
182
+ results['processed'] += 1
183
+
184
+ if results['processed'] > 0:
185
+ logger.info(f"Processed {results['processed']} overdue maintenances: {results['successful']} successful, {results['failed']} failed")
186
+
187
+ return results
188
+
189
+ def get_upcoming_maintenances(self, hours: int = 24) -> List[Dict[str, Any]]:
190
+ """
191
+ Get upcoming maintenance events.
192
+
193
+ Args:
194
+ hours: Look ahead this many hours
195
+
196
+ Returns:
197
+ List of upcoming maintenance info
198
+ """
199
+ upcoming = ScheduledMaintenance.get_upcoming_maintenances(hours=hours)
200
+
201
+ return [
202
+ {
203
+ 'id': maintenance.id,
204
+ 'title': maintenance.title,
205
+ 'scheduled_start': maintenance.scheduled_start.isoformat(),
206
+ 'estimated_duration': maintenance.estimated_duration.total_seconds(),
207
+ 'sites_count': maintenance.affected_sites_count,
208
+ 'priority': maintenance.priority,
209
+ 'time_until_start': maintenance.time_until_start.total_seconds() if maintenance.time_until_start else None,
210
+ 'auto_enable': maintenance.auto_enable,
211
+ 'template': maintenance.template
212
+ }
213
+ for maintenance in upcoming
214
+ ]
215
+
216
+ def get_active_maintenances(self) -> List[Dict[str, Any]]:
217
+ """Get currently active maintenance events."""
218
+ active = ScheduledMaintenance.objects.filter(
219
+ status=ScheduledMaintenance.Status.ACTIVE
220
+ )
221
+
222
+ return [
223
+ {
224
+ 'id': maintenance.id,
225
+ 'title': maintenance.title,
226
+ 'started_at': maintenance.actual_start.isoformat() if maintenance.actual_start else None,
227
+ 'scheduled_end': maintenance.scheduled_end.isoformat(),
228
+ 'sites_count': maintenance.affected_sites_count,
229
+ 'priority': maintenance.priority,
230
+ 'time_until_end': maintenance.time_until_end.total_seconds() if maintenance.time_until_end else None,
231
+ 'auto_disable': maintenance.auto_disable,
232
+ 'is_overdue': maintenance.is_overdue
233
+ }
234
+ for maintenance in active
235
+ ]
236
+
237
+ def get_maintenance_calendar(self, days: int = 30) -> Dict[str, List[Dict[str, Any]]]:
238
+ """
239
+ Get maintenance calendar for specified days.
240
+
241
+ Args:
242
+ days: Number of days to look ahead
243
+
244
+ Returns:
245
+ Dict with dates as keys and maintenance events as values
246
+ """
247
+ end_date = timezone.now() + timedelta(days=days)
248
+
249
+ maintenances = ScheduledMaintenance.objects.filter(
250
+ scheduled_start__gte=timezone.now(),
251
+ scheduled_start__lte=end_date
252
+ ).order_by('scheduled_start')
253
+
254
+ calendar = {}
255
+
256
+ for maintenance in maintenances:
257
+ date_key = maintenance.scheduled_start.date().isoformat()
258
+
259
+ if date_key not in calendar:
260
+ calendar[date_key] = []
261
+
262
+ calendar[date_key].append({
263
+ 'id': maintenance.id,
264
+ 'title': maintenance.title,
265
+ 'start_time': maintenance.scheduled_start.time().isoformat(),
266
+ 'duration': maintenance.estimated_duration.total_seconds(),
267
+ 'sites_count': maintenance.affected_sites_count,
268
+ 'priority': maintenance.priority,
269
+ 'status': maintenance.status
270
+ })
271
+
272
+ return calendar
273
+
274
+ def bulk_schedule_maintenance(self,
275
+ title_template: str,
276
+ sites_groups: List[List[CloudflareSite]],
277
+ start_times: List[datetime],
278
+ estimated_duration: timedelta,
279
+ **kwargs) -> List[ScheduledMaintenance]:
280
+ """
281
+ Bulk schedule multiple maintenance events.
282
+
283
+ Args:
284
+ title_template: Template for titles (can include {index})
285
+ sites_groups: List of site groups for each maintenance
286
+ start_times: List of start times for each maintenance
287
+ estimated_duration: Duration for all maintenances
288
+ **kwargs: Additional arguments for create_scheduled_maintenance
289
+
290
+ Returns:
291
+ List of created ScheduledMaintenance instances
292
+ """
293
+ if len(sites_groups) != len(start_times):
294
+ raise ValueError("sites_groups and start_times must have same length")
295
+
296
+ created_maintenances = []
297
+
298
+ for i, (sites, start_time) in enumerate(zip(sites_groups, start_times)):
299
+ title = title_template.format(index=i+1, start_time=start_time.strftime('%Y-%m-%d %H:%M'))
300
+
301
+ maintenance = self.create_scheduled_maintenance(
302
+ title=title,
303
+ scheduled_start=start_time,
304
+ estimated_duration=estimated_duration,
305
+ sites=sites,
306
+ **kwargs
307
+ )
308
+
309
+ created_maintenances.append(maintenance)
310
+
311
+ logger.info(f"Bulk created {len(created_maintenances)} scheduled maintenances")
312
+ return created_maintenances
313
+
314
+ def cancel_conflicting_maintenances(self,
315
+ new_start: datetime,
316
+ new_end: datetime,
317
+ sites: List[CloudflareSite],
318
+ reason: str = "Conflicting maintenance") -> List[ScheduledMaintenance]:
319
+ """
320
+ Cancel maintenance events that conflict with a new maintenance window.
321
+
322
+ Args:
323
+ new_start: Start time of new maintenance
324
+ new_end: End time of new maintenance
325
+ sites: Sites affected by new maintenance
326
+ reason: Reason for cancellation
327
+
328
+ Returns:
329
+ List of cancelled maintenance events
330
+ """
331
+ site_ids = [site.id for site in sites]
332
+
333
+ # Find conflicting maintenances
334
+ conflicting = ScheduledMaintenance.objects.filter(
335
+ status__in=[ScheduledMaintenance.Status.SCHEDULED, ScheduledMaintenance.Status.ACTIVE],
336
+ sites__id__in=site_ids,
337
+ scheduled_start__lt=new_end,
338
+ scheduled_end__gt=new_start
339
+ ).distinct()
340
+
341
+ cancelled = []
342
+
343
+ for maintenance in conflicting:
344
+ result = maintenance.cancel_maintenance(reason=reason)
345
+ if result['success']:
346
+ cancelled.append(maintenance)
347
+ logger.info(f"Cancelled conflicting maintenance: {maintenance.title}")
348
+
349
+ return cancelled
350
+
351
+
352
+ # Global instance
353
+ scheduled_maintenance_service = ScheduledMaintenanceService()
354
+
355
+
356
+ # Convenience functions
357
+ def schedule_maintenance_for_sites(sites: List[CloudflareSite],
358
+ title: str,
359
+ start_time: datetime,
360
+ duration_hours: int = 2,
361
+ **kwargs) -> ScheduledMaintenance:
362
+ """Convenience function to schedule maintenance for sites."""
363
+ return scheduled_maintenance_service.create_scheduled_maintenance(
364
+ title=title,
365
+ scheduled_start=start_time,
366
+ estimated_duration=timedelta(hours=duration_hours),
367
+ sites=sites,
368
+ **kwargs
369
+ )
370
+
371
+
372
+ def process_scheduled_maintenances() -> Dict[str, Any]:
373
+ """Process all due and overdue maintenances."""
374
+ due_results = scheduled_maintenance_service.process_due_maintenances()
375
+ overdue_results = scheduled_maintenance_service.process_overdue_maintenances()
376
+
377
+ return {
378
+ 'due_maintenances': due_results,
379
+ 'overdue_maintenances': overdue_results,
380
+ 'total_processed': due_results['processed'] + overdue_results['processed']
381
+ }