runbooks 0.7.6__py3-none-any.whl → 0.7.9__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 (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +5 -1
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +871 -0
  6. runbooks/cfat/assessment/runner.py +122 -11
  7. runbooks/cfat/models.py +6 -2
  8. runbooks/common/logger.py +14 -0
  9. runbooks/common/rich_utils.py +451 -0
  10. runbooks/enterprise/__init__.py +68 -0
  11. runbooks/enterprise/error_handling.py +411 -0
  12. runbooks/enterprise/logging.py +439 -0
  13. runbooks/enterprise/multi_tenant.py +583 -0
  14. runbooks/finops/README.md +468 -241
  15. runbooks/finops/__init__.py +39 -3
  16. runbooks/finops/cli.py +83 -18
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +812 -164
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +485 -51
  22. runbooks/finops/optimizer.py +823 -0
  23. runbooks/finops/tests/__init__.py +19 -0
  24. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  25. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  26. runbooks/finops/tests/run_tests.py +305 -0
  27. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  28. runbooks/finops/tests/test_integration.py +477 -0
  29. runbooks/finops/tests/test_performance.py +380 -0
  30. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  31. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  32. runbooks/finops/tests/test_single_account_features.py +715 -0
  33. runbooks/finops/tests/validate_test_suite.py +220 -0
  34. runbooks/finops/types.py +1 -1
  35. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  36. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  37. runbooks/inventory/collectors/aws_comprehensive.py +442 -0
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/discovery.md +1 -1
  41. runbooks/inventory/list_ec2_instances.py +18 -20
  42. runbooks/inventory/list_ssm_parameters.py +31 -3
  43. runbooks/inventory/organizations_discovery.py +1269 -0
  44. runbooks/inventory/rich_inventory_display.py +393 -0
  45. runbooks/inventory/run_on_multi_accounts.py +35 -19
  46. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  47. runbooks/inventory/runbooks.security.run_script.log +0 -0
  48. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  49. runbooks/main.py +2215 -119
  50. runbooks/metrics/dora_metrics_engine.py +599 -0
  51. runbooks/operate/__init__.py +2 -2
  52. runbooks/operate/base.py +122 -10
  53. runbooks/operate/deployment_framework.py +1032 -0
  54. runbooks/operate/deployment_validator.py +853 -0
  55. runbooks/operate/dynamodb_operations.py +10 -6
  56. runbooks/operate/ec2_operations.py +319 -11
  57. runbooks/operate/executive_dashboard.py +779 -0
  58. runbooks/operate/mcp_integration.py +750 -0
  59. runbooks/operate/nat_gateway_operations.py +1120 -0
  60. runbooks/operate/networking_cost_heatmap.py +685 -0
  61. runbooks/operate/privatelink_operations.py +940 -0
  62. runbooks/operate/s3_operations.py +10 -6
  63. runbooks/operate/vpc_endpoints.py +644 -0
  64. runbooks/operate/vpc_operations.py +1038 -0
  65. runbooks/remediation/__init__.py +2 -2
  66. runbooks/remediation/acm_remediation.py +1 -1
  67. runbooks/remediation/base.py +1 -1
  68. runbooks/remediation/cloudtrail_remediation.py +1 -1
  69. runbooks/remediation/cognito_remediation.py +1 -1
  70. runbooks/remediation/dynamodb_remediation.py +1 -1
  71. runbooks/remediation/ec2_remediation.py +1 -1
  72. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  73. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  74. runbooks/remediation/kms_remediation.py +1 -1
  75. runbooks/remediation/lambda_remediation.py +1 -1
  76. runbooks/remediation/multi_account.py +1 -1
  77. runbooks/remediation/rds_remediation.py +1 -1
  78. runbooks/remediation/s3_block_public_access.py +1 -1
  79. runbooks/remediation/s3_enable_access_logging.py +1 -1
  80. runbooks/remediation/s3_encryption.py +1 -1
  81. runbooks/remediation/s3_remediation.py +1 -1
  82. runbooks/remediation/vpc_remediation.py +475 -0
  83. runbooks/security/__init__.py +3 -1
  84. runbooks/security/compliance_automation.py +632 -0
  85. runbooks/security/report_generator.py +10 -0
  86. runbooks/security/run_script.py +31 -5
  87. runbooks/security/security_baseline_tester.py +169 -30
  88. runbooks/security/security_export.py +477 -0
  89. runbooks/validation/__init__.py +10 -0
  90. runbooks/validation/benchmark.py +484 -0
  91. runbooks/validation/cli.py +356 -0
  92. runbooks/validation/mcp_validator.py +768 -0
  93. runbooks/vpc/__init__.py +38 -0
  94. runbooks/vpc/config.py +212 -0
  95. runbooks/vpc/cost_engine.py +347 -0
  96. runbooks/vpc/heatmap_engine.py +605 -0
  97. runbooks/vpc/manager_interface.py +634 -0
  98. runbooks/vpc/networking_wrapper.py +1260 -0
  99. runbooks/vpc/rich_formatters.py +679 -0
  100. runbooks/vpc/tests/__init__.py +5 -0
  101. runbooks/vpc/tests/conftest.py +356 -0
  102. runbooks/vpc/tests/test_cli_integration.py +530 -0
  103. runbooks/vpc/tests/test_config.py +458 -0
  104. runbooks/vpc/tests/test_cost_engine.py +479 -0
  105. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  106. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
  107. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
  108. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
  109. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.7.6.dist-info → runbooks-0.7.9.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,21 @@ 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(Panel(
204
+ f"[yellow]Would start instance {instance_id}[/yellow]",
205
+ title="🏃 DRY-RUN MODE",
206
+ border_style="yellow"
207
+ ))
197
208
  result.mark_completed(OperationStatus.DRY_RUN)
198
209
  else:
199
210
  response = self.execute_aws_call(ec2_client, "start_instances", InstanceIds=[instance_id])
200
211
  result.response_data = response
201
212
  result.mark_completed(OperationStatus.SUCCESS)
202
- logger.info(f"Successfully started instance {instance_id}")
213
+ console.print(f"[green]✅ Successfully started instance {instance_id}[/green]")
203
214
 
204
215
  except ClientError as e:
205
216
  error_msg = f"Failed to start instance {instance_id}: {e}"
206
- logger.error(error_msg)
217
+ console.print(f"[red]❌ {error_msg}[/red]")
207
218
  result.mark_completed(OperationStatus.FAILED, error_msg)
208
219
 
209
220
  results.append(result)
@@ -772,6 +783,303 @@ class EC2Operations(BaseOperation):
772
783
 
773
784
  return results
774
785
 
786
+ def analyze_rightsizing(self, context: OperationContext, days: int = 14) -> OperationResult:
787
+ """
788
+ Analyze EC2 instances for rightsizing opportunities using CloudWatch metrics.
789
+
790
+ Args:
791
+ context: Operation execution context
792
+ days: Number of days to analyze (default: 14)
793
+
794
+ Returns:
795
+ OperationResult with rightsizing recommendations
796
+ """
797
+ result = OperationResult(
798
+ operation_id=f"analyze_rightsizing_{context.account_id}",
799
+ operation_name="analyze_rightsizing",
800
+ resource_id=f"account:{context.account_id}",
801
+ resource_type="account",
802
+ )
803
+
804
+ try:
805
+ # Get all running instances
806
+ response = self.client.describe_instances(Filters=[{"Name": "instance-state-name", "Values": ["running"]}])
807
+
808
+ rightsizing_recommendations = []
809
+ cloudwatch = boto3.client("cloudwatch", region_name=context.region)
810
+
811
+ for reservation in response["Reservations"]:
812
+ for instance in reservation["Instances"]:
813
+ instance_id = instance["InstanceId"]
814
+ current_type = instance["InstanceType"]
815
+
816
+ # Get CPU utilization metrics
817
+ cpu_metrics = cloudwatch.get_metric_statistics(
818
+ Namespace="AWS/EC2",
819
+ MetricName="CPUUtilization",
820
+ Dimensions=[{"Name": "InstanceId", "Value": instance_id}],
821
+ StartTime=datetime.utcnow() - timedelta(days=days),
822
+ EndTime=datetime.utcnow(),
823
+ Period=3600,
824
+ Statistics=["Average", "Maximum"],
825
+ )
826
+
827
+ if cpu_metrics["Datapoints"]:
828
+ avg_cpu = sum(dp["Average"] for dp in cpu_metrics["Datapoints"]) / len(
829
+ cpu_metrics["Datapoints"]
830
+ )
831
+ max_cpu = max(dp["Maximum"] for dp in cpu_metrics["Datapoints"])
832
+
833
+ recommendation = self._generate_rightsizing_recommendation(
834
+ instance_id, current_type, avg_cpu, max_cpu
835
+ )
836
+
837
+ if recommendation:
838
+ rightsizing_recommendations.append(recommendation)
839
+
840
+ result.add_output("rightsizing_recommendations", rightsizing_recommendations)
841
+ result.add_output("total_instances_analyzed", sum(len(r["Instances"]) for r in response["Reservations"]))
842
+ result.add_output("optimization_opportunities", len(rightsizing_recommendations))
843
+
844
+ result.mark_completed(
845
+ OperationStatus.SUCCESS, f"Analyzed {len(rightsizing_recommendations)} rightsizing opportunities"
846
+ )
847
+
848
+ except Exception as e:
849
+ error_msg = f"Failed to analyze rightsizing opportunities: {e}"
850
+ logger.error(error_msg)
851
+ result.mark_completed(OperationStatus.FAILED, error_msg)
852
+
853
+ return result
854
+
855
+ def optimize_instance_types(self, context: OperationContext, recommendations: List[Dict]) -> List[OperationResult]:
856
+ """
857
+ Apply instance type optimizations based on rightsizing recommendations.
858
+
859
+ Args:
860
+ context: Operation execution context
861
+ recommendations: List of rightsizing recommendations
862
+
863
+ Returns:
864
+ List of OperationResults for each optimization
865
+ """
866
+ results = []
867
+
868
+ for rec in recommendations:
869
+ result = OperationResult(
870
+ operation_id=f"optimize_{rec['instance_id']}",
871
+ operation_name="optimize_instance_types",
872
+ resource_id=rec["instance_id"],
873
+ resource_type="ec2_instance",
874
+ )
875
+
876
+ try:
877
+ instance_id = rec["instance_id"]
878
+ new_instance_type = rec["recommended_type"]
879
+
880
+ if context.dry_run:
881
+ result.add_output("action", "DRY_RUN")
882
+ result.add_output("would_change_type", f"{rec['current_type']} -> {new_instance_type}")
883
+ result.add_output("estimated_monthly_savings", rec.get("estimated_savings", 0))
884
+ result.mark_completed(
885
+ OperationStatus.SUCCESS, f"DRY RUN: Would optimize {instance_id} to {new_instance_type}"
886
+ )
887
+ else:
888
+ # Stop instance first
889
+ self.client.stop_instances(InstanceIds=[instance_id])
890
+
891
+ # Wait for instance to stop
892
+ waiter = self.client.get_waiter("instance_stopped")
893
+ waiter.wait(InstanceIds=[instance_id])
894
+
895
+ # Modify instance type
896
+ self.client.modify_instance_attribute(
897
+ InstanceId=instance_id, InstanceType={"Value": new_instance_type}
898
+ )
899
+
900
+ # Start instance
901
+ self.client.start_instances(InstanceIds=[instance_id])
902
+
903
+ result.add_output("action", "OPTIMIZED")
904
+ result.add_output("previous_type", rec["current_type"])
905
+ result.add_output("new_type", new_instance_type)
906
+ result.add_output("estimated_monthly_savings", rec.get("estimated_savings", 0))
907
+
908
+ result.mark_completed(
909
+ OperationStatus.SUCCESS, f"Successfully optimized {instance_id} to {new_instance_type}"
910
+ )
911
+
912
+ except Exception as e:
913
+ error_msg = f"Failed to optimize instance {rec['instance_id']}: {e}"
914
+ logger.error(error_msg)
915
+ result.mark_completed(OperationStatus.FAILED, error_msg)
916
+
917
+ results.append(result)
918
+
919
+ return results
920
+
921
+ def generate_cost_recommendations(
922
+ self, context: OperationContext, target_savings_pct: float = 30.0
923
+ ) -> OperationResult:
924
+ """
925
+ Generate comprehensive cost optimization recommendations for EC2 resources.
926
+
927
+ Args:
928
+ context: Operation execution context
929
+ target_savings_pct: Target savings percentage (default: 30%)
930
+
931
+ Returns:
932
+ OperationResult with cost recommendations
933
+ """
934
+ result = OperationResult(
935
+ operation_id=f"cost_recommendations_{context.account_id}",
936
+ operation_name="generate_cost_recommendations",
937
+ resource_id=f"account:{context.account_id}",
938
+ resource_type="account",
939
+ )
940
+
941
+ try:
942
+ recommendations = []
943
+ total_monthly_spend = 0
944
+ potential_savings = 0
945
+
946
+ # Analyze rightsizing opportunities
947
+ rightsizing_result = self.analyze_rightsizing(context)
948
+ if rightsizing_result.status == OperationStatus.SUCCESS:
949
+ rightsizing_recs = rightsizing_result.outputs.get("rightsizing_recommendations", [])
950
+ for rec in rightsizing_recs:
951
+ recommendations.append(
952
+ {
953
+ "type": "rightsizing",
954
+ "resource_id": rec["instance_id"],
955
+ "current_cost": rec.get("current_monthly_cost", 0),
956
+ "optimized_cost": rec.get("optimized_monthly_cost", 0),
957
+ "monthly_savings": rec.get("estimated_savings", 0),
958
+ "recommendation": f"Rightsize {rec['current_type']} to {rec['recommended_type']}",
959
+ "risk_level": "low",
960
+ "implementation_effort": "medium",
961
+ }
962
+ )
963
+ total_monthly_spend += rec.get("current_monthly_cost", 0)
964
+ potential_savings += rec.get("estimated_savings", 0)
965
+
966
+ # Analyze unused resources
967
+ unused_volumes = self.cleanup_unused_volumes(context)
968
+ if hasattr(unused_volumes, "outputs") and unused_volumes.outputs.get("unused_volumes"):
969
+ for volume in unused_volumes.outputs["unused_volumes"]:
970
+ volume_cost = volume.get("monthly_cost", 50) # Estimate $50/month per unused volume
971
+ recommendations.append(
972
+ {
973
+ "type": "resource_cleanup",
974
+ "resource_id": volume["VolumeId"],
975
+ "current_cost": volume_cost,
976
+ "optimized_cost": 0,
977
+ "monthly_savings": volume_cost,
978
+ "recommendation": f"Delete unused EBS volume {volume['VolumeId']}",
979
+ "risk_level": "low",
980
+ "implementation_effort": "low",
981
+ }
982
+ )
983
+ total_monthly_spend += volume_cost
984
+ potential_savings += volume_cost
985
+
986
+ # Calculate overall metrics
987
+ if total_monthly_spend > 0:
988
+ savings_percentage = (potential_savings / total_monthly_spend) * 100
989
+ meets_target = savings_percentage >= target_savings_pct
990
+ else:
991
+ savings_percentage = 0
992
+ meets_target = False
993
+
994
+ result.add_output("recommendations", recommendations)
995
+ result.add_output("total_recommendations", len(recommendations))
996
+ result.add_output("current_monthly_spend", total_monthly_spend)
997
+ result.add_output("potential_monthly_savings", potential_savings)
998
+ result.add_output("potential_annual_savings", potential_savings * 12)
999
+ result.add_output("savings_percentage", savings_percentage)
1000
+ result.add_output("meets_target", meets_target)
1001
+ result.add_output("target_savings_pct", target_savings_pct)
1002
+
1003
+ result.mark_completed(
1004
+ OperationStatus.SUCCESS,
1005
+ f"Generated {len(recommendations)} cost optimization recommendations with {savings_percentage:.1f}% potential savings",
1006
+ )
1007
+
1008
+ except Exception as e:
1009
+ error_msg = f"Failed to generate cost recommendations: {e}"
1010
+ logger.error(error_msg)
1011
+ result.mark_completed(OperationStatus.FAILED, error_msg)
1012
+
1013
+ return result
1014
+
1015
+ def _generate_rightsizing_recommendation(
1016
+ self, instance_id: str, current_type: str, avg_cpu: float, max_cpu: float
1017
+ ) -> Optional[Dict]:
1018
+ """Generate rightsizing recommendation based on CPU metrics."""
1019
+ # Instance type cost mapping (simplified for demonstration)
1020
+ type_costs = {
1021
+ "t3.nano": 4,
1022
+ "t3.micro": 8,
1023
+ "t3.small": 17,
1024
+ "t3.medium": 34,
1025
+ "t3.large": 67,
1026
+ "t3.xlarge": 134,
1027
+ "t3.2xlarge": 268,
1028
+ "m5.large": 78,
1029
+ "m5.xlarge": 156,
1030
+ "m5.2xlarge": 312,
1031
+ "m5.4xlarge": 624,
1032
+ "c5.large": 73,
1033
+ "c5.xlarge": 146,
1034
+ "c5.2xlarge": 292,
1035
+ "c5.4xlarge": 584,
1036
+ }
1037
+
1038
+ current_monthly_cost = type_costs.get(current_type, 100)
1039
+
1040
+ # Rightsizing logic based on CPU utilization
1041
+ if avg_cpu < 10 and max_cpu < 25:
1042
+ # Significantly underutilized - downsize by 2 levels
1043
+ if "xlarge" in current_type:
1044
+ recommended_type = current_type.replace("xlarge", "large")
1045
+ elif "large" in current_type:
1046
+ recommended_type = current_type.replace("large", "medium")
1047
+ elif "medium" in current_type:
1048
+ recommended_type = current_type.replace("medium", "small")
1049
+ else:
1050
+ return None # Already smallest size
1051
+
1052
+ elif avg_cpu < 20 and max_cpu < 50:
1053
+ # Underutilized - downsize by 1 level
1054
+ if "2xlarge" in current_type:
1055
+ recommended_type = current_type.replace("2xlarge", "xlarge")
1056
+ elif "xlarge" in current_type:
1057
+ recommended_type = current_type.replace("xlarge", "large")
1058
+ elif "large" in current_type:
1059
+ recommended_type = current_type.replace("large", "medium")
1060
+ else:
1061
+ return None # Already optimal or too small
1062
+ else:
1063
+ return None # No optimization needed
1064
+
1065
+ optimized_monthly_cost = type_costs.get(recommended_type, current_monthly_cost * 0.7)
1066
+ estimated_savings = current_monthly_cost - optimized_monthly_cost
1067
+
1068
+ if estimated_savings > 5: # Only recommend if savings > $5/month
1069
+ return {
1070
+ "instance_id": instance_id,
1071
+ "current_type": current_type,
1072
+ "recommended_type": recommended_type,
1073
+ "avg_cpu_utilization": avg_cpu,
1074
+ "max_cpu_utilization": max_cpu,
1075
+ "current_monthly_cost": current_monthly_cost,
1076
+ "optimized_monthly_cost": optimized_monthly_cost,
1077
+ "estimated_savings": estimated_savings,
1078
+ "confidence": "high" if avg_cpu < 15 else "medium",
1079
+ }
1080
+
1081
+ return None
1082
+
775
1083
 
776
1084
  # Lambda handlers to append to ec2_operations.py
777
1085
 
@@ -873,8 +1181,8 @@ def main():
873
1181
  import sys
874
1182
 
875
1183
  if len(sys.argv) < 2:
876
- print("Usage: python ec2_operations.py <operation>")
877
- print("Operations: terminate, run, cleanup-volumes, cleanup-eips")
1184
+ console.print("[yellow]Usage: python ec2_operations.py <operation>[/yellow]")
1185
+ console.print("[blue]Operations: terminate, run, cleanup-volumes, cleanup-eips[/blue]")
878
1186
  sys.exit(1)
879
1187
 
880
1188
  operation = sys.argv[1]
@@ -913,9 +1221,9 @@ def main():
913
1221
 
914
1222
  for result in results:
915
1223
  if result.success:
916
- print(f"✅ {result.operation_type} completed successfully")
1224
+ console.print(f"[green]✅ {result.operation_type} completed successfully[/green]")
917
1225
  else:
918
- print(f"❌ {result.operation_type} failed: {result.error_message}")
1226
+ console.print(f"[red]❌ {result.operation_type} failed: {result.error_message}[/red]")
919
1227
 
920
1228
  except Exception as e:
921
1229
  logger.error(f"Error during operation: {e}")