Skip to main content
Glama
vm.py20.2 kB
""" VM-related tools for Proxmox MCP. This module provides tools for managing and interacting with Proxmox VMs: - Listing all VMs across the cluster with their status - Retrieving detailed VM information including: * Resource allocation (CPU, memory) * Runtime status * Node placement - Executing commands within VMs via QEMU guest agent - Handling VM console operations - VM power management (start, stop, shutdown, reset) - VM creation with customizable specifications The tools implement fallback mechanisms for scenarios where detailed VM information might be temporarily unavailable. """ from typing import List, Optional from mcp.types import TextContent as Content from .base import ProxmoxTool from .definitions import GET_VMS_DESC, EXECUTE_VM_COMMAND_DESC from .console.manager import VMConsoleManager class VMTools(ProxmoxTool): """Tools for managing Proxmox VMs. Provides functionality for: - Retrieving cluster-wide VM information - Getting detailed VM status and configuration - Executing commands within VMs - Managing VM console operations - VM power management (start, stop, shutdown, reset) - VM creation with customizable specifications Implements fallback mechanisms for scenarios where detailed VM information might be temporarily unavailable. Integrates with QEMU guest agent for VM command execution. """ def __init__(self, proxmox_api): """Initialize VM tools. Args: proxmox_api: Initialized ProxmoxAPI instance """ super().__init__(proxmox_api) self.console_manager = VMConsoleManager(proxmox_api) def get_vms(self) -> List[Content]: """List all virtual machines across the cluster with detailed status. Retrieves comprehensive information for each VM including: - Basic identification (ID, name) - Runtime status (running, stopped) - Resource allocation and usage: * CPU cores * Memory allocation and usage - Node placement Implements a fallback mechanism that returns basic information if detailed configuration retrieval fails for any VM. Returns: List of Content objects containing formatted VM information: { "vmid": "100", "name": "vm-name", "status": "running/stopped", "node": "node-name", "cpus": core_count, "memory": { "used": bytes, "total": bytes } } Raises: RuntimeError: If the cluster-wide VM query fails """ try: result = [] for node in self.proxmox.nodes.get(): node_name = node["node"] vms = self.proxmox.nodes(node_name).qemu.get() for vm in vms: vmid = vm["vmid"] # Get VM config for CPU cores try: config = self.proxmox.nodes(node_name).qemu(vmid).config.get() result.append({ "vmid": vmid, "name": vm["name"], "status": vm["status"], "node": node_name, "cpus": config.get("cores", "N/A"), "memory": { "used": vm.get("mem", 0), "total": vm.get("maxmem", 0) } }) except Exception: # Fallback if can't get config result.append({ "vmid": vmid, "name": vm["name"], "status": vm["status"], "node": node_name, "cpus": "N/A", "memory": { "used": vm.get("mem", 0), "total": vm.get("maxmem", 0) } }) return self._format_response(result, "vms") except Exception as e: self._handle_error("get VMs", e) def create_vm(self, node: str, vmid: str, name: str, cpus: int, memory: int, disk_size: int, storage: Optional[str] = None, ostype: Optional[str] = None) -> List[Content]: """Create a new virtual machine with specified configuration. Args: node: Host node name (e.g., 'pve') vmid: New VM ID number (e.g., '200') name: VM name (e.g., 'my-new-vm') cpus: Number of CPU cores (e.g., 1, 2, 4) memory: Memory size in MB (e.g., 2048 for 2GB) disk_size: Disk size in GB (e.g., 10, 20, 50) storage: Storage name (e.g., 'local-lvm', 'vm-storage'). If None, will auto-detect ostype: OS type (e.g., 'l26' for Linux, 'win10' for Windows). Default: 'l26' Returns: List of Content objects containing creation result Raises: ValueError: If VM ID already exists or invalid parameters RuntimeError: If VM creation fails """ try: # Check if VM ID already exists try: existing_vm = self.proxmox.nodes(node).qemu(vmid).config.get() raise ValueError(f"VM {vmid} already exists on node {node}") except Exception as e: if "does not exist" not in str(e).lower(): raise e # Get storage information storage_list = self.proxmox.nodes(node).storage.get() storage_info = {} for s in storage_list: storage_info[s["storage"]] = s # Auto-detect storage if not specified if storage is None: # Prefer local-lvm for VM images first for s in storage_list: if s["storage"] == "local-lvm" and "images" in s.get("content", ""): storage = s["storage"] break if storage is None: # Then try vm-storage for s in storage_list: if s["storage"] == "vm-storage" and "images" in s.get("content", ""): storage = s["storage"] break if storage is None: # Fallback to any storage that supports images for s in storage_list: if "images" in s.get("content", ""): storage = s["storage"] break if storage is None: raise ValueError("No suitable storage found for VM images") # Validate storage exists and supports images if storage not in storage_info: raise ValueError(f"Storage '{storage}' not found on node {node}") if "images" not in storage_info[storage].get("content", ""): raise ValueError(f"Storage '{storage}' does not support VM images") # Determine appropriate disk format based on storage type storage_type = storage_info[storage]["type"] if storage_type in ["lvm", "lvmthin"]: # LVM storages use raw format and no cloudinit disk_format = "raw" vm_config_storage = { "scsi0": f"{storage}:{disk_size},format={disk_format}", } elif storage_type in ["dir", "nfs", "cifs"]: # File-based storages can use qcow2 disk_format = "qcow2" vm_config_storage = { "scsi0": f"{storage}:{disk_size},format={disk_format}", "ide2": f"{storage}:cloudinit", } else: # Default to raw for unknown storage types disk_format = "raw" vm_config_storage = { "scsi0": f"{storage}:{disk_size},format={disk_format}", } # Set default OS type if ostype is None: ostype = "l26" # Linux 2.6+ kernel # Prepare VM configuration vm_config = { "vmid": vmid, "name": name, "cores": cpus, "memory": memory, "ostype": ostype, "scsihw": "virtio-scsi-pci", "boot": "order=scsi0", "agent": "1", # Enable QEMU guest agent "vga": "std", "net0": "virtio,bridge=vmbr0", } # Add storage configuration vm_config.update(vm_config_storage) # Create the VM task_result = self.proxmox.nodes(node).qemu.create(**vm_config) cloudinit_note = "" if storage_type in ["lvm", "lvmthin"]: cloudinit_note = "\n ⚠️ Note: LVM storage doesn't support cloud-init image" result_text = f"""🎉 VM {vmid} created successfully! 📋 VM Configuration: • Name: {name} • Node: {node} • VM ID: {vmid} • CPU Cores: {cpus} • Memory: {memory} MB ({memory/1024:.1f} GB) • Disk: {disk_size} GB ({storage}, {disk_format} format) • Storage Type: {storage_type} • OS Type: {ostype} • Network: virtio (bridge=vmbr0) • QEMU Agent: Enabled{cloudinit_note} 🔧 Task ID: {task_result} 💡 Next steps: 1. Upload an ISO to install the operating system 2. Start the VM using start_vm tool 3. Access the console to complete OS installation""" return [Content(type="text", text=result_text)] except ValueError as e: raise e except Exception as e: self._handle_error(f"create VM {vmid}", e) def start_vm(self, node: str, vmid: str) -> List[Content]: """Start a virtual machine. Args: node: Host node name (e.g., 'pve1', 'proxmox-node2') vmid: VM ID number (e.g., '100', '101') Returns: List of Content objects containing operation result Raises: ValueError: If VM is not found RuntimeError: If start operation fails """ try: # Check if VM exists and get current status vm_status = self.proxmox.nodes(node).qemu(vmid).status.current.get() current_status = vm_status.get("status") if current_status == "running": result_text = f"🟢 VM {vmid} is already running" else: # Start the VM task_result = self.proxmox.nodes(node).qemu(vmid).status.start.post() result_text = f"🚀 VM {vmid} start initiated successfully\nTask ID: {task_result}" return [Content(type="text", text=result_text)] except Exception as e: if "does not exist" in str(e).lower() or "not found" in str(e).lower(): raise ValueError(f"VM {vmid} not found on node {node}") self._handle_error(f"start VM {vmid}", e) def stop_vm(self, node: str, vmid: str) -> List[Content]: """Stop a virtual machine (force stop). Args: node: Host node name (e.g., 'pve1', 'proxmox-node2') vmid: VM ID number (e.g., '100', '101') Returns: List of Content objects containing operation result Raises: ValueError: If VM is not found RuntimeError: If stop operation fails """ try: # Check if VM exists and get current status vm_status = self.proxmox.nodes(node).qemu(vmid).status.current.get() current_status = vm_status.get("status") if current_status == "stopped": result_text = f"🔴 VM {vmid} is already stopped" else: # Stop the VM task_result = self.proxmox.nodes(node).qemu(vmid).status.stop.post() result_text = f"🛑 VM {vmid} stop initiated successfully\nTask ID: {task_result}" return [Content(type="text", text=result_text)] except Exception as e: if "does not exist" in str(e).lower() or "not found" in str(e).lower(): raise ValueError(f"VM {vmid} not found on node {node}") self._handle_error(f"stop VM {vmid}", e) def shutdown_vm(self, node: str, vmid: str) -> List[Content]: """Shutdown a virtual machine gracefully. Args: node: Host node name (e.g., 'pve1', 'proxmox-node2') vmid: VM ID number (e.g., '100', '101') Returns: List of Content objects containing operation result Raises: ValueError: If VM is not found RuntimeError: If shutdown operation fails """ try: # Check if VM exists and get current status vm_status = self.proxmox.nodes(node).qemu(vmid).status.current.get() current_status = vm_status.get("status") if current_status == "stopped": result_text = f"🔴 VM {vmid} is already stopped" else: # Shutdown the VM gracefully task_result = self.proxmox.nodes(node).qemu(vmid).status.shutdown.post() result_text = f"💤 VM {vmid} graceful shutdown initiated\nTask ID: {task_result}" return [Content(type="text", text=result_text)] except Exception as e: if "does not exist" in str(e).lower() or "not found" in str(e).lower(): raise ValueError(f"VM {vmid} not found on node {node}") self._handle_error(f"shutdown VM {vmid}", e) def reset_vm(self, node: str, vmid: str) -> List[Content]: """Reset (restart) a virtual machine. Args: node: Host node name (e.g., 'pve1', 'proxmox-node2') vmid: VM ID number (e.g., '100', '101') Returns: List of Content objects containing operation result Raises: ValueError: If VM is not found RuntimeError: If reset operation fails """ try: # Check if VM exists and get current status vm_status = self.proxmox.nodes(node).qemu(vmid).status.current.get() current_status = vm_status.get("status") if current_status == "stopped": result_text = f"⚠️ Cannot reset VM {vmid}: VM is currently stopped\nUse start_vm to start it first" else: # Reset the VM task_result = self.proxmox.nodes(node).qemu(vmid).status.reset.post() result_text = f"🔄 VM {vmid} reset initiated successfully\nTask ID: {task_result}" return [Content(type="text", text=result_text)] except Exception as e: if "does not exist" in str(e).lower() or "not found" in str(e).lower(): raise ValueError(f"VM {vmid} not found on node {node}") self._handle_error(f"reset VM {vmid}", e) async def execute_command(self, node: str, vmid: str, command: str) -> List[Content]: """Execute a command in a VM via QEMU guest agent. Uses the QEMU guest agent to execute commands within a running VM. Requires: - VM must be running - QEMU guest agent must be installed and running in the VM - Command execution permissions must be enabled Args: node: Host node name (e.g., 'pve1', 'proxmox-node2') vmid: VM ID number (e.g., '100', '101') command: Shell command to run (e.g., 'uname -a', 'systemctl status nginx') Returns: List of Content objects containing formatted command output: { "success": true/false, "output": "command output", "error": "error message if any" } Raises: ValueError: If VM is not found, not running, or guest agent is not available RuntimeError: If command execution fails due to permissions or other issues """ try: result = await self.console_manager.execute_command(node, vmid, command) # Use the command output formatter from ProxmoxFormatters from ..formatting import ProxmoxFormatters formatted = ProxmoxFormatters.format_command_output( success=result["success"], command=command, output=result["output"], error=result.get("error") ) return [Content(type="text", text=formatted)] except Exception as e: self._handle_error(f"execute command on VM {vmid}", e) def delete_vm(self, node: str, vmid: str, force: bool = False) -> List[Content]: """Delete/remove a virtual machine completely. This will permanently delete the VM and all its associated data including: - VM configuration - Virtual disks - Snapshots WARNING: This operation cannot be undone! Args: node: Host node name (e.g., 'pve1', 'proxmox-node2') vmid: VM ID number (e.g., '100', '101') force: Force deletion even if VM is running (will stop first) Returns: List of Content objects containing deletion result Raises: ValueError: If VM is not found or is running and force=False RuntimeError: If deletion fails """ try: # Check if VM exists and get current status try: vm_status = self.proxmox.nodes(node).qemu(vmid).status.current.get() current_status = vm_status.get("status") vm_name = vm_status.get("name", f"VM-{vmid}") except Exception as e: if "does not exist" in str(e).lower() or "not found" in str(e).lower(): raise ValueError(f"VM {vmid} not found on node {node}") raise e # Check if VM is running if current_status == "running": if not force: raise ValueError(f"VM {vmid} ({vm_name}) is currently running. " f"Please stop it first or use force=True to stop and delete.") else: # Force stop the VM first self.proxmox.nodes(node).qemu(vmid).status.stop.post() result_text = f"🛑 Stopping VM {vmid} ({vm_name}) before deletion...\n" else: result_text = f"🗑️ Deleting VM {vmid} ({vm_name})...\n" # Delete the VM task_result = self.proxmox.nodes(node).qemu(vmid).delete() result_text += f"""🗑️ VM {vmid} ({vm_name}) deletion initiated successfully! ⚠️ WARNING: This operation will permanently remove: • VM configuration • All virtual disks • All snapshots • Cannot be undone! 🔧 Task ID: {task_result} ✅ VM {vmid} ({vm_name}) is being deleted from node {node}""" return [Content(type="text", text=result_text)] except ValueError as e: raise e except Exception as e: self._handle_error(f"delete VM {vmid}", e)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/RekklesNA/ProxmoxMCP-Plus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server