Files
gh-openshift-eng-ai-helpers…/skills/must-gather-analyzer/scripts/analyze_clusterversion.py
2025-11-30 08:46:06 +08:00

262 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Analyze ClusterVersion from must-gather data.
Displays output similar to 'oc get clusterversion' command.
"""
import sys
import os
import yaml
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional
def parse_clusterversion(file_path: Path) -> Optional[Dict[str, Any]]:
"""Parse the clusterversion YAML file."""
try:
with open(file_path, 'r') as f:
doc = yaml.safe_load(f)
if doc and doc.get('kind') == 'ClusterVersion':
return doc
except Exception as e:
print(f"Warning: Failed to parse {file_path}: {e}", file=sys.stderr)
return None
def get_condition_status(conditions: list, condition_type: str) -> str:
"""Get status for a specific condition type."""
for condition in conditions:
if condition.get('type') == condition_type:
return condition.get('status', 'Unknown')
return 'Unknown'
def calculate_duration(timestamp_str: str) -> str:
"""Calculate duration from timestamp to now."""
try:
ts = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
now = datetime.now(ts.tzinfo)
delta = now - ts
days = delta.days
hours = delta.seconds // 3600
minutes = (delta.seconds % 3600) // 60
if days > 0:
return f"{days}d"
elif hours > 0:
return f"{hours}h"
elif minutes > 0:
return f"{minutes}m"
else:
return "<1m"
except Exception:
return ""
def format_clusterversion(cv: Dict[str, Any]) -> Dict[str, str]:
"""Format ClusterVersion for display."""
name = cv.get('metadata', {}).get('name', 'version')
status = cv.get('status', {})
# Get version from desired
desired = status.get('desired', {})
version = desired.get('version', '')
# Get available updates count
available_updates = status.get('availableUpdates')
if available_updates and isinstance(available_updates, list):
available = str(len(available_updates))
elif available_updates is None:
available = ''
else:
available = '0'
# Get conditions
conditions = status.get('conditions', [])
progressing = get_condition_status(conditions, 'Progressing')
since = ''
# Get time since progressing started (if true) or since last update
for condition in conditions:
if condition.get('type') == 'Progressing':
last_transition = condition.get('lastTransitionTime')
if last_transition:
since = calculate_duration(last_transition)
break
# Get status message
status_msg = ''
for condition in conditions:
if condition.get('type') == 'Progressing' and condition.get('status') == 'True':
status_msg = condition.get('message', '')[:80]
break
# If not progressing, check if failed
if progressing != 'True':
for condition in conditions:
if condition.get('type') == 'Failing' and condition.get('status') == 'True':
status_msg = condition.get('message', '')[:80]
break
return {
'name': name,
'version': version,
'available': available,
'progressing': progressing,
'since': since,
'status': status_msg
}
def print_clusterversion_table(cv_info: Dict[str, str]):
"""Print ClusterVersion in a formatted table like 'oc get clusterversion'."""
# Print header
print(f"{'NAME':<10} {'VERSION':<50} {'AVAILABLE':<11} {'PROGRESSING':<13} {'SINCE':<7} STATUS")
# Print row
name = cv_info['name'][:10]
version = cv_info['version'][:50]
available = cv_info['available'][:11]
progressing = cv_info['progressing'][:13]
since = cv_info['since'][:7]
status = cv_info['status']
print(f"{name:<10} {version:<50} {available:<11} {progressing:<13} {since:<7} {status}")
def print_detailed_info(cv: Dict[str, Any]):
"""Print detailed cluster version information."""
status = cv.get('status', {})
spec = cv.get('spec', {})
print(f"\n{'='*80}")
print("CLUSTER VERSION DETAILS")
print(f"{'='*80}")
# Cluster ID
cluster_id = spec.get('clusterID', 'unknown')
print(f"Cluster ID: {cluster_id}")
# Desired version
desired = status.get('desired', {})
print(f"Desired Version: {desired.get('version', 'unknown')}")
print(f"Desired Image: {desired.get('image', 'unknown')}")
# Version hash
version_hash = status.get('versionHash', '')
if version_hash:
print(f"Version Hash: {version_hash}")
# Upstream
upstream = spec.get('upstream', '')
if upstream:
print(f"Update Server: {upstream}")
# Conditions
conditions = status.get('conditions', [])
print(f"\nCONDITIONS:")
for condition in conditions:
cond_type = condition.get('type', 'Unknown')
cond_status = condition.get('status', 'Unknown')
last_transition = condition.get('lastTransitionTime', '')
message = condition.get('message', '')
# Calculate time since transition
age = calculate_duration(last_transition) if last_transition else ''
status_indicator = "" if cond_status == "True" else "" if cond_status == "False" else ""
print(f" {status_indicator} {cond_type}: {cond_status} (for {age})")
if message and cond_status == 'True':
print(f" Message: {message[:100]}")
# Update history
history = status.get('history', [])
if history:
print(f"\nUPDATE HISTORY (last 5):")
for i, entry in enumerate(history[:5]):
state = entry.get('state', 'Unknown')
version = entry.get('version', 'unknown')
image = entry.get('image', '')
completion_time = entry.get('completionTime', '')
age = calculate_duration(completion_time) if completion_time else ''
print(f" {i+1}. {version} - {state} {f'({age} ago)' if age else ''}")
# Available updates
available_updates = status.get('availableUpdates')
if available_updates and isinstance(available_updates, list) and len(available_updates) > 0:
print(f"\nAVAILABLE UPDATES ({len(available_updates)}):")
for i, update in enumerate(available_updates[:5]):
version = update.get('version', 'unknown')
image = update.get('image', '')
print(f" {i+1}. {version}")
elif available_updates is None:
print(f"\nAVAILABLE UPDATES: Unable to retrieve updates")
# Capabilities
capabilities = status.get('capabilities', {})
enabled_caps = capabilities.get('enabledCapabilities', [])
if enabled_caps:
print(f"\nENABLED CAPABILITIES ({len(enabled_caps)}):")
# Print in columns
for i in range(0, len(enabled_caps), 3):
caps = enabled_caps[i:i+3]
print(f" {', '.join(caps)}")
print(f"{'='*80}\n")
def analyze_clusterversion(must_gather_path: str):
"""Analyze ClusterVersion in a must-gather directory."""
base_path = Path(must_gather_path)
# Find ClusterVersion file
possible_patterns = [
"cluster-scoped-resources/config.openshift.io/clusterversions/version.yaml",
"*/cluster-scoped-resources/config.openshift.io/clusterversions/version.yaml",
]
cv = None
for pattern in possible_patterns:
for cv_file in base_path.glob(pattern):
cv = parse_clusterversion(cv_file)
if cv:
break
if cv:
break
if not cv:
print("No ClusterVersion found.")
return 1
# Format and print table
cv_info = format_clusterversion(cv)
print_clusterversion_table(cv_info)
# Print detailed information
print_detailed_info(cv)
return 0
def main():
if len(sys.argv) < 2:
print("Usage: analyze_clusterversion.py <must-gather-directory>", file=sys.stderr)
print("\nExample:", file=sys.stderr)
print(" analyze_clusterversion.py ./must-gather.local.123456789", file=sys.stderr)
return 1
must_gather_path = sys.argv[1]
if not os.path.isdir(must_gather_path):
print(f"Error: Directory not found: {must_gather_path}", file=sys.stderr)
return 1
return analyze_clusterversion(must_gather_path)
if __name__ == '__main__':
sys.exit(main())