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
@@ -1,375 +1,261 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Simplified maintenance management command.
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
Single command instead of 2+ complex commands.
|
5
|
+
Usage: python manage.py maintenance enable/disable/status/sync domain.com
|
6
6
|
"""
|
7
7
|
|
8
|
-
import asyncio
|
9
8
|
from django.core.management.base import BaseCommand, CommandError
|
10
|
-
from django.
|
11
|
-
from
|
12
|
-
from typing import List, Optional
|
9
|
+
from django.db import transaction
|
10
|
+
from typing import Any, Optional
|
13
11
|
|
14
|
-
from
|
15
|
-
from
|
16
|
-
|
17
|
-
User = get_user_model()
|
12
|
+
from ...models import CloudflareSite, MaintenanceLog
|
13
|
+
from ...services import MaintenanceService
|
18
14
|
|
19
15
|
|
20
16
|
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
|
-
"""
|
17
|
+
"""Simple maintenance management command."""
|
40
18
|
|
41
|
-
help = 'Manage maintenance mode for
|
19
|
+
help = 'Manage maintenance mode for Cloudflare sites'
|
42
20
|
|
43
|
-
def add_arguments(self, parser):
|
21
|
+
def add_arguments(self, parser) -> None:
|
44
22
|
"""Add command arguments."""
|
45
|
-
# Main action
|
46
23
|
parser.add_argument(
|
47
24
|
'action',
|
48
|
-
choices=['enable', 'disable', 'status', '
|
25
|
+
choices=['enable', 'disable', 'status', 'sync', 'list'],
|
49
26
|
help='Action to perform'
|
50
27
|
)
|
51
28
|
|
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
29
|
parser.add_argument(
|
63
|
-
'
|
64
|
-
|
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'
|
30
|
+
'domain',
|
31
|
+
nargs='?',
|
32
|
+
help='Domain to operate on (required for enable/disable/status/sync)'
|
73
33
|
)
|
74
34
|
|
75
|
-
# Maintenance options
|
76
35
|
parser.add_argument(
|
77
36
|
'--reason',
|
78
|
-
default='
|
79
|
-
help='Reason for maintenance'
|
80
|
-
)
|
81
|
-
parser.add_argument(
|
82
|
-
'--message',
|
83
|
-
help='Custom maintenance message'
|
37
|
+
default='Maintenance via CLI',
|
38
|
+
help='Reason for enabling maintenance (default: "Maintenance via CLI")'
|
84
39
|
)
|
85
40
|
|
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
41
|
parser.add_argument(
|
93
42
|
'--force',
|
94
43
|
action='store_true',
|
95
|
-
help='
|
44
|
+
help='Skip confirmation prompts'
|
96
45
|
)
|
97
46
|
|
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
47
|
parser.add_argument(
|
112
48
|
'--verbose',
|
113
49
|
action='store_true',
|
114
|
-
help='
|
50
|
+
help='Show detailed output'
|
115
51
|
)
|
116
52
|
|
117
|
-
def handle(self, *args, **options):
|
118
|
-
"""Handle command
|
119
|
-
|
120
|
-
|
53
|
+
def handle(self, *args: Any, **options: Any) -> None:
|
54
|
+
"""Handle the command."""
|
55
|
+
action = options['action']
|
56
|
+
domain = options['domain']
|
57
|
+
reason = options['reason']
|
58
|
+
force = options['force']
|
59
|
+
verbose = options['verbose']
|
60
|
+
|
61
|
+
# Validate arguments
|
62
|
+
if action in ['enable', 'disable', 'status', 'sync'] and not domain:
|
63
|
+
raise CommandError(f"Domain is required for '{action}' action")
|
121
64
|
|
122
65
|
try:
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
elif
|
130
|
-
|
131
|
-
elif
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
asyncio.run(self._handle_discover(user))
|
137
|
-
|
66
|
+
if action == 'list':
|
67
|
+
self._handle_list(verbose)
|
68
|
+
elif action == 'status':
|
69
|
+
self._handle_status(domain, verbose)
|
70
|
+
elif action == 'enable':
|
71
|
+
self._handle_enable(domain, reason, force, verbose)
|
72
|
+
elif action == 'disable':
|
73
|
+
self._handle_disable(domain, force, verbose)
|
74
|
+
elif action == 'sync':
|
75
|
+
self._handle_sync(domain, verbose)
|
76
|
+
|
77
|
+
except CloudflareSite.DoesNotExist:
|
78
|
+
raise CommandError(f"Site '{domain}' not found. Use 'list' action to see available sites.")
|
138
79
|
except Exception as e:
|
139
|
-
raise CommandError(f"
|
80
|
+
raise CommandError(f"Operation failed: {str(e)}")
|
140
81
|
|
141
|
-
def
|
142
|
-
"""
|
143
|
-
|
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'])
|
82
|
+
def _handle_list(self, verbose: bool) -> None:
|
83
|
+
"""List all sites."""
|
84
|
+
sites = CloudflareSite.objects.all().order_by('name')
|
162
85
|
|
163
|
-
if
|
164
|
-
|
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."))
|
86
|
+
if not sites:
|
87
|
+
self.stdout.write(self.style.WARNING("No sites configured"))
|
180
88
|
return
|
181
89
|
|
182
|
-
|
183
|
-
self.stdout.write(
|
184
|
-
for site in sites.all()[:10]: # Show first 10
|
185
|
-
self.stdout.write(f" - {site.domain} ({site.environment})")
|
90
|
+
self.stdout.write(self.style.SUCCESS(f"Found {sites.count()} sites:"))
|
91
|
+
self.stdout.write("")
|
186
92
|
|
187
|
-
|
188
|
-
self.
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
if
|
194
|
-
self.stdout.write("
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
93
|
+
for site in sites:
|
94
|
+
status_style = self.style.WARNING if site.maintenance_active else self.style.SUCCESS
|
95
|
+
status_text = "🔧 MAINTENANCE" if site.maintenance_active else "🟢 ACTIVE"
|
96
|
+
|
97
|
+
self.stdout.write(f" {status_style(status_text)} {site.name} ({site.domain})")
|
98
|
+
|
99
|
+
if verbose:
|
100
|
+
self.stdout.write(f" Zone ID: {site.zone_id}")
|
101
|
+
self.stdout.write(f" Account ID: {site.account_id}")
|
102
|
+
self.stdout.write(f" Created: {site.created_at.strftime('%Y-%m-%d %H:%M')}")
|
103
|
+
if site.last_maintenance_at:
|
104
|
+
self.stdout.write(f" Last Maintenance: {site.last_maintenance_at.strftime('%Y-%m-%d %H:%M')}")
|
105
|
+
|
106
|
+
# Show recent logs
|
107
|
+
recent_logs = site.logs.all()[:3]
|
108
|
+
if recent_logs:
|
109
|
+
self.stdout.write(" Recent logs:")
|
110
|
+
for log in recent_logs:
|
111
|
+
status_emoji = {
|
112
|
+
MaintenanceLog.Status.SUCCESS: "✅",
|
113
|
+
MaintenanceLog.Status.FAILED: "❌",
|
114
|
+
MaintenanceLog.Status.PENDING: "⏳"
|
115
|
+
}.get(log.status, "❓")
|
116
|
+
|
117
|
+
self.stdout.write(f" {status_emoji} {log.get_action_display()} - {log.created_at.strftime('%m-%d %H:%M')}")
|
118
|
+
|
119
|
+
self.stdout.write("")
|
207
120
|
|
208
|
-
|
209
|
-
"""
|
210
|
-
|
121
|
+
def _handle_status(self, domain: str, verbose: bool) -> None:
|
122
|
+
"""Show status for specific site."""
|
123
|
+
site = CloudflareSite.objects.get(domain=domain)
|
211
124
|
|
212
|
-
if
|
213
|
-
|
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}")
|
125
|
+
status_style = self.style.WARNING if site.maintenance_active else self.style.SUCCESS
|
126
|
+
status_text = "🔧 MAINTENANCE ACTIVE" if site.maintenance_active else "🟢 ACTIVE"
|
222
127
|
|
223
|
-
|
224
|
-
|
128
|
+
self.stdout.write(f"Status for {site.name} ({domain}):")
|
129
|
+
self.stdout.write(f" {status_style(status_text)}")
|
225
130
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
131
|
+
if verbose:
|
132
|
+
self.stdout.write(f" Zone ID: {site.zone_id}")
|
133
|
+
self.stdout.write(f" Account ID: {site.account_id}")
|
134
|
+
self.stdout.write(f" Created: {site.created_at.strftime('%Y-%m-%d %H:%M')}")
|
135
|
+
self.stdout.write(f" Updated: {site.updated_at.strftime('%Y-%m-%d %H:%M')}")
|
136
|
+
|
137
|
+
if site.last_maintenance_at:
|
138
|
+
self.stdout.write(f" Last Maintenance: {site.last_maintenance_at.strftime('%Y-%m-%d %H:%M')}")
|
139
|
+
|
140
|
+
# Show recent logs
|
141
|
+
recent_logs = site.logs.all()[:5]
|
142
|
+
if recent_logs:
|
143
|
+
self.stdout.write(" Recent activity:")
|
144
|
+
for log in recent_logs:
|
145
|
+
status_emoji = {
|
146
|
+
MaintenanceLog.Status.SUCCESS: "✅",
|
147
|
+
MaintenanceLog.Status.FAILED: "❌",
|
148
|
+
MaintenanceLog.Status.PENDING: "⏳"
|
149
|
+
}.get(log.status, "❓")
|
150
|
+
|
151
|
+
duration_text = f" ({log.duration_seconds}s)" if log.duration_seconds else ""
|
152
|
+
self.stdout.write(f" {status_emoji} {log.get_action_display()}{duration_text} - {log.created_at.strftime('%Y-%m-%d %H:%M')}")
|
153
|
+
|
154
|
+
if log.error_message and verbose:
|
155
|
+
self.stdout.write(f" Error: {log.error_message[:100]}")
|
241
156
|
|
242
|
-
|
243
|
-
"""
|
244
|
-
|
157
|
+
def _handle_enable(self, domain: str, reason: str, force: bool, verbose: bool) -> None:
|
158
|
+
"""Enable maintenance for site."""
|
159
|
+
site = CloudflareSite.objects.get(domain=domain)
|
245
160
|
|
246
|
-
if
|
247
|
-
self.stdout.write(self.style.WARNING("
|
161
|
+
if site.maintenance_active:
|
162
|
+
self.stdout.write(self.style.WARNING(f"Maintenance is already active for {domain}"))
|
248
163
|
return
|
249
164
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
165
|
+
# Confirmation
|
166
|
+
if not force:
|
167
|
+
confirm = input(f"Enable maintenance for {site.name} ({domain})? [y/N]: ")
|
168
|
+
if confirm.lower() not in ['y', 'yes']:
|
169
|
+
self.stdout.write("Cancelled")
|
170
|
+
return
|
256
171
|
|
257
|
-
|
258
|
-
if
|
259
|
-
self.stdout.write("
|
260
|
-
for status, count in status_summary.items():
|
261
|
-
self.stdout.write(f" {status.title()}: {count} sites")
|
172
|
+
self.stdout.write(f"Enabling maintenance for {domain}...")
|
173
|
+
if verbose:
|
174
|
+
self.stdout.write(f"Reason: {reason}")
|
262
175
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
'unknown': '❓',
|
272
|
-
'error': '❌'
|
273
|
-
}.get(site_info['status'], '❓')
|
176
|
+
try:
|
177
|
+
with transaction.atomic():
|
178
|
+
service = MaintenanceService(site)
|
179
|
+
log_entry = service.enable_maintenance(reason)
|
180
|
+
|
181
|
+
if log_entry.status == MaintenanceLog.Status.SUCCESS:
|
182
|
+
duration_text = f" ({log_entry.duration_seconds}s)" if log_entry.duration_seconds else ""
|
183
|
+
self.stdout.write(self.style.SUCCESS(f"✅ Maintenance enabled successfully{duration_text}"))
|
274
184
|
|
275
|
-
|
276
|
-
|
277
|
-
|
185
|
+
if verbose and log_entry.cloudflare_response:
|
186
|
+
self.stdout.write("Cloudflare response:")
|
187
|
+
import json
|
188
|
+
self.stdout.write(json.dumps(log_entry.cloudflare_response, indent=2))
|
189
|
+
else:
|
190
|
+
self.stdout.write(self.style.ERROR(f"❌ Failed to enable maintenance: {log_entry.error_message}"))
|
191
|
+
|
192
|
+
except Exception as e:
|
193
|
+
self.stdout.write(self.style.ERROR(f"❌ Error: {str(e)}"))
|
278
194
|
|
279
|
-
def
|
280
|
-
"""
|
281
|
-
|
195
|
+
def _handle_disable(self, domain: str, force: bool, verbose: bool) -> None:
|
196
|
+
"""Disable maintenance for site."""
|
197
|
+
site = CloudflareSite.objects.get(domain=domain)
|
282
198
|
|
283
|
-
if
|
284
|
-
self.stdout.write(self.style.WARNING("
|
199
|
+
if not site.maintenance_active:
|
200
|
+
self.stdout.write(self.style.WARNING(f"Maintenance is not active for {domain}"))
|
285
201
|
return
|
286
202
|
|
287
|
-
#
|
288
|
-
|
203
|
+
# Confirmation
|
204
|
+
if not force:
|
205
|
+
confirm = input(f"Disable maintenance for {site.name} ({domain})? [y/N]: ")
|
206
|
+
if confirm.lower() not in ['y', 'yes']:
|
207
|
+
self.stdout.write("Cancelled")
|
208
|
+
return
|
289
209
|
|
290
|
-
|
291
|
-
self.stdout.write(
|
292
|
-
f"{'Domain':<30} {'Environment':<12} {'Status':<12} {'Project':<20} {'Maintenance':<12}"
|
293
|
-
)
|
294
|
-
self.stdout.write("-" * 86)
|
210
|
+
self.stdout.write(f"Disabling maintenance for {domain}...")
|
295
211
|
|
296
|
-
|
297
|
-
|
298
|
-
|
212
|
+
try:
|
213
|
+
with transaction.atomic():
|
214
|
+
service = MaintenanceService(site)
|
215
|
+
log_entry = service.disable_maintenance()
|
299
216
|
|
300
|
-
|
301
|
-
f"
|
302
|
-
f"
|
303
|
-
|
217
|
+
if log_entry.status == MaintenanceLog.Status.SUCCESS:
|
218
|
+
duration_text = f" ({log_entry.duration_seconds}s)" if log_entry.duration_seconds else ""
|
219
|
+
self.stdout.write(self.style.SUCCESS(f"✅ Maintenance disabled successfully{duration_text}"))
|
220
|
+
|
221
|
+
if verbose and log_entry.cloudflare_response:
|
222
|
+
self.stdout.write("Cloudflare response:")
|
223
|
+
import json
|
224
|
+
self.stdout.write(json.dumps(log_entry.cloudflare_response, indent=2))
|
225
|
+
else:
|
226
|
+
self.stdout.write(self.style.ERROR(f"❌ Failed to disable maintenance: {log_entry.error_message}"))
|
227
|
+
|
228
|
+
except Exception as e:
|
229
|
+
self.stdout.write(self.style.ERROR(f"❌ Error: {str(e)}"))
|
304
230
|
|
305
|
-
|
306
|
-
"""
|
307
|
-
|
308
|
-
if not api_token:
|
309
|
-
raise CommandError("--api-token is required for site discovery")
|
231
|
+
def _handle_sync(self, domain: str, verbose: bool) -> None:
|
232
|
+
"""Sync site from Cloudflare."""
|
233
|
+
site = CloudflareSite.objects.get(domain=domain)
|
310
234
|
|
311
|
-
self.stdout.write("
|
235
|
+
self.stdout.write(f"Syncing {domain} from Cloudflare...")
|
312
236
|
|
313
237
|
try:
|
314
|
-
|
238
|
+
service = MaintenanceService(site)
|
239
|
+
log_entry = service.sync_site_from_cloudflare()
|
315
240
|
|
316
|
-
if
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
241
|
+
if log_entry.status == MaintenanceLog.Status.SUCCESS:
|
242
|
+
duration_text = f" ({log_entry.duration_seconds}s)" if log_entry.duration_seconds else ""
|
243
|
+
self.stdout.write(self.style.SUCCESS(f"✅ Sync completed successfully{duration_text}"))
|
244
|
+
|
245
|
+
if verbose and log_entry.cloudflare_response:
|
246
|
+
response = log_entry.cloudflare_response
|
247
|
+
if 'updated_fields' in response and response['updated_fields']:
|
248
|
+
self.stdout.write(f"Updated fields: {', '.join(response['updated_fields'])}")
|
249
|
+
|
250
|
+
maintenance_status = response.get('maintenance_active', 'unknown')
|
251
|
+
self.stdout.write(f"Current maintenance status: {maintenance_status}")
|
252
|
+
|
253
|
+
if verbose:
|
254
|
+
import json
|
255
|
+
self.stdout.write("Full Cloudflare response:")
|
256
|
+
self.stdout.write(json.dumps(response, indent=2))
|
322
257
|
else:
|
323
|
-
self.stdout.write(self.style.
|
258
|
+
self.stdout.write(self.style.ERROR(f"❌ Sync failed: {log_entry.error_message}"))
|
324
259
|
|
325
260
|
except Exception as e:
|
326
|
-
|
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}%"))
|
261
|
+
self.stdout.write(self.style.ERROR(f"❌ Error: {str(e)}"))
|