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
runbooks/common/profile_utils.py
CHANGED
@@ -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
|
-
|
75
|
-
|
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
|
]
|
runbooks/common/rich_utils.py
CHANGED
@@ -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 =
|
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 =
|
69
|
-
annual_cost: float =
|
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
|
122
|
-
|
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
|
-
|
365
|
-
|
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 =
|
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 =
|
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 (
|
347
|
-
|
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
|