runbooks 0.7.9__py3-none-any.whl → 0.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +1 -1
- runbooks/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/compliance.py +4 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- runbooks/cloudops/__init__.py +123 -0
- runbooks/cloudops/base.py +385 -0
- runbooks/cloudops/cost_optimizer.py +811 -0
- runbooks/cloudops/infrastructure_optimizer.py +29 -0
- runbooks/cloudops/interfaces.py +828 -0
- runbooks/cloudops/lifecycle_manager.py +29 -0
- runbooks/cloudops/mcp_cost_validation.py +678 -0
- runbooks/cloudops/models.py +251 -0
- runbooks/cloudops/monitoring_automation.py +29 -0
- runbooks/cloudops/notebook_framework.py +676 -0
- runbooks/cloudops/security_enforcer.py +449 -0
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/mcp_cost_explorer_integration.py +900 -0
- runbooks/common/mcp_integration.py +548 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +172 -1
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +377 -458
- runbooks/finops/__init__.py +4 -21
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +59 -5
- runbooks/finops/cost_optimizer.py +1340 -0
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +990 -232
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +8 -7
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +184 -1829
- runbooks/finops/helpers.py +509 -196
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +15 -15
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/schemas.py +589 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -0
- runbooks/finops/tests/test_reference_images_validation.py +1 -1
- runbooks/inventory/README.md +12 -1
- runbooks/inventory/core/collector.py +157 -29
- runbooks/inventory/list_ec2_instances.py +9 -6
- runbooks/inventory/list_ssm_parameters.py +10 -10
- runbooks/inventory/organizations_discovery.py +210 -164
- runbooks/inventory/rich_inventory_display.py +74 -107
- runbooks/inventory/run_on_multi_accounts.py +13 -13
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/main.py +1371 -240
- runbooks/metrics/dora_metrics_engine.py +711 -17
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/base.py +215 -47
- runbooks/operate/ec2_operations.py +435 -5
- runbooks/operate/iam_operations.py +598 -3
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +508 -0
- runbooks/operate/s3_operations.py +508 -0
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/base.py +5 -3
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +265 -33
- runbooks/security/cloudops_automation_security_validator.py +1164 -0
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +930 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/executive_security_dashboard.py +1247 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/multi_account_security_controls.py +2254 -0
- runbooks/security/real_time_security_monitor.py +1196 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +39 -52
- runbooks/security/security_export.py +99 -120
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/production_monitoring_framework.py +584 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +2 -2
- runbooks/validation/benchmark.py +154 -149
- runbooks/validation/cli.py +159 -147
- runbooks/validation/mcp_validator.py +291 -248
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +68 -36
- runbooks/vpc/rich_formatters.py +22 -8
- runbooks-0.9.1.dist-info/METADATA +308 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/RECORD +120 -59
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- runbooks-0.7.9.dist-info/METADATA +0 -636
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,508 @@
|
|
1
|
+
"""
|
2
|
+
AWS RDS Operations for cost optimization and security compliance.
|
3
|
+
|
4
|
+
This module provides comprehensive RDS database management capabilities including
|
5
|
+
cost optimization through underutilized instance cleanup, security compliance
|
6
|
+
through access control automation, and reserved instance management.
|
7
|
+
|
8
|
+
Extracted from unSkript notebooks and enhanced with enterprise patterns.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import asyncio
|
12
|
+
from datetime import datetime, timedelta, timezone
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
14
|
+
|
15
|
+
import boto3
|
16
|
+
from botocore.exceptions import ClientError
|
17
|
+
from rich.console import Console
|
18
|
+
from rich.panel import Panel
|
19
|
+
from rich.progress import track
|
20
|
+
from rich.table import Table
|
21
|
+
|
22
|
+
from runbooks.common.aws_helpers import get_paginated_results
|
23
|
+
from runbooks.common.profile_utils import get_profile_for_operation
|
24
|
+
from runbooks.common.rich_utils import (
|
25
|
+
console,
|
26
|
+
create_table,
|
27
|
+
format_cost,
|
28
|
+
print_error,
|
29
|
+
print_header,
|
30
|
+
print_info,
|
31
|
+
print_success,
|
32
|
+
print_warning,
|
33
|
+
)
|
34
|
+
from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
|
35
|
+
|
36
|
+
|
37
|
+
class RDSOperations(BaseOperation):
|
38
|
+
"""AWS RDS operations for cost optimization and security compliance."""
|
39
|
+
|
40
|
+
def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = True):
|
41
|
+
"""Initialize RDS operations."""
|
42
|
+
super().__init__(profile=profile, region=region, dry_run=dry_run)
|
43
|
+
self.service = 'rds'
|
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'
|
53
|
+
}
|
54
|
+
|
55
|
+
def find_low_cpu_instances(
|
56
|
+
self,
|
57
|
+
context: OperationContext,
|
58
|
+
utilization_threshold: float = 10.0,
|
59
|
+
duration_minutes: int = 60
|
60
|
+
) -> List[OperationResult]:
|
61
|
+
"""
|
62
|
+
Find RDS instances with low CPU utilization.
|
63
|
+
|
64
|
+
Extracted from: AWS_Delete_RDS_Instances_with_Low_CPU_Utilization.ipynb
|
65
|
+
|
66
|
+
Args:
|
67
|
+
context: Operation context
|
68
|
+
utilization_threshold: CPU threshold percentage (default 10%)
|
69
|
+
duration_minutes: Duration to analyze metrics (default 60 minutes)
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
List of operation results with low CPU instances
|
73
|
+
"""
|
74
|
+
print_header("RDS Low CPU Analysis", "Cost Optimization")
|
75
|
+
|
76
|
+
results = []
|
77
|
+
regions_to_check = [context.region] if context.region else self._get_all_regions()
|
78
|
+
|
79
|
+
for region in track(regions_to_check, description="Analyzing regions..."):
|
80
|
+
try:
|
81
|
+
# Get RDS client for region
|
82
|
+
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
|
+
|
86
|
+
# Get all RDS instances in region
|
87
|
+
instances = get_paginated_results(
|
88
|
+
rds_client,
|
89
|
+
'describe_db_instances',
|
90
|
+
'DBInstances'
|
91
|
+
)
|
92
|
+
|
93
|
+
region_low_cpu_instances = []
|
94
|
+
|
95
|
+
for db in instances:
|
96
|
+
try:
|
97
|
+
db_identifier = db['DBInstanceIdentifier']
|
98
|
+
|
99
|
+
# Get CPU metrics from CloudWatch
|
100
|
+
end_time = datetime.utcnow()
|
101
|
+
start_time = end_time - timedelta(minutes=duration_minutes)
|
102
|
+
|
103
|
+
response = cloudwatch_client.get_metric_data(
|
104
|
+
MetricDataQueries=[
|
105
|
+
{
|
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
|
+
]
|
117
|
+
},
|
118
|
+
'Period': 3600, # 1 hour periods
|
119
|
+
'Stat': 'Average'
|
120
|
+
},
|
121
|
+
'ReturnData': True
|
122
|
+
}
|
123
|
+
],
|
124
|
+
StartTime=start_time,
|
125
|
+
EndTime=end_time
|
126
|
+
)
|
127
|
+
|
128
|
+
# Process CPU utilization data
|
129
|
+
if (response['MetricDataResults'] and
|
130
|
+
response['MetricDataResults'][0]['Values']):
|
131
|
+
|
132
|
+
cpu_values = response['MetricDataResults'][0]['Values']
|
133
|
+
avg_cpu = sum(cpu_values) / len(cpu_values)
|
134
|
+
|
135
|
+
if avg_cpu < utilization_threshold:
|
136
|
+
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')
|
144
|
+
}
|
145
|
+
region_low_cpu_instances.append(instance_info)
|
146
|
+
|
147
|
+
except ClientError as e:
|
148
|
+
print_warning(f"Could not get metrics for {db_identifier}: {e}")
|
149
|
+
continue
|
150
|
+
|
151
|
+
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
|
+
|
163
|
+
print_info(f"Found {len(region_low_cpu_instances)} low CPU instances in {region}")
|
164
|
+
|
165
|
+
except ClientError as e:
|
166
|
+
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
|
+
|
173
|
+
if results:
|
174
|
+
total_instances = sum(
|
175
|
+
len(r.data) for r in results
|
176
|
+
if r.status == OperationStatus.SUCCESS and r.data
|
177
|
+
)
|
178
|
+
print_success(f"Analysis complete. Found {total_instances} instances below {utilization_threshold}% CPU")
|
179
|
+
else:
|
180
|
+
print_info("No instances found below threshold")
|
181
|
+
|
182
|
+
return results
|
183
|
+
|
184
|
+
def get_publicly_accessible_instances(self, context: OperationContext) -> List[OperationResult]:
|
185
|
+
"""
|
186
|
+
Get all publicly accessible RDS instances.
|
187
|
+
|
188
|
+
Extracted from: AWS_Secure_Publicly_Accessible_RDS_Instances.ipynb
|
189
|
+
|
190
|
+
Args:
|
191
|
+
context: Operation context
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
List of operation results with publicly accessible instances
|
195
|
+
"""
|
196
|
+
print_header("RDS Public Access Analysis", "Security Compliance")
|
197
|
+
|
198
|
+
results = []
|
199
|
+
regions_to_check = [context.region] if context.region else self._get_all_regions()
|
200
|
+
|
201
|
+
for region in track(regions_to_check, description="Scanning regions..."):
|
202
|
+
try:
|
203
|
+
session = self._get_aws_session(context.profile)
|
204
|
+
rds_client = session.client('rds', region_name=region)
|
205
|
+
|
206
|
+
# Get all RDS instances
|
207
|
+
instances = get_paginated_results(
|
208
|
+
rds_client,
|
209
|
+
'describe_db_instances',
|
210
|
+
'DBInstances'
|
211
|
+
)
|
212
|
+
|
213
|
+
public_instances = []
|
214
|
+
|
215
|
+
for db in instances:
|
216
|
+
if db.get('PubliclyAccessible', False):
|
217
|
+
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
|
226
|
+
}
|
227
|
+
public_instances.append(instance_info)
|
228
|
+
|
229
|
+
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
|
+
))
|
238
|
+
print_warning(f"Found {len(public_instances)} publicly accessible instances in {region}")
|
239
|
+
|
240
|
+
except ClientError as e:
|
241
|
+
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
|
+
|
253
|
+
if total_public > 0:
|
254
|
+
print_warning(f"Security Alert: {total_public} publicly accessible RDS instances found")
|
255
|
+
else:
|
256
|
+
print_success("No publicly accessible RDS instances found")
|
257
|
+
|
258
|
+
return results
|
259
|
+
|
260
|
+
def secure_public_instances(
|
261
|
+
self,
|
262
|
+
context: OperationContext,
|
263
|
+
instance_identifiers: List[str]
|
264
|
+
) -> List[OperationResult]:
|
265
|
+
"""
|
266
|
+
Make RDS instances not publicly accessible.
|
267
|
+
|
268
|
+
Extracted from: AWS_Secure_Publicly_Accessible_RDS_Instances.ipynb
|
269
|
+
|
270
|
+
Args:
|
271
|
+
context: Operation context
|
272
|
+
instance_identifiers: List of instance identifiers to secure
|
273
|
+
|
274
|
+
Returns:
|
275
|
+
List of operation results
|
276
|
+
"""
|
277
|
+
print_header("RDS Security Remediation", "Making Instances Private")
|
278
|
+
|
279
|
+
if self.dry_run:
|
280
|
+
print_info("DRY RUN: Would secure the following instances:")
|
281
|
+
for instance_id in instance_identifiers:
|
282
|
+
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
|
+
|
289
|
+
results = []
|
290
|
+
|
291
|
+
for instance_identifier in track(instance_identifiers, description="Securing instances..."):
|
292
|
+
try:
|
293
|
+
# Determine region for instance (simplified - assumes current region)
|
294
|
+
session = self._get_aws_session(context.profile)
|
295
|
+
rds_client = session.client('rds', region_name=context.region)
|
296
|
+
|
297
|
+
# Modify instance to make it not publicly accessible
|
298
|
+
response = rds_client.modify_db_instance(
|
299
|
+
DBInstanceIdentifier=instance_identifier,
|
300
|
+
PubliclyAccessible=False
|
301
|
+
)
|
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
|
+
|
309
|
+
print_success(f"Initiated security modification for {instance_identifier}")
|
310
|
+
|
311
|
+
except ClientError as e:
|
312
|
+
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
|
+
|
319
|
+
return results
|
320
|
+
|
321
|
+
def analyze_reserved_instance_opportunities(
|
322
|
+
self,
|
323
|
+
context: OperationContext,
|
324
|
+
threshold_days: int = 30
|
325
|
+
) -> List[OperationResult]:
|
326
|
+
"""
|
327
|
+
Find long-running instances without reserved instances.
|
328
|
+
|
329
|
+
Extracted from: AWS_Purchase_Reserved_Instances_For_Long_Running_RDS_Instances.ipynb
|
330
|
+
|
331
|
+
Args:
|
332
|
+
context: Operation context
|
333
|
+
threshold_days: Minimum days running to consider for RI (default 30)
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
List of operation results with RI opportunities
|
337
|
+
"""
|
338
|
+
print_header("RDS Reserved Instance Analysis", "Cost Optimization")
|
339
|
+
|
340
|
+
results = []
|
341
|
+
regions_to_check = [context.region] if context.region else self._get_all_regions()
|
342
|
+
|
343
|
+
for region in track(regions_to_check, description="Analyzing RI opportunities..."):
|
344
|
+
try:
|
345
|
+
session = self._get_aws_session(context.profile)
|
346
|
+
rds_client = session.client('rds', region_name=region)
|
347
|
+
|
348
|
+
# Get reserved instances per region
|
349
|
+
reserved_response = rds_client.describe_reserved_db_instances()
|
350
|
+
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
|
+
|
356
|
+
# Get running instances
|
357
|
+
instances = get_paginated_results(
|
358
|
+
rds_client,
|
359
|
+
'describe_db_instances',
|
360
|
+
'DBInstances'
|
361
|
+
)
|
362
|
+
|
363
|
+
ri_opportunities = []
|
364
|
+
|
365
|
+
for db in instances:
|
366
|
+
if db.get('DBInstanceStatus') == 'available':
|
367
|
+
# Check if instance has been running long enough
|
368
|
+
create_time = db.get('InstanceCreateTime')
|
369
|
+
if create_time:
|
370
|
+
uptime = datetime.now(timezone.utc) - create_time
|
371
|
+
if uptime > timedelta(days=threshold_days):
|
372
|
+
|
373
|
+
instance_class = db.get('DBInstanceClass')
|
374
|
+
reserved_count = reserved_by_class.get(instance_class, 0)
|
375
|
+
|
376
|
+
# Count running instances of this class
|
377
|
+
running_count = sum(
|
378
|
+
1 for inst in instances
|
379
|
+
if (inst.get('DBInstanceClass') == instance_class and
|
380
|
+
inst.get('DBInstanceStatus') == 'available')
|
381
|
+
)
|
382
|
+
|
383
|
+
if running_count > reserved_count:
|
384
|
+
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
|
393
|
+
}
|
394
|
+
ri_opportunities.append(ri_opportunity)
|
395
|
+
|
396
|
+
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
|
+
|
407
|
+
print_info(f"Found {len(ri_opportunities)} RI opportunities in {region}")
|
408
|
+
|
409
|
+
except ClientError as e:
|
410
|
+
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
|
+
|
417
|
+
return results
|
418
|
+
|
419
|
+
def delete_instance_safely(
|
420
|
+
self,
|
421
|
+
context: OperationContext,
|
422
|
+
instance_identifier: str,
|
423
|
+
skip_final_snapshot: bool = False
|
424
|
+
) -> OperationResult:
|
425
|
+
"""
|
426
|
+
Safely delete an RDS instance with proper safeguards.
|
427
|
+
|
428
|
+
Extracted from: AWS_Delete_RDS_Instances_with_Low_CPU_Utilization.ipynb
|
429
|
+
|
430
|
+
Args:
|
431
|
+
context: Operation context
|
432
|
+
instance_identifier: RDS instance identifier
|
433
|
+
skip_final_snapshot: Whether to skip final snapshot (default False)
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
Operation result
|
437
|
+
"""
|
438
|
+
print_header(f"RDS Instance Deletion", f"Instance: {instance_identifier}")
|
439
|
+
|
440
|
+
if self.dry_run:
|
441
|
+
print_warning(f"DRY RUN: Would delete RDS instance {instance_identifier}")
|
442
|
+
return OperationResult(
|
443
|
+
status=OperationStatus.SUCCESS,
|
444
|
+
data={'dry_run': True, 'instance_id': instance_identifier},
|
445
|
+
metadata={'operation': 'delete_instance_dry_run'}
|
446
|
+
)
|
447
|
+
|
448
|
+
# Safety confirmation for destructive operation
|
449
|
+
print_warning(f"DESTRUCTIVE OPERATION: Preparing to delete RDS instance {instance_identifier}")
|
450
|
+
|
451
|
+
try:
|
452
|
+
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
|
+
|
460
|
+
if not skip_final_snapshot:
|
461
|
+
# Create snapshot with timestamp
|
462
|
+
timestamp = datetime.utcnow().strftime('%Y-%m-%d-%H-%M')
|
463
|
+
delete_params['FinalDBSnapshotIdentifier'] = f"{instance_identifier}-final-{timestamp}"
|
464
|
+
print_info(f"Creating final snapshot: {delete_params['FinalDBSnapshotIdentifier']}")
|
465
|
+
|
466
|
+
response = rds_client.delete_db_instance(**delete_params)
|
467
|
+
|
468
|
+
print_success(f"RDS instance {instance_identifier} deletion initiated")
|
469
|
+
|
470
|
+
return OperationResult(
|
471
|
+
status=OperationStatus.SUCCESS,
|
472
|
+
data={'instance_id': instance_identifier, 'response': response},
|
473
|
+
metadata={'operation': 'delete_instance', 'final_snapshot': not skip_final_snapshot}
|
474
|
+
)
|
475
|
+
|
476
|
+
except ClientError as e:
|
477
|
+
print_error(f"Failed to delete instance {instance_identifier}: {e}")
|
478
|
+
return OperationResult(
|
479
|
+
status=OperationStatus.FAILED,
|
480
|
+
error=str(e),
|
481
|
+
metadata={'instance_id': instance_identifier}
|
482
|
+
)
|
483
|
+
|
484
|
+
def execute_operation(self, operation: str, context: OperationContext, **kwargs) -> List[OperationResult]:
|
485
|
+
"""Execute the specified RDS operation."""
|
486
|
+
if operation not in self.supported_operations:
|
487
|
+
raise ValueError(f"Unsupported operation: {operation}")
|
488
|
+
|
489
|
+
operation_method = getattr(self, operation)
|
490
|
+
return operation_method(context, **kwargs)
|
491
|
+
|
492
|
+
def _get_all_regions(self) -> List[str]:
|
493
|
+
"""Get all available AWS regions for RDS."""
|
494
|
+
try:
|
495
|
+
session = boto3.Session()
|
496
|
+
return session.get_available_regions('rds')
|
497
|
+
except Exception:
|
498
|
+
# Fallback to common regions if API call fails
|
499
|
+
return [
|
500
|
+
'us-east-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1',
|
501
|
+
'us-west-1', 'eu-central-1', 'ap-southeast-2'
|
502
|
+
]
|
503
|
+
|
504
|
+
def _get_aws_session(self, profile: Optional[str] = None) -> boto3.Session:
|
505
|
+
"""Get AWS session with proper profile handling."""
|
506
|
+
if profile:
|
507
|
+
return boto3.Session(profile_name=profile)
|
508
|
+
return boto3.Session()
|