runbooks 0.9.8__py3-none-any.whl → 1.0.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/cloud_foundations_assessment.py +626 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/aws_pricing.py +388 -0
- runbooks/common/aws_pricing_api.py +205 -0
- runbooks/common/aws_utils.py +2 -2
- runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
- runbooks/common/cross_account_manager.py +606 -0
- runbooks/common/enhanced_exception_handler.py +4 -0
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +96 -2
- runbooks/common/rich_utils.py +3 -0
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/markdown_exporter.py +441 -0
- runbooks/finops/nat_gateway_optimizer.py +57 -20
- runbooks/finops/optimizer.py +2 -0
- runbooks/finops/single_dashboard.py +2 -2
- runbooks/finops/vpc_cleanup_exporter.py +330 -0
- runbooks/finops/vpc_cleanup_optimizer.py +895 -40
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1148 -88
- runbooks/inventory/discovery.md +389 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +4 -7
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +91 -1
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1292 -0
- runbooks/inventory/verify_ec2_security_groups.py +3 -1
- runbooks/inventory/vpc_analyzer.py +825 -7
- runbooks/inventory/vpc_flow_analyzer.py +36 -42
- runbooks/main.py +969 -42
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/networking_cost_heatmap.py +4 -3
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +50 -2
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commvault_ec2_analysis.py +6 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/validation/__init__.py +21 -1
- runbooks/validation/comprehensive_2way_validator.py +1996 -0
- runbooks/validation/mcp_validator.py +904 -94
- runbooks/validation/terraform_citations_validator.py +363 -0
- runbooks/validation/terraform_drift_detector.py +1098 -0
- runbooks/vpc/cleanup_wrapper.py +231 -10
- runbooks/vpc/config.py +310 -62
- runbooks/vpc/cross_account_session.py +308 -0
- runbooks/vpc/heatmap_engine.py +96 -29
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1551 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- runbooks/vpc/tests/test_cost_engine.py +1 -1
- runbooks/vpc/unified_scenarios.py +3269 -0
- runbooks/vpc/vpc_cleanup_integration.py +516 -82
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/RECORD +75 -51
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -779,15 +779,32 @@ class AWSO5MCPValidator:
|
|
779
779
|
print_warning(f"Flow Logs validation failed: {e}")
|
780
780
|
|
781
781
|
def _calculate_accuracy(self, runbooks_value: Any, mcp_value: Any) -> float:
|
782
|
-
"""Calculate accuracy percentage between runbooks and MCP values."""
|
782
|
+
"""Calculate accuracy percentage between runbooks and MCP values with enterprise tolerance."""
|
783
783
|
|
784
784
|
if isinstance(runbooks_value, (int, float)) and isinstance(mcp_value, (int, float)):
|
785
|
-
|
786
|
-
|
785
|
+
# Perfect match
|
786
|
+
if runbooks_value == mcp_value:
|
787
|
+
return 100.0
|
787
788
|
|
788
|
-
|
789
|
-
|
790
|
-
|
789
|
+
# Both zero
|
790
|
+
if mcp_value == 0 and runbooks_value == 0:
|
791
|
+
return 100.0
|
792
|
+
|
793
|
+
# One zero, other non-zero
|
794
|
+
if mcp_value == 0 or runbooks_value == 0:
|
795
|
+
return 0.0
|
796
|
+
|
797
|
+
# Calculate percentage variance
|
798
|
+
max_value = max(abs(runbooks_value), abs(mcp_value))
|
799
|
+
variance_percent = abs(runbooks_value - mcp_value) / max_value * 100
|
800
|
+
|
801
|
+
# Apply enterprise tolerance (±5% acceptable)
|
802
|
+
if variance_percent <= 5.0:
|
803
|
+
return 100.0
|
804
|
+
else:
|
805
|
+
# Scale accuracy based on variance beyond tolerance
|
806
|
+
accuracy = max(0.0, 100.0 - (variance_percent - 5.0))
|
807
|
+
return min(100.0, accuracy)
|
791
808
|
|
792
809
|
elif runbooks_value == mcp_value:
|
793
810
|
return 100.0
|
@@ -37,6 +37,31 @@ from ..utils.logger import configure_logger
|
|
37
37
|
|
38
38
|
logger = configure_logger(__name__)
|
39
39
|
|
40
|
+
# Global Organizations cache to prevent duplicate API calls across all instances
|
41
|
+
_GLOBAL_ORGS_CACHE = {
|
42
|
+
'data': None,
|
43
|
+
'timestamp': None,
|
44
|
+
'ttl_minutes': 30
|
45
|
+
}
|
46
|
+
|
47
|
+
def _get_global_organizations_cache():
|
48
|
+
"""Get cached Organizations data if valid (module-level cache)."""
|
49
|
+
if not _GLOBAL_ORGS_CACHE['timestamp']:
|
50
|
+
return None
|
51
|
+
|
52
|
+
cache_age_minutes = (datetime.now(timezone.utc) - _GLOBAL_ORGS_CACHE['timestamp']).total_seconds() / 60
|
53
|
+
if cache_age_minutes < _GLOBAL_ORGS_CACHE['ttl_minutes']:
|
54
|
+
console.print("[blue]🚀 Global performance optimization: Using cached Organizations data[/blue]")
|
55
|
+
return _GLOBAL_ORGS_CACHE['data']
|
56
|
+
return None
|
57
|
+
|
58
|
+
def _set_global_organizations_cache(data):
|
59
|
+
"""Cache Organizations data globally (module-level cache)."""
|
60
|
+
_GLOBAL_ORGS_CACHE['data'] = data
|
61
|
+
_GLOBAL_ORGS_CACHE['timestamp'] = datetime.now(timezone.utc)
|
62
|
+
accounts_count = len(data.get('accounts', {}).get('discovered_accounts', [])) if data else 0
|
63
|
+
console.print(f"[green]✅ Global Organizations cache: {accounts_count} accounts (TTL: {_GLOBAL_ORGS_CACHE['ttl_minutes']}min)[/green]")
|
64
|
+
|
40
65
|
# Enterprise 4-Profile AWS SSO Architecture (Proven FinOps Success Pattern)
|
41
66
|
ENTERPRISE_PROFILES = {
|
42
67
|
"BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185", # Cost Explorer access
|
@@ -210,6 +235,51 @@ class EnhancedOrganizationsDiscovery:
|
|
210
235
|
"performance_grade": None,
|
211
236
|
}
|
212
237
|
|
238
|
+
# Organizations discovery cache to prevent duplicate calls (performance optimization)
|
239
|
+
self._organizations_cache = None
|
240
|
+
self._organizations_cache_timestamp = None
|
241
|
+
self._cache_ttl_minutes = 30
|
242
|
+
|
243
|
+
def _is_organizations_cache_valid(self) -> bool:
|
244
|
+
"""Check if Organizations cache is still valid."""
|
245
|
+
if not self._organizations_cache_timestamp:
|
246
|
+
return False
|
247
|
+
|
248
|
+
from datetime import datetime, timedelta
|
249
|
+
cache_age_minutes = (datetime.now() - self._organizations_cache_timestamp).total_seconds() / 60
|
250
|
+
return cache_age_minutes < self._cache_ttl_minutes
|
251
|
+
|
252
|
+
async def discover_all_accounts(self) -> Dict:
|
253
|
+
"""
|
254
|
+
Cached wrapper for Organizations discovery to prevent duplicate API calls.
|
255
|
+
|
256
|
+
This method implements both global and instance-level caching to avoid the
|
257
|
+
performance penalty of duplicate Organizations API calls when multiple
|
258
|
+
components need the same account data.
|
259
|
+
"""
|
260
|
+
# Check global cache first (shared across all instances)
|
261
|
+
global_cached_result = _get_global_organizations_cache()
|
262
|
+
if global_cached_result:
|
263
|
+
return global_cached_result
|
264
|
+
|
265
|
+
# Check instance cache
|
266
|
+
if self._is_organizations_cache_valid() and self._organizations_cache:
|
267
|
+
console.print("[blue]🚀 Performance optimization: Using cached Organizations data[/blue]")
|
268
|
+
return self._organizations_cache
|
269
|
+
|
270
|
+
# Cache miss - perform discovery
|
271
|
+
console.print("[cyan]🔍 Performing Organizations discovery (cache miss)[/cyan]")
|
272
|
+
results = await self.discover_organization_structure()
|
273
|
+
|
274
|
+
# Cache the results
|
275
|
+
if results and results.get('accounts'):
|
276
|
+
self._organizations_cache = results
|
277
|
+
from datetime import datetime
|
278
|
+
self._organizations_cache_timestamp = datetime.now()
|
279
|
+
console.print(f"[green]✅ Cached Organizations data: {len(results.get('accounts', {}).get('discovered_accounts', []))} accounts (TTL: {self._cache_ttl_minutes}min)[/green]")
|
280
|
+
|
281
|
+
return results
|
282
|
+
|
213
283
|
def initialize_sessions(self) -> Dict[str, str]:
|
214
284
|
"""
|
215
285
|
Initialize AWS sessions with 4-profile architecture and comprehensive validation
|
@@ -350,6 +420,17 @@ class EnhancedOrganizationsDiscovery:
|
|
350
420
|
)
|
351
421
|
|
352
422
|
logger.info("🏢 Starting enhanced organization structure discovery with performance tracking")
|
423
|
+
|
424
|
+
# Check global cache first to prevent duplicate calls
|
425
|
+
cached_result = _get_global_organizations_cache()
|
426
|
+
if cached_result:
|
427
|
+
# Update metrics and return cached result
|
428
|
+
self.current_benchmark.finish(success=True)
|
429
|
+
self.discovery_metrics["performance_grade"] = self.current_benchmark.get_performance_grade()
|
430
|
+
self.discovery_metrics["end_time"] = self.current_benchmark.end_time
|
431
|
+
self.discovery_metrics["duration_seconds"] = self.current_benchmark.duration_seconds
|
432
|
+
return cached_result
|
433
|
+
|
353
434
|
self.discovery_metrics["start_time"] = self.current_benchmark.start_time
|
354
435
|
|
355
436
|
with Status("Initializing enterprise discovery...", console=console, spinner="dots"):
|
@@ -452,7 +533,7 @@ class EnhancedOrganizationsDiscovery:
|
|
452
533
|
performance_benchmark_dict = asdict(self.current_benchmark)
|
453
534
|
performance_benchmark_dict["performance_grade"] = self.current_benchmark.get_performance_grade()
|
454
535
|
|
455
|
-
|
536
|
+
discovery_result = {
|
456
537
|
"status": "completed",
|
457
538
|
"discovery_type": "enhanced_organization_structure",
|
458
539
|
"organization_info": org_info,
|
@@ -464,6 +545,11 @@ class EnhancedOrganizationsDiscovery:
|
|
464
545
|
"performance_benchmark": performance_benchmark_dict,
|
465
546
|
"timestamp": datetime.now().isoformat(),
|
466
547
|
}
|
548
|
+
|
549
|
+
# Cache the successful result to prevent duplicate calls
|
550
|
+
_set_global_organizations_cache(discovery_result)
|
551
|
+
|
552
|
+
return discovery_result
|
467
553
|
|
468
554
|
except Exception as e:
|
469
555
|
# Handle discovery failure
|
@@ -1313,3 +1399,7 @@ if __name__ == "__main__":
|
|
1313
1399
|
)
|
1314
1400
|
|
1315
1401
|
asyncio.run(main())
|
1402
|
+
|
1403
|
+
|
1404
|
+
# Alias for backward compatibility
|
1405
|
+
OrganizationsDiscoveryEngine = EnhancedOrganizationsDiscovery
|
@@ -348,13 +348,141 @@ def display_account_tree(accounts_data: Dict[str, Dict]) -> None:
|
|
348
348
|
console.print(tree)
|
349
349
|
|
350
350
|
|
351
|
+
def display_results_rich(
|
352
|
+
results_list: List[Dict[str, Any]],
|
353
|
+
fdisplay_dict: Dict[str, Dict],
|
354
|
+
defaultAction: Any = None,
|
355
|
+
file_to_save: Optional[str] = None,
|
356
|
+
subdisplay: bool = False,
|
357
|
+
title: str = "Inventory Results"
|
358
|
+
) -> None:
|
359
|
+
"""
|
360
|
+
Rich CLI replacement for legacy display_results function.
|
361
|
+
|
362
|
+
Provides backwards-compatible interface while using Rich formatting.
|
363
|
+
|
364
|
+
Args:
|
365
|
+
results_list: List of dictionaries with resource data
|
366
|
+
fdisplay_dict: Display configuration dictionary with format:
|
367
|
+
{'FieldName': {'DisplayOrder': 1, 'Heading': 'Display Name', 'Condition': [optional_filter]}}
|
368
|
+
defaultAction: Default value for missing fields
|
369
|
+
file_to_save: Optional filename to save results
|
370
|
+
subdisplay: Whether this is a sub-display (affects formatting)
|
371
|
+
title: Title for the table display
|
372
|
+
|
373
|
+
Example:
|
374
|
+
display_dict = {
|
375
|
+
'AccountId': {'DisplayOrder': 1, 'Heading': 'Account'},
|
376
|
+
'Region': {'DisplayOrder': 2, 'Heading': 'Region'},
|
377
|
+
'InstanceId': {'DisplayOrder': 3, 'Heading': 'Instance ID'}
|
378
|
+
}
|
379
|
+
display_results_rich(instance_data, display_dict, title="EC2 Instances")
|
380
|
+
"""
|
381
|
+
from datetime import datetime
|
382
|
+
|
383
|
+
if not results_list:
|
384
|
+
print_info("ℹ️ No results to display")
|
385
|
+
return
|
386
|
+
|
387
|
+
# Sort display fields by DisplayOrder
|
388
|
+
sorted_fields = sorted(fdisplay_dict.items(), key=lambda x: x[1].get('DisplayOrder', 999))
|
389
|
+
|
390
|
+
# Create Rich table
|
391
|
+
table = create_table(
|
392
|
+
title=f"📊 {title}",
|
393
|
+
caption=f"Found {len(results_list)} results • {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
394
|
+
)
|
395
|
+
|
396
|
+
# Add columns based on display dictionary
|
397
|
+
for field_name, field_config in sorted_fields:
|
398
|
+
heading = field_config.get('Heading', field_name)
|
399
|
+
table.add_column(heading, style="cyan" if "Id" in field_name else "white", no_wrap=True)
|
400
|
+
|
401
|
+
# Add rows
|
402
|
+
for result in results_list[:100]: # Limit to first 100 for performance
|
403
|
+
row_data = []
|
404
|
+
|
405
|
+
for field_name, field_config in sorted_fields:
|
406
|
+
# Apply condition filter if specified
|
407
|
+
condition = field_config.get('Condition', [])
|
408
|
+
if condition:
|
409
|
+
value = result.get(field_name, defaultAction)
|
410
|
+
if value not in condition:
|
411
|
+
continue
|
412
|
+
|
413
|
+
# Get field value with default fallback
|
414
|
+
value = result.get(field_name, defaultAction)
|
415
|
+
if value is None:
|
416
|
+
value = "N/A"
|
417
|
+
|
418
|
+
# Format value as string
|
419
|
+
row_data.append(str(value)[:50]) # Truncate long values
|
420
|
+
|
421
|
+
table.add_row(*row_data)
|
422
|
+
|
423
|
+
# Display the table
|
424
|
+
console.print(table)
|
425
|
+
|
426
|
+
# Show truncation notice if needed
|
427
|
+
if len(results_list) > 100:
|
428
|
+
console.print(f"[dim]Showing first 100 results. Total found: {len(results_list)}[/dim]")
|
429
|
+
|
430
|
+
# Save to file if requested
|
431
|
+
if file_to_save:
|
432
|
+
try:
|
433
|
+
import json
|
434
|
+
with open(file_to_save, 'w') as f:
|
435
|
+
json.dump(results_list, f, indent=2, default=str)
|
436
|
+
print_success(f"💾 Results saved to: {file_to_save}")
|
437
|
+
except Exception as e:
|
438
|
+
print_error(f"Failed to save file: {e}")
|
439
|
+
|
440
|
+
|
441
|
+
def display_progress_rich(current: int, total: int, description: str = "Processing") -> None:
|
442
|
+
"""
|
443
|
+
Rich CLI replacement for legacy progress indicators.
|
444
|
+
|
445
|
+
Args:
|
446
|
+
current: Current progress count
|
447
|
+
total: Total items to process
|
448
|
+
description: Description of the operation
|
449
|
+
"""
|
450
|
+
percentage = (current / total * 100) if total > 0 else 0
|
451
|
+
console.print(f"[cyan]{description}:[/cyan] {current}/{total} ({percentage:.1f}%)", end="\r")
|
452
|
+
|
453
|
+
|
454
|
+
def print_colorized_rich(text: str, color: str = "white") -> None:
|
455
|
+
"""
|
456
|
+
Rich CLI replacement for colorama print statements.
|
457
|
+
|
458
|
+
Args:
|
459
|
+
text: Text to print
|
460
|
+
color: Color name (red, green, yellow, cyan, blue, white)
|
461
|
+
"""
|
462
|
+
color_map = {
|
463
|
+
"red": "red",
|
464
|
+
"green": "green",
|
465
|
+
"yellow": "yellow",
|
466
|
+
"cyan": "cyan",
|
467
|
+
"blue": "blue",
|
468
|
+
"white": "white",
|
469
|
+
"magenta": "magenta"
|
470
|
+
}
|
471
|
+
|
472
|
+
rich_color = color_map.get(color.lower(), "white")
|
473
|
+
console.print(f"[{rich_color}]{text}[/{rich_color}]")
|
474
|
+
|
475
|
+
|
351
476
|
# Export public functions
|
352
477
|
__all__ = [
|
353
478
|
"display_inventory_header",
|
354
|
-
"create_inventory_progress",
|
479
|
+
"create_inventory_progress",
|
355
480
|
"display_ec2_inventory_results",
|
356
481
|
"display_generic_inventory_results",
|
357
482
|
"display_inventory_error",
|
358
483
|
"display_multi_resource_summary",
|
359
484
|
"display_account_tree",
|
485
|
+
"display_results_rich",
|
486
|
+
"display_progress_rich",
|
487
|
+
"print_colorized_rich",
|
360
488
|
]
|