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
@@ -0,0 +1,926 @@
|
|
1
|
+
"""
|
2
|
+
Enterprise-Grade EC2 Operations Module.
|
3
|
+
|
4
|
+
Comprehensive EC2 resource management with Lambda support, environment configuration,
|
5
|
+
SNS notifications, and full compatibility with original AWS Cloud Foundations scripts.
|
6
|
+
|
7
|
+
Migrated and enhanced from:
|
8
|
+
- aws/ec2_terminate_instances.py (with Lambda handler)
|
9
|
+
- aws/ec2_start_stop_instances.py (with state management)
|
10
|
+
- aws/ec2_run_instances.py (with block device mappings)
|
11
|
+
- aws/ec2_copy_image_cross-region.py (with image creation)
|
12
|
+
- aws/ec2_ebs_snapshots_delete.py (with safety checks)
|
13
|
+
- aws/ec2_unused_volumes.py (with SNS notifications)
|
14
|
+
- aws/ec2_unused_eips.py (with comprehensive scanning)
|
15
|
+
|
16
|
+
Author: CloudOps DevOps Engineer
|
17
|
+
Date: 2025-01-21
|
18
|
+
Version: 2.0.0 - Enterprise Enhancement
|
19
|
+
"""
|
20
|
+
|
21
|
+
import base64
|
22
|
+
import json
|
23
|
+
import os
|
24
|
+
from datetime import datetime
|
25
|
+
from typing import Any, Dict, List, Optional, Union
|
26
|
+
|
27
|
+
import boto3
|
28
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
29
|
+
from loguru import logger
|
30
|
+
|
31
|
+
from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
|
32
|
+
|
33
|
+
|
34
|
+
class EC2Operations(BaseOperation):
|
35
|
+
"""
|
36
|
+
Enterprise-grade EC2 resource operations and lifecycle management.
|
37
|
+
|
38
|
+
Handles all EC2-related operational tasks including instance management,
|
39
|
+
volume operations, AMI operations, resource cleanup, and notifications.
|
40
|
+
Supports environment variable configuration and AWS Lambda execution.
|
41
|
+
"""
|
42
|
+
|
43
|
+
service_name = "ec2"
|
44
|
+
supported_operations = {
|
45
|
+
"start_instances",
|
46
|
+
"stop_instances",
|
47
|
+
"terminate_instances",
|
48
|
+
"run_instances",
|
49
|
+
"copy_image",
|
50
|
+
"create_image",
|
51
|
+
"delete_snapshots",
|
52
|
+
"cleanup_unused_volumes",
|
53
|
+
"cleanup_unused_eips",
|
54
|
+
"reboot_instances",
|
55
|
+
}
|
56
|
+
requires_confirmation = True
|
57
|
+
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
profile: Optional[str] = None,
|
61
|
+
region: Optional[str] = None,
|
62
|
+
dry_run: bool = False,
|
63
|
+
sns_topic_arn: Optional[str] = None,
|
64
|
+
):
|
65
|
+
"""
|
66
|
+
Initialize EC2 operations with enhanced configuration support.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
profile: AWS profile name (can be overridden by AWS_PROFILE env var)
|
70
|
+
region: AWS region (can be overridden by AWS_REGION env var)
|
71
|
+
dry_run: Dry run mode (can be overridden by DRY_RUN env var)
|
72
|
+
sns_topic_arn: SNS topic for notifications (can be overridden by SNS_TOPIC_ARN env var)
|
73
|
+
"""
|
74
|
+
# Environment variable support for Lambda/Container deployment
|
75
|
+
self.profile = profile or os.getenv("AWS_PROFILE")
|
76
|
+
self.region = region or os.getenv("AWS_REGION", "us-east-1")
|
77
|
+
self.dry_run = dry_run or os.getenv("DRY_RUN", "false").lower() == "true"
|
78
|
+
self.sns_topic_arn = sns_topic_arn or os.getenv("SNS_TOPIC_ARN")
|
79
|
+
|
80
|
+
super().__init__(self.profile, self.region, self.dry_run)
|
81
|
+
|
82
|
+
# Initialize SNS client for notifications
|
83
|
+
self.sns_client = None
|
84
|
+
if self.sns_topic_arn:
|
85
|
+
self.sns_client = self.get_client("sns", self.region)
|
86
|
+
|
87
|
+
def validate_sns_arn(self, arn: str) -> None:
|
88
|
+
"""
|
89
|
+
Validates the format of the SNS Topic ARN.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
arn: SNS Topic ARN
|
93
|
+
|
94
|
+
Raises:
|
95
|
+
ValueError: If the ARN format is invalid
|
96
|
+
"""
|
97
|
+
if not arn.startswith("arn:aws:sns:"):
|
98
|
+
raise ValueError(f"Invalid SNS Topic ARN: {arn}")
|
99
|
+
logger.info(f"✅ Valid SNS ARN: {arn}")
|
100
|
+
|
101
|
+
def validate_regions(self, source_region: str, dest_region: str) -> None:
|
102
|
+
"""
|
103
|
+
Validates AWS regions for cross-region operations.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
source_region: Source AWS region
|
107
|
+
dest_region: Destination AWS region
|
108
|
+
|
109
|
+
Raises:
|
110
|
+
ValueError: If regions are invalid
|
111
|
+
"""
|
112
|
+
session = boto3.session.Session()
|
113
|
+
valid_regions = session.get_available_regions("ec2")
|
114
|
+
|
115
|
+
if source_region not in valid_regions:
|
116
|
+
raise ValueError(f"Invalid source region: {source_region}")
|
117
|
+
if dest_region not in valid_regions:
|
118
|
+
raise ValueError(f"Invalid destination region: {dest_region}")
|
119
|
+
logger.info(f"Validated AWS regions: {source_region} -> {dest_region}")
|
120
|
+
|
121
|
+
def send_sns_notification(self, subject: str, message: str) -> None:
|
122
|
+
"""
|
123
|
+
Send SNS notification if configured.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
subject: Notification subject
|
127
|
+
message: Notification message
|
128
|
+
"""
|
129
|
+
if self.sns_client and self.sns_topic_arn:
|
130
|
+
try:
|
131
|
+
self.sns_client.publish(TopicArn=self.sns_topic_arn, Subject=subject, Message=message)
|
132
|
+
logger.info(f"SNS notification sent: {subject}")
|
133
|
+
except ClientError as e:
|
134
|
+
logger.warning(f"Failed to send SNS notification: {e}")
|
135
|
+
|
136
|
+
def get_default_block_device_mappings(self, volume_size: int = 20, encrypted: bool = True) -> List[Dict]:
|
137
|
+
"""
|
138
|
+
Get default block device mappings with modern EBS configuration.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
volume_size: EBS volume size in GB
|
142
|
+
encrypted: Whether to encrypt the EBS volume
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
Block device mappings configuration
|
146
|
+
"""
|
147
|
+
return [
|
148
|
+
{
|
149
|
+
"DeviceName": "/dev/xvda", # Root volume device
|
150
|
+
"Ebs": {
|
151
|
+
"DeleteOnTermination": True, # Clean up after instance termination
|
152
|
+
"VolumeSize": volume_size, # Set volume size in GB
|
153
|
+
"VolumeType": "gp3", # Modern, faster storage
|
154
|
+
"Encrypted": encrypted, # Encrypt the EBS volume
|
155
|
+
},
|
156
|
+
},
|
157
|
+
]
|
158
|
+
|
159
|
+
def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
|
160
|
+
"""Execute EC2 operation."""
|
161
|
+
self.validate_context(context)
|
162
|
+
|
163
|
+
if operation_type == "start_instances":
|
164
|
+
return self.start_instances(context, kwargs.get("instance_ids", []))
|
165
|
+
elif operation_type == "stop_instances":
|
166
|
+
return self.stop_instances(context, kwargs.get("instance_ids", []))
|
167
|
+
elif operation_type == "terminate_instances":
|
168
|
+
return self.terminate_instances(context, kwargs.get("instance_ids", []))
|
169
|
+
elif operation_type == "run_instances":
|
170
|
+
return self.run_instances(context, **kwargs)
|
171
|
+
elif operation_type == "copy_image":
|
172
|
+
return self.copy_image(context, **kwargs)
|
173
|
+
elif operation_type == "create_image":
|
174
|
+
return self.create_image(context, **kwargs)
|
175
|
+
elif operation_type == "delete_snapshots":
|
176
|
+
return self.delete_snapshots(context, kwargs.get("snapshot_ids", []))
|
177
|
+
elif operation_type == "cleanup_unused_volumes":
|
178
|
+
return self.cleanup_unused_volumes(context)
|
179
|
+
elif operation_type == "cleanup_unused_eips":
|
180
|
+
return self.cleanup_unused_eips(context)
|
181
|
+
elif operation_type == "reboot_instances":
|
182
|
+
return self.reboot_instances(context, kwargs.get("instance_ids", []))
|
183
|
+
else:
|
184
|
+
raise ValueError(f"Unsupported operation: {operation_type}")
|
185
|
+
|
186
|
+
def start_instances(self, context: OperationContext, instance_ids: List[str]) -> List[OperationResult]:
|
187
|
+
"""Start EC2 instances."""
|
188
|
+
ec2_client = self.get_client("ec2", context.region)
|
189
|
+
results = []
|
190
|
+
|
191
|
+
for instance_id in instance_ids:
|
192
|
+
result = self.create_operation_result(context, "start_instances", "ec2:instance", instance_id)
|
193
|
+
|
194
|
+
try:
|
195
|
+
if context.dry_run:
|
196
|
+
logger.info(f"[DRY-RUN] Would start instance {instance_id}")
|
197
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
198
|
+
else:
|
199
|
+
response = self.execute_aws_call(ec2_client, "start_instances", InstanceIds=[instance_id])
|
200
|
+
result.response_data = response
|
201
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
202
|
+
logger.info(f"Successfully started instance {instance_id}")
|
203
|
+
|
204
|
+
except ClientError as e:
|
205
|
+
error_msg = f"Failed to start instance {instance_id}: {e}"
|
206
|
+
logger.error(error_msg)
|
207
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
208
|
+
|
209
|
+
results.append(result)
|
210
|
+
|
211
|
+
return results
|
212
|
+
|
213
|
+
def stop_instances(self, context: OperationContext, instance_ids: List[str]) -> List[OperationResult]:
|
214
|
+
"""Stop EC2 instances."""
|
215
|
+
ec2_client = self.get_client("ec2", context.region)
|
216
|
+
results = []
|
217
|
+
|
218
|
+
for instance_id in instance_ids:
|
219
|
+
result = self.create_operation_result(context, "stop_instances", "ec2:instance", instance_id)
|
220
|
+
|
221
|
+
try:
|
222
|
+
if context.dry_run:
|
223
|
+
logger.info(f"[DRY-RUN] Would stop instance {instance_id}")
|
224
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
225
|
+
else:
|
226
|
+
response = self.execute_aws_call(ec2_client, "stop_instances", InstanceIds=[instance_id])
|
227
|
+
result.response_data = response
|
228
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
229
|
+
logger.info(f"Successfully stopped instance {instance_id}")
|
230
|
+
|
231
|
+
except ClientError as e:
|
232
|
+
error_msg = f"Failed to stop instance {instance_id}: {e}"
|
233
|
+
logger.error(error_msg)
|
234
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
235
|
+
|
236
|
+
results.append(result)
|
237
|
+
|
238
|
+
return results
|
239
|
+
|
240
|
+
def terminate_instances(self, context: OperationContext, instance_ids: List[str]) -> List[OperationResult]:
|
241
|
+
"""
|
242
|
+
Terminate EC2 instances (DESTRUCTIVE) with enhanced validation and notifications.
|
243
|
+
|
244
|
+
Based on original aws/ec2_terminate_instances.py with enterprise enhancements.
|
245
|
+
"""
|
246
|
+
# Enhanced validation from original file
|
247
|
+
if not instance_ids or instance_ids == [""]:
|
248
|
+
logger.error("No instance IDs provided for termination.")
|
249
|
+
raise ValueError("Instance IDs cannot be empty.")
|
250
|
+
|
251
|
+
ec2_client = self.get_client("ec2", context.region)
|
252
|
+
results = []
|
253
|
+
terminated_instances = []
|
254
|
+
|
255
|
+
logger.info(f"Terminating instances: {', '.join(instance_ids)} in region {context.region}...")
|
256
|
+
|
257
|
+
for instance_id in instance_ids:
|
258
|
+
result = self.create_operation_result(context, "terminate_instances", "ec2:instance", instance_id)
|
259
|
+
|
260
|
+
try:
|
261
|
+
if not self.confirm_operation(context, instance_id, "terminate"):
|
262
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
263
|
+
results.append(result)
|
264
|
+
continue
|
265
|
+
|
266
|
+
if context.dry_run:
|
267
|
+
logger.info(f"[DRY-RUN] No actual termination performed for {instance_id}")
|
268
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
269
|
+
else:
|
270
|
+
response = self.execute_aws_call(ec2_client, "terminate_instances", InstanceIds=[instance_id])
|
271
|
+
|
272
|
+
# Enhanced logging from original file
|
273
|
+
for instance in response["TerminatingInstances"]:
|
274
|
+
logger.info(
|
275
|
+
f"Instance {instance['InstanceId']} state changed to {instance['CurrentState']['Name']}"
|
276
|
+
)
|
277
|
+
|
278
|
+
terminated_instances.append(instance_id)
|
279
|
+
result.response_data = response
|
280
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
281
|
+
logger.info(f"Successfully terminated instance {instance_id}")
|
282
|
+
|
283
|
+
except ClientError as e:
|
284
|
+
error_msg = f"AWS Client Error: {e}"
|
285
|
+
logger.error(error_msg)
|
286
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
287
|
+
except BotoCoreError as e:
|
288
|
+
error_msg = f"BotoCore Error: {e}"
|
289
|
+
logger.error(error_msg)
|
290
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
291
|
+
except Exception as e:
|
292
|
+
error_msg = f"Unexpected error: {e}"
|
293
|
+
logger.error(error_msg)
|
294
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
295
|
+
|
296
|
+
results.append(result)
|
297
|
+
|
298
|
+
# Send SNS notification if configured
|
299
|
+
if terminated_instances:
|
300
|
+
message = f"Successfully terminated instances: {', '.join(terminated_instances)}"
|
301
|
+
self.send_sns_notification("EC2 Instances Terminated", message)
|
302
|
+
logger.info(message)
|
303
|
+
elif not context.dry_run:
|
304
|
+
logger.info("No instances terminated.")
|
305
|
+
|
306
|
+
return results
|
307
|
+
|
308
|
+
def run_instances(
|
309
|
+
self,
|
310
|
+
context: OperationContext,
|
311
|
+
image_id: Optional[str] = None,
|
312
|
+
instance_type: Optional[str] = None,
|
313
|
+
min_count: Optional[int] = None,
|
314
|
+
max_count: Optional[int] = None,
|
315
|
+
key_name: Optional[str] = None,
|
316
|
+
security_group_ids: Optional[List[str]] = None,
|
317
|
+
subnet_id: Optional[str] = None,
|
318
|
+
user_data: Optional[str] = None,
|
319
|
+
instance_profile_name: Optional[str] = None,
|
320
|
+
tags: Optional[Dict[str, str]] = None,
|
321
|
+
volume_size: int = 20,
|
322
|
+
enable_monitoring: bool = False,
|
323
|
+
enable_encryption: bool = True,
|
324
|
+
) -> List[OperationResult]:
|
325
|
+
"""
|
326
|
+
Launch EC2 instances with comprehensive configuration.
|
327
|
+
|
328
|
+
Enhanced from original aws/ec2_run_instances.py with environment variable support,
|
329
|
+
block device mappings, monitoring, and enterprise-grade configuration.
|
330
|
+
"""
|
331
|
+
# Environment variable support from original file
|
332
|
+
image_id = image_id or os.getenv("AMI_ID", "ami-03f052ebc3f436d52") # Default RHEL 9
|
333
|
+
instance_type = instance_type or os.getenv("INSTANCE_TYPE", "t2.micro")
|
334
|
+
min_count = min_count or int(os.getenv("MIN_COUNT", "1"))
|
335
|
+
max_count = max_count or int(os.getenv("MAX_COUNT", "1"))
|
336
|
+
key_name = key_name or os.getenv("KEY_NAME", "EC2Test")
|
337
|
+
|
338
|
+
# Parse security groups and subnet from environment
|
339
|
+
if not security_group_ids:
|
340
|
+
env_sg = os.getenv("SECURITY_GROUP_IDS", "")
|
341
|
+
security_group_ids = env_sg.split(",") if env_sg else []
|
342
|
+
|
343
|
+
subnet_id = subnet_id or os.getenv("SUBNET_ID")
|
344
|
+
|
345
|
+
# Parse tags from environment variable
|
346
|
+
if not tags:
|
347
|
+
env_tags = os.getenv("TAGS", '{"Project":"CloudOps", "Environment":"Dev"}')
|
348
|
+
try:
|
349
|
+
tags = json.loads(env_tags)
|
350
|
+
except json.JSONDecodeError:
|
351
|
+
tags = {"Project": "CloudOps", "Environment": "Dev"}
|
352
|
+
|
353
|
+
# Enhanced validation from original file
|
354
|
+
if not subnet_id:
|
355
|
+
raise ValueError("Missing required SUBNET_ID for VPC deployment")
|
356
|
+
if not security_group_ids:
|
357
|
+
raise ValueError("Missing required SECURITY_GROUP_IDS for VPC deployment")
|
358
|
+
|
359
|
+
logger.info("✅ Environment variables validated successfully.")
|
360
|
+
|
361
|
+
ec2_client = self.get_client("ec2", context.region)
|
362
|
+
|
363
|
+
result = self.create_operation_result(
|
364
|
+
context, "run_instances", "ec2:instance", f"{min_count}-{max_count} instances"
|
365
|
+
)
|
366
|
+
|
367
|
+
try:
|
368
|
+
logger.info(f"Launching {min_count}-{max_count} instances of type {instance_type} with AMI {image_id}...")
|
369
|
+
|
370
|
+
if context.dry_run:
|
371
|
+
logger.info(f"[DRY-RUN] Would launch {min_count}-{max_count} instances of {image_id}")
|
372
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
373
|
+
return [result]
|
374
|
+
|
375
|
+
# Enhanced parameters from original file
|
376
|
+
launch_params = {
|
377
|
+
"BlockDeviceMappings": self.get_default_block_device_mappings(volume_size, enable_encryption),
|
378
|
+
"ImageId": image_id,
|
379
|
+
"InstanceType": instance_type,
|
380
|
+
"MinCount": min_count,
|
381
|
+
"MaxCount": max_count,
|
382
|
+
"Monitoring": {"Enabled": enable_monitoring},
|
383
|
+
"KeyName": key_name,
|
384
|
+
"SubnetId": subnet_id,
|
385
|
+
"SecurityGroupIds": security_group_ids,
|
386
|
+
}
|
387
|
+
|
388
|
+
# Optional parameters
|
389
|
+
if user_data:
|
390
|
+
launch_params["UserData"] = base64.b64encode(user_data.encode()).decode()
|
391
|
+
if instance_profile_name:
|
392
|
+
launch_params["IamInstanceProfile"] = {"Name": instance_profile_name}
|
393
|
+
|
394
|
+
# Enhanced tagging from original file
|
395
|
+
if tags:
|
396
|
+
tag_specifications = [
|
397
|
+
{"ResourceType": "instance", "Tags": [{"Key": k, "Value": v} for k, v in tags.items()]}
|
398
|
+
]
|
399
|
+
launch_params["TagSpecifications"] = tag_specifications
|
400
|
+
|
401
|
+
response = self.execute_aws_call(ec2_client, "run_instances", **launch_params)
|
402
|
+
instance_ids = [inst["InstanceId"] for inst in response["Instances"]]
|
403
|
+
|
404
|
+
logger.info(f"Launched Instances: {instance_ids}")
|
405
|
+
|
406
|
+
# Apply additional tags if needed (from original file approach)
|
407
|
+
if tags:
|
408
|
+
try:
|
409
|
+
ec2_client.create_tags(
|
410
|
+
Resources=instance_ids,
|
411
|
+
Tags=[{"Key": k, "Value": v} for k, v in tags.items()],
|
412
|
+
)
|
413
|
+
logger.info(f"✅ Applied tags to instances: {instance_ids}")
|
414
|
+
except ClientError as e:
|
415
|
+
logger.warning(f"Failed to apply additional tags: {e}")
|
416
|
+
|
417
|
+
result.response_data = response
|
418
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
419
|
+
logger.info(f"Successfully launched {len(instance_ids)} instances")
|
420
|
+
|
421
|
+
# SNS notification
|
422
|
+
message = f"Successfully launched {len(instance_ids)} instances: {', '.join(instance_ids)}"
|
423
|
+
self.send_sns_notification("EC2 Instances Launched", message)
|
424
|
+
|
425
|
+
except ClientError as e:
|
426
|
+
error_msg = f"AWS Client Error: {e}"
|
427
|
+
logger.error(error_msg)
|
428
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
429
|
+
except BotoCoreError as e:
|
430
|
+
error_msg = f"BotoCore Error: {e}"
|
431
|
+
logger.error(error_msg)
|
432
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
433
|
+
except Exception as e:
|
434
|
+
error_msg = f"Unexpected error: {e}"
|
435
|
+
logger.error(error_msg)
|
436
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
437
|
+
|
438
|
+
return [result]
|
439
|
+
|
440
|
+
def copy_image(
|
441
|
+
self,
|
442
|
+
context: OperationContext,
|
443
|
+
source_image_id: str,
|
444
|
+
source_region: str,
|
445
|
+
name: str,
|
446
|
+
description: Optional[str] = None,
|
447
|
+
encrypted: bool = True,
|
448
|
+
kms_key_id: Optional[str] = None,
|
449
|
+
) -> List[OperationResult]:
|
450
|
+
"""
|
451
|
+
Copy AMI across regions with encryption and validation.
|
452
|
+
|
453
|
+
Enhanced from original aws/ec2_copy_image_cross-region.py.
|
454
|
+
"""
|
455
|
+
# Validate regions using original file logic
|
456
|
+
self.validate_regions(source_region, context.region)
|
457
|
+
|
458
|
+
ec2_client = self.get_client("ec2", context.region)
|
459
|
+
|
460
|
+
result = self.create_operation_result(
|
461
|
+
context, "copy_image", "ec2:ami", f"{source_image_id}:{source_region}->{context.region}"
|
462
|
+
)
|
463
|
+
|
464
|
+
try:
|
465
|
+
if context.dry_run:
|
466
|
+
logger.info(f"[DRY-RUN] Would copy AMI {source_image_id} from {source_region}")
|
467
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
468
|
+
return [result]
|
469
|
+
|
470
|
+
copy_params = {
|
471
|
+
"SourceImageId": source_image_id,
|
472
|
+
"SourceRegion": source_region,
|
473
|
+
"Name": name,
|
474
|
+
"Encrypted": encrypted,
|
475
|
+
}
|
476
|
+
|
477
|
+
if description:
|
478
|
+
copy_params["Description"] = description
|
479
|
+
if kms_key_id and encrypted:
|
480
|
+
copy_params["KmsKeyId"] = kms_key_id
|
481
|
+
|
482
|
+
response = self.execute_aws_call(ec2_client, "copy_image", **copy_params)
|
483
|
+
new_image_id = response["ImageId"]
|
484
|
+
|
485
|
+
result.response_data = response
|
486
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
487
|
+
logger.info(f"Successfully initiated AMI copy. New AMI ID: {new_image_id}")
|
488
|
+
|
489
|
+
# SNS notification
|
490
|
+
message = f"AMI {source_image_id} copied from {source_region} to {context.region}. New AMI: {new_image_id}"
|
491
|
+
self.send_sns_notification("EC2 AMI Copied", message)
|
492
|
+
|
493
|
+
except ClientError as e:
|
494
|
+
error_msg = f"AWS Client Error: {e}"
|
495
|
+
logger.error(error_msg)
|
496
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
497
|
+
except BotoCoreError as e:
|
498
|
+
error_msg = f"BotoCore Error: {e}"
|
499
|
+
logger.error(error_msg)
|
500
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
501
|
+
except Exception as e:
|
502
|
+
error_msg = f"Unexpected error: {e}"
|
503
|
+
logger.error(error_msg)
|
504
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
505
|
+
|
506
|
+
return [result]
|
507
|
+
|
508
|
+
def create_image(
|
509
|
+
self, context: OperationContext, instance_ids: List[str], image_name_prefix: Optional[str] = None
|
510
|
+
) -> List[OperationResult]:
|
511
|
+
"""
|
512
|
+
Create AMI images from EC2 instances.
|
513
|
+
|
514
|
+
Based on original aws/ec2_copy_image_cross-region.py image creation functionality.
|
515
|
+
"""
|
516
|
+
# Environment variable support
|
517
|
+
image_name_prefix = image_name_prefix or os.getenv("IMAGE_NAME_PREFIX", "Demo-Boto")
|
518
|
+
|
519
|
+
if not instance_ids:
|
520
|
+
raise ValueError("No instance IDs provided for image creation.")
|
521
|
+
|
522
|
+
ec2_resource = boto3.resource("ec2", region_name=context.region)
|
523
|
+
results = []
|
524
|
+
created_images = []
|
525
|
+
|
526
|
+
for instance_id in instance_ids:
|
527
|
+
result = self.create_operation_result(context, "create_image", "ec2:ami", instance_id)
|
528
|
+
|
529
|
+
try:
|
530
|
+
instance = ec2_resource.Instance(instance_id)
|
531
|
+
image_name = f"{image_name_prefix}-{instance_id}"
|
532
|
+
|
533
|
+
logger.info(f"Creating image for instance {instance_id} with name '{image_name}'...")
|
534
|
+
|
535
|
+
if context.dry_run:
|
536
|
+
logger.info(f"[DRY-RUN] Image creation for {instance_id} skipped.")
|
537
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
538
|
+
else:
|
539
|
+
image = instance.create_image(Name=image_name, Description=f"Image for {instance_id}")
|
540
|
+
created_images.append(image.id)
|
541
|
+
|
542
|
+
result.response_data = {"ImageId": image.id, "ImageName": image_name}
|
543
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
544
|
+
logger.info(f"Created image: {image.id}")
|
545
|
+
|
546
|
+
except ClientError as e:
|
547
|
+
error_msg = f"AWS Client Error: {e}"
|
548
|
+
logger.error(error_msg)
|
549
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
550
|
+
except BotoCoreError as e:
|
551
|
+
error_msg = f"BotoCore Error: {e}"
|
552
|
+
logger.error(error_msg)
|
553
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
554
|
+
except Exception as e:
|
555
|
+
error_msg = f"Unexpected error: {e}"
|
556
|
+
logger.error(error_msg)
|
557
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
558
|
+
|
559
|
+
results.append(result)
|
560
|
+
|
561
|
+
# SNS notification
|
562
|
+
if created_images:
|
563
|
+
message = f"Successfully created {len(created_images)} AMI images: {', '.join(created_images)}"
|
564
|
+
self.send_sns_notification("EC2 AMI Images Created", message)
|
565
|
+
logger.info(message)
|
566
|
+
|
567
|
+
return results
|
568
|
+
|
569
|
+
def delete_snapshots(
|
570
|
+
self, context: OperationContext, snapshot_ids: List[str], delete_owned_only: bool = True
|
571
|
+
) -> List[OperationResult]:
|
572
|
+
"""Delete EBS snapshots with safety checks."""
|
573
|
+
ec2_client = self.get_client("ec2", context.region)
|
574
|
+
results = []
|
575
|
+
|
576
|
+
for snapshot_id in snapshot_ids:
|
577
|
+
result = self.create_operation_result(context, "delete_snapshots", "ec2:snapshot", snapshot_id)
|
578
|
+
|
579
|
+
try:
|
580
|
+
if not self.confirm_operation(context, snapshot_id, "delete EBS snapshot"):
|
581
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
582
|
+
results.append(result)
|
583
|
+
continue
|
584
|
+
|
585
|
+
if context.dry_run:
|
586
|
+
logger.info(f"[DRY-RUN] Would delete snapshot {snapshot_id}")
|
587
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
588
|
+
results.append(result)
|
589
|
+
continue
|
590
|
+
|
591
|
+
self.execute_aws_call(ec2_client, "delete_snapshot", SnapshotId=snapshot_id)
|
592
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
593
|
+
logger.info(f"Successfully deleted snapshot {snapshot_id}")
|
594
|
+
|
595
|
+
except ClientError as e:
|
596
|
+
error_msg = f"Failed to delete snapshot {snapshot_id}: {e}"
|
597
|
+
logger.error(error_msg)
|
598
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
599
|
+
|
600
|
+
results.append(result)
|
601
|
+
|
602
|
+
return results
|
603
|
+
|
604
|
+
def cleanup_unused_volumes(self, context: OperationContext) -> List[OperationResult]:
|
605
|
+
"""
|
606
|
+
Identify unused EBS volumes with detailed reporting and SNS notifications.
|
607
|
+
|
608
|
+
Enhanced from original aws/ec2_unused_volumes.py with comprehensive scanning.
|
609
|
+
"""
|
610
|
+
ec2_client = self.get_client("ec2", context.region)
|
611
|
+
|
612
|
+
result = self.create_operation_result(context, "cleanup_unused_volumes", "ec2:volume", "scan")
|
613
|
+
|
614
|
+
try:
|
615
|
+
logger.info("🔍 Fetching all EBS volumes...")
|
616
|
+
|
617
|
+
# Get all volumes (not just available ones for comprehensive reporting)
|
618
|
+
volumes_response = self.execute_aws_call(ec2_client, "describe_volumes")
|
619
|
+
|
620
|
+
unused_volumes = []
|
621
|
+
|
622
|
+
# Enhanced loop with detailed analysis from original file
|
623
|
+
for vol in volumes_response["Volumes"]:
|
624
|
+
if len(vol.get("Attachments", [])) == 0: # Unattached volumes
|
625
|
+
# Enhanced volume details from original file
|
626
|
+
volume_details = {
|
627
|
+
"VolumeId": vol["VolumeId"],
|
628
|
+
"Size": vol["Size"],
|
629
|
+
"State": vol["State"],
|
630
|
+
"Encrypted": vol.get("Encrypted", False),
|
631
|
+
"VolumeType": vol.get("VolumeType", "unknown"),
|
632
|
+
"CreateTime": str(vol["CreateTime"]),
|
633
|
+
}
|
634
|
+
unused_volumes.append(volume_details)
|
635
|
+
|
636
|
+
# Debug logging from original file
|
637
|
+
logger.debug(f"Unattached Volume: {json.dumps(volume_details, default=str)}")
|
638
|
+
|
639
|
+
result.response_data = {
|
640
|
+
"unused_volumes": unused_volumes,
|
641
|
+
"count": len(unused_volumes),
|
642
|
+
"total_scanned": len(volumes_response["Volumes"]),
|
643
|
+
}
|
644
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
645
|
+
logger.info(
|
646
|
+
f"✅ Found {len(unused_volumes)} unused volumes out of {len(volumes_response['Volumes'])} total volumes"
|
647
|
+
)
|
648
|
+
|
649
|
+
# SNS notification with detailed report from original file
|
650
|
+
if unused_volumes:
|
651
|
+
message = f"Found {len(unused_volumes)} unused EBS volumes in {context.region}:\n"
|
652
|
+
for vol in unused_volumes[:10]: # Limit to first 10 for readability
|
653
|
+
message += f"- {vol['VolumeId']} ({vol['Size']}GB, {vol['VolumeType']}, {vol['State']})\n"
|
654
|
+
if len(unused_volumes) > 10:
|
655
|
+
message += f"... and {len(unused_volumes) - 10} more volumes"
|
656
|
+
|
657
|
+
self.send_sns_notification("Unused EBS Volumes Found", message)
|
658
|
+
|
659
|
+
except ClientError as e:
|
660
|
+
error_msg = f"❌ AWS Client Error: {e}"
|
661
|
+
logger.error(error_msg)
|
662
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
663
|
+
except BotoCoreError as e:
|
664
|
+
error_msg = f"❌ BotoCore Error: {e}"
|
665
|
+
logger.error(error_msg)
|
666
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
667
|
+
except Exception as e:
|
668
|
+
error_msg = f"❌ Unexpected error: {e}"
|
669
|
+
logger.error(error_msg)
|
670
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
671
|
+
|
672
|
+
return [result]
|
673
|
+
|
674
|
+
def cleanup_unused_eips(self, context: OperationContext) -> List[OperationResult]:
|
675
|
+
"""
|
676
|
+
Identify unused Elastic IPs with detailed reporting and SNS notifications.
|
677
|
+
|
678
|
+
Enhanced from original aws/ec2_unused_eips.py with comprehensive scanning.
|
679
|
+
"""
|
680
|
+
ec2_client = self.get_client("ec2", context.region)
|
681
|
+
|
682
|
+
result = self.create_operation_result(context, "cleanup_unused_eips", "ec2:eip", "scan")
|
683
|
+
|
684
|
+
try:
|
685
|
+
logger.info("🔍 Fetching all Elastic IP addresses...")
|
686
|
+
|
687
|
+
addresses_response = self.execute_aws_call(ec2_client, "describe_addresses")
|
688
|
+
unassociated_eips = []
|
689
|
+
eip_details = []
|
690
|
+
|
691
|
+
for address in addresses_response["Addresses"]:
|
692
|
+
# Enhanced analysis from original file
|
693
|
+
if "InstanceId" not in address and "NetworkInterfaceId" not in address:
|
694
|
+
eip_info = {
|
695
|
+
"AllocationId": address.get("AllocationId", "N/A"),
|
696
|
+
"PublicIp": address.get("PublicIp", "N/A"),
|
697
|
+
"Domain": address.get("Domain", "classic"),
|
698
|
+
"AssociationId": address.get("AssociationId"),
|
699
|
+
}
|
700
|
+
unassociated_eips.append(address.get("AllocationId", address.get("PublicIp")))
|
701
|
+
eip_details.append(eip_info)
|
702
|
+
|
703
|
+
# Debug logging from original file
|
704
|
+
logger.debug(f"Unassociated EIP: {json.dumps(eip_info, default=str)}")
|
705
|
+
|
706
|
+
result.response_data = {
|
707
|
+
"unused_eips": unassociated_eips,
|
708
|
+
"eip_details": eip_details,
|
709
|
+
"count": len(unassociated_eips),
|
710
|
+
"total_scanned": len(addresses_response["Addresses"]),
|
711
|
+
}
|
712
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
713
|
+
logger.info(
|
714
|
+
f"✅ Found {len(unassociated_eips)} unused EIPs out of {len(addresses_response['Addresses'])} total EIPs"
|
715
|
+
)
|
716
|
+
|
717
|
+
# SNS notification with detailed report
|
718
|
+
if unassociated_eips:
|
719
|
+
message = f"Found {len(unassociated_eips)} unused Elastic IPs in {context.region}:\n"
|
720
|
+
for eip in eip_details[:10]: # Limit to first 10 for readability
|
721
|
+
message += f"- {eip['PublicIp']} ({eip['AllocationId']}, {eip['Domain']})\n"
|
722
|
+
if len(eip_details) > 10:
|
723
|
+
message += f"... and {len(eip_details) - 10} more EIPs"
|
724
|
+
|
725
|
+
self.send_sns_notification("Unused Elastic IPs Found", message)
|
726
|
+
|
727
|
+
except ClientError as e:
|
728
|
+
error_msg = f"❌ AWS Client Error: {e}"
|
729
|
+
logger.error(error_msg)
|
730
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
731
|
+
except BotoCoreError as e:
|
732
|
+
error_msg = f"❌ BotoCore Error: {e}"
|
733
|
+
logger.error(error_msg)
|
734
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
735
|
+
except Exception as e:
|
736
|
+
error_msg = f"❌ Unexpected error: {e}"
|
737
|
+
logger.error(error_msg)
|
738
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
739
|
+
|
740
|
+
return [result]
|
741
|
+
|
742
|
+
def reboot_instances(self, context: OperationContext, instance_ids: List[str]) -> List[OperationResult]:
|
743
|
+
"""Reboot EC2 instances."""
|
744
|
+
ec2_client = self.get_client("ec2", context.region)
|
745
|
+
results = []
|
746
|
+
|
747
|
+
for instance_id in instance_ids:
|
748
|
+
result = self.create_operation_result(context, "reboot_instances", "ec2:instance", instance_id)
|
749
|
+
|
750
|
+
try:
|
751
|
+
if not self.confirm_operation(context, instance_id, "reboot"):
|
752
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
753
|
+
results.append(result)
|
754
|
+
continue
|
755
|
+
|
756
|
+
if context.dry_run:
|
757
|
+
logger.info(f"[DRY-RUN] Would reboot instance {instance_id}")
|
758
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
759
|
+
else:
|
760
|
+
response = self.execute_aws_call(ec2_client, "reboot_instances", InstanceIds=[instance_id])
|
761
|
+
|
762
|
+
result.response_data = response
|
763
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
764
|
+
logger.info(f"Successfully rebooted instance {instance_id}")
|
765
|
+
|
766
|
+
except ClientError as e:
|
767
|
+
error_msg = f"Failed to reboot instance {instance_id}: {e}"
|
768
|
+
logger.error(error_msg)
|
769
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
770
|
+
|
771
|
+
results.append(result)
|
772
|
+
|
773
|
+
return results
|
774
|
+
|
775
|
+
|
776
|
+
# Lambda handlers to append to ec2_operations.py
|
777
|
+
|
778
|
+
# ==============================
|
779
|
+
# AWS LAMBDA HANDLERS
|
780
|
+
# ==============================
|
781
|
+
|
782
|
+
|
783
|
+
def lambda_handler_terminate_instances(event, context):
|
784
|
+
"""
|
785
|
+
AWS Lambda handler for terminating EC2 instances.
|
786
|
+
|
787
|
+
Based on original aws/ec2_terminate_instances.py Lambda handler.
|
788
|
+
"""
|
789
|
+
try:
|
790
|
+
from runbooks.inventory.models.account import AWSAccount
|
791
|
+
from runbooks.operate.base import OperationContext
|
792
|
+
|
793
|
+
instance_ids = event.get("instance_ids", os.getenv("INSTANCE_IDS", "").split(","))
|
794
|
+
region = event.get("region", os.getenv("AWS_REGION", "us-east-1"))
|
795
|
+
|
796
|
+
if not instance_ids or instance_ids == [""]:
|
797
|
+
logger.error("No instance IDs provided in the Lambda event or environment.")
|
798
|
+
raise ValueError("Instance IDs are required to terminate EC2 instances.")
|
799
|
+
|
800
|
+
ec2_ops = EC2Operations()
|
801
|
+
account = AWSAccount(account_id="current", account_name="lambda-execution")
|
802
|
+
operation_context = OperationContext(
|
803
|
+
account=account,
|
804
|
+
region=region,
|
805
|
+
operation_type="terminate_instances",
|
806
|
+
resource_types=["ec2:instance"],
|
807
|
+
dry_run=False,
|
808
|
+
)
|
809
|
+
|
810
|
+
results = ec2_ops.terminate_instances(operation_context, instance_ids)
|
811
|
+
terminated_instances = [r.resource_id for r in results if r.success]
|
812
|
+
|
813
|
+
return {
|
814
|
+
"statusCode": 200,
|
815
|
+
"body": {
|
816
|
+
"message": "Instances terminated successfully.",
|
817
|
+
"terminated_instances": terminated_instances,
|
818
|
+
},
|
819
|
+
}
|
820
|
+
except Exception as e:
|
821
|
+
logger.error(f"Lambda function failed: {e}")
|
822
|
+
return {"statusCode": 500, "body": {"message": str(e)}}
|
823
|
+
|
824
|
+
|
825
|
+
def lambda_handler_run_instances(event, context):
|
826
|
+
"""AWS Lambda handler for launching EC2 instances."""
|
827
|
+
try:
|
828
|
+
from runbooks.inventory.models.account import AWSAccount
|
829
|
+
from runbooks.operate.base import OperationContext
|
830
|
+
|
831
|
+
region = event.get("region", os.getenv("AWS_REGION", "us-east-1"))
|
832
|
+
|
833
|
+
ec2_ops = EC2Operations()
|
834
|
+
account = AWSAccount(account_id="current", account_name="lambda-execution")
|
835
|
+
operation_context = OperationContext(
|
836
|
+
account=account,
|
837
|
+
region=region,
|
838
|
+
operation_type="run_instances",
|
839
|
+
resource_types=["ec2:instance"],
|
840
|
+
dry_run=False,
|
841
|
+
)
|
842
|
+
|
843
|
+
kwargs = {
|
844
|
+
"image_id": event.get("image_id"),
|
845
|
+
"instance_type": event.get("instance_type"),
|
846
|
+
"min_count": event.get("min_count"),
|
847
|
+
"max_count": event.get("max_count"),
|
848
|
+
"key_name": event.get("key_name"),
|
849
|
+
"security_group_ids": event.get("security_group_ids"),
|
850
|
+
"subnet_id": event.get("subnet_id"),
|
851
|
+
"tags": event.get("tags"),
|
852
|
+
}
|
853
|
+
|
854
|
+
results = ec2_ops.run_instances(operation_context, **kwargs)
|
855
|
+
|
856
|
+
if results and results[0].success:
|
857
|
+
instance_ids = [inst["InstanceId"] for inst in results[0].response_data["Instances"]]
|
858
|
+
return {
|
859
|
+
"statusCode": 200,
|
860
|
+
"body": {"message": "Instances launched successfully.", "instance_ids": instance_ids},
|
861
|
+
}
|
862
|
+
else:
|
863
|
+
return {"statusCode": 500, "body": {"message": "Failed to launch instances"}}
|
864
|
+
|
865
|
+
except Exception as e:
|
866
|
+
logger.error(f"Lambda Handler Error: {e}")
|
867
|
+
return {"statusCode": 500, "body": {"error": str(e)}}
|
868
|
+
|
869
|
+
|
870
|
+
# CLI Support
|
871
|
+
def main():
|
872
|
+
"""Main entry point for standalone execution."""
|
873
|
+
import sys
|
874
|
+
|
875
|
+
if len(sys.argv) < 2:
|
876
|
+
print("Usage: python ec2_operations.py <operation>")
|
877
|
+
print("Operations: terminate, run, cleanup-volumes, cleanup-eips")
|
878
|
+
sys.exit(1)
|
879
|
+
|
880
|
+
operation = sys.argv[1]
|
881
|
+
|
882
|
+
try:
|
883
|
+
from runbooks.inventory.models.account import AWSAccount
|
884
|
+
from runbooks.operate.base import OperationContext
|
885
|
+
|
886
|
+
ec2_ops = EC2Operations()
|
887
|
+
account = AWSAccount(account_id="current", account_name="cli-execution")
|
888
|
+
operation_context = OperationContext(
|
889
|
+
account=account,
|
890
|
+
region=os.getenv("AWS_REGION", "us-east-1"),
|
891
|
+
operation_type=operation,
|
892
|
+
resource_types=["ec2"],
|
893
|
+
dry_run=os.getenv("DRY_RUN", "false").lower() == "true",
|
894
|
+
)
|
895
|
+
|
896
|
+
if operation == "terminate":
|
897
|
+
instance_ids = os.getenv("INSTANCE_IDS", "").split(",")
|
898
|
+
if not instance_ids or instance_ids == [""]:
|
899
|
+
raise ValueError("INSTANCE_IDS environment variable is required")
|
900
|
+
results = ec2_ops.terminate_instances(operation_context, instance_ids)
|
901
|
+
|
902
|
+
elif operation == "run":
|
903
|
+
results = ec2_ops.run_instances(operation_context)
|
904
|
+
|
905
|
+
elif operation == "cleanup-volumes":
|
906
|
+
results = ec2_ops.cleanup_unused_volumes(operation_context)
|
907
|
+
|
908
|
+
elif operation == "cleanup-eips":
|
909
|
+
results = ec2_ops.cleanup_unused_eips(operation_context)
|
910
|
+
|
911
|
+
else:
|
912
|
+
raise ValueError(f"Unknown operation: {operation}")
|
913
|
+
|
914
|
+
for result in results:
|
915
|
+
if result.success:
|
916
|
+
print(f"✅ {result.operation_type} completed successfully")
|
917
|
+
else:
|
918
|
+
print(f"❌ {result.operation_type} failed: {result.error_message}")
|
919
|
+
|
920
|
+
except Exception as e:
|
921
|
+
logger.error(f"Error during operation: {e}")
|
922
|
+
sys.exit(1)
|
923
|
+
|
924
|
+
|
925
|
+
if __name__ == "__main__":
|
926
|
+
main()
|