#!/usr/bin/env python3 """ List all Proxmox VMs with VMID, Name, IP Address, FQDN, and Description. This script connects to a Proxmox cluster and retrieves comprehensive information about all virtual machines (both QEMU VMs and LXC containers). """ import os import sys import json from typing import Dict, List, Optional, Any from proxmoxer import ProxmoxAPI def load_env_file(env_path: str = None) -> dict: """Load environment variables from .env file.""" if env_path is None: env_path = os.path.expanduser('~/.env') env_vars = {} if os.path.exists(env_path): try: with open(env_path, 'r') as f: for line in f: line = line.strip() # Skip comments and empty lines if not line or line.startswith('#'): continue # Parse KEY=VALUE format if '=' in line: key, value = line.split('=', 1) key = key.strip() value = value.strip().strip('"').strip("'") env_vars[key] = value except Exception as e: print(f"Warning: Could not load {env_path}: {e}", file=sys.stderr) return env_vars def get_proxmox_connection() -> ProxmoxAPI: """Initialize Proxmox API connection from environment variables or config file.""" # Load from ~/.env file first env_vars = load_env_file() # Try environment variables first (they override .env file) host = os.getenv('PROXMOX_HOST', env_vars.get('PROXMOX_HOST', '192.168.6.247')) port = int(os.getenv('PROXMOX_PORT', env_vars.get('PROXMOX_PORT', '8006'))) user = os.getenv('PROXMOX_USER', env_vars.get('PROXMOX_USER', 'root@pam')) token_name = os.getenv('PROXMOX_TOKEN_NAME', env_vars.get('PROXMOX_TOKEN_NAME', 'mcpserver')) token_value = os.getenv('PROXMOX_TOKEN_VALUE', env_vars.get('PROXMOX_TOKEN_VALUE')) password = os.getenv('PROXMOX_PASSWORD', env_vars.get('PROXMOX_PASSWORD')) verify_ssl = os.getenv('PROXMOX_VERIFY_SSL', env_vars.get('PROXMOX_VERIFY_SSL', 'false')).lower() == 'true' # If no token value, try to load from JSON config file if not token_value and not password: config_path = os.getenv('PROXMOX_MCP_CONFIG') if config_path and os.path.exists(config_path): with open(config_path) as f: config = json.load(f) host = config.get('proxmox', {}).get('host', host) port = config.get('proxmox', {}).get('port', port) verify_ssl = config.get('proxmox', {}).get('verify_ssl', verify_ssl) user = config.get('auth', {}).get('user', user) token_name = config.get('auth', {}).get('token_name', token_name) token_value = config.get('auth', {}).get('token_value') password = config.get('auth', {}).get('password') if not token_value and not password: print("Error: PROXMOX_TOKEN_VALUE or PROXMOX_PASSWORD required", file=sys.stderr) print("\nCredentials can be provided via:", file=sys.stderr) print(" 1. Environment variables", file=sys.stderr) print(" 2. ~/.env file (automatically loaded)", file=sys.stderr) print(" 3. JSON config file (set PROXMOX_MCP_CONFIG)", file=sys.stderr) print("\nExample ~/.env file:", file=sys.stderr) print(" PROXMOX_HOST=your-proxmox-host", file=sys.stderr) print(" PROXMOX_USER=root@pam", file=sys.stderr) print(" PROXMOX_TOKEN_NAME=your-token-name", file=sys.stderr) print(" PROXMOX_TOKEN_VALUE=your-token-value", file=sys.stderr) sys.exit(1) try: if token_value: proxmox = ProxmoxAPI( host=host, port=port, user=user, token_name=token_name, token_value=token_value, verify_ssl=verify_ssl, service='PVE' ) else: # Use password authentication proxmox = ProxmoxAPI( host=host, port=port, user=user, password=password, verify_ssl=verify_ssl, service='PVE' ) # Test connection proxmox.version.get() return proxmox except Exception as e: error_msg = str(e) if "ConnectTimeoutError" in error_msg or "timed out" in error_msg: print(f"Error: Connection to Proxmox host '{host}:{port}' timed out", file=sys.stderr) print(f" - Verify the host is reachable: ping {host}", file=sys.stderr) print(f" - Check firewall allows port {port}", file=sys.stderr) print(f" - Verify PROXMOX_HOST in ~/.env is correct", file=sys.stderr) elif "401" in error_msg or "authentication" in error_msg.lower(): print(f"Error: Authentication failed", file=sys.stderr) print(f" - Verify PROXMOX_TOKEN_VALUE or PROXMOX_PASSWORD in ~/.env", file=sys.stderr) print(f" - Check user permissions in Proxmox", file=sys.stderr) else: print(f"Error connecting to Proxmox: {e}", file=sys.stderr) sys.exit(1) def get_vm_ip_address(proxmox: ProxmoxAPI, node: str, vmid: str, vm_type: str) -> str: """Get IP address of a VM.""" ip_addresses = [] try: # Try to get IP from network interfaces via guest agent (for QEMU) or config if vm_type == 'qemu': # Try QEMU guest agent first try: interfaces = proxmox.nodes(node).qemu(vmid).agent('network-get-interfaces').get() if interfaces and 'result' in interfaces: for iface in interfaces['result']: if 'ip-addresses' in iface: for ip_info in iface['ip-addresses']: if ip_info.get('ip-address-type') == 'ipv4' and not ip_info.get('ip-address', '').startswith('127.'): ip_addresses.append(ip_info['ip-address']) except: pass # Try to get from config (for static IPs or if guest agent not available) try: config = proxmox.nodes(node).qemu(vmid).config.get() if vm_type == 'qemu' else proxmox.nodes(node).lxc(vmid).config.get() # Check for IP in network config for key, value in config.items(): if key.startswith('net') and isinstance(value, str): # Parse network config like "virtio=00:11:22:33:44:55,bridge=vmbr0,ip=192.168.1.100/24" if 'ip=' in value: ip_part = value.split('ip=')[1].split(',')[0].split('/')[0] if ip_part and ip_part not in ip_addresses: ip_addresses.append(ip_part) except: pass # For LXC, try to execute hostname -I command if vm_type == 'lxc' and not ip_addresses: try: result = proxmox.nodes(node).lxc(vmid).exec.post(command='hostname -I') if result and 'out' in result: ips = result['out'].strip().split() ip_addresses.extend([ip for ip in ips if not ip.startswith('127.')]) except: pass except Exception: pass return ', '.join(ip_addresses) if ip_addresses else 'N/A' def get_vm_fqdn(proxmox: ProxmoxAPI, node: str, vmid: str, vm_type: str) -> str: """Get FQDN of a VM.""" fqdn = None try: # Get hostname from config config = proxmox.nodes(node).qemu(vmid).config.get() if vm_type == 'qemu' else proxmox.nodes(node).lxc(vmid).config.get() hostname = config.get('hostname') or config.get('name', '').split('.')[0] if hostname: # Try to get full FQDN by executing hostname -f try: if vm_type == 'qemu': result = proxmox.nodes(node).qemu(vmid).agent('exec').post(command={'command': 'hostname -f'}) else: result = proxmox.nodes(node).lxc(vmid).exec.post(command='hostname -f') if result: if vm_type == 'qemu' and 'result' in result and 'out-data' in result['result']: fqdn = result['result']['out-data'].strip() elif vm_type == 'lxc' and 'out' in result: fqdn = result['out'].strip() except: # Fallback to hostname from config if hostname: fqdn = hostname except Exception: pass return fqdn if fqdn else 'N/A' def get_vm_description(proxmox: ProxmoxAPI, node: str, vmid: str, vm_type: str) -> str: """Get description of a VM.""" try: config = proxmox.nodes(node).qemu(vmid).config.get() if vm_type == 'qemu' else proxmox.nodes(node).lxc(vmid).config.get() return config.get('description', 'N/A') except Exception: return 'N/A' def list_all_vms(proxmox: ProxmoxAPI) -> List[Dict[str, Any]]: """List all VMs across all nodes.""" all_vms = [] try: nodes = proxmox.nodes.get() for node in nodes: node_name = node['node'] # Get QEMU VMs try: qemu_vms = proxmox.nodes(node_name).qemu.get() for vm in qemu_vms: vmid = str(vm['vmid']) name = vm.get('name', f'VM-{vmid}') # Get additional info description = get_vm_description(proxmox, node_name, vmid, 'qemu') ip_address = get_vm_ip_address(proxmox, node_name, vmid, 'qemu') fqdn = get_vm_fqdn(proxmox, node_name, vmid, 'qemu') all_vms.append({ 'vmid': vmid, 'name': name, 'type': 'QEMU', 'node': node_name, 'status': vm.get('status', 'unknown'), 'ip_address': ip_address, 'fqdn': fqdn, 'description': description }) except Exception as e: print(f"Warning: Could not get QEMU VMs from node {node_name}: {e}", file=sys.stderr) # Get LXC containers try: lxc_vms = proxmox.nodes(node_name).lxc.get() for vm in lxc_vms: vmid = str(vm['vmid']) name = vm.get('name', f'CT-{vmid}') # Get additional info description = get_vm_description(proxmox, node_name, vmid, 'lxc') ip_address = get_vm_ip_address(proxmox, node_name, vmid, 'lxc') fqdn = get_vm_fqdn(proxmox, node_name, vmid, 'lxc') all_vms.append({ 'vmid': vmid, 'name': name, 'type': 'LXC', 'node': node_name, 'status': vm.get('status', 'unknown'), 'ip_address': ip_address, 'fqdn': fqdn, 'description': description }) except Exception as e: print(f"Warning: Could not get LXC containers from node {node_name}: {e}", file=sys.stderr) except Exception as e: print(f"Error listing VMs: {e}", file=sys.stderr) sys.exit(1) # Sort by VMID all_vms.sort(key=lambda x: int(x['vmid'])) return all_vms def print_vm_table(vms: List[Dict[str, Any]]): """Print VMs in a formatted table.""" if not vms: print("No VMs found.") return # Calculate column widths col_widths = { 'vmid': max(len('VMID'), max(len(vm['vmid']) for vm in vms)), 'name': max(len('Name'), max(len(vm['name']) for vm in vms)), 'type': max(len('Type'), max(len(vm['type']) for vm in vms)), 'ip_address': max(len('IP Address'), max(len(vm['ip_address']) for vm in vms)), 'fqdn': max(len('FQDN'), max(len(vm['fqdn']) for vm in vms)), 'description': max(len('Description'), max(len(vm['description']) for vm in vms if vm['description'] != 'N/A')) } # Print header header = f"{'VMID':<{col_widths['vmid']}} | {'Name':<{col_widths['name']}} | {'Type':<{col_widths['type']}} | {'IP Address':<{col_widths['ip_address']}} | {'FQDN':<{col_widths['fqdn']}} | {'Description':<{col_widths['description']}}" print(header) print('-' * len(header)) # Print VMs for vm in vms: row = f"{vm['vmid']:<{col_widths['vmid']}} | {vm['name']:<{col_widths['name']}} | {vm['type']:<{col_widths['type']}} | {vm['ip_address']:<{col_widths['ip_address']}} | {vm['fqdn']:<{col_widths['fqdn']}} | {vm['description']:<{col_widths['description']}}" print(row) def main(): """Main function.""" proxmox = get_proxmox_connection() vms = list_all_vms(proxmox) print_vm_table(vms) if __name__ == '__main__': main()