Files
proxmox/scripts/comprehensive-proxmox-inventory.py
defiQUG fbda1b4beb
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands
- CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround
- CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check
- NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere
- MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates
- LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 15:46:57 -08:00

434 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Comprehensive Proxmox Inventory Script
Queries all Proxmox hosts, lists all VMIDs with IPs, endpoints, ports, FQDNs,
and identifies NPMplus instances with their configurations.
"""
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional, Any
from collections import defaultdict
# Proxmox Hosts
PROXMOX_HOSTS = {
"ml110": "192.168.11.10",
"r630-01": "192.168.11.11",
"r630-02": "192.168.11.12"
}
SSH_OPTS = "-o StrictHostKeyChecking=no -o ConnectTimeout=5"
# Known port mappings for common services
SERVICE_PORTS = {
"80": "HTTP",
"443": "HTTPS",
"3000": "Node.js API",
"4000": "GraphQL/Blockscout",
"5432": "PostgreSQL",
"6379": "Redis",
"8006": "Proxmox Web UI",
"8043": "Omada",
"8200": "Vault",
"8545": "Besu HTTP RPC",
"8546": "Besu WebSocket RPC",
"9000": "Web3Signer",
"9545": "Prometheus Metrics",
"30303": "P2P Networking",
}
def run_ssh_command(host: str, command: str) -> Optional[str]:
"""Run SSH command on Proxmox host"""
try:
result = subprocess.run(
["ssh", *SSH_OPTS.split(), f"root@{host}", command],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
return result.stdout.strip()
return None
except Exception as e:
print(f"Error running SSH command on {host}: {e}", file=sys.stderr)
return None
def get_node_name(host: str) -> Optional[str]:
"""Get Proxmox node name"""
return run_ssh_command(host, "hostname")
def get_all_vms(host: str, node: str) -> List[Dict[str, Any]]:
"""Get all QEMU VMs from a Proxmox host"""
vms = []
command = f"pvesh get /nodes/{node}/qemu --output-format json"
output = run_ssh_command(host, command)
if output:
try:
vm_list = json.loads(output)
for vm in vm_list:
vmid = str(vm.get('vmid', ''))
name = vm.get('name', f'VM-{vmid}')
status = vm.get('status', 'unknown')
vms.append({
'vmid': vmid,
'name': name,
'type': 'QEMU',
'status': status,
'host': host,
'node': node
})
except json.JSONDecodeError:
pass
return vms
def get_all_containers(host: str, node: str) -> List[Dict[str, Any]]:
"""Get all LXC containers from a Proxmox host"""
containers = []
command = f"pvesh get /nodes/{node}/lxc --output-format json"
output = run_ssh_command(host, command)
if output:
try:
container_list = json.loads(output)
for container in container_list:
vmid = str(container.get('vmid', ''))
name = container.get('name', f'CT-{vmid}')
status = container.get('status', 'unknown')
containers.append({
'vmid': vmid,
'name': name,
'type': 'LXC',
'status': status,
'host': host,
'node': node
})
except json.JSONDecodeError:
pass
return containers
def get_vm_config(host: str, node: str, vmid: str, vm_type: str) -> Dict[str, Any]:
"""Get VM/container configuration"""
config = {}
command = f"pvesh get /nodes/{node}/{vm_type}/{vmid}/config --output-format json"
output = run_ssh_command(host, command)
if output:
try:
config = json.loads(output)
except json.JSONDecodeError:
pass
return config
def get_vm_ip(host: str, node: str, vmid: str, vm_type: str) -> Optional[str]:
"""Get VM IP address"""
if vm_type == 'lxc':
# For LXC, get IP from config or running container
config = get_vm_config(host, node, vmid, vm_type)
net_config = config.get('net0', '')
if 'ip=' in net_config:
ip_part = net_config.split('ip=')[1].split(',')[0].split('/')[0]
if ip_part and ip_part not in ['dhcp', 'auto']:
return ip_part
# Try to get IP from running container
if config.get('status') == 'running':
ip = run_ssh_command(host, f"pct exec {vmid} -- hostname -I 2>/dev/null | awk '{{print $1}}'")
if ip and not ip.startswith('127.'):
return ip
else:
# For QEMU, try agent
command = f"pvesh get /nodes/{node}/qemu/{vmid}/agent/network-get-interfaces --output-format json"
output = run_ssh_command(host, command)
if output:
try:
data = json.loads(output)
if 'result' in data:
for iface in data['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.'):
return ip_info['ip-address']
except json.JSONDecodeError:
pass
return None
def get_vm_hostname(host: str, node: str, vmid: str, vm_type: str) -> Optional[str]:
"""Get VM hostname"""
config = get_vm_config(host, node, vmid, vm_type)
return config.get('hostname') or config.get('name')
def get_vm_description(host: str, node: str, vmid: str, vm_type: str) -> Optional[str]:
"""Get VM description"""
config = get_vm_config(host, node, vmid, vm_type)
return config.get('description')
def get_npmplus_config() -> List[Dict[str, Any]]:
"""Get NPMplus configuration via API"""
project_root = Path(__file__).parent.parent
env_file = project_root / ".env"
npm_email = None
npm_password = None
if env_file.exists():
with open(env_file) as f:
for line in f:
if line.startswith("NPM_EMAIL="):
npm_email = line.split("=", 1)[1].strip().strip('"').strip("'")
elif line.startswith("NPM_PASSWORD="):
npm_password = line.split("=", 1)[1].strip().strip('"').strip("'")
if not npm_email or not npm_password:
return []
npm_url = "https://192.168.11.166:81"
try:
import urllib.request
import ssl
# Authenticate
auth_data = json.dumps({
"identity": npm_email,
"secret": npm_password
}).encode()
context = ssl._create_unverified_context()
req = urllib.request.Request(
f"{npm_url}/api/tokens",
data=auth_data,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, context=context) as response:
token_response = json.loads(response.read().decode())
token = token_response.get("token")
if not token:
return []
# Get proxy hosts
req = urllib.request.Request(
f"{npm_url}/api/nginx/proxy-hosts",
headers={"Authorization": f"Bearer {token}"}
)
with urllib.request.urlopen(req, context=context) as response:
proxy_hosts = json.loads(response.read().decode())
return proxy_hosts
except Exception as e:
print(f"Error fetching NPMplus config: {e}", file=sys.stderr)
return []
def identify_ports_from_config(config: Dict[str, Any], vm_type: str) -> List[str]:
"""Identify ports from VM configuration"""
ports = []
# Common ports based on service type
name = config.get('name', '').lower()
hostname = config.get('hostname', '').lower()
if 'rpc' in name or 'rpc' in hostname:
ports.extend(['8545', '8546', '30303', '9545'])
if 'besu' in name or 'besu' in hostname:
ports.extend(['8545', '8546', '30303', '9545'])
if 'postgres' in name or 'postgres' in hostname:
ports.append('5432')
if 'redis' in name or 'redis' in hostname:
ports.append('6379')
if 'vault' in name or 'vault' in hostname:
ports.append('8200')
if 'web3signer' in name or 'web3signer' in hostname:
ports.append('9000')
if 'nginx' in name or 'npm' in name:
ports.extend(['80', '81', '443'])
if 'blockscout' in name or 'explorer' in name:
ports.extend(['80', '4000'])
if 'api' in name and '3000' not in str(ports):
ports.append('3000')
if 'frontend' in name or 'web' in name:
ports.append('80')
return list(set(ports)) # Remove duplicates
def main():
print("=" * 100)
print("COMPREHENSIVE PROXMOX INVENTORY REPORT")
print("=" * 100)
print()
all_vms = []
npmplus_vmids = []
# Query all Proxmox hosts
for hostname, host_ip in PROXMOX_HOSTS.items():
print(f"📡 Querying {hostname} ({host_ip})...")
node = get_node_name(host_ip)
if not node:
print(f" ⚠️ Could not connect to {hostname}")
continue
print(f" ✓ Connected to node: {node}")
# Get VMs and containers
vms = get_all_vms(host_ip, node)
containers = get_all_containers(host_ip, node)
print(f" ✓ Found {len(vms)} QEMU VMs and {len(containers)} LXC containers")
# Process VMs
for vm in vms:
vmid = vm['vmid']
config = get_vm_config(host_ip, node, vmid, 'qemu')
ip = get_vm_ip(host_ip, node, vmid, 'qemu')
hostname_vm = get_vm_hostname(host_ip, node, vmid, 'qemu')
description = get_vm_description(host_ip, node, vmid, 'qemu')
ports = identify_ports_from_config(config, 'qemu')
vm.update({
'ip': ip,
'hostname': hostname_vm,
'description': description,
'ports': ports,
'fqdn': hostname_vm
})
all_vms.append(vm)
# Check if NPMplus
if 'npmplus' in (vm.get('name', '') + ' ' + (hostname_vm or '')).lower():
npmplus_vmids.append(vmid)
# Process containers
for container in containers:
vmid = container['vmid']
config = get_vm_config(host_ip, node, vmid, 'lxc')
ip = get_vm_ip(host_ip, node, vmid, 'lxc')
hostname_vm = get_vm_hostname(host_ip, node, vmid, 'lxc')
description = get_vm_description(host_ip, node, vmid, 'lxc')
ports = identify_ports_from_config(config, 'lxc')
container.update({
'ip': ip,
'hostname': hostname_vm,
'description': description,
'ports': ports,
'fqdn': hostname_vm
})
all_vms.append(container)
# Check if NPMplus
if 'npmplus' in (container.get('name', '') + ' ' + (hostname_vm or '')).lower():
npmplus_vmids.append(vmid)
print()
print("=" * 100)
print("ALL VMIDs INVENTORY")
print("=" * 100)
print()
# Sort by VMID
all_vms.sort(key=lambda x: (int(x['vmid']) if x['vmid'].isdigit() else 999999, x['host']))
# Print table header
print(f"{'VMID':<8} {'Type':<6} {'Name':<30} {'Host':<12} {'IP Address':<18} {'FQDN':<30} {'Status':<10} {'Ports':<30}")
print("-" * 150)
for vm in all_vms:
vmid = vm['vmid']
vm_type = vm['type']
name = vm.get('name', 'N/A')[:28]
host = vm.get('host', 'N/A')
ip = vm.get('ip', 'N/A')
fqdn = (vm.get('fqdn') or vm.get('hostname') or 'N/A')[:28]
status = vm.get('status', 'unknown')
ports = ', '.join(vm.get('ports', []))[:28] if vm.get('ports') else 'N/A'
print(f"{vmid:<8} {vm_type:<6} {name:<30} {host:<12} {ip:<18} {fqdn:<30} {status:<10} {ports:<30}")
print()
print("=" * 100)
print("NPMPLUS INSTANCES")
print("=" * 100)
print()
if npmplus_vmids:
print(f"Found NPMplus running on VMIDs: {', '.join(npmplus_vmids)}")
print()
for vmid in npmplus_vmids:
vm = next((v for v in all_vms if v['vmid'] == vmid), None)
if vm:
print(f"VMID {vmid}:")
print(f" Name: {vm.get('name', 'N/A')}")
print(f" Type: {vm.get('type', 'N/A')}")
print(f" Host: {vm.get('host', 'N/A')}")
print(f" IP: {vm.get('ip', 'N/A')}")
print(f" FQDN: {vm.get('fqdn', 'N/A')}")
print(f" Status: {vm.get('status', 'N/A')}")
print(f" Ports: {', '.join(vm.get('ports', []))}")
print()
else:
print("No NPMplus instances found")
print()
print("=" * 100)
print("NPMPLUS CONFIGURATION")
print("=" * 100)
print()
npmplus_config = get_npmplus_config()
if npmplus_config:
print(f"Found {len(npmplus_config)} proxy host configurations in NPMplus")
print()
print(f"{'ID':<6} {'Domain(s)':<50} {'Target':<25} {'Port':<8} {'Scheme':<8} {'WebSocket':<10}")
print("-" * 120)
for host in sorted(npmplus_config, key=lambda x: x.get('id', 0)):
host_id = str(host.get('id', 'N/A'))
domain_names = host.get('domain_names', [])
domains = ', '.join(domain_names) if isinstance(domain_names, list) else str(domain_names)
forward_host = host.get('forward_host', 'N/A')
forward_port = str(host.get('forward_port', 'N/A'))
forward_scheme = host.get('forward_scheme', 'http')
websocket = 'Yes' if host.get('forward_websocket', False) else 'No'
print(f"{host_id:<6} {domains[:48]:<50} {forward_host:<25} {forward_port:<8} {forward_scheme:<8} {websocket:<10}")
print()
print("Detailed Configuration:")
print()
for host in sorted(npmplus_config, key=lambda x: x.get('id', 0)):
print(f"Proxy Host ID: {host.get('id')}")
print(f" Domain Names: {', '.join(host.get('domain_names', []))}")
print(f" Forward Host: {host.get('forward_host')}")
print(f" Forward Port: {host.get('forward_port')}")
print(f" Forward Scheme: {host.get('forward_scheme')}")
print(f" WebSocket Support: {host.get('forward_websocket', False)}")
print(f" SSL Enabled: {host.get('ssl_enabled', False)}")
print(f" Block Exploits: {host.get('block_exploits', False)}")
print(f" Cache Assets: {host.get('cache_assets', False)}")
print()
else:
print("Could not retrieve NPMplus configuration (check .env file for NPM_EMAIL and NPM_PASSWORD)")
print()
# Summary
print("=" * 100)
print("SUMMARY")
print("=" * 100)
print()
print(f"Total VMIDs: {len(all_vms)}")
print(f" QEMU VMs: {len([v for v in all_vms if v['type'] == 'QEMU'])}")
print(f" LXC Containers: {len([v for v in all_vms if v['type'] == 'LXC'])}")
print(f" Running: {len([v for v in all_vms if v.get('status') == 'running'])}")
print(f" Stopped: {len([v for v in all_vms if v.get('status') == 'stopped'])}")
print(f"NPMplus Instances: {len(npmplus_vmids)}")
print(f"NPMplus Proxy Hosts: {len(npmplus_config)}")
print()
if __name__ == "__main__":
main()