django-cfg 1.2.17__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.17.dist-info → django_cfg-1.2.18.dist-info}/METADATA +52 -1
- {django_cfg-1.2.17.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.17.dist-info → django_cfg-1.2.18.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.17.dist-info → django_cfg-1.2.18.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.17.dist-info → django_cfg-1.2.18.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,241 @@
|
|
1
|
+
"""
|
2
|
+
Management command for processing scheduled maintenance events.
|
3
|
+
|
4
|
+
Handles automatic start/stop of scheduled maintenance windows.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from django.core.management.base import BaseCommand
|
8
|
+
from django.utils import timezone
|
9
|
+
from datetime import timedelta
|
10
|
+
|
11
|
+
from ...services.scheduled_maintenance_service import scheduled_maintenance_service
|
12
|
+
from ...models import ScheduledMaintenance
|
13
|
+
|
14
|
+
|
15
|
+
class Command(BaseCommand):
|
16
|
+
"""Process scheduled maintenance events."""
|
17
|
+
|
18
|
+
help = 'Process scheduled maintenance events (start due, complete overdue)'
|
19
|
+
|
20
|
+
def add_arguments(self, parser):
|
21
|
+
"""Add command arguments."""
|
22
|
+
parser.add_argument(
|
23
|
+
'--dry-run',
|
24
|
+
action='store_true',
|
25
|
+
help='Show what would be processed without making changes'
|
26
|
+
)
|
27
|
+
|
28
|
+
parser.add_argument(
|
29
|
+
'--upcoming',
|
30
|
+
type=int,
|
31
|
+
default=24,
|
32
|
+
help='Show upcoming maintenances within N hours (default: 24)'
|
33
|
+
)
|
34
|
+
|
35
|
+
parser.add_argument(
|
36
|
+
'--verbose',
|
37
|
+
action='store_true',
|
38
|
+
help='Enable verbose output'
|
39
|
+
)
|
40
|
+
|
41
|
+
def handle(self, *args, **options):
|
42
|
+
"""Handle the command execution."""
|
43
|
+
verbosity = 2 if options['verbose'] else 1
|
44
|
+
|
45
|
+
if options['dry_run']:
|
46
|
+
self.stdout.write(
|
47
|
+
self.style.WARNING('🔍 DRY RUN MODE - No changes will be made')
|
48
|
+
)
|
49
|
+
|
50
|
+
# Show current status
|
51
|
+
self._show_current_status(verbosity)
|
52
|
+
|
53
|
+
if not options['dry_run']:
|
54
|
+
# Process due maintenances
|
55
|
+
self._process_due_maintenances(verbosity)
|
56
|
+
|
57
|
+
# Process overdue maintenances
|
58
|
+
self._process_overdue_maintenances(verbosity)
|
59
|
+
else:
|
60
|
+
# Show what would be processed
|
61
|
+
self._show_dry_run_info()
|
62
|
+
|
63
|
+
# Show upcoming maintenances
|
64
|
+
self._show_upcoming_maintenances(options['upcoming'], verbosity)
|
65
|
+
|
66
|
+
def _show_current_status(self, verbosity: int):
|
67
|
+
"""Show current maintenance status."""
|
68
|
+
self.stdout.write("\n" + "="*60)
|
69
|
+
self.stdout.write(self.style.SUCCESS("📊 SCHEDULED MAINTENANCE STATUS"))
|
70
|
+
self.stdout.write("="*60)
|
71
|
+
|
72
|
+
# Active maintenances
|
73
|
+
active = scheduled_maintenance_service.get_active_maintenances()
|
74
|
+
if active:
|
75
|
+
self.stdout.write(f"\n🔧 Active Maintenances ({len(active)}):")
|
76
|
+
for maintenance in active:
|
77
|
+
time_left = ""
|
78
|
+
if maintenance['time_until_end']:
|
79
|
+
hours = int(maintenance['time_until_end'] // 3600)
|
80
|
+
minutes = int((maintenance['time_until_end'] % 3600) // 60)
|
81
|
+
time_left = f" ({hours}h {minutes}m remaining)"
|
82
|
+
|
83
|
+
overdue = " ⚠️ OVERDUE" if maintenance['is_overdue'] else ""
|
84
|
+
|
85
|
+
self.stdout.write(
|
86
|
+
f" • {maintenance['title']}{time_left}{overdue}"
|
87
|
+
)
|
88
|
+
if verbosity >= 2:
|
89
|
+
self.stdout.write(f" Sites: {maintenance['sites_count']}, Priority: {maintenance['priority']}")
|
90
|
+
else:
|
91
|
+
self.stdout.write("\n✅ No active maintenances")
|
92
|
+
|
93
|
+
# Due maintenances
|
94
|
+
due = ScheduledMaintenance.get_due_maintenances()
|
95
|
+
if due:
|
96
|
+
self.stdout.write(f"\n⏰ Due to Start ({due.count()}):")
|
97
|
+
for maintenance in due:
|
98
|
+
auto_text = " (auto)" if maintenance.auto_enable else " (manual)"
|
99
|
+
self.stdout.write(f" • {maintenance.title}{auto_text}")
|
100
|
+
if verbosity >= 2:
|
101
|
+
self.stdout.write(f" Sites: {maintenance.affected_sites_count}, Scheduled: {maintenance.scheduled_start}")
|
102
|
+
|
103
|
+
# Overdue maintenances
|
104
|
+
overdue = ScheduledMaintenance.get_overdue_maintenances()
|
105
|
+
if overdue:
|
106
|
+
self.stdout.write(f"\n⚠️ Overdue to Complete ({overdue.count()}):")
|
107
|
+
for maintenance in overdue:
|
108
|
+
auto_text = " (auto)" if maintenance.auto_disable else " (manual)"
|
109
|
+
self.stdout.write(f" • {maintenance.title}{auto_text}")
|
110
|
+
if verbosity >= 2:
|
111
|
+
overdue_time = timezone.now() - maintenance.scheduled_end
|
112
|
+
hours = int(overdue_time.total_seconds() // 3600)
|
113
|
+
minutes = int((overdue_time.total_seconds() % 3600) // 60)
|
114
|
+
self.stdout.write(f" Overdue by: {hours}h {minutes}m")
|
115
|
+
|
116
|
+
def _process_due_maintenances(self, verbosity: int):
|
117
|
+
"""Process maintenances that are due to start."""
|
118
|
+
self.stdout.write("\n" + "-"*40)
|
119
|
+
self.stdout.write("🚀 Processing Due Maintenances")
|
120
|
+
self.stdout.write("-"*40)
|
121
|
+
|
122
|
+
results = scheduled_maintenance_service.process_due_maintenances()
|
123
|
+
|
124
|
+
if results['processed'] == 0:
|
125
|
+
self.stdout.write("✅ No due maintenances to process")
|
126
|
+
return
|
127
|
+
|
128
|
+
self.stdout.write(f"📊 Processed: {results['processed']}")
|
129
|
+
self.stdout.write(f"✅ Successful: {results['successful']}")
|
130
|
+
|
131
|
+
if results['failed'] > 0:
|
132
|
+
self.stdout.write(f"❌ Failed: {results['failed']}")
|
133
|
+
|
134
|
+
# Show details
|
135
|
+
if verbosity >= 2 and results['details']:
|
136
|
+
self.stdout.write("\n📋 Details:")
|
137
|
+
for detail in results['details']:
|
138
|
+
status = "✅" if detail['success'] else "❌"
|
139
|
+
self.stdout.write(f" {status} {detail['title']}")
|
140
|
+
if detail.get('sites_affected'):
|
141
|
+
self.stdout.write(f" Sites affected: {detail['sites_affected']}")
|
142
|
+
if detail.get('error'):
|
143
|
+
self.stdout.write(f" Error: {detail['error']}")
|
144
|
+
|
145
|
+
def _process_overdue_maintenances(self, verbosity: int):
|
146
|
+
"""Process maintenances that are overdue to complete."""
|
147
|
+
self.stdout.write("\n" + "-"*40)
|
148
|
+
self.stdout.write("🏁 Processing Overdue Maintenances")
|
149
|
+
self.stdout.write("-"*40)
|
150
|
+
|
151
|
+
results = scheduled_maintenance_service.process_overdue_maintenances()
|
152
|
+
|
153
|
+
if results['processed'] == 0:
|
154
|
+
self.stdout.write("✅ No overdue maintenances to process")
|
155
|
+
return
|
156
|
+
|
157
|
+
self.stdout.write(f"📊 Processed: {results['processed']}")
|
158
|
+
self.stdout.write(f"✅ Successful: {results['successful']}")
|
159
|
+
|
160
|
+
if results['failed'] > 0:
|
161
|
+
self.stdout.write(f"❌ Failed: {results['failed']}")
|
162
|
+
|
163
|
+
# Show details
|
164
|
+
if verbosity >= 2 and results['details']:
|
165
|
+
self.stdout.write("\n📋 Details:")
|
166
|
+
for detail in results['details']:
|
167
|
+
status = "✅" if detail['success'] else "❌"
|
168
|
+
self.stdout.write(f" {status} {detail['title']}")
|
169
|
+
if detail.get('sites_affected'):
|
170
|
+
self.stdout.write(f" Sites affected: {detail['sites_affected']}")
|
171
|
+
if detail.get('actual_duration'):
|
172
|
+
duration_hours = detail['actual_duration'] / 3600
|
173
|
+
self.stdout.write(f" Duration: {duration_hours:.1f}h")
|
174
|
+
if detail.get('error'):
|
175
|
+
self.stdout.write(f" Error: {detail['error']}")
|
176
|
+
|
177
|
+
def _show_dry_run_info(self):
|
178
|
+
"""Show what would be processed in dry run mode."""
|
179
|
+
self.stdout.write("\n" + "-"*40)
|
180
|
+
self.stdout.write("🔍 Dry Run - What Would Be Processed")
|
181
|
+
self.stdout.write("-"*40)
|
182
|
+
|
183
|
+
# Due maintenances
|
184
|
+
due = ScheduledMaintenance.get_due_maintenances().filter(auto_enable=True)
|
185
|
+
if due:
|
186
|
+
self.stdout.write(f"\n🚀 Would start {due.count()} maintenances:")
|
187
|
+
for maintenance in due:
|
188
|
+
self.stdout.write(f" • {maintenance.title} ({maintenance.affected_sites_count} sites)")
|
189
|
+
|
190
|
+
# Overdue maintenances
|
191
|
+
overdue = ScheduledMaintenance.get_overdue_maintenances().filter(auto_disable=True)
|
192
|
+
if overdue:
|
193
|
+
self.stdout.write(f"\n🏁 Would complete {overdue.count()} maintenances:")
|
194
|
+
for maintenance in overdue:
|
195
|
+
self.stdout.write(f" • {maintenance.title} ({maintenance.affected_sites_count} sites)")
|
196
|
+
|
197
|
+
if not due and not overdue:
|
198
|
+
self.stdout.write("✅ Nothing to process")
|
199
|
+
|
200
|
+
def _show_upcoming_maintenances(self, hours: int, verbosity: int):
|
201
|
+
"""Show upcoming maintenance events."""
|
202
|
+
upcoming = scheduled_maintenance_service.get_upcoming_maintenances(hours=hours)
|
203
|
+
|
204
|
+
if not upcoming:
|
205
|
+
self.stdout.write(f"\n📅 No maintenances scheduled in next {hours} hours")
|
206
|
+
return
|
207
|
+
|
208
|
+
self.stdout.write(f"\n📅 Upcoming Maintenances (next {hours}h):")
|
209
|
+
|
210
|
+
for maintenance in upcoming:
|
211
|
+
# Calculate time until start
|
212
|
+
time_until = maintenance['time_until_start']
|
213
|
+
if time_until:
|
214
|
+
hours_until = int(time_until // 3600)
|
215
|
+
minutes_until = int((time_until % 3600) // 60)
|
216
|
+
time_str = f"in {hours_until}h {minutes_until}m"
|
217
|
+
else:
|
218
|
+
time_str = "now"
|
219
|
+
|
220
|
+
priority_emoji = {
|
221
|
+
'low': '🟢',
|
222
|
+
'normal': '🟡',
|
223
|
+
'high': '🟠',
|
224
|
+
'critical': '🔴'
|
225
|
+
}.get(maintenance['priority'], '⚪')
|
226
|
+
|
227
|
+
auto_text = " (auto)" if maintenance['auto_enable'] else " (manual)"
|
228
|
+
|
229
|
+
self.stdout.write(
|
230
|
+
f" {priority_emoji} {maintenance['title']} - {time_str}{auto_text}"
|
231
|
+
)
|
232
|
+
|
233
|
+
if verbosity >= 2:
|
234
|
+
start_time = maintenance['scheduled_start'][:16].replace('T', ' ')
|
235
|
+
duration_hours = maintenance['estimated_duration'] / 3600
|
236
|
+
self.stdout.write(
|
237
|
+
f" Start: {start_time}, Duration: {duration_hours:.1f}h, Sites: {maintenance['sites_count']}"
|
238
|
+
)
|
239
|
+
|
240
|
+
self.stdout.write(f"\n💡 Run with --dry-run to see what would be processed")
|
241
|
+
self.stdout.write(f"⏰ Current time: {timezone.now().strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
@@ -1,168 +1,209 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Management command for syncing sites with Cloudflare zones.
|
3
3
|
|
4
|
-
|
4
|
+
Automatically discovers and syncs Cloudflare zones with Django models.
|
5
5
|
"""
|
6
6
|
|
7
7
|
from django.core.management.base import BaseCommand, CommandError
|
8
|
-
from django.contrib.auth import get_user_model
|
9
|
-
from django.db import transaction
|
10
8
|
from django.utils import timezone
|
11
|
-
from typing import
|
12
|
-
import logging
|
9
|
+
from typing import Optional
|
13
10
|
|
14
|
-
from ...models import
|
15
|
-
from ...services import
|
16
|
-
|
17
|
-
User = get_user_model()
|
18
|
-
logger = logging.getLogger(__name__)
|
11
|
+
from ...models import CloudflareApiKey
|
12
|
+
from ...services.site_sync_service import SiteSyncService
|
19
13
|
|
20
14
|
|
21
15
|
class Command(BaseCommand):
|
22
|
-
"""
|
16
|
+
"""Sync sites with Cloudflare zones."""
|
23
17
|
|
24
|
-
help = '
|
18
|
+
help = 'Sync CloudflareSite models with actual Cloudflare zones'
|
25
19
|
|
26
20
|
def add_arguments(self, parser):
|
27
21
|
"""Add command arguments."""
|
28
22
|
parser.add_argument(
|
29
|
-
'--
|
23
|
+
'--api-key',
|
30
24
|
type=str,
|
31
|
-
help='
|
32
|
-
required=True
|
33
|
-
)
|
34
|
-
|
35
|
-
parser.add_argument(
|
36
|
-
'--api-token',
|
37
|
-
type=str,
|
38
|
-
help='Cloudflare API token (if not provided, will try to get from user config)',
|
25
|
+
help='Name of specific API key to sync (default: all active keys)'
|
39
26
|
)
|
40
27
|
|
41
28
|
parser.add_argument(
|
42
29
|
'--dry-run',
|
43
30
|
action='store_true',
|
44
|
-
help='Show what would be
|
31
|
+
help='Show what would be changed without making changes'
|
45
32
|
)
|
46
33
|
|
47
34
|
parser.add_argument(
|
48
|
-
'--force',
|
35
|
+
'--force-update',
|
49
36
|
action='store_true',
|
50
|
-
help='
|
51
|
-
)
|
52
|
-
|
53
|
-
parser.add_argument(
|
54
|
-
'--environment',
|
55
|
-
type=str,
|
56
|
-
choices=['production', 'staging', 'development', 'testing'],
|
57
|
-
default='production',
|
58
|
-
help='Default environment for new sites (default: production)',
|
59
|
-
)
|
60
|
-
|
61
|
-
parser.add_argument(
|
62
|
-
'--project',
|
63
|
-
type=str,
|
64
|
-
help='Default project name for new sites',
|
65
|
-
)
|
66
|
-
|
67
|
-
parser.add_argument(
|
68
|
-
'--tags',
|
69
|
-
type=str,
|
70
|
-
nargs='*',
|
71
|
-
help='Default tags for new sites (space-separated)',
|
37
|
+
help='Update existing sites even if they haven\'t changed'
|
72
38
|
)
|
73
39
|
|
74
40
|
parser.add_argument(
|
75
41
|
'--verbose',
|
76
42
|
action='store_true',
|
77
|
-
help='Enable verbose output'
|
43
|
+
help='Enable verbose output'
|
78
44
|
)
|
79
45
|
|
80
46
|
def handle(self, *args, **options):
|
81
|
-
"""
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
self.stdout.write(self.style.ERROR(f"❌ {error}"))
|
88
|
-
raise CommandError("Invalid parameters")
|
89
|
-
|
90
|
-
# Show warnings
|
91
|
-
for warning in validation['warnings']:
|
92
|
-
self.stdout.write(self.style.WARNING(f"⚠️ {warning}"))
|
93
|
-
|
94
|
-
# Initialize service
|
95
|
-
sync_service = SyncCommandService.create_from_params(
|
96
|
-
api_token=validation['api_token']
|
47
|
+
"""Handle the command execution."""
|
48
|
+
verbosity = 2 if options['verbose'] else 1
|
49
|
+
|
50
|
+
if options['dry_run']:
|
51
|
+
self.stdout.write(
|
52
|
+
self.style.WARNING('🔍 DRY RUN MODE - No changes will be made')
|
97
53
|
)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
54
|
+
|
55
|
+
# Get API keys to sync
|
56
|
+
if options['api_key']:
|
57
|
+
try:
|
58
|
+
api_keys = [CloudflareApiKey.objects.get(
|
59
|
+
name=options['api_key'],
|
60
|
+
is_active=True
|
61
|
+
)]
|
62
|
+
self.stdout.write(f"📡 Syncing specific API key: {options['api_key']}")
|
63
|
+
except CloudflareApiKey.DoesNotExist:
|
64
|
+
raise CommandError(f"API key '{options['api_key']}' not found or inactive")
|
65
|
+
else:
|
66
|
+
api_keys = CloudflareApiKey.objects.filter(is_active=True)
|
67
|
+
self.stdout.write(f"📡 Syncing all {api_keys.count()} active API keys")
|
68
|
+
|
69
|
+
if not api_keys:
|
70
|
+
self.stdout.write(
|
71
|
+
self.style.WARNING('⚠️ No active API keys found')
|
109
72
|
)
|
73
|
+
return
|
74
|
+
|
75
|
+
# Sync each API key
|
76
|
+
total_stats = {
|
77
|
+
'discovered': 0,
|
78
|
+
'created': 0,
|
79
|
+
'updated': 0,
|
80
|
+
'skipped': 0,
|
81
|
+
'errors': 0
|
82
|
+
}
|
83
|
+
|
84
|
+
for api_key in api_keys:
|
85
|
+
self.stdout.write(f"\n🔑 Processing API key: {api_key.name}")
|
110
86
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
environment=options['environment']
|
131
|
-
)
|
132
|
-
|
133
|
-
# Old methods removed - logic moved to SyncCommandService
|
134
|
-
|
135
|
-
def _display_results(self, stats: Dict[str, int], dry_run: bool) -> None:
|
136
|
-
"""Display synchronization results."""
|
137
|
-
mode = "DRY RUN" if dry_run else "COMPLETED"
|
87
|
+
try:
|
88
|
+
sync_service = SiteSyncService(api_key)
|
89
|
+
stats = sync_service.sync_zones(
|
90
|
+
force_update=options['force_update'],
|
91
|
+
dry_run=options['dry_run']
|
92
|
+
)
|
93
|
+
|
94
|
+
# Update totals
|
95
|
+
for key in total_stats:
|
96
|
+
total_stats[key] += stats[key]
|
97
|
+
|
98
|
+
# Display results for this API key
|
99
|
+
self._display_api_key_results(api_key.name, stats, verbosity)
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
self.stdout.write(
|
103
|
+
self.style.ERROR(f'❌ Failed to sync {api_key.name}: {e}')
|
104
|
+
)
|
105
|
+
total_stats['errors'] += 1
|
138
106
|
|
139
|
-
|
140
|
-
self.
|
107
|
+
# Display overall summary
|
108
|
+
self._display_summary(total_stats, options['dry_run'])
|
109
|
+
|
110
|
+
def _display_api_key_results(self, api_key_name: str, stats: dict, verbosity: int):
|
111
|
+
"""Display results for a single API key."""
|
112
|
+
if stats['errors'] > 0:
|
113
|
+
self.stdout.write(
|
114
|
+
self.style.ERROR(
|
115
|
+
f" ❌ {stats['errors']} errors occurred"
|
116
|
+
)
|
117
|
+
)
|
141
118
|
|
142
119
|
if stats['created'] > 0:
|
120
|
+
action = "Would create" if stats.get('dry_run') else "Created"
|
143
121
|
self.stdout.write(
|
144
|
-
self.style.SUCCESS(
|
122
|
+
self.style.SUCCESS(
|
123
|
+
f" ✅ {action} {stats['created']} new sites"
|
124
|
+
)
|
145
125
|
)
|
146
126
|
|
147
127
|
if stats['updated'] > 0:
|
128
|
+
action = "Would update" if stats.get('dry_run') else "Updated"
|
148
129
|
self.stdout.write(
|
149
|
-
self.style.SUCCESS(
|
130
|
+
self.style.SUCCESS(
|
131
|
+
f" 🔄 {action} {stats['updated']} existing sites"
|
132
|
+
)
|
150
133
|
)
|
151
134
|
|
152
135
|
if stats['skipped'] > 0:
|
153
136
|
self.stdout.write(
|
154
|
-
self.style.WARNING(
|
137
|
+
self.style.WARNING(
|
138
|
+
f" ⏭️ Skipped {stats['skipped']} sites (no changes)"
|
139
|
+
)
|
155
140
|
)
|
156
141
|
|
142
|
+
# Verbose output - show individual sites
|
143
|
+
if verbosity >= 2 and stats.get('sites'):
|
144
|
+
self.stdout.write(" 📋 Site details:")
|
145
|
+
for site_info in stats['sites']:
|
146
|
+
if site_info['action'] == 'created':
|
147
|
+
self.stdout.write(f" ➕ Created: {site_info['domain']}")
|
148
|
+
elif site_info['action'] == 'updated':
|
149
|
+
self.stdout.write(f" 🔄 Updated: {site_info['domain']}")
|
150
|
+
elif site_info['action'] == 'would_create':
|
151
|
+
self.stdout.write(f" ➕ Would create: {site_info['domain']}")
|
152
|
+
elif site_info['action'] == 'would_update':
|
153
|
+
self.stdout.write(f" 🔄 Would update: {site_info['domain']}")
|
154
|
+
if 'changes' in site_info:
|
155
|
+
for field, change in site_info['changes'].items():
|
156
|
+
self.stdout.write(
|
157
|
+
f" • {field}: {change['old']} → {change['new']}"
|
158
|
+
)
|
159
|
+
elif site_info['action'] == 'error':
|
160
|
+
self.stdout.write(
|
161
|
+
self.style.ERROR(
|
162
|
+
f" ❌ Error: {site_info['domain']} - {site_info['error']}"
|
163
|
+
)
|
164
|
+
)
|
165
|
+
|
166
|
+
def _display_summary(self, stats: dict, dry_run: bool):
|
167
|
+
"""Display overall summary."""
|
168
|
+
self.stdout.write("\n" + "="*50)
|
169
|
+
self.stdout.write(
|
170
|
+
self.style.SUCCESS("📊 SYNC SUMMARY") if not dry_run
|
171
|
+
else self.style.WARNING("📊 DRY RUN SUMMARY")
|
172
|
+
)
|
173
|
+
self.stdout.write("="*50)
|
174
|
+
|
175
|
+
if stats['discovered'] > 0:
|
176
|
+
self.stdout.write(f"🔍 Zones discovered: {stats['discovered']}")
|
177
|
+
|
178
|
+
if stats['created'] > 0:
|
179
|
+
action = "Would be created" if dry_run else "Created"
|
180
|
+
self.stdout.write(
|
181
|
+
self.style.SUCCESS(f"✅ Sites {action.lower()}: {stats['created']}")
|
182
|
+
)
|
183
|
+
|
184
|
+
if stats['updated'] > 0:
|
185
|
+
action = "Would be updated" if dry_run else "Updated"
|
186
|
+
self.stdout.write(
|
187
|
+
self.style.SUCCESS(f"🔄 Sites {action.lower()}: {stats['updated']}")
|
188
|
+
)
|
189
|
+
|
190
|
+
if stats['skipped'] > 0:
|
191
|
+
self.stdout.write(f"⏭️ Sites skipped: {stats['skipped']}")
|
192
|
+
|
157
193
|
if stats['errors'] > 0:
|
158
194
|
self.stdout.write(
|
159
|
-
self.style.ERROR(f"❌ Errors: {stats['errors']}
|
195
|
+
self.style.ERROR(f"❌ Errors: {stats['errors']}")
|
160
196
|
)
|
161
197
|
|
162
|
-
|
163
|
-
|
198
|
+
total_processed = stats['created'] + stats['updated'] + stats['skipped']
|
199
|
+
if total_processed > 0:
|
200
|
+
self.stdout.write(f"\n📈 Total sites processed: {total_processed}")
|
164
201
|
|
165
|
-
if dry_run:
|
202
|
+
if dry_run and (stats['created'] > 0 or stats['updated'] > 0):
|
166
203
|
self.stdout.write(
|
167
|
-
self.style.
|
204
|
+
self.style.WARNING(
|
205
|
+
"\n💡 Run without --dry-run to apply these changes"
|
206
|
+
)
|
168
207
|
)
|
208
|
+
|
209
|
+
self.stdout.write(f"⏰ Completed at: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
@@ -1,20 +1,15 @@
|
|
1
1
|
"""
|
2
|
-
Custom managers for maintenance
|
2
|
+
Custom managers for maintenance models.
|
3
3
|
|
4
|
-
|
5
|
-
for CloudflareSite, MaintenanceEvent, and related models.
|
4
|
+
Simplified managers with useful query methods.
|
6
5
|
"""
|
7
6
|
|
8
|
-
from .
|
9
|
-
from .
|
10
|
-
from .monitoring import MonitoringTargetManager
|
11
|
-
from .deployments import CloudflareDeploymentManager
|
7
|
+
from .cloudflare_site_manager import CloudflareSiteManager, CloudflareSiteQuerySet
|
8
|
+
from .maintenance_log_manager import MaintenanceLogManager, MaintenanceLogQuerySet
|
12
9
|
|
13
10
|
__all__ = [
|
14
11
|
'CloudflareSiteManager',
|
15
|
-
'
|
16
|
-
'
|
17
|
-
'
|
18
|
-
'MonitoringTargetManager',
|
19
|
-
'CloudflareDeploymentManager',
|
12
|
+
'CloudflareSiteQuerySet',
|
13
|
+
'MaintenanceLogManager',
|
14
|
+
'MaintenanceLogQuerySet',
|
20
15
|
]
|