runbooks 0.1.7__py3-none-any.whl → 0.1.9__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 +18 -0
- runbooks/security_baseline/checklist/account_level_bucket_public_access.py +87 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +66 -0
- runbooks/security_baseline/checklist/bucket_public_access.py +83 -0
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +67 -0
- runbooks/security_baseline/checklist/direct_attached_policy.py +70 -0
- runbooks/security_baseline/checklist/guardduty_enabled.py +72 -0
- runbooks/security_baseline/checklist/iam_password_policy.py +44 -0
- runbooks/security_baseline/checklist/iam_user_mfa.py +39 -0
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +56 -0
- runbooks/security_baseline/checklist/multi_region_trail.py +65 -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 +69 -0
- runbooks/security_baseline/checklist/trusted_advisor.py +24 -0
- runbooks/security_baseline/report_generator.py +150 -0
- runbooks/security_baseline/run_script.py +76 -0
- runbooks/security_baseline/security_baseline_tester.py +184 -0
- runbooks/security_baseline/utils/__init__.py +2 -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.9.dist-info}/METADATA +2 -2
- runbooks-0.1.9.dist-info/RECORD +54 -0
- runbooks-0.1.7.dist-info/RECORD +0 -6
- {runbooks-0.1.7.dist-info → runbooks-0.1.9.dist-info}/WHEEL +0 -0
- {runbooks-0.1.7.dist-info → runbooks-0.1.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.1.7.dist-info → runbooks-0.1.9.dist-info}/top_level.txt +0 -0
runbooks/__init__.py
CHANGED
runbooks/aws/__init__.py
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
## src/runbooks/aws/__init__.py
|
2
|
+
"""AWS Runbooks Initialization Module."""
|
3
|
+
|
4
|
+
import importlib
|
5
|
+
import os
|
6
|
+
import sys
|
7
|
+
|
8
|
+
from runbooks.utils.logger import configure_logger
|
9
|
+
|
10
|
+
logger = configure_logger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
def discover_scripts():
|
14
|
+
"""
|
15
|
+
Dynamically discovers and lists all AWS scripts in this package.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
dict: A mapping of script names to their main functions.
|
19
|
+
"""
|
20
|
+
scripts = {}
|
21
|
+
aws_path = os.path.dirname(__file__)
|
22
|
+
for filename in os.listdir(aws_path):
|
23
|
+
if filename.endswith(".py") and filename != "__init__.py":
|
24
|
+
module_name = f"runbooks.aws.{filename[:-3]}"
|
25
|
+
try:
|
26
|
+
module = importlib.import_module(module_name)
|
27
|
+
if hasattr(module, "main"):
|
28
|
+
scripts[filename[:-3]] = module.main
|
29
|
+
except Exception as e:
|
30
|
+
logger.error(f"Error importing {module_name}: {e}")
|
31
|
+
return scripts
|
32
|
+
|
33
|
+
|
34
|
+
def run_script(script_name, *args):
|
35
|
+
"""
|
36
|
+
Executes the given script by name.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
script_name (str): The name of the script to execute.
|
40
|
+
*args: Additional arguments to pass to the script.
|
41
|
+
"""
|
42
|
+
scripts = discover_scripts()
|
43
|
+
if script_name in scripts:
|
44
|
+
try:
|
45
|
+
scripts[script_name](*args)
|
46
|
+
except Exception as e:
|
47
|
+
logger.error(f"Error executing script {script_name}: {e}")
|
48
|
+
else:
|
49
|
+
logger.error(f"Script {script_name} not found.")
|
50
|
+
sys.exit(1)
|
51
|
+
|
52
|
+
|
53
|
+
if __name__ == "__main__":
|
54
|
+
if len(sys.argv) < 2:
|
55
|
+
logger.error("Usage: python -m runbooks.aws <script_name> [<args>]")
|
56
|
+
sys.exit(1)
|
57
|
+
|
58
|
+
run_script(sys.argv[1], *sys.argv[2:])
|
@@ -0,0 +1,231 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
DynamoDB Operations: Put Item, Delete Item, and Batch Write.
|
5
|
+
|
6
|
+
This script supports the following functionalities:
|
7
|
+
1. Insert or update a single item (Put Item).
|
8
|
+
2. Retrieve and delete a single item (Delete Item).
|
9
|
+
3. Batch insert multiple items efficiently (Batch Write).
|
10
|
+
|
11
|
+
Designed for usage in Python, Docker, and AWS Lambda environments.
|
12
|
+
|
13
|
+
Author: nnthanh101@gmail.com
|
14
|
+
Date: 2025-01-09
|
15
|
+
Version: 1.0.0
|
16
|
+
"""
|
17
|
+
|
18
|
+
import json
|
19
|
+
import os
|
20
|
+
from typing import Dict, List
|
21
|
+
|
22
|
+
import boto3
|
23
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
24
|
+
|
25
|
+
from runbooks.utils.logger import configure_logger
|
26
|
+
|
27
|
+
## ✅ Configure Logger
|
28
|
+
logger = configure_logger(__name__)
|
29
|
+
|
30
|
+
# ==============================
|
31
|
+
# CONFIGURATION VARIABLES
|
32
|
+
# ==============================
|
33
|
+
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
|
34
|
+
TABLE_NAME = os.getenv("TABLE_NAME", "employees")
|
35
|
+
MAX_BATCH_ITEMS = int(os.getenv("MAX_BATCH_ITEMS", 100))
|
36
|
+
|
37
|
+
|
38
|
+
# ==============================
|
39
|
+
# AWS CLIENT INITIALIZATION
|
40
|
+
# ==============================
|
41
|
+
try:
|
42
|
+
dynamodb = boto3.resource("dynamodb", region_name=AWS_REGION)
|
43
|
+
table = dynamodb.Table(TABLE_NAME)
|
44
|
+
logger.info(f"✅ DynamoDB Table '{TABLE_NAME}' initialized successfully.")
|
45
|
+
except Exception as e:
|
46
|
+
logger.error(f"❌ Failed to initialize DynamoDB table: {e}")
|
47
|
+
raise
|
48
|
+
|
49
|
+
|
50
|
+
# ==============================
|
51
|
+
# FUNCTION: PUT ITEM
|
52
|
+
# ==============================
|
53
|
+
def put_item(emp_id: str, name: str, salary: int) -> None:
|
54
|
+
"""
|
55
|
+
Inserts or updates a single item in DynamoDB.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
emp_id (str): Employee ID.
|
59
|
+
name (str): Employee name.
|
60
|
+
salary (int): Employee salary.
|
61
|
+
|
62
|
+
Raises:
|
63
|
+
Exception: If item insertion fails.
|
64
|
+
"""
|
65
|
+
try:
|
66
|
+
logger.info(f"🚀 Inserting/Updating item in table '{TABLE_NAME}'...")
|
67
|
+
table.put_item(Item={"emp_id": emp_id, "name": name, "salary": salary})
|
68
|
+
logger.info(f"✅ Item added successfully: emp_id={emp_id}, name={name}, salary={salary}")
|
69
|
+
|
70
|
+
except ClientError as e:
|
71
|
+
logger.error(f"❌ AWS Client Error: {e}")
|
72
|
+
raise
|
73
|
+
|
74
|
+
except Exception as e:
|
75
|
+
logger.error(f"❌ Unexpected Error: {e}")
|
76
|
+
raise
|
77
|
+
|
78
|
+
|
79
|
+
# ==============================
|
80
|
+
# FUNCTION: DELETE ITEM
|
81
|
+
# ==============================
|
82
|
+
def delete_item(emp_id: str) -> Dict:
|
83
|
+
"""
|
84
|
+
Retrieves and deletes a single item from DynamoDB.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
emp_id (str): Employee ID.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
Dict: Deleted item details.
|
91
|
+
|
92
|
+
Raises:
|
93
|
+
Exception: If retrieval or deletion fails.
|
94
|
+
"""
|
95
|
+
try:
|
96
|
+
## ✅ 1. Retrieve the item
|
97
|
+
logger.info(f"🔍 Retrieving item with emp_id={emp_id}...")
|
98
|
+
response = table.get_item(Key={"emp_id": emp_id})
|
99
|
+
|
100
|
+
if "Item" not in response:
|
101
|
+
raise ValueError(f"Item with emp_id={emp_id} not found.")
|
102
|
+
item = response["Item"]
|
103
|
+
logger.info(f"✅ Item retrieved: {item}")
|
104
|
+
|
105
|
+
## ✅ 2. Delete the item
|
106
|
+
logger.info(f"🗑️ Deleting item with emp_id={emp_id}...")
|
107
|
+
table.delete_item(Key={"emp_id": emp_id})
|
108
|
+
logger.info(f"✅ Item deleted successfully: emp_id={emp_id}")
|
109
|
+
|
110
|
+
return item
|
111
|
+
|
112
|
+
except ClientError as e:
|
113
|
+
logger.error(f"❌ AWS Client Error: {e}")
|
114
|
+
raise
|
115
|
+
|
116
|
+
except BotoCoreError as e:
|
117
|
+
logger.error(f"❌ BotoCore Error: {e}")
|
118
|
+
raise
|
119
|
+
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(f"❌ Unexpected Error: {e}")
|
122
|
+
raise
|
123
|
+
|
124
|
+
|
125
|
+
# ==============================
|
126
|
+
# FUNCTION: BATCH WRITE ITEMS
|
127
|
+
# ==============================
|
128
|
+
def batch_write_items(batch_size: int = MAX_BATCH_ITEMS) -> None:
|
129
|
+
"""
|
130
|
+
Inserts multiple items into DynamoDB using batch writer.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
batch_size (int): Number of items to write in a batch.
|
134
|
+
|
135
|
+
Raises:
|
136
|
+
Exception: If batch write fails.
|
137
|
+
"""
|
138
|
+
try:
|
139
|
+
logger.info(f"🚀 Starting batch write with {batch_size} items...")
|
140
|
+
with table.batch_writer() as batch:
|
141
|
+
for i in range(batch_size):
|
142
|
+
batch.put_item(
|
143
|
+
Item={
|
144
|
+
"emp_id": str(i),
|
145
|
+
"name": f"Name-{i}",
|
146
|
+
"salary": 50000 + i * 100, ## Incremental salary
|
147
|
+
}
|
148
|
+
)
|
149
|
+
logger.info(f"✅ Batch write completed successfully with {batch_size} items.")
|
150
|
+
|
151
|
+
except ClientError as e:
|
152
|
+
logger.error(f"❌ AWS Client Error: {e}")
|
153
|
+
raise
|
154
|
+
|
155
|
+
except BotoCoreError as e:
|
156
|
+
logger.error(f"❌ BotoCore Error: {e}")
|
157
|
+
raise
|
158
|
+
|
159
|
+
except Exception as e:
|
160
|
+
logger.error(f"❌ Unexpected Error: {e}")
|
161
|
+
raise
|
162
|
+
|
163
|
+
|
164
|
+
# ==============================
|
165
|
+
# MAIN FUNCTION (CLI/DOCKER)
|
166
|
+
# ==============================
|
167
|
+
def main():
|
168
|
+
"""
|
169
|
+
Main function for CLI/Docker execution.
|
170
|
+
"""
|
171
|
+
try:
|
172
|
+
## Use-Case 1: Put Item
|
173
|
+
put_item(emp_id="2", name="John Doe", salary=75000)
|
174
|
+
|
175
|
+
## Use-Case 2: Delete Item
|
176
|
+
delete_item(emp_id="2")
|
177
|
+
|
178
|
+
## Use-Case 3: Batch Write Items
|
179
|
+
batch_write_items(batch_size=MAX_BATCH_ITEMS)
|
180
|
+
|
181
|
+
except Exception as e:
|
182
|
+
logger.error(f"❌ Error in main execution: {e}")
|
183
|
+
raise
|
184
|
+
|
185
|
+
|
186
|
+
# ==============================
|
187
|
+
# AWS LAMBDA HANDLER
|
188
|
+
# ==============================
|
189
|
+
def lambda_handler(event, context):
|
190
|
+
"""
|
191
|
+
AWS Lambda handler for DynamoDB operations.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
event (dict): AWS Lambda event with action details.
|
195
|
+
context: AWS Lambda context object.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
dict: Status code and message.
|
199
|
+
"""
|
200
|
+
try:
|
201
|
+
action = event.get("action")
|
202
|
+
emp_id = event.get("emp_id")
|
203
|
+
name = event.get("name")
|
204
|
+
salary = event.get("salary", 0)
|
205
|
+
batch_size = int(event.get("batch_size", MAX_BATCH_ITEMS))
|
206
|
+
|
207
|
+
if action == "put":
|
208
|
+
put_item(emp_id, name, salary)
|
209
|
+
return {"statusCode": 200, "body": f"Item {emp_id} inserted."}
|
210
|
+
|
211
|
+
elif action == "delete":
|
212
|
+
item = delete_item(emp_id)
|
213
|
+
return {"statusCode": 200, "body": f"Item {item} deleted."}
|
214
|
+
|
215
|
+
elif action == "batch_write":
|
216
|
+
batch_write_items(batch_size)
|
217
|
+
return {"statusCode": 200, "body": "Batch write completed."}
|
218
|
+
|
219
|
+
else:
|
220
|
+
raise ValueError("Invalid action. Use 'put', 'delete', or 'batch_write'.")
|
221
|
+
|
222
|
+
except Exception as e:
|
223
|
+
logger.error(f"❌ Lambda Error: {e}")
|
224
|
+
return {"statusCode": 500, "body": str(e)}
|
225
|
+
|
226
|
+
|
227
|
+
# ==============================
|
228
|
+
# SCRIPT ENTRY POINT
|
229
|
+
# ==============================
|
230
|
+
if __name__ == "__main__":
|
231
|
+
main()
|
@@ -0,0 +1,195 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
EC2 Image Creation and Cross-Region Copy Script.
|
4
|
+
|
5
|
+
Author: nnthanh101@gmail.com
|
6
|
+
Date: 2025-01-08
|
7
|
+
Version: 2.0.0
|
8
|
+
"""
|
9
|
+
|
10
|
+
import json
|
11
|
+
import logging
|
12
|
+
import os
|
13
|
+
from typing import List
|
14
|
+
|
15
|
+
import boto3
|
16
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
17
|
+
|
18
|
+
# ==============================
|
19
|
+
# CONFIGURATIONS
|
20
|
+
# ==============================
|
21
|
+
SOURCE_REGION = os.getenv("SOURCE_REGION", "ap-southeast-2") ## Source AWS region
|
22
|
+
DEST_REGION = os.getenv("DEST_REGION", "us-east-1") ## Destination AWS region
|
23
|
+
INSTANCE_IDS = os.getenv("INSTANCE_IDS", "i-0067eeaab6c8188fd").split(",") ## Comma-separated instance IDs
|
24
|
+
IMAGE_NAME_PREFIX = os.getenv("IMAGE_NAME_PREFIX", "Demo-Boto") ## Image name prefix
|
25
|
+
DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true" ## Dry-run mode
|
26
|
+
|
27
|
+
# ==============================
|
28
|
+
# LOGGING CONFIGURATION
|
29
|
+
# ==============================
|
30
|
+
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
# ==============================
|
34
|
+
# AWS CLIENT INITIALIZATION
|
35
|
+
# ==============================
|
36
|
+
source_ec2 = boto3.resource("ec2", region_name=SOURCE_REGION)
|
37
|
+
source_client = boto3.client("ec2", region_name=SOURCE_REGION)
|
38
|
+
dest_client = boto3.client("ec2", region_name=DEST_REGION)
|
39
|
+
|
40
|
+
|
41
|
+
# ==============================
|
42
|
+
# VALIDATION UTILITIES
|
43
|
+
# ==============================
|
44
|
+
def validate_regions(source_region: str, dest_region: str) -> None:
|
45
|
+
"""
|
46
|
+
Validates AWS regions.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
source_region (str): Source AWS region.
|
50
|
+
dest_region (str): Destination AWS region.
|
51
|
+
|
52
|
+
Raises:
|
53
|
+
ValueError: If regions are invalid.
|
54
|
+
"""
|
55
|
+
session = boto3.session.Session()
|
56
|
+
valid_regions = session.get_available_regions("ec2")
|
57
|
+
|
58
|
+
if source_region not in valid_regions:
|
59
|
+
raise ValueError(f"Invalid source region: {source_region}")
|
60
|
+
if dest_region not in valid_regions:
|
61
|
+
raise ValueError(f"Invalid destination region: {dest_region}")
|
62
|
+
logger.info(f"Validated AWS regions: {source_region} -> {dest_region}")
|
63
|
+
|
64
|
+
|
65
|
+
# ==============================
|
66
|
+
# CREATE IMAGES
|
67
|
+
# ==============================
|
68
|
+
def create_images(instance_ids: List[str]) -> List[str]:
|
69
|
+
"""
|
70
|
+
Creates AMI images for specified instances.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
instance_ids (List[str]): List of EC2 instance IDs.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
List[str]: List of created image IDs.
|
77
|
+
"""
|
78
|
+
image_ids = []
|
79
|
+
for instance_id in instance_ids:
|
80
|
+
try:
|
81
|
+
instance = source_ec2.Instance(instance_id)
|
82
|
+
image_name = f"{IMAGE_NAME_PREFIX}-{instance_id}"
|
83
|
+
logger.info(f"Creating image for instance {instance_id} with name '{image_name}' ...")
|
84
|
+
|
85
|
+
if DRY_RUN:
|
86
|
+
logger.info(f"[DRY-RUN] Image creation for {instance_id} skipped.")
|
87
|
+
else:
|
88
|
+
image = instance.create_image(Name=image_name, Description=f"Image for {instance_id}")
|
89
|
+
image_ids.append(image.id)
|
90
|
+
logger.info(f"Created image: {image.id}")
|
91
|
+
|
92
|
+
except ClientError as e:
|
93
|
+
logger.error(f"Failed to create image for instance {instance_id}: {e}")
|
94
|
+
continue
|
95
|
+
|
96
|
+
return image_ids
|
97
|
+
|
98
|
+
|
99
|
+
# ==============================
|
100
|
+
# WAIT FOR IMAGES
|
101
|
+
# ==============================
|
102
|
+
def wait_for_images(image_ids: List[str]) -> None:
|
103
|
+
"""
|
104
|
+
Waits until the AMIs images to be available.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
image_ids (List[str]): List of image IDs to monitor.
|
108
|
+
"""
|
109
|
+
try:
|
110
|
+
logger.info("Waiting for images to be available...")
|
111
|
+
## Get waiter for image_available
|
112
|
+
waiter = source_client.get_waiter("image_available")
|
113
|
+
waiter.wait(Filters=[{"Name": "image-id", "Values": image_ids}])
|
114
|
+
logger.info("All Images are now available.")
|
115
|
+
except ClientError as e:
|
116
|
+
logger.error(f"Error waiting for AMIs: {e}")
|
117
|
+
raise
|
118
|
+
|
119
|
+
|
120
|
+
# ==============================
|
121
|
+
# COPY IMAGES TO DESTINATION
|
122
|
+
# ==============================
|
123
|
+
def copy_images(image_ids: List[str]) -> None:
|
124
|
+
"""
|
125
|
+
Copies AMIs Images to the destination region.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
image_ids (List[str]): List of source image IDs.
|
129
|
+
"""
|
130
|
+
for image_id in image_ids:
|
131
|
+
try:
|
132
|
+
copy_name = f"{IMAGE_NAME_PREFIX}-Copy-{image_id}"
|
133
|
+
logger.info(f"Copying image {image_id} to {DEST_REGION} with name '{copy_name}' ...")
|
134
|
+
|
135
|
+
if DRY_RUN:
|
136
|
+
logger.info(f"[DRY-RUN] Image copy for {image_id} skipped.")
|
137
|
+
else:
|
138
|
+
dest_client.copy_image(
|
139
|
+
Name=copy_name,
|
140
|
+
SourceImageId=image_id,
|
141
|
+
SourceRegion=SOURCE_REGION,
|
142
|
+
Description=f"Copy of {image_id} from {SOURCE_REGION}",
|
143
|
+
)
|
144
|
+
logger.info(f"Image {image_id} copied successfully.")
|
145
|
+
except ClientError as e:
|
146
|
+
logger.error(f"Failed to copy image {image_id}: {e}")
|
147
|
+
continue
|
148
|
+
|
149
|
+
|
150
|
+
# ==============================
|
151
|
+
# MAIN FUNCTION
|
152
|
+
# ==============================
|
153
|
+
def main():
|
154
|
+
"""
|
155
|
+
CLI Entry Point.
|
156
|
+
"""
|
157
|
+
try:
|
158
|
+
## ✅ Validate regions
|
159
|
+
validate_regions(SOURCE_REGION, DEST_REGION)
|
160
|
+
|
161
|
+
## ✅ Part 1. Create AMI Images
|
162
|
+
image_ids = create_images(INSTANCE_IDS)
|
163
|
+
if not image_ids:
|
164
|
+
logger.warning("No images created. Exiting.")
|
165
|
+
return
|
166
|
+
|
167
|
+
## ✅ Part 2. Wait for AMI Images to be available
|
168
|
+
wait_for_images(image_ids)
|
169
|
+
|
170
|
+
## ✅ Part 3. Copy images to destination region
|
171
|
+
copy_images(image_ids)
|
172
|
+
|
173
|
+
except Exception as e:
|
174
|
+
logger.error(f"Unexpected error: {e}")
|
175
|
+
exit(1)
|
176
|
+
|
177
|
+
|
178
|
+
# ==============================
|
179
|
+
# LAMBDA HANDLER
|
180
|
+
# ==============================
|
181
|
+
def lambda_handler(event, context):
|
182
|
+
"""
|
183
|
+
AWS Lambda Entry Point.
|
184
|
+
"""
|
185
|
+
try:
|
186
|
+
main()
|
187
|
+
return {"statusCode": 200, "body": "Process completed successfully."}
|
188
|
+
|
189
|
+
except Exception as e:
|
190
|
+
logger.error(f"Lambda Error: {e}")
|
191
|
+
return {"statusCode": 500, "body": str(e)}
|
192
|
+
|
193
|
+
|
194
|
+
if __name__ == "__main__":
|
195
|
+
main()
|
@@ -0,0 +1,202 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
AWS EC2 Describe Instances Tool.
|
4
|
+
|
5
|
+
Lists EC2 instances based on optional filters. Supports Python CLI, Docker, and AWS Lambda.
|
6
|
+
|
7
|
+
Author: nnthanh101@gmail.com
|
8
|
+
Date: 2025-01-08
|
9
|
+
Version: 1.0.0
|
10
|
+
"""
|
11
|
+
|
12
|
+
import json
|
13
|
+
import os
|
14
|
+
import sys
|
15
|
+
from typing import Dict, List, Optional
|
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
|
+
AWS_REGION = os.getenv("AWS_REGION", "ap-southeast-2") # Default Region
|
29
|
+
DEFAULT_TAG_KEY = os.getenv("DEFAULT_TAG_KEY", "Env") # Default Tag Key
|
30
|
+
DEFAULT_TAG_VALUE = os.getenv("DEFAULT_TAG_VALUE", "Prod") # Default Tag Value
|
31
|
+
|
32
|
+
# AWS Client
|
33
|
+
ec2_client = boto3.client("ec2", region_name=AWS_REGION)
|
34
|
+
|
35
|
+
|
36
|
+
# ==============================
|
37
|
+
# EC2 UTILITIES
|
38
|
+
# ==============================
|
39
|
+
def describe_instances(filters: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, str]]:
|
40
|
+
"""
|
41
|
+
Describes EC2 instances based on the provided filters.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
filters (List[Dict[str, str]], optional): List of filters for querying EC2 instances.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
List[Dict[str, str]]: List of EC2 instance details.
|
48
|
+
"""
|
49
|
+
try:
|
50
|
+
## ✅ Apply Default Filter if None Provided
|
51
|
+
filters = filters or [{"Name": f"tag:{DEFAULT_TAG_KEY}", "Values": [DEFAULT_TAG_VALUE]}]
|
52
|
+
logger.info(f"🔍 Querying EC2 instances with filters: {filters}")
|
53
|
+
|
54
|
+
instances = []
|
55
|
+
paginator = ec2_client.get_paginator("describe_instances")
|
56
|
+
|
57
|
+
## ✅ Paginate Results
|
58
|
+
for page in paginator.paginate(Filters=filters):
|
59
|
+
for reservation in page["Reservations"]:
|
60
|
+
for instance in reservation["Instances"]:
|
61
|
+
instances.append(
|
62
|
+
{
|
63
|
+
"InstanceId": instance["InstanceId"],
|
64
|
+
"State": instance["State"]["Name"],
|
65
|
+
"InstanceType": instance["InstanceType"],
|
66
|
+
"LaunchTime": str(instance["LaunchTime"]),
|
67
|
+
"Tags": instance.get("Tags", []),
|
68
|
+
}
|
69
|
+
)
|
70
|
+
|
71
|
+
## ✅ Log Results
|
72
|
+
logger.info(f"✅ Found {len(instances)} instance(s).")
|
73
|
+
return instances
|
74
|
+
|
75
|
+
except ClientError as e:
|
76
|
+
logger.error(f"❌ AWS Client Error: {e}")
|
77
|
+
raise
|
78
|
+
|
79
|
+
except BotoCoreError as e:
|
80
|
+
logger.error(f"❌ BotoCore Error: {e}")
|
81
|
+
raise
|
82
|
+
|
83
|
+
except Exception as e:
|
84
|
+
logger.error(f"❌ Unexpected Error: {e}")
|
85
|
+
raise
|
86
|
+
|
87
|
+
|
88
|
+
# ==============================
|
89
|
+
# DISPLAY UTILITIES
|
90
|
+
# ==============================
|
91
|
+
def display_instances(instances: List[Dict[str, str]]) -> None:
|
92
|
+
"""
|
93
|
+
Displays instance details in Markdown table format.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
instances (List[Dict[str, str]]): List of EC2 instance details.
|
97
|
+
"""
|
98
|
+
if not instances:
|
99
|
+
print("No instances found.")
|
100
|
+
return
|
101
|
+
|
102
|
+
## ✅ Markdown Table Header
|
103
|
+
table_header = (
|
104
|
+
"| Instance ID | State | Type | Launch Time | Tags |"
|
105
|
+
)
|
106
|
+
table_divider = (
|
107
|
+
"|----------------|------------|------------|--------------------------|--------------------------------|"
|
108
|
+
)
|
109
|
+
print(table_header)
|
110
|
+
print(table_divider)
|
111
|
+
|
112
|
+
## ✅ Print Each Instance Row
|
113
|
+
for instance in instances:
|
114
|
+
tags = ", ".join([f"{tag['Key']}={tag['Value']}" for tag in instance.get("Tags", [])])
|
115
|
+
print(
|
116
|
+
f"| {instance['InstanceId']:15} | {instance['State']:10} | {instance['InstanceType']:10} | "
|
117
|
+
f"{instance['LaunchTime']:24} | {tags:30} |"
|
118
|
+
)
|
119
|
+
|
120
|
+
|
121
|
+
def display_instances_json(instances: List[Dict[str, str]]) -> None:
|
122
|
+
"""
|
123
|
+
Displays instance details in JSON format for automation tools.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
instances (List[Dict[str, str]]): List of EC2 instance details.
|
127
|
+
"""
|
128
|
+
print(json.dumps(instances, indent=4)) if instances else print("No instances found.")
|
129
|
+
|
130
|
+
|
131
|
+
# ==============================
|
132
|
+
# CLI HANDLER
|
133
|
+
# ==============================
|
134
|
+
def main():
|
135
|
+
"""
|
136
|
+
Main function for CLI execution.
|
137
|
+
"""
|
138
|
+
try:
|
139
|
+
## ✅ Parse Command-Line Arguments
|
140
|
+
tag_key = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_TAG_KEY
|
141
|
+
tag_value = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_TAG_VALUE
|
142
|
+
|
143
|
+
## ✅ Construct Filters
|
144
|
+
filters = [{"Name": f"tag:{tag_key}", "Values": [tag_value]}]
|
145
|
+
|
146
|
+
## ✅ Fetch and Display Instances
|
147
|
+
instances = describe_instances(filters)
|
148
|
+
## ✅ Display Instances: Default to markdown table format
|
149
|
+
# display_instances_json(instances)
|
150
|
+
display_instances(instances)
|
151
|
+
|
152
|
+
except Exception as e:
|
153
|
+
logger.error(f"❌ Fatal Error: {e}")
|
154
|
+
sys.exit(1)
|
155
|
+
|
156
|
+
|
157
|
+
# ==============================
|
158
|
+
# AWS LAMBDA HANDLER
|
159
|
+
# ==============================
|
160
|
+
def lambda_handler(event, context):
|
161
|
+
"""
|
162
|
+
AWS Lambda Handler for describing EC2 instances.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
event (dict): AWS event data.
|
166
|
+
context: AWS Lambda context.
|
167
|
+
"""
|
168
|
+
try:
|
169
|
+
## ✅ Extract Inputs from Event
|
170
|
+
tag_key = event.get("tag_key", DEFAULT_TAG_KEY)
|
171
|
+
tag_value = event.get("tag_value", DEFAULT_TAG_VALUE)
|
172
|
+
output_format = event.get("output_format", "table") ## Supports 'table' or 'json'
|
173
|
+
|
174
|
+
## ✅ Construct Filters
|
175
|
+
filters = [{"Name": f"tag:{tag_key}", "Values": [tag_value]}]
|
176
|
+
|
177
|
+
## ✅ Fetch EC2 Instances
|
178
|
+
instances = describe_instances(filters)
|
179
|
+
|
180
|
+
## ✅ Return Lambda Response: Generate Output
|
181
|
+
if output_format == "json":
|
182
|
+
return {"statusCode": 200, "body": json.dumps(instances, indent=4)}
|
183
|
+
else:
|
184
|
+
table_output = []
|
185
|
+
for instance in instances:
|
186
|
+
tags = ", ".join([f"{tag['Key']}={tag['Value']}" for tag in instance.get("Tags", [])])
|
187
|
+
table_output.append(
|
188
|
+
f"| {instance['InstanceId']:15} | {instance['State']:10} | {instance['InstanceType']:10} | "
|
189
|
+
f"{instance['LaunchTime']:24} | {tags:30} |"
|
190
|
+
)
|
191
|
+
return {"statusCode": 200, "body": "\n".join(table_output)}
|
192
|
+
|
193
|
+
except Exception as e:
|
194
|
+
logger.error(f"❌ Lambda Error: {e}")
|
195
|
+
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
196
|
+
|
197
|
+
|
198
|
+
# ==============================
|
199
|
+
# SCRIPT ENTRY POINT
|
200
|
+
# ==============================
|
201
|
+
if __name__ == "__main__":
|
202
|
+
main()
|