runbooks 0.1.6__py3-none-any.whl → 0.1.8__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 (56) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/aws/__init__.py +58 -0
  3. runbooks/aws/dynamodb_operations.py +231 -0
  4. runbooks/aws/ec2_copy_image_cross-region.py +195 -0
  5. runbooks/aws/ec2_describe_instances.py +202 -0
  6. runbooks/aws/ec2_ebs_snapshots_delete.py +186 -0
  7. runbooks/aws/ec2_run_instances.py +207 -0
  8. runbooks/aws/ec2_start_stop_instances.py +199 -0
  9. runbooks/aws/ec2_terminate_instances.py +143 -0
  10. runbooks/aws/ec2_unused_eips.py +196 -0
  11. runbooks/aws/ec2_unused_volumes.py +184 -0
  12. runbooks/aws/s3_create_bucket.py +140 -0
  13. runbooks/aws/s3_list_buckets.py +152 -0
  14. runbooks/aws/s3_list_objects.py +151 -0
  15. runbooks/aws/s3_object_operations.py +183 -0
  16. runbooks/aws/tagging_lambda_handler.py +172 -0
  17. runbooks/python101/calculator.py +34 -0
  18. runbooks/python101/config.py +1 -0
  19. runbooks/python101/exceptions.py +16 -0
  20. runbooks/python101/file_manager.py +218 -0
  21. runbooks/python101/toolkit.py +153 -0
  22. runbooks/security_baseline/__init__.py +0 -0
  23. runbooks/security_baseline/checklist/__init__.py +17 -0
  24. runbooks/security_baseline/checklist/account_level_bucket_public_access.py +86 -0
  25. runbooks/security_baseline/checklist/alternate_contacts.py +65 -0
  26. runbooks/security_baseline/checklist/bucket_public_access.py +82 -0
  27. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +66 -0
  28. runbooks/security_baseline/checklist/direct_attached_policy.py +69 -0
  29. runbooks/security_baseline/checklist/guardduty_enabled.py +71 -0
  30. runbooks/security_baseline/checklist/iam_password_policy.py +43 -0
  31. runbooks/security_baseline/checklist/iam_user_mfa.py +39 -0
  32. runbooks/security_baseline/checklist/multi_region_instance_usage.py +55 -0
  33. runbooks/security_baseline/checklist/multi_region_trail.py +64 -0
  34. runbooks/security_baseline/checklist/root_access_key.py +72 -0
  35. runbooks/security_baseline/checklist/root_mfa.py +39 -0
  36. runbooks/security_baseline/checklist/root_usage.py +128 -0
  37. runbooks/security_baseline/checklist/trail_enabled.py +68 -0
  38. runbooks/security_baseline/checklist/trusted_advisor.py +24 -0
  39. runbooks/security_baseline/report_generator.py +149 -0
  40. runbooks/security_baseline/run_script.py +76 -0
  41. runbooks/security_baseline/security_baseline_tester.py +179 -0
  42. runbooks/security_baseline/utils/__init__.py +1 -0
  43. runbooks/security_baseline/utils/common.py +109 -0
  44. runbooks/security_baseline/utils/enums.py +44 -0
  45. runbooks/security_baseline/utils/language.py +762 -0
  46. runbooks/security_baseline/utils/level_const.py +5 -0
  47. runbooks/security_baseline/utils/permission_list.py +26 -0
  48. runbooks/utils/__init__.py +0 -0
  49. runbooks/utils/logger.py +36 -0
  50. {runbooks-0.1.6.dist-info → runbooks-0.1.8.dist-info}/METADATA +9 -1
  51. runbooks-0.1.8.dist-info/RECORD +54 -0
  52. runbooks-0.1.8.dist-info/entry_points.txt +3 -0
  53. runbooks-0.1.6.dist-info/RECORD +0 -6
  54. runbooks-0.1.6.dist-info/entry_points.txt +0 -2
  55. {runbooks-0.1.6.dist-info → runbooks-0.1.8.dist-info}/WHEEL +0 -0
  56. {runbooks-0.1.6.dist-info → runbooks-0.1.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Terminate EC2 Instances Script.
5
+
6
+ This script provides a robust and production-grade solution to terminate EC2 instances.
7
+ It supports execution as a standalone Python script, within Docker containers, or as an AWS Lambda function.
8
+
9
+ Author: CloudOps DevOps Engineer
10
+ Date: 2025-01-08
11
+ Version: 1.0.0
12
+ """
13
+
14
+ import logging
15
+ import os
16
+ from typing import List
17
+
18
+ import boto3
19
+ from botocore.exceptions import BotoCoreError, ClientError
20
+
21
+ # ==============================
22
+ # CONFIGURATION
23
+ # ==============================
24
+ REGION = os.getenv("AWS_REGION", "us-east-1")
25
+ INSTANCE_IDS = os.getenv("INSTANCE_IDS", "").split(",") ## Example: 'i-0158ab7a03bb6a954,i-04a8f37b92b7c1a78'
26
+ DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true"
27
+
28
+ # ==============================
29
+ # LOGGING CONFIGURATION
30
+ # ==============================
31
+ logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # ==============================
35
+ # AWS CLIENT INITIALIZATION
36
+ # ==============================
37
+ ec2_client = boto3.client("ec2", region_name=REGION)
38
+
39
+
40
+ # ==============================
41
+ # FUNCTION: Terminate EC2 Instances
42
+ # ==============================
43
+ def terminate_instances(instance_ids: List[str]) -> List[str]:
44
+ """
45
+ Terminates specified EC2 instances.
46
+
47
+ Args:
48
+ instance_ids (List[str]): List of EC2 instance IDs to terminate.
49
+
50
+ Returns:
51
+ List[str]: List of successfully terminated instance IDs.
52
+ """
53
+ try:
54
+ if not instance_ids or instance_ids == [""]:
55
+ logger.error("No instance IDs provided for termination.")
56
+ raise ValueError("Instance IDs cannot be empty.")
57
+
58
+ logger.info(f"Terminating instances: {', '.join(instance_ids)} in region {REGION}...")
59
+ if DRY_RUN:
60
+ logger.info("[DRY-RUN] No actual termination performed.")
61
+ return []
62
+
63
+ # Perform termination
64
+ response = ec2_client.terminate_instances(InstanceIds=instance_ids)
65
+
66
+ terminated_instances = [instance["InstanceId"] for instance in response["TerminatingInstances"]]
67
+ for instance in response["TerminatingInstances"]:
68
+ logger.info(f"Instance {instance['InstanceId']} state changed to {instance['CurrentState']['Name']}.")
69
+ return terminated_instances
70
+
71
+ except ClientError as e:
72
+ logger.error(f"AWS Client Error: {e}")
73
+ raise
74
+
75
+ except BotoCoreError as e:
76
+ logger.error(f"BotoCore Error: {e}")
77
+ raise
78
+
79
+ except Exception as e:
80
+ logger.error(f"Unexpected error: {e}")
81
+ raise
82
+
83
+
84
+ # ==============================
85
+ # MAIN FUNCTION (for CLI/Docker)
86
+ # ==============================
87
+ def main():
88
+ """
89
+ Main entry point for standalone execution (CLI or Docker).
90
+ """
91
+ try:
92
+ # Ensure instance IDs are provided
93
+ if not INSTANCE_IDS or INSTANCE_IDS == [""]:
94
+ logger.error("No instance IDs provided. Set INSTANCE_IDS environment variable.")
95
+ raise ValueError("Instance IDs are required to terminate EC2 instances.")
96
+
97
+ # Terminate instances
98
+ terminated_instances = terminate_instances(INSTANCE_IDS)
99
+ if terminated_instances:
100
+ logger.info(f"Successfully terminated instances: {', '.join(terminated_instances)}")
101
+ else:
102
+ logger.info("No instances terminated (Dry-Run mode or empty list).")
103
+
104
+ except Exception as e:
105
+ logger.error(f"Error during instance termination: {e}")
106
+ raise
107
+
108
+
109
+ # ==============================
110
+ # AWS LAMBDA HANDLER
111
+ # ==============================
112
+ def lambda_handler(event, context):
113
+ """
114
+ AWS Lambda handler for terminating EC2 instances.
115
+
116
+ Args:
117
+ event (dict): AWS Lambda event payload. Expected to include instance IDs.
118
+ context: AWS Lambda context object.
119
+ """
120
+ try:
121
+ instance_ids = event.get("instance_ids", INSTANCE_IDS)
122
+ if not instance_ids or instance_ids == [""]:
123
+ logger.error("No instance IDs provided in the Lambda event or environment.")
124
+ raise ValueError("Instance IDs are required to terminate EC2 instances.")
125
+
126
+ terminated_instances = terminate_instances(instance_ids)
127
+ return {
128
+ "statusCode": 200,
129
+ "body": {
130
+ "message": "Instances terminated successfully.",
131
+ "terminated_instances": terminated_instances,
132
+ },
133
+ }
134
+ except Exception as e:
135
+ logger.error(f"Lambda function failed: {e}")
136
+ return {"statusCode": 500, "body": {"message": str(e)}}
137
+
138
+
139
+ # ==============================
140
+ # SCRIPT ENTRY POINT
141
+ # ==============================
142
+ if __name__ == "__main__":
143
+ main()
@@ -0,0 +1,196 @@
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)}
@@ -0,0 +1,184 @@
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(TopicArn=SNS_TOPIC_ARN, Subject="Unused EBS Volumes Report", Message=email_body)
130
+ logger.info("✅ SNS notification sent successfully.")
131
+
132
+ except ClientError as e:
133
+ logger.error(f"❌ SNS Client Error: {e}")
134
+ raise
135
+
136
+ except Exception as e:
137
+ logger.error(f"❌ Unexpected error while sending SNS notification: {e}")
138
+ raise
139
+
140
+
141
+ # ==============================
142
+ # MAIN FUNCTION
143
+ # ==============================
144
+ def main() -> None:
145
+ """
146
+ Main function to find unused volumes and send notifications.
147
+ """
148
+ try:
149
+ ## ✅ Validate Inputs/Configuration
150
+ validate_sns_arn(SNS_TOPIC_ARN)
151
+
152
+ ## ✅ Find Unused Volumes
153
+ unused_volumes = find_unused_volumes()
154
+
155
+ if unused_volumes:
156
+ ## ✅ Send SNS Notification if unused volumes exist
157
+ send_sns_notification(unused_volumes)
158
+ else:
159
+ logger.info("⚠️ No unused volumes found. Exiting without notification.")
160
+
161
+ except Exception as e:
162
+ logger.error(f"❌ Fatal Error: {e}")
163
+ sys.exit(1)
164
+
165
+
166
+ # ==============================
167
+ # LAMBDA HANDLER
168
+ # ==============================
169
+ def lambda_handler(event, context):
170
+ """
171
+ AWS Lambda Handler for unused EBS volume detection and notification.
172
+
173
+ Args:
174
+ event (dict): AWS Lambda event.
175
+ context: AWS Lambda context.
176
+ """
177
+ main() # Reuse the main function for Lambda
178
+
179
+
180
+ # ==============================
181
+ # ENTRY POINT
182
+ # ==============================
183
+ if __name__ == "__main__":
184
+ main()
@@ -0,0 +1,140 @@
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, ACL="private", CreateBucketConfiguration={"LocationConstraint": region}
91
+ )
92
+
93
+ logger.info(f"✅ Bucket '{bucket_name}' created successfully at {response['Location']}.")
94
+ return response["Location"]
95
+
96
+ except ClientError as e:
97
+ logger.error(f"❌ AWS Client Error: {e}")
98
+ raise
99
+
100
+ except BotoCoreError as e:
101
+ logger.error(f"❌ BotoCore Error: {e}")
102
+ raise
103
+
104
+ except Exception as e:
105
+ logger.error(f"❌ Unexpected error: {e}")
106
+ raise
107
+
108
+
109
+ # ==============================
110
+ # MAIN FUNCTION
111
+ # ==============================
112
+ def main() -> None:
113
+ """
114
+ Main entry point for script execution.
115
+ """
116
+ try:
117
+ ## ✅ Parse Arguments or Use Environment Variables
118
+ bucket_name = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_BUCKET_NAME
119
+ region = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_REGION
120
+
121
+ ## ✅ Validate Input
122
+ validate_bucket_name(bucket_name)
123
+
124
+ ## ✅ Create S3 Bucket
125
+ create_s3_bucket(bucket_name, region)
126
+
127
+ except ValueError as e:
128
+ logger.error(f"❌ Input Validation Error: {e}")
129
+ sys.exit(1)
130
+
131
+ except Exception as e:
132
+ logger.error(f"❌ Fatal Error: {e}")
133
+ sys.exit(1)
134
+
135
+
136
+ # ==============================
137
+ # ENTRY POINT
138
+ # ==============================
139
+ if __name__ == "__main__":
140
+ main()