#!/usr/bin/env python3 """ Analyze Node resources from must-gather data. Displays output similar to 'oc get nodes' 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_node(file_path: Path) -> Optional[Dict[str, Any]]: """Parse a single node YAML file.""" try: with open(file_path, 'r') as f: doc = yaml.safe_load(f) if doc and doc.get('kind') == 'Node': 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 if days > 0: return f"{days}d" elif hours > 0: return f"{hours}h" else: return "<1h" except Exception: return "" def get_node_roles(labels: Dict[str, str]) -> str: """Extract node roles from labels.""" roles = [] for key in labels: if key.startswith('node-role.kubernetes.io/'): role = key.split('/')[-1] if role: roles.append(role) return ','.join(sorted(roles)) if roles else '' def get_node_status(node: Dict[str, Any]) -> Dict[str, Any]: """Extract node status information.""" metadata = node.get('metadata', {}) status = node.get('status', {}) name = metadata.get('name', 'unknown') labels = metadata.get('labels', {}) creation_time = metadata.get('creationTimestamp', '') # Get roles roles = get_node_roles(labels) # Get conditions conditions = status.get('conditions', []) ready_condition = 'Unknown' node_issues = [] for condition in conditions: cond_type = condition.get('type', '') cond_status = condition.get('status', 'Unknown') if cond_type == 'Ready': ready_condition = cond_status elif cond_status == 'True' and cond_type in ['MemoryPressure', 'DiskPressure', 'PIDPressure', 'NetworkUnavailable']: node_issues.append(cond_type) # Determine overall status if ready_condition == 'True': node_status = 'Ready' elif ready_condition == 'False': node_status = 'NotReady' else: node_status = 'Unknown' # Add issues to status if node_issues: node_status = f"{node_status},{','.join(node_issues)}" # Get version node_info = status.get('nodeInfo', {}) version = node_info.get('kubeletVersion', '') # Get age age = calculate_age(creation_time) if creation_time else '' # Internal IP addresses = status.get('addresses', []) internal_ip = '' for addr in addresses: if addr.get('type') == 'InternalIP': internal_ip = addr.get('address', '') break # OS Image os_image = node_info.get('osImage', '') return { 'name': name, 'status': node_status, 'roles': roles, 'age': age, 'version': version, 'internal_ip': internal_ip, 'os_image': os_image, 'is_problem': node_status != 'Ready' or len(node_issues) > 0 } def print_nodes_table(nodes: List[Dict[str, Any]]): """Print nodes in a formatted table like 'oc get nodes'.""" if not nodes: print("No resources found.") return # Print header print(f"{'NAME':<50} {'STATUS':<30} {'ROLES':<20} {'AGE':<7} VERSION") # Print rows for node in nodes: name = node['name'][:50] status = node['status'][:30] roles = node['roles'][:20] age = node['age'][:7] version = node['version'] print(f"{name:<50} {status:<30} {roles:<20} {age:<7} {version}") def analyze_nodes(must_gather_path: str, problems_only: bool = False): """Analyze all nodes in a must-gather directory.""" base_path = Path(must_gather_path) # Find all node YAML files possible_patterns = [ "cluster-scoped-resources/core/nodes/*.yaml", "*/cluster-scoped-resources/core/nodes/*.yaml", ] nodes = [] for pattern in possible_patterns: for node_file in base_path.glob(pattern): # Skip the nodes.yaml file that contains all nodes if node_file.name == 'nodes.yaml': continue node = parse_node(node_file) if node: node_status = get_node_status(node) nodes.append(node_status) if not nodes: print("No resources found.") return 1 # Remove duplicates seen = set() unique_nodes = [] for n in nodes: if n['name'] not in seen: seen.add(n['name']) unique_nodes.append(n) # Sort by name unique_nodes.sort(key=lambda x: x['name']) # Filter if problems only if problems_only: unique_nodes = [n for n in unique_nodes if n['is_problem']] if not unique_nodes: print("No resources found.") return 0 # Print results print_nodes_table(unique_nodes) return 0 def main(): parser = argparse.ArgumentParser( description='Analyze node resources from must-gather data', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s ./must-gather.local.123456789 %(prog)s ./must-gather.local.123456789 --problems-only """ ) parser.add_argument('must_gather_path', help='Path to must-gather directory') parser.add_argument('-p', '--problems-only', action='store_true', help='Show only nodes 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_nodes(args.must_gather_path, args.problems_only) if __name__ == '__main__': sys.exit(main())