runbooks 0.7.9__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +1 -1
- runbooks/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- 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_integration.py +539 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +171 -0
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +339 -451
- 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_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 +29 -1880
- 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/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/main.py +740 -134
- 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 +7 -5
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +164 -33
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +931 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +36 -49
- 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/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 +265 -236
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +62 -33
- runbooks/vpc/rich_formatters.py +22 -8
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/METADATA +136 -54
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/RECORD +94 -55
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,10 @@
|
|
1
1
|
import csv
|
2
2
|
import json
|
3
3
|
import os
|
4
|
+
import threading
|
5
|
+
import time
|
4
6
|
from collections import defaultdict
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
5
8
|
from datetime import date, datetime, timedelta
|
6
9
|
from typing import Any, Dict, List, Optional, Tuple
|
7
10
|
|
@@ -9,18 +12,69 @@ from boto3.session import Session
|
|
9
12
|
from rich.console import Console
|
10
13
|
|
11
14
|
from runbooks.finops.aws_client import get_account_id
|
15
|
+
from runbooks.finops.iam_guidance import handle_cost_explorer_error
|
12
16
|
from runbooks.finops.types import BudgetInfo, CostData, EC2Summary, ProfileData
|
13
17
|
|
14
18
|
console = Console()
|
15
19
|
|
20
|
+
# Enterprise batch processing configuration
|
21
|
+
BATCH_COST_EXPLORER_SIZE = 5 # Optimal batch size for Cost Explorer API to avoid rate limiting
|
22
|
+
MAX_CONCURRENT_COST_CALLS = 10 # AWS Cost Explorer rate limit consideration
|
16
23
|
|
17
|
-
|
24
|
+
# Service filtering configuration for analytical insights
|
25
|
+
NON_ANALYTICAL_SERVICES = ["Tax"] # Services excluded from Top N analysis per user requirements
|
26
|
+
|
27
|
+
|
28
|
+
def filter_analytical_services(
|
29
|
+
services_dict: Dict[str, float], excluded_services: List[str] = None
|
30
|
+
) -> Dict[str, float]:
|
31
|
+
"""
|
32
|
+
Filter out non-analytical services from service cost data.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
services_dict: Dictionary of service names to costs
|
36
|
+
excluded_services: List of service patterns to exclude (defaults to NON_ANALYTICAL_SERVICES)
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
Dictionary with non-analytical services filtered out
|
40
|
+
|
41
|
+
Example:
|
42
|
+
>>> services = {'Amazon EC2': 100.0, 'Tax': 10.0, 'S3': 50.0}
|
43
|
+
>>> filtered = filter_analytical_services(services)
|
44
|
+
>>> filtered
|
45
|
+
{'Amazon EC2': 100.0, 'S3': 50.0}
|
46
|
+
"""
|
47
|
+
if excluded_services is None:
|
48
|
+
excluded_services = NON_ANALYTICAL_SERVICES
|
49
|
+
|
50
|
+
filtered_services = {}
|
51
|
+
filtered_count = 0
|
52
|
+
|
53
|
+
for service_name, cost in services_dict.items():
|
54
|
+
should_exclude = any(excluded in service_name for excluded in excluded_services)
|
55
|
+
if not should_exclude:
|
56
|
+
filtered_services[service_name] = cost
|
57
|
+
else:
|
58
|
+
filtered_count += 1
|
59
|
+
|
60
|
+
# Debug logging for enterprise troubleshooting
|
61
|
+
if filtered_count > 0:
|
62
|
+
excluded_names = [
|
63
|
+
name for name in services_dict.keys() if any(excluded in name for excluded in excluded_services)
|
64
|
+
]
|
65
|
+
console.log(f"[dim yellow]🔍 Filtered {filtered_count} non-analytical services: {', '.join(excluded_names)}[/]")
|
66
|
+
|
67
|
+
return filtered_services
|
68
|
+
|
69
|
+
|
70
|
+
def get_trend(session: Session, tag: Optional[List[str]] = None, account_id: Optional[str] = None) -> Dict[str, Any]:
|
18
71
|
"""
|
19
72
|
Get cost trend data for an AWS account.
|
20
73
|
|
21
74
|
Args:
|
22
75
|
session: The boto3 session to use
|
23
76
|
tag: Optional list of tags in "Key=Value" format to filter resources.
|
77
|
+
account_id: Optional account ID to filter costs to specific account (multi-account support)
|
24
78
|
|
25
79
|
"""
|
26
80
|
ce = session.client("ce")
|
@@ -30,30 +84,32 @@ def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, An
|
|
30
84
|
key, value = t.split("=", 1)
|
31
85
|
tag_filters.append({"Key": key, "Values": [value]})
|
32
86
|
|
33
|
-
|
87
|
+
# Build filters for trend data (similar to get_cost_data)
|
88
|
+
filters = []
|
89
|
+
|
90
|
+
# Add account filtering if account_id is provided
|
91
|
+
if account_id:
|
92
|
+
account_filter = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
|
93
|
+
filters.append(account_filter)
|
94
|
+
|
95
|
+
# Add tag filtering if provided
|
34
96
|
if tag_filters:
|
35
|
-
|
36
|
-
|
97
|
+
for tag_filter in tag_filters:
|
98
|
+
tag_filter_dict = {
|
37
99
|
"Tags": {
|
38
|
-
"Key":
|
39
|
-
"Values":
|
100
|
+
"Key": tag_filter["Key"],
|
101
|
+
"Values": tag_filter["Values"],
|
40
102
|
"MatchOptions": ["EQUALS"],
|
41
103
|
}
|
42
104
|
}
|
105
|
+
filters.append(tag_filter_dict)
|
43
106
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
"Values": f["Values"],
|
51
|
-
"MatchOptions": ["EQUALS"],
|
52
|
-
}
|
53
|
-
}
|
54
|
-
for f in tag_filters
|
55
|
-
]
|
56
|
-
}
|
107
|
+
# Combine filters appropriately
|
108
|
+
filter_param: Optional[Dict[str, Any]] = None
|
109
|
+
if len(filters) == 1:
|
110
|
+
filter_param = filters[0]
|
111
|
+
elif len(filters) > 1:
|
112
|
+
filter_param = {"And": filters}
|
57
113
|
kwargs = {}
|
58
114
|
if filter_param:
|
59
115
|
kwargs["Filter"] = filter_param
|
@@ -81,6 +137,8 @@ def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, An
|
|
81
137
|
monthly_costs.append((month, cost))
|
82
138
|
except Exception as e:
|
83
139
|
console.log(f"[yellow]Error getting monthly trend data: {e}[/]")
|
140
|
+
if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
|
141
|
+
handle_cost_explorer_error(e, profile)
|
84
142
|
monthly_costs = []
|
85
143
|
|
86
144
|
return {
|
@@ -90,11 +148,109 @@ def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, An
|
|
90
148
|
}
|
91
149
|
|
92
150
|
|
151
|
+
def get_batch_cost_data(
|
152
|
+
sessions: List[Tuple[Session, str]],
|
153
|
+
time_range: Optional[int] = None,
|
154
|
+
tag: Optional[List[str]] = None,
|
155
|
+
max_workers: int = MAX_CONCURRENT_COST_CALLS,
|
156
|
+
) -> Dict[str, CostData]:
|
157
|
+
"""
|
158
|
+
Enterprise batch cost data retrieval with parallel processing.
|
159
|
+
|
160
|
+
Optimizes Cost Explorer API calls by processing multiple accounts concurrently
|
161
|
+
while respecting AWS rate limits and providing circuit breaker protection.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
sessions: List of (session, profile_name) tuples for batch processing
|
165
|
+
time_range: Optional time range in days for cost data
|
166
|
+
tag: Optional list of tags for filtering
|
167
|
+
max_workers: Maximum concurrent API calls (default: 10 for rate limiting)
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Dictionary mapping profile_name to CostData results
|
171
|
+
|
172
|
+
Performance: 5-10x faster than sequential processing for 10+ accounts
|
173
|
+
"""
|
174
|
+
if not sessions:
|
175
|
+
return {}
|
176
|
+
|
177
|
+
console.log(f"[blue]Enterprise batch processing: {len(sessions)} accounts with {max_workers} workers[/]")
|
178
|
+
start_time = time.time()
|
179
|
+
results = {}
|
180
|
+
|
181
|
+
# Thread-safe result collection
|
182
|
+
results_lock = threading.Lock()
|
183
|
+
|
184
|
+
def _process_single_cost_data(session_info: Tuple[Session, str]) -> Tuple[str, CostData]:
|
185
|
+
"""Process cost data for a single session."""
|
186
|
+
session, profile_name = session_info
|
187
|
+
try:
|
188
|
+
# Extract account ID from profile if it's in Organizations API format (profile@accountId)
|
189
|
+
account_id = None
|
190
|
+
if "@" in profile_name:
|
191
|
+
_, account_id = profile_name.split("@", 1)
|
192
|
+
|
193
|
+
cost_data = get_cost_data(session, time_range, tag, False, profile_name, account_id)
|
194
|
+
return profile_name, cost_data
|
195
|
+
except Exception as e:
|
196
|
+
console.log(f"[yellow]Batch cost data error for {profile_name}: {str(e)[:50]}[/]")
|
197
|
+
# Return empty cost data structure for failed accounts
|
198
|
+
return profile_name, {
|
199
|
+
"account_id": get_account_id(session) or "unknown",
|
200
|
+
"current_month": 0.0,
|
201
|
+
"last_month": 0.0,
|
202
|
+
"current_month_cost_by_service": [],
|
203
|
+
"budgets": [],
|
204
|
+
"current_period_name": "Current month's cost",
|
205
|
+
"previous_period_name": "Last month's cost",
|
206
|
+
"time_range": time_range,
|
207
|
+
"current_period_start": "",
|
208
|
+
"current_period_end": "",
|
209
|
+
"previous_period_start": "",
|
210
|
+
"previous_period_end": "",
|
211
|
+
"monthly_costs": None,
|
212
|
+
"costs_by_service": {},
|
213
|
+
}
|
214
|
+
|
215
|
+
# Execute batch processing with ThreadPoolExecutor
|
216
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
217
|
+
future_to_profile = {
|
218
|
+
executor.submit(_process_single_cost_data, session_info): session_info[1] for session_info in sessions
|
219
|
+
}
|
220
|
+
|
221
|
+
processed = 0
|
222
|
+
for future in as_completed(future_to_profile, timeout=120): # 2 minute timeout for batch
|
223
|
+
try:
|
224
|
+
profile_name, cost_data = future.result(timeout=30) # 30s per account
|
225
|
+
|
226
|
+
with results_lock:
|
227
|
+
results[profile_name] = cost_data
|
228
|
+
processed += 1
|
229
|
+
|
230
|
+
if processed % 5 == 0: # Progress logging every 5 accounts
|
231
|
+
console.log(f"[green]Batch progress: {processed}/{len(sessions)} accounts processed[/]")
|
232
|
+
|
233
|
+
except Exception as e:
|
234
|
+
profile_name = future_to_profile[future]
|
235
|
+
console.log(f"[yellow]Batch timeout for {profile_name}: {str(e)[:50]}[/]")
|
236
|
+
# Continue processing other accounts
|
237
|
+
|
238
|
+
execution_time = time.time() - start_time
|
239
|
+
console.log(
|
240
|
+
f"[green]✅ Batch cost processing completed: {len(results)}/{len(sessions)} accounts in {execution_time:.1f}s[/]"
|
241
|
+
)
|
242
|
+
console.log(f"[dim]Performance: {len(sessions) / execution_time:.1f} accounts/second[/]")
|
243
|
+
|
244
|
+
return results
|
245
|
+
|
246
|
+
|
93
247
|
def get_cost_data(
|
94
248
|
session: Session,
|
95
249
|
time_range: Optional[int] = None,
|
96
250
|
tag: Optional[List[str]] = None,
|
97
251
|
get_trend: bool = False,
|
252
|
+
profile_name: Optional[str] = None,
|
253
|
+
account_id: Optional[str] = None,
|
98
254
|
) -> CostData:
|
99
255
|
"""
|
100
256
|
Get cost data for an AWS account.
|
@@ -104,6 +260,8 @@ def get_cost_data(
|
|
104
260
|
time_range: Optional time range in days for cost data (default: current month)
|
105
261
|
tag: Optional list of tags in "Key=Value" format to filter resources.
|
106
262
|
get_trend: Optional boolean to get trend data for last 6 months (default).
|
263
|
+
profile_name: Optional AWS profile name for enhanced error messaging
|
264
|
+
account_id: Optional account ID to filter costs to specific account (multi-account support)
|
107
265
|
|
108
266
|
"""
|
109
267
|
ce = session.client("ce")
|
@@ -116,30 +274,33 @@ def get_cost_data(
|
|
116
274
|
key, value = t.split("=", 1)
|
117
275
|
tag_filters.append({"Key": key, "Values": [value]})
|
118
276
|
|
119
|
-
|
277
|
+
# Build filter parameters for Cost Explorer API
|
278
|
+
filters = []
|
279
|
+
|
280
|
+
# Add account filtering if account_id is provided (critical for multi-account scenarios)
|
281
|
+
if account_id:
|
282
|
+
account_filter = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
|
283
|
+
filters.append(account_filter)
|
284
|
+
console.log(f"[blue]Account filtering enabled: {account_id}[/]")
|
285
|
+
|
286
|
+
# Add tag filtering if provided
|
120
287
|
if tag_filters:
|
121
|
-
|
122
|
-
|
288
|
+
for tag_filter in tag_filters:
|
289
|
+
tag_filter_dict = {
|
123
290
|
"Tags": {
|
124
|
-
"Key":
|
125
|
-
"Values":
|
291
|
+
"Key": tag_filter["Key"],
|
292
|
+
"Values": tag_filter["Values"],
|
126
293
|
"MatchOptions": ["EQUALS"],
|
127
294
|
}
|
128
295
|
}
|
296
|
+
filters.append(tag_filter_dict)
|
129
297
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
"Values": f["Values"],
|
137
|
-
"MatchOptions": ["EQUALS"],
|
138
|
-
}
|
139
|
-
}
|
140
|
-
for f in tag_filters
|
141
|
-
]
|
142
|
-
}
|
298
|
+
# Combine filters appropriately
|
299
|
+
filter_param: Optional[Dict[str, Any]] = None
|
300
|
+
if len(filters) == 1:
|
301
|
+
filter_param = filters[0]
|
302
|
+
elif len(filters) > 1:
|
303
|
+
filter_param = {"And": filters}
|
143
304
|
kwargs = {}
|
144
305
|
if filter_param:
|
145
306
|
kwargs["Filter"] = filter_param
|
@@ -173,6 +334,8 @@ def get_cost_data(
|
|
173
334
|
)
|
174
335
|
except Exception as e:
|
175
336
|
console.log(f"[yellow]Error getting current period cost: {e}[/]")
|
337
|
+
if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
|
338
|
+
handle_cost_explorer_error(e, profile_name)
|
176
339
|
this_period = {"ResultsByTime": [{"Total": {"UnblendedCost": {"Amount": 0}}}]}
|
177
340
|
|
178
341
|
try:
|
@@ -187,6 +350,8 @@ def get_cost_data(
|
|
187
350
|
)
|
188
351
|
except Exception as e:
|
189
352
|
console.log(f"[yellow]Error getting previous period cost: {e}[/]")
|
353
|
+
if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
|
354
|
+
handle_cost_explorer_error(e, profile_name)
|
190
355
|
previous_period = {"ResultsByTime": [{"Total": {"UnblendedCost": {"Amount": 0}}}]}
|
191
356
|
|
192
357
|
try:
|
@@ -199,6 +364,8 @@ def get_cost_data(
|
|
199
364
|
)
|
200
365
|
except Exception as e:
|
201
366
|
console.log(f"[yellow]Error getting current period cost by service: {e}[/]")
|
367
|
+
if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
|
368
|
+
handle_cost_explorer_error(e, profile_name)
|
202
369
|
current_period_cost_by_service = {"ResultsByTime": [{"Groups": []}]}
|
203
370
|
|
204
371
|
# Aggregate cost by service across all days
|
@@ -244,11 +411,18 @@ def get_cost_data(
|
|
244
411
|
current_period_name = f"Current {time_range} days cost" if time_range else "Current month's cost"
|
245
412
|
previous_period_name = f"Previous {time_range} days cost" if time_range else "Last month's cost"
|
246
413
|
|
414
|
+
# Create costs_by_service dictionary for easy service lookup
|
415
|
+
costs_by_service = {}
|
416
|
+
for service, amount in aggregated_service_costs.items():
|
417
|
+
if amount > 0.001: # Filter out negligible costs
|
418
|
+
costs_by_service[service] = amount
|
419
|
+
|
247
420
|
return {
|
248
421
|
"account_id": account_id,
|
249
422
|
"current_month": current_period_cost,
|
250
423
|
"last_month": previous_period_cost,
|
251
424
|
"current_month_cost_by_service": aggregated_groups,
|
425
|
+
"costs_by_service": costs_by_service, # Added for multi_dashboard compatibility
|
252
426
|
"budgets": budgets_data,
|
253
427
|
"current_period_name": current_period_name,
|
254
428
|
"previous_period_name": previous_period_name,
|