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.
Files changed (61) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/README.md +305 -0
  3. django_cfg/apps/maintenance/__init__.py +27 -0
  4. django_cfg/apps/maintenance/admin/__init__.py +28 -0
  5. django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
  6. django_cfg/apps/maintenance/admin/events_admin.py +374 -0
  7. django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
  8. django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
  9. django_cfg/apps/maintenance/apps.py +105 -0
  10. django_cfg/apps/maintenance/management/__init__.py +0 -0
  11. django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
  12. django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
  13. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
  14. django_cfg/apps/maintenance/managers/__init__.py +20 -0
  15. django_cfg/apps/maintenance/managers/deployments.py +287 -0
  16. django_cfg/apps/maintenance/managers/events.py +374 -0
  17. django_cfg/apps/maintenance/managers/monitoring.py +301 -0
  18. django_cfg/apps/maintenance/managers/sites.py +335 -0
  19. django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
  20. django_cfg/apps/maintenance/migrations/__init__.py +0 -0
  21. django_cfg/apps/maintenance/models/__init__.py +27 -0
  22. django_cfg/apps/maintenance/models/cloudflare.py +316 -0
  23. django_cfg/apps/maintenance/models/maintenance.py +334 -0
  24. django_cfg/apps/maintenance/models/monitoring.py +393 -0
  25. django_cfg/apps/maintenance/models/sites.py +419 -0
  26. django_cfg/apps/maintenance/serializers/__init__.py +60 -0
  27. django_cfg/apps/maintenance/serializers/actions.py +310 -0
  28. django_cfg/apps/maintenance/serializers/base.py +44 -0
  29. django_cfg/apps/maintenance/serializers/deployments.py +209 -0
  30. django_cfg/apps/maintenance/serializers/events.py +210 -0
  31. django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
  32. django_cfg/apps/maintenance/serializers/sites.py +213 -0
  33. django_cfg/apps/maintenance/services/README.md +168 -0
  34. django_cfg/apps/maintenance/services/__init__.py +21 -0
  35. django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
  36. django_cfg/apps/maintenance/services/dns_manager.py +497 -0
  37. django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
  38. django_cfg/apps/maintenance/services/site_sync.py +448 -0
  39. django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
  40. django_cfg/apps/maintenance/services/worker_manager.py +264 -0
  41. django_cfg/apps/maintenance/signals.py +38 -0
  42. django_cfg/apps/maintenance/urls.py +36 -0
  43. django_cfg/apps/maintenance/views/__init__.py +18 -0
  44. django_cfg/apps/maintenance/views/base.py +61 -0
  45. django_cfg/apps/maintenance/views/deployments.py +175 -0
  46. django_cfg/apps/maintenance/views/events.py +204 -0
  47. django_cfg/apps/maintenance/views/monitoring.py +213 -0
  48. django_cfg/apps/maintenance/views/sites.py +338 -0
  49. django_cfg/apps/urls.py +5 -1
  50. django_cfg/core/config.py +34 -3
  51. django_cfg/core/generation.py +15 -10
  52. django_cfg/models/cloudflare.py +316 -0
  53. django_cfg/models/revolution.py +1 -1
  54. django_cfg/models/tasks.py +1 -1
  55. django_cfg/modules/base.py +12 -5
  56. django_cfg/modules/django_unfold/dashboard.py +16 -1
  57. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/METADATA +2 -1
  58. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/RECORD +61 -13
  59. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/WHEEL +0 -0
  60. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/entry_points.txt +0 -0
  61. {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
+ ]