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,186 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Script to delete EBS snapshots older than the specified retention period.
5
+
6
+ Author: nnthanh101@gmail.com
7
+ Date: 2025-01-09
8
+ Version: 1.0.0
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ from datetime import datetime, timedelta, timezone
15
+ from typing import Dict, List
16
+
17
+ import boto3
18
+ from botocore.exceptions import BotoCoreError, ClientError
19
+
20
+ from runbooks.utils.logger import configure_logger
21
+
22
+ ## ✅ Configure Logger
23
+ logger = configure_logger(__name__)
24
+
25
+ # ==============================
26
+ # CONFIGURATIONS
27
+ # ==============================
28
+ ## ✅ Default Environment Variables
29
+ RETENTION_DAYS = int(os.getenv("RETENTION_DAYS", 30)) ## Default retention: 30 days
30
+ DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true" ## Dry run mode
31
+ OWNER_ID = os.getenv("OWNER_ID", "self") ## Default Owner ID (current AWS account)
32
+
33
+
34
+ # ==============================
35
+ # AWS CLIENT INITIALIZATION
36
+ # ==============================
37
+ ec2 = boto3.resource("ec2")
38
+
39
+
40
+ # ==============================
41
+ # UTILITY FUNCTIONS
42
+ # ==============================
43
+ def get_old_snapshots(retention_days: int, owner_id: str) -> List[Dict[str, str]]:
44
+ """
45
+ Retrieves EBS snapshots older than the retention period.
46
+
47
+ Args:
48
+ retention_days (int): Number of days to retain snapshots.
49
+ owner_id (str): AWS account ID for snapshot filtering.
50
+
51
+ Returns:
52
+ List[Dict[str, str]]: List of snapshots metadata.
53
+ """
54
+ try:
55
+ logger.info(f"Retrieving snapshots owned by '{owner_id}' older than {retention_days} days ...")
56
+ cutoff_time = datetime.now(tz=timezone.utc) - timedelta(days=retention_days)
57
+
58
+ ## ✅ Fetch snapshots
59
+ snapshots = ec2.snapshots.filter(OwnerIds=[owner_id])
60
+
61
+ ## ✅ Filter old snapshots
62
+ old_snapshots = [
63
+ {
64
+ "SnapshotId": snap.snapshot_id,
65
+ "StartTime": str(snap.start_time),
66
+ "State": snap.state,
67
+ "VolumeId": snap.volume_id,
68
+ "Description": snap.description,
69
+ }
70
+ for snap in snapshots
71
+ if snap.start_time < cutoff_time
72
+ ]
73
+
74
+ logger.info(f"Found {len(old_snapshots)} snapshots older than {retention_days} days.")
75
+ return old_snapshots
76
+
77
+ except (BotoCoreError, ClientError) as e:
78
+ logger.error(f"AWS Error: {e}")
79
+ raise
80
+ except Exception as e:
81
+ logger.error(f"Unexpected error: {e}")
82
+ raise
83
+
84
+
85
+ def delete_snapshots(snapshots: List[Dict[str, str]], dry_run: bool) -> None:
86
+ """
87
+ Deletes specified snapshots based on dry-run mode.
88
+
89
+ Args:
90
+ snapshots (List[Dict[str, str]]): List of snapshots to delete.
91
+ dry_run (bool): If true, no actual deletion will be performed.
92
+ """
93
+ for snap in snapshots:
94
+ try:
95
+ snapshot = ec2.Snapshot(snap["SnapshotId"])
96
+ if dry_run:
97
+ logger.info(f"[DRY-RUN] Snapshot {snap['SnapshotId']} would be deleted.")
98
+ else:
99
+ snapshot.delete()
100
+ logger.info(f"Deleted snapshot: {snap['SnapshotId']} - Created on {snap['StartTime']}")
101
+
102
+ except (BotoCoreError, ClientError) as e:
103
+ logger.error(f"Failed to delete snapshot {snap['SnapshotId']}: {e}")
104
+ except Exception as e:
105
+ logger.error(f"Unexpected error for snapshot {snap['SnapshotId']}: {e}")
106
+
107
+
108
+ def output_snapshots(snapshots: List[Dict[str, str]], format_type: str = "table") -> None:
109
+ """
110
+ Displays snapshots in either markdown table or JSON format.
111
+
112
+ Args:
113
+ snapshots (List[Dict[str, str]]): Snapshots to display.
114
+ format_type (str): Output format ('table' or 'json').
115
+ """
116
+ if format_type == "json":
117
+ print(json.dumps(snapshots, indent=4))
118
+ else:
119
+ print(
120
+ "| Snapshot ID | Volume ID | Created At | State | Description |"
121
+ )
122
+ print(
123
+ "|-------------------|------------------|-------------------------|------------|--------------------------|"
124
+ )
125
+ for snap in snapshots:
126
+ print(
127
+ f"| {snap['SnapshotId']:18} | {snap['VolumeId']:16} | {snap['StartTime']:23} "
128
+ f"| {snap['State']:10} | {snap['Description'][:25]:25} |"
129
+ )
130
+
131
+
132
+ # ==============================
133
+ # MAIN FUNCTION
134
+ # ==============================
135
+ def main():
136
+ """
137
+ Main function for CLI usage.
138
+ """
139
+ try:
140
+ ## ✅ Fetch Old Snapshots
141
+ old_snapshots = get_old_snapshots(RETENTION_DAYS, OWNER_ID)
142
+
143
+ ## ✅ Display Snapshots
144
+ output_format = sys.argv[1] if len(sys.argv) > 1 else "table"
145
+ output_snapshots(old_snapshots, format_type=output_format)
146
+
147
+ ## ✅ Delete Snapshots
148
+ delete_snapshots(old_snapshots, dry_run=DRY_RUN)
149
+
150
+ except Exception as e:
151
+ logger.error(f"Failed to execute script: {e}")
152
+ sys.exit(1)
153
+
154
+
155
+ # ==============================
156
+ # LAMBDA HANDLER
157
+ # ==============================
158
+ def lambda_handler(event, context):
159
+ """
160
+ AWS Lambda handler for deleting EBS snapshots.
161
+
162
+ Args:
163
+ event (dict): Input event data.
164
+ context: Lambda execution context.
165
+ """
166
+ try:
167
+ retention_days = int(event.get("retention_days", RETENTION_DAYS))
168
+ dry_run = event.get("dry_run", DRY_RUN)
169
+ output_format = event.get("output_format", "json")
170
+
171
+ ## ✅ Fetch and Display Snapshots
172
+ old_snapshots = get_old_snapshots(retention_days, OWNER_ID)
173
+ output_snapshots(old_snapshots, format_type=output_format)
174
+
175
+ ## ✅ Delete Snapshots
176
+ delete_snapshots(old_snapshots, dry_run=dry_run)
177
+
178
+ return {"statusCode": 200, "body": json.dumps({"deleted": len(old_snapshots)})}
179
+
180
+ except Exception as e:
181
+ logger.error(f"Lambda Error: {e}")
182
+ return {"statusCode": 500, "body": str(e)}
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ AWS EC2 Instance Launcher.
5
+
6
+ Author: nnthanh101@gmail.com
7
+ Date: 2025-01-07
8
+ Version: 1.0.0
9
+
10
+ Description:
11
+ - Launches EC2 instances in a VPC with specified configurations using AWS Boto3.
12
+ - Works with both Python runtime and AWS Lambda.
13
+ - Supports environment-based configuration for scalability.
14
+
15
+ IAM Role Permissions:
16
+ - ec2:RunInstances
17
+ - ec2:CreateTags
18
+ - ec2:DescribeSecurityGroups
19
+ - ec2:DescribeInstances
20
+ """
21
+
22
+ import json
23
+ import os
24
+ from typing import Dict, List
25
+
26
+ import boto3
27
+ from botocore.exceptions import BotoCoreError, ClientError
28
+
29
+ from runbooks.utils.logger import configure_logger
30
+
31
+ ## ✅ Configure Logger
32
+ logger = configure_logger(__name__)
33
+
34
+ # ==============================
35
+ # CONFIGURATION
36
+ # ==============================
37
+ DEFAULT_AMI_ID = os.getenv(
38
+ "AMI_ID", "ami-03f052ebc3f436d52"
39
+ ) ## Default Red Hat Enterprise Linux 9 (HVM), SSD Volume Type
40
+ DEFAULT_INSTANCE_TYPE = os.getenv("INSTANCE_TYPE", "t2.micro") ## Default instance type
41
+ DEFAULT_MIN_COUNT = int(os.getenv("MIN_COUNT", "1")) ## Min EC2 instances
42
+ DEFAULT_MAX_COUNT = int(os.getenv("MAX_COUNT", "1")) ## Max EC2 instances
43
+ KEY_NAME = os.getenv("KEY_NAME", "EC2Test") ## SSH Key Pair Name: my-key = EC2Test
44
+ ## VPC Security Group IDs
45
+ # SECURITY_GROUPS = os.getenv('SECURITY_GROUPS', 'default,vpc_endpoint_security_group').split(',')
46
+ SECURITY_GROUP_IDS = os.getenv("SECURITY_GROUP_IDS", "sg-0b0ee7b0b75210174,sg-0b056d8059a91607d").split(",")
47
+ SUBNET_ID = os.getenv("SUBNET_ID", "subnet-094569c6e3ccaa04d") ## Required Subnet-ID for VPC-based deployment
48
+ TAGS = os.getenv("TAGS", '{"Project":"CloudOps", "Environment":"Dev"}') ## Default tags
49
+
50
+ ## ✅ Block Device Mappings Configuration
51
+ OS_BlockDeviceMappings = [
52
+ {
53
+ "DeviceName": "/dev/xvda", ## Root volume device
54
+ "Ebs": {
55
+ "DeleteOnTermination": True, ## Clean up after instance termination
56
+ "VolumeSize": 20, ## Set volume size in GB
57
+ "VolumeType": "gp3", ## Modern, faster storage
58
+ "Encrypted": True, ## Encrypt the EBS volume
59
+ },
60
+ },
61
+ ]
62
+ OS_Monitoring = {"Enabled": False}
63
+
64
+
65
+ # ==============================
66
+ # AWS CLIENT INITIALIZATION
67
+ # ==============================
68
+ def get_ec2_client():
69
+ """
70
+ Initializes AWS EC2 client.
71
+ """
72
+ return boto3.client("ec2")
73
+
74
+
75
+ # ==============================
76
+ # EC2 INSTANCE LAUNCH FUNCTION
77
+ # ==============================
78
+ def launch_ec2_instances(
79
+ ec2_client,
80
+ ami_id: str,
81
+ instance_type: str,
82
+ min_count: int,
83
+ max_count: int,
84
+ key_name: str,
85
+ subnet_id: str,
86
+ security_group_ids: List[str],
87
+ tags: Dict[str, str] = None,
88
+ ) -> List[str]:
89
+ """
90
+ Launches EC2 instances and applies tags.
91
+
92
+ Args:
93
+ ec2_client: EC2 boto3 client.
94
+ ami_id (str): AMI ID for the instance.
95
+ instance_type (str): EC2 instance type.
96
+ min_count (int): Minimum number of instances.
97
+ max_count (int): Maximum number of instances.
98
+ key_name (str): SSH key pair name.
99
+ subnet_id (str): Subnet ID for launching in a VPC.
100
+ security_group_ids (List[str]): Security group IDs for VPC.
101
+ tags (Dict[str, str]): Tags to apply to instances.
102
+
103
+ Returns:
104
+ List[str]: List of launched instance IDs.
105
+ """
106
+ try:
107
+ logger.info("Validates required environment variables for launching EC2 instances.")
108
+ if not SUBNET_ID:
109
+ raise ValueError("❌ Missing required SUBNET_ID environment variable.")
110
+ if not SECURITY_GROUP_IDS or SECURITY_GROUP_IDS == [""]:
111
+ raise ValueError("❌ Missing required SECURITY_GROUP_IDS environment variable.")
112
+ logger.info("✅ Environment variables validated successfully.")
113
+
114
+ logger.info(f"Launching {min_count}-{max_count} instances of type {instance_type} with AMI {ami_id}...")
115
+
116
+ ## ✅ Construct parameters
117
+ params = {
118
+ "BlockDeviceMappings": OS_BlockDeviceMappings,
119
+ "ImageId": ami_id,
120
+ "InstanceType": instance_type,
121
+ "MinCount": min_count,
122
+ "MaxCount": max_count,
123
+ "Monitoring": OS_Monitoring,
124
+ "KeyName": key_name,
125
+ "SubnetId": subnet_id, ## VPC subnet
126
+ "SecurityGroupIds": security_group_ids, ## VPC Security Group IDs
127
+ }
128
+
129
+ ## ✅ Launch Instances
130
+ response = ec2_client.run_instances(**params)
131
+
132
+ ## ✅ Extract Instance IDs
133
+ instance_ids = [instance["InstanceId"] for instance in response["Instances"]]
134
+ logger.info(f"Launched Instances: {instance_ids}")
135
+
136
+ ## ✅ Apply Tags
137
+ if tags:
138
+ ec2_client.create_tags(Resources=instance_ids, Tags=[{"Key": k, "Value": v} for k, v in tags.items()])
139
+ logger.info(f"Applied tags: {tags}")
140
+
141
+ return instance_ids
142
+
143
+ except ClientError as e:
144
+ logger.error(f"AWS Client Error: {e}")
145
+ raise
146
+ except BotoCoreError as e:
147
+ logger.error(f"BotoCore Error: {e}")
148
+ raise
149
+ except Exception as e:
150
+ logger.error(f"Unexpected error: {e}")
151
+ raise
152
+
153
+
154
+ # ==============================
155
+ # MAIN HANDLER
156
+ # ==============================
157
+ def lambda_handler(event, context):
158
+ """
159
+ AWS Lambda Handler for launching EC2 instances.
160
+
161
+ Args:
162
+ event (dict): AWS event data.
163
+ context: AWS Lambda context.
164
+ """
165
+ try:
166
+ ## ✅ Initialize EC2 Client
167
+ ec2_client = get_ec2_client()
168
+
169
+ ## Parse tags from environment variable
170
+ tags = json.loads(TAGS)
171
+ ## ✅ Launch EC2 Instances
172
+ instance_ids = launch_ec2_instances(
173
+ ec2_client=ec2_client,
174
+ ami_id=DEFAULT_AMI_ID,
175
+ instance_type=DEFAULT_INSTANCE_TYPE,
176
+ min_count=DEFAULT_MIN_COUNT,
177
+ max_count=DEFAULT_MAX_COUNT,
178
+ key_name=KEY_NAME,
179
+ subnet_id=SUBNET_ID,
180
+ security_group_ids=SECURITY_GROUP_IDS,
181
+ tags=tags,
182
+ )
183
+
184
+ ## ✅ Return Success Response
185
+ return {"statusCode": 200, "body": json.dumps({"message": "Instances launched", "InstanceIDs": instance_ids})}
186
+ except Exception as e:
187
+ logger.error(f"Lambda Handler Error: {e}")
188
+ return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
189
+
190
+
191
+ if __name__ == "__main__":
192
+ # ✅ CLI Execution for Python Runtime
193
+ ec2_client = get_ec2_client()
194
+ tags = json.loads(TAGS)
195
+
196
+ instance_ids = launch_ec2_instances(
197
+ ec2_client=ec2_client,
198
+ ami_id=DEFAULT_AMI_ID,
199
+ instance_type=DEFAULT_INSTANCE_TYPE,
200
+ min_count=DEFAULT_MIN_COUNT,
201
+ max_count=DEFAULT_MAX_COUNT,
202
+ key_name=KEY_NAME,
203
+ subnet_id=SUBNET_ID,
204
+ security_group_ids=SECURITY_GROUP_IDS,
205
+ tags=tags,
206
+ )
207
+ print(f"Launched Instances: {instance_ids}")
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ EC2 Instance Scheduler for Start/Stop Based on Tags.
5
+
6
+ Author: nnthanh101@gmail.com
7
+ Date: 2025-01-07
8
+ Version: 1.0.0
9
+
10
+ Description:
11
+ - Start or Stop EC2 instances tagged with "AutoStart" = "True".
12
+ - Works in both AWS Lambda and Python environments.
13
+
14
+ Requirements:
15
+ - IAM Permissions:
16
+ * ec2:DescribeInstances
17
+ * ec2:StartInstances
18
+ * ec2:StopInstances
19
+
20
+ Environment Variables (Optional):
21
+ - LOG_LEVEL: Logging verbosity (default: INFO)
22
+
23
+ Usage (Python CLI):
24
+ python ec2_instance_scheduler.py --action=start
25
+
26
+ Usage (Lambda):
27
+ Trigger event with: {"action": "start"} or {"action": "stop"}
28
+ """
29
+
30
+ import argparse ## For CLI mode support
31
+ import json
32
+ import os
33
+ import sys
34
+ from typing import Dict, List
35
+
36
+ import boto3
37
+ from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError, PartialCredentialsError
38
+
39
+ from runbooks.utils.logger import configure_logger
40
+
41
+ ## ✅ Configure Logger
42
+ logger = configure_logger(__name__)
43
+
44
+
45
+ # ==============================
46
+ # AWS CLIENT INITIALIZATION
47
+ # ==============================
48
+ def get_ec2_client():
49
+ """
50
+ Initializes the EC2 client.
51
+
52
+ Returns:
53
+ boto3.client: EC2 client.
54
+ """
55
+ try:
56
+ return boto3.client("ec2")
57
+ except (NoCredentialsError, PartialCredentialsError) as e:
58
+ logger.error(f"AWS credentials not found or incomplete: {e}")
59
+ sys.exit(1)
60
+
61
+
62
+ # ==============================
63
+ # INSTANCE OPERATIONS
64
+ # ==============================
65
+ def fetch_instances(client, tag_key: str, tag_value: str) -> List[str]:
66
+ """
67
+ Fetches instance IDs based on tags.
68
+
69
+ Args:
70
+ client (boto3.client): EC2 client.
71
+ tag_key (str): Tag key to filter instances.
72
+ tag_value (str): Tag value to filter instances.
73
+
74
+ Returns:
75
+ List[str]: List of instance IDs.
76
+ """
77
+ try:
78
+ logger.info("Fetching instances with tag: %s=%s", tag_key, tag_value)
79
+
80
+ ## Filter instances based on the tag
81
+ response = client.describe_instances(
82
+ # Filters=[{'Name': f"tag:AutoStart", 'Values': ['True']}]
83
+ Filters=[{"Name": f"tag:{tag_key}", "Values": [tag_value]}]
84
+ )
85
+
86
+ ## Extract list of Instance IDs
87
+ instance_ids = [
88
+ instance["InstanceId"] for reservation in response["Reservations"] for instance in reservation["Instances"]
89
+ ]
90
+
91
+ if not instance_ids:
92
+ logger.warning("No matching instances found.")
93
+ else:
94
+ logger.info("Found instances: %s", instance_ids)
95
+
96
+ return instance_ids
97
+
98
+ except ClientError as e:
99
+ logger.error(f"AWS Client Error: {e}")
100
+ raise
101
+ except BotoCoreError as e:
102
+ logger.error(f"BotoCore Error: {e}")
103
+ raise
104
+
105
+
106
+ def perform_action(client, instance_ids: List[str], action: str) -> None:
107
+ """
108
+ Performs the specified action (start/stop) on the instances.
109
+
110
+ Args:
111
+ client (boto3.client): EC2 client.
112
+ instance_ids (List[str]): List of instance IDs.
113
+ action (str): The action to perform ("start" or "stop").
114
+ """
115
+ if not instance_ids:
116
+ logger.warning("No instances to process for action: %s", action)
117
+ return
118
+
119
+ try:
120
+ if action == "start":
121
+ logger.info("Starting instances: %s", instance_ids)
122
+ response = client.start_instances(InstanceIds=instance_ids)
123
+ elif action == "stop":
124
+ logger.info("Stopping instances: %s", instance_ids)
125
+ response = client.stop_instances(InstanceIds=instance_ids)
126
+ else:
127
+ raise ValueError(f"Invalid action: {action}")
128
+
129
+ logger.info("Action '%s' completed successfully.", action)
130
+ logger.debug("Response: %s", response)
131
+
132
+ except ClientError as e:
133
+ logger.error(f"AWS Client Error during '{action}': {e}")
134
+ raise
135
+ except BotoCoreError as e:
136
+ logger.error(f"BotoCore Error during '{action}': {e}")
137
+ raise
138
+
139
+
140
+ # ==============================
141
+ # MAIN HANDLER
142
+ # ==============================
143
+ def lambda_handler(event, context):
144
+ """
145
+ AWS Lambda handler for EC2 start/stop scheduler.
146
+
147
+ Args:
148
+ event (dict): AWS event data.
149
+ context: AWS Lambda context.
150
+ """
151
+ try:
152
+ ## ✅ Parse Action from Event
153
+ action = event.get("action")
154
+ if action not in ["start", "stop"]:
155
+ raise ValueError("Invalid action. Supported actions: 'start' or 'stop'.")
156
+
157
+ ## ✅ Initialize AWS Client
158
+ ec2_client = get_ec2_client()
159
+
160
+ # ✅ Fetch Instances and Perform Action
161
+ instance_ids = fetch_instances(ec2_client, tag_key="AutoStart", tag_value="True")
162
+ perform_action(ec2_client, instance_ids, action)
163
+
164
+ return {"statusCode": 200, "body": json.dumps(f"Action '{action}' completed successfully.")}
165
+
166
+ except Exception as e:
167
+ logger.error(f"Error: {e}")
168
+ return {"statusCode": 500, "body": json.dumps(f"Error: {str(e)}")}
169
+
170
+
171
+ def main():
172
+ """
173
+ CLI Entry Point for Python Usage.
174
+ """
175
+ parser = argparse.ArgumentParser(description="EC2 Scheduler Script")
176
+ parser.add_argument("--action", choices=["start", "stop"], required=True, help="Action to perform (start/stop).")
177
+ args = parser.parse_args()
178
+
179
+ try:
180
+ ## ✅ CLI Execution
181
+ action = args.action
182
+ ec2_client = get_ec2_client()
183
+ instance_ids = fetch_instances(ec2_client, tag_key="AutoStart", tag_value="True")
184
+ perform_action(ec2_client, instance_ids, action)
185
+ logger.info(f"Action '{action}' completed successfully.")
186
+ except Exception as e:
187
+ logger.error(f"Failed to execute action: {e}")
188
+ sys.exit(1)
189
+
190
+
191
+ # ==============================
192
+ # SCRIPT ENTRY POINT
193
+ # ==============================
194
+ if __name__ == "__main__":
195
+ # Detect environment
196
+ if "AWS_LAMBDA_FUNCTION_NAME" in os.environ:
197
+ lambda_handler({}, None) # Placeholder event/context for Lambda testing
198
+ else:
199
+ main()