runbooks 0.1.7__tar.gz → 0.1.8__tar.gz

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 (61) hide show
  1. {runbooks-0.1.7/src/runbooks.egg-info → runbooks-0.1.8}/PKG-INFO +2 -2
  2. {runbooks-0.1.7 → runbooks-0.1.8}/pyproject.toml +11 -5
  3. {runbooks-0.1.7 → runbooks-0.1.8}/src/runbooks/__init__.py +1 -1
  4. runbooks-0.1.8/src/runbooks/aws/__init__.py +58 -0
  5. runbooks-0.1.8/src/runbooks/aws/dynamodb_operations.py +231 -0
  6. runbooks-0.1.8/src/runbooks/aws/ec2_copy_image_cross-region.py +195 -0
  7. runbooks-0.1.8/src/runbooks/aws/ec2_describe_instances.py +202 -0
  8. runbooks-0.1.8/src/runbooks/aws/ec2_ebs_snapshots_delete.py +186 -0
  9. runbooks-0.1.8/src/runbooks/aws/ec2_run_instances.py +207 -0
  10. runbooks-0.1.8/src/runbooks/aws/ec2_start_stop_instances.py +199 -0
  11. runbooks-0.1.8/src/runbooks/aws/ec2_terminate_instances.py +143 -0
  12. runbooks-0.1.8/src/runbooks/aws/ec2_unused_eips.py +196 -0
  13. runbooks-0.1.8/src/runbooks/aws/ec2_unused_volumes.py +184 -0
  14. runbooks-0.1.8/src/runbooks/aws/s3_create_bucket.py +140 -0
  15. runbooks-0.1.8/src/runbooks/aws/s3_list_buckets.py +152 -0
  16. runbooks-0.1.8/src/runbooks/aws/s3_list_objects.py +151 -0
  17. runbooks-0.1.8/src/runbooks/aws/s3_object_operations.py +183 -0
  18. runbooks-0.1.8/src/runbooks/aws/tagging_lambda_handler.py +172 -0
  19. runbooks-0.1.8/src/runbooks/python101/calculator.py +34 -0
  20. runbooks-0.1.8/src/runbooks/python101/config.py +1 -0
  21. runbooks-0.1.8/src/runbooks/python101/exceptions.py +16 -0
  22. runbooks-0.1.8/src/runbooks/python101/file_manager.py +218 -0
  23. runbooks-0.1.8/src/runbooks/python101/toolkit.py +153 -0
  24. runbooks-0.1.8/src/runbooks/security_baseline/__init__.py +0 -0
  25. runbooks-0.1.8/src/runbooks/security_baseline/checklist/__init__.py +17 -0
  26. runbooks-0.1.8/src/runbooks/security_baseline/checklist/account_level_bucket_public_access.py +86 -0
  27. runbooks-0.1.8/src/runbooks/security_baseline/checklist/alternate_contacts.py +65 -0
  28. runbooks-0.1.8/src/runbooks/security_baseline/checklist/bucket_public_access.py +82 -0
  29. runbooks-0.1.8/src/runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +66 -0
  30. runbooks-0.1.8/src/runbooks/security_baseline/checklist/direct_attached_policy.py +69 -0
  31. runbooks-0.1.8/src/runbooks/security_baseline/checklist/guardduty_enabled.py +71 -0
  32. runbooks-0.1.8/src/runbooks/security_baseline/checklist/iam_password_policy.py +43 -0
  33. runbooks-0.1.8/src/runbooks/security_baseline/checklist/iam_user_mfa.py +39 -0
  34. runbooks-0.1.8/src/runbooks/security_baseline/checklist/multi_region_instance_usage.py +55 -0
  35. runbooks-0.1.8/src/runbooks/security_baseline/checklist/multi_region_trail.py +64 -0
  36. runbooks-0.1.8/src/runbooks/security_baseline/checklist/root_access_key.py +72 -0
  37. runbooks-0.1.8/src/runbooks/security_baseline/checklist/root_mfa.py +39 -0
  38. runbooks-0.1.8/src/runbooks/security_baseline/checklist/root_usage.py +128 -0
  39. runbooks-0.1.8/src/runbooks/security_baseline/checklist/trail_enabled.py +68 -0
  40. runbooks-0.1.8/src/runbooks/security_baseline/checklist/trusted_advisor.py +24 -0
  41. runbooks-0.1.8/src/runbooks/security_baseline/report_generator.py +149 -0
  42. runbooks-0.1.8/src/runbooks/security_baseline/run_script.py +76 -0
  43. runbooks-0.1.8/src/runbooks/security_baseline/security_baseline_tester.py +179 -0
  44. runbooks-0.1.8/src/runbooks/security_baseline/utils/__init__.py +1 -0
  45. runbooks-0.1.8/src/runbooks/security_baseline/utils/common.py +109 -0
  46. runbooks-0.1.8/src/runbooks/security_baseline/utils/enums.py +44 -0
  47. runbooks-0.1.8/src/runbooks/security_baseline/utils/language.py +762 -0
  48. runbooks-0.1.8/src/runbooks/security_baseline/utils/level_const.py +5 -0
  49. runbooks-0.1.8/src/runbooks/security_baseline/utils/permission_list.py +26 -0
  50. runbooks-0.1.8/src/runbooks/utils/__init__.py +0 -0
  51. runbooks-0.1.8/src/runbooks/utils/logger.py +36 -0
  52. {runbooks-0.1.7 → runbooks-0.1.8/src/runbooks.egg-info}/PKG-INFO +2 -2
  53. runbooks-0.1.8/src/runbooks.egg-info/SOURCES.txt +58 -0
  54. {runbooks-0.1.7 → runbooks-0.1.8}/src/runbooks.egg-info/requires.txt +1 -1
  55. runbooks-0.1.7/src/runbooks.egg-info/SOURCES.txt +0 -10
  56. {runbooks-0.1.7 → runbooks-0.1.8}/LICENSE +0 -0
  57. {runbooks-0.1.7 → runbooks-0.1.8}/README.md +0 -0
  58. {runbooks-0.1.7 → runbooks-0.1.8}/setup.cfg +0 -0
  59. {runbooks-0.1.7 → runbooks-0.1.8}/src/runbooks.egg-info/dependency_links.txt +0 -0
  60. {runbooks-0.1.7 → runbooks-0.1.8}/src/runbooks.egg-info/entry_points.txt +0 -0
  61. {runbooks-0.1.7 → runbooks-0.1.8}/src/runbooks.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: runbooks
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: CloudOps Automation Toolkit for DevOps and SRE teams.
5
5
  Author-email: runbooks maintainers <nnthanh101@gmail.com>
6
6
  License: Apache License
@@ -255,7 +255,7 @@ Requires-Dist: pandas>=2.2.3
255
255
  Requires-Dist: plotly>=5.24.1
256
256
  Requires-Dist: vizro>=0.1.30
257
257
  Requires-Dist: vizro-ai>=0.3.2
258
- Requires-Dist: runbooks>=0.1.7
258
+ Requires-Dist: runbooks>=0.1.8
259
259
 
260
260
  # 🔥 CloudOps Automation at Scale 🦅
261
261
 
@@ -2,7 +2,7 @@
2
2
  ## Metadata: https://docs.astral.sh/uv/concepts/projects/config/
3
3
  name = "runbooks"
4
4
  ## Incremented for new release
5
- version = "0.1.7"
5
+ version = "0.1.8"
6
6
  description = "CloudOps Automation Toolkit for DevOps and SRE teams."
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.11"
@@ -85,7 +85,7 @@ dependencies = [
85
85
  "vizro-ai>=0.3.2",
86
86
 
87
87
  ## 1xOps/CloudOps-Runbooks: https://pypi.org/project/runbooks/
88
- "runbooks>=0.1.7"
88
+ "runbooks>=0.1.8"
89
89
  ]
90
90
 
91
91
  [dependency-groups]
@@ -106,15 +106,21 @@ requires = ["setuptools"]
106
106
  build-backend = "setuptools.build_meta"
107
107
 
108
108
  [tool.setuptools]
109
- package-dir = {"" = "src"} ## Use 'src' as root
110
- packages = ["runbooks"]
109
+ # packages = ["runbooks"]
110
+ package-dir = {"" = "src"} ## Tells setuptools that code is in `src/`
111
+ # include-package-data = true ## If you want non-.py files included too
111
112
  license-files = []
112
113
  # license-files = ["LICENSE"]
113
114
 
115
+ [tool.setuptools.packages.find]
116
+ where = ["src"] ## Look inside `src` for packages
117
+ include = ["runbooks*"] ## Include runbooks and its subpackages
118
+ exclude = ["tests*"] ## Exclude test folders
119
+
114
120
  [tool.versioningit]
115
121
  vcs = "git"
116
122
  tag2version = "v{base}"
117
- default-version = "0.1.7"
123
+ default-version = "0.1.8"
118
124
 
119
125
  [tool.pytest.ini_options]
120
126
  ## Test Configuration
@@ -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()