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.
- runbooks/__init__.py +1 -1
- runbooks/base.py +2 -2
- runbooks/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +546 -522
- runbooks/cfat/assessment/runner.py +129 -10
- runbooks/cfat/models.py +6 -2
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/logger.py +14 -0
- runbooks/common/mcp_integration.py +539 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +622 -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/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +129 -14
- runbooks/finops/__init__.py +22 -3
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +90 -33
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +1334 -399
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +526 -0
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +41 -0
- runbooks/finops/helpers.py +639 -323
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +396 -395
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -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/README.md +12 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
- runbooks/inventory/collectors/aws_comprehensive.py +192 -185
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +299 -12
- runbooks/inventory/list_ec2_instances.py +21 -20
- runbooks/inventory/list_ssm_parameters.py +31 -3
- runbooks/inventory/organizations_discovery.py +1315 -0
- runbooks/inventory/rich_inventory_display.py +360 -0
- runbooks/inventory/run_on_multi_accounts.py +32 -16
- 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 +4171 -1615
- runbooks/metrics/dora_metrics_engine.py +1293 -0
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/__init__.py +2 -2
- runbooks/operate/base.py +291 -11
- 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 +321 -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/README.md +489 -13
- 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/commons.py +8 -4
- 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/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +166 -33
- runbooks/security/compliance_automation.py +634 -0
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +931 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/report_generator.py +10 -0
- runbooks/security/run_script.py +27 -5
- runbooks/security/security_baseline_tester.py +153 -27
- runbooks/security/security_export.py +456 -0
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +10 -0
- runbooks/validation/benchmark.py +489 -0
- runbooks/validation/cli.py +368 -0
- runbooks/validation/mcp_validator.py +797 -0
- runbooks/vpc/README.md +478 -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 +649 -0
- runbooks/vpc/networking_wrapper.py +1289 -0
- runbooks/vpc/rich_formatters.py +693 -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.7.dist-info → runbooks-0.9.0.dist-info}/METADATA +175 -65
- {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/RECORD +157 -60
- {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/entry_points.txt +1 -1
- {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/WHEEL +0 -0
- {runbooks-0.7.7.dist-info → runbooks-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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,23 @@ class EC2Operations(BaseOperation):
|
|
193
200
|
|
194
201
|
try:
|
195
202
|
if context.dry_run:
|
196
|
-
|
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
|
-
|
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
|
-
|
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}")
|