claude-configs/skills/proxmox/proxmox_client.py
Cal Corum 8a1d15911f Initial commit: Claude Code configuration backup
Version control Claude Code configuration including:
- Global instructions (CLAUDE.md)
- User settings (settings.json)
- Custom agents (architect, designer, engineer, etc.)
- Custom skills (create-skill templates and workflows)

Excludes session data, secrets, cache, and temporary files per .gitignore.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 16:34:21 -06:00

822 lines
24 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Proxmox API Client Library for Jarvis PAI
Provides high-level interface for Proxmox VE operations
"""
import json
import os
from pathlib import Path
from typing import Dict, List, Optional, Any
from proxmoxer import ProxmoxAPI
import urllib3
# Disable SSL warnings for self-signed certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class ProxmoxClient:
"""High-level Proxmox VE API client"""
def __init__(self, credentials_path: Optional[str] = None):
"""
Initialize Proxmox client with credentials
Args:
credentials_path: Path to credentials JSON file
Defaults to ~/.claude/secrets/proxmox.json
"""
if credentials_path is None:
credentials_path = os.path.expanduser("~/.claude/secrets/proxmox.json")
self.creds = self._load_credentials(credentials_path)
self.proxmox = self._connect()
self.node = self.creds.get("node", "pve")
def _load_credentials(self, path: str) -> Dict[str, Any]:
"""Load credentials from JSON file"""
with open(path, 'r') as f:
return json.load(f)
def _connect(self) -> ProxmoxAPI:
"""Establish connection to Proxmox API"""
# Split token_id into user and token parts
# Format: "root@pam!jarvis" -> user="root@pam", token="jarvis"
token_id_parts = self.creds["token_id"].split("!")
user = token_id_parts[0]
token_name = token_id_parts[1]
return ProxmoxAPI(
self.creds["host"],
port=self.creds.get("port", 8006),
user=user,
token_name=token_name,
token_value=self.creds["token_secret"],
verify_ssl=self.creds.get("verify_ssl", False)
)
# === VM Lifecycle Operations ===
def list_vms(self, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
List all VMs on a node
Args:
node: Proxmox node name (defaults to configured node)
Returns:
List of VM dictionaries with vmid, name, status, etc.
"""
node = node or self.node
return self.proxmox.nodes(node).qemu.get()
def get_vm(self, vmid: int, node: Optional[str] = None) -> Dict[str, Any]:
"""
Get detailed information about a specific VM
Args:
vmid: VM ID
node: Proxmox node name
Returns:
Dictionary with VM configuration and status
"""
node = node or self.node
config = self.proxmox.nodes(node).qemu(vmid).config.get()
status = self.proxmox.nodes(node).qemu(vmid).status.current.get()
return {**config, **status}
def start_vm(self, vmid: int, node: Optional[str] = None) -> str:
"""
Start a VM
Args:
vmid: VM ID
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).status.start.post()
def stop_vm(self, vmid: int, node: Optional[str] = None, force: bool = False) -> str:
"""
Stop a VM gracefully (or force shutdown)
Args:
vmid: VM ID
node: Proxmox node name
force: If True, force immediate shutdown
Returns:
Task ID (UPID)
"""
node = node or self.node
if force:
return self.proxmox.nodes(node).qemu(vmid).status.stop.post(forceStop=1)
return self.proxmox.nodes(node).qemu(vmid).status.stop.post()
def restart_vm(self, vmid: int, node: Optional[str] = None) -> str:
"""
Restart a VM
Args:
vmid: VM ID
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).status.reboot.post()
def shutdown_vm(self, vmid: int, node: Optional[str] = None, timeout: int = 60) -> str:
"""
Graceful VM shutdown with timeout
Args:
vmid: VM ID
node: Proxmox node name
timeout: Seconds to wait before forcing shutdown
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).status.shutdown.post(timeout=timeout)
# === VM Creation and Cloning ===
def create_vm(self, vmid: int, name: str, **kwargs) -> str:
"""
Create a new VM
Args:
vmid: VM ID
name: VM name
**kwargs: Additional VM parameters (memory, cores, etc.)
Returns:
Task ID (UPID)
"""
node = kwargs.pop("node", self.node)
params = {"vmid": vmid, "name": name, **kwargs}
return self.proxmox.nodes(node).qemu.post(**params)
def clone_vm(self, vmid: int, newid: int, name: Optional[str] = None,
node: Optional[str] = None, full: bool = True) -> str:
"""
Clone an existing VM
Args:
vmid: Source VM ID
newid: New VM ID
name: Name for cloned VM
node: Proxmox node name
full: If True, create full clone (not linked)
Returns:
Task ID (UPID)
"""
node = node or self.node
params = {"newid": newid, "full": 1 if full else 0}
if name:
params["name"] = name
return self.proxmox.nodes(node).qemu(vmid).clone.post(**params)
def delete_vm(self, vmid: int, node: Optional[str] = None, purge: bool = True) -> str:
"""
Delete a VM
Args:
vmid: VM ID
node: Proxmox node name
purge: If True, also delete from backup jobs and HA
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).delete(purge=1 if purge else 0)
# === Snapshot Management ===
def list_snapshots(self, vmid: int, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
List all snapshots for a VM
Args:
vmid: VM ID
node: Proxmox node name
Returns:
List of snapshot dictionaries
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).snapshot.get()
def create_snapshot(self, vmid: int, snapname: str, description: str = "",
vmstate: bool = False, node: Optional[str] = None) -> str:
"""
Create a VM snapshot
Args:
vmid: VM ID
snapname: Snapshot name
description: Snapshot description
vmstate: If True, include RAM state (for running VMs)
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
params = {
"snapname": snapname,
"description": description,
"vmstate": 1 if vmstate else 0
}
return self.proxmox.nodes(node).qemu(vmid).snapshot.post(**params)
def delete_snapshot(self, vmid: int, snapname: str, node: Optional[str] = None) -> str:
"""
Delete a VM snapshot
Args:
vmid: VM ID
snapname: Snapshot name
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).snapshot(snapname).delete()
def rollback_snapshot(self, vmid: int, snapname: str, node: Optional[str] = None) -> str:
"""
Rollback VM to a snapshot
Args:
vmid: VM ID
snapname: Snapshot name
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).snapshot(snapname).rollback.post()
# === Resource Monitoring ===
def get_vm_status(self, vmid: int, node: Optional[str] = None) -> Dict[str, Any]:
"""
Get current VM status and resource usage
Args:
vmid: VM ID
node: Proxmox node name
Returns:
Dictionary with status, CPU, memory, disk, network stats
"""
node = node or self.node
return self.proxmox.nodes(node).qemu(vmid).status.current.get()
def get_node_status(self, node: Optional[str] = None) -> Dict[str, Any]:
"""
Get node status and resource usage
Args:
node: Proxmox node name
Returns:
Dictionary with node stats (CPU, memory, storage, uptime)
"""
node = node or self.node
return self.proxmox.nodes(node).status.get()
def list_nodes(self) -> List[Dict[str, Any]]:
"""
List all nodes in the Proxmox cluster
Returns:
List of node dictionaries
"""
return self.proxmox.nodes.get()
# === Container (LXC) Operations ===
def list_containers(self, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
List all LXC containers on a node
Args:
node: Proxmox node name
Returns:
List of container dictionaries
"""
node = node or self.node
return self.proxmox.nodes(node).lxc.get()
def start_container(self, vmid: int, node: Optional[str] = None) -> str:
"""Start an LXC container"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).status.start.post()
def stop_container(self, vmid: int, node: Optional[str] = None) -> str:
"""Stop an LXC container"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).status.stop.post()
def get_container(self, vmid: int, node: Optional[str] = None) -> Dict[str, Any]:
"""
Get detailed information about a specific LXC container
Args:
vmid: Container ID
node: Proxmox node name
Returns:
Dictionary with container configuration and status
"""
node = node or self.node
config = self.proxmox.nodes(node).lxc(vmid).config.get()
status = self.proxmox.nodes(node).lxc(vmid).status.current.get()
return {**config, **status}
def get_container_status(self, vmid: int, node: Optional[str] = None) -> Dict[str, Any]:
"""
Get current container status and resource usage
Args:
vmid: Container ID
node: Proxmox node name
Returns:
Dictionary with status, CPU, memory, disk, network stats
"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).status.current.get()
def create_container(self, vmid: int, ostemplate: str, hostname: str,
storage: str = "local-lvm", password: Optional[str] = None,
ssh_public_keys: Optional[str] = None,
node: Optional[str] = None, **kwargs) -> str:
"""
Create a new LXC container
Args:
vmid: Container ID
ostemplate: OS template (e.g., "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst")
hostname: Container hostname
storage: Storage for container root filesystem
password: Root password (optional, SSH keys preferred)
ssh_public_keys: SSH public keys for root access
node: Proxmox node name
**kwargs: Additional container parameters (memory, cores, rootfs, net0, etc.)
Returns:
Task ID (UPID)
"""
node = node or self.node
params = {
"vmid": vmid,
"ostemplate": ostemplate,
"hostname": hostname,
"storage": storage,
**kwargs
}
if password:
params["password"] = password
if ssh_public_keys:
params["ssh-public-keys"] = ssh_public_keys
return self.proxmox.nodes(node).lxc.post(**params)
def configure_container_for_docker(self, vmid: int, node: Optional[str] = None) -> bool:
"""
Configure an LXC container to support Docker (nested containers)
Args:
vmid: Container ID
node: Proxmox node name
Returns:
True if configuration successful
"""
node = node or self.node
# Required features for Docker support
docker_config = {
"features": "nesting=1,keyctl=1",
"unprivileged": 0, # Privileged container for Docker
}
try:
self.proxmox.nodes(node).lxc(vmid).config.put(**docker_config)
return True
except Exception as e:
print(f"Error configuring container for Docker: {e}")
return False
def restart_container(self, vmid: int, node: Optional[str] = None) -> str:
"""
Restart an LXC container
Args:
vmid: Container ID
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).status.reboot.post()
def shutdown_container(self, vmid: int, node: Optional[str] = None, timeout: int = 60) -> str:
"""
Graceful container shutdown with timeout
Args:
vmid: Container ID
node: Proxmox node name
timeout: Seconds to wait before forcing shutdown
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).status.shutdown.post(timeout=timeout)
def delete_container(self, vmid: int, node: Optional[str] = None, purge: bool = True) -> str:
"""
Delete an LXC container
Args:
vmid: Container ID
node: Proxmox node name
purge: If True, also delete from backup jobs
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).delete(purge=1 if purge else 0)
def clone_container(self, vmid: int, newid: int, hostname: Optional[str] = None,
node: Optional[str] = None, full: bool = True) -> str:
"""
Clone an existing LXC container
Args:
vmid: Source container ID
newid: New container ID
hostname: Hostname for cloned container (optional)
node: Proxmox node name
full: If True, create full clone (not linked)
Returns:
Task ID (UPID)
"""
node = node or self.node
params = {"newid": newid, "full": 1 if full else 0}
if hostname:
params["hostname"] = hostname
return self.proxmox.nodes(node).lxc(vmid).clone.post(**params)
def list_container_snapshots(self, vmid: int, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
List all snapshots for an LXC container
Args:
vmid: Container ID
node: Proxmox node name
Returns:
List of snapshot dictionaries
"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).snapshot.get()
def create_container_snapshot(self, vmid: int, snapname: str, description: str = "",
node: Optional[str] = None) -> str:
"""
Create an LXC container snapshot
Args:
vmid: Container ID
snapname: Snapshot name
description: Snapshot description
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
params = {
"snapname": snapname,
"description": description
}
return self.proxmox.nodes(node).lxc(vmid).snapshot.post(**params)
def delete_container_snapshot(self, vmid: int, snapname: str, node: Optional[str] = None) -> str:
"""
Delete an LXC container snapshot
Args:
vmid: Container ID
snapname: Snapshot name
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).snapshot(snapname).delete()
def rollback_container_snapshot(self, vmid: int, snapname: str, node: Optional[str] = None) -> str:
"""
Rollback container to a snapshot
Args:
vmid: Container ID
snapname: Snapshot name
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).lxc(vmid).snapshot(snapname).rollback.post()
def get_all_containers_status(self, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Get status summary for all LXC containers
Args:
node: Proxmox node name
Returns:
List of container status dictionaries
"""
containers = self.list_containers(node)
result = []
for ct in containers:
status = self.get_container_status(ct["vmid"], node)
result.append({
"vmid": ct["vmid"],
"name": ct.get("name", ""),
"status": status.get("status", "unknown"),
"cpu": status.get("cpu", 0),
"mem": status.get("mem", 0),
"maxmem": status.get("maxmem", 0),
"disk": status.get("disk", 0),
"maxdisk": status.get("maxdisk", 0),
"uptime": status.get("uptime", 0)
})
return result
def list_container_templates(self, node: Optional[str] = None, storage: str = "local") -> List[Dict[str, Any]]:
"""
List available LXC templates
Args:
node: Proxmox node name
storage: Storage containing templates
Returns:
List of available templates
"""
node = node or self.node
return self.proxmox.nodes(node).storage(storage).content.get(content="vztmpl")
# === Storage Operations ===
def list_storage(self, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
List all storage on a node
Args:
node: Proxmox node name
Returns:
List of storage dictionaries
"""
node = node or self.node
return self.proxmox.nodes(node).storage.get()
def get_storage_status(self, storage: str, node: Optional[str] = None) -> Dict[str, Any]:
"""
Get storage status and usage
Args:
storage: Storage name
node: Proxmox node name
Returns:
Dictionary with storage stats
"""
node = node or self.node
return self.proxmox.nodes(node).storage(storage).status.get()
# === Task Management ===
def get_task_status(self, upid: str, node: Optional[str] = None) -> Dict[str, Any]:
"""
Get status of a Proxmox task
Args:
upid: Task ID (UPID)
node: Proxmox node name
Returns:
Dictionary with task status and progress
"""
node = node or self.node
return self.proxmox.nodes(node).tasks(upid).status.get()
def wait_for_task(self, upid: str, node: Optional[str] = None,
timeout: int = 300, poll_interval: int = 2) -> bool:
"""
Wait for a task to complete
Args:
upid: Task ID (UPID)
node: Proxmox node name
timeout: Maximum seconds to wait
poll_interval: Seconds between status checks
Returns:
True if task succeeded, False if failed or timeout
"""
import time
node = node or self.node
elapsed = 0
while elapsed < timeout:
status = self.get_task_status(upid, node)
if status.get("status") == "stopped":
return status.get("exitstatus") == "OK"
time.sleep(poll_interval)
elapsed += poll_interval
return False
# === Backup Operations ===
def create_backup(self, vmid: int, storage: str = "local",
mode: str = "snapshot", compress: str = "zstd",
node: Optional[str] = None) -> str:
"""
Create a VM backup
Args:
vmid: VM ID
storage: Backup storage location
mode: Backup mode (snapshot, suspend, stop)
compress: Compression algorithm (zstd, gzip, lzo)
node: Proxmox node name
Returns:
Task ID (UPID)
"""
node = node or self.node
return self.proxmox.nodes(node).vzdump.post(
vmid=vmid,
storage=storage,
mode=mode,
compress=compress
)
def list_backups(self, storage: str = "local",
node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
List backups in storage
Args:
storage: Storage name
node: Proxmox node name
Returns:
List of backup dictionaries
"""
node = node or self.node
return self.proxmox.nodes(node).storage(storage).content.get(content="backup")
# === Network Configuration ===
def list_networks(self, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
List network interfaces on a node
Args:
node: Proxmox node name
Returns:
List of network interface dictionaries
"""
node = node or self.node
return self.proxmox.nodes(node).network.get()
# === Convenience Methods ===
def get_vm_by_name(self, name: str, node: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Find a VM by name
Args:
name: VM name
node: Proxmox node name
Returns:
VM dictionary or None if not found
"""
vms = self.list_vms(node)
for vm in vms:
if vm.get("name") == name:
return self.get_vm(vm["vmid"], node)
return None
def get_all_vms_status(self, node: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Get status summary for all VMs
Args:
node: Proxmox node name
Returns:
List of VM status dictionaries with vmid, name, status, resources
"""
vms = self.list_vms(node)
result = []
for vm in vms:
status = self.get_vm_status(vm["vmid"], node)
result.append({
"vmid": vm["vmid"],
"name": vm.get("name", ""),
"status": status.get("status", "unknown"),
"cpu": status.get("cpu", 0),
"mem": status.get("mem", 0),
"maxmem": status.get("maxmem", 0),
"disk": status.get("disk", 0),
"maxdisk": status.get("maxdisk", 0),
"uptime": status.get("uptime", 0)
})
return result
def main():
"""CLI interface for testing"""
import sys
if len(sys.argv) < 2:
print("Usage: proxmox_client.py <command> [args...]")
print("\nCommands:")
print(" list - List all VMs")
print(" status <vmid> - Get VM status")
print(" start <vmid> - Start a VM")
print(" stop <vmid> - Stop a VM")
print(" nodes - List all nodes")
sys.exit(1)
client = ProxmoxClient()
command = sys.argv[1]
if command == "list":
vms = client.get_all_vms_status()
for vm in vms:
print(f"VM {vm['vmid']}: {vm['name']} - {vm['status']}")
elif command == "status" and len(sys.argv) > 2:
vmid = int(sys.argv[2])
status = client.get_vm_status(vmid)
print(json.dumps(status, indent=2))
elif command == "start" and len(sys.argv) > 2:
vmid = int(sys.argv[2])
upid = client.start_vm(vmid)
print(f"Starting VM {vmid}... Task ID: {upid}")
elif command == "stop" and len(sys.argv) > 2:
vmid = int(sys.argv[2])
upid = client.stop_vm(vmid)
print(f"Stopping VM {vmid}... Task ID: {upid}")
elif command == "nodes":
nodes = client.list_nodes()
for node in nodes:
print(f"Node: {node['node']} - Status: {node['status']}")
else:
print(f"Unknown command: {command}")
sys.exit(1)
if __name__ == "__main__":
main()