runbooks 1.1.3__py3-none-any.whl → 1.1.5__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 +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +8 -8
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +274 -0
- runbooks/cli/commands/finops.py +1164 -0
- runbooks/cli/commands/inventory.py +379 -0
- runbooks/cli/commands/operate.py +239 -0
- runbooks/cli/commands/security.py +248 -0
- runbooks/cli/commands/validation.py +825 -0
- runbooks/cli/commands/vpc.py +310 -0
- runbooks/cli/registry.py +107 -0
- runbooks/cloudops/__init__.py +23 -30
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +549 -547
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +226 -227
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +179 -215
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +341 -0
- runbooks/common/aws_utils.py +75 -80
- runbooks/common/business_logic.py +127 -105
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
- runbooks/common/cross_account_manager.py +198 -205
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +235 -0
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +478 -495
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +176 -194
- runbooks/common/patterns.py +204 -0
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +248 -39
- runbooks/common/rich_utils.py +643 -92
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +29 -33
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +488 -622
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +40 -37
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +230 -292
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +338 -175
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1513 -482
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +25 -29
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +77 -78
- runbooks/finops/scenarios.py +1278 -439
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +263 -269
- runbooks/finops/vpc_cleanup_exporter.py +191 -146
- runbooks/finops/vpc_cleanup_optimizer.py +593 -575
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +111 -101
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +56 -52
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +733 -696
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +3 -3
- runbooks/main.py +152 -9147
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/metrics/dora_metrics_engine.py +2 -2
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +33 -10
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +648 -618
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +71 -67
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +91 -65
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +49 -44
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/integration_test_enterprise_security.py +5 -3
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/run_script.py +1 -1
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/mcp_reliability_engine.py +6 -6
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +51 -48
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +754 -708
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +190 -162
- runbooks/vpc/mcp_no_eni_validator.py +681 -640
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1302 -1129
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -956
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.3.dist-info/METADATA +0 -799
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -16,23 +16,31 @@ IMPROVEMENTS:
|
|
16
16
|
- Support for MANAGEMENT_PROFILE testing
|
17
17
|
"""
|
18
18
|
|
19
|
-
import
|
19
|
+
import asyncio
|
20
20
|
import json
|
21
|
+
import logging
|
22
|
+
import os
|
21
23
|
import time
|
22
|
-
import asyncio
|
23
24
|
from datetime import datetime, timedelta, timezone
|
24
|
-
from typing import Dict, List, Optional, Tuple
|
25
|
-
import os
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple
|
26
26
|
|
27
|
-
import click
|
28
27
|
import boto3
|
28
|
+
import click
|
29
29
|
from botocore.exceptions import ClientError
|
30
30
|
|
31
|
+
from ..common.profile_utils import get_profile_for_operation
|
31
32
|
from ..common.rich_utils import (
|
32
|
-
console,
|
33
|
-
|
33
|
+
console,
|
34
|
+
create_panel,
|
35
|
+
create_progress_bar,
|
36
|
+
create_table,
|
37
|
+
format_cost,
|
38
|
+
print_error,
|
39
|
+
print_header,
|
40
|
+
print_info,
|
41
|
+
print_success,
|
42
|
+
print_warning,
|
34
43
|
)
|
35
|
-
from ..common.profile_utils import get_profile_for_operation
|
36
44
|
|
37
45
|
logger = logging.getLogger(__name__)
|
38
46
|
|
@@ -59,12 +67,12 @@ class EnhancedRDSSnapshotOptimizer:
|
|
59
67
|
|
60
68
|
# Discovery metrics
|
61
69
|
self.discovery_stats = {
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
70
|
+
"total_discovered": 0,
|
71
|
+
"manual_snapshots": 0,
|
72
|
+
"automated_snapshots": 0,
|
73
|
+
"accounts_covered": set(),
|
74
|
+
"total_storage_gb": 0,
|
75
|
+
"estimated_monthly_cost": 0.0,
|
68
76
|
}
|
69
77
|
|
70
78
|
# PHASE 2 FIX: Dynamic pricing instead of static values
|
@@ -79,7 +87,7 @@ class EnhancedRDSSnapshotOptimizer:
|
|
79
87
|
self.session = boto3.Session(profile_name=resolved_profile)
|
80
88
|
|
81
89
|
# Verify access
|
82
|
-
sts_client = self.session.client(
|
90
|
+
sts_client = self.session.client("sts")
|
83
91
|
identity = sts_client.get_caller_identity()
|
84
92
|
|
85
93
|
print_success(f"✅ Session initialized: {resolved_profile} (Account: {identity['Account']})")
|
@@ -105,40 +113,33 @@ class EnhancedRDSSnapshotOptimizer:
|
|
105
113
|
return cached_price
|
106
114
|
|
107
115
|
# Query AWS Pricing API
|
108
|
-
pricing_client = self.session.client(
|
116
|
+
pricing_client = self.session.client("pricing", region_name="us-east-1")
|
109
117
|
|
110
118
|
response = pricing_client.get_products(
|
111
|
-
ServiceCode=
|
119
|
+
ServiceCode="AmazonRDS",
|
112
120
|
Filters=[
|
113
|
-
{
|
114
|
-
|
115
|
-
'Field': 'productFamily',
|
116
|
-
'Value': 'Database Storage'
|
117
|
-
},
|
118
|
-
{
|
119
|
-
'Type': 'TERM_MATCH',
|
120
|
-
'Field': 'usageType',
|
121
|
-
'Value': 'SnapshotUsage:db.gp2'
|
122
|
-
}
|
121
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Database Storage"},
|
122
|
+
{"Type": "TERM_MATCH", "Field": "usageType", "Value": "SnapshotUsage:db.gp2"},
|
123
123
|
],
|
124
|
-
MaxResults=1
|
124
|
+
MaxResults=1,
|
125
125
|
)
|
126
126
|
|
127
|
-
if response.get(
|
127
|
+
if response.get("PriceList"):
|
128
128
|
import json
|
129
|
-
|
129
|
+
|
130
|
+
price_item = json.loads(response["PriceList"][0])
|
130
131
|
|
131
132
|
# Extract pricing from AWS pricing structure
|
132
|
-
terms = price_item.get(
|
133
|
-
on_demand = terms.get(
|
133
|
+
terms = price_item.get("terms", {})
|
134
|
+
on_demand = terms.get("OnDemand", {})
|
134
135
|
|
135
136
|
for term_key, term_value in on_demand.items():
|
136
|
-
price_dimensions = term_value.get(
|
137
|
+
price_dimensions = term_value.get("priceDimensions", {})
|
137
138
|
for dimension_key, dimension_value in price_dimensions.items():
|
138
|
-
price_per_unit = dimension_value.get(
|
139
|
-
usd_price = price_per_unit.get(
|
139
|
+
price_per_unit = dimension_value.get("pricePerUnit", {})
|
140
|
+
usd_price = price_per_unit.get("USD", "0")
|
140
141
|
|
141
|
-
if usd_price and usd_price !=
|
142
|
+
if usd_price and usd_price != "0":
|
142
143
|
dynamic_price = float(usd_price)
|
143
144
|
|
144
145
|
# Cache the result
|
@@ -156,7 +157,9 @@ class EnhancedRDSSnapshotOptimizer:
|
|
156
157
|
print_warning(f"Pricing API error: {str(e)[:50]}... Using fallback")
|
157
158
|
return 0.095
|
158
159
|
|
159
|
-
def discover_snapshots_via_config_aggregator(
|
160
|
+
def discover_snapshots_via_config_aggregator(
|
161
|
+
self, target_account_id: str = None, manual_only: bool = False
|
162
|
+
) -> List[Dict]:
|
160
163
|
"""
|
161
164
|
Discover RDS snapshots using AWS Config aggregator with direct RDS API fallback
|
162
165
|
|
@@ -196,7 +199,7 @@ class EnhancedRDSSnapshotOptimizer:
|
|
196
199
|
|
197
200
|
if processed_snapshot:
|
198
201
|
# Apply manual filter if requested
|
199
|
-
if manual_only and processed_snapshot.get(
|
202
|
+
if manual_only and processed_snapshot.get("SnapshotType") != "manual":
|
200
203
|
continue # Skip automated snapshots when manual_only=True
|
201
204
|
|
202
205
|
discovered_snapshots.append(processed_snapshot)
|
@@ -224,7 +227,9 @@ class EnhancedRDSSnapshotOptimizer:
|
|
224
227
|
if "NoSuchConfigurationAggregatorException" in str(e):
|
225
228
|
print_warning("🏢 Organization Config aggregator not accessible from this account")
|
226
229
|
print_info("💡 For organization-wide analysis: Use MANAGEMENT_PROFILE")
|
227
|
-
print_info(
|
230
|
+
print_info(
|
231
|
+
"💡 For single-account analysis: This account may not have RDS snapshots or Config aggregator access"
|
232
|
+
)
|
228
233
|
print_info("🔍 Alternative: Check AWS Console → RDS → Snapshots for manual verification")
|
229
234
|
else:
|
230
235
|
print_warning("Ensure MANAGEMENT_PROFILE has Config aggregator access")
|
@@ -243,8 +248,14 @@ class EnhancedRDSSnapshotOptimizer:
|
|
243
248
|
"""
|
244
249
|
try:
|
245
250
|
expected_test_accounts = {
|
246
|
-
|
247
|
-
|
251
|
+
"91893567291",
|
252
|
+
"142964829704",
|
253
|
+
"363435891329",
|
254
|
+
"507583929055",
|
255
|
+
"614294421455",
|
256
|
+
"695366013198",
|
257
|
+
"761860562159",
|
258
|
+
"802669565615",
|
248
259
|
}
|
249
260
|
expected_total_snapshots = 71
|
250
261
|
|
@@ -253,8 +264,8 @@ class EnhancedRDSSnapshotOptimizer:
|
|
253
264
|
account_breakdown = {}
|
254
265
|
|
255
266
|
for snapshot in discovered_snapshots:
|
256
|
-
account_id = snapshot.get(
|
257
|
-
if account_id !=
|
267
|
+
account_id = snapshot.get("AccountId", "unknown")
|
268
|
+
if account_id != "unknown":
|
258
269
|
discovered_accounts.add(account_id)
|
259
270
|
if account_id not in account_breakdown:
|
260
271
|
account_breakdown[account_id] = 0
|
@@ -275,30 +286,36 @@ class EnhancedRDSSnapshotOptimizer:
|
|
275
286
|
{"header": "📊 Metric", "style": "cyan bold"},
|
276
287
|
{"header": "🔢 Expected", "style": "green bold"},
|
277
288
|
{"header": "🔢 Discovered", "style": "blue bold"},
|
278
|
-
{"header": "📈 Status", "style": "yellow bold"}
|
279
|
-
]
|
289
|
+
{"header": "📈 Status", "style": "yellow bold"},
|
290
|
+
],
|
280
291
|
)
|
281
292
|
|
282
293
|
# Total snapshots validation
|
283
|
-
snapshot_coverage = (
|
284
|
-
|
294
|
+
snapshot_coverage = (
|
295
|
+
(total_discovered / expected_total_snapshots) * 100 if expected_total_snapshots > 0 else 0
|
296
|
+
)
|
297
|
+
snapshot_status = (
|
298
|
+
"✅ Good" if snapshot_coverage >= 80 else "⚠️ Gap" if snapshot_coverage >= 60 else "❌ Poor"
|
299
|
+
)
|
285
300
|
|
286
301
|
validation_table.add_row(
|
287
302
|
"Total Snapshots",
|
288
303
|
str(expected_total_snapshots),
|
289
304
|
str(total_discovered),
|
290
|
-
f"{snapshot_status} ({snapshot_coverage:.1f}%)"
|
305
|
+
f"{snapshot_status} ({snapshot_coverage:.1f}%)",
|
291
306
|
)
|
292
307
|
|
293
308
|
# Account coverage validation
|
294
309
|
account_coverage = (len(test_accounts_found) / len(expected_test_accounts)) * 100
|
295
|
-
account_status =
|
310
|
+
account_status = (
|
311
|
+
"✅ Complete" if account_coverage == 100 else f"⚠️ Partial ({len(missing_test_accounts)} missing)"
|
312
|
+
)
|
296
313
|
|
297
314
|
validation_table.add_row(
|
298
315
|
"Test Accounts",
|
299
316
|
str(len(expected_test_accounts)),
|
300
317
|
str(len(test_accounts_found)),
|
301
|
-
f"{account_status} ({account_coverage:.1f}%)"
|
318
|
+
f"{account_status} ({account_coverage:.1f}%)",
|
302
319
|
)
|
303
320
|
|
304
321
|
console.print(validation_table)
|
@@ -318,7 +335,9 @@ class EnhancedRDSSnapshotOptimizer:
|
|
318
335
|
print_info(" • Consider direct RDS API calls for gap analysis")
|
319
336
|
|
320
337
|
elif total_discovered >= expected_total_snapshots:
|
321
|
-
print_success(
|
338
|
+
print_success(
|
339
|
+
f"✅ Discovery Success: Found {total_discovered} snapshots (≥{expected_total_snapshots} expected)"
|
340
|
+
)
|
322
341
|
|
323
342
|
if target_account_id and target_account_id in account_breakdown:
|
324
343
|
target_count = account_breakdown[target_account_id]
|
@@ -336,16 +355,16 @@ class EnhancedRDSSnapshotOptimizer:
|
|
336
355
|
try:
|
337
356
|
# Extract base metadata
|
338
357
|
snapshot_info = {
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
358
|
+
"DBSnapshotIdentifier": config_data.get("resourceId", "unknown"),
|
359
|
+
"AccountId": config_data.get("accountId", "unknown"),
|
360
|
+
"Region": config_data.get("awsRegion", "unknown"),
|
361
|
+
"DiscoveryMethod": "config_aggregator",
|
362
|
+
"ConfigCaptureTime": config_data.get("configurationItemCaptureTime"),
|
363
|
+
"ResourceCreationTime": config_data.get("resourceCreationTime"),
|
345
364
|
}
|
346
365
|
|
347
366
|
# Parse configuration details
|
348
|
-
configuration = config_data.get(
|
367
|
+
configuration = config_data.get("configuration", {})
|
349
368
|
if isinstance(configuration, str):
|
350
369
|
try:
|
351
370
|
configuration = json.loads(configuration)
|
@@ -362,99 +381,61 @@ class EnhancedRDSSnapshotOptimizer:
|
|
362
381
|
return configuration[field_name]
|
363
382
|
return default
|
364
383
|
|
365
|
-
snapshot_info.update(
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
# Timestamps with variations
|
401
|
-
'SnapshotCreateTime': safe_extract([
|
402
|
-
'snapshotCreateTime', 'SnapshotCreateTime', 'createTime'
|
403
|
-
]),
|
404
|
-
|
405
|
-
'InstanceCreateTime': safe_extract([
|
406
|
-
'instanceCreateTime', 'InstanceCreateTime'
|
407
|
-
]),
|
408
|
-
|
409
|
-
# Network and location
|
410
|
-
'VpcId': safe_extract([
|
411
|
-
'vpcId', 'VpcId', 'vpc'
|
412
|
-
]),
|
413
|
-
|
414
|
-
'AvailabilityZone': safe_extract([
|
415
|
-
'availabilityZone', 'AvailabilityZone', 'az'
|
416
|
-
]),
|
417
|
-
|
418
|
-
# Licensing and security
|
419
|
-
'LicenseModel': safe_extract([
|
420
|
-
'licenseModel', 'LicenseModel'
|
421
|
-
], 'unknown'),
|
422
|
-
|
423
|
-
'KmsKeyId': safe_extract([
|
424
|
-
'kmsKeyId', 'KmsKeyId', 'kmsKey'
|
425
|
-
]),
|
426
|
-
|
427
|
-
'IAMDatabaseAuthenticationEnabled': bool(safe_extract([
|
428
|
-
'iAMDatabaseAuthenticationEnabled', 'IAMDatabaseAuthenticationEnabled'
|
429
|
-
], False)),
|
430
|
-
|
431
|
-
# Tags with enhanced processing
|
432
|
-
'TagList': safe_extract([
|
433
|
-
'tagList', 'TagList', 'tags', 'Tags'
|
434
|
-
], [])
|
435
|
-
})
|
384
|
+
snapshot_info.update(
|
385
|
+
{
|
386
|
+
# Core identifiers with variations
|
387
|
+
"DBInstanceIdentifier": safe_extract(
|
388
|
+
["dBInstanceIdentifier", "dbInstanceIdentifier", "DBInstanceIdentifier"], "unknown"
|
389
|
+
),
|
390
|
+
"SnapshotType": safe_extract(["snapshotType", "SnapshotType", "type"], "unknown"),
|
391
|
+
"Status": safe_extract(["status", "Status", "snapshotStatus"], "unknown"),
|
392
|
+
"Engine": safe_extract(["engine", "Engine", "engineType"], "unknown"),
|
393
|
+
"EngineVersion": safe_extract(["engineVersion", "EngineVersion"], "unknown"),
|
394
|
+
# Storage details with type coercion
|
395
|
+
"AllocatedStorage": int(
|
396
|
+
safe_extract(["allocatedStorage", "AllocatedStorage", "storageSize"], 0) or 0
|
397
|
+
),
|
398
|
+
"StorageType": safe_extract(["storageType", "StorageType"], "gp2"),
|
399
|
+
"Encrypted": bool(safe_extract(["encrypted", "Encrypted", "storageEncrypted"], False)),
|
400
|
+
# Timestamps with variations
|
401
|
+
"SnapshotCreateTime": safe_extract(["snapshotCreateTime", "SnapshotCreateTime", "createTime"]),
|
402
|
+
"InstanceCreateTime": safe_extract(["instanceCreateTime", "InstanceCreateTime"]),
|
403
|
+
# Network and location
|
404
|
+
"VpcId": safe_extract(["vpcId", "VpcId", "vpc"]),
|
405
|
+
"AvailabilityZone": safe_extract(["availabilityZone", "AvailabilityZone", "az"]),
|
406
|
+
# Licensing and security
|
407
|
+
"LicenseModel": safe_extract(["licenseModel", "LicenseModel"], "unknown"),
|
408
|
+
"KmsKeyId": safe_extract(["kmsKeyId", "KmsKeyId", "kmsKey"]),
|
409
|
+
"IAMDatabaseAuthenticationEnabled": bool(
|
410
|
+
safe_extract(
|
411
|
+
["iAMDatabaseAuthenticationEnabled", "IAMDatabaseAuthenticationEnabled"], False
|
412
|
+
)
|
413
|
+
),
|
414
|
+
# Tags with enhanced processing
|
415
|
+
"TagList": safe_extract(["tagList", "TagList", "tags", "Tags"], []),
|
416
|
+
}
|
417
|
+
)
|
436
418
|
|
437
419
|
# Calculate age and cost estimates
|
438
|
-
snapshot_create_time = snapshot_info.get(
|
420
|
+
snapshot_create_time = snapshot_info.get("SnapshotCreateTime")
|
439
421
|
if snapshot_create_time:
|
440
422
|
try:
|
441
423
|
if isinstance(snapshot_create_time, str):
|
442
|
-
create_time = datetime.fromisoformat(
|
443
|
-
snapshot_create_time.replace('Z', '+00:00')
|
444
|
-
)
|
424
|
+
create_time = datetime.fromisoformat(snapshot_create_time.replace("Z", "+00:00"))
|
445
425
|
else:
|
446
426
|
create_time = snapshot_create_time
|
447
427
|
|
448
428
|
age_days = (datetime.now(timezone.utc) - create_time).days
|
449
|
-
snapshot_info[
|
429
|
+
snapshot_info["AgeDays"] = age_days
|
450
430
|
|
451
431
|
# PHASE 2 FIX: Calculate storage cost using dynamic pricing
|
452
|
-
allocated_storage = snapshot_info.get(
|
432
|
+
allocated_storage = snapshot_info.get("AllocatedStorage", 0)
|
453
433
|
if allocated_storage > 0:
|
454
434
|
# Get dynamic pricing if not already cached
|
455
435
|
if self.snapshot_cost_per_gb_month is None:
|
456
436
|
try:
|
457
437
|
import asyncio
|
438
|
+
|
458
439
|
loop = asyncio.new_event_loop()
|
459
440
|
asyncio.set_event_loop(loop)
|
460
441
|
self.snapshot_cost_per_gb_month = loop.run_until_complete(
|
@@ -466,17 +447,17 @@ class EnhancedRDSSnapshotOptimizer:
|
|
466
447
|
self.snapshot_cost_per_gb_month = 0.095
|
467
448
|
|
468
449
|
monthly_cost = allocated_storage * self.snapshot_cost_per_gb_month
|
469
|
-
snapshot_info[
|
470
|
-
snapshot_info[
|
450
|
+
snapshot_info["EstimatedMonthlyCost"] = round(monthly_cost, 2)
|
451
|
+
snapshot_info["EstimatedAnnualCost"] = round(monthly_cost * 12, 2)
|
471
452
|
else:
|
472
|
-
snapshot_info[
|
473
|
-
snapshot_info[
|
453
|
+
snapshot_info["EstimatedMonthlyCost"] = 0.0
|
454
|
+
snapshot_info["EstimatedAnnualCost"] = 0.0
|
474
455
|
|
475
456
|
except Exception as e:
|
476
457
|
logger.debug(f"Failed to calculate snapshot age/cost: {e}")
|
477
|
-
snapshot_info[
|
478
|
-
snapshot_info[
|
479
|
-
snapshot_info[
|
458
|
+
snapshot_info["AgeDays"] = 0
|
459
|
+
snapshot_info["EstimatedMonthlyCost"] = 0.0
|
460
|
+
snapshot_info["EstimatedAnnualCost"] = 0.0
|
480
461
|
|
481
462
|
return snapshot_info
|
482
463
|
|
@@ -486,23 +467,23 @@ class EnhancedRDSSnapshotOptimizer:
|
|
486
467
|
|
487
468
|
def _update_discovery_stats(self, snapshot: Dict) -> None:
|
488
469
|
"""Update discovery statistics with processed snapshot"""
|
489
|
-
self.discovery_stats[
|
470
|
+
self.discovery_stats["total_discovered"] += 1
|
490
471
|
|
491
|
-
snapshot_type = snapshot.get(
|
492
|
-
if snapshot_type ==
|
493
|
-
self.discovery_stats[
|
494
|
-
elif snapshot_type ==
|
495
|
-
self.discovery_stats[
|
472
|
+
snapshot_type = snapshot.get("SnapshotType", "").lower()
|
473
|
+
if snapshot_type == "manual":
|
474
|
+
self.discovery_stats["manual_snapshots"] += 1
|
475
|
+
elif snapshot_type == "automated":
|
476
|
+
self.discovery_stats["automated_snapshots"] += 1
|
496
477
|
|
497
|
-
account_id = snapshot.get(
|
498
|
-
if account_id !=
|
499
|
-
self.discovery_stats[
|
478
|
+
account_id = snapshot.get("AccountId", "unknown")
|
479
|
+
if account_id != "unknown":
|
480
|
+
self.discovery_stats["accounts_covered"].add(account_id)
|
500
481
|
|
501
|
-
allocated_storage = snapshot.get(
|
502
|
-
self.discovery_stats[
|
482
|
+
allocated_storage = snapshot.get("AllocatedStorage", 0)
|
483
|
+
self.discovery_stats["total_storage_gb"] += allocated_storage
|
503
484
|
|
504
|
-
monthly_cost = snapshot.get(
|
505
|
-
self.discovery_stats[
|
485
|
+
monthly_cost = snapshot.get("EstimatedMonthlyCost", 0.0)
|
486
|
+
self.discovery_stats["estimated_monthly_cost"] += monthly_cost
|
506
487
|
|
507
488
|
def _display_discovery_summary(self) -> None:
|
508
489
|
"""Display enhanced discovery summary"""
|
@@ -515,39 +496,23 @@ class EnhancedRDSSnapshotOptimizer:
|
|
515
496
|
columns=[
|
516
497
|
{"header": "📊 Metric", "style": "cyan bold"},
|
517
498
|
{"header": "🔢 Count", "style": "green bold"},
|
518
|
-
{"header": "ℹ️ Details", "style": "blue"}
|
519
|
-
]
|
499
|
+
{"header": "ℹ️ Details", "style": "blue"},
|
500
|
+
],
|
520
501
|
)
|
521
502
|
|
522
|
-
discovery_table.add_row(
|
523
|
-
|
524
|
-
|
525
|
-
"All snapshot types"
|
526
|
-
)
|
527
|
-
discovery_table.add_row(
|
528
|
-
"Manual Snapshots",
|
529
|
-
str(stats['manual_snapshots']),
|
530
|
-
"Cleanup candidates"
|
531
|
-
)
|
532
|
-
discovery_table.add_row(
|
533
|
-
"Automated Snapshots",
|
534
|
-
str(stats['automated_snapshots']),
|
535
|
-
"Retention policy managed"
|
536
|
-
)
|
503
|
+
discovery_table.add_row("Total Snapshots Discovered", str(stats["total_discovered"]), "All snapshot types")
|
504
|
+
discovery_table.add_row("Manual Snapshots", str(stats["manual_snapshots"]), "Cleanup candidates")
|
505
|
+
discovery_table.add_row("Automated Snapshots", str(stats["automated_snapshots"]), "Retention policy managed")
|
537
506
|
discovery_table.add_row(
|
538
507
|
"Accounts Covered",
|
539
|
-
str(len(stats[
|
540
|
-
f"Account IDs: {', '.join(sorted(stats['accounts_covered']))}"
|
508
|
+
str(len(stats["accounts_covered"])),
|
509
|
+
f"Account IDs: {', '.join(sorted(stats['accounts_covered']))}",
|
541
510
|
)
|
542
511
|
discovery_table.add_row(
|
543
|
-
"Total Storage",
|
544
|
-
f"{stats['total_storage_gb']:,} GB",
|
545
|
-
f"${stats['estimated_monthly_cost']:,.2f}/month"
|
512
|
+
"Total Storage", f"{stats['total_storage_gb']:,} GB", f"${stats['estimated_monthly_cost']:,.2f}/month"
|
546
513
|
)
|
547
514
|
discovery_table.add_row(
|
548
|
-
"Estimated Annual Cost",
|
549
|
-
format_cost(stats['estimated_monthly_cost'] * 12),
|
550
|
-
"Current snapshot storage cost"
|
515
|
+
"Estimated Annual Cost", format_cost(stats["estimated_monthly_cost"] * 12), "Current snapshot storage cost"
|
551
516
|
)
|
552
517
|
|
553
518
|
console.print(discovery_table)
|
@@ -567,95 +532,80 @@ class EnhancedRDSSnapshotOptimizer:
|
|
567
532
|
print_header(f"Enhanced RDS Snapshot Optimization Analysis")
|
568
533
|
|
569
534
|
# Categorize snapshots by type and age
|
570
|
-
manual_snapshots = [s for s in snapshots if s.get(
|
571
|
-
automated_snapshots = [s for s in snapshots if s.get(
|
535
|
+
manual_snapshots = [s for s in snapshots if s.get("SnapshotType", "").lower() == "manual"]
|
536
|
+
automated_snapshots = [s for s in snapshots if s.get("SnapshotType", "").lower() == "automated"]
|
572
537
|
|
573
538
|
# ENHANCED OPTIMIZATION LOGIC: Multiple optimization categories
|
574
539
|
optimization_categories = []
|
575
540
|
|
576
541
|
# Category 1: Old manual snapshots (conservative cleanup)
|
577
|
-
old_manual_snapshots = [
|
578
|
-
s for s in manual_snapshots
|
579
|
-
if s.get('AgeDays', 0) >= age_threshold
|
580
|
-
]
|
542
|
+
old_manual_snapshots = [s for s in manual_snapshots if s.get("AgeDays", 0) >= age_threshold]
|
581
543
|
|
582
544
|
# Category 2: Very old automated snapshots (>365 days - potential retention review)
|
583
|
-
very_old_automated = [
|
584
|
-
s for s in automated_snapshots
|
585
|
-
if s.get('AgeDays', 0) >= 365
|
586
|
-
]
|
545
|
+
very_old_automated = [s for s in automated_snapshots if s.get("AgeDays", 0) >= 365]
|
587
546
|
|
588
547
|
# Category 3: Automated snapshots >180 days (retention policy review)
|
589
548
|
old_automated_review = [
|
590
|
-
s for s in automated_snapshots
|
591
|
-
if s.get('AgeDays', 0) >= 180 and s.get('AgeDays', 0) < 365
|
549
|
+
s for s in automated_snapshots if s.get("AgeDays", 0) >= 180 and s.get("AgeDays", 0) < 365
|
592
550
|
]
|
593
551
|
|
594
552
|
# Category 4: All snapshots >90 days (comprehensive review scenario)
|
595
|
-
all_old_snapshots = [
|
596
|
-
s for s in snapshots
|
597
|
-
if s.get('AgeDays', 0) >= age_threshold
|
598
|
-
]
|
553
|
+
all_old_snapshots = [s for s in snapshots if s.get("AgeDays", 0) >= age_threshold]
|
599
554
|
|
600
555
|
# Calculate savings for different optimization scenarios
|
601
556
|
scenarios = {
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
557
|
+
"conservative_manual": {
|
558
|
+
"snapshots": old_manual_snapshots,
|
559
|
+
"description": f"Manual snapshots >{age_threshold} days (safe cleanup)",
|
560
|
+
"risk_level": "Low",
|
606
561
|
},
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
562
|
+
"automated_review": {
|
563
|
+
"snapshots": very_old_automated,
|
564
|
+
"description": "Automated snapshots >365 days (retention review)",
|
565
|
+
"risk_level": "Medium",
|
611
566
|
},
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
567
|
+
"comprehensive": {
|
568
|
+
"snapshots": all_old_snapshots,
|
569
|
+
"description": f"All snapshots >{age_threshold} days (comprehensive review)",
|
570
|
+
"risk_level": "Medium-High",
|
571
|
+
},
|
572
|
+
"retention_optimization": {
|
573
|
+
"snapshots": old_automated_review,
|
574
|
+
"description": "Automated snapshots 180-365 days (policy optimization)",
|
575
|
+
"risk_level": "Low-Medium",
|
616
576
|
},
|
617
|
-
'retention_optimization': {
|
618
|
-
'snapshots': old_automated_review,
|
619
|
-
'description': 'Automated snapshots 180-365 days (policy optimization)',
|
620
|
-
'risk_level': 'Low-Medium'
|
621
|
-
}
|
622
577
|
}
|
623
578
|
|
624
579
|
# Calculate savings for each scenario
|
625
580
|
optimization_results = {}
|
626
581
|
for scenario_name, scenario_data in scenarios.items():
|
627
|
-
snapshots_list = scenario_data[
|
628
|
-
storage_gb = sum(s.get(
|
629
|
-
monthly_cost = sum(s.get(
|
582
|
+
snapshots_list = scenario_data["snapshots"]
|
583
|
+
storage_gb = sum(s.get("AllocatedStorage", 0) for s in snapshots_list)
|
584
|
+
monthly_cost = sum(s.get("EstimatedMonthlyCost", 0) for s in snapshots_list)
|
630
585
|
annual_savings = monthly_cost * 12
|
631
586
|
|
632
587
|
optimization_results[scenario_name] = {
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
588
|
+
"count": len(snapshots_list),
|
589
|
+
"storage_gb": storage_gb,
|
590
|
+
"monthly_cost": monthly_cost,
|
591
|
+
"annual_savings": annual_savings,
|
592
|
+
"description": scenario_data["description"],
|
593
|
+
"risk_level": scenario_data["risk_level"],
|
594
|
+
"snapshots": snapshots_list,
|
640
595
|
}
|
641
596
|
|
642
597
|
# Account breakdown for the most realistic scenario (comprehensive review)
|
643
|
-
primary_scenario = optimization_results[
|
598
|
+
primary_scenario = optimization_results["comprehensive"]
|
644
599
|
account_breakdown = {}
|
645
|
-
for snapshot in primary_scenario[
|
646
|
-
account_id = snapshot.get(
|
600
|
+
for snapshot in primary_scenario["snapshots"]:
|
601
|
+
account_id = snapshot.get("AccountId", "unknown")
|
647
602
|
if account_id not in account_breakdown:
|
648
|
-
account_breakdown[account_id] = {
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
account_breakdown[account_id]['count'] += 1
|
656
|
-
account_breakdown[account_id]['storage_gb'] += snapshot.get('AllocatedStorage', 0)
|
657
|
-
account_breakdown[account_id]['monthly_cost'] += snapshot.get('EstimatedMonthlyCost', 0.0)
|
658
|
-
account_breakdown[account_id]['snapshots'].append(snapshot.get('DBSnapshotIdentifier', 'unknown'))
|
603
|
+
account_breakdown[account_id] = {"count": 0, "storage_gb": 0, "monthly_cost": 0.0, "snapshots": []}
|
604
|
+
|
605
|
+
account_breakdown[account_id]["count"] += 1
|
606
|
+
account_breakdown[account_id]["storage_gb"] += snapshot.get("AllocatedStorage", 0)
|
607
|
+
account_breakdown[account_id]["monthly_cost"] += snapshot.get("EstimatedMonthlyCost", 0.0)
|
608
|
+
account_breakdown[account_id]["snapshots"].append(snapshot.get("DBSnapshotIdentifier", "unknown"))
|
659
609
|
|
660
610
|
# Display comprehensive optimization results
|
661
611
|
optimization_table = create_table(
|
@@ -666,8 +616,8 @@ class EnhancedRDSSnapshotOptimizer:
|
|
666
616
|
{"header": "📊 Snapshots", "style": "green bold"},
|
667
617
|
{"header": "💾 Storage (GB)", "style": "yellow bold"},
|
668
618
|
{"header": "💵 Annual Savings", "style": "red bold"},
|
669
|
-
{"header": "⚠️ Risk Level", "style": "blue bold"}
|
670
|
-
]
|
619
|
+
{"header": "⚠️ Risk Level", "style": "blue bold"},
|
620
|
+
],
|
671
621
|
)
|
672
622
|
|
673
623
|
# Current state (baseline)
|
@@ -675,37 +625,37 @@ class EnhancedRDSSnapshotOptimizer:
|
|
675
625
|
"📊 Current State (All Snapshots)",
|
676
626
|
str(len(snapshots)),
|
677
627
|
f"{sum(s.get('AllocatedStorage', 0) for s in snapshots):,}",
|
678
|
-
format_cost(sum(s.get(
|
679
|
-
"Baseline"
|
628
|
+
format_cost(sum(s.get("EstimatedMonthlyCost", 0) for s in snapshots) * 12),
|
629
|
+
"Baseline",
|
680
630
|
)
|
681
631
|
|
682
632
|
# Display all optimization scenarios
|
683
|
-
scenario_priorities = [
|
633
|
+
scenario_priorities = ["conservative_manual", "retention_optimization", "automated_review", "comprehensive"]
|
684
634
|
scenario_icons = {
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
635
|
+
"conservative_manual": "🟢",
|
636
|
+
"retention_optimization": "🟡",
|
637
|
+
"automated_review": "🟠",
|
638
|
+
"comprehensive": "🔴",
|
689
639
|
}
|
690
640
|
|
691
641
|
for scenario_name in scenario_priorities:
|
692
642
|
if scenario_name in optimization_results:
|
693
643
|
scenario = optimization_results[scenario_name]
|
694
|
-
icon = scenario_icons.get(scenario_name,
|
644
|
+
icon = scenario_icons.get(scenario_name, "📋")
|
695
645
|
|
696
646
|
optimization_table.add_row(
|
697
647
|
f"{icon} {scenario['description']}",
|
698
|
-
str(scenario[
|
648
|
+
str(scenario["count"]),
|
699
649
|
f"{scenario['storage_gb']:,}",
|
700
|
-
format_cost(scenario[
|
701
|
-
scenario[
|
650
|
+
format_cost(scenario["annual_savings"]),
|
651
|
+
scenario["risk_level"],
|
702
652
|
)
|
703
653
|
|
704
654
|
console.print(optimization_table)
|
705
655
|
|
706
656
|
# Recommended scenario analysis
|
707
|
-
recommended_scenario = optimization_results[
|
708
|
-
if recommended_scenario[
|
657
|
+
recommended_scenario = optimization_results["comprehensive"] # Most realistic
|
658
|
+
if recommended_scenario["annual_savings"] > 0:
|
709
659
|
print_success(
|
710
660
|
f"💰 RECOMMENDED: Comprehensive review scenario - "
|
711
661
|
f"{recommended_scenario['count']} snapshots, "
|
@@ -724,18 +674,18 @@ class EnhancedRDSSnapshotOptimizer:
|
|
724
674
|
{"header": "📸 Snapshots", "style": "green bold"},
|
725
675
|
{"header": "💾 Storage (GB)", "style": "yellow bold"},
|
726
676
|
{"header": "💰 Monthly Savings", "style": "red"},
|
727
|
-
{"header": "💵 Annual Savings", "style": "red bold"}
|
728
|
-
]
|
677
|
+
{"header": "💵 Annual Savings", "style": "red bold"},
|
678
|
+
],
|
729
679
|
)
|
730
680
|
|
731
681
|
for account_id, data in sorted(account_breakdown.items()):
|
732
|
-
annual_savings = data[
|
682
|
+
annual_savings = data["monthly_cost"] * 12
|
733
683
|
account_table.add_row(
|
734
684
|
account_id,
|
735
|
-
str(data[
|
685
|
+
str(data["count"]),
|
736
686
|
f"{data['storage_gb']:,}",
|
737
|
-
format_cost(data[
|
738
|
-
format_cost(annual_savings)
|
687
|
+
format_cost(data["monthly_cost"]),
|
688
|
+
format_cost(annual_savings),
|
739
689
|
)
|
740
690
|
|
741
691
|
console.print(account_table)
|
@@ -744,7 +694,7 @@ class EnhancedRDSSnapshotOptimizer:
|
|
744
694
|
target_account = "142964829704"
|
745
695
|
if target_account in account_breakdown:
|
746
696
|
target_data = account_breakdown[target_account]
|
747
|
-
target_annual = target_data[
|
697
|
+
target_annual = target_data["monthly_cost"] * 12
|
748
698
|
|
749
699
|
print_success(
|
750
700
|
f"🎯 Target Account {target_account}: "
|
@@ -754,7 +704,7 @@ class EnhancedRDSSnapshotOptimizer:
|
|
754
704
|
)
|
755
705
|
|
756
706
|
# Enhanced detailed snapshot table with all requested columns
|
757
|
-
if recommended_scenario[
|
707
|
+
if recommended_scenario["snapshots"]:
|
758
708
|
print_info(f"\n📋 Detailed Snapshot Analysis for Cleanup Candidates:")
|
759
709
|
|
760
710
|
detailed_table = create_table(
|
@@ -768,38 +718,37 @@ class EnhancedRDSSnapshotOptimizer:
|
|
768
718
|
{"header": "🗑️ Can be Deleted", "style": "red bold"},
|
769
719
|
{"header": "⚙️ Type", "style": "magenta bold"},
|
770
720
|
{"header": "📅 Created", "style": "bright_blue"},
|
771
|
-
{"header": "🏷️ Tags", "style": "dim"}
|
772
|
-
]
|
721
|
+
{"header": "🏷️ Tags", "style": "dim"},
|
722
|
+
],
|
773
723
|
)
|
774
724
|
|
775
725
|
# Sort snapshots by account ID, then by age (oldest first)
|
776
726
|
sorted_snapshots = sorted(
|
777
|
-
recommended_scenario[
|
778
|
-
key=lambda x: (x.get('AccountId', 'unknown'), -x.get('AgeDays', 0))
|
727
|
+
recommended_scenario["snapshots"], key=lambda x: (x.get("AccountId", "unknown"), -x.get("AgeDays", 0))
|
779
728
|
)
|
780
729
|
|
781
730
|
# Display first 50 snapshots to avoid overwhelming output
|
782
731
|
display_limit = 50
|
783
732
|
for i, snapshot in enumerate(sorted_snapshots[:display_limit]):
|
784
733
|
# Account ID
|
785
|
-
account_id = snapshot.get(
|
734
|
+
account_id = snapshot.get("AccountId", "unknown")
|
786
735
|
|
787
736
|
# Snapshot ID
|
788
|
-
snapshot_id = snapshot.get(
|
737
|
+
snapshot_id = snapshot.get("DBSnapshotIdentifier", "unknown")
|
789
738
|
|
790
739
|
# DB Instance ID
|
791
|
-
db_instance_id = snapshot.get(
|
740
|
+
db_instance_id = snapshot.get("DBInstanceIdentifier", "unknown")
|
792
741
|
|
793
742
|
# Size in GiB
|
794
|
-
size_gib = snapshot.get(
|
743
|
+
size_gib = snapshot.get("AllocatedStorage", 0)
|
795
744
|
|
796
745
|
# Can be Deleted analysis
|
797
|
-
age_days = snapshot.get(
|
798
|
-
snapshot_type = snapshot.get(
|
746
|
+
age_days = snapshot.get("AgeDays", 0)
|
747
|
+
snapshot_type = snapshot.get("SnapshotType", "unknown").lower()
|
799
748
|
|
800
|
-
if snapshot_type ==
|
749
|
+
if snapshot_type == "manual" and age_days >= age_threshold:
|
801
750
|
can_delete = "✅ Yes (Manual)"
|
802
|
-
elif snapshot_type ==
|
751
|
+
elif snapshot_type == "automated" and age_days >= 365:
|
803
752
|
can_delete = "⚠️ Review Policy"
|
804
753
|
elif age_days >= age_threshold:
|
805
754
|
can_delete = "📋 Needs Review"
|
@@ -807,28 +756,28 @@ class EnhancedRDSSnapshotOptimizer:
|
|
807
756
|
can_delete = "❌ Keep"
|
808
757
|
|
809
758
|
# Manual/Automated
|
810
|
-
type_display = "🔧 Manual" if snapshot_type ==
|
759
|
+
type_display = "🔧 Manual" if snapshot_type == "manual" else "🤖 Automated"
|
811
760
|
|
812
761
|
# Creation Time
|
813
|
-
create_time = snapshot.get(
|
762
|
+
create_time = snapshot.get("SnapshotCreateTime")
|
814
763
|
if create_time:
|
815
764
|
if isinstance(create_time, str):
|
816
765
|
create_time_display = create_time[:10] # YYYY-MM-DD
|
817
766
|
else:
|
818
|
-
create_time_display = create_time.strftime(
|
767
|
+
create_time_display = create_time.strftime("%Y-%m-%d")
|
819
768
|
else:
|
820
|
-
create_time_display =
|
769
|
+
create_time_display = "unknown"
|
821
770
|
|
822
771
|
# Tags
|
823
|
-
tag_list = snapshot.get(
|
772
|
+
tag_list = snapshot.get("TagList", [])
|
824
773
|
if tag_list and isinstance(tag_list, list):
|
825
774
|
# Display first 2 tags to avoid table width issues
|
826
|
-
tag_names = [tag.get(
|
827
|
-
tags_display =
|
775
|
+
tag_names = [tag.get("Key", "") for tag in tag_list[:2] if isinstance(tag, dict)]
|
776
|
+
tags_display = ", ".join(tag_names) if tag_names else "None"
|
828
777
|
if len(tag_list) > 2:
|
829
|
-
tags_display += f" (+{len(tag_list)-2})"
|
778
|
+
tags_display += f" (+{len(tag_list) - 2})"
|
830
779
|
else:
|
831
|
-
tags_display =
|
780
|
+
tags_display = "None"
|
832
781
|
|
833
782
|
detailed_table.add_row(
|
834
783
|
account_id,
|
@@ -838,31 +787,28 @@ class EnhancedRDSSnapshotOptimizer:
|
|
838
787
|
can_delete,
|
839
788
|
type_display,
|
840
789
|
create_time_display,
|
841
|
-
tags_display
|
790
|
+
tags_display,
|
842
791
|
)
|
843
792
|
|
844
793
|
console.print(detailed_table)
|
845
794
|
|
846
795
|
# Show summary if we hit the display limit
|
847
|
-
total_candidates = len(recommended_scenario[
|
796
|
+
total_candidates = len(recommended_scenario["snapshots"])
|
848
797
|
if total_candidates > display_limit:
|
849
|
-
print_info(
|
850
|
-
f"📊 Showing top {display_limit} snapshots. "
|
851
|
-
f"Total cleanup candidates: {total_candidates}"
|
852
|
-
)
|
798
|
+
print_info(f"📊 Showing top {display_limit} snapshots. Total cleanup candidates: {total_candidates}")
|
853
799
|
|
854
800
|
# Return enhanced optimization results with multiple scenarios
|
855
801
|
return {
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
802
|
+
"total_snapshots": len(snapshots),
|
803
|
+
"manual_snapshots": len(manual_snapshots),
|
804
|
+
"automated_snapshots": len(automated_snapshots),
|
805
|
+
"optimization_scenarios": optimization_results,
|
806
|
+
"account_breakdown": account_breakdown,
|
807
|
+
"target_account_data": account_breakdown.get("142964829704", {}),
|
862
808
|
# Legacy compatibility (use comprehensive scenario as primary)
|
863
|
-
|
864
|
-
|
865
|
-
|
809
|
+
"cleanup_candidates": primary_scenario["count"],
|
810
|
+
"potential_monthly_savings": primary_scenario["monthly_cost"],
|
811
|
+
"potential_annual_savings": primary_scenario["annual_savings"],
|
866
812
|
}
|
867
813
|
|
868
814
|
def display_comprehensive_snapshot_table(self, snapshots: List[Dict], manual_only: bool = False) -> None:
|
@@ -879,18 +825,15 @@ class EnhancedRDSSnapshotOptimizer:
|
|
879
825
|
return
|
880
826
|
|
881
827
|
# Import embedded MCP validator
|
882
|
-
from .
|
828
|
+
from .mcp_validator import EmbeddedMCPValidator
|
883
829
|
|
884
830
|
# Initialize MCP validator
|
885
|
-
mcp_validator = EmbeddedMCPValidator(
|
886
|
-
profiles=[self.profile] if self.profile else [],
|
887
|
-
console=console
|
888
|
-
)
|
831
|
+
mcp_validator = EmbeddedMCPValidator(profiles=[self.profile] if self.profile else [], console=console)
|
889
832
|
|
890
833
|
# Create comprehensive table
|
891
834
|
table = create_table(
|
892
835
|
title=f"RDS Snapshots Analysis ({len(snapshots)} total{'Manual only' if manual_only else ''})",
|
893
|
-
caption="Complete snapshot inventory with MCP validation"
|
836
|
+
caption="Complete snapshot inventory with MCP validation",
|
894
837
|
)
|
895
838
|
|
896
839
|
# Add columns as requested by user
|
@@ -905,32 +848,32 @@ class EnhancedRDSSnapshotOptimizer:
|
|
905
848
|
table.add_column("MCP-checked", style="bright_green", justify="center", no_wrap=True)
|
906
849
|
|
907
850
|
# Sort snapshots by creation time (oldest to newest)
|
908
|
-
sorted_snapshots = sorted(snapshots, key=lambda s: s.get(
|
851
|
+
sorted_snapshots = sorted(snapshots, key=lambda s: s.get("SnapshotCreateTime", ""))
|
909
852
|
|
910
853
|
# Add rows for each snapshot
|
911
854
|
for snapshot in sorted_snapshots:
|
912
855
|
# Basic fields
|
913
|
-
account_id = snapshot.get(
|
914
|
-
snapshot_id = snapshot.get(
|
915
|
-
db_instance_id = snapshot.get(
|
916
|
-
size_gb = snapshot.get(
|
917
|
-
snapshot_type = snapshot.get(
|
856
|
+
account_id = snapshot.get("AccountId", "unknown")
|
857
|
+
snapshot_id = snapshot.get("DBSnapshotIdentifier", "unknown")
|
858
|
+
db_instance_id = snapshot.get("DBInstanceIdentifier", "unknown")
|
859
|
+
size_gb = snapshot.get("AllocatedStorage", 0)
|
860
|
+
snapshot_type = snapshot.get("SnapshotType", "unknown")
|
918
861
|
|
919
862
|
# Age-based deletion recommendation
|
920
|
-
age_days = snapshot.get(
|
921
|
-
can_delete = "YES" if (snapshot_type ==
|
863
|
+
age_days = snapshot.get("AgeDays", 0)
|
864
|
+
can_delete = "YES" if (snapshot_type == "manual" and age_days > 90) else "NO"
|
922
865
|
can_delete_style = "[red]YES[/red]" if can_delete == "YES" else "[green]NO[/green]"
|
923
866
|
|
924
867
|
# Manual/Automated display
|
925
|
-
type_display = "[yellow]Manual[/yellow]" if snapshot_type ==
|
868
|
+
type_display = "[yellow]Manual[/yellow]" if snapshot_type == "manual" else "[blue]Automated[/blue]"
|
926
869
|
|
927
870
|
# Creation time (formatted)
|
928
|
-
create_time = snapshot.get(
|
871
|
+
create_time = snapshot.get("SnapshotCreateTime", "")
|
929
872
|
if create_time:
|
930
873
|
try:
|
931
874
|
if isinstance(create_time, str):
|
932
|
-
dt = datetime.fromisoformat(create_time.replace(
|
933
|
-
create_time_display = dt.strftime(
|
875
|
+
dt = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
|
876
|
+
create_time_display = dt.strftime("%Y-%m-%d")
|
934
877
|
else:
|
935
878
|
create_time_display = str(create_time)[:10]
|
936
879
|
except:
|
@@ -939,11 +882,11 @@ class EnhancedRDSSnapshotOptimizer:
|
|
939
882
|
create_time_display = "Unknown"
|
940
883
|
|
941
884
|
# Tags (formatted)
|
942
|
-
tags = snapshot.get(
|
885
|
+
tags = snapshot.get("TagList", [])
|
943
886
|
if tags and isinstance(tags, list):
|
944
887
|
tag_strs = []
|
945
888
|
for tag in tags[:2]: # Show first 2 tags
|
946
|
-
if isinstance(tag, dict) and
|
889
|
+
if isinstance(tag, dict) and "Key" in tag:
|
947
890
|
tag_strs.append(f"{tag['Key']}:{tag.get('Value', '')}")
|
948
891
|
tags_display = ", ".join(tag_strs)
|
949
892
|
if len(tags) > 2:
|
@@ -965,15 +908,15 @@ class EnhancedRDSSnapshotOptimizer:
|
|
965
908
|
type_display,
|
966
909
|
create_time_display,
|
967
910
|
tags_display,
|
968
|
-
mcp_display
|
911
|
+
mcp_display,
|
969
912
|
)
|
970
913
|
|
971
914
|
console.print(table)
|
972
915
|
|
973
916
|
# Display summary statistics
|
974
|
-
manual_count = len([s for s in snapshots if s.get(
|
975
|
-
automated_count = len([s for s in snapshots if s.get(
|
976
|
-
total_size = sum(s.get(
|
917
|
+
manual_count = len([s for s in snapshots if s.get("SnapshotType") == "manual"])
|
918
|
+
automated_count = len([s for s in snapshots if s.get("SnapshotType") == "automated"])
|
919
|
+
total_size = sum(s.get("AllocatedStorage", 0) for s in snapshots)
|
977
920
|
|
978
921
|
print_info(f"📊 Summary: {len(snapshots)} total snapshots ({manual_count} manual, {automated_count} automated)")
|
979
922
|
print_info(f"💾 Total Storage: {total_size:,} GiB")
|
@@ -985,12 +928,12 @@ class EnhancedRDSSnapshotOptimizer:
|
|
985
928
|
"""
|
986
929
|
try:
|
987
930
|
# Basic validation - check if snapshot data is consistent
|
988
|
-
required_fields = [
|
931
|
+
required_fields = ["DBSnapshotIdentifier", "AccountId", "AllocatedStorage"]
|
989
932
|
has_required = all(snapshot.get(field) for field in required_fields)
|
990
933
|
|
991
934
|
# Additional checks
|
992
|
-
size_valid = isinstance(snapshot.get(
|
993
|
-
account_valid = len(str(snapshot.get(
|
935
|
+
size_valid = isinstance(snapshot.get("AllocatedStorage"), int) and snapshot.get("AllocatedStorage", 0) > 0
|
936
|
+
account_valid = len(str(snapshot.get("AccountId", ""))) == 12
|
994
937
|
|
995
938
|
return has_required and size_valid and account_valid
|
996
939
|
except Exception:
|
@@ -1000,7 +943,7 @@ class EnhancedRDSSnapshotOptimizer:
|
|
1000
943
|
"""Original Config aggregator discovery method"""
|
1001
944
|
try:
|
1002
945
|
# Use ap-southeast-2 where organization aggregator is configured
|
1003
|
-
config_client = self.session.client(
|
946
|
+
config_client = self.session.client("config", region_name="ap-southeast-2")
|
1004
947
|
|
1005
948
|
print_info("🔍 Discovering RDS snapshots via AWS Config organization aggregator...")
|
1006
949
|
|
@@ -1033,19 +976,19 @@ class EnhancedRDSSnapshotOptimizer:
|
|
1033
976
|
|
1034
977
|
while True:
|
1035
978
|
query_params = {
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
979
|
+
"ConfigurationAggregatorName": "organization-aggregator",
|
980
|
+
"Expression": query_expression,
|
981
|
+
"Limit": 100, # Maximum allowed by Config API
|
1039
982
|
}
|
1040
983
|
|
1041
984
|
if next_token:
|
1042
|
-
query_params[
|
985
|
+
query_params["NextToken"] = next_token
|
1043
986
|
|
1044
987
|
response = config_client.select_aggregate_resource_config(**query_params)
|
1045
|
-
results = response.get(
|
988
|
+
results = response.get("Results", [])
|
1046
989
|
all_results.extend(results)
|
1047
990
|
|
1048
|
-
next_token = response.get(
|
991
|
+
next_token = response.get("NextToken")
|
1049
992
|
if not next_token:
|
1050
993
|
break
|
1051
994
|
|
@@ -1063,33 +1006,40 @@ class EnhancedRDSSnapshotOptimizer:
|
|
1063
1006
|
|
1064
1007
|
# User's 8 test accounts
|
1065
1008
|
test_accounts = {
|
1066
|
-
|
1067
|
-
|
1009
|
+
"91893567291",
|
1010
|
+
"142964829704",
|
1011
|
+
"363435891329",
|
1012
|
+
"507583929055",
|
1013
|
+
"614294421455",
|
1014
|
+
"695366013198",
|
1015
|
+
"761860562159",
|
1016
|
+
"802669565615",
|
1068
1017
|
}
|
1069
1018
|
|
1070
1019
|
discovered_snapshots = []
|
1071
1020
|
|
1072
1021
|
# Test regions where snapshots might exist
|
1073
|
-
regions = [
|
1022
|
+
regions = ["ap-southeast-2", "us-east-1", "us-west-2", "eu-west-1"]
|
1074
1023
|
|
1075
1024
|
for region in regions:
|
1076
1025
|
try:
|
1077
|
-
rds_client = self.session.client(
|
1026
|
+
rds_client = self.session.client("rds", region_name=region)
|
1078
1027
|
print_info(f"🌏 Scanning region {region}...")
|
1079
1028
|
|
1080
1029
|
# Get snapshots
|
1081
|
-
paginator = rds_client.get_paginator(
|
1030
|
+
paginator = rds_client.get_paginator("describe_db_snapshots")
|
1082
1031
|
|
1083
|
-
snapshot_type =
|
1084
|
-
page_iterator = paginator.paginate(
|
1085
|
-
SnapshotType=snapshot_type,
|
1086
|
-
MaxRecords=100
|
1087
|
-
)
|
1032
|
+
snapshot_type = "manual" if manual_only else "all"
|
1033
|
+
page_iterator = paginator.paginate(SnapshotType=snapshot_type, MaxRecords=100)
|
1088
1034
|
|
1089
1035
|
for page in page_iterator:
|
1090
|
-
for snapshot in page.get(
|
1036
|
+
for snapshot in page.get("DBSnapshots", []):
|
1091
1037
|
# Extract account from ARN
|
1092
|
-
account_id =
|
1038
|
+
account_id = (
|
1039
|
+
snapshot.get("DBSnapshotArn", "").split(":")[4]
|
1040
|
+
if snapshot.get("DBSnapshotArn")
|
1041
|
+
else "unknown"
|
1042
|
+
)
|
1093
1043
|
|
1094
1044
|
# Filter for test accounts if no specific target, or match target
|
1095
1045
|
if target_account_id:
|
@@ -1111,44 +1061,46 @@ class EnhancedRDSSnapshotOptimizer:
|
|
1111
1061
|
try:
|
1112
1062
|
# Map RDS API fields to our standardized format
|
1113
1063
|
processed_snapshot = {
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1064
|
+
"DBSnapshotIdentifier": rds_snapshot.get("DBSnapshotIdentifier"),
|
1065
|
+
"DBInstanceIdentifier": rds_snapshot.get("DBInstanceIdentifier"),
|
1066
|
+
"SnapshotCreateTime": rds_snapshot.get("SnapshotCreateTime"),
|
1067
|
+
"Engine": rds_snapshot.get("Engine"),
|
1068
|
+
"AllocatedStorage": rds_snapshot.get("AllocatedStorage"),
|
1069
|
+
"Status": rds_snapshot.get("Status"),
|
1070
|
+
"Port": rds_snapshot.get("Port"),
|
1071
|
+
"SnapshotType": rds_snapshot.get("SnapshotType"),
|
1072
|
+
"Encrypted": rds_snapshot.get("Encrypted"),
|
1073
|
+
"KmsKeyId": rds_snapshot.get("KmsKeyId"),
|
1074
|
+
"TagList": rds_snapshot.get("TagList", []),
|
1075
|
+
"Region": rds_snapshot.get("AvailabilityZone", "").split("-")[0]
|
1076
|
+
if rds_snapshot.get("AvailabilityZone")
|
1077
|
+
else "unknown",
|
1078
|
+
"DiscoveryMethod": "direct_rds_api",
|
1127
1079
|
}
|
1128
1080
|
|
1129
1081
|
# Extract account from ARN
|
1130
|
-
arn = rds_snapshot.get(
|
1082
|
+
arn = rds_snapshot.get("DBSnapshotArn", "")
|
1131
1083
|
if arn:
|
1132
|
-
account_id = arn.split(
|
1133
|
-
processed_snapshot[
|
1084
|
+
account_id = arn.split(":")[4]
|
1085
|
+
processed_snapshot["AccountId"] = account_id
|
1134
1086
|
|
1135
1087
|
# Calculate age and costs
|
1136
|
-
snapshot_create_time = processed_snapshot.get(
|
1088
|
+
snapshot_create_time = processed_snapshot.get("SnapshotCreateTime")
|
1137
1089
|
if snapshot_create_time:
|
1138
1090
|
if isinstance(snapshot_create_time, str):
|
1139
|
-
create_time = datetime.fromisoformat(snapshot_create_time.replace(
|
1091
|
+
create_time = datetime.fromisoformat(snapshot_create_time.replace("Z", "+00:00"))
|
1140
1092
|
else:
|
1141
1093
|
create_time = snapshot_create_time
|
1142
1094
|
|
1143
1095
|
age_days = (datetime.now(timezone.utc) - create_time).days
|
1144
|
-
processed_snapshot[
|
1096
|
+
processed_snapshot["AgeDays"] = age_days
|
1145
1097
|
|
1146
1098
|
# Calculate costs
|
1147
|
-
allocated_storage = processed_snapshot.get(
|
1099
|
+
allocated_storage = processed_snapshot.get("AllocatedStorage", 0)
|
1148
1100
|
if allocated_storage > 0:
|
1149
1101
|
monthly_cost = allocated_storage * 0.095 # Use fallback pricing
|
1150
|
-
processed_snapshot[
|
1151
|
-
processed_snapshot[
|
1102
|
+
processed_snapshot["EstimatedMonthlyCost"] = round(monthly_cost, 2)
|
1103
|
+
processed_snapshot["EstimatedAnnualCost"] = round(monthly_cost * 12, 2)
|
1152
1104
|
|
1153
1105
|
return processed_snapshot
|
1154
1106
|
|
@@ -1158,16 +1110,16 @@ class EnhancedRDSSnapshotOptimizer:
|
|
1158
1110
|
|
1159
1111
|
|
1160
1112
|
@click.command()
|
1161
|
-
@click.option(
|
1162
|
-
@click.option(
|
1163
|
-
@click.option(
|
1164
|
-
@click.option(
|
1165
|
-
@click.option(
|
1166
|
-
@click.option(
|
1167
|
-
@click.option(
|
1168
|
-
@click.option(
|
1169
|
-
@click.option(
|
1170
|
-
@click.option(
|
1113
|
+
@click.option("--all", "-a", is_flag=True, help="Organization-wide discovery using management profile")
|
1114
|
+
@click.option("--profile", help="AWS profile for authentication or target account ID for filtering")
|
1115
|
+
@click.option("--target-account", help="[DEPRECATED] Use --profile instead. Target account ID for filtering")
|
1116
|
+
@click.option("--age-threshold", type=int, default=90, help="Age threshold for cleanup (days)")
|
1117
|
+
@click.option("--days", type=int, help="Age threshold in days (alias for --age-threshold)")
|
1118
|
+
@click.option("--aging", type=int, help="Age threshold in days (alias for --age-threshold)")
|
1119
|
+
@click.option("--manual", is_flag=True, help="Filter only manual snapshots (exclude automated)")
|
1120
|
+
@click.option("--dry-run/--execute", default=True, help="Analysis mode vs execution mode")
|
1121
|
+
@click.option("--output-file", help="Export results to CSV file")
|
1122
|
+
@click.option("--analyze", is_flag=True, help="Perform comprehensive optimization analysis")
|
1171
1123
|
def optimize_rds_snapshots(
|
1172
1124
|
all: bool,
|
1173
1125
|
profile: str,
|
@@ -1178,7 +1130,7 @@ def optimize_rds_snapshots(
|
|
1178
1130
|
manual: bool,
|
1179
1131
|
dry_run: bool,
|
1180
1132
|
output_file: str,
|
1181
|
-
analyze: bool
|
1133
|
+
analyze: bool,
|
1182
1134
|
):
|
1183
1135
|
"""
|
1184
1136
|
Enhanced RDS Snapshot Cost Optimizer
|
@@ -1213,7 +1165,7 @@ def optimize_rds_snapshots(
|
|
1213
1165
|
print_info(f"🌐 Organization-wide discovery using profile: {profile}")
|
1214
1166
|
else:
|
1215
1167
|
# Default to MANAGEMENT_PROFILE environment variable or current profile
|
1216
|
-
auth_profile = os.getenv(
|
1168
|
+
auth_profile = os.getenv("MANAGEMENT_PROFILE")
|
1217
1169
|
if auth_profile:
|
1218
1170
|
print_info(f"🌐 Organization-wide discovery using MANAGEMENT_PROFILE: {auth_profile}")
|
1219
1171
|
else:
|
@@ -1233,14 +1185,14 @@ def optimize_rds_snapshots(
|
|
1233
1185
|
elif target_account:
|
1234
1186
|
print_warning("🚨 [DEPRECATED] --target-account is deprecated. Use --profile instead")
|
1235
1187
|
target_account_id = target_account
|
1236
|
-
auth_profile = os.getenv(
|
1188
|
+
auth_profile = os.getenv("MANAGEMENT_PROFILE") or profile
|
1237
1189
|
print_info(f"🎯 Target account analysis (deprecated): {target_account_id}")
|
1238
1190
|
elif profile:
|
1239
1191
|
# Check if profile looks like account ID vs profile name
|
1240
1192
|
if profile.isdigit() and len(profile) == 12:
|
1241
1193
|
target_account_id = profile
|
1242
1194
|
# Use management profile for authentication when targeting specific account
|
1243
|
-
auth_profile = os.getenv(
|
1195
|
+
auth_profile = os.getenv("MANAGEMENT_PROFILE") or "${MANAGEMENT_PROFILE}"
|
1244
1196
|
print_info(f"🎯 Target account analysis: {target_account_id}")
|
1245
1197
|
print_info(f"🔐 Authentication via: {auth_profile}")
|
1246
1198
|
else:
|
@@ -1271,8 +1223,7 @@ def optimize_rds_snapshots(
|
|
1271
1223
|
|
1272
1224
|
# Discover snapshots via Config aggregator
|
1273
1225
|
snapshots = optimizer.discover_snapshots_via_config_aggregator(
|
1274
|
-
target_account_id=target_account_id,
|
1275
|
-
manual_only=manual
|
1226
|
+
target_account_id=target_account_id, manual_only=manual
|
1276
1227
|
)
|
1277
1228
|
|
1278
1229
|
if not snapshots:
|
@@ -1288,25 +1239,23 @@ def optimize_rds_snapshots(
|
|
1288
1239
|
|
1289
1240
|
# Summary panel
|
1290
1241
|
panel_content = f"""
|
1291
|
-
📊 Discovery Results: {optimization_results[
|
1292
|
-
💾 Manual Snapshots: {optimization_results[
|
1293
|
-
🎯 Cleanup Candidates: {optimization_results[
|
1294
|
-
💰 Potential Savings: {format_cost(optimization_results[
|
1242
|
+
📊 Discovery Results: {optimization_results["total_snapshots"]} total snapshots
|
1243
|
+
💾 Manual Snapshots: {optimization_results["manual_snapshots"]} (review candidates)
|
1244
|
+
🎯 Cleanup Candidates: {optimization_results["cleanup_candidates"]} (>{final_age_threshold} days)
|
1245
|
+
💰 Potential Savings: {format_cost(optimization_results["potential_annual_savings"])} annually
|
1295
1246
|
"""
|
1296
1247
|
|
1297
|
-
console.print(
|
1298
|
-
panel_content.strip(),
|
1299
|
-
|
1300
|
-
border_style="green"
|
1301
|
-
))
|
1248
|
+
console.print(
|
1249
|
+
create_panel(panel_content.strip(), title="RDS Snapshot Optimization Summary", border_style="green")
|
1250
|
+
)
|
1302
1251
|
|
1303
1252
|
# Display comprehensive snapshot table
|
1304
1253
|
optimizer.display_comprehensive_snapshot_table(snapshots, manual_only=manual)
|
1305
1254
|
|
1306
1255
|
# Target account specific results
|
1307
|
-
if target_account and optimization_results[
|
1308
|
-
target_data = optimization_results[
|
1309
|
-
target_annual = target_data[
|
1256
|
+
if target_account and optimization_results["target_account_data"]:
|
1257
|
+
target_data = optimization_results["target_account_data"]
|
1258
|
+
target_annual = target_data["monthly_cost"] * 12
|
1310
1259
|
|
1311
1260
|
print_success(
|
1312
1261
|
f"🎯 Target Account {target_account} Results: "
|
@@ -1318,9 +1267,9 @@ def optimize_rds_snapshots(
|
|
1318
1267
|
if output_file:
|
1319
1268
|
export_results(snapshots, output_file, optimization_results if analyze else None)
|
1320
1269
|
|
1321
|
-
#
|
1270
|
+
# Cost optimization validation
|
1322
1271
|
if analyze:
|
1323
|
-
|
1272
|
+
_validate_cost_targets(optimization_results)
|
1324
1273
|
|
1325
1274
|
except Exception as e:
|
1326
1275
|
print_error(f"RDS snapshot optimization failed: {e}")
|
@@ -1332,12 +1281,21 @@ def export_results(snapshots: List[Dict], output_file: str, optimization_results
|
|
1332
1281
|
try:
|
1333
1282
|
import csv
|
1334
1283
|
|
1335
|
-
with open(output_file,
|
1284
|
+
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
|
1336
1285
|
# Define CSV fieldnames
|
1337
1286
|
fieldnames = [
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1287
|
+
"DBSnapshotIdentifier",
|
1288
|
+
"AccountId",
|
1289
|
+
"Region",
|
1290
|
+
"SnapshotType",
|
1291
|
+
"AgeDays",
|
1292
|
+
"AllocatedStorage",
|
1293
|
+
"EstimatedMonthlyCost",
|
1294
|
+
"EstimatedAnnualCost",
|
1295
|
+
"Engine",
|
1296
|
+
"Status",
|
1297
|
+
"Encrypted",
|
1298
|
+
"DiscoveryMethod",
|
1341
1299
|
]
|
1342
1300
|
|
1343
1301
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
@@ -1345,45 +1303,43 @@ def export_results(snapshots: List[Dict], output_file: str, optimization_results
|
|
1345
1303
|
|
1346
1304
|
for snapshot in snapshots:
|
1347
1305
|
# Create row with only the fieldnames we want
|
1348
|
-
row = {field: snapshot.get(field,
|
1306
|
+
row = {field: snapshot.get(field, "") for field in fieldnames}
|
1349
1307
|
writer.writerow(row)
|
1350
1308
|
|
1351
1309
|
print_success(f"✅ Exported {len(snapshots)} snapshots to {output_file}")
|
1352
1310
|
|
1353
1311
|
if optimization_results:
|
1354
|
-
print_info(
|
1312
|
+
print_info(
|
1313
|
+
f"📊 Optimization potential: {format_cost(optimization_results['potential_annual_savings'])} annually"
|
1314
|
+
)
|
1355
1315
|
|
1356
1316
|
except Exception as e:
|
1357
1317
|
print_error(f"Failed to export results: {e}")
|
1358
1318
|
|
1359
1319
|
|
1360
|
-
def
|
1361
|
-
"""Validate
|
1320
|
+
def _validate_cost_targets(optimization_results: Dict) -> None:
|
1321
|
+
"""Validate cost optimization targets for measurable annual savings"""
|
1362
1322
|
target_min = 5000.0
|
1363
1323
|
target_max = 24000.0
|
1364
|
-
actual_savings = optimization_results[
|
1324
|
+
actual_savings = optimization_results["potential_annual_savings"]
|
1365
1325
|
|
1366
1326
|
if actual_savings >= target_min:
|
1367
1327
|
if actual_savings <= target_max:
|
1368
1328
|
print_success(
|
1369
|
-
f"🎯
|
1329
|
+
f"🎯 Cost Target Achievement: "
|
1370
1330
|
f"${actual_savings:,.0f} within target range "
|
1371
1331
|
f"(${target_min:,.0f}-${target_max:,.0f})"
|
1372
1332
|
)
|
1373
1333
|
else:
|
1374
1334
|
print_success(
|
1375
|
-
f"🎯
|
1376
|
-
f"${actual_savings:,.0f} exceeds maximum target "
|
1377
|
-
f"(${target_max:,.0f})"
|
1335
|
+
f"🎯 Cost Target Exceeded: ${actual_savings:,.0f} exceeds maximum target (${target_max:,.0f})"
|
1378
1336
|
)
|
1379
1337
|
else:
|
1380
1338
|
percentage = (actual_savings / target_min) * 100
|
1381
1339
|
print_warning(
|
1382
|
-
f"📊
|
1383
|
-
f"${actual_savings:,.0f} is {percentage:.1f}% of minimum target "
|
1384
|
-
f"(${target_min:,.0f})"
|
1340
|
+
f"📊 Cost Analysis: ${actual_savings:,.0f} is {percentage:.1f}% of minimum target (${target_min:,.0f})"
|
1385
1341
|
)
|
1386
1342
|
|
1387
1343
|
|
1388
1344
|
if __name__ == "__main__":
|
1389
|
-
optimize_rds_snapshots()
|
1345
|
+
optimize_rds_snapshots()
|