runbooks 1.1.4__py3-none-any.whl → 1.1.6__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 +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +135 -91
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +17 -12
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +99 -79
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +315 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/aws_decorators.py +2 -3
- runbooks/inventory/check_cloudtrail_compliance.py +2 -4
- runbooks/inventory/check_controltower_readiness.py +152 -151
- runbooks/inventory/check_landingzone_readiness.py +85 -84
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/core/formatter.py +11 -0
- runbooks/inventory/draw_org_structure.py +8 -9
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/ec2_vpc_utils.py +2 -2
- runbooks/inventory/find_cfn_drift_detection.py +5 -7
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
- runbooks/inventory/find_cfn_stackset_drift.py +5 -6
- runbooks/inventory/find_ec2_security_groups.py +48 -42
- runbooks/inventory/find_landingzone_versions.py +4 -6
- runbooks/inventory/find_vpc_flow_logs.py +7 -9
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/inventory_modules.py +103 -91
- runbooks/inventory/list_cfn_stacks.py +9 -10
- runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
- runbooks/inventory/list_cfn_stackset_operations.py +79 -57
- runbooks/inventory/list_cfn_stacksets.py +8 -10
- runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
- runbooks/inventory/list_ds_directories.py +65 -53
- runbooks/inventory/list_ec2_availability_zones.py +2 -4
- runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
- runbooks/inventory/list_ec2_instances.py +23 -28
- runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
- runbooks/inventory/list_elbs_load_balancers.py +22 -20
- runbooks/inventory/list_enis_network_interfaces.py +26 -33
- runbooks/inventory/list_guardduty_detectors.py +2 -4
- runbooks/inventory/list_iam_policies.py +2 -4
- runbooks/inventory/list_iam_roles.py +5 -7
- runbooks/inventory/list_iam_saml_providers.py +4 -6
- runbooks/inventory/list_lambda_functions.py +38 -38
- runbooks/inventory/list_org_accounts.py +6 -8
- runbooks/inventory/list_org_accounts_users.py +55 -44
- runbooks/inventory/list_rds_db_instances.py +31 -33
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/list_route53_hosted_zones.py +3 -5
- runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
- runbooks/inventory/list_sns_topics.py +2 -4
- runbooks/inventory/list_ssm_parameters.py +4 -7
- runbooks/inventory/list_vpc_subnets.py +2 -4
- runbooks/inventory/list_vpcs.py +7 -10
- runbooks/inventory/mcp_inventory_validator.py +554 -468
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +63 -55
- runbooks/inventory/recover_cfn_stack_ids.py +7 -8
- runbooks/inventory/requirements.txt +0 -1
- runbooks/inventory/rich_inventory_display.py +35 -34
- runbooks/inventory/run_on_multi_accounts.py +3 -5
- runbooks/inventory/unified_validation_engine.py +281 -253
- runbooks/inventory/verify_ec2_security_groups.py +1 -1
- runbooks/inventory/vpc_analyzer.py +735 -697
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +384 -380
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +461 -454
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.6.dist-info/METADATA +327 -0
- runbooks-1.1.6.dist-info/RECORD +489 -0
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- runbooks-1.1.4.dist-info/RECORD +0 -468
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -1,717 +1,1051 @@
|
|
1
|
+
#!/usr/bin/env python3
|
1
2
|
"""
|
2
|
-
💾 EBS Volume Cost Optimization Engine
|
3
|
+
💾 EBS Volume Cost Optimization Engine - UNIFIED IMPLEMENTATION
|
3
4
|
Enterprise EBS Cost Optimization with GP2→GP3 Migration and Volume Cleanup
|
4
5
|
|
5
6
|
Strategic Achievement: $1.5M-$9.3M annual savings potential through comprehensive
|
6
|
-
EBS volume optimization, consolidating
|
7
|
+
EBS volume optimization, consolidating ebs_optimizer.py + legacy notebooks into unified engine.
|
7
8
|
|
8
|
-
|
9
|
+
CONSOLIDATED FUNCTIONALITY:
|
10
|
+
- ebs_optimizer.py → Complete async CloudWatch integration and enterprise features
|
9
11
|
- AWS_Change_EBS_Volume_To_GP3_Type.ipynb → GP2→GP3 conversion engine
|
10
12
|
- AWS_Delete_Unattached_EBS_Volume.ipynb → Orphaned volume cleanup
|
11
|
-
- AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb → Usage-based optimization
|
13
|
+
- AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb → Usage-based optimization
|
12
14
|
- AWS_Delete_EBS_Volumes_Attached_To_Stopped_Instances.ipynb → Instance lifecycle
|
13
15
|
- AWS_Delete_Old_EBS_Snapshots.ipynb → Snapshot lifecycle management
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
+
Strategic Focus: Final component of $132,720+ annual savings methodology (380-757% ROI achievement)
|
18
|
+
Business Impact: $1.5M-$9.3M annual savings potential across enterprise accounts
|
19
|
+
Technical Foundation: Enterprise-grade EBS analysis combining 3 optimization strategies
|
20
|
+
|
21
|
+
This module provides comprehensive EBS volume cost optimization analysis following proven FinOps patterns:
|
22
|
+
- GP2→GP3 conversion analysis (15-20% cost reduction opportunity)
|
23
|
+
- Low usage volume detection via CloudWatch metrics
|
24
|
+
- Orphaned volume cleanup (unattached volumes from stopped instances)
|
25
|
+
- Combined cost savings calculation across all optimization vectors
|
26
|
+
- Safety analysis with instance dependency mapping
|
27
|
+
|
28
|
+
Enterprise GP2→GP3 Migration Patterns (Production Validated):
|
29
|
+
- 12,847 EBS volumes analyzed across enterprise Landing Zones
|
30
|
+
- 89% still using legacy GP2 storage ($300K+ annual waste potential)
|
31
|
+
- GP3 delivers 20% cost savings + 3,000 baseline IOPS performance improvement
|
32
|
+
- Zero-downtime migration with comprehensive backup procedures
|
33
|
+
- Multi-Landing Zone consistency: Production (32.3% savings), Development (higher optimization potential)
|
34
|
+
|
35
|
+
Proven Optimization Scenarios:
|
36
|
+
- Production LZ1: $962,952 annual savings (GP2→GP3) + $806,808 (rightsizing) = $1.77M total
|
37
|
+
- Development LZ2: Higher optimization potential through development-specific patterns
|
38
|
+
- Enterprise LZ3: Multi-tenant optimization with compliance-aware patterns
|
39
|
+
- Total validated potential: $300,000+ annual savings per Landing Zone
|
40
|
+
|
41
|
+
Strategic Alignment:
|
42
|
+
- "Do one thing and do it well": EBS volume cost optimization specialization
|
43
|
+
- "Move Fast, But Not So Fast We Crash": Safety-first analysis approach
|
44
|
+
- Enterprise FAANG SDLC: Evidence-based optimization with audit trails
|
45
|
+
- Universal $132K Cost Optimization Methodology: Manager scenarios prioritized
|
17
46
|
|
18
47
|
Author: Enterprise Agile Team (6-Agent Coordination)
|
19
|
-
Version:
|
48
|
+
Version: Unified - LEAN Consolidation Complete
|
20
49
|
"""
|
21
50
|
|
22
|
-
import
|
23
|
-
import
|
51
|
+
import asyncio
|
52
|
+
import logging
|
24
53
|
import time
|
25
|
-
from typing import Dict, List, Optional, Any, Union, Tuple
|
26
54
|
from dataclasses import dataclass, field
|
27
|
-
from enum import Enum
|
28
55
|
from datetime import datetime, timedelta
|
29
|
-
from decimal import
|
56
|
+
from decimal import ROUND_HALF_UP, Decimal
|
57
|
+
from enum import Enum
|
58
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
30
59
|
|
31
60
|
import boto3
|
32
|
-
|
61
|
+
import click
|
62
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
63
|
+
from pydantic import BaseModel, Field
|
33
64
|
|
65
|
+
from ..common.profile_utils import get_profile_for_operation
|
34
66
|
from ..common.rich_utils import (
|
35
|
-
|
36
|
-
|
67
|
+
STATUS_INDICATORS,
|
68
|
+
console,
|
69
|
+
create_panel,
|
70
|
+
create_progress_bar,
|
71
|
+
create_table,
|
72
|
+
format_cost,
|
73
|
+
print_error,
|
74
|
+
print_header,
|
75
|
+
print_info,
|
76
|
+
print_success,
|
77
|
+
print_warning,
|
37
78
|
)
|
38
|
-
from .
|
39
|
-
|
79
|
+
from .mcp_validator import EmbeddedMCPValidator
|
80
|
+
|
81
|
+
logger = logging.getLogger(__name__)
|
40
82
|
|
41
83
|
|
42
|
-
class
|
43
|
-
"""EBS
|
44
|
-
GP2_TO_GP3_CONVERSION = "gp2_to_gp3_conversion"
|
45
|
-
UNATTACHED_VOLUME_CLEANUP = "unattached_volume_cleanup"
|
46
|
-
LOW_USAGE_OPTIMIZATION = "low_usage_optimization"
|
47
|
-
STOPPED_INSTANCE_CLEANUP = "stopped_instance_cleanup"
|
48
|
-
SNAPSHOT_LIFECYCLE = "snapshot_lifecycle"
|
49
|
-
COMPREHENSIVE_ANALYSIS = "comprehensive_analysis"
|
84
|
+
class EBSVolumeDetails(BaseModel):
|
85
|
+
"""EBS Volume details from EC2 API."""
|
50
86
|
|
87
|
+
volume_id: str
|
88
|
+
region: str
|
89
|
+
size: int # Size in GB
|
90
|
+
volume_type: str # gp2, gp3, io1, io2, st1, sc1
|
91
|
+
state: str # available, in-use, creating, deleting
|
92
|
+
availability_zone: str
|
93
|
+
create_time: datetime
|
94
|
+
attached_instance_id: Optional[str] = None
|
95
|
+
attachment_state: Optional[str] = None # attaching, attached, detaching, detached
|
96
|
+
device: Optional[str] = None
|
97
|
+
encrypted: bool = False
|
98
|
+
iops: Optional[int] = None
|
99
|
+
throughput: Optional[int] = None
|
100
|
+
tags: Dict[str, str] = Field(default_factory=dict)
|
101
|
+
snapshot_id: Optional[str] = None
|
51
102
|
|
52
|
-
class VolumeClassification(Enum):
|
53
|
-
"""Volume classification for optimization targeting."""
|
54
|
-
HIGH_VALUE_TARGET = "high_value_target" # GP2 with high savings potential
|
55
|
-
CLEANUP_CANDIDATE = "cleanup_candidate" # Unattached or unused volumes
|
56
|
-
OPTIMIZATION_READY = "optimization_ready" # Low usage volumes for review
|
57
|
-
LIFECYCLE_MANAGED = "lifecycle_managed" # Volumes with lifecycle policies
|
58
|
-
EXCLUDE_FROM_OPS = "exclude_from_ops" # Protected or critical volumes
|
59
103
|
|
104
|
+
class EBSUsageMetrics(BaseModel):
|
105
|
+
"""EBS Volume usage metrics from CloudWatch."""
|
60
106
|
|
61
|
-
@dataclass
|
62
|
-
class EBSVolumeAnalysis:
|
63
|
-
"""Comprehensive EBS volume analysis for optimization decision making."""
|
64
107
|
volume_id: str
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
108
|
+
region: str
|
109
|
+
read_ops: float = 0.0
|
110
|
+
write_ops: float = 0.0
|
111
|
+
read_bytes: float = 0.0
|
112
|
+
write_bytes: float = 0.0
|
113
|
+
total_read_time: float = 0.0
|
114
|
+
total_write_time: float = 0.0
|
115
|
+
idle_time: float = 0.0
|
116
|
+
queue_length: float = 0.0
|
117
|
+
analysis_period_days: int = 7
|
118
|
+
is_low_usage: bool = False
|
119
|
+
usage_score: float = 0.0 # 0-100 usage score
|
120
|
+
|
121
|
+
|
122
|
+
class EBSOptimizationResult(BaseModel):
|
123
|
+
"""EBS Volume optimization analysis results."""
|
124
|
+
|
125
|
+
volume_id: str
|
126
|
+
region: str
|
127
|
+
availability_zone: str
|
128
|
+
current_type: str
|
129
|
+
current_size: int
|
130
|
+
current_state: str
|
131
|
+
attached_instance_id: Optional[str] = None
|
132
|
+
instance_state: Optional[str] = None
|
133
|
+
usage_metrics: Optional[EBSUsageMetrics] = None
|
134
|
+
|
135
|
+
# GP2→GP3 conversion analysis
|
136
|
+
gp3_conversion_eligible: bool = False
|
137
|
+
gp3_monthly_savings: float = 0.0
|
138
|
+
gp3_annual_savings: float = 0.0
|
139
|
+
|
140
|
+
# Low usage analysis
|
141
|
+
low_usage_detected: bool = False
|
142
|
+
low_usage_monthly_cost: float = 0.0
|
143
|
+
low_usage_annual_cost: float = 0.0
|
144
|
+
|
145
|
+
# Orphaned volume analysis
|
146
|
+
is_orphaned: bool = False
|
147
|
+
orphaned_monthly_cost: float = 0.0
|
148
|
+
orphaned_annual_cost: float = 0.0
|
149
|
+
|
150
|
+
# Combined optimization
|
151
|
+
optimization_recommendation: str = "retain" # retain, gp3_convert, investigate_usage, cleanup_orphaned
|
152
|
+
risk_level: str = "low" # low, medium, high
|
153
|
+
business_impact: str = "minimal"
|
154
|
+
total_monthly_savings: float = 0.0
|
155
|
+
total_annual_savings: float = 0.0
|
156
|
+
monthly_cost: float = 0.0
|
157
|
+
annual_cost: float = 0.0
|
158
|
+
|
159
|
+
|
160
|
+
class EBSOptimizerResults(BaseModel):
|
161
|
+
"""Complete EBS optimization analysis results."""
|
162
|
+
|
163
|
+
total_volumes: int = 0
|
164
|
+
gp2_volumes: int = 0
|
165
|
+
gp3_eligible_volumes: int = 0
|
166
|
+
low_usage_volumes: int = 0
|
167
|
+
orphaned_volumes: int = 0
|
168
|
+
analyzed_regions: List[str] = Field(default_factory=list)
|
169
|
+
optimization_results: List[EBSOptimizationResult] = Field(default_factory=list)
|
170
|
+
|
171
|
+
# Cost breakdown
|
172
|
+
total_monthly_cost: float = 0.0
|
173
|
+
total_annual_cost: float = 0.0
|
174
|
+
gp3_potential_monthly_savings: float = 0.0
|
175
|
+
gp3_potential_annual_savings: float = 0.0
|
176
|
+
low_usage_potential_monthly_savings: float = 0.0
|
177
|
+
low_usage_potential_annual_savings: float = 0.0
|
178
|
+
orphaned_potential_monthly_savings: float = 0.0
|
179
|
+
orphaned_potential_annual_savings: float = 0.0
|
180
|
+
total_potential_monthly_savings: float = 0.0
|
181
|
+
total_potential_annual_savings: float = 0.0
|
182
|
+
|
183
|
+
execution_time_seconds: float = 0.0
|
184
|
+
mcp_validation_accuracy: float = 0.0
|
185
|
+
analysis_timestamp: datetime = Field(default_factory=datetime.now)
|
94
186
|
|
95
187
|
|
96
188
|
class EBSCostOptimizer:
|
97
189
|
"""
|
98
|
-
|
99
|
-
|
100
|
-
Consolidates 5+ legacy
|
101
|
-
|
190
|
+
EBS Volume Cost Optimization Platform - Enterprise FinOps Storage Engine
|
191
|
+
|
192
|
+
Consolidates ebs_optimizer.py + 5+ legacy notebooks into unified optimization engine
|
193
|
+
following $132,720+ methodology with proven FinOps patterns targeting $1.5M-$9.3M annual savings:
|
194
|
+
- Multi-region discovery and analysis across enterprise accounts
|
195
|
+
- GP2→GP3 conversion analysis for 15-20% cost reduction
|
196
|
+
- CloudWatch metrics integration for usage validation
|
197
|
+
- Orphaned volume detection and cleanup analysis
|
198
|
+
- Combined cost calculation with MCP validation (≥99.5% accuracy)
|
199
|
+
- Evidence generation for Manager/Financial/CTO executive reporting
|
200
|
+
- Business-focused naming for executive presentation readiness
|
102
201
|
"""
|
103
|
-
|
104
|
-
def __init__(
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
202
|
+
|
203
|
+
def __init__(self, profile_name: Optional[str] = None, regions: Optional[List[str]] = None):
|
204
|
+
"""Initialize EBS optimizer with enterprise profile support."""
|
205
|
+
self.profile_name = profile_name
|
206
|
+
self.regions = regions or ["us-east-1", "us-west-2", "eu-west-1"]
|
207
|
+
|
208
|
+
# Initialize AWS session with profile priority system
|
209
|
+
self.session = boto3.Session(profile_name=get_profile_for_operation("operational", profile_name))
|
210
|
+
|
211
|
+
# EBS pricing using dynamic AWS pricing engine for universal compatibility
|
212
|
+
self.ebs_pricing = self._initialize_dynamic_ebs_pricing()
|
213
|
+
|
214
|
+
# GP3 conversion savings percentage
|
215
|
+
self.gp3_savings_percentage = 0.20 # 20% savings GP2→GP3
|
216
|
+
|
217
|
+
# Low usage thresholds for CloudWatch analysis
|
218
|
+
self.low_usage_threshold_ops = 10 # Read/Write operations per day
|
219
|
+
self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
|
220
|
+
self.analysis_period_days = 7
|
221
|
+
|
222
|
+
def _initialize_dynamic_ebs_pricing(self) -> Dict[str, float]:
|
223
|
+
"""Initialize dynamic EBS pricing using AWS pricing engine for universal compatibility."""
|
224
|
+
try:
|
225
|
+
from ..common.aws_pricing import get_service_monthly_cost
|
226
|
+
|
227
|
+
# Get dynamic pricing for common EBS volume types in us-east-1 (base region)
|
228
|
+
base_region = "us-east-1"
|
229
|
+
|
230
|
+
return {
|
231
|
+
"gp2": get_service_monthly_cost("ebs_gp2", base_region, self.profile_name),
|
232
|
+
"gp3": get_service_monthly_cost("ebs_gp3", base_region, self.profile_name),
|
233
|
+
"io1": get_service_monthly_cost("ebs_io1", base_region, self.profile_name),
|
234
|
+
"io2": get_service_monthly_cost("ebs_io2", base_region, self.profile_name),
|
235
|
+
"st1": get_service_monthly_cost("ebs_st1", base_region, self.profile_name),
|
236
|
+
"sc1": get_service_monthly_cost("ebs_sc1", base_region, self.profile_name),
|
237
|
+
}
|
238
|
+
except Exception as e:
|
239
|
+
print_warning(f"Dynamic EBS pricing initialization failed: {e}")
|
240
|
+
print_warning("Attempting AWS Pricing API fallback with universal profile support")
|
241
|
+
|
242
|
+
try:
|
243
|
+
from ..common.aws_pricing import get_aws_pricing_engine
|
244
|
+
|
245
|
+
# Use AWS Pricing API with profile support for universal compatibility
|
246
|
+
pricing_engine = get_aws_pricing_engine(profile=self.profile_name, enable_fallback=True)
|
247
|
+
|
248
|
+
# Get actual AWS pricing instead of hardcoded values
|
249
|
+
gp2_pricing = pricing_engine.get_ebs_pricing("gp2", "us-east-1")
|
250
|
+
gp3_pricing = pricing_engine.get_ebs_pricing("gp3", "us-east-1")
|
251
|
+
io1_pricing = pricing_engine.get_ebs_pricing("io1", "us-east-1")
|
252
|
+
io2_pricing = pricing_engine.get_ebs_pricing("io2", "us-east-1")
|
253
|
+
st1_pricing = pricing_engine.get_ebs_pricing("st1", "us-east-1")
|
254
|
+
sc1_pricing = pricing_engine.get_ebs_pricing("sc1", "us-east-1")
|
255
|
+
|
256
|
+
return {
|
257
|
+
"gp2": gp2_pricing.monthly_cost_per_gb,
|
258
|
+
"gp3": gp3_pricing.monthly_cost_per_gb,
|
259
|
+
"io1": io1_pricing.monthly_cost_per_gb,
|
260
|
+
"io2": io2_pricing.monthly_cost_per_gb,
|
261
|
+
"st1": st1_pricing.monthly_cost_per_gb,
|
262
|
+
"sc1": sc1_pricing.monthly_cost_per_gb,
|
263
|
+
}
|
264
|
+
|
265
|
+
except Exception as pricing_error:
|
266
|
+
print_error(
|
267
|
+
f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine EBS pricing without AWS API access: {pricing_error}"
|
268
|
+
)
|
269
|
+
print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
|
270
|
+
|
271
|
+
# Return error state instead of hardcoded values to maintain enterprise compliance
|
272
|
+
raise RuntimeError(
|
273
|
+
"Universal compatibility mode requires dynamic AWS pricing API access. "
|
274
|
+
"Please ensure your AWS profile has pricing:GetProducts permissions or configure "
|
275
|
+
"appropriate billing/management profile access."
|
276
|
+
)
|
277
|
+
|
278
|
+
async def analyze_ebs_volumes(self, dry_run: bool = True) -> EBSOptimizerResults:
|
110
279
|
"""
|
111
|
-
|
112
|
-
|
280
|
+
Comprehensive EBS volume cost optimization analysis.
|
281
|
+
|
113
282
|
Args:
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
self.aws_profile = aws_profile
|
119
|
-
self.enterprise_config = enterprise_config
|
120
|
-
self.mcp_validator = mcp_validator or create_enterprise_validator()
|
121
|
-
|
122
|
-
# Enterprise wrapper integration
|
123
|
-
if enterprise_config:
|
124
|
-
self.enterprise_wrapper = create_enterprise_wrapper("cost_optimization", enterprise_config)
|
125
|
-
else:
|
126
|
-
self.enterprise_wrapper = None
|
127
|
-
|
128
|
-
# Cost calculation constants (current AWS pricing)
|
129
|
-
self.pricing = {
|
130
|
-
"gp2_per_gb_month": 0.10,
|
131
|
-
"gp3_per_gb_month": 0.08, # 20% cost reduction
|
132
|
-
"gp3_baseline_iops": 3000,
|
133
|
-
"gp3_baseline_throughput": 125,
|
134
|
-
"snapshot_storage_per_gb_month": 0.05
|
135
|
-
}
|
136
|
-
|
137
|
-
# Optimization thresholds
|
138
|
-
self.thresholds = {
|
139
|
-
"low_usage_iops_threshold": 100, # IOPS per month average
|
140
|
-
"unattached_days_threshold": 7, # Days unattached before cleanup candidate
|
141
|
-
"stopped_instance_days_threshold": 30, # Days instance stopped
|
142
|
-
"old_snapshot_days_threshold": 90 # Days for old snapshot cleanup
|
143
|
-
}
|
144
|
-
|
145
|
-
def analyze_comprehensive_ebs_optimization(
|
146
|
-
self,
|
147
|
-
regions: Optional[List[str]] = None,
|
148
|
-
include_snapshots: bool = True,
|
149
|
-
dry_run: bool = True
|
150
|
-
) -> EBSOptimizationResult:
|
151
|
-
"""
|
152
|
-
Perform comprehensive EBS optimization analysis across all optimization types.
|
153
|
-
|
154
|
-
Strategic Focus: Complete EBS portfolio analysis with quantified business impact
|
155
|
-
for enterprise financial decision making.
|
283
|
+
dry_run: Safety mode - READ-ONLY analysis only
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
Complete analysis results with optimization recommendations
|
156
287
|
"""
|
157
|
-
print_header("EBS Volume Cost Optimization
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
"
|
220
|
-
|
221
|
-
|
288
|
+
print_header("EBS Volume Cost Optimization Platform", "Enterprise FinOps Storage Analysis v1.0")
|
289
|
+
|
290
|
+
if not dry_run:
|
291
|
+
print_warning("⚠️ Dry-run disabled - This optimizer is READ-ONLY analysis only")
|
292
|
+
print_info("All EBS operations require manual execution after review")
|
293
|
+
|
294
|
+
analysis_start_time = time.time()
|
295
|
+
|
296
|
+
try:
|
297
|
+
with create_progress_bar() as progress:
|
298
|
+
# Step 1: Multi-region EBS volume discovery
|
299
|
+
discovery_task = progress.add_task("Discovering EBS volumes...", total=len(self.regions))
|
300
|
+
volumes = await self._discover_ebs_volumes_multi_region(progress, discovery_task)
|
301
|
+
|
302
|
+
if not volumes:
|
303
|
+
print_warning("No EBS volumes found in specified regions")
|
304
|
+
return EBSOptimizerResults(
|
305
|
+
analyzed_regions=self.regions,
|
306
|
+
analysis_timestamp=datetime.now(),
|
307
|
+
execution_time_seconds=time.time() - analysis_start_time,
|
308
|
+
)
|
309
|
+
|
310
|
+
# Step 2: Usage metrics analysis via CloudWatch
|
311
|
+
metrics_task = progress.add_task("Analyzing usage metrics...", total=len(volumes))
|
312
|
+
usage_metrics = await self._analyze_usage_metrics(volumes, progress, metrics_task)
|
313
|
+
|
314
|
+
# Step 3: Instance attachment validation
|
315
|
+
attachment_task = progress.add_task("Validating instance attachments...", total=len(volumes))
|
316
|
+
validated_volumes = await self._validate_instance_attachments(volumes, progress, attachment_task)
|
317
|
+
|
318
|
+
# Step 4: Comprehensive optimization analysis
|
319
|
+
optimization_task = progress.add_task("Calculating optimization potential...", total=len(volumes))
|
320
|
+
optimization_results = await self._calculate_optimization_recommendations(
|
321
|
+
validated_volumes, usage_metrics, progress, optimization_task
|
322
|
+
)
|
323
|
+
|
324
|
+
# Step 5: MCP validation
|
325
|
+
validation_task = progress.add_task("MCP validation...", total=1)
|
326
|
+
mcp_accuracy = await self._validate_with_mcp(optimization_results, progress, validation_task)
|
327
|
+
|
328
|
+
# Compile comprehensive results with cost breakdowns
|
329
|
+
results = self._compile_results(volumes, optimization_results, mcp_accuracy, analysis_start_time)
|
330
|
+
|
331
|
+
# Display executive summary
|
332
|
+
self._display_executive_summary(results)
|
333
|
+
|
334
|
+
return results
|
335
|
+
|
336
|
+
except Exception as e:
|
337
|
+
print_error(f"EBS optimization analysis failed: {e}")
|
338
|
+
logger.error(f"EBS analysis error: {e}", exc_info=True)
|
339
|
+
raise
|
340
|
+
|
341
|
+
async def _discover_ebs_volumes_multi_region(self, progress, task_id) -> List[EBSVolumeDetails]:
|
342
|
+
"""Discover EBS volumes across multiple regions."""
|
343
|
+
volumes = []
|
344
|
+
|
345
|
+
for region in self.regions:
|
346
|
+
try:
|
347
|
+
ec2_client = self.session.client("ec2", region_name=region)
|
348
|
+
|
349
|
+
# Get all EBS volumes in region
|
350
|
+
paginator = ec2_client.get_paginator("describe_volumes")
|
351
|
+
page_iterator = paginator.paginate()
|
352
|
+
|
353
|
+
for page in page_iterator:
|
354
|
+
for volume in page.get("Volumes", []):
|
355
|
+
# Extract tags
|
356
|
+
tags = {tag["Key"]: tag["Value"] for tag in volume.get("Tags", [])}
|
357
|
+
|
358
|
+
# Get attachment details
|
359
|
+
attachments = volume.get("Attachments", [])
|
360
|
+
attached_instance_id = None
|
361
|
+
attachment_state = None
|
362
|
+
device = None
|
363
|
+
|
364
|
+
if attachments:
|
365
|
+
attachment = attachments[0] # Take first attachment
|
366
|
+
attached_instance_id = attachment.get("InstanceId")
|
367
|
+
attachment_state = attachment.get("State")
|
368
|
+
device = attachment.get("Device")
|
369
|
+
|
370
|
+
volumes.append(
|
371
|
+
EBSVolumeDetails(
|
372
|
+
volume_id=volume["VolumeId"],
|
373
|
+
region=region,
|
374
|
+
size=volume["Size"],
|
375
|
+
volume_type=volume["VolumeType"],
|
376
|
+
state=volume["State"],
|
377
|
+
availability_zone=volume["AvailabilityZone"],
|
378
|
+
create_time=volume["CreateTime"],
|
379
|
+
attached_instance_id=attached_instance_id,
|
380
|
+
attachment_state=attachment_state,
|
381
|
+
device=device,
|
382
|
+
encrypted=volume.get("Encrypted", False),
|
383
|
+
iops=volume.get("Iops"),
|
384
|
+
throughput=volume.get("Throughput"),
|
385
|
+
tags=tags,
|
386
|
+
snapshot_id=volume.get("SnapshotId"),
|
387
|
+
)
|
388
|
+
)
|
389
|
+
|
390
|
+
print_info(f"Region {region}: {len([v for v in volumes if v.region == region])} EBS volumes discovered")
|
391
|
+
|
392
|
+
except ClientError as e:
|
393
|
+
print_warning(f"Region {region}: Access denied or region unavailable - {e.response['Error']['Code']}")
|
394
|
+
except Exception as e:
|
395
|
+
print_error(f"Region {region}: Discovery error - {str(e)}")
|
396
|
+
|
397
|
+
progress.advance(task_id)
|
398
|
+
|
399
|
+
return volumes
|
400
|
+
|
401
|
+
async def _analyze_usage_metrics(
|
402
|
+
self, volumes: List[EBSVolumeDetails], progress, task_id
|
403
|
+
) -> Dict[str, EBSUsageMetrics]:
|
404
|
+
"""Analyze EBS volume usage metrics via CloudWatch."""
|
405
|
+
usage_metrics = {}
|
406
|
+
end_time = datetime.utcnow()
|
407
|
+
start_time = end_time - timedelta(days=self.analysis_period_days)
|
408
|
+
|
409
|
+
for volume in volumes:
|
410
|
+
try:
|
411
|
+
cloudwatch = self.session.client("cloudwatch", region_name=volume.region)
|
412
|
+
|
413
|
+
# Get volume usage metrics
|
414
|
+
read_ops = await self._get_cloudwatch_metric(
|
415
|
+
cloudwatch, volume.volume_id, "VolumeReadOps", start_time, end_time
|
416
|
+
)
|
417
|
+
|
418
|
+
write_ops = await self._get_cloudwatch_metric(
|
419
|
+
cloudwatch, volume.volume_id, "VolumeWriteOps", start_time, end_time
|
420
|
+
)
|
421
|
+
|
422
|
+
read_bytes = await self._get_cloudwatch_metric(
|
423
|
+
cloudwatch, volume.volume_id, "VolumeReadBytes", start_time, end_time
|
424
|
+
)
|
425
|
+
|
426
|
+
write_bytes = await self._get_cloudwatch_metric(
|
427
|
+
cloudwatch, volume.volume_id, "VolumeWriteBytes", start_time, end_time
|
428
|
+
)
|
429
|
+
|
430
|
+
total_read_time = await self._get_cloudwatch_metric(
|
431
|
+
cloudwatch, volume.volume_id, "VolumeTotalReadTime", start_time, end_time
|
432
|
+
)
|
433
|
+
|
434
|
+
total_write_time = await self._get_cloudwatch_metric(
|
435
|
+
cloudwatch, volume.volume_id, "VolumeTotalWriteTime", start_time, end_time
|
436
|
+
)
|
437
|
+
|
438
|
+
# Calculate usage score and low usage detection
|
439
|
+
total_ops = read_ops + write_ops
|
440
|
+
total_bytes = read_bytes + write_bytes
|
441
|
+
|
442
|
+
# Usage score calculation (0-100)
|
443
|
+
usage_score = min(100, (total_ops / (self.low_usage_threshold_ops * self.analysis_period_days)) * 100)
|
444
|
+
|
445
|
+
# Low usage detection
|
446
|
+
is_low_usage = total_ops < (
|
447
|
+
self.low_usage_threshold_ops * self.analysis_period_days
|
448
|
+
) and total_bytes < (self.low_usage_threshold_bytes * self.analysis_period_days)
|
449
|
+
|
450
|
+
usage_metrics[volume.volume_id] = EBSUsageMetrics(
|
451
|
+
volume_id=volume.volume_id,
|
452
|
+
region=volume.region,
|
453
|
+
read_ops=read_ops,
|
454
|
+
write_ops=write_ops,
|
455
|
+
read_bytes=read_bytes,
|
456
|
+
write_bytes=write_bytes,
|
457
|
+
total_read_time=total_read_time,
|
458
|
+
total_write_time=total_write_time,
|
459
|
+
analysis_period_days=self.analysis_period_days,
|
460
|
+
is_low_usage=is_low_usage,
|
461
|
+
usage_score=usage_score,
|
462
|
+
)
|
463
|
+
|
464
|
+
except Exception as e:
|
465
|
+
print_warning(f"Metrics unavailable for {volume.volume_id}: {str(e)}")
|
466
|
+
# Create default metrics for volumes without CloudWatch access
|
467
|
+
usage_metrics[volume.volume_id] = EBSUsageMetrics(
|
468
|
+
volume_id=volume.volume_id,
|
469
|
+
region=volume.region,
|
470
|
+
analysis_period_days=self.analysis_period_days,
|
471
|
+
is_low_usage=False, # Conservative assumption without metrics
|
472
|
+
usage_score=50.0, # Neutral score
|
473
|
+
)
|
474
|
+
|
475
|
+
progress.advance(task_id)
|
476
|
+
|
477
|
+
return usage_metrics
|
478
|
+
|
479
|
+
async def _get_cloudwatch_metric(
|
480
|
+
self, cloudwatch, volume_id: str, metric_name: str, start_time: datetime, end_time: datetime
|
481
|
+
) -> float:
|
482
|
+
"""Get CloudWatch metric data for EBS volume."""
|
483
|
+
try:
|
484
|
+
response = cloudwatch.get_metric_statistics(
|
485
|
+
Namespace="AWS/EBS",
|
486
|
+
MetricName=metric_name,
|
487
|
+
Dimensions=[{"Name": "VolumeId", "Value": volume_id}],
|
488
|
+
StartTime=start_time,
|
489
|
+
EndTime=end_time,
|
490
|
+
Period=86400, # Daily data points
|
491
|
+
Statistics=["Sum"],
|
492
|
+
)
|
493
|
+
|
494
|
+
# Sum all data points over the analysis period
|
495
|
+
total = sum(datapoint["Sum"] for datapoint in response.get("Datapoints", []))
|
496
|
+
return total
|
497
|
+
|
498
|
+
except Exception as e:
|
499
|
+
logger.warning(f"CloudWatch metric {metric_name} unavailable for {volume_id}: {e}")
|
500
|
+
return 0.0
|
501
|
+
|
502
|
+
async def _validate_instance_attachments(
|
503
|
+
self, volumes: List[EBSVolumeDetails], progress, task_id
|
504
|
+
) -> List[EBSVolumeDetails]:
|
505
|
+
"""Validate EBS volume attachments and instance states."""
|
506
|
+
validated_volumes = []
|
507
|
+
|
508
|
+
for volume in volumes:
|
509
|
+
try:
|
510
|
+
# For attached volumes, verify instance exists and get its state
|
511
|
+
if volume.attached_instance_id:
|
512
|
+
ec2_client = self.session.client("ec2", region_name=volume.region)
|
513
|
+
|
514
|
+
try:
|
515
|
+
response = ec2_client.describe_instances(InstanceIds=[volume.attached_instance_id])
|
516
|
+
|
517
|
+
if response.get("Reservations"):
|
518
|
+
instance = response["Reservations"][0]["Instances"][0]
|
519
|
+
instance_state = instance["State"]["Name"]
|
520
|
+
|
521
|
+
# Update volume with instance state information
|
522
|
+
volume_copy = volume.copy()
|
523
|
+
# Add instance_state as a field that can be accessed later
|
524
|
+
volume_copy.__dict__["instance_state"] = instance_state
|
525
|
+
validated_volumes.append(volume_copy)
|
526
|
+
else:
|
527
|
+
# Instance not found - volume is effectively orphaned
|
528
|
+
volume_copy = volume.copy()
|
529
|
+
volume_copy.__dict__["instance_state"] = "terminated"
|
530
|
+
validated_volumes.append(volume_copy)
|
531
|
+
|
532
|
+
except ClientError:
|
533
|
+
# Instance not found or not accessible - consider orphaned
|
534
|
+
volume_copy = volume.copy()
|
535
|
+
volume_copy.__dict__["instance_state"] = "not_found"
|
536
|
+
validated_volumes.append(volume_copy)
|
537
|
+
else:
|
538
|
+
# Unattached volume - keep as is
|
539
|
+
validated_volumes.append(volume)
|
540
|
+
|
541
|
+
except Exception as e:
|
542
|
+
print_warning(f"Attachment validation failed for {volume.volume_id}: {str(e)}")
|
543
|
+
validated_volumes.append(volume) # Add with original data
|
544
|
+
|
545
|
+
progress.advance(task_id)
|
546
|
+
|
547
|
+
return validated_volumes
|
548
|
+
|
549
|
+
async def _calculate_optimization_recommendations(
|
550
|
+
self, volumes: List[EBSVolumeDetails], usage_metrics: Dict[str, EBSUsageMetrics], progress, task_id
|
551
|
+
) -> List[EBSOptimizationResult]:
|
552
|
+
"""Calculate comprehensive optimization recommendations and potential savings."""
|
553
|
+
optimization_results = []
|
554
|
+
|
555
|
+
for volume in volumes:
|
556
|
+
try:
|
557
|
+
metrics = usage_metrics.get(volume.volume_id)
|
558
|
+
instance_state = getattr(volume, "instance_state", None)
|
559
|
+
|
560
|
+
# Calculate current monthly cost using dynamic pricing (enterprise compliance)
|
561
|
+
volume_pricing = self.ebs_pricing.get(volume.volume_type)
|
562
|
+
if volume_pricing is None:
|
563
|
+
# Dynamic fallback for unknown volume types - no hardcoded values
|
564
|
+
try:
|
565
|
+
from ..common.aws_pricing import get_aws_pricing_engine
|
566
|
+
|
567
|
+
pricing_engine = get_aws_pricing_engine(profile=self.profile_name, enable_fallback=True)
|
568
|
+
volume_pricing_result = pricing_engine.get_ebs_pricing(volume.volume_type, "us-east-1")
|
569
|
+
volume_pricing = volume_pricing_result.monthly_cost_per_gb
|
570
|
+
print_info(f"Dynamic pricing resolved for {volume.volume_type}: ${volume_pricing:.4f}/GB/month")
|
571
|
+
except Exception as e:
|
572
|
+
print_error(
|
573
|
+
f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine pricing for {volume.volume_type}: {e}"
|
574
|
+
)
|
575
|
+
print_warning(
|
576
|
+
"Universal compatibility requires dynamic pricing - hardcoded values not permitted"
|
577
|
+
)
|
578
|
+
raise RuntimeError(
|
579
|
+
f"Universal compatibility mode requires dynamic AWS pricing for volume type '{volume.volume_type}'. "
|
580
|
+
f"Please ensure your AWS profile has pricing:GetProducts permissions."
|
581
|
+
)
|
582
|
+
|
583
|
+
monthly_cost = volume.size * volume_pricing
|
584
|
+
annual_cost = monthly_cost * 12
|
585
|
+
|
586
|
+
# Initialize optimization analysis
|
587
|
+
gp3_conversion_eligible = False
|
588
|
+
gp3_monthly_savings = 0.0
|
589
|
+
low_usage_detected = False
|
590
|
+
low_usage_monthly_cost = 0.0
|
591
|
+
is_orphaned = False
|
592
|
+
orphaned_monthly_cost = 0.0
|
593
|
+
|
594
|
+
recommendation = "retain" # Default
|
595
|
+
risk_level = "low"
|
596
|
+
business_impact = "minimal"
|
597
|
+
|
598
|
+
# 1. GP2→GP3 conversion analysis
|
599
|
+
if volume.volume_type == "gp2":
|
600
|
+
gp3_conversion_eligible = True
|
601
|
+
gp3_monthly_savings = monthly_cost * self.gp3_savings_percentage
|
602
|
+
|
603
|
+
if not metrics or not metrics.is_low_usage:
|
604
|
+
recommendation = "gp3_convert"
|
605
|
+
business_impact = "cost_savings"
|
606
|
+
|
607
|
+
# 2. Low usage detection
|
608
|
+
if metrics and metrics.is_low_usage:
|
609
|
+
low_usage_detected = True
|
610
|
+
low_usage_monthly_cost = monthly_cost
|
611
|
+
|
612
|
+
if volume.state == "available" or (instance_state in ["stopped", "terminated"]):
|
613
|
+
recommendation = "investigate_usage"
|
614
|
+
risk_level = "medium"
|
615
|
+
business_impact = "potential_cleanup"
|
616
|
+
|
617
|
+
# 3. Orphaned volume detection
|
618
|
+
if volume.state == "available" or (
|
619
|
+
volume.attached_instance_id and instance_state in ["stopped", "terminated", "not_found"]
|
620
|
+
):
|
621
|
+
is_orphaned = True
|
622
|
+
orphaned_monthly_cost = monthly_cost
|
623
|
+
|
624
|
+
if instance_state in ["terminated", "not_found"]:
|
625
|
+
recommendation = "cleanup_orphaned"
|
626
|
+
risk_level = "low"
|
627
|
+
business_impact = "safe_cleanup"
|
628
|
+
elif instance_state == "stopped":
|
629
|
+
recommendation = "investigate_usage"
|
630
|
+
risk_level = "medium"
|
631
|
+
business_impact = "potential_cleanup"
|
632
|
+
|
633
|
+
# Calculate total potential savings (non-overlapping)
|
634
|
+
total_monthly_savings = 0.0
|
635
|
+
|
636
|
+
if recommendation == "cleanup_orphaned":
|
637
|
+
total_monthly_savings = orphaned_monthly_cost
|
638
|
+
elif recommendation == "investigate_usage":
|
639
|
+
total_monthly_savings = low_usage_monthly_cost * 0.7 # Conservative estimate
|
640
|
+
elif recommendation == "gp3_convert":
|
641
|
+
total_monthly_savings = gp3_monthly_savings
|
642
|
+
|
643
|
+
optimization_results.append(
|
644
|
+
EBSOptimizationResult(
|
645
|
+
volume_id=volume.volume_id,
|
646
|
+
region=volume.region,
|
647
|
+
availability_zone=volume.availability_zone,
|
648
|
+
current_type=volume.volume_type,
|
649
|
+
current_size=volume.size,
|
650
|
+
current_state=volume.state,
|
651
|
+
attached_instance_id=volume.attached_instance_id,
|
652
|
+
instance_state=instance_state,
|
653
|
+
usage_metrics=metrics,
|
654
|
+
gp3_conversion_eligible=gp3_conversion_eligible,
|
655
|
+
gp3_monthly_savings=gp3_monthly_savings,
|
656
|
+
gp3_annual_savings=gp3_monthly_savings * 12,
|
657
|
+
low_usage_detected=low_usage_detected,
|
658
|
+
low_usage_monthly_cost=low_usage_monthly_cost,
|
659
|
+
low_usage_annual_cost=low_usage_monthly_cost * 12,
|
660
|
+
is_orphaned=is_orphaned,
|
661
|
+
orphaned_monthly_cost=orphaned_monthly_cost,
|
662
|
+
orphaned_annual_cost=orphaned_monthly_cost * 12,
|
663
|
+
optimization_recommendation=recommendation,
|
664
|
+
risk_level=risk_level,
|
665
|
+
business_impact=business_impact,
|
666
|
+
total_monthly_savings=total_monthly_savings,
|
667
|
+
total_annual_savings=total_monthly_savings * 12,
|
668
|
+
monthly_cost=monthly_cost,
|
669
|
+
annual_cost=annual_cost,
|
670
|
+
)
|
671
|
+
)
|
672
|
+
|
673
|
+
except Exception as e:
|
674
|
+
print_error(f"Optimization calculation failed for {volume.volume_id}: {str(e)}")
|
675
|
+
|
676
|
+
progress.advance(task_id)
|
677
|
+
|
678
|
+
return optimization_results
|
679
|
+
|
680
|
+
async def _validate_with_mcp(self, optimization_results: List[EBSOptimizationResult], progress, task_id) -> float:
|
681
|
+
"""Validate optimization results with embedded MCP validator."""
|
682
|
+
try:
|
683
|
+
# Prepare validation data in FinOps format
|
684
|
+
validation_data = {
|
685
|
+
"total_annual_cost": sum(result.annual_cost for result in optimization_results),
|
686
|
+
"potential_annual_savings": sum(result.total_annual_savings for result in optimization_results),
|
687
|
+
"volumes_analyzed": len(optimization_results),
|
688
|
+
"regions_analyzed": list(set(result.region for result in optimization_results)),
|
689
|
+
"analysis_timestamp": datetime.now().isoformat(),
|
222
690
|
}
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
691
|
+
|
692
|
+
# Initialize MCP validator if profile is available
|
693
|
+
if self.profile_name:
|
694
|
+
mcp_validator = EmbeddedMCPValidator([self.profile_name])
|
695
|
+
validation_results = await mcp_validator.validate_cost_data_async(validation_data)
|
696
|
+
accuracy = validation_results.get("total_accuracy", 0.0)
|
697
|
+
|
698
|
+
if accuracy >= 99.5:
|
699
|
+
print_success(f"MCP Validation: {accuracy:.1f}% accuracy achieved (target: ≥99.5%)")
|
700
|
+
else:
|
701
|
+
print_warning(f"MCP Validation: {accuracy:.1f}% accuracy (target: ≥99.5%)")
|
702
|
+
|
703
|
+
progress.advance(task_id)
|
704
|
+
return accuracy
|
705
|
+
else:
|
706
|
+
print_info("MCP validation skipped - no profile specified")
|
707
|
+
progress.advance(task_id)
|
708
|
+
return 0.0
|
709
|
+
|
710
|
+
except Exception as e:
|
711
|
+
print_warning(f"MCP validation failed: {str(e)}")
|
712
|
+
progress.advance(task_id)
|
713
|
+
return 0.0
|
714
|
+
|
715
|
+
def _compile_results(
|
246
716
|
self,
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
for
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
executive_summary = f"""
|
292
|
-
EBS GP2 to GP3 Conversion Analysis Summary:
|
293
|
-
|
294
|
-
💰 **Financial Impact**: ${annual_savings:,.0f} annual savings ({business_impact['cost_reduction_percentage']:.1f}% reduction)
|
295
|
-
📊 **Volume Analysis**: {len(gp2_volumes)} GP2 volumes analyzed, {business_impact['conversion_candidates']} conversion candidates
|
296
|
-
⚡ **Performance Benefit**: GP3 provides 20% cost savings with enhanced baseline performance
|
297
|
-
🛡️ **Risk Assessment**: Low risk - AWS-supported conversion with rollback capability
|
298
|
-
"""
|
299
|
-
|
300
|
-
print_success(f"GP2→GP3 Analysis: ${annual_savings:,.0f} annual savings potential")
|
301
|
-
|
302
|
-
return EBSOptimizationResult(
|
303
|
-
optimization_type=EBSOptimizationType.GP2_TO_GP3_CONVERSION,
|
304
|
-
total_volumes_analyzed=len(gp2_volumes),
|
305
|
-
optimization_candidates=business_impact['conversion_candidates'],
|
306
|
-
estimated_annual_savings=annual_savings,
|
307
|
-
implementation_complexity="Low - AWS native conversion tools available",
|
308
|
-
business_impact=business_impact,
|
309
|
-
technical_recommendations=[
|
310
|
-
"Prioritize high-volume GP2 instances for maximum savings impact",
|
311
|
-
"Schedule conversions during maintenance windows",
|
312
|
-
"Monitor performance metrics post-conversion for 30 days",
|
313
|
-
"Implement automated GP3 selection for new volume creation"
|
314
|
-
],
|
315
|
-
executive_summary=executive_summary,
|
316
|
-
detailed_analysis=gp2_volumes,
|
317
|
-
validation_metrics={}
|
717
|
+
volumes: List[EBSVolumeDetails],
|
718
|
+
optimization_results: List[EBSOptimizationResult],
|
719
|
+
mcp_accuracy: float,
|
720
|
+
analysis_start_time: float,
|
721
|
+
) -> EBSOptimizerResults:
|
722
|
+
"""Compile comprehensive EBS optimization results."""
|
723
|
+
|
724
|
+
# Count volumes by type and optimization opportunity
|
725
|
+
gp2_volumes = len([v for v in volumes if v.volume_type == "gp2"])
|
726
|
+
gp3_eligible_volumes = len([r for r in optimization_results if r.gp3_conversion_eligible])
|
727
|
+
low_usage_volumes = len([r for r in optimization_results if r.low_usage_detected])
|
728
|
+
orphaned_volumes = len([r for r in optimization_results if r.is_orphaned])
|
729
|
+
|
730
|
+
# Calculate cost breakdowns
|
731
|
+
total_monthly_cost = sum(result.monthly_cost for result in optimization_results)
|
732
|
+
total_annual_cost = total_monthly_cost * 12
|
733
|
+
|
734
|
+
gp3_potential_monthly_savings = sum(result.gp3_monthly_savings for result in optimization_results)
|
735
|
+
low_usage_potential_monthly_savings = sum(result.low_usage_monthly_cost for result in optimization_results)
|
736
|
+
orphaned_potential_monthly_savings = sum(result.orphaned_monthly_cost for result in optimization_results)
|
737
|
+
total_potential_monthly_savings = sum(result.total_monthly_savings for result in optimization_results)
|
738
|
+
|
739
|
+
return EBSOptimizerResults(
|
740
|
+
total_volumes=len(volumes),
|
741
|
+
gp2_volumes=gp2_volumes,
|
742
|
+
gp3_eligible_volumes=gp3_eligible_volumes,
|
743
|
+
low_usage_volumes=low_usage_volumes,
|
744
|
+
orphaned_volumes=orphaned_volumes,
|
745
|
+
analyzed_regions=self.regions,
|
746
|
+
optimization_results=optimization_results,
|
747
|
+
total_monthly_cost=total_monthly_cost,
|
748
|
+
total_annual_cost=total_annual_cost,
|
749
|
+
gp3_potential_monthly_savings=gp3_potential_monthly_savings,
|
750
|
+
gp3_potential_annual_savings=gp3_potential_monthly_savings * 12,
|
751
|
+
low_usage_potential_monthly_savings=low_usage_potential_monthly_savings,
|
752
|
+
low_usage_potential_annual_savings=low_usage_potential_monthly_savings * 12,
|
753
|
+
orphaned_potential_monthly_savings=orphaned_potential_monthly_savings,
|
754
|
+
orphaned_potential_annual_savings=orphaned_potential_monthly_savings * 12,
|
755
|
+
total_potential_monthly_savings=total_potential_monthly_savings,
|
756
|
+
total_potential_annual_savings=total_potential_monthly_savings * 12,
|
757
|
+
execution_time_seconds=time.time() - analysis_start_time,
|
758
|
+
mcp_validation_accuracy=mcp_accuracy,
|
759
|
+
analysis_timestamp=datetime.now(),
|
318
760
|
)
|
319
|
-
|
320
|
-
def
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
761
|
+
|
762
|
+
def _display_executive_summary(self, results: EBSOptimizerResults) -> None:
|
763
|
+
"""Display executive summary with Rich CLI formatting."""
|
764
|
+
|
765
|
+
# Executive Summary Panel
|
766
|
+
summary_content = f"""
|
767
|
+
💰 Total Annual Cost: {format_cost(results.total_annual_cost)}
|
768
|
+
📊 Potential Savings: {format_cost(results.total_potential_annual_savings)}
|
769
|
+
🎯 EBS Volumes Analyzed: {results.total_volumes}
|
770
|
+
💾 GP2 Volumes: {results.gp2_volumes} ({results.gp3_eligible_volumes} GP3 eligible)
|
771
|
+
📉 Low Usage: {results.low_usage_volumes} volumes
|
772
|
+
🔓 Orphaned: {results.orphaned_volumes} volumes
|
773
|
+
🌍 Regions: {", ".join(results.analyzed_regions)}
|
774
|
+
⚡ Analysis Time: {results.execution_time_seconds:.2f}s
|
775
|
+
✅ MCP Accuracy: {results.mcp_validation_accuracy:.1f}%
|
326
776
|
"""
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
777
|
+
|
778
|
+
console.print(
|
779
|
+
create_panel(summary_content.strip(), title="🏆 EBS Volume Optimization Summary", border_style="green")
|
780
|
+
)
|
781
|
+
|
782
|
+
# Optimization Breakdown Panel
|
783
|
+
breakdown_content = f"""
|
784
|
+
🔄 GP2→GP3 Conversion: {format_cost(results.gp3_potential_annual_savings)} potential savings
|
785
|
+
📉 Low Usage Cleanup: {format_cost(results.low_usage_potential_annual_savings)} potential savings
|
786
|
+
🧹 Orphaned Cleanup: {format_cost(results.orphaned_potential_annual_savings)} potential savings
|
787
|
+
📈 Total Optimization: {format_cost(results.total_potential_annual_savings)} annual savings potential
|
331
788
|
"""
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
unattached_volumes = []
|
337
|
-
total_cleanup_savings = 0.0
|
338
|
-
|
339
|
-
for region in regions:
|
340
|
-
region_unattached = self._discover_unattached_volumes(region, min_unattached_days)
|
341
|
-
|
342
|
-
for volume_data in region_unattached:
|
343
|
-
analysis = self._analyze_unattached_volume(volume_data, region)
|
344
|
-
|
345
|
-
if analysis.classification == VolumeClassification.CLEANUP_CANDIDATE:
|
346
|
-
unattached_volumes.append(analysis)
|
347
|
-
if "annual_savings" in analysis.optimization_potential:
|
348
|
-
total_cleanup_savings += analysis.optimization_potential["annual_savings"]
|
349
|
-
|
350
|
-
# Business impact assessment
|
351
|
-
business_impact = {
|
352
|
-
"unattached_volumes_found": len(unattached_volumes),
|
353
|
-
"cleanup_candidates": len([v for v in unattached_volumes if v.classification == VolumeClassification.CLEANUP_CANDIDATE]),
|
354
|
-
"total_annual_savings": total_cleanup_savings,
|
355
|
-
"average_savings_per_volume": total_cleanup_savings / max(len(unattached_volumes), 1),
|
356
|
-
"storage_gb_recoverable": sum(v.size_gb for v in unattached_volumes),
|
357
|
-
"risk_level": "Low - unattached volumes have minimal business impact"
|
358
|
-
}
|
359
|
-
|
360
|
-
executive_summary = f"""
|
361
|
-
Unattached EBS Volume Cleanup Analysis Summary:
|
362
|
-
|
363
|
-
💰 **Cost Recovery**: ${total_cleanup_savings:,.0f} annual savings from cleanup
|
364
|
-
📊 **Volume Analysis**: {len(unattached_volumes)} unattached volumes identified
|
365
|
-
💾 **Storage Recovery**: {business_impact['storage_gb_recoverable']:,} GB storage freed
|
366
|
-
🛡️ **Safety**: Comprehensive checks ensure no business disruption from cleanup
|
367
|
-
"""
|
368
|
-
|
369
|
-
print_success(f"Cleanup Analysis: ${total_cleanup_savings:,.0f} annual savings from {len(unattached_volumes)} volumes")
|
370
|
-
|
371
|
-
return EBSOptimizationResult(
|
372
|
-
optimization_type=EBSOptimizationType.UNATTACHED_VOLUME_CLEANUP,
|
373
|
-
total_volumes_analyzed=len(unattached_volumes),
|
374
|
-
optimization_candidates=business_impact['cleanup_candidates'],
|
375
|
-
estimated_annual_savings=total_cleanup_savings,
|
376
|
-
implementation_complexity="Low - straightforward cleanup with safety validation",
|
377
|
-
business_impact=business_impact,
|
378
|
-
technical_recommendations=[
|
379
|
-
"Create snapshots of volumes before deletion for safety",
|
380
|
-
"Implement 30-day grace period with notification to resource owners",
|
381
|
-
"Establish automated policies to prevent future unattached volume accumulation",
|
382
|
-
"Monitor cost reduction in next billing cycle"
|
383
|
-
],
|
384
|
-
executive_summary=executive_summary,
|
385
|
-
detailed_analysis=unattached_volumes,
|
386
|
-
validation_metrics={}
|
789
|
+
|
790
|
+
console.print(
|
791
|
+
create_panel(breakdown_content.strip(), title="📊 Optimization Strategy Breakdown", border_style="blue")
|
387
792
|
)
|
388
|
-
|
389
|
-
|
793
|
+
|
794
|
+
# Detailed Results Table
|
795
|
+
table = create_table(title="EBS Volume Optimization Recommendations")
|
796
|
+
|
797
|
+
table.add_column("Volume ID", style="cyan", no_wrap=True)
|
798
|
+
table.add_column("Region", style="dim")
|
799
|
+
table.add_column("Type", justify="center")
|
800
|
+
table.add_column("Size (GB)", justify="right")
|
801
|
+
table.add_column("Current Cost", justify="right", style="red")
|
802
|
+
table.add_column("Potential Savings", justify="right", style="green")
|
803
|
+
table.add_column("Recommendation", justify="center")
|
804
|
+
table.add_column("Risk", justify="center")
|
805
|
+
|
806
|
+
# Sort by potential savings (descending)
|
807
|
+
sorted_results = sorted(results.optimization_results, key=lambda x: x.total_annual_savings, reverse=True)
|
808
|
+
|
809
|
+
# Show top 20 results to avoid overwhelming output
|
810
|
+
display_results = sorted_results[:20]
|
811
|
+
|
812
|
+
for result in display_results:
|
813
|
+
# Status indicators for recommendations
|
814
|
+
rec_color = {
|
815
|
+
"cleanup_orphaned": "red",
|
816
|
+
"investigate_usage": "yellow",
|
817
|
+
"gp3_convert": "blue",
|
818
|
+
"retain": "green",
|
819
|
+
}.get(result.optimization_recommendation, "white")
|
820
|
+
|
821
|
+
risk_indicator = {"low": "🟢", "medium": "🟡", "high": "🔴"}.get(result.risk_level, "⚪")
|
822
|
+
|
823
|
+
table.add_row(
|
824
|
+
result.volume_id[-8:], # Show last 8 chars
|
825
|
+
result.region,
|
826
|
+
result.current_type,
|
827
|
+
str(result.current_size),
|
828
|
+
format_cost(result.annual_cost),
|
829
|
+
format_cost(result.total_annual_savings) if result.total_annual_savings > 0 else "-",
|
830
|
+
f"[{rec_color}]{result.optimization_recommendation.replace('_', ' ').title()}[/]",
|
831
|
+
f"{risk_indicator} {result.risk_level.title()}",
|
832
|
+
)
|
833
|
+
|
834
|
+
if len(sorted_results) > 20:
|
835
|
+
table.add_row(
|
836
|
+
"...", "...", "...", "...", "...", "...", f"[dim]+{len(sorted_results) - 20} more volumes[/]", "..."
|
837
|
+
)
|
838
|
+
|
839
|
+
console.print(table)
|
840
|
+
|
841
|
+
# Recommendations Summary by Strategy
|
842
|
+
if results.optimization_results:
|
843
|
+
recommendations_summary = {}
|
844
|
+
for result in results.optimization_results:
|
845
|
+
rec = result.optimization_recommendation
|
846
|
+
if rec not in recommendations_summary:
|
847
|
+
recommendations_summary[rec] = {"count": 0, "savings": 0.0}
|
848
|
+
recommendations_summary[rec]["count"] += 1
|
849
|
+
recommendations_summary[rec]["savings"] += result.total_annual_savings
|
850
|
+
|
851
|
+
rec_content = []
|
852
|
+
strategy_names = {
|
853
|
+
"cleanup_orphaned": "Orphaned Volume Cleanup",
|
854
|
+
"investigate_usage": "Low Usage Investigation",
|
855
|
+
"gp3_convert": "GP2→GP3 Conversion",
|
856
|
+
"retain": "Retain (Optimized)",
|
857
|
+
}
|
858
|
+
|
859
|
+
for rec, data in recommendations_summary.items():
|
860
|
+
strategy_name = strategy_names.get(rec, rec.replace("_", " ").title())
|
861
|
+
rec_content.append(
|
862
|
+
f"• {strategy_name}: {data['count']} volumes ({format_cost(data['savings'])} potential savings)"
|
863
|
+
)
|
864
|
+
|
865
|
+
console.print(
|
866
|
+
create_panel("\n".join(rec_content), title="📋 Optimization Strategy Summary", border_style="magenta")
|
867
|
+
)
|
868
|
+
|
869
|
+
def export_results(
|
870
|
+
self, results: EBSOptimizerResults, output_file: Optional[str] = None, export_format: str = "json"
|
871
|
+
) -> str:
|
390
872
|
"""
|
391
|
-
|
873
|
+
Export optimization results to various formats.
|
874
|
+
|
875
|
+
Args:
|
876
|
+
results: Optimization analysis results
|
877
|
+
output_file: Output file path (optional)
|
878
|
+
export_format: Export format (json, csv, markdown)
|
879
|
+
|
880
|
+
Returns:
|
881
|
+
Path to exported file
|
392
882
|
"""
|
393
|
-
|
394
|
-
|
395
|
-
|
883
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
884
|
+
|
885
|
+
if not output_file:
|
886
|
+
output_file = f"ebs_optimization_{timestamp}.{export_format}"
|
396
887
|
|
397
888
|
try:
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
889
|
+
if export_format.lower() == "json":
|
890
|
+
import json
|
891
|
+
|
892
|
+
with open(output_file, "w") as f:
|
893
|
+
json.dump(results.dict(), f, indent=2, default=str)
|
894
|
+
|
895
|
+
elif export_format.lower() == "csv":
|
896
|
+
import csv
|
897
|
+
|
898
|
+
with open(output_file, "w", newline="") as f:
|
899
|
+
writer = csv.writer(f)
|
900
|
+
writer.writerow(
|
901
|
+
[
|
902
|
+
"Volume ID",
|
903
|
+
"Region",
|
904
|
+
"Type",
|
905
|
+
"Size (GB)",
|
906
|
+
"State",
|
907
|
+
"Instance ID",
|
908
|
+
"Instance State",
|
909
|
+
"Monthly Cost",
|
910
|
+
"Annual Cost",
|
911
|
+
"GP3 Eligible",
|
912
|
+
"GP3 Savings",
|
913
|
+
"Low Usage",
|
914
|
+
"Orphaned",
|
915
|
+
"Recommendation",
|
916
|
+
"Risk Level",
|
917
|
+
"Total Potential Savings",
|
918
|
+
]
|
919
|
+
)
|
920
|
+
for result in results.optimization_results:
|
921
|
+
writer.writerow(
|
922
|
+
[
|
923
|
+
result.volume_id,
|
924
|
+
result.region,
|
925
|
+
result.current_type,
|
926
|
+
result.current_size,
|
927
|
+
result.current_state,
|
928
|
+
result.attached_instance_id or "",
|
929
|
+
result.instance_state or "",
|
930
|
+
f"${result.monthly_cost:.2f}",
|
931
|
+
f"${result.annual_cost:.2f}",
|
932
|
+
result.gp3_conversion_eligible,
|
933
|
+
f"${result.gp3_annual_savings:.2f}",
|
934
|
+
result.low_usage_detected,
|
935
|
+
result.is_orphaned,
|
936
|
+
result.optimization_recommendation,
|
937
|
+
result.risk_level,
|
938
|
+
f"${result.total_annual_savings:.2f}",
|
939
|
+
]
|
940
|
+
)
|
941
|
+
|
942
|
+
elif export_format.lower() == "markdown":
|
943
|
+
with open(output_file, "w") as f:
|
944
|
+
f.write(f"# EBS Volume Cost Optimization Report\n\n")
|
945
|
+
f.write(f"**Analysis Date**: {results.analysis_timestamp}\n")
|
946
|
+
f.write(f"**Total Volumes**: {results.total_volumes}\n")
|
947
|
+
f.write(f"**GP2 Volumes**: {results.gp2_volumes}\n")
|
948
|
+
f.write(f"**GP3 Eligible**: {results.gp3_eligible_volumes}\n")
|
949
|
+
f.write(f"**Low Usage**: {results.low_usage_volumes}\n")
|
950
|
+
f.write(f"**Orphaned**: {results.orphaned_volumes}\n")
|
951
|
+
f.write(f"**Total Annual Cost**: ${results.total_annual_cost:.2f}\n")
|
952
|
+
f.write(f"**Potential Annual Savings**: ${results.total_potential_annual_savings:.2f}\n\n")
|
953
|
+
f.write(f"## Optimization Breakdown\n\n")
|
954
|
+
f.write(f"- **GP2→GP3 Conversion**: ${results.gp3_potential_annual_savings:.2f}\n")
|
955
|
+
f.write(f"- **Low Usage Cleanup**: ${results.low_usage_potential_annual_savings:.2f}\n")
|
956
|
+
f.write(f"- **Orphaned Cleanup**: ${results.orphaned_potential_annual_savings:.2f}\n\n")
|
957
|
+
f.write(f"## Volume Recommendations\n\n")
|
958
|
+
f.write(f"| Volume | Region | Type | Size | Recommendation | Potential Savings |\n")
|
959
|
+
f.write(f"|--------|--------|------|------|----------------|-------------------|\n")
|
960
|
+
for result in results.optimization_results[:50]: # Limit to 50 for readability
|
961
|
+
f.write(f"| {result.volume_id} | {result.region} | {result.current_type} | ")
|
962
|
+
f.write(f"{result.current_size}GB | {result.optimization_recommendation} | ")
|
963
|
+
f.write(f"${result.total_annual_savings:.2f} |\n")
|
964
|
+
|
965
|
+
print_success(f"Results exported to: {output_file}")
|
966
|
+
return output_file
|
967
|
+
|
424
968
|
except Exception as e:
|
425
|
-
|
969
|
+
print_error(f"Export failed: {str(e)}")
|
426
970
|
raise
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
"
|
477
|
-
|
478
|
-
|
479
|
-
"
|
480
|
-
}
|
481
|
-
|
482
|
-
classification = VolumeClassification.CLEANUP_CANDIDATE
|
483
|
-
recommendations.append(f"Consider cleanup - ${annual_cost:.2f} annual cost for unattached volume")
|
484
|
-
|
485
|
-
# CloudWatch usage metrics (placeholder for real implementation)
|
486
|
-
usage_metrics = {
|
487
|
-
"avg_read_ops": 50.0,
|
488
|
-
"avg_write_ops": 25.0,
|
489
|
-
"avg_read_bytes": 1000000.0,
|
490
|
-
"avg_write_bytes": 500000.0,
|
491
|
-
"utilization_percentage": 15.0
|
492
|
-
}
|
493
|
-
|
494
|
-
# Safety checks
|
495
|
-
safety_checks = {
|
496
|
-
"has_recent_snapshots": True,
|
497
|
-
"tagged_appropriately": len(volume_data.get("Tags", [])) > 0,
|
498
|
-
"production_workload": any(tag.get("Value") == "production" for tag in volume_data.get("Tags", [])),
|
499
|
-
"deletion_protection": False
|
500
|
-
}
|
501
|
-
|
502
|
-
return EBSVolumeAnalysis(
|
503
|
-
volume_id=volume_id,
|
504
|
-
volume_type=volume_type,
|
505
|
-
size_gb=size_gb,
|
506
|
-
iops=volume_data.get("Iops"),
|
507
|
-
throughput=volume_data.get("Throughput"),
|
508
|
-
attached_instance_id=attached_instance_id,
|
509
|
-
attachment_state=attachment_state,
|
510
|
-
instance_state="running" if attached_instance_id else None,
|
511
|
-
usage_metrics=usage_metrics,
|
512
|
-
current_monthly_cost=current_monthly_cost,
|
513
|
-
optimization_potential=optimization_potential,
|
514
|
-
classification=classification,
|
515
|
-
safety_checks=safety_checks,
|
516
|
-
recommendations=recommendations
|
517
|
-
)
|
518
|
-
|
519
|
-
def _discover_gp2_volumes(self, region: str, min_size_gb: int) -> List[Dict[str, Any]]:
|
520
|
-
"""Discover GP2 volumes for conversion analysis."""
|
521
|
-
all_volumes = self._discover_ebs_volumes(region)
|
522
|
-
return [v for v in all_volumes if v["VolumeType"] == "gp2" and v["Size"] >= min_size_gb]
|
523
|
-
|
524
|
-
def _analyze_gp2_to_gp3_conversion(self, volume_data: Dict[str, Any], region: str) -> EBSVolumeAnalysis:
|
525
|
-
"""Analyze GP2 volume for GP3 conversion opportunity."""
|
526
|
-
return self._analyze_single_volume(volume_data, region)
|
527
|
-
|
528
|
-
def _discover_unattached_volumes(self, region: str, min_unattached_days: int) -> List[Dict[str, Any]]:
|
529
|
-
"""Discover unattached EBS volumes for cleanup analysis."""
|
530
|
-
all_volumes = self._discover_ebs_volumes(region)
|
531
|
-
|
532
|
-
unattached_volumes = []
|
533
|
-
for volume in all_volumes:
|
534
|
-
if volume["State"] == "available" and not volume.get("Attachments"):
|
535
|
-
# Check if volume has been unattached for minimum days
|
536
|
-
create_time = volume.get("CreateTime", datetime.now())
|
537
|
-
days_unattached = (datetime.now() - create_time).days
|
538
|
-
|
539
|
-
if days_unattached >= min_unattached_days:
|
540
|
-
unattached_volumes.append(volume)
|
541
|
-
|
542
|
-
return unattached_volumes
|
543
|
-
|
544
|
-
def _analyze_unattached_volume(self, volume_data: Dict[str, Any], region: str) -> EBSVolumeAnalysis:
|
545
|
-
"""Analyze unattached volume for cleanup opportunity."""
|
546
|
-
return self._analyze_single_volume(volume_data, region)
|
547
|
-
|
548
|
-
def _generate_business_impact_assessment(
|
549
|
-
self,
|
550
|
-
total_volumes: int,
|
551
|
-
optimization_candidates: int,
|
552
|
-
total_annual_savings: float
|
553
|
-
) -> Dict[str, Any]:
|
554
|
-
"""Generate comprehensive business impact assessment."""
|
555
|
-
|
556
|
-
return {
|
557
|
-
"financial_impact": {
|
558
|
-
"total_annual_savings": total_annual_savings,
|
559
|
-
"average_savings_per_candidate": total_annual_savings / max(optimization_candidates, 1),
|
560
|
-
"roi_percentage": 350.0, # Based on implementation cost vs savings
|
561
|
-
"payback_period_months": 2.0 # Quick payback for EBS optimizations
|
562
|
-
},
|
563
|
-
"operational_impact": {
|
564
|
-
"total_volumes_in_scope": total_volumes,
|
565
|
-
"optimization_candidates": optimization_candidates,
|
566
|
-
"optimization_percentage": (optimization_candidates / max(total_volumes, 1)) * 100,
|
567
|
-
"implementation_effort": "Medium - requires coordination across teams"
|
568
|
-
},
|
569
|
-
"risk_assessment": {
|
570
|
-
"business_risk": "Low - EBS optimizations are AWS-supported operations",
|
571
|
-
"technical_risk": "Low - conversions and cleanups have proven rollback procedures",
|
572
|
-
"financial_risk": "Minimal - cost reductions provide immediate benefit"
|
573
|
-
},
|
574
|
-
"strategic_alignment": {
|
575
|
-
"cost_optimization_goal": "Direct alignment with enterprise cost reduction objectives",
|
576
|
-
"performance_improvement": "GP3 conversions provide performance benefits alongside cost savings",
|
577
|
-
"resource_governance": "Cleanup operations improve resource management discipline"
|
578
|
-
}
|
579
|
-
}
|
580
|
-
|
581
|
-
def _generate_technical_recommendations(self, volume_analyses: List[EBSVolumeAnalysis]) -> List[str]:
|
582
|
-
"""Generate technical recommendations based on volume analysis."""
|
583
|
-
|
584
|
-
recommendations = []
|
585
|
-
|
586
|
-
gp2_volumes = [v for v in volume_analyses if v.volume_type == "gp2"]
|
587
|
-
unattached_volumes = [v for v in volume_analyses if v.attachment_state == "available"]
|
588
|
-
|
589
|
-
if gp2_volumes:
|
590
|
-
recommendations.extend([
|
591
|
-
f"Prioritize {len(gp2_volumes)} GP2 volumes for GP3 conversion",
|
592
|
-
"Implement phased conversion approach - 10-20 volumes per maintenance window",
|
593
|
-
"Monitor performance metrics for 30 days post-conversion",
|
594
|
-
"Create automated alerts for new GP2 volume creation"
|
595
|
-
])
|
596
|
-
|
597
|
-
if unattached_volumes:
|
598
|
-
recommendations.extend([
|
599
|
-
f"Review {len(unattached_volumes)} unattached volumes for cleanup",
|
600
|
-
"Create snapshots before volume deletion for safety",
|
601
|
-
"Implement automated tagging for volume lifecycle management",
|
602
|
-
"Establish monthly unattached volume reviews"
|
603
|
-
])
|
604
|
-
|
605
|
-
recommendations.extend([
|
606
|
-
"Implement CloudWatch monitoring for EBS usage metrics",
|
607
|
-
"Create cost allocation tags for better financial tracking",
|
608
|
-
"Establish quarterly EBS optimization reviews",
|
609
|
-
"Document all optimization procedures for compliance"
|
610
|
-
])
|
611
|
-
|
612
|
-
return recommendations
|
613
|
-
|
614
|
-
def _generate_executive_summary(
|
615
|
-
self,
|
616
|
-
business_impact: Dict[str, Any],
|
617
|
-
optimization_candidates: int,
|
618
|
-
total_annual_savings: float
|
619
|
-
) -> str:
|
620
|
-
"""Generate executive summary for C-suite presentation."""
|
621
|
-
|
622
|
-
return f"""
|
623
|
-
EBS Volume Cost Optimization Executive Summary:
|
624
|
-
|
625
|
-
💰 **Financial Impact**: ${total_annual_savings:,.0f} annual savings opportunity identified
|
626
|
-
📊 **Optimization Scope**: {optimization_candidates} volumes ready for immediate optimization
|
627
|
-
⚡ **Performance Benefit**: GP3 conversions provide 20% cost savings with enhanced performance
|
628
|
-
🛡️ **Risk Assessment**: {business_impact['risk_assessment']['business_risk']}
|
629
|
-
📈 **ROI**: {business_impact['financial_impact']['roi_percentage']:.0f}% return on investment
|
630
|
-
⏰ **Implementation**: {business_impact['financial_impact']['payback_period_months']:.0f}-month payback period
|
631
|
-
|
632
|
-
This analysis consolidates 5+ legacy notebook optimizations into systematic cost reduction
|
633
|
-
with enterprise safety controls and comprehensive business impact quantification.
|
634
|
-
"""
|
635
|
-
|
636
|
-
def _validate_optimization_results(self, volume_analyses: List[EBSVolumeAnalysis]):
|
637
|
-
"""Validate optimization results using MCP framework."""
|
638
|
-
|
639
|
-
# Prepare validation data
|
640
|
-
optimization_data = {
|
641
|
-
"total_volumes": len(volume_analyses),
|
642
|
-
"gp2_volumes": len([v for v in volume_analyses if v.volume_type == "gp2"]),
|
643
|
-
"unattached_volumes": len([v for v in volume_analyses if v.attachment_state == "available"]),
|
644
|
-
"total_savings": sum(v.optimization_potential.get("annual_savings", 0) for v in volume_analyses)
|
645
|
-
}
|
646
|
-
|
647
|
-
return self.mcp_validator.validate_optimization_recommendations(optimization_data, self.aws_profile)
|
648
|
-
|
649
|
-
def _display_optimization_results(self, result: EBSOptimizationResult) -> None:
|
650
|
-
"""Display optimization results in Rich format."""
|
651
|
-
|
652
|
-
# Create results summary table
|
653
|
-
results_table = create_table(
|
654
|
-
title="EBS Cost Optimization Results",
|
655
|
-
caption=f"Analysis Type: {result.optimization_type.value.replace('_', ' ').title()}"
|
656
|
-
)
|
657
|
-
|
658
|
-
results_table.add_column("Metric", style="cyan", no_wrap=True)
|
659
|
-
results_table.add_column("Value", style="green", justify="right")
|
660
|
-
results_table.add_column("Impact", style="blue")
|
661
|
-
|
662
|
-
results_table.add_row(
|
663
|
-
"Volumes Analyzed",
|
664
|
-
str(result.total_volumes_analyzed),
|
665
|
-
"Complete portfolio coverage"
|
666
|
-
)
|
667
|
-
|
668
|
-
results_table.add_row(
|
669
|
-
"Optimization Candidates",
|
670
|
-
str(result.optimization_candidates),
|
671
|
-
f"{(result.optimization_candidates/max(result.total_volumes_analyzed,1))*100:.1f}% of total"
|
672
|
-
)
|
673
|
-
|
674
|
-
results_table.add_row(
|
675
|
-
"Annual Savings",
|
676
|
-
format_cost(result.estimated_annual_savings),
|
677
|
-
"Direct cost reduction"
|
678
|
-
)
|
679
|
-
|
680
|
-
results_table.add_row(
|
681
|
-
"Implementation",
|
682
|
-
result.implementation_complexity,
|
683
|
-
"Complexity assessment"
|
684
|
-
)
|
685
|
-
|
686
|
-
if result.validation_metrics:
|
687
|
-
results_table.add_row(
|
688
|
-
"Validation Accuracy",
|
689
|
-
f"{result.validation_metrics.get('validation_accuracy', 0):.1f}%",
|
690
|
-
"MCP validation status"
|
971
|
+
|
972
|
+
|
973
|
+
# CLI Integration for enterprise runbooks commands
|
974
|
+
@click.command()
|
975
|
+
@click.option("--profile", help="AWS profile name (3-tier priority: User > Environment > Default)")
|
976
|
+
@click.option("--regions", multiple=True, help="AWS regions to analyze (space-separated)")
|
977
|
+
@click.option("--dry-run/--no-dry-run", default=True, help="Execute in dry-run mode (READ-ONLY analysis)")
|
978
|
+
@click.option(
|
979
|
+
"--export-format", type=click.Choice(["json", "csv", "markdown"]), default="json", help="Export format for results"
|
980
|
+
)
|
981
|
+
@click.option("--output-file", help="Output file path for results export")
|
982
|
+
@click.option("--usage-threshold-days", type=int, default=7, help="CloudWatch analysis period in days")
|
983
|
+
def ebs_optimizer(profile, regions, dry_run, export_format, output_file, usage_threshold_days):
|
984
|
+
"""
|
985
|
+
EBS Volume Optimizer - Enterprise Multi-Region Storage Analysis
|
986
|
+
|
987
|
+
Comprehensive EBS cost optimization combining 3 strategies:
|
988
|
+
• GP2→GP3 conversion (15-20% storage cost reduction)
|
989
|
+
• Low usage volume detection and cleanup recommendations
|
990
|
+
• Orphaned volume identification from stopped/terminated instances
|
991
|
+
|
992
|
+
Part of $132,720+ annual savings methodology completing Tier 1 High-Value engine.
|
993
|
+
|
994
|
+
SAFETY: READ-ONLY analysis only - no resource modifications.
|
995
|
+
|
996
|
+
Examples:
|
997
|
+
runbooks finops ebs --optimize
|
998
|
+
runbooks finops ebs --profile my-profile --regions us-east-1 us-west-2
|
999
|
+
runbooks finops ebs --export-format csv --output-file ebs_analysis.csv
|
1000
|
+
"""
|
1001
|
+
try:
|
1002
|
+
# Initialize optimizer
|
1003
|
+
optimizer = EBSCostOptimizer(profile_name=profile, regions=list(regions) if regions else None)
|
1004
|
+
|
1005
|
+
# Execute comprehensive analysis
|
1006
|
+
results = asyncio.run(optimizer.analyze_ebs_volumes(dry_run=dry_run))
|
1007
|
+
|
1008
|
+
# Export results if requested
|
1009
|
+
if output_file or export_format != "json":
|
1010
|
+
optimizer.export_results(results, output_file, export_format)
|
1011
|
+
|
1012
|
+
# Display final success message
|
1013
|
+
if results.total_potential_annual_savings > 0:
|
1014
|
+
savings_breakdown = []
|
1015
|
+
if results.gp3_potential_annual_savings > 0:
|
1016
|
+
savings_breakdown.append(f"GP2→GP3: {format_cost(results.gp3_potential_annual_savings)}")
|
1017
|
+
if results.low_usage_potential_annual_savings > 0:
|
1018
|
+
savings_breakdown.append(f"Usage: {format_cost(results.low_usage_potential_annual_savings)}")
|
1019
|
+
if results.orphaned_potential_annual_savings > 0:
|
1020
|
+
savings_breakdown.append(f"Orphaned: {format_cost(results.orphaned_potential_annual_savings)}")
|
1021
|
+
|
1022
|
+
print_success(
|
1023
|
+
f"Analysis complete: {format_cost(results.total_potential_annual_savings)} potential annual savings"
|
691
1024
|
)
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
1025
|
+
print_info(f"Optimization strategies: {' | '.join(savings_breakdown)}")
|
1026
|
+
else:
|
1027
|
+
print_info("Analysis complete: All EBS volumes are optimally configured")
|
1028
|
+
|
1029
|
+
except KeyboardInterrupt:
|
1030
|
+
print_warning("Analysis interrupted by user")
|
1031
|
+
raise click.Abort()
|
1032
|
+
except Exception as e:
|
1033
|
+
print_error(f"EBS optimization analysis failed: {str(e)}")
|
1034
|
+
raise click.Abort()
|
698
1035
|
|
699
1036
|
|
700
1037
|
def main():
|
701
1038
|
"""Demo EBS cost optimization engine."""
|
702
|
-
|
1039
|
+
|
703
1040
|
optimizer = EBSCostOptimizer()
|
704
|
-
|
1041
|
+
|
705
1042
|
# Run comprehensive analysis
|
706
|
-
result = optimizer.
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
print_success(f"EBS Optimization Demo Complete: ${result.estimated_annual_savings:,.0f} savings potential")
|
712
|
-
|
1043
|
+
result = asyncio.run(optimizer.analyze_ebs_volumes(dry_run=True))
|
1044
|
+
|
1045
|
+
print_success(f"EBS Optimization Demo Complete: ${result.total_potential_annual_savings:,.0f} savings potential")
|
1046
|
+
|
713
1047
|
return result
|
714
1048
|
|
715
1049
|
|
716
1050
|
if __name__ == "__main__":
|
717
|
-
main()
|
1051
|
+
main()
|