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
runbooks/aws/ec2_unused_eips.py
DELETED
@@ -1,196 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
"""
|
4
|
-
Find and Report Unused Elastic IPs (EIPs) via AWS SES.
|
5
|
-
|
6
|
-
Author: nnthanh101@gmail.com
|
7
|
-
Date: 2025-01-07
|
8
|
-
Version: 2.0.0
|
9
|
-
|
10
|
-
Description:
|
11
|
-
- Identifies unused Elastic IPs in AWS.
|
12
|
-
- Sends details via email using AWS Simple Email Service (SES).
|
13
|
-
|
14
|
-
Requirements:
|
15
|
-
- IAM Role Permissions:
|
16
|
-
* ec2:DescribeAddresses
|
17
|
-
* ses:SendEmail
|
18
|
-
- Environment Variables:
|
19
|
-
* SOURCE_EMAIL: Sender email address verified in SES.
|
20
|
-
* DEST_EMAIL: Recipient email address.
|
21
|
-
"""
|
22
|
-
|
23
|
-
import json
|
24
|
-
import logging
|
25
|
-
import os
|
26
|
-
from typing import Dict, List
|
27
|
-
|
28
|
-
import boto3
|
29
|
-
from botocore.exceptions import BotoCoreError, ClientError
|
30
|
-
|
31
|
-
from runbooks.utils.logger import configure_logger
|
32
|
-
|
33
|
-
## ✅ Configure Logger
|
34
|
-
logger = configure_logger(__name__)
|
35
|
-
|
36
|
-
|
37
|
-
# ==============================
|
38
|
-
# AWS CLIENT INITIALIZATION
|
39
|
-
# ==============================
|
40
|
-
def get_boto3_clients():
|
41
|
-
"""
|
42
|
-
Initializes AWS clients for EC2 and SES.
|
43
|
-
|
44
|
-
Returns:
|
45
|
-
Tuple[boto3.client, boto3.client]: EC2 and SES clients.
|
46
|
-
"""
|
47
|
-
ec2_client = boto3.client("ec2")
|
48
|
-
ses_client = boto3.client("ses")
|
49
|
-
return ec2_client, ses_client
|
50
|
-
|
51
|
-
|
52
|
-
# ==============================
|
53
|
-
# CONFIGURATION VARIABLES
|
54
|
-
# ==============================
|
55
|
-
def load_environment_variables():
|
56
|
-
"""
|
57
|
-
Loads and validates environment variables required for execution.
|
58
|
-
|
59
|
-
Returns:
|
60
|
-
Tuple[str, str]: Source and destination email addresses.
|
61
|
-
"""
|
62
|
-
source_email = os.getenv("SOURCE_EMAIL")
|
63
|
-
dest_email = os.getenv("DEST_EMAIL")
|
64
|
-
|
65
|
-
if not source_email or not dest_email:
|
66
|
-
raise ValueError("Environment variables SOURCE_EMAIL and DEST_EMAIL must be set.")
|
67
|
-
|
68
|
-
return source_email, dest_email
|
69
|
-
|
70
|
-
|
71
|
-
# ==============================
|
72
|
-
# EIP UTILITIES
|
73
|
-
# ==============================
|
74
|
-
def get_unused_eips(ec2_client) -> List[Dict[str, str]]:
|
75
|
-
"""
|
76
|
-
Fetches unused Elastic IPs (EIPs).
|
77
|
-
|
78
|
-
Args:
|
79
|
-
ec2_client (boto3.client): EC2 client.
|
80
|
-
|
81
|
-
Returns:
|
82
|
-
List[Dict[str, str]]: List of unused EIPs with details.
|
83
|
-
"""
|
84
|
-
try:
|
85
|
-
response = ec2_client.describe_addresses()
|
86
|
-
unused_eips = []
|
87
|
-
|
88
|
-
for address in response["Addresses"]:
|
89
|
-
if "InstanceId" not in address: ## Not associated with an instance
|
90
|
-
unused_eips.append(
|
91
|
-
{
|
92
|
-
"PublicIp": address["PublicIp"],
|
93
|
-
"AllocationId": address["AllocationId"],
|
94
|
-
"Domain": address.get("Domain", "N/A"), ## VPC or standard
|
95
|
-
}
|
96
|
-
)
|
97
|
-
|
98
|
-
logger.info(f"Found {len(unused_eips)} unused EIPs.")
|
99
|
-
return unused_eips
|
100
|
-
|
101
|
-
except ClientError as e:
|
102
|
-
logger.error(f"Failed to describe addresses: {e.response['Error']['Code']} - {e}")
|
103
|
-
raise
|
104
|
-
except BotoCoreError as e:
|
105
|
-
logger.error(f"BotoCore error occurred: {e}")
|
106
|
-
raise
|
107
|
-
|
108
|
-
|
109
|
-
# ==============================
|
110
|
-
# EMAIL UTILITIES
|
111
|
-
# ==============================
|
112
|
-
def format_eip_report(eips: List[Dict[str, str]]) -> str:
|
113
|
-
"""
|
114
|
-
Formats the EIPs data as a markdown table for email reporting.
|
115
|
-
|
116
|
-
Args:
|
117
|
-
eips (List[Dict[str, str]]): List of unused EIPs.
|
118
|
-
|
119
|
-
Returns:
|
120
|
-
str: Formatted markdown table.
|
121
|
-
"""
|
122
|
-
if not eips:
|
123
|
-
return "No unused EIPs found."
|
124
|
-
|
125
|
-
## Table Header
|
126
|
-
table = "| Public IP | Allocation ID | Domain |\n|-----------|----------------|--------|\n"
|
127
|
-
|
128
|
-
## Table Rows
|
129
|
-
for eip in eips:
|
130
|
-
table += f"| {eip['PublicIp']} | {eip['AllocationId']} | {eip['Domain']} |\n"
|
131
|
-
|
132
|
-
return table
|
133
|
-
|
134
|
-
|
135
|
-
def send_email_report(ses_client, source_email: str, dest_email: str, eips: List[Dict[str, str]]) -> None:
|
136
|
-
"""
|
137
|
-
Sends an email report via AWS SES.
|
138
|
-
|
139
|
-
Args:
|
140
|
-
ses_client (boto3.client): SES client.
|
141
|
-
source_email (str): Sender email address.
|
142
|
-
dest_email (str): Recipient email address.
|
143
|
-
eips (List[Dict[str, str]]): List of unused EIPs.
|
144
|
-
"""
|
145
|
-
try:
|
146
|
-
subject = "AWS Report: Unused Elastic IPs (EIPs)"
|
147
|
-
body = format_eip_report(eips)
|
148
|
-
|
149
|
-
logger.info(f"Sending email from {source_email} to {dest_email}...")
|
150
|
-
ses_client.send_email(
|
151
|
-
Source=source_email,
|
152
|
-
Destination={"ToAddresses": [dest_email]},
|
153
|
-
Message={
|
154
|
-
"Subject": {"Data": subject, "Charset": "utf-8"},
|
155
|
-
"Body": {"Text": {"Data": body, "Charset": "utf-8"}},
|
156
|
-
},
|
157
|
-
)
|
158
|
-
logger.info("Email sent successfully.")
|
159
|
-
|
160
|
-
except ClientError as e:
|
161
|
-
logger.error(f"Failed to send email: {e.response['Error']['Code']} - {e}")
|
162
|
-
raise
|
163
|
-
except BotoCoreError as e:
|
164
|
-
logger.error(f"BotoCore error occurred: {e}")
|
165
|
-
raise
|
166
|
-
|
167
|
-
|
168
|
-
# ==============================
|
169
|
-
# MAIN HANDLER
|
170
|
-
# ==============================
|
171
|
-
def lambda_handler(event, context):
|
172
|
-
"""
|
173
|
-
AWS Lambda handler for reporting unused Elastic IPs.
|
174
|
-
|
175
|
-
Args:
|
176
|
-
event (dict): AWS event data.
|
177
|
-
context: AWS Lambda context.
|
178
|
-
"""
|
179
|
-
try:
|
180
|
-
## ✅ Load configurations
|
181
|
-
source_email, dest_email = load_environment_variables()
|
182
|
-
|
183
|
-
## ✅ Initialize AWS clients
|
184
|
-
ec2_client, ses_client = get_boto3_clients()
|
185
|
-
|
186
|
-
## ✅ Fetch unused EIPs
|
187
|
-
unused_eips = get_unused_eips(ec2_client)
|
188
|
-
|
189
|
-
## ✅ Send email report using SES
|
190
|
-
send_email_report(ses_client, source_email, dest_email, unused_eips)
|
191
|
-
|
192
|
-
return {"statusCode": 200, "body": "Report sent successfully."}
|
193
|
-
|
194
|
-
except Exception as e:
|
195
|
-
logger.error(f"Error: {e}")
|
196
|
-
return {"statusCode": 500, "body": str(e)}
|
@@ -1,188 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
AWS EC2 Unused Volume Checker with SNS Notification.
|
4
|
-
|
5
|
-
Finds unattached EBS volumes and sends the details via SNS notification.
|
6
|
-
|
7
|
-
Author: nnthanh101@gmail.com
|
8
|
-
Date: 2025-01-08
|
9
|
-
Version: 1.0.0
|
10
|
-
"""
|
11
|
-
|
12
|
-
import json
|
13
|
-
import logging
|
14
|
-
import os
|
15
|
-
import sys
|
16
|
-
from typing import Dict, List
|
17
|
-
|
18
|
-
import boto3
|
19
|
-
from botocore.exceptions import BotoCoreError, ClientError
|
20
|
-
|
21
|
-
from runbooks.utils.logger import configure_logger # Reusable logger utility
|
22
|
-
|
23
|
-
## ✅ Configure Logger
|
24
|
-
logger = configure_logger(__name__)
|
25
|
-
|
26
|
-
# ==============================
|
27
|
-
# CONFIGURATION VARIABLES
|
28
|
-
# ==============================
|
29
|
-
AWS_REGION = os.getenv("AWS_REGION", "ap-southeast-2")
|
30
|
-
SNS_TOPIC_ARN = os.getenv("SNS_TOPIC_ARN", "arn:aws:sns:ap-southeast-2:999999999999:1cloudops")
|
31
|
-
|
32
|
-
# ==============================
|
33
|
-
# AWS CLIENT INITIALIZATION
|
34
|
-
# ==============================
|
35
|
-
ec2_client = boto3.client("ec2", region_name=AWS_REGION)
|
36
|
-
sns_client = boto3.client("sns", region_name=AWS_REGION)
|
37
|
-
|
38
|
-
|
39
|
-
# ==============================
|
40
|
-
# VALIDATION UTILITIES
|
41
|
-
# ==============================
|
42
|
-
def validate_sns_arn(arn: str) -> None:
|
43
|
-
"""
|
44
|
-
Validates the format of the SNS Topic ARN.
|
45
|
-
|
46
|
-
Args:
|
47
|
-
arn (str): SNS Topic ARN.
|
48
|
-
|
49
|
-
Raises:
|
50
|
-
ValueError: If the ARN format is invalid.
|
51
|
-
"""
|
52
|
-
if not arn.startswith("arn:aws:sns:"):
|
53
|
-
raise ValueError(f"Invalid SNS Topic ARN: {arn}")
|
54
|
-
logger.info(f"✅ Valid SNS ARN: {arn}")
|
55
|
-
|
56
|
-
|
57
|
-
# ==============================
|
58
|
-
# CORE FUNCTION: FIND UNUSED VOLUMES
|
59
|
-
# ==============================
|
60
|
-
def find_unused_volumes() -> List[Dict[str, str]]:
|
61
|
-
"""
|
62
|
-
Identifies unused (unattached) EBS volumes in the AWS account.
|
63
|
-
|
64
|
-
Returns:
|
65
|
-
List[Dict[str, str]]: List of unused volumes with details.
|
66
|
-
"""
|
67
|
-
try:
|
68
|
-
## ✅ Retrieve all volumes
|
69
|
-
logger.info("🔍 Fetching all EBS volumes...")
|
70
|
-
response = ec2_client.describe_volumes()
|
71
|
-
|
72
|
-
## ✅ Initialize Unused Volumes List
|
73
|
-
unused_volumes = []
|
74
|
-
|
75
|
-
## ✅ Enhanced Loop with Debug Logs
|
76
|
-
for vol in response["Volumes"]:
|
77
|
-
if len(vol.get("Attachments", [])) == 0: ## Unattached volumes
|
78
|
-
## Log detailed info for debugging
|
79
|
-
logger.debug(f"Unattached Volume: {json.dumps(vol, default=str)}")
|
80
|
-
|
81
|
-
## Append Volume Details
|
82
|
-
unused_volumes.append(
|
83
|
-
{
|
84
|
-
"VolumeId": vol["VolumeId"],
|
85
|
-
"Size": vol["Size"],
|
86
|
-
"State": vol["State"],
|
87
|
-
"Encrypted": vol.get("Encrypted", False),
|
88
|
-
"VolumeType": vol.get("VolumeType", "unknown"),
|
89
|
-
"CreateTime": str(vol["CreateTime"]),
|
90
|
-
}
|
91
|
-
)
|
92
|
-
|
93
|
-
logger.info(f"✅ Found {len(unused_volumes)} unused volumes.")
|
94
|
-
return unused_volumes
|
95
|
-
|
96
|
-
except ClientError as e:
|
97
|
-
logger.error(f"❌ AWS Client Error: {e}")
|
98
|
-
raise
|
99
|
-
|
100
|
-
except Exception as e:
|
101
|
-
logger.error(f"❌ Unexpected error: {e}")
|
102
|
-
raise
|
103
|
-
|
104
|
-
|
105
|
-
# ==============================
|
106
|
-
# NOTIFICATION FUNCTION: SEND EMAIL
|
107
|
-
# ==============================
|
108
|
-
def send_sns_notification(unused_volumes: List[Dict[str, str]]) -> None:
|
109
|
-
"""
|
110
|
-
Sends unused EBS volume details via SNS notification.
|
111
|
-
|
112
|
-
Args:
|
113
|
-
unused_volumes (List[Dict[str, str]]): List of unused volumes.
|
114
|
-
|
115
|
-
Raises:
|
116
|
-
Exception: If SNS publish fails.
|
117
|
-
"""
|
118
|
-
try:
|
119
|
-
## ✅ Prepare Email Body (Markdown for Readability)
|
120
|
-
email_body = "### Unused EBS Volumes Report 📊\n\n"
|
121
|
-
email_body += "| VolumeId | Size (GiB) | State | Encrypted | VolumeType | CreateTime |\n"
|
122
|
-
email_body += "|----------|------------|-------|-----------|------------|------------|\n"
|
123
|
-
for vol in unused_volumes:
|
124
|
-
email_body += f"| {vol['VolumeId']} | {vol['Size']} | {vol['State']} | {vol['Encrypted']} | {vol['VolumeType']} | {vol['CreateTime']} |\n"
|
125
|
-
|
126
|
-
## ✅ Publish to SNS
|
127
|
-
logger.info(f"Sending notification to SNS topic: {SNS_TOPIC_ARN}...")
|
128
|
-
logger.info(f"📤 Sending SNS notification to SNS topic: {SNS_TOPIC_ARN}")
|
129
|
-
sns_client.publish(
|
130
|
-
TopicArn=SNS_TOPIC_ARN,
|
131
|
-
Subject="Unused EBS Volumes Report",
|
132
|
-
Message=email_body,
|
133
|
-
)
|
134
|
-
logger.info("✅ SNS notification sent successfully.")
|
135
|
-
|
136
|
-
except ClientError as e:
|
137
|
-
logger.error(f"❌ SNS Client Error: {e}")
|
138
|
-
raise
|
139
|
-
|
140
|
-
except Exception as e:
|
141
|
-
logger.error(f"❌ Unexpected error while sending SNS notification: {e}")
|
142
|
-
raise
|
143
|
-
|
144
|
-
|
145
|
-
# ==============================
|
146
|
-
# MAIN FUNCTION
|
147
|
-
# ==============================
|
148
|
-
def main() -> None:
|
149
|
-
"""
|
150
|
-
Main function to find unused volumes and send notifications.
|
151
|
-
"""
|
152
|
-
try:
|
153
|
-
## ✅ Validate Inputs/Configuration
|
154
|
-
validate_sns_arn(SNS_TOPIC_ARN)
|
155
|
-
|
156
|
-
## ✅ Find Unused Volumes
|
157
|
-
unused_volumes = find_unused_volumes()
|
158
|
-
|
159
|
-
if unused_volumes:
|
160
|
-
## ✅ Send SNS Notification if unused volumes exist
|
161
|
-
send_sns_notification(unused_volumes)
|
162
|
-
else:
|
163
|
-
logger.info("⚠️ No unused volumes found. Exiting without notification.")
|
164
|
-
|
165
|
-
except Exception as e:
|
166
|
-
logger.error(f"❌ Fatal Error: {e}")
|
167
|
-
sys.exit(1)
|
168
|
-
|
169
|
-
|
170
|
-
# ==============================
|
171
|
-
# LAMBDA HANDLER
|
172
|
-
# ==============================
|
173
|
-
def lambda_handler(event, context):
|
174
|
-
"""
|
175
|
-
AWS Lambda Handler for unused EBS volume detection and notification.
|
176
|
-
|
177
|
-
Args:
|
178
|
-
event (dict): AWS Lambda event.
|
179
|
-
context: AWS Lambda context.
|
180
|
-
"""
|
181
|
-
main() # Reuse the main function for Lambda
|
182
|
-
|
183
|
-
|
184
|
-
# ==============================
|
185
|
-
# ENTRY POINT
|
186
|
-
# ==============================
|
187
|
-
if __name__ == "__main__":
|
188
|
-
main()
|
runbooks/aws/s3_create_bucket.py
DELETED
@@ -1,142 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
AWS S3 Bucket Creator Script.
|
4
|
-
|
5
|
-
Author: nnthanh101@gmail.com
|
6
|
-
Date: 2025-01-09
|
7
|
-
Version: 1.0.0
|
8
|
-
"""
|
9
|
-
|
10
|
-
import os
|
11
|
-
import sys
|
12
|
-
from typing import Optional
|
13
|
-
|
14
|
-
import boto3
|
15
|
-
from botocore.exceptions import BotoCoreError, ClientError
|
16
|
-
|
17
|
-
from runbooks.utils.logger import configure_logger # # Import reusable logger
|
18
|
-
|
19
|
-
## Initialize Logger
|
20
|
-
logger = configure_logger("list_s3_buckets")
|
21
|
-
|
22
|
-
# ==============================
|
23
|
-
# CONFIGURATION VARIABLES
|
24
|
-
# ==============================
|
25
|
-
DEFAULT_BUCKET_NAME = os.getenv("S3_BUCKET_NAME", "1cloudops") # Default bucket name
|
26
|
-
DEFAULT_REGION = os.getenv("AWS_REGION", "ap-southeast-2") # Default AWS region
|
27
|
-
|
28
|
-
|
29
|
-
# ==============================
|
30
|
-
# VALIDATION UTILITIES
|
31
|
-
# ==============================
|
32
|
-
def validate_bucket_name(bucket_name: str) -> None:
|
33
|
-
"""
|
34
|
-
Validates an S3 bucket name based on AWS naming rules.
|
35
|
-
|
36
|
-
Args:
|
37
|
-
bucket_name (str): The bucket name to validate.
|
38
|
-
|
39
|
-
Raises:
|
40
|
-
ValueError: If the bucket name is invalid.
|
41
|
-
"""
|
42
|
-
import re
|
43
|
-
|
44
|
-
## ✅ AWS Bucket Naming Rules
|
45
|
-
if len(bucket_name) < 3 or len(bucket_name) > 63:
|
46
|
-
raise ValueError("Bucket name must be between 3 and 63 characters long.")
|
47
|
-
|
48
|
-
if not re.match(r"^[a-z0-9.-]+$", bucket_name):
|
49
|
-
raise ValueError("Bucket name can only contain lowercase letters, numbers, hyphens (-), and periods (.).")
|
50
|
-
|
51
|
-
if bucket_name.startswith(".") or bucket_name.endswith("."):
|
52
|
-
raise ValueError("Bucket name cannot start or end with a period (.)")
|
53
|
-
|
54
|
-
if ".." in bucket_name:
|
55
|
-
raise ValueError("Bucket name cannot contain consecutive periods (..).")
|
56
|
-
|
57
|
-
logger.info(f"✅ Bucket name '{bucket_name}' is valid.")
|
58
|
-
|
59
|
-
|
60
|
-
# ==============================
|
61
|
-
# CORE FUNCTION: CREATE BUCKET
|
62
|
-
# ==============================
|
63
|
-
def create_s3_bucket(bucket_name: str, region: str) -> Optional[str]:
|
64
|
-
"""
|
65
|
-
Creates an S3 bucket in the specified AWS region.
|
66
|
-
|
67
|
-
Args:
|
68
|
-
bucket_name (str): The name of the S3 bucket to create.
|
69
|
-
region (str): The AWS region where the bucket will be created.
|
70
|
-
|
71
|
-
Returns:
|
72
|
-
Optional[str]: The location of the created bucket if successful, None otherwise.
|
73
|
-
|
74
|
-
Raises:
|
75
|
-
Exception: Raises error if bucket creation fails.
|
76
|
-
"""
|
77
|
-
## ✅ Initialize S3 Client
|
78
|
-
try:
|
79
|
-
s3_client = boto3.client("s3", region_name=region)
|
80
|
-
logger.info(f"Creating bucket '{bucket_name}' in region '{region}'...")
|
81
|
-
|
82
|
-
## ✅ Create bucket with LocationConstraint
|
83
|
-
if region == "us-east-1": ## Special case: us-east-1 doesn't require LocationConstraint
|
84
|
-
response = s3_client.create_bucket(
|
85
|
-
Bucket=bucket_name,
|
86
|
-
ACL="private", ## Set access control to private
|
87
|
-
)
|
88
|
-
else:
|
89
|
-
response = s3_client.create_bucket(
|
90
|
-
Bucket=bucket_name,
|
91
|
-
ACL="private",
|
92
|
-
CreateBucketConfiguration={"LocationConstraint": region},
|
93
|
-
)
|
94
|
-
|
95
|
-
logger.info(f"✅ Bucket '{bucket_name}' created successfully at {response['Location']}.")
|
96
|
-
return response["Location"]
|
97
|
-
|
98
|
-
except ClientError as e:
|
99
|
-
logger.error(f"❌ AWS Client Error: {e}")
|
100
|
-
raise
|
101
|
-
|
102
|
-
except BotoCoreError as e:
|
103
|
-
logger.error(f"❌ BotoCore Error: {e}")
|
104
|
-
raise
|
105
|
-
|
106
|
-
except Exception as e:
|
107
|
-
logger.error(f"❌ Unexpected error: {e}")
|
108
|
-
raise
|
109
|
-
|
110
|
-
|
111
|
-
# ==============================
|
112
|
-
# MAIN FUNCTION
|
113
|
-
# ==============================
|
114
|
-
def main() -> None:
|
115
|
-
"""
|
116
|
-
Main entry point for script execution.
|
117
|
-
"""
|
118
|
-
try:
|
119
|
-
## ✅ Parse Arguments or Use Environment Variables
|
120
|
-
bucket_name = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_BUCKET_NAME
|
121
|
-
region = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_REGION
|
122
|
-
|
123
|
-
## ✅ Validate Input
|
124
|
-
validate_bucket_name(bucket_name)
|
125
|
-
|
126
|
-
## ✅ Create S3 Bucket
|
127
|
-
create_s3_bucket(bucket_name, region)
|
128
|
-
|
129
|
-
except ValueError as e:
|
130
|
-
logger.error(f"❌ Input Validation Error: {e}")
|
131
|
-
sys.exit(1)
|
132
|
-
|
133
|
-
except Exception as e:
|
134
|
-
logger.error(f"❌ Fatal Error: {e}")
|
135
|
-
sys.exit(1)
|
136
|
-
|
137
|
-
|
138
|
-
# ==============================
|
139
|
-
# ENTRY POINT
|
140
|
-
# ==============================
|
141
|
-
if __name__ == "__main__":
|
142
|
-
main()
|
runbooks/aws/s3_list_buckets.py
DELETED
@@ -1,152 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
"""
|
4
|
-
AWS S3 Bucket Listing Utility with logging and error handling.
|
5
|
-
|
6
|
-
Author: nnthanh101@gmail.com
|
7
|
-
Date: 2025-01-05
|
8
|
-
Version: 1.0.0
|
9
|
-
|
10
|
-
Description:
|
11
|
-
This script lists all S3 buckets in the AWS account using the Boto3 library.
|
12
|
-
It implements robust error handling, logging, and modularization for high standards.
|
13
|
-
|
14
|
-
Usage:
|
15
|
-
python list_s3_buckets.py
|
16
|
-
"""
|
17
|
-
|
18
|
-
import json
|
19
|
-
import os
|
20
|
-
from typing import Dict, List
|
21
|
-
|
22
|
-
import boto3
|
23
|
-
from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
|
24
|
-
from tabulate import tabulate
|
25
|
-
|
26
|
-
from runbooks.utils.logger import configure_logger
|
27
|
-
|
28
|
-
## Initialize Logger
|
29
|
-
logger = configure_logger("list_s3_buckets")
|
30
|
-
|
31
|
-
|
32
|
-
def get_s3_client(region: str = None) -> boto3.client:
|
33
|
-
"""
|
34
|
-
Initializes and returns a Boto3 S3 client with optional region support.
|
35
|
-
|
36
|
-
Args:
|
37
|
-
region (str, optional): AWS region. Defaults to None (uses environment or AWS config).
|
38
|
-
|
39
|
-
Returns:
|
40
|
-
boto3.client: Configured S3 client.
|
41
|
-
|
42
|
-
Raises:
|
43
|
-
NoCredentialsError: Raised if AWS credentials are missing.
|
44
|
-
PartialCredentialsError: Raised if AWS credentials are incomplete.
|
45
|
-
"""
|
46
|
-
try:
|
47
|
-
## ✅ Allow region override if specified
|
48
|
-
session = boto3.Session(region_name=region) if region else boto3.Session()
|
49
|
-
client = session.client("s3")
|
50
|
-
logger.info("S3 client initialized successfully.")
|
51
|
-
return client
|
52
|
-
except (NoCredentialsError, PartialCredentialsError) as e:
|
53
|
-
# logger.error("Please configure them using AWS CLI or environment variables.")
|
54
|
-
logger.error(f"AWS Credentials Error: {str(e)}")
|
55
|
-
raise
|
56
|
-
except Exception as e:
|
57
|
-
logger.error(f"Unexpected error: {str(e)}")
|
58
|
-
raise
|
59
|
-
|
60
|
-
|
61
|
-
def list_s3_buckets(client: boto3.client) -> List[Dict[str, str]]:
|
62
|
-
"""
|
63
|
-
Lists all S3 buckets in the AWS account.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
client (boto3.client): Pre-configured S3 client.
|
67
|
-
|
68
|
-
Returns:
|
69
|
-
List[Dict[str, str]]: List of bucket details including name and creation date.
|
70
|
-
|
71
|
-
Raises:
|
72
|
-
ClientError: For API errors returned by AWS.
|
73
|
-
"""
|
74
|
-
try:
|
75
|
-
## ✅ Call AWS API to list buckets
|
76
|
-
response = client.list_buckets()
|
77
|
-
|
78
|
-
## ✅ Extract bucket names and creation dates
|
79
|
-
# bucket_list = [{"Name": bucket['Name'], "CreationDate": str(bucket['CreationDate'])} for bucket in buckets]
|
80
|
-
bucket_list = [
|
81
|
-
{
|
82
|
-
"Name": bucket["Name"],
|
83
|
-
"CreationDate": bucket["CreationDate"].strftime("%Y-%m-%d %H:%M:%S"),
|
84
|
-
"Owner": {
|
85
|
-
"DisplayName": response["Owner"].get("DisplayName", "N/A"),
|
86
|
-
# "ID": response['Owner'].get('ID', 'N/A')
|
87
|
-
},
|
88
|
-
}
|
89
|
-
for bucket in response.get("Buckets", [])
|
90
|
-
]
|
91
|
-
|
92
|
-
## ✅ Log the number of buckets found
|
93
|
-
if not bucket_list:
|
94
|
-
logger.warning("No buckets found.")
|
95
|
-
else:
|
96
|
-
logger.info(f"Found {len(bucket_list)} S3 bucket(s).")
|
97
|
-
|
98
|
-
return bucket_list
|
99
|
-
except ClientError as e:
|
100
|
-
logger.error(f"Failed to list buckets: {e.response['Error']['Message']}")
|
101
|
-
raise
|
102
|
-
except Exception as e:
|
103
|
-
logger.error(f"Unexpected error: {str(e)}")
|
104
|
-
raise
|
105
|
-
|
106
|
-
|
107
|
-
def display_buckets(buckets: List[Dict[str, str]]) -> None:
|
108
|
-
"""
|
109
|
-
Displays bucket details in JSON format for readability.
|
110
|
-
|
111
|
-
Args:
|
112
|
-
buckets (List[Dict[str, str]]): List of bucket details.
|
113
|
-
"""
|
114
|
-
|
115
|
-
# print(json.dumps(buckets, indent=4)) if buckets else print("No buckets found.")
|
116
|
-
## ✅ Prepare Table Headers and Rows
|
117
|
-
headers = ["Name", "Creation Date", "Owner Display Name", "Owner ID"]
|
118
|
-
rows = [
|
119
|
-
# [bucket["Name"], bucket["CreationDate"], bucket["Owner"]["DisplayName"], bucket["Owner"]["ID"]]
|
120
|
-
[bucket["Name"], bucket["CreationDate"], bucket["Owner"]["DisplayName"]]
|
121
|
-
for bucket in buckets
|
122
|
-
]
|
123
|
-
|
124
|
-
## ✅ Render Markdown Table
|
125
|
-
print("### AWS S3 Buckets\n")
|
126
|
-
## Creating & printing the Markdown Table
|
127
|
-
table = tabulate(rows, headers=headers, tablefmt="github", missingval="N/A")
|
128
|
-
print(table)
|
129
|
-
|
130
|
-
|
131
|
-
## ==============================
|
132
|
-
## MAIN FUNCTION
|
133
|
-
## ==============================
|
134
|
-
def main() -> None:
|
135
|
-
"""
|
136
|
-
Main entry point for listing S3 buckets.
|
137
|
-
"""
|
138
|
-
try:
|
139
|
-
## ✅ Load AWS region dynamically (fallback to default)
|
140
|
-
region = os.getenv("AWS_REGION", "us-east-1")
|
141
|
-
## ✅ Initialize S3 client
|
142
|
-
client = get_s3_client(region)
|
143
|
-
## ✅ Retrieve bucket list
|
144
|
-
buckets = list_s3_buckets(client)
|
145
|
-
## ✅ Display results
|
146
|
-
display_buckets(buckets)
|
147
|
-
except Exception as e:
|
148
|
-
logger.error(f"Program terminated with error: {str(e)}")
|
149
|
-
|
150
|
-
|
151
|
-
if __name__ == "__main__":
|
152
|
-
main()
|