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,496 @@
1
+ """
2
+ CloudWatch Operations Module.
3
+
4
+ Provides comprehensive CloudWatch resource management capabilities including
5
+ log group management, retention policies, and monitoring configuration.
6
+
7
+ Migrated and enhanced from:
8
+ - inventory/update_cloudwatch_logs_retention_policy.py
9
+ """
10
+
11
+ import json
12
+ from datetime import datetime
13
+ from typing import Any, Dict, List, Optional, Union
14
+
15
+ import boto3
16
+ from botocore.exceptions import ClientError
17
+ from loguru import logger
18
+
19
+ from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
20
+
21
+
22
+ class CloudWatchOperations(BaseOperation):
23
+ """
24
+ CloudWatch resource operations and lifecycle management.
25
+
26
+ Handles all CloudWatch-related operational tasks including log group management,
27
+ retention policy updates, and monitoring configuration.
28
+ """
29
+
30
+ service_name = "cloudwatch"
31
+ supported_operations = {
32
+ "create_log_group",
33
+ "delete_log_group",
34
+ "update_log_retention_policy",
35
+ "create_metric_alarm",
36
+ "delete_metric_alarm",
37
+ "put_metric_data",
38
+ "create_dashboard",
39
+ "delete_dashboard",
40
+ "tag_log_group",
41
+ "untag_log_group",
42
+ }
43
+ requires_confirmation = True
44
+
45
+ def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = False):
46
+ """Initialize CloudWatch operations."""
47
+ super().__init__(profile, region, dry_run)
48
+
49
+ def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
50
+ """
51
+ Execute CloudWatch operation.
52
+
53
+ Args:
54
+ context: Operation context
55
+ operation_type: Type of operation to execute
56
+ **kwargs: Operation-specific arguments
57
+
58
+ Returns:
59
+ List of operation results
60
+ """
61
+ self.validate_context(context)
62
+
63
+ if operation_type == "create_log_group":
64
+ return self.create_log_group(context, **kwargs)
65
+ elif operation_type == "delete_log_group":
66
+ return self.delete_log_group(context, kwargs.get("log_group_name"))
67
+ elif operation_type == "update_log_retention_policy":
68
+ return self.update_log_retention_policy(context, **kwargs)
69
+ elif operation_type == "create_metric_alarm":
70
+ return self.create_metric_alarm(context, **kwargs)
71
+ elif operation_type == "delete_metric_alarm":
72
+ return self.delete_metric_alarm(context, kwargs.get("alarm_name"))
73
+ elif operation_type == "put_metric_data":
74
+ return self.put_metric_data(context, **kwargs)
75
+ elif operation_type == "create_dashboard":
76
+ return self.create_dashboard(context, **kwargs)
77
+ elif operation_type == "delete_dashboard":
78
+ return self.delete_dashboard(context, kwargs.get("dashboard_name"))
79
+ elif operation_type == "tag_log_group":
80
+ return self.tag_log_group(context, **kwargs)
81
+ elif operation_type == "untag_log_group":
82
+ return self.untag_log_group(context, **kwargs)
83
+ else:
84
+ raise ValueError(f"Unsupported operation: {operation_type}")
85
+
86
+ def create_log_group(
87
+ self,
88
+ context: OperationContext,
89
+ log_group_name: str,
90
+ kms_key_id: Optional[str] = None,
91
+ tags: Optional[Dict[str, str]] = None,
92
+ retention_in_days: Optional[int] = None,
93
+ ) -> List[OperationResult]:
94
+ """
95
+ Create CloudWatch log group.
96
+
97
+ Args:
98
+ context: Operation context
99
+ log_group_name: Name of log group to create
100
+ kms_key_id: KMS key ID for encryption
101
+ tags: Log group tags
102
+ retention_in_days: Log retention period in days
103
+
104
+ Returns:
105
+ List of operation results
106
+ """
107
+ logs_client = self.get_client("logs", context.region)
108
+
109
+ result = self.create_operation_result(context, "create_log_group", "logs:log_group", log_group_name)
110
+
111
+ try:
112
+ if context.dry_run:
113
+ logger.info(f"[DRY-RUN] Would create log group {log_group_name}")
114
+ result.mark_completed(OperationStatus.DRY_RUN)
115
+ return [result]
116
+
117
+ create_params = {"logGroupName": log_group_name}
118
+
119
+ if kms_key_id:
120
+ create_params["kmsKeyId"] = kms_key_id
121
+ if tags:
122
+ create_params["tags"] = tags
123
+
124
+ response = self.execute_aws_call(logs_client, "create_log_group", **create_params)
125
+
126
+ # Set retention policy if specified
127
+ if retention_in_days:
128
+ self.execute_aws_call(
129
+ logs_client, "put_retention_policy", logGroupName=log_group_name, retentionInDays=retention_in_days
130
+ )
131
+ logger.info(f"Set retention policy to {retention_in_days} days for {log_group_name}")
132
+
133
+ result.response_data = response
134
+ result.mark_completed(OperationStatus.SUCCESS)
135
+ logger.info(f"Successfully created log group {log_group_name}")
136
+
137
+ except ClientError as e:
138
+ error_msg = f"Failed to create log group {log_group_name}: {e}"
139
+ logger.error(error_msg)
140
+ result.mark_completed(OperationStatus.FAILED, error_msg)
141
+
142
+ return [result]
143
+
144
+ def delete_log_group(self, context: OperationContext, log_group_name: str) -> List[OperationResult]:
145
+ """
146
+ Delete CloudWatch log group.
147
+
148
+ Args:
149
+ context: Operation context
150
+ log_group_name: Name of log group to delete
151
+
152
+ Returns:
153
+ List of operation results
154
+ """
155
+ logs_client = self.get_client("logs", context.region)
156
+
157
+ result = self.create_operation_result(context, "delete_log_group", "logs:log_group", log_group_name)
158
+
159
+ try:
160
+ if not self.confirm_operation(context, log_group_name, "delete log group"):
161
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
162
+ return [result]
163
+
164
+ if context.dry_run:
165
+ logger.info(f"[DRY-RUN] Would delete log group {log_group_name}")
166
+ result.mark_completed(OperationStatus.DRY_RUN)
167
+ else:
168
+ response = self.execute_aws_call(logs_client, "delete_log_group", logGroupName=log_group_name)
169
+
170
+ result.response_data = response
171
+ result.mark_completed(OperationStatus.SUCCESS)
172
+ logger.info(f"Successfully deleted log group {log_group_name}")
173
+
174
+ except ClientError as e:
175
+ error_msg = f"Failed to delete log group {log_group_name}: {e}"
176
+ logger.error(error_msg)
177
+ result.mark_completed(OperationStatus.FAILED, error_msg)
178
+
179
+ return [result]
180
+
181
+ def update_log_retention_policy(
182
+ self,
183
+ context: OperationContext,
184
+ log_group_name: Optional[str] = None,
185
+ retention_in_days: int = 30,
186
+ update_all_log_groups: bool = False,
187
+ ) -> List[OperationResult]:
188
+ """
189
+ Update CloudWatch log retention policy.
190
+
191
+ Migrated from inventory/update_cloudwatch_logs_retention_policy.py
192
+
193
+ Args:
194
+ context: Operation context
195
+ log_group_name: Specific log group name (if None, updates all)
196
+ retention_in_days: Retention period in days
197
+ update_all_log_groups: Whether to update all log groups
198
+
199
+ Returns:
200
+ List of operation results
201
+ """
202
+ logs_client = self.get_client("logs", context.region)
203
+ results = []
204
+
205
+ try:
206
+ if log_group_name:
207
+ # Update specific log group
208
+ log_groups = [log_group_name]
209
+ elif update_all_log_groups:
210
+ # Get all log groups
211
+ paginator = logs_client.get_paginator("describe_log_groups")
212
+ log_groups = []
213
+
214
+ for page in paginator.paginate():
215
+ for log_group in page["logGroups"]:
216
+ log_groups.append(log_group["logGroupName"])
217
+ else:
218
+ raise ValueError("Either log_group_name must be specified or update_all_log_groups must be True")
219
+
220
+ for lg_name in log_groups:
221
+ result = self.create_operation_result(context, "update_log_retention_policy", "logs:log_group", lg_name)
222
+
223
+ try:
224
+ if context.dry_run:
225
+ logger.info(f"[DRY-RUN] Would set retention to {retention_in_days} days for {lg_name}")
226
+ result.mark_completed(OperationStatus.DRY_RUN)
227
+ else:
228
+ response = self.execute_aws_call(
229
+ logs_client, "put_retention_policy", logGroupName=lg_name, retentionInDays=retention_in_days
230
+ )
231
+
232
+ result.response_data = response
233
+ result.mark_completed(OperationStatus.SUCCESS)
234
+ logger.info(f"Successfully updated retention policy for {lg_name}")
235
+
236
+ except ClientError as e:
237
+ error_msg = f"Failed to update retention policy for {lg_name}: {e}"
238
+ logger.error(error_msg)
239
+ result.mark_completed(OperationStatus.FAILED, error_msg)
240
+
241
+ results.append(result)
242
+
243
+ except Exception as e:
244
+ error_msg = f"Failed to update log retention policies: {e}"
245
+ logger.error(error_msg)
246
+ result = self.create_operation_result(
247
+ context, "update_log_retention_policy", "logs:operation", "batch_update"
248
+ )
249
+ result.mark_completed(OperationStatus.FAILED, error_msg)
250
+ results.append(result)
251
+
252
+ return results
253
+
254
+ def create_metric_alarm(
255
+ self,
256
+ context: OperationContext,
257
+ alarm_name: str,
258
+ comparison_operator: str,
259
+ evaluation_periods: int,
260
+ metric_name: str,
261
+ namespace: str,
262
+ period: int,
263
+ statistic: str,
264
+ threshold: float,
265
+ actions_enabled: bool = True,
266
+ alarm_actions: Optional[List[str]] = None,
267
+ alarm_description: Optional[str] = None,
268
+ dimensions: Optional[List[Dict[str, str]]] = None,
269
+ insufficient_data_actions: Optional[List[str]] = None,
270
+ ok_actions: Optional[List[str]] = None,
271
+ tags: Optional[List[Dict[str, str]]] = None,
272
+ unit: Optional[str] = None,
273
+ ) -> List[OperationResult]:
274
+ """
275
+ Create CloudWatch metric alarm.
276
+
277
+ Args:
278
+ context: Operation context
279
+ alarm_name: Name of alarm to create
280
+ comparison_operator: Comparison operator
281
+ evaluation_periods: Number of evaluation periods
282
+ metric_name: Metric name
283
+ namespace: Metric namespace
284
+ period: Period in seconds
285
+ statistic: Statistic type
286
+ threshold: Alarm threshold
287
+ actions_enabled: Whether actions are enabled
288
+ alarm_actions: Alarm actions
289
+ alarm_description: Alarm description
290
+ dimensions: Metric dimensions
291
+ insufficient_data_actions: Insufficient data actions
292
+ ok_actions: OK actions
293
+ tags: Alarm tags
294
+ unit: Metric unit
295
+
296
+ Returns:
297
+ List of operation results
298
+ """
299
+ cloudwatch_client = self.get_client("cloudwatch", context.region)
300
+
301
+ result = self.create_operation_result(context, "create_metric_alarm", "cloudwatch:alarm", alarm_name)
302
+
303
+ try:
304
+ if context.dry_run:
305
+ logger.info(f"[DRY-RUN] Would create CloudWatch alarm {alarm_name}")
306
+ result.mark_completed(OperationStatus.DRY_RUN)
307
+ return [result]
308
+
309
+ alarm_params = {
310
+ "AlarmName": alarm_name,
311
+ "ComparisonOperator": comparison_operator,
312
+ "EvaluationPeriods": evaluation_periods,
313
+ "MetricName": metric_name,
314
+ "Namespace": namespace,
315
+ "Period": period,
316
+ "Statistic": statistic,
317
+ "Threshold": threshold,
318
+ "ActionsEnabled": actions_enabled,
319
+ }
320
+
321
+ if alarm_actions:
322
+ alarm_params["AlarmActions"] = alarm_actions
323
+ if alarm_description:
324
+ alarm_params["AlarmDescription"] = alarm_description
325
+ if dimensions:
326
+ alarm_params["Dimensions"] = dimensions
327
+ if insufficient_data_actions:
328
+ alarm_params["InsufficientDataActions"] = insufficient_data_actions
329
+ if ok_actions:
330
+ alarm_params["OKActions"] = ok_actions
331
+ if tags:
332
+ alarm_params["Tags"] = tags
333
+ if unit:
334
+ alarm_params["Unit"] = unit
335
+
336
+ response = self.execute_aws_call(cloudwatch_client, "put_metric_alarm", **alarm_params)
337
+
338
+ result.response_data = response
339
+ result.mark_completed(OperationStatus.SUCCESS)
340
+ logger.info(f"Successfully created CloudWatch alarm {alarm_name}")
341
+
342
+ except ClientError as e:
343
+ error_msg = f"Failed to create CloudWatch alarm {alarm_name}: {e}"
344
+ logger.error(error_msg)
345
+ result.mark_completed(OperationStatus.FAILED, error_msg)
346
+
347
+ return [result]
348
+
349
+ def delete_metric_alarm(self, context: OperationContext, alarm_name: str) -> List[OperationResult]:
350
+ """
351
+ Delete CloudWatch metric alarm.
352
+
353
+ Args:
354
+ context: Operation context
355
+ alarm_name: Name of alarm to delete
356
+
357
+ Returns:
358
+ List of operation results
359
+ """
360
+ cloudwatch_client = self.get_client("cloudwatch", context.region)
361
+
362
+ result = self.create_operation_result(context, "delete_metric_alarm", "cloudwatch:alarm", alarm_name)
363
+
364
+ try:
365
+ if not self.confirm_operation(context, alarm_name, "delete CloudWatch alarm"):
366
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
367
+ return [result]
368
+
369
+ if context.dry_run:
370
+ logger.info(f"[DRY-RUN] Would delete CloudWatch alarm {alarm_name}")
371
+ result.mark_completed(OperationStatus.DRY_RUN)
372
+ else:
373
+ response = self.execute_aws_call(cloudwatch_client, "delete_alarms", AlarmNames=[alarm_name])
374
+
375
+ result.response_data = response
376
+ result.mark_completed(OperationStatus.SUCCESS)
377
+ logger.info(f"Successfully deleted CloudWatch alarm {alarm_name}")
378
+
379
+ except ClientError as e:
380
+ error_msg = f"Failed to delete CloudWatch alarm {alarm_name}: {e}"
381
+ logger.error(error_msg)
382
+ result.mark_completed(OperationStatus.FAILED, error_msg)
383
+
384
+ return [result]
385
+
386
+ def put_metric_data(
387
+ self, context: OperationContext, namespace: str, metric_data: List[Dict[str, Any]]
388
+ ) -> List[OperationResult]:
389
+ """
390
+ Put custom metric data to CloudWatch.
391
+
392
+ Args:
393
+ context: Operation context
394
+ namespace: Metric namespace
395
+ metric_data: List of metric data points
396
+
397
+ Returns:
398
+ List of operation results
399
+ """
400
+ cloudwatch_client = self.get_client("cloudwatch", context.region)
401
+
402
+ result = self.create_operation_result(context, "put_metric_data", "cloudwatch:metric", namespace)
403
+
404
+ try:
405
+ if context.dry_run:
406
+ logger.info(f"[DRY-RUN] Would put {len(metric_data)} metric data points to {namespace}")
407
+ result.mark_completed(OperationStatus.DRY_RUN)
408
+ else:
409
+ response = self.execute_aws_call(
410
+ cloudwatch_client, "put_metric_data", Namespace=namespace, MetricData=metric_data
411
+ )
412
+
413
+ result.response_data = response
414
+ result.mark_completed(OperationStatus.SUCCESS)
415
+ logger.info(f"Successfully put {len(metric_data)} metric data points to {namespace}")
416
+
417
+ except ClientError as e:
418
+ error_msg = f"Failed to put metric data to {namespace}: {e}"
419
+ logger.error(error_msg)
420
+ result.mark_completed(OperationStatus.FAILED, error_msg)
421
+
422
+ return [result]
423
+
424
+ def tag_log_group(
425
+ self, context: OperationContext, log_group_name: str, tags: Dict[str, str]
426
+ ) -> List[OperationResult]:
427
+ """
428
+ Add tags to CloudWatch log group.
429
+
430
+ Args:
431
+ context: Operation context
432
+ log_group_name: Name of log group to tag
433
+ tags: Tags to add
434
+
435
+ Returns:
436
+ List of operation results
437
+ """
438
+ logs_client = self.get_client("logs", context.region)
439
+
440
+ result = self.create_operation_result(context, "tag_log_group", "logs:log_group", log_group_name)
441
+
442
+ try:
443
+ if context.dry_run:
444
+ logger.info(f"[DRY-RUN] Would add {len(tags)} tags to log group {log_group_name}")
445
+ result.mark_completed(OperationStatus.DRY_RUN)
446
+ else:
447
+ response = self.execute_aws_call(logs_client, "tag_log_group", logGroupName=log_group_name, tags=tags)
448
+
449
+ result.response_data = response
450
+ result.mark_completed(OperationStatus.SUCCESS)
451
+ logger.info(f"Successfully added {len(tags)} tags to log group {log_group_name}")
452
+
453
+ except ClientError as e:
454
+ error_msg = f"Failed to tag log group {log_group_name}: {e}"
455
+ logger.error(error_msg)
456
+ result.mark_completed(OperationStatus.FAILED, error_msg)
457
+
458
+ return [result]
459
+
460
+ def untag_log_group(
461
+ self, context: OperationContext, log_group_name: str, tag_keys: List[str]
462
+ ) -> List[OperationResult]:
463
+ """
464
+ Remove tags from CloudWatch log group.
465
+
466
+ Args:
467
+ context: Operation context
468
+ log_group_name: Name of log group to untag
469
+ tag_keys: Tag keys to remove
470
+
471
+ Returns:
472
+ List of operation results
473
+ """
474
+ logs_client = self.get_client("logs", context.region)
475
+
476
+ result = self.create_operation_result(context, "untag_log_group", "logs:log_group", log_group_name)
477
+
478
+ try:
479
+ if context.dry_run:
480
+ logger.info(f"[DRY-RUN] Would remove {len(tag_keys)} tags from log group {log_group_name}")
481
+ result.mark_completed(OperationStatus.DRY_RUN)
482
+ else:
483
+ response = self.execute_aws_call(
484
+ logs_client, "untag_log_group", logGroupName=log_group_name, tags=tag_keys
485
+ )
486
+
487
+ result.response_data = response
488
+ result.mark_completed(OperationStatus.SUCCESS)
489
+ logger.info(f"Successfully removed {len(tag_keys)} tags from log group {log_group_name}")
490
+
491
+ except ClientError as e:
492
+ error_msg = f"Failed to untag log group {log_group_name}: {e}"
493
+ logger.error(error_msg)
494
+ result.mark_completed(OperationStatus.FAILED, error_msg)
495
+
496
+ return [result]