runbooks 1.1.4__py3-none-any.whl → 1.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +135 -91
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +17 -12
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +99 -79
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +315 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/aws_decorators.py +2 -3
  113. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  114. runbooks/inventory/check_controltower_readiness.py +152 -151
  115. runbooks/inventory/check_landingzone_readiness.py +85 -84
  116. runbooks/inventory/cloud_foundations_integration.py +144 -149
  117. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  118. runbooks/inventory/collectors/aws_networking.py +109 -99
  119. runbooks/inventory/collectors/base.py +4 -0
  120. runbooks/inventory/core/collector.py +495 -313
  121. runbooks/inventory/core/formatter.py +11 -0
  122. runbooks/inventory/draw_org_structure.py +8 -9
  123. runbooks/inventory/drift_detection_cli.py +69 -96
  124. runbooks/inventory/ec2_vpc_utils.py +2 -2
  125. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  126. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  127. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  128. runbooks/inventory/find_ec2_security_groups.py +48 -42
  129. runbooks/inventory/find_landingzone_versions.py +4 -6
  130. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  131. runbooks/inventory/inventory_mcp_cli.py +48 -46
  132. runbooks/inventory/inventory_modules.py +103 -91
  133. runbooks/inventory/list_cfn_stacks.py +9 -10
  134. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  135. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  136. runbooks/inventory/list_cfn_stacksets.py +8 -10
  137. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  138. runbooks/inventory/list_ds_directories.py +65 -53
  139. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  140. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  141. runbooks/inventory/list_ec2_instances.py +23 -28
  142. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  143. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  144. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  145. runbooks/inventory/list_guardduty_detectors.py +2 -4
  146. runbooks/inventory/list_iam_policies.py +2 -4
  147. runbooks/inventory/list_iam_roles.py +5 -7
  148. runbooks/inventory/list_iam_saml_providers.py +4 -6
  149. runbooks/inventory/list_lambda_functions.py +38 -38
  150. runbooks/inventory/list_org_accounts.py +6 -8
  151. runbooks/inventory/list_org_accounts_users.py +55 -44
  152. runbooks/inventory/list_rds_db_instances.py +31 -33
  153. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  154. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  155. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  156. runbooks/inventory/list_sns_topics.py +2 -4
  157. runbooks/inventory/list_ssm_parameters.py +4 -7
  158. runbooks/inventory/list_vpc_subnets.py +2 -4
  159. runbooks/inventory/list_vpcs.py +7 -10
  160. runbooks/inventory/mcp_inventory_validator.py +554 -468
  161. runbooks/inventory/mcp_vpc_validator.py +359 -442
  162. runbooks/inventory/organizations_discovery.py +63 -55
  163. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  164. runbooks/inventory/requirements.txt +0 -1
  165. runbooks/inventory/rich_inventory_display.py +35 -34
  166. runbooks/inventory/run_on_multi_accounts.py +3 -5
  167. runbooks/inventory/unified_validation_engine.py +281 -253
  168. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  169. runbooks/inventory/vpc_analyzer.py +735 -697
  170. runbooks/inventory/vpc_architecture_validator.py +293 -348
  171. runbooks/inventory/vpc_dependency_analyzer.py +384 -380
  172. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  173. runbooks/main.py +49 -34
  174. runbooks/main_final.py +91 -60
  175. runbooks/main_minimal.py +22 -10
  176. runbooks/main_optimized.py +131 -100
  177. runbooks/main_ultra_minimal.py +7 -2
  178. runbooks/mcp/__init__.py +36 -0
  179. runbooks/mcp/integration.py +679 -0
  180. runbooks/monitoring/performance_monitor.py +9 -4
  181. runbooks/operate/dynamodb_operations.py +3 -1
  182. runbooks/operate/ec2_operations.py +145 -137
  183. runbooks/operate/iam_operations.py +146 -152
  184. runbooks/operate/networking_cost_heatmap.py +29 -8
  185. runbooks/operate/rds_operations.py +223 -254
  186. runbooks/operate/s3_operations.py +107 -118
  187. runbooks/operate/vpc_operations.py +646 -616
  188. runbooks/remediation/base.py +1 -1
  189. runbooks/remediation/commons.py +10 -7
  190. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  191. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  192. runbooks/remediation/multi_account.py +24 -21
  193. runbooks/remediation/rds_snapshot_list.py +86 -60
  194. runbooks/remediation/remediation_cli.py +92 -146
  195. runbooks/remediation/universal_account_discovery.py +83 -79
  196. runbooks/remediation/workspaces_list.py +46 -41
  197. runbooks/security/__init__.py +19 -0
  198. runbooks/security/assessment_runner.py +1150 -0
  199. runbooks/security/baseline_checker.py +812 -0
  200. runbooks/security/cloudops_automation_security_validator.py +509 -535
  201. runbooks/security/compliance_automation_engine.py +17 -17
  202. runbooks/security/config/__init__.py +2 -2
  203. runbooks/security/config/compliance_config.py +50 -50
  204. runbooks/security/config_template_generator.py +63 -76
  205. runbooks/security/enterprise_security_framework.py +1 -1
  206. runbooks/security/executive_security_dashboard.py +519 -508
  207. runbooks/security/multi_account_security_controls.py +959 -1210
  208. runbooks/security/real_time_security_monitor.py +422 -444
  209. runbooks/security/security_baseline_tester.py +1 -1
  210. runbooks/security/security_cli.py +143 -112
  211. runbooks/security/test_2way_validation.py +439 -0
  212. runbooks/security/two_way_validation_framework.py +852 -0
  213. runbooks/sre/production_monitoring_framework.py +167 -177
  214. runbooks/tdd/__init__.py +15 -0
  215. runbooks/tdd/cli.py +1071 -0
  216. runbooks/utils/__init__.py +14 -17
  217. runbooks/utils/logger.py +7 -2
  218. runbooks/utils/version_validator.py +50 -47
  219. runbooks/validation/__init__.py +6 -6
  220. runbooks/validation/cli.py +9 -3
  221. runbooks/validation/comprehensive_2way_validator.py +745 -704
  222. runbooks/validation/mcp_validator.py +906 -228
  223. runbooks/validation/terraform_citations_validator.py +104 -115
  224. runbooks/validation/terraform_drift_detector.py +461 -454
  225. runbooks/vpc/README.md +617 -0
  226. runbooks/vpc/__init__.py +8 -1
  227. runbooks/vpc/analyzer.py +577 -0
  228. runbooks/vpc/cleanup_wrapper.py +476 -413
  229. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  230. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  231. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  232. runbooks/vpc/config.py +92 -97
  233. runbooks/vpc/cost_engine.py +411 -148
  234. runbooks/vpc/cost_explorer_integration.py +553 -0
  235. runbooks/vpc/cross_account_session.py +101 -106
  236. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  237. runbooks/vpc/eni_gate_validator.py +961 -0
  238. runbooks/vpc/heatmap_engine.py +185 -160
  239. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  240. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  241. runbooks/vpc/networking_wrapper.py +15 -8
  242. runbooks/vpc/pdca_remediation_planner.py +528 -0
  243. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  244. runbooks/vpc/runbooks_adapter.py +1167 -241
  245. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  246. runbooks/vpc/test_data_loader.py +358 -0
  247. runbooks/vpc/tests/conftest.py +314 -4
  248. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  249. runbooks/vpc/tests/test_cost_engine.py +0 -2
  250. runbooks/vpc/topology_generator.py +326 -0
  251. runbooks/vpc/unified_scenarios.py +1297 -1124
  252. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  253. runbooks-1.1.6.dist-info/METADATA +327 -0
  254. runbooks-1.1.6.dist-info/RECORD +489 -0
  255. runbooks/finops/README.md +0 -414
  256. runbooks/finops/accuracy_cross_validator.py +0 -647
  257. runbooks/finops/business_cases.py +0 -950
  258. runbooks/finops/dashboard_router.py +0 -922
  259. runbooks/finops/ebs_optimizer.py +0 -973
  260. runbooks/finops/embedded_mcp_validator.py +0 -1629
  261. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  262. runbooks/finops/finops_dashboard.py +0 -584
  263. runbooks/finops/finops_scenarios.py +0 -1218
  264. runbooks/finops/legacy_migration.py +0 -730
  265. runbooks/finops/multi_dashboard.py +0 -1519
  266. runbooks/finops/single_dashboard.py +0 -1113
  267. runbooks/finops/unlimited_scenarios.py +0 -393
  268. runbooks-1.1.4.dist-info/METADATA +0 -800
  269. runbooks-1.1.4.dist-info/RECORD +0 -468
  270. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  271. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  272. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  273. {runbooks-1.1.4.dist-info → runbooks-1.1.6.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 logging
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, Any
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, print_header, print_success, print_error, print_warning, print_info,
33
- create_table, create_progress_bar, format_cost, create_panel
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
- 'total_discovered': 0,
63
- 'manual_snapshots': 0,
64
- 'automated_snapshots': 0,
65
- 'accounts_covered': set(),
66
- 'total_storage_gb': 0,
67
- 'estimated_monthly_cost': 0.0
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('sts')
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('pricing', region_name='us-east-1')
116
+ pricing_client = self.session.client("pricing", region_name="us-east-1")
109
117
 
110
118
  response = pricing_client.get_products(
111
- ServiceCode='AmazonRDS',
119
+ ServiceCode="AmazonRDS",
112
120
  Filters=[
113
- {
114
- 'Type': 'TERM_MATCH',
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('PriceList'):
127
+ if response.get("PriceList"):
128
128
  import json
129
- price_item = json.loads(response['PriceList'][0])
129
+
130
+ price_item = json.loads(response["PriceList"][0])
130
131
 
131
132
  # Extract pricing from AWS pricing structure
132
- terms = price_item.get('terms', {})
133
- on_demand = terms.get('OnDemand', {})
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('priceDimensions', {})
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('pricePerUnit', {})
139
- usd_price = price_per_unit.get('USD', '0')
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 != '0':
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(self, target_account_id: str = None, manual_only: bool = False) -> List[Dict]:
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('SnapshotType') != 'manual':
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("💡 For single-account analysis: This account may not have RDS snapshots or Config aggregator access")
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
- '91893567291', '142964829704', '363435891329', '507583929055',
247
- '614294421455', '695366013198', '761860562159', '802669565615'
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('AccountId', 'unknown')
257
- if account_id != 'unknown':
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 = (total_discovered / expected_total_snapshots) * 100 if expected_total_snapshots > 0 else 0
284
- snapshot_status = "✅ Good" if snapshot_coverage >= 80 else "⚠️ Gap" if snapshot_coverage >= 60 else "❌ Poor"
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 = "✅ Complete" if account_coverage == 100 else f"⚠️ Partial ({len(missing_test_accounts)} missing)"
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(f"✅ Discovery Success: Found {total_discovered} snapshots (≥{expected_total_snapshots} expected)")
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
- 'DBSnapshotIdentifier': config_data.get('resourceId', 'unknown'),
340
- 'AccountId': config_data.get('accountId', 'unknown'),
341
- 'Region': config_data.get('awsRegion', 'unknown'),
342
- 'DiscoveryMethod': 'config_aggregator',
343
- 'ConfigCaptureTime': config_data.get('configurationItemCaptureTime'),
344
- 'ResourceCreationTime': config_data.get('resourceCreationTime')
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('configuration', {})
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
- # Core identifiers with variations
367
- 'DBInstanceIdentifier': safe_extract([
368
- 'dBInstanceIdentifier', 'dbInstanceIdentifier', 'DBInstanceIdentifier'
369
- ], 'unknown'),
370
-
371
- 'SnapshotType': safe_extract([
372
- 'snapshotType', 'SnapshotType', 'type'
373
- ], 'unknown'),
374
-
375
- 'Status': safe_extract([
376
- 'status', 'Status', 'snapshotStatus'
377
- ], 'unknown'),
378
-
379
- 'Engine': safe_extract([
380
- 'engine', 'Engine', 'engineType'
381
- ], 'unknown'),
382
-
383
- 'EngineVersion': safe_extract([
384
- 'engineVersion', 'EngineVersion'
385
- ], 'unknown'),
386
-
387
- # Storage details with type coercion
388
- 'AllocatedStorage': int(safe_extract([
389
- 'allocatedStorage', 'AllocatedStorage', 'storageSize'
390
- ], 0) or 0),
391
-
392
- 'StorageType': safe_extract([
393
- 'storageType', 'StorageType'
394
- ], 'gp2'),
395
-
396
- 'Encrypted': bool(safe_extract([
397
- 'encrypted', 'Encrypted', 'storageEncrypted'
398
- ], False)),
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('SnapshotCreateTime')
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['AgeDays'] = age_days
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('AllocatedStorage', 0)
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['EstimatedMonthlyCost'] = round(monthly_cost, 2)
470
- snapshot_info['EstimatedAnnualCost'] = round(monthly_cost * 12, 2)
450
+ snapshot_info["EstimatedMonthlyCost"] = round(monthly_cost, 2)
451
+ snapshot_info["EstimatedAnnualCost"] = round(monthly_cost * 12, 2)
471
452
  else:
472
- snapshot_info['EstimatedMonthlyCost'] = 0.0
473
- snapshot_info['EstimatedAnnualCost'] = 0.0
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['AgeDays'] = 0
478
- snapshot_info['EstimatedMonthlyCost'] = 0.0
479
- snapshot_info['EstimatedAnnualCost'] = 0.0
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['total_discovered'] += 1
470
+ self.discovery_stats["total_discovered"] += 1
490
471
 
491
- snapshot_type = snapshot.get('SnapshotType', '').lower()
492
- if snapshot_type == 'manual':
493
- self.discovery_stats['manual_snapshots'] += 1
494
- elif snapshot_type == 'automated':
495
- self.discovery_stats['automated_snapshots'] += 1
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('AccountId', 'unknown')
498
- if account_id != 'unknown':
499
- self.discovery_stats['accounts_covered'].add(account_id)
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('AllocatedStorage', 0)
502
- self.discovery_stats['total_storage_gb'] += allocated_storage
482
+ allocated_storage = snapshot.get("AllocatedStorage", 0)
483
+ self.discovery_stats["total_storage_gb"] += allocated_storage
503
484
 
504
- monthly_cost = snapshot.get('EstimatedMonthlyCost', 0.0)
505
- self.discovery_stats['estimated_monthly_cost'] += monthly_cost
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
- "Total Snapshots Discovered",
524
- str(stats['total_discovered']),
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['accounts_covered'])),
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('SnapshotType', '').lower() == 'manual']
571
- automated_snapshots = [s for s in snapshots if s.get('SnapshotType', '').lower() == 'automated']
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
- 'conservative_manual': {
603
- 'snapshots': old_manual_snapshots,
604
- 'description': f'Manual snapshots >{age_threshold} days (safe cleanup)',
605
- 'risk_level': 'Low'
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
- 'automated_review': {
608
- 'snapshots': very_old_automated,
609
- 'description': 'Automated snapshots >365 days (retention review)',
610
- 'risk_level': 'Medium'
562
+ "automated_review": {
563
+ "snapshots": very_old_automated,
564
+ "description": "Automated snapshots >365 days (retention review)",
565
+ "risk_level": "Medium",
611
566
  },
612
- 'comprehensive': {
613
- 'snapshots': all_old_snapshots,
614
- 'description': f'All snapshots >{age_threshold} days (comprehensive review)',
615
- 'risk_level': 'Medium-High'
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['snapshots']
628
- storage_gb = sum(s.get('AllocatedStorage', 0) for s in snapshots_list)
629
- monthly_cost = sum(s.get('EstimatedMonthlyCost', 0) for s in snapshots_list)
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
- 'count': len(snapshots_list),
634
- 'storage_gb': storage_gb,
635
- 'monthly_cost': monthly_cost,
636
- 'annual_savings': annual_savings,
637
- 'description': scenario_data['description'],
638
- 'risk_level': scenario_data['risk_level'],
639
- 'snapshots': snapshots_list
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['comprehensive']
598
+ primary_scenario = optimization_results["comprehensive"]
644
599
  account_breakdown = {}
645
- for snapshot in primary_scenario['snapshots']:
646
- account_id = snapshot.get('AccountId', 'unknown')
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
- 'count': 0,
650
- 'storage_gb': 0,
651
- 'monthly_cost': 0.0,
652
- 'snapshots': []
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('EstimatedMonthlyCost', 0) for s in snapshots) * 12),
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 = ['conservative_manual', 'retention_optimization', 'automated_review', 'comprehensive']
633
+ scenario_priorities = ["conservative_manual", "retention_optimization", "automated_review", "comprehensive"]
684
634
  scenario_icons = {
685
- 'conservative_manual': '🟢',
686
- 'retention_optimization': '🟡',
687
- 'automated_review': '🟠',
688
- 'comprehensive': '🔴'
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['count']),
648
+ str(scenario["count"]),
699
649
  f"{scenario['storage_gb']:,}",
700
- format_cost(scenario['annual_savings']),
701
- scenario['risk_level']
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['comprehensive'] # Most realistic
708
- if recommended_scenario['annual_savings'] > 0:
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['monthly_cost'] * 12
682
+ annual_savings = data["monthly_cost"] * 12
733
683
  account_table.add_row(
734
684
  account_id,
735
- str(data['count']),
685
+ str(data["count"]),
736
686
  f"{data['storage_gb']:,}",
737
- format_cost(data['monthly_cost']),
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['monthly_cost'] * 12
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['snapshots']:
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['snapshots'],
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('AccountId', 'unknown')
734
+ account_id = snapshot.get("AccountId", "unknown")
786
735
 
787
736
  # Snapshot ID
788
- snapshot_id = snapshot.get('DBSnapshotIdentifier', 'unknown')
737
+ snapshot_id = snapshot.get("DBSnapshotIdentifier", "unknown")
789
738
 
790
739
  # DB Instance ID
791
- db_instance_id = snapshot.get('DBInstanceIdentifier', 'unknown')
740
+ db_instance_id = snapshot.get("DBInstanceIdentifier", "unknown")
792
741
 
793
742
  # Size in GiB
794
- size_gib = snapshot.get('AllocatedStorage', 0)
743
+ size_gib = snapshot.get("AllocatedStorage", 0)
795
744
 
796
745
  # Can be Deleted analysis
797
- age_days = snapshot.get('AgeDays', 0)
798
- snapshot_type = snapshot.get('SnapshotType', 'unknown').lower()
746
+ age_days = snapshot.get("AgeDays", 0)
747
+ snapshot_type = snapshot.get("SnapshotType", "unknown").lower()
799
748
 
800
- if snapshot_type == 'manual' and age_days >= age_threshold:
749
+ if snapshot_type == "manual" and age_days >= age_threshold:
801
750
  can_delete = "✅ Yes (Manual)"
802
- elif snapshot_type == 'automated' and age_days >= 365:
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 == 'manual' else "🤖 Automated"
759
+ type_display = "🔧 Manual" if snapshot_type == "manual" else "🤖 Automated"
811
760
 
812
761
  # Creation Time
813
- create_time = snapshot.get('SnapshotCreateTime')
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('%Y-%m-%d')
767
+ create_time_display = create_time.strftime("%Y-%m-%d")
819
768
  else:
820
- create_time_display = 'unknown'
769
+ create_time_display = "unknown"
821
770
 
822
771
  # Tags
823
- tag_list = snapshot.get('TagList', [])
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('Key', '') for tag in tag_list[:2] if isinstance(tag, dict)]
827
- tags_display = ', '.join(tag_names) if tag_names else 'None'
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 = 'None'
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['snapshots'])
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
- 'total_snapshots': len(snapshots),
857
- 'manual_snapshots': len(manual_snapshots),
858
- 'automated_snapshots': len(automated_snapshots),
859
- 'optimization_scenarios': optimization_results,
860
- 'account_breakdown': account_breakdown,
861
- 'target_account_data': account_breakdown.get("142964829704", {}),
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
- 'cleanup_candidates': primary_scenario['count'],
864
- 'potential_monthly_savings': primary_scenario['monthly_cost'],
865
- 'potential_annual_savings': primary_scenario['annual_savings']
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 .embedded_mcp_validator import EmbeddedMCPValidator
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('SnapshotCreateTime', ''))
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('AccountId', 'unknown')
914
- snapshot_id = snapshot.get('DBSnapshotIdentifier', 'unknown')
915
- db_instance_id = snapshot.get('DBInstanceIdentifier', 'unknown')
916
- size_gb = snapshot.get('AllocatedStorage', 0)
917
- snapshot_type = snapshot.get('SnapshotType', 'unknown')
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('AgeDays', 0)
921
- can_delete = "YES" if (snapshot_type == 'manual' and age_days > 90) else "NO"
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 == 'manual' else "[blue]Automated[/blue]"
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('SnapshotCreateTime', '')
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('Z', '+00:00'))
933
- create_time_display = dt.strftime('%Y-%m-%d')
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('TagList', [])
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 'Key' in tag:
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('SnapshotType') == 'manual'])
975
- automated_count = len([s for s in snapshots if s.get('SnapshotType') == 'automated'])
976
- total_size = sum(s.get('AllocatedStorage', 0) for s in snapshots)
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 = ['DBSnapshotIdentifier', 'AccountId', 'AllocatedStorage']
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('AllocatedStorage'), int) and snapshot.get('AllocatedStorage', 0) > 0
993
- account_valid = len(str(snapshot.get('AccountId', ''))) == 12
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('config', region_name='ap-southeast-2')
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
- 'ConfigurationAggregatorName': 'organization-aggregator',
1037
- 'Expression': query_expression,
1038
- 'Limit': 100 # Maximum allowed by Config API
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['NextToken'] = next_token
985
+ query_params["NextToken"] = next_token
1043
986
 
1044
987
  response = config_client.select_aggregate_resource_config(**query_params)
1045
- results = response.get('Results', [])
988
+ results = response.get("Results", [])
1046
989
  all_results.extend(results)
1047
990
 
1048
- next_token = response.get('NextToken')
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
- '91893567291', '142964829704', '363435891329', '507583929055',
1067
- '614294421455', '695366013198', '761860562159', '802669565615'
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 = ['ap-southeast-2', 'us-east-1', 'us-west-2', 'eu-west-1']
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('rds', region_name=region)
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('describe_db_snapshots')
1030
+ paginator = rds_client.get_paginator("describe_db_snapshots")
1082
1031
 
1083
- snapshot_type = 'manual' if manual_only else 'all'
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('DBSnapshots', []):
1036
+ for snapshot in page.get("DBSnapshots", []):
1091
1037
  # Extract account from ARN
1092
- account_id = snapshot.get('DBSnapshotArn', '').split(':')[4] if snapshot.get('DBSnapshotArn') else 'unknown'
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
- 'DBSnapshotIdentifier': rds_snapshot.get('DBSnapshotIdentifier'),
1115
- 'DBInstanceIdentifier': rds_snapshot.get('DBInstanceIdentifier'),
1116
- 'SnapshotCreateTime': rds_snapshot.get('SnapshotCreateTime'),
1117
- 'Engine': rds_snapshot.get('Engine'),
1118
- 'AllocatedStorage': rds_snapshot.get('AllocatedStorage'),
1119
- 'Status': rds_snapshot.get('Status'),
1120
- 'Port': rds_snapshot.get('Port'),
1121
- 'SnapshotType': rds_snapshot.get('SnapshotType'),
1122
- 'Encrypted': rds_snapshot.get('Encrypted'),
1123
- 'KmsKeyId': rds_snapshot.get('KmsKeyId'),
1124
- 'TagList': rds_snapshot.get('TagList', []),
1125
- 'Region': rds_snapshot.get('AvailabilityZone', '').split('-')[0] if rds_snapshot.get('AvailabilityZone') else 'unknown',
1126
- 'DiscoveryMethod': 'direct_rds_api'
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('DBSnapshotArn', '')
1082
+ arn = rds_snapshot.get("DBSnapshotArn", "")
1131
1083
  if arn:
1132
- account_id = arn.split(':')[4]
1133
- processed_snapshot['AccountId'] = account_id
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('SnapshotCreateTime')
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('Z', '+00:00'))
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['AgeDays'] = age_days
1096
+ processed_snapshot["AgeDays"] = age_days
1145
1097
 
1146
1098
  # Calculate costs
1147
- allocated_storage = processed_snapshot.get('AllocatedStorage', 0)
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['EstimatedMonthlyCost'] = round(monthly_cost, 2)
1151
- processed_snapshot['EstimatedAnnualCost'] = round(monthly_cost * 12, 2)
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('--all', '-a', is_flag=True, help='Organization-wide discovery using management profile')
1162
- @click.option('--profile', help='AWS profile for authentication or target account ID for filtering')
1163
- @click.option('--target-account', help='[DEPRECATED] Use --profile instead. Target account ID for filtering')
1164
- @click.option('--age-threshold', type=int, default=90, help='Age threshold for cleanup (days)')
1165
- @click.option('--days', type=int, help='Age threshold in days (alias for --age-threshold)')
1166
- @click.option('--aging', type=int, help='Age threshold in days (alias for --age-threshold)')
1167
- @click.option('--manual', is_flag=True, help='Filter only manual snapshots (exclude automated)')
1168
- @click.option('--dry-run/--execute', default=True, help='Analysis mode vs execution mode')
1169
- @click.option('--output-file', help='Export results to CSV file')
1170
- @click.option('--analyze', is_flag=True, help='Perform comprehensive optimization analysis')
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('MANAGEMENT_PROFILE')
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('MANAGEMENT_PROFILE') or profile
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('MANAGEMENT_PROFILE') or '${MANAGEMENT_PROFILE}'
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['total_snapshots']} total snapshots
1292
- 💾 Manual Snapshots: {optimization_results['manual_snapshots']} (review candidates)
1293
- 🎯 Cleanup Candidates: {optimization_results['cleanup_candidates']} (>{final_age_threshold} days)
1294
- 💰 Potential Savings: {format_cost(optimization_results['potential_annual_savings'])} annually
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(create_panel(
1298
- panel_content.strip(),
1299
- title="RDS Snapshot Optimization Summary",
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['target_account_data']:
1308
- target_data = optimization_results['target_account_data']
1309
- target_annual = target_data['monthly_cost'] * 12
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
- # JIRA FinOps-23 validation
1270
+ # Cost optimization validation
1322
1271
  if analyze:
1323
- validate_jira_targets(optimization_results)
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, 'w', newline='', encoding='utf-8') as csvfile:
1284
+ with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
1336
1285
  # Define CSV fieldnames
1337
1286
  fieldnames = [
1338
- 'DBSnapshotIdentifier', 'AccountId', 'Region', 'SnapshotType',
1339
- 'AgeDays', 'AllocatedStorage', 'EstimatedMonthlyCost', 'EstimatedAnnualCost',
1340
- 'Engine', 'Status', 'Encrypted', 'DiscoveryMethod'
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, '') for field in fieldnames}
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(f"📊 Optimization potential: {format_cost(optimization_results['potential_annual_savings'])} annually")
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 validate_jira_targets(optimization_results: Dict) -> None:
1361
- """Validate against JIRA FinOps-23 targets (measurable range annual savings)"""
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['potential_annual_savings']
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"🎯 JIRA FinOps-23 Target Achievement: "
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"🎯 JIRA FinOps-23 Target Exceeded: "
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"📊 JIRA FinOps-23 Analysis: "
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()