Initial commit
This commit is contained in:
224
skills/must-gather-analyzer/scripts/analyze_pods.py
Executable file
224
skills/must-gather-analyzer/scripts/analyze_pods.py
Executable file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze Pod resources from must-gather data.
|
||||
Displays output similar to 'oc get pods -A' command.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
def parse_pod(file_path: Path) -> Optional[Dict[str, Any]]:
|
||||
"""Parse a single pod YAML file."""
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
doc = yaml.safe_load(f)
|
||||
if doc and doc.get('kind') == 'Pod':
|
||||
return doc
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to parse {file_path}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def calculate_age(creation_timestamp: str) -> str:
|
||||
"""Calculate age from creation timestamp."""
|
||||
try:
|
||||
ts = datetime.fromisoformat(creation_timestamp.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 get_pod_status(pod: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Extract pod status information."""
|
||||
metadata = pod.get('metadata', {})
|
||||
status = pod.get('status', {})
|
||||
spec = pod.get('spec', {})
|
||||
|
||||
name = metadata.get('name', 'unknown')
|
||||
namespace = metadata.get('namespace', 'unknown')
|
||||
creation_time = metadata.get('creationTimestamp', '')
|
||||
|
||||
# Get container statuses
|
||||
container_statuses = status.get('containerStatuses', [])
|
||||
init_container_statuses = status.get('initContainerStatuses', [])
|
||||
|
||||
# Calculate ready containers
|
||||
total_containers = len(spec.get('containers', []))
|
||||
ready_containers = sum(1 for cs in container_statuses if cs.get('ready', False))
|
||||
|
||||
# Get overall phase
|
||||
phase = status.get('phase', 'Unknown')
|
||||
|
||||
# Determine more specific status
|
||||
pod_status = phase
|
||||
reason = status.get('reason', '')
|
||||
|
||||
# Check for specific container states
|
||||
for cs in container_statuses:
|
||||
state = cs.get('state', {})
|
||||
if 'waiting' in state:
|
||||
waiting = state['waiting']
|
||||
pod_status = waiting.get('reason', 'Waiting')
|
||||
elif 'terminated' in state:
|
||||
terminated = state['terminated']
|
||||
if terminated.get('exitCode', 0) != 0:
|
||||
pod_status = terminated.get('reason', 'Error')
|
||||
|
||||
# Check init containers
|
||||
for ics in init_container_statuses:
|
||||
state = ics.get('state', {})
|
||||
if 'waiting' in state:
|
||||
waiting = state['waiting']
|
||||
if waiting.get('reason') in ['CrashLoopBackOff', 'ImagePullBackOff', 'ErrImagePull']:
|
||||
pod_status = f"Init:{waiting.get('reason', 'Waiting')}"
|
||||
|
||||
# Calculate total restarts
|
||||
total_restarts = sum(cs.get('restartCount', 0) for cs in container_statuses)
|
||||
|
||||
# Calculate age
|
||||
age = calculate_age(creation_time) if creation_time else ''
|
||||
|
||||
return {
|
||||
'namespace': namespace,
|
||||
'name': name,
|
||||
'ready': f"{ready_containers}/{total_containers}",
|
||||
'status': pod_status,
|
||||
'restarts': str(total_restarts),
|
||||
'age': age,
|
||||
'node': spec.get('nodeName', ''),
|
||||
'is_problem': pod_status not in ['Running', 'Succeeded', 'Completed'] or total_restarts > 0
|
||||
}
|
||||
|
||||
|
||||
def print_pods_table(pods: List[Dict[str, Any]], show_namespace: bool = True):
|
||||
"""Print pods in a formatted table like 'oc get pods'."""
|
||||
if not pods:
|
||||
print("No resources found.")
|
||||
return
|
||||
|
||||
# Print header
|
||||
if show_namespace:
|
||||
print(f"{'NAMESPACE':<42} {'NAME':<50} {'READY':<7} {'STATUS':<20} {'RESTARTS':<9} AGE")
|
||||
else:
|
||||
print(f"{'NAME':<50} {'READY':<7} {'STATUS':<20} {'RESTARTS':<9} AGE")
|
||||
|
||||
# Print rows
|
||||
for pod in pods:
|
||||
name = pod['name'][:50]
|
||||
ready = pod['ready'][:7]
|
||||
status = pod['status'][:20]
|
||||
restarts = pod['restarts'][:9]
|
||||
age = pod['age']
|
||||
|
||||
if show_namespace:
|
||||
namespace = pod['namespace'][:42]
|
||||
print(f"{namespace:<42} {name:<50} {ready:<7} {status:<20} {restarts:<9} {age}")
|
||||
else:
|
||||
print(f"{name:<50} {ready:<7} {status:<20} {restarts:<9} {age}")
|
||||
|
||||
|
||||
def analyze_pods(must_gather_path: str, namespace: Optional[str] = None, problems_only: bool = False):
|
||||
"""Analyze all pods in a must-gather directory."""
|
||||
base_path = Path(must_gather_path)
|
||||
|
||||
pods = []
|
||||
|
||||
# Find all pod YAML files
|
||||
# Structure: namespaces/<namespace>/pods/<pod-name>/<pod-name>.yaml
|
||||
if namespace:
|
||||
# Specific namespace
|
||||
patterns = [
|
||||
f"namespaces/{namespace}/pods/*/*.yaml",
|
||||
f"*/namespaces/{namespace}/pods/*/*.yaml",
|
||||
]
|
||||
else:
|
||||
# All namespaces
|
||||
patterns = [
|
||||
"namespaces/*/pods/*/*.yaml",
|
||||
"*/namespaces/*/pods/*/*.yaml",
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
for pod_file in base_path.glob(pattern):
|
||||
pod = parse_pod(pod_file)
|
||||
if pod:
|
||||
pod_status = get_pod_status(pod)
|
||||
pods.append(pod_status)
|
||||
|
||||
if not pods:
|
||||
print("No resources found.")
|
||||
return 1
|
||||
|
||||
# Remove duplicates
|
||||
seen = set()
|
||||
unique_pods = []
|
||||
for p in pods:
|
||||
key = f"{p['namespace']}/{p['name']}"
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_pods.append(p)
|
||||
|
||||
# Sort by namespace, then name
|
||||
unique_pods.sort(key=lambda x: (x['namespace'], x['name']))
|
||||
|
||||
# Filter if problems only
|
||||
if problems_only:
|
||||
unique_pods = [p for p in unique_pods if p['is_problem']]
|
||||
if not unique_pods:
|
||||
print("No resources found.")
|
||||
return 0
|
||||
|
||||
# Print results
|
||||
print_pods_table(unique_pods, show_namespace=(namespace is None))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze pod resources from must-gather data',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s ./must-gather.local.123456789
|
||||
%(prog)s ./must-gather.local.123456789 --namespace openshift-etcd
|
||||
%(prog)s ./must-gather.local.123456789 --problems-only
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('must_gather_path', help='Path to must-gather directory')
|
||||
parser.add_argument('-n', '--namespace', help='Filter by namespace')
|
||||
parser.add_argument('-p', '--problems-only', action='store_true',
|
||||
help='Show only pods with issues')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isdir(args.must_gather_path):
|
||||
print(f"Error: Directory not found: {args.must_gather_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return analyze_pods(args.must_gather_path, args.namespace, args.problems_only)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user