runbooks 0.7.7__py3-none-any.whl → 0.9.0__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 (157) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +2 -2
  3. runbooks/cfat/README.md +12 -1
  4. runbooks/cfat/__init__.py +8 -4
  5. runbooks/cfat/assessment/collectors.py +171 -14
  6. runbooks/cfat/assessment/compliance.py +546 -522
  7. runbooks/cfat/assessment/runner.py +129 -10
  8. runbooks/cfat/models.py +6 -2
  9. runbooks/common/__init__.py +152 -0
  10. runbooks/common/accuracy_validator.py +1039 -0
  11. runbooks/common/context_logger.py +440 -0
  12. runbooks/common/cross_module_integration.py +594 -0
  13. runbooks/common/enhanced_exception_handler.py +1108 -0
  14. runbooks/common/enterprise_audit_integration.py +634 -0
  15. runbooks/common/logger.py +14 -0
  16. runbooks/common/mcp_integration.py +539 -0
  17. runbooks/common/performance_monitor.py +387 -0
  18. runbooks/common/profile_utils.py +216 -0
  19. runbooks/common/rich_utils.py +622 -0
  20. runbooks/enterprise/__init__.py +68 -0
  21. runbooks/enterprise/error_handling.py +411 -0
  22. runbooks/enterprise/logging.py +439 -0
  23. runbooks/enterprise/multi_tenant.py +583 -0
  24. runbooks/feedback/user_feedback_collector.py +440 -0
  25. runbooks/finops/README.md +129 -14
  26. runbooks/finops/__init__.py +22 -3
  27. runbooks/finops/account_resolver.py +279 -0
  28. runbooks/finops/accuracy_cross_validator.py +638 -0
  29. runbooks/finops/aws_client.py +721 -36
  30. runbooks/finops/budget_integration.py +313 -0
  31. runbooks/finops/cli.py +90 -33
  32. runbooks/finops/cost_processor.py +211 -37
  33. runbooks/finops/dashboard_router.py +900 -0
  34. runbooks/finops/dashboard_runner.py +1334 -399
  35. runbooks/finops/embedded_mcp_validator.py +288 -0
  36. runbooks/finops/enhanced_dashboard_runner.py +526 -0
  37. runbooks/finops/enhanced_progress.py +327 -0
  38. runbooks/finops/enhanced_trend_visualization.py +423 -0
  39. runbooks/finops/finops_dashboard.py +41 -0
  40. runbooks/finops/helpers.py +639 -323
  41. runbooks/finops/iam_guidance.py +400 -0
  42. runbooks/finops/markdown_exporter.py +466 -0
  43. runbooks/finops/multi_dashboard.py +1502 -0
  44. runbooks/finops/optimizer.py +396 -395
  45. runbooks/finops/profile_processor.py +2 -2
  46. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  47. runbooks/finops/runbooks.security.report_generator.log +0 -0
  48. runbooks/finops/runbooks.security.run_script.log +0 -0
  49. runbooks/finops/runbooks.security.security_export.log +0 -0
  50. runbooks/finops/service_mapping.py +195 -0
  51. runbooks/finops/single_dashboard.py +710 -0
  52. runbooks/finops/tests/__init__.py +19 -0
  53. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  54. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  55. runbooks/finops/tests/run_tests.py +305 -0
  56. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  57. runbooks/finops/tests/test_integration.py +477 -0
  58. runbooks/finops/tests/test_performance.py +380 -0
  59. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  60. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  61. runbooks/finops/tests/test_single_account_features.py +715 -0
  62. runbooks/finops/tests/validate_test_suite.py +220 -0
  63. runbooks/finops/types.py +1 -1
  64. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  65. runbooks/inventory/README.md +12 -1
  66. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  67. runbooks/inventory/collectors/aws_comprehensive.py +192 -185
  68. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  69. runbooks/inventory/core/collector.py +299 -12
  70. runbooks/inventory/list_ec2_instances.py +21 -20
  71. runbooks/inventory/list_ssm_parameters.py +31 -3
  72. runbooks/inventory/organizations_discovery.py +1315 -0
  73. runbooks/inventory/rich_inventory_display.py +360 -0
  74. runbooks/inventory/run_on_multi_accounts.py +32 -16
  75. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  76. runbooks/inventory/runbooks.security.run_script.log +0 -0
  77. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  78. runbooks/main.py +4171 -1615
  79. runbooks/metrics/dora_metrics_engine.py +1293 -0
  80. runbooks/monitoring/performance_monitor.py +433 -0
  81. runbooks/operate/README.md +394 -0
  82. runbooks/operate/__init__.py +2 -2
  83. runbooks/operate/base.py +291 -11
  84. runbooks/operate/deployment_framework.py +1032 -0
  85. runbooks/operate/deployment_validator.py +853 -0
  86. runbooks/operate/dynamodb_operations.py +10 -6
  87. runbooks/operate/ec2_operations.py +321 -11
  88. runbooks/operate/executive_dashboard.py +779 -0
  89. runbooks/operate/mcp_integration.py +750 -0
  90. runbooks/operate/nat_gateway_operations.py +1120 -0
  91. runbooks/operate/networking_cost_heatmap.py +685 -0
  92. runbooks/operate/privatelink_operations.py +940 -0
  93. runbooks/operate/s3_operations.py +10 -6
  94. runbooks/operate/vpc_endpoints.py +644 -0
  95. runbooks/operate/vpc_operations.py +1038 -0
  96. runbooks/remediation/README.md +489 -13
  97. runbooks/remediation/__init__.py +2 -2
  98. runbooks/remediation/acm_remediation.py +1 -1
  99. runbooks/remediation/base.py +1 -1
  100. runbooks/remediation/cloudtrail_remediation.py +1 -1
  101. runbooks/remediation/cognito_remediation.py +1 -1
  102. runbooks/remediation/commons.py +8 -4
  103. runbooks/remediation/dynamodb_remediation.py +1 -1
  104. runbooks/remediation/ec2_remediation.py +1 -1
  105. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  106. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  107. runbooks/remediation/kms_remediation.py +1 -1
  108. runbooks/remediation/lambda_remediation.py +1 -1
  109. runbooks/remediation/multi_account.py +1 -1
  110. runbooks/remediation/rds_remediation.py +1 -1
  111. runbooks/remediation/s3_block_public_access.py +1 -1
  112. runbooks/remediation/s3_enable_access_logging.py +1 -1
  113. runbooks/remediation/s3_encryption.py +1 -1
  114. runbooks/remediation/s3_remediation.py +1 -1
  115. runbooks/remediation/vpc_remediation.py +475 -0
  116. runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
  117. runbooks/security/README.md +12 -1
  118. runbooks/security/__init__.py +166 -33
  119. runbooks/security/compliance_automation.py +634 -0
  120. runbooks/security/compliance_automation_engine.py +1021 -0
  121. runbooks/security/enterprise_security_framework.py +931 -0
  122. runbooks/security/enterprise_security_policies.json +293 -0
  123. runbooks/security/integration_test_enterprise_security.py +879 -0
  124. runbooks/security/module_security_integrator.py +641 -0
  125. runbooks/security/report_generator.py +10 -0
  126. runbooks/security/run_script.py +27 -5
  127. runbooks/security/security_baseline_tester.py +153 -27
  128. runbooks/security/security_export.py +456 -0
  129. runbooks/sre/README.md +472 -0
  130. runbooks/sre/__init__.py +33 -0
  131. runbooks/sre/mcp_reliability_engine.py +1049 -0
  132. runbooks/sre/performance_optimization_engine.py +1032 -0
  133. runbooks/sre/reliability_monitoring_framework.py +1011 -0
  134. runbooks/validation/__init__.py +10 -0
  135. runbooks/validation/benchmark.py +489 -0
  136. runbooks/validation/cli.py +368 -0
  137. runbooks/validation/mcp_validator.py +797 -0
  138. runbooks/vpc/README.md +478 -0
  139. runbooks/vpc/__init__.py +38 -0
  140. runbooks/vpc/config.py +212 -0
  141. runbooks/vpc/cost_engine.py +347 -0
  142. runbooks/vpc/heatmap_engine.py +605 -0
  143. runbooks/vpc/manager_interface.py +649 -0
  144. runbooks/vpc/networking_wrapper.py +1289 -0
  145. runbooks/vpc/rich_formatters.py +693 -0
  146. runbooks/vpc/tests/__init__.py +5 -0
  147. runbooks/vpc/tests/conftest.py +356 -0
  148. runbooks/vpc/tests/test_cli_integration.py +530 -0
  149. runbooks/vpc/tests/test_config.py +458 -0
  150. runbooks/vpc/tests/test_cost_engine.py +479 -0
  151. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  152. {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/METADATA +175 -65
  153. {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/RECORD +157 -60
  154. {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/entry_points.txt +1 -1
  155. {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/WHEEL +0 -0
  156. {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/licenses/LICENSE +0 -0
  157. {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/top_level.txt +0 -0
@@ -20,9 +20,13 @@ from typing import Any, Dict, List, Optional, Union
20
20
  import boto3
21
21
  from botocore.exceptions import BotoCoreError, ClientError
22
22
  from loguru import logger
23
+ from rich.console import Console
23
24
 
24
25
  from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
25
26
 
27
+ # Initialize Rich console for enhanced CLI output
28
+ console = Console()
29
+
26
30
 
27
31
  class DynamoDBOperations(BaseOperation):
28
32
  """
@@ -746,8 +750,8 @@ def main():
746
750
  import sys
747
751
 
748
752
  if len(sys.argv) < 2:
749
- print("Usage: python dynamodb_operations.py <operation> [args...]")
750
- print("Operations: put, delete, batch-write, create-table, backup-table")
753
+ console.print("[yellow]Usage: python dynamodb_operations.py <operation> [args...][/yellow]")
754
+ console.print("[blue]Operations: put, delete, batch-write, create-table, backup-table[/blue]")
751
755
  sys.exit(1)
752
756
 
753
757
  operation = sys.argv[1]
@@ -794,14 +798,14 @@ def main():
794
798
  else:
795
799
  raise ValueError(f"Unknown operation: {operation}")
796
800
 
797
- # Print results
801
+ # Print results with Rich formatting
798
802
  for result in results:
799
803
  if result.success:
800
- print(f"✅ {result.operation_type} completed successfully")
804
+ console.print(f"[green]✅ {result.operation_type} completed successfully[/green]")
801
805
  if result.response_data:
802
- print(f" Data: {json.dumps(result.response_data, default=str, indent=2)}")
806
+ console.print(f"[blue] Data: {json.dumps(result.response_data, default=str, indent=2)}[/blue]")
803
807
  else:
804
- print(f"❌ {result.operation_type} failed: {result.error_message}")
808
+ console.print(f"[red]❌ {result.operation_type} failed: {result.error_message}[/red]")
805
809
 
806
810
  except Exception as e:
807
811
  logger.error(f"Error during operation: {e}")
@@ -21,14 +21,18 @@ Version: 2.0.0 - Enterprise Enhancement
21
21
  import base64
22
22
  import json
23
23
  import os
24
- from datetime import datetime
24
+ from datetime import datetime, timedelta
25
25
  from typing import Any, Dict, List, Optional, Union
26
26
 
27
27
  import boto3
28
28
  from botocore.exceptions import BotoCoreError, ClientError
29
29
  from loguru import logger
30
+ from rich.console import Console
31
+ from rich.panel import Panel
32
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
33
+ from rich.table import Table
30
34
 
31
- from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
35
+ from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus, console
32
36
 
33
37
 
34
38
  class EC2Operations(BaseOperation):
@@ -52,6 +56,9 @@ class EC2Operations(BaseOperation):
52
56
  "cleanup_unused_volumes",
53
57
  "cleanup_unused_eips",
54
58
  "reboot_instances",
59
+ "analyze_rightsizing",
60
+ "optimize_instance_types",
61
+ "generate_cost_recommendations",
55
62
  }
56
63
  requires_confirmation = True
57
64
 
@@ -96,7 +103,7 @@ class EC2Operations(BaseOperation):
96
103
  """
97
104
  if not arn.startswith("arn:aws:sns:"):
98
105
  raise ValueError(f"Invalid SNS Topic ARN: {arn}")
99
- logger.info(f"✅ Valid SNS ARN: {arn}")
106
+ console.print(f"[green]✅ Valid SNS ARN: {arn}[/green]")
100
107
 
101
108
  def validate_regions(self, source_region: str, dest_region: str) -> None:
102
109
  """
@@ -116,7 +123,7 @@ class EC2Operations(BaseOperation):
116
123
  raise ValueError(f"Invalid source region: {source_region}")
117
124
  if dest_region not in valid_regions:
118
125
  raise ValueError(f"Invalid destination region: {dest_region}")
119
- logger.info(f"Validated AWS regions: {source_region} -> {dest_region}")
126
+ console.print(f"[blue]🌍 Validated AWS regions: {source_region} -> {dest_region}[/blue]")
120
127
 
121
128
  def send_sns_notification(self, subject: str, message: str) -> None:
122
129
  """
@@ -193,17 +200,23 @@ class EC2Operations(BaseOperation):
193
200
 
194
201
  try:
195
202
  if context.dry_run:
196
- logger.info(f"[DRY-RUN] Would start instance {instance_id}")
203
+ console.print(
204
+ Panel(
205
+ f"[yellow]Would start instance {instance_id}[/yellow]",
206
+ title="🏃 DRY-RUN MODE",
207
+ border_style="yellow",
208
+ )
209
+ )
197
210
  result.mark_completed(OperationStatus.DRY_RUN)
198
211
  else:
199
212
  response = self.execute_aws_call(ec2_client, "start_instances", InstanceIds=[instance_id])
200
213
  result.response_data = response
201
214
  result.mark_completed(OperationStatus.SUCCESS)
202
- logger.info(f"Successfully started instance {instance_id}")
215
+ console.print(f"[green]✅ Successfully started instance {instance_id}[/green]")
203
216
 
204
217
  except ClientError as e:
205
218
  error_msg = f"Failed to start instance {instance_id}: {e}"
206
- logger.error(error_msg)
219
+ console.print(f"[red]❌ {error_msg}[/red]")
207
220
  result.mark_completed(OperationStatus.FAILED, error_msg)
208
221
 
209
222
  results.append(result)
@@ -772,6 +785,303 @@ class EC2Operations(BaseOperation):
772
785
 
773
786
  return results
774
787
 
788
+ def analyze_rightsizing(self, context: OperationContext, days: int = 14) -> OperationResult:
789
+ """
790
+ Analyze EC2 instances for rightsizing opportunities using CloudWatch metrics.
791
+
792
+ Args:
793
+ context: Operation execution context
794
+ days: Number of days to analyze (default: 14)
795
+
796
+ Returns:
797
+ OperationResult with rightsizing recommendations
798
+ """
799
+ result = OperationResult(
800
+ operation_id=f"analyze_rightsizing_{context.account_id}",
801
+ operation_name="analyze_rightsizing",
802
+ resource_id=f"account:{context.account_id}",
803
+ resource_type="account",
804
+ )
805
+
806
+ try:
807
+ # Get all running instances
808
+ response = self.client.describe_instances(Filters=[{"Name": "instance-state-name", "Values": ["running"]}])
809
+
810
+ rightsizing_recommendations = []
811
+ cloudwatch = boto3.client("cloudwatch", region_name=context.region)
812
+
813
+ for reservation in response["Reservations"]:
814
+ for instance in reservation["Instances"]:
815
+ instance_id = instance["InstanceId"]
816
+ current_type = instance["InstanceType"]
817
+
818
+ # Get CPU utilization metrics
819
+ cpu_metrics = cloudwatch.get_metric_statistics(
820
+ Namespace="AWS/EC2",
821
+ MetricName="CPUUtilization",
822
+ Dimensions=[{"Name": "InstanceId", "Value": instance_id}],
823
+ StartTime=datetime.utcnow() - timedelta(days=days),
824
+ EndTime=datetime.utcnow(),
825
+ Period=3600,
826
+ Statistics=["Average", "Maximum"],
827
+ )
828
+
829
+ if cpu_metrics["Datapoints"]:
830
+ avg_cpu = sum(dp["Average"] for dp in cpu_metrics["Datapoints"]) / len(
831
+ cpu_metrics["Datapoints"]
832
+ )
833
+ max_cpu = max(dp["Maximum"] for dp in cpu_metrics["Datapoints"])
834
+
835
+ recommendation = self._generate_rightsizing_recommendation(
836
+ instance_id, current_type, avg_cpu, max_cpu
837
+ )
838
+
839
+ if recommendation:
840
+ rightsizing_recommendations.append(recommendation)
841
+
842
+ result.add_output("rightsizing_recommendations", rightsizing_recommendations)
843
+ result.add_output("total_instances_analyzed", sum(len(r["Instances"]) for r in response["Reservations"]))
844
+ result.add_output("optimization_opportunities", len(rightsizing_recommendations))
845
+
846
+ result.mark_completed(
847
+ OperationStatus.SUCCESS, f"Analyzed {len(rightsizing_recommendations)} rightsizing opportunities"
848
+ )
849
+
850
+ except Exception as e:
851
+ error_msg = f"Failed to analyze rightsizing opportunities: {e}"
852
+ logger.error(error_msg)
853
+ result.mark_completed(OperationStatus.FAILED, error_msg)
854
+
855
+ return result
856
+
857
+ def optimize_instance_types(self, context: OperationContext, recommendations: List[Dict]) -> List[OperationResult]:
858
+ """
859
+ Apply instance type optimizations based on rightsizing recommendations.
860
+
861
+ Args:
862
+ context: Operation execution context
863
+ recommendations: List of rightsizing recommendations
864
+
865
+ Returns:
866
+ List of OperationResults for each optimization
867
+ """
868
+ results = []
869
+
870
+ for rec in recommendations:
871
+ result = OperationResult(
872
+ operation_id=f"optimize_{rec['instance_id']}",
873
+ operation_name="optimize_instance_types",
874
+ resource_id=rec["instance_id"],
875
+ resource_type="ec2_instance",
876
+ )
877
+
878
+ try:
879
+ instance_id = rec["instance_id"]
880
+ new_instance_type = rec["recommended_type"]
881
+
882
+ if context.dry_run:
883
+ result.add_output("action", "DRY_RUN")
884
+ result.add_output("would_change_type", f"{rec['current_type']} -> {new_instance_type}")
885
+ result.add_output("estimated_monthly_savings", rec.get("estimated_savings", 0))
886
+ result.mark_completed(
887
+ OperationStatus.SUCCESS, f"DRY RUN: Would optimize {instance_id} to {new_instance_type}"
888
+ )
889
+ else:
890
+ # Stop instance first
891
+ self.client.stop_instances(InstanceIds=[instance_id])
892
+
893
+ # Wait for instance to stop
894
+ waiter = self.client.get_waiter("instance_stopped")
895
+ waiter.wait(InstanceIds=[instance_id])
896
+
897
+ # Modify instance type
898
+ self.client.modify_instance_attribute(
899
+ InstanceId=instance_id, InstanceType={"Value": new_instance_type}
900
+ )
901
+
902
+ # Start instance
903
+ self.client.start_instances(InstanceIds=[instance_id])
904
+
905
+ result.add_output("action", "OPTIMIZED")
906
+ result.add_output("previous_type", rec["current_type"])
907
+ result.add_output("new_type", new_instance_type)
908
+ result.add_output("estimated_monthly_savings", rec.get("estimated_savings", 0))
909
+
910
+ result.mark_completed(
911
+ OperationStatus.SUCCESS, f"Successfully optimized {instance_id} to {new_instance_type}"
912
+ )
913
+
914
+ except Exception as e:
915
+ error_msg = f"Failed to optimize instance {rec['instance_id']}: {e}"
916
+ logger.error(error_msg)
917
+ result.mark_completed(OperationStatus.FAILED, error_msg)
918
+
919
+ results.append(result)
920
+
921
+ return results
922
+
923
+ def generate_cost_recommendations(
924
+ self, context: OperationContext, target_savings_pct: float = 30.0
925
+ ) -> OperationResult:
926
+ """
927
+ Generate comprehensive cost optimization recommendations for EC2 resources.
928
+
929
+ Args:
930
+ context: Operation execution context
931
+ target_savings_pct: Target savings percentage (default: 30%)
932
+
933
+ Returns:
934
+ OperationResult with cost recommendations
935
+ """
936
+ result = OperationResult(
937
+ operation_id=f"cost_recommendations_{context.account_id}",
938
+ operation_name="generate_cost_recommendations",
939
+ resource_id=f"account:{context.account_id}",
940
+ resource_type="account",
941
+ )
942
+
943
+ try:
944
+ recommendations = []
945
+ total_monthly_spend = 0
946
+ potential_savings = 0
947
+
948
+ # Analyze rightsizing opportunities
949
+ rightsizing_result = self.analyze_rightsizing(context)
950
+ if rightsizing_result.status == OperationStatus.SUCCESS:
951
+ rightsizing_recs = rightsizing_result.outputs.get("rightsizing_recommendations", [])
952
+ for rec in rightsizing_recs:
953
+ recommendations.append(
954
+ {
955
+ "type": "rightsizing",
956
+ "resource_id": rec["instance_id"],
957
+ "current_cost": rec.get("current_monthly_cost", 0),
958
+ "optimized_cost": rec.get("optimized_monthly_cost", 0),
959
+ "monthly_savings": rec.get("estimated_savings", 0),
960
+ "recommendation": f"Rightsize {rec['current_type']} to {rec['recommended_type']}",
961
+ "risk_level": "low",
962
+ "implementation_effort": "medium",
963
+ }
964
+ )
965
+ total_monthly_spend += rec.get("current_monthly_cost", 0)
966
+ potential_savings += rec.get("estimated_savings", 0)
967
+
968
+ # Analyze unused resources
969
+ unused_volumes = self.cleanup_unused_volumes(context)
970
+ if hasattr(unused_volumes, "outputs") and unused_volumes.outputs.get("unused_volumes"):
971
+ for volume in unused_volumes.outputs["unused_volumes"]:
972
+ volume_cost = volume.get("monthly_cost", 50) # Estimate $50/month per unused volume
973
+ recommendations.append(
974
+ {
975
+ "type": "resource_cleanup",
976
+ "resource_id": volume["VolumeId"],
977
+ "current_cost": volume_cost,
978
+ "optimized_cost": 0,
979
+ "monthly_savings": volume_cost,
980
+ "recommendation": f"Delete unused EBS volume {volume['VolumeId']}",
981
+ "risk_level": "low",
982
+ "implementation_effort": "low",
983
+ }
984
+ )
985
+ total_monthly_spend += volume_cost
986
+ potential_savings += volume_cost
987
+
988
+ # Calculate overall metrics
989
+ if total_monthly_spend > 0:
990
+ savings_percentage = (potential_savings / total_monthly_spend) * 100
991
+ meets_target = savings_percentage >= target_savings_pct
992
+ else:
993
+ savings_percentage = 0
994
+ meets_target = False
995
+
996
+ result.add_output("recommendations", recommendations)
997
+ result.add_output("total_recommendations", len(recommendations))
998
+ result.add_output("current_monthly_spend", total_monthly_spend)
999
+ result.add_output("potential_monthly_savings", potential_savings)
1000
+ result.add_output("potential_annual_savings", potential_savings * 12)
1001
+ result.add_output("savings_percentage", savings_percentage)
1002
+ result.add_output("meets_target", meets_target)
1003
+ result.add_output("target_savings_pct", target_savings_pct)
1004
+
1005
+ result.mark_completed(
1006
+ OperationStatus.SUCCESS,
1007
+ f"Generated {len(recommendations)} cost optimization recommendations with {savings_percentage:.1f}% potential savings",
1008
+ )
1009
+
1010
+ except Exception as e:
1011
+ error_msg = f"Failed to generate cost recommendations: {e}"
1012
+ logger.error(error_msg)
1013
+ result.mark_completed(OperationStatus.FAILED, error_msg)
1014
+
1015
+ return result
1016
+
1017
+ def _generate_rightsizing_recommendation(
1018
+ self, instance_id: str, current_type: str, avg_cpu: float, max_cpu: float
1019
+ ) -> Optional[Dict]:
1020
+ """Generate rightsizing recommendation based on CPU metrics."""
1021
+ # Instance type cost mapping (simplified for demonstration)
1022
+ type_costs = {
1023
+ "t3.nano": 4,
1024
+ "t3.micro": 8,
1025
+ "t3.small": 17,
1026
+ "t3.medium": 34,
1027
+ "t3.large": 67,
1028
+ "t3.xlarge": 134,
1029
+ "t3.2xlarge": 268,
1030
+ "m5.large": 78,
1031
+ "m5.xlarge": 156,
1032
+ "m5.2xlarge": 312,
1033
+ "m5.4xlarge": 624,
1034
+ "c5.large": 73,
1035
+ "c5.xlarge": 146,
1036
+ "c5.2xlarge": 292,
1037
+ "c5.4xlarge": 584,
1038
+ }
1039
+
1040
+ current_monthly_cost = type_costs.get(current_type, 100)
1041
+
1042
+ # Rightsizing logic based on CPU utilization
1043
+ if avg_cpu < 10 and max_cpu < 25:
1044
+ # Significantly underutilized - downsize by 2 levels
1045
+ if "xlarge" in current_type:
1046
+ recommended_type = current_type.replace("xlarge", "large")
1047
+ elif "large" in current_type:
1048
+ recommended_type = current_type.replace("large", "medium")
1049
+ elif "medium" in current_type:
1050
+ recommended_type = current_type.replace("medium", "small")
1051
+ else:
1052
+ return None # Already smallest size
1053
+
1054
+ elif avg_cpu < 20 and max_cpu < 50:
1055
+ # Underutilized - downsize by 1 level
1056
+ if "2xlarge" in current_type:
1057
+ recommended_type = current_type.replace("2xlarge", "xlarge")
1058
+ elif "xlarge" in current_type:
1059
+ recommended_type = current_type.replace("xlarge", "large")
1060
+ elif "large" in current_type:
1061
+ recommended_type = current_type.replace("large", "medium")
1062
+ else:
1063
+ return None # Already optimal or too small
1064
+ else:
1065
+ return None # No optimization needed
1066
+
1067
+ optimized_monthly_cost = type_costs.get(recommended_type, current_monthly_cost * 0.7)
1068
+ estimated_savings = current_monthly_cost - optimized_monthly_cost
1069
+
1070
+ if estimated_savings > 5: # Only recommend if savings > $5/month
1071
+ return {
1072
+ "instance_id": instance_id,
1073
+ "current_type": current_type,
1074
+ "recommended_type": recommended_type,
1075
+ "avg_cpu_utilization": avg_cpu,
1076
+ "max_cpu_utilization": max_cpu,
1077
+ "current_monthly_cost": current_monthly_cost,
1078
+ "optimized_monthly_cost": optimized_monthly_cost,
1079
+ "estimated_savings": estimated_savings,
1080
+ "confidence": "high" if avg_cpu < 15 else "medium",
1081
+ }
1082
+
1083
+ return None
1084
+
775
1085
 
776
1086
  # Lambda handlers to append to ec2_operations.py
777
1087
 
@@ -873,8 +1183,8 @@ def main():
873
1183
  import sys
874
1184
 
875
1185
  if len(sys.argv) < 2:
876
- print("Usage: python ec2_operations.py <operation>")
877
- print("Operations: terminate, run, cleanup-volumes, cleanup-eips")
1186
+ console.print("[yellow]Usage: python ec2_operations.py <operation>[/yellow]")
1187
+ console.print("[blue]Operations: terminate, run, cleanup-volumes, cleanup-eips[/blue]")
878
1188
  sys.exit(1)
879
1189
 
880
1190
  operation = sys.argv[1]
@@ -913,9 +1223,9 @@ def main():
913
1223
 
914
1224
  for result in results:
915
1225
  if result.success:
916
- print(f"✅ {result.operation_type} completed successfully")
1226
+ console.print(f"[green]✅ {result.operation_type} completed successfully[/green]")
917
1227
  else:
918
- print(f"❌ {result.operation_type} failed: {result.error_message}")
1228
+ console.print(f"[red]❌ {result.operation_type} failed: {result.error_message}[/red]")
919
1229
 
920
1230
  except Exception as e:
921
1231
  logger.error(f"Error during operation: {e}")