runbooks 1.0.0__py3-none-any.whl → 1.0.2__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/cfat/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/models.py +20 -14
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1070 -105
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/date_utils.py +115 -0
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/enhanced_exception_handler.py +10 -7
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +86 -118
- runbooks/common/rich_utils.py +3 -3
- runbooks/common/sre_performance_suite.py +574 -0
- runbooks/finops/business_case_config.py +314 -0
- runbooks/finops/cost_processor.py +19 -4
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_cost_optimizer.py +1 -1
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/nat_gateway_optimizer.py +46 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- runbooks/finops/vpc_cleanup_optimizer.py +22 -29
- runbooks/inventory/core/collector.py +51 -28
- runbooks/inventory/discovery.md +197 -247
- runbooks/inventory/inventory_modules.py +2 -2
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/models/account.py +5 -3
- runbooks/inventory/models/inventory.py +1 -1
- runbooks/inventory/models/resource.py +5 -3
- runbooks/inventory/organizations_discovery.py +102 -13
- runbooks/inventory/unified_validation_engine.py +2 -15
- runbooks/main.py +255 -92
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +17 -13
- runbooks/operate/vpc_operations.py +82 -13
- runbooks/remediation/base.py +3 -1
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +66 -18
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/security/compliance_automation_engine.py +99 -20
- runbooks/security/config/__init__.py +24 -0
- runbooks/security/config/compliance_config.py +255 -0
- runbooks/security/config/compliance_weights_example.json +22 -0
- runbooks/security/config_template_generator.py +500 -0
- runbooks/security/security_cli.py +377 -0
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +26 -15
- runbooks/validation/mcp_validator.py +62 -8
- runbooks/vpc/config.py +49 -15
- runbooks/vpc/cross_account_session.py +5 -1
- runbooks/vpc/heatmap_engine.py +438 -59
- runbooks/vpc/mcp_no_eni_validator.py +115 -36
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +3 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/RECORD +85 -79
- 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/tests/results_test_finops_dashboard.xml +0 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/top_level.txt +0 -0
@@ -22,7 +22,8 @@ AWS_AVAILABLE = True
|
|
22
22
|
|
23
23
|
def get_aws_profiles() -> List[str]:
|
24
24
|
"""Stub implementation - use dashboard_runner.py instead."""
|
25
|
-
|
25
|
+
import os
|
26
|
+
return ["default", os.getenv("BILLING_PROFILE", "default-billing-profile")]
|
26
27
|
|
27
28
|
|
28
29
|
def get_account_id(profile: str = "default") -> str:
|
@@ -46,10 +47,10 @@ class FinOpsConfig:
|
|
46
47
|
include_budget_data: bool = True
|
47
48
|
include_resource_analysis: bool = True
|
48
49
|
|
49
|
-
# Legacy compatibility properties with environment
|
50
|
-
billing_profile: str = "
|
51
|
-
management_profile: str = "
|
52
|
-
operational_profile: str = "
|
50
|
+
# Legacy compatibility properties with universal environment support
|
51
|
+
billing_profile: str = field(default_factory=lambda: os.getenv("BILLING_PROFILE", "default-billing-profile"))
|
52
|
+
management_profile: str = field(default_factory=lambda: os.getenv("MANAGEMENT_PROFILE", "default-management-profile"))
|
53
|
+
operational_profile: str = field(default_factory=lambda: os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"))
|
53
54
|
|
54
55
|
# Additional expected attributes from tests
|
55
56
|
time_range_days: int = 30
|
@@ -1,18 +1,18 @@
|
|
1
1
|
"""
|
2
|
-
FinOps Business Scenarios -
|
2
|
+
FinOps Business Scenarios - Dynamic Business Case Framework
|
3
3
|
|
4
|
-
Strategic Achievement:
|
5
|
-
-
|
6
|
-
-
|
7
|
-
-
|
4
|
+
Strategic Achievement: Enterprise business case management with configurable scenarios
|
5
|
+
- Dynamic scenario configuration with environment variable overrides
|
6
|
+
- Business-focused naming conventions replacing hardcoded JIRA references
|
7
|
+
- Scalable template system for unlimited business case expansion
|
8
8
|
|
9
9
|
This module provides business-oriented wrapper functions for executive presentations
|
10
10
|
calling proven technical implementations from src/runbooks/remediation/ modules.
|
11
11
|
|
12
12
|
Strategic Alignment:
|
13
|
-
- "Do one thing and do it well":
|
14
|
-
- "Move Fast, But Not So Fast We Crash": Proven technical implementations
|
15
|
-
- Enterprise FAANG SDLC: Evidence-based cost optimization with
|
13
|
+
- "Do one thing and do it well": Dynamic configuration management with enterprise templates
|
14
|
+
- "Move Fast, But Not So Fast We Crash": Proven technical implementations with configurable business cases
|
15
|
+
- Enterprise FAANG SDLC: Evidence-based cost optimization with reusable template framework
|
16
16
|
"""
|
17
17
|
|
18
18
|
import asyncio
|
@@ -30,6 +30,10 @@ from ..common.rich_utils import (
|
|
30
30
|
)
|
31
31
|
from ..remediation import workspaces_list, rds_snapshot_list
|
32
32
|
from . import commvault_ec2_analysis
|
33
|
+
from .business_case_config import (
|
34
|
+
get_business_case_config, get_scenario_display_name, get_scenario_savings_range,
|
35
|
+
format_business_achievement, migrate_legacy_scenario_reference
|
36
|
+
)
|
33
37
|
|
34
38
|
logger = logging.getLogger(__name__)
|
35
39
|
|
@@ -55,9 +59,9 @@ def create_business_scenarios_validated(profile_name: Optional[str] = None) -> D
|
|
55
59
|
commvault_data = scenarios_analyzer._get_real_commvault_data()
|
56
60
|
|
57
61
|
scenarios = {
|
58
|
-
'
|
59
|
-
'
|
60
|
-
'
|
62
|
+
'workspaces': workspaces_data,
|
63
|
+
'rds_snapshots': rds_data,
|
64
|
+
'backup_investigation': commvault_data,
|
61
65
|
'metadata': {
|
62
66
|
'generated_at': datetime.now().isoformat(),
|
63
67
|
'data_source': 'Real AWS APIs via runbooks',
|
@@ -70,29 +74,32 @@ def create_business_scenarios_validated(profile_name: Optional[str] = None) -> D
|
|
70
74
|
|
71
75
|
except Exception as e:
|
72
76
|
logger.error(f"Error creating validated scenarios: {e}")
|
73
|
-
# Return fallback business scenarios
|
77
|
+
# Return fallback business scenarios using dynamic configuration
|
78
|
+
config = get_business_case_config()
|
79
|
+
workspaces_scenario = config.get_scenario('workspaces')
|
80
|
+
rds_scenario = config.get_scenario('rds-snapshots')
|
81
|
+
backup_scenario = config.get_scenario('backup-investigation')
|
82
|
+
|
74
83
|
return {
|
75
|
-
'
|
76
|
-
'title':
|
77
|
-
'
|
78
|
-
'
|
79
|
-
'risk_level': 'Low'
|
84
|
+
'workspaces': {
|
85
|
+
'title': workspaces_scenario.display_name if workspaces_scenario else 'WorkSpaces Resource Optimization',
|
86
|
+
'savings_range': workspaces_scenario.savings_range_display if workspaces_scenario else '$12K-15K/year',
|
87
|
+
'risk_level': workspaces_scenario.risk_level if workspaces_scenario else 'Low'
|
80
88
|
},
|
81
|
-
'
|
82
|
-
'title': 'RDS
|
83
|
-
'
|
84
|
-
'
|
85
|
-
'risk_level': 'Medium'
|
89
|
+
'rds_snapshots': {
|
90
|
+
'title': rds_scenario.display_name if rds_scenario else 'RDS Storage Optimization',
|
91
|
+
'savings_range': rds_scenario.savings_range_display if rds_scenario else '$5K-24K/year',
|
92
|
+
'risk_level': rds_scenario.risk_level if rds_scenario else 'Medium'
|
86
93
|
},
|
87
|
-
'
|
88
|
-
'title': '
|
94
|
+
'backup_investigation': {
|
95
|
+
'title': backup_scenario.display_name if backup_scenario else 'Backup Infrastructure Analysis',
|
89
96
|
'framework_status': 'Investigation Ready',
|
90
|
-
'risk_level': 'Medium'
|
97
|
+
'risk_level': backup_scenario.risk_level if backup_scenario else 'Medium'
|
91
98
|
},
|
92
99
|
'metadata': {
|
93
100
|
'generated_at': datetime.now().isoformat(),
|
94
|
-
'data_source': '
|
95
|
-
'validation_method': '
|
101
|
+
'data_source': 'Dynamic business case configuration',
|
102
|
+
'validation_method': 'Template-based business scenarios',
|
96
103
|
'version': '0.9.5'
|
97
104
|
}
|
98
105
|
}
|
runbooks/finops/iam_guidance.py
CHANGED
@@ -11,6 +11,8 @@ from rich.console import Console
|
|
11
11
|
from rich.panel import Panel
|
12
12
|
from rich.table import Table
|
13
13
|
|
14
|
+
from runbooks.common import get_aws_cli_example_period
|
15
|
+
|
14
16
|
console = Console()
|
15
17
|
|
16
18
|
|
@@ -311,6 +313,9 @@ def handle_cost_explorer_error(error: Exception, profile_name: Optional[str] = N
|
|
311
313
|
|
312
314
|
def _display_single_account_cost_explorer_guidance(error: Exception, profile_name: Optional[str] = None):
|
313
315
|
"""Display context-aware guidance for single account Cost Explorer limitations."""
|
316
|
+
|
317
|
+
# Get dynamic date period for CLI examples
|
318
|
+
start_date, end_date = get_aws_cli_example_period()
|
314
319
|
|
315
320
|
# Main explanation panel
|
316
321
|
explanation_panel = Panel(
|
@@ -354,7 +359,7 @@ def _display_single_account_cost_explorer_guidance(error: Exception, profile_nam
|
|
354
359
|
f"[green]✅ Recommended Solutions:[/green]\n\n"
|
355
360
|
f"{solution_commands}\n\n"
|
356
361
|
f"[bold]🎯 Quick Test Commands:[/bold]\n"
|
357
|
-
f"• Test billing access: `aws ce get-cost-and-usage --time-period Start=
|
362
|
+
f"• Test billing access: `aws ce get-cost-and-usage --time-period Start={start_date},End={end_date} --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
|
358
363
|
f"• List available profiles: `aws configure list-profiles`\n"
|
359
364
|
f"• Check current identity: `aws sts get-caller-identity --profile {profile_name or 'your-profile'}`\n\n"
|
360
365
|
f"[bold]💡 Alternative Approach:[/bold]\n"
|
@@ -143,7 +143,7 @@ class NATGatewayOptimizer:
|
|
143
143
|
# NAT Gateway pricing - using dynamic pricing engine
|
144
144
|
# Base monthly cost calculation (will be applied per region)
|
145
145
|
self._base_monthly_cost_us_east_1 = get_service_monthly_cost("nat_gateway", "us-east-1")
|
146
|
-
self.nat_gateway_data_processing_cost =
|
146
|
+
self.nat_gateway_data_processing_cost = get_service_monthly_cost("data_transfer", "us-east-1") # Dynamic data transfer pricing
|
147
147
|
|
148
148
|
# Enterprise thresholds for optimization recommendations
|
149
149
|
self.low_usage_threshold_connections = 10 # Active connections per day
|
@@ -155,9 +155,9 @@ class NATGatewayOptimizer:
|
|
155
155
|
try:
|
156
156
|
return get_service_monthly_cost("nat_gateway", region)
|
157
157
|
except Exception:
|
158
|
-
# Fallback to regional cost calculation
|
158
|
+
# Fallback to regional cost calculation using dynamic pricing
|
159
159
|
from ..common.aws_pricing import calculate_regional_cost
|
160
|
-
return calculate_regional_cost(self._base_monthly_cost_us_east_1, region)
|
160
|
+
return calculate_regional_cost(self._base_monthly_cost_us_east_1, region, "nat_gateway", self.profile_name)
|
161
161
|
|
162
162
|
async def analyze_nat_gateways(self, dry_run: bool = True) -> NATGatewayOptimizerResults:
|
163
163
|
"""
|
@@ -778,39 +778,58 @@ class EnhancedVPCCostOptimizer:
|
|
778
778
|
self.cost_model = self._initialize_dynamic_cost_model()
|
779
779
|
|
780
780
|
def _initialize_dynamic_cost_model(self) -> Dict[str, float]:
|
781
|
-
"""Initialize dynamic cost model using AWS pricing engine."""
|
781
|
+
"""Initialize dynamic cost model using AWS pricing engine with universal compatibility."""
|
782
782
|
try:
|
783
783
|
# Get base pricing for us-east-1, then apply regional multipliers as needed
|
784
784
|
base_region = "us-east-1"
|
785
785
|
|
786
786
|
return {
|
787
|
-
"nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region),
|
788
|
-
"nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region),
|
789
|
-
"transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region),
|
790
|
-
"vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region),
|
791
|
-
"
|
792
|
-
"
|
787
|
+
"nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region, self.profile),
|
788
|
+
"nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region, self.profile),
|
789
|
+
"transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region, self.profile),
|
790
|
+
"vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region, self.profile),
|
791
|
+
"vpc_endpoint_interface_hourly": get_service_monthly_cost("vpc_endpoint_interface", base_region, self.profile) / (24 * 30),
|
792
|
+
"transit_gateway_attachment_hourly": get_service_monthly_cost("transit_gateway_attachment", base_region, self.profile) / (24 * 30),
|
793
|
+
"data_transfer_regional": get_service_monthly_cost("data_transfer", base_region, self.profile),
|
794
|
+
"data_transfer_internet": get_service_monthly_cost("data_transfer", base_region, self.profile) * 4.5, # Internet is ~4.5x higher
|
793
795
|
}
|
794
796
|
except Exception as e:
|
795
797
|
print_warning(f"Dynamic pricing initialization failed: {e}")
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
"nat_gateway_hourly": 0.045,
|
800
|
-
"nat_gateway_data_processing": 0.045, # per GB
|
801
|
-
"transit_gateway_monthly": 36.50,
|
802
|
-
"transit_gateway_attachment_hourly": 0.05,
|
803
|
-
"vpc_endpoint_interface_hourly": 0.01,
|
804
|
-
"data_transfer_regional": 0.01, # per GB within region
|
805
|
-
"data_transfer_cross_region": 0.02, # per GB cross-region
|
806
|
-
"data_transfer_internet": 0.09 # per GB to internet
|
807
|
-
}
|
798
|
+
print_warning("Attempting AWS Pricing API fallback with universal profile support")
|
799
|
+
# Enhanced fallback with AWS Pricing API integration
|
800
|
+
from ..common.aws_pricing import get_aws_pricing_engine, AWSOfficialPricingEngine
|
808
801
|
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
802
|
+
try:
|
803
|
+
# Use AWS Pricing API with profile support for universal compatibility
|
804
|
+
pricing_engine = get_aws_pricing_engine(profile=self.profile, enable_fallback=True)
|
805
|
+
|
806
|
+
# Get actual AWS pricing instead of hardcoded values
|
807
|
+
nat_gateway_pricing = pricing_engine.get_nat_gateway_pricing("us-east-1")
|
808
|
+
transit_gateway_pricing = pricing_engine.get_transit_gateway_pricing("us-east-1")
|
809
|
+
vpc_endpoint_pricing = pricing_engine.get_vpc_endpoint_pricing("us-east-1")
|
810
|
+
data_transfer_pricing = pricing_engine.get_data_transfer_pricing("us-east-1", "internet")
|
811
|
+
|
812
|
+
return {
|
813
|
+
"nat_gateway_monthly": nat_gateway_pricing.monthly_cost,
|
814
|
+
"nat_gateway_data_processing": data_transfer_pricing.cost_per_gb,
|
815
|
+
"transit_gateway_monthly": transit_gateway_pricing.monthly_cost,
|
816
|
+
"transit_gateway_attachment_hourly": transit_gateway_pricing.attachment_hourly_cost,
|
817
|
+
"vpc_endpoint_interface_hourly": vpc_endpoint_pricing.interface_hourly_cost,
|
818
|
+
"data_transfer_regional": data_transfer_pricing.cost_per_gb * 0.1, # Regional is ~10% of internet cost
|
819
|
+
"data_transfer_cross_region": data_transfer_pricing.cost_per_gb * 0.2, # Cross-region is ~20% of internet cost
|
820
|
+
"data_transfer_internet": data_transfer_pricing.cost_per_gb
|
821
|
+
}
|
822
|
+
|
823
|
+
except Exception as pricing_error:
|
824
|
+
print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine pricing without AWS API access: {pricing_error}")
|
825
|
+
print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
|
826
|
+
|
827
|
+
# Return error state instead of hardcoded values to maintain enterprise compliance
|
828
|
+
raise RuntimeError(
|
829
|
+
"Universal compatibility mode requires dynamic AWS pricing API access. "
|
830
|
+
"Please ensure your AWS profile has pricing:GetProducts permissions or configure "
|
831
|
+
"appropriate billing/management profile access."
|
832
|
+
)
|
814
833
|
|
815
834
|
async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
|
816
835
|
regions: Optional[List[str]] = None) -> Dict[str, Any]:
|
@@ -591,6 +591,6 @@ if __name__ == '__main__':
|
|
591
591
|
config=config,
|
592
592
|
optimization_focus=OptimizationCategory.COST_OPTIMIZATION
|
593
593
|
)
|
594
|
-
print(f"Dashboard created with {len(result.export_files)} export files")
|
594
|
+
console.print(f"Dashboard created with {len(result.export_files)} export files")
|
595
595
|
|
596
596
|
asyncio.run(test_dashboard())
|
runbooks/finops/schemas.py
CHANGED
@@ -26,7 +26,7 @@ from typing import Dict, List, Optional, Union, Any, Literal
|
|
26
26
|
from enum import Enum
|
27
27
|
import re
|
28
28
|
|
29
|
-
from pydantic import BaseModel, Field,
|
29
|
+
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
|
30
30
|
from pydantic.types import UUID4, PositiveFloat, NonNegativeFloat
|
31
31
|
|
32
32
|
|
@@ -106,7 +106,8 @@ class CostBreakdown(BaseSchema):
|
|
106
106
|
percentage_of_total: float = Field(..., ge=0, le=100)
|
107
107
|
resource_count: int = Field(..., ge=0)
|
108
108
|
|
109
|
-
@
|
109
|
+
@field_validator('service_name')
|
110
|
+
@classmethod
|
110
111
|
def validate_service_name(cls, v):
|
111
112
|
"""Validate AWS service names."""
|
112
113
|
# Common AWS service patterns
|
@@ -130,14 +131,15 @@ class CostBreakdown(BaseSchema):
|
|
130
131
|
|
131
132
|
return v.strip()
|
132
133
|
|
133
|
-
@
|
134
|
-
|
134
|
+
@field_validator('annual_cost')
|
135
|
+
@classmethod
|
136
|
+
def validate_annual_cost_consistency(cls, v, info):
|
135
137
|
"""Ensure annual cost is approximately 12x monthly cost."""
|
136
|
-
if 'monthly_cost' in
|
137
|
-
expected_annual =
|
138
|
+
if 'monthly_cost' in info.data:
|
139
|
+
expected_annual = info.data['monthly_cost'] * 12
|
138
140
|
# Allow 1% tolerance for rounding differences
|
139
141
|
if abs(v - expected_annual) > (expected_annual * 0.01):
|
140
|
-
raise ValueError(f'Annual cost {v} should be approximately 12x monthly cost {
|
142
|
+
raise ValueError(f'Annual cost {v} should be approximately 12x monthly cost {info.data["monthly_cost"]}')
|
141
143
|
return v
|
142
144
|
|
143
145
|
|
@@ -169,7 +171,8 @@ class OptimizationScenario(BaseSchema):
|
|
169
171
|
validation_timestamp: Optional[datetime] = Field(None)
|
170
172
|
mcp_variance_percent: Optional[float] = Field(None, ge=0, le=100)
|
171
173
|
|
172
|
-
@
|
174
|
+
@field_validator('scenario_name')
|
175
|
+
@classmethod
|
173
176
|
def validate_scenario_name(cls, v):
|
174
177
|
"""Validate scenario naming conventions."""
|
175
178
|
# Ensure professional naming
|
@@ -177,27 +180,30 @@ class OptimizationScenario(BaseSchema):
|
|
177
180
|
raise ValueError('Scenario name must start with capital letter and contain only letters, numbers, spaces, hyphens, and parentheses')
|
178
181
|
return v.strip()
|
179
182
|
|
180
|
-
@
|
181
|
-
|
183
|
+
@field_validator('annual_savings')
|
184
|
+
@classmethod
|
185
|
+
def validate_annual_savings_consistency(cls, v, info):
|
182
186
|
"""Ensure annual savings consistency with monthly savings."""
|
183
|
-
if 'monthly_savings' in
|
184
|
-
expected_annual =
|
187
|
+
if 'monthly_savings' in info.data:
|
188
|
+
expected_annual = info.data['monthly_savings'] * 12
|
185
189
|
if abs(v - expected_annual) > (expected_annual * 0.01): # 1% tolerance
|
186
|
-
raise ValueError(f'Annual savings {v} should be approximately 12x monthly savings {
|
190
|
+
raise ValueError(f'Annual savings {v} should be approximately 12x monthly savings {info.data["monthly_savings"]}')
|
187
191
|
return v
|
188
192
|
|
189
|
-
@
|
190
|
-
|
193
|
+
@field_validator('payback_period_months')
|
194
|
+
@classmethod
|
195
|
+
def calculate_payback_period(cls, v, info):
|
191
196
|
"""Calculate payback period if not provided."""
|
192
|
-
if v is None and 'implementation_cost' in
|
193
|
-
impl_cost =
|
194
|
-
monthly_savings =
|
197
|
+
if v is None and 'implementation_cost' in info.data and 'monthly_savings' in info.data:
|
198
|
+
impl_cost = info.data['implementation_cost']
|
199
|
+
monthly_savings = info.data['monthly_savings']
|
195
200
|
if monthly_savings > 0:
|
196
201
|
calculated_payback = impl_cost / monthly_savings
|
197
202
|
return round(calculated_payback, 1)
|
198
203
|
return v
|
199
204
|
|
200
|
-
@
|
205
|
+
@field_validator('affected_services')
|
206
|
+
@classmethod
|
201
207
|
def validate_aws_services(cls, v):
|
202
208
|
"""Validate AWS service names in affected services."""
|
203
209
|
common_services = {
|
@@ -215,7 +221,8 @@ class OptimizationScenario(BaseSchema):
|
|
215
221
|
|
216
222
|
return v
|
217
223
|
|
218
|
-
@
|
224
|
+
@field_validator('affected_accounts')
|
225
|
+
@classmethod
|
219
226
|
def validate_account_ids(cls, v):
|
220
227
|
"""Validate AWS account ID format."""
|
221
228
|
account_pattern = r'^\d{12}$|^[\w\-\.]{1,50}$' # 12-digit ID or account name
|
@@ -270,47 +277,50 @@ class CostOptimizationResult(BaseSchema):
|
|
270
277
|
default=[ExportFormat.JSON, ExportFormat.CSV, ExportFormat.PDF]
|
271
278
|
)
|
272
279
|
|
273
|
-
@
|
274
|
-
|
280
|
+
@field_validator('total_potential_annual_savings')
|
281
|
+
@classmethod
|
282
|
+
def validate_annual_consistency(cls, v, info):
|
275
283
|
"""Validate annual savings consistency."""
|
276
|
-
if 'total_potential_monthly_savings' in
|
277
|
-
expected =
|
284
|
+
if 'total_potential_monthly_savings' in info.data:
|
285
|
+
expected = info.data['total_potential_monthly_savings'] * 12
|
278
286
|
if abs(v - expected) > (expected * 0.01):
|
279
287
|
raise ValueError('Annual savings must be approximately 12x monthly savings')
|
280
288
|
return v
|
281
289
|
|
282
|
-
@
|
283
|
-
|
290
|
+
@field_validator('savings_percentage')
|
291
|
+
@classmethod
|
292
|
+
def calculate_savings_percentage(cls, v, info):
|
284
293
|
"""Validate or calculate savings percentage."""
|
285
|
-
if 'current_monthly_spend' in
|
286
|
-
current_spend =
|
294
|
+
if 'current_monthly_spend' in info.data and 'total_potential_monthly_savings' in info.data:
|
295
|
+
current_spend = info.data['current_monthly_spend']
|
287
296
|
if current_spend > 0:
|
288
|
-
calculated = (
|
297
|
+
calculated = (info.data['total_potential_monthly_savings'] / current_spend) * 100
|
289
298
|
if abs(v - calculated) > 0.1: # 0.1% tolerance
|
290
299
|
raise ValueError(f'Savings percentage {v}% inconsistent with calculated {calculated:.1f}%')
|
291
300
|
return v
|
292
301
|
|
293
|
-
@
|
294
|
-
|
302
|
+
@field_validator('total_scenarios')
|
303
|
+
@classmethod
|
304
|
+
def validate_scenario_count(cls, v, info):
|
295
305
|
"""Ensure scenario count matches actual scenarios."""
|
296
|
-
if 'optimization_scenarios' in
|
297
|
-
actual_count = len(
|
306
|
+
if 'optimization_scenarios' in info.data:
|
307
|
+
actual_count = len(info.data['optimization_scenarios'])
|
298
308
|
if v != actual_count:
|
299
309
|
raise ValueError(f'Total scenarios {v} does not match actual scenarios count {actual_count}')
|
300
310
|
return v
|
301
311
|
|
302
|
-
@
|
303
|
-
def validate_complexity_distribution(
|
312
|
+
@model_validator(mode='after')
|
313
|
+
def validate_complexity_distribution(self):
|
304
314
|
"""Validate complexity scenario counts."""
|
305
|
-
scenarios =
|
315
|
+
scenarios = self.optimization_scenarios or []
|
306
316
|
if scenarios:
|
307
317
|
low_count = sum(1 for s in scenarios if s.complexity == ComplexityLevel.LOW)
|
308
318
|
medium_count = sum(1 for s in scenarios if s.complexity == ComplexityLevel.MEDIUM)
|
309
319
|
high_count = sum(1 for s in scenarios if s.complexity == ComplexityLevel.HIGH)
|
310
320
|
|
311
|
-
expected_low =
|
312
|
-
expected_medium =
|
313
|
-
expected_high =
|
321
|
+
expected_low = self.low_complexity_scenarios or 0
|
322
|
+
expected_medium = self.medium_complexity_scenarios or 0
|
323
|
+
expected_high = self.high_complexity_scenarios or 0
|
314
324
|
|
315
325
|
if (low_count != expected_low or
|
316
326
|
medium_count != expected_medium or
|
@@ -320,7 +330,7 @@ class CostOptimizationResult(BaseSchema):
|
|
320
330
|
f'actual L:{low_count} M:{medium_count} H:{high_count}'
|
321
331
|
)
|
322
332
|
|
323
|
-
return
|
333
|
+
return self
|
324
334
|
|
325
335
|
|
326
336
|
# Business Interface Schemas
|
@@ -350,7 +360,8 @@ class ExecutiveSummary(BaseSchema):
|
|
350
360
|
data_validation_status: ValidationStatus = Field(...)
|
351
361
|
last_validated: datetime = Field(...)
|
352
362
|
|
353
|
-
@
|
363
|
+
@field_validator('roi_percentage')
|
364
|
+
@classmethod
|
354
365
|
def validate_reasonable_roi(cls, v):
|
355
366
|
"""Ensure ROI is reasonable for executive presentation."""
|
356
367
|
if v > 1000: # 1000% ROI
|
@@ -377,12 +388,13 @@ class MCPValidationResult(BaseSchema):
|
|
377
388
|
mcp_source: str = Field(..., min_length=1)
|
378
389
|
response_time_seconds: Optional[PositiveFloat] = Field(None, le=300) # 5 minute timeout
|
379
390
|
|
380
|
-
@
|
381
|
-
|
391
|
+
@field_validator('variance_percent')
|
392
|
+
@classmethod
|
393
|
+
def calculate_variance_percent(cls, v, info):
|
382
394
|
"""Calculate and validate variance percentage."""
|
383
|
-
if 'notebook_value' in
|
384
|
-
notebook_val =
|
385
|
-
mcp_val =
|
395
|
+
if 'notebook_value' in info.data and 'mcp_value' in info.data:
|
396
|
+
notebook_val = info.data['notebook_value']
|
397
|
+
mcp_val = info.data['mcp_value']
|
386
398
|
|
387
399
|
if notebook_val > 0:
|
388
400
|
calculated = abs((notebook_val - mcp_val) / notebook_val) * 100
|
@@ -441,22 +453,24 @@ class ComprehensiveTestSuite(BaseSchema):
|
|
441
453
|
meets_production_criteria: bool = Field(...)
|
442
454
|
quality_score: float = Field(..., ge=0, le=100)
|
443
455
|
|
444
|
-
@
|
445
|
-
|
456
|
+
@field_validator('passed_tests')
|
457
|
+
@classmethod
|
458
|
+
def validate_test_counts(cls, v, info):
|
446
459
|
"""Ensure test counts are consistent."""
|
447
|
-
if 'failed_tests' in
|
448
|
-
calculated_total = v +
|
449
|
-
if calculated_total !=
|
450
|
-
raise ValueError(f'Test counts inconsistent: {calculated_total} ≠ {
|
460
|
+
if 'failed_tests' in info.data and 'skipped_tests' in info.data and 'total_tests' in info.data:
|
461
|
+
calculated_total = v + info.data['failed_tests'] + info.data['skipped_tests']
|
462
|
+
if calculated_total != info.data['total_tests']:
|
463
|
+
raise ValueError(f'Test counts inconsistent: {calculated_total} ≠ {info.data["total_tests"]}')
|
451
464
|
return v
|
452
465
|
|
453
|
-
@
|
454
|
-
|
466
|
+
@field_validator('pass_rate_percent')
|
467
|
+
@classmethod
|
468
|
+
def calculate_pass_rate(cls, v, info):
|
455
469
|
"""Calculate and validate pass rate."""
|
456
|
-
if 'passed_tests' in
|
457
|
-
total =
|
470
|
+
if 'passed_tests' in info.data and 'total_tests' in info.data:
|
471
|
+
total = info.data['total_tests']
|
458
472
|
if total > 0:
|
459
|
-
calculated = (
|
473
|
+
calculated = (info.data['passed_tests'] / total) * 100
|
460
474
|
if abs(v - calculated) > 0.01:
|
461
475
|
raise ValueError(f'Pass rate {v}% inconsistent with calculated {calculated:.2f}%')
|
462
476
|
return v
|
@@ -480,7 +494,8 @@ class ExportMetadata(BaseSchema):
|
|
480
494
|
export_validated: bool = Field(...)
|
481
495
|
validation_errors: List[str] = Field(default_factory=list)
|
482
496
|
|
483
|
-
@
|
497
|
+
@field_validator('file_path')
|
498
|
+
@classmethod
|
484
499
|
def validate_file_path(cls, v):
|
485
500
|
"""Validate file path format."""
|
486
501
|
# Basic path validation
|
@@ -346,18 +346,34 @@ class SingleAccountDashboard:
|
|
346
346
|
# Integrate quarterly data into trend data structure
|
347
347
|
corrected_trend_data["quarterly_costs_by_service"] = quarterly_costs
|
348
348
|
|
349
|
-
#
|
349
|
+
# Enhanced trend analysis context with MCP validation awareness
|
350
350
|
if "period_metadata" in corrected_trend_data:
|
351
351
|
metadata = corrected_trend_data["period_metadata"]
|
352
352
|
current_days = metadata.get("current_days", 0)
|
353
353
|
previous_days = metadata.get("previous_days", 0)
|
354
|
+
days_difference = metadata.get("days_difference", abs(current_days - previous_days))
|
354
355
|
reliability = metadata.get("trend_reliability", "unknown")
|
356
|
+
alignment_strategy = metadata.get("period_alignment_strategy", "standard")
|
355
357
|
|
358
|
+
# ENHANCED LOGIC: Reduce warnings when using intelligent period alignment
|
356
359
|
if metadata.get("is_partial_comparison", False):
|
357
|
-
|
358
|
-
|
360
|
+
if alignment_strategy == "equal_days":
|
361
|
+
# Equal-day comparison reduces the severity of partial period concerns
|
362
|
+
print_info(f"🔄 Enhanced period alignment: {current_days} vs {previous_days} days (equal-day strategy)")
|
363
|
+
if reliability in ["high", "medium_with_validation_support"]:
|
364
|
+
print_success(f"✅ Trend reliability: {reliability} (enhanced alignment)")
|
365
|
+
else:
|
366
|
+
print_info(f"Trend reliability: {reliability}")
|
367
|
+
else:
|
368
|
+
# Standard partial period warning for traditional comparisons
|
369
|
+
print_warning(f"⚠️ Partial period comparison: {current_days} vs {previous_days} days")
|
370
|
+
print_info(f"Trend reliability: {reliability}")
|
371
|
+
|
372
|
+
# Add context for very small differences
|
373
|
+
if days_difference <= 5:
|
374
|
+
print_info(f"💡 Small period difference ({days_difference} days) - trends should be reliable")
|
359
375
|
else:
|
360
|
-
print_success(f"Equal period comparison: {current_days} vs {previous_days} days")
|
376
|
+
print_success(f"✅ Equal period comparison: {current_days} vs {previous_days} days")
|
361
377
|
|
362
378
|
return corrected_trend_data
|
363
379
|
|
@@ -34,6 +34,8 @@ try:
|
|
34
34
|
except ImportError:
|
35
35
|
# Define mock_costexplorer as a no-op decorator for compatibility
|
36
36
|
def mock_costexplorer(func):
|
37
|
+
# Dynamic test period for consistent test data
|
38
|
+
test_period = get_test_date_period(30)
|
37
39
|
def wrapper(*args, **kwargs):
|
38
40
|
return func(*args, **kwargs)
|
39
41
|
|
@@ -128,7 +130,7 @@ class TestAWSIntegrationWithMoto:
|
|
128
130
|
mock_cost_data = {
|
129
131
|
"ResultsByTime": [
|
130
132
|
{
|
131
|
-
"TimePeriod": {"Start": "
|
133
|
+
"TimePeriod": {"Start": test_period["Start"], "End": test_period["End"]},
|
132
134
|
"Total": {"UnblendedCost": {"Amount": "50000.00", "Unit": "USD"}},
|
133
135
|
"Groups": [
|
134
136
|
{"Keys": ["EC2-Instance"], "Metrics": {"UnblendedCost": {"Amount": "20000.00", "Unit": "USD"}}},
|
@@ -16,6 +16,7 @@ from datetime import datetime
|
|
16
16
|
from typing import Any, Dict, List
|
17
17
|
|
18
18
|
from .markdown_exporter import MarkdownExporter
|
19
|
+
from runbooks.common.rich_utils import console
|
19
20
|
|
20
21
|
|
21
22
|
def _format_tags_for_display(tags_dict: Dict[str, str]) -> str:
|
@@ -76,7 +77,7 @@ def export_vpc_cleanup_results(vpc_result: Any, export_formats: List[str], outpu
|
|
76
77
|
)
|
77
78
|
results['markdown'] = markdown_filename
|
78
79
|
except Exception as e:
|
79
|
-
print(f"Warning: Markdown export failed: {e}")
|
80
|
+
console.print(f"[yellow]Warning: Markdown export failed: {e}[/yellow]")
|
80
81
|
results['markdown'] = None
|
81
82
|
|
82
83
|
# Real implementations for other formats
|