django-cfg 1.2.15__py3-none-any.whl → 1.2.17__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 +34 -3
- django_cfg/core/generation.py +15 -10
- django_cfg/models/cloudflare.py +316 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +1 -1
- django_cfg/modules/base.py +12 -5
- django_cfg/modules/django_unfold/dashboard.py +16 -1
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/METADATA +2 -1
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/RECORD +61 -13
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,375 @@
|
|
1
|
+
"""
|
2
|
+
Multi-site maintenance management command.
|
3
|
+
|
4
|
+
Provides CLI interface for managing maintenance mode across multiple sites
|
5
|
+
with ORM-like syntax and bulk operations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
from django.core.management.base import BaseCommand, CommandError
|
10
|
+
from django.contrib.auth import get_user_model
|
11
|
+
from django.utils import timezone
|
12
|
+
from typing import List, Optional
|
13
|
+
|
14
|
+
from django_cfg.apps.maintenance.services import MaintenanceManager
|
15
|
+
from django_cfg.apps.maintenance.models import CloudflareSite
|
16
|
+
|
17
|
+
User = get_user_model()
|
18
|
+
|
19
|
+
|
20
|
+
class Command(BaseCommand):
|
21
|
+
"""
|
22
|
+
Multi-site maintenance management command.
|
23
|
+
|
24
|
+
Examples:
|
25
|
+
# Enable maintenance for all production sites
|
26
|
+
python manage.py maintenance enable --environment production
|
27
|
+
|
28
|
+
# Disable maintenance for specific project
|
29
|
+
python manage.py maintenance disable --project myproject
|
30
|
+
|
31
|
+
# Check status of all sites
|
32
|
+
python manage.py maintenance status
|
33
|
+
|
34
|
+
# Enable maintenance with custom message
|
35
|
+
python manage.py maintenance enable --domain example.com --message "Upgrading database"
|
36
|
+
|
37
|
+
# Bulk operations with filters
|
38
|
+
python manage.py maintenance enable --tag critical --reason "Security patch"
|
39
|
+
"""
|
40
|
+
|
41
|
+
help = 'Manage maintenance mode for multiple Cloudflare sites'
|
42
|
+
|
43
|
+
def add_arguments(self, parser):
|
44
|
+
"""Add command arguments."""
|
45
|
+
# Main action
|
46
|
+
parser.add_argument(
|
47
|
+
'action',
|
48
|
+
choices=['enable', 'disable', 'status', 'list', 'discover'],
|
49
|
+
help='Action to perform'
|
50
|
+
)
|
51
|
+
|
52
|
+
# Site filters
|
53
|
+
parser.add_argument(
|
54
|
+
'--domain',
|
55
|
+
help='Specific domain to target'
|
56
|
+
)
|
57
|
+
parser.add_argument(
|
58
|
+
'--environment',
|
59
|
+
choices=['production', 'staging', 'development', 'testing'],
|
60
|
+
help='Filter by environment'
|
61
|
+
)
|
62
|
+
parser.add_argument(
|
63
|
+
'--project',
|
64
|
+
help='Filter by project name'
|
65
|
+
)
|
66
|
+
parser.add_argument(
|
67
|
+
'--tag',
|
68
|
+
help='Filter by tag'
|
69
|
+
)
|
70
|
+
parser.add_argument(
|
71
|
+
'--owner',
|
72
|
+
help='Filter by owner username'
|
73
|
+
)
|
74
|
+
|
75
|
+
# Maintenance options
|
76
|
+
parser.add_argument(
|
77
|
+
'--reason',
|
78
|
+
default='Manual maintenance via CLI',
|
79
|
+
help='Reason for maintenance'
|
80
|
+
)
|
81
|
+
parser.add_argument(
|
82
|
+
'--message',
|
83
|
+
help='Custom maintenance message'
|
84
|
+
)
|
85
|
+
|
86
|
+
# Operation options
|
87
|
+
parser.add_argument(
|
88
|
+
'--dry-run',
|
89
|
+
action='store_true',
|
90
|
+
help='Show what would be done without actually doing it'
|
91
|
+
)
|
92
|
+
parser.add_argument(
|
93
|
+
'--force',
|
94
|
+
action='store_true',
|
95
|
+
help='Force operation without confirmation'
|
96
|
+
)
|
97
|
+
|
98
|
+
# Discovery options
|
99
|
+
parser.add_argument(
|
100
|
+
'--api-token',
|
101
|
+
help='Cloudflare API token for site discovery'
|
102
|
+
)
|
103
|
+
|
104
|
+
# Output options
|
105
|
+
parser.add_argument(
|
106
|
+
'--format',
|
107
|
+
choices=['table', 'json', 'csv'],
|
108
|
+
default='table',
|
109
|
+
help='Output format'
|
110
|
+
)
|
111
|
+
parser.add_argument(
|
112
|
+
'--verbose',
|
113
|
+
action='store_true',
|
114
|
+
help='Verbose output'
|
115
|
+
)
|
116
|
+
|
117
|
+
def handle(self, *args, **options):
|
118
|
+
"""Handle command execution."""
|
119
|
+
self.options = options
|
120
|
+
self.verbosity = options.get('verbosity', 1)
|
121
|
+
|
122
|
+
try:
|
123
|
+
# Get user for operations
|
124
|
+
user = self._get_user(options.get('owner'))
|
125
|
+
|
126
|
+
# Execute action
|
127
|
+
if options['action'] == 'enable':
|
128
|
+
asyncio.run(self._handle_enable(user))
|
129
|
+
elif options['action'] == 'disable':
|
130
|
+
asyncio.run(self._handle_disable(user))
|
131
|
+
elif options['action'] == 'status':
|
132
|
+
asyncio.run(self._handle_status(user))
|
133
|
+
elif options['action'] == 'list':
|
134
|
+
self._handle_list(user)
|
135
|
+
elif options['action'] == 'discover':
|
136
|
+
asyncio.run(self._handle_discover(user))
|
137
|
+
|
138
|
+
except Exception as e:
|
139
|
+
raise CommandError(f"Command failed: {str(e)}")
|
140
|
+
|
141
|
+
def _get_user(self, username: Optional[str] = None) -> User:
|
142
|
+
"""Get user for operations."""
|
143
|
+
if username:
|
144
|
+
try:
|
145
|
+
return User.objects.get(username=username)
|
146
|
+
except User.DoesNotExist:
|
147
|
+
raise CommandError(f"User '{username}' not found")
|
148
|
+
else:
|
149
|
+
# Use first superuser if no user specified
|
150
|
+
superuser = User.objects.filter(is_superuser=True).first()
|
151
|
+
if not superuser:
|
152
|
+
raise CommandError("No superuser found. Please specify --owner or create a superuser.")
|
153
|
+
return superuser
|
154
|
+
|
155
|
+
def _get_sites_queryset(self, user: User):
|
156
|
+
"""Get filtered sites queryset based on command options."""
|
157
|
+
sites = multi_site_manager.sites(user)
|
158
|
+
|
159
|
+
# Apply filters
|
160
|
+
if self.options.get('domain'):
|
161
|
+
sites = sites.filter(domain=self.options['domain'])
|
162
|
+
|
163
|
+
if self.options.get('environment'):
|
164
|
+
sites = sites.filter(environment=self.options['environment'])
|
165
|
+
|
166
|
+
if self.options.get('project'):
|
167
|
+
sites = sites.filter(project=self.options['project'])
|
168
|
+
|
169
|
+
if self.options.get('tag'):
|
170
|
+
sites = sites.with_tag(self.options['tag'])
|
171
|
+
|
172
|
+
return sites
|
173
|
+
|
174
|
+
async def _handle_enable(self, user: User):
|
175
|
+
"""Handle enable maintenance action."""
|
176
|
+
sites = self._get_sites_queryset(user)
|
177
|
+
|
178
|
+
if sites.count() == 0:
|
179
|
+
self.stdout.write(self.style.WARNING("No sites match the specified filters."))
|
180
|
+
return
|
181
|
+
|
182
|
+
# Show what will be affected
|
183
|
+
self.stdout.write(f"Will enable maintenance for {sites.count()} sites:")
|
184
|
+
for site in sites.all()[:10]: # Show first 10
|
185
|
+
self.stdout.write(f" - {site.domain} ({site.environment})")
|
186
|
+
|
187
|
+
if sites.count() > 10:
|
188
|
+
self.stdout.write(f" ... and {sites.count() - 10} more sites")
|
189
|
+
|
190
|
+
# Confirm unless forced
|
191
|
+
if not self.options.get('force') and not self.options.get('dry_run'):
|
192
|
+
confirm = input("\nProceed? (y/N): ")
|
193
|
+
if confirm.lower() != 'y':
|
194
|
+
self.stdout.write("Operation cancelled.")
|
195
|
+
return
|
196
|
+
|
197
|
+
# Execute operation
|
198
|
+
result = await sites.enable_maintenance(
|
199
|
+
reason=self.options['reason'],
|
200
|
+
message=self.options.get('message'),
|
201
|
+
user=user,
|
202
|
+
dry_run=self.options.get('dry_run', False)
|
203
|
+
)
|
204
|
+
|
205
|
+
# Display results
|
206
|
+
self._display_bulk_result("Enable Maintenance", result)
|
207
|
+
|
208
|
+
async def _handle_disable(self, user: User):
|
209
|
+
"""Handle disable maintenance action."""
|
210
|
+
sites = self._get_sites_queryset(user).in_maintenance()
|
211
|
+
|
212
|
+
if sites.count() == 0:
|
213
|
+
self.stdout.write(self.style.WARNING("No sites in maintenance match the specified filters."))
|
214
|
+
return
|
215
|
+
|
216
|
+
# Show what will be affected
|
217
|
+
self.stdout.write(f"Will disable maintenance for {sites.count()} sites:")
|
218
|
+
for site in sites.all()[:10]:
|
219
|
+
duration = site.maintenance_duration
|
220
|
+
duration_str = f" ({duration})" if duration else ""
|
221
|
+
self.stdout.write(f" - {site.domain}{duration_str}")
|
222
|
+
|
223
|
+
if sites.count() > 10:
|
224
|
+
self.stdout.write(f" ... and {sites.count() - 10} more sites")
|
225
|
+
|
226
|
+
# Confirm unless forced
|
227
|
+
if not self.options.get('force') and not self.options.get('dry_run'):
|
228
|
+
confirm = input("\nProceed? (y/N): ")
|
229
|
+
if confirm.lower() != 'y':
|
230
|
+
self.stdout.write("Operation cancelled.")
|
231
|
+
return
|
232
|
+
|
233
|
+
# Execute operation
|
234
|
+
result = await sites.disable_maintenance(
|
235
|
+
user=user,
|
236
|
+
dry_run=self.options.get('dry_run', False)
|
237
|
+
)
|
238
|
+
|
239
|
+
# Display results
|
240
|
+
self._display_bulk_result("Disable Maintenance", result)
|
241
|
+
|
242
|
+
async def _handle_status(self, user: User):
|
243
|
+
"""Handle status check action."""
|
244
|
+
sites = self._get_sites_queryset(user)
|
245
|
+
|
246
|
+
if sites.count() == 0:
|
247
|
+
self.stdout.write(self.style.WARNING("No sites match the specified filters."))
|
248
|
+
return
|
249
|
+
|
250
|
+
self.stdout.write(f"Checking status of {sites.count()} sites...")
|
251
|
+
|
252
|
+
result = await sites.check_status()
|
253
|
+
|
254
|
+
# Display status summary
|
255
|
+
self.stdout.write(self.style.SUCCESS(f"\n✅ Status check completed for {result['total']} sites"))
|
256
|
+
|
257
|
+
status_summary = result.get('status_summary', {})
|
258
|
+
if status_summary:
|
259
|
+
self.stdout.write("\nStatus Summary:")
|
260
|
+
for status, count in status_summary.items():
|
261
|
+
self.stdout.write(f" {status.title()}: {count} sites")
|
262
|
+
|
263
|
+
# Display individual site status if verbose
|
264
|
+
if self.options.get('verbose'):
|
265
|
+
self.stdout.write("\nIndividual Site Status:")
|
266
|
+
for site_info in result.get('sites', []):
|
267
|
+
status_icon = {
|
268
|
+
'active': '🟢',
|
269
|
+
'maintenance': '🔧',
|
270
|
+
'offline': '🔴',
|
271
|
+
'unknown': '❓',
|
272
|
+
'error': '❌'
|
273
|
+
}.get(site_info['status'], '❓')
|
274
|
+
|
275
|
+
self.stdout.write(
|
276
|
+
f" {status_icon} {site_info['domain']}: {site_info['status']}"
|
277
|
+
)
|
278
|
+
|
279
|
+
def _handle_list(self, user: User):
|
280
|
+
"""Handle list sites action."""
|
281
|
+
sites = self._get_sites_queryset(user)
|
282
|
+
|
283
|
+
if sites.count() == 0:
|
284
|
+
self.stdout.write(self.style.WARNING("No sites match the specified filters."))
|
285
|
+
return
|
286
|
+
|
287
|
+
# Display sites in table format
|
288
|
+
self.stdout.write(f"\nFound {sites.count()} sites:\n")
|
289
|
+
|
290
|
+
# Header
|
291
|
+
self.stdout.write(
|
292
|
+
f"{'Domain':<30} {'Environment':<12} {'Status':<12} {'Project':<20} {'Maintenance':<12}"
|
293
|
+
)
|
294
|
+
self.stdout.write("-" * 86)
|
295
|
+
|
296
|
+
# Sites
|
297
|
+
for site in sites.all():
|
298
|
+
maintenance_status = "Active" if site.maintenance_active else "Inactive"
|
299
|
+
|
300
|
+
self.stdout.write(
|
301
|
+
f"{site.domain:<30} {site.environment:<12} {site.current_status:<12} "
|
302
|
+
f"{(site.project or 'None'):<20} {maintenance_status:<12}"
|
303
|
+
)
|
304
|
+
|
305
|
+
async def _handle_discover(self, user: User):
|
306
|
+
"""Handle site discovery action."""
|
307
|
+
api_token = self.options.get('api_token')
|
308
|
+
if not api_token:
|
309
|
+
raise CommandError("--api-token is required for site discovery")
|
310
|
+
|
311
|
+
self.stdout.write("Discovering Cloudflare sites...")
|
312
|
+
|
313
|
+
try:
|
314
|
+
discovered_sites = await multi_site_manager.discover_sites(api_token, user)
|
315
|
+
|
316
|
+
if discovered_sites:
|
317
|
+
self.stdout.write(
|
318
|
+
self.style.SUCCESS(f"✅ Discovered {len(discovered_sites)} new sites:")
|
319
|
+
)
|
320
|
+
for site in discovered_sites:
|
321
|
+
self.stdout.write(f" - {site.domain} ({site.environment})")
|
322
|
+
else:
|
323
|
+
self.stdout.write(self.style.WARNING("No new sites discovered."))
|
324
|
+
|
325
|
+
except Exception as e:
|
326
|
+
raise CommandError(f"Site discovery failed: {str(e)}")
|
327
|
+
|
328
|
+
def _display_bulk_result(self, operation: str, result):
|
329
|
+
"""Display bulk operation results."""
|
330
|
+
if result.dry_run:
|
331
|
+
self.stdout.write(self.style.WARNING(f"\n🔍 DRY RUN - {operation}"))
|
332
|
+
self.stdout.write(f"Would affect {len(result.would_affect)} sites:")
|
333
|
+
for domain in result.would_affect[:10]:
|
334
|
+
self.stdout.write(f" - {domain}")
|
335
|
+
if len(result.would_affect) > 10:
|
336
|
+
self.stdout.write(f" ... and {len(result.would_affect) - 10} more")
|
337
|
+
return
|
338
|
+
|
339
|
+
# Real operation results
|
340
|
+
self.stdout.write(f"\n{operation} Results:")
|
341
|
+
self.stdout.write(f"Total sites: {result.total}")
|
342
|
+
|
343
|
+
if result.successful:
|
344
|
+
self.stdout.write(
|
345
|
+
self.style.SUCCESS(f"✅ Successful: {len(result.successful)} sites")
|
346
|
+
)
|
347
|
+
if self.options.get('verbose'):
|
348
|
+
for domain in result.successful:
|
349
|
+
self.stdout.write(f" ✅ {domain}")
|
350
|
+
|
351
|
+
if result.failed:
|
352
|
+
self.stdout.write(
|
353
|
+
self.style.ERROR(f"❌ Failed: {len(result.failed)} sites")
|
354
|
+
)
|
355
|
+
for failure in result.failed:
|
356
|
+
self.stdout.write(f" ❌ {failure['site']}: {failure['reason']}")
|
357
|
+
|
358
|
+
if result.skipped:
|
359
|
+
self.stdout.write(
|
360
|
+
self.style.WARNING(f"⏭️ Skipped: {len(result.skipped)} sites")
|
361
|
+
)
|
362
|
+
if self.options.get('verbose'):
|
363
|
+
for skip in result.skipped:
|
364
|
+
self.stdout.write(f" ⏭️ {skip['site']}: {skip['reason']}")
|
365
|
+
|
366
|
+
# Success rate
|
367
|
+
success_rate = result.success_rate
|
368
|
+
if success_rate == 100.0:
|
369
|
+
style = self.style.SUCCESS
|
370
|
+
elif success_rate >= 80.0:
|
371
|
+
style = self.style.WARNING
|
372
|
+
else:
|
373
|
+
style = self.style.ERROR
|
374
|
+
|
375
|
+
self.stdout.write(style(f"\nSuccess rate: {success_rate:.1f}%"))
|
@@ -0,0 +1,168 @@
|
|
1
|
+
"""
|
2
|
+
Django management command for synchronizing sites with Cloudflare.
|
3
|
+
|
4
|
+
Fetches zones from Cloudflare API and creates/updates CloudflareSite records.
|
5
|
+
"""
|
6
|
+
|
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
|
+
from django.utils import timezone
|
11
|
+
from typing import Dict, List, Any, Optional
|
12
|
+
import logging
|
13
|
+
|
14
|
+
from ...models import CloudflareSite
|
15
|
+
from ...services import SyncCommandService
|
16
|
+
|
17
|
+
User = get_user_model()
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class Command(BaseCommand):
|
22
|
+
"""Synchronize sites with Cloudflare zones."""
|
23
|
+
|
24
|
+
help = 'Synchronize CloudflareSite records with Cloudflare zones'
|
25
|
+
|
26
|
+
def add_arguments(self, parser):
|
27
|
+
"""Add command arguments."""
|
28
|
+
parser.add_argument(
|
29
|
+
'--user',
|
30
|
+
type=str,
|
31
|
+
help='Username or email of the site owner (required)',
|
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)',
|
39
|
+
)
|
40
|
+
|
41
|
+
parser.add_argument(
|
42
|
+
'--dry-run',
|
43
|
+
action='store_true',
|
44
|
+
help='Show what would be synchronized without making changes',
|
45
|
+
)
|
46
|
+
|
47
|
+
parser.add_argument(
|
48
|
+
'--force',
|
49
|
+
action='store_true',
|
50
|
+
help='Force update existing sites (overwrite local changes)',
|
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)',
|
72
|
+
)
|
73
|
+
|
74
|
+
parser.add_argument(
|
75
|
+
'--verbose',
|
76
|
+
action='store_true',
|
77
|
+
help='Enable verbose output',
|
78
|
+
)
|
79
|
+
|
80
|
+
def handle(self, *args, **options):
|
81
|
+
"""Execute the command."""
|
82
|
+
try:
|
83
|
+
# Validate parameters
|
84
|
+
validation = self._validate_parameters(options)
|
85
|
+
if not validation['valid']:
|
86
|
+
for error in validation['errors']:
|
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']
|
97
|
+
)
|
98
|
+
|
99
|
+
# Perform sync
|
100
|
+
self.stdout.write("🔍 Syncing sites with Cloudflare...")
|
101
|
+
result = sync_service.sync_user_sites_command(
|
102
|
+
user=validation['user'],
|
103
|
+
dry_run=options['dry_run'],
|
104
|
+
force_update=options['force'],
|
105
|
+
environment=options['environment'],
|
106
|
+
project=options.get('project') or '',
|
107
|
+
tags=options.get('tags') or [],
|
108
|
+
verbose=options['verbose']
|
109
|
+
)
|
110
|
+
|
111
|
+
if result['success']:
|
112
|
+
# Display results
|
113
|
+
self._display_results(result['stats'], options['dry_run'])
|
114
|
+
else:
|
115
|
+
self.stdout.write(self.style.ERROR(f"❌ Sync failed: {result['error']}"))
|
116
|
+
raise CommandError(result['error'])
|
117
|
+
|
118
|
+
except Exception as e:
|
119
|
+
logger.exception("Command failed")
|
120
|
+
raise CommandError(f"Synchronization failed: {e}")
|
121
|
+
|
122
|
+
def _validate_parameters(self, options: Dict[str, Any]) -> Dict[str, Any]:
|
123
|
+
"""Validate command parameters using service."""
|
124
|
+
# Create temporary service for validation
|
125
|
+
temp_service = SyncCommandService.create_from_params("dummy_token")
|
126
|
+
|
127
|
+
return temp_service.validate_sync_parameters(
|
128
|
+
user_identifier=options['user'],
|
129
|
+
api_token=options.get('api_token'),
|
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"
|
138
|
+
|
139
|
+
self.stdout.write(f"\n🎯 Synchronization {mode}")
|
140
|
+
self.stdout.write("=" * 40)
|
141
|
+
|
142
|
+
if stats['created'] > 0:
|
143
|
+
self.stdout.write(
|
144
|
+
self.style.SUCCESS(f"✨ Created: {stats['created']} sites")
|
145
|
+
)
|
146
|
+
|
147
|
+
if stats['updated'] > 0:
|
148
|
+
self.stdout.write(
|
149
|
+
self.style.SUCCESS(f"🔄 Updated: {stats['updated']} sites")
|
150
|
+
)
|
151
|
+
|
152
|
+
if stats['skipped'] > 0:
|
153
|
+
self.stdout.write(
|
154
|
+
self.style.WARNING(f"⏭️ Skipped: {stats['skipped']} sites")
|
155
|
+
)
|
156
|
+
|
157
|
+
if stats['errors'] > 0:
|
158
|
+
self.stdout.write(
|
159
|
+
self.style.ERROR(f"❌ Errors: {stats['errors']} sites")
|
160
|
+
)
|
161
|
+
|
162
|
+
total = sum(stats.values())
|
163
|
+
self.stdout.write(f"\n📊 Total processed: {total} zones")
|
164
|
+
|
165
|
+
if dry_run:
|
166
|
+
self.stdout.write(
|
167
|
+
self.style.NOTICE("\n💡 Run without --dry-run to apply changes")
|
168
|
+
)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""
|
2
|
+
Custom managers for maintenance app models.
|
3
|
+
|
4
|
+
Provides enhanced querying capabilities and business logic methods
|
5
|
+
for CloudflareSite, MaintenanceEvent, and related models.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .sites import CloudflareSiteManager, SiteGroupManager
|
9
|
+
from .events import MaintenanceEventManager, MaintenanceLogManager
|
10
|
+
from .monitoring import MonitoringTargetManager
|
11
|
+
from .deployments import CloudflareDeploymentManager
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
'CloudflareSiteManager',
|
15
|
+
'SiteGroupManager',
|
16
|
+
'MaintenanceEventManager',
|
17
|
+
'MaintenanceLogManager',
|
18
|
+
'MonitoringTargetManager',
|
19
|
+
'CloudflareDeploymentManager',
|
20
|
+
]
|