runbooks 0.7.0__py3-none-any.whl → 0.7.6__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 (132) hide show
  1. runbooks/__init__.py +87 -37
  2. runbooks/cfat/README.md +300 -49
  3. runbooks/cfat/__init__.py +2 -2
  4. runbooks/finops/__init__.py +1 -1
  5. runbooks/finops/cli.py +1 -1
  6. runbooks/inventory/collectors/__init__.py +8 -0
  7. runbooks/inventory/collectors/aws_management.py +791 -0
  8. runbooks/inventory/collectors/aws_networking.py +3 -3
  9. runbooks/main.py +3389 -782
  10. runbooks/operate/__init__.py +207 -0
  11. runbooks/operate/base.py +311 -0
  12. runbooks/operate/cloudformation_operations.py +619 -0
  13. runbooks/operate/cloudwatch_operations.py +496 -0
  14. runbooks/operate/dynamodb_operations.py +812 -0
  15. runbooks/operate/ec2_operations.py +926 -0
  16. runbooks/operate/iam_operations.py +569 -0
  17. runbooks/operate/s3_operations.py +1211 -0
  18. runbooks/operate/tagging_operations.py +655 -0
  19. runbooks/remediation/CLAUDE.md +100 -0
  20. runbooks/remediation/DOME9.md +218 -0
  21. runbooks/remediation/README.md +26 -0
  22. runbooks/remediation/Tests/__init__.py +0 -0
  23. runbooks/remediation/Tests/update_policy.py +74 -0
  24. runbooks/remediation/__init__.py +95 -0
  25. runbooks/remediation/acm_cert_expired_unused.py +98 -0
  26. runbooks/remediation/acm_remediation.py +875 -0
  27. runbooks/remediation/api_gateway_list.py +167 -0
  28. runbooks/remediation/base.py +643 -0
  29. runbooks/remediation/cloudtrail_remediation.py +908 -0
  30. runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
  31. runbooks/remediation/cognito_active_users.py +78 -0
  32. runbooks/remediation/cognito_remediation.py +856 -0
  33. runbooks/remediation/cognito_user_password_reset.py +163 -0
  34. runbooks/remediation/commons.py +455 -0
  35. runbooks/remediation/dynamodb_optimize.py +155 -0
  36. runbooks/remediation/dynamodb_remediation.py +744 -0
  37. runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
  38. runbooks/remediation/ec2_public_ips.py +134 -0
  39. runbooks/remediation/ec2_remediation.py +892 -0
  40. runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
  41. runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
  42. runbooks/remediation/ec2_unused_security_groups.py +202 -0
  43. runbooks/remediation/kms_enable_key_rotation.py +651 -0
  44. runbooks/remediation/kms_remediation.py +717 -0
  45. runbooks/remediation/lambda_list.py +243 -0
  46. runbooks/remediation/lambda_remediation.py +971 -0
  47. runbooks/remediation/multi_account.py +569 -0
  48. runbooks/remediation/rds_instance_list.py +199 -0
  49. runbooks/remediation/rds_remediation.py +873 -0
  50. runbooks/remediation/rds_snapshot_list.py +192 -0
  51. runbooks/remediation/requirements.txt +118 -0
  52. runbooks/remediation/s3_block_public_access.py +159 -0
  53. runbooks/remediation/s3_bucket_public_access.py +143 -0
  54. runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
  55. runbooks/remediation/s3_downloader.py +215 -0
  56. runbooks/remediation/s3_enable_access_logging.py +562 -0
  57. runbooks/remediation/s3_encryption.py +526 -0
  58. runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
  59. runbooks/remediation/s3_list.py +141 -0
  60. runbooks/remediation/s3_object_search.py +201 -0
  61. runbooks/remediation/s3_remediation.py +816 -0
  62. runbooks/remediation/scan_for_phrase.py +425 -0
  63. runbooks/remediation/workspaces_list.py +220 -0
  64. runbooks/security/__init__.py +9 -10
  65. runbooks/security/security_baseline_tester.py +4 -2
  66. runbooks-0.7.6.dist-info/METADATA +608 -0
  67. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
  68. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
  69. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
  70. jupyter-agent/.env +0 -2
  71. jupyter-agent/.env.template +0 -2
  72. jupyter-agent/.gitattributes +0 -35
  73. jupyter-agent/.gradio/certificate.pem +0 -31
  74. jupyter-agent/README.md +0 -16
  75. jupyter-agent/__main__.log +0 -8
  76. jupyter-agent/app.py +0 -256
  77. jupyter-agent/cloudops-agent.png +0 -0
  78. jupyter-agent/ds-system-prompt.txt +0 -154
  79. jupyter-agent/jupyter-agent.png +0 -0
  80. jupyter-agent/llama3_template.jinja +0 -123
  81. jupyter-agent/requirements.txt +0 -9
  82. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
  83. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
  84. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
  85. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
  86. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
  87. jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
  88. jupyter-agent/utils.py +0 -409
  89. runbooks/aws/__init__.py +0 -58
  90. runbooks/aws/dynamodb_operations.py +0 -231
  91. runbooks/aws/ec2_copy_image_cross-region.py +0 -195
  92. runbooks/aws/ec2_describe_instances.py +0 -202
  93. runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
  94. runbooks/aws/ec2_run_instances.py +0 -213
  95. runbooks/aws/ec2_start_stop_instances.py +0 -212
  96. runbooks/aws/ec2_terminate_instances.py +0 -143
  97. runbooks/aws/ec2_unused_eips.py +0 -196
  98. runbooks/aws/ec2_unused_volumes.py +0 -188
  99. runbooks/aws/s3_create_bucket.py +0 -142
  100. runbooks/aws/s3_list_buckets.py +0 -152
  101. runbooks/aws/s3_list_objects.py +0 -156
  102. runbooks/aws/s3_object_operations.py +0 -183
  103. runbooks/aws/tagging_lambda_handler.py +0 -183
  104. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
  105. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
  106. runbooks/inventory/aws_organization.png +0 -0
  107. runbooks/inventory/cfn_move_stack_instances.py +0 -1526
  108. runbooks/inventory/delete_s3_buckets_objects.py +0 -169
  109. runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
  110. runbooks/inventory/update_aws_actions.py +0 -173
  111. runbooks/inventory/update_cfn_stacksets.py +0 -1215
  112. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
  113. runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
  114. runbooks/inventory/update_s3_public_access_block.py +0 -539
  115. runbooks/organizations/__init__.py +0 -12
  116. runbooks/organizations/manager.py +0 -374
  117. runbooks-0.7.0.dist-info/METADATA +0 -375
  118. /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
  119. /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
  120. /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
  121. /runbooks/inventory/{tests → Tests}/setup.py +0 -0
  122. /runbooks/inventory/{tests → Tests}/src.py +0 -0
  123. /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
  124. /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
  125. /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
  126. /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
  127. /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
  128. /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
  129. /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
  130. /runbooks/{aws → operate}/tags.json +0 -0
  131. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
  132. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,195 +0,0 @@
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()
@@ -1,202 +0,0 @@
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()
@@ -1,186 +0,0 @@
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()