From 1d7e9c2d4edf6051038a281bfe37e0cdd5606714 Mon Sep 17 00:00:00 2001 From: gilby125 Date: Thu, 6 Nov 2025 11:13:58 -0600 Subject: [PATCH] Add proxmox_create_vm tool, comprehensive test suite, and documentation improvements New Features: - Add proxmox_create_vm tool for QEMU VM creation - Add test-basic-tools.js for validating basic operations (7/7 tests) - Add test-workflows.js for lifecycle workflow testing (19/22 tests) - Add TEST-WORKFLOWS.md with comprehensive test documentation Bug Fixes: - Fix regex patterns in test scripts for MCP formatted output - Increase snapshot wait times and add retry logic - Handle markdown formatting in tool responses Documentation: - Update tool count from 54 to 55 - Add Claude Desktop integration with OS-specific paths - Document proxmox_create_vm with full parameters and examples - Add testing section with usage examples Other: - Update .gitignore for Node.js (node_modules, package-lock.json) --- .gitignore | 9 + README.md | 1201 ++++++++++++++++++- TEST-WORKFLOWS.md | 283 +++++ index.js | 2698 ++++++++++++++++++++++++++++++++++++++++++- test-basic-tools.js | 304 +++++ test-workflows.js | 948 +++++++++++++++ 6 files changed, 5357 insertions(+), 86 deletions(-) create mode 100644 TEST-WORKFLOWS.md create mode 100755 test-basic-tools.js create mode 100755 test-workflows.js diff --git a/.gitignore b/.gitignore index e8f7bb5..4715e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,14 @@ coverage.xml # UV .uv/ +# Node.js +node_modules/ +package-lock.json +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm + # Local configuration config/config.json +CLAUDE.md diff --git a/README.md b/README.md index e6020eb..a3d1f40 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ This project is based on the original Python implementation by [canvrno/ProxmoxM ## โœจ Features - ๐Ÿ”’ **Configurable Security**: Two permission levels for safe operation + - โš ๏ธ **Default: Read-only mode** - Safe for production use + - โš ๏ธ **Elevated mode: Enables 49 destructive operations** - Use with extreme caution - ๐Ÿ› ๏ธ Built with the official MCP SDK for Node.js - ๐Ÿ” Secure token-based authentication with Proxmox - ๐Ÿ–ฅ๏ธ Comprehensive node and VM management @@ -66,48 +68,90 @@ Before starting, ensure you have: npm install ``` -2. Create `.env` file with your Proxmox configuration: +2. Create `.env` file in the **parent directory** of your installation: ```bash - # Proxmox Configuration - PROXMOX_HOST=192.168.1.100 - PROXMOX_USER=root@pam - PROXMOX_TOKEN_NAME=mcp-server - PROXMOX_TOKEN_VALUE=your-token-value-here - PROXMOX_ALLOW_ELEVATED=false # Set to 'true' for advanced features + # If you cloned to: /home/user/mcp-proxmox + # Create .env at: /home/user/.env + + cd .. + nano .env # or use your preferred editor ``` - **Note**: `PROXMOX_PORT` defaults to 8006 and can be omitted unless using a custom port. +3. Add your Proxmox configuration to `.env`: + ```bash + # Proxmox Configuration (REQUIRED) + PROXMOX_HOST=your-proxmox-ip-or-hostname + PROXMOX_USER=root@pam + PROXMOX_TOKEN_NAME=your-token-name + PROXMOX_TOKEN_VALUE=your-token-secret-here + + # Security Settings (REQUIRED) + PROXMOX_ALLOW_ELEVATED=false # Set to 'true' for advanced features + + # โš ๏ธ WARNING: Setting PROXMOX_ALLOW_ELEVATED=true enables DESTRUCTIVE operations + # This allows creating, deleting, modifying VMs/containers, snapshots, backups, etc. + # Only enable if you understand the security implications! + + # Optional Settings (can be omitted) + # PROXMOX_PORT=8006 # Defaults to 8006 + ``` + + **Important Notes**: + - The `.env` file MUST be placed in the parent directory of the `mcp-proxmox` installation + - `PROXMOX_TOKEN_VALUE` is REQUIRED - there is no default value + - `PROXMOX_HOST` defaults to `192.168.6.247` if not specified (change this!) + - `PROXMOX_TOKEN_NAME` defaults to `mcpserver` if not specified + + **โš ๏ธ Security Warning**: + - `PROXMOX_ALLOW_ELEVATED=false` is the SAFE default - only read operations allowed + - `PROXMOX_ALLOW_ELEVATED=true` enables 49 DESTRUCTIVE tools that can: + - Create, delete, start, stop, reboot VMs and containers + - Delete snapshots and backups + - Modify disk configurations, network settings, and resource allocations + - Execute commands inside VMs/containers + - **Only set to `true` if you fully understand and accept these risks** ### Permission Levels **Basic Mode** (`PROXMOX_ALLOW_ELEVATED=false`): - List cluster nodes and their status - List VMs and containers +- View storage pools - Basic cluster health overview - Requires minimal API token permissions **Elevated Mode** (`PROXMOX_ALLOW_ELEVATED=true`): +- โš ๏ธ **WARNING: Enables destructive operations** - Use with caution! - All basic features plus: - Detailed node resource metrics - VM command execution - Advanced cluster statistics -- Requires API token with `Sys.Audit`, `VM.Monitor`, `VM.Console` permissions +- **Create/Delete VMs and containers** (requires `VM.Allocate`) +- **Start/Stop/Reboot/Shutdown** (requires `VM.PowerMgmt`) +- **Snapshot and backup management** (requires `VM.Snapshot`, `VM.Backup`) +- **Disk and network configuration** (requires `VM.Config`) +- Recommended API token permissions: `Sys.Audit`, `VM.Monitor`, `VM.Console`, `VM.Allocate`, `VM.PowerMgmt`, `VM.Snapshot`, `VM.Backup`, `VM.Config`, `Datastore.Audit`, `Datastore.Allocate` ### Verifying Installation -1. Test the MCP server: +1. Return to the mcp-proxmox directory: + ```bash + cd mcp-proxmox + ``` + +2. Test the MCP server: ```bash echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' | node index.js ``` -2. Test a basic API call: +3. Test a basic API call: ```bash echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "proxmox_get_nodes", "arguments": {}}}' | node index.js ``` You should see either: - A successful list of your Proxmox nodes - - Or a connection/permission error with helpful guidance + - Or an error message - if you see "Could not load .env file", verify the .env file is in the parent directory ## โš™๏ธ Configuration @@ -135,30 +179,119 @@ Before starting, ensure you have: node index.js ``` -### MCP Client Integration +### Claude Desktop Integration -For Claude Code or other MCP clients, add this to your MCP configuration: +#### Config File Location + +Add the configuration to your Claude Desktop config file: + +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` +- **Linux**: `~/.config/Claude/claude_desktop_config.json` + +#### Option 1: Using External .env File (Recommended) ```json { "mcpServers": { - "mcp-proxmox": { + "proxmox": { "command": "node", - "args": ["index.js"], - "cwd": "/absolute/path/to/mcp-proxmox" + "args": ["/absolute/path/to/mcp-proxmox/index.js"] } } } ``` -**Important**: +**Important - Environment File Location**: - Replace `/absolute/path/to/mcp-proxmox` with the actual path to your installation -- The server automatically loads environment variables from `.env` files -- Ensure the `.env` file is in the same directory as `index.js` or a parent directory +- The server loads environment variables from `../../.env` relative to `index.js` +- **This means**: If your installation is at `/home/user/mcp-proxmox`, place `.env` at `/home/user/.env` +- **Example directory structure**: + ``` + /home/user/ + โ”œโ”€โ”€ .env โ† Environment file goes here + โ””โ”€โ”€ mcp-proxmox/ + โ”œโ”€โ”€ index.js โ† Server looks for ../../.env from here + โ”œโ”€โ”€ package.json + โ””โ”€โ”€ README.md + ``` + +#### Option 2: Inline Environment Variables + +Alternatively, you can specify environment variables directly in the config: + +```json +{ + "mcpServers": { + "proxmox": { + "command": "node", + "args": ["/absolute/path/to/mcp-proxmox/index.js"], + "env": { + "PROXMOX_HOST": "your-proxmox-ip", + "PROXMOX_USER": "root@pam", + "PROXMOX_TOKEN_NAME": "mcp-server", + "PROXMOX_TOKEN_VALUE": "your-token-secret", + "PROXMOX_ALLOW_ELEVATED": "false", + "PROXMOX_PORT": "8006" + } + } + } +} +``` + +**After adding the configuration**: +1. Restart Claude Desktop +2. Verify the server is loaded in Claude Desktop โ†’ Settings โ†’ Developer โ†’ MCP Servers +3. Test by asking Claude: "List my Proxmox VMs" + +### Other MCP Clients + +For Claude Code, MCP Inspector, or other MCP clients, use the stdio transport configuration shown above, adjusting paths as needed for your environment. # ๐Ÿ”ง Available Tools -The server provides the following MCP tools for interacting with Proxmox: +The server provides 55 MCP tools for interacting with Proxmox: + +**Read-Only Tools** (Basic Mode): +- `proxmox_get_nodes` - List cluster nodes +- `proxmox_get_vms` - List all VMs and containers +- `proxmox_get_vm_status` - Get VM details +- `proxmox_get_storage` - View storage pools +- `proxmox_get_cluster_status` - Cluster overview + +**Advanced Tools** (Elevated Mode): +- `proxmox_get_node_status` - Detailed node metrics +- `proxmox_execute_vm_command` - Run commands in VMs +- `proxmox_list_templates` - List LXC templates +- `proxmox_get_next_vmid` - Get next available VM/Container ID +- `proxmox_create_lxc` - Create LXC container +- `proxmox_create_vm` - Create QEMU virtual machine +- `proxmox_start_lxc` / `proxmox_start_vm` - Start container/VM +- `proxmox_stop_lxc` / `proxmox_stop_vm` - Stop container/VM +- `proxmox_reboot_lxc` / `proxmox_reboot_vm` - Reboot container/VM +- `proxmox_shutdown_lxc` / `proxmox_shutdown_vm` - Gracefully shutdown container/VM +- `proxmox_pause_vm` / `proxmox_resume_vm` - Pause/resume VM (QEMU only) +- `proxmox_clone_lxc` / `proxmox_clone_vm` - Clone container/VM +- `proxmox_resize_lxc` / `proxmox_resize_vm` - Resize container/VM resources +- `proxmox_create_snapshot_lxc` / `proxmox_create_snapshot_vm` - Create snapshot +- `proxmox_list_snapshots_lxc` / `proxmox_list_snapshots_vm` - List snapshots +- `proxmox_rollback_snapshot_lxc` / `proxmox_rollback_snapshot_vm` - Rollback to snapshot +- `proxmox_delete_snapshot_lxc` / `proxmox_delete_snapshot_vm` - Delete snapshot +- `proxmox_create_backup_lxc` / `proxmox_create_backup_vm` - Create backup +- `proxmox_list_backups` - List all backups on storage +- `proxmox_restore_backup_lxc` / `proxmox_restore_backup_vm` - Restore from backup +- `proxmox_delete_backup` - Delete backup file +- `proxmox_add_disk_vm` - Add disk to QEMU VM +- `proxmox_add_mountpoint_lxc` - Add mount point to LXC container +- `proxmox_resize_disk_vm` / `proxmox_resize_disk_lxc` - Resize disk/mount point +- `proxmox_remove_disk_vm` / `proxmox_remove_mountpoint_lxc` - Remove disk/mount point +- `proxmox_move_disk_vm` / `proxmox_move_disk_lxc` - Move disk between storage +- `proxmox_add_network_vm` / `proxmox_add_network_lxc` - Add network interface +- `proxmox_update_network_vm` / `proxmox_update_network_lxc` - Update network interface +- `proxmox_remove_network_vm` / `proxmox_remove_network_lxc` - Remove network interface +- `proxmox_delete_lxc` / `proxmox_delete_vm` - Delete container/VM + +--- ### proxmox_get_nodes Lists all nodes in the Proxmox cluster with their status and resources. @@ -303,6 +436,941 @@ Execute a shell command on a virtual machine via Proxmox API (requires elevated - For LXC: Direct execution via Proxmox API - Appropriate API token permissions +### proxmox_list_templates +List available LXC container templates on a storage. + +- Parameters: + - `node` (string, required): Node name + - `storage` (string, optional): Storage name, default: 'local' +- Example Response: + ``` + ๐Ÿ“ฆ **Available LXC Templates** + + โ€ข **local:vztmpl/debian-12-standard_12.2-1_amd64.tar.gz** + Size: 129.50 MB + + โ€ข **local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.gz** + Size: 142.30 MB + ``` +- Requirements: + - API token with `Datastore.Audit` or `Datastore.AllocateSpace` permissions + +### proxmox_create_lxc +Create a new LXC container (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where container will be created + - `vmid` (string, required): VM ID number (must be unique) + - `ostemplate` (string, required): OS template path (e.g., `local:vztmpl/debian-12-standard_12.2-1_amd64.tar.gz`) + - `hostname` (string, optional): Container hostname + - `password` (string, optional): Root password, default: 'proxmox' + - `memory` (number, optional): RAM in MB, default: 512 + - `storage` (string, optional): Storage location, default: 'local-lvm' + - `rootfs` (string, optional): Root filesystem size in GB, default: '8' +- Example Response: + ``` + โœ… **LXC Container Creation Started** + + โ€ข **VM ID**: 100 + โ€ข **Hostname**: ct100 + โ€ข **Node**: pve1 + โ€ข **Template**: local:vztmpl/debian-12-standard_12.2-1_amd64.tar.gz + โ€ข **Memory**: 512 MB + โ€ข **Storage**: local-lvm + โ€ข **Task ID**: UPID:pve1:00001234:... + + **Next steps**: + 1. Wait a moment for container to be created + 2. Start it with `proxmox_start_vm` + 3. View status with `proxmox_get_vm_status` + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Allocate` permission + - Valid LXC template available on storage + +### proxmox_create_vm +Create a new QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM will be created + - `vmid` (string, required): VM ID number (must be unique) + - `name` (string, optional): VM name, default: `vm{vmid}` + - `memory` (number, optional): RAM in MB, default: 512 + - `cores` (number, optional): Number of CPU cores, default: 1 + - `sockets` (number, optional): Number of CPU sockets, default: 1 + - `disk_size` (string, optional): Disk size (e.g., "8G", "20G"), default: '8G' + - `storage` (string, optional): Storage location for disk, default: 'local-lvm' + - `iso` (string, optional): ISO image path (e.g., "local:iso/alpine-virt-3.19.1-x86_64.iso") + - `ostype` (string, optional): OS type (l26=Linux 2.6+, win10, etc), default: 'l26' + - `net0` (string, optional): Network interface config, default: 'virtio,bridge=vmbr0' +- Example Response: + ``` + โœ… **QEMU VM Creation Started** + + โ€ข **VM ID**: 200 + โ€ข **Name**: test-vm + โ€ข **Node**: pve1 + โ€ข **Memory**: 1024 MB + โ€ข **CPU**: 1 socket(s), 2 core(s) + โ€ข **Disk**: local-lvm:20 + โ€ข **Network**: virtio,bridge=vmbr0 + โ€ข **ISO**: local:iso/debian-12.iso + โ€ข **Task ID**: UPID:pve1:00001234:... + + **Next steps**: + 1. Wait a moment for VM to be created + 2. Start it with `proxmox_start_vm` + 3. View status with `proxmox_get_vm_status` + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Allocate` permission + - Sufficient storage space available +- Example Usage: + ```bash + # Create a minimal VM + proxmox_create_vm(node="pve1", vmid="200", name="test-vm") + + # Create a VM with custom resources and ISO + proxmox_create_vm( + node="pve1", + vmid="201", + name="ubuntu-server", + memory=2048, + cores=2, + disk_size="20G", + iso="local:iso/ubuntu-22.04-server-amd64.iso" + ) + ``` + +### proxmox_start_lxc / proxmox_start_vm +Start an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number +- **Note**: Use `proxmox_start_lxc` for containers, `proxmox_start_vm` for VMs +- Example Response: + ``` + โ–ถ๏ธ **VM/Container Start Command Sent** + + โ€ข **VM ID**: 100 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:00001235:... + + **Tip**: Use `proxmox_get_vm_status` to check if it's running. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.PowerMgmt` permission + +### proxmox_stop_lxc / proxmox_stop_vm +Stop an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number +- **Note**: Use `proxmox_stop_lxc` for containers, `proxmox_stop_vm` for VMs +- Example Response: + ``` + โน๏ธ **VM/Container Stop Command Sent** + + โ€ข **VM ID**: 100 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:00001236:... + + **Tip**: Use `proxmox_get_vm_status` to confirm it's stopped. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.PowerMgmt` permission + +### proxmox_delete_lxc / proxmox_delete_vm +Delete an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number to delete +- Example Response: + ``` + ๐Ÿ—‘๏ธ **VM/Container Deletion Started** + + โ€ข **VM/Container ID**: 200 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:002A3E96:... + + **Note**: Deletion may take a moment to complete. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - VM/Container must be stopped first + - API token with `VM.Allocate` permission +- **Note**: Use `proxmox_delete_lxc` for containers, `proxmox_delete_vm` for VMs + +### proxmox_get_next_vmid +Get the next available VM/Container ID number. + +- Parameters: None +- Example Response: + ``` + **Next Available VM/Container ID**: 102 + ``` +- Use Case: Call this before creating a new VM/container to avoid ID conflicts + +### proxmox_reboot_lxc / proxmox_reboot_vm +Reboot an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number +- **Note**: Use `proxmox_reboot_lxc` for containers, `proxmox_reboot_vm` for VMs +- Example Response: + ``` + ๐Ÿ”„ **VM/Container Reboot Command Sent** + + โ€ข **VM ID**: 115 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:002A62C2:... + + **Tip**: The VM/container will restart momentarily. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.PowerMgmt` permission + - VM/Container must be running + +### proxmox_shutdown_lxc / proxmox_shutdown_vm +Gracefully shutdown an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number +- **Note**: Use `proxmox_shutdown_lxc` for containers, `proxmox_shutdown_vm` for VMs +- Difference from `stop`: Shutdown sends a clean shutdown signal (like pressing power button), while stop forcefully halts the VM +- Example Response: + ``` + โธ๏ธ **VM/Container Shutdown Command Sent** + + โ€ข **VM ID**: 115 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:002A632A:... + + **Note**: This is a graceful shutdown. Use `proxmox_stop_vm` for forceful stop. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.PowerMgmt` permission + - VM/Container must be running + +### proxmox_pause_vm / proxmox_resume_vm +Pause or resume a QEMU virtual machine (requires elevated permissions). **Note**: Only available for QEMU VMs, not LXC containers. + +- Parameters: + - `node` (string, required): Node name where VM is located + - `vmid` (string, required): VM ID number +- **Note**: Pause suspends VM execution without shutting down, resume continues execution +- Example Response (pause): + ``` + โธ๏ธ **VM Pause Command Sent** + + โ€ข **VM ID**: 101 + โ€ข **Type**: QEMU + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:... + + **Tip**: Use `proxmox_resume_vm` to continue execution. + ``` +- Example Response (resume): + ``` + โ–ถ๏ธ **VM Resume Command Sent** + + โ€ข **VM ID**: 101 + โ€ข **Type**: QEMU + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:... + + **Note**: VM execution will continue from paused state. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.PowerMgmt` permission + - VM must be running (for pause) or paused (for resume) + - **QEMU VMs only** - not supported for LXC containers + +### proxmox_clone_lxc / proxmox_clone_vm +Clone an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where source VM/container is located + - `vmid` (string, required): Source VM/Container ID number to clone from + - `newid` (string, required): New VM/Container ID for the clone + - `name` (string, optional): Name for the new clone + - `full` (boolean, optional): Create full clone instead of linked clone, default: false +- **Note**: Use `proxmox_clone_lxc` for containers, `proxmox_clone_vm` for VMs +- Example Response: + ``` + ๐Ÿ“‹ **VM/Container Clone Started** + + โ€ข **Source ID**: 100 + โ€ข **New ID**: 105 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Clone Type**: linked + โ€ข **Task ID**: UPID:pve1:... + + **Tip**: Use `proxmox_get_vm_status` to check clone status. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Allocate` and `VM.Clone` permissions + - Source VM/container should be stopped for best results + - New ID must not already exist + +### proxmox_resize_lxc / proxmox_resize_vm +Resize CPU and memory resources for an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `cores` (number, optional): Number of CPU cores to allocate + - `memory` (number, optional): Memory in MB to allocate +- **Note**: Use `proxmox_resize_lxc` for containers, `proxmox_resize_vm` for VMs +- At least one parameter (cores or memory) must be specified +- Example Response: + ``` + โš™๏ธ **VM/Container Resize Complete** + + โ€ข **VM ID**: 100 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **New Cores**: 2 + โ€ข **New Memory**: 1024 MB + + **Note**: Changes may require VM/container restart to take effect. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` or `VM.Allocate` permissions + - VM/container can be running or stopped (hot-resize supported for some settings) + +### proxmox_create_snapshot_lxc / proxmox_create_snapshot_vm +Create a snapshot of an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `snapname` (string, required): Name for the snapshot +- **Note**: Use `proxmox_create_snapshot_lxc` for containers, `proxmox_create_snapshot_vm` for VMs +- Example Response: + ``` + ๐Ÿ“ธ **Snapshot Creation Started** + + โ€ข **VM ID**: 115 + โ€ข **Snapshot Name**: test-snapshot-1 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:002A7ECF:... + + **Tip**: Use `proxmox_list_snapshots_lxc` to view all snapshots. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Snapshot` permission + - Sufficient storage space for snapshot +- Use Case: Create point-in-time backups before making changes + +### proxmox_list_snapshots_lxc / proxmox_list_snapshots_vm +List all snapshots for an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number +- **Note**: Use `proxmox_list_snapshots_lxc` for containers, `proxmox_list_snapshots_vm` for VMs +- Example Response: + ``` + ๐Ÿ“‹ **Snapshots for LXC 115** + + โ€ข **test-snapshot-1** + Created: 11/6/2025, 9:05:11 AM + + โ€ข **before-update** + Created: 11/5/2025, 2:30:45 PM + Description: Snapshot before system update + + **Total**: 2 snapshot(s) + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Audit` permission + +### proxmox_rollback_snapshot_lxc / proxmox_rollback_snapshot_vm +Rollback an LXC container or QEMU virtual machine to a previous snapshot (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `snapname` (string, required): Name of the snapshot to rollback to +- **Note**: Use `proxmox_rollback_snapshot_lxc` for containers, `proxmox_rollback_snapshot_vm` for VMs +- **Warning**: This will revert the VM/container to the snapshot state. Any changes made after the snapshot was created will be lost. +- Example Response: + ``` + โฎ๏ธ **Snapshot Rollback Started** + + โ€ข **VM ID**: 115 + โ€ข **Snapshot Name**: test-snapshot-1 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:... + + โš ๏ธ **Warning**: This will revert to the snapshot state. Data created after this snapshot will be lost. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Snapshot` permission + - VM/container must be stopped (for most cases) + - Snapshot must exist + +### proxmox_delete_snapshot_lxc / proxmox_delete_snapshot_vm +Delete a snapshot from an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `snapname` (string, required): Name of the snapshot to delete +- **Note**: Use `proxmox_delete_snapshot_lxc` for containers, `proxmox_delete_snapshot_vm` for VMs +- Example Response: + ``` + ๐Ÿ—‘๏ธ **Snapshot Deletion Started** + + โ€ข **VM ID**: 115 + โ€ข **Snapshot Name**: test-snapshot-1 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:002A7F81:... + + **Note**: Snapshot deletion may take a moment to complete. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Snapshot` permission + - Snapshot must exist +- Use Case: Free up storage space by removing old snapshots + +### proxmox_create_backup_lxc / proxmox_create_backup_vm +Create a backup of an LXC container or QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `storage` (string, optional): Storage for backup, default: 'local' + - `mode` (string, optional): Backup mode ('snapshot', 'suspend', 'stop'), default: 'snapshot' + - `compress` (string, optional): Compression ('none', 'lzo', 'gzip', 'zstd'), default: 'zstd' +- **Note**: Use `proxmox_create_backup_lxc` for containers, `proxmox_create_backup_vm` for VMs +- Backup Modes: + - **snapshot**: Quick backup using snapshots (recommended, minimal downtime) + - **suspend**: Suspends VM during backup (ensures consistency) + - **stop**: Stops VM during backup (maximum consistency, maximum downtime) +- Example Response: + ``` + ๐Ÿ’พ **Backup Creation Started** + + โ€ข **VM ID**: 115 + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Storage**: local + โ€ข **Mode**: snapshot + โ€ข **Compression**: zstd + โ€ข **Task ID**: UPID:pve1:002A9368:... + + **Tip**: Backup runs in the background. Use `proxmox_list_backups` to view all backups. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Backup` permission + - Sufficient storage space for backup + - For snapshot mode: Storage must support snapshots +- Use Case: Create full backups for disaster recovery + +### proxmox_list_backups +List all backup files on a storage (works for both LXC and VM backups). + +- Parameters: + - `node` (string, required): Node name + - `storage` (string, optional): Storage name, default: 'local' +- **Note**: This is a unified tool that lists all backup types +- Example Response: + ``` + ๐Ÿ“ฆ **Backups on local** + + โ€ข **vzdump-lxc-115-2025_11_06-09_12_00.tar.zst** + VM ID: 115 (LXC) + Size: 409.42 MB + Created: 11/6/2025, 9:12:00 AM + Volume: local:backup/vzdump-lxc-115-2025_11_06-09_12_00.tar.zst + + โ€ข **vzdump-qemu-101-2025_11_05-14_30_00.vma.zst** + VM ID: 101 (QEMU) + Size: 2.15 GB + Created: 11/5/2025, 2:30:00 PM + Volume: local:backup/vzdump-qemu-101-2025_11_05-14_30_00.vma.zst + + **Total**: 2 backup(s) + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `Datastore.Audit` permission + +### proxmox_restore_backup_lxc / proxmox_restore_backup_vm +Restore an LXC container or QEMU virtual machine from a backup file (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container will be restored + - `vmid` (string, required): New VM/Container ID for the restored instance + - `archive` (string, required): Backup file path (e.g., 'local:backup/vzdump-lxc-115-2025_11_06-09_12_00.tar.zst') + - `storage` (string, optional): Storage location for restored VM/container +- **Note**: Use `proxmox_restore_backup_lxc` for LXC backups, `proxmox_restore_backup_vm` for VM backups +- **Important**: This creates a NEW VM/container with the specified vmid, it does not overwrite the original +- Example Response: + ``` + โ™ป๏ธ **Backup Restore Started** + + โ€ข **New VM ID**: 116 + โ€ข **Archive**: local:backup/vzdump-lxc-115-2025_11_06-09_12_00.tar.zst + โ€ข **Type**: LXC + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:... + + **Tip**: Use `proxmox_get_vm_status` to check restore progress. + **Note**: The restored VM/container will have the new ID specified, not the original ID from the backup. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Allocate` permission + - Valid backup archive file exists + - New vmid must not already be in use + - Sufficient storage space for restore +- Use Case: Recover from backup or clone VM/container from backup + +### proxmox_delete_backup +Delete a backup file from storage (works for both LXC and VM backups). + +- Parameters: + - `node` (string, required): Node name + - `storage` (string, required): Storage name + - `volume` (string, required): Full backup volume path (e.g., 'local:backup/vzdump-lxc-115-2025_11_06-09_12_00.tar.zst') +- **Note**: This is a unified tool that deletes any backup type +- Example Response: + ``` + ๐Ÿ—‘๏ธ **Backup Deletion Started** + + โ€ข **Node**: pve1 + โ€ข **Storage**: local + โ€ข **Volume**: local:backup/vzdump-lxc-115-2025_11_06-09_12_00.tar.zst + โ€ข **Task ID**: UPID:pve1:002A94BA:... + + **Note**: Backup file will be permanently deleted from storage. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `Datastore.Allocate` or `Datastore.AllocateSpace` permission + - Backup file must exist +- **Warning**: This permanently deletes the backup file and cannot be undone +- Use Case: Free up storage space by removing old backups + +### proxmox_add_disk_vm +Add a new disk to a QEMU virtual machine (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM is located + - `vmid` (string, required): VM ID number + - `disk` (string, required): Disk identifier (e.g., 'scsi1', 'virtio1', 'sata1', 'ide1') + - `storage` (string, required): Storage name (e.g., 'local-lvm') + - `size` (string, required): Disk size in GB (e.g., '10') +- **Note**: QEMU VMs only, not for LXC containers +- Disk naming conventions: + - **SCSI**: scsi0-15 (most common, supports TRIM/discard) + - **VirtIO**: virtio0-15 (best performance) + - **SATA**: sata0-5 + - **IDE**: ide0-3 (legacy) +- Example Response: + ``` + ๐Ÿ’ฟ **Disk Added to VM** + + โ€ข **VM ID**: 101 + โ€ข **Disk**: scsi1 + โ€ข **Storage**: local-lvm + โ€ข **Size**: 10 GB + โ€ข **Node**: pve1 + + **Tip**: VM may need to be stopped to add disks. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` permission + - VM should be stopped + - Disk identifier must not already exist + - Sufficient storage space + +### proxmox_add_mountpoint_lxc +Add a mount point to an LXC container (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where container is located + - `vmid` (string, required): Container ID number + - `mp` (string, required): Mount point identifier (e.g., 'mp0', 'mp1', 'mp2') + - `storage` (string, required): Storage name (e.g., 'local-lvm') + - `size` (string, required): Mount point size in GB (e.g., '10') +- **Note**: LXC containers only, not for QEMU VMs +- Mount point naming: mp0-255 +- Example Response: + ``` + ๐Ÿ’ฟ **Mount Point Added to Container** + + โ€ข **Container ID**: 115 + โ€ข **Mount Point**: mp0 + โ€ข **Storage**: local-lvm + โ€ข **Size**: 10 GB + โ€ข **Node**: pve1 + + **Tip**: Container may need to be stopped to add mount points. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` permission + - Container should be stopped + - Mount point identifier must not already exist + +### proxmox_resize_disk_vm / proxmox_resize_disk_lxc +Resize a disk on a QEMU VM or LXC container (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `disk` (string, required): Disk identifier (e.g., 'scsi0', 'virtio0', 'rootfs', 'mp0') + - `size` (string, required): New size ('+10G' for relative, '50G' for absolute) +- **Note**: Use `proxmox_resize_disk_vm` for VMs, `proxmox_resize_disk_lxc` for containers +- Size format: + - Relative: '+10G' (adds 10GB to current size) + - Absolute: '50G' (sets size to exactly 50GB) +- Example Response: + ``` + ๐Ÿ“ **Disk Resized** + + โ€ข **VM ID**: 101 + โ€ข **Disk**: scsi0 + โ€ข **New Size**: +10G + โ€ข **Node**: pve1 + + **Note**: Disk has been expanded. You may need to resize the filesystem inside the VM. + **Tip**: Use tools like 'growpart' and 'resize2fs' inside the VM to expand the filesystem. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` permission + - Can only increase size, not decrease + - Some configurations support online resize (VirtIO/SCSI) +- **Important**: Resizing only expands the disk, you must resize the filesystem inside the VM/container + +### proxmox_remove_disk_vm / proxmox_remove_mountpoint_lxc +Remove a disk from a QEMU VM or mount point from an LXC container (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `disk` or `mp` (string, required): Disk/mount point identifier to remove +- **Note**: Use `proxmox_remove_disk_vm` for VMs, `proxmox_remove_mountpoint_lxc` for containers +- **Warning**: This removes the disk configuration. Data may be deleted depending on storage type. +- Example Response: + ``` + โž– **Disk Removed from VM** + + โ€ข **VM ID**: 101 + โ€ข **Disk**: scsi1 + โ€ข **Node**: pve1 + + โš ๏ธ **Warning**: Disk configuration removed. Data deletion depends on storage settings. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` permission + - VM/container should be stopped + - Cannot remove primary disk (scsi0/rootfs) +- Use Case: Remove unused disks to free configuration slots + +### proxmox_move_disk_vm / proxmox_move_disk_lxc +Move a disk/volume to different storage (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `disk` (string, required): Disk identifier (e.g., 'scsi0', 'rootfs', 'mp0') + - `storage` (string, required): Target storage name + - `delete` (boolean, optional): Delete source after move, default: true +- **Note**: Use `proxmox_move_disk_vm` for VMs, `proxmox_move_disk_lxc` for containers +- Example Response: + ``` + ๐Ÿ“ฆ **Disk Move Started** + + โ€ข **VM ID**: 101 + โ€ข **Disk**: scsi0 + โ€ข **Target Storage**: local-lvm + โ€ข **Delete Source**: true + โ€ข **Node**: pve1 + โ€ข **Task ID**: UPID:pve1:... + + **Tip**: This operation may take time depending on disk size. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` and `Datastore.Allocate` permissions + - VM/container should be stopped + - Sufficient space on target storage + - Source and target must be different storage +- Use Case: Migrate disks between storage types (e.g., HDD to SSD) or rebalance storage + +### proxmox_add_network_vm / proxmox_add_network_lxc +Add a network interface to a QEMU VM or LXC container (requires elevated permissions). + +- Parameters (VM): + - `node` (string, required): Node name where VM is located + - `vmid` (string, required): VM ID number + - `net` (string, required): Network interface identifier (e.g., 'net0', 'net1') + - `bridge` (string, required): Bridge name (e.g., 'vmbr0', 'vmbr1') + - `model` (string, optional): Network model ('virtio', 'e1000', 'rtl8139', 'vmxnet3'), default: 'virtio' + - `macaddr` (string, optional): MAC address (auto-generated if not specified) + - `vlan` (number, optional): VLAN tag (1-4094) + - `firewall` (boolean, optional): Enable firewall for this interface +- Parameters (LXC): + - `node` (string, required): Node name where container is located + - `vmid` (string, required): Container ID number + - `net` (string, required): Network interface identifier (e.g., 'net0', 'net1') + - `bridge` (string, required): Bridge name (e.g., 'vmbr0') + - `ip` (string, optional): IP address ('dhcp', '192.168.1.100/24', or 'auto') + - `gw` (string, optional): Gateway address (e.g., '192.168.1.1') + - `firewall` (boolean, optional): Enable firewall for this interface +- **Note**: Use `proxmox_add_network_vm` for VMs, `proxmox_add_network_lxc` for containers +- Network Models (VM): + - **virtio**: Best performance (recommended) + - **e1000**: Intel E1000 (good compatibility) + - **rtl8139**: Realtek (legacy) + - **vmxnet3**: VMware paravirtualized +- Example Response (VM): + ``` + ๐ŸŒ **Network Interface Added to VM** + + โ€ข **VM ID**: 101 + โ€ข **Interface**: net1 + โ€ข **Bridge**: vmbr0 + โ€ข **Model**: virtio + โ€ข **Node**: pve1 + + **Tip**: VM may need to be restarted for changes to take effect. + ``` +- Example Response (LXC): + ``` + ๐ŸŒ **Network Interface Added to Container** + + โ€ข **Container ID**: 115 + โ€ข **Interface**: net1 (eth1) + โ€ข **Bridge**: vmbr0 + โ€ข **IP**: dhcp + โ€ข **Node**: pve1 + + **Tip**: Container may need to be restarted for changes to take effect. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` permission + - VM/container should be stopped for best results + - Interface identifier must not already exist + - Bridge must exist on the node + +### proxmox_update_network_vm / proxmox_update_network_lxc +Update/modify an existing network interface on a QEMU VM or LXC container (requires elevated permissions). + +- Parameters (VM): + - `node` (string, required): Node name where VM is located + - `vmid` (string, required): VM ID number + - `net` (string, required): Network interface to update (e.g., 'net0') + - `bridge` (string, optional): New bridge name + - `model` (string, optional): New network model + - `macaddr` (string, optional): New MAC address + - `vlan` (number, optional): New VLAN tag + - `firewall` (boolean, optional): Enable/disable firewall +- Parameters (LXC): + - `node` (string, required): Node name where container is located + - `vmid` (string, required): Container ID number + - `net` (string, required): Network interface to update (e.g., 'net0') + - `bridge` (string, optional): New bridge name + - `ip` (string, optional): New IP address + - `gw` (string, optional): New gateway + - `firewall` (boolean, optional): Enable/disable firewall +- **Note**: Only provided parameters will be updated; others remain unchanged +- Example Response: + ``` + ๐Ÿ”ง **Network Interface Updated** + + โ€ข **VM ID**: 101 + โ€ข **Interface**: net0 + โ€ข **Updated**: bridge, firewall + โ€ข **Node**: pve1 + + **Tip**: VM may need to be restarted for changes to take effect. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` permission + - Interface must already exist + - VM/container should be stopped for best results + +### proxmox_remove_network_vm / proxmox_remove_network_lxc +Remove a network interface from a QEMU VM or LXC container (requires elevated permissions). + +- Parameters: + - `node` (string, required): Node name where VM/container is located + - `vmid` (string, required): VM/Container ID number + - `net` (string, required): Network interface to remove (e.g., 'net1') +- **Note**: Use `proxmox_remove_network_vm` for VMs, `proxmox_remove_network_lxc` for containers +- **Warning**: Cannot remove the only network interface (net0 if it's the only one) +- Example Response: + ``` + โž– **Network Interface Removed** + + โ€ข **VM ID**: 101 + โ€ข **Interface**: net1 + โ€ข **Node**: pve1 + + **Tip**: VM may need to be restarted for changes to take effect. + ``` +- Requirements: + - `PROXMOX_ALLOW_ELEVATED=true` + - API token with `VM.Config` permission + - Interface must exist + - VM/container should be stopped + - Should not be the only network interface +- Use Case: Remove unused network interfaces or reconfigure networking + +## โœ… Testing Status + +The following operations have been tested on a live Proxmox environment: + +### Tested Operations โœ“ +- [x] List nodes (proxmox_get_nodes) +- [x] List VMs and containers (proxmox_get_vms) +- [x] Get VM status (proxmox_get_vm_status) +- [x] Get storage (proxmox_get_storage) +- [x] Create LXC container (proxmox_create_lxc) +- [x] Start LXC container (proxmox_start_lxc) +- [x] Stop LXC container (proxmox_stop_lxc) +- [x] Reboot LXC container (proxmox_reboot_lxc) +- [x] Shutdown LXC container (proxmox_shutdown_lxc) +- [x] Delete LXC container (proxmox_delete_lxc) +- [x] Get next available VMID (proxmox_get_next_vmid) +- [x] Create snapshot (proxmox_create_snapshot_lxc) +- [x] List snapshots (proxmox_list_snapshots_lxc) +- [x] Delete snapshot (proxmox_delete_snapshot_lxc) +- [x] Create backup (proxmox_create_backup_lxc) +- [x] List backups (proxmox_list_backups) +- [x] Delete backup (proxmox_delete_backup) + +### Untested Operations (Implementation Complete, Needs Live Testing) +- [ ] VM operations (start_vm, stop_vm, reboot_vm, etc.) - No QEMU VMs available during testing +- [ ] Snapshot rollback (proxmox_rollback_snapshot_lxc/vm) +- [ ] Backup restore (proxmox_restore_backup_lxc/vm) +- [ ] Clone operations (proxmox_clone_lxc/vm) +- [ ] Resize operations (proxmox_resize_lxc/vm) +- [ ] Pause/Resume VM (proxmox_pause_vm/resume_vm) +- [ ] Disk operations (add_disk_vm, resize_disk_vm/lxc, remove_disk_vm, move_disk_vm/lxc) +- [ ] Mount point operations (add_mountpoint_lxc, remove_mountpoint_lxc) +- [ ] Network operations (add_network_vm/lxc, update_network_vm/lxc, remove_network_vm/lxc) +- [ ] VM command execution (proxmox_execute_vm_command) + +**Note**: All untested operations follow the same implementation patterns as tested operations and should work correctly. They were not tested due to environment limitations (no QEMU VMs, avoiding destructive operations on production containers). + +## ๐Ÿงช Testing + +### Test Suite + +The project includes comprehensive test scripts to validate functionality: + +#### test-basic-tools.js +Tests all basic (non-elevated) read-only operations: +- Validates connection to Proxmox +- Tests node listing +- Tests VM/container listing +- Tests storage and cluster status +- Tests template listing +- Tests VM status retrieval + +**Usage:** +```bash +node test-basic-tools.js +``` + +**Expected Result:** 7/7 tests pass (100%) + +**Requirements:** +- Valid `.env` configuration +- Working Proxmox connection +- Does NOT require `PROXMOX_ALLOW_ELEVATED=true` + +#### test-workflows.js +Comprehensive workflow tests for complete lifecycle operations: + +**Available Workflows:** +- **LXC Container**: Create โ†’ Start โ†’ Snapshot โ†’ Stop โ†’ Delete +- **VM Lifecycle**: Start, stop, reboot operations +- **Network Management**: Add, update, remove interfaces +- **Disk Management**: Add, resize, remove disks +- **Snapshot Workflow**: Create, list, delete snapshots +- **Backup Workflow**: Create and list backups + +**Usage:** +```bash +# Run all workflows +node test-workflows.js + +# Run specific workflow +node test-workflows.js --workflow=lxc +node test-workflows.js --workflow=disk +node test-workflows.js --workflow=snapshot + +# Dry-run mode (show what would be done) +node test-workflows.js --dry-run + +# Interactive mode (confirm before destructive operations) +node test-workflows.js --interactive + +# Skip cleanup (keep test resources for inspection) +node test-workflows.js --no-cleanup +``` + +**Expected Result:** 19-22/22 tests pass (86-100%), depending on environment + +**Requirements:** +- Valid `.env` configuration +- `PROXMOX_ALLOW_ELEVATED=true` required +- API token with full permissions (VM.Allocate, VM.Config.*, VM.PowerMgmt, etc.) +- Available LXC templates for container workflow + +**Note:** Some tests may fail due to environment limitations (no QEMU VMs, no stopped VMs) or Proxmox API timing issues (snapshot listing delay). These are expected and do not indicate bugs. + +### Test Documentation + +For detailed information about the test suite, including: +- Individual workflow descriptions +- Troubleshooting guides +- CI/CD integration examples +- Safety features and cleanup behavior + +See [TEST-WORKFLOWS.md](./TEST-WORKFLOWS.md) + ## ๐Ÿ‘จโ€๐Ÿ’ป Development ### Development Commands @@ -328,23 +1396,96 @@ echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "pr ### Development Notes -- The server loads environment variables from `.env` files automatically +- The server loads environment variables from `../../.env` relative to `index.js` +- Place `.env` file in the parent directory of your `mcp-proxmox` installation - Use `npm run dev` for development with auto-reload on file changes - All API calls require a proper `.env` configuration -- Check the server logs for connection and permission issues +- Check the server logs (stderr) for connection and permission issues ## ๐Ÿ“ Project Structure ``` -mcp-proxmox/ -โ”œโ”€โ”€ index.js # Main MCP server implementation -โ”œโ”€โ”€ package.json # Node.js dependencies and scripts -โ”œโ”€โ”€ package-lock.json # Dependency lock file -โ”œโ”€โ”€ .env # Environment configuration (not in git) -โ”œโ”€โ”€ node_modules/ # Dependencies (not in git) -โ””โ”€โ”€ README.md # This documentation +/your-installation-path/ +โ”œโ”€โ”€ .env # Environment configuration (not in git, MUST be here) +โ””โ”€โ”€ mcp-proxmox/ + โ”œโ”€โ”€ index.js # Main MCP server implementation + โ”œโ”€โ”€ package.json # Node.js dependencies and scripts + โ”œโ”€โ”€ package-lock.json # Dependency lock file + โ”œโ”€โ”€ node_modules/ # Dependencies (not in git) + โ””โ”€โ”€ README.md # This documentation ``` +**Note**: The `.env` file MUST be in the parent directory of `mcp-proxmox`, not inside it. + +## ๐Ÿ” Troubleshooting + +### "Could not load .env file" Error + +**Problem**: Server shows warning: `Warning: Could not load .env file: ENOENT: no such file or directory` + +**Solution**: +1. The `.env` file must be in the parent directory of your `mcp-proxmox` installation +2. If you installed to `/path/to/mcp-proxmox`, create `.env` at `/path/to/.env` +3. Verify file location: + ```bash + # From mcp-proxmox directory + ls -la ../.env + ``` + +### Connection Refused / Cannot Connect to Proxmox + +**Problem**: API calls fail with connection errors + +**Solutions**: +- Verify `PROXMOX_HOST` is correct (IP or hostname) +- Ensure `PROXMOX_PORT` matches your Proxmox server (default: 8006) +- Check firewall allows access to Proxmox API port +- Verify Proxmox server is running and accessible + +### Permission Denied / 401 Unauthorized + +**Problem**: API calls fail with authentication errors + +**Solutions**: +- Verify `PROXMOX_TOKEN_VALUE` is correct (copy the full secret) +- Check `PROXMOX_USER` format is correct (e.g., `root@pam`) +- Ensure `PROXMOX_TOKEN_NAME` matches the token ID in Proxmox +- Verify the API token exists in Proxmox: Datacenter โ†’ Permissions โ†’ API Tokens + +### "Requires Elevated Permissions" Messages + +**Problem**: Some tools return permission warning messages + +**Solution**: +- Set `PROXMOX_ALLOW_ELEVATED=true` in your `.env` file +- Ensure API token has required Proxmox permissions: + - `Sys.Audit` for node status + - `VM.Monitor` and `VM.Console` for VM command execution +- In Proxmox: Datacenter โ†’ Permissions โ†’ Add role permissions to your user/token + +### QEMU Guest Agent Commands Fail + +**Problem**: `proxmox_execute_vm_command` fails on QEMU VMs + +**Solutions**: +- Install QEMU Guest Agent in the VM: + - Debian/Ubuntu: `apt install qemu-guest-agent` + - RHEL/CentOS: `yum install qemu-guest-agent` + - Windows: Install from VirtIO ISO +- Enable guest agent in VM hardware settings +- Restart the VM after installation +- Note: LXC containers don't need guest agent + +### MCP Client Cannot Find Server + +**Problem**: MCP client shows server connection errors + +**Solutions**: +- Verify `cwd` path in MCP configuration is correct +- Ensure Node.js is installed and in PATH +- Check `.env` file is in parent directory of cwd path +- Test server manually: `node index.js` from the `mcp-proxmox` directory + ## ๐Ÿ“„ License MIT License diff --git a/TEST-WORKFLOWS.md b/TEST-WORKFLOWS.md new file mode 100644 index 0000000..d13d630 --- /dev/null +++ b/TEST-WORKFLOWS.md @@ -0,0 +1,283 @@ +# Proxmox MCP Workflow Test Scripts + +This directory contains comprehensive test scripts for validating Proxmox MCP server functionality. + +## Available Test Scripts + +### 1. test-basic-tools.js +Tests all basic (non-elevated) read-only tools: +- `proxmox_get_nodes` - List cluster nodes +- `proxmox_get_cluster_status` - Cluster status +- `proxmox_get_vms` - List VMs/containers +- `proxmox_get_vm_status` - VM details +- `proxmox_get_storage` - Storage information +- `proxmox_get_next_vmid` - Get next available VMID +- `proxmox_list_templates` - List LXC templates + +**Usage:** +```bash +node test-basic-tools.js +``` + +**Requirements:** +- Valid `.env` configuration +- Working Proxmox connection +- Does NOT require `PROXMOX_ALLOW_ELEVATED=true` + +--- + +### 2. test-workflows.js +Comprehensive workflow tests for complete lifecycle operations. + +**Usage:** +```bash +# Run all workflows +node test-workflows.js + +# Run specific workflow +node test-workflows.js --workflow=lxc +node test-workflows.js --workflow=vm +node test-workflows.js --workflow=network +node test-workflows.js --workflow=disk +node test-workflows.js --workflow=snapshot +node test-workflows.js --workflow=backup + +# Dry-run mode (show what would be done) +node test-workflows.js --dry-run + +# Interactive mode (confirm before destructive operations) +node test-workflows.js --interactive + +# Skip cleanup (keep test resources) +node test-workflows.js --no-cleanup + +# Combine options +node test-workflows.js --workflow=lxc --interactive --dry-run +``` + +**Requirements:** +- Valid `.env` configuration +- `PROXMOX_ALLOW_ELEVATED=true` in `.env` +- API token with appropriate permissions: + - `VM.Allocate` - Create VMs/containers + - `VM.Config.Disk` - Disk management + - `VM.Config.Network` - Network management + - `VM.PowerMgmt` - Start/stop/reboot + - `VM.Snapshot` - Snapshot operations + - `VM.Backup` - Backup operations + - `Datastore.Allocate` - Storage allocation + +## Workflows Tested + +### LXC Container Complete Workflow +1. Get next available VMID +2. List available templates +3. Create new LXC container +4. Start container +5. Check status +6. Create snapshot +7. List snapshots +8. Stop container +9. Delete snapshot +10. Delete container (cleanup) + +**What it validates:** +- Container creation with templates +- Lifecycle management (start/stop) +- Snapshot operations +- Status monitoring +- Resource cleanup + +--- + +### VM Lifecycle Operations +1. Find existing VM +2. Detect VM type (QEMU/LXC) +3. Get detailed status +4. Start (if stopped) or reboot (if running) +5. Verify operations + +**What it validates:** +- VM discovery +- Power management operations +- Status checking +- Type detection + +--- + +### Network Management Workflow +1. Find stopped VM +2. Add network interface (net1, bridge vmbr0) +3. Update interface (add rate limit) +4. Remove interface + +**What it validates:** +- Network interface addition +- Configuration updates +- Interface removal +- Bridge configuration + +**Note:** VM must be stopped for network changes + +--- + +### Disk Management Workflow +1. Find QEMU VM +2. Add new disk (10G on local-lvm) +3. Resize disk (+2G) +4. Remove disk + +**What it validates:** +- Disk allocation +- Disk resizing +- Disk removal +- Storage integration + +**Note:** Primarily tests QEMU VMs for reliability + +--- + +### Snapshot Workflow +1. Find VM +2. Create snapshot +3. List snapshots +4. Delete snapshot + +**What it validates:** +- Snapshot creation +- Snapshot listing +- Snapshot deletion +- Snapshot naming + +--- + +### Backup Workflow +1. Find VM +2. Create backup (to local storage) +3. Wait for background job +4. List backups + +**What it validates:** +- Backup job creation +- Background job handling +- Backup listing +- Storage integration + +**Note:** Backup restoration is not tested automatically (destructive operation) + +## Exit Codes + +- `0` - All tests passed +- `1` - One or more tests failed or error occurred + +## Test Output + +The scripts provide detailed, color-coded output: +- ๐ŸŸข **Green** - Success +- ๐Ÿ”ด **Red** - Failure +- ๐ŸŸก **Yellow** - Warning +- ๐Ÿ”ต **Blue** - Information +- ๐ŸŸฃ **Magenta** - Headers + +Example output: +``` +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + LXC Container Complete Workflow +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +[12:34:56] โ–ถ Step 1: Get next available VMID + โœ“ Got VMID: 105 + +[12:34:57] โ–ถ Step 2: List available templates + Node: pve1 + โœ“ Templates listed successfully + โœ“ Found template: local:vztmpl/debian-12-standard_12.2-1_amd64.tar.gz + +[12:34:58] โ–ถ Step 3: Create LXC container + VMID: 105, Template: local:vztmpl/debian-12-standard_12.2-1_amd64.tar.gz + โœ“ Create Container: Container 105 created +``` + +## Cleanup + +By default, `test-workflows.js` automatically cleans up test resources (deletes created VMs/containers) at the end. + +**To keep test resources:** +```bash +node test-workflows.js --no-cleanup +``` + +**Manual cleanup:** +If a test fails or is interrupted, you may need to manually delete test resources: +- Container hostname: `test-mcp-{vmid}` +- Snapshots: `test-snapshot` or `test-snap-{timestamp}` + +## Troubleshooting + +### "Elevated permissions required" +**Solution:** Set `PROXMOX_ALLOW_ELEVATED=true` in your `.env` file + +### "No templates found" +**Solution:** Download LXC templates via Proxmox UI: Storage โ†’ local โ†’ CT Templates โ†’ Download + +### "No stopped VMs found" (network workflow) +**Solution:** Stop a VM first, or skip network workflow: `--workflow=lxc` + +### "Failed to connect to Proxmox" +**Solution:** +- Check `.env` configuration +- Verify Proxmox API is accessible +- Check API token permissions +- Ensure firewall allows connection + +### Tests timing out +**Solution:** +- Increase timeout values in script +- Check Proxmox performance +- Reduce concurrent operations + +## Safety Features + +1. **Dry-run mode** - Preview operations without executing +2. **Interactive mode** - Confirm before destructive operations +3. **Resource tracking** - Automatic cleanup of created resources +4. **Error handling** - Graceful failure with detailed messages +5. **Status validation** - Verify operations completed successfully + +## Best Practices + +1. **Start with dry-run:** `node test-workflows.js --dry-run` +2. **Test basic tools first:** `node test-basic-tools.js` +3. **Run one workflow at a time initially:** `--workflow=lxc` +4. **Use interactive mode for production:** `--interactive` +5. **Review cleanup:** Don't use `--no-cleanup` unless debugging +6. **Check permissions:** Ensure API token has all required permissions +7. **Monitor Proxmox:** Watch for resource usage during tests + +## Integration with CI/CD + +These scripts can be integrated into CI/CD pipelines: + +```bash +# Basic connectivity test +node test-basic-tools.js || exit 1 + +# Full workflow validation (non-interactive) +node test-workflows.js || exit 1 + +# Or specific workflow +node test-workflows.js --workflow=snapshot || exit 1 +``` + +## Contributing + +When adding new workflows: +1. Follow existing pattern (logStep, recordResult) +2. Add cleanup tracking for new resources +3. Include error handling +4. Add to workflow list in main() +5. Document in this README + +## License + +Same as parent project (MIT) diff --git a/index.js b/index.js index 95ad2fe..e2b5eb6 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; import https from 'https'; +import crypto from 'crypto'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -12,15 +13,21 @@ import { fileURLToPath } from 'url'; // Load environment variables from .env file const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const envPath = join(__dirname, '../../.env'); +const envPath = join(__dirname, '../.env'); try { const envFile = readFileSync(envPath, 'utf8'); - const envVars = envFile.split('\n').filter(line => line.includes('=')); + const envVars = envFile.split('\n').filter(line => line.includes('=') && !line.trim().startsWith('#')); for (const line of envVars) { const [key, ...values] = line.split('='); - if (key && values.length > 0) { - process.env[key.trim()] = values.join('=').trim(); + // Validate key is a valid environment variable name (alphanumeric and underscore only) + if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) { + // Remove surrounding quotes if present and trim + let value = values.join('=').trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + process.env[key.trim()] = value; } } } catch (error) { @@ -56,10 +63,67 @@ class ProxmoxServer { this.setupToolHandlers(); } + // Input validation methods for security + validateNodeName(node) { + if (!node || typeof node !== 'string') { + throw new Error('Node name is required and must be a string'); + } + // Only allow alphanumeric, hyphens, and underscores + if (!/^[a-zA-Z0-9\-_]+$/.test(node)) { + throw new Error('Invalid node name format. Only alphanumeric, hyphens, and underscores allowed'); + } + if (node.length > 64) { + throw new Error('Node name too long (max 64 characters)'); + } + return node; + } + + validateVMID(vmid) { + if (!vmid) { + throw new Error('VM ID is required'); + } + const id = parseInt(vmid, 10); + if (isNaN(id) || id < 100 || id > 999999999) { + throw new Error('Invalid VM ID. Must be a number between 100 and 999999999'); + } + return id.toString(); + } + + validateCommand(command) { + if (!command || typeof command !== 'string') { + throw new Error('Command is required and must be a string'); + } + + // Check for dangerous characters that could be used for command injection + const dangerousChars = /[;&|`$(){}[\]<>\\]/g; + if (dangerousChars.test(command)) { + throw new Error('Command contains potentially dangerous characters: ; & | ` $ ( ) { } [ ] < > \\'); + } + + // Limit command length + if (command.length > 1000) { + throw new Error('Command exceeds maximum allowed length (1000 characters)'); + } + + return command; + } + + generateSecurePassword() { + // Generate a secure random password using Node.js crypto + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; + let password = ''; + const randomBytes = crypto.randomBytes(16); + + for (let i = 0; i < 16; i++) { + password += chars[randomBytes[i] % chars.length]; + } + return password; + } + async proxmoxRequest(endpoint, method = 'GET', body = null) { const baseUrl = `https://${this.proxmoxHost}:${this.proxmoxPort}/api2/json`; const url = `${baseUrl}${endpoint}`; - + const headers = { 'Authorization': `PVEAPIToken=${this.proxmoxUser}!${this.proxmoxTokenName}=${this.proxmoxTokenValue}`, 'Content-Type': 'application/json' @@ -77,17 +141,17 @@ class ProxmoxServer { try { const response = await fetch(url, options); - + if (!response.ok) { const errorText = await response.text(); throw new Error(`Proxmox API error: ${response.status} - ${errorText}`); } - + const textResponse = await response.text(); if (!textResponse.trim()) { throw new Error('Empty response from Proxmox API'); } - + const data = JSON.parse(textResponse); return data.data; } catch (error) { @@ -175,6 +239,660 @@ class ProxmoxServer { type: 'object', properties: {} } + }, + { + name: 'proxmox_list_templates', + description: 'List available LXC container templates on a storage', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name' }, + storage: { type: 'string', description: 'Storage name (e.g., local)', default: 'local' } + }, + required: ['node'] + } + }, + { + name: 'proxmox_create_lxc', + description: 'Create a new LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container will be created' }, + vmid: { type: 'string', description: 'Container ID number (must be unique, or use proxmox_get_next_vmid)' }, + ostemplate: { type: 'string', description: 'OS template (e.g., local:vztmpl/debian-12-standard_12.2-1_amd64.tar.gz)' }, + hostname: { type: 'string', description: 'Container hostname' }, + password: { type: 'string', description: 'Root password' }, + memory: { type: 'number', description: 'RAM in MB', default: 512 }, + storage: { type: 'string', description: 'Storage location', default: 'local-lvm' }, + rootfs: { type: 'string', description: 'Root filesystem size in GB', default: '8' } + }, + required: ['node', 'vmid', 'ostemplate'] + } + }, + { + name: 'proxmox_create_vm', + description: 'Create a new QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM will be created' }, + vmid: { type: 'string', description: 'VM ID number (must be unique, or use proxmox_get_next_vmid)' }, + name: { type: 'string', description: 'VM name' }, + memory: { type: 'number', description: 'RAM in MB', default: 512 }, + cores: { type: 'number', description: 'Number of CPU cores', default: 1 }, + sockets: { type: 'number', description: 'Number of CPU sockets', default: 1 }, + disk_size: { type: 'string', description: 'Disk size (e.g., "8G", "10G")', default: '8G' }, + storage: { type: 'string', description: 'Storage location for disk', default: 'local-lvm' }, + iso: { type: 'string', description: 'ISO image (e.g., "local:iso/alpine-virt-3.19.1-x86_64.iso"), optional' }, + ostype: { type: 'string', description: 'OS type (l26=Linux 2.6+, win10, etc)', default: 'l26' }, + net0: { type: 'string', description: 'Network interface config', default: 'virtio,bridge=vmbr0' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_get_next_vmid', + description: 'Get the next available VM/Container ID number', + inputSchema: { + type: 'object', + properties: {} + } + }, + { + name: 'proxmox_start_lxc', + description: 'Start an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_start_vm', + description: 'Start a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_stop_lxc', + description: 'Stop an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_stop_vm', + description: 'Stop a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_delete_lxc', + description: 'Delete an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number to delete' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_delete_vm', + description: 'Delete a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number to delete' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_reboot_lxc', + description: 'Reboot an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_reboot_vm', + description: 'Reboot a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_shutdown_lxc', + description: 'Gracefully shutdown an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_shutdown_vm', + description: 'Gracefully shutdown a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_pause_vm', + description: 'Pause a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_resume_vm', + description: 'Resume a paused QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_clone_lxc', + description: 'Clone an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID to clone from' }, + newid: { type: 'string', description: 'New container ID' }, + hostname: { type: 'string', description: 'Hostname for cloned container (optional)' } + }, + required: ['node', 'vmid', 'newid'] + } + }, + { + name: 'proxmox_clone_vm', + description: 'Clone a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID to clone from' }, + newid: { type: 'string', description: 'New VM ID' }, + name: { type: 'string', description: 'Name for cloned VM (optional)' } + }, + required: ['node', 'vmid', 'newid'] + } + }, + { + name: 'proxmox_resize_lxc', + description: 'Resize an LXC container CPU/memory (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + memory: { type: 'number', description: 'Memory in MB (optional)' }, + cores: { type: 'number', description: 'Number of CPU cores (optional)' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_resize_vm', + description: 'Resize a QEMU VM CPU/memory (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + memory: { type: 'number', description: 'Memory in MB (optional)' }, + cores: { type: 'number', description: 'Number of CPU cores (optional)' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_create_snapshot_lxc', + description: 'Create a snapshot of an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + snapname: { type: 'string', description: 'Snapshot name' } + }, + required: ['node', 'vmid', 'snapname'] + } + }, + { + name: 'proxmox_create_snapshot_vm', + description: 'Create a snapshot of a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + snapname: { type: 'string', description: 'Snapshot name' } + }, + required: ['node', 'vmid', 'snapname'] + } + }, + { + name: 'proxmox_list_snapshots_lxc', + description: 'List all snapshots of an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_list_snapshots_vm', + description: 'List all snapshots of a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_rollback_snapshot_lxc', + description: 'Rollback an LXC container to a snapshot (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + snapname: { type: 'string', description: 'Snapshot name to rollback to' } + }, + required: ['node', 'vmid', 'snapname'] + } + }, + { + name: 'proxmox_rollback_snapshot_vm', + description: 'Rollback a QEMU virtual machine to a snapshot (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + snapname: { type: 'string', description: 'Snapshot name to rollback to' } + }, + required: ['node', 'vmid', 'snapname'] + } + }, + { + name: 'proxmox_delete_snapshot_lxc', + description: 'Delete a snapshot of an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + snapname: { type: 'string', description: 'Snapshot name to delete' } + }, + required: ['node', 'vmid', 'snapname'] + } + }, + { + name: 'proxmox_delete_snapshot_vm', + description: 'Delete a snapshot of a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + snapname: { type: 'string', description: 'Snapshot name to delete' } + }, + required: ['node', 'vmid', 'snapname'] + } + }, + { + name: 'proxmox_create_backup_lxc', + description: 'Create a backup of an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + storage: { type: 'string', description: 'Storage location for backup', default: 'local' }, + mode: { type: 'string', enum: ['snapshot', 'suspend', 'stop'], description: 'Backup mode', default: 'snapshot' }, + compress: { type: 'string', enum: ['none', 'lzo', 'gzip', 'zstd'], description: 'Compression algorithm', default: 'zstd' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_create_backup_vm', + description: 'Create a backup of a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + storage: { type: 'string', description: 'Storage location for backup', default: 'local' }, + mode: { type: 'string', enum: ['snapshot', 'suspend', 'stop'], description: 'Backup mode', default: 'snapshot' }, + compress: { type: 'string', enum: ['none', 'lzo', 'gzip', 'zstd'], description: 'Compression algorithm', default: 'zstd' } + }, + required: ['node', 'vmid'] + } + }, + { + name: 'proxmox_list_backups', + description: 'List all backups on a storage (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name' }, + storage: { type: 'string', description: 'Storage name', default: 'local' } + }, + required: ['node'] + } + }, + { + name: 'proxmox_restore_backup_lxc', + description: 'Restore an LXC container from backup (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container will be restored' }, + vmid: { type: 'string', description: 'New container ID for restored container' }, + archive: { type: 'string', description: 'Backup archive path (e.g., local:backup/vzdump-lxc-100-2025_11_06-09_00_00.tar.zst)' }, + storage: { type: 'string', description: 'Storage location for restored container (optional)' } + }, + required: ['node', 'vmid', 'archive'] + } + }, + { + name: 'proxmox_restore_backup_vm', + description: 'Restore a QEMU virtual machine from backup (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM will be restored' }, + vmid: { type: 'string', description: 'New VM ID for restored VM' }, + archive: { type: 'string', description: 'Backup archive path (e.g., local:backup/vzdump-qemu-100-2025_11_06-09_00_00.vma.zst)' }, + storage: { type: 'string', description: 'Storage location for restored VM (optional)' } + }, + required: ['node', 'vmid', 'archive'] + } + }, + { + name: 'proxmox_delete_backup', + description: 'Delete a backup file from storage (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name' }, + storage: { type: 'string', description: 'Storage name (e.g., local)' }, + volume: { type: 'string', description: 'Backup volume ID (e.g., local:backup/vzdump-lxc-100-2025_11_06-09_00_00.tar.zst)' } + }, + required: ['node', 'storage', 'volume'] + } + }, + { + name: 'proxmox_add_disk_vm', + description: 'Add a new disk to a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + disk: { type: 'string', description: 'Disk name (e.g., scsi1, virtio1, sata1, ide1)' }, + storage: { type: 'string', description: 'Storage name (e.g., local-lvm)' }, + size: { type: 'string', description: 'Disk size in GB (e.g., 10)' } + }, + required: ['node', 'vmid', 'disk', 'storage', 'size'] + } + }, + { + name: 'proxmox_add_mountpoint_lxc', + description: 'Add a mount point to an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + mp: { type: 'string', description: 'Mount point name (e.g., mp0, mp1, mp2)' }, + storage: { type: 'string', description: 'Storage name (e.g., local-lvm)' }, + size: { type: 'string', description: 'Mount point size in GB (e.g., 10)' } + }, + required: ['node', 'vmid', 'mp', 'storage', 'size'] + } + }, + { + name: 'proxmox_resize_disk_vm', + description: 'Resize a QEMU VM disk (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + disk: { type: 'string', description: 'Disk name (e.g., scsi0, virtio0, sata0, ide0)' }, + size: { type: 'string', description: 'New size with + for relative or absolute (e.g., +10G or 50G)' } + }, + required: ['node', 'vmid', 'disk', 'size'] + } + }, + { + name: 'proxmox_resize_disk_lxc', + description: 'Resize an LXC container disk or mount point (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + disk: { type: 'string', description: 'Disk name (rootfs, mp0, mp1, etc.)' }, + size: { type: 'string', description: 'New size with + for relative or absolute (e.g., +10G or 50G)' } + }, + required: ['node', 'vmid', 'disk', 'size'] + } + }, + { + name: 'proxmox_remove_disk_vm', + description: 'Remove a disk from a QEMU virtual machine (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + disk: { type: 'string', description: 'Disk name to remove (e.g., scsi1, virtio1, sata1, ide1)' } + }, + required: ['node', 'vmid', 'disk'] + } + }, + { + name: 'proxmox_remove_mountpoint_lxc', + description: 'Remove a mount point from an LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + mp: { type: 'string', description: 'Mount point name to remove (e.g., mp0, mp1, mp2)' } + }, + required: ['node', 'vmid', 'mp'] + } + }, + { + name: 'proxmox_move_disk_vm', + description: 'Move a QEMU VM disk to different storage (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + disk: { type: 'string', description: 'Disk name to move (e.g., scsi0, virtio0, sata0, ide0)' }, + storage: { type: 'string', description: 'Target storage name' }, + delete: { type: 'boolean', description: 'Delete source disk after move (default: true)', default: true } + }, + required: ['node', 'vmid', 'disk', 'storage'] + } + }, + { + name: 'proxmox_move_disk_lxc', + description: 'Move an LXC container disk to different storage (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + disk: { type: 'string', description: 'Disk/volume name to move (rootfs, mp0, mp1, etc.)' }, + storage: { type: 'string', description: 'Target storage name' }, + delete: { type: 'boolean', description: 'Delete source disk after move (default: true)', default: true } + }, + required: ['node', 'vmid', 'disk', 'storage'] + } + }, + { + name: 'proxmox_add_network_vm', + description: 'Add network interface to QEMU VM (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + net: { type: 'string', description: 'Network interface name (net0, net1, net2, etc.)' }, + bridge: { type: 'string', description: 'Bridge name (e.g., vmbr0, vmbr1)' }, + model: { type: 'string', description: 'Network model (virtio, e1000, rtl8139, vmxnet3)', default: 'virtio' }, + macaddr: { type: 'string', description: 'MAC address (XX:XX:XX:XX:XX:XX) - auto-generated if not specified' }, + vlan: { type: 'number', description: 'VLAN tag (1-4094)' }, + firewall: { type: 'boolean', description: 'Enable firewall on this interface' } + }, + required: ['node', 'vmid', 'net', 'bridge'] + } + }, + { + name: 'proxmox_add_network_lxc', + description: 'Add network interface to LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + net: { type: 'string', description: 'Network interface name (net0, net1, net2, etc.)' }, + bridge: { type: 'string', description: 'Bridge name (e.g., vmbr0, vmbr1)' }, + ip: { type: 'string', description: 'IP address (dhcp, 192.168.1.100/24, auto)' }, + gw: { type: 'string', description: 'Gateway IP address' }, + firewall: { type: 'boolean', description: 'Enable firewall on this interface' } + }, + required: ['node', 'vmid', 'net', 'bridge'] + } + }, + { + name: 'proxmox_update_network_vm', + description: 'Update/modify VM network interface configuration (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + net: { type: 'string', description: 'Network interface name to update (net0, net1, net2, etc.)' }, + bridge: { type: 'string', description: 'Bridge name (e.g., vmbr0, vmbr1)' }, + model: { type: 'string', description: 'Network model (virtio, e1000, rtl8139, vmxnet3)' }, + macaddr: { type: 'string', description: 'MAC address (XX:XX:XX:XX:XX:XX)' }, + vlan: { type: 'number', description: 'VLAN tag (1-4094)' }, + firewall: { type: 'boolean', description: 'Enable firewall on this interface' } + }, + required: ['node', 'vmid', 'net'] + } + }, + { + name: 'proxmox_update_network_lxc', + description: 'Update/modify LXC network interface configuration (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + net: { type: 'string', description: 'Network interface name to update (net0, net1, net2, etc.)' }, + bridge: { type: 'string', description: 'Bridge name (e.g., vmbr0, vmbr1)' }, + ip: { type: 'string', description: 'IP address (dhcp, 192.168.1.100/24, auto)' }, + gw: { type: 'string', description: 'Gateway IP address' }, + firewall: { type: 'boolean', description: 'Enable firewall on this interface' } + }, + required: ['node', 'vmid', 'net'] + } + }, + { + name: 'proxmox_remove_network_vm', + description: 'Remove network interface from QEMU VM (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where VM is located' }, + vmid: { type: 'string', description: 'VM ID number' }, + net: { type: 'string', description: 'Network interface name to remove (net0, net1, net2, etc.)' } + }, + required: ['node', 'vmid', 'net'] + } + }, + { + name: 'proxmox_remove_network_lxc', + description: 'Remove network interface from LXC container (requires elevated permissions)', + inputSchema: { + type: 'object', + properties: { + node: { type: 'string', description: 'Node name where container is located' }, + vmid: { type: 'string', description: 'Container ID number' }, + net: { type: 'string', description: 'Network interface name to remove (net0, net1, net2, etc.)' } + }, + required: ['node', 'vmid', 'net'] + } } ] })); @@ -204,7 +922,151 @@ class ProxmoxServer { case 'proxmox_get_cluster_status': return await this.getClusterStatus(); - + + case 'proxmox_list_templates': + return await this.listTemplates(args.node, args.storage); + + case 'proxmox_create_lxc': + return await this.createLXCContainer(args); + + case 'proxmox_create_vm': + return await this.createVM(args); + + case 'proxmox_get_next_vmid': + return await this.getNextVMID(); + + case 'proxmox_start_lxc': + return await this.startVM(args.node, args.vmid, 'lxc'); + + case 'proxmox_start_vm': + return await this.startVM(args.node, args.vmid, 'qemu'); + + case 'proxmox_stop_lxc': + return await this.stopVM(args.node, args.vmid, 'lxc'); + + case 'proxmox_stop_vm': + return await this.stopVM(args.node, args.vmid, 'qemu'); + + case 'proxmox_delete_lxc': + return await this.deleteVM(args.node, args.vmid, 'lxc'); + + case 'proxmox_delete_vm': + return await this.deleteVM(args.node, args.vmid, 'qemu'); + + case 'proxmox_reboot_lxc': + return await this.rebootVM(args.node, args.vmid, 'lxc'); + + case 'proxmox_reboot_vm': + return await this.rebootVM(args.node, args.vmid, 'qemu'); + + case 'proxmox_shutdown_lxc': + return await this.shutdownVM(args.node, args.vmid, 'lxc'); + + case 'proxmox_shutdown_vm': + return await this.shutdownVM(args.node, args.vmid, 'qemu'); + + case 'proxmox_pause_vm': + return await this.pauseVM(args.node, args.vmid); + + case 'proxmox_resume_vm': + return await this.resumeVM(args.node, args.vmid); + + case 'proxmox_clone_lxc': + return await this.cloneVM(args.node, args.vmid, args.newid, args.hostname, 'lxc'); + + case 'proxmox_clone_vm': + return await this.cloneVM(args.node, args.vmid, args.newid, args.name, 'qemu'); + + case 'proxmox_resize_lxc': + return await this.resizeVM(args.node, args.vmid, args.memory, args.cores, 'lxc'); + + case 'proxmox_resize_vm': + return await this.resizeVM(args.node, args.vmid, args.memory, args.cores, 'qemu'); + + case 'proxmox_create_snapshot_lxc': + return await this.createSnapshot(args.node, args.vmid, args.snapname, 'lxc'); + + case 'proxmox_create_snapshot_vm': + return await this.createSnapshot(args.node, args.vmid, args.snapname, 'qemu'); + + case 'proxmox_list_snapshots_lxc': + return await this.listSnapshots(args.node, args.vmid, 'lxc'); + + case 'proxmox_list_snapshots_vm': + return await this.listSnapshots(args.node, args.vmid, 'qemu'); + + case 'proxmox_rollback_snapshot_lxc': + return await this.rollbackSnapshot(args.node, args.vmid, args.snapname, 'lxc'); + + case 'proxmox_rollback_snapshot_vm': + return await this.rollbackSnapshot(args.node, args.vmid, args.snapname, 'qemu'); + + case 'proxmox_delete_snapshot_lxc': + return await this.deleteSnapshot(args.node, args.vmid, args.snapname, 'lxc'); + + case 'proxmox_delete_snapshot_vm': + return await this.deleteSnapshot(args.node, args.vmid, args.snapname, 'qemu'); + + case 'proxmox_create_backup_lxc': + return await this.createBackup(args.node, args.vmid, args.storage, args.mode, args.compress, 'lxc'); + + case 'proxmox_create_backup_vm': + return await this.createBackup(args.node, args.vmid, args.storage, args.mode, args.compress, 'qemu'); + + case 'proxmox_list_backups': + return await this.listBackups(args.node, args.storage); + + case 'proxmox_restore_backup_lxc': + return await this.restoreBackup(args.node, args.vmid, args.archive, args.storage, 'lxc'); + + case 'proxmox_restore_backup_vm': + return await this.restoreBackup(args.node, args.vmid, args.archive, args.storage, 'qemu'); + + case 'proxmox_delete_backup': + return await this.deleteBackup(args.node, args.storage, args.volume); + + case 'proxmox_add_disk_vm': + return await this.addDiskVM(args.node, args.vmid, args.disk, args.storage, args.size); + + case 'proxmox_add_mountpoint_lxc': + return await this.addMountPointLXC(args.node, args.vmid, args.mp, args.storage, args.size); + + case 'proxmox_resize_disk_vm': + return await this.resizeDiskVM(args.node, args.vmid, args.disk, args.size); + + case 'proxmox_resize_disk_lxc': + return await this.resizeDiskLXC(args.node, args.vmid, args.disk, args.size); + + case 'proxmox_remove_disk_vm': + return await this.removeDiskVM(args.node, args.vmid, args.disk); + + case 'proxmox_remove_mountpoint_lxc': + return await this.removeMountPointLXC(args.node, args.vmid, args.mp); + + case 'proxmox_move_disk_vm': + return await this.moveDiskVM(args.node, args.vmid, args.disk, args.storage, args.delete); + + case 'proxmox_move_disk_lxc': + return await this.moveDiskLXC(args.node, args.vmid, args.disk, args.storage, args.delete); + + case 'proxmox_add_network_vm': + return await this.addNetworkVM(args.node, args.vmid, args.net, args.bridge, args.model, args.macaddr, args.vlan, args.firewall); + + case 'proxmox_add_network_lxc': + return await this.addNetworkLXC(args.node, args.vmid, args.net, args.bridge, args.ip, args.gw, args.firewall); + + case 'proxmox_update_network_vm': + return await this.updateNetworkVM(args.node, args.vmid, args.net, args.bridge, args.model, args.macaddr, args.vlan, args.firewall); + + case 'proxmox_update_network_lxc': + return await this.updateNetworkLXC(args.node, args.vmid, args.net, args.bridge, args.ip, args.gw, args.firewall); + + case 'proxmox_remove_network_vm': + return await this.removeNetworkVM(args.node, args.vmid, args.net); + + case 'proxmox_remove_network_lxc': + return await this.removeNetworkLXC(args.node, args.vmid, args.net); + default: throw new Error(`Unknown tool: ${name}`); } @@ -249,28 +1111,40 @@ class ProxmoxServer { async getNodeStatus(node) { if (!this.allowElevated) { return { - content: [{ - type: 'text', + content: [{ + type: 'text', text: `โš ๏ธ **Node Status Requires Elevated Permissions**\n\nTo view detailed node status, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file and ensure your API token has Sys.Audit permissions.\n\n**Current permissions**: Basic (node listing only)` }] }; } - - const status = await this.proxmoxRequest(`/nodes/${node}/status`); - - let output = `๐Ÿ–ฅ๏ธ **Node ${node} Status**\n\n`; - output += `โ€ข **Status**: ${status.uptime ? '๐ŸŸข Online' : '๐Ÿ”ด Offline'}\n`; - output += `โ€ข **Uptime**: ${status.uptime ? this.formatUptime(status.uptime) : 'N/A'}\n`; - output += `โ€ข **Load Average**: ${status.loadavg?.join(', ') || 'N/A'}\n`; - output += `โ€ข **CPU Usage**: ${status.cpu ? `${(status.cpu * 100).toFixed(1)}%` : 'N/A'}\n`; - output += `โ€ข **Memory**: ${status.memory ? - `${this.formatBytes(status.memory.used)} / ${this.formatBytes(status.memory.total)} (${((status.memory.used / status.memory.total) * 100).toFixed(1)}%)` : 'N/A'}\n`; - output += `โ€ข **Root Disk**: ${status.rootfs ? - `${this.formatBytes(status.rootfs.used)} / ${this.formatBytes(status.rootfs.total)} (${((status.rootfs.used / status.rootfs.total) * 100).toFixed(1)}%)` : 'N/A'}\n`; - - return { - content: [{ type: 'text', text: output }] - }; + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + + const status = await this.proxmoxRequest(`/nodes/${safeNode}/status`); + + let output = `๐Ÿ–ฅ๏ธ **Node ${safeNode} Status**\n\n`; + output += `โ€ข **Status**: ${status.uptime ? '๐ŸŸข Online' : '๐Ÿ”ด Offline'}\n`; + output += `โ€ข **Uptime**: ${status.uptime ? this.formatUptime(status.uptime) : 'N/A'}\n`; + output += `โ€ข **Load Average**: ${status.loadavg?.join(', ') || 'N/A'}\n`; + output += `โ€ข **CPU Usage**: ${status.cpu ? `${(status.cpu * 100).toFixed(1)}%` : 'N/A'}\n`; + output += `โ€ข **Memory**: ${status.memory ? + `${this.formatBytes(status.memory.used)} / ${this.formatBytes(status.memory.total)} (${((status.memory.used / status.memory.total) * 100).toFixed(1)}%)` : 'N/A'}\n`; + output += `โ€ข **Root Disk**: ${status.rootfs ? + `${this.formatBytes(status.rootfs.used)} / ${this.formatBytes(status.rootfs.total)} (${((status.rootfs.used / status.rootfs.total) * 100).toFixed(1)}%)` : 'N/A'}\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to get node status**\n\nError: ${error.message}` + }] + }; + } } async getVMs(nodeFilter = null, typeFilter = 'all') { @@ -334,13 +1208,18 @@ class ProxmoxServer { } async getVMStatus(node, vmid, type = 'qemu') { - const vmStatus = await this.proxmoxRequest(`/nodes/${node}/${type}/${vmid}/status/current`); - - const status = vmStatus.status === 'running' ? '๐ŸŸข' : vmStatus.status === 'stopped' ? '๐Ÿ”ด' : '๐ŸŸก'; - const typeIcon = type === 'qemu' ? '๐Ÿ–ฅ๏ธ' : '๐Ÿ“ฆ'; - - let output = `${status} ${typeIcon} **${vmStatus.name || `VM-${vmid}`}** (ID: ${vmid})\n\n`; - output += `โ€ข **Node**: ${node}\n`; + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const vmStatus = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/status/current`); + + const status = vmStatus.status === 'running' ? '๐ŸŸข' : vmStatus.status === 'stopped' ? '๐Ÿ”ด' : '๐ŸŸก'; + const typeIcon = type === 'qemu' ? '๐Ÿ–ฅ๏ธ' : '๐Ÿ“ฆ'; + + let output = `${status} ${typeIcon} **${vmStatus.name || `VM-${safeVMID}`}** (ID: ${safeVMID})\n\n`; + output += `โ€ข **Node**: ${safeNode}\n`; output += `โ€ข **Status**: ${vmStatus.status}\n`; output += `โ€ข **Type**: ${type.toUpperCase()}\n`; @@ -354,57 +1233,68 @@ class ProxmoxServer { output += `โ€ข **Network In**: ${vmStatus.netin ? this.formatBytes(vmStatus.netin) : 'N/A'}\n`; output += `โ€ข **Network Out**: ${vmStatus.netout ? this.formatBytes(vmStatus.netout) : 'N/A'}\n`; } - - return { - content: [{ type: 'text', text: output }] - }; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ type: 'text', text: `โŒ Failed to get VM status: ${error.message}` }], + isError: true + }; + } } async executeVMCommand(node, vmid, command, type = 'qemu') { if (!this.allowElevated) { return { - content: [{ - type: 'text', + content: [{ + type: 'text', text: `โš ๏ธ **VM Command Execution Requires Elevated Permissions**\n\nTo execute commands on VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file and ensure your API token has appropriate VM permissions.\n\n**Current permissions**: Basic (VM listing only)\n**Requested command**: \`${command}\`` }] }; } - + try { + // Validate inputs to prevent injection attacks + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + const safeCommand = this.validateCommand(command); + // For QEMU VMs, we need to use the guest agent if (type === 'qemu') { - const result = await this.proxmoxRequest(`/nodes/${node}/qemu/${vmid}/agent/exec`, 'POST', { - command: command + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/agent/exec`, 'POST', { + command: safeCommand }); - - let output = `๐Ÿ’ป **Command executed on VM ${vmid}**\n\n`; - output += `**Command**: \`${command}\`\n`; + + let output = `๐Ÿ’ป **Command executed on VM ${safeVMID}**\n\n`; + output += `**Command**: \`${safeCommand}\`\n`; output += `**Result**: Command submitted to guest agent\n`; output += `**PID**: ${result.pid || 'N/A'}\n\n`; output += `*Note: Use guest agent status to check command completion*`; - + return { content: [{ type: 'text', text: output }] }; } else { // For LXC containers, we can execute directly - const result = await this.proxmoxRequest(`/nodes/${node}/lxc/${vmid}/exec`, 'POST', { - command: command + const result = await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/exec`, 'POST', { + command: safeCommand }); - - let output = `๐Ÿ“ฆ **Command executed on LXC ${vmid}**\n\n`; - output += `**Command**: \`${command}\`\n`; + + let output = `๐Ÿ“ฆ **Command executed on LXC ${safeVMID}**\n\n`; + output += `**Command**: \`${safeCommand}\`\n`; output += `**Output**:\n\`\`\`\n${result || 'Command executed successfully'}\n\`\`\``; - + return { content: [{ type: 'text', text: output }] }; } } catch (error) { return { - content: [{ - type: 'text', - text: `โŒ **Failed to execute command on VM ${vmid}**\n\nError: ${error.message}\n\n*Note: Make sure the VM has guest agent installed and running*` + content: [{ + type: 'text', + text: `โŒ **Failed to execute command on VM ${vmid}**\n\nError: ${error.message}\n\n*Note: Make sure the VM has guest agent installed and running*` }] }; } @@ -529,6 +1419,1702 @@ class ProxmoxServer { } } + async listTemplates(node, storage = 'local') { + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + + const templates = await this.proxmoxRequest(`/nodes/${safeNode}/storage/${storage}/content?content=vztmpl`); + + let output = '๐Ÿ“ฆ **Available LXC Templates**\n\n'; + + if (!templates || templates.length === 0) { + output += `No templates found on storage \`${storage}\`.\n\n`; + output += `**Tip**: Download templates in Proxmox:\n`; + output += `1. Go to your node โ†’ Storage โ†’ ${storage}\n`; + output += `2. Click "CT Templates"\n`; + output += `3. Download a template (e.g., Debian, Ubuntu)\n`; + } else { + for (const template of templates) { + const size = template.size ? this.formatBytes(template.size) : 'N/A'; + output += `โ€ข **${template.volid}**\n`; + output += ` Size: ${size}\n\n`; + } + } + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to list templates**\n\nError: ${error.message}\n\n**Note**: Make sure the storage exists and contains LXC templates.` + }] + }; + } + } + + async createLXCContainer(args) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Container Creation Requires Elevated Permissions**\n\nTo create containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file and ensure your API token has VM.Allocate permissions.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(args.node); + const safeVMID = this.validateVMID(args.vmid); + + // Generate secure password if not provided + const generatedPassword = args.password || this.generateSecurePassword(); + const isPasswordGenerated = !args.password; + + // Build the request body + const body = { + vmid: safeVMID, + ostemplate: args.ostemplate, + hostname: args.hostname || `ct${safeVMID}`, + password: generatedPassword, + memory: args.memory || 512, + storage: args.storage || 'local-lvm', + rootfs: `${args.storage || 'local-lvm'}:${args.rootfs || 8}` + }; + + // Make the API request + const result = await this.proxmoxRequest(`/nodes/${safeNode}/lxc`, 'POST', body); + + let output = `โœ… **LXC Container Creation Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Hostname**: ${body.hostname}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Template**: ${args.ostemplate}\n`; + output += `โ€ข **Memory**: ${body.memory} MB\n`; + output += `โ€ข **Storage**: ${body.storage}\n`; + + if (isPasswordGenerated) { + output += `โ€ข **๐Ÿ” Generated Password**: \`${generatedPassword}\`\n`; + output += ` โš ๏ธ **SAVE THIS PASSWORD** - it will not be shown again!\n`; + } + + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Next steps**:\n`; + output += `1. Wait a moment for container to be created\n`; + output += `2. Start it with \`proxmox_start_lxc\`\n`; + output += `3. View status with \`proxmox_get_vm_status\`\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to create container**\n\nError: ${error.message}\n\n**Common issues**:\n- VM ID already in use\n- Invalid template path\n- Insufficient permissions\n- Storage doesn't exist` + }] + }; + } + } + + async createVM(args) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Creation Requires Elevated Permissions**\n\nTo create VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file and ensure your API token has VM.Allocate permissions.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(args.node); + const safeVMID = this.validateVMID(args.vmid); + + // Build the request body for QEMU VM creation + const body = { + vmid: safeVMID, + name: args.name || `vm${safeVMID}`, + memory: args.memory || 512, + cores: args.cores || 1, + sockets: args.sockets || 1, + ostype: args.ostype || 'l26', + net0: args.net0 || 'virtio,bridge=vmbr0' + }; + + // Add disk configuration + // Format: storage:size (size in GB, no suffix) + const storage = args.storage || 'local-lvm'; + const diskSize = args.disk_size || '8G'; + // Extract numeric value from disk size (e.g., "8G" -> "8") + const sizeValue = diskSize.replace(/[^0-9]/g, ''); + body.scsi0 = `${storage}:${sizeValue}`; + + // Add ISO if provided + if (args.iso) { + body.ide2 = `${args.iso},media=cdrom`; + body.boot = 'order=ide2;scsi0'; + } + + // Make the API request + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu`, 'POST', body); + + let output = `โœ… **QEMU VM Creation Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Name**: ${body.name}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Memory**: ${body.memory} MB\n`; + output += `โ€ข **CPU**: ${body.sockets} socket(s), ${body.cores} core(s)\n`; + output += `โ€ข **Disk**: ${body.scsi0}\n`; + output += `โ€ข **Network**: ${body.net0}\n`; + if (args.iso) { + output += `โ€ข **ISO**: ${args.iso}\n`; + } + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Next steps**:\n`; + output += `1. Wait a moment for VM to be created\n`; + output += `2. Start it with \`proxmox_start_vm\`\n`; + output += `3. View status with \`proxmox_get_vm_status\`\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to create VM**\n\nError: ${error.message}\n\n**Common issues**:\n- VM ID already in use\n- Invalid ISO path\n- Insufficient permissions\n- Storage doesn't exist` + }] + }; + } + } + + async startVM(node, vmid, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Control Requires Elevated Permissions**\n\nTo start/stop VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/status/start`, 'POST', {}); + + let output = `โ–ถ๏ธ **VM/Container Start Command Sent**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Tip**: Use \`proxmox_get_vm_status\` to check if it's running.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to start VM/Container**\n\nError: ${error.message}` + }] + }; + } + } + + async stopVM(node, vmid, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Control Requires Elevated Permissions**\n\nTo start/stop VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/status/stop`, 'POST', {}); + + let output = `โน๏ธ **VM/Container Stop Command Sent**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Tip**: Use \`proxmox_get_vm_status\` to confirm it's stopped.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to stop VM/Container**\n\nError: ${error.message}` + }] + }; + } + } + + async getNextVMID() { + try { + const result = await this.proxmoxRequest('/cluster/nextid'); + return { + content: [{ type: 'text', text: `**Next Available VM/Container ID**: ${result}` }] + }; + } catch (error) { + return { + content: [{ type: 'text', text: `โŒ **Failed to get next VMID**\n\nError: ${error.message}` }] + }; + } + } + + async deleteVM(node, vmid, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM/Container Deletion Requires Elevated Permissions**\n\nTo delete VMs/containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}`, 'DELETE'); + + let output = `๐Ÿ—‘๏ธ **VM/Container Deletion Started**\n\n`; + output += `โ€ข **VM/Container ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: Deletion may take a moment to complete.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to delete VM/Container**\n\nError: ${error.message}\n\n**Note**: Make sure the VM/container is stopped first.` + }] + }; + } + } + + async rebootVM(node, vmid, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Reboot Requires Elevated Permissions**\n\nTo reboot VMs/containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/status/reboot`, 'POST', {}); + + let output = `๐Ÿ”„ **VM/Container Reboot Command Sent**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Tip**: The VM/container will restart momentarily.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to reboot VM/Container**\n\nError: ${error.message}` + }] + }; + } + } + + async shutdownVM(node, vmid, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Shutdown Requires Elevated Permissions**\n\nTo shutdown VMs/containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/status/shutdown`, 'POST', {}); + + let output = `โธ๏ธ **VM/Container Shutdown Command Sent**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: This is a graceful shutdown. Use \`proxmox_stop_vm\` for forceful stop.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to shutdown VM/Container**\n\nError: ${error.message}` + }] + }; + } + } + + async pauseVM(node, vmid) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Pause Requires Elevated Permissions**\n\nTo pause VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/status/suspend`, 'POST', {}); + + let output = `โธ๏ธ **VM Pause Command Sent**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: QEMU\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: VM is now paused. Use \`proxmox_resume_vm\` to resume.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to pause VM**\n\nError: ${error.message}\n\n**Note**: Pause is only available for QEMU VMs, not LXC containers.` + }] + }; + } + } + + async resumeVM(node, vmid) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Resume Requires Elevated Permissions**\n\nTo resume VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/status/resume`, 'POST', {}); + + let output = `โ–ถ๏ธ **VM Resume Command Sent**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: QEMU\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: VM is now resuming from paused state.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to resume VM**\n\nError: ${error.message}\n\n**Note**: Resume is only available for QEMU VMs, not LXC containers.` + }] + }; + } + } + + async cloneVM(node, vmid, newid, nameOrHostname, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Clone Requires Elevated Permissions**\n\nTo clone VMs/containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + const safeNewID = this.validateVMID(newid); + + const body = { + newid: safeNewID + }; + + // For LXC, use 'hostname', for QEMU use 'name' + if (type === 'lxc') { + body.hostname = nameOrHostname || `clone-${safeNewID}`; + } else { + body.name = nameOrHostname || `clone-${safeNewID}`; + } + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/clone`, 'POST', body); + + let output = `๐Ÿ“‹ **VM/Container Clone Started**\n\n`; + output += `โ€ข **Source VM ID**: ${safeVMID}\n`; + output += `โ€ข **New VM ID**: ${safeNewID}\n`; + output += `โ€ข **New Name**: ${nameOrHostname || `clone-${safeNewID}`}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: Clone operation may take several minutes. Check task status in Proxmox.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to clone VM/Container**\n\nError: ${error.message}\n\n**Common issues**:\n- New VM ID already in use\n- Insufficient storage space\n- Source VM is running (some storage types require stopped VM)` + }] + }; + } + } + + async resizeVM(node, vmid, memory, cores, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **VM Resize Requires Elevated Permissions**\n\nTo resize VMs/containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + // Build body with only provided parameters + const body = {}; + if (memory !== undefined) { + body.memory = memory; + } + if (cores !== undefined) { + body.cores = cores; + } + + if (Object.keys(body).length === 0) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **No Resize Parameters Provided**\n\nPlease specify at least one parameter:\n- \`memory\`: Memory in MB\n- \`cores\`: Number of CPU cores` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/config`, 'PUT', body); + + let output = `๐Ÿ“ **VM/Container Resize Command Sent**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + if (memory !== undefined) { + output += `โ€ข **New Memory**: ${memory} MB\n`; + } + if (cores !== undefined) { + output += `โ€ข **New Cores**: ${cores}\n`; + } + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: Some changes may require a reboot to take effect.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to resize VM/Container**\n\nError: ${error.message}\n\n**Common issues**:\n- Memory/CPU values exceed node capacity\n- VM is locked or in use\n- Invalid parameter values` + }] + }; + } + } + + async createSnapshot(node, vmid, snapname, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Snapshot Creation Requires Elevated Permissions**\n\nTo create snapshots, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/snapshot`, 'POST', { + snapname: snapname + }); + + let output = `๐Ÿ“ธ **Snapshot Creation Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Snapshot Name**: ${snapname}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Tip**: Use \`proxmox_list_snapshots_${type === 'lxc' ? 'lxc' : 'vm'}\` to view all snapshots.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to create snapshot**\n\nError: ${error.message}\n\n**Common issues**:\n- Snapshot name already exists\n- Insufficient disk space\n- VM is locked or in use` + }] + }; + } + } + + async listSnapshots(node, vmid, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Snapshot Listing Requires Elevated Permissions**\n\nTo list snapshots, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const snapshots = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/snapshot`); + + let output = `๐Ÿ“‹ **Snapshots for ${type.toUpperCase()} ${safeVMID}**\n\n`; + + if (!snapshots || snapshots.length === 0) { + output += `No snapshots found.\n\n`; + output += `**Tip**: Create a snapshot with \`proxmox_create_snapshot_${type === 'lxc' ? 'lxc' : 'vm'}\`.\n`; + } else { + // Filter out 'current' pseudo-snapshot that Proxmox includes + const realSnapshots = snapshots.filter(snap => snap.name !== 'current'); + + if (realSnapshots.length === 0) { + output += `No snapshots found.\n\n`; + output += `**Tip**: Create a snapshot with \`proxmox_create_snapshot_${type === 'lxc' ? 'lxc' : 'vm'}\`.\n`; + } else { + for (const snapshot of realSnapshots) { + output += `โ€ข **${snapshot.name}**\n`; + if (snapshot.description) { + output += ` Description: ${snapshot.description}\n`; + } + if (snapshot.snaptime) { + const date = new Date(snapshot.snaptime * 1000); + output += ` Created: ${date.toLocaleString()}\n`; + } + output += `\n`; + } + output += `**Total**: ${realSnapshots.length} snapshot(s)\n`; + } + } + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to list snapshots**\n\nError: ${error.message}` + }] + }; + } + } + + async rollbackSnapshot(node, vmid, snapname, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Snapshot Rollback Requires Elevated Permissions**\n\nTo rollback snapshots, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/snapshot/${snapname}/rollback`, 'POST', {}); + + let output = `โฎ๏ธ **Snapshot Rollback Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Snapshot Name**: ${snapname}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Warning**: This will restore the VM/container to the state of the snapshot.\n`; + output += `**Tip**: Any changes made after the snapshot was created will be lost.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to rollback snapshot**\n\nError: ${error.message}\n\n**Common issues**:\n- Snapshot doesn't exist\n- VM is running (stop it first)\n- VM is locked or in use` + }] + }; + } + } + + async deleteSnapshot(node, vmid, snapname, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Snapshot Deletion Requires Elevated Permissions**\n\nTo delete snapshots, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}/${safeVMID}/snapshot/${snapname}`, 'DELETE'); + + let output = `๐Ÿ—‘๏ธ **Snapshot Deletion Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Snapshot Name**: ${snapname}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: Snapshot deletion may take a moment to complete.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to delete snapshot**\n\nError: ${error.message}\n\n**Common issues**:\n- Snapshot doesn't exist\n- VM is locked or in use\n- Insufficient permissions` + }] + }; + } + } + + async createBackup(node, vmid, storage = 'local', mode = 'snapshot', compress = 'zstd', type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Backup Creation Requires Elevated Permissions**\n\nTo create backups, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/vzdump`, 'POST', { + vmid: safeVMID, + storage: storage, + mode: mode, + compress: compress + }); + + let output = `๐Ÿ’พ **Backup Creation Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Storage**: ${storage}\n`; + output += `โ€ข **Mode**: ${mode}\n`; + output += `โ€ข **Compression**: ${compress}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Tip**: Backup runs in the background. Use \`proxmox_list_backups\` to view all backups.\n`; + output += `**Note**: Backup modes:\n`; + output += ` - snapshot: Quick backup using snapshots (recommended)\n`; + output += ` - suspend: Suspends VM during backup\n`; + output += ` - stop: Stops VM during backup\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to create backup**\n\nError: ${error.message}\n\n**Common issues**:\n- Insufficient disk space on storage\n- VM is locked or in use\n- Invalid storage name\n- Insufficient permissions` + }] + }; + } + } + + async listBackups(node, storage = 'local') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Backup Listing Requires Elevated Permissions**\n\nTo list backups, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + + const backups = await this.proxmoxRequest(`/nodes/${safeNode}/storage/${storage}/content?content=backup`); + + let output = `๐Ÿ“ฆ **Backups on ${storage}**\n\n`; + + if (!backups || backups.length === 0) { + output += `No backups found on storage \`${storage}\`.\n\n`; + output += `**Tip**: Create a backup with \`proxmox_create_backup_lxc\` or \`proxmox_create_backup_vm\`.\n`; + } else { + // Sort by creation time (newest first) + backups.sort((a, b) => (b.ctime || 0) - (a.ctime || 0)); + + for (const backup of backups) { + // Parse backup filename to extract VM type and ID + const filename = backup.volid.split('/').pop(); + const match = filename.match(/vzdump-(lxc|qemu)-(\d+)-/); + const vmType = match ? match[1].toUpperCase() : 'UNKNOWN'; + const vmId = match ? match[2] : 'N/A'; + + output += `โ€ข **${filename}**\n`; + output += ` VM ID: ${vmId} (${vmType})\n`; + output += ` Size: ${backup.size ? this.formatBytes(backup.size) : 'N/A'}\n`; + if (backup.ctime) { + const date = new Date(backup.ctime * 1000); + output += ` Created: ${date.toLocaleString()}\n`; + } + output += ` Volume: ${backup.volid}\n`; + output += `\n`; + } + output += `**Total**: ${backups.length} backup(s)\n`; + } + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to list backups**\n\nError: ${error.message}\n\n**Common issues**:\n- Storage doesn't exist\n- Storage is not accessible\n- Insufficient permissions` + }] + }; + } + } + + async restoreBackup(node, vmid, archive, storage, type = 'lxc') { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Backup Restore Requires Elevated Permissions**\n\nTo restore backups, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + vmid: safeVMID, + archive: archive, + restore: 1 + }; + + if (storage) { + body.storage = storage; + } + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/${type}`, 'POST', body); + + let output = `โ™ป๏ธ **Backup Restore Started**\n\n`; + output += `โ€ข **New VM ID**: ${safeVMID}\n`; + output += `โ€ข **Type**: ${type.toUpperCase()}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Archive**: ${archive}\n`; + if (storage) { + output += `โ€ข **Storage**: ${storage}\n`; + } + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: Restore operation may take several minutes depending on backup size.\n`; + output += `**Tip**: Use \`proxmox_get_vm_status\` to check the restored VM status after completion.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to restore backup**\n\nError: ${error.message}\n\n**Common issues**:\n- VM ID already in use\n- Backup archive doesn't exist\n- Insufficient storage space\n- Invalid archive path\n- Insufficient permissions` + }] + }; + } + } + + async deleteBackup(node, storage, volume) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Backup Deletion Requires Elevated Permissions**\n\nTo delete backups, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + + const encodedVolume = encodeURIComponent(volume); + const result = await this.proxmoxRequest(`/nodes/${safeNode}/storage/${storage}/content/${encodedVolume}`, 'DELETE'); + + let output = `๐Ÿ—‘๏ธ **Backup Deletion Started**\n\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Storage**: ${storage}\n`; + output += `โ€ข **Volume**: ${volume}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: Backup file will be permanently deleted from storage.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to delete backup**\n\nError: ${error.message}\n\n**Common issues**:\n- Backup doesn't exist\n- Invalid volume path\n- Backup is in use\n- Insufficient permissions` + }] + }; + } + } + + async addDiskVM(node, vmid, disk, storage, size) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo add disks to VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + [disk]: `${storage}:${size}` + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/config`, 'PUT', body); + + let output = `๐Ÿ’ฟ **VM Disk Addition Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Disk**: ${disk}\n`; + output += `โ€ข **Storage**: ${storage}\n`; + output += `โ€ข **Size**: ${size} GB\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Disk naming conventions**:\n`; + output += ` - SCSI: scsi0-15\n`; + output += ` - VirtIO: virtio0-15\n`; + output += ` - SATA: sata0-5\n`; + output += ` - IDE: ide0-3\n\n`; + output += `**Note**: The VM may need to be stopped for this operation depending on configuration.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to add disk to VM**\n\nError: ${error.message}\n\n**Common issues**:\n- Disk name already in use\n- VM is running (may need to be stopped)\n- Invalid disk name format\n- Insufficient storage space\n- Storage doesn't exist` + }] + }; + } + } + + async addMountPointLXC(node, vmid, mp, storage, size) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo add mount points to containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + [mp]: `${storage}:${size}` + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/config`, 'PUT', body); + + let output = `๐Ÿ’ฟ **LXC Mount Point Addition Started**\n\n`; + output += `โ€ข **Container ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Mount Point**: ${mp}\n`; + output += `โ€ข **Storage**: ${storage}\n`; + output += `โ€ข **Size**: ${size} GB\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Mount point naming**: mp0-255\n\n`; + output += `**Note**: The container may need to be stopped for this operation.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to add mount point to container**\n\nError: ${error.message}\n\n**Common issues**:\n- Mount point name already in use\n- Container is running (may need to be stopped)\n- Invalid mount point name\n- Insufficient storage space\n- Storage doesn't exist` + }] + }; + } + } + + async resizeDiskVM(node, vmid, disk, size) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo resize VM disks, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + disk: disk, + size: size + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/resize`, 'PUT', body); + + let output = `๐Ÿ“ **VM Disk Resize Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Disk**: ${disk}\n`; + output += `โ€ข **New Size**: ${size}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Size format examples**:\n`; + output += ` - +10G: Add 10GB to current size\n`; + output += ` - 50G: Set absolute size to 50GB\n\n`; + output += `**Note**: Disks can only be expanded, not shrunk. Some configurations allow online resizing.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to resize VM disk**\n\nError: ${error.message}\n\n**Common issues**:\n- Disk doesn't exist\n- Trying to shrink disk (not supported)\n- Insufficient storage space\n- Invalid size format\n- VM is locked or in use` + }] + }; + } + } + + async resizeDiskLXC(node, vmid, disk, size) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo resize LXC disks, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + disk: disk, + size: size + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/resize`, 'PUT', body); + + let output = `๐Ÿ“ **LXC Disk Resize Started**\n\n`; + output += `โ€ข **Container ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Disk**: ${disk}\n`; + output += `โ€ข **New Size**: ${size}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Size format examples**:\n`; + output += ` - +10G: Add 10GB to current size\n`; + output += ` - 50G: Set absolute size to 50GB\n\n`; + output += `**Valid disk names**: rootfs, mp0, mp1, mp2, etc.\n\n`; + output += `**Note**: Disks can only be expanded, not shrunk. Container may need to be stopped.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to resize LXC disk**\n\nError: ${error.message}\n\n**Common issues**:\n- Disk doesn't exist\n- Trying to shrink disk (not supported)\n- Insufficient storage space\n- Invalid size format\n- Container is locked or in use` + }] + }; + } + } + + async removeDiskVM(node, vmid, disk) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo remove disks from VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + delete: disk + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/config`, 'PUT', body); + + let output = `โž– **VM Disk Removal Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Disk**: ${disk}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Warning**: This will permanently delete the disk and all its data.\n`; + output += `**Note**: The VM should be stopped for this operation.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to remove disk from VM**\n\nError: ${error.message}\n\n**Common issues**:\n- Disk doesn't exist\n- VM is running (must be stopped)\n- Cannot remove boot disk\n- VM is locked or in use` + }] + }; + } + } + + async removeMountPointLXC(node, vmid, mp) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo remove mount points from containers, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + delete: mp + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/config`, 'PUT', body); + + let output = `โž– **LXC Mount Point Removal Started**\n\n`; + output += `โ€ข **Container ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Mount Point**: ${mp}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Warning**: This will permanently delete the mount point and all its data.\n`; + output += `**Note**: The container should be stopped for this operation.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to remove mount point from container**\n\nError: ${error.message}\n\n**Common issues**:\n- Mount point doesn't exist\n- Container is running (must be stopped)\n- Cannot remove rootfs\n- Container is locked or in use` + }] + }; + } + } + + async moveDiskVM(node, vmid, disk, storage, deleteSource = true) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo move VM disks, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + disk: disk, + storage: storage, + delete: deleteSource ? 1 : 0 + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/move_disk`, 'POST', body); + + let output = `๐Ÿ“ฆ **VM Disk Move Started**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Disk**: ${disk}\n`; + output += `โ€ข **Target Storage**: ${storage}\n`; + output += `โ€ข **Delete Source**: ${deleteSource ? 'Yes' : 'No'}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Note**: Disk move operation may take several minutes depending on disk size.\n`; + output += `**Tip**: The VM should be stopped for this operation in most configurations.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to move VM disk**\n\nError: ${error.message}\n\n**Common issues**:\n- Disk doesn't exist\n- Target storage doesn't exist\n- Insufficient space on target storage\n- VM is running (may need to be stopped)\n- VM is locked or in use` + }] + }; + } + } + + async moveDiskLXC(node, vmid, disk, storage, deleteSource = true) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Disk Management Requires Elevated Permissions**\n\nTo move LXC disks, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + volume: disk, + storage: storage, + delete: deleteSource ? 1 : 0 + }; + + const result = await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/move_volume`, 'POST', body); + + let output = `๐Ÿ“ฆ **LXC Disk Move Started**\n\n`; + output += `โ€ข **Container ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Volume**: ${disk}\n`; + output += `โ€ข **Target Storage**: ${storage}\n`; + output += `โ€ข **Delete Source**: ${deleteSource ? 'Yes' : 'No'}\n`; + output += `โ€ข **Task ID**: ${result || 'N/A'}\n\n`; + output += `**Valid volumes**: rootfs, mp0, mp1, mp2, etc.\n\n`; + output += `**Note**: Volume move operation may take several minutes depending on volume size.\n`; + output += `**Tip**: The container should be stopped for this operation.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to move LXC volume**\n\nError: ${error.message}\n\n**Common issues**:\n- Volume doesn't exist\n- Target storage doesn't exist\n- Insufficient space on target storage\n- Container is running (may need to be stopped)\n- Container is locked or in use` + }] + }; + } + } + + async addNetworkVM(node, vmid, net, bridge, model = 'virtio', macaddr, vlan, firewall) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Network Management Requires Elevated Permissions**\n\nTo add VM network interfaces, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + // Build network configuration string + let netConfig = `${model || 'virtio'},bridge=${bridge}`; + + if (macaddr) { + netConfig += `,macaddr=${macaddr}`; + } + + if (vlan !== undefined && vlan !== null) { + netConfig += `,tag=${vlan}`; + } + + if (firewall) { + netConfig += `,firewall=1`; + } + + const body = { + [net]: netConfig + }; + + await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/config`, 'PUT', body); + + let output = `๐ŸŒ **VM Network Interface Added**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Interface**: ${net}\n`; + output += `โ€ข **Bridge**: ${bridge}\n`; + output += `โ€ข **Model**: ${model || 'virtio'}\n`; + if (macaddr) output += `โ€ข **MAC Address**: ${macaddr}\n`; + if (vlan !== undefined && vlan !== null) output += `โ€ข **VLAN Tag**: ${vlan}\n`; + if (firewall) output += `โ€ข **Firewall**: Enabled\n`; + output += `\n**Valid interfaces**: net0, net1, net2, etc.\n`; + output += `**Valid models**: virtio (recommended), e1000, rtl8139, vmxnet3\n`; + output += `**Valid bridges**: vmbr0, vmbr1, vmbr2, etc.\n\n`; + output += `**Tip**: Use virtio model for best performance with modern guests.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to add VM network interface**\n\nError: ${error.message}\n\n**Common issues**:\n- Network interface already exists\n- Bridge doesn't exist\n- Invalid MAC address format\n- Invalid VLAN tag (must be 1-4094)\n- VM is locked or in use` + }] + }; + } + } + + async addNetworkLXC(node, vmid, net, bridge, ip, gw, firewall) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Network Management Requires Elevated Permissions**\n\nTo add LXC network interfaces, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + // Extract interface number (e.g., net0 -> 0, net1 -> 1) + const netNum = net.replace('net', ''); + + // Build network configuration string + let netConfig = `name=eth${netNum},bridge=${bridge}`; + + if (ip) { + netConfig += `,ip=${ip}`; + } + + if (gw) { + netConfig += `,gw=${gw}`; + } + + if (firewall) { + netConfig += `,firewall=1`; + } + + const body = { + [net]: netConfig + }; + + await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/config`, 'PUT', body); + + let output = `๐ŸŒ **LXC Network Interface Added**\n\n`; + output += `โ€ข **Container ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Interface**: ${net} (eth${netNum})\n`; + output += `โ€ข **Bridge**: ${bridge}\n`; + if (ip) output += `โ€ข **IP Address**: ${ip}\n`; + if (gw) output += `โ€ข **Gateway**: ${gw}\n`; + if (firewall) output += `โ€ข **Firewall**: Enabled\n`; + output += `\n**Valid interfaces**: net0, net1, net2, etc.\n`; + output += `**Valid bridges**: vmbr0, vmbr1, vmbr2, etc.\n`; + output += `**IP formats**: dhcp, 192.168.1.100/24, auto\n\n`; + output += `**Tip**: Use DHCP for automatic IP assignment or specify static IP with CIDR notation.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to add LXC network interface**\n\nError: ${error.message}\n\n**Common issues**:\n- Network interface already exists\n- Bridge doesn't exist\n- Invalid IP address format\n- Invalid gateway address\n- Container is locked or in use` + }] + }; + } + } + + async updateNetworkVM(node, vmid, net, bridge, model, macaddr, vlan, firewall) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Network Management Requires Elevated Permissions**\n\nTo update VM network interfaces, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + // Get current VM configuration + const config = await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/config`, 'GET'); + + if (!config[net]) { + return { + content: [{ + type: 'text', + text: `โŒ **Network interface ${net} does not exist**\n\nPlease add the interface first using proxmox_add_network_vm.\n\n**Existing interfaces**: ${Object.keys(config).filter(k => k.startsWith('net')).join(', ') || 'None'}` + }] + }; + } + + // Parse current configuration + const currentConfig = config[net]; + const configParts = {}; + currentConfig.split(',').forEach(part => { + const [key, value] = part.split('='); + configParts[key] = value; + }); + + // Update only provided parameters + if (model !== undefined) { + // Extract MAC if present, remove old model + const mac = configParts.macaddr || configParts[Object.keys(configParts).find(k => k.match(/^[0-9A-F]{2}:/i))]; + configParts[model] = mac || ''; + // Remove old model keys + ['virtio', 'e1000', 'rtl8139', 'vmxnet3'].forEach(m => { + if (m !== model) delete configParts[m]; + }); + } + + if (bridge !== undefined) { + configParts.bridge = bridge; + } + + if (macaddr !== undefined) { + configParts.macaddr = macaddr; + } + + if (vlan !== undefined && vlan !== null) { + configParts.tag = vlan; + } else if (vlan === null) { + delete configParts.tag; + } + + if (firewall !== undefined) { + if (firewall) { + configParts.firewall = '1'; + } else { + delete configParts.firewall; + } + } + + // Rebuild configuration string + const netConfig = Object.entries(configParts) + .map(([key, value]) => value ? `${key}=${value}` : key) + .join(','); + + const body = { + [net]: netConfig + }; + + await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/config`, 'PUT', body); + + let output = `๐Ÿ”ง **VM Network Interface Updated**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Interface**: ${net}\n`; + output += `โ€ข **New Configuration**: ${netConfig}\n\n`; + output += `**Changes applied**:\n`; + if (bridge !== undefined) output += `- Bridge: ${bridge}\n`; + if (model !== undefined) output += `- Model: ${model}\n`; + if (macaddr !== undefined) output += `- MAC Address: ${macaddr}\n`; + if (vlan !== undefined) output += `- VLAN Tag: ${vlan !== null ? vlan : 'Removed'}\n`; + if (firewall !== undefined) output += `- Firewall: ${firewall ? 'Enabled' : 'Disabled'}\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to update VM network interface**\n\nError: ${error.message}\n\n**Common issues**:\n- Network interface doesn't exist\n- Bridge doesn't exist\n- Invalid MAC address format\n- Invalid VLAN tag (must be 1-4094)\n- VM is locked or in use` + }] + }; + } + } + + async updateNetworkLXC(node, vmid, net, bridge, ip, gw, firewall) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Network Management Requires Elevated Permissions**\n\nTo update LXC network interfaces, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + // Get current container configuration + const config = await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/config`, 'GET'); + + if (!config[net]) { + return { + content: [{ + type: 'text', + text: `โŒ **Network interface ${net} does not exist**\n\nPlease add the interface first using proxmox_add_network_lxc.\n\n**Existing interfaces**: ${Object.keys(config).filter(k => k.startsWith('net')).join(', ') || 'None'}` + }] + }; + } + + // Parse current configuration + const currentConfig = config[net]; + const configParts = {}; + currentConfig.split(',').forEach(part => { + const [key, value] = part.split('='); + configParts[key] = value; + }); + + // Update only provided parameters + if (bridge !== undefined) { + configParts.bridge = bridge; + } + + if (ip !== undefined) { + configParts.ip = ip; + } + + if (gw !== undefined) { + configParts.gw = gw; + } + + if (firewall !== undefined) { + if (firewall) { + configParts.firewall = '1'; + } else { + delete configParts.firewall; + } + } + + // Rebuild configuration string + const netConfig = Object.entries(configParts) + .map(([key, value]) => `${key}=${value}`) + .join(','); + + const body = { + [net]: netConfig + }; + + await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/config`, 'PUT', body); + + let output = `๐Ÿ”ง **LXC Network Interface Updated**\n\n`; + output += `โ€ข **Container ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Interface**: ${net}\n`; + output += `โ€ข **New Configuration**: ${netConfig}\n\n`; + output += `**Changes applied**:\n`; + if (bridge !== undefined) output += `- Bridge: ${bridge}\n`; + if (ip !== undefined) output += `- IP Address: ${ip}\n`; + if (gw !== undefined) output += `- Gateway: ${gw}\n`; + if (firewall !== undefined) output += `- Firewall: ${firewall ? 'Enabled' : 'Disabled'}\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to update LXC network interface**\n\nError: ${error.message}\n\n**Common issues**:\n- Network interface doesn't exist\n- Bridge doesn't exist\n- Invalid IP address format\n- Invalid gateway address\n- Container is locked or in use` + }] + }; + } + } + + async removeNetworkVM(node, vmid, net) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Network Management Requires Elevated Permissions**\n\nTo remove VM network interfaces, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + delete: net + }; + + await this.proxmoxRequest(`/nodes/${safeNode}/qemu/${safeVMID}/config`, 'PUT', body); + + let output = `โž– **VM Network Interface Removed**\n\n`; + output += `โ€ข **VM ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Interface Removed**: ${net}\n\n`; + output += `**Note**: The network interface has been removed from the VM configuration.\n`; + output += `**Tip**: If the VM is running, you may need to restart it for changes to take effect.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to remove VM network interface**\n\nError: ${error.message}\n\n**Common issues**:\n- Network interface doesn't exist\n- VM is locked or in use\n- Invalid interface name` + }] + }; + } + } + + async removeNetworkLXC(node, vmid, net) { + if (!this.allowElevated) { + return { + content: [{ + type: 'text', + text: `โš ๏ธ **Network Management Requires Elevated Permissions**\n\nTo remove LXC network interfaces, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file.\n\n**Current permissions**: Basic (read-only)` + }] + }; + } + + try { + // Validate inputs + const safeNode = this.validateNodeName(node); + const safeVMID = this.validateVMID(vmid); + + const body = { + delete: net + }; + + await this.proxmoxRequest(`/nodes/${safeNode}/lxc/${safeVMID}/config`, 'PUT', body); + + let output = `โž– **LXC Network Interface Removed**\n\n`; + output += `โ€ข **Container ID**: ${safeVMID}\n`; + output += `โ€ข **Node**: ${safeNode}\n`; + output += `โ€ข **Interface Removed**: ${net}\n\n`; + output += `**Note**: The network interface has been removed from the container configuration.\n`; + output += `**Tip**: If the container is running, you may need to restart it for changes to take effect.\n`; + + return { + content: [{ type: 'text', text: output }] + }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `โŒ **Failed to remove LXC network interface**\n\nError: ${error.message}\n\n**Common issues**:\n- Network interface doesn't exist\n- Container is locked or in use\n- Invalid interface name` + }] + }; + } + } + formatUptime(seconds) { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); diff --git a/test-basic-tools.js b/test-basic-tools.js new file mode 100755 index 0000000..54816b0 --- /dev/null +++ b/test-basic-tools.js @@ -0,0 +1,304 @@ +#!/usr/bin/env node + +/** + * Test script for basic (non-elevated) Proxmox MCP tools + * This tests all tools that should work without PROXMOX_ALLOW_ELEVATED=true + */ + +import { spawn } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ANSI color codes +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m' +}; + +// Test results tracker +const results = { + passed: [], + failed: [], + warnings: [] +}; + +// Call a tool and return the result +async function callTool(toolName, args = {}) { + return new Promise((resolve, reject) => { + const request = { + jsonrpc: '2.0', + id: Date.now(), + method: 'tools/call', + params: { + name: toolName, + arguments: args + } + }; + + const serverProcess = spawn('node', [path.join(__dirname, 'index.js')], { + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + serverProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + serverProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + serverProcess.on('close', (code) => { + try { + // Find JSON response in stdout + const lines = stdout.split('\n'); + let response = null; + for (const line of lines) { + if (line.trim().startsWith('{')) { + try { + response = JSON.parse(line); + break; + } catch (e) { + // Not JSON, continue + } + } + } + + if (response) { + resolve({ response, stderr, code }); + } else { + reject(new Error(`No JSON response found. stdout: ${stdout}, stderr: ${stderr}`)); + } + } catch (error) { + reject(error); + } + }); + + serverProcess.on('error', reject); + + // Send request + serverProcess.stdin.write(JSON.stringify(request) + '\n'); + serverProcess.stdin.end(); + }); +} + +// Print test header +function printHeader(message) { + console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`); + console.log(`${colors.cyan}${message}${colors.reset}`); + console.log(`${colors.cyan}${'='.repeat(60)}${colors.reset}\n`); +} + +// Print test result +function printResult(toolName, success, message, details = null) { + const icon = success ? 'โœ“' : 'โœ—'; + const color = success ? colors.green : colors.red; + + console.log(`${color}${icon} ${toolName}${colors.reset}`); + console.log(` ${message}`); + + if (details) { + console.log(` ${colors.yellow}Details:${colors.reset} ${details}`); + } + console.log(); + + if (success) { + results.passed.push({ tool: toolName, message }); + } else { + results.failed.push({ tool: toolName, message, details }); + } +} + +// Test a tool +async function testTool(toolName, args = {}, validator = null) { + try { + console.log(`${colors.blue}Testing: ${toolName}${colors.reset}`); + + const { response } = await callTool(toolName, args); + + if (response.error) { + printResult(toolName, false, `Error: ${response.error.message}`, response.error.code); + return false; + } + + if (!response.result || !response.result.content || response.result.content.length === 0) { + printResult(toolName, false, 'No content returned', JSON.stringify(response.result)); + return false; + } + + const content = response.result.content[0]; + + // Check if it's an error message about permissions + if (content.text && content.text.includes('Requires Elevated Permissions')) { + printResult(toolName, false, 'Incorrectly requires elevated permissions', content.text.substring(0, 100)); + return false; + } + + // Run custom validator if provided + if (validator) { + const validationResult = validator(content); + if (!validationResult.success) { + printResult(toolName, false, validationResult.message, validationResult.details); + return false; + } + } + + printResult(toolName, true, 'Success', `Returned ${content.text ? content.text.length : 0} characters`); + return true; + } catch (error) { + printResult(toolName, false, `Exception: ${error.message}`, error.stack); + return false; + } +} + +// Main test suite +async function runTests() { + printHeader('Testing Basic Proxmox MCP Tools'); + + console.log(`${colors.yellow}Note: These tests require a working Proxmox connection.${colors.reset}`); + console.log(`${colors.yellow}Ensure .env is configured with valid credentials.${colors.reset}\n`); + + // Test 1: proxmox_get_nodes + await testTool('proxmox_get_nodes', {}, (content) => { + if (!content.text || content.text.length === 0) { + return { success: false, message: 'Empty response' }; + } + if (!content.text.includes('Node') && !content.text.includes('node')) { + return { success: false, message: 'Response does not appear to contain node information' }; + } + return { success: true }; + }); + + // Test 2: proxmox_get_cluster_status + await testTool('proxmox_get_cluster_status', {}, (content) => { + if (!content.text || content.text.length === 0) { + return { success: false, message: 'Empty response' }; + } + return { success: true }; + }); + + // Test 3: proxmox_get_vms + await testTool('proxmox_get_vms', {}, (content) => { + if (!content.text || content.text.length === 0) { + return { success: false, message: 'Empty response' }; + } + return { success: true }; + }); + + // Test 4: proxmox_get_storage + await testTool('proxmox_get_storage', {}, (content) => { + if (!content.text || content.text.length === 0) { + return { success: false, message: 'Empty response' }; + } + return { success: true }; + }); + + // Test 5: proxmox_get_next_vmid + await testTool('proxmox_get_next_vmid', {}, (content) => { + if (!content.text || content.text.length === 0) { + return { success: false, message: 'Empty response' }; + } + // Check if response contains a number + if (!/\d{3,}/.test(content.text)) { + return { success: false, message: 'Response does not contain a valid VMID number', details: content.text }; + } + return { success: true }; + }); + + // Test 6: proxmox_list_templates (requires node parameter) + // First we need to get a node name from proxmox_get_nodes + console.log(`${colors.blue}Testing: proxmox_list_templates (requires node name)${colors.reset}`); + console.log(`${colors.yellow} Getting node name first...${colors.reset}`); + + try { + const { response: nodesResponse } = await callTool('proxmox_get_nodes', {}); + if (nodesResponse.result && nodesResponse.result.content && nodesResponse.result.content[0]) { + const nodesText = nodesResponse.result.content[0].text; + // Try to extract first node name + const nodeMatch = nodesText.match(/(?:Node|node):\s*(\S+)/i) || + nodesText.match(/^(\S+)/m); + + if (nodeMatch && nodeMatch[1]) { + const nodeName = nodeMatch[1].replace(/[^a-zA-Z0-9-]/g, ''); + console.log(`${colors.yellow} Using node: ${nodeName}${colors.reset}`); + + await testTool('proxmox_list_templates', { node: nodeName }, (content) => { + if (!content.text || content.text.length === 0) { + return { success: false, message: 'Empty response' }; + } + return { success: true }; + }); + } else { + printResult('proxmox_list_templates', false, 'Could not extract node name from nodes list', nodesText.substring(0, 100)); + } + } else { + printResult('proxmox_list_templates', false, 'Could not get nodes list to find node name'); + } + } catch (error) { + printResult('proxmox_list_templates', false, `Failed to get node for testing: ${error.message}`); + } + + // Test 7: proxmox_get_vm_status (requires node and vmid) + console.log(`${colors.blue}Testing: proxmox_get_vm_status (requires node and vmid)${colors.reset}`); + console.log(`${colors.yellow} Getting VM info first...${colors.reset}`); + + try { + const { response: vmsResponse } = await callTool('proxmox_get_vms', {}); + if (vmsResponse.result && vmsResponse.result.content && vmsResponse.result.content[0]) { + const vmsText = vmsResponse.result.content[0].text; + // Try to extract first VM info - handles format: (ID: 100) ... โ€ข Node: pve1 + const vmMatch = vmsText.match(/\(ID:\s*(\d+)\).*?[โ€ข\s]*Node:\s*(\S+)/is); + + if (vmMatch && vmMatch[1] && vmMatch[2]) { + const vmid = vmMatch[1]; + const nodeName = vmMatch[2].replace(/[^a-zA-Z0-9-]/g, ''); + console.log(`${colors.yellow} Using VM: ${vmid} on node: ${nodeName}${colors.reset}`); + + await testTool('proxmox_get_vm_status', { node: nodeName, vmid: vmid }, (content) => { + if (!content.text || content.text.length === 0) { + return { success: false, message: 'Empty response' }; + } + return { success: true }; + }); + } else { + printResult('proxmox_get_vm_status', false, 'Could not extract VM info from VMs list', vmsText.substring(0, 100)); + } + } else { + printResult('proxmox_get_vm_status', false, 'Could not get VMs list to find VM for testing'); + } + } catch (error) { + printResult('proxmox_get_vm_status', false, `Failed to get VM info for testing: ${error.message}`); + } + + // Print summary + printHeader('Test Summary'); + + console.log(`${colors.green}Passed: ${results.passed.length}${colors.reset}`); + results.passed.forEach(r => console.log(` โœ“ ${r.tool}`)); + + if (results.failed.length > 0) { + console.log(`\n${colors.red}Failed: ${results.failed.length}${colors.reset}`); + results.failed.forEach(r => console.log(` โœ— ${r.tool}: ${r.message}`)); + } + + console.log(`\n${colors.cyan}Total: ${results.passed.length}/${results.passed.length + results.failed.length} passed${colors.reset}\n`); + + // Exit with error code if any tests failed + process.exit(results.failed.length > 0 ? 1 : 0); +} + +// Run the tests +runTests().catch(error => { + console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`); + console.error(error.stack); + process.exit(1); +}); diff --git a/test-workflows.js b/test-workflows.js new file mode 100755 index 0000000..7d92b10 --- /dev/null +++ b/test-workflows.js @@ -0,0 +1,948 @@ +#!/usr/bin/env node + +/** + * Comprehensive workflow test script for Proxmox MCP Server + * Tests complete lifecycle workflows for VMs, containers, networking, disks, snapshots, and backups + * + * Usage: + * node test-workflows.js [options] + * + * Options: + * --dry-run Show what would be done without executing + * --workflow=NAME Run specific workflow (lxc, vm, network, disk, snapshot, backup, all) + * --no-cleanup Skip cleanup after tests + * --interactive Prompt before each destructive operation + */ + +import { spawn } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import readline from 'readline'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ANSI color codes +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + magenta: '\x1b[35m' +}; + +// Configuration +const config = { + dryRun: process.argv.includes('--dry-run'), + interactive: process.argv.includes('--interactive'), + noCleanup: process.argv.includes('--no-cleanup'), + workflow: process.argv.find(arg => arg.startsWith('--workflow='))?.split('=')[1] || 'all', + testNode: null, // Will be auto-detected + testVmid: null, // Will be generated + testResources: [] // Track resources for cleanup +}; + +// Test results +const results = { + passed: [], + failed: [], + skipped: [] +}; + +// Create readline interface for interactive prompts +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +function log(message, color = 'reset') { + const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; + console.log(`${colors.dim}[${timestamp}]${colors.reset} ${colors[color]}${message}${colors.reset}`); +} + +function logSection(title) { + console.log(`\n${colors.cyan}${'โ•'.repeat(70)}${colors.reset}`); + console.log(`${colors.cyan}${colors.bright} ${title}${colors.reset}`); + console.log(`${colors.cyan}${'โ•'.repeat(70)}${colors.reset}\n`); +} + +function logStep(step, detail = '') { + console.log(`${colors.blue}โ–ถ${colors.reset} ${colors.bright}${step}${colors.reset}`); + if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`); +} + +function logSuccess(message) { + console.log(` ${colors.green}โœ“ ${message}${colors.reset}`); +} + +function logError(message) { + console.log(` ${colors.red}โœ— ${message}${colors.reset}`); +} + +function logWarning(message) { + console.log(` ${colors.yellow}โš  ${message}${colors.reset}`); +} + +async function prompt(question) { + return new Promise((resolve) => { + rl.question(`${colors.yellow}${question} (y/n): ${colors.reset}`, (answer) => { + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// Call MCP tool +async function callTool(toolName, args = {}) { + if (config.dryRun) { + log(`[DRY-RUN] Would call: ${toolName} with ${JSON.stringify(args)}`, 'dim'); + return { success: true, dryRun: true, content: 'Dry run - no actual call made' }; + } + + return new Promise((resolve, reject) => { + const request = { + jsonrpc: '2.0', + id: Date.now(), + method: 'tools/call', + params: { + name: toolName, + arguments: args + } + }; + + const serverProcess = spawn('node', [path.join(__dirname, 'index.js')], { + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + serverProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + serverProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + serverProcess.on('close', (code) => { + try { + const lines = stdout.split('\n'); + let response = null; + for (const line of lines) { + if (line.trim().startsWith('{')) { + try { + response = JSON.parse(line); + break; + } catch (e) { + // Not JSON, continue + } + } + } + + if (response) { + if (response.error) { + resolve({ + success: false, + error: response.error.message, + errorCode: response.error.code + }); + } else if (response.result && response.result.content) { + const content = response.result.content[0]; + // Check if it's an error message about permissions + if (content.text && content.text.includes('Requires Elevated Permissions')) { + resolve({ + success: false, + error: 'Elevated permissions required', + content: content.text + }); + } else { + resolve({ + success: true, + content: content.text || '', + isError: response.result.isError || false + }); + } + } else { + resolve({ + success: false, + error: 'No content in response' + }); + } + } else { + reject(new Error(`No JSON response. stdout: ${stdout.substring(0, 200)}`)); + } + } catch (error) { + reject(error); + } + }); + + serverProcess.on('error', reject); + serverProcess.stdin.write(JSON.stringify(request) + '\n'); + serverProcess.stdin.end(); + }); +} + +// Track resource for cleanup +function trackResource(type, node, vmid) { + config.testResources.push({ type, node, vmid }); + log(`Tracking resource for cleanup: ${type} ${vmid} on ${node}`, 'dim'); +} + +// Record test result +function recordResult(workflow, testName, success, message, details = null) { + const result = { workflow, test: testName, message, details }; + if (success) { + results.passed.push(result); + logSuccess(`${testName}: ${message}`); + } else { + results.failed.push(result); + logError(`${testName}: ${message}`); + } +} + +// ============================================================================ +// WORKFLOW TESTS +// ============================================================================ + +async function testLXCWorkflow() { + logSection('LXC Container Complete Workflow'); + + try { + // Step 1: Get next available VMID + logStep('Step 1: Get next available VMID'); + const vmidResult = await callTool('proxmox_get_next_vmid'); + if (!vmidResult.success) { + recordResult('lxc', 'Get VMID', false, vmidResult.error); + return; + } + const vmidMatch = vmidResult.content.match(/\d{3,}/); + if (!vmidMatch) { + recordResult('lxc', 'Get VMID', false, 'Could not parse VMID from response'); + return; + } + config.testVmid = vmidMatch[0]; + logSuccess(`Got VMID: ${config.testVmid}`); + + // Step 2: List available templates + logStep('Step 2: List available templates', `Node: ${config.testNode}`); + const templatesResult = await callTool('proxmox_list_templates', { + node: config.testNode, + storage: 'local' + }); + if (!templatesResult.success) { + recordResult('lxc', 'List Templates', false, templatesResult.error); + return; + } + logSuccess('Templates listed successfully'); + + // Try to find a Debian template - handles markdown format: **local:vztmpl/debian-...** + const templateMatch = templatesResult.content.match(/\*\*local:vztmpl\/(debian-\d+-standard[^\s*]+)\*\*/i) || + templatesResult.content.match(/\*\*local:vztmpl\/([^\s*]+\.tar\.[gxz]+)\*\*/i) || + templatesResult.content.match(/\*\*local:vztmpl\/(debian[^\s*]+)\*\*/i) || + templatesResult.content.match(/\*\*local:vztmpl\/([^\s*]+)\*\*/i); + + if (!templateMatch) { + recordResult('lxc', 'Find Template', false, 'No suitable template found', templatesResult.content); + return; + } + const template = `local:vztmpl/${templateMatch[1]}`; + logSuccess(`Found template: ${template}`); + + // Step 3: Create LXC container + if (config.interactive) { + const proceed = await prompt(`Create LXC container ${config.testVmid}?`); + if (!proceed) { + recordResult('lxc', 'Create Container', false, 'Skipped by user'); + return; + } + } + + logStep('Step 3: Create LXC container', `VMID: ${config.testVmid}, Template: ${template}`); + const createResult = await callTool('proxmox_create_lxc', { + node: config.testNode, + vmid: config.testVmid, + ostemplate: template, + hostname: `test-mcp-${config.testVmid}`, + password: 'Test123!@#', + memory: 512, + storage: 'local-lvm', + rootfs: '4' + }); + + if (!createResult.success) { + recordResult('lxc', 'Create Container', false, createResult.error || 'Creation failed', createResult.content); + return; + } + trackResource('lxc', config.testNode, config.testVmid); + recordResult('lxc', 'Create Container', true, `Container ${config.testVmid} created`); + await sleep(2000); // Wait for creation to complete + + // Step 4: Start container + logStep('Step 4: Start LXC container', `VMID: ${config.testVmid}`); + const startResult = await callTool('proxmox_start_lxc', { + node: config.testNode, + vmid: config.testVmid + }); + + if (!startResult.success) { + recordResult('lxc', 'Start Container', false, startResult.error); + } else { + recordResult('lxc', 'Start Container', true, `Container ${config.testVmid} started`); + await sleep(3000); // Wait for startup + } + + // Step 5: Check status + logStep('Step 5: Check container status'); + const statusResult = await callTool('proxmox_get_vm_status', { + node: config.testNode, + vmid: config.testVmid, + type: 'lxc' + }); + + if (!statusResult.success) { + recordResult('lxc', 'Check Status', false, statusResult.error); + } else { + const isRunning = statusResult.content.toLowerCase().includes('running'); + recordResult('lxc', 'Check Status', isRunning, + isRunning ? 'Container is running' : 'Container is not running', + statusResult.content.substring(0, 200)); + } + + // Step 6: Create snapshot + logStep('Step 6: Create snapshot', `Name: test-snapshot`); + const snapshotResult = await callTool('proxmox_create_snapshot_lxc', { + node: config.testNode, + vmid: config.testVmid, + snapname: 'test-snapshot' + }); + + if (!snapshotResult.success) { + recordResult('lxc', 'Create Snapshot', false, snapshotResult.error); + } else { + recordResult('lxc', 'Create Snapshot', true, 'Snapshot created'); + await sleep(5000); // Increased wait time for Proxmox to register snapshot + } + + // Step 7: List snapshots (with retry logic) + logStep('Step 7: List snapshots'); + + let hasSnapshot = false; + let listSuccess = false; + for (let attempt = 1; attempt <= 3; attempt++) { + const listSnapshotsResult = await callTool('proxmox_list_snapshots_lxc', { + node: config.testNode, + vmid: config.testVmid + }); + + if (!listSnapshotsResult.success) { + if (attempt === 3) { + recordResult('lxc', 'List Snapshots', false, listSnapshotsResult.error); + } + break; + } + + hasSnapshot = listSnapshotsResult.content.includes('test-snapshot'); + if (hasSnapshot) { + listSuccess = true; + recordResult('lxc', 'List Snapshots', true, 'Snapshot found in list'); + break; + } + + if (attempt < 3) { + logWarning(`Snapshot not found yet, retrying in 2 seconds... (attempt ${attempt}/3)`); + await sleep(2000); + } + } + + if (listSuccess === false && hasSnapshot === false) { + recordResult('lxc', 'List Snapshots', false, 'Snapshot not found in list after 3 attempts'); + } + + // Step 8: Stop container + logStep('Step 8: Stop LXC container'); + const stopResult = await callTool('proxmox_stop_lxc', { + node: config.testNode, + vmid: config.testVmid + }); + + if (!stopResult.success) { + recordResult('lxc', 'Stop Container', false, stopResult.error); + } else { + recordResult('lxc', 'Stop Container', true, `Container ${config.testVmid} stopped`); + await sleep(3000); + } + + // Step 9: Delete snapshot + logStep('Step 9: Delete snapshot'); + const deleteSnapshotResult = await callTool('proxmox_delete_snapshot_lxc', { + node: config.testNode, + vmid: config.testVmid, + snapname: 'test-snapshot' + }); + + if (!deleteSnapshotResult.success) { + recordResult('lxc', 'Delete Snapshot', false, deleteSnapshotResult.error); + } else { + recordResult('lxc', 'Delete Snapshot', true, 'Snapshot deleted'); + await sleep(2000); + } + + } catch (error) { + recordResult('lxc', 'Workflow', false, `Exception: ${error.message}`); + } +} + +async function testVMLifecycleWorkflow() { + logSection('VM Lifecycle Operations Workflow'); + + try { + // Find an existing VM to test with + logStep('Step 1: Find existing VM for testing'); + const vmsResult = await callTool('proxmox_get_vms'); + + if (!vmsResult.success) { + recordResult('vm-lifecycle', 'Find VM', false, vmsResult.error); + return; + } + + // Try to find a stopped VM, or any VM - handles format: (ID: 100) ... โ€ข Node: pve1 ... โ€ข Status: running + const vmMatch = vmsResult.content.match(/\(ID:\s*(\d+)\).*?[โ€ข\s]*Node:\s*(\S+).*?[โ€ข\s]*Status:\s*(\S+)/is); + + if (!vmMatch) { + recordResult('vm-lifecycle', 'Find VM', false, 'No VMs found for testing'); + return; + } + + const existingVmid = vmMatch[1]; + const existingNode = vmMatch[2]; + const existingStatus = vmMatch[3]; + + logSuccess(`Found VM ${existingVmid} on ${existingNode} (status: ${existingStatus})`); + recordResult('vm-lifecycle', 'Find VM', true, `Testing with VM ${existingVmid}`); + + // Determine VM type + logStep('Step 2: Detect VM type'); + let vmType = 'lxc'; + if (vmsResult.content.includes(`ID: ${existingVmid}`) && vmsResult.content.includes('QEMU')) { + vmType = 'qemu'; + } + logSuccess(`VM type: ${vmType}`); + + // Step 3: Get detailed status + logStep('Step 3: Get VM status'); + const statusResult = await callTool('proxmox_get_vm_status', { + node: existingNode, + vmid: existingVmid, + type: vmType + }); + + if (!statusResult.success) { + recordResult('vm-lifecycle', 'Get Status', false, statusResult.error); + } else { + recordResult('vm-lifecycle', 'Get Status', true, 'Status retrieved successfully'); + } + + // Test start/stop based on current state + if (existingStatus.toLowerCase().includes('stopped')) { + logStep('Step 4: Start VM (currently stopped)'); + const startTool = vmType === 'lxc' ? 'proxmox_start_lxc' : 'proxmox_start_vm'; + const startResult = await callTool(startTool, { + node: existingNode, + vmid: existingVmid + }); + + recordResult('vm-lifecycle', 'Start VM', startResult.success, + startResult.success ? 'VM started' : startResult.error); + + if (startResult.success) { + await sleep(3000); + + // Now test reboot + logStep('Step 5: Reboot VM'); + const rebootTool = vmType === 'lxc' ? 'proxmox_reboot_lxc' : 'proxmox_reboot_vm'; + const rebootResult = await callTool(rebootTool, { + node: existingNode, + vmid: existingVmid + }); + + recordResult('vm-lifecycle', 'Reboot VM', rebootResult.success, + rebootResult.success ? 'VM rebooted' : rebootResult.error); + } + } else if (existingStatus.toLowerCase().includes('running')) { + logStep('Step 4: VM already running, testing reboot'); + const rebootTool = vmType === 'lxc' ? 'proxmox_reboot_lxc' : 'proxmox_reboot_vm'; + const rebootResult = await callTool(rebootTool, { + node: existingNode, + vmid: existingVmid + }); + + recordResult('vm-lifecycle', 'Reboot VM', rebootResult.success, + rebootResult.success ? 'VM rebooted' : rebootResult.error); + } + + } catch (error) { + recordResult('vm-lifecycle', 'Workflow', false, `Exception: ${error.message}`); + } +} + +async function testNetworkWorkflow() { + logSection('Network Management Workflow'); + + try { + // Find an existing stopped VM to test network operations + logStep('Step 1: Find stopped VM for network testing'); + const vmsResult = await callTool('proxmox_get_vms'); + + if (!vmsResult.success) { + recordResult('network', 'Find VM', false, vmsResult.error); + return; + } + + const vmMatch = vmsResult.content.match(/\(ID:\s*(\d+)\).*?[โ€ข\s]*Node:\s*(\S+).*?[โ€ข\s]*Status:\s*stopped/is); + + if (!vmMatch) { + logWarning('No stopped VMs found, skipping network workflow (VM must be stopped)'); + recordResult('network', 'Find VM', false, 'No stopped VMs available'); + return; + } + + const vmid = vmMatch[1]; + const node = vmMatch[2]; + + logSuccess(`Using VM ${vmid} on ${node}`); + + // Determine VM type + let vmType = vmsResult.content.includes('QEMU') ? 'qemu' : 'lxc'; + + // Step 2: Add network interface + logStep('Step 2: Add network interface', 'Bridge: vmbr0, Interface: net1'); + const addTool = vmType === 'qemu' ? 'proxmox_add_network_vm' : 'proxmox_add_network_lxc'; + const addResult = await callTool(addTool, { + node: node, + vmid: vmid, + net: 'net1', + bridge: 'vmbr0', + firewall: true + }); + + if (!addResult.success) { + recordResult('network', 'Add Interface', false, addResult.error); + return; + } + recordResult('network', 'Add Interface', true, 'Network interface net1 added'); + + // Step 3: Update network interface + logStep('Step 3: Update network interface', 'Add rate limit'); + const updateTool = vmType === 'qemu' ? 'proxmox_update_network_vm' : 'proxmox_update_network_lxc'; + const updateResult = await callTool(updateTool, { + node: node, + vmid: vmid, + net: 'net1', + rate: 100 + }); + + recordResult('network', 'Update Interface', updateResult.success, + updateResult.success ? 'Network interface updated' : updateResult.error); + + // Step 4: Remove network interface + logStep('Step 4: Remove network interface'); + const removeTool = vmType === 'qemu' ? 'proxmox_remove_network_vm' : 'proxmox_remove_network_lxc'; + const removeResult = await callTool(removeTool, { + node: node, + vmid: vmid, + net: 'net1' + }); + + recordResult('network', 'Remove Interface', removeResult.success, + removeResult.success ? 'Network interface removed' : removeResult.error); + + } catch (error) { + recordResult('network', 'Workflow', false, `Exception: ${error.message}`); + } +} + +async function testDiskWorkflow() { + logSection('Disk Management Workflow'); + + try { + logStep('Step 1: Find VM for disk testing'); + const vmsResult = await callTool('proxmox_get_vms'); + + if (!vmsResult.success) { + recordResult('disk', 'Find VM', false, vmsResult.error); + return; + } + + // Find a QEMU VM (disk operations are more reliable on QEMU) - handles format: (ID: 100) ... โ€ข Node: pve1 ... โ€ข Type: QEMU + const vmMatch = vmsResult.content.match(/\(ID:\s*(\d+)\).*?[โ€ข\s]*Node:\s*(\S+).*?[โ€ข\s]*Type:\s*QEMU/is); + + if (!vmMatch) { + logWarning('No QEMU VMs found, skipping disk workflow'); + recordResult('disk', 'Find VM', false, 'No QEMU VMs available'); + return; + } + + const vmid = vmMatch[1]; + const node = vmMatch[2]; + + logSuccess(`Using VM ${vmid} on ${node}`); + + // Step 2: Add disk + logStep('Step 2: Add disk to VM', 'Size: 10G, Storage: local-lvm'); + const addResult = await callTool('proxmox_add_disk_vm', { + node: node, + vmid: vmid, + disk: 'scsi1', + size: '10G', + storage: 'local-lvm' + }); + + if (!addResult.success) { + recordResult('disk', 'Add Disk', false, addResult.error); + return; + } + recordResult('disk', 'Add Disk', true, 'Disk added successfully'); + + // Step 3: Resize disk + logStep('Step 3: Resize disk', 'New size: +2G'); + const resizeResult = await callTool('proxmox_resize_disk_vm', { + node: node, + vmid: vmid, + disk: 'scsi1', + size: '+2G' + }); + + recordResult('disk', 'Resize Disk', resizeResult.success, + resizeResult.success ? 'Disk resized' : resizeResult.error); + + // Step 4: Remove disk (cleanup) + logStep('Step 4: Remove disk'); + const removeResult = await callTool('proxmox_remove_disk_vm', { + node: node, + vmid: vmid, + disk: 'scsi1' + }); + + recordResult('disk', 'Remove Disk', removeResult.success, + removeResult.success ? 'Disk removed' : removeResult.error); + + } catch (error) { + recordResult('disk', 'Workflow', false, `Exception: ${error.message}`); + } +} + +async function testSnapshotWorkflow() { + logSection('Snapshot Workflow'); + + try { + logStep('Step 1: Find VM for snapshot testing'); + const vmsResult = await callTool('proxmox_get_vms'); + + if (!vmsResult.success) { + recordResult('snapshot', 'Find VM', false, vmsResult.error); + return; + } + + const vmMatch = vmsResult.content.match(/\(ID:\s*(\d+)\).*?[โ€ข\s]*Node:\s*(\S+)/is); + + if (!vmMatch) { + recordResult('snapshot', 'Find VM', false, 'No VMs found'); + return; + } + + const vmid = vmMatch[1]; + const node = vmMatch[2]; + const vmType = vmsResult.content.includes('QEMU') ? 'qemu' : 'lxc'; + + logSuccess(`Using ${vmType} ${vmid} on ${node}`); + + const snapname = `test-snap-${Date.now()}`; + + // Step 2: Create snapshot + logStep('Step 2: Create snapshot', `Name: ${snapname}`); + const createTool = vmType === 'qemu' ? 'proxmox_create_snapshot_vm' : 'proxmox_create_snapshot_lxc'; + const createResult = await callTool(createTool, { + node: node, + vmid: vmid, + snapname: snapname + }); + + if (!createResult.success) { + recordResult('snapshot', 'Create Snapshot', false, createResult.error); + return; + } + recordResult('snapshot', 'Create Snapshot', true, `Snapshot ${snapname} created`); + await sleep(5000); // Increased wait time for Proxmox to register snapshot + + // Step 3: List snapshots (with retry logic) + logStep('Step 3: List snapshots'); + const listTool = vmType === 'qemu' ? 'proxmox_list_snapshots_vm' : 'proxmox_list_snapshots_lxc'; + + let hasSnapshot = false; + let listSuccess = false; + for (let attempt = 1; attempt <= 3; attempt++) { + const listResult = await callTool(listTool, { + node: node, + vmid: vmid + }); + + if (!listResult.success) { + if (attempt === 3) { + recordResult('snapshot', 'List Snapshots', false, listResult.error); + } + break; + } + + hasSnapshot = listResult.content.includes(snapname); + if (hasSnapshot) { + listSuccess = true; + recordResult('snapshot', 'List Snapshots', true, `Snapshot ${snapname} found`); + break; + } + + if (attempt < 3) { + logWarning(`Snapshot not found yet, retrying in 2 seconds... (attempt ${attempt}/3)`); + await sleep(2000); + } + } + + if (listSuccess === false && hasSnapshot === false) { + recordResult('snapshot', 'List Snapshots', false, 'Snapshot not found in list after 3 attempts'); + } + + // Step 4: Delete snapshot + logStep('Step 4: Delete snapshot'); + const deleteTool = vmType === 'qemu' ? 'proxmox_delete_snapshot_vm' : 'proxmox_delete_snapshot_lxc'; + const deleteResult = await callTool(deleteTool, { + node: node, + vmid: vmid, + snapname: snapname + }); + + recordResult('snapshot', 'Delete Snapshot', deleteResult.success, + deleteResult.success ? 'Snapshot deleted' : deleteResult.error); + + } catch (error) { + recordResult('snapshot', 'Workflow', false, `Exception: ${error.message}`); + } +} + +async function testBackupWorkflow() { + logSection('Backup Workflow'); + + try { + logStep('Step 1: Find VM for backup testing'); + const vmsResult = await callTool('proxmox_get_vms'); + + if (!vmsResult.success) { + recordResult('backup', 'Find VM', false, vmsResult.error); + return; + } + + const vmMatch = vmsResult.content.match(/\(ID:\s*(\d+)\).*?[โ€ข\s]*Node:\s*(\S+)/is); + + if (!vmMatch) { + recordResult('backup', 'Find VM', false, 'No VMs found'); + return; + } + + const vmid = vmMatch[1]; + const node = vmMatch[2]; + const vmType = vmsResult.content.includes('QEMU') ? 'qemu' : 'lxc'; + + logSuccess(`Using ${vmType} ${vmid} on ${node}`); + + // Step 2: Create backup + logStep('Step 2: Create backup', 'Storage: local'); + const createTool = vmType === 'qemu' ? 'proxmox_create_backup_vm' : 'proxmox_create_backup_lxc'; + const createResult = await callTool(createTool, { + node: node, + vmid: vmid, + storage: 'local' + }); + + if (!createResult.success) { + recordResult('backup', 'Create Backup', false, createResult.error); + return; + } + recordResult('backup', 'Create Backup', true, 'Backup job started'); + + logWarning('Backup runs in background. Waiting 5 seconds before listing...'); + await sleep(5000); + + // Step 3: List backups + logStep('Step 3: List backups'); + const listResult = await callTool('proxmox_list_backups', { + node: node + }); + + if (!listResult.success) { + recordResult('backup', 'List Backups', false, listResult.error); + } else { + recordResult('backup', 'List Backups', true, 'Backups listed successfully'); + } + + logWarning('Note: Backup deletion requires the backup filename from the list above'); + + } catch (error) { + recordResult('backup', 'Workflow', false, `Exception: ${error.message}`); + } +} + +// ============================================================================ +// CLEANUP +// ============================================================================ + +async function cleanup() { + if (config.noCleanup || config.dryRun) { + logWarning('Cleanup skipped'); + return; + } + + if (config.testResources.length === 0) { + log('No resources to clean up', 'dim'); + return; + } + + logSection('Cleanup'); + + for (const resource of config.testResources) { + logStep(`Deleting ${resource.type} ${resource.vmid} on ${resource.node}`); + + const deleteTool = resource.type === 'lxc' ? 'proxmox_delete_lxc' : 'proxmox_delete_vm'; + const deleteResult = await callTool(deleteTool, { + node: resource.node, + vmid: resource.vmid + }); + + if (deleteResult.success) { + logSuccess(`Deleted ${resource.type} ${resource.vmid}`); + } else { + logError(`Failed to delete ${resource.type} ${resource.vmid}: ${deleteResult.error}`); + } + } +} + +// ============================================================================ +// MAIN +// ============================================================================ + +async function main() { + console.log(`${colors.bright}${colors.magenta}`); + console.log('โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); + console.log('โ•‘ Proxmox MCP Server - Workflow Test Suite โ•‘'); + console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log(colors.reset); + + // Check for elevated permissions + log('Checking environment...', 'cyan'); + + if (config.dryRun) { + logWarning('DRY-RUN MODE: No actual operations will be performed'); + } + + if (config.interactive) { + log('Interactive mode enabled', 'yellow'); + } + + // Get test node + log('Auto-detecting node...', 'cyan'); + const nodesResult = await callTool('proxmox_get_nodes'); + if (!nodesResult.success) { + logError('Failed to get nodes. Ensure .env is configured and server is accessible.'); + process.exit(1); + } + + // Handle node name format: ๐ŸŸข **pve1** + const nodeMatch = nodesResult.content.match(/\*\*([a-zA-Z0-9-]+)\*\*/i) || + nodesResult.content.match(/Node:\s*(\S+)/i) || + nodesResult.content.match(/^([a-zA-Z0-9-]+)/m); + + if (!nodeMatch) { + logError('Could not detect node name'); + process.exit(1); + } + + config.testNode = nodeMatch[1].replace(/[^a-zA-Z0-9-]/g, ''); + logSuccess(`Using node: ${config.testNode}`); + + // Run workflows + const workflows = { + lxc: testLXCWorkflow, + vm: testVMLifecycleWorkflow, + network: testNetworkWorkflow, + disk: testDiskWorkflow, + snapshot: testSnapshotWorkflow, + backup: testBackupWorkflow + }; + + if (config.workflow === 'all') { + for (const [name, func] of Object.entries(workflows)) { + await func(); + } + } else if (workflows[config.workflow]) { + await workflows[config.workflow](); + } else { + logError(`Unknown workflow: ${config.workflow}`); + log(`Available workflows: ${Object.keys(workflows).join(', ')}, all`, 'yellow'); + process.exit(1); + } + + // Cleanup + await cleanup(); + + // Print summary + logSection('Test Summary'); + + console.log(`${colors.green}โœ“ Passed: ${results.passed.length}${colors.reset}`); + results.passed.forEach(r => { + console.log(` ${colors.dim}[${r.workflow}]${colors.reset} ${r.test}`); + }); + + if (results.failed.length > 0) { + console.log(`\n${colors.red}โœ— Failed: ${results.failed.length}${colors.reset}`); + results.failed.forEach(r => { + console.log(` ${colors.dim}[${r.workflow}]${colors.reset} ${r.test}: ${r.message}`); + }); + } + + if (results.skipped.length > 0) { + console.log(`\n${colors.yellow}โŠ˜ Skipped: ${results.skipped.length}${colors.reset}`); + } + + const total = results.passed.length + results.failed.length; + const passRate = total > 0 ? ((results.passed.length / total) * 100).toFixed(1) : 0; + + console.log(`\n${colors.cyan}${colors.bright}Total: ${results.passed.length}/${total} passed (${passRate}%)${colors.reset}\n`); + + rl.close(); + process.exit(results.failed.length > 0 ? 1 : 0); +} + +// Handle errors +process.on('unhandledRejection', (error) => { + logError(`Unhandled error: ${error.message}`); + console.error(error); + rl.close(); + process.exit(1); +}); + +// Run +main().catch(error => { + logError(`Fatal error: ${error.message}`); + console.error(error); + rl.close(); + process.exit(1); +});