runbooks 0.9.1__py3-none-any.whl → 0.9.4__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 +2 -1
- runbooks/finops/accuracy_cross_validator.py +12 -3
- 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 +771 -0
- runbooks/finops/multi_dashboard.py +30 -15
- 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.4.dist-info/METADATA +563 -0
- {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/RECORD +41 -38
- {runbooks-0.9.1.dist-info → runbooks-0.9.4.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.1.dist-info/METADATA +0 -308
- {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/WHEEL +0 -0
- {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.1.dist-info → runbooks-0.9.4.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,72 @@
|
|
1
1
|
"""
|
2
2
|
🚨 HIGH-RISK: WorkSpaces Management - Analyze and manage WorkSpaces with deletion capabilities.
|
3
|
+
|
4
|
+
JIRA AWSO-24: Enhanced WorkSpaces cleanup with cost calculation for $12,518 annual savings
|
5
|
+
Accounts: 339712777494, 802669565615, 142964829704, 507583929055
|
6
|
+
Types: STANDARD, PERFORMANCE, VALUE in AUTO_STOP mode
|
3
7
|
"""
|
4
8
|
|
5
9
|
import logging
|
6
10
|
from datetime import datetime, timedelta, timezone
|
11
|
+
from typing import Dict, List, Optional
|
7
12
|
|
8
13
|
import click
|
9
14
|
from botocore.exceptions import ClientError
|
10
15
|
|
11
16
|
from .commons import display_aws_account_info, get_client, write_to_csv
|
17
|
+
from ..common.rich_utils import (
|
18
|
+
console, print_header, print_success, print_error, print_warning,
|
19
|
+
create_table, create_progress_bar, format_cost
|
20
|
+
)
|
12
21
|
|
13
22
|
logger = logging.getLogger(__name__)
|
14
23
|
|
15
24
|
|
25
|
+
def calculate_workspace_monthly_cost(workspace_bundle_id: str, running_mode: str) -> float:
|
26
|
+
"""
|
27
|
+
Calculate monthly cost for WorkSpace based on bundle and running mode.
|
28
|
+
|
29
|
+
JIRA AWSO-24: Cost calculations for $12,518 annual savings target
|
30
|
+
Based on AWS WorkSpaces pricing: https://aws.amazon.com/workspaces/pricing/
|
31
|
+
"""
|
32
|
+
# WorkSpaces pricing by bundle type (monthly USD)
|
33
|
+
bundle_costs = {
|
34
|
+
# Value bundles
|
35
|
+
"wsb-bh8rsxt14": {"name": "Value", "monthly": 25.0, "hourly": 0.22}, # Windows 10 Value
|
36
|
+
"wsb-3t36q8qkj": {"name": "Value", "monthly": 25.0, "hourly": 0.22}, # Amazon Linux 2 Value
|
37
|
+
|
38
|
+
# Standard bundles
|
39
|
+
"wsb-92tn3b7gx": {"name": "Standard", "monthly": 35.0, "hourly": 0.50}, # Windows 10 Standard
|
40
|
+
"wsb-2bs6k5lgj": {"name": "Standard", "monthly": 35.0, "hourly": 0.50}, # Amazon Linux 2 Standard
|
41
|
+
|
42
|
+
# Performance bundles
|
43
|
+
"wsb-gk1wpk43z": {"name": "Performance", "monthly": 68.0, "hourly": 0.85}, # Windows 10 Performance
|
44
|
+
"wsb-1b5w6vnzg": {"name": "Performance", "monthly": 68.0, "hourly": 0.85}, # Amazon Linux 2 Performance
|
45
|
+
|
46
|
+
# PowerPro bundles
|
47
|
+
"wsb-8vbljg4r6": {"name": "PowerPro", "monthly": 134.0, "hourly": 1.50}, # Windows 10 PowerPro
|
48
|
+
"wsb-vbljg4r61": {"name": "PowerPro", "monthly": 134.0, "hourly": 1.50}, # Amazon Linux 2 PowerPro
|
49
|
+
|
50
|
+
# Graphics bundles
|
51
|
+
"wsb-1pzkp0bx8": {"name": "Graphics", "monthly": 144.0, "hourly": 1.75}, # Windows 10 Graphics
|
52
|
+
"wsb-pszkp0bx9": {"name": "Graphics", "monthly": 144.0, "hourly": 1.75}, # Amazon Linux 2 Graphics
|
53
|
+
}
|
54
|
+
|
55
|
+
# Get bundle info or use default
|
56
|
+
bundle_info = bundle_costs.get(workspace_bundle_id, {"name": "Standard", "monthly": 35.0, "hourly": 0.50})
|
57
|
+
|
58
|
+
# Calculate cost based on running mode
|
59
|
+
if running_mode.upper() == "AUTO_STOP":
|
60
|
+
# Auto-stop: Pay monthly fee + hourly usage (simplified to monthly for unused)
|
61
|
+
return bundle_info["monthly"]
|
62
|
+
elif running_mode.upper() == "ALWAYS_ON":
|
63
|
+
# Always-on: Pay monthly fee only
|
64
|
+
return bundle_info["monthly"]
|
65
|
+
else:
|
66
|
+
# Unknown mode, use monthly
|
67
|
+
return bundle_info["monthly"]
|
68
|
+
|
69
|
+
|
16
70
|
def get_workspace_usage_by_hours(workspace_id, start_time, end_time):
|
17
71
|
"""Get WorkSpace usage hours from CloudWatch metrics."""
|
18
72
|
try:
|
@@ -44,30 +98,43 @@ def get_workspace_usage_by_hours(workspace_id, start_time, end_time):
|
|
44
98
|
@click.option("--delete-unused", is_flag=True, help="🚨 HIGH-RISK: Delete unused WorkSpaces")
|
45
99
|
@click.option("--unused-days", default=90, help="Days threshold for considering WorkSpace unused")
|
46
100
|
@click.option("--confirm", is_flag=True, help="Skip confirmation prompts (dangerous!)")
|
101
|
+
@click.option("--calculate-savings", is_flag=True, help="Calculate cost savings for cleanup")
|
102
|
+
@click.option("--analyze", is_flag=True, help="Perform detailed cost analysis")
|
103
|
+
@click.option("--dry-run", is_flag=True, default=True, help="Preview actions without execution")
|
47
104
|
def get_workspaces(
|
48
105
|
output_file: str = "/tmp/workspaces.csv",
|
49
106
|
days: int = 30,
|
50
107
|
delete_unused: bool = False,
|
51
108
|
unused_days: int = 90,
|
52
109
|
confirm: bool = False,
|
110
|
+
calculate_savings: bool = False,
|
111
|
+
analyze: bool = False,
|
112
|
+
dry_run: bool = True,
|
53
113
|
):
|
54
|
-
"""
|
55
|
-
|
114
|
+
"""
|
115
|
+
🚨 HIGH-RISK: Analyze WorkSpaces usage and optionally delete unused ones.
|
116
|
+
|
117
|
+
JIRA AWSO-24: Enhanced WorkSpaces cleanup with cost calculation for $12,518 annual savings
|
118
|
+
"""
|
119
|
+
|
120
|
+
print_header("WorkSpaces Cost Optimization Analysis", "v0.9.1")
|
121
|
+
|
56
122
|
# HIGH-RISK OPERATION WARNING
|
57
123
|
if delete_unused and not confirm:
|
58
|
-
|
59
|
-
|
124
|
+
print_warning("🚨 HIGH-RISK OPERATION: WorkSpace deletion")
|
125
|
+
print_warning("This operation will permanently delete WorkSpaces and all user data")
|
60
126
|
if not click.confirm("Do you want to continue?"):
|
61
|
-
|
127
|
+
print_error("Operation cancelled by user")
|
62
128
|
return
|
63
129
|
|
64
|
-
|
130
|
+
account_info = display_aws_account_info()
|
131
|
+
console.print(f"[cyan]Analyzing WorkSpaces in {account_info}[/cyan]")
|
65
132
|
|
66
133
|
try:
|
67
134
|
ws_client = get_client("workspaces")
|
68
135
|
|
69
|
-
# Get all WorkSpaces
|
70
|
-
|
136
|
+
# Get all WorkSpaces with progress bar
|
137
|
+
console.print("[yellow]Collecting WorkSpaces data...[/yellow]")
|
71
138
|
paginator = ws_client.get_paginator("describe_workspaces")
|
72
139
|
data = []
|
73
140
|
|
@@ -76,19 +143,29 @@ def get_workspaces(
|
|
76
143
|
start_time = end_time - timedelta(days=days)
|
77
144
|
unused_threshold = end_time - timedelta(days=unused_days)
|
78
145
|
|
79
|
-
|
146
|
+
console.print(f"[dim]Analyzing usage from {start_time.strftime('%Y-%m-%d')} to {end_time.strftime('%Y-%m-%d')}[/dim]")
|
80
147
|
|
81
|
-
|
148
|
+
# Collect all workspaces first for progress tracking
|
149
|
+
all_workspaces = []
|
82
150
|
for page in paginator.paginate():
|
83
151
|
workspaces = page.get("Workspaces", [])
|
84
|
-
|
85
|
-
|
86
|
-
|
152
|
+
all_workspaces.extend(workspaces)
|
153
|
+
|
154
|
+
total_cost = 0.0
|
155
|
+
unused_cost = 0.0
|
156
|
+
|
157
|
+
with create_progress_bar() as progress:
|
158
|
+
task_id = progress.add_task(
|
159
|
+
f"Analyzing {len(all_workspaces)} WorkSpaces...",
|
160
|
+
total=len(all_workspaces)
|
161
|
+
)
|
162
|
+
|
163
|
+
for workspace in all_workspaces:
|
87
164
|
workspace_id = workspace["WorkspaceId"]
|
88
165
|
username = workspace["UserName"]
|
89
166
|
state = workspace["State"]
|
90
|
-
|
91
|
-
|
167
|
+
bundle_id = workspace["BundleId"]
|
168
|
+
running_mode = workspace["WorkspaceProperties"]["RunningMode"]
|
92
169
|
|
93
170
|
# Get connection status
|
94
171
|
try:
|
@@ -121,46 +198,120 @@ def get_workspaces(
|
|
121
198
|
# Determine if workspace is unused
|
122
199
|
is_unused = last_connection is None or last_connection < unused_threshold
|
123
200
|
|
201
|
+
# Calculate cost (JIRA AWSO-24 enhancement)
|
202
|
+
monthly_cost = 0.0
|
203
|
+
if calculate_savings or analyze:
|
204
|
+
monthly_cost = calculate_workspace_monthly_cost(bundle_id, running_mode)
|
205
|
+
total_cost += monthly_cost
|
206
|
+
if is_unused:
|
207
|
+
unused_cost += monthly_cost
|
208
|
+
|
124
209
|
workspace_data = {
|
125
210
|
"WorkspaceId": workspace_id,
|
126
211
|
"UserName": username,
|
127
212
|
"State": state,
|
128
|
-
"RunningMode":
|
213
|
+
"RunningMode": running_mode,
|
129
214
|
"OperatingSystem": workspace["WorkspaceProperties"]["OperatingSystemName"],
|
130
|
-
"BundleId":
|
215
|
+
"BundleId": bundle_id,
|
131
216
|
"LastConnection": last_connection_str,
|
132
217
|
"DaysSinceConnection": days_since_connection,
|
133
218
|
"ConnectionState": connection_state,
|
134
219
|
f"UsageHours_{days}days": usage_hours,
|
135
220
|
"IsUnused": is_unused,
|
136
221
|
"UnusedThreshold": f"{unused_days} days",
|
222
|
+
"MonthlyCost": monthly_cost,
|
223
|
+
"AnnualCost": monthly_cost * 12,
|
137
224
|
}
|
138
225
|
|
139
226
|
data.append(workspace_data)
|
140
|
-
|
141
|
-
# Log status
|
142
|
-
if is_unused:
|
143
|
-
logger.warning(f" ⚠ UNUSED: Last connection {days_since_connection} days ago")
|
144
|
-
else:
|
145
|
-
logger.info(f" ✓ Active: {usage_hours}h usage in {days} days")
|
227
|
+
progress.advance(task_id)
|
146
228
|
|
147
229
|
# Export data
|
148
230
|
write_to_csv(data, output_file)
|
149
|
-
|
231
|
+
print_success(f"WorkSpaces analysis exported to: {output_file}")
|
150
232
|
|
151
233
|
# Analyze unused WorkSpaces
|
152
234
|
unused_workspaces = [ws for ws in data if ws["IsUnused"]]
|
153
235
|
|
154
|
-
|
155
|
-
|
156
|
-
|
236
|
+
# Create summary table with Rich CLI
|
237
|
+
print_header("WorkSpaces Analysis Summary")
|
238
|
+
|
239
|
+
summary_table = create_table(
|
240
|
+
title="WorkSpaces Cost Analysis - JIRA AWSO-24",
|
241
|
+
columns=[
|
242
|
+
{"header": "Metric", "style": "cyan"},
|
243
|
+
{"header": "Value", "style": "green bold"},
|
244
|
+
{"header": "Monthly Cost", "style": "red"},
|
245
|
+
{"header": "Annual Cost", "style": "red bold"}
|
246
|
+
]
|
247
|
+
)
|
248
|
+
|
249
|
+
# Basic metrics
|
250
|
+
summary_table.add_row(
|
251
|
+
"Total WorkSpaces",
|
252
|
+
str(len(data)),
|
253
|
+
format_cost(total_cost) if calculate_savings or analyze else "N/A",
|
254
|
+
format_cost(total_cost * 12) if calculate_savings or analyze else "N/A"
|
255
|
+
)
|
256
|
+
|
257
|
+
summary_table.add_row(
|
258
|
+
f"Unused WorkSpaces (>{unused_days} days)",
|
259
|
+
str(len(unused_workspaces)),
|
260
|
+
format_cost(unused_cost) if calculate_savings or analyze else "N/A",
|
261
|
+
format_cost(unused_cost * 12) if calculate_savings or analyze else "N/A"
|
262
|
+
)
|
263
|
+
|
264
|
+
if calculate_savings or analyze:
|
265
|
+
potential_savings_monthly = unused_cost
|
266
|
+
potential_savings_annual = unused_cost * 12
|
267
|
+
|
268
|
+
summary_table.add_row(
|
269
|
+
"🎯 Potential Savings",
|
270
|
+
f"{len(unused_workspaces)} WorkSpaces",
|
271
|
+
format_cost(potential_savings_monthly),
|
272
|
+
format_cost(potential_savings_annual)
|
273
|
+
)
|
274
|
+
|
275
|
+
console.print(summary_table)
|
157
276
|
|
158
277
|
if unused_workspaces:
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
278
|
+
print_warning(f"⚠ Found {len(unused_workspaces)} unused WorkSpaces:")
|
279
|
+
|
280
|
+
# Create detailed unused workspaces table
|
281
|
+
unused_table = create_table(
|
282
|
+
title="Unused WorkSpaces Details",
|
283
|
+
columns=[
|
284
|
+
{"header": "WorkSpace ID", "style": "cyan"},
|
285
|
+
{"header": "Username", "style": "blue"},
|
286
|
+
{"header": "Days Since Connection", "style": "yellow"},
|
287
|
+
{"header": "Running Mode", "style": "green"},
|
288
|
+
{"header": "Monthly Cost", "style": "red"},
|
289
|
+
{"header": "State", "style": "magenta"}
|
290
|
+
]
|
291
|
+
)
|
292
|
+
|
293
|
+
for ws in unused_workspaces[:10]: # Show first 10 for readability
|
294
|
+
unused_table.add_row(
|
295
|
+
ws['WorkspaceId'],
|
296
|
+
ws['UserName'],
|
297
|
+
str(ws['DaysSinceConnection']),
|
298
|
+
ws['RunningMode'],
|
299
|
+
format_cost(ws['MonthlyCost']) if ws['MonthlyCost'] > 0 else "N/A",
|
300
|
+
ws['State']
|
163
301
|
)
|
302
|
+
|
303
|
+
console.print(unused_table)
|
304
|
+
|
305
|
+
if len(unused_workspaces) > 10:
|
306
|
+
console.print(f"[dim]... and {len(unused_workspaces) - 10} more unused WorkSpaces[/dim]")
|
307
|
+
|
308
|
+
# Target validation (JIRA AWSO-24: $12,518 annual savings)
|
309
|
+
if calculate_savings or analyze:
|
310
|
+
target_annual_savings = 12518.0 # JIRA AWSO-24 target
|
311
|
+
if potential_savings_annual >= target_annual_savings * 0.8: # 80% of target
|
312
|
+
print_success(f"🎯 Target Achievement: {potential_savings_annual/target_annual_savings*100:.1f}% of $12,518 annual savings target")
|
313
|
+
else:
|
314
|
+
print_warning(f"📊 Analysis: {potential_savings_annual/target_annual_savings*100:.1f}% of $12,518 annual savings target")
|
164
315
|
|
165
316
|
# Handle deletion of unused WorkSpaces
|
166
317
|
if delete_unused and unused_workspaces:
|
runbooks/security/__init__.py
CHANGED
@@ -210,8 +210,10 @@ from .run_script import parse_arguments
|
|
210
210
|
from .security_baseline_tester import SecurityBaselineTester
|
211
211
|
from .security_export import SecurityExporter
|
212
212
|
|
213
|
+
# Import centralized version from main runbooks package
|
214
|
+
from runbooks import __version__
|
215
|
+
|
213
216
|
# Version info
|
214
|
-
__version__ = "1.2.0"
|
215
217
|
__author__ = "CloudOps Enterprise Security Team"
|
216
218
|
|
217
219
|
# Public API
|
runbooks/sre/__init__.py
CHANGED
runbooks/utils/__init__.py
CHANGED
@@ -22,6 +22,15 @@ except ImportError:
|
|
22
22
|
# Legacy utilities
|
23
23
|
from runbooks.utils.logger import configure_logger
|
24
24
|
|
25
|
+
# Version management utilities
|
26
|
+
from runbooks.utils.version_validator import (
|
27
|
+
check_pyproject_version,
|
28
|
+
get_all_module_versions,
|
29
|
+
print_version_report,
|
30
|
+
validate_version_consistency,
|
31
|
+
VersionDriftError,
|
32
|
+
)
|
33
|
+
|
25
34
|
|
26
35
|
def setup_logging(debug: bool = False, log_file: Optional[str] = None) -> None:
|
27
36
|
"""
|
@@ -65,9 +74,9 @@ def setup_logging(debug: bool = False, log_file: Optional[str] = None) -> None:
|
|
65
74
|
# Fallback to standard logging
|
66
75
|
import logging
|
67
76
|
|
68
|
-
|
77
|
+
log_level_value = logging.DEBUG if debug else logging.INFO
|
69
78
|
logging.basicConfig(
|
70
|
-
level=
|
79
|
+
level=log_level_value,
|
71
80
|
format="%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
|
72
81
|
handlers=[logging.StreamHandler(sys.stderr)],
|
73
82
|
)
|
@@ -76,12 +85,71 @@ def setup_logging(debug: bool = False, log_file: Optional[str] = None) -> None:
|
|
76
85
|
log_path = Path(log_file)
|
77
86
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
78
87
|
file_handler = logging.FileHandler(log_path)
|
79
|
-
file_handler.
|
80
|
-
|
81
|
-
)
|
88
|
+
file_handler.setLevel(log_level_value)
|
89
|
+
formatter = logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s")
|
90
|
+
file_handler.setFormatter(formatter)
|
82
91
|
logging.getLogger().addHandler(file_handler)
|
83
92
|
|
84
|
-
|
93
|
+
|
94
|
+
def setup_enhanced_logging(log_level: str = "INFO", json_output: bool = False, debug: bool = False) -> None:
|
95
|
+
"""
|
96
|
+
Configure enhanced enterprise logging with Rich CLI integration and user-type specific output.
|
97
|
+
|
98
|
+
This function initializes the global enhanced logger that provides:
|
99
|
+
- User-type specific formatting (DEBUG=tech, INFO=standard, WARNING=business, ERROR=all)
|
100
|
+
- Rich CLI integration with beautiful formatting
|
101
|
+
- Structured JSON output option for programmatic use
|
102
|
+
- AWS API tracing for technical users
|
103
|
+
- Business recommendations for warning level
|
104
|
+
- Clear error solutions for all users
|
105
|
+
|
106
|
+
Args:
|
107
|
+
log_level: Log level (DEBUG, INFO, WARNING, ERROR)
|
108
|
+
json_output: Enable structured JSON output for programmatic use
|
109
|
+
debug: Legacy debug flag for backward compatibility
|
110
|
+
"""
|
111
|
+
try:
|
112
|
+
from runbooks.enterprise.logging import configure_enterprise_logging, get_context_logger
|
113
|
+
from runbooks.common.rich_utils import get_context_aware_console
|
114
|
+
|
115
|
+
# Override level if debug flag is set (backward compatibility)
|
116
|
+
if debug:
|
117
|
+
log_level = "DEBUG"
|
118
|
+
|
119
|
+
# Get context-aware console for Rich CLI integration
|
120
|
+
try:
|
121
|
+
rich_console = get_context_aware_console()
|
122
|
+
except ImportError:
|
123
|
+
rich_console = None
|
124
|
+
|
125
|
+
# Configure global enhanced logger
|
126
|
+
logger = configure_enterprise_logging(
|
127
|
+
level=log_level,
|
128
|
+
rich_console=rich_console,
|
129
|
+
json_output=json_output
|
130
|
+
)
|
131
|
+
|
132
|
+
# Log initialization success with user-type appropriate message
|
133
|
+
if log_level == "DEBUG":
|
134
|
+
logger.debug_tech(
|
135
|
+
"Enhanced logging initialized with Rich CLI integration",
|
136
|
+
aws_api={"service": "logging", "operation": "initialize"},
|
137
|
+
duration=0.001
|
138
|
+
)
|
139
|
+
elif log_level == "INFO":
|
140
|
+
logger.info_standard("CloudOps Runbooks logging initialized")
|
141
|
+
elif log_level == "WARNING":
|
142
|
+
logger.warning_business(
|
143
|
+
"Business-focused logging enabled",
|
144
|
+
recommendation="Use --log-level INFO for standard operations"
|
145
|
+
)
|
146
|
+
else:
|
147
|
+
logger.error_all("Minimal error-only logging enabled")
|
148
|
+
|
149
|
+
except ImportError as e:
|
150
|
+
# Fallback to standard logging if enhanced logging not available
|
151
|
+
setup_logging(debug=debug)
|
152
|
+
print(f"Warning: Enhanced logging not available, falling back to standard logging: {e}")
|
85
153
|
|
86
154
|
|
87
155
|
def validate_aws_profile(profile: str) -> bool:
|
@@ -196,9 +264,16 @@ def retry_with_backoff(max_retries: int = 3, backoff_factor: float = 1.0):
|
|
196
264
|
|
197
265
|
__all__ = [
|
198
266
|
"setup_logging",
|
267
|
+
"setup_enhanced_logging",
|
199
268
|
"validate_aws_profile",
|
200
269
|
"ensure_directory",
|
201
270
|
"format_size",
|
202
271
|
"retry_with_backoff",
|
203
272
|
"configure_logger",
|
273
|
+
# Version management
|
274
|
+
"check_pyproject_version",
|
275
|
+
"get_all_module_versions",
|
276
|
+
"print_version_report",
|
277
|
+
"validate_version_consistency",
|
278
|
+
"VersionDriftError",
|
204
279
|
]
|
@@ -0,0 +1,241 @@
|
|
1
|
+
"""
|
2
|
+
Version Management Validation Utilities
|
3
|
+
|
4
|
+
This module provides utilities to detect and prevent version drift across
|
5
|
+
the runbooks package and its modules.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import importlib
|
9
|
+
import sys
|
10
|
+
import warnings
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Dict, List, Optional, Tuple
|
13
|
+
|
14
|
+
# Avoid circular imports by defining central version directly
|
15
|
+
CENTRAL_VERSION = "0.9.3" # Must match runbooks.__init__.__version__
|
16
|
+
|
17
|
+
|
18
|
+
class VersionDriftError(Exception):
|
19
|
+
"""Raised when version drift is detected in CI/CD pipeline."""
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
def get_all_module_versions() -> Dict[str, str]:
|
24
|
+
"""
|
25
|
+
Collect versions from all runbooks modules.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
Dict mapping module names to their reported versions
|
29
|
+
"""
|
30
|
+
modules = [
|
31
|
+
"runbooks",
|
32
|
+
"runbooks.finops",
|
33
|
+
"runbooks.operate",
|
34
|
+
"runbooks.security",
|
35
|
+
"runbooks.cfat",
|
36
|
+
"runbooks.inventory",
|
37
|
+
"runbooks.remediation",
|
38
|
+
"runbooks.vpc",
|
39
|
+
"runbooks.sre",
|
40
|
+
"runbooks.cloudops"
|
41
|
+
]
|
42
|
+
|
43
|
+
versions = {}
|
44
|
+
|
45
|
+
for module_name in modules:
|
46
|
+
try:
|
47
|
+
# Import the module
|
48
|
+
module = importlib.import_module(module_name)
|
49
|
+
|
50
|
+
# Get version
|
51
|
+
if hasattr(module, '__version__'):
|
52
|
+
versions[module_name] = module.__version__
|
53
|
+
else:
|
54
|
+
versions[module_name] = "No version found"
|
55
|
+
|
56
|
+
except ImportError as e:
|
57
|
+
versions[module_name] = f"Import error: {e}"
|
58
|
+
except Exception as e:
|
59
|
+
versions[module_name] = f"Error: {e}"
|
60
|
+
|
61
|
+
return versions
|
62
|
+
|
63
|
+
|
64
|
+
def check_pyproject_version() -> Tuple[bool, str, str]:
|
65
|
+
"""
|
66
|
+
Check if pyproject.toml version matches centralized version.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Tuple of (is_matching, pyproject_version, central_version)
|
70
|
+
"""
|
71
|
+
try:
|
72
|
+
from importlib.metadata import version as _pkg_version
|
73
|
+
pyproject_version = _pkg_version("runbooks")
|
74
|
+
return (pyproject_version == CENTRAL_VERSION, pyproject_version, CENTRAL_VERSION)
|
75
|
+
except Exception as e:
|
76
|
+
return (False, f"Error reading pyproject.toml: {e}", CENTRAL_VERSION)
|
77
|
+
|
78
|
+
|
79
|
+
def validate_version_consistency(strict: bool = False) -> Dict[str, any]:
|
80
|
+
"""
|
81
|
+
Validate version consistency across all modules.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
strict: If True, raise VersionDriftError on inconsistencies
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
Dictionary with validation results
|
88
|
+
|
89
|
+
Raises:
|
90
|
+
VersionDriftError: If strict=True and inconsistencies found
|
91
|
+
"""
|
92
|
+
results = {
|
93
|
+
"central_version": CENTRAL_VERSION,
|
94
|
+
"module_versions": get_all_module_versions(),
|
95
|
+
"pyproject_check": check_pyproject_version(),
|
96
|
+
"inconsistencies": [],
|
97
|
+
"all_consistent": True
|
98
|
+
}
|
99
|
+
|
100
|
+
# Check module versions
|
101
|
+
for module_name, module_version in results["module_versions"].items():
|
102
|
+
if module_name == "runbooks":
|
103
|
+
continue # Skip root module
|
104
|
+
|
105
|
+
if isinstance(module_version, str) and not module_version.startswith(("Error:", "No version", "Import error:")):
|
106
|
+
if module_version != CENTRAL_VERSION:
|
107
|
+
inconsistency = {
|
108
|
+
"module": module_name,
|
109
|
+
"expected": CENTRAL_VERSION,
|
110
|
+
"found": module_version,
|
111
|
+
"type": "module_version_mismatch"
|
112
|
+
}
|
113
|
+
results["inconsistencies"].append(inconsistency)
|
114
|
+
results["all_consistent"] = False
|
115
|
+
|
116
|
+
# Check pyproject.toml
|
117
|
+
is_matching, pyproject_version, _ = results["pyproject_check"]
|
118
|
+
if not is_matching and not pyproject_version.startswith("Error:"):
|
119
|
+
inconsistency = {
|
120
|
+
"module": "pyproject.toml",
|
121
|
+
"expected": CENTRAL_VERSION,
|
122
|
+
"found": pyproject_version,
|
123
|
+
"type": "pyproject_version_mismatch"
|
124
|
+
}
|
125
|
+
results["inconsistencies"].append(inconsistency)
|
126
|
+
results["all_consistent"] = False
|
127
|
+
|
128
|
+
# Raise error in strict mode
|
129
|
+
if strict and not results["all_consistent"]:
|
130
|
+
error_msg = f"Version drift detected:\n"
|
131
|
+
for inc in results["inconsistencies"]:
|
132
|
+
error_msg += f" - {inc['module']}: expected {inc['expected']}, got {inc['found']}\n"
|
133
|
+
raise VersionDriftError(error_msg)
|
134
|
+
|
135
|
+
return results
|
136
|
+
|
137
|
+
|
138
|
+
def print_version_report() -> None:
|
139
|
+
"""Print a formatted version consistency report."""
|
140
|
+
try:
|
141
|
+
from rich.console import Console
|
142
|
+
from rich.table import Table
|
143
|
+
from rich.panel import Panel
|
144
|
+
|
145
|
+
console = Console()
|
146
|
+
validation_results = validate_version_consistency()
|
147
|
+
|
148
|
+
# Header
|
149
|
+
console.print(Panel(
|
150
|
+
f"[bold blue]Version Management Report[/bold blue]\n"
|
151
|
+
f"Central Version: [green]{validation_results['central_version']}[/green]",
|
152
|
+
title="Runbooks Version Validation"
|
153
|
+
))
|
154
|
+
|
155
|
+
# Module versions table
|
156
|
+
table = Table(title="Module Version Status")
|
157
|
+
table.add_column("Module", style="cyan")
|
158
|
+
table.add_column("Version", style="green")
|
159
|
+
table.add_column("Status", style="magenta")
|
160
|
+
|
161
|
+
for module_name, version in validation_results["module_versions"].items():
|
162
|
+
if isinstance(version, str) and version.startswith(("Error:", "No version", "Import error:")):
|
163
|
+
status = "[red]Error[/red]"
|
164
|
+
version_display = f"[red]{version}[/red]"
|
165
|
+
elif version == validation_results["central_version"]:
|
166
|
+
status = "[green]✓ OK[/green]"
|
167
|
+
version_display = f"[green]{version}[/green]"
|
168
|
+
else:
|
169
|
+
status = "[red]✗ Mismatch[/red]"
|
170
|
+
version_display = f"[red]{version}[/red]"
|
171
|
+
|
172
|
+
table.add_row(module_name, version_display, status)
|
173
|
+
|
174
|
+
console.print(table)
|
175
|
+
|
176
|
+
# Pyproject.toml check
|
177
|
+
is_matching, pyproject_version, central_version = validation_results["pyproject_check"]
|
178
|
+
if is_matching:
|
179
|
+
console.print("[green]✓ pyproject.toml version matches central version[/green]")
|
180
|
+
else:
|
181
|
+
console.print(f"[red]✗ pyproject.toml version mismatch: {pyproject_version} vs {central_version}[/red]")
|
182
|
+
|
183
|
+
# Overall status
|
184
|
+
if validation_results["all_consistent"]:
|
185
|
+
console.print("\n[bold green]✓ All versions are consistent[/bold green]")
|
186
|
+
else:
|
187
|
+
console.print("\n[bold red]✗ Version inconsistencies detected[/bold red]")
|
188
|
+
for inc in validation_results["inconsistencies"]:
|
189
|
+
console.print(f" - {inc['module']}: expected {inc['expected']}, got {inc['found']}")
|
190
|
+
|
191
|
+
except ImportError:
|
192
|
+
# Fallback without Rich
|
193
|
+
validation_results = validate_version_consistency()
|
194
|
+
|
195
|
+
print(f"=== Version Management Report ===")
|
196
|
+
print(f"Central Version: {validation_results['central_version']}")
|
197
|
+
print()
|
198
|
+
|
199
|
+
print("Module Versions:")
|
200
|
+
for module_name, version in validation_results["module_versions"].items():
|
201
|
+
status = "OK" if version == validation_results["central_version"] else "MISMATCH"
|
202
|
+
print(f" {module_name}: {version} ({status})")
|
203
|
+
|
204
|
+
is_matching, pyproject_version, _ = validation_results["pyproject_check"]
|
205
|
+
pyproject_status = "OK" if is_matching else "MISMATCH"
|
206
|
+
print(f" pyproject.toml: {pyproject_version} ({pyproject_status})")
|
207
|
+
|
208
|
+
if validation_results["all_consistent"]:
|
209
|
+
print("\n✓ All versions are consistent")
|
210
|
+
else:
|
211
|
+
print("\n✗ Version inconsistencies detected:")
|
212
|
+
for inc in validation_results["inconsistencies"]:
|
213
|
+
print(f" - {inc['module']}: expected {inc['expected']}, got {inc['found']}")
|
214
|
+
|
215
|
+
|
216
|
+
def cli_main():
|
217
|
+
"""CLI entry point for version validation."""
|
218
|
+
import argparse
|
219
|
+
|
220
|
+
parser = argparse.ArgumentParser(description="Validate runbooks version consistency")
|
221
|
+
parser.add_argument("--strict", action="store_true",
|
222
|
+
help="Exit with error code if inconsistencies found")
|
223
|
+
|
224
|
+
args = parser.parse_args()
|
225
|
+
|
226
|
+
try:
|
227
|
+
print_version_report()
|
228
|
+
|
229
|
+
if args.strict:
|
230
|
+
validate_version_consistency(strict=True)
|
231
|
+
|
232
|
+
except VersionDriftError as e:
|
233
|
+
print(f"\nERROR: {e}")
|
234
|
+
sys.exit(1)
|
235
|
+
except Exception as e:
|
236
|
+
print(f"Unexpected error: {e}")
|
237
|
+
sys.exit(1)
|
238
|
+
|
239
|
+
|
240
|
+
if __name__ == "__main__":
|
241
|
+
cli_main()
|