Proxmox VM 정보 추출 자동화 스크립트

1. 개발 목표

Proxmox VE환경에서 운영 중인 모든 가상머신과 컨테이너의 핵심 자원 정보를 자동으로 수집하여, 자원 현황 파악 및 관리를 용이하게 하고자 함. 최종 결과물은 사람이 읽기 쉬운 YAML 형식과 데이터 활용에 용이한 Excel형식으로 동시에 생성하는 것을 목표로 함.

2. 적용 기술 및 구성

  • 개발 언어: Python 3

  • 핵심 라이브러리:

    • proxmoxer: Proxmox VE API와의 통신을 담당하는 핵심 라이브러리.

    • PyYAML: 수집된 데이터를 YAML 형식의 파일로 변환 및 저장.

    • openpyxl: 수집된 데이터를 Excel(.xlsx) 형식의 파일로 변환 및 저장.

  • 인증 방식:

    • 최종적으로 Proxmox에서 공식적으로 권장하고, 가장 안정적인 API 토큰(API Token) 인증 방식을 채택하여 안정적인 연결을 확보함.


3. 스크립트의 주요 기능

스크립트는 다음과 같은 순서로 동작하며, 지정된 정보를 수집하여 파일로 저장합니다.

  1. API 연결: 사용자가 스크립트에 입력한 API 토큰 정보를 사용하여 Proxmox 서버에 안전하게 연결합니다.

  2. 노드 순회: 클러스터 내의 모든 물리 서버(노드)를 자동으로 탐색합니다.

  3. 자원 정보 수집: 각 노드를 순회하며 아래의 정보를 수집합니다.

    • id: 가상머신/컨테이너의 고유 ID (vmid)

    • node: 서버 노드
    • hostname: 가상머신/컨테이너의 이름

    • ip: QEMU Guest Agent를 통해 확인한 내부 IP 주소 (Agent 미설치 시 'N/A')

    • vcpu: 할당된 가상 CPU 코어 수

    • ram_gb: 할당된 메모리 용량 (단위를 GB로 변환하여 표시)

    • disk_gb: 할당된 디스크 총용량 (단위를 GB로 변환하여 표시)

    • note: Proxmox 웹 UI의 'Notes' 탭에 작성된 내용 (상세 설정 조회)

  4. 파일 생성: 수집된 모든 정보를 취합하여 proxmox_inventory.ymlproxmox_inventory.xlsx 두 개의 파일로 동시 저장합니다.


proxmox_exporter.py
import os
from proxmoxer import ProxmoxAPI
import yaml
from openpyxl import Workbook

# --- Proxmox 접속 정보 (API 토큰 방식) ---

PROXMOX_HOST = 'changeme'  # IP를 입력

PROXMOX_TOKEN_ID = 'changeme' # Token ID
PROXMOX_TOKEN_SECRET = 'changeme' # Secret 값

# SSL 인증서 경고를 무시
VERIFY_SSL = False

def get_guest_ip(proxmox, node, vmid, guest_type):
    """QEMU Guest Agent를 통해 게스트의 IP 주소를 가져옵니다."""
    try:
        if guest_type == 'qemu':
            interfaces = proxmox.nodes(node).qemu(vmid).agent.get('network-get-interfaces')['result']
        else: # lxc
            interfaces = proxmox.nodes(node).lxc(vmid).agent.get('network-get-interfaces')['result']

        for iface in interfaces:
            if 'ip-addresses' not in iface:
                continue
            for ip in iface['ip-addresses']:
                if ip.get('ip-address-type') == 'ipv4' and not ip.get('ip-address', '').startswith('127.'):
                    return ip['ip-address']
    except Exception:
        pass
    return 'N/A'

def collect_proxmox_data():
    """Proxmox 서버에서 VM 및 컨테이너 정보를 수집합니다."""
    try:
        proxmox = ProxmoxAPI(
            PROXMOX_HOST,
            user='root@pam',
            token_name=PROXMOX_TOKEN_ID.split('!')[1],
            token_value=PROXMOX_TOKEN_SECRET,
            verify_ssl=VERIFY_SSL
        )
    except Exception as e:
        print(f"Error connecting to Proxmox: {e}")
        return None

    all_guests_info = []
    print("Successfully connected to Proxmox. Collecting guest information...")

    for node in proxmox.nodes.get():
        node_name = node['node']
        print(f"Scanning node: {node_name}")

        # 가상머신(VM) 정보 수집
        for vm_summary in proxmox.nodes(node_name).qemu.get():
            vmid = vm_summary['vmid']
            vm_config = proxmox.nodes(node_name).qemu(vmid).config.get()

            vm_info = {
                'id': vmid,
                'node': node_name,
                'hostname': vm_summary['name'],
                'ip': get_guest_ip(proxmox, node_name, vmid, 'qemu'),
                'vcpu': vm_summary['cpus'],
                'ram_gb': round(vm_summary['maxmem'] / (1024**3), 2),
                'disk_gb': round(vm_summary.get('maxdisk', 0) / (1024**3), 2),
                'note': vm_config.get('description', '').replace('\n', ' ')
            }
            all_guests_info.append(vm_info)

        # 컨테이너(LXC) 정보 수집
        for lxc_summary in proxmox.nodes(node_name).lxc.get():
            vmid = lxc_summary['vmid']
            lxc_config = proxmox.nodes(node_name).lxc(vmid).config.get()

            lxc_info = {
                'id': vmid,
                'node': node_name,
                'hostname': lxc_summary['name'],
                'ip': get_guest_ip(proxmox, node_name, vmid, 'lxc'),
                'vcpu': lxc_summary['cpus'],
                'ram_gb': round(lxc_summary['maxmem'] / (1024**3), 2),
                'disk_gb': round(lxc_summary.get('maxdisk', 0) / (1024**3), 2),
                'note': lxc_config.get('description', '').replace('\n', ' ')
            }
            all_guests_info.append(lxc_info)

    return sorted(all_guests_info, key=lambda x: x['id'])

def save_to_yaml(data, filename='proxmox_inventory.yml'):
    with open(filename, 'w', encoding='utf-8') as f:
        yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
    print(f"✅ Data successfully saved to {filename}")

def save_to_excel(data, filename='proxmox_inventory.xlsx'):
    if not data: return
    wb = Workbook()
    ws = wb.active
    ws.title = "Proxmox Inventory"
    headers = list(data[0].keys())
    ws.append(headers)
    for item in data:
        ws.append(list(item.values()))
    for col in ws.columns:
        max_length = 0
        column = col[0].column_letter
        for cell in col:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(cell.value)
            except: pass
        adjusted_width = (max_length + 2)
        ws.column_dimensions[column].width = adjusted_width
    wb.save(filename)
    print(f"✅ Data successfully saved to {filename}")


if __name__ == "__main__":
    script_dir = os.path.dirname(os.path.abspath(__file__))
    if script_dir: os.chdir(script_dir)

    print("Attempting to collect data from Proxmox using API Token...")
    inventory_data = collect_proxmox_data()

    if inventory_data:
        print(f"Successfully collected data for {len(inventory_data)} guests.")
        save_to_yaml(inventory_data)
        save_to_excel(inventory_data)
    else:
        print("Could not collect any data. Please check API Token, host, and permissions.")