runbooks 0.7.0__py3-none-any.whl → 0.7.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.
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
"""
|
2
|
+
RDS Instance Cost Analysis - Analyze RDS instances for cost optimization opportunities.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from datetime import datetime, timedelta, timezone
|
7
|
+
from functools import lru_cache
|
8
|
+
|
9
|
+
import click
|
10
|
+
from botocore.exceptions import ClientError
|
11
|
+
|
12
|
+
from .commons import display_aws_account_info, get_client, write_to_csv
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
@lru_cache(maxsize=32)
|
18
|
+
def get_rds_reserved_instances():
|
19
|
+
"""Get RDS reserved instances with caching for performance."""
|
20
|
+
try:
|
21
|
+
rds_client = get_client("rds")
|
22
|
+
response = rds_client.describe_reserved_db_instances()
|
23
|
+
logger.debug(f"Found {len(response.get('ReservedDBInstances', []))} reserved instances")
|
24
|
+
return response
|
25
|
+
except ClientError as e:
|
26
|
+
logger.warning(f"Could not get reserved instances: {e}")
|
27
|
+
return {"ReservedDBInstances": []}
|
28
|
+
|
29
|
+
|
30
|
+
def get_cloudwatch_metrics(cloudwatch, instance_id, metric_name, days=7):
|
31
|
+
"""Get CloudWatch metrics for RDS instance."""
|
32
|
+
try:
|
33
|
+
end_time = datetime.now(tz=timezone.utc)
|
34
|
+
start_time = end_time - timedelta(days=days)
|
35
|
+
|
36
|
+
response = cloudwatch.get_metric_statistics(
|
37
|
+
Namespace="AWS/RDS",
|
38
|
+
MetricName=metric_name,
|
39
|
+
Dimensions=[{"Name": "DBInstanceIdentifier", "Value": instance_id}],
|
40
|
+
StartTime=start_time,
|
41
|
+
EndTime=end_time,
|
42
|
+
Period=86400, # Daily average
|
43
|
+
Statistics=["Average", "Maximum"],
|
44
|
+
)
|
45
|
+
|
46
|
+
datapoints = response.get("Datapoints", [])
|
47
|
+
if datapoints:
|
48
|
+
avg_value = sum(dp["Average"] for dp in datapoints) / len(datapoints)
|
49
|
+
max_value = max(dp["Maximum"] for dp in datapoints)
|
50
|
+
return round(avg_value, 2), round(max_value, 2)
|
51
|
+
else:
|
52
|
+
logger.debug(f"No metrics data for {instance_id} - {metric_name}")
|
53
|
+
return 0, 0
|
54
|
+
|
55
|
+
except ClientError as e:
|
56
|
+
logger.warning(f"Could not get {metric_name} metrics for {instance_id}: {e}")
|
57
|
+
return 0, 0
|
58
|
+
|
59
|
+
|
60
|
+
@click.command()
|
61
|
+
@click.option("--output-file", default="/tmp/rds_cost_optimization.csv", help="Output CSV file path")
|
62
|
+
@click.option("--days", default=7, help="Number of days for CloudWatch metrics analysis")
|
63
|
+
@click.option("--include-storage", is_flag=True, help="Include detailed storage analysis")
|
64
|
+
def get_rds_list(output_file, days, include_storage):
|
65
|
+
"""Analyze RDS instances for cost optimization and performance insights."""
|
66
|
+
logger.info(f"Analyzing RDS instances in {display_aws_account_info()}")
|
67
|
+
|
68
|
+
try:
|
69
|
+
rds = get_client("rds")
|
70
|
+
cloudwatch = get_client("cloudwatch")
|
71
|
+
|
72
|
+
# Get RDS instances
|
73
|
+
logger.info("Collecting RDS instance data...")
|
74
|
+
response = rds.describe_db_instances()
|
75
|
+
instances = response.get("DBInstances", [])
|
76
|
+
|
77
|
+
if not instances:
|
78
|
+
logger.info("No RDS instances found")
|
79
|
+
return
|
80
|
+
|
81
|
+
logger.info(f"Found {len(instances)} RDS instances to analyze")
|
82
|
+
|
83
|
+
# Get reserved instances for cost analysis
|
84
|
+
reserved_instances = get_rds_reserved_instances()
|
85
|
+
reserved_classes = {
|
86
|
+
ri["DBInstanceClass"]
|
87
|
+
for ri in reserved_instances.get("ReservedDBInstances", [])
|
88
|
+
if ri.get("State") == "active"
|
89
|
+
}
|
90
|
+
|
91
|
+
data = []
|
92
|
+
optimization_candidates = []
|
93
|
+
|
94
|
+
for i, instance in enumerate(instances, 1):
|
95
|
+
instance_id = instance["DBInstanceIdentifier"]
|
96
|
+
instance_class = instance["DBInstanceClass"]
|
97
|
+
|
98
|
+
logger.info(f"Analyzing instance {i}/{len(instances)}: {instance_id}")
|
99
|
+
|
100
|
+
# Basic instance information
|
101
|
+
instance_data = {
|
102
|
+
"InstanceIdentifier": instance_id,
|
103
|
+
"Engine": instance.get("Engine", "Unknown"),
|
104
|
+
"EngineVersion": instance.get("EngineVersion", "Unknown"),
|
105
|
+
"InstanceClass": instance_class,
|
106
|
+
"StorageType": instance.get("StorageType", "Unknown"),
|
107
|
+
"AllocatedStorage": instance.get("AllocatedStorage", 0),
|
108
|
+
"MultiAZ": instance.get("MultiAZ", False),
|
109
|
+
"DBInstanceStatus": instance.get("DBInstanceStatus", "Unknown"),
|
110
|
+
"AvailabilityZone": instance.get("AvailabilityZone", "Unknown"),
|
111
|
+
}
|
112
|
+
|
113
|
+
# Determine purchase type
|
114
|
+
purchase_type = "Reserved" if instance_class in reserved_classes else "On-Demand"
|
115
|
+
instance_data["PurchaseType"] = purchase_type
|
116
|
+
|
117
|
+
# Get CloudWatch metrics
|
118
|
+
logger.debug(f" Getting CloudWatch metrics for {instance_id}...")
|
119
|
+
|
120
|
+
# CPU Utilization
|
121
|
+
cpu_avg, cpu_max = get_cloudwatch_metrics(cloudwatch, instance_id, "CPUUtilization", days)
|
122
|
+
instance_data[f"CPUUtilization_Avg_{days}d"] = cpu_avg
|
123
|
+
instance_data[f"CPUUtilization_Max_{days}d"] = cpu_max
|
124
|
+
|
125
|
+
# Memory metrics (FreeableMemory)
|
126
|
+
mem_avg, mem_max = get_cloudwatch_metrics(cloudwatch, instance_id, "FreeableMemory", days)
|
127
|
+
# Convert bytes to GB
|
128
|
+
mem_avg_gb = round(mem_avg / (1024**3), 2) if mem_avg > 0 else 0
|
129
|
+
mem_max_gb = round(mem_max / (1024**3), 2) if mem_max > 0 else 0
|
130
|
+
instance_data[f"FreeMemory_Avg_GB_{days}d"] = mem_avg_gb
|
131
|
+
instance_data[f"FreeMemory_Max_GB_{days}d"] = mem_max_gb
|
132
|
+
|
133
|
+
# Storage analysis if requested
|
134
|
+
if include_storage:
|
135
|
+
try:
|
136
|
+
storage_response = rds.describe_db_instances(DBInstanceIdentifier=instance_id)
|
137
|
+
db_instance = storage_response["DBInstances"][0]
|
138
|
+
instance_data["StorageEncrypted"] = db_instance.get("StorageEncrypted", False)
|
139
|
+
instance_data["Iops"] = db_instance.get("Iops", "Standard")
|
140
|
+
except ClientError as e:
|
141
|
+
logger.debug(f"Could not get storage details for {instance_id}: {e}")
|
142
|
+
|
143
|
+
# Cost optimization analysis
|
144
|
+
optimization_notes = []
|
145
|
+
|
146
|
+
# Low CPU utilization suggests downsizing
|
147
|
+
if cpu_avg < 20:
|
148
|
+
optimization_notes.append(f"Low CPU utilization ({cpu_avg}%)")
|
149
|
+
optimization_candidates.append(instance_id)
|
150
|
+
|
151
|
+
# High memory availability suggests downsizing
|
152
|
+
if mem_avg_gb > 2: # More than 2GB free on average
|
153
|
+
optimization_notes.append(f"High free memory ({mem_avg_gb}GB avg)")
|
154
|
+
|
155
|
+
# Reserved instance recommendation
|
156
|
+
if purchase_type == "On-Demand" and cpu_avg > 10: # Active instance
|
157
|
+
optimization_notes.append("Consider Reserved Instance")
|
158
|
+
|
159
|
+
instance_data["OptimizationNotes"] = "; ".join(optimization_notes) if optimization_notes else "None"
|
160
|
+
instance_data["OptimizationCandidate"] = instance_id in optimization_candidates
|
161
|
+
|
162
|
+
data.append(instance_data)
|
163
|
+
|
164
|
+
# Log summary for this instance
|
165
|
+
logger.info(f" → {instance_class}, CPU: {cpu_avg}%, Free Mem: {mem_avg_gb}GB, {purchase_type}")
|
166
|
+
|
167
|
+
# Export results
|
168
|
+
write_to_csv(data, output_file)
|
169
|
+
logger.info(f"RDS analysis exported to: {output_file}")
|
170
|
+
|
171
|
+
# Summary report
|
172
|
+
logger.info("\n=== ANALYSIS SUMMARY ===")
|
173
|
+
logger.info(f"Total RDS instances: {len(instances)}")
|
174
|
+
logger.info(f"Reserved instances: {sum(1 for d in data if d['PurchaseType'] == 'Reserved')}")
|
175
|
+
logger.info(f"On-Demand instances: {sum(1 for d in data if d['PurchaseType'] == 'On-Demand')}")
|
176
|
+
logger.info(f"Optimization candidates: {len(optimization_candidates)}")
|
177
|
+
|
178
|
+
if optimization_candidates:
|
179
|
+
logger.warning(f"⚠ Instances with low utilization ({days}d avg):")
|
180
|
+
for instance_id in optimization_candidates:
|
181
|
+
instance_info = next(d for d in data if d["InstanceIdentifier"] == instance_id)
|
182
|
+
cpu_avg = instance_info[f"CPUUtilization_Avg_{days}d"]
|
183
|
+
logger.warning(f" - {instance_id}: {cpu_avg}% CPU")
|
184
|
+
else:
|
185
|
+
logger.info("✓ No obvious optimization candidates found")
|
186
|
+
|
187
|
+
# Engine distribution
|
188
|
+
engines = {}
|
189
|
+
for instance in data:
|
190
|
+
engine = instance["Engine"]
|
191
|
+
engines[engine] = engines.get(engine, 0) + 1
|
192
|
+
|
193
|
+
logger.info("Engine distribution:")
|
194
|
+
for engine, count in sorted(engines.items()):
|
195
|
+
logger.info(f" {engine}: {count} instances")
|
196
|
+
|
197
|
+
except Exception as e:
|
198
|
+
logger.error(f"Failed to analyze RDS instances: {e}")
|
199
|
+
raise
|