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
|
# 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
|
## Features
|
||||||
|
|
||||||
- Support for ESXi and vCenter Server connections
|
- **94 MCP Tools** - Complete vSphere management capabilities
|
||||||
- Real-time communication based on SSE (Server-Sent Events)
|
- **6 MCP Resources** - Real-time access to VMs, hosts, datastores, networks, and clusters
|
||||||
- RESTful API interface with JSON-RPC support
|
- **Modular Architecture** - 13 specialized mixins organized by function
|
||||||
- API key authentication
|
- **Full vCenter & ESXi Support** - Works with standalone hosts or vCenter Server
|
||||||
- Complete virtual machine lifecycle management
|
- **Guest Operations** - Execute commands, transfer files inside VMs via VMware Tools
|
||||||
- Real-time performance monitoring
|
- **Serial Console Access** - Network serial ports for headless VMs and network appliances
|
||||||
- SSL/TLS secure connection support
|
- **VM Screenshots** - Capture console screenshots for monitoring or documentation
|
||||||
- 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)
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. Install dependencies:
|
### Installation
|
||||||
|
|
||||||
```bash
|
```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
|
Create a `.env` file or set environment variables:
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
```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:
|
# Or if installed
|
||||||
|
esxi-mcp-server
|
||||||
```http
|
|
||||||
POST /sse/messages
|
|
||||||
Authorization: Bearer your-api-key
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Main Tool Interfaces
|
### Add to Claude Code
|
||||||
|
|
||||||
1. Create VM
|
```bash
|
||||||
```json
|
claude mcp add esxi "uvx esxi-mcp-server"
|
||||||
{
|
|
||||||
"name": "vm-name",
|
|
||||||
"cpu": 2,
|
|
||||||
"memory": 4096,
|
|
||||||
"datastore": "datastore-name",
|
|
||||||
"network": "network-name"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Clone VM
|
## Available Tools (94 Total)
|
||||||
```json
|
|
||||||
{
|
### VM Lifecycle (6 tools)
|
||||||
"template_name": "source-vm",
|
| Tool | Description |
|
||||||
"new_name": "new-vm-name"
|
|------|-------------|
|
||||||
}
|
| `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
|
## Configuration Options
|
||||||
```json
|
|
||||||
{
|
| Variable | Description | Default |
|
||||||
"name": "vm-name"
|
|----------|-------------|---------|
|
||||||
}
|
| `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
|
## Examples
|
||||||
```json
|
|
||||||
{
|
### Create a VM and Install an OS
|
||||||
"name": "vm-name"
|
|
||||||
}
|
```
|
||||||
|
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
|
User: Run "uname -a" on the linux-server VM
|
||||||
GET vmstats://{vm_name}
|
|
||||||
|
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 |
|
```
|
||||||
|-----------|-------------|----------|---------|
|
User: Set up a serial console on my Cisco router VM
|
||||||
| 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 |
|
|
||||||
|
|
||||||
## Environment Variables
|
Claude: [Calls setup_serial_port with name="cisco-router", protocol="telnet"]
|
||||||
|
|
||||||
All configuration items support environment variable settings, following these naming rules:
|
Serial port configured. You can connect via:
|
||||||
- VCENTER_HOST
|
telnet://10.20.0.22:4521
|
||||||
- VCENTER_USER
|
```
|
||||||
- VCENTER_PASSWORD
|
|
||||||
- VCENTER_DATACENTER
|
|
||||||
- VCENTER_CLUSTER
|
|
||||||
- VCENTER_DATASTORE
|
|
||||||
- VCENTER_NETWORK
|
|
||||||
- VCENTER_INSECURE
|
|
||||||
- MCP_API_KEY
|
|
||||||
- MCP_LOG_FILE
|
|
||||||
- MCP_LOG_LEVEL
|
|
||||||
|
|
||||||
## Security Recommendations
|
## Requirements
|
||||||
|
|
||||||
1. Production Environment:
|
- Python 3.11+
|
||||||
- Use valid SSL certificates
|
- VMware vSphere 7.0+ (ESXi or vCenter)
|
||||||
- Enable API key authentication
|
- VMware Tools (for guest operations)
|
||||||
- Set appropriate log levels
|
|
||||||
- Restrict API access scope
|
|
||||||
|
|
||||||
2. Testing Environment:
|
## Development
|
||||||
- Set insecure: true to skip SSL verification
|
|
||||||
- Use more detailed log level (DEBUG)
|
```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
|
## License
|
||||||
|
|
||||||
MIT License
|
MIT License - See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Issues and Pull Requests are welcome!
|
Contributions welcome! Please read the contributing guidelines and submit a PR.
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
### v0.0.1
|
|
||||||
- Initial release
|
|
||||||
- Basic VM management functionality
|
|
||||||
- SSE communication support
|
|
||||||
- API key authentication
|
|
||||||
- Performance monitoring
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Bright8192
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- VMware pyvmomi team
|
- Built with [FastMCP](https://github.com/jlowin/fastmcp)
|
||||||
- MCP Protocol development team
|
- 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."""
|
"""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.disk_management import DiskManagementMixin
|
||||||
from esxi_mcp_server.mixins.guest_ops import GuestOpsMixin
|
from esxi_mcp_server.mixins.guest_ops import GuestOpsMixin
|
||||||
from esxi_mcp_server.mixins.host_management import HostManagementMixin
|
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.ovf_management import OVFManagementMixin
|
||||||
from esxi_mcp_server.mixins.power_ops import PowerOpsMixin
|
from esxi_mcp_server.mixins.power_ops import PowerOpsMixin
|
||||||
from esxi_mcp_server.mixins.resources import ResourcesMixin
|
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.snapshots import SnapshotsMixin
|
||||||
from esxi_mcp_server.mixins.vcenter_ops import VCenterOpsMixin
|
from esxi_mcp_server.mixins.vcenter_ops import VCenterOpsMixin
|
||||||
from esxi_mcp_server.mixins.vm_lifecycle import VMLifecycleMixin
|
from esxi_mcp_server.mixins.vm_lifecycle import VMLifecycleMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"VMLifecycleMixin",
|
"ConsoleMixin",
|
||||||
"PowerOpsMixin",
|
|
||||||
"SnapshotsMixin",
|
|
||||||
"MonitoringMixin",
|
|
||||||
"GuestOpsMixin",
|
|
||||||
"ResourcesMixin",
|
|
||||||
"DiskManagementMixin",
|
"DiskManagementMixin",
|
||||||
|
"GuestOpsMixin",
|
||||||
|
"HostManagementMixin",
|
||||||
|
"MonitoringMixin",
|
||||||
"NICManagementMixin",
|
"NICManagementMixin",
|
||||||
"OVFManagementMixin",
|
"OVFManagementMixin",
|
||||||
"HostManagementMixin",
|
"PowerOpsMixin",
|
||||||
|
"ResourcesMixin",
|
||||||
|
"SerialPortMixin",
|
||||||
|
"SnapshotsMixin",
|
||||||
"VCenterOpsMixin",
|
"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.config import Settings, get_settings
|
||||||
from esxi_mcp_server.connection import VMwareConnection
|
from esxi_mcp_server.connection import VMwareConnection
|
||||||
from esxi_mcp_server.mixins import (
|
from esxi_mcp_server.mixins import (
|
||||||
|
ConsoleMixin,
|
||||||
DiskManagementMixin,
|
DiskManagementMixin,
|
||||||
GuestOpsMixin,
|
GuestOpsMixin,
|
||||||
HostManagementMixin,
|
HostManagementMixin,
|
||||||
@ -17,6 +18,7 @@ from esxi_mcp_server.mixins import (
|
|||||||
OVFManagementMixin,
|
OVFManagementMixin,
|
||||||
PowerOpsMixin,
|
PowerOpsMixin,
|
||||||
ResourcesMixin,
|
ResourcesMixin,
|
||||||
|
SerialPortMixin,
|
||||||
SnapshotsMixin,
|
SnapshotsMixin,
|
||||||
VCenterOpsMixin,
|
VCenterOpsMixin,
|
||||||
VMLifecycleMixin,
|
VMLifecycleMixin,
|
||||||
@ -78,6 +80,8 @@ def create_server(settings: Settings | None = None) -> FastMCP:
|
|||||||
OVFManagementMixin(conn),
|
OVFManagementMixin(conn),
|
||||||
HostManagementMixin(conn),
|
HostManagementMixin(conn),
|
||||||
VCenterOpsMixin(conn),
|
VCenterOpsMixin(conn),
|
||||||
|
ConsoleMixin(conn),
|
||||||
|
SerialPortMixin(conn),
|
||||||
]
|
]
|
||||||
|
|
||||||
tool_count = 0
|
tool_count = 0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user