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.
- runbooks/__init__.py +1 -1
- runbooks/base.py +5 -1
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +871 -0
- runbooks/cfat/assessment/runner.py +122 -11
- runbooks/cfat/models.py +6 -2
- runbooks/common/logger.py +14 -0
- runbooks/common/rich_utils.py +451 -0
- runbooks/enterprise/__init__.py +68 -0
- runbooks/enterprise/error_handling.py +411 -0
- runbooks/enterprise/logging.py +439 -0
- runbooks/enterprise/multi_tenant.py +583 -0
- runbooks/finops/README.md +468 -241
- runbooks/finops/__init__.py +39 -3
- runbooks/finops/cli.py +83 -18
- runbooks/finops/cross_validation.py +375 -0
- runbooks/finops/dashboard_runner.py +812 -164
- runbooks/finops/enhanced_dashboard_runner.py +525 -0
- runbooks/finops/finops_dashboard.py +1892 -0
- runbooks/finops/helpers.py +485 -51
- runbooks/finops/optimizer.py +823 -0
- runbooks/finops/tests/__init__.py +19 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
- runbooks/finops/tests/run_comprehensive_tests.py +421 -0
- runbooks/finops/tests/run_tests.py +305 -0
- runbooks/finops/tests/test_finops_dashboard.py +705 -0
- runbooks/finops/tests/test_integration.py +477 -0
- runbooks/finops/tests/test_performance.py +380 -0
- runbooks/finops/tests/test_performance_benchmarks.py +500 -0
- runbooks/finops/tests/test_reference_images_validation.py +867 -0
- runbooks/finops/tests/test_single_account_features.py +715 -0
- runbooks/finops/tests/validate_test_suite.py +220 -0
- runbooks/finops/types.py +1 -1
- runbooks/hitl/enhanced_workflow_engine.py +725 -0
- runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
- runbooks/inventory/collectors/aws_comprehensive.py +442 -0
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +172 -13
- runbooks/inventory/discovery.md +1 -1
- runbooks/inventory/list_ec2_instances.py +18 -20
- runbooks/inventory/list_ssm_parameters.py +31 -3
- runbooks/inventory/organizations_discovery.py +1269 -0
- runbooks/inventory/rich_inventory_display.py +393 -0
- runbooks/inventory/run_on_multi_accounts.py +35 -19
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/vpc_flow_analyzer.py +1030 -0
- runbooks/main.py +2215 -119
- runbooks/metrics/dora_metrics_engine.py +599 -0
- runbooks/operate/__init__.py +2 -2
- runbooks/operate/base.py +122 -10
- runbooks/operate/deployment_framework.py +1032 -0
- runbooks/operate/deployment_validator.py +853 -0
- runbooks/operate/dynamodb_operations.py +10 -6
- runbooks/operate/ec2_operations.py +319 -11
- runbooks/operate/executive_dashboard.py +779 -0
- runbooks/operate/mcp_integration.py +750 -0
- runbooks/operate/nat_gateway_operations.py +1120 -0
- runbooks/operate/networking_cost_heatmap.py +685 -0
- runbooks/operate/privatelink_operations.py +940 -0
- runbooks/operate/s3_operations.py +10 -6
- runbooks/operate/vpc_endpoints.py +644 -0
- runbooks/operate/vpc_operations.py +1038 -0
- runbooks/remediation/__init__.py +2 -2
- runbooks/remediation/acm_remediation.py +1 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/cloudtrail_remediation.py +1 -1
- runbooks/remediation/cognito_remediation.py +1 -1
- runbooks/remediation/dynamodb_remediation.py +1 -1
- runbooks/remediation/ec2_remediation.py +1 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
- runbooks/remediation/kms_enable_key_rotation.py +1 -1
- runbooks/remediation/kms_remediation.py +1 -1
- runbooks/remediation/lambda_remediation.py +1 -1
- runbooks/remediation/multi_account.py +1 -1
- runbooks/remediation/rds_remediation.py +1 -1
- runbooks/remediation/s3_block_public_access.py +1 -1
- runbooks/remediation/s3_enable_access_logging.py +1 -1
- runbooks/remediation/s3_encryption.py +1 -1
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/remediation/vpc_remediation.py +475 -0
- runbooks/security/__init__.py +3 -1
- runbooks/security/compliance_automation.py +632 -0
- runbooks/security/report_generator.py +10 -0
- runbooks/security/run_script.py +31 -5
- runbooks/security/security_baseline_tester.py +169 -30
- runbooks/security/security_export.py +477 -0
- runbooks/validation/__init__.py +10 -0
- runbooks/validation/benchmark.py +484 -0
- runbooks/validation/cli.py +356 -0
- runbooks/validation/mcp_validator.py +768 -0
- runbooks/vpc/__init__.py +38 -0
- runbooks/vpc/config.py +212 -0
- runbooks/vpc/cost_engine.py +347 -0
- runbooks/vpc/heatmap_engine.py +605 -0
- runbooks/vpc/manager_interface.py +634 -0
- runbooks/vpc/networking_wrapper.py +1260 -0
- runbooks/vpc/rich_formatters.py +679 -0
- runbooks/vpc/tests/__init__.py +5 -0
- runbooks/vpc/tests/conftest.py +356 -0
- runbooks/vpc/tests/test_cli_integration.py +530 -0
- runbooks/vpc/tests/test_config.py +458 -0
- runbooks/vpc/tests/test_cost_engine.py +479 -0
- runbooks/vpc/tests/test_networking_wrapper.py +512 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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}")
|