runbooks 0.9.9__py3-none-any.whl → 1.0.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/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/cloud_foundations_assessment.py +626 -0
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1353 -0
- runbooks/common/aws_pricing_api.py +205 -0
- runbooks/common/aws_utils.py +2 -2
- runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
- runbooks/common/cross_account_manager.py +606 -0
- runbooks/common/date_utils.py +115 -0
- runbooks/common/enhanced_exception_handler.py +14 -7
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +127 -72
- runbooks/common/rich_utils.py +3 -3
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +10 -4
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/markdown_exporter.py +217 -2
- runbooks/finops/nat_gateway_optimizer.py +76 -20
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +28 -26
- runbooks/finops/vpc_cleanup_optimizer.py +363 -16
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1177 -94
- runbooks/inventory/discovery.md +339 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +6 -9
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +104 -9
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1279 -0
- runbooks/inventory/verify_ec2_security_groups.py +3 -1
- runbooks/inventory/vpc_analyzer.py +825 -7
- runbooks/inventory/vpc_flow_analyzer.py +36 -42
- runbooks/main.py +708 -47
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +21 -16
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +100 -12
- runbooks/remediation/base.py +4 -2
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +68 -15
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- 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/__init__.py +21 -1
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +2007 -0
- runbooks/validation/mcp_validator.py +965 -101
- runbooks/validation/terraform_citations_validator.py +363 -0
- runbooks/validation/terraform_drift_detector.py +1098 -0
- runbooks/vpc/cleanup_wrapper.py +231 -10
- runbooks/vpc/config.py +346 -73
- runbooks/vpc/cross_account_session.py +312 -0
- runbooks/vpc/heatmap_engine.py +115 -41
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1630 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +4 -2
- runbooks/vpc/unified_scenarios.py +73 -3
- runbooks/vpc/vpc_cleanup_integration.py +512 -78
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
- 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-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Dynamic Date Utilities for CloudOps Runbooks Platform
|
4
|
+
|
5
|
+
Replaces hardcoded 2024 dates with dynamic date generation following manager's
|
6
|
+
"No hardcoded values" requirement. Supports current month/year calculations
|
7
|
+
for all test data and AWS API time period generation.
|
8
|
+
|
9
|
+
Strategic Alignment: "Do one thing and do it well" - Focused date utility
|
10
|
+
KISS Principle: Simple, reusable date functions for all modules
|
11
|
+
"""
|
12
|
+
|
13
|
+
from datetime import datetime, timedelta
|
14
|
+
from typing import Dict, Tuple
|
15
|
+
|
16
|
+
|
17
|
+
def get_current_year() -> int:
|
18
|
+
"""Get current year dynamically."""
|
19
|
+
return datetime.now().year
|
20
|
+
|
21
|
+
|
22
|
+
def get_current_month_period() -> Dict[str, str]:
|
23
|
+
"""
|
24
|
+
Generate current month's start and end dates for AWS API calls.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
28
|
+
"""
|
29
|
+
now = datetime.now()
|
30
|
+
start_date = now.replace(day=1).strftime('%Y-%m-%d')
|
31
|
+
|
32
|
+
# Get last day of current month
|
33
|
+
if now.month == 12:
|
34
|
+
next_month = now.replace(year=now.year + 1, month=1, day=1)
|
35
|
+
else:
|
36
|
+
next_month = now.replace(month=now.month + 1, day=1)
|
37
|
+
|
38
|
+
end_date = (next_month - timedelta(days=1)).strftime('%Y-%m-%d')
|
39
|
+
|
40
|
+
return {
|
41
|
+
'Start': start_date,
|
42
|
+
'End': end_date
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
def get_previous_month_period() -> Dict[str, str]:
|
47
|
+
"""
|
48
|
+
Generate previous month's start and end dates for AWS API calls.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
52
|
+
"""
|
53
|
+
now = datetime.now()
|
54
|
+
|
55
|
+
# Get first day of previous month
|
56
|
+
if now.month == 1:
|
57
|
+
prev_month = now.replace(year=now.year - 1, month=12, day=1)
|
58
|
+
else:
|
59
|
+
prev_month = now.replace(month=now.month - 1, day=1)
|
60
|
+
|
61
|
+
start_date = prev_month.strftime('%Y-%m-%d')
|
62
|
+
|
63
|
+
# Get last day of previous month
|
64
|
+
end_date = (now.replace(day=1) - timedelta(days=1)).strftime('%Y-%m-%d')
|
65
|
+
|
66
|
+
return {
|
67
|
+
'Start': start_date,
|
68
|
+
'End': end_date
|
69
|
+
}
|
70
|
+
|
71
|
+
|
72
|
+
def get_test_date_period(days_back: int = 30) -> Dict[str, str]:
|
73
|
+
"""
|
74
|
+
Generate test date periods for consistent test data.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
days_back: Number of days back from today for start date
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
81
|
+
"""
|
82
|
+
end_date = datetime.now().strftime('%Y-%m-%d')
|
83
|
+
start_date = (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d')
|
84
|
+
|
85
|
+
return {
|
86
|
+
'Start': start_date,
|
87
|
+
'End': end_date
|
88
|
+
}
|
89
|
+
|
90
|
+
|
91
|
+
def get_aws_cli_example_period() -> Tuple[str, str]:
|
92
|
+
"""
|
93
|
+
Generate example date period for AWS CLI documentation.
|
94
|
+
Uses yesterday and today to ensure valid time range.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Tuple of (start_date, end_date) in YYYY-MM-DD format
|
98
|
+
"""
|
99
|
+
today = datetime.now()
|
100
|
+
yesterday = today - timedelta(days=1)
|
101
|
+
|
102
|
+
return (
|
103
|
+
yesterday.strftime('%Y-%m-%d'),
|
104
|
+
today.strftime('%Y-%m-%d')
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
def get_collection_timestamp() -> str:
|
109
|
+
"""
|
110
|
+
Generate collection timestamp for test data.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
ISO format timestamp string
|
114
|
+
"""
|
115
|
+
return datetime.now().isoformat()
|
@@ -323,6 +323,10 @@ class EnterpriseExceptionHandler:
|
|
323
323
|
Returns:
|
324
324
|
Enhanced error if performance target significantly exceeded, None otherwise
|
325
325
|
"""
|
326
|
+
# Defensive check for None values
|
327
|
+
if execution_time is None or performance_target is None or performance_target == 0:
|
328
|
+
return None
|
329
|
+
|
326
330
|
performance_ratio = execution_time / performance_target
|
327
331
|
|
328
332
|
# Only create error if performance significantly exceeded (>150% of target)
|
@@ -968,13 +972,15 @@ class EnterpriseExceptionHandler:
|
|
968
972
|
}
|
969
973
|
|
970
974
|
def _initialize_profile_recommendations(self) -> Dict[str, List[str]]:
|
971
|
-
"""Initialize profile recommendations for different operations."""
|
975
|
+
"""Initialize profile recommendations for different operations with universal support."""
|
976
|
+
import os
|
977
|
+
# Environment variable-driven profile recommendations
|
972
978
|
return {
|
973
|
-
"inventory": ["
|
974
|
-
"operate": ["
|
975
|
-
"finops": ["
|
976
|
-
"security": ["
|
977
|
-
"cfat": ["
|
979
|
+
"inventory": [os.getenv("MANAGEMENT_PROFILE", "management-profile"), os.getenv("CENTRALISED_OPS_PROFILE", "ops-profile")],
|
980
|
+
"operate": [os.getenv("CENTRALISED_OPS_PROFILE", "ops-profile"), os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
981
|
+
"finops": [os.getenv("BILLING_PROFILE", "billing-profile"), os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
982
|
+
"security": [os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
983
|
+
"cfat": [os.getenv("MANAGEMENT_PROFILE", "management-profile")],
|
978
984
|
}
|
979
985
|
|
980
986
|
def _analyze_aws_error(self, error: ClientError, error_code: str, service: str, operation: str) -> Dict[str, Any]:
|
@@ -987,7 +993,8 @@ class EnterpriseExceptionHandler:
|
|
987
993
|
# Service-specific adjustments
|
988
994
|
if service == "ce" and error_code == "AccessDenied":
|
989
995
|
# Cost Explorer requires special billing permissions
|
990
|
-
|
996
|
+
import os
|
997
|
+
base_analysis["recommended_profiles"] = [os.getenv("BILLING_PROFILE", "billing-profile")]
|
991
998
|
|
992
999
|
return base_analysis
|
993
1000
|
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""
|
2
|
+
Environment Variable Utilities - Enterprise Hardcoded Value Elimination
|
3
|
+
|
4
|
+
This module provides enterprise-compliant environment variable utilities
|
5
|
+
that enforce zero hardcoded defaults policy across the entire codebase.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
|
12
|
+
def get_required_env(var_name: str) -> str:
|
13
|
+
"""
|
14
|
+
Get required string environment variable - NO hardcoded defaults.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
var_name: Environment variable name
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
Environment variable value
|
21
|
+
|
22
|
+
Raises:
|
23
|
+
ValueError: If environment variable is not set
|
24
|
+
"""
|
25
|
+
value = os.getenv(var_name)
|
26
|
+
if value is None:
|
27
|
+
raise ValueError(f"Environment variable {var_name} required - no hardcoded defaults allowed")
|
28
|
+
return value
|
29
|
+
|
30
|
+
|
31
|
+
def get_required_env_int(var_name: str) -> int:
|
32
|
+
"""
|
33
|
+
Get required integer environment variable - NO hardcoded defaults.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
var_name: Environment variable name
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
Environment variable value as integer
|
40
|
+
|
41
|
+
Raises:
|
42
|
+
ValueError: If environment variable is not set or not a valid integer
|
43
|
+
"""
|
44
|
+
value = get_required_env(var_name)
|
45
|
+
try:
|
46
|
+
return int(value)
|
47
|
+
except ValueError as e:
|
48
|
+
raise ValueError(f"Environment variable {var_name} must be a valid integer, got: {value}") from e
|
49
|
+
|
50
|
+
|
51
|
+
def get_required_env_float(var_name: str) -> float:
|
52
|
+
"""
|
53
|
+
Get required float environment variable - NO hardcoded defaults.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
var_name: Environment variable name
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Environment variable value as float
|
60
|
+
|
61
|
+
Raises:
|
62
|
+
ValueError: If environment variable is not set or not a valid float
|
63
|
+
"""
|
64
|
+
value = get_required_env(var_name)
|
65
|
+
try:
|
66
|
+
return float(value)
|
67
|
+
except ValueError as e:
|
68
|
+
raise ValueError(f"Environment variable {var_name} must be a valid float, got: {value}") from e
|
69
|
+
|
70
|
+
|
71
|
+
def get_required_env_bool(var_name: str) -> bool:
|
72
|
+
"""
|
73
|
+
Get required boolean environment variable - NO hardcoded defaults.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
var_name: Environment variable name
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
Environment variable value as boolean
|
80
|
+
|
81
|
+
Raises:
|
82
|
+
ValueError: If environment variable is not set
|
83
|
+
"""
|
84
|
+
value = get_required_env(var_name).lower()
|
85
|
+
if value in ('true', '1', 'yes', 'on'):
|
86
|
+
return True
|
87
|
+
elif value in ('false', '0', 'no', 'off'):
|
88
|
+
return False
|
89
|
+
else:
|
90
|
+
raise ValueError(f"Environment variable {var_name} must be a valid boolean (true/false), got: {value}")
|
91
|
+
|
92
|
+
|
93
|
+
# Legacy compatibility function names for existing code
|
94
|
+
_get_required_env_float = get_required_env_float
|
95
|
+
_get_required_env_int = get_required_env_int
|
96
|
+
_get_required_env = get_required_env
|
@@ -79,10 +79,11 @@ class MCPCostExplorerIntegration:
|
|
79
79
|
performance_target_seconds: Performance target for operations
|
80
80
|
"""
|
81
81
|
|
82
|
-
# Profile configuration with
|
83
|
-
|
84
|
-
self.
|
85
|
-
self.
|
82
|
+
# Profile configuration with universal environment support
|
83
|
+
from runbooks.common.profile_utils import get_profile_for_operation
|
84
|
+
self.billing_profile = billing_profile or get_profile_for_operation("billing", None)
|
85
|
+
self.management_profile = management_profile or get_profile_for_operation("management", None)
|
86
|
+
self.single_account_profile = single_account_profile or get_profile_for_operation("single_account", None)
|
86
87
|
|
87
88
|
# Validation configuration
|
88
89
|
self.tolerance_percent = tolerance_percent
|
@@ -150,6 +150,11 @@ class EnterpriseMCPIntegrator:
|
|
150
150
|
self.validation_threshold = 99.5 # Enterprise accuracy requirement
|
151
151
|
self.tolerance_percent = 5.0 # ±5% tolerance for validation
|
152
152
|
|
153
|
+
# Organizations API caching to prevent duplicate calls
|
154
|
+
self._organizations_cache = {}
|
155
|
+
self._organizations_cache_timestamp = None
|
156
|
+
self._cache_ttl_minutes = 30 # 30-minute TTL for Organizations data
|
157
|
+
|
153
158
|
# Initialize enterprise profile architecture
|
154
159
|
self._initialize_enterprise_profiles()
|
155
160
|
|
@@ -176,6 +181,27 @@ class EnterpriseMCPIntegrator:
|
|
176
181
|
except Exception as e:
|
177
182
|
print_error(f"Failed to initialize {profile_type} profile: {str(e)}")
|
178
183
|
|
184
|
+
def _is_organizations_cache_valid(self) -> bool:
|
185
|
+
"""Check if Organizations cache is still valid."""
|
186
|
+
if not self._organizations_cache_timestamp:
|
187
|
+
return False
|
188
|
+
|
189
|
+
cache_age_minutes = (datetime.now() - self._organizations_cache_timestamp).total_seconds() / 60
|
190
|
+
return cache_age_minutes < self._cache_ttl_minutes
|
191
|
+
|
192
|
+
def get_cached_organization_accounts(self) -> Optional[List[Dict[str, Any]]]:
|
193
|
+
"""Get cached Organizations data if valid."""
|
194
|
+
if self._is_organizations_cache_valid() and 'accounts' in self._organizations_cache:
|
195
|
+
print_info("Using cached Organizations data (performance optimization)")
|
196
|
+
return self._organizations_cache['accounts']
|
197
|
+
return None
|
198
|
+
|
199
|
+
def cache_organization_accounts(self, accounts: List[Dict[str, Any]]) -> None:
|
200
|
+
"""Cache Organizations data for performance optimization."""
|
201
|
+
self._organizations_cache = {'accounts': accounts}
|
202
|
+
self._organizations_cache_timestamp = datetime.now()
|
203
|
+
print_success(f"Cached Organizations data: {len(accounts)} accounts (TTL: {self._cache_ttl_minutes}min)")
|
204
|
+
|
179
205
|
async def validate_inventory_operations(self, inventory_data: Dict[str, Any]) -> MCPValidationResult:
|
180
206
|
"""
|
181
207
|
Validate inventory operations using MCP integration.
|
@@ -496,11 +522,32 @@ class EnterpriseMCPIntegrator:
|
|
496
522
|
async def _validate_resource_counts(self, inventory_data: Dict, progress, task) -> None:
|
497
523
|
"""Validate resource counts across services."""
|
498
524
|
try:
|
499
|
-
|
525
|
+
# Enhanced: Handle both dict and string inputs for robust data structure handling
|
526
|
+
if isinstance(inventory_data, str):
|
527
|
+
# Handle case where inventory_data is a JSON string
|
528
|
+
try:
|
529
|
+
inventory_data = json.loads(inventory_data)
|
530
|
+
except json.JSONDecodeError:
|
531
|
+
print_warning(f"Invalid JSON string in inventory data")
|
532
|
+
return
|
533
|
+
|
534
|
+
resources = inventory_data.get("resources", []) if isinstance(inventory_data, dict) else []
|
535
|
+
|
536
|
+
# Enhanced: Ensure resources is always a list
|
537
|
+
if not isinstance(resources, list):
|
538
|
+
resources = []
|
539
|
+
|
500
540
|
service_counts = {}
|
501
541
|
|
502
542
|
for resource in resources:
|
503
|
-
|
543
|
+
# Enhanced: Handle both dict and string resource entries
|
544
|
+
if isinstance(resource, dict):
|
545
|
+
service = resource.get("service", "unknown")
|
546
|
+
elif isinstance(resource, str):
|
547
|
+
service = "unknown"
|
548
|
+
else:
|
549
|
+
service = "unknown"
|
550
|
+
|
504
551
|
service_counts[service] = service_counts.get(service, 0) + 1
|
505
552
|
|
506
553
|
progress.update(task, advance=40, description=f"Validated {len(resources)} resources...")
|