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,812 @@
|
|
1
|
+
"""
|
2
|
+
Enterprise-Grade DynamoDB Operations Module.
|
3
|
+
|
4
|
+
Comprehensive DynamoDB resource management with Lambda support, environment configuration,
|
5
|
+
and full compatibility with original AWS Cloud Foundations scripts.
|
6
|
+
|
7
|
+
Migrated and enhanced from:
|
8
|
+
- aws/dynamodb_operations.py (with Lambda handlers, environment variables, and batch operations)
|
9
|
+
|
10
|
+
Author: CloudOps DevOps Engineer
|
11
|
+
Date: 2025-01-21
|
12
|
+
Version: 2.0.0 - Enterprise Enhancement
|
13
|
+
"""
|
14
|
+
|
15
|
+
import json
|
16
|
+
import os
|
17
|
+
from datetime import datetime
|
18
|
+
from typing import Any, Dict, List, Optional, Union
|
19
|
+
|
20
|
+
import boto3
|
21
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
22
|
+
from loguru import logger
|
23
|
+
|
24
|
+
from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
|
25
|
+
|
26
|
+
|
27
|
+
class DynamoDBOperations(BaseOperation):
|
28
|
+
"""
|
29
|
+
Enterprise-grade DynamoDB resource operations and lifecycle management.
|
30
|
+
|
31
|
+
Handles all DynamoDB-related operational tasks including table management,
|
32
|
+
data operations, backup/restore operations, and batch processing.
|
33
|
+
Supports environment variable configuration and AWS Lambda execution.
|
34
|
+
"""
|
35
|
+
|
36
|
+
service_name = "dynamodb"
|
37
|
+
supported_operations = {
|
38
|
+
"create_table",
|
39
|
+
"delete_table",
|
40
|
+
"put_item",
|
41
|
+
"delete_item",
|
42
|
+
"update_item",
|
43
|
+
"batch_write_item",
|
44
|
+
"create_backup",
|
45
|
+
"restore_table",
|
46
|
+
"update_table_throughput",
|
47
|
+
"enable_point_in_time_recovery",
|
48
|
+
}
|
49
|
+
requires_confirmation = True
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
profile: Optional[str] = None,
|
54
|
+
region: Optional[str] = None,
|
55
|
+
dry_run: bool = False,
|
56
|
+
table_name: Optional[str] = None,
|
57
|
+
):
|
58
|
+
"""
|
59
|
+
Initialize DynamoDB operations with enhanced configuration support.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
profile: AWS profile name (can be overridden by AWS_PROFILE env var)
|
63
|
+
region: AWS region (can be overridden by AWS_REGION env var)
|
64
|
+
dry_run: Dry run mode (can be overridden by DRY_RUN env var)
|
65
|
+
table_name: Default table name (can be overridden by TABLE_NAME env var)
|
66
|
+
"""
|
67
|
+
# Environment variable support for Lambda/Container deployment
|
68
|
+
self.profile = profile or os.getenv("AWS_PROFILE")
|
69
|
+
self.region = region or os.getenv("AWS_REGION", "us-east-1")
|
70
|
+
self.dry_run = dry_run or os.getenv("DRY_RUN", "false").lower() == "true"
|
71
|
+
|
72
|
+
# DynamoDB-specific environment variables from original file
|
73
|
+
self.default_table_name = table_name or os.getenv("TABLE_NAME", "employees")
|
74
|
+
self.max_batch_items = int(os.getenv("MAX_BATCH_ITEMS", "100"))
|
75
|
+
|
76
|
+
super().__init__(self.profile, self.region, self.dry_run)
|
77
|
+
|
78
|
+
# Initialize DynamoDB resource (from original file approach)
|
79
|
+
self.dynamodb_resource = None
|
80
|
+
self.default_table = None
|
81
|
+
|
82
|
+
def get_dynamodb_resource(self) -> Any:
|
83
|
+
"""Get DynamoDB resource with caching."""
|
84
|
+
if not self.dynamodb_resource:
|
85
|
+
self.dynamodb_resource = boto3.resource("dynamodb", region_name=self.region)
|
86
|
+
return self.dynamodb_resource
|
87
|
+
|
88
|
+
def get_table(self, table_name: Optional[str] = None) -> Any:
|
89
|
+
"""
|
90
|
+
Get DynamoDB table resource.
|
91
|
+
|
92
|
+
Based on original aws/dynamodb_operations.py table initialization.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
table_name: Table name (uses default if not provided)
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
DynamoDB table resource
|
99
|
+
"""
|
100
|
+
table_name = table_name or self.default_table_name
|
101
|
+
dynamodb = self.get_dynamodb_resource()
|
102
|
+
return dynamodb.Table(table_name)
|
103
|
+
|
104
|
+
def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
|
105
|
+
"""
|
106
|
+
Execute DynamoDB operation.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
context: Operation context
|
110
|
+
operation_type: Type of operation to execute
|
111
|
+
**kwargs: Operation-specific arguments
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
List of operation results
|
115
|
+
"""
|
116
|
+
self.validate_context(context)
|
117
|
+
|
118
|
+
if operation_type == "create_table":
|
119
|
+
return self.create_table(context, **kwargs)
|
120
|
+
elif operation_type == "delete_table":
|
121
|
+
return self.delete_table(context, kwargs.get("table_name"))
|
122
|
+
elif operation_type == "put_item":
|
123
|
+
return self.put_item(context, **kwargs)
|
124
|
+
elif operation_type == "delete_item":
|
125
|
+
return self.delete_item(context, **kwargs)
|
126
|
+
elif operation_type == "update_item":
|
127
|
+
return self.update_item(context, **kwargs)
|
128
|
+
elif operation_type == "batch_write_item":
|
129
|
+
return self.batch_write_item(context, **kwargs)
|
130
|
+
elif operation_type == "create_backup":
|
131
|
+
return self.create_backup(context, **kwargs)
|
132
|
+
elif operation_type == "restore_table":
|
133
|
+
return self.restore_table(context, **kwargs)
|
134
|
+
elif operation_type == "update_table_throughput":
|
135
|
+
return self.update_table_throughput(context, **kwargs)
|
136
|
+
elif operation_type == "enable_point_in_time_recovery":
|
137
|
+
return self.enable_point_in_time_recovery(context, kwargs.get("table_name"))
|
138
|
+
else:
|
139
|
+
raise ValueError(f"Unsupported operation: {operation_type}")
|
140
|
+
|
141
|
+
def create_table(
|
142
|
+
self,
|
143
|
+
context: OperationContext,
|
144
|
+
table_name: str,
|
145
|
+
key_schema: List[Dict[str, str]],
|
146
|
+
attribute_definitions: List[Dict[str, str]],
|
147
|
+
billing_mode: str = "PAY_PER_REQUEST",
|
148
|
+
provisioned_throughput: Optional[Dict[str, int]] = None,
|
149
|
+
tags: Optional[List[Dict[str, str]]] = None,
|
150
|
+
) -> List[OperationResult]:
|
151
|
+
"""
|
152
|
+
Create DynamoDB table.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
context: Operation context
|
156
|
+
table_name: Name of table to create
|
157
|
+
key_schema: Primary key schema definition
|
158
|
+
attribute_definitions: Attribute definitions
|
159
|
+
billing_mode: PAY_PER_REQUEST or PROVISIONED
|
160
|
+
provisioned_throughput: Read/write capacity units (if PROVISIONED)
|
161
|
+
tags: Table tags
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
List of operation results
|
165
|
+
"""
|
166
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
167
|
+
|
168
|
+
result = self.create_operation_result(context, "create_table", "dynamodb:table", table_name)
|
169
|
+
|
170
|
+
try:
|
171
|
+
if context.dry_run:
|
172
|
+
logger.info(f"[DRY-RUN] Would create DynamoDB table {table_name}")
|
173
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
174
|
+
return [result]
|
175
|
+
|
176
|
+
create_params = {
|
177
|
+
"TableName": table_name,
|
178
|
+
"KeySchema": key_schema,
|
179
|
+
"AttributeDefinitions": attribute_definitions,
|
180
|
+
"BillingMode": billing_mode,
|
181
|
+
}
|
182
|
+
|
183
|
+
if billing_mode == "PROVISIONED" and provisioned_throughput:
|
184
|
+
create_params["ProvisionedThroughput"] = provisioned_throughput
|
185
|
+
|
186
|
+
if tags:
|
187
|
+
create_params["Tags"] = tags
|
188
|
+
|
189
|
+
response = self.execute_aws_call(dynamodb_client, "create_table", **create_params)
|
190
|
+
|
191
|
+
result.response_data = response
|
192
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
193
|
+
logger.info(f"Successfully created DynamoDB table {table_name}")
|
194
|
+
|
195
|
+
except ClientError as e:
|
196
|
+
error_msg = f"Failed to create DynamoDB table {table_name}: {e}"
|
197
|
+
logger.error(error_msg)
|
198
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
199
|
+
|
200
|
+
return [result]
|
201
|
+
|
202
|
+
def delete_table(self, context: OperationContext, table_name: str) -> List[OperationResult]:
|
203
|
+
"""
|
204
|
+
Delete DynamoDB table.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
context: Operation context
|
208
|
+
table_name: Name of table to delete
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
List of operation results
|
212
|
+
"""
|
213
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
214
|
+
|
215
|
+
result = self.create_operation_result(context, "delete_table", "dynamodb:table", table_name)
|
216
|
+
|
217
|
+
try:
|
218
|
+
if not self.confirm_operation(context, table_name, "delete table"):
|
219
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
220
|
+
return [result]
|
221
|
+
|
222
|
+
if context.dry_run:
|
223
|
+
logger.info(f"[DRY-RUN] Would delete DynamoDB table {table_name}")
|
224
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
225
|
+
else:
|
226
|
+
response = self.execute_aws_call(dynamodb_client, "delete_table", TableName=table_name)
|
227
|
+
|
228
|
+
result.response_data = response
|
229
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
230
|
+
logger.info(f"Successfully deleted DynamoDB table {table_name}")
|
231
|
+
|
232
|
+
except ClientError as e:
|
233
|
+
error_msg = f"Failed to delete DynamoDB table {table_name}: {e}"
|
234
|
+
logger.error(error_msg)
|
235
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
236
|
+
|
237
|
+
return [result]
|
238
|
+
|
239
|
+
def put_item(
|
240
|
+
self,
|
241
|
+
context: OperationContext,
|
242
|
+
table_name: str,
|
243
|
+
item: Dict[str, Any],
|
244
|
+
condition_expression: Optional[str] = None,
|
245
|
+
) -> List[OperationResult]:
|
246
|
+
"""
|
247
|
+
Put item into DynamoDB table.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
context: Operation context
|
251
|
+
table_name: Target table name
|
252
|
+
item: Item data to insert
|
253
|
+
condition_expression: Optional condition for put operation
|
254
|
+
|
255
|
+
Returns:
|
256
|
+
List of operation results
|
257
|
+
"""
|
258
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
259
|
+
|
260
|
+
# Generate item identifier for tracking
|
261
|
+
item_id = str(item.get("id", item.get("pk", "unknown")))
|
262
|
+
|
263
|
+
result = self.create_operation_result(context, "put_item", "dynamodb:item", f"{table_name}#{item_id}")
|
264
|
+
|
265
|
+
try:
|
266
|
+
if context.dry_run:
|
267
|
+
logger.info(f"[DRY-RUN] Would put item in table {table_name}")
|
268
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
269
|
+
else:
|
270
|
+
put_params = {"TableName": table_name, "Item": item}
|
271
|
+
|
272
|
+
if condition_expression:
|
273
|
+
put_params["ConditionExpression"] = condition_expression
|
274
|
+
|
275
|
+
response = self.execute_aws_call(dynamodb_client, "put_item", **put_params)
|
276
|
+
|
277
|
+
result.response_data = response
|
278
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
279
|
+
logger.info(f"Successfully put item in table {table_name}")
|
280
|
+
|
281
|
+
except ClientError as e:
|
282
|
+
error_msg = f"Failed to put item in table {table_name}: {e}"
|
283
|
+
logger.error(error_msg)
|
284
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
285
|
+
|
286
|
+
return [result]
|
287
|
+
|
288
|
+
def delete_item(
|
289
|
+
self,
|
290
|
+
context: OperationContext,
|
291
|
+
table_name: str,
|
292
|
+
key: Dict[str, Any],
|
293
|
+
condition_expression: Optional[str] = None,
|
294
|
+
) -> List[OperationResult]:
|
295
|
+
"""
|
296
|
+
Delete item from DynamoDB table.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
context: Operation context
|
300
|
+
table_name: Source table name
|
301
|
+
key: Primary key of item to delete
|
302
|
+
condition_expression: Optional condition for delete operation
|
303
|
+
|
304
|
+
Returns:
|
305
|
+
List of operation results
|
306
|
+
"""
|
307
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
308
|
+
|
309
|
+
# Generate item identifier for tracking
|
310
|
+
key_str = str(key.get("id", key.get("pk", "unknown")))
|
311
|
+
|
312
|
+
result = self.create_operation_result(context, "delete_item", "dynamodb:item", f"{table_name}#{key_str}")
|
313
|
+
|
314
|
+
try:
|
315
|
+
if not self.confirm_operation(context, f"{table_name}#{key_str}", "delete item"):
|
316
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
317
|
+
return [result]
|
318
|
+
|
319
|
+
if context.dry_run:
|
320
|
+
logger.info(f"[DRY-RUN] Would delete item from table {table_name}")
|
321
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
322
|
+
else:
|
323
|
+
delete_params = {"TableName": table_name, "Key": key}
|
324
|
+
|
325
|
+
if condition_expression:
|
326
|
+
delete_params["ConditionExpression"] = condition_expression
|
327
|
+
|
328
|
+
response = self.execute_aws_call(dynamodb_client, "delete_item", **delete_params)
|
329
|
+
|
330
|
+
result.response_data = response
|
331
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
332
|
+
logger.info(f"Successfully deleted item from table {table_name}")
|
333
|
+
|
334
|
+
except ClientError as e:
|
335
|
+
error_msg = f"Failed to delete item from table {table_name}: {e}"
|
336
|
+
logger.error(error_msg)
|
337
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
338
|
+
|
339
|
+
return [result]
|
340
|
+
|
341
|
+
def update_item(
|
342
|
+
self,
|
343
|
+
context: OperationContext,
|
344
|
+
table_name: str,
|
345
|
+
key: Dict[str, Any],
|
346
|
+
update_expression: str,
|
347
|
+
expression_attribute_values: Optional[Dict[str, Any]] = None,
|
348
|
+
expression_attribute_names: Optional[Dict[str, str]] = None,
|
349
|
+
condition_expression: Optional[str] = None,
|
350
|
+
) -> List[OperationResult]:
|
351
|
+
"""
|
352
|
+
Update item in DynamoDB table.
|
353
|
+
|
354
|
+
Args:
|
355
|
+
context: Operation context
|
356
|
+
table_name: Target table name
|
357
|
+
key: Primary key of item to update
|
358
|
+
update_expression: Update expression
|
359
|
+
expression_attribute_values: Attribute values for expression
|
360
|
+
expression_attribute_names: Attribute names for expression
|
361
|
+
condition_expression: Optional condition for update operation
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
List of operation results
|
365
|
+
"""
|
366
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
367
|
+
|
368
|
+
# Generate item identifier for tracking
|
369
|
+
key_str = str(key.get("id", key.get("pk", "unknown")))
|
370
|
+
|
371
|
+
result = self.create_operation_result(context, "update_item", "dynamodb:item", f"{table_name}#{key_str}")
|
372
|
+
|
373
|
+
try:
|
374
|
+
if context.dry_run:
|
375
|
+
logger.info(f"[DRY-RUN] Would update item in table {table_name}")
|
376
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
377
|
+
else:
|
378
|
+
update_params = {"TableName": table_name, "Key": key, "UpdateExpression": update_expression}
|
379
|
+
|
380
|
+
if expression_attribute_values:
|
381
|
+
update_params["ExpressionAttributeValues"] = expression_attribute_values
|
382
|
+
if expression_attribute_names:
|
383
|
+
update_params["ExpressionAttributeNames"] = expression_attribute_names
|
384
|
+
if condition_expression:
|
385
|
+
update_params["ConditionExpression"] = condition_expression
|
386
|
+
|
387
|
+
response = self.execute_aws_call(dynamodb_client, "update_item", **update_params)
|
388
|
+
|
389
|
+
result.response_data = response
|
390
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
391
|
+
logger.info(f"Successfully updated item in table {table_name}")
|
392
|
+
|
393
|
+
except ClientError as e:
|
394
|
+
error_msg = f"Failed to update item in table {table_name}: {e}"
|
395
|
+
logger.error(error_msg)
|
396
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
397
|
+
|
398
|
+
return [result]
|
399
|
+
|
400
|
+
def batch_write_item(
|
401
|
+
self, context: OperationContext, request_items: Dict[str, List[Dict[str, Any]]]
|
402
|
+
) -> List[OperationResult]:
|
403
|
+
"""
|
404
|
+
Batch write items to DynamoDB tables.
|
405
|
+
|
406
|
+
Args:
|
407
|
+
context: Operation context
|
408
|
+
request_items: Batch write request items by table name
|
409
|
+
|
410
|
+
Returns:
|
411
|
+
List of operation results
|
412
|
+
"""
|
413
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
414
|
+
|
415
|
+
result = self.create_operation_result(context, "batch_write_item", "dynamodb:batch", "batch_operation")
|
416
|
+
|
417
|
+
try:
|
418
|
+
if context.dry_run:
|
419
|
+
item_count = sum(len(items) for items in request_items.values())
|
420
|
+
logger.info(f"[DRY-RUN] Would batch write {item_count} items")
|
421
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
422
|
+
else:
|
423
|
+
response = self.execute_aws_call(dynamodb_client, "batch_write_item", RequestItems=request_items)
|
424
|
+
|
425
|
+
result.response_data = response
|
426
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
427
|
+
|
428
|
+
processed_count = sum(len(items) for items in request_items.values())
|
429
|
+
unprocessed_count = len(response.get("UnprocessedItems", {}))
|
430
|
+
logger.info(
|
431
|
+
f"Successfully processed {processed_count - unprocessed_count} items, {unprocessed_count} unprocessed"
|
432
|
+
)
|
433
|
+
|
434
|
+
except ClientError as e:
|
435
|
+
error_msg = f"Failed to batch write items: {e}"
|
436
|
+
logger.error(error_msg)
|
437
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
438
|
+
|
439
|
+
return [result]
|
440
|
+
|
441
|
+
def create_backup(
|
442
|
+
self, context: OperationContext, table_name: str, backup_name: Optional[str] = None
|
443
|
+
) -> List[OperationResult]:
|
444
|
+
"""
|
445
|
+
Create backup of DynamoDB table.
|
446
|
+
|
447
|
+
Args:
|
448
|
+
context: Operation context
|
449
|
+
table_name: Table to backup
|
450
|
+
backup_name: Name for backup (defaults to table_name_timestamp)
|
451
|
+
|
452
|
+
Returns:
|
453
|
+
List of operation results
|
454
|
+
"""
|
455
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
456
|
+
|
457
|
+
if not backup_name:
|
458
|
+
backup_name = f"{table_name}_backup_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}"
|
459
|
+
|
460
|
+
result = self.create_operation_result(context, "create_backup", "dynamodb:backup", backup_name)
|
461
|
+
|
462
|
+
try:
|
463
|
+
if context.dry_run:
|
464
|
+
logger.info(f"[DRY-RUN] Would create backup {backup_name} for table {table_name}")
|
465
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
466
|
+
else:
|
467
|
+
response = self.execute_aws_call(
|
468
|
+
dynamodb_client, "create_backup", TableName=table_name, BackupName=backup_name
|
469
|
+
)
|
470
|
+
|
471
|
+
result.response_data = response
|
472
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
473
|
+
logger.info(f"Successfully created backup {backup_name} for table {table_name}")
|
474
|
+
|
475
|
+
except ClientError as e:
|
476
|
+
error_msg = f"Failed to create backup for table {table_name}: {e}"
|
477
|
+
logger.error(error_msg)
|
478
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
479
|
+
|
480
|
+
return [result]
|
481
|
+
|
482
|
+
def restore_table(
|
483
|
+
self,
|
484
|
+
context: OperationContext,
|
485
|
+
source_table_arn: str,
|
486
|
+
target_table_name: str,
|
487
|
+
restore_date_time: Optional[datetime] = None,
|
488
|
+
billing_mode_override: Optional[str] = None,
|
489
|
+
) -> List[OperationResult]:
|
490
|
+
"""
|
491
|
+
Restore DynamoDB table from backup or point-in-time.
|
492
|
+
|
493
|
+
Args:
|
494
|
+
context: Operation context
|
495
|
+
source_table_arn: ARN of source table or backup
|
496
|
+
target_table_name: Name for restored table
|
497
|
+
restore_date_time: Point-in-time to restore to
|
498
|
+
billing_mode_override: Override billing mode for restored table
|
499
|
+
|
500
|
+
Returns:
|
501
|
+
List of operation results
|
502
|
+
"""
|
503
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
504
|
+
|
505
|
+
result = self.create_operation_result(context, "restore_table", "dynamodb:table", target_table_name)
|
506
|
+
|
507
|
+
try:
|
508
|
+
if context.dry_run:
|
509
|
+
logger.info(f"[DRY-RUN] Would restore table {target_table_name}")
|
510
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
511
|
+
else:
|
512
|
+
restore_params = {"TargetTableName": target_table_name, "SourceTableArn": source_table_arn}
|
513
|
+
|
514
|
+
if restore_date_time:
|
515
|
+
restore_params["RestoreDateTime"] = restore_date_time
|
516
|
+
if billing_mode_override:
|
517
|
+
restore_params["BillingModeOverride"] = billing_mode_override
|
518
|
+
|
519
|
+
response = self.execute_aws_call(dynamodb_client, "restore_table_from_backup", **restore_params)
|
520
|
+
|
521
|
+
result.response_data = response
|
522
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
523
|
+
logger.info(f"Successfully initiated restore of table {target_table_name}")
|
524
|
+
|
525
|
+
except ClientError as e:
|
526
|
+
error_msg = f"Failed to restore table {target_table_name}: {e}"
|
527
|
+
logger.error(error_msg)
|
528
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
529
|
+
|
530
|
+
return [result]
|
531
|
+
|
532
|
+
def update_table_throughput(
|
533
|
+
self, context: OperationContext, table_name: str, provisioned_throughput: Dict[str, int]
|
534
|
+
) -> List[OperationResult]:
|
535
|
+
"""
|
536
|
+
Update DynamoDB table throughput settings.
|
537
|
+
|
538
|
+
Args:
|
539
|
+
context: Operation context
|
540
|
+
table_name: Table to update
|
541
|
+
provisioned_throughput: New read/write capacity units
|
542
|
+
|
543
|
+
Returns:
|
544
|
+
List of operation results
|
545
|
+
"""
|
546
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
547
|
+
|
548
|
+
result = self.create_operation_result(context, "update_table_throughput", "dynamodb:table", table_name)
|
549
|
+
|
550
|
+
try:
|
551
|
+
if context.dry_run:
|
552
|
+
logger.info(f"[DRY-RUN] Would update throughput for table {table_name}")
|
553
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
554
|
+
else:
|
555
|
+
response = self.execute_aws_call(
|
556
|
+
dynamodb_client, "update_table", TableName=table_name, ProvisionedThroughput=provisioned_throughput
|
557
|
+
)
|
558
|
+
|
559
|
+
result.response_data = response
|
560
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
561
|
+
logger.info(f"Successfully updated throughput for table {table_name}")
|
562
|
+
|
563
|
+
except ClientError as e:
|
564
|
+
error_msg = f"Failed to update throughput for table {table_name}: {e}"
|
565
|
+
logger.error(error_msg)
|
566
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
567
|
+
|
568
|
+
return [result]
|
569
|
+
|
570
|
+
def enable_point_in_time_recovery(self, context: OperationContext, table_name: str) -> List[OperationResult]:
|
571
|
+
"""
|
572
|
+
Enable point-in-time recovery for DynamoDB table.
|
573
|
+
|
574
|
+
Args:
|
575
|
+
context: Operation context
|
576
|
+
table_name: Table to enable PITR for
|
577
|
+
|
578
|
+
Returns:
|
579
|
+
List of operation results
|
580
|
+
"""
|
581
|
+
dynamodb_client = self.get_client("dynamodb", context.region)
|
582
|
+
|
583
|
+
result = self.create_operation_result(context, "enable_point_in_time_recovery", "dynamodb:table", table_name)
|
584
|
+
|
585
|
+
try:
|
586
|
+
if context.dry_run:
|
587
|
+
logger.info(f"[DRY-RUN] Would enable PITR for table {table_name}")
|
588
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
589
|
+
else:
|
590
|
+
response = self.execute_aws_call(
|
591
|
+
dynamodb_client,
|
592
|
+
"update_continuous_backups",
|
593
|
+
TableName=table_name,
|
594
|
+
PointInTimeRecoverySpecification={"PointInTimeRecoveryEnabled": True},
|
595
|
+
)
|
596
|
+
|
597
|
+
result.response_data = response
|
598
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
599
|
+
logger.info(f"Successfully enabled PITR for table {table_name}")
|
600
|
+
|
601
|
+
except ClientError as e:
|
602
|
+
error_msg = f"Failed to enable PITR for table {table_name}: {e}"
|
603
|
+
logger.error(error_msg)
|
604
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
605
|
+
|
606
|
+
return [result]
|
607
|
+
|
608
|
+
def batch_write_items_enhanced(
|
609
|
+
self, context: OperationContext, table_name: Optional[str] = None, batch_size: Optional[int] = None
|
610
|
+
) -> List[OperationResult]:
|
611
|
+
"""
|
612
|
+
Batch write multiple items efficiently using resource-based approach.
|
613
|
+
|
614
|
+
Enhanced from original aws/dynamodb_operations.py batch_write_items functionality.
|
615
|
+
|
616
|
+
Args:
|
617
|
+
context: Operation context
|
618
|
+
table_name: Target table name (uses default if not provided)
|
619
|
+
batch_size: Number of items to write (uses MAX_BATCH_ITEMS env var if not provided)
|
620
|
+
|
621
|
+
Returns:
|
622
|
+
List of operation results
|
623
|
+
"""
|
624
|
+
# Environment variable support from original file
|
625
|
+
table_name = table_name or self.default_table_name
|
626
|
+
batch_size = batch_size or self.max_batch_items
|
627
|
+
|
628
|
+
# Use resource-based approach from original file
|
629
|
+
table = self.get_table(table_name)
|
630
|
+
|
631
|
+
result = self.create_operation_result(
|
632
|
+
context, "batch_write_items", "dynamodb:table", f"{table_name}#{batch_size}items"
|
633
|
+
)
|
634
|
+
|
635
|
+
try:
|
636
|
+
logger.info(f"🚀 Starting batch write with {batch_size} items...")
|
637
|
+
|
638
|
+
if context.dry_run:
|
639
|
+
logger.info(f"[DRY-RUN] Would batch write {batch_size} items to table {table_name}")
|
640
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
641
|
+
return [result]
|
642
|
+
|
643
|
+
# Use batch_writer from original file
|
644
|
+
with table.batch_writer() as batch:
|
645
|
+
for i in range(batch_size):
|
646
|
+
batch.put_item(
|
647
|
+
Item={
|
648
|
+
"emp_id": str(i),
|
649
|
+
"name": f"Name-{i}",
|
650
|
+
"salary": 50000 + i * 100, # Incremental salary from original
|
651
|
+
}
|
652
|
+
)
|
653
|
+
|
654
|
+
result.response_data = {"items_written": batch_size, "table": table_name}
|
655
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
656
|
+
logger.info(f"✅ Batch write completed successfully with {batch_size} items.")
|
657
|
+
|
658
|
+
except ClientError as e:
|
659
|
+
error_msg = f"❌ AWS Client Error: {e}"
|
660
|
+
logger.error(error_msg)
|
661
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
662
|
+
except BotoCoreError as e:
|
663
|
+
error_msg = f"❌ BotoCore Error: {e}"
|
664
|
+
logger.error(error_msg)
|
665
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
666
|
+
except Exception as e:
|
667
|
+
error_msg = f"❌ Unexpected Error: {e}"
|
668
|
+
logger.error(error_msg)
|
669
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
670
|
+
|
671
|
+
return [result]
|
672
|
+
|
673
|
+
|
674
|
+
# ==============================
|
675
|
+
# AWS LAMBDA HANDLERS
|
676
|
+
# ==============================
|
677
|
+
|
678
|
+
|
679
|
+
def lambda_handler_dynamodb_operations(event, context):
|
680
|
+
"""
|
681
|
+
AWS Lambda handler for DynamoDB operations.
|
682
|
+
|
683
|
+
Based on original aws/dynamodb_operations.py Lambda handler with enhanced action routing.
|
684
|
+
|
685
|
+
Args:
|
686
|
+
event (dict): AWS Lambda event with action details
|
687
|
+
context: AWS Lambda context object
|
688
|
+
"""
|
689
|
+
try:
|
690
|
+
from runbooks.inventory.models.account import AWSAccount
|
691
|
+
from runbooks.operate.base import OperationContext
|
692
|
+
|
693
|
+
action = event.get("action")
|
694
|
+
emp_id = event.get("emp_id")
|
695
|
+
name = event.get("name")
|
696
|
+
salary = event.get("salary", 0)
|
697
|
+
batch_size = int(event.get("batch_size", os.getenv("MAX_BATCH_ITEMS", "100")))
|
698
|
+
table_name = event.get("table_name", os.getenv("TABLE_NAME", "employees"))
|
699
|
+
region = event.get("region", os.getenv("AWS_REGION", "us-east-1"))
|
700
|
+
|
701
|
+
dynamodb_ops = DynamoDBOperations(table_name=table_name)
|
702
|
+
account = AWSAccount(account_id="current", account_name="lambda-execution")
|
703
|
+
operation_context = OperationContext(
|
704
|
+
account=account, region=region, operation_type=action, resource_types=["dynamodb:table"], dry_run=False
|
705
|
+
)
|
706
|
+
|
707
|
+
if action == "put":
|
708
|
+
results = dynamodb_ops.put_item(
|
709
|
+
operation_context, table_name=table_name, emp_id=emp_id, name=name, salary=salary
|
710
|
+
)
|
711
|
+
return {"statusCode": 200, "body": f"Item {emp_id} inserted."}
|
712
|
+
|
713
|
+
elif action == "delete":
|
714
|
+
results = dynamodb_ops.delete_item(operation_context, table_name=table_name, key={"emp_id": emp_id})
|
715
|
+
if results and results[0].success:
|
716
|
+
item = results[0].response_data
|
717
|
+
return {"statusCode": 200, "body": f"Item {item} deleted."}
|
718
|
+
else:
|
719
|
+
return {"statusCode": 500, "body": "Failed to delete item"}
|
720
|
+
|
721
|
+
elif action == "batch_write":
|
722
|
+
results = dynamodb_ops.batch_write_items_enhanced(
|
723
|
+
operation_context, table_name=table_name, batch_size=batch_size
|
724
|
+
)
|
725
|
+
return {"statusCode": 200, "body": "Batch write completed."}
|
726
|
+
|
727
|
+
else:
|
728
|
+
raise ValueError("Invalid action. Use 'put', 'delete', or 'batch_write'.")
|
729
|
+
|
730
|
+
except Exception as e:
|
731
|
+
logger.error(f"❌ Lambda Error: {e}")
|
732
|
+
return {"statusCode": 500, "body": str(e)}
|
733
|
+
|
734
|
+
|
735
|
+
# ==============================
|
736
|
+
# SCRIPT ENTRY POINT (CLI Support)
|
737
|
+
# ==============================
|
738
|
+
|
739
|
+
|
740
|
+
def main():
|
741
|
+
"""
|
742
|
+
Main entry point for standalone execution (CLI or Docker).
|
743
|
+
|
744
|
+
Provides compatibility with original AWS script execution patterns.
|
745
|
+
"""
|
746
|
+
import sys
|
747
|
+
|
748
|
+
if len(sys.argv) < 2:
|
749
|
+
print("Usage: python dynamodb_operations.py <operation> [args...]")
|
750
|
+
print("Operations: put, delete, batch-write, create-table, backup-table")
|
751
|
+
sys.exit(1)
|
752
|
+
|
753
|
+
operation = sys.argv[1]
|
754
|
+
|
755
|
+
try:
|
756
|
+
from runbooks.inventory.models.account import AWSAccount
|
757
|
+
from runbooks.operate.base import OperationContext
|
758
|
+
|
759
|
+
dynamodb_ops = DynamoDBOperations()
|
760
|
+
account = AWSAccount(account_id="current", account_name="cli-execution")
|
761
|
+
operation_context = OperationContext(
|
762
|
+
account=account,
|
763
|
+
region=os.getenv("AWS_REGION", "us-east-1"),
|
764
|
+
operation_type=operation.replace("-", "_"),
|
765
|
+
resource_types=["dynamodb"],
|
766
|
+
dry_run=os.getenv("DRY_RUN", "false").lower() == "true",
|
767
|
+
)
|
768
|
+
|
769
|
+
if operation == "put":
|
770
|
+
# Example: put item with environment variables or defaults
|
771
|
+
results = dynamodb_ops.put_item(operation_context, emp_id="2", name="John Doe", salary=75000)
|
772
|
+
|
773
|
+
elif operation == "delete":
|
774
|
+
# Example: delete item
|
775
|
+
results = dynamodb_ops.delete_item(operation_context, key={"emp_id": "2"})
|
776
|
+
|
777
|
+
elif operation == "batch-write":
|
778
|
+
# Example: batch write items
|
779
|
+
batch_size = int(os.getenv("MAX_BATCH_ITEMS", "100"))
|
780
|
+
results = dynamodb_ops.batch_write_items_enhanced(operation_context, batch_size=batch_size)
|
781
|
+
|
782
|
+
elif operation == "create-table":
|
783
|
+
# Example: create table
|
784
|
+
table_name = os.getenv("TABLE_NAME", "employees")
|
785
|
+
key_schema = [{"AttributeName": "emp_id", "KeyType": "HASH"}]
|
786
|
+
attribute_definitions = [{"AttributeName": "emp_id", "AttributeType": "S"}]
|
787
|
+
results = dynamodb_ops.create_table(operation_context, table_name, key_schema, attribute_definitions)
|
788
|
+
|
789
|
+
elif operation == "backup-table":
|
790
|
+
# Example: backup table
|
791
|
+
table_name = os.getenv("TABLE_NAME", "employees")
|
792
|
+
results = dynamodb_ops.create_backup(operation_context, table_name)
|
793
|
+
|
794
|
+
else:
|
795
|
+
raise ValueError(f"Unknown operation: {operation}")
|
796
|
+
|
797
|
+
# Print results
|
798
|
+
for result in results:
|
799
|
+
if result.success:
|
800
|
+
print(f"✅ {result.operation_type} completed successfully")
|
801
|
+
if result.response_data:
|
802
|
+
print(f" Data: {json.dumps(result.response_data, default=str, indent=2)}")
|
803
|
+
else:
|
804
|
+
print(f"❌ {result.operation_type} failed: {result.error_message}")
|
805
|
+
|
806
|
+
except Exception as e:
|
807
|
+
logger.error(f"Error during operation: {e}")
|
808
|
+
sys.exit(1)
|
809
|
+
|
810
|
+
|
811
|
+
if __name__ == "__main__":
|
812
|
+
main()
|