runbooks 0.9.2__py3-none-any.whl → 0.9.5__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 +15 -6
- runbooks/cfat/__init__.py +3 -1
- runbooks/cloudops/__init__.py +3 -1
- runbooks/common/aws_utils.py +367 -0
- runbooks/common/enhanced_logging_example.py +239 -0
- runbooks/common/enhanced_logging_integration_example.py +257 -0
- runbooks/common/logging_integration_helper.py +344 -0
- runbooks/common/profile_utils.py +8 -6
- runbooks/common/rich_utils.py +347 -3
- runbooks/enterprise/logging.py +400 -38
- runbooks/finops/README.md +262 -406
- runbooks/finops/__init__.py +44 -1
- runbooks/finops/accuracy_cross_validator.py +12 -3
- runbooks/finops/business_cases.py +552 -0
- runbooks/finops/commvault_ec2_analysis.py +415 -0
- runbooks/finops/cost_processor.py +718 -42
- runbooks/finops/dashboard_router.py +44 -22
- runbooks/finops/dashboard_runner.py +302 -39
- runbooks/finops/embedded_mcp_validator.py +358 -48
- runbooks/finops/finops_scenarios.py +1122 -0
- runbooks/finops/helpers.py +182 -0
- runbooks/finops/multi_dashboard.py +30 -15
- runbooks/finops/scenarios.py +789 -0
- runbooks/finops/single_dashboard.py +386 -58
- runbooks/finops/types.py +29 -4
- runbooks/inventory/__init__.py +2 -1
- runbooks/main.py +522 -29
- runbooks/operate/__init__.py +3 -1
- runbooks/remediation/__init__.py +3 -1
- runbooks/remediation/commons.py +55 -16
- runbooks/remediation/commvault_ec2_analysis.py +259 -0
- runbooks/remediation/rds_snapshot_list.py +267 -102
- runbooks/remediation/workspaces_list.py +182 -31
- runbooks/security/__init__.py +3 -1
- runbooks/sre/__init__.py +2 -1
- runbooks/utils/__init__.py +81 -6
- runbooks/utils/version_validator.py +241 -0
- runbooks/vpc/__init__.py +2 -1
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/METADATA +98 -60
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/RECORD +44 -39
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/entry_points.txt +1 -0
- runbooks/inventory/cloudtrail.md +0 -727
- runbooks/inventory/discovery.md +0 -81
- runbooks/remediation/CLAUDE.md +0 -100
- runbooks/remediation/DOME9.md +0 -218
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +0 -506
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/WHEEL +0 -0
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,7 @@ Implementation: Embedded validation eliminates external MCP server requirements
|
|
11
11
|
|
12
12
|
import asyncio
|
13
13
|
import time
|
14
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
14
15
|
from datetime import datetime, timedelta
|
15
16
|
from typing import Any, Dict, List, Optional, Tuple
|
16
17
|
|
@@ -36,6 +37,12 @@ class EmbeddedMCPValidator:
|
|
36
37
|
|
37
38
|
Provides real-time cost validation without external MCP server dependencies.
|
38
39
|
Ensures >=99.5% accuracy for enterprise financial compliance.
|
40
|
+
|
41
|
+
Enhanced Features:
|
42
|
+
- Organization-level total validation
|
43
|
+
- Service-level cost breakdown validation
|
44
|
+
- Real-time variance detection with ±5% tolerance
|
45
|
+
- Visual indicators for validation status
|
39
46
|
"""
|
40
47
|
|
41
48
|
def __init__(self, profiles: List[str], console: Optional[Console] = None):
|
@@ -45,6 +52,8 @@ class EmbeddedMCPValidator:
|
|
45
52
|
self.aws_sessions = {}
|
46
53
|
self.validation_threshold = 99.5 # Enterprise accuracy requirement
|
47
54
|
self.tolerance_percent = 5.0 # ±5% tolerance for validation
|
55
|
+
self.validation_cache = {} # Cache for performance optimization
|
56
|
+
self.cache_ttl = 300 # 5 minutes cache TTL
|
48
57
|
|
49
58
|
# Initialize AWS sessions for each profile
|
50
59
|
self._initialize_aws_sessions()
|
@@ -80,52 +89,87 @@ class EmbeddedMCPValidator:
|
|
80
89
|
"validation_method": "embedded_mcp_direct_aws_api",
|
81
90
|
}
|
82
91
|
|
92
|
+
# Enhanced parallel processing for <20s performance target
|
93
|
+
self.console.log(f"[blue]⚡ Starting parallel MCP validation with {min(5, len(self.aws_sessions))} workers[/]")
|
94
|
+
|
83
95
|
with Progress(
|
84
96
|
SpinnerColumn(),
|
85
97
|
TextColumn("[progress.description]{task.description}"),
|
86
98
|
BarColumn(),
|
87
|
-
TaskProgressColumn(),
|
99
|
+
TaskProgressColumn(),
|
88
100
|
TimeElapsedColumn(),
|
89
101
|
console=self.console,
|
90
102
|
) as progress:
|
91
|
-
task = progress.add_task("
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
103
|
+
task = progress.add_task("Parallel MCP validation (enhanced performance)...", total=len(self.aws_sessions))
|
104
|
+
|
105
|
+
# Parallel execution with ThreadPoolExecutor for <20s target
|
106
|
+
with ThreadPoolExecutor(max_workers=min(5, len(self.aws_sessions))) as executor:
|
107
|
+
# Submit all validation tasks
|
108
|
+
future_to_profile = {}
|
109
|
+
for profile, session in self.aws_sessions.items():
|
110
|
+
future = executor.submit(self._validate_profile_sync, profile, session, runbooks_data)
|
111
|
+
future_to_profile[future] = profile
|
112
|
+
|
113
|
+
# Collect results as they complete (maintain progress visibility)
|
114
|
+
for future in as_completed(future_to_profile):
|
115
|
+
profile = future_to_profile[future]
|
116
|
+
try:
|
117
|
+
accuracy_result = future.result()
|
118
|
+
if accuracy_result: # Only append successful results
|
119
|
+
validation_results["profile_results"].append(accuracy_result)
|
120
|
+
progress.advance(task)
|
121
|
+
except Exception as e:
|
122
|
+
print_warning(f"Parallel validation failed for {profile[:20]}...: {str(e)[:40]}")
|
123
|
+
progress.advance(task)
|
97
124
|
|
98
|
-
|
99
|
-
|
125
|
+
# Calculate overall validation metrics
|
126
|
+
self._finalize_validation_results(validation_results)
|
127
|
+
return validation_results
|
100
128
|
|
101
|
-
|
102
|
-
|
103
|
-
|
129
|
+
def _validate_profile_sync(self, profile: str, session: boto3.Session, runbooks_data: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
130
|
+
"""Synchronous wrapper for profile validation (for parallel execution)."""
|
131
|
+
try:
|
132
|
+
# Get independent cost data from AWS API
|
133
|
+
aws_cost_data = asyncio.run(self._get_independent_cost_data(session, profile))
|
104
134
|
|
105
|
-
|
135
|
+
# Find corresponding runbooks data
|
136
|
+
runbooks_cost_data = self._extract_runbooks_cost_data(runbooks_data, profile)
|
106
137
|
|
107
|
-
|
108
|
-
|
109
|
-
|
138
|
+
# Calculate accuracy
|
139
|
+
accuracy_result = self._calculate_accuracy(runbooks_cost_data, aws_cost_data, profile)
|
140
|
+
return accuracy_result
|
110
141
|
|
111
|
-
|
112
|
-
|
113
|
-
|
142
|
+
except Exception as e:
|
143
|
+
# Return None for failed validations (handled in calling function)
|
144
|
+
return None
|
114
145
|
|
115
|
-
async def _get_independent_cost_data(self, session: boto3.Session, profile: str) -> Dict[str, Any]:
|
116
|
-
"""Get independent cost data
|
146
|
+
async def _get_independent_cost_data(self, session: boto3.Session, profile: str, start_date_override: Optional[str] = None, end_date_override: Optional[str] = None) -> Dict[str, Any]:
|
147
|
+
"""Get independent cost data with precise time window alignment to runbooks."""
|
117
148
|
try:
|
118
149
|
ce_client = session.client("ce", region_name="us-east-1")
|
119
150
|
|
120
|
-
#
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
151
|
+
# CRITICAL FIX: Use exact same time calculation as cost_processor.py
|
152
|
+
if start_date_override and end_date_override:
|
153
|
+
# Use exact time window from calling function (perfect alignment)
|
154
|
+
start_date = start_date_override
|
155
|
+
end_date = end_date_override
|
156
|
+
self.console.log(f"[cyan]🔍 MCP Time Window: {start_date} to {end_date} (aligned with runbooks)[/]")
|
157
|
+
else:
|
158
|
+
# EXACT MATCH: Import and use same logic as cost_processor.py get_cost_data()
|
159
|
+
from datetime import date, timedelta
|
160
|
+
today = date.today()
|
161
|
+
|
162
|
+
# Use EXACT same logic as cost_processor.py lines 554-567
|
163
|
+
start_date = today.replace(day=1).isoformat() # First day of current month
|
164
|
+
end_date = (today + timedelta(days=1)).isoformat() # AWS CE end date is exclusive (today + 1)
|
165
|
+
|
166
|
+
self.console.log(f"[cyan]📅 MCP Synchronized: {start_date} to {end_date} (matching cost_processor.py)[/]")
|
167
|
+
|
168
|
+
# Get cost and usage data (matching runbooks parameters exactly)
|
125
169
|
response = ce_client.get_cost_and_usage(
|
126
|
-
TimePeriod={"Start": start_date
|
170
|
+
TimePeriod={"Start": start_date, "End": end_date},
|
127
171
|
Granularity="MONTHLY",
|
128
|
-
Metrics=["
|
172
|
+
Metrics=["UnblendedCost"], # Match CLI using UnblendedCost not BlendedCost
|
129
173
|
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
|
130
174
|
)
|
131
175
|
|
@@ -137,7 +181,7 @@ class EmbeddedMCPValidator:
|
|
137
181
|
for result in response["ResultsByTime"]:
|
138
182
|
for group in result.get("Groups", []):
|
139
183
|
service = group.get("Keys", ["Unknown"])[0]
|
140
|
-
cost = float(group.get("Metrics", {}).get("
|
184
|
+
cost = float(group.get("Metrics", {}).get("UnblendedCost", {}).get("Amount", 0))
|
141
185
|
services_cost[service] = cost
|
142
186
|
total_cost += cost
|
143
187
|
|
@@ -159,29 +203,75 @@ class EmbeddedMCPValidator:
|
|
159
203
|
}
|
160
204
|
|
161
205
|
def _extract_runbooks_cost_data(self, runbooks_data: Dict[str, Any], profile: str) -> Dict[str, Any]:
|
162
|
-
"""
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
206
|
+
"""
|
207
|
+
Extract cost data from runbooks results for comparison.
|
208
|
+
|
209
|
+
CRITICAL FIX: Handle the actual data structure from runbooks dashboard.
|
210
|
+
Data format: {profile_name: {total_cost: float, services: dict}}
|
211
|
+
"""
|
212
|
+
try:
|
213
|
+
# Handle nested profile structure from single_dashboard.py
|
214
|
+
if profile in runbooks_data:
|
215
|
+
profile_data = runbooks_data[profile]
|
216
|
+
total_cost = profile_data.get("total_cost", 0.0)
|
217
|
+
services = profile_data.get("services", {})
|
218
|
+
else:
|
219
|
+
# Fallback: Look for direct keys (legacy format)
|
220
|
+
total_cost = runbooks_data.get("total_cost", 0.0)
|
221
|
+
services = runbooks_data.get("services", {})
|
222
|
+
|
223
|
+
# Apply same NON_ANALYTICAL_SERVICES filtering as cost_processor.py
|
224
|
+
from .cost_processor import filter_analytical_services
|
225
|
+
filtered_services = filter_analytical_services(services)
|
226
|
+
|
227
|
+
return {
|
228
|
+
"profile": profile,
|
229
|
+
"total_cost": float(total_cost),
|
230
|
+
"services": filtered_services,
|
231
|
+
"data_source": "runbooks_finops_analysis",
|
232
|
+
"extraction_method": "profile_nested" if profile in runbooks_data else "direct_keys"
|
233
|
+
}
|
234
|
+
except Exception as e:
|
235
|
+
self.console.log(f"[yellow]Warning: Error extracting runbooks data for {profile}: {str(e)}[/]")
|
236
|
+
return {
|
237
|
+
"profile": profile,
|
238
|
+
"total_cost": 0.0,
|
239
|
+
"services": {},
|
240
|
+
"data_source": "runbooks_finops_analysis_error",
|
241
|
+
"error": str(e)
|
242
|
+
}
|
171
243
|
|
172
244
|
def _calculate_accuracy(self, runbooks_data: Dict, aws_data: Dict, profile: str) -> Dict[str, Any]:
|
173
|
-
"""
|
245
|
+
"""
|
246
|
+
Calculate accuracy between runbooks and AWS API data.
|
247
|
+
|
248
|
+
CRITICAL FIX: Handle zero values correctly and improve accuracy calculation.
|
249
|
+
"""
|
174
250
|
try:
|
175
251
|
runbooks_cost = float(runbooks_data.get("total_cost", 0))
|
176
252
|
aws_cost = float(aws_data.get("total_cost", 0))
|
177
253
|
|
178
|
-
|
179
|
-
|
254
|
+
# CRITICAL FIX: Improved accuracy calculation for enterprise standards
|
255
|
+
if runbooks_cost == 0 and aws_cost == 0:
|
256
|
+
# Both zero - perfect accuracy
|
257
|
+
accuracy_percent = 100.0
|
258
|
+
elif runbooks_cost == 0 and aws_cost > 0:
|
259
|
+
# Runbooks missing cost data - major accuracy issue
|
260
|
+
accuracy_percent = 0.0
|
261
|
+
self.console.log(f"[red]⚠️ Profile {profile}: Runbooks shows $0.00 but MCP shows ${aws_cost:.2f}[/]")
|
262
|
+
elif aws_cost == 0 and runbooks_cost > 0:
|
263
|
+
# MCP missing data - moderate accuracy issue
|
264
|
+
accuracy_percent = 50.0 # Give partial credit as MCP may have different data access
|
265
|
+
self.console.log(f"[yellow]⚠️ Profile {profile}: MCP shows $0.00 but Runbooks shows ${runbooks_cost:.2f}[/]")
|
180
266
|
else:
|
181
|
-
|
267
|
+
# Both have values - calculate variance-based accuracy
|
268
|
+
max_cost = max(runbooks_cost, aws_cost)
|
269
|
+
variance_percent = abs(runbooks_cost - aws_cost) / max_cost * 100
|
270
|
+
accuracy_percent = max(0.0, 100.0 - variance_percent)
|
182
271
|
|
183
|
-
# Determine validation status
|
272
|
+
# Determine validation status with enhanced thresholds
|
184
273
|
passed = accuracy_percent >= self.validation_threshold
|
274
|
+
tolerance_met = abs(runbooks_cost - aws_cost) / max(max(runbooks_cost, aws_cost), 0.01) * 100 <= self.tolerance_percent
|
185
275
|
|
186
276
|
return {
|
187
277
|
"profile": profile,
|
@@ -189,9 +279,11 @@ class EmbeddedMCPValidator:
|
|
189
279
|
"aws_api_cost": aws_cost,
|
190
280
|
"accuracy_percent": accuracy_percent,
|
191
281
|
"passed_validation": passed,
|
192
|
-
"tolerance_met":
|
282
|
+
"tolerance_met": tolerance_met,
|
193
283
|
"cost_difference": abs(runbooks_cost - aws_cost),
|
284
|
+
"variance_percent": abs(runbooks_cost - aws_cost) / max(max(runbooks_cost, aws_cost), 0.01) * 100,
|
194
285
|
"validation_status": "PASSED" if passed else "FAILED",
|
286
|
+
"accuracy_category": self._categorize_accuracy(accuracy_percent),
|
195
287
|
}
|
196
288
|
|
197
289
|
except Exception as e:
|
@@ -203,6 +295,19 @@ class EmbeddedMCPValidator:
|
|
203
295
|
"validation_status": "ERROR",
|
204
296
|
}
|
205
297
|
|
298
|
+
def _categorize_accuracy(self, accuracy_percent: float) -> str:
|
299
|
+
"""Categorize accuracy level for reporting."""
|
300
|
+
if accuracy_percent >= 99.5:
|
301
|
+
return "EXCELLENT"
|
302
|
+
elif accuracy_percent >= 95.0:
|
303
|
+
return "GOOD"
|
304
|
+
elif accuracy_percent >= 90.0:
|
305
|
+
return "ACCEPTABLE"
|
306
|
+
elif accuracy_percent >= 50.0:
|
307
|
+
return "NEEDS_IMPROVEMENT"
|
308
|
+
else:
|
309
|
+
return "CRITICAL_ISSUE"
|
310
|
+
|
206
311
|
def _finalize_validation_results(self, validation_results: Dict[str, Any]) -> None:
|
207
312
|
"""Calculate overall validation metrics and status."""
|
208
313
|
profile_results = validation_results["profile_results"]
|
@@ -230,23 +335,31 @@ class EmbeddedMCPValidator:
|
|
230
335
|
|
231
336
|
self.console.print(f"\n[bright_cyan]🔍 Embedded MCP Validation Results[/]")
|
232
337
|
|
233
|
-
# Display per-profile results
|
338
|
+
# Display per-profile results with enhanced detail
|
234
339
|
for profile_result in results.get("profile_results", []):
|
235
340
|
accuracy = profile_result.get("accuracy_percent", 0)
|
236
341
|
status = profile_result.get("validation_status", "UNKNOWN")
|
237
342
|
profile = profile_result.get("profile", "Unknown")
|
343
|
+
runbooks_cost = profile_result.get("runbooks_cost", 0)
|
344
|
+
aws_cost = profile_result.get("aws_api_cost", 0)
|
345
|
+
cost_diff = profile_result.get("cost_difference", 0)
|
346
|
+
category = profile_result.get("accuracy_category", "UNKNOWN")
|
238
347
|
|
239
|
-
if status == "PASSED":
|
348
|
+
if status == "PASSED" and accuracy >= 99.5:
|
240
349
|
icon = "✅"
|
241
350
|
color = "green"
|
242
|
-
elif status == "
|
351
|
+
elif status == "PASSED" and accuracy >= 95.0:
|
352
|
+
icon = "✅"
|
353
|
+
color = "bright_green"
|
354
|
+
elif accuracy >= 50.0:
|
243
355
|
icon = "⚠️"
|
244
356
|
color = "yellow"
|
245
357
|
else:
|
246
358
|
icon = "❌"
|
247
359
|
color = "red"
|
248
360
|
|
249
|
-
self.console.print(f"[dim] {profile[:30]}: {icon} [{color}]{accuracy:.1f}% accuracy[/]
|
361
|
+
self.console.print(f"[dim] {profile[:30]}: {icon} [{color}]{accuracy:.1f}% accuracy[/] "
|
362
|
+
f"[dim](Runbooks: ${runbooks_cost:.2f}, MCP: ${aws_cost:.2f}, Δ: ${cost_diff:.2f})[/][/dim]")
|
250
363
|
|
251
364
|
# Overall summary
|
252
365
|
if passed:
|
@@ -265,6 +378,203 @@ class EmbeddedMCPValidator:
|
|
265
378
|
asyncio.set_event_loop(loop)
|
266
379
|
|
267
380
|
return loop.run_until_complete(self.validate_cost_data_async(runbooks_data))
|
381
|
+
|
382
|
+
def validate_organization_total(self, runbooks_total: float, profiles: Optional[List[str]] = None) -> Dict[str, Any]:
|
383
|
+
"""
|
384
|
+
Cross-validate organization total with MCP calculation using parallel processing.
|
385
|
+
|
386
|
+
Args:
|
387
|
+
runbooks_total: Total cost calculated by runbooks (e.g., $7,254.46)
|
388
|
+
profiles: List of profiles to validate (uses self.profiles if None)
|
389
|
+
|
390
|
+
Returns:
|
391
|
+
Validation result with variance analysis
|
392
|
+
"""
|
393
|
+
profiles = profiles or self.profiles
|
394
|
+
cache_key = f"org_total_{','.join(sorted(profiles))}"
|
395
|
+
|
396
|
+
# Check cache first
|
397
|
+
if cache_key in self.validation_cache:
|
398
|
+
cached_time, cached_result = self.validation_cache[cache_key]
|
399
|
+
if time.time() - cached_time < self.cache_ttl:
|
400
|
+
self.console.print("[dim]Using cached MCP validation result[/dim]")
|
401
|
+
return cached_result
|
402
|
+
|
403
|
+
# Calculate MCP total from AWS API using parallel processing
|
404
|
+
mcp_total = 0.0
|
405
|
+
validated_profiles = 0
|
406
|
+
|
407
|
+
def fetch_profile_cost(profile: str) -> Tuple[str, float, bool]:
|
408
|
+
"""Fetch cost for a single profile."""
|
409
|
+
if profile not in self.aws_sessions:
|
410
|
+
return profile, 0.0, False
|
411
|
+
|
412
|
+
try:
|
413
|
+
session = self.aws_sessions[profile]
|
414
|
+
# Rate limiting for AWS API (max 5 calls per second)
|
415
|
+
time.sleep(0.2)
|
416
|
+
|
417
|
+
# CRITICAL FIX: Use same time calculation as runbooks for organization totals
|
418
|
+
from datetime import date, timedelta
|
419
|
+
today = date.today()
|
420
|
+
start_date = today.replace(day=1).isoformat()
|
421
|
+
end_date = (today + timedelta(days=1)).isoformat()
|
422
|
+
|
423
|
+
cost_data = asyncio.run(self._get_independent_cost_data(session, profile, start_date, end_date))
|
424
|
+
return profile, cost_data.get("total_cost", 0), True
|
425
|
+
except Exception as e:
|
426
|
+
print_warning(f"Skipping profile {profile[:20]}... in org validation: {str(e)[:30]}")
|
427
|
+
return profile, 0.0, False
|
428
|
+
|
429
|
+
with Progress(
|
430
|
+
SpinnerColumn(),
|
431
|
+
TextColumn("[progress.description]{task.description}"),
|
432
|
+
BarColumn(),
|
433
|
+
TaskProgressColumn(),
|
434
|
+
console=self.console,
|
435
|
+
transient=True
|
436
|
+
) as progress:
|
437
|
+
task = progress.add_task("Validating organization total (parallel)...", total=len(profiles))
|
438
|
+
|
439
|
+
# Use ThreadPoolExecutor for parallel validation (max 5 workers for AWS API rate limits)
|
440
|
+
max_workers = min(5, len(profiles))
|
441
|
+
|
442
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
443
|
+
# Submit all profile validations
|
444
|
+
future_to_profile = {
|
445
|
+
executor.submit(fetch_profile_cost, profile): profile
|
446
|
+
for profile in profiles
|
447
|
+
}
|
448
|
+
|
449
|
+
# Process completed validations
|
450
|
+
for future in as_completed(future_to_profile):
|
451
|
+
profile_name, cost, success = future.result()
|
452
|
+
if success:
|
453
|
+
mcp_total += cost
|
454
|
+
validated_profiles += 1
|
455
|
+
progress.advance(task)
|
456
|
+
|
457
|
+
# Calculate variance
|
458
|
+
variance = 0.0
|
459
|
+
if runbooks_total > 0:
|
460
|
+
variance = abs(runbooks_total - mcp_total) / runbooks_total * 100
|
461
|
+
|
462
|
+
passed = variance <= self.tolerance_percent
|
463
|
+
|
464
|
+
result = {
|
465
|
+
'runbooks_total': runbooks_total,
|
466
|
+
'mcp_total': mcp_total,
|
467
|
+
'variance_percent': variance,
|
468
|
+
'passed': passed,
|
469
|
+
'tolerance_percent': self.tolerance_percent,
|
470
|
+
'profiles_validated': validated_profiles,
|
471
|
+
'total_profiles': len(profiles),
|
472
|
+
'validation_status': 'PASSED' if passed else 'VARIANCE_DETECTED',
|
473
|
+
'action_required': None if passed else f'Investigate {variance:.2f}% variance',
|
474
|
+
'timestamp': datetime.now().isoformat()
|
475
|
+
}
|
476
|
+
|
477
|
+
# Cache the result
|
478
|
+
self.validation_cache[cache_key] = (time.time(), result)
|
479
|
+
|
480
|
+
# Display validation result
|
481
|
+
self._display_organization_validation(result)
|
482
|
+
|
483
|
+
return result
|
484
|
+
|
485
|
+
def validate_service_costs(self, service_breakdown: Dict[str, float], profile: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None) -> Dict[str, Any]:
|
486
|
+
"""
|
487
|
+
Cross-validate individual service costs with time window alignment.
|
488
|
+
|
489
|
+
Args:
|
490
|
+
service_breakdown: Dictionary of service names to costs (e.g., {'WorkSpaces': 3869.91})
|
491
|
+
profile: Profile to use for validation (uses first available if None)
|
492
|
+
start_date: Start date for validation period (ISO format, matches runbooks)
|
493
|
+
end_date: End date for validation period (ISO format, matches runbooks)
|
494
|
+
|
495
|
+
Returns:
|
496
|
+
Service-level validation results with time window alignment
|
497
|
+
"""
|
498
|
+
profile = profile or (self.profiles[0] if self.profiles else None)
|
499
|
+
if not profile or profile not in self.aws_sessions:
|
500
|
+
return {'error': 'No valid profile for service validation'}
|
501
|
+
|
502
|
+
session = self.aws_sessions[profile]
|
503
|
+
validations = {}
|
504
|
+
|
505
|
+
# Get MCP service costs with aligned time window - CRITICAL FIX for synchronization
|
506
|
+
try:
|
507
|
+
# Ensure time window alignment with runbooks dashboard
|
508
|
+
mcp_data = asyncio.run(self._get_independent_cost_data(session, profile, start_date, end_date))
|
509
|
+
mcp_services = mcp_data.get('services', {})
|
510
|
+
|
511
|
+
# Apply same service filtering as runbooks
|
512
|
+
from .cost_processor import filter_analytical_services
|
513
|
+
mcp_services = filter_analytical_services(mcp_services)
|
514
|
+
|
515
|
+
# Validate each service
|
516
|
+
for service, runbooks_cost in service_breakdown.items():
|
517
|
+
if runbooks_cost > 100: # Only validate significant costs
|
518
|
+
mcp_cost = mcp_services.get(service, 0.0)
|
519
|
+
|
520
|
+
variance = 0.0
|
521
|
+
if runbooks_cost > 0:
|
522
|
+
variance = abs(runbooks_cost - mcp_cost) / runbooks_cost * 100
|
523
|
+
|
524
|
+
validations[service] = {
|
525
|
+
'runbooks_cost': runbooks_cost,
|
526
|
+
'mcp_cost': mcp_cost,
|
527
|
+
'variance_percent': variance,
|
528
|
+
'passed': variance <= self.tolerance_percent,
|
529
|
+
'status': 'PASSED' if variance <= self.tolerance_percent else 'VARIANCE'
|
530
|
+
}
|
531
|
+
|
532
|
+
# Display service validation results
|
533
|
+
self._display_service_validation(validations)
|
534
|
+
|
535
|
+
except Exception as e:
|
536
|
+
print_error(f"Service validation failed: {str(e)[:50]}")
|
537
|
+
return {'error': str(e)}
|
538
|
+
|
539
|
+
return {
|
540
|
+
'services': validations,
|
541
|
+
'validated_count': len(validations),
|
542
|
+
'passed_count': sum(1 for v in validations.values() if v['passed']),
|
543
|
+
'timestamp': datetime.now().isoformat()
|
544
|
+
}
|
545
|
+
|
546
|
+
def _display_organization_validation(self, result: Dict[str, Any]) -> None:
|
547
|
+
"""Display organization total validation with visual indicators."""
|
548
|
+
if result['passed']:
|
549
|
+
self.console.print(f"\n[green]✅ Organization Total MCP Validation: PASSED[/green]")
|
550
|
+
self.console.print(f"[dim] Runbooks: ${result['runbooks_total']:,.2f}[/dim]")
|
551
|
+
self.console.print(f"[dim] MCP: ${result['mcp_total']:,.2f}[/dim]")
|
552
|
+
self.console.print(f"[dim] Variance: {result['variance_percent']:.2f}% (within ±{self.tolerance_percent}%)[/dim]")
|
553
|
+
else:
|
554
|
+
self.console.print(f"\n[yellow]⚠️ Organization Total MCP Variance Detected[/yellow]")
|
555
|
+
self.console.print(f"[yellow] Runbooks: ${result['runbooks_total']:,.2f}[/yellow]")
|
556
|
+
self.console.print(f"[yellow] MCP: ${result['mcp_total']:,.2f}[/yellow]")
|
557
|
+
self.console.print(f"[yellow] Variance: {result['variance_percent']:.2f}% (exceeds ±{self.tolerance_percent}%)[/yellow]")
|
558
|
+
self.console.print(f"[dim yellow] Action: {result['action_required']}[/dim yellow]")
|
559
|
+
|
560
|
+
def _display_service_validation(self, validations: Dict[str, Dict]) -> None:
|
561
|
+
"""Display service-level validation results."""
|
562
|
+
if validations:
|
563
|
+
self.console.print("\n[bright_cyan]Service-Level MCP Validation:[/bright_cyan]")
|
564
|
+
|
565
|
+
for service, validation in validations.items():
|
566
|
+
if validation['passed']:
|
567
|
+
icon = "✅"
|
568
|
+
color = "green"
|
569
|
+
else:
|
570
|
+
icon = "⚠️"
|
571
|
+
color = "yellow"
|
572
|
+
|
573
|
+
self.console.print(
|
574
|
+
f"[dim] {service:20s}: {icon} [{color}]"
|
575
|
+
f"${validation['runbooks_cost']:,.2f} vs ${validation['mcp_cost']:,.2f} "
|
576
|
+
f"({validation['variance_percent']:.1f}% variance)[/][/dim]"
|
577
|
+
)
|
268
578
|
|
269
579
|
|
270
580
|
def create_embedded_mcp_validator(profiles: List[str], console: Optional[Console] = None) -> EmbeddedMCPValidator:
|