runbooks 0.7.0__py3-none-any.whl → 0.7.6__py3-none-any.whl

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