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.
Files changed (55) 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.7.dist-info → runbooks-0.1.8.dist-info}/METADATA +2 -2
  51. runbooks-0.1.8.dist-info/RECORD +54 -0
  52. runbooks-0.1.7.dist-info/RECORD +0 -6
  53. {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/WHEEL +0 -0
  54. {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/entry_points.txt +0 -0
  55. {runbooks-0.1.7.dist-info → runbooks-0.1.8.dist-info}/top_level.txt +0 -0
runbooks/__init__.py CHANGED
@@ -4,4 +4,4 @@ runbooks
4
4
  Provides utility functions for math operations, AWS S3 interactions, and data management.
5
5
  """
6
6
 
7
- # __version__ = "0.1.7"
7
+ # __version__ = "0.1.8"
@@ -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()