runbooks 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +1 -1
- runbooks/cloudops/models.py +20 -14
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +10 -3
- runbooks/common/sre_performance_suite.py +574 -0
- runbooks/finops/business_case_config.py +314 -0
- runbooks/finops/cost_processor.py +19 -4
- runbooks/finops/ebs_cost_optimizer.py +1 -1
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- runbooks/inventory/models/account.py +5 -3
- runbooks/inventory/models/inventory.py +1 -1
- runbooks/inventory/models/resource.py +5 -3
- runbooks/inventory/organizations_discovery.py +89 -5
- runbooks/main.py +182 -61
- runbooks/operate/vpc_operations.py +60 -31
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/vpc/config.py +17 -8
- runbooks/vpc/heatmap_engine.py +425 -53
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/METADATA +15 -15
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/RECORD +35 -27
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/WHEEL +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,587 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
--dry-run Implementation Examples for CloudOps Runbooks Modules
|
4
|
+
|
5
|
+
This module provides practical examples of how to integrate the universal dry-run
|
6
|
+
safety framework into existing and new runbooks modules.
|
7
|
+
|
8
|
+
Strategic Alignment:
|
9
|
+
- "Do one thing and do it well" - Consistent dry-run behavior across modules
|
10
|
+
- Enterprise safety standards with comprehensive examples
|
11
|
+
|
12
|
+
Author: CloudOps Runbooks Team
|
13
|
+
Version: 1.0.0 - Implementation Examples
|
14
|
+
"""
|
15
|
+
|
16
|
+
from typing import Any, Dict, List, Optional
|
17
|
+
import boto3
|
18
|
+
from botocore.exceptions import ClientError
|
19
|
+
|
20
|
+
from runbooks.common.dry_run_framework import (
|
21
|
+
DryRunContext,
|
22
|
+
OperationType,
|
23
|
+
dry_run_operation,
|
24
|
+
discovery_operation,
|
25
|
+
analysis_operation,
|
26
|
+
assessment_operation,
|
27
|
+
resource_creation_operation,
|
28
|
+
resource_deletion_operation,
|
29
|
+
remediation_operation,
|
30
|
+
framework
|
31
|
+
)
|
32
|
+
from runbooks.common.rich_utils import console, print_success, print_warning, print_error
|
33
|
+
|
34
|
+
|
35
|
+
# =============================================================================
|
36
|
+
# 1. DISCOVERY OPERATIONS (inventory, scan modules)
|
37
|
+
# =============================================================================
|
38
|
+
|
39
|
+
@discovery_operation
|
40
|
+
def collect_ec2_instances(
|
41
|
+
dry_run_context: DryRunContext,
|
42
|
+
profile: Optional[str] = None,
|
43
|
+
regions: Optional[List[str]] = None
|
44
|
+
) -> Dict[str, Any]:
|
45
|
+
"""
|
46
|
+
Example discovery operation - EC2 instance collection.
|
47
|
+
|
48
|
+
Discovery operations are inherently safe, so dry-run simulates API calls
|
49
|
+
for testing purposes only.
|
50
|
+
"""
|
51
|
+
if dry_run_context.enabled:
|
52
|
+
# Simulation mode - no real API calls
|
53
|
+
console.print("[dim]🔄 Simulating EC2 instance discovery...[/dim]")
|
54
|
+
|
55
|
+
# Return simulated data
|
56
|
+
return {
|
57
|
+
"instances": [
|
58
|
+
{"id": "i-sim123", "type": "t3.micro", "state": "running"},
|
59
|
+
{"id": "i-sim456", "type": "m5.large", "state": "stopped"}
|
60
|
+
],
|
61
|
+
"simulated": True,
|
62
|
+
"region_count": len(regions or ["us-east-1"]),
|
63
|
+
"total_discovered": 2
|
64
|
+
}
|
65
|
+
|
66
|
+
else:
|
67
|
+
# Real discovery operation
|
68
|
+
console.print("[cyan]🔍 Discovering EC2 instances across regions...[/cyan]")
|
69
|
+
|
70
|
+
session = boto3.Session(profile_name=profile)
|
71
|
+
instances = []
|
72
|
+
|
73
|
+
for region in regions or ["us-east-1"]:
|
74
|
+
try:
|
75
|
+
ec2 = session.client('ec2', region_name=region)
|
76
|
+
response = ec2.describe_instances()
|
77
|
+
|
78
|
+
for reservation in response['Reservations']:
|
79
|
+
for instance in reservation['Instances']:
|
80
|
+
instances.append({
|
81
|
+
"id": instance['InstanceId'],
|
82
|
+
"type": instance['InstanceType'],
|
83
|
+
"state": instance['State']['Name'],
|
84
|
+
"region": region
|
85
|
+
})
|
86
|
+
|
87
|
+
except ClientError as e:
|
88
|
+
print_warning(f"Could not access region {region}: {e}")
|
89
|
+
|
90
|
+
print_success(f"Discovered {len(instances)} EC2 instances")
|
91
|
+
|
92
|
+
return {
|
93
|
+
"instances": instances,
|
94
|
+
"simulated": False,
|
95
|
+
"region_count": len(regions or ["us-east-1"]),
|
96
|
+
"total_discovered": len(instances)
|
97
|
+
}
|
98
|
+
|
99
|
+
|
100
|
+
# =============================================================================
|
101
|
+
# 2. ANALYSIS OPERATIONS (finops, security assess, vpc analyze modules)
|
102
|
+
# =============================================================================
|
103
|
+
|
104
|
+
@analysis_operation
|
105
|
+
def analyze_cost_optimization(
|
106
|
+
dry_run_context: DryRunContext,
|
107
|
+
profile: Optional[str] = None,
|
108
|
+
account_id: Optional[str] = None
|
109
|
+
) -> Dict[str, Any]:
|
110
|
+
"""
|
111
|
+
Example analysis operation - cost optimization analysis.
|
112
|
+
|
113
|
+
Analysis operations are read-only, so dry-run shows what would be analyzed
|
114
|
+
without making API calls.
|
115
|
+
"""
|
116
|
+
if dry_run_context.enabled:
|
117
|
+
# Preview mode - show what would be analyzed
|
118
|
+
console.print("[dim]📊 Preview: Cost analysis scope[/dim]")
|
119
|
+
console.print(f"[dim] • Account: {account_id or 'Current account'}[/dim]")
|
120
|
+
console.print(f"[dim] • Services: EC2, RDS, S3, Lambda[/dim]")
|
121
|
+
console.print(f"[dim] • Time range: Last 30 days[/dim]")
|
122
|
+
|
123
|
+
return {
|
124
|
+
"preview": True,
|
125
|
+
"scope": {
|
126
|
+
"account_id": account_id,
|
127
|
+
"services": ["EC2", "RDS", "S3", "Lambda"],
|
128
|
+
"time_range": "30 days"
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
else:
|
133
|
+
# Real cost analysis
|
134
|
+
console.print("[green]💰 Analyzing cost optimization opportunities...[/green]")
|
135
|
+
|
136
|
+
session = boto3.Session(profile_name=profile)
|
137
|
+
cost_explorer = session.client('ce')
|
138
|
+
|
139
|
+
# Real cost analysis logic here
|
140
|
+
# This is a simplified example
|
141
|
+
|
142
|
+
print_success("Cost analysis completed")
|
143
|
+
|
144
|
+
return {
|
145
|
+
"analysis_complete": True,
|
146
|
+
"recommendations": [
|
147
|
+
{"service": "EC2", "potential_savings": "$150/month"},
|
148
|
+
{"service": "RDS", "potential_savings": "$75/month"}
|
149
|
+
],
|
150
|
+
"total_potential_savings": "$225/month"
|
151
|
+
}
|
152
|
+
|
153
|
+
|
154
|
+
# =============================================================================
|
155
|
+
# 3. ASSESSMENT OPERATIONS (cfat assess module)
|
156
|
+
# =============================================================================
|
157
|
+
|
158
|
+
@assessment_operation
|
159
|
+
def assess_security_compliance(
|
160
|
+
dry_run_context: DryRunContext,
|
161
|
+
profile: Optional[str] = None,
|
162
|
+
frameworks: Optional[List[str]] = None
|
163
|
+
) -> Dict[str, Any]:
|
164
|
+
"""
|
165
|
+
Example assessment operation - security compliance assessment.
|
166
|
+
|
167
|
+
Assessment operations are read-only, so dry-run shows assessment scope.
|
168
|
+
"""
|
169
|
+
frameworks = frameworks or ["SOC2", "PCI-DSS", "HIPAA"]
|
170
|
+
|
171
|
+
if dry_run_context.enabled:
|
172
|
+
# Preview mode - show assessment scope
|
173
|
+
console.print("[dim]🔍 Preview: Security assessment scope[/dim]")
|
174
|
+
console.print(f"[dim] • Frameworks: {', '.join(frameworks)}[/dim]")
|
175
|
+
console.print(f"[dim] • Services to check: IAM, S3, EC2, VPC[/dim]")
|
176
|
+
console.print(f"[dim] • Estimated duration: 5-10 minutes[/dim]")
|
177
|
+
|
178
|
+
return {
|
179
|
+
"preview": True,
|
180
|
+
"frameworks": frameworks,
|
181
|
+
"estimated_checks": 45
|
182
|
+
}
|
183
|
+
|
184
|
+
else:
|
185
|
+
# Real security assessment
|
186
|
+
console.print("[blue]🔒 Conducting security compliance assessment...[/blue]")
|
187
|
+
|
188
|
+
session = boto3.Session(profile_name=profile)
|
189
|
+
|
190
|
+
# Real assessment logic here
|
191
|
+
# This is a simplified example
|
192
|
+
|
193
|
+
results = {
|
194
|
+
"frameworks_assessed": frameworks,
|
195
|
+
"total_checks": 45,
|
196
|
+
"passed": 38,
|
197
|
+
"failed": 7,
|
198
|
+
"compliance_score": "84.4%"
|
199
|
+
}
|
200
|
+
|
201
|
+
print_success(f"Assessment completed - Compliance score: {results['compliance_score']}")
|
202
|
+
|
203
|
+
return results
|
204
|
+
|
205
|
+
|
206
|
+
# =============================================================================
|
207
|
+
# 4. RESOURCE CREATION OPERATIONS (operate module)
|
208
|
+
# =============================================================================
|
209
|
+
|
210
|
+
@resource_creation_operation(estimated_impact="Create 1 EC2 instance (~$25/month)")
|
211
|
+
def create_ec2_instance(
|
212
|
+
dry_run_context: DryRunContext,
|
213
|
+
instance_type: str = "t3.micro",
|
214
|
+
image_id: Optional[str] = None,
|
215
|
+
profile: Optional[str] = None
|
216
|
+
) -> Dict[str, Any]:
|
217
|
+
"""
|
218
|
+
Example resource creation operation - EC2 instance creation.
|
219
|
+
|
220
|
+
Resource creation operations default to dry-run for safety.
|
221
|
+
"""
|
222
|
+
if dry_run_context.enabled:
|
223
|
+
# Preview mode - show what would be created
|
224
|
+
console.print("[yellow]🔄 Preview: EC2 instance creation[/yellow]")
|
225
|
+
console.print(f"[dim] • Instance type: {instance_type}[/dim]")
|
226
|
+
console.print(f"[dim] • AMI ID: {image_id or 'Latest Amazon Linux 2'}[/dim]")
|
227
|
+
console.print(f"[dim] • Estimated cost: ~$25/month[/dim]")
|
228
|
+
|
229
|
+
return {
|
230
|
+
"preview": True,
|
231
|
+
"instance_type": instance_type,
|
232
|
+
"image_id": image_id,
|
233
|
+
"estimated_monthly_cost": 25.00
|
234
|
+
}
|
235
|
+
|
236
|
+
else:
|
237
|
+
# Real instance creation
|
238
|
+
console.print("[green]🚀 Creating EC2 instance...[/green]")
|
239
|
+
|
240
|
+
session = boto3.Session(profile_name=profile)
|
241
|
+
ec2 = session.client('ec2')
|
242
|
+
|
243
|
+
# Use default AMI if not specified
|
244
|
+
if not image_id:
|
245
|
+
# Get latest Amazon Linux 2 AMI
|
246
|
+
images = ec2.describe_images(
|
247
|
+
Owners=['amazon'],
|
248
|
+
Filters=[
|
249
|
+
{'Name': 'name', 'Values': ['amzn2-ami-hvm-*']},
|
250
|
+
{'Name': 'architecture', 'Values': ['x86_64']},
|
251
|
+
{'Name': 'virtualization-type', 'Values': ['hvm']}
|
252
|
+
]
|
253
|
+
)
|
254
|
+
if images['Images']:
|
255
|
+
image_id = sorted(images['Images'], key=lambda x: x['CreationDate'], reverse=True)[0]['ImageId']
|
256
|
+
|
257
|
+
try:
|
258
|
+
response = ec2.run_instances(
|
259
|
+
ImageId=image_id,
|
260
|
+
MinCount=1,
|
261
|
+
MaxCount=1,
|
262
|
+
InstanceType=instance_type,
|
263
|
+
TagSpecifications=[
|
264
|
+
{
|
265
|
+
'ResourceType': 'instance',
|
266
|
+
'Tags': [
|
267
|
+
{'Key': 'CreatedBy', 'Value': 'CloudOps-Runbooks'},
|
268
|
+
{'Key': 'Purpose', 'Value': 'Testing'}
|
269
|
+
]
|
270
|
+
}
|
271
|
+
]
|
272
|
+
)
|
273
|
+
|
274
|
+
instance_id = response['Instances'][0]['InstanceId']
|
275
|
+
print_success(f"EC2 instance created: {instance_id}")
|
276
|
+
|
277
|
+
return {
|
278
|
+
"instance_id": instance_id,
|
279
|
+
"instance_type": instance_type,
|
280
|
+
"image_id": image_id,
|
281
|
+
"created": True
|
282
|
+
}
|
283
|
+
|
284
|
+
except ClientError as e:
|
285
|
+
print_error(f"Failed to create instance: {e}")
|
286
|
+
raise
|
287
|
+
|
288
|
+
|
289
|
+
# =============================================================================
|
290
|
+
# 5. RESOURCE DELETION OPERATIONS (operate module)
|
291
|
+
# =============================================================================
|
292
|
+
|
293
|
+
@resource_deletion_operation(estimated_impact="Delete EC2 instances (~$150/month savings)")
|
294
|
+
def terminate_ec2_instances(
|
295
|
+
dry_run_context: DryRunContext,
|
296
|
+
instance_ids: List[str],
|
297
|
+
profile: Optional[str] = None
|
298
|
+
) -> Dict[str, Any]:
|
299
|
+
"""
|
300
|
+
Example resource deletion operation - EC2 instance termination.
|
301
|
+
|
302
|
+
Resource deletion operations default to dry-run and require confirmation.
|
303
|
+
"""
|
304
|
+
if dry_run_context.enabled:
|
305
|
+
# Preview mode - show what would be deleted
|
306
|
+
console.print("[red]⚠️ Preview: EC2 instance termination[/red]")
|
307
|
+
console.print(f"[dim] • Instances to terminate: {len(instance_ids)}[/dim]")
|
308
|
+
console.print(f"[dim] • Instance IDs: {', '.join(instance_ids)}[/dim]")
|
309
|
+
console.print(f"[dim] • Estimated savings: ~$150/month[/dim]")
|
310
|
+
console.print(f"[dim] • ⚠️ THIS OPERATION IS IRREVERSIBLE[/dim]")
|
311
|
+
|
312
|
+
return {
|
313
|
+
"preview": True,
|
314
|
+
"instances_to_terminate": instance_ids,
|
315
|
+
"estimated_savings": 150.00,
|
316
|
+
"irreversible": True
|
317
|
+
}
|
318
|
+
|
319
|
+
else:
|
320
|
+
# Real instance termination
|
321
|
+
console.print("[red]💥 Terminating EC2 instances...[/red]")
|
322
|
+
|
323
|
+
session = boto3.Session(profile_name=profile)
|
324
|
+
ec2 = session.client('ec2')
|
325
|
+
|
326
|
+
try:
|
327
|
+
response = ec2.terminate_instances(InstanceIds=instance_ids)
|
328
|
+
|
329
|
+
terminated = []
|
330
|
+
for instance in response['TerminatingInstances']:
|
331
|
+
terminated.append({
|
332
|
+
'instance_id': instance['InstanceId'],
|
333
|
+
'current_state': instance['CurrentState']['Name'],
|
334
|
+
'previous_state': instance['PreviousState']['Name']
|
335
|
+
})
|
336
|
+
|
337
|
+
print_success(f"Successfully initiated termination of {len(terminated)} instances")
|
338
|
+
|
339
|
+
return {
|
340
|
+
"terminated_instances": terminated,
|
341
|
+
"count": len(terminated),
|
342
|
+
"operation": "terminate"
|
343
|
+
}
|
344
|
+
|
345
|
+
except ClientError as e:
|
346
|
+
print_error(f"Failed to terminate instances: {e}")
|
347
|
+
raise
|
348
|
+
|
349
|
+
|
350
|
+
# =============================================================================
|
351
|
+
# 6. SECURITY REMEDIATION OPERATIONS (remediation module)
|
352
|
+
# =============================================================================
|
353
|
+
|
354
|
+
@remediation_operation(estimated_impact="Fix S3 public buckets (security improvement)")
|
355
|
+
def fix_public_s3_buckets(
|
356
|
+
dry_run_context: DryRunContext,
|
357
|
+
bucket_names: List[str],
|
358
|
+
profile: Optional[str] = None
|
359
|
+
) -> Dict[str, Any]:
|
360
|
+
"""
|
361
|
+
Example security remediation operation - fix public S3 buckets.
|
362
|
+
|
363
|
+
Remediation operations default to dry-run and require confirmation.
|
364
|
+
"""
|
365
|
+
if dry_run_context.enabled:
|
366
|
+
# Preview mode - show what would be fixed
|
367
|
+
console.print("[yellow]🔧 Preview: S3 bucket security remediation[/yellow]")
|
368
|
+
console.print(f"[dim] • Buckets to secure: {len(bucket_names)}[/dim]")
|
369
|
+
console.print(f"[dim] • Bucket names: {', '.join(bucket_names)}[/dim]")
|
370
|
+
console.print(f"[dim] • Actions: Remove public access, update policies[/dim]")
|
371
|
+
|
372
|
+
return {
|
373
|
+
"preview": True,
|
374
|
+
"buckets_to_fix": bucket_names,
|
375
|
+
"remediation_actions": [
|
376
|
+
"Remove public read access",
|
377
|
+
"Remove public write access",
|
378
|
+
"Update bucket policies",
|
379
|
+
"Enable access logging"
|
380
|
+
]
|
381
|
+
}
|
382
|
+
|
383
|
+
else:
|
384
|
+
# Real security remediation
|
385
|
+
console.print("[green]🔒 Applying security remediation to S3 buckets...[/green]")
|
386
|
+
|
387
|
+
session = boto3.Session(profile_name=profile)
|
388
|
+
s3 = session.client('s3')
|
389
|
+
|
390
|
+
remediated_buckets = []
|
391
|
+
|
392
|
+
for bucket_name in bucket_names:
|
393
|
+
try:
|
394
|
+
# Block public access
|
395
|
+
s3.put_public_access_block(
|
396
|
+
Bucket=bucket_name,
|
397
|
+
PublicAccessBlockConfiguration={
|
398
|
+
'BlockPublicAcls': True,
|
399
|
+
'IgnorePublicAcls': True,
|
400
|
+
'BlockPublicPolicy': True,
|
401
|
+
'RestrictPublicBuckets': True
|
402
|
+
}
|
403
|
+
)
|
404
|
+
|
405
|
+
remediated_buckets.append({
|
406
|
+
'bucket_name': bucket_name,
|
407
|
+
'status': 'secured',
|
408
|
+
'actions_applied': ['public_access_blocked']
|
409
|
+
})
|
410
|
+
|
411
|
+
except ClientError as e:
|
412
|
+
print_warning(f"Could not secure bucket {bucket_name}: {e}")
|
413
|
+
remediated_buckets.append({
|
414
|
+
'bucket_name': bucket_name,
|
415
|
+
'status': 'failed',
|
416
|
+
'error': str(e)
|
417
|
+
})
|
418
|
+
|
419
|
+
successful = len([b for b in remediated_buckets if b['status'] == 'secured'])
|
420
|
+
print_success(f"Successfully secured {successful}/{len(bucket_names)} S3 buckets")
|
421
|
+
|
422
|
+
return {
|
423
|
+
"buckets_processed": remediated_buckets,
|
424
|
+
"successful_count": successful,
|
425
|
+
"total_count": len(bucket_names)
|
426
|
+
}
|
427
|
+
|
428
|
+
|
429
|
+
# =============================================================================
|
430
|
+
# 7. DIRECT FRAMEWORK USAGE (without decorators)
|
431
|
+
# =============================================================================
|
432
|
+
|
433
|
+
def custom_operation_with_framework(
|
434
|
+
dry_run: bool = True,
|
435
|
+
operation_name: str = "custom_operation",
|
436
|
+
resources: Optional[List[str]] = None
|
437
|
+
) -> Dict[str, Any]:
|
438
|
+
"""
|
439
|
+
Example of using the dry-run framework directly without decorators.
|
440
|
+
|
441
|
+
This approach gives you full control over the dry-run behavior
|
442
|
+
and is useful for complex operations that need custom handling.
|
443
|
+
"""
|
444
|
+
# Create dry-run context
|
445
|
+
context = framework.create_context(
|
446
|
+
dry_run=dry_run,
|
447
|
+
operation_type=OperationType.RESOURCE_MODIFY,
|
448
|
+
module_name="example",
|
449
|
+
operation_name=operation_name,
|
450
|
+
target_resources=resources,
|
451
|
+
estimated_impact="Moderate configuration changes"
|
452
|
+
)
|
453
|
+
|
454
|
+
# Display banner
|
455
|
+
framework.display_dry_run_banner(context)
|
456
|
+
|
457
|
+
# Request confirmation if needed
|
458
|
+
if not framework.confirm_operation(context):
|
459
|
+
return {"cancelled": True}
|
460
|
+
|
461
|
+
# Log operation start
|
462
|
+
framework.log_operation_start(context, {"custom_parameter": "example"})
|
463
|
+
|
464
|
+
try:
|
465
|
+
if context.enabled:
|
466
|
+
# Dry-run logic
|
467
|
+
console.print("[dim]🔄 Simulating custom operation...[/dim]")
|
468
|
+
result = {"simulated": True, "resources": resources or []}
|
469
|
+
else:
|
470
|
+
# Real operation logic
|
471
|
+
console.print("[green]⚡ Executing custom operation...[/green]")
|
472
|
+
# Your actual operation code here
|
473
|
+
result = {"executed": True, "resources": resources or []}
|
474
|
+
|
475
|
+
# Log success
|
476
|
+
framework.log_operation_complete(context, success=True, results=result)
|
477
|
+
|
478
|
+
return result
|
479
|
+
|
480
|
+
except Exception as e:
|
481
|
+
# Log failure
|
482
|
+
framework.log_operation_complete(context, success=False, error=str(e))
|
483
|
+
raise
|
484
|
+
|
485
|
+
|
486
|
+
# =============================================================================
|
487
|
+
# 8. MIGRATION HELPER FUNCTIONS
|
488
|
+
# =============================================================================
|
489
|
+
|
490
|
+
def migrate_legacy_dry_run_function(legacy_function: callable) -> callable:
|
491
|
+
"""
|
492
|
+
Helper function to migrate legacy dry-run implementations to the new framework.
|
493
|
+
|
494
|
+
This function can wrap existing functions that have their own dry-run logic
|
495
|
+
and upgrade them to use the unified framework.
|
496
|
+
"""
|
497
|
+
def wrapper(*args, **kwargs):
|
498
|
+
# Extract dry_run parameter
|
499
|
+
dry_run = kwargs.get('dry_run', True)
|
500
|
+
|
501
|
+
# Determine operation type based on function name/behavior
|
502
|
+
func_name = legacy_function.__name__
|
503
|
+
if 'create' in func_name or 'provision' in func_name:
|
504
|
+
op_type = OperationType.RESOURCE_CREATE
|
505
|
+
elif 'delete' in func_name or 'terminate' in func_name or 'remove' in func_name:
|
506
|
+
op_type = OperationType.RESOURCE_DELETE
|
507
|
+
elif 'modify' in func_name or 'update' in func_name or 'change' in func_name:
|
508
|
+
op_type = OperationType.RESOURCE_MODIFY
|
509
|
+
elif 'fix' in func_name or 'remediat' in func_name:
|
510
|
+
op_type = OperationType.REMEDIATION
|
511
|
+
else:
|
512
|
+
op_type = OperationType.ANALYSIS
|
513
|
+
|
514
|
+
# Create context
|
515
|
+
context = framework.create_context(
|
516
|
+
dry_run=dry_run,
|
517
|
+
operation_type=op_type,
|
518
|
+
module_name=legacy_function.__module__.split('.')[-2] if '.' in legacy_function.__module__ else 'legacy',
|
519
|
+
operation_name=func_name
|
520
|
+
)
|
521
|
+
|
522
|
+
# Apply framework behavior
|
523
|
+
framework.display_dry_run_banner(context)
|
524
|
+
|
525
|
+
if not framework.confirm_operation(context):
|
526
|
+
return None
|
527
|
+
|
528
|
+
framework.log_operation_start(context)
|
529
|
+
|
530
|
+
try:
|
531
|
+
# Call the original function with updated context
|
532
|
+
kwargs['dry_run'] = context.enabled
|
533
|
+
result = legacy_function(*args, **kwargs)
|
534
|
+
|
535
|
+
framework.log_operation_complete(context, success=True, results={"migrated": True})
|
536
|
+
return result
|
537
|
+
|
538
|
+
except Exception as e:
|
539
|
+
framework.log_operation_complete(context, success=False, error=str(e))
|
540
|
+
raise
|
541
|
+
|
542
|
+
return wrapper
|
543
|
+
|
544
|
+
|
545
|
+
# =============================================================================
|
546
|
+
# Example CLI Integration
|
547
|
+
# =============================================================================
|
548
|
+
|
549
|
+
if __name__ == "__main__":
|
550
|
+
"""
|
551
|
+
Example CLI usage demonstrating the dry-run framework.
|
552
|
+
"""
|
553
|
+
import click
|
554
|
+
|
555
|
+
@click.group()
|
556
|
+
def cli():
|
557
|
+
"""Example CLI with dry-run framework integration."""
|
558
|
+
pass
|
559
|
+
|
560
|
+
@cli.command()
|
561
|
+
@click.option('--dry-run/--no-dry-run', default=True, help='Enable dry-run mode')
|
562
|
+
@click.option('--profile', help='AWS profile name')
|
563
|
+
@click.option('--regions', multiple=True, help='AWS regions')
|
564
|
+
def discover(dry_run, profile, regions):
|
565
|
+
"""Discover EC2 instances with dry-run support."""
|
566
|
+
result = collect_ec2_instances(
|
567
|
+
profile=profile,
|
568
|
+
regions=list(regions) if regions else None,
|
569
|
+
dry_run=dry_run
|
570
|
+
)
|
571
|
+
console.print(f"Discovery result: {result}")
|
572
|
+
|
573
|
+
@cli.command()
|
574
|
+
@click.option('--dry-run/--no-dry-run', default=True, help='Enable dry-run mode')
|
575
|
+
@click.option('--instance-type', default='t3.micro', help='Instance type')
|
576
|
+
@click.option('--profile', help='AWS profile name')
|
577
|
+
def create(dry_run, instance_type, profile):
|
578
|
+
"""Create EC2 instance with dry-run support."""
|
579
|
+
result = create_ec2_instance(
|
580
|
+
instance_type=instance_type,
|
581
|
+
profile=profile,
|
582
|
+
dry_run=dry_run
|
583
|
+
)
|
584
|
+
console.print(f"Creation result: {result}")
|
585
|
+
|
586
|
+
# Run CLI
|
587
|
+
cli()
|