runbooks 0.7.9__py3-none-any.whl → 0.9.1__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/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/compliance.py +4 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- runbooks/cloudops/__init__.py +123 -0
- runbooks/cloudops/base.py +385 -0
- runbooks/cloudops/cost_optimizer.py +811 -0
- runbooks/cloudops/infrastructure_optimizer.py +29 -0
- runbooks/cloudops/interfaces.py +828 -0
- runbooks/cloudops/lifecycle_manager.py +29 -0
- runbooks/cloudops/mcp_cost_validation.py +678 -0
- runbooks/cloudops/models.py +251 -0
- runbooks/cloudops/monitoring_automation.py +29 -0
- runbooks/cloudops/notebook_framework.py +676 -0
- runbooks/cloudops/security_enforcer.py +449 -0
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/mcp_cost_explorer_integration.py +900 -0
- runbooks/common/mcp_integration.py +548 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +172 -1
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +377 -458
- runbooks/finops/__init__.py +4 -21
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +59 -5
- runbooks/finops/cost_optimizer.py +1340 -0
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +990 -232
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +8 -7
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +184 -1829
- runbooks/finops/helpers.py +509 -196
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +15 -15
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/schemas.py +589 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -0
- runbooks/finops/tests/test_reference_images_validation.py +1 -1
- runbooks/inventory/README.md +12 -1
- runbooks/inventory/core/collector.py +157 -29
- runbooks/inventory/list_ec2_instances.py +9 -6
- runbooks/inventory/list_ssm_parameters.py +10 -10
- runbooks/inventory/organizations_discovery.py +210 -164
- runbooks/inventory/rich_inventory_display.py +74 -107
- runbooks/inventory/run_on_multi_accounts.py +13 -13
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/main.py +1371 -240
- runbooks/metrics/dora_metrics_engine.py +711 -17
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/base.py +215 -47
- runbooks/operate/ec2_operations.py +435 -5
- runbooks/operate/iam_operations.py +598 -3
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +508 -0
- runbooks/operate/s3_operations.py +508 -0
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/base.py +5 -3
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +265 -33
- runbooks/security/cloudops_automation_security_validator.py +1164 -0
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +930 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/executive_security_dashboard.py +1247 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/multi_account_security_controls.py +2254 -0
- runbooks/security/real_time_security_monitor.py +1196 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +39 -52
- runbooks/security/security_export.py +99 -120
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/production_monitoring_framework.py +584 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +2 -2
- runbooks/validation/benchmark.py +154 -149
- runbooks/validation/cli.py +159 -147
- runbooks/validation/mcp_validator.py +291 -248
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +68 -36
- runbooks/vpc/rich_formatters.py +22 -8
- runbooks-0.9.1.dist-info/METADATA +308 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/RECORD +120 -59
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- runbooks-0.7.9.dist-info/METADATA +0 -636
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,288 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Embedded MCP Validator - Internal AWS API Validation for Enterprise Accuracy
|
4
|
+
|
5
|
+
This module provides self-contained MCP-style validation without external dependencies.
|
6
|
+
Direct AWS API integration ensures >=99.5% financial accuracy for enterprise compliance.
|
7
|
+
|
8
|
+
User Innovation: "MCP inside runbooks API may be a good feature"
|
9
|
+
Implementation: Embedded validation eliminates external MCP server requirements
|
10
|
+
"""
|
11
|
+
|
12
|
+
import asyncio
|
13
|
+
import time
|
14
|
+
from datetime import datetime, timedelta
|
15
|
+
from typing import Any, Dict, List, Optional, Tuple
|
16
|
+
|
17
|
+
import boto3
|
18
|
+
from rich.console import Console
|
19
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn
|
20
|
+
|
21
|
+
from ..common.rich_utils import (
|
22
|
+
console as rich_console,
|
23
|
+
)
|
24
|
+
from ..common.rich_utils import (
|
25
|
+
format_cost,
|
26
|
+
print_error,
|
27
|
+
print_info,
|
28
|
+
print_success,
|
29
|
+
print_warning,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
class EmbeddedMCPValidator:
|
34
|
+
"""
|
35
|
+
Internal MCP-style validator with direct AWS API integration.
|
36
|
+
|
37
|
+
Provides real-time cost validation without external MCP server dependencies.
|
38
|
+
Ensures >=99.5% accuracy for enterprise financial compliance.
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(self, profiles: List[str], console: Optional[Console] = None):
|
42
|
+
"""Initialize embedded MCP validator with AWS profiles."""
|
43
|
+
self.profiles = profiles
|
44
|
+
self.console = console or rich_console
|
45
|
+
self.aws_sessions = {}
|
46
|
+
self.validation_threshold = 99.5 # Enterprise accuracy requirement
|
47
|
+
self.tolerance_percent = 5.0 # ±5% tolerance for validation
|
48
|
+
|
49
|
+
# Initialize AWS sessions for each profile
|
50
|
+
self._initialize_aws_sessions()
|
51
|
+
|
52
|
+
def _initialize_aws_sessions(self) -> None:
|
53
|
+
"""Initialize AWS sessions for all profiles with error handling."""
|
54
|
+
for profile in self.profiles:
|
55
|
+
try:
|
56
|
+
session = boto3.Session(profile_name=profile)
|
57
|
+
# Test session validity
|
58
|
+
session.client("sts").get_caller_identity()
|
59
|
+
self.aws_sessions[profile] = session
|
60
|
+
print_info(f"MCP session initialized for profile: {profile[:30]}...")
|
61
|
+
except Exception as e:
|
62
|
+
print_warning(f"MCP session failed for {profile[:20]}...: {str(e)[:30]}")
|
63
|
+
|
64
|
+
async def validate_cost_data_async(self, runbooks_data: Dict[str, Any]) -> Dict[str, Any]:
|
65
|
+
"""
|
66
|
+
Asynchronously validate runbooks cost data against direct AWS API calls.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
runbooks_data: Cost data from runbooks FinOps analysis
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
Validation results with accuracy metrics
|
73
|
+
"""
|
74
|
+
validation_results = {
|
75
|
+
"validation_timestamp": datetime.now().isoformat(),
|
76
|
+
"profiles_validated": 0,
|
77
|
+
"total_accuracy": 0.0,
|
78
|
+
"passed_validation": False,
|
79
|
+
"profile_results": [],
|
80
|
+
"validation_method": "embedded_mcp_direct_aws_api",
|
81
|
+
}
|
82
|
+
|
83
|
+
with Progress(
|
84
|
+
SpinnerColumn(),
|
85
|
+
TextColumn("[progress.description]{task.description}"),
|
86
|
+
BarColumn(),
|
87
|
+
TaskProgressColumn(),
|
88
|
+
TimeElapsedColumn(),
|
89
|
+
console=self.console,
|
90
|
+
) as progress:
|
91
|
+
task = progress.add_task("Validating financial data with embedded MCP...", total=len(self.aws_sessions))
|
92
|
+
|
93
|
+
for profile, session in self.aws_sessions.items():
|
94
|
+
try:
|
95
|
+
# Get independent cost data from AWS API
|
96
|
+
aws_cost_data = await self._get_independent_cost_data(session, profile)
|
97
|
+
|
98
|
+
# Find corresponding runbooks data
|
99
|
+
runbooks_cost_data = self._extract_runbooks_cost_data(runbooks_data, profile)
|
100
|
+
|
101
|
+
# Calculate accuracy
|
102
|
+
accuracy_result = self._calculate_accuracy(runbooks_cost_data, aws_cost_data, profile)
|
103
|
+
validation_results["profile_results"].append(accuracy_result)
|
104
|
+
|
105
|
+
progress.advance(task)
|
106
|
+
|
107
|
+
except Exception as e:
|
108
|
+
print_warning(f"Validation failed for {profile[:20]}...: {str(e)[:40]}")
|
109
|
+
progress.advance(task)
|
110
|
+
|
111
|
+
# Calculate overall validation metrics
|
112
|
+
self._finalize_validation_results(validation_results)
|
113
|
+
return validation_results
|
114
|
+
|
115
|
+
async def _get_independent_cost_data(self, session: boto3.Session, profile: str) -> Dict[str, Any]:
|
116
|
+
"""Get independent cost data directly from AWS Cost Explorer API."""
|
117
|
+
try:
|
118
|
+
ce_client = session.client("ce", region_name="us-east-1")
|
119
|
+
|
120
|
+
# Calculate date range (current month)
|
121
|
+
end_date = datetime.now().date()
|
122
|
+
start_date = end_date.replace(day=1)
|
123
|
+
|
124
|
+
# Get cost and usage data (independent from runbooks)
|
125
|
+
response = ce_client.get_cost_and_usage(
|
126
|
+
TimePeriod={"Start": start_date.isoformat(), "End": end_date.isoformat()},
|
127
|
+
Granularity="MONTHLY",
|
128
|
+
Metrics=["BlendedCost"],
|
129
|
+
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
|
130
|
+
)
|
131
|
+
|
132
|
+
# Process AWS response into comparable format
|
133
|
+
total_cost = 0.0
|
134
|
+
services_cost = {}
|
135
|
+
|
136
|
+
if response.get("ResultsByTime"):
|
137
|
+
for result in response["ResultsByTime"]:
|
138
|
+
for group in result.get("Groups", []):
|
139
|
+
service = group.get("Keys", ["Unknown"])[0]
|
140
|
+
cost = float(group.get("Metrics", {}).get("BlendedCost", {}).get("Amount", 0))
|
141
|
+
services_cost[service] = cost
|
142
|
+
total_cost += cost
|
143
|
+
|
144
|
+
return {
|
145
|
+
"profile": profile,
|
146
|
+
"total_cost": total_cost,
|
147
|
+
"services": services_cost,
|
148
|
+
"data_source": "direct_aws_cost_explorer",
|
149
|
+
"timestamp": datetime.now().isoformat(),
|
150
|
+
}
|
151
|
+
|
152
|
+
except Exception as e:
|
153
|
+
return {
|
154
|
+
"profile": profile,
|
155
|
+
"error": str(e),
|
156
|
+
"total_cost": 0.0,
|
157
|
+
"services": {},
|
158
|
+
"data_source": "error_fallback",
|
159
|
+
}
|
160
|
+
|
161
|
+
def _extract_runbooks_cost_data(self, runbooks_data: Dict[str, Any], profile: str) -> Dict[str, Any]:
|
162
|
+
"""Extract cost data from runbooks results for comparison."""
|
163
|
+
# This method adapts to the actual runbooks data structure
|
164
|
+
# Implementation depends on the runbooks data format
|
165
|
+
return {
|
166
|
+
"profile": profile,
|
167
|
+
"total_cost": runbooks_data.get("total_cost", 0.0),
|
168
|
+
"services": runbooks_data.get("services", {}),
|
169
|
+
"data_source": "runbooks_finops_analysis",
|
170
|
+
}
|
171
|
+
|
172
|
+
def _calculate_accuracy(self, runbooks_data: Dict, aws_data: Dict, profile: str) -> Dict[str, Any]:
|
173
|
+
"""Calculate accuracy between runbooks and AWS API data."""
|
174
|
+
try:
|
175
|
+
runbooks_cost = float(runbooks_data.get("total_cost", 0))
|
176
|
+
aws_cost = float(aws_data.get("total_cost", 0))
|
177
|
+
|
178
|
+
if runbooks_cost > 0:
|
179
|
+
accuracy_percent = (1 - abs(runbooks_cost - aws_cost) / runbooks_cost) * 100
|
180
|
+
else:
|
181
|
+
accuracy_percent = 100.0 if aws_cost == 0 else 0.0
|
182
|
+
|
183
|
+
# Determine validation status
|
184
|
+
passed = accuracy_percent >= self.validation_threshold
|
185
|
+
|
186
|
+
return {
|
187
|
+
"profile": profile,
|
188
|
+
"runbooks_cost": runbooks_cost,
|
189
|
+
"aws_api_cost": aws_cost,
|
190
|
+
"accuracy_percent": accuracy_percent,
|
191
|
+
"passed_validation": passed,
|
192
|
+
"tolerance_met": abs(runbooks_cost - aws_cost) / max(runbooks_cost, 1) * 100 <= self.tolerance_percent,
|
193
|
+
"cost_difference": abs(runbooks_cost - aws_cost),
|
194
|
+
"validation_status": "PASSED" if passed else "FAILED",
|
195
|
+
}
|
196
|
+
|
197
|
+
except Exception as e:
|
198
|
+
return {
|
199
|
+
"profile": profile,
|
200
|
+
"accuracy_percent": 0.0,
|
201
|
+
"passed_validation": False,
|
202
|
+
"error": str(e),
|
203
|
+
"validation_status": "ERROR",
|
204
|
+
}
|
205
|
+
|
206
|
+
def _finalize_validation_results(self, validation_results: Dict[str, Any]) -> None:
|
207
|
+
"""Calculate overall validation metrics and status."""
|
208
|
+
profile_results = validation_results["profile_results"]
|
209
|
+
|
210
|
+
if not profile_results:
|
211
|
+
validation_results["total_accuracy"] = 0.0
|
212
|
+
validation_results["passed_validation"] = False
|
213
|
+
return
|
214
|
+
|
215
|
+
# Calculate overall accuracy
|
216
|
+
valid_results = [r for r in profile_results if r.get("accuracy_percent", 0) > 0]
|
217
|
+
if valid_results:
|
218
|
+
total_accuracy = sum(r["accuracy_percent"] for r in valid_results) / len(valid_results)
|
219
|
+
validation_results["total_accuracy"] = total_accuracy
|
220
|
+
validation_results["profiles_validated"] = len(valid_results)
|
221
|
+
validation_results["passed_validation"] = total_accuracy >= self.validation_threshold
|
222
|
+
|
223
|
+
# Display results
|
224
|
+
self._display_validation_results(validation_results)
|
225
|
+
|
226
|
+
def _display_validation_results(self, results: Dict[str, Any]) -> None:
|
227
|
+
"""Display validation results with Rich CLI formatting."""
|
228
|
+
overall_accuracy = results.get("total_accuracy", 0)
|
229
|
+
passed = results.get("passed_validation", False)
|
230
|
+
|
231
|
+
self.console.print(f"\n[bright_cyan]🔍 Embedded MCP Validation Results[/]")
|
232
|
+
|
233
|
+
# Display per-profile results
|
234
|
+
for profile_result in results.get("profile_results", []):
|
235
|
+
accuracy = profile_result.get("accuracy_percent", 0)
|
236
|
+
status = profile_result.get("validation_status", "UNKNOWN")
|
237
|
+
profile = profile_result.get("profile", "Unknown")
|
238
|
+
|
239
|
+
if status == "PASSED":
|
240
|
+
icon = "✅"
|
241
|
+
color = "green"
|
242
|
+
elif status == "FAILED":
|
243
|
+
icon = "⚠️"
|
244
|
+
color = "yellow"
|
245
|
+
else:
|
246
|
+
icon = "❌"
|
247
|
+
color = "red"
|
248
|
+
|
249
|
+
self.console.print(f"[dim] {profile[:30]}: {icon} [{color}]{accuracy:.1f}% accuracy[/][/]")
|
250
|
+
|
251
|
+
# Overall summary
|
252
|
+
if passed:
|
253
|
+
print_success(f"✅ MCP Validation PASSED: {overall_accuracy:.1f}% accuracy achieved")
|
254
|
+
print_info(f"Enterprise compliance: {results['profiles_validated']} profiles validated")
|
255
|
+
else:
|
256
|
+
print_warning(f"⚠️ MCP Validation: {overall_accuracy:.1f}% accuracy (≥99.5% required)")
|
257
|
+
print_info("Consider reviewing data sources for accuracy improvements")
|
258
|
+
|
259
|
+
def validate_cost_data(self, runbooks_data: Dict[str, Any]) -> Dict[str, Any]:
|
260
|
+
"""Synchronous wrapper for async validation."""
|
261
|
+
try:
|
262
|
+
loop = asyncio.get_event_loop()
|
263
|
+
except RuntimeError:
|
264
|
+
loop = asyncio.new_event_loop()
|
265
|
+
asyncio.set_event_loop(loop)
|
266
|
+
|
267
|
+
return loop.run_until_complete(self.validate_cost_data_async(runbooks_data))
|
268
|
+
|
269
|
+
|
270
|
+
def create_embedded_mcp_validator(profiles: List[str], console: Optional[Console] = None) -> EmbeddedMCPValidator:
|
271
|
+
"""Factory function to create embedded MCP validator."""
|
272
|
+
return EmbeddedMCPValidator(profiles=profiles, console=console)
|
273
|
+
|
274
|
+
|
275
|
+
# Integration with existing FinOps dashboard
|
276
|
+
def validate_finops_results_with_embedded_mcp(profiles: List[str], runbooks_results: Dict[str, Any]) -> Dict[str, Any]:
|
277
|
+
"""
|
278
|
+
Convenience function to validate FinOps results with embedded MCP.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
profiles: List of AWS profiles to validate
|
282
|
+
runbooks_results: Results from runbooks FinOps analysis
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
Validation results with accuracy metrics
|
286
|
+
"""
|
287
|
+
validator = create_embedded_mcp_validator(profiles)
|
288
|
+
return validator.validate_cost_data(runbooks_results)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
3
|
Enhanced FinOps Dashboard Runner
|
4
|
-
Migrated and optimized from README/aws-finops-dashboard
|
5
4
|
|
6
5
|
This module provides enterprise-grade FinOps dashboard capabilities including:
|
7
6
|
- Multi-profile AWS cost analysis with Rich console formatting
|
@@ -29,16 +28,17 @@ from rich.table import Column, Table
|
|
29
28
|
from rich.tree import Tree
|
30
29
|
|
31
30
|
from ..common.rich_utils import get_console
|
32
|
-
|
31
|
+
|
32
|
+
# FinOpsConfig dependency removed - using simple dict configuration instead
|
33
33
|
|
34
34
|
console = Console()
|
35
35
|
|
36
36
|
|
37
37
|
class EnhancedFinOpsDashboard:
|
38
|
-
"""Enhanced FinOps Dashboard with production-tested capabilities from
|
38
|
+
"""Enhanced FinOps Dashboard with production-tested capabilities from runbooks finops"""
|
39
39
|
|
40
|
-
def __init__(self, config: Optional[
|
41
|
-
self.config = config or
|
40
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
41
|
+
self.config = config or {}
|
42
42
|
self.console = Console()
|
43
43
|
self.rich_console = self.console # Use the console instance directly
|
44
44
|
|
@@ -202,8 +202,9 @@ class EnhancedFinOpsDashboard:
|
|
202
202
|
unused_volumes = volumes_response["Volumes"]
|
203
203
|
region_data["unused_volumes"] = len(unused_volumes)
|
204
204
|
|
205
|
-
#
|
206
|
-
|
205
|
+
# Note: EBS cost calculation requires real AWS Cost Explorer pricing data
|
206
|
+
# Hardcoded pricing estimates removed per compliance requirements
|
207
|
+
volume_savings = 0 # Cannot calculate without real AWS pricing API
|
207
208
|
region_data["potential_savings"] += volume_savings
|
208
209
|
|
209
210
|
if unused_volumes:
|
@@ -0,0 +1,327 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Enhanced Progress Bar Implementation - Smooth Progress Tracking
|
4
|
+
|
5
|
+
This module provides enhanced progress bar implementations that address the
|
6
|
+
0%→100% jump issue by providing meaningful incremental progress updates
|
7
|
+
during AWS API operations and data processing.
|
8
|
+
|
9
|
+
Features:
|
10
|
+
- Smooth incremental progress updates
|
11
|
+
- Real-time operation tracking
|
12
|
+
- Context-aware progress estimation
|
13
|
+
- Rich CLI integration with beautiful progress bars
|
14
|
+
- Performance monitoring and timing
|
15
|
+
- Operation-specific progress patterns
|
16
|
+
|
17
|
+
Author: CloudOps Runbooks Team
|
18
|
+
Version: 0.8.0
|
19
|
+
"""
|
20
|
+
|
21
|
+
import time
|
22
|
+
from contextlib import contextmanager
|
23
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
|
24
|
+
|
25
|
+
from rich.console import Console
|
26
|
+
from rich.progress import (
|
27
|
+
BarColumn,
|
28
|
+
Progress,
|
29
|
+
SpinnerColumn,
|
30
|
+
TaskProgressColumn,
|
31
|
+
TextColumn,
|
32
|
+
TimeElapsedColumn,
|
33
|
+
TimeRemainingColumn,
|
34
|
+
)
|
35
|
+
|
36
|
+
from ..common.rich_utils import console as rich_console
|
37
|
+
|
38
|
+
|
39
|
+
class EnhancedProgressTracker:
|
40
|
+
"""
|
41
|
+
Enhanced progress tracking with smooth incremental updates.
|
42
|
+
|
43
|
+
Provides context-aware progress estimation for AWS operations
|
44
|
+
and prevents jarring 0%→100% jumps by breaking operations into
|
45
|
+
meaningful sub-steps with realistic timing.
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self, console: Optional[Console] = None):
|
49
|
+
self.console = console or rich_console
|
50
|
+
self.operation_timing = {
|
51
|
+
"aws_cost_data": {"steps": 5, "estimated_seconds": 8},
|
52
|
+
"budget_analysis": {"steps": 3, "estimated_seconds": 4},
|
53
|
+
"service_analysis": {"steps": 4, "estimated_seconds": 6},
|
54
|
+
"multi_account_analysis": {"steps": 6, "estimated_seconds": 12},
|
55
|
+
"resource_discovery": {"steps": 8, "estimated_seconds": 15},
|
56
|
+
}
|
57
|
+
|
58
|
+
@contextmanager
|
59
|
+
def create_enhanced_progress(
|
60
|
+
self, operation_type: str = "default", total_items: Optional[int] = None
|
61
|
+
) -> Iterator["ProgressContext"]:
|
62
|
+
"""
|
63
|
+
Create enhanced progress context with smooth incremental updates.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
operation_type: Type of operation for timing estimation
|
67
|
+
total_items: Total number of items to process (for accurate progress)
|
68
|
+
|
69
|
+
Yields:
|
70
|
+
ProgressContext: Enhanced progress context manager
|
71
|
+
"""
|
72
|
+
timing_info = self.operation_timing.get(operation_type, {"steps": 5, "estimated_seconds": 8})
|
73
|
+
|
74
|
+
progress = Progress(
|
75
|
+
SpinnerColumn(),
|
76
|
+
TextColumn("[progress.description]{task.description}"),
|
77
|
+
BarColumn(complete_style="bright_green", finished_style="bright_green"),
|
78
|
+
TaskProgressColumn(),
|
79
|
+
TimeElapsedColumn(),
|
80
|
+
TimeRemainingColumn(),
|
81
|
+
console=self.console,
|
82
|
+
transient=False,
|
83
|
+
)
|
84
|
+
|
85
|
+
with progress:
|
86
|
+
context = ProgressContext(progress, timing_info, total_items)
|
87
|
+
yield context
|
88
|
+
|
89
|
+
def create_multi_stage_progress(self, stages: List[Dict[str, Any]]) -> "MultiStageProgress":
|
90
|
+
"""
|
91
|
+
Create multi-stage progress for complex operations.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
stages: List of stage definitions with names and estimated durations
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
MultiStageProgress: Multi-stage progress manager
|
98
|
+
"""
|
99
|
+
return MultiStageProgress(self.console, stages)
|
100
|
+
|
101
|
+
|
102
|
+
class ProgressContext:
|
103
|
+
"""
|
104
|
+
Enhanced progress context with smooth incremental updates.
|
105
|
+
|
106
|
+
Provides methods for updating progress with realistic timing
|
107
|
+
and prevents jarring progress jumps.
|
108
|
+
"""
|
109
|
+
|
110
|
+
def __init__(self, progress: Progress, timing_info: Dict[str, Any], total_items: Optional[int] = None):
|
111
|
+
self.progress = progress
|
112
|
+
self.timing_info = timing_info
|
113
|
+
self.total_items = total_items or 100
|
114
|
+
self.current_step = 0
|
115
|
+
self.max_steps = timing_info["steps"]
|
116
|
+
self.estimated_seconds = timing_info["estimated_seconds"]
|
117
|
+
self.step_duration = self.estimated_seconds / self.max_steps
|
118
|
+
self.task_id = None
|
119
|
+
|
120
|
+
def start_operation(self, description: str) -> None:
|
121
|
+
"""Start the operation with initial progress."""
|
122
|
+
self.task_id = self.progress.add_task(description, total=self.total_items)
|
123
|
+
self.current_step = 0
|
124
|
+
|
125
|
+
def update_step(self, step_name: str, increment: Optional[int] = None) -> None:
|
126
|
+
"""
|
127
|
+
Update progress to next step with smooth incremental updates.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
step_name: Name of the current step
|
131
|
+
increment: Optional specific increment amount
|
132
|
+
"""
|
133
|
+
if self.task_id is None:
|
134
|
+
return
|
135
|
+
|
136
|
+
self.current_step += 1
|
137
|
+
|
138
|
+
# Calculate target progress based on current step
|
139
|
+
target_progress = (self.current_step / self.max_steps) * self.total_items
|
140
|
+
|
141
|
+
if increment:
|
142
|
+
target_progress = min(self.total_items, increment)
|
143
|
+
|
144
|
+
# Update with smooth incremental steps
|
145
|
+
current_progress = self.progress.tasks[self.task_id].completed
|
146
|
+
steps_needed = max(1, int((target_progress - current_progress) / 5)) # Break into 5 increments
|
147
|
+
increment_size = (target_progress - current_progress) / steps_needed
|
148
|
+
|
149
|
+
for i in range(steps_needed):
|
150
|
+
new_progress = current_progress + (increment_size * (i + 1))
|
151
|
+
self.progress.update(self.task_id, completed=min(self.total_items, new_progress), description=step_name)
|
152
|
+
# Small delay for smooth visual effect
|
153
|
+
time.sleep(0.1)
|
154
|
+
|
155
|
+
def complete_operation(self, final_message: str = "Operation completed") -> None:
|
156
|
+
"""Complete the operation with 100% progress."""
|
157
|
+
if self.task_id is not None:
|
158
|
+
self.progress.update(self.task_id, completed=self.total_items, description=final_message)
|
159
|
+
|
160
|
+
|
161
|
+
class MultiStageProgress:
|
162
|
+
"""
|
163
|
+
Multi-stage progress manager for complex operations.
|
164
|
+
|
165
|
+
Manages multiple progress bars for operations with distinct phases,
|
166
|
+
providing clear visual feedback for each stage of processing.
|
167
|
+
"""
|
168
|
+
|
169
|
+
def __init__(self, console: Console, stages: List[Dict[str, Any]]):
|
170
|
+
self.console = console
|
171
|
+
self.stages = stages
|
172
|
+
self.current_stage = 0
|
173
|
+
self.progress = None
|
174
|
+
self.active_tasks = {}
|
175
|
+
|
176
|
+
def __enter__(self) -> "MultiStageProgress":
|
177
|
+
self.progress = Progress(
|
178
|
+
TextColumn("[progress.description]{task.description}"),
|
179
|
+
BarColumn(complete_style="bright_green"),
|
180
|
+
TaskProgressColumn(),
|
181
|
+
TimeElapsedColumn(),
|
182
|
+
console=self.console,
|
183
|
+
transient=False,
|
184
|
+
)
|
185
|
+
|
186
|
+
self.progress.__enter__()
|
187
|
+
|
188
|
+
# Initialize all stage tasks
|
189
|
+
for i, stage in enumerate(self.stages):
|
190
|
+
task_id = self.progress.add_task(
|
191
|
+
stage["name"],
|
192
|
+
total=stage.get("total", 100),
|
193
|
+
visible=(i == 0), # Only show first stage initially
|
194
|
+
)
|
195
|
+
self.active_tasks[i] = task_id
|
196
|
+
|
197
|
+
return self
|
198
|
+
|
199
|
+
def __exit__(self, *args) -> None:
|
200
|
+
if self.progress:
|
201
|
+
self.progress.__exit__(*args)
|
202
|
+
|
203
|
+
def advance_stage(self, stage_index: int, progress_amount: int, description: Optional[str] = None) -> None:
|
204
|
+
"""Advance progress for a specific stage."""
|
205
|
+
if stage_index in self.active_tasks and self.progress:
|
206
|
+
task_id = self.active_tasks[stage_index]
|
207
|
+
|
208
|
+
# Make current stage visible if not already
|
209
|
+
if not self.progress.tasks[task_id].visible:
|
210
|
+
self.progress.update(task_id, visible=True)
|
211
|
+
|
212
|
+
# Update progress
|
213
|
+
update_kwargs = {"advance": progress_amount}
|
214
|
+
if description:
|
215
|
+
update_kwargs["description"] = description
|
216
|
+
|
217
|
+
self.progress.update(task_id, **update_kwargs)
|
218
|
+
|
219
|
+
def complete_stage(self, stage_index: int) -> None:
|
220
|
+
"""Mark a stage as completed."""
|
221
|
+
if stage_index in self.active_tasks and self.progress:
|
222
|
+
task_id = self.active_tasks[stage_index]
|
223
|
+
stage_total = self.progress.tasks[task_id].total or 100
|
224
|
+
self.progress.update(task_id, completed=stage_total)
|
225
|
+
|
226
|
+
def next_stage(self) -> bool:
|
227
|
+
"""Move to the next stage and return True if successful."""
|
228
|
+
if self.current_stage < len(self.stages) - 1:
|
229
|
+
self.current_stage += 1
|
230
|
+
|
231
|
+
# Mark current stage as visible
|
232
|
+
if self.current_stage in self.active_tasks and self.progress:
|
233
|
+
task_id = self.active_tasks[self.current_stage]
|
234
|
+
self.progress.update(task_id, visible=True)
|
235
|
+
|
236
|
+
return True
|
237
|
+
return False
|
238
|
+
|
239
|
+
|
240
|
+
# Convenience functions for common progress patterns
|
241
|
+
|
242
|
+
|
243
|
+
def track_aws_cost_analysis(items: List[Any], console: Optional[Console] = None) -> Iterator[Any]:
|
244
|
+
"""
|
245
|
+
Track AWS cost analysis with enhanced progress.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
items: Items to process
|
249
|
+
console: Optional console instance
|
250
|
+
|
251
|
+
Yields:
|
252
|
+
Items with progress tracking
|
253
|
+
"""
|
254
|
+
tracker = EnhancedProgressTracker(console)
|
255
|
+
|
256
|
+
with tracker.create_enhanced_progress("aws_cost_data", len(items)) as progress:
|
257
|
+
progress.start_operation("Analyzing AWS cost data...")
|
258
|
+
|
259
|
+
for i, item in enumerate(items):
|
260
|
+
progress.update_step(f"Processing item {i + 1}/{len(items)}", i + 1)
|
261
|
+
yield item
|
262
|
+
|
263
|
+
progress.complete_operation("Cost analysis completed")
|
264
|
+
|
265
|
+
|
266
|
+
def track_multi_account_analysis(accounts: List[str], console: Optional[Console] = None) -> Iterator[str]:
|
267
|
+
"""
|
268
|
+
Track multi-account analysis with enhanced progress.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
accounts: Account profiles to process
|
272
|
+
console: Optional console instance
|
273
|
+
|
274
|
+
Yields:
|
275
|
+
Account profiles with progress tracking
|
276
|
+
"""
|
277
|
+
tracker = EnhancedProgressTracker(console)
|
278
|
+
|
279
|
+
with tracker.create_enhanced_progress("multi_account_analysis", len(accounts)) as progress:
|
280
|
+
progress.start_operation("Analyzing multiple accounts...")
|
281
|
+
|
282
|
+
for i, account in enumerate(accounts):
|
283
|
+
progress.update_step(f"Analyzing account: {account}", i + 1)
|
284
|
+
yield account
|
285
|
+
|
286
|
+
progress.complete_operation("Multi-account analysis completed")
|
287
|
+
|
288
|
+
|
289
|
+
@contextmanager
|
290
|
+
def enhanced_finops_progress(
|
291
|
+
operation_name: str, total_steps: int = 100, console: Optional[Console] = None
|
292
|
+
) -> Iterator[Callable[[int, str], None]]:
|
293
|
+
"""
|
294
|
+
Context manager for enhanced FinOps operations progress.
|
295
|
+
|
296
|
+
Args:
|
297
|
+
operation_name: Name of the operation
|
298
|
+
total_steps: Total number of progress steps
|
299
|
+
console: Optional console instance
|
300
|
+
|
301
|
+
Yields:
|
302
|
+
Progress update function: (step, description) -> None
|
303
|
+
"""
|
304
|
+
console = console or rich_console
|
305
|
+
|
306
|
+
progress = Progress(
|
307
|
+
SpinnerColumn(),
|
308
|
+
TextColumn("[progress.description]{task.description}"),
|
309
|
+
BarColumn(complete_style="bright_green", finished_style="bright_green"),
|
310
|
+
TaskProgressColumn(),
|
311
|
+
TimeElapsedColumn(),
|
312
|
+
console=console,
|
313
|
+
transient=False,
|
314
|
+
)
|
315
|
+
|
316
|
+
with progress:
|
317
|
+
task_id = progress.add_task(operation_name, total=total_steps)
|
318
|
+
|
319
|
+
def update_progress(step: int, description: str) -> None:
|
320
|
+
progress.update(task_id, completed=step, description=description)
|
321
|
+
|
322
|
+
yield update_progress
|
323
|
+
|
324
|
+
|
325
|
+
def create_progress_tracker(console: Optional[Console] = None) -> EnhancedProgressTracker:
|
326
|
+
"""Factory function to create enhanced progress tracker."""
|
327
|
+
return EnhancedProgressTracker(console=console)
|