runbooks 0.1.7__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.
- runbooks/__init__.py +1 -1
- runbooks/aws/__init__.py +58 -0
- runbooks/aws/dynamodb_operations.py +231 -0
- runbooks/aws/ec2_copy_image_cross-region.py +195 -0
- runbooks/aws/ec2_describe_instances.py +202 -0
- runbooks/aws/ec2_ebs_snapshots_delete.py +186 -0
- runbooks/aws/ec2_run_instances.py +207 -0
- runbooks/aws/ec2_start_stop_instances.py +199 -0
- runbooks/aws/ec2_terminate_instances.py +143 -0
- runbooks/aws/ec2_unused_eips.py +196 -0
- runbooks/aws/ec2_unused_volumes.py +184 -0
- runbooks/aws/s3_create_bucket.py +140 -0
- runbooks/aws/s3_list_buckets.py +152 -0
- runbooks/aws/s3_list_objects.py +151 -0
- runbooks/aws/s3_object_operations.py +183 -0
- runbooks/aws/tagging_lambda_handler.py +172 -0
- runbooks/python101/calculator.py +34 -0
- runbooks/python101/config.py +1 -0
- runbooks/python101/exceptions.py +16 -0
- runbooks/python101/file_manager.py +218 -0
- runbooks/python101/toolkit.py +153 -0
- runbooks/security_baseline/__init__.py +0 -0
- runbooks/security_baseline/checklist/__init__.py +17 -0
- runbooks/security_baseline/checklist/account_level_bucket_public_access.py +86 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +65 -0
- runbooks/security_baseline/checklist/bucket_public_access.py +82 -0
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +66 -0
- runbooks/security_baseline/checklist/direct_attached_policy.py +69 -0
- runbooks/security_baseline/checklist/guardduty_enabled.py +71 -0
- runbooks/security_baseline/checklist/iam_password_policy.py +43 -0
- runbooks/security_baseline/checklist/iam_user_mfa.py +39 -0
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +55 -0
- runbooks/security_baseline/checklist/multi_region_trail.py +64 -0
- runbooks/security_baseline/checklist/root_access_key.py +72 -0
- runbooks/security_baseline/checklist/root_mfa.py +39 -0
- runbooks/security_baseline/checklist/root_usage.py +128 -0
- runbooks/security_baseline/checklist/trail_enabled.py +68 -0
- runbooks/security_baseline/checklist/trusted_advisor.py +24 -0
- runbooks/security_baseline/report_generator.py +149 -0
- runbooks/security_baseline/run_script.py +76 -0
- runbooks/security_baseline/security_baseline_tester.py +179 -0
- runbooks/security_baseline/utils/__init__.py +1 -0
- runbooks/security_baseline/utils/common.py +109 -0
- runbooks/security_baseline/utils/enums.py +44 -0
- runbooks/security_baseline/utils/language.py +762 -0
- runbooks/security_baseline/utils/level_const.py +5 -0
- runbooks/security_baseline/utils/permission_list.py +26 -0
- runbooks/utils/__init__.py +0 -0
- runbooks/utils/logger.py +36 -0
- {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/METADATA +2 -2
- runbooks-0.1.8.dist-info/RECORD +54 -0
- runbooks-0.1.7.dist-info/RECORD +0 -6
- {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/WHEEL +0 -0
- {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/entry_points.txt +0 -0
- {runbooks-0.1.7.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()
|