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.
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {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()
|