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,619 @@
1
+ """
2
+ CloudFormation Operations Module.
3
+
4
+ Provides comprehensive CloudFormation resource management capabilities including
5
+ stack operations, StackSet management, and infrastructure automation.
6
+
7
+ Migrated and enhanced from:
8
+ - inventory/cfn_move_stack_instances.py
9
+ - inventory/update_cfn_stacksets.py
10
+ - inventory/lockdown_cfn_stackset_role.py
11
+ """
12
+
13
+ import json
14
+ from datetime import datetime
15
+ from typing import Any, Dict, List, Optional, Union
16
+
17
+ import boto3
18
+ from botocore.exceptions import ClientError
19
+ from loguru import logger
20
+
21
+ from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
22
+
23
+
24
+ class CloudFormationOperations(BaseOperation):
25
+ """
26
+ CloudFormation resource operations and lifecycle management.
27
+
28
+ Handles all CloudFormation-related operational tasks including stack management,
29
+ StackSet operations, and infrastructure automation workflows.
30
+ """
31
+
32
+ service_name = "cloudformation"
33
+ supported_operations = {
34
+ "create_stack",
35
+ "update_stack",
36
+ "delete_stack",
37
+ "create_stack_set",
38
+ "update_stack_set",
39
+ "delete_stack_set",
40
+ "create_stack_instances",
41
+ "update_stack_instances",
42
+ "delete_stack_instances",
43
+ "move_stack_instances",
44
+ "lockdown_stackset_role",
45
+ "enable_drift_detection",
46
+ "detect_stack_drift",
47
+ "cancel_update_stack",
48
+ }
49
+ requires_confirmation = True
50
+
51
+ def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = False):
52
+ """Initialize CloudFormation operations."""
53
+ super().__init__(profile, region, dry_run)
54
+
55
+ def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
56
+ """
57
+ Execute CloudFormation operation.
58
+
59
+ Args:
60
+ context: Operation context
61
+ operation_type: Type of operation to execute
62
+ **kwargs: Operation-specific arguments
63
+
64
+ Returns:
65
+ List of operation results
66
+ """
67
+ self.validate_context(context)
68
+
69
+ if operation_type == "create_stack":
70
+ return self.create_stack(context, **kwargs)
71
+ elif operation_type == "update_stack":
72
+ return self.update_stack(context, **kwargs)
73
+ elif operation_type == "delete_stack":
74
+ return self.delete_stack(context, kwargs.get("stack_name"))
75
+ elif operation_type == "create_stack_set":
76
+ return self.create_stack_set(context, **kwargs)
77
+ elif operation_type == "update_stack_set":
78
+ return self.update_stack_set(context, **kwargs)
79
+ elif operation_type == "delete_stack_set":
80
+ return self.delete_stack_set(context, kwargs.get("stack_set_name"))
81
+ elif operation_type == "create_stack_instances":
82
+ return self.create_stack_instances(context, **kwargs)
83
+ elif operation_type == "update_stack_instances":
84
+ return self.update_stack_instances(context, **kwargs)
85
+ elif operation_type == "delete_stack_instances":
86
+ return self.delete_stack_instances(context, **kwargs)
87
+ elif operation_type == "move_stack_instances":
88
+ return self.move_stack_instances(context, **kwargs)
89
+ elif operation_type == "lockdown_stackset_role":
90
+ return self.lockdown_stackset_role(context, **kwargs)
91
+ elif operation_type == "enable_drift_detection":
92
+ return self.enable_drift_detection(context, kwargs.get("stack_name"))
93
+ elif operation_type == "detect_stack_drift":
94
+ return self.detect_stack_drift(context, kwargs.get("stack_name"))
95
+ elif operation_type == "cancel_update_stack":
96
+ return self.cancel_update_stack(context, kwargs.get("stack_name"))
97
+ else:
98
+ raise ValueError(f"Unsupported operation: {operation_type}")
99
+
100
+ def create_stack(
101
+ self,
102
+ context: OperationContext,
103
+ stack_name: str,
104
+ template_body: Optional[str] = None,
105
+ template_url: Optional[str] = None,
106
+ parameters: Optional[List[Dict[str, str]]] = None,
107
+ capabilities: Optional[List[str]] = None,
108
+ tags: Optional[List[Dict[str, str]]] = None,
109
+ role_arn: Optional[str] = None,
110
+ enable_termination_protection: bool = False,
111
+ ) -> List[OperationResult]:
112
+ """
113
+ Create CloudFormation stack.
114
+
115
+ Args:
116
+ context: Operation context
117
+ stack_name: Name of stack to create
118
+ template_body: Template body as string
119
+ template_url: Template URL
120
+ parameters: Stack parameters
121
+ capabilities: Required capabilities
122
+ tags: Stack tags
123
+ role_arn: Service role ARN
124
+ enable_termination_protection: Enable termination protection
125
+
126
+ Returns:
127
+ List of operation results
128
+ """
129
+ cfn_client = self.get_client("cloudformation", context.region)
130
+
131
+ result = self.create_operation_result(context, "create_stack", "cloudformation:stack", stack_name)
132
+
133
+ try:
134
+ if context.dry_run:
135
+ logger.info(f"[DRY-RUN] Would create CloudFormation stack {stack_name}")
136
+ result.mark_completed(OperationStatus.DRY_RUN)
137
+ return [result]
138
+
139
+ create_params = {
140
+ "StackName": stack_name,
141
+ "EnableTerminationProtection": enable_termination_protection,
142
+ }
143
+
144
+ if template_body:
145
+ create_params["TemplateBody"] = template_body
146
+ elif template_url:
147
+ create_params["TemplateURL"] = template_url
148
+ else:
149
+ raise ValueError("Either template_body or template_url must be provided")
150
+
151
+ if parameters:
152
+ create_params["Parameters"] = parameters
153
+ if capabilities:
154
+ create_params["Capabilities"] = capabilities
155
+ if tags:
156
+ create_params["Tags"] = tags
157
+ if role_arn:
158
+ create_params["RoleARN"] = role_arn
159
+
160
+ response = self.execute_aws_call(cfn_client, "create_stack", **create_params)
161
+
162
+ result.response_data = response
163
+ result.mark_completed(OperationStatus.SUCCESS)
164
+ logger.info(f"Successfully created CloudFormation stack {stack_name}")
165
+
166
+ except ClientError as e:
167
+ error_msg = f"Failed to create CloudFormation stack {stack_name}: {e}"
168
+ logger.error(error_msg)
169
+ result.mark_completed(OperationStatus.FAILED, error_msg)
170
+
171
+ return [result]
172
+
173
+ def delete_stack(
174
+ self, context: OperationContext, stack_name: str, role_arn: Optional[str] = None
175
+ ) -> List[OperationResult]:
176
+ """
177
+ Delete CloudFormation stack.
178
+
179
+ Args:
180
+ context: Operation context
181
+ stack_name: Name of stack to delete
182
+ role_arn: Service role ARN
183
+
184
+ Returns:
185
+ List of operation results
186
+ """
187
+ cfn_client = self.get_client("cloudformation", context.region)
188
+
189
+ result = self.create_operation_result(context, "delete_stack", "cloudformation:stack", stack_name)
190
+
191
+ try:
192
+ if not self.confirm_operation(context, stack_name, "delete CloudFormation stack"):
193
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
194
+ return [result]
195
+
196
+ if context.dry_run:
197
+ logger.info(f"[DRY-RUN] Would delete CloudFormation stack {stack_name}")
198
+ result.mark_completed(OperationStatus.DRY_RUN)
199
+ else:
200
+ delete_params = {"StackName": stack_name}
201
+ if role_arn:
202
+ delete_params["RoleARN"] = role_arn
203
+
204
+ response = self.execute_aws_call(cfn_client, "delete_stack", **delete_params)
205
+
206
+ result.response_data = response
207
+ result.mark_completed(OperationStatus.SUCCESS)
208
+ logger.info(f"Successfully initiated deletion of CloudFormation stack {stack_name}")
209
+
210
+ except ClientError as e:
211
+ error_msg = f"Failed to delete CloudFormation stack {stack_name}: {e}"
212
+ logger.error(error_msg)
213
+ result.mark_completed(OperationStatus.FAILED, error_msg)
214
+
215
+ return [result]
216
+
217
+ def move_stack_instances(
218
+ self,
219
+ context: OperationContext,
220
+ source_stack_set_name: str,
221
+ target_stack_set_name: str,
222
+ account_ids: List[str],
223
+ regions: List[str],
224
+ operation_preferences: Optional[Dict[str, Any]] = None,
225
+ ) -> List[OperationResult]:
226
+ """
227
+ Move stack instances between StackSets.
228
+
229
+ Migrated from inventory/cfn_move_stack_instances.py
230
+
231
+ Args:
232
+ context: Operation context
233
+ source_stack_set_name: Source StackSet name
234
+ target_stack_set_name: Target StackSet name
235
+ account_ids: Account IDs to move
236
+ regions: Regions to move
237
+ operation_preferences: Operation preferences
238
+
239
+ Returns:
240
+ List of operation results
241
+ """
242
+ cfn_client = self.get_client("cloudformation", context.region)
243
+
244
+ result = self.create_operation_result(
245
+ context,
246
+ "move_stack_instances",
247
+ "cloudformation:stackset",
248
+ f"{source_stack_set_name} -> {target_stack_set_name}",
249
+ )
250
+
251
+ try:
252
+ if not self.confirm_operation(context, f"{len(account_ids)} instances", "move StackSet instances"):
253
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
254
+ return [result]
255
+
256
+ if context.dry_run:
257
+ logger.info(
258
+ f"[DRY-RUN] Would move {len(account_ids)} instances from {source_stack_set_name} to {target_stack_set_name}"
259
+ )
260
+ result.mark_completed(OperationStatus.DRY_RUN)
261
+ return [result]
262
+
263
+ # Step 1: Delete instances from source StackSet
264
+ delete_params = {"StackSetName": source_stack_set_name, "Accounts": account_ids, "Regions": regions}
265
+
266
+ if operation_preferences:
267
+ delete_params["OperationPreferences"] = operation_preferences
268
+
269
+ delete_response = self.execute_aws_call(cfn_client, "delete_stack_instances", **delete_params)
270
+
271
+ delete_operation_id = delete_response["OperationId"]
272
+ logger.info(f"Initiated deletion from source StackSet: {delete_operation_id}")
273
+
274
+ # Wait for deletion to complete (simplified - in production, implement proper polling)
275
+ # For now, we'll return the operation ID for monitoring
276
+
277
+ # Step 2: Create instances in target StackSet
278
+ create_params = {"StackSetName": target_stack_set_name, "Accounts": account_ids, "Regions": regions}
279
+
280
+ if operation_preferences:
281
+ create_params["OperationPreferences"] = operation_preferences
282
+
283
+ create_response = self.execute_aws_call(cfn_client, "create_stack_instances", **create_params)
284
+
285
+ create_operation_id = create_response["OperationId"]
286
+ logger.info(f"Initiated creation in target StackSet: {create_operation_id}")
287
+
288
+ result.response_data = {
289
+ "delete_operation_id": delete_operation_id,
290
+ "create_operation_id": create_operation_id,
291
+ "moved_accounts": account_ids,
292
+ "moved_regions": regions,
293
+ }
294
+ result.mark_completed(OperationStatus.SUCCESS)
295
+ logger.info(f"Successfully initiated move of {len(account_ids)} instances")
296
+
297
+ except ClientError as e:
298
+ error_msg = f"Failed to move stack instances: {e}"
299
+ logger.error(error_msg)
300
+ result.mark_completed(OperationStatus.FAILED, error_msg)
301
+
302
+ return [result]
303
+
304
+ def lockdown_stackset_role(
305
+ self,
306
+ context: OperationContext,
307
+ target_role_name: str = "AWSCloudFormationStackSetExecutionRole",
308
+ management_account_id: Optional[str] = None,
309
+ lock_policy: bool = True,
310
+ ) -> List[OperationResult]:
311
+ """
312
+ Lockdown CloudFormation StackSet execution role.
313
+
314
+ Migrated from inventory/lockdown_cfn_stackset_role.py
315
+
316
+ Args:
317
+ context: Operation context
318
+ target_role_name: Role name to lockdown
319
+ management_account_id: Management account ID to restrict access to
320
+ lock_policy: Whether to apply restrictive policy
321
+
322
+ Returns:
323
+ List of operation results
324
+ """
325
+ iam_client = self.get_client("iam", context.region)
326
+
327
+ result = self.create_operation_result(context, "lockdown_stackset_role", "iam:role", target_role_name)
328
+
329
+ try:
330
+ if not self.confirm_operation(context, target_role_name, "lockdown StackSet role"):
331
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
332
+ return [result]
333
+
334
+ if context.dry_run:
335
+ logger.info(f"[DRY-RUN] Would lockdown StackSet role {target_role_name}")
336
+ result.mark_completed(OperationStatus.DRY_RUN)
337
+ return [result]
338
+
339
+ # Get current role
340
+ role_response = self.execute_aws_call(iam_client, "get_role", RoleName=target_role_name)
341
+
342
+ current_trust_policy = role_response["Role"]["AssumeRolePolicyDocument"]
343
+
344
+ if lock_policy and management_account_id:
345
+ # Create restrictive trust policy
346
+ locked_trust_policy = {
347
+ "Version": "2012-10-17",
348
+ "Statement": [
349
+ {
350
+ "Effect": "Allow",
351
+ "Principal": {"AWS": f"arn:aws:iam::{management_account_id}:root"},
352
+ "Action": "sts:AssumeRole",
353
+ "Condition": {"StringEquals": {"aws:PrincipalServiceName": "cloudformation.amazonaws.com"}},
354
+ }
355
+ ],
356
+ }
357
+
358
+ # Update trust policy
359
+ response = self.execute_aws_call(
360
+ iam_client,
361
+ "update_assume_role_policy",
362
+ RoleName=target_role_name,
363
+ PolicyDocument=json.dumps(locked_trust_policy),
364
+ )
365
+
366
+ result.response_data = {
367
+ "role_name": target_role_name,
368
+ "previous_policy": current_trust_policy,
369
+ "new_policy": locked_trust_policy,
370
+ "management_account_id": management_account_id,
371
+ }
372
+ result.mark_completed(OperationStatus.SUCCESS)
373
+ logger.info(f"Successfully locked down StackSet role {target_role_name}")
374
+ else:
375
+ result.response_data = {
376
+ "role_name": target_role_name,
377
+ "current_policy": current_trust_policy,
378
+ "action": "Policy retrieved but not modified",
379
+ }
380
+ result.mark_completed(OperationStatus.SUCCESS)
381
+ logger.info(f"Retrieved current policy for role {target_role_name}")
382
+
383
+ except ClientError as e:
384
+ error_msg = f"Failed to lockdown StackSet role {target_role_name}: {e}"
385
+ logger.error(error_msg)
386
+ result.mark_completed(OperationStatus.FAILED, error_msg)
387
+
388
+ return [result]
389
+
390
+ def create_stack_set(
391
+ self,
392
+ context: OperationContext,
393
+ stack_set_name: str,
394
+ template_body: Optional[str] = None,
395
+ template_url: Optional[str] = None,
396
+ parameters: Optional[List[Dict[str, str]]] = None,
397
+ capabilities: Optional[List[str]] = None,
398
+ tags: Optional[List[Dict[str, str]]] = None,
399
+ administration_role_arn: Optional[str] = None,
400
+ execution_role_name: Optional[str] = None,
401
+ permission_model: str = "SERVICE_MANAGED",
402
+ ) -> List[OperationResult]:
403
+ """
404
+ Create CloudFormation StackSet.
405
+
406
+ Args:
407
+ context: Operation context
408
+ stack_set_name: Name of StackSet to create
409
+ template_body: Template body as string
410
+ template_url: Template URL
411
+ parameters: StackSet parameters
412
+ capabilities: Required capabilities
413
+ tags: StackSet tags
414
+ administration_role_arn: Administration role ARN
415
+ execution_role_name: Execution role name
416
+ permission_model: Permission model (SERVICE_MANAGED or SELF_MANAGED)
417
+
418
+ Returns:
419
+ List of operation results
420
+ """
421
+ cfn_client = self.get_client("cloudformation", context.region)
422
+
423
+ result = self.create_operation_result(context, "create_stack_set", "cloudformation:stackset", stack_set_name)
424
+
425
+ try:
426
+ if context.dry_run:
427
+ logger.info(f"[DRY-RUN] Would create CloudFormation StackSet {stack_set_name}")
428
+ result.mark_completed(OperationStatus.DRY_RUN)
429
+ return [result]
430
+
431
+ create_params = {
432
+ "StackSetName": stack_set_name,
433
+ "PermissionModel": permission_model,
434
+ }
435
+
436
+ if template_body:
437
+ create_params["TemplateBody"] = template_body
438
+ elif template_url:
439
+ create_params["TemplateURL"] = template_url
440
+ else:
441
+ raise ValueError("Either template_body or template_url must be provided")
442
+
443
+ if parameters:
444
+ create_params["Parameters"] = parameters
445
+ if capabilities:
446
+ create_params["Capabilities"] = capabilities
447
+ if tags:
448
+ create_params["Tags"] = tags
449
+ if administration_role_arn:
450
+ create_params["AdministrationRoleARN"] = administration_role_arn
451
+ if execution_role_name:
452
+ create_params["ExecutionRoleName"] = execution_role_name
453
+
454
+ response = self.execute_aws_call(cfn_client, "create_stack_set", **create_params)
455
+
456
+ result.response_data = response
457
+ result.mark_completed(OperationStatus.SUCCESS)
458
+ logger.info(f"Successfully created CloudFormation StackSet {stack_set_name}")
459
+
460
+ except ClientError as e:
461
+ error_msg = f"Failed to create CloudFormation StackSet {stack_set_name}: {e}"
462
+ logger.error(error_msg)
463
+ result.mark_completed(OperationStatus.FAILED, error_msg)
464
+
465
+ return [result]
466
+
467
+ def delete_stack_set(self, context: OperationContext, stack_set_name: str) -> List[OperationResult]:
468
+ """
469
+ Delete CloudFormation StackSet.
470
+
471
+ Args:
472
+ context: Operation context
473
+ stack_set_name: Name of StackSet to delete
474
+
475
+ Returns:
476
+ List of operation results
477
+ """
478
+ cfn_client = self.get_client("cloudformation", context.region)
479
+
480
+ result = self.create_operation_result(context, "delete_stack_set", "cloudformation:stackset", stack_set_name)
481
+
482
+ try:
483
+ if not self.confirm_operation(context, stack_set_name, "delete CloudFormation StackSet"):
484
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
485
+ return [result]
486
+
487
+ if context.dry_run:
488
+ logger.info(f"[DRY-RUN] Would delete CloudFormation StackSet {stack_set_name}")
489
+ result.mark_completed(OperationStatus.DRY_RUN)
490
+ else:
491
+ response = self.execute_aws_call(cfn_client, "delete_stack_set", StackSetName=stack_set_name)
492
+
493
+ result.response_data = response
494
+ result.mark_completed(OperationStatus.SUCCESS)
495
+ logger.info(f"Successfully deleted CloudFormation StackSet {stack_set_name}")
496
+
497
+ except ClientError as e:
498
+ error_msg = f"Failed to delete CloudFormation StackSet {stack_set_name}: {e}"
499
+ logger.error(error_msg)
500
+ result.mark_completed(OperationStatus.FAILED, error_msg)
501
+
502
+ return [result]
503
+
504
+ def create_stack_instances(
505
+ self,
506
+ context: OperationContext,
507
+ stack_set_name: str,
508
+ account_ids: List[str],
509
+ regions: List[str],
510
+ parameter_overrides: Optional[List[Dict[str, str]]] = None,
511
+ operation_preferences: Optional[Dict[str, Any]] = None,
512
+ ) -> List[OperationResult]:
513
+ """
514
+ Create CloudFormation StackSet instances.
515
+
516
+ Args:
517
+ context: Operation context
518
+ stack_set_name: StackSet name
519
+ account_ids: Target account IDs
520
+ regions: Target regions
521
+ parameter_overrides: Parameter overrides
522
+ operation_preferences: Operation preferences
523
+
524
+ Returns:
525
+ List of operation results
526
+ """
527
+ cfn_client = self.get_client("cloudformation", context.region)
528
+
529
+ result = self.create_operation_result(
530
+ context, "create_stack_instances", "cloudformation:stackset", stack_set_name
531
+ )
532
+
533
+ try:
534
+ if context.dry_run:
535
+ logger.info(f"[DRY-RUN] Would create {len(account_ids)} stack instances in StackSet {stack_set_name}")
536
+ result.mark_completed(OperationStatus.DRY_RUN)
537
+ return [result]
538
+
539
+ create_params = {"StackSetName": stack_set_name, "Accounts": account_ids, "Regions": regions}
540
+
541
+ if parameter_overrides:
542
+ create_params["ParameterOverrides"] = parameter_overrides
543
+ if operation_preferences:
544
+ create_params["OperationPreferences"] = operation_preferences
545
+
546
+ response = self.execute_aws_call(cfn_client, "create_stack_instances", **create_params)
547
+
548
+ result.response_data = response
549
+ result.mark_completed(OperationStatus.SUCCESS)
550
+ logger.info(f"Successfully initiated creation of {len(account_ids)} stack instances")
551
+
552
+ except ClientError as e:
553
+ error_msg = f"Failed to create stack instances: {e}"
554
+ logger.error(error_msg)
555
+ result.mark_completed(OperationStatus.FAILED, error_msg)
556
+
557
+ return [result]
558
+
559
+ def delete_stack_instances(
560
+ self,
561
+ context: OperationContext,
562
+ stack_set_name: str,
563
+ account_ids: List[str],
564
+ regions: List[str],
565
+ retain_stacks: bool = False,
566
+ operation_preferences: Optional[Dict[str, Any]] = None,
567
+ ) -> List[OperationResult]:
568
+ """
569
+ Delete CloudFormation StackSet instances.
570
+
571
+ Args:
572
+ context: Operation context
573
+ stack_set_name: StackSet name
574
+ account_ids: Target account IDs
575
+ regions: Target regions
576
+ retain_stacks: Whether to retain stacks
577
+ operation_preferences: Operation preferences
578
+
579
+ Returns:
580
+ List of operation results
581
+ """
582
+ cfn_client = self.get_client("cloudformation", context.region)
583
+
584
+ result = self.create_operation_result(
585
+ context, "delete_stack_instances", "cloudformation:stackset", stack_set_name
586
+ )
587
+
588
+ try:
589
+ if not self.confirm_operation(context, f"{len(account_ids)} instances", "delete StackSet instances"):
590
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
591
+ return [result]
592
+
593
+ if context.dry_run:
594
+ logger.info(f"[DRY-RUN] Would delete {len(account_ids)} stack instances from StackSet {stack_set_name}")
595
+ result.mark_completed(OperationStatus.DRY_RUN)
596
+ return [result]
597
+
598
+ delete_params = {
599
+ "StackSetName": stack_set_name,
600
+ "Accounts": account_ids,
601
+ "Regions": regions,
602
+ "RetainStacks": retain_stacks,
603
+ }
604
+
605
+ if operation_preferences:
606
+ delete_params["OperationPreferences"] = operation_preferences
607
+
608
+ response = self.execute_aws_call(cfn_client, "delete_stack_instances", **delete_params)
609
+
610
+ result.response_data = response
611
+ result.mark_completed(OperationStatus.SUCCESS)
612
+ logger.info(f"Successfully initiated deletion of {len(account_ids)} stack instances")
613
+
614
+ except ClientError as e:
615
+ error_msg = f"Failed to delete stack instances: {e}"
616
+ logger.error(error_msg)
617
+ result.mark_completed(OperationStatus.FAILED, error_msg)
618
+
619
+ return [result]