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.
Files changed (247) 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/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -36,456 +36,420 @@ from runbooks.operate.base import BaseOperation, OperationContext, OperationResu
36
36
 
37
37
  class RDSOperations(BaseOperation):
38
38
  """AWS RDS operations for cost optimization and security compliance."""
39
-
39
+
40
40
  def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = True):
41
41
  """Initialize RDS operations."""
42
42
  super().__init__(profile=profile, region=region, dry_run=dry_run)
43
- self.service = 'rds'
43
+ self.service = "rds"
44
44
  self.supported_operations = {
45
- 'find_low_cpu_instances',
46
- 'delete_instance_safely',
47
- 'get_publicly_accessible_instances',
48
- 'secure_public_instances',
49
- 'analyze_reserved_instance_opportunities',
50
- 'get_instance_metrics',
51
- 'list_instances',
52
- 'get_instance_details'
45
+ "find_low_cpu_instances",
46
+ "delete_instance_safely",
47
+ "get_publicly_accessible_instances",
48
+ "secure_public_instances",
49
+ "analyze_reserved_instance_opportunities",
50
+ "get_instance_metrics",
51
+ "list_instances",
52
+ "get_instance_details",
53
53
  }
54
54
 
55
55
  def find_low_cpu_instances(
56
- self,
57
- context: OperationContext,
58
- utilization_threshold: float = 10.0,
59
- duration_minutes: int = 60
56
+ self, context: OperationContext, utilization_threshold: float = 10.0, duration_minutes: int = 60
60
57
  ) -> List[OperationResult]:
61
58
  """
62
59
  Find RDS instances with low CPU utilization.
63
-
60
+
64
61
  Extracted from: AWS_Delete_RDS_Instances_with_Low_CPU_Utilization.ipynb
65
-
62
+
66
63
  Args:
67
64
  context: Operation context
68
65
  utilization_threshold: CPU threshold percentage (default 10%)
69
66
  duration_minutes: Duration to analyze metrics (default 60 minutes)
70
-
67
+
71
68
  Returns:
72
69
  List of operation results with low CPU instances
73
70
  """
74
71
  print_header("RDS Low CPU Analysis", "Cost Optimization")
75
-
72
+
76
73
  results = []
77
74
  regions_to_check = [context.region] if context.region else self._get_all_regions()
78
-
75
+
79
76
  for region in track(regions_to_check, description="Analyzing regions..."):
80
77
  try:
81
78
  # Get RDS client for region
82
79
  session = self._get_aws_session(context.profile)
83
- rds_client = session.client('rds', region_name=region)
84
- cloudwatch_client = session.client('cloudwatch', region_name=region)
85
-
80
+ rds_client = session.client("rds", region_name=region)
81
+ cloudwatch_client = session.client("cloudwatch", region_name=region)
82
+
86
83
  # Get all RDS instances in region
87
- instances = get_paginated_results(
88
- rds_client,
89
- 'describe_db_instances',
90
- 'DBInstances'
91
- )
92
-
84
+ instances = get_paginated_results(rds_client, "describe_db_instances", "DBInstances")
85
+
93
86
  region_low_cpu_instances = []
94
-
87
+
95
88
  for db in instances:
96
89
  try:
97
- db_identifier = db['DBInstanceIdentifier']
98
-
90
+ db_identifier = db["DBInstanceIdentifier"]
91
+
99
92
  # Get CPU metrics from CloudWatch
100
93
  end_time = datetime.utcnow()
101
94
  start_time = end_time - timedelta(minutes=duration_minutes)
102
-
95
+
103
96
  response = cloudwatch_client.get_metric_data(
104
97
  MetricDataQueries=[
105
98
  {
106
- 'Id': 'cpu',
107
- 'MetricStat': {
108
- 'Metric': {
109
- 'Namespace': 'AWS/RDS',
110
- 'MetricName': 'CPUUtilization',
111
- 'Dimensions': [
112
- {
113
- 'Name': 'DBInstanceIdentifier',
114
- 'Value': db_identifier
115
- }
116
- ]
99
+ "Id": "cpu",
100
+ "MetricStat": {
101
+ "Metric": {
102
+ "Namespace": "AWS/RDS",
103
+ "MetricName": "CPUUtilization",
104
+ "Dimensions": [{"Name": "DBInstanceIdentifier", "Value": db_identifier}],
117
105
  },
118
- 'Period': 3600, # 1 hour periods
119
- 'Stat': 'Average'
106
+ "Period": 3600, # 1 hour periods
107
+ "Stat": "Average",
120
108
  },
121
- 'ReturnData': True
109
+ "ReturnData": True,
122
110
  }
123
111
  ],
124
112
  StartTime=start_time,
125
- EndTime=end_time
113
+ EndTime=end_time,
126
114
  )
127
-
115
+
128
116
  # Process CPU utilization data
129
- if (response['MetricDataResults'] and
130
- response['MetricDataResults'][0]['Values']):
131
-
132
- cpu_values = response['MetricDataResults'][0]['Values']
117
+ if response["MetricDataResults"] and response["MetricDataResults"][0]["Values"]:
118
+ cpu_values = response["MetricDataResults"][0]["Values"]
133
119
  avg_cpu = sum(cpu_values) / len(cpu_values)
134
-
120
+
135
121
  if avg_cpu < utilization_threshold:
136
122
  instance_info = {
137
- 'region': region,
138
- 'instance_id': db_identifier,
139
- 'instance_class': db.get('DBInstanceClass'),
140
- 'engine': db.get('Engine'),
141
- 'avg_cpu_utilization': round(avg_cpu, 2),
142
- 'status': db.get('DBInstanceStatus'),
143
- 'creation_time': db.get('InstanceCreateTime')
123
+ "region": region,
124
+ "instance_id": db_identifier,
125
+ "instance_class": db.get("DBInstanceClass"),
126
+ "engine": db.get("Engine"),
127
+ "avg_cpu_utilization": round(avg_cpu, 2),
128
+ "status": db.get("DBInstanceStatus"),
129
+ "creation_time": db.get("InstanceCreateTime"),
144
130
  }
145
131
  region_low_cpu_instances.append(instance_info)
146
-
132
+
147
133
  except ClientError as e:
148
134
  print_warning(f"Could not get metrics for {db_identifier}: {e}")
149
135
  continue
150
-
136
+
151
137
  if region_low_cpu_instances:
152
- results.append(OperationResult(
153
- status=OperationStatus.SUCCESS,
154
- data=region_low_cpu_instances,
155
- metadata={
156
- 'region': region,
157
- 'threshold': utilization_threshold,
158
- 'duration_minutes': duration_minutes,
159
- 'instances_found': len(region_low_cpu_instances)
160
- }
161
- ))
162
-
138
+ results.append(
139
+ OperationResult(
140
+ status=OperationStatus.SUCCESS,
141
+ data=region_low_cpu_instances,
142
+ metadata={
143
+ "region": region,
144
+ "threshold": utilization_threshold,
145
+ "duration_minutes": duration_minutes,
146
+ "instances_found": len(region_low_cpu_instances),
147
+ },
148
+ )
149
+ )
150
+
163
151
  print_info(f"Found {len(region_low_cpu_instances)} low CPU instances in {region}")
164
-
152
+
165
153
  except ClientError as e:
166
154
  print_error(f"Error analyzing region {region}: {e}")
167
- results.append(OperationResult(
168
- status=OperationStatus.FAILED,
169
- error=str(e),
170
- metadata={'region': region}
171
- ))
172
-
155
+ results.append(
156
+ OperationResult(status=OperationStatus.FAILED, error=str(e), metadata={"region": region})
157
+ )
158
+
173
159
  if results:
174
- total_instances = sum(
175
- len(r.data) for r in results
176
- if r.status == OperationStatus.SUCCESS and r.data
177
- )
160
+ total_instances = sum(len(r.data) for r in results if r.status == OperationStatus.SUCCESS and r.data)
178
161
  print_success(f"Analysis complete. Found {total_instances} instances below {utilization_threshold}% CPU")
179
162
  else:
180
163
  print_info("No instances found below threshold")
181
-
164
+
182
165
  return results
183
166
 
184
167
  def get_publicly_accessible_instances(self, context: OperationContext) -> List[OperationResult]:
185
168
  """
186
169
  Get all publicly accessible RDS instances.
187
-
170
+
188
171
  Extracted from: AWS_Secure_Publicly_Accessible_RDS_Instances.ipynb
189
-
172
+
190
173
  Args:
191
174
  context: Operation context
192
-
175
+
193
176
  Returns:
194
177
  List of operation results with publicly accessible instances
195
178
  """
196
179
  print_header("RDS Public Access Analysis", "Security Compliance")
197
-
180
+
198
181
  results = []
199
182
  regions_to_check = [context.region] if context.region else self._get_all_regions()
200
-
183
+
201
184
  for region in track(regions_to_check, description="Scanning regions..."):
202
185
  try:
203
186
  session = self._get_aws_session(context.profile)
204
- rds_client = session.client('rds', region_name=region)
205
-
187
+ rds_client = session.client("rds", region_name=region)
188
+
206
189
  # Get all RDS instances
207
- instances = get_paginated_results(
208
- rds_client,
209
- 'describe_db_instances',
210
- 'DBInstances'
211
- )
212
-
190
+ instances = get_paginated_results(rds_client, "describe_db_instances", "DBInstances")
191
+
213
192
  public_instances = []
214
-
193
+
215
194
  for db in instances:
216
- if db.get('PubliclyAccessible', False):
195
+ if db.get("PubliclyAccessible", False):
217
196
  instance_info = {
218
- 'region': region,
219
- 'instance_id': db['DBInstanceIdentifier'],
220
- 'instance_class': db.get('DBInstanceClass'),
221
- 'engine': db.get('Engine'),
222
- 'status': db.get('DBInstanceStatus'),
223
- 'endpoint': db.get('Endpoint', {}).get('Address'),
224
- 'port': db.get('Endpoint', {}).get('Port'),
225
- 'vpc_id': db.get('DBSubnetGroup', {}).get('VpcId') if db.get('DBSubnetGroup') else None
197
+ "region": region,
198
+ "instance_id": db["DBInstanceIdentifier"],
199
+ "instance_class": db.get("DBInstanceClass"),
200
+ "engine": db.get("Engine"),
201
+ "status": db.get("DBInstanceStatus"),
202
+ "endpoint": db.get("Endpoint", {}).get("Address"),
203
+ "port": db.get("Endpoint", {}).get("Port"),
204
+ "vpc_id": db.get("DBSubnetGroup", {}).get("VpcId") if db.get("DBSubnetGroup") else None,
226
205
  }
227
206
  public_instances.append(instance_info)
228
-
207
+
229
208
  if public_instances:
230
- results.append(OperationResult(
231
- status=OperationStatus.SUCCESS,
232
- data=public_instances,
233
- metadata={
234
- 'region': region,
235
- 'public_instances_found': len(public_instances)
236
- }
237
- ))
209
+ results.append(
210
+ OperationResult(
211
+ status=OperationStatus.SUCCESS,
212
+ data=public_instances,
213
+ metadata={"region": region, "public_instances_found": len(public_instances)},
214
+ )
215
+ )
238
216
  print_warning(f"Found {len(public_instances)} publicly accessible instances in {region}")
239
-
217
+
240
218
  except ClientError as e:
241
219
  print_error(f"Error scanning region {region}: {e}")
242
- results.append(OperationResult(
243
- status=OperationStatus.FAILED,
244
- error=str(e),
245
- metadata={'region': region}
246
- ))
247
-
248
- total_public = sum(
249
- len(r.data) for r in results
250
- if r.status == OperationStatus.SUCCESS and r.data
251
- )
252
-
220
+ results.append(
221
+ OperationResult(status=OperationStatus.FAILED, error=str(e), metadata={"region": region})
222
+ )
223
+
224
+ total_public = sum(len(r.data) for r in results if r.status == OperationStatus.SUCCESS and r.data)
225
+
253
226
  if total_public > 0:
254
227
  print_warning(f"Security Alert: {total_public} publicly accessible RDS instances found")
255
228
  else:
256
229
  print_success("No publicly accessible RDS instances found")
257
-
230
+
258
231
  return results
259
232
 
260
233
  def secure_public_instances(
261
- self,
262
- context: OperationContext,
263
- instance_identifiers: List[str]
234
+ self, context: OperationContext, instance_identifiers: List[str]
264
235
  ) -> List[OperationResult]:
265
236
  """
266
237
  Make RDS instances not publicly accessible.
267
-
238
+
268
239
  Extracted from: AWS_Secure_Publicly_Accessible_RDS_Instances.ipynb
269
-
240
+
270
241
  Args:
271
242
  context: Operation context
272
243
  instance_identifiers: List of instance identifiers to secure
273
-
244
+
274
245
  Returns:
275
246
  List of operation results
276
247
  """
277
248
  print_header("RDS Security Remediation", "Making Instances Private")
278
-
249
+
279
250
  if self.dry_run:
280
251
  print_info("DRY RUN: Would secure the following instances:")
281
252
  for instance_id in instance_identifiers:
282
253
  print_info(f" - {instance_id} (would set PubliclyAccessible=False)")
283
- return [OperationResult(
284
- status=OperationStatus.SUCCESS,
285
- data={'dry_run': True, 'instances': instance_identifiers},
286
- metadata={'operation': 'secure_instances_dry_run'}
287
- )]
288
-
254
+ return [
255
+ OperationResult(
256
+ status=OperationStatus.SUCCESS,
257
+ data={"dry_run": True, "instances": instance_identifiers},
258
+ metadata={"operation": "secure_instances_dry_run"},
259
+ )
260
+ ]
261
+
289
262
  results = []
290
-
263
+
291
264
  for instance_identifier in track(instance_identifiers, description="Securing instances..."):
292
265
  try:
293
266
  # Determine region for instance (simplified - assumes current region)
294
267
  session = self._get_aws_session(context.profile)
295
- rds_client = session.client('rds', region_name=context.region)
296
-
268
+ rds_client = session.client("rds", region_name=context.region)
269
+
297
270
  # Modify instance to make it not publicly accessible
298
271
  response = rds_client.modify_db_instance(
299
- DBInstanceIdentifier=instance_identifier,
300
- PubliclyAccessible=False
272
+ DBInstanceIdentifier=instance_identifier, PubliclyAccessible=False
301
273
  )
302
-
303
- results.append(OperationResult(
304
- status=OperationStatus.SUCCESS,
305
- data={'instance_id': instance_identifier, 'response': response},
306
- metadata={'operation': 'modify_public_access'}
307
- ))
308
-
274
+
275
+ results.append(
276
+ OperationResult(
277
+ status=OperationStatus.SUCCESS,
278
+ data={"instance_id": instance_identifier, "response": response},
279
+ metadata={"operation": "modify_public_access"},
280
+ )
281
+ )
282
+
309
283
  print_success(f"Initiated security modification for {instance_identifier}")
310
-
284
+
311
285
  except ClientError as e:
312
286
  print_error(f"Failed to secure instance {instance_identifier}: {e}")
313
- results.append(OperationResult(
314
- status=OperationStatus.FAILED,
315
- error=str(e),
316
- metadata={'instance_id': instance_identifier}
317
- ))
318
-
287
+ results.append(
288
+ OperationResult(
289
+ status=OperationStatus.FAILED, error=str(e), metadata={"instance_id": instance_identifier}
290
+ )
291
+ )
292
+
319
293
  return results
320
294
 
321
295
  def analyze_reserved_instance_opportunities(
322
- self,
323
- context: OperationContext,
324
- threshold_days: int = 30
296
+ self, context: OperationContext, threshold_days: int = 30
325
297
  ) -> List[OperationResult]:
326
298
  """
327
299
  Find long-running instances without reserved instances.
328
-
300
+
329
301
  Extracted from: AWS_Purchase_Reserved_Instances_For_Long_Running_RDS_Instances.ipynb
330
-
302
+
331
303
  Args:
332
304
  context: Operation context
333
305
  threshold_days: Minimum days running to consider for RI (default 30)
334
-
306
+
335
307
  Returns:
336
308
  List of operation results with RI opportunities
337
309
  """
338
310
  print_header("RDS Reserved Instance Analysis", "Cost Optimization")
339
-
311
+
340
312
  results = []
341
313
  regions_to_check = [context.region] if context.region else self._get_all_regions()
342
-
314
+
343
315
  for region in track(regions_to_check, description="Analyzing RI opportunities..."):
344
316
  try:
345
317
  session = self._get_aws_session(context.profile)
346
- rds_client = session.client('rds', region_name=region)
347
-
318
+ rds_client = session.client("rds", region_name=region)
319
+
348
320
  # Get reserved instances per region
349
321
  reserved_response = rds_client.describe_reserved_db_instances()
350
322
  reserved_by_class = {}
351
-
352
- for reserved in reserved_response.get('ReservedDBInstances', []):
353
- instance_class = reserved['DBInstanceClass']
354
- reserved_by_class[instance_class] = reserved_by_class.get(instance_class, 0) + reserved.get('DBInstanceCount', 1)
355
-
323
+
324
+ for reserved in reserved_response.get("ReservedDBInstances", []):
325
+ instance_class = reserved["DBInstanceClass"]
326
+ reserved_by_class[instance_class] = reserved_by_class.get(instance_class, 0) + reserved.get(
327
+ "DBInstanceCount", 1
328
+ )
329
+
356
330
  # Get running instances
357
- instances = get_paginated_results(
358
- rds_client,
359
- 'describe_db_instances',
360
- 'DBInstances'
361
- )
362
-
331
+ instances = get_paginated_results(rds_client, "describe_db_instances", "DBInstances")
332
+
363
333
  ri_opportunities = []
364
-
334
+
365
335
  for db in instances:
366
- if db.get('DBInstanceStatus') == 'available':
336
+ if db.get("DBInstanceStatus") == "available":
367
337
  # Check if instance has been running long enough
368
- create_time = db.get('InstanceCreateTime')
338
+ create_time = db.get("InstanceCreateTime")
369
339
  if create_time:
370
340
  uptime = datetime.now(timezone.utc) - create_time
371
341
  if uptime > timedelta(days=threshold_days):
372
-
373
- instance_class = db.get('DBInstanceClass')
342
+ instance_class = db.get("DBInstanceClass")
374
343
  reserved_count = reserved_by_class.get(instance_class, 0)
375
-
344
+
376
345
  # Count running instances of this class
377
346
  running_count = sum(
378
- 1 for inst in instances
379
- if (inst.get('DBInstanceClass') == instance_class and
380
- inst.get('DBInstanceStatus') == 'available')
347
+ 1
348
+ for inst in instances
349
+ if (
350
+ inst.get("DBInstanceClass") == instance_class
351
+ and inst.get("DBInstanceStatus") == "available"
352
+ )
381
353
  )
382
-
354
+
383
355
  if running_count > reserved_count:
384
356
  ri_opportunity = {
385
- 'region': region,
386
- 'instance_id': db['DBInstanceIdentifier'],
387
- 'instance_class': instance_class,
388
- 'engine': db.get('Engine'),
389
- 'running_days': uptime.days,
390
- 'reserved_count': reserved_count,
391
- 'running_count': running_count,
392
- 'ri_gap': running_count - reserved_count
357
+ "region": region,
358
+ "instance_id": db["DBInstanceIdentifier"],
359
+ "instance_class": instance_class,
360
+ "engine": db.get("Engine"),
361
+ "running_days": uptime.days,
362
+ "reserved_count": reserved_count,
363
+ "running_count": running_count,
364
+ "ri_gap": running_count - reserved_count,
393
365
  }
394
366
  ri_opportunities.append(ri_opportunity)
395
-
367
+
396
368
  if ri_opportunities:
397
- results.append(OperationResult(
398
- status=OperationStatus.SUCCESS,
399
- data=ri_opportunities,
400
- metadata={
401
- 'region': region,
402
- 'threshold_days': threshold_days,
403
- 'opportunities_found': len(ri_opportunities)
404
- }
405
- ))
406
-
369
+ results.append(
370
+ OperationResult(
371
+ status=OperationStatus.SUCCESS,
372
+ data=ri_opportunities,
373
+ metadata={
374
+ "region": region,
375
+ "threshold_days": threshold_days,
376
+ "opportunities_found": len(ri_opportunities),
377
+ },
378
+ )
379
+ )
380
+
407
381
  print_info(f"Found {len(ri_opportunities)} RI opportunities in {region}")
408
-
382
+
409
383
  except ClientError as e:
410
384
  print_error(f"Error analyzing RI opportunities in {region}: {e}")
411
- results.append(OperationResult(
412
- status=OperationStatus.FAILED,
413
- error=str(e),
414
- metadata={'region': region}
415
- ))
416
-
385
+ results.append(
386
+ OperationResult(status=OperationStatus.FAILED, error=str(e), metadata={"region": region})
387
+ )
388
+
417
389
  return results
418
390
 
419
391
  def delete_instance_safely(
420
- self,
421
- context: OperationContext,
422
- instance_identifier: str,
423
- skip_final_snapshot: bool = False
392
+ self, context: OperationContext, instance_identifier: str, skip_final_snapshot: bool = False
424
393
  ) -> OperationResult:
425
394
  """
426
395
  Safely delete an RDS instance with proper safeguards.
427
-
396
+
428
397
  Extracted from: AWS_Delete_RDS_Instances_with_Low_CPU_Utilization.ipynb
429
-
398
+
430
399
  Args:
431
400
  context: Operation context
432
401
  instance_identifier: RDS instance identifier
433
402
  skip_final_snapshot: Whether to skip final snapshot (default False)
434
-
403
+
435
404
  Returns:
436
405
  Operation result
437
406
  """
438
407
  print_header(f"RDS Instance Deletion", f"Instance: {instance_identifier}")
439
-
408
+
440
409
  if self.dry_run:
441
410
  print_warning(f"DRY RUN: Would delete RDS instance {instance_identifier}")
442
411
  return OperationResult(
443
412
  status=OperationStatus.SUCCESS,
444
- data={'dry_run': True, 'instance_id': instance_identifier},
445
- metadata={'operation': 'delete_instance_dry_run'}
413
+ data={"dry_run": True, "instance_id": instance_identifier},
414
+ metadata={"operation": "delete_instance_dry_run"},
446
415
  )
447
-
416
+
448
417
  # Safety confirmation for destructive operation
449
418
  print_warning(f"DESTRUCTIVE OPERATION: Preparing to delete RDS instance {instance_identifier}")
450
-
419
+
451
420
  try:
452
421
  session = self._get_aws_session(context.profile)
453
- rds_client = session.client('rds', region_name=context.region)
454
-
455
- delete_params = {
456
- 'DBInstanceIdentifier': instance_identifier,
457
- 'SkipFinalSnapshot': skip_final_snapshot
458
- }
459
-
422
+ rds_client = session.client("rds", region_name=context.region)
423
+
424
+ delete_params = {"DBInstanceIdentifier": instance_identifier, "SkipFinalSnapshot": skip_final_snapshot}
425
+
460
426
  if not skip_final_snapshot:
461
427
  # Create snapshot with timestamp
462
- timestamp = datetime.utcnow().strftime('%Y-%m-%d-%H-%M')
463
- delete_params['FinalDBSnapshotIdentifier'] = f"{instance_identifier}-final-{timestamp}"
428
+ timestamp = datetime.utcnow().strftime("%Y-%m-%d-%H-%M")
429
+ delete_params["FinalDBSnapshotIdentifier"] = f"{instance_identifier}-final-{timestamp}"
464
430
  print_info(f"Creating final snapshot: {delete_params['FinalDBSnapshotIdentifier']}")
465
-
431
+
466
432
  response = rds_client.delete_db_instance(**delete_params)
467
-
433
+
468
434
  print_success(f"RDS instance {instance_identifier} deletion initiated")
469
-
435
+
470
436
  return OperationResult(
471
437
  status=OperationStatus.SUCCESS,
472
- data={'instance_id': instance_identifier, 'response': response},
473
- metadata={'operation': 'delete_instance', 'final_snapshot': not skip_final_snapshot}
438
+ data={"instance_id": instance_identifier, "response": response},
439
+ metadata={"operation": "delete_instance", "final_snapshot": not skip_final_snapshot},
474
440
  )
475
-
441
+
476
442
  except ClientError as e:
477
443
  print_error(f"Failed to delete instance {instance_identifier}: {e}")
478
444
  return OperationResult(
479
- status=OperationStatus.FAILED,
480
- error=str(e),
481
- metadata={'instance_id': instance_identifier}
445
+ status=OperationStatus.FAILED, error=str(e), metadata={"instance_id": instance_identifier}
482
446
  )
483
447
 
484
448
  def execute_operation(self, operation: str, context: OperationContext, **kwargs) -> List[OperationResult]:
485
449
  """Execute the specified RDS operation."""
486
450
  if operation not in self.supported_operations:
487
451
  raise ValueError(f"Unsupported operation: {operation}")
488
-
452
+
489
453
  operation_method = getattr(self, operation)
490
454
  return operation_method(context, **kwargs)
491
455
 
@@ -493,16 +457,21 @@ class RDSOperations(BaseOperation):
493
457
  """Get all available AWS regions for RDS."""
494
458
  try:
495
459
  session = boto3.Session()
496
- return session.get_available_regions('rds')
460
+ return session.get_available_regions("rds")
497
461
  except Exception:
498
462
  # Fallback to common regions if API call fails
499
463
  return [
500
- 'us-east-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1',
501
- 'us-west-1', 'eu-central-1', 'ap-southeast-2'
464
+ "us-east-1",
465
+ "us-west-2",
466
+ "eu-west-1",
467
+ "ap-southeast-1",
468
+ "us-west-1",
469
+ "eu-central-1",
470
+ "ap-southeast-2",
502
471
  ]
503
472
 
504
473
  def _get_aws_session(self, profile: Optional[str] = None) -> boto3.Session:
505
474
  """Get AWS session with proper profile handling."""
506
475
  if profile:
507
476
  return boto3.Session(profile_name=profile)
508
- return boto3.Session()
477
+ return boto3.Session()