diff --git a/promox-inventory.py b/promox-inventory.py new file mode 100644 index 0000000..70dd027 --- /dev/null +++ b/promox-inventory.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 + +import sys +import re +import subprocess +import json +import socket +import os + +def run_local_cmd(cmd): + try: + result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10) + if result.returncode != 0: + return "" + return result.stdout.strip() + except subprocess.TimeoutExpired: + return "" + +def run_ssh_cmd(host, user, cmd, key=None): + ssh_cmd = f"ssh -o BatchMode=yes -o ConnectTimeout=5 " + if key: + ssh_cmd += f"-i {key} " + ssh_cmd += f"{user}@{host} '{cmd}'" + return run_local_cmd(ssh_cmd) + +def parse_vm_list(output): + vms = [] + for line in output.strip().splitlines()[1:]: + fields = line.split() + if len(fields) >= 2: + vms.append(fields[0]) + return vms + +def get_config_lines(host, user, use_ssh=False, key=None, vmid=None, ct=False): + cmd = f"{'pct' if ct else 'qm'} config {vmid}" + if use_ssh: + return run_ssh_cmd(host, user, cmd, key).splitlines() + return run_local_cmd(cmd).splitlines() + +def collect_node(host, user, use_ssh=False, key=None): + rows = [] + # Node Info + hostname = run_ssh_cmd(host, user, "hostname -f", key) if use_ssh else run_local_cmd("hostname -f") + ip = run_ssh_cmd(host, user, "hostname -I | awk '{print $1}'", key) if use_ssh else run_local_cmd("hostname -I | awk '{print $1}'") + cpu = run_ssh_cmd(host, user, "nproc", key) if use_ssh else run_local_cmd("nproc") + ram = run_ssh_cmd(host, user, "free -m | awk '/Mem:/ {print $2\"M\"}'", key) if use_ssh else run_local_cmd("free -m | awk '/Mem:/ {print $2\"M\"}'") + + node_info = { + 'ID': '0', + 'Hostname': hostname if hostname else '-', + 'IP': ip if ip else '-', + 'Typ': 'Node', + 'CPU': cpu if cpu else 'UNL', + 'RAM': ram if ram else '-', + 'Storage': "-", + 'Tags': "-" + } + rows.append(node_info) + + # VMs + cmd_qm = "qm list" + vmids = parse_vm_list(run_ssh_cmd(host, user, cmd_qm, key) if use_ssh else run_local_cmd(cmd_qm)) + for vmid in vmids: + cfg = get_config_lines(host, user, use_ssh, key, vmid) + row = parse_config_line(cfg, vmid, "VM") + rows.append(row) + + # CTs + cmd_pct = "pct list" + ctids = parse_vm_list(run_ssh_cmd(host, user, cmd_pct, key) if use_ssh else run_local_cmd(cmd_pct)) + for ctid in ctids: + cfg = get_config_lines(host, user, use_ssh, key, ctid, ct=True) + row = parse_config_line(cfg, ctid, "CT") + rows.append(row) + + return rows + +def parse_config_line(cfg_lines, vmid, vmtype): + cfg = "\n".join(cfg_lines) + def extract(field, default="-"): + for line in cfg_lines: + if line.strip().startswith(f"{field}:"): + return line.split(":", 1)[1].strip() + return default + + name = extract("name", f"{vmtype}-{vmid}") + hostname = extract("hostname", name) + cores = extract("cores", "UNL") + memory = extract("memory", "0") + "M" + + ip = "-" + for line in cfg_lines: + if "ip=" in line: + ip = line.split("ip=")[1].split(',')[0] + break + + tags = "Yes" if any(line.strip().startswith("tags:") for line in cfg_lines) else "No" + + storage_parts = [] + for line in cfg_lines: + if any(disk in line for disk in ["scsi", "ide", "virtio", "sata", "mp", "rootfs"]): + if "none" in line: + continue + m = re.search(r"size=([^, ]+)", line) + if m: + storage_parts.append(m.group(1)) + storage = "+".join(storage_parts) if storage_parts else "-" + + return { + 'ID': vmid, + 'Hostname': hostname, + 'IP': ip, + 'Typ': vmtype, + 'CPU': cores, + 'RAM': memory, + 'Storage': storage, + 'Tags': tags + } + +def print_table(rows): + columns = ['ID', 'Hostname', 'IP', 'Typ', 'CPU', 'RAM', 'Storage', 'Tags'] + max_width = { + 'ID': 8, + 'Hostname': 25, + 'IP': 17, + 'Typ': 7, + 'CPU': 6, + 'RAM': 8, + 'Storage': 20, + 'Tags': 5, + } + + # Header + header = "" + for col in columns: + header += f"{col.ljust(max_width[col])} " + print(header.strip()) + print('-' * (sum(max_width.values()) + len(columns) - 1)) + + # Rows + for row in rows: + line = "" + for col in columns: + val = str(row.get(col, '-')) + if len(val) > max_width[col]: + val = val[:max_width[col]-3] + "..." + line += f"{val.ljust(max_width[col])} " + print(line.strip()) + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Proxmox Inventory") + parser.add_argument("--local", action="store_true", help="Nur lokale Node abfragen") + parser.add_argument("--nodes", help="Komma-separierte Liste von Nodes") + parser.add_argument("--user", default="root", help="SSH User") + parser.add_argument("--key", help="Pfad zum SSH-Key") + args = parser.parse_args() + + hosts = ["localhost"] if args.local else (args.nodes.split(",") if args.nodes else []) + if not hosts: + print("Fehler: Entweder --local oder --nodes muss angegeben werden.") + sys.exit(1) + + use_ssh = not args.local + + all_rows = [] + for h in hosts: + all_rows.extend(collect_node(h, args.user, use_ssh, args.key)) + + print_table(all_rows) + +if __name__ == "__main__": + main()