runbooks 1.0.0__py3-none-any.whl → 1.0.1__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/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1070 -105
- runbooks/common/date_utils.py +115 -0
- runbooks/common/enhanced_exception_handler.py +10 -7
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/profile_utils.py +76 -115
- runbooks/common/rich_utils.py +3 -3
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/nat_gateway_optimizer.py +46 -27
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_optimizer.py +22 -29
- runbooks/inventory/core/collector.py +51 -28
- runbooks/inventory/discovery.md +197 -247
- runbooks/inventory/inventory_modules.py +2 -2
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/organizations_discovery.py +13 -8
- runbooks/inventory/unified_validation_engine.py +2 -15
- runbooks/main.py +74 -32
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +17 -13
- runbooks/operate/vpc_operations.py +52 -12
- runbooks/remediation/base.py +3 -1
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +66 -18
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- runbooks/security/compliance_automation_engine.py +99 -20
- runbooks/security/config/__init__.py +24 -0
- runbooks/security/config/compliance_config.py +255 -0
- runbooks/security/config/compliance_weights_example.json +22 -0
- runbooks/security/config_template_generator.py +500 -0
- runbooks/security/security_cli.py +377 -0
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +26 -15
- runbooks/validation/mcp_validator.py +62 -8
- runbooks/vpc/config.py +32 -7
- runbooks/vpc/cross_account_session.py +5 -1
- runbooks/vpc/heatmap_engine.py +21 -14
- runbooks/vpc/mcp_no_eni_validator.py +115 -36
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +3 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/METADATA +1 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/RECORD +63 -65
- 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/tests/results_test_finops_dashboard.xml +0 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- 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-1.0.0.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Dynamic Date Utilities for CloudOps Runbooks Platform
|
4
|
+
|
5
|
+
Replaces hardcoded 2024 dates with dynamic date generation following manager's
|
6
|
+
"No hardcoded values" requirement. Supports current month/year calculations
|
7
|
+
for all test data and AWS API time period generation.
|
8
|
+
|
9
|
+
Strategic Alignment: "Do one thing and do it well" - Focused date utility
|
10
|
+
KISS Principle: Simple, reusable date functions for all modules
|
11
|
+
"""
|
12
|
+
|
13
|
+
from datetime import datetime, timedelta
|
14
|
+
from typing import Dict, Tuple
|
15
|
+
|
16
|
+
|
17
|
+
def get_current_year() -> int:
|
18
|
+
"""Get current year dynamically."""
|
19
|
+
return datetime.now().year
|
20
|
+
|
21
|
+
|
22
|
+
def get_current_month_period() -> Dict[str, str]:
|
23
|
+
"""
|
24
|
+
Generate current month's start and end dates for AWS API calls.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
28
|
+
"""
|
29
|
+
now = datetime.now()
|
30
|
+
start_date = now.replace(day=1).strftime('%Y-%m-%d')
|
31
|
+
|
32
|
+
# Get last day of current month
|
33
|
+
if now.month == 12:
|
34
|
+
next_month = now.replace(year=now.year + 1, month=1, day=1)
|
35
|
+
else:
|
36
|
+
next_month = now.replace(month=now.month + 1, day=1)
|
37
|
+
|
38
|
+
end_date = (next_month - timedelta(days=1)).strftime('%Y-%m-%d')
|
39
|
+
|
40
|
+
return {
|
41
|
+
'Start': start_date,
|
42
|
+
'End': end_date
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
def get_previous_month_period() -> Dict[str, str]:
|
47
|
+
"""
|
48
|
+
Generate previous month's start and end dates for AWS API calls.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
52
|
+
"""
|
53
|
+
now = datetime.now()
|
54
|
+
|
55
|
+
# Get first day of previous month
|
56
|
+
if now.month == 1:
|
57
|
+
prev_month = now.replace(year=now.year - 1, month=12, day=1)
|
58
|
+
else:
|
59
|
+
prev_month = now.replace(month=now.month - 1, day=1)
|
60
|
+
|
61
|
+
start_date = prev_month.strftime('%Y-%m-%d')
|
62
|
+
|
63
|
+
# Get last day of previous month
|
64
|
+
end_date = (now.replace(day=1) - timedelta(days=1)).strftime('%Y-%m-%d')
|
65
|
+
|
66
|
+
return {
|
67
|
+
'Start': start_date,
|
68
|
+
'End': end_date
|
69
|
+
}
|
70
|
+
|
71
|
+
|
72
|
+
def get_test_date_period(days_back: int = 30) -> Dict[str, str]:
|
73
|
+
"""
|
74
|
+
Generate test date periods for consistent test data.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
days_back: Number of days back from today for start date
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
81
|
+
"""
|
82
|
+
end_date = datetime.now().strftime('%Y-%m-%d')
|
83
|
+
start_date = (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d')
|
84
|
+
|
85
|
+
return {
|
86
|
+
'Start': start_date,
|
87
|
+
'End': end_date
|
88
|
+
}
|
89
|
+
|
90
|
+
|
91
|
+
def get_aws_cli_example_period() -> Tuple[str, str]:
|
92
|
+
"""
|
93
|
+
Generate example date period for AWS CLI documentation.
|
94
|
+
Uses yesterday and today to ensure valid time range.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Tuple of (start_date, end_date) in YYYY-MM-DD format
|
98
|
+
"""
|
99
|
+
today = datetime.now()
|
100
|
+
yesterday = today - timedelta(days=1)
|
101
|
+
|
102
|
+
return (
|
103
|
+
yesterday.strftime('%Y-%m-%d'),
|
104
|
+
today.strftime('%Y-%m-%d')
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
def get_collection_timestamp() -> str:
|
109
|
+
"""
|
110
|
+
Generate collection timestamp for test data.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
ISO format timestamp string
|
114
|
+
"""
|
115
|
+
return datetime.now().isoformat()
|
@@ -972,13 +972,15 @@ class EnterpriseExceptionHandler:
|
|
972
972
|
}
|
973
973
|
|
974
974
|
def _initialize_profile_recommendations(self) -> Dict[str, List[str]]:
|
975
|
-
"""Initialize profile recommendations for different operations."""
|
975
|
+
"""Initialize profile recommendations for different operations with universal support."""
|
976
|
+
import os
|
977
|
+
# Environment variable-driven profile recommendations
|
976
978
|
return {
|
977
|
-
"inventory": ["
|
978
|
-
"operate": ["
|
979
|
-
"finops": ["
|
980
|
-
"security": ["
|
981
|
-
"cfat": ["
|
979
|
+
"inventory": [os.getenv("MANAGEMENT_PROFILE", "management-profile"), os.getenv("CENTRALISED_OPS_PROFILE", "ops-profile")],
|
980
|
+
"operate": [os.getenv("CENTRALISED_OPS_PROFILE", "ops-profile"), os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
981
|
+
"finops": [os.getenv("BILLING_PROFILE", "billing-profile"), os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
982
|
+
"security": [os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
983
|
+
"cfat": [os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
982
984
|
}
|
983
985
|
|
984
986
|
def _analyze_aws_error(self, error: ClientError, error_code: str, service: str, operation: str) -> Dict[str, Any]:
|
@@ -991,7 +993,8 @@ class EnterpriseExceptionHandler:
|
|
991
993
|
# Service-specific adjustments
|
992
994
|
if service == "ce" and error_code == "AccessDenied":
|
993
995
|
# Cost Explorer requires special billing permissions
|
994
|
-
|
996
|
+
import os
|
997
|
+
base_analysis["recommended_profiles"] = [os.getenv("BILLING_PROFILE", "billing-profile")]
|
995
998
|
|
996
999
|
return base_analysis
|
997
1000
|
|
@@ -79,10 +79,11 @@ class MCPCostExplorerIntegration:
|
|
79
79
|
performance_target_seconds: Performance target for operations
|
80
80
|
"""
|
81
81
|
|
82
|
-
# Profile configuration with
|
83
|
-
|
84
|
-
self.
|
85
|
-
self.
|
82
|
+
# Profile configuration with universal environment support
|
83
|
+
from runbooks.common.profile_utils import get_profile_for_operation
|
84
|
+
self.billing_profile = billing_profile or get_profile_for_operation("billing", None)
|
85
|
+
self.management_profile = management_profile or get_profile_for_operation("management", None)
|
86
|
+
self.single_account_profile = single_account_profile or get_profile_for_operation("single_account", None)
|
86
87
|
|
87
88
|
# Validation configuration
|
88
89
|
self.tolerance_percent = tolerance_percent
|
runbooks/common/profile_utils.py
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
Profile Management
|
3
|
+
Universal AWS Profile Management for CloudOps Runbooks Platform
|
4
4
|
|
5
|
-
This module provides
|
6
|
-
|
5
|
+
This module provides truly universal AWS profile management that works with ANY AWS setup:
|
6
|
+
- Single account setups
|
7
|
+
- Multi-account setups
|
8
|
+
- Any profile naming convention
|
9
|
+
- No specific environment variable requirements
|
7
10
|
|
8
11
|
Features:
|
9
|
-
-
|
10
|
-
-
|
11
|
-
-
|
12
|
-
-
|
13
|
-
- Profile validation and error handling
|
12
|
+
- Universal compatibility: User --profile → AWS_PROFILE → "default"
|
13
|
+
- Works with ANY AWS profile names (not just specific test profiles)
|
14
|
+
- No hardcoded environment variable assumptions
|
15
|
+
- Simple, reliable profile selection for all users
|
14
16
|
|
15
17
|
Author: CloudOps Runbooks Team
|
16
|
-
Version: 0.
|
18
|
+
Version: 1.0.0 - Universal Compatibility
|
17
19
|
"""
|
18
20
|
|
19
21
|
import os
|
@@ -24,35 +26,25 @@ import boto3
|
|
24
26
|
|
25
27
|
from runbooks.common.rich_utils import console
|
26
28
|
|
27
|
-
# Profile cache to reduce duplicate calls (
|
29
|
+
# Profile cache to reduce duplicate calls (performance optimization)
|
28
30
|
_profile_cache = {}
|
29
31
|
_cache_timestamp = None
|
30
32
|
_cache_ttl = 300 # 5 minutes cache TTL
|
31
33
|
|
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
|
-
|
42
34
|
|
43
35
|
def get_profile_for_operation(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
|
44
36
|
"""
|
45
|
-
|
37
|
+
Universal AWS profile selection that works with ANY AWS setup.
|
46
38
|
|
47
|
-
PRIORITY ORDER (
|
39
|
+
SIMPLE PRIORITY ORDER (Universal Compatibility):
|
48
40
|
1. User-specified profile (--profile parameter) - HIGHEST PRIORITY
|
49
|
-
2.
|
50
|
-
3.
|
41
|
+
2. AWS_PROFILE environment variable - STANDARD AWS CONVENTION
|
42
|
+
3. "default" profile - AWS STANDARD FALLBACK
|
51
43
|
|
52
|
-
|
44
|
+
Works with ANY profile names and ANY AWS setup - no specific environment variable requirements.
|
53
45
|
|
54
46
|
Args:
|
55
|
-
operation_type: Type of operation (
|
47
|
+
operation_type: Type of operation (informational only, not used for profile selection)
|
56
48
|
user_specified_profile: Profile specified by user via --profile parameter
|
57
49
|
|
58
50
|
Returns:
|
@@ -63,7 +55,7 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
|
|
63
55
|
"""
|
64
56
|
global _profile_cache, _cache_timestamp
|
65
57
|
|
66
|
-
# Check cache first to reduce duplicate calls (
|
58
|
+
# Check cache first to reduce duplicate calls (performance optimization)
|
67
59
|
cache_key = f"{operation_type}:{user_specified_profile or 'None'}"
|
68
60
|
current_time = time.time()
|
69
61
|
|
@@ -82,33 +74,26 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
|
|
82
74
|
# PRIORITY 1: User-specified profile ALWAYS takes precedence
|
83
75
|
if user_specified_profile and user_specified_profile != "default":
|
84
76
|
if user_specified_profile in available_profiles:
|
85
|
-
console.log(f"[green]Using user-specified profile
|
77
|
+
console.log(f"[green]Using user-specified profile: {user_specified_profile}[/]")
|
86
78
|
# Cache the result to reduce duplicate calls
|
87
79
|
_profile_cache[cache_key] = user_specified_profile
|
88
80
|
return user_specified_profile
|
89
81
|
else:
|
90
|
-
console.log(f"[red]Error:
|
91
|
-
|
82
|
+
console.log(f"[red]Error: Profile '{user_specified_profile}' not found in AWS config[/]")
|
83
|
+
console.log(f"[yellow]Available profiles: {', '.join(available_profiles)}[/]")
|
92
84
|
raise SystemExit(1)
|
93
85
|
|
94
|
-
# PRIORITY 2:
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
"operational": os.getenv("AWS_CENTRALISED_OPS_PROFILE") or os.getenv("CENTRALISED_OPS_PROFILE"),
|
99
|
-
"single_account": os.getenv("AWS_SINGLE_ACCOUNT_PROFILE") or os.getenv("SINGLE_AWS_PROFILE"),
|
100
|
-
}
|
101
|
-
|
102
|
-
env_profile = profile_map.get(operation_type)
|
103
|
-
if env_profile and env_profile in available_profiles:
|
104
|
-
console.log(f"[dim cyan]Using {operation_type} profile from environment: {env_profile}[/]")
|
86
|
+
# PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
|
87
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
88
|
+
if aws_profile and aws_profile in available_profiles:
|
89
|
+
console.log(f"[dim cyan]Using AWS_PROFILE environment variable: {aws_profile}[/]")
|
105
90
|
# Cache the result to reduce duplicate calls
|
106
|
-
_profile_cache[cache_key] =
|
107
|
-
return
|
91
|
+
_profile_cache[cache_key] = aws_profile
|
92
|
+
return aws_profile
|
108
93
|
|
109
|
-
# PRIORITY 3: Default profile (
|
110
|
-
default_profile =
|
111
|
-
console.log(f"[yellow]
|
94
|
+
# PRIORITY 3: Default profile (AWS standard fallback)
|
95
|
+
default_profile = "default"
|
96
|
+
console.log(f"[yellow]Using default AWS profile: {default_profile}[/]")
|
112
97
|
# Cache the result to reduce duplicate calls
|
113
98
|
_profile_cache[cache_key] = default_profile
|
114
99
|
return default_profile
|
@@ -116,11 +101,11 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
|
|
116
101
|
|
117
102
|
def resolve_profile_for_operation_silent(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
|
118
103
|
"""
|
119
|
-
|
120
|
-
Uses the same logic as get_profile_for_operation but without console output.
|
104
|
+
Universal AWS profile resolution without logging (for display purposes).
|
105
|
+
Uses the same universal logic as get_profile_for_operation but without console output.
|
121
106
|
|
122
107
|
Args:
|
123
|
-
operation_type: Type of operation (
|
108
|
+
operation_type: Type of operation (informational only, not used for profile selection)
|
124
109
|
user_specified_profile: Profile specified by user via --profile parameter
|
125
110
|
|
126
111
|
Returns:
|
@@ -139,78 +124,72 @@ def resolve_profile_for_operation_silent(operation_type: str, user_specified_pro
|
|
139
124
|
# Don't fall back - user explicitly chose this profile
|
140
125
|
raise SystemExit(1)
|
141
126
|
|
142
|
-
# PRIORITY 2:
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
"operational": os.getenv("AWS_CENTRALISED_OPS_PROFILE") or os.getenv("CENTRALISED_OPS_PROFILE"),
|
147
|
-
"single_account": os.getenv("AWS_SINGLE_ACCOUNT_PROFILE") or os.getenv("SINGLE_AWS_PROFILE"),
|
148
|
-
}
|
149
|
-
|
150
|
-
env_profile = profile_map.get(operation_type)
|
151
|
-
if env_profile and env_profile in available_profiles:
|
152
|
-
return env_profile
|
127
|
+
# PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
|
128
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
129
|
+
if aws_profile and aws_profile in available_profiles:
|
130
|
+
return aws_profile
|
153
131
|
|
154
|
-
# PRIORITY 3: Default profile (
|
155
|
-
return
|
132
|
+
# PRIORITY 3: Default profile (AWS standard fallback)
|
133
|
+
return "default"
|
156
134
|
|
157
135
|
|
158
136
|
def create_cost_session(profile: Optional[str] = None) -> boto3.Session:
|
159
137
|
"""
|
160
|
-
Create a boto3 session
|
161
|
-
|
138
|
+
Create a boto3 session for cost operations with universal profile support.
|
139
|
+
Works with ANY AWS profile configuration.
|
162
140
|
|
163
141
|
Args:
|
164
142
|
profile: User-specified profile (from --profile parameter)
|
165
143
|
|
166
144
|
Returns:
|
167
|
-
boto3.Session: Session configured for
|
145
|
+
boto3.Session: Session configured for AWS operations
|
168
146
|
"""
|
169
|
-
|
170
|
-
return boto3.Session(profile_name=
|
147
|
+
selected_profile = get_profile_for_operation("cost", profile)
|
148
|
+
return boto3.Session(profile_name=selected_profile)
|
171
149
|
|
172
150
|
|
173
151
|
def create_management_session(profile: Optional[str] = None) -> boto3.Session:
|
174
152
|
"""
|
175
|
-
Create a boto3 session
|
176
|
-
|
153
|
+
Create a boto3 session for management operations with universal profile support.
|
154
|
+
Works with ANY AWS profile configuration.
|
177
155
|
|
178
156
|
Args:
|
179
157
|
profile: User-specified profile (from --profile parameter)
|
180
158
|
|
181
159
|
Returns:
|
182
|
-
boto3.Session: Session configured for
|
160
|
+
boto3.Session: Session configured for AWS operations
|
183
161
|
"""
|
184
|
-
|
185
|
-
return boto3.Session(profile_name=
|
162
|
+
selected_profile = get_profile_for_operation("management", profile)
|
163
|
+
return boto3.Session(profile_name=selected_profile)
|
186
164
|
|
187
165
|
|
188
166
|
def create_operational_session(profile: Optional[str] = None) -> boto3.Session:
|
189
167
|
"""
|
190
|
-
Create a boto3 session
|
191
|
-
|
168
|
+
Create a boto3 session for operational tasks with universal profile support.
|
169
|
+
Works with ANY AWS profile configuration.
|
192
170
|
|
193
171
|
Args:
|
194
172
|
profile: User-specified profile (from --profile parameter)
|
195
173
|
|
196
174
|
Returns:
|
197
|
-
boto3.Session: Session configured for
|
175
|
+
boto3.Session: Session configured for AWS operations
|
198
176
|
"""
|
199
|
-
|
200
|
-
return boto3.Session(profile_name=
|
177
|
+
selected_profile = get_profile_for_operation("operational", profile)
|
178
|
+
return boto3.Session(profile_name=selected_profile)
|
201
179
|
|
202
180
|
|
203
|
-
def
|
181
|
+
def get_current_profile_info() -> Dict[str, Optional[str]]:
|
204
182
|
"""
|
205
|
-
Get current
|
183
|
+
Get current AWS profile information using universal approach.
|
184
|
+
Works with ANY AWS setup without hardcoded environment variable assumptions.
|
206
185
|
|
207
186
|
Returns:
|
208
|
-
Dict
|
187
|
+
Dict with current profile information
|
209
188
|
"""
|
210
189
|
return {
|
211
|
-
"
|
212
|
-
"
|
213
|
-
"
|
190
|
+
"aws_profile": os.getenv("AWS_PROFILE"),
|
191
|
+
"default_profile": "default",
|
192
|
+
"available_profiles": boto3.Session().available_profiles
|
214
193
|
}
|
215
194
|
|
216
195
|
|
@@ -247,10 +226,10 @@ def validate_profile_access(profile_name: str, operation_type: str = "general")
|
|
247
226
|
|
248
227
|
def get_available_profiles_for_validation() -> list:
|
249
228
|
"""
|
250
|
-
Get available AWS profiles for validation - universal
|
229
|
+
Get available AWS profiles for validation - truly universal approach.
|
251
230
|
|
252
|
-
Returns all configured AWS profiles for validation without hardcoded assumptions.
|
253
|
-
|
231
|
+
Returns all configured AWS profiles for validation without ANY hardcoded assumptions.
|
232
|
+
Works with any AWS setup: single account, multi-account, any profile naming convention.
|
254
233
|
|
255
234
|
Returns:
|
256
235
|
list: Available AWS profile names for validation
|
@@ -259,38 +238,20 @@ def get_available_profiles_for_validation() -> list:
|
|
259
238
|
# Get all available profiles from AWS CLI configuration
|
260
239
|
available_profiles = boto3.Session().available_profiles
|
261
240
|
|
262
|
-
#
|
263
|
-
system_profiles = {'default', 'none', 'null', ''}
|
264
|
-
|
265
|
-
# Return profiles for validation, including default if it's the only one
|
241
|
+
# Start with AWS_PROFILE if set
|
266
242
|
validation_profiles = []
|
243
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
244
|
+
if aws_profile and aws_profile in available_profiles:
|
245
|
+
validation_profiles.append(aws_profile)
|
267
246
|
|
268
|
-
# Add
|
269
|
-
|
270
|
-
|
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:
|
247
|
+
# Add all other available profiles (universal approach)
|
248
|
+
for profile in available_profiles:
|
249
|
+
if profile not in validation_profiles:
|
283
250
|
validation_profiles.append(profile)
|
284
251
|
|
285
|
-
#
|
252
|
+
# Ensure we have at least one profile to test
|
286
253
|
if not validation_profiles:
|
287
|
-
|
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')
|
254
|
+
validation_profiles = ['default']
|
294
255
|
|
295
256
|
return validation_profiles
|
296
257
|
|
@@ -303,10 +264,10 @@ def get_available_profiles_for_validation() -> list:
|
|
303
264
|
__all__ = [
|
304
265
|
"get_profile_for_operation",
|
305
266
|
"resolve_profile_for_operation_silent",
|
306
|
-
"create_cost_session",
|
267
|
+
"create_cost_session",
|
307
268
|
"create_management_session",
|
308
269
|
"create_operational_session",
|
309
|
-
"
|
270
|
+
"get_current_profile_info",
|
310
271
|
"validate_profile_access",
|
311
272
|
"get_available_profiles_for_validation",
|
312
273
|
]
|
runbooks/common/rich_utils.py
CHANGED
@@ -358,8 +358,8 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
|
|
358
358
|
meaningful information for identification. Full names remain available for AWS API calls.
|
359
359
|
|
360
360
|
Examples:
|
361
|
-
'
|
362
|
-
'
|
361
|
+
'your-admin-Billing-ReadOnlyAccess-123456789012' → 'your-admin-Billing-1234...'
|
362
|
+
'your-centralised-ops-ReadOnlyAccess-987654321098' → 'your-centralised-ops-9876...'
|
363
363
|
'short-profile' → 'short-profile' (no truncation needed)
|
364
364
|
|
365
365
|
Args:
|
@@ -398,7 +398,7 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
|
|
398
398
|
|
399
399
|
# Strategy 1: Keep meaningful prefix + account ID suffix
|
400
400
|
if len(parts) >= 4 and parts[-1].isdigit():
|
401
|
-
# Enterprise pattern:
|
401
|
+
# Enterprise pattern: your-admin-Billing-ReadOnlyAccess-123456789012
|
402
402
|
account_id = parts[-1]
|
403
403
|
prefix_parts = parts[:-2] # Skip permissions part for brevity
|
404
404
|
|
@@ -132,38 +132,57 @@ def estimate_resource_costs(session: boto3.Session, regions: List[str]) -> Dict[
|
|
132
132
|
}
|
133
133
|
|
134
134
|
try:
|
135
|
-
# EC2 Instance cost estimation
|
135
|
+
# EC2 Instance cost estimation using dynamic AWS pricing
|
136
136
|
profile_name = session.profile_name if hasattr(session, "profile_name") else None
|
137
137
|
ec2_data = ec2_summary(session, regions, profile_name)
|
138
|
+
|
139
|
+
from ..common.aws_pricing import get_ec2_monthly_cost, get_aws_pricing_engine
|
140
|
+
from ..common.rich_utils import console
|
141
|
+
|
138
142
|
for instance_type, count in ec2_data.items():
|
139
143
|
if count > 0:
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
"
|
144
|
-
|
145
|
-
|
146
|
-
"
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
144
|
+
try:
|
145
|
+
# Use dynamic AWS pricing - NO hardcoded values
|
146
|
+
# Assume primary region for cost estimation
|
147
|
+
primary_region = regions[0] if regions else "us-east-1"
|
148
|
+
monthly_cost_per_instance = get_ec2_monthly_cost(instance_type, primary_region)
|
149
|
+
total_monthly_cost = monthly_cost_per_instance * count
|
150
|
+
estimated_costs["EC2-Instance"] += total_monthly_cost
|
151
|
+
|
152
|
+
console.print(
|
153
|
+
f"[dim]Dynamic pricing: {count}x {instance_type} = "
|
154
|
+
f"${total_monthly_cost:.2f}/month[/]"
|
155
|
+
)
|
156
|
+
|
157
|
+
except Exception as e:
|
158
|
+
console.print(
|
159
|
+
f"[yellow]⚠ Warning: Could not get dynamic pricing for {instance_type}: {e}[/yellow]"
|
160
|
+
)
|
161
|
+
|
162
|
+
try:
|
163
|
+
# Use fallback pricing engine with AWS patterns
|
164
|
+
pricing_engine = get_aws_pricing_engine(enable_fallback=True)
|
165
|
+
primary_region = regions[0] if regions else "us-east-1"
|
166
|
+
result = pricing_engine.get_ec2_instance_pricing(instance_type, primary_region)
|
167
|
+
total_monthly_cost = result.monthly_cost * count
|
168
|
+
estimated_costs["EC2-Instance"] += total_monthly_cost
|
169
|
+
|
170
|
+
console.print(
|
171
|
+
f"[dim]Fallback pricing: {count}x {instance_type} = "
|
172
|
+
f"${total_monthly_cost:.2f}/month[/]"
|
173
|
+
)
|
174
|
+
|
175
|
+
except Exception as fallback_error:
|
176
|
+
console.print(
|
177
|
+
f"[red]⚠ ERROR: All pricing methods failed for {instance_type}: {fallback_error}[/red]"
|
178
|
+
)
|
179
|
+
console.print(
|
180
|
+
f"[red]Skipping cost estimation for {count}x {instance_type}[/red]"
|
181
|
+
)
|
182
|
+
logger.error(
|
183
|
+
f"ENTERPRISE VIOLATION: Cannot estimate cost for {instance_type} "
|
184
|
+
f"without hardcoded values. Instance type skipped."
|
185
|
+
)
|
167
186
|
|
168
187
|
# Add some EC2-Other costs (EBS, snapshots, etc.)
|
169
188
|
estimated_costs["EC2-Other"] = estimated_costs["EC2-Instance"] * 0.3
|