runbooks 1.0.1__py3-none-any.whl → 1.0.2__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 (34) hide show
  1. runbooks/cloudops/models.py +20 -14
  2. runbooks/common/aws_pricing_api.py +276 -44
  3. runbooks/common/dry_run_examples.py +587 -0
  4. runbooks/common/dry_run_framework.py +520 -0
  5. runbooks/common/memory_optimization.py +533 -0
  6. runbooks/common/performance_optimization_engine.py +1153 -0
  7. runbooks/common/profile_utils.py +10 -3
  8. runbooks/common/sre_performance_suite.py +574 -0
  9. runbooks/finops/business_case_config.py +314 -0
  10. runbooks/finops/cost_processor.py +19 -4
  11. runbooks/finops/ebs_cost_optimizer.py +1 -1
  12. runbooks/finops/embedded_mcp_validator.py +642 -36
  13. runbooks/finops/executive_export.py +789 -0
  14. runbooks/finops/finops_scenarios.py +34 -27
  15. runbooks/finops/notebook_utils.py +1 -1
  16. runbooks/finops/schemas.py +73 -58
  17. runbooks/finops/single_dashboard.py +20 -4
  18. runbooks/finops/vpc_cleanup_exporter.py +2 -1
  19. runbooks/inventory/models/account.py +5 -3
  20. runbooks/inventory/models/inventory.py +1 -1
  21. runbooks/inventory/models/resource.py +5 -3
  22. runbooks/inventory/organizations_discovery.py +89 -5
  23. runbooks/main.py +182 -61
  24. runbooks/operate/vpc_operations.py +60 -31
  25. runbooks/remediation/workspaces_list.py +2 -2
  26. runbooks/vpc/config.py +17 -8
  27. runbooks/vpc/heatmap_engine.py +425 -53
  28. runbooks/vpc/performance_optimized_analyzer.py +546 -0
  29. {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
  30. {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/RECORD +34 -26
  31. {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
  32. {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
  33. {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
  34. {runbooks-1.0.1.dist-info → runbooks-1.0.2.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()