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
@@ -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
|
+
}
|