add console and serial port mixins, expand to 94 tools
New features: - ConsoleMixin: vm_screenshot, wait_for_vm_tools, get_vm_tools_status - SerialPortMixin: setup_serial_port, get_serial_port, connect_serial_port, clear_serial_port, remove_serial_port Enables: - VM console screenshots via vSphere HTTP API - VMware Tools status monitoring and wait utilities - Network serial ports for headless VMs and network appliances README rewritten with comprehensive documentation of all 94 tools.
This commit is contained in:
parent
d771db2e1d
commit
8e28b84e59
437
README.md
437
README.md
@ -1,188 +1,345 @@
|
||||
# ESXi MCP Server
|
||||
|
||||
A VMware ESXi/vCenter management server based on MCP (Model Control Protocol), providing simple REST API interfaces for virtual machine management.
|
||||
A comprehensive VMware vSphere management server implementing the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), enabling AI assistants like Claude to manage virtual infrastructure through natural language.
|
||||
|
||||
## Why ESXi MCP Server?
|
||||
|
||||
Traditional VMware management requires navigating complex UIs or writing scripts. With ESXi MCP Server, you can simply ask:
|
||||
|
||||
> "Create a new VM with 4 CPUs and 8GB RAM, then take a snapshot before installing the OS"
|
||||
|
||||
And watch it happen. The server exposes **94 tools** covering every aspect of vSphere management.
|
||||
|
||||
## Features
|
||||
|
||||
- Support for ESXi and vCenter Server connections
|
||||
- Real-time communication based on SSE (Server-Sent Events)
|
||||
- RESTful API interface with JSON-RPC support
|
||||
- API key authentication
|
||||
- Complete virtual machine lifecycle management
|
||||
- Real-time performance monitoring
|
||||
- SSL/TLS secure connection support
|
||||
- Flexible configuration options (YAML/JSON/Environment Variables)
|
||||
|
||||
## Core Functions
|
||||
|
||||
- Virtual Machine Management
|
||||
- Create VM
|
||||
- Clone VM
|
||||
- Delete VM
|
||||
- Power On/Off operations
|
||||
- List all VMs
|
||||
- Performance Monitoring
|
||||
- CPU usage
|
||||
- Memory usage
|
||||
- Storage usage
|
||||
- Network traffic statistics
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- pyVmomi
|
||||
- PyYAML
|
||||
- uvicorn
|
||||
- mcp-core (Machine Control Protocol core library)
|
||||
- **94 MCP Tools** - Complete vSphere management capabilities
|
||||
- **6 MCP Resources** - Real-time access to VMs, hosts, datastores, networks, and clusters
|
||||
- **Modular Architecture** - 13 specialized mixins organized by function
|
||||
- **Full vCenter & ESXi Support** - Works with standalone hosts or vCenter Server
|
||||
- **Guest Operations** - Execute commands, transfer files inside VMs via VMware Tools
|
||||
- **Serial Console Access** - Network serial ports for headless VMs and network appliances
|
||||
- **VM Screenshots** - Capture console screenshots for monitoring or documentation
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Install dependencies:
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
pip install pyvmomi pyyaml uvicorn mcp-core
|
||||
# Install with uv (recommended)
|
||||
uvx esxi-mcp-server
|
||||
|
||||
# Or install with pip
|
||||
pip install esxi-mcp-server
|
||||
```
|
||||
|
||||
2. Create configuration file `config.yaml`:
|
||||
### Configuration
|
||||
|
||||
```yaml
|
||||
vcenter_host: "your-vcenter-ip"
|
||||
vcenter_user: "administrator@vsphere.local"
|
||||
vcenter_password: "your-password"
|
||||
datacenter: "your-datacenter" # Optional
|
||||
cluster: "your-cluster" # Optional
|
||||
datastore: "your-datastore" # Optional
|
||||
network: "VM Network" # Optional
|
||||
insecure: true # Skip SSL certificate verification
|
||||
api_key: "your-api-key" # API access key
|
||||
log_file: "./logs/vmware_mcp.log" # Log file path
|
||||
log_level: "INFO" # Log level
|
||||
```
|
||||
|
||||
3. Run the server:
|
||||
Create a `.env` file or set environment variables:
|
||||
|
||||
```bash
|
||||
python server.py -c config.yaml
|
||||
VCENTER_HOST=vcenter.example.com
|
||||
VCENTER_USER=administrator@vsphere.local
|
||||
VCENTER_PASSWORD=your-password
|
||||
VCENTER_INSECURE=true # Skip SSL verification (dev only)
|
||||
```
|
||||
|
||||
## API Interface
|
||||
### Run the Server
|
||||
|
||||
### Authentication
|
||||
```bash
|
||||
# Using uvx
|
||||
uvx esxi-mcp-server
|
||||
|
||||
All privileged operations require authentication first:
|
||||
|
||||
```http
|
||||
POST /sse/messages
|
||||
Authorization: Bearer your-api-key
|
||||
# Or if installed
|
||||
esxi-mcp-server
|
||||
```
|
||||
|
||||
### Main Tool Interfaces
|
||||
### Add to Claude Code
|
||||
|
||||
1. Create VM
|
||||
```json
|
||||
{
|
||||
"name": "vm-name",
|
||||
"cpu": 2,
|
||||
"memory": 4096,
|
||||
"datastore": "datastore-name",
|
||||
"network": "network-name"
|
||||
}
|
||||
```bash
|
||||
claude mcp add esxi "uvx esxi-mcp-server"
|
||||
```
|
||||
|
||||
2. Clone VM
|
||||
```json
|
||||
{
|
||||
"template_name": "source-vm",
|
||||
"new_name": "new-vm-name"
|
||||
}
|
||||
## Available Tools (94 Total)
|
||||
|
||||
### VM Lifecycle (6 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_vms` | List all virtual machines |
|
||||
| `get_vm_info` | Get detailed VM information |
|
||||
| `create_vm` | Create a new virtual machine |
|
||||
| `clone_vm` | Clone from template or existing VM |
|
||||
| `delete_vm` | Delete a virtual machine |
|
||||
| `reconfigure_vm` | Modify CPU, memory, annotation |
|
||||
| `rename_vm` | Rename a virtual machine |
|
||||
|
||||
### Power Operations (6 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `power_on_vm` | Power on a VM |
|
||||
| `power_off_vm` | Power off a VM (hard) |
|
||||
| `shutdown_guest` | Graceful guest OS shutdown |
|
||||
| `reboot_guest` | Graceful guest OS reboot |
|
||||
| `suspend_vm` | Suspend a VM |
|
||||
| `reset_vm` | Hard reset a VM |
|
||||
|
||||
### Snapshots (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_snapshots` | List all snapshots |
|
||||
| `create_snapshot` | Create a new snapshot |
|
||||
| `revert_to_snapshot` | Revert to a snapshot |
|
||||
| `delete_snapshot` | Delete a snapshot |
|
||||
| `delete_all_snapshots` | Remove all snapshots |
|
||||
|
||||
### Guest Operations (7 tools)
|
||||
*Requires VMware Tools running in the guest*
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_guest_processes` | List processes in guest OS |
|
||||
| `run_command_in_guest` | Execute command in guest |
|
||||
| `read_guest_file` | Read file from guest OS |
|
||||
| `write_guest_file` | Write file to guest OS |
|
||||
| `list_guest_directory` | List directory contents |
|
||||
| `create_guest_directory` | Create directory in guest |
|
||||
| `delete_guest_file` | Delete file or directory |
|
||||
|
||||
### Console & Monitoring (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `vm_screenshot` | Capture VM console screenshot |
|
||||
| `wait_for_vm_tools` | Wait for VMware Tools to be ready |
|
||||
| `get_vm_tools_status` | Get VMware Tools status |
|
||||
| `get_vm_stats` | Get VM performance statistics |
|
||||
| `get_host_stats` | Get host performance statistics |
|
||||
|
||||
### Serial Port Management (5 tools)
|
||||
*For network appliances and headless VMs*
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_serial_port` | Get serial port configuration |
|
||||
| `setup_serial_port` | Configure network serial port |
|
||||
| `connect_serial_port` | Connect/disconnect serial port |
|
||||
| `clear_serial_port` | Reset serial port connection |
|
||||
| `remove_serial_port` | Remove serial port from VM |
|
||||
|
||||
### Disk Management (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_disks` | List VM disks |
|
||||
| `add_disk` | Add a new disk |
|
||||
| `remove_disk` | Remove a disk |
|
||||
| `resize_disk` | Expand disk size |
|
||||
| `get_disk_info` | Get disk details |
|
||||
|
||||
### NIC Management (6 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_nics` | List VM network adapters |
|
||||
| `add_nic` | Add a network adapter |
|
||||
| `remove_nic` | Remove a network adapter |
|
||||
| `connect_nic` | Connect/disconnect NIC |
|
||||
| `change_nic_network` | Change NIC network |
|
||||
| `get_nic_info` | Get NIC details |
|
||||
|
||||
### OVF/OVA Management (5 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `deploy_ovf` | Deploy VM from OVF template |
|
||||
| `export_ovf` | Export VM to OVF |
|
||||
| `list_ovf_networks` | List OVF network mappings |
|
||||
| `upload_to_datastore` | Upload file to datastore |
|
||||
| `download_from_datastore` | Download file from datastore |
|
||||
|
||||
### Host Management (10 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_hosts` | List ESXi hosts |
|
||||
| `get_host_info` | Get host details |
|
||||
| `get_host_hardware` | Get hardware information |
|
||||
| `get_host_networking` | Get network configuration |
|
||||
| `list_services` | List host services |
|
||||
| `get_service_status` | Get service status |
|
||||
| `start_service` | Start a host service |
|
||||
| `stop_service` | Stop a host service |
|
||||
| `restart_service` | Restart a host service |
|
||||
| `get_ntp_config` | Get NTP configuration |
|
||||
|
||||
### Datastore & Resources (8 tools)
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_datastore_info` | Get datastore details |
|
||||
| `browse_datastore` | Browse datastore files |
|
||||
| `get_vcenter_info` | Get vCenter information |
|
||||
| `get_resource_pool_info` | Get resource pool details |
|
||||
| `get_network_info` | Get network details |
|
||||
| `list_templates` | List VM templates |
|
||||
| `get_alarms` | Get active alarms |
|
||||
| `get_recent_events` | Get recent events |
|
||||
|
||||
### vCenter Operations (18 tools)
|
||||
*Available when connected to vCenter Server*
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_folders` | List VM folders |
|
||||
| `create_folder` | Create a folder |
|
||||
| `delete_folder` | Delete a folder |
|
||||
| `move_vm_to_folder` | Move VM to folder |
|
||||
| `list_clusters` | List clusters |
|
||||
| `get_cluster_info` | Get cluster details |
|
||||
| `list_resource_pools` | List resource pools |
|
||||
| `create_resource_pool` | Create resource pool |
|
||||
| `delete_resource_pool` | Delete resource pool |
|
||||
| `move_vm_to_resource_pool` | Move VM to resource pool |
|
||||
| `list_tags` | List tags |
|
||||
| `get_vm_tags` | Get tags on a VM |
|
||||
| `apply_tag_to_vm` | Apply tag to VM |
|
||||
| `remove_tag_from_vm` | Remove tag from VM |
|
||||
| `migrate_vm` | Migrate VM to another host |
|
||||
| `list_recent_tasks` | List recent tasks |
|
||||
| `list_recent_events` | List recent events |
|
||||
| `cancel_task` | Cancel a running task |
|
||||
|
||||
## MCP Resources
|
||||
|
||||
Access real-time data through MCP resources:
|
||||
|
||||
| Resource URI | Description |
|
||||
|--------------|-------------|
|
||||
| `esxi://vms` | All virtual machines |
|
||||
| `esxi://hosts` | All ESXi hosts |
|
||||
| `esxi://datastores` | All datastores |
|
||||
| `esxi://networks` | All networks |
|
||||
| `esxi://clusters` | All clusters |
|
||||
| `esxi://resource-pools` | All resource pools |
|
||||
|
||||
## Architecture
|
||||
|
||||
The server uses a modular mixin architecture:
|
||||
|
||||
```
|
||||
esxi_mcp_server/
|
||||
├── server.py # FastMCP server setup
|
||||
├── connection.py # VMware connection management
|
||||
├── config.py # Settings and configuration
|
||||
└── mixins/
|
||||
├── vm_lifecycle.py # VM CRUD operations
|
||||
├── power_ops.py # Power management
|
||||
├── snapshots.py # Snapshot management
|
||||
├── guest_ops.py # Guest OS operations
|
||||
├── console.py # Screenshots & Tools monitoring
|
||||
├── serial_port.py # Serial console access
|
||||
├── disk_management.py # Disk operations
|
||||
├── nic_management.py # Network adapter operations
|
||||
├── ovf_management.py # OVF/OVA handling
|
||||
├── host_management.py # Host operations
|
||||
├── monitoring.py # Performance monitoring
|
||||
├── resources.py # MCP resources
|
||||
└── vcenter_ops.py # vCenter-specific operations
|
||||
```
|
||||
|
||||
3. Delete VM
|
||||
```json
|
||||
{
|
||||
"name": "vm-name"
|
||||
}
|
||||
## Configuration Options
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `VCENTER_HOST` | vCenter/ESXi hostname or IP | *required* |
|
||||
| `VCENTER_USER` | Username | *required* |
|
||||
| `VCENTER_PASSWORD` | Password | *required* |
|
||||
| `VCENTER_INSECURE` | Skip SSL verification | `false` |
|
||||
| `VCENTER_DATACENTER` | Target datacenter | *auto-detect* |
|
||||
| `VCENTER_CLUSTER` | Target cluster | *auto-detect* |
|
||||
| `VCENTER_DATASTORE` | Default datastore | *auto-detect* |
|
||||
| `VCENTER_NETWORK` | Default network | *auto-detect* |
|
||||
| `MCP_TRANSPORT` | Transport mode (`stdio` or `sse`) | `stdio` |
|
||||
| `LOG_LEVEL` | Logging level | `INFO` |
|
||||
|
||||
## Docker Support
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t esxi-mcp-server .
|
||||
|
||||
# Run
|
||||
docker run -d \
|
||||
-e VCENTER_HOST=vcenter.example.com \
|
||||
-e VCENTER_USER=admin@vsphere.local \
|
||||
-e VCENTER_PASSWORD=secret \
|
||||
esxi-mcp-server
|
||||
```
|
||||
|
||||
4. Power Operations
|
||||
```json
|
||||
{
|
||||
"name": "vm-name"
|
||||
}
|
||||
## Examples
|
||||
|
||||
### Create a VM and Install an OS
|
||||
|
||||
```
|
||||
User: Create a new VM called "web-server" with 4 CPUs, 8GB RAM, and a 100GB disk
|
||||
|
||||
Claude: I'll create that VM for you.
|
||||
[Calls create_vm with name="web-server", cpu=4, memory_mb=8192, disk_gb=100]
|
||||
|
||||
VM 'web-server' created successfully.
|
||||
|
||||
User: Power it on and take a screenshot
|
||||
|
||||
Claude: [Calls power_on_vm, then vm_screenshot]
|
||||
|
||||
The VM is now running. Here's the console screenshot showing the BIOS boot screen.
|
||||
```
|
||||
|
||||
### Resource Monitoring Interface
|
||||
### Guest Operations
|
||||
|
||||
Get VM performance data:
|
||||
```http
|
||||
GET vmstats://{vm_name}
|
||||
```
|
||||
User: Run "uname -a" on the linux-server VM
|
||||
|
||||
Claude: [Calls run_command_in_guest with command="/usr/bin/uname", arguments="-a"]
|
||||
|
||||
The command returned:
|
||||
Linux linux-server 5.15.0-generic #1 SMP x86_64 GNU/Linux
|
||||
```
|
||||
|
||||
## Configuration
|
||||
### Serial Console for Network Appliances
|
||||
|
||||
| Parameter | Description | Required | Default |
|
||||
|-----------|-------------|----------|---------|
|
||||
| vcenter_host | vCenter/ESXi server address | Yes | - |
|
||||
| vcenter_user | Login username | Yes | - |
|
||||
| vcenter_password | Login password | Yes | - |
|
||||
| datacenter | Datacenter name | No | Auto-select first |
|
||||
| cluster | Cluster name | No | Auto-select first |
|
||||
| datastore | Storage name | No | Auto-select largest available |
|
||||
| network | Network name | No | VM Network |
|
||||
| insecure | Skip SSL verification | No | false |
|
||||
| api_key | API access key | No | - |
|
||||
| log_file | Log file path | No | Console output |
|
||||
| log_level | Log level | No | INFO |
|
||||
```
|
||||
User: Set up a serial console on my Cisco router VM
|
||||
|
||||
## Environment Variables
|
||||
Claude: [Calls setup_serial_port with name="cisco-router", protocol="telnet"]
|
||||
|
||||
All configuration items support environment variable settings, following these naming rules:
|
||||
- VCENTER_HOST
|
||||
- VCENTER_USER
|
||||
- VCENTER_PASSWORD
|
||||
- VCENTER_DATACENTER
|
||||
- VCENTER_CLUSTER
|
||||
- VCENTER_DATASTORE
|
||||
- VCENTER_NETWORK
|
||||
- VCENTER_INSECURE
|
||||
- MCP_API_KEY
|
||||
- MCP_LOG_FILE
|
||||
- MCP_LOG_LEVEL
|
||||
Serial port configured. You can connect via:
|
||||
telnet://10.20.0.22:4521
|
||||
```
|
||||
|
||||
## Security Recommendations
|
||||
## Requirements
|
||||
|
||||
1. Production Environment:
|
||||
- Use valid SSL certificates
|
||||
- Enable API key authentication
|
||||
- Set appropriate log levels
|
||||
- Restrict API access scope
|
||||
- Python 3.11+
|
||||
- VMware vSphere 7.0+ (ESXi or vCenter)
|
||||
- VMware Tools (for guest operations)
|
||||
|
||||
2. Testing Environment:
|
||||
- Set insecure: true to skip SSL verification
|
||||
- Use more detailed log level (DEBUG)
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/yourusername/esxi-mcp-server.git
|
||||
cd esxi-mcp-server
|
||||
|
||||
# Install dependencies
|
||||
uv sync
|
||||
|
||||
# Run tests
|
||||
uv run python test_client.py
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
MIT License - See [LICENSE](LICENSE) for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and Pull Requests are welcome!
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.0.1
|
||||
- Initial release
|
||||
- Basic VM management functionality
|
||||
- SSE communication support
|
||||
- API key authentication
|
||||
- Performance monitoring
|
||||
|
||||
## Author
|
||||
|
||||
Bright8192
|
||||
Contributions welcome! Please read the contributing guidelines and submit a PR.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- VMware pyvmomi team
|
||||
- MCP Protocol development team
|
||||
- Built with [FastMCP](https://github.com/jlowin/fastmcp)
|
||||
- Uses [pyVmomi](https://github.com/vmware/pyvmomi) for vSphere API
|
||||
- Inspired by the Model Context Protocol specification
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""MCP Mixins for ESXi operations organized by category."""
|
||||
|
||||
from esxi_mcp_server.mixins.console import ConsoleMixin
|
||||
from esxi_mcp_server.mixins.disk_management import DiskManagementMixin
|
||||
from esxi_mcp_server.mixins.guest_ops import GuestOpsMixin
|
||||
from esxi_mcp_server.mixins.host_management import HostManagementMixin
|
||||
@ -8,20 +9,23 @@ from esxi_mcp_server.mixins.nic_management import NICManagementMixin
|
||||
from esxi_mcp_server.mixins.ovf_management import OVFManagementMixin
|
||||
from esxi_mcp_server.mixins.power_ops import PowerOpsMixin
|
||||
from esxi_mcp_server.mixins.resources import ResourcesMixin
|
||||
from esxi_mcp_server.mixins.serial_port import SerialPortMixin
|
||||
from esxi_mcp_server.mixins.snapshots import SnapshotsMixin
|
||||
from esxi_mcp_server.mixins.vcenter_ops import VCenterOpsMixin
|
||||
from esxi_mcp_server.mixins.vm_lifecycle import VMLifecycleMixin
|
||||
|
||||
__all__ = [
|
||||
"VMLifecycleMixin",
|
||||
"PowerOpsMixin",
|
||||
"SnapshotsMixin",
|
||||
"MonitoringMixin",
|
||||
"GuestOpsMixin",
|
||||
"ResourcesMixin",
|
||||
"ConsoleMixin",
|
||||
"DiskManagementMixin",
|
||||
"GuestOpsMixin",
|
||||
"HostManagementMixin",
|
||||
"MonitoringMixin",
|
||||
"NICManagementMixin",
|
||||
"OVFManagementMixin",
|
||||
"HostManagementMixin",
|
||||
"PowerOpsMixin",
|
||||
"ResourcesMixin",
|
||||
"SerialPortMixin",
|
||||
"SnapshotsMixin",
|
||||
"VCenterOpsMixin",
|
||||
"VMLifecycleMixin",
|
||||
]
|
||||
|
||||
178
src/esxi_mcp_server/mixins/console.py
Normal file
178
src/esxi_mcp_server/mixins/console.py
Normal file
@ -0,0 +1,178 @@
|
||||
"""VM Console operations - screenshots and tools monitoring."""
|
||||
|
||||
import base64
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import requests
|
||||
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
|
||||
from mcp.types import ToolAnnotations
|
||||
from pyVmomi import vim
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esxi_mcp_server.connection import VMwareConnection
|
||||
|
||||
|
||||
class ConsoleMixin(MCPMixin):
|
||||
"""VM console operations - screenshots and VMware Tools monitoring."""
|
||||
|
||||
def __init__(self, conn: "VMwareConnection"):
|
||||
self.conn = conn
|
||||
|
||||
@mcp_tool(
|
||||
name="wait_for_vm_tools",
|
||||
description="Wait for VMware Tools to become available on a VM. Useful after powering on a VM.",
|
||||
annotations=ToolAnnotations(readOnlyHint=True),
|
||||
)
|
||||
def wait_for_vm_tools(
|
||||
self, name: str, timeout: int = 120, poll_interval: int = 5
|
||||
) -> dict[str, Any]:
|
||||
"""Wait for VMware Tools to become available.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
timeout: Maximum seconds to wait (default: 120)
|
||||
poll_interval: Seconds between status checks (default: 5)
|
||||
|
||||
Returns:
|
||||
Dict with tools status, version, and guest info when ready
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
start_time = datetime.now()
|
||||
end_time = start_time + timedelta(seconds=timeout)
|
||||
|
||||
while datetime.now() < end_time:
|
||||
tools_status = vm.guest.toolsStatus if vm.guest else None
|
||||
|
||||
if tools_status == vim.vm.GuestInfo.ToolsStatus.toolsOk:
|
||||
return {
|
||||
"status": "ready",
|
||||
"tools_status": str(tools_status),
|
||||
"tools_version": vm.guest.toolsVersion if vm.guest else None,
|
||||
"tools_running_status": (
|
||||
vm.guest.toolsRunningStatus if vm.guest else None
|
||||
),
|
||||
"ip_address": vm.guest.ipAddress if vm.guest else None,
|
||||
"hostname": vm.guest.hostName if vm.guest else None,
|
||||
"guest_os": vm.guest.guestFullName if vm.guest else None,
|
||||
"wait_time_seconds": (datetime.now() - start_time).total_seconds(),
|
||||
}
|
||||
|
||||
time.sleep(poll_interval)
|
||||
|
||||
# Timeout reached
|
||||
return {
|
||||
"status": "timeout",
|
||||
"tools_status": str(vm.guest.toolsStatus) if vm.guest else None,
|
||||
"message": f"VMware Tools not ready after {timeout} seconds",
|
||||
"wait_time_seconds": timeout,
|
||||
}
|
||||
|
||||
@mcp_tool(
|
||||
name="get_vm_tools_status",
|
||||
description="Get current VMware Tools status for a VM",
|
||||
annotations=ToolAnnotations(readOnlyHint=True),
|
||||
)
|
||||
def get_vm_tools_status(self, name: str) -> dict[str, Any]:
|
||||
"""Get VMware Tools status without waiting.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
|
||||
Returns:
|
||||
Dict with current tools status and guest info
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
return {
|
||||
"tools_status": str(vm.guest.toolsStatus) if vm.guest else None,
|
||||
"tools_version": vm.guest.toolsVersion if vm.guest else None,
|
||||
"tools_running_status": (
|
||||
vm.guest.toolsRunningStatus if vm.guest else None
|
||||
),
|
||||
"tools_version_status": (
|
||||
str(vm.guest.toolsVersionStatus) if vm.guest else None
|
||||
),
|
||||
"ip_address": vm.guest.ipAddress if vm.guest else None,
|
||||
"hostname": vm.guest.hostName if vm.guest else None,
|
||||
"guest_os": vm.guest.guestFullName if vm.guest else None,
|
||||
"guest_id": vm.guest.guestId if vm.guest else None,
|
||||
"guest_state": vm.guest.guestState if vm.guest else None,
|
||||
}
|
||||
|
||||
@mcp_tool(
|
||||
name="vm_screenshot",
|
||||
description="Capture a screenshot of the VM console. Returns base64-encoded PNG image.",
|
||||
annotations=ToolAnnotations(readOnlyHint=True),
|
||||
)
|
||||
def vm_screenshot(
|
||||
self,
|
||||
name: str,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Capture VM console screenshot via vSphere HTTP API.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
width: Optional width to scale the image
|
||||
height: Optional height to scale the image
|
||||
|
||||
Returns:
|
||||
Dict with base64-encoded image data and metadata
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
# Build screenshot URL
|
||||
# Format: https://{host}/screen?id={moid}
|
||||
host = self.conn.settings.vcenter_host
|
||||
moid = vm._moId
|
||||
screenshot_url = f"https://{host}/screen?id={moid}"
|
||||
|
||||
# Add optional scaling parameters
|
||||
params = []
|
||||
if width:
|
||||
params.append(f"w={width}")
|
||||
if height:
|
||||
params.append(f"h={height}")
|
||||
if params:
|
||||
screenshot_url += "&" + "&".join(params)
|
||||
|
||||
# Build auth header
|
||||
username = self.conn.settings.vcenter_user
|
||||
password = self.conn.settings.vcenter_password.get_secret_value()
|
||||
auth = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
|
||||
|
||||
# Make request
|
||||
try:
|
||||
response = requests.get(
|
||||
screenshot_url,
|
||||
headers={"Authorization": f"Basic {auth}"},
|
||||
verify=not self.conn.settings.vcenter_insecure,
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
raise ValueError(f"Failed to capture screenshot: {e}") from e
|
||||
|
||||
# Encode image as base64
|
||||
image_data = base64.b64encode(response.content).decode("ascii")
|
||||
content_type = response.headers.get("Content-Type", "image/png")
|
||||
|
||||
return {
|
||||
"vm_name": name,
|
||||
"moid": moid,
|
||||
"content_type": content_type,
|
||||
"size_bytes": len(response.content),
|
||||
"image_base64": image_data,
|
||||
"width": width,
|
||||
"height": height,
|
||||
}
|
||||
312
src/esxi_mcp_server/mixins/serial_port.py
Normal file
312
src/esxi_mcp_server/mixins/serial_port.py
Normal file
@ -0,0 +1,312 @@
|
||||
"""Serial Port Management - network console access for VMs."""
|
||||
|
||||
import random
|
||||
import socket
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
|
||||
from mcp.types import ToolAnnotations
|
||||
from pyVmomi import vim
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esxi_mcp_server.connection import VMwareConnection
|
||||
|
||||
|
||||
class SerialPortMixin(MCPMixin):
|
||||
"""Serial port management for VM network console access.
|
||||
|
||||
Network serial ports allow telnet/TCP connections to VM consoles,
|
||||
useful for headless VMs, network appliances, or serial console access.
|
||||
|
||||
Supported protocols:
|
||||
- telnet: Telnet over TCP (can negotiate SSL)
|
||||
- telnets: Telnet over SSL over TCP
|
||||
- tcp: Unencrypted TCP
|
||||
- tcp+ssl: Encrypted SSL over TCP
|
||||
"""
|
||||
|
||||
def __init__(self, conn: "VMwareConnection"):
|
||||
self.conn = conn
|
||||
|
||||
def _get_serial_port(self, vm: vim.VirtualMachine) -> vim.vm.device.VirtualSerialPort | None:
|
||||
"""Find existing serial port with URI backing on a VM."""
|
||||
if not vm.config:
|
||||
return None
|
||||
for device in vm.config.hardware.device:
|
||||
if (
|
||||
isinstance(device, vim.vm.device.VirtualSerialPort)
|
||||
and isinstance(device.backing, vim.vm.device.VirtualSerialPort.URIBackingInfo)
|
||||
):
|
||||
return device
|
||||
return None
|
||||
|
||||
def _find_unused_port(self, host_ip: str, start: int = 2000, end: int = 9000) -> int:
|
||||
"""Find an unused TCP port on the ESXi host."""
|
||||
# Try random ports in range until we find one that's available
|
||||
attempts = 0
|
||||
max_attempts = 50
|
||||
while attempts < max_attempts:
|
||||
port = random.randint(start, end)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(1)
|
||||
try:
|
||||
result = sock.connect_ex((host_ip, port))
|
||||
if result != 0: # Port not in use
|
||||
return port
|
||||
except (OSError, TimeoutError):
|
||||
return port # Likely available
|
||||
finally:
|
||||
sock.close()
|
||||
attempts += 1
|
||||
|
||||
raise ValueError(f"Could not find unused port in range {start}-{end}")
|
||||
|
||||
@mcp_tool(
|
||||
name="get_serial_port",
|
||||
description="Get current serial port configuration for a VM",
|
||||
annotations=ToolAnnotations(readOnlyHint=True),
|
||||
)
|
||||
def get_serial_port(self, name: str) -> dict[str, Any]:
|
||||
"""Get serial port configuration.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
|
||||
Returns:
|
||||
Dict with serial port details or message if not configured
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
serial_port = self._get_serial_port(vm)
|
||||
if not serial_port:
|
||||
return {
|
||||
"configured": False,
|
||||
"message": "No network serial port configured",
|
||||
}
|
||||
|
||||
backing = serial_port.backing
|
||||
return {
|
||||
"configured": True,
|
||||
"label": serial_port.deviceInfo.label,
|
||||
"connected": serial_port.connectable.connected if serial_port.connectable else None,
|
||||
"start_connected": serial_port.connectable.startConnected if serial_port.connectable else None,
|
||||
"direction": backing.direction if backing else None,
|
||||
"service_uri": backing.serviceURI if backing else None,
|
||||
"yield_on_poll": serial_port.yieldOnPoll,
|
||||
}
|
||||
|
||||
@mcp_tool(
|
||||
name="setup_serial_port",
|
||||
description="Configure a network serial port on a VM for console access. VM must be powered off.",
|
||||
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True),
|
||||
)
|
||||
def setup_serial_port(
|
||||
self,
|
||||
name: str,
|
||||
protocol: str = "telnet",
|
||||
port: int | None = None,
|
||||
direction: str = "server",
|
||||
yield_on_poll: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Setup or update network serial port.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
protocol: Protocol to use (telnet, telnets, tcp, tcp+ssl). Default: telnet
|
||||
port: TCP port number. If not specified, auto-assigns unused port.
|
||||
direction: 'server' (VM listens) or 'client' (VM connects). Default: server
|
||||
yield_on_poll: Enable CPU yield behavior. Default: True
|
||||
|
||||
Returns:
|
||||
Dict with configured serial port URI and details
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
# Check VM is powered off
|
||||
if vm.runtime.powerState != vim.VirtualMachine.PowerState.poweredOff:
|
||||
raise ValueError(f"VM '{name}' must be powered off to configure serial port")
|
||||
|
||||
# Validate protocol
|
||||
valid_protocols = ["telnet", "telnets", "tcp", "tcp+ssl", "tcp4", "tcp6"]
|
||||
if protocol not in valid_protocols:
|
||||
raise ValueError(f"Invalid protocol '{protocol}'. Must be one of: {valid_protocols}")
|
||||
|
||||
# Validate direction
|
||||
if direction not in ["server", "client"]:
|
||||
raise ValueError("Direction must be 'server' or 'client'")
|
||||
|
||||
# Find or assign port
|
||||
if port is None:
|
||||
host = vm.runtime.host
|
||||
host_ip = host.name if host else self.conn.settings.vcenter_host
|
||||
port = self._find_unused_port(host_ip)
|
||||
|
||||
# Build service URI
|
||||
service_uri = f"{protocol}://:{port}"
|
||||
|
||||
# Build spec
|
||||
serial_spec = vim.vm.device.VirtualDeviceSpec()
|
||||
existing_port = self._get_serial_port(vm)
|
||||
|
||||
if existing_port:
|
||||
# Edit existing
|
||||
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
|
||||
serial_spec.device = existing_port
|
||||
else:
|
||||
# Add new
|
||||
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
|
||||
serial_spec.device = vim.vm.device.VirtualSerialPort()
|
||||
|
||||
# Configure backing
|
||||
serial_spec.device.yieldOnPoll = yield_on_poll
|
||||
serial_spec.device.backing = vim.vm.device.VirtualSerialPort.URIBackingInfo()
|
||||
serial_spec.device.backing.direction = direction
|
||||
serial_spec.device.backing.serviceURI = service_uri
|
||||
|
||||
# Configure connectable
|
||||
serial_spec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo()
|
||||
serial_spec.device.connectable.startConnected = True
|
||||
serial_spec.device.connectable.allowGuestControl = True
|
||||
serial_spec.device.connectable.connected = False # Will connect on power on
|
||||
|
||||
# Apply config
|
||||
spec = vim.vm.ConfigSpec()
|
||||
spec.deviceChange = [serial_spec]
|
||||
task = vm.ReconfigVM_Task(spec=spec)
|
||||
self.conn.wait_for_task(task)
|
||||
|
||||
# Get ESXi host info for connection string
|
||||
host = vm.runtime.host
|
||||
host_ip = host.name if host else self.conn.settings.vcenter_host
|
||||
|
||||
return {
|
||||
"vm_name": name,
|
||||
"service_uri": service_uri,
|
||||
"connection_string": f"{protocol}://{host_ip}:{port}",
|
||||
"protocol": protocol,
|
||||
"port": port,
|
||||
"direction": direction,
|
||||
"yield_on_poll": yield_on_poll,
|
||||
"operation": "updated" if existing_port else "created",
|
||||
}
|
||||
|
||||
@mcp_tool(
|
||||
name="connect_serial_port",
|
||||
description="Connect or disconnect an existing serial port on a VM",
|
||||
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True),
|
||||
)
|
||||
def connect_serial_port(self, name: str, connected: bool = True) -> dict[str, Any]:
|
||||
"""Connect or disconnect serial port.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
connected: True to connect, False to disconnect. Default: True
|
||||
|
||||
Returns:
|
||||
Dict with result
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
serial_port = self._get_serial_port(vm)
|
||||
if not serial_port:
|
||||
raise ValueError(f"No network serial port configured on VM '{name}'")
|
||||
|
||||
# Build edit spec
|
||||
serial_spec = vim.vm.device.VirtualDeviceSpec()
|
||||
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
|
||||
serial_spec.device = serial_port
|
||||
serial_spec.device.connectable.connected = connected
|
||||
|
||||
spec = vim.vm.ConfigSpec()
|
||||
spec.deviceChange = [serial_spec]
|
||||
task = vm.ReconfigVM_Task(spec=spec)
|
||||
self.conn.wait_for_task(task)
|
||||
|
||||
return {
|
||||
"vm_name": name,
|
||||
"connected": connected,
|
||||
"service_uri": serial_port.backing.serviceURI if serial_port.backing else None,
|
||||
}
|
||||
|
||||
@mcp_tool(
|
||||
name="clear_serial_port",
|
||||
description="Reset serial port by disconnecting and reconnecting (clears stuck connections)",
|
||||
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True),
|
||||
)
|
||||
def clear_serial_port(self, name: str) -> dict[str, Any]:
|
||||
"""Clear serial port by cycling connection state.
|
||||
|
||||
Useful for clearing stuck or stale connections.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
|
||||
Returns:
|
||||
Dict with result
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
serial_port = self._get_serial_port(vm)
|
||||
if not serial_port:
|
||||
raise ValueError(f"No network serial port configured on VM '{name}'")
|
||||
|
||||
# Disconnect
|
||||
self.connect_serial_port(name, connected=False)
|
||||
time.sleep(1)
|
||||
|
||||
# Reconnect
|
||||
self.connect_serial_port(name, connected=True)
|
||||
|
||||
return {
|
||||
"vm_name": name,
|
||||
"status": "cleared",
|
||||
"service_uri": serial_port.backing.serviceURI if serial_port.backing else None,
|
||||
"message": "Serial port disconnected and reconnected",
|
||||
}
|
||||
|
||||
@mcp_tool(
|
||||
name="remove_serial_port",
|
||||
description="Remove the network serial port from a VM. VM must be powered off.",
|
||||
annotations=ToolAnnotations(destructiveHint=True, idempotentHint=True),
|
||||
)
|
||||
def remove_serial_port(self, name: str) -> str:
|
||||
"""Remove serial port from VM.
|
||||
|
||||
Args:
|
||||
name: VM name
|
||||
|
||||
Returns:
|
||||
Success message
|
||||
"""
|
||||
vm = self.conn.find_vm(name)
|
||||
if not vm:
|
||||
raise ValueError(f"VM '{name}' not found")
|
||||
|
||||
# Check VM is powered off
|
||||
if vm.runtime.powerState != vim.VirtualMachine.PowerState.poweredOff:
|
||||
raise ValueError(f"VM '{name}' must be powered off to remove serial port")
|
||||
|
||||
serial_port = self._get_serial_port(vm)
|
||||
if not serial_port:
|
||||
return f"No network serial port configured on VM '{name}'"
|
||||
|
||||
# Build remove spec
|
||||
serial_spec = vim.vm.device.VirtualDeviceSpec()
|
||||
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.remove
|
||||
serial_spec.device = serial_port
|
||||
|
||||
spec = vim.vm.ConfigSpec()
|
||||
spec.deviceChange = [serial_spec]
|
||||
task = vm.ReconfigVM_Task(spec=spec)
|
||||
self.conn.wait_for_task(task)
|
||||
|
||||
return f"Serial port removed from VM '{name}'"
|
||||
@ -9,6 +9,7 @@ from fastmcp import FastMCP
|
||||
from esxi_mcp_server.config import Settings, get_settings
|
||||
from esxi_mcp_server.connection import VMwareConnection
|
||||
from esxi_mcp_server.mixins import (
|
||||
ConsoleMixin,
|
||||
DiskManagementMixin,
|
||||
GuestOpsMixin,
|
||||
HostManagementMixin,
|
||||
@ -17,6 +18,7 @@ from esxi_mcp_server.mixins import (
|
||||
OVFManagementMixin,
|
||||
PowerOpsMixin,
|
||||
ResourcesMixin,
|
||||
SerialPortMixin,
|
||||
SnapshotsMixin,
|
||||
VCenterOpsMixin,
|
||||
VMLifecycleMixin,
|
||||
@ -78,6 +80,8 @@ def create_server(settings: Settings | None = None) -> FastMCP:
|
||||
OVFManagementMixin(conn),
|
||||
HostManagementMixin(conn),
|
||||
VCenterOpsMixin(conn),
|
||||
ConsoleMixin(conn),
|
||||
SerialPortMixin(conn),
|
||||
]
|
||||
|
||||
tool_count = 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user