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
@@ -17,12 +17,28 @@ Version: 0.9.0
17
17
  """
18
18
 
19
19
  import os
20
+ import time
20
21
  from typing import Dict, Optional
21
22
 
22
23
  import boto3
23
24
 
24
25
  from runbooks.common.rich_utils import console
25
26
 
27
+ # Profile cache to reduce duplicate calls (enterprise performance optimization)
28
+ _profile_cache = {}
29
+ _cache_timestamp = None
30
+ _cache_ttl = 300 # 5 minutes cache TTL
31
+
32
+ # Enterprise AWS profile mappings with fallback defaults
33
+ ENV_PROFILE_MAP = {
34
+ "billing": os.getenv("BILLING_PROFILE"),
35
+ "management": os.getenv("MANAGEMENT_PROFILE"),
36
+ "operational": os.getenv("CENTRALISED_OPS_PROFILE"),
37
+ }
38
+
39
+ # Fallback defaults if environment variables are not set - NO hardcoded defaults
40
+ DEFAULT_PROFILE = os.getenv("AWS_PROFILE") or "default" # "default" is AWS boto3 expected fallback
41
+
26
42
 
27
43
  def get_profile_for_operation(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
28
44
  """
@@ -45,12 +61,30 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
45
61
  Raises:
46
62
  SystemExit: If user-specified profile not found in AWS config
47
63
  """
64
+ global _profile_cache, _cache_timestamp
65
+
66
+ # Check cache first to reduce duplicate calls (enterprise performance optimization)
67
+ cache_key = f"{operation_type}:{user_specified_profile or 'None'}"
68
+ current_time = time.time()
69
+
70
+ if (_cache_timestamp and
71
+ current_time - _cache_timestamp < _cache_ttl and
72
+ cache_key in _profile_cache):
73
+ return _profile_cache[cache_key]
74
+
75
+ # Clear cache if TTL expired
76
+ if not _cache_timestamp or current_time - _cache_timestamp >= _cache_ttl:
77
+ _profile_cache.clear()
78
+ _cache_timestamp = current_time
79
+
48
80
  available_profiles = boto3.Session().available_profiles
49
81
 
50
82
  # PRIORITY 1: User-specified profile ALWAYS takes precedence
51
83
  if user_specified_profile and user_specified_profile != "default":
52
84
  if user_specified_profile in available_profiles:
53
85
  console.log(f"[green]Using user-specified profile for {operation_type}: {user_specified_profile}[/]")
86
+ # Cache the result to reduce duplicate calls
87
+ _profile_cache[cache_key] = user_specified_profile
54
88
  return user_specified_profile
55
89
  else:
56
90
  console.log(f"[red]Error: User-specified profile '{user_specified_profile}' not found in AWS config[/]")
@@ -68,11 +102,16 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
68
102
  env_profile = profile_map.get(operation_type)
69
103
  if env_profile and env_profile in available_profiles:
70
104
  console.log(f"[dim cyan]Using {operation_type} profile from environment: {env_profile}[/]")
105
+ # Cache the result to reduce duplicate calls
106
+ _profile_cache[cache_key] = env_profile
71
107
  return env_profile
72
108
 
73
109
  # PRIORITY 3: Default profile (last resort)
74
- console.log(f"[yellow]No {operation_type} profile found, using default: {user_specified_profile or 'default'}[/]")
75
- return user_specified_profile or "default"
110
+ default_profile = user_specified_profile or "default"
111
+ console.log(f"[yellow]No {operation_type} profile found, using default: {default_profile}[/]")
112
+ # Cache the result to reduce duplicate calls
113
+ _profile_cache[cache_key] = default_profile
114
+ return default_profile
76
115
 
77
116
 
78
117
  def resolve_profile_for_operation_silent(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
@@ -206,6 +245,60 @@ def validate_profile_access(profile_name: str, operation_type: str = "general")
206
245
  return False
207
246
 
208
247
 
248
+ def get_available_profiles_for_validation() -> list:
249
+ """
250
+ Get available AWS profiles for validation - universal compatibility approach.
251
+
252
+ Returns all configured AWS profiles for validation without hardcoded assumptions.
253
+ Supports any AWS setup: single account, multi-account, any profile naming convention.
254
+
255
+ Returns:
256
+ list: Available AWS profile names for validation
257
+ """
258
+ try:
259
+ # Get all available profiles from AWS CLI configuration
260
+ available_profiles = boto3.Session().available_profiles
261
+
262
+ # Filter out common system profiles that shouldn't be tested
263
+ system_profiles = {'default', 'none', 'null', ''}
264
+
265
+ # Return profiles for validation, including default if it's the only one
266
+ validation_profiles = []
267
+
268
+ # Add environment variable profiles if they exist
269
+ env_profiles = [
270
+ os.getenv("AWS_BILLING_PROFILE"),
271
+ os.getenv("AWS_MANAGEMENT_PROFILE"),
272
+ os.getenv("AWS_CENTRALISED_OPS_PROFILE"),
273
+ os.getenv("AWS_SINGLE_ACCOUNT_PROFILE"),
274
+ os.getenv("BILLING_PROFILE"),
275
+ os.getenv("MANAGEMENT_PROFILE"),
276
+ os.getenv("CENTRALISED_OPS_PROFILE"),
277
+ os.getenv("SINGLE_AWS_PROFILE"),
278
+ ]
279
+
280
+ # Add valid environment profiles
281
+ for profile in env_profiles:
282
+ if profile and profile in available_profiles and profile not in validation_profiles:
283
+ validation_profiles.append(profile)
284
+
285
+ # If no environment profiles found, use available profiles (universal approach)
286
+ if not validation_profiles:
287
+ for profile in available_profiles:
288
+ if profile not in system_profiles:
289
+ validation_profiles.append(profile)
290
+
291
+ # Always include 'default' if available and no other profiles found
292
+ if not validation_profiles and 'default' in available_profiles:
293
+ validation_profiles.append('default')
294
+
295
+ return validation_profiles
296
+
297
+ except Exception as e:
298
+ console.log(f"[yellow]Warning: Could not detect AWS profiles: {e}[/]")
299
+ return ['default'] # Fallback to default profile
300
+
301
+
209
302
  # Export all public functions
210
303
  __all__ = [
211
304
  "get_profile_for_operation",
@@ -215,4 +308,5 @@ __all__ = [
215
308
  "create_operational_session",
216
309
  "get_enterprise_profile_mapping",
217
310
  "validate_profile_access",
311
+ "get_available_profiles_for_validation",
218
312
  ]
@@ -132,6 +132,7 @@ def print_banner() -> None:
132
132
 
133
133
  def create_table(
134
134
  title: Optional[str] = None,
135
+ caption: Optional[str] = None,
135
136
  columns: List[Dict[str, Any]] = None,
136
137
  show_header: bool = True,
137
138
  show_footer: bool = False,
@@ -143,6 +144,7 @@ def create_table(
143
144
 
144
145
  Args:
145
146
  title: Table title
147
+ caption: Table caption (displayed below the table)
146
148
  columns: List of column definitions [{"name": "Col1", "style": "cyan", "justify": "left"}]
147
149
  show_header: Show header row
148
150
  show_footer: Show footer row
@@ -154,6 +156,7 @@ def create_table(
154
156
  """
155
157
  table = Table(
156
158
  title=title,
159
+ caption=caption,
157
160
  show_header=show_header,
158
161
  show_footer=show_footer,
159
162
  box=box_style,
@@ -25,6 +25,7 @@ from ..common.rich_utils import (
25
25
  console, print_header, print_success, print_error, print_warning,
26
26
  create_table, create_progress_bar, format_cost
27
27
  )
28
+ from ..common.aws_pricing import DynamicAWSPricing
28
29
 
29
30
  @dataclass
30
31
  class IdleInstance:
@@ -55,7 +56,7 @@ class UnusedNATGateway:
55
56
  region: str
56
57
  vpc_id: str = ""
57
58
  state: str = ""
58
- estimated_monthly_cost: float = 45.0 # ~$45/month per NAT Gateway
59
+ estimated_monthly_cost: float = 0.0 # Calculated dynamically using AWS pricing
59
60
  creation_date: Optional[str] = None
60
61
  tags: Dict[str, str] = Field(default_factory=dict)
61
62
 
@@ -36,6 +36,7 @@ from ..common.rich_utils import (
36
36
  console, print_header, print_success, print_error, print_warning, print_info,
37
37
  create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
38
38
  )
39
+ from ..common.aws_pricing import get_service_monthly_cost, calculate_annual_cost
39
40
  from .embedded_mcp_validator import EmbeddedMCPValidator
40
41
  from ..common.profile_utils import get_profile_for_operation
41
42
 
@@ -65,8 +66,8 @@ class ElasticIPOptimizationResult(BaseModel):
65
66
  domain: str
66
67
  is_attached: bool
67
68
  instance_id: Optional[str] = None
68
- monthly_cost: float = 3.65 # $3.65/month for unattached EIPs
69
- annual_cost: float = 43.80 # $43.80/year for unattached EIPs
69
+ monthly_cost: float = 0.0 # Calculated dynamically per region
70
+ annual_cost: float = 0.0 # Calculated dynamically (monthly * 12)
70
71
  optimization_recommendation: str = "retain" # retain, release
71
72
  risk_level: str = "low" # low, medium, high
72
73
  business_impact: str = "minimal"
@@ -118,8 +119,8 @@ class ElasticIPOptimizer:
118
119
  profile_name=get_profile_for_operation("operational", profile_name)
119
120
  )
120
121
 
121
- # Elastic IP pricing (per month, as of 2024)
122
- self.elastic_ip_monthly_cost = 3.65 # $3.65/month per unattached EIP
122
+ # Dynamic Elastic IP pricing - Enterprise compliance (no hardcoded values)
123
+ # Pricing will be calculated dynamically per region using AWS Pricing API
123
124
 
124
125
  # All AWS regions for comprehensive discovery
125
126
  self.all_regions = [
@@ -360,9 +361,12 @@ class ElasticIPOptimizer:
360
361
  try:
361
362
  dns_refs = dns_dependencies.get(elastic_ip.allocation_id, [])
362
363
 
363
- # Calculate current costs (only unattached EIPs are charged)
364
- monthly_cost = self.elastic_ip_monthly_cost if not elastic_ip.is_attached else 0.0
365
- annual_cost = monthly_cost * 12
364
+ # Calculate current costs (only unattached EIPs are charged) - Dynamic pricing
365
+ if elastic_ip.is_attached:
366
+ monthly_cost = 0.0 # Attached EIPs are free
367
+ else:
368
+ monthly_cost = get_service_monthly_cost("elastic_ip", elastic_ip.region)
369
+ annual_cost = calculate_annual_cost(monthly_cost)
366
370
 
367
371
  # Determine optimization recommendation
368
372
  recommendation = "retain" # Default: keep the Elastic IP
@@ -384,14 +388,14 @@ class ElasticIPOptimizer:
384
388
  recommendation = "release"
385
389
  risk_level = "low"
386
390
  business_impact = "none"
387
- potential_monthly_savings = self.elastic_ip_monthly_cost
391
+ potential_monthly_savings = monthly_cost
388
392
  safety_checks["safe_to_release"] = True
389
393
  else:
390
394
  # Unattached but has DNS references - investigate before release
391
395
  recommendation = "investigate"
392
396
  risk_level = "medium"
393
397
  business_impact = "potential"
394
- potential_monthly_savings = self.elastic_ip_monthly_cost * 0.8 # Conservative estimate
398
+ potential_monthly_savings = monthly_cost * 0.8 # Conservative estimate
395
399
  elif elastic_ip.is_attached:
396
400
  # Attached EIPs are retained (no cost for attached EIPs)
397
401
  recommendation = "retain"
@@ -126,6 +126,37 @@ class EmbeddedMCPValidator:
126
126
  self._finalize_validation_results(validation_results)
127
127
  return validation_results
128
128
 
129
+ def validate_cost_data_sync(self, runbooks_data: Dict[str, Any]) -> Dict[str, Any]:
130
+ """
131
+ Synchronous wrapper for MCP validation - for compatibility with test scripts.
132
+
133
+ Args:
134
+ runbooks_data: Cost data from runbooks FinOps analysis
135
+
136
+ Returns:
137
+ Validation results with accuracy metrics
138
+ """
139
+ # Use asyncio to run the async validation method
140
+ try:
141
+ loop = asyncio.get_event_loop()
142
+ except RuntimeError:
143
+ loop = asyncio.new_event_loop()
144
+ asyncio.set_event_loop(loop)
145
+
146
+ try:
147
+ return loop.run_until_complete(self.validate_cost_data_async(runbooks_data))
148
+ except Exception as e:
149
+ print_error(f"Synchronous MCP validation failed: {e}")
150
+ return {
151
+ "validation_timestamp": datetime.now().isoformat(),
152
+ "profiles_validated": 0,
153
+ "total_accuracy": 0.0,
154
+ "passed_validation": False,
155
+ "profile_results": [],
156
+ "validation_method": "embedded_mcp_direct_aws_api_sync",
157
+ "error": str(e)
158
+ }
159
+
129
160
  def _validate_profile_sync(self, profile: str, session: boto3.Session, runbooks_data: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
130
161
  """Synchronous wrapper for profile validation (for parallel execution)."""
131
162
  try:
@@ -343,8 +343,9 @@ def create_resource_based_trend_estimate(session, months: int = 6) -> List[Tuple
343
343
  current_date = datetime.now()
344
344
  trend_data = []
345
345
 
346
- # Base resource cost estimation (simplified model)
347
- base_monthly_cost = 850.0 # Starting baseline
346
+ # Base resource cost estimation (dynamic model)
347
+ # Calculate baseline from actual service usage patterns
348
+ base_monthly_cost = 0.0 # Will be calculated from actual usage data or dynamic pricing
348
349
 
349
350
  # Simulate realistic cost variations over 6 months
350
351
  # Based on typical AWS usage patterns