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.
Files changed (75) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  3. runbooks/cloudops/cost_optimizer.py +95 -33
  4. runbooks/common/aws_pricing.py +388 -0
  5. runbooks/common/aws_pricing_api.py +205 -0
  6. runbooks/common/aws_utils.py +2 -2
  7. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  8. runbooks/common/cross_account_manager.py +606 -0
  9. runbooks/common/enhanced_exception_handler.py +4 -0
  10. runbooks/common/env_utils.py +96 -0
  11. runbooks/common/mcp_integration.py +49 -2
  12. runbooks/common/organizations_client.py +579 -0
  13. runbooks/common/profile_utils.py +96 -2
  14. runbooks/common/rich_utils.py +3 -0
  15. runbooks/finops/cost_optimizer.py +2 -1
  16. runbooks/finops/elastic_ip_optimizer.py +13 -9
  17. runbooks/finops/embedded_mcp_validator.py +31 -0
  18. runbooks/finops/enhanced_trend_visualization.py +3 -2
  19. runbooks/finops/markdown_exporter.py +441 -0
  20. runbooks/finops/nat_gateway_optimizer.py +57 -20
  21. runbooks/finops/optimizer.py +2 -0
  22. runbooks/finops/single_dashboard.py +2 -2
  23. runbooks/finops/vpc_cleanup_exporter.py +330 -0
  24. runbooks/finops/vpc_cleanup_optimizer.py +895 -40
  25. runbooks/inventory/__init__.py +10 -1
  26. runbooks/inventory/cloud_foundations_integration.py +409 -0
  27. runbooks/inventory/core/collector.py +1148 -88
  28. runbooks/inventory/discovery.md +389 -0
  29. runbooks/inventory/drift_detection_cli.py +327 -0
  30. runbooks/inventory/inventory_mcp_cli.py +171 -0
  31. runbooks/inventory/inventory_modules.py +4 -7
  32. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  33. runbooks/inventory/mcp_vpc_validator.py +23 -6
  34. runbooks/inventory/organizations_discovery.py +91 -1
  35. runbooks/inventory/rich_inventory_display.py +129 -1
  36. runbooks/inventory/unified_validation_engine.py +1292 -0
  37. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  38. runbooks/inventory/vpc_analyzer.py +825 -7
  39. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  40. runbooks/main.py +969 -42
  41. runbooks/monitoring/performance_monitor.py +11 -7
  42. runbooks/operate/dynamodb_operations.py +6 -5
  43. runbooks/operate/ec2_operations.py +3 -2
  44. runbooks/operate/networking_cost_heatmap.py +4 -3
  45. runbooks/operate/s3_operations.py +13 -12
  46. runbooks/operate/vpc_operations.py +50 -2
  47. runbooks/remediation/base.py +1 -1
  48. runbooks/remediation/commvault_ec2_analysis.py +6 -1
  49. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  50. runbooks/remediation/rds_snapshot_list.py +5 -3
  51. runbooks/validation/__init__.py +21 -1
  52. runbooks/validation/comprehensive_2way_validator.py +1996 -0
  53. runbooks/validation/mcp_validator.py +904 -94
  54. runbooks/validation/terraform_citations_validator.py +363 -0
  55. runbooks/validation/terraform_drift_detector.py +1098 -0
  56. runbooks/vpc/cleanup_wrapper.py +231 -10
  57. runbooks/vpc/config.py +310 -62
  58. runbooks/vpc/cross_account_session.py +308 -0
  59. runbooks/vpc/heatmap_engine.py +96 -29
  60. runbooks/vpc/manager_interface.py +9 -9
  61. runbooks/vpc/mcp_no_eni_validator.py +1551 -0
  62. runbooks/vpc/networking_wrapper.py +14 -8
  63. runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
  64. runbooks/vpc/runbooks.security.report_generator.log +0 -0
  65. runbooks/vpc/runbooks.security.run_script.log +0 -0
  66. runbooks/vpc/runbooks.security.security_export.log +0 -0
  67. runbooks/vpc/tests/test_cost_engine.py +1 -1
  68. runbooks/vpc/unified_scenarios.py +3269 -0
  69. runbooks/vpc/vpc_cleanup_integration.py +516 -82
  70. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
  71. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/RECORD +75 -51
  72. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
  73. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
  74. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
  75. {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
- if mcp_value == 0:
786
- return 100.0 if runbooks_value == 0 else 0.0
785
+ # Perfect match
786
+ if runbooks_value == mcp_value:
787
+ return 100.0
787
788
 
788
- variance = abs(runbooks_value - mcp_value) / max(abs(mcp_value), 1)
789
- accuracy = max(0, 100 - (variance * 100))
790
- return min(100.0, accuracy)
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
- return {
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
  ]