runbooks 0.7.9__py3-none-any.whl → 0.9.0__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.
- runbooks/__init__.py +1 -1
- runbooks/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/mcp_integration.py +539 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +171 -0
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +339 -451
- runbooks/finops/__init__.py +4 -21
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +59 -5
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +990 -232
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +8 -7
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +29 -1880
- runbooks/finops/helpers.py +509 -196
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +15 -15
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -0
- runbooks/finops/tests/test_reference_images_validation.py +1 -1
- runbooks/inventory/README.md +12 -1
- runbooks/inventory/core/collector.py +157 -29
- runbooks/inventory/list_ec2_instances.py +9 -6
- runbooks/inventory/list_ssm_parameters.py +10 -10
- runbooks/inventory/organizations_discovery.py +210 -164
- runbooks/inventory/rich_inventory_display.py +74 -107
- runbooks/inventory/run_on_multi_accounts.py +13 -13
- runbooks/main.py +740 -134
- runbooks/metrics/dora_metrics_engine.py +711 -17
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/base.py +215 -47
- runbooks/operate/ec2_operations.py +7 -5
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +164 -33
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +931 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +36 -49
- runbooks/security/security_export.py +99 -120
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +2 -2
- runbooks/validation/benchmark.py +154 -149
- runbooks/validation/cli.py +159 -147
- runbooks/validation/mcp_validator.py +265 -236
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +62 -33
- runbooks/vpc/rich_formatters.py +22 -8
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/METADATA +136 -54
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/RECORD +94 -55
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ Scope: Enhanced multi-account discovery for 200+ accounts with Organizations API
|
|
8
8
|
|
9
9
|
ENHANCED: 4-Profile AWS SSO Architecture & Performance Benchmarking (v0.8.0)
|
10
10
|
- Proven FinOps success patterns: 61 accounts, $474,406 validated
|
11
|
-
- Performance targets: <45s for multi-account discovery operations
|
11
|
+
- Performance targets: <45s for multi-account discovery operations
|
12
12
|
- Comprehensive error handling with profile fallbacks
|
13
13
|
- Enterprise-grade reliability and monitoring
|
14
14
|
"""
|
@@ -26,7 +26,7 @@ import boto3
|
|
26
26
|
from botocore.exceptions import ClientError, NoCredentialsError
|
27
27
|
from rich.console import Console
|
28
28
|
from rich.panel import Panel
|
29
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn,
|
29
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
30
30
|
from rich.status import Status
|
31
31
|
from rich.table import Table
|
32
32
|
|
@@ -39,16 +39,17 @@ logger = configure_logger(__name__)
|
|
39
39
|
|
40
40
|
# Enterprise 4-Profile AWS SSO Architecture (Proven FinOps Success Pattern)
|
41
41
|
ENTERPRISE_PROFILES = {
|
42
|
-
"BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185",
|
43
|
-
"MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185",
|
44
|
-
"CENTRALISED_OPS_PROFILE": "ams-centralised-ops-ReadOnlyAccess-335083429030",
|
45
|
-
"SINGLE_ACCOUNT_PROFILE": "ams-shared-services-non-prod-ReadOnlyAccess-499201730520" # Single account ops
|
42
|
+
"BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185", # Cost Explorer access
|
43
|
+
"MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185", # Organizations access
|
44
|
+
"CENTRALISED_OPS_PROFILE": "ams-centralised-ops-ReadOnlyAccess-335083429030", # Operations access
|
45
|
+
"SINGLE_ACCOUNT_PROFILE": "ams-shared-services-non-prod-ReadOnlyAccess-499201730520", # Single account ops
|
46
46
|
}
|
47
47
|
|
48
|
-
|
48
|
+
|
49
|
+
@dataclass
|
49
50
|
class PerformanceBenchmark:
|
50
51
|
"""Performance benchmarking for enterprise scale operations"""
|
51
|
-
|
52
|
+
|
52
53
|
operation_name: str
|
53
54
|
start_time: datetime
|
54
55
|
end_time: Optional[datetime] = None
|
@@ -58,18 +59,18 @@ class PerformanceBenchmark:
|
|
58
59
|
error_message: Optional[str] = None
|
59
60
|
accounts_processed: int = 0
|
60
61
|
api_calls_made: int = 0
|
61
|
-
|
62
|
+
|
62
63
|
def finish(self, success: bool = True, error_message: Optional[str] = None):
|
63
64
|
"""Mark benchmark as complete"""
|
64
65
|
self.end_time = datetime.now(timezone.utc)
|
65
66
|
self.duration_seconds = (self.end_time - self.start_time).total_seconds()
|
66
67
|
self.success = success
|
67
68
|
self.error_message = error_message
|
68
|
-
|
69
|
+
|
69
70
|
def is_within_target(self) -> bool:
|
70
71
|
"""Check if operation completed within target time"""
|
71
72
|
return self.duration_seconds <= self.target_seconds
|
72
|
-
|
73
|
+
|
73
74
|
def get_performance_grade(self) -> str:
|
74
75
|
"""Get performance grade based on target achievement"""
|
75
76
|
if not self.success:
|
@@ -77,13 +78,13 @@ class PerformanceBenchmark:
|
|
77
78
|
elif self.duration_seconds <= self.target_seconds * 0.5:
|
78
79
|
return "A+" # Exceptional performance (under 50% of target)
|
79
80
|
elif self.duration_seconds <= self.target_seconds * 0.75:
|
80
|
-
return "A"
|
81
|
+
return "A" # Excellent performance (under 75% of target)
|
81
82
|
elif self.duration_seconds <= self.target_seconds:
|
82
|
-
return "B"
|
83
|
+
return "B" # Good performance (within target)
|
83
84
|
elif self.duration_seconds <= self.target_seconds * 1.5:
|
84
|
-
return "C"
|
85
|
+
return "C" # Acceptable performance (within 150% of target)
|
85
86
|
else:
|
86
|
-
return "D"
|
87
|
+
return "D" # Poor performance (over 150% of target)
|
87
88
|
|
88
89
|
|
89
90
|
@dataclass
|
@@ -143,7 +144,7 @@ class CrossAccountRole:
|
|
143
144
|
class EnhancedOrganizationsDiscovery:
|
144
145
|
"""
|
145
146
|
Enhanced multi-account discovery with 4-Profile AWS SSO Architecture
|
146
|
-
|
147
|
+
|
147
148
|
Implements proven FinOps success patterns with enterprise-grade reliability:
|
148
149
|
- 4-profile AWS SSO architecture with failover
|
149
150
|
- Performance benchmarking targeting <45s operations
|
@@ -166,7 +167,7 @@ class EnhancedOrganizationsDiscovery:
|
|
166
167
|
|
167
168
|
Args:
|
168
169
|
management_profile: AWS profile with Organizations read access
|
169
|
-
billing_profile: AWS profile with Cost Explorer access
|
170
|
+
billing_profile: AWS profile with Cost Explorer access
|
170
171
|
operational_profile: AWS profile with operational access
|
171
172
|
single_account_profile: AWS profile for single account operations
|
172
173
|
max_workers: Maximum concurrent workers for parallel operations
|
@@ -177,14 +178,14 @@ class EnhancedOrganizationsDiscovery:
|
|
177
178
|
self.billing_profile = billing_profile or ENTERPRISE_PROFILES["BILLING_PROFILE"]
|
178
179
|
self.operational_profile = operational_profile or ENTERPRISE_PROFILES["CENTRALISED_OPS_PROFILE"]
|
179
180
|
self.single_account_profile = single_account_profile or ENTERPRISE_PROFILES["SINGLE_ACCOUNT_PROFILE"]
|
180
|
-
|
181
|
+
|
181
182
|
self.max_workers = max_workers
|
182
183
|
self.performance_target_seconds = performance_target_seconds
|
183
184
|
|
184
185
|
# Initialize session storage for all 4 profiles
|
185
186
|
self.sessions = {}
|
186
187
|
self.clients = {}
|
187
|
-
|
188
|
+
|
188
189
|
# Cache for discovered data
|
189
190
|
self.accounts_cache: Dict[str, AWSAccount] = {}
|
190
191
|
self.ous_cache: Dict[str, OrganizationalUnit] = {}
|
@@ -212,7 +213,7 @@ class EnhancedOrganizationsDiscovery:
|
|
212
213
|
def initialize_sessions(self) -> Dict[str, str]:
|
213
214
|
"""
|
214
215
|
Initialize AWS sessions with 4-profile architecture and comprehensive validation
|
215
|
-
|
216
|
+
|
216
217
|
Implements enterprise-grade session management with:
|
217
218
|
- Profile validation and credential verification
|
218
219
|
- Comprehensive error handling and fallback
|
@@ -221,11 +222,11 @@ class EnhancedOrganizationsDiscovery:
|
|
221
222
|
"""
|
222
223
|
profiles_to_test = [
|
223
224
|
("management", self.management_profile),
|
224
|
-
("billing", self.billing_profile),
|
225
|
+
("billing", self.billing_profile),
|
225
226
|
("operational", self.operational_profile),
|
226
227
|
("single_account", self.single_account_profile),
|
227
228
|
]
|
228
|
-
|
229
|
+
|
229
230
|
session_results = {
|
230
231
|
"status": "initializing",
|
231
232
|
"profiles_tested": 0,
|
@@ -243,25 +244,24 @@ class EnhancedOrganizationsDiscovery:
|
|
243
244
|
TimeElapsedColumn(),
|
244
245
|
console=console,
|
245
246
|
) as progress:
|
246
|
-
|
247
247
|
task = progress.add_task("Initializing AWS profiles...", total=len(profiles_to_test))
|
248
|
-
|
248
|
+
|
249
249
|
for profile_type, profile_name in profiles_to_test:
|
250
250
|
progress.update(task, description=f"Testing profile: {profile_type}")
|
251
251
|
session_results["profiles_tested"] += 1
|
252
252
|
self.discovery_metrics["profiles_tested"] += 1
|
253
|
-
|
253
|
+
|
254
254
|
try:
|
255
255
|
# Create session and verify credentials
|
256
256
|
session = boto3.Session(profile_name=profile_name)
|
257
257
|
sts_client = session.client("sts")
|
258
258
|
identity = sts_client.get_caller_identity()
|
259
|
-
|
259
|
+
|
260
260
|
# Store successful session
|
261
261
|
self.sessions[profile_type] = session
|
262
262
|
session_results["profiles_successful"] += 1
|
263
263
|
self.discovery_metrics["profiles_successful"] += 1
|
264
|
-
|
264
|
+
|
265
265
|
# Store session details
|
266
266
|
session_results["session_details"][profile_type] = {
|
267
267
|
"profile_name": profile_name,
|
@@ -270,7 +270,7 @@ class EnhancedOrganizationsDiscovery:
|
|
270
270
|
"user_id": identity["UserId"],
|
271
271
|
"status": "active",
|
272
272
|
}
|
273
|
-
|
273
|
+
|
274
274
|
# Initialize specific clients based on profile type
|
275
275
|
if profile_type == "management":
|
276
276
|
self.clients["organizations"] = session.client("organizations")
|
@@ -283,54 +283,62 @@ class EnhancedOrganizationsDiscovery:
|
|
283
283
|
self.clients["sts_operational"] = sts_client
|
284
284
|
elif profile_type == "single_account":
|
285
285
|
self.clients["sts_single"] = sts_client
|
286
|
-
|
286
|
+
|
287
287
|
console.print(f"✅ [green]{profile_type}[/green]: {identity['Account']} ({profile_name})")
|
288
|
-
|
288
|
+
|
289
289
|
except (NoCredentialsError, ClientError) as e:
|
290
290
|
error_msg = f"Profile '{profile_type}' ({profile_name}) failed: {str(e)}"
|
291
291
|
session_results["errors"].append(error_msg)
|
292
292
|
self.discovery_metrics["errors_encountered"] += 1
|
293
293
|
console.print(f"❌ [red]{profile_type}[/red]: {str(e)}")
|
294
|
-
|
294
|
+
|
295
295
|
# Add warning about missing profile
|
296
|
-
session_results["warnings"].append(
|
297
|
-
|
296
|
+
session_results["warnings"].append(
|
297
|
+
f"Profile {profile_type} unavailable - some features may be limited"
|
298
|
+
)
|
299
|
+
|
298
300
|
progress.advance(task)
|
299
|
-
|
301
|
+
|
300
302
|
# Determine overall status
|
301
303
|
if session_results["profiles_successful"] == 0:
|
302
304
|
session_results["status"] = "failed"
|
303
305
|
session_results["message"] = "No AWS profiles could be initialized - check credentials"
|
304
306
|
elif session_results["profiles_successful"] < len(profiles_to_test):
|
305
307
|
session_results["status"] = "partial"
|
306
|
-
session_results["message"] =
|
308
|
+
session_results["message"] = (
|
309
|
+
f"Initialized {session_results['profiles_successful']}/{len(profiles_to_test)} profiles"
|
310
|
+
)
|
307
311
|
else:
|
308
312
|
session_results["status"] = "success"
|
309
|
-
session_results["message"] =
|
310
|
-
|
313
|
+
session_results["message"] = (
|
314
|
+
f"All {session_results['profiles_successful']} profiles initialized successfully"
|
315
|
+
)
|
316
|
+
|
311
317
|
# Display summary panel
|
312
318
|
summary_text = f"""
|
313
|
-
[green]✅ Successful:[/green] {session_results[
|
314
|
-
[yellow]⚠️ Warnings:[/yellow] {len(session_results[
|
315
|
-
[red]❌ Errors:[/red] {len(session_results[
|
319
|
+
[green]✅ Successful:[/green] {session_results["profiles_successful"]}/{len(profiles_to_test)} profiles
|
320
|
+
[yellow]⚠️ Warnings:[/yellow] {len(session_results["warnings"])} profile issues
|
321
|
+
[red]❌ Errors:[/red] {len(session_results["errors"])} initialization failures
|
316
322
|
"""
|
317
|
-
|
318
|
-
console.print(
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
323
|
+
|
324
|
+
console.print(
|
325
|
+
Panel(
|
326
|
+
summary_text.strip(),
|
327
|
+
title=f"[bold cyan]4-Profile AWS SSO Initialization[/bold cyan]",
|
328
|
+
title_align="left",
|
329
|
+
border_style="cyan",
|
330
|
+
)
|
331
|
+
)
|
332
|
+
|
325
333
|
return session_results
|
326
334
|
|
327
335
|
async def discover_organization_structure(self) -> Dict:
|
328
336
|
"""
|
329
337
|
Discover complete organization structure with performance benchmarking
|
330
|
-
|
338
|
+
|
331
339
|
Enhanced with:
|
332
340
|
- Performance benchmark tracking (<45s target)
|
333
|
-
- Rich console progress monitoring
|
341
|
+
- Rich console progress monitoring
|
334
342
|
- Comprehensive error recovery
|
335
343
|
- Multi-profile fallback support
|
336
344
|
"""
|
@@ -338,23 +346,33 @@ class EnhancedOrganizationsDiscovery:
|
|
338
346
|
self.current_benchmark = PerformanceBenchmark(
|
339
347
|
operation_name="organization_structure_discovery",
|
340
348
|
start_time=datetime.now(timezone.utc),
|
341
|
-
target_seconds=self.performance_target_seconds
|
349
|
+
target_seconds=self.performance_target_seconds,
|
342
350
|
)
|
343
|
-
|
351
|
+
|
344
352
|
logger.info("🏢 Starting enhanced organization structure discovery with performance tracking")
|
345
353
|
self.discovery_metrics["start_time"] = self.current_benchmark.start_time
|
346
|
-
|
354
|
+
|
347
355
|
with Status("Initializing enterprise discovery...", console=console, spinner="dots"):
|
348
356
|
try:
|
349
357
|
# Initialize sessions with 4-profile architecture
|
350
358
|
session_result = self.initialize_sessions()
|
351
359
|
if session_result["status"] == "failed":
|
352
360
|
self.current_benchmark.finish(success=False, error_message="Profile initialization failed")
|
361
|
+
# CRITICAL FIX: Ensure performance_grade and metrics are set during early failures
|
362
|
+
self.discovery_metrics["performance_grade"] = self.current_benchmark.get_performance_grade()
|
363
|
+
self.discovery_metrics["end_time"] = self.current_benchmark.end_time
|
364
|
+
self.discovery_metrics["duration_seconds"] = self.current_benchmark.duration_seconds
|
365
|
+
self.discovery_metrics["errors_encountered"] += 1
|
366
|
+
# Create performance benchmark dict with computed performance_grade for early failures
|
367
|
+
performance_benchmark_dict = asdict(self.current_benchmark)
|
368
|
+
performance_benchmark_dict["performance_grade"] = self.current_benchmark.get_performance_grade()
|
369
|
+
|
353
370
|
return {
|
354
|
-
"status": "error",
|
371
|
+
"status": "error",
|
355
372
|
"error": "Profile initialization failed",
|
356
373
|
"session_result": session_result,
|
357
|
-
"
|
374
|
+
"metrics": self.discovery_metrics,
|
375
|
+
"performance_benchmark": performance_benchmark_dict,
|
358
376
|
}
|
359
377
|
|
360
378
|
# Continue with partial profile set if needed
|
@@ -370,16 +388,15 @@ class EnhancedOrganizationsDiscovery:
|
|
370
388
|
TimeElapsedColumn(),
|
371
389
|
console=console,
|
372
390
|
) as progress:
|
373
|
-
|
374
391
|
discovery_task = progress.add_task("Discovering organization structure...", total=5)
|
375
|
-
|
392
|
+
|
376
393
|
# Discover accounts
|
377
394
|
progress.update(discovery_task, description="Discovering accounts...")
|
378
395
|
accounts_result = await self._discover_accounts()
|
379
396
|
self.current_benchmark.accounts_processed = accounts_result.get("total_accounts", 0)
|
380
397
|
progress.advance(discovery_task)
|
381
398
|
|
382
|
-
# Discover organizational units
|
399
|
+
# Discover organizational units
|
383
400
|
progress.update(discovery_task, description="Discovering organizational units...")
|
384
401
|
ous_result = await self._discover_organizational_units()
|
385
402
|
progress.advance(discovery_task)
|
@@ -403,7 +420,7 @@ class EnhancedOrganizationsDiscovery:
|
|
403
420
|
self.current_benchmark.finish(success=True)
|
404
421
|
self.current_benchmark.api_calls_made = self.discovery_metrics["api_calls_made"]
|
405
422
|
self.benchmarks.append(self.current_benchmark)
|
406
|
-
|
423
|
+
|
407
424
|
# Calculate final metrics
|
408
425
|
self.discovery_metrics["end_time"] = self.current_benchmark.end_time
|
409
426
|
self.discovery_metrics["duration_seconds"] = self.current_benchmark.duration_seconds
|
@@ -416,18 +433,24 @@ class EnhancedOrganizationsDiscovery:
|
|
416
433
|
|
417
434
|
⏱️ Duration: [bold {performance_color}]{self.current_benchmark.duration_seconds:.1f}s[/bold {performance_color}] (Target: {self.performance_target_seconds}s)
|
418
435
|
📈 Grade: [bold {performance_color}]{self.current_benchmark.get_performance_grade()}[/bold {performance_color}]
|
419
|
-
🏢 Accounts: [yellow]{self.discovery_metrics[
|
420
|
-
🏗️ OUs: [yellow]{self.discovery_metrics[
|
421
|
-
🔐 Roles: [yellow]{self.discovery_metrics[
|
422
|
-
📡 API Calls: [blue]{self.discovery_metrics[
|
436
|
+
🏢 Accounts: [yellow]{self.discovery_metrics["accounts_discovered"]}[/yellow]
|
437
|
+
🏗️ OUs: [yellow]{self.discovery_metrics["ous_discovered"]}[/yellow]
|
438
|
+
🔐 Roles: [yellow]{self.discovery_metrics["roles_discovered"]}[/yellow]
|
439
|
+
📡 API Calls: [blue]{self.discovery_metrics["api_calls_made"]}[/blue]
|
423
440
|
"""
|
424
|
-
|
425
|
-
console.print(
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
441
|
+
|
442
|
+
console.print(
|
443
|
+
Panel(
|
444
|
+
performance_text.strip(),
|
445
|
+
title="[bold green]✅ Discovery Complete[/bold green]",
|
446
|
+
title_align="left",
|
447
|
+
border_style="green" if self.current_benchmark.is_within_target() else "red",
|
448
|
+
)
|
449
|
+
)
|
450
|
+
|
451
|
+
# Create performance benchmark dict with computed performance_grade
|
452
|
+
performance_benchmark_dict = asdict(self.current_benchmark)
|
453
|
+
performance_benchmark_dict["performance_grade"] = self.current_benchmark.get_performance_grade()
|
431
454
|
|
432
455
|
return {
|
433
456
|
"status": "completed",
|
@@ -438,7 +461,7 @@ class EnhancedOrganizationsDiscovery:
|
|
438
461
|
"cross_account_roles": roles_result,
|
439
462
|
"session_info": session_result,
|
440
463
|
"metrics": self.discovery_metrics,
|
441
|
-
"performance_benchmark":
|
464
|
+
"performance_benchmark": performance_benchmark_dict,
|
442
465
|
"timestamp": datetime.now().isoformat(),
|
443
466
|
}
|
444
467
|
|
@@ -446,27 +469,45 @@ class EnhancedOrganizationsDiscovery:
|
|
446
469
|
# Handle discovery failure
|
447
470
|
error_message = f"Organization discovery failed: {str(e)}"
|
448
471
|
logger.error(error_message)
|
449
|
-
|
472
|
+
|
450
473
|
if self.current_benchmark:
|
451
474
|
self.current_benchmark.finish(success=False, error_message=error_message)
|
452
475
|
self.benchmarks.append(self.current_benchmark)
|
453
|
-
|
476
|
+
# CRITICAL FIX: Ensure performance_grade is always set, even during errors
|
477
|
+
self.discovery_metrics["performance_grade"] = self.current_benchmark.get_performance_grade()
|
478
|
+
else:
|
479
|
+
# No benchmark available - set failed performance grade
|
480
|
+
self.discovery_metrics["performance_grade"] = "F"
|
481
|
+
|
454
482
|
self.discovery_metrics["errors_encountered"] += 1
|
455
|
-
|
483
|
+
# Ensure end_time and duration are set for error cases
|
484
|
+
self.discovery_metrics["end_time"] = datetime.now(timezone.utc)
|
485
|
+
if self.discovery_metrics["start_time"]:
|
486
|
+
duration = (
|
487
|
+
self.discovery_metrics["end_time"] - self.discovery_metrics["start_time"]
|
488
|
+
).total_seconds()
|
489
|
+
self.discovery_metrics["duration_seconds"] = duration
|
490
|
+
|
491
|
+
# Create performance benchmark dict with computed performance_grade for errors
|
492
|
+
performance_benchmark_dict = None
|
493
|
+
if self.current_benchmark:
|
494
|
+
performance_benchmark_dict = asdict(self.current_benchmark)
|
495
|
+
performance_benchmark_dict["performance_grade"] = self.current_benchmark.get_performance_grade()
|
496
|
+
|
456
497
|
return {
|
457
|
-
"status": "error",
|
458
|
-
"error": error_message,
|
498
|
+
"status": "error",
|
499
|
+
"error": error_message,
|
459
500
|
"metrics": self.discovery_metrics,
|
460
|
-
"performance_benchmark":
|
501
|
+
"performance_benchmark": performance_benchmark_dict,
|
461
502
|
}
|
462
503
|
|
463
504
|
async def _discover_accounts(self) -> Dict:
|
464
505
|
"""
|
465
506
|
Discover all accounts in the organization using 4-profile architecture
|
466
|
-
|
507
|
+
|
467
508
|
Enhanced with:
|
468
509
|
- Multi-profile fallback support
|
469
|
-
- Comprehensive error handling
|
510
|
+
- Comprehensive error handling
|
470
511
|
- Performance optimizations
|
471
512
|
- Rich progress tracking
|
472
513
|
"""
|
@@ -514,13 +555,13 @@ class EnhancedOrganizationsDiscovery:
|
|
514
555
|
active_accounts = [a for a in accounts if a.status == "ACTIVE"]
|
515
556
|
suspended_accounts = [a for a in accounts if a.status == "SUSPENDED"]
|
516
557
|
closed_accounts = [a for a in accounts if a.status == "CLOSED"]
|
517
|
-
|
558
|
+
|
518
559
|
logger.info(f"✅ Discovered {len(accounts)} total accounts ({len(active_accounts)} active)")
|
519
560
|
|
520
561
|
return {
|
521
562
|
"total_accounts": len(accounts),
|
522
563
|
"active_accounts": len(active_accounts),
|
523
|
-
"suspended_accounts": len(suspended_accounts),
|
564
|
+
"suspended_accounts": len(suspended_accounts),
|
524
565
|
"closed_accounts": len(closed_accounts),
|
525
566
|
"accounts": [asdict(account) for account in accounts],
|
526
567
|
"discovery_method": "organizations_api",
|
@@ -530,7 +571,7 @@ class EnhancedOrganizationsDiscovery:
|
|
530
571
|
except ClientError as e:
|
531
572
|
logger.error(f"Failed to discover accounts via Organizations API: {e}")
|
532
573
|
self.discovery_metrics["errors_encountered"] += 1
|
533
|
-
|
574
|
+
|
534
575
|
# Attempt fallback discovery
|
535
576
|
logger.info("Attempting fallback account discovery...")
|
536
577
|
return await self._discover_accounts_fallback()
|
@@ -538,19 +579,19 @@ class EnhancedOrganizationsDiscovery:
|
|
538
579
|
async def _discover_accounts_fallback(self) -> Dict:
|
539
580
|
"""
|
540
581
|
Fallback account discovery when Organizations API is not available
|
541
|
-
|
582
|
+
|
542
583
|
Uses individual profile sessions to identify accessible accounts
|
543
584
|
"""
|
544
585
|
logger.info("🔄 Using fallback account discovery via individual profiles")
|
545
|
-
|
586
|
+
|
546
587
|
discovered_accounts = {}
|
547
|
-
|
588
|
+
|
548
589
|
for profile_type, session in self.sessions.items():
|
549
590
|
try:
|
550
591
|
sts_client = session.client("sts")
|
551
592
|
identity = sts_client.get_caller_identity()
|
552
593
|
account_id = identity["Account"]
|
553
|
-
|
594
|
+
|
554
595
|
if account_id not in discovered_accounts:
|
555
596
|
# Create account info from STS identity
|
556
597
|
account = AWSAccount(
|
@@ -560,28 +601,28 @@ class EnhancedOrganizationsDiscovery:
|
|
560
601
|
status="ACTIVE", # Assume active if accessible
|
561
602
|
joined_method="UNKNOWN",
|
562
603
|
joined_timestamp=None,
|
563
|
-
tags={"DiscoveryMethod": "fallback", "ProfileType": profile_type}
|
604
|
+
tags={"DiscoveryMethod": "fallback", "ProfileType": profile_type},
|
564
605
|
)
|
565
|
-
|
606
|
+
|
566
607
|
discovered_accounts[account_id] = account
|
567
608
|
self.accounts_cache[account_id] = account
|
568
|
-
|
609
|
+
|
569
610
|
self.discovery_metrics["api_calls_made"] += 1
|
570
|
-
|
611
|
+
|
571
612
|
except Exception as e:
|
572
613
|
logger.debug(f"Could not get identity for profile {profile_type}: {e}")
|
573
614
|
continue
|
574
|
-
|
615
|
+
|
575
616
|
accounts = list(discovered_accounts.values())
|
576
617
|
self.discovery_metrics["accounts_discovered"] = len(accounts)
|
577
|
-
|
618
|
+
|
578
619
|
logger.info(f"✅ Fallback discovery found {len(accounts)} accessible accounts")
|
579
|
-
|
620
|
+
|
580
621
|
return {
|
581
622
|
"total_accounts": len(accounts),
|
582
623
|
"active_accounts": len(accounts), # All fallback accounts assumed active
|
583
624
|
"suspended_accounts": 0,
|
584
|
-
"closed_accounts": 0,
|
625
|
+
"closed_accounts": 0,
|
585
626
|
"accounts": [asdict(account) for account in accounts],
|
586
627
|
"discovery_method": "fallback_sts",
|
587
628
|
"profile_used": "multiple",
|
@@ -590,7 +631,7 @@ class EnhancedOrganizationsDiscovery:
|
|
590
631
|
async def _discover_organizational_units(self) -> Dict:
|
591
632
|
"""
|
592
633
|
Discover all organizational units with enhanced error handling
|
593
|
-
|
634
|
+
|
594
635
|
Enhanced with:
|
595
636
|
- Multi-profile fallback support
|
596
637
|
- Comprehensive error recovery
|
@@ -606,12 +647,12 @@ class EnhancedOrganizationsDiscovery:
|
|
606
647
|
"total_ous": 0,
|
607
648
|
"organizational_units": [],
|
608
649
|
"discovery_method": "unavailable",
|
609
|
-
"message": "Organizations API not accessible - OU discovery skipped"
|
650
|
+
"message": "Organizations API not accessible - OU discovery skipped",
|
610
651
|
}
|
611
652
|
|
612
653
|
try:
|
613
654
|
organizations_client = self.clients["organizations"]
|
614
|
-
|
655
|
+
|
615
656
|
# Get root OU
|
616
657
|
roots_response = organizations_client.list_roots()
|
617
658
|
if not roots_response.get("Roots"):
|
@@ -621,9 +662,9 @@ class EnhancedOrganizationsDiscovery:
|
|
621
662
|
"total_ous": 0,
|
622
663
|
"organizational_units": [],
|
623
664
|
"discovery_method": "organizations_api",
|
624
|
-
"message": "No root OUs found in organization"
|
665
|
+
"message": "No root OUs found in organization",
|
625
666
|
}
|
626
|
-
|
667
|
+
|
627
668
|
root_id = roots_response["Roots"][0]["Id"]
|
628
669
|
self.discovery_metrics["api_calls_made"] += 1
|
629
670
|
|
@@ -634,7 +675,7 @@ class EnhancedOrganizationsDiscovery:
|
|
634
675
|
except ClientError as ou_error:
|
635
676
|
logger.warning(f"Partial OU discovery failed: {ou_error}")
|
636
677
|
# Continue with what we have discovered so far
|
637
|
-
|
678
|
+
|
638
679
|
self.discovery_metrics["ous_discovered"] = len(all_ous)
|
639
680
|
|
640
681
|
logger.info(f"✅ Discovered {len(all_ous)} organizational units")
|
@@ -650,7 +691,7 @@ class EnhancedOrganizationsDiscovery:
|
|
650
691
|
except ClientError as e:
|
651
692
|
logger.error(f"Failed to discover OUs: {e}")
|
652
693
|
self.discovery_metrics["errors_encountered"] += 1
|
653
|
-
|
694
|
+
|
654
695
|
# Return graceful failure result instead of raising
|
655
696
|
return {
|
656
697
|
"root_id": None,
|
@@ -658,14 +699,14 @@ class EnhancedOrganizationsDiscovery:
|
|
658
699
|
"organizational_units": [],
|
659
700
|
"discovery_method": "failed",
|
660
701
|
"error": str(e),
|
661
|
-
"message": "OU discovery failed - continuing without organizational structure"
|
702
|
+
"message": "OU discovery failed - continuing without organizational structure",
|
662
703
|
}
|
663
704
|
|
664
705
|
async def _discover_ou_recursive(self, parent_id: str, ou_list: List[OrganizationalUnit]):
|
665
706
|
"""Recursively discover organizational units with enhanced error handling"""
|
666
707
|
try:
|
667
708
|
organizations_client = self.clients["organizations"]
|
668
|
-
|
709
|
+
|
669
710
|
# Get child OUs
|
670
711
|
paginator = organizations_client.get_paginator("list_organizational_units_for_parent")
|
671
712
|
|
@@ -702,7 +743,7 @@ class EnhancedOrganizationsDiscovery:
|
|
702
743
|
|
703
744
|
try:
|
704
745
|
organizations_client = self.clients["organizations"]
|
705
|
-
|
746
|
+
|
706
747
|
for account_id, account in self.accounts_cache.items():
|
707
748
|
# Find which OU this account belongs to
|
708
749
|
try:
|
@@ -821,14 +862,14 @@ class EnhancedOrganizationsDiscovery:
|
|
821
862
|
logger.warning("Organizations client not available - using fallback organization info")
|
822
863
|
return {
|
823
864
|
"organization_id": "unavailable",
|
824
|
-
"master_account_id": "unavailable",
|
865
|
+
"master_account_id": "unavailable",
|
825
866
|
"master_account_email": "unavailable",
|
826
867
|
"feature_set": "unavailable",
|
827
868
|
"available_policy_types": [],
|
828
869
|
"discovery_method": "unavailable",
|
829
|
-
"message": "Organizations API not accessible"
|
870
|
+
"message": "Organizations API not accessible",
|
830
871
|
}
|
831
|
-
|
872
|
+
|
832
873
|
try:
|
833
874
|
organizations_client = self.clients["organizations"]
|
834
875
|
org_response = organizations_client.describe_organization()
|
@@ -849,18 +890,18 @@ class EnhancedOrganizationsDiscovery:
|
|
849
890
|
return {
|
850
891
|
"organization_id": "error",
|
851
892
|
"master_account_id": "error",
|
852
|
-
"master_account_email": "error",
|
893
|
+
"master_account_email": "error",
|
853
894
|
"feature_set": "error",
|
854
895
|
"available_policy_types": [],
|
855
896
|
"discovery_method": "failed",
|
856
897
|
"error": str(e),
|
857
|
-
"message": "Organization info retrieval failed"
|
898
|
+
"message": "Organization info retrieval failed",
|
858
899
|
}
|
859
900
|
|
860
901
|
async def get_cost_validation_data(self, time_range_days: int = 30) -> Dict:
|
861
902
|
"""
|
862
903
|
Get cost data for validation and analysis using 4-profile architecture
|
863
|
-
|
904
|
+
|
864
905
|
Enhanced with:
|
865
906
|
- Billing profile validation and fallback
|
866
907
|
- Comprehensive error handling
|
@@ -880,7 +921,7 @@ class EnhancedOrganizationsDiscovery:
|
|
880
921
|
"cost_by_account": {},
|
881
922
|
"high_spend_accounts": {},
|
882
923
|
"discovery_method": "unavailable",
|
883
|
-
"message": "Billing profile not accessible - cost data unavailable"
|
924
|
+
"message": "Billing profile not accessible - cost data unavailable",
|
884
925
|
}
|
885
926
|
|
886
927
|
try:
|
@@ -918,11 +959,15 @@ class EnhancedOrganizationsDiscovery:
|
|
918
959
|
|
919
960
|
# Enhanced cost analysis
|
920
961
|
high_spend_accounts = {
|
921
|
-
k: round(v, 2)
|
962
|
+
k: round(v, 2)
|
963
|
+
for k, v in cost_by_account.items()
|
964
|
+
if v > 1000 # >$1000/month
|
922
965
|
}
|
923
|
-
|
966
|
+
|
924
967
|
medium_spend_accounts = {
|
925
|
-
k: round(v, 2)
|
968
|
+
k: round(v, 2)
|
969
|
+
for k, v in cost_by_account.items()
|
970
|
+
if 100 <= v <= 1000 # $100-$1000/month
|
926
971
|
}
|
927
972
|
|
928
973
|
logger.info(f"✅ Cost validation complete: ${total_cost:.2f} across {len(cost_by_account)} accounts")
|
@@ -939,16 +984,16 @@ class EnhancedOrganizationsDiscovery:
|
|
939
984
|
"profile_used": "billing",
|
940
985
|
"cost_breakdown": {
|
941
986
|
"high_spend_count": len(high_spend_accounts),
|
942
|
-
"medium_spend_count": len(medium_spend_accounts),
|
987
|
+
"medium_spend_count": len(medium_spend_accounts),
|
943
988
|
"low_spend_count": len(cost_by_account) - len(high_spend_accounts) - len(medium_spend_accounts),
|
944
989
|
"average_cost_per_account": round(total_cost / len(cost_by_account), 2) if cost_by_account else 0,
|
945
|
-
}
|
990
|
+
},
|
946
991
|
}
|
947
992
|
|
948
993
|
except ClientError as e:
|
949
994
|
logger.error(f"Failed to get cost data: {e}")
|
950
995
|
self.discovery_metrics["errors_encountered"] += 1
|
951
|
-
|
996
|
+
|
952
997
|
return {
|
953
998
|
"status": "error",
|
954
999
|
"time_range_days": time_range_days,
|
@@ -1089,16 +1134,16 @@ async def run_enhanced_organizations_discovery(
|
|
1089
1134
|
) -> Dict:
|
1090
1135
|
"""
|
1091
1136
|
Run complete enhanced organizations discovery workflow with 4-profile architecture
|
1092
|
-
|
1137
|
+
|
1093
1138
|
Implements proven FinOps success patterns with enterprise-grade reliability:
|
1094
1139
|
- 4-profile AWS SSO architecture with failover
|
1095
|
-
- Performance benchmarking targeting <45s operations
|
1140
|
+
- Performance benchmarking targeting <45s operations
|
1096
1141
|
- Comprehensive error handling and profile fallbacks
|
1097
1142
|
- Rich console progress tracking and monitoring
|
1098
|
-
|
1143
|
+
|
1099
1144
|
Args:
|
1100
1145
|
management_profile: AWS profile with Organizations access (defaults to proven enterprise profile)
|
1101
|
-
billing_profile: AWS profile with Cost Explorer access (defaults to proven enterprise profile)
|
1146
|
+
billing_profile: AWS profile with Cost Explorer access (defaults to proven enterprise profile)
|
1102
1147
|
operational_profile: AWS profile with operational access (defaults to proven enterprise profile)
|
1103
1148
|
single_account_profile: AWS profile for single account operations (defaults to proven enterprise profile)
|
1104
1149
|
performance_target_seconds: Performance target for discovery operations (default: 45s)
|
@@ -1107,17 +1152,19 @@ async def run_enhanced_organizations_discovery(
|
|
1107
1152
|
Complete discovery results with organization structure, costs, analysis, and performance metrics
|
1108
1153
|
"""
|
1109
1154
|
|
1110
|
-
console.print(
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1155
|
+
console.print(
|
1156
|
+
Panel.fit(
|
1157
|
+
"[bold bright_cyan]🚀 Enhanced Organizations Discovery[/bold bright_cyan]\n\n"
|
1158
|
+
"[green]✨ Features:[/green]\n"
|
1159
|
+
"• 4-Profile AWS SSO Architecture\n"
|
1160
|
+
"• Performance Benchmarking (<45s target)\n"
|
1161
|
+
"• Comprehensive Error Handling\n"
|
1162
|
+
"• Multi-Account Enterprise Scale\n\n"
|
1163
|
+
"[yellow]⚡ Initializing enhanced discovery engine...[/yellow]",
|
1164
|
+
title="Enterprise Discovery Engine v0.8.0",
|
1165
|
+
style="bright_cyan",
|
1166
|
+
)
|
1167
|
+
)
|
1121
1168
|
|
1122
1169
|
discovery = EnhancedOrganizationsDiscovery(
|
1123
1170
|
management_profile=management_profile,
|
@@ -1125,14 +1172,14 @@ async def run_enhanced_organizations_discovery(
|
|
1125
1172
|
operational_profile=operational_profile,
|
1126
1173
|
single_account_profile=single_account_profile,
|
1127
1174
|
max_workers=50,
|
1128
|
-
performance_target_seconds=performance_target_seconds
|
1175
|
+
performance_target_seconds=performance_target_seconds,
|
1129
1176
|
)
|
1130
1177
|
|
1131
1178
|
# Run main discovery with performance benchmarking
|
1132
1179
|
org_results = await discovery.discover_organization_structure()
|
1133
1180
|
|
1134
1181
|
if org_results["status"] == "completed":
|
1135
|
-
# Add cost validation using billing profile
|
1182
|
+
# Add cost validation using billing profile
|
1136
1183
|
cost_data = await discovery.get_cost_validation_data()
|
1137
1184
|
org_results["cost_validation"] = cost_data
|
1138
1185
|
|
@@ -1143,19 +1190,20 @@ async def run_enhanced_organizations_discovery(
|
|
1143
1190
|
# Add hierarchy visualization
|
1144
1191
|
hierarchy_viz = discovery.generate_account_hierarchy_visualization()
|
1145
1192
|
org_results["hierarchy_visualization"] = hierarchy_viz
|
1146
|
-
|
1193
|
+
|
1147
1194
|
# Add performance summary
|
1148
1195
|
org_results["performance_summary"] = {
|
1149
1196
|
"benchmarks_completed": len(discovery.benchmarks),
|
1150
1197
|
"total_duration": org_results["performance_benchmark"]["duration_seconds"],
|
1151
|
-
"performance_grade": org_results["performance_benchmark"]
|
1198
|
+
"performance_grade": org_results["performance_benchmark"].get("performance_grade", "N/A"),
|
1152
1199
|
"target_achieved": discovery.current_benchmark.is_within_target() if discovery.current_benchmark else False,
|
1153
1200
|
"profiles_successful": org_results["session_info"]["profiles_successful"],
|
1154
|
-
"api_calls_total": org_results["metrics"]["api_calls_made"]
|
1201
|
+
"api_calls_total": org_results["metrics"]["api_calls_made"],
|
1155
1202
|
}
|
1156
1203
|
|
1157
1204
|
return org_results
|
1158
1205
|
|
1206
|
+
|
1159
1207
|
# Legacy compatibility function
|
1160
1208
|
async def run_organizations_discovery(
|
1161
1209
|
management_profile: str = "ams-admin-ReadOnlyAccess-909135376185",
|
@@ -1163,12 +1211,12 @@ async def run_organizations_discovery(
|
|
1163
1211
|
) -> Dict:
|
1164
1212
|
"""
|
1165
1213
|
Legacy compatibility function - redirects to enhanced discovery
|
1166
|
-
|
1214
|
+
|
1167
1215
|
Returns:
|
1168
1216
|
Complete discovery results using enhanced 4-profile architecture
|
1169
1217
|
"""
|
1170
1218
|
console.print("[yellow]ℹ️ Using enhanced discovery engine for improved reliability and performance[/yellow]")
|
1171
|
-
|
1219
|
+
|
1172
1220
|
return await run_enhanced_organizations_discovery(
|
1173
1221
|
management_profile=management_profile,
|
1174
1222
|
billing_profile=billing_profile,
|
@@ -1191,7 +1239,7 @@ if __name__ == "__main__":
|
|
1191
1239
|
help=f"AWS profile with Cost Explorer access (default: {ENTERPRISE_PROFILES['BILLING_PROFILE']})",
|
1192
1240
|
)
|
1193
1241
|
parser.add_argument(
|
1194
|
-
"--operational-profile",
|
1242
|
+
"--operational-profile",
|
1195
1243
|
help=f"AWS profile with operational access (default: {ENTERPRISE_PROFILES['CENTRALISED_OPS_PROFILE']})",
|
1196
1244
|
)
|
1197
1245
|
parser.add_argument(
|
@@ -1205,11 +1253,7 @@ if __name__ == "__main__":
|
|
1205
1253
|
help="Performance target in seconds (default: 45s)",
|
1206
1254
|
)
|
1207
1255
|
parser.add_argument("--output", "-o", default="enhanced_organizations_discovery.json", help="Output file path")
|
1208
|
-
parser.add_argument(
|
1209
|
-
"--legacy",
|
1210
|
-
action="store_true",
|
1211
|
-
help="Use legacy discovery method (compatibility mode)"
|
1212
|
-
)
|
1256
|
+
parser.add_argument("--legacy", action="store_true", help="Use legacy discovery method (compatibility mode)")
|
1213
1257
|
|
1214
1258
|
args = parser.parse_args()
|
1215
1259
|
|
@@ -1218,7 +1262,7 @@ if __name__ == "__main__":
|
|
1218
1262
|
console.print("[yellow]⚠️ Using legacy compatibility mode[/yellow]")
|
1219
1263
|
results = await run_organizations_discovery(
|
1220
1264
|
management_profile=args.management_profile or ENTERPRISE_PROFILES["MANAGEMENT_PROFILE"],
|
1221
|
-
billing_profile=args.billing_profile or ENTERPRISE_PROFILES["BILLING_PROFILE"]
|
1265
|
+
billing_profile=args.billing_profile or ENTERPRISE_PROFILES["BILLING_PROFILE"],
|
1222
1266
|
)
|
1223
1267
|
else:
|
1224
1268
|
console.print("[cyan]🚀 Using enhanced 4-profile discovery engine[/cyan]")
|
@@ -1235,35 +1279,37 @@ if __name__ == "__main__":
|
|
1235
1279
|
json.dump(results, f, indent=2, default=str)
|
1236
1280
|
|
1237
1281
|
# Create enhanced Rich formatted summary
|
1238
|
-
accounts_count = results.get(
|
1239
|
-
ous_count = results.get(
|
1240
|
-
monthly_cost = results.get(
|
1241
|
-
|
1282
|
+
accounts_count = results.get("accounts", {}).get("total_accounts", 0)
|
1283
|
+
ous_count = results.get("organizational_units", {}).get("total_ous", 0)
|
1284
|
+
monthly_cost = results.get("cost_validation", {}).get("total_monthly_cost", 0)
|
1285
|
+
|
1242
1286
|
# Performance metrics if available
|
1243
|
-
performance_grade = results.get(
|
1244
|
-
duration = results.get(
|
1245
|
-
profiles_successful = results.get(
|
1246
|
-
|
1287
|
+
performance_grade = results.get("performance_benchmark", {}).get("performance_grade", "N/A")
|
1288
|
+
duration = results.get("performance_benchmark", {}).get("duration_seconds", 0)
|
1289
|
+
profiles_successful = results.get("session_info", {}).get("profiles_successful", 0)
|
1290
|
+
|
1247
1291
|
summary_table = Table(show_header=False, box=None)
|
1248
1292
|
summary_table.add_column("Metric", style="cyan", no_wrap=True)
|
1249
1293
|
summary_table.add_column("Value", style="green")
|
1250
|
-
|
1294
|
+
|
1251
1295
|
summary_table.add_row("📊 Accounts discovered:", f"{accounts_count}")
|
1252
1296
|
summary_table.add_row("🏢 OUs discovered:", f"{ous_count}")
|
1253
1297
|
summary_table.add_row("💰 Monthly cost:", f"${monthly_cost:,.2f}" if monthly_cost else "N/A")
|
1254
|
-
|
1298
|
+
|
1255
1299
|
if not args.legacy:
|
1256
1300
|
summary_table.add_row("⚡ Performance grade:", f"{performance_grade}")
|
1257
1301
|
summary_table.add_row("⏱️ Duration:", f"{duration:.1f}s")
|
1258
1302
|
summary_table.add_row("🔧 Profiles active:", f"{profiles_successful}/4")
|
1259
|
-
|
1303
|
+
|
1260
1304
|
title_color = "green" if performance_grade in ["A+", "A", "B"] else "yellow"
|
1261
|
-
|
1262
|
-
console.print(
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1305
|
+
|
1306
|
+
console.print(
|
1307
|
+
Panel(
|
1308
|
+
summary_table,
|
1309
|
+
title=f"[{title_color}]✅ Enhanced Discovery Complete - Results saved to {args.output}[/{title_color}]",
|
1310
|
+
title_align="left",
|
1311
|
+
border_style=title_color,
|
1312
|
+
)
|
1313
|
+
)
|
1268
1314
|
|
1269
1315
|
asyncio.run(main())
|