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
runbooks/finops/ebs_optimizer.py
CHANGED
@@ -166,15 +166,8 @@ class EBSOptimizer:
|
|
166
166
|
profile_name=get_profile_for_operation("operational", profile_name)
|
167
167
|
)
|
168
168
|
|
169
|
-
# EBS pricing
|
170
|
-
self.ebs_pricing =
|
171
|
-
'gp2': 0.10, # $0.10/GB/month
|
172
|
-
'gp3': 0.08, # $0.08/GB/month (20% cheaper than GP2)
|
173
|
-
'io1': 0.125, # $0.125/GB/month
|
174
|
-
'io2': 0.125, # $0.125/GB/month
|
175
|
-
'st1': 0.045, # $0.045/GB/month
|
176
|
-
'sc1': 0.025, # $0.025/GB/month
|
177
|
-
}
|
169
|
+
# EBS pricing using dynamic AWS pricing engine for universal compatibility
|
170
|
+
self.ebs_pricing = self._initialize_dynamic_ebs_pricing()
|
178
171
|
|
179
172
|
# GP3 conversion savings percentage
|
180
173
|
self.gp3_savings_percentage = 0.20 # 20% savings GP2→GP3
|
@@ -184,6 +177,60 @@ class EBSOptimizer:
|
|
184
177
|
self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
|
185
178
|
self.analysis_period_days = 7
|
186
179
|
|
180
|
+
def _initialize_dynamic_ebs_pricing(self) -> Dict[str, float]:
|
181
|
+
"""Initialize dynamic EBS pricing using AWS pricing engine for universal compatibility."""
|
182
|
+
try:
|
183
|
+
from ..common.aws_pricing import get_service_monthly_cost
|
184
|
+
|
185
|
+
# Get dynamic pricing for common EBS volume types in us-east-1 (base region)
|
186
|
+
base_region = "us-east-1"
|
187
|
+
|
188
|
+
return {
|
189
|
+
'gp2': get_service_monthly_cost("ebs_gp2", base_region, self.profile_name),
|
190
|
+
'gp3': get_service_monthly_cost("ebs_gp3", base_region, self.profile_name),
|
191
|
+
'io1': get_service_monthly_cost("ebs_io1", base_region, self.profile_name),
|
192
|
+
'io2': get_service_monthly_cost("ebs_io2", base_region, self.profile_name),
|
193
|
+
'st1': get_service_monthly_cost("ebs_st1", base_region, self.profile_name),
|
194
|
+
'sc1': get_service_monthly_cost("ebs_sc1", base_region, self.profile_name),
|
195
|
+
}
|
196
|
+
except Exception as e:
|
197
|
+
print_warning(f"Dynamic EBS pricing initialization failed: {e}")
|
198
|
+
print_warning("Attempting AWS Pricing API fallback with universal profile support")
|
199
|
+
|
200
|
+
try:
|
201
|
+
from ..common.aws_pricing import get_aws_pricing_engine
|
202
|
+
|
203
|
+
# Use AWS Pricing API with profile support for universal compatibility
|
204
|
+
pricing_engine = get_aws_pricing_engine(profile=self.profile_name, enable_fallback=True)
|
205
|
+
|
206
|
+
# Get actual AWS pricing instead of hardcoded values
|
207
|
+
gp2_pricing = pricing_engine.get_ebs_pricing("gp2", "us-east-1")
|
208
|
+
gp3_pricing = pricing_engine.get_ebs_pricing("gp3", "us-east-1")
|
209
|
+
io1_pricing = pricing_engine.get_ebs_pricing("io1", "us-east-1")
|
210
|
+
io2_pricing = pricing_engine.get_ebs_pricing("io2", "us-east-1")
|
211
|
+
st1_pricing = pricing_engine.get_ebs_pricing("st1", "us-east-1")
|
212
|
+
sc1_pricing = pricing_engine.get_ebs_pricing("sc1", "us-east-1")
|
213
|
+
|
214
|
+
return {
|
215
|
+
'gp2': gp2_pricing.monthly_cost_per_gb,
|
216
|
+
'gp3': gp3_pricing.monthly_cost_per_gb,
|
217
|
+
'io1': io1_pricing.monthly_cost_per_gb,
|
218
|
+
'io2': io2_pricing.monthly_cost_per_gb,
|
219
|
+
'st1': st1_pricing.monthly_cost_per_gb,
|
220
|
+
'sc1': sc1_pricing.monthly_cost_per_gb,
|
221
|
+
}
|
222
|
+
|
223
|
+
except Exception as pricing_error:
|
224
|
+
print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine EBS pricing without AWS API access: {pricing_error}")
|
225
|
+
print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
|
226
|
+
|
227
|
+
# Return error state instead of hardcoded values to maintain enterprise compliance
|
228
|
+
raise RuntimeError(
|
229
|
+
"Universal compatibility mode requires dynamic AWS pricing API access. "
|
230
|
+
"Please ensure your AWS profile has pricing:GetProducts permissions or configure "
|
231
|
+
"appropriate billing/management profile access."
|
232
|
+
)
|
233
|
+
|
187
234
|
async def analyze_ebs_volumes(self, dry_run: bool = True) -> EBSOptimizerResults:
|
188
235
|
"""
|
189
236
|
Comprehensive EBS volume cost optimization analysis.
|
@@ -409,10 +409,15 @@ if __name__ == "__main__":
|
|
409
409
|
console.print("[bold bright_cyan]🚀 CloudOps Runbooks - Enhanced Trend Analysis[/]")
|
410
410
|
console.print("[dim]QA Testing Specialist Implementation - Reference Image Compliance[/]")
|
411
411
|
|
412
|
+
import os
|
413
|
+
# Use environment-driven values for universal compatibility
|
414
|
+
account_id = os.getenv("AWS_ACCOUNT_ID", "123456789012")
|
415
|
+
profile = os.getenv("SINGLE_AWS_PROFILE", "default-single-profile")
|
416
|
+
|
412
417
|
visualizer.create_enhanced_trend_display(
|
413
418
|
monthly_costs=trend_data,
|
414
|
-
account_id=
|
415
|
-
profile=
|
419
|
+
account_id=account_id,
|
420
|
+
profile=profile,
|
416
421
|
)
|
417
422
|
|
418
423
|
# Export to JSON (contract compliance)
|
@@ -22,7 +22,8 @@ AWS_AVAILABLE = True
|
|
22
22
|
|
23
23
|
def get_aws_profiles() -> List[str]:
|
24
24
|
"""Stub implementation - use dashboard_runner.py instead."""
|
25
|
-
|
25
|
+
import os
|
26
|
+
return ["default", os.getenv("BILLING_PROFILE", "default-billing-profile")]
|
26
27
|
|
27
28
|
|
28
29
|
def get_account_id(profile: str = "default") -> str:
|
@@ -46,10 +47,10 @@ class FinOpsConfig:
|
|
46
47
|
include_budget_data: bool = True
|
47
48
|
include_resource_analysis: bool = True
|
48
49
|
|
49
|
-
# Legacy compatibility properties with environment
|
50
|
-
billing_profile: str = "
|
51
|
-
management_profile: str = "
|
52
|
-
operational_profile: str = "
|
50
|
+
# Legacy compatibility properties with universal environment support
|
51
|
+
billing_profile: str = field(default_factory=lambda: os.getenv("BILLING_PROFILE", "default-billing-profile"))
|
52
|
+
management_profile: str = field(default_factory=lambda: os.getenv("MANAGEMENT_PROFILE", "default-management-profile"))
|
53
|
+
operational_profile: str = field(default_factory=lambda: os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"))
|
53
54
|
|
54
55
|
# Additional expected attributes from tests
|
55
56
|
time_range_days: int = 30
|
runbooks/finops/iam_guidance.py
CHANGED
@@ -11,6 +11,8 @@ from rich.console import Console
|
|
11
11
|
from rich.panel import Panel
|
12
12
|
from rich.table import Table
|
13
13
|
|
14
|
+
from runbooks.common import get_aws_cli_example_period
|
15
|
+
|
14
16
|
console = Console()
|
15
17
|
|
16
18
|
|
@@ -311,6 +313,9 @@ def handle_cost_explorer_error(error: Exception, profile_name: Optional[str] = N
|
|
311
313
|
|
312
314
|
def _display_single_account_cost_explorer_guidance(error: Exception, profile_name: Optional[str] = None):
|
313
315
|
"""Display context-aware guidance for single account Cost Explorer limitations."""
|
316
|
+
|
317
|
+
# Get dynamic date period for CLI examples
|
318
|
+
start_date, end_date = get_aws_cli_example_period()
|
314
319
|
|
315
320
|
# Main explanation panel
|
316
321
|
explanation_panel = Panel(
|
@@ -354,7 +359,7 @@ def _display_single_account_cost_explorer_guidance(error: Exception, profile_nam
|
|
354
359
|
f"[green]✅ Recommended Solutions:[/green]\n\n"
|
355
360
|
f"{solution_commands}\n\n"
|
356
361
|
f"[bold]🎯 Quick Test Commands:[/bold]\n"
|
357
|
-
f"• Test billing access: `aws ce get-cost-and-usage --time-period Start=
|
362
|
+
f"• Test billing access: `aws ce get-cost-and-usage --time-period Start={start_date},End={end_date} --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
|
358
363
|
f"• List available profiles: `aws configure list-profiles`\n"
|
359
364
|
f"• Check current identity: `aws sts get-caller-identity --profile {profile_name or 'your-profile'}`\n\n"
|
360
365
|
f"[bold]💡 Alternative Approach:[/bold]\n"
|
@@ -143,7 +143,7 @@ class NATGatewayOptimizer:
|
|
143
143
|
# NAT Gateway pricing - using dynamic pricing engine
|
144
144
|
# Base monthly cost calculation (will be applied per region)
|
145
145
|
self._base_monthly_cost_us_east_1 = get_service_monthly_cost("nat_gateway", "us-east-1")
|
146
|
-
self.nat_gateway_data_processing_cost =
|
146
|
+
self.nat_gateway_data_processing_cost = get_service_monthly_cost("data_transfer", "us-east-1") # Dynamic data transfer pricing
|
147
147
|
|
148
148
|
# Enterprise thresholds for optimization recommendations
|
149
149
|
self.low_usage_threshold_connections = 10 # Active connections per day
|
@@ -155,9 +155,9 @@ class NATGatewayOptimizer:
|
|
155
155
|
try:
|
156
156
|
return get_service_monthly_cost("nat_gateway", region)
|
157
157
|
except Exception:
|
158
|
-
# Fallback to regional cost calculation
|
158
|
+
# Fallback to regional cost calculation using dynamic pricing
|
159
159
|
from ..common.aws_pricing import calculate_regional_cost
|
160
|
-
return calculate_regional_cost(self._base_monthly_cost_us_east_1, region)
|
160
|
+
return calculate_regional_cost(self._base_monthly_cost_us_east_1, region, "nat_gateway", self.profile_name)
|
161
161
|
|
162
162
|
async def analyze_nat_gateways(self, dry_run: bool = True) -> NATGatewayOptimizerResults:
|
163
163
|
"""
|
@@ -778,39 +778,58 @@ class EnhancedVPCCostOptimizer:
|
|
778
778
|
self.cost_model = self._initialize_dynamic_cost_model()
|
779
779
|
|
780
780
|
def _initialize_dynamic_cost_model(self) -> Dict[str, float]:
|
781
|
-
"""Initialize dynamic cost model using AWS pricing engine."""
|
781
|
+
"""Initialize dynamic cost model using AWS pricing engine with universal compatibility."""
|
782
782
|
try:
|
783
783
|
# Get base pricing for us-east-1, then apply regional multipliers as needed
|
784
784
|
base_region = "us-east-1"
|
785
785
|
|
786
786
|
return {
|
787
|
-
"nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region),
|
788
|
-
"nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region),
|
789
|
-
"transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region),
|
790
|
-
"vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region),
|
791
|
-
"
|
792
|
-
"
|
787
|
+
"nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region, self.profile),
|
788
|
+
"nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region, self.profile),
|
789
|
+
"transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region, self.profile),
|
790
|
+
"vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region, self.profile),
|
791
|
+
"vpc_endpoint_interface_hourly": get_service_monthly_cost("vpc_endpoint_interface", base_region, self.profile) / (24 * 30),
|
792
|
+
"transit_gateway_attachment_hourly": get_service_monthly_cost("transit_gateway_attachment", base_region, self.profile) / (24 * 30),
|
793
|
+
"data_transfer_regional": get_service_monthly_cost("data_transfer", base_region, self.profile),
|
794
|
+
"data_transfer_internet": get_service_monthly_cost("data_transfer", base_region, self.profile) * 4.5, # Internet is ~4.5x higher
|
793
795
|
}
|
794
796
|
except Exception as e:
|
795
797
|
print_warning(f"Dynamic pricing initialization failed: {e}")
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
"nat_gateway_hourly": 0.045,
|
800
|
-
"nat_gateway_data_processing": 0.045, # per GB
|
801
|
-
"transit_gateway_monthly": 36.50,
|
802
|
-
"transit_gateway_attachment_hourly": 0.05,
|
803
|
-
"vpc_endpoint_interface_hourly": 0.01,
|
804
|
-
"data_transfer_regional": 0.01, # per GB within region
|
805
|
-
"data_transfer_cross_region": 0.02, # per GB cross-region
|
806
|
-
"data_transfer_internet": 0.09 # per GB to internet
|
807
|
-
}
|
798
|
+
print_warning("Attempting AWS Pricing API fallback with universal profile support")
|
799
|
+
# Enhanced fallback with AWS Pricing API integration
|
800
|
+
from ..common.aws_pricing import get_aws_pricing_engine, AWSOfficialPricingEngine
|
808
801
|
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
802
|
+
try:
|
803
|
+
# Use AWS Pricing API with profile support for universal compatibility
|
804
|
+
pricing_engine = get_aws_pricing_engine(profile=self.profile, enable_fallback=True)
|
805
|
+
|
806
|
+
# Get actual AWS pricing instead of hardcoded values
|
807
|
+
nat_gateway_pricing = pricing_engine.get_nat_gateway_pricing("us-east-1")
|
808
|
+
transit_gateway_pricing = pricing_engine.get_transit_gateway_pricing("us-east-1")
|
809
|
+
vpc_endpoint_pricing = pricing_engine.get_vpc_endpoint_pricing("us-east-1")
|
810
|
+
data_transfer_pricing = pricing_engine.get_data_transfer_pricing("us-east-1", "internet")
|
811
|
+
|
812
|
+
return {
|
813
|
+
"nat_gateway_monthly": nat_gateway_pricing.monthly_cost,
|
814
|
+
"nat_gateway_data_processing": data_transfer_pricing.cost_per_gb,
|
815
|
+
"transit_gateway_monthly": transit_gateway_pricing.monthly_cost,
|
816
|
+
"transit_gateway_attachment_hourly": transit_gateway_pricing.attachment_hourly_cost,
|
817
|
+
"vpc_endpoint_interface_hourly": vpc_endpoint_pricing.interface_hourly_cost,
|
818
|
+
"data_transfer_regional": data_transfer_pricing.cost_per_gb * 0.1, # Regional is ~10% of internet cost
|
819
|
+
"data_transfer_cross_region": data_transfer_pricing.cost_per_gb * 0.2, # Cross-region is ~20% of internet cost
|
820
|
+
"data_transfer_internet": data_transfer_pricing.cost_per_gb
|
821
|
+
}
|
822
|
+
|
823
|
+
except Exception as pricing_error:
|
824
|
+
print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine pricing without AWS API access: {pricing_error}")
|
825
|
+
print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
|
826
|
+
|
827
|
+
# Return error state instead of hardcoded values to maintain enterprise compliance
|
828
|
+
raise RuntimeError(
|
829
|
+
"Universal compatibility mode requires dynamic AWS pricing API access. "
|
830
|
+
"Please ensure your AWS profile has pricing:GetProducts permissions or configure "
|
831
|
+
"appropriate billing/management profile access."
|
832
|
+
)
|
814
833
|
|
815
834
|
async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
|
816
835
|
regions: Optional[List[str]] = None) -> Dict[str, Any]:
|
@@ -34,6 +34,8 @@ try:
|
|
34
34
|
except ImportError:
|
35
35
|
# Define mock_costexplorer as a no-op decorator for compatibility
|
36
36
|
def mock_costexplorer(func):
|
37
|
+
# Dynamic test period for consistent test data
|
38
|
+
test_period = get_test_date_period(30)
|
37
39
|
def wrapper(*args, **kwargs):
|
38
40
|
return func(*args, **kwargs)
|
39
41
|
|
@@ -128,7 +130,7 @@ class TestAWSIntegrationWithMoto:
|
|
128
130
|
mock_cost_data = {
|
129
131
|
"ResultsByTime": [
|
130
132
|
{
|
131
|
-
"TimePeriod": {"Start": "
|
133
|
+
"TimePeriod": {"Start": test_period["Start"], "End": test_period["End"]},
|
132
134
|
"Total": {"UnblendedCost": {"Amount": "50000.00", "Unit": "USD"}},
|
133
135
|
"Groups": [
|
134
136
|
{"Keys": ["EC2-Instance"], "Metrics": {"UnblendedCost": {"Amount": "20000.00", "Unit": "USD"}}},
|
@@ -610,18 +610,9 @@ class VPCCleanupOptimizer:
|
|
610
610
|
|
611
611
|
print_header("🌐 Real-Time NO-ENI VPC Discovery", "MCP-Validated VPC Cleanup Analysis")
|
612
612
|
|
613
|
-
#
|
614
|
-
from runbooks.common.profile_utils import get_enterprise_profile_mapping
|
615
|
-
enterprise_profiles = get_enterprise_profile_mapping()
|
616
|
-
|
617
|
-
# Override with current profile if available
|
618
|
-
current_profile_type = self._determine_profile_type(self.profile)
|
619
|
-
if current_profile_type:
|
620
|
-
enterprise_profiles[current_profile_type] = self.profile
|
621
|
-
|
622
|
-
# Initialize MCP validator for dynamic discovery
|
613
|
+
# Initialize MCP validator with universal profile support
|
623
614
|
print_info("🔧 Initializing dynamic MCP validator...")
|
624
|
-
mcp_validator = NOENIVPCMCPValidator(
|
615
|
+
mcp_validator = NOENIVPCMCPValidator(user_profile=self.profile)
|
625
616
|
|
626
617
|
# Perform dynamic discovery across all accounts
|
627
618
|
print_info("🚀 Starting real-time discovery across all AWS accounts...")
|
@@ -641,7 +632,7 @@ class VPCCleanupOptimizer:
|
|
641
632
|
# Get detailed VPC information for each NO-ENI VPC
|
642
633
|
try:
|
643
634
|
# Use appropriate session for this account
|
644
|
-
session = self._get_session_for_account(target.account_id
|
635
|
+
session = self._get_session_for_account(target.account_id)
|
645
636
|
ec2_client = session.client('ec2', region_name=target.region)
|
646
637
|
|
647
638
|
# Get VPC details
|
@@ -708,26 +699,28 @@ class VPCCleanupOptimizer:
|
|
708
699
|
return 'CENTRALISED_OPS'
|
709
700
|
return None
|
710
701
|
|
711
|
-
def _get_session_for_account(self, account_id: str
|
712
|
-
"""Get appropriate session for accessing a specific account."""
|
702
|
+
def _get_session_for_account(self, account_id: str) -> boto3.Session:
|
703
|
+
"""Get appropriate session for accessing a specific account using universal profile management."""
|
704
|
+
from runbooks.common.profile_utils import get_profile_for_operation
|
705
|
+
|
713
706
|
# In enterprise setup, would assume role here
|
714
|
-
# For now, return session with best available profile
|
707
|
+
# For now, return session with best available profile using three-tier priority system
|
715
708
|
|
716
|
-
#
|
717
|
-
|
709
|
+
# Try different operation types in priority order
|
710
|
+
profile_types = ['management', 'operational', 'billing']
|
718
711
|
|
719
|
-
for profile_type in
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
712
|
+
for profile_type in profile_types:
|
713
|
+
try:
|
714
|
+
profile_name = get_profile_for_operation(profile_type, self.profile)
|
715
|
+
session = boto3.Session(profile_name=profile_name)
|
716
|
+
# Verify access
|
717
|
+
sts_client = session.client('sts')
|
718
|
+
identity = sts_client.get_caller_identity()
|
719
|
+
|
720
|
+
if identity['Account'] == account_id:
|
721
|
+
return session
|
722
|
+
except Exception:
|
723
|
+
continue
|
731
724
|
|
732
725
|
# Fallback to current session
|
733
726
|
return self.session
|
@@ -42,12 +42,13 @@ try:
|
|
42
42
|
ENHANCED_PROFILES_AVAILABLE = True
|
43
43
|
except ImportError:
|
44
44
|
ENHANCED_PROFILES_AVAILABLE = False
|
45
|
-
# Fallback profile definitions
|
45
|
+
# Fallback profile definitions with universal environment support
|
46
|
+
import os
|
46
47
|
ENTERPRISE_PROFILES = {
|
47
|
-
"BILLING_PROFILE": "
|
48
|
-
"MANAGEMENT_PROFILE": "
|
49
|
-
"CENTRALISED_OPS_PROFILE": "
|
50
|
-
"SINGLE_ACCOUNT_PROFILE": "
|
48
|
+
"BILLING_PROFILE": os.getenv("BILLING_PROFILE", "default-billing-profile"),
|
49
|
+
"MANAGEMENT_PROFILE": os.getenv("MANAGEMENT_PROFILE", "default-management-profile"),
|
50
|
+
"CENTRALISED_OPS_PROFILE": os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"),
|
51
|
+
"SINGLE_ACCOUNT_PROFILE": os.getenv("SINGLE_AWS_PROFILE", "default-single-profile"),
|
51
52
|
}
|
52
53
|
|
53
54
|
|
@@ -141,28 +142,35 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
|
|
141
142
|
|
142
143
|
Strategic Alignment: "Do one thing and do it well"
|
143
144
|
- Single profile override pattern: --profile takes precedence
|
144
|
-
-
|
145
|
-
-
|
145
|
+
- Universal AWS environment compatibility: works with ANY profile configuration
|
146
|
+
- Graceful fallback system for discovery across different AWS setups
|
146
147
|
|
147
148
|
Returns:
|
148
149
|
str: The active profile to use for all operations
|
149
150
|
"""
|
150
|
-
#
|
151
|
+
# PRIMARY: User --profile parameter takes absolute precedence (Universal Compatibility)
|
151
152
|
if self.profile:
|
152
|
-
print_info(f"Using user-specified profile
|
153
|
-
logger.info("Profile override via --profile parameter -
|
153
|
+
print_info(f"✅ Universal AWS Compatibility: Using user-specified profile '{self.profile}'")
|
154
|
+
logger.info("Profile override via --profile parameter - universal environment support")
|
154
155
|
return self.profile
|
155
156
|
|
156
|
-
#
|
157
|
+
# SECONDARY: Environment variable fallback with intelligent prioritization
|
158
|
+
# Priority order: Management > Billing > Operations > Default (Organizations discovery preference)
|
157
159
|
env_profile = (
|
158
160
|
os.getenv("MANAGEMENT_PROFILE") or
|
159
161
|
os.getenv("BILLING_PROFILE") or
|
160
162
|
os.getenv("CENTRALISED_OPS_PROFILE") or
|
163
|
+
os.getenv("SINGLE_AWS_PROFILE") or
|
161
164
|
"default"
|
162
165
|
)
|
163
166
|
|
164
|
-
|
165
|
-
|
167
|
+
if env_profile != "default":
|
168
|
+
print_info(f"✅ Universal AWS Compatibility: Using environment profile '{env_profile}'")
|
169
|
+
logger.info(f"Environment variable profile selected: {env_profile}")
|
170
|
+
else:
|
171
|
+
print_info("✅ Universal AWS Compatibility: Using 'default' profile - works with any AWS CLI configuration")
|
172
|
+
logger.info("Using default profile - universal compatibility mode")
|
173
|
+
|
166
174
|
return env_profile
|
167
175
|
|
168
176
|
def _initialize_collectors(self) -> Dict[str, str]:
|
@@ -226,21 +234,19 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
|
|
226
234
|
|
227
235
|
def get_organization_accounts(self) -> List[str]:
|
228
236
|
"""
|
229
|
-
Get list of accounts in AWS Organization.
|
237
|
+
Get list of accounts in AWS Organization with universal compatibility.
|
230
238
|
|
231
239
|
Strategic Alignment: "Do one thing and do it well"
|
232
|
-
-
|
233
|
-
-
|
234
|
-
-
|
240
|
+
- Universal AWS environment compatibility: works with ANY Organizations setup
|
241
|
+
- Intelligent fallback system: Organizations → standalone account detection
|
242
|
+
- Graceful handling of different permission scenarios
|
235
243
|
"""
|
236
244
|
try:
|
237
|
-
# Use
|
238
|
-
|
239
|
-
management_profile = self.active_profile
|
240
|
-
management_session = create_management_session(profile=management_profile)
|
245
|
+
# Use active profile for Organizations operations (Universal Compatibility)
|
246
|
+
management_session = create_management_session(profile=self.active_profile)
|
241
247
|
organizations_client = management_session.client("organizations")
|
242
248
|
|
243
|
-
print_info("
|
249
|
+
print_info(f"🔍 Universal Discovery: Attempting Organizations API with profile '{self.active_profile}'...")
|
244
250
|
response = self._make_aws_call(organizations_client.list_accounts)
|
245
251
|
|
246
252
|
accounts = []
|
@@ -248,14 +254,31 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
|
|
248
254
|
if account["Status"] == "ACTIVE":
|
249
255
|
accounts.append(account["Id"])
|
250
256
|
|
251
|
-
|
252
|
-
|
253
|
-
|
257
|
+
if accounts:
|
258
|
+
print_success(f"✅ Organizations Discovery: Found {len(accounts)} active accounts in organization")
|
259
|
+
logger.info(f"Organizations discovery successful: {len(accounts)} accounts with profile {self.active_profile}")
|
260
|
+
return accounts
|
261
|
+
else:
|
262
|
+
print_warning("⚠️ Organizations Discovery: No active accounts found in organization")
|
263
|
+
return [self.get_account_id()]
|
254
264
|
|
255
265
|
except Exception as e:
|
256
|
-
|
257
|
-
|
258
|
-
|
266
|
+
# Enhanced error messages for different AWS environment scenarios
|
267
|
+
error_message = str(e).lower()
|
268
|
+
|
269
|
+
if "accessdenied" in error_message or "unauthorized" in error_message:
|
270
|
+
print_warning(f"⚠️ Universal Compatibility: Profile '{self.active_profile}' lacks Organizations permissions")
|
271
|
+
print_info("💡 Single Account Mode: Continuing with current account (universal compatibility)")
|
272
|
+
elif "organizationsnotinuse" in error_message:
|
273
|
+
print_info(f"ℹ️ Standalone Account: Profile '{self.active_profile}' not in an AWS Organization")
|
274
|
+
print_info("💡 Single Account Mode: Continuing with current account")
|
275
|
+
else:
|
276
|
+
print_warning(f"⚠️ Organizations Discovery Failed: {e}")
|
277
|
+
print_info("💡 Fallback Mode: Continuing with current account for universal compatibility")
|
278
|
+
|
279
|
+
logger.warning(f"Organization discovery failed, graceful fallback: {e}")
|
280
|
+
|
281
|
+
# Universal fallback: always return current account for single-account operations
|
259
282
|
return [self.get_account_id()]
|
260
283
|
|
261
284
|
def get_current_account_id(self) -> str:
|