diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..337948b --- /dev/null +++ b/.mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "proxmox": { + "command": "/home/bill/ProxmoxMCP/venv/bin/python", + "args": ["-m", "proxmox_mcp.server"], + "cwd": "/home/bill/ProxmoxMCP", + "env": { + "PYTHONPATH": "/home/bill/ProxmoxMCP/src", + "PROXMOX_MCP_CONFIG": "/home/bill/ProxmoxMCP/proxmox-config/config.json" + } + } + } +} diff --git a/proxmox-config/config.json b/proxmox-config/config.json new file mode 100644 index 0000000..fdc49f1 --- /dev/null +++ b/proxmox-config/config.json @@ -0,0 +1,65 @@ +{ + "clusters": [ + { + "name": "Building 1-ABE", + "proxmox": { + "host": "10.8.0.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "1125dae8-0bd6-4342-a385-7da770aff655" + } + }, + { + "name": "Building 2", + "proxmox": { + "host": "10.8.64.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "9f4912cb-dcc9-4e6a-968f-636f2a003454" + } + }, + { + "name": "Building 3", + "proxmox": { + "host": "10.8.128.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "7a3e4cf0-ada2-459a-8037-b8bdf932a09b" + } + }, + { + "name": "Building 4", + "proxmox": { + "host": "10.8.192.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "dcf00cab-3369-4084-8d42-3bc282a08d73" + } + } + ], + "logging": { + "level": "DEBUG", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "file": "proxmox_mcp.log" + } +} diff --git a/proxmox-config/config.template.json b/proxmox-config/config.template.json new file mode 100644 index 0000000..fdc49f1 --- /dev/null +++ b/proxmox-config/config.template.json @@ -0,0 +1,65 @@ +{ + "clusters": [ + { + "name": "Building 1-ABE", + "proxmox": { + "host": "10.8.0.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "1125dae8-0bd6-4342-a385-7da770aff655" + } + }, + { + "name": "Building 2", + "proxmox": { + "host": "10.8.64.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "9f4912cb-dcc9-4e6a-968f-636f2a003454" + } + }, + { + "name": "Building 3", + "proxmox": { + "host": "10.8.128.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "7a3e4cf0-ada2-459a-8037-b8bdf932a09b" + } + }, + { + "name": "Building 4", + "proxmox": { + "host": "10.8.192.200", + "port": 8006, + "verify_ssl": false, + "service": "PVE" + }, + "auth": { + "user": "root@pam", + "token_name": "mcp-token", + "token_value": "dcf00cab-3369-4084-8d42-3bc282a08d73" + } + } + ], + "logging": { + "level": "DEBUG", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "file": "proxmox_mcp.log" + } +} diff --git a/src/proxmox_mcp/config/loader.py b/src/proxmox_mcp/config/loader.py index 98af6f6..4a8b11a 100644 --- a/src/proxmox_mcp/config/loader.py +++ b/src/proxmox_mcp/config/loader.py @@ -3,7 +3,8 @@ This module handles loading and validation of server configuration: - JSON configuration file loading -- Environment variable handling +- Multi-cluster configuration support +- Legacy single-cluster config conversion - Configuration validation using Pydantic models - Error handling for invalid configurations @@ -15,49 +16,69 @@ from typing import Optional from .models import Config +def _convert_legacy_config(config_data: dict) -> dict: + """Convert legacy single-cluster config to new multi-cluster format. + + Legacy format: + { + "proxmox": {...}, + "auth": {...}, + "logging": {...} + } + + New format: + { + "clusters": [{"name": "default", "proxmox": {...}, "auth": {...}}], + "logging": {...} + } + """ + return { + "clusters": [{ + "name": "default", + "proxmox": config_data["proxmox"], + "auth": config_data["auth"] + }], + "logging": config_data.get("logging", {"level": "INFO"}) + } + +def _validate_cluster_names(config_data: dict) -> None: + """Validate that all cluster names are unique.""" + names = [c["name"] for c in config_data.get("clusters", [])] + if len(names) != len(set(names)): + duplicates = [n for n in names if names.count(n) > 1] + raise ValueError(f"Duplicate cluster names found: {set(duplicates)}") + def load_config(config_path: Optional[str] = None) -> Config: """Load and validate configuration from JSON file. - Performs the following steps: - 1. Verifies config path is provided - 2. Loads JSON configuration file - 3. Validates required fields are present - 4. Converts to typed Config object using Pydantic - - Configuration must include: - - Proxmox connection settings (host, port, etc.) - - Authentication credentials (user, token) - - Logging configuration - + Supports both multi-cluster and legacy single-cluster formats. + Legacy configs are automatically converted to multi-cluster format. + + Multi-cluster format: + { + "clusters": [ + {"name": "Building 4", "proxmox": {...}, "auth": {...}}, + {"name": "Building 3", "proxmox": {...}, "auth": {...}} + ], + "logging": {...} + } + + Legacy format (auto-converted): + { + "proxmox": {...}, + "auth": {...}, + "logging": {...} + } + Args: config_path: Path to the JSON configuration file - If not provided, raises ValueError Returns: - Config object containing validated configuration: - { - "proxmox": { - "host": "proxmox-host", - "port": 8006, - ... - }, - "auth": { - "user": "username", - "token_name": "token-name", - ... - }, - "logging": { - "level": "INFO", - ... - } - } + Config object containing validated configuration Raises: - ValueError: If: - - Config path is not provided - - JSON is invalid - - Required fields are missing - - Field values are invalid + ValueError: If config path not provided, JSON invalid, + required fields missing, or duplicate cluster names """ if not config_path: raise ValueError("PROXMOX_MCP_CONFIG environment variable must be set") @@ -65,8 +86,22 @@ def load_config(config_path: Optional[str] = None) -> Config: try: with open(config_path) as f: config_data = json.load(f) - if not config_data.get('proxmox', {}).get('host'): - raise ValueError("Proxmox host cannot be empty") + + # Check if this is a legacy config (has 'proxmox' at root level) + if "proxmox" in config_data and "clusters" not in config_data: + if not config_data.get('proxmox', {}).get('host'): + raise ValueError("Proxmox host cannot be empty") + config_data = _convert_legacy_config(config_data) + + # Validate new format + if "clusters" in config_data: + if not config_data["clusters"]: + raise ValueError("At least one cluster must be configured") + _validate_cluster_names(config_data) + for cluster in config_data["clusters"]: + if not cluster.get("proxmox", {}).get("host"): + raise ValueError(f"Proxmox host cannot be empty for cluster '{cluster.get('name', 'unknown')}'") + return Config(**config_data) except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON in config file: {e}") diff --git a/src/proxmox_mcp/config/models.py b/src/proxmox_mcp/config/models.py index 2e8a3c7..777f07e 100644 --- a/src/proxmox_mcp/config/models.py +++ b/src/proxmox_mcp/config/models.py @@ -13,7 +13,7 @@ - Field descriptions - Required vs optional field handling """ -from typing import Optional, Annotated +from typing import Optional, Annotated, List from pydantic import BaseModel, Field class NodeStatus(BaseModel): @@ -59,7 +59,7 @@ class AuthConfig(BaseModel): class LoggingConfig(BaseModel): """Model for logging configuration. - + Defines logging parameters with sensible defaults. Supports both file and console logging with customizable format and log levels. @@ -68,13 +68,22 @@ class LoggingConfig(BaseModel): format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # Optional: Log format file: Optional[str] = None # Optional: Log file path (default: None for console logging) +class ClusterConfig(BaseModel): + """Model for a single Proxmox cluster configuration. + + Combines connection settings and authentication for one cluster. + Used in multi-cluster configurations where each cluster has a + unique name identifier. + """ + name: str # Required: Cluster identifier (e.g., 'Building 4', 'prod') + proxmox: ProxmoxConfig # Required: Connection settings for this cluster + auth: AuthConfig # Required: Authentication for this cluster + class Config(BaseModel): """Root configuration model. - - Combines all configuration models into a single validated - configuration object. All sections are required to ensure - proper server operation. + + Supports multi-cluster configurations where each cluster + has its own connection and authentication settings. """ - proxmox: ProxmoxConfig # Required: Proxmox connection settings - auth: AuthConfig # Required: Authentication credentials + clusters: List[ClusterConfig] # Required: List of cluster configurations logging: LoggingConfig # Required: Logging configuration diff --git a/src/proxmox_mcp/core/proxmox.py b/src/proxmox_mcp/core/proxmox.py index 2466de1..7a3b7b0 100644 --- a/src/proxmox_mcp/core/proxmox.py +++ b/src/proxmox_mcp/core/proxmox.py @@ -12,9 +12,9 @@ across the MCP server. """ import logging -from typing import Dict, Any +from typing import Dict, Any, List from proxmoxer import ProxmoxAPI -from ..config.models import ProxmoxConfig, AuthConfig +from ..config.models import ProxmoxConfig, AuthConfig, ClusterConfig class ProxmoxManager: """Manager class for Proxmox API operations. @@ -101,7 +101,7 @@ def _setup_api(self) -> ProxmoxAPI: def get_api(self) -> ProxmoxAPI: """Get the initialized Proxmox API instance. - + Provides access to the configured and tested ProxmoxAPI instance for making API calls. The instance maintains connection state and handles authentication automatically. @@ -110,3 +110,67 @@ def get_api(self) -> ProxmoxAPI: ProxmoxAPI instance ready for making API calls """ return self.api + + +class ProxmoxClusterManager: + """Manager for multiple Proxmox clusters. + + This class handles: + - Initialization of multiple ProxmoxManager instances + - Cluster name to manager mapping + - API access by cluster name + - Cluster enumeration + + Each cluster is identified by a unique name and has its own + connection and authentication configuration. + """ + + def __init__(self, clusters: List[ClusterConfig]): + """Initialize the cluster manager with multiple clusters. + + Args: + clusters: List of cluster configurations + + Raises: + ValueError: If no clusters are provided + """ + self.logger = logging.getLogger("proxmox-mcp.cluster-manager") + + if not clusters: + raise ValueError("At least one cluster must be configured") + + self.managers: Dict[str, ProxmoxManager] = {} + + for cluster in clusters: + self.logger.info(f"Initializing cluster: {cluster.name}") + try: + self.managers[cluster.name] = ProxmoxManager(cluster.proxmox, cluster.auth) + self.logger.info(f"Successfully connected to cluster: {cluster.name}") + except Exception as e: + self.logger.error(f"Failed to connect to cluster '{cluster.name}': {e}") + raise RuntimeError(f"Failed to connect to cluster '{cluster.name}': {e}") + + def get_api(self, cluster_name: str) -> ProxmoxAPI: + """Get the Proxmox API instance for a specific cluster. + + Args: + cluster_name: Name of the cluster to access + + Returns: + ProxmoxAPI instance for the specified cluster + + Raises: + ValueError: If the cluster name is not found + """ + if cluster_name not in self.managers: + available = ', '.join(sorted(self.managers.keys())) + raise ValueError(f"Unknown cluster '{cluster_name}'. Available clusters: {available}") + return self.managers[cluster_name].get_api() + + def list_clusters(self) -> List[str]: + """List all configured cluster names. + + Returns: + List of cluster names in alphabetical order + """ + return sorted(self.managers.keys()) diff --git a/src/proxmox_mcp/server.py b/src/proxmox_mcp/server.py index 3a77324..78d728f 100644 --- a/src/proxmox_mcp/server.py +++ b/src/proxmox_mcp/server.py @@ -4,7 +4,7 @@ This module implements the core MCP server for Proxmox integration, providing: - Configuration loading and validation - Logging setup -- Proxmox API connection management +- Multi-cluster Proxmox API connection management - MCP tool registration and routing - Signal handling for graceful shutdown @@ -27,7 +27,7 @@ from .config.loader import load_config from .core.logging import setup_logging -from .core.proxmox import ProxmoxManager +from .core.proxmox import ProxmoxClusterManager from .tools.node import NodeTools from .tools.vm import VMTools from .tools.storage import StorageTools @@ -39,7 +39,8 @@ EXECUTE_VM_COMMAND_DESC, GET_CONTAINERS_DESC, GET_STORAGE_DESC, - GET_CLUSTER_STATUS_DESC + GET_CLUSTER_STATUS_DESC, + LIST_CLUSTERS_DESC ) class ProxmoxMCPServer: @@ -53,67 +54,84 @@ def __init__(self, config_path: Optional[str] = None): """ self.config = load_config(config_path) self.logger = setup_logging(self.config.logging) - - # Initialize core components - self.proxmox_manager = ProxmoxManager(self.config.proxmox, self.config.auth) - self.proxmox = self.proxmox_manager.get_api() - - # Initialize tools - self.node_tools = NodeTools(self.proxmox) - self.vm_tools = VMTools(self.proxmox) - self.storage_tools = StorageTools(self.proxmox) - self.cluster_tools = ClusterTools(self.proxmox) - + + # Initialize cluster manager with all configured clusters + self.cluster_manager = ProxmoxClusterManager(self.config.clusters) + + # Initialize tools with cluster manager + self.node_tools = NodeTools(self.cluster_manager) + self.vm_tools = VMTools(self.cluster_manager) + self.storage_tools = StorageTools(self.cluster_manager) + self.cluster_tools = ClusterTools(self.cluster_manager) + # Initialize MCP server self.mcp = FastMCP("ProxmoxMCP") self._setup_tools() def _setup_tools(self) -> None: """Register MCP tools with the server. - + Initializes and registers all available tools with the MCP server: + - Cluster listing tool - Node management tools (list nodes, get status) - VM operation tools (list VMs, execute commands) - Storage management tools (list storage) - Cluster tools (get cluster status) - + Each tool is registered with appropriate descriptions and parameter - validation using Pydantic models. + validation using Pydantic models. All tools require a cluster parameter + to specify which Proxmox cluster to target. """ - + + # List clusters tool + @self.mcp.tool(description=LIST_CLUSTERS_DESC) + def list_clusters(): + clusters = self.cluster_manager.list_clusters() + return [Content(type="text", text=f"Available clusters: {', '.join(clusters)}")] + # Node tools @self.mcp.tool(description=GET_NODES_DESC) - def get_nodes(): - return self.node_tools.get_nodes() + def get_nodes( + cluster: Annotated[str, Field(description="Cluster name (e.g. 'Building 4', 'Building 1-ABE')")] + ): + return self.node_tools.get_nodes(cluster) @self.mcp.tool(description=GET_NODE_STATUS_DESC) def get_node_status( + cluster: Annotated[str, Field(description="Cluster name (e.g. 'Building 4', 'Building 1-ABE')")], node: Annotated[str, Field(description="Name/ID of node to query (e.g. 'pve1', 'proxmox-node2')")] ): - return self.node_tools.get_node_status(node) + return self.node_tools.get_node_status(cluster, node) # VM tools @self.mcp.tool(description=GET_VMS_DESC) - def get_vms(): - return self.vm_tools.get_vms() + def get_vms( + cluster: Annotated[str, Field(description="Cluster name (e.g. 'Building 4', 'Building 1-ABE')")] + ): + return self.vm_tools.get_vms(cluster) @self.mcp.tool(description=EXECUTE_VM_COMMAND_DESC) async def execute_vm_command( + cluster: Annotated[str, Field(description="Cluster name (e.g. 'Building 4', 'Building 1-ABE')")], node: Annotated[str, Field(description="Host node name (e.g. 'pve1', 'proxmox-node2')")], vmid: Annotated[str, Field(description="VM ID number (e.g. '100', '101')")], command: Annotated[str, Field(description="Shell command to run (e.g. 'uname -a', 'systemctl status nginx')")] ): - return await self.vm_tools.execute_command(node, vmid, command) + return await self.vm_tools.execute_command(cluster, node, vmid, command) # Storage tools @self.mcp.tool(description=GET_STORAGE_DESC) - def get_storage(): - return self.storage_tools.get_storage() + def get_storage( + cluster: Annotated[str, Field(description="Cluster name (e.g. 'Building 4', 'Building 1-ABE')")] + ): + return self.storage_tools.get_storage(cluster) # Cluster tools @self.mcp.tool(description=GET_CLUSTER_STATUS_DESC) - def get_cluster_status(): - return self.cluster_tools.get_cluster_status() + def get_cluster_status( + cluster: Annotated[str, Field(description="Cluster name (e.g. 'Building 4', 'Building 1-ABE')")] + ): + return self.cluster_tools.get_cluster_status(cluster) def start(self) -> None: """Start the MCP server. diff --git a/src/proxmox_mcp/tools/base.py b/src/proxmox_mcp/tools/base.py index 978b59e..78ab1d2 100644 --- a/src/proxmox_mcp/tools/base.py +++ b/src/proxmox_mcp/tools/base.py @@ -15,29 +15,44 @@ from mcp.types import TextContent as Content from proxmoxer import ProxmoxAPI from ..formatting import ProxmoxTemplates +from ..core.proxmox import ProxmoxClusterManager class ProxmoxTool: """Base class for Proxmox MCP tools. - + This class provides common functionality used by all Proxmox tool implementations: - - Proxmox API access + - Multi-cluster Proxmox API access - Standardized logging - Response formatting - Error handling - + All tool classes should inherit from this base class to ensure consistent behavior and error handling across the MCP server. """ - def __init__(self, proxmox_api: ProxmoxAPI): - """Initialize the tool. + def __init__(self, cluster_manager: ProxmoxClusterManager): + """Initialize the tool with cluster manager. Args: - proxmox_api: Initialized ProxmoxAPI instance + cluster_manager: Initialized ProxmoxClusterManager instance """ - self.proxmox = proxmox_api + self.cluster_manager = cluster_manager self.logger = logging.getLogger(f"proxmox-mcp.{self.__class__.__name__.lower()}") + def get_api(self, cluster: str) -> ProxmoxAPI: + """Get the Proxmox API instance for a specific cluster. + + Args: + cluster: Name of the cluster to access + + Returns: + ProxmoxAPI instance for the specified cluster + + Raises: + ValueError: If the cluster name is not found + """ + return self.cluster_manager.get_api(cluster) + def _format_response(self, data: Any, resource_type: Optional[str] = None) -> List[Content]: """Format response data into MCP content using templates. diff --git a/src/proxmox_mcp/tools/cluster.py b/src/proxmox_mcp/tools/cluster.py index 44acdf8..68b7128 100644 --- a/src/proxmox_mcp/tools/cluster.py +++ b/src/proxmox_mcp/tools/cluster.py @@ -28,7 +28,7 @@ class ClusterTools(ProxmoxTool): proper operation of the Proxmox environment. """ - def get_cluster_status(self) -> List[Content]: + def get_cluster_status(self, cluster: str) -> List[Content]: """Get overall Proxmox cluster health and configuration status. Retrieves comprehensive cluster information including: @@ -36,35 +36,26 @@ def get_cluster_status(self) -> List[Content]: - Quorum status (essential for cluster operations) - Active node count and health - Resource distribution and status - + This information is critical for: - Ensuring cluster stability - Monitoring node membership - Verifying resource availability - Detecting potential issues + Args: + cluster: Name of the cluster to query (e.g., 'Building 4') + Returns: - List of Content objects containing formatted cluster status: - { - "name": "cluster-name", - "quorum": true/false, - "nodes": count, - "resources": [ - { - "type": "resource-type", - "status": "status" - } - ] - } + List of Content objects containing formatted cluster status Raises: - RuntimeError: If cluster status query fails due to: - - Network connectivity issues - - Authentication problems - - API endpoint failures + ValueError: If the cluster name is not found + RuntimeError: If cluster status query fails """ try: - result = self.proxmox.cluster.status.get() + api = self.get_api(cluster) + result = api.cluster.status.get() status = { "name": result[0].get("name") if result else None, "quorum": result[0].get("quorate"), @@ -72,5 +63,7 @@ def get_cluster_status(self) -> List[Content]: "resources": [res for res in result if res.get("type") == "resource"] } return self._format_response(status, "cluster") + except ValueError: + raise except Exception as e: self._handle_error("get cluster status", e) diff --git a/src/proxmox_mcp/tools/console/manager.py b/src/proxmox_mcp/tools/console/manager.py index 5c9ea58..b935c6b 100644 --- a/src/proxmox_mcp/tools/console/manager.py +++ b/src/proxmox_mcp/tools/console/manager.py @@ -19,13 +19,13 @@ class VMConsoleManager: """Manager class for VM console operations. - + Provides functionality for: - Executing commands in VM consoles - Managing command execution lifecycle - Handling command output and errors - Monitoring execution status - + Uses QEMU guest agent for reliable command execution with: - VM state verification before execution - Asynchronous command processing @@ -33,16 +33,16 @@ class VMConsoleManager: - Comprehensive error handling """ - def __init__(self, proxmox_api): + def __init__(self, cluster_manager): """Initialize the VM console manager. Args: - proxmox_api: Initialized ProxmoxAPI instance + cluster_manager: Initialized ProxmoxClusterManager instance """ - self.proxmox = proxmox_api + self.cluster_manager = cluster_manager self.logger = logging.getLogger("proxmox-mcp.vm-console") - async def execute_command(self, node: str, vmid: str, command: str) -> Dict[str, Any]: + async def execute_command(self, cluster: str, node: str, vmid: str, command: str) -> Dict[str, Any]: """Execute a command in a VM's console via QEMU guest agent. Implements a two-phase command execution process: @@ -50,18 +50,19 @@ async def execute_command(self, node: str, vmid: str, command: str) -> Dict[str, - Verifies VM exists and is running - Initiates command execution via guest agent - Captures command PID for tracking - + 2. Result Collection: - Monitors command execution status - Captures command output and errors - Handles completion status - + Requirements: - VM must be running - QEMU guest agent must be installed and active - Command execution permissions must be enabled Args: + cluster: Name of the cluster (e.g., 'Building 4') node: Name of the node where VM is running (e.g., 'pve1') vmid: ID of the VM to execute command in (e.g., '100') command: Shell command to execute in the VM @@ -77,6 +78,7 @@ async def execute_command(self, node: str, vmid: str, command: str) -> Dict[str, Raises: ValueError: If: + - Cluster is not found - VM is not found - VM is not running - Guest agent is not available @@ -86,18 +88,21 @@ async def execute_command(self, node: str, vmid: str, command: str) -> Dict[str, - API communication errors occur """ try: + # Get the API for the specified cluster + proxmox = self.cluster_manager.get_api(cluster) + # Verify VM exists and is running - vm_status = self.proxmox.nodes(node).qemu(vmid).status.current.get() + vm_status = proxmox.nodes(node).qemu(vmid).status.current.get() if vm_status["status"] != "running": self.logger.error(f"Failed to execute command on VM {vmid}: VM is not running") raise ValueError(f"VM {vmid} on node {node} is not running") # Get VM's console - self.logger.info(f"Executing command on VM {vmid} (node: {node}): {command}") - + self.logger.info(f"Executing command on VM {vmid} (node: {node}, cluster: {cluster}): {command}") + # Get the API endpoint # Use the guest agent exec endpoint - endpoint = self.proxmox.nodes(node).qemu(vmid).agent + endpoint = proxmox.nodes(node).qemu(vmid).agent self.logger.debug(f"Using API endpoint: {endpoint}") # Execute the command using two-step process diff --git a/src/proxmox_mcp/tools/definitions.py b/src/proxmox_mcp/tools/definitions.py index 4f2ed77..7a8ee9c 100644 --- a/src/proxmox_mcp/tools/definitions.py +++ b/src/proxmox_mcp/tools/definitions.py @@ -5,12 +5,16 @@ # Node tool descriptions GET_NODES_DESC = """List all nodes in the Proxmox cluster with their status, CPU, memory, and role information. +Parameters: +cluster* - Cluster name (e.g. 'Building 4', 'Building 1-ABE') + Example: {"node": "pve1", "status": "online", "cpu_usage": 0.15, "memory": {"used": "8GB", "total": "32GB"}}""" GET_NODE_STATUS_DESC = """Get detailed status information for a specific Proxmox node. Parameters: +cluster* - Cluster name (e.g. 'Building 4', 'Building 1-ABE') node* - Name/ID of node to query (e.g. 'pve1') Example: @@ -19,12 +23,16 @@ # VM tool descriptions GET_VMS_DESC = """List all virtual machines across the cluster with their status and resource usage. +Parameters: +cluster* - Cluster name (e.g. 'Building 4', 'Building 1-ABE') + Example: {"vmid": "100", "name": "ubuntu", "status": "running", "cpu": 2, "memory": 4096}""" EXECUTE_VM_COMMAND_DESC = """Execute commands in a VM via QEMU guest agent. Parameters: +cluster* - Cluster name (e.g. 'Building 4', 'Building 1-ABE') node* - Host node name (e.g. 'pve1') vmid* - VM ID number (e.g. '100') command* - Shell command to run (e.g. 'uname -a') @@ -35,17 +43,34 @@ # Container tool descriptions GET_CONTAINERS_DESC = """List all LXC containers across the cluster with their status and configuration. +Parameters: +cluster* - Cluster name (e.g. 'Building 4', 'Building 1-ABE') + Example: {"vmid": "200", "name": "nginx", "status": "running", "template": "ubuntu-20.04"}""" # Storage tool descriptions GET_STORAGE_DESC = """List storage pools across the cluster with their usage and configuration. +Parameters: +cluster* - Cluster name (e.g. 'Building 4', 'Building 1-ABE') + Example: {"storage": "local-lvm", "type": "lvm", "used": "500GB", "total": "1TB"}""" # Cluster tool descriptions GET_CLUSTER_STATUS_DESC = """Get overall Proxmox cluster health and configuration status. +Parameters: +cluster* - Cluster name (e.g. 'Building 4', 'Building 1-ABE') + Example: {"name": "proxmox", "quorum": "ok", "nodes": 3, "ha_status": "active"}""" + +# List clusters tool description +LIST_CLUSTERS_DESC = """List all configured Proxmox clusters. + +Returns a list of available cluster names that can be used with other tools. + +Example: +["Building 1-ABE", "Building 2", "Building 3", "Building 4"]""" diff --git a/src/proxmox_mcp/tools/node.py b/src/proxmox_mcp/tools/node.py index 120c426..28b15f1 100644 --- a/src/proxmox_mcp/tools/node.py +++ b/src/proxmox_mcp/tools/node.py @@ -30,44 +30,39 @@ class NodeTools(ProxmoxTool): node information might be temporarily unavailable. """ - def get_nodes(self) -> List[Content]: - """List all nodes in the Proxmox cluster with detailed status. + def get_nodes(self, cluster: str) -> List[Content]: + """List all nodes in a Proxmox cluster with detailed status. Retrieves comprehensive information for each node including: - Basic status (online/offline) - Uptime statistics - CPU configuration and count - Memory usage and capacity - + Implements a fallback mechanism that returns basic information if detailed status retrieval fails for any node. + Args: + cluster: Name of the cluster to query (e.g., 'Building 4') + Returns: - List of Content objects containing formatted node information: - { - "node": "node_name", - "status": "online/offline", - "uptime": seconds, - "maxcpu": cpu_count, - "memory": { - "used": bytes, - "total": bytes - } - } + List of Content objects containing formatted node information Raises: + ValueError: If the cluster name is not found RuntimeError: If the cluster-wide node query fails """ try: - result = self.proxmox.nodes.get() + api = self.get_api(cluster) + result = api.nodes.get() nodes = [] - + # Get detailed info for each node for node in result: node_name = node["node"] try: # Get detailed status for each node - status = self.proxmox.nodes(node_name).status.get() + status = api.nodes(node_name).status.get() nodes.append({ "node": node_name, "status": node["status"], @@ -91,10 +86,12 @@ def get_nodes(self) -> List[Content]: } }) return self._format_response(nodes, "nodes") + except ValueError: + raise except Exception as e: self._handle_error("get nodes", e) - def get_node_status(self, node: str) -> List[Content]: + def get_node_status(self, cluster: str, node: str) -> List[Content]: """Get detailed status information for a specific node. Retrieves comprehensive status information including: @@ -106,30 +103,21 @@ def get_node_status(self, node: str) -> List[Content]: - Running tasks and services Args: + cluster: Name of the cluster to query (e.g., 'Building 4') node: Name/ID of node to query (e.g., 'pve1', 'proxmox-node2') Returns: - List of Content objects containing detailed node status: - { - "uptime": seconds, - "cpu": { - "usage": percentage, - "cores": count - }, - "memory": { - "used": bytes, - "total": bytes, - "free": bytes - }, - ...additional status fields - } + List of Content objects containing detailed node status Raises: - ValueError: If the specified node is not found + ValueError: If the cluster name or node is not found RuntimeError: If status retrieval fails (node offline, network issues) """ try: - result = self.proxmox.nodes(node).status.get() + api = self.get_api(cluster) + result = api.nodes(node).status.get() return self._format_response((node, result), "node_status") + except ValueError: + raise except Exception as e: self._handle_error(f"get status for node {node}", e) diff --git a/src/proxmox_mcp/tools/storage.py b/src/proxmox_mcp/tools/storage.py index 3bb054b..af85e68 100644 --- a/src/proxmox_mcp/tools/storage.py +++ b/src/proxmox_mcp/tools/storage.py @@ -30,8 +30,8 @@ class StorageTools(ProxmoxTool): storage information might be temporarily unavailable. """ - def get_storage(self) -> List[Content]: - """List storage pools across the cluster with detailed status. + def get_storage(self, cluster: str) -> List[Content]: + """List storage pools across a cluster with detailed status. Retrieves comprehensive information for each storage pool including: - Basic identification (name, type) @@ -41,33 +41,29 @@ def get_storage(self) -> List[Content]: * Used space * Total capacity * Available space - + Implements a fallback mechanism that returns basic information if detailed status retrieval fails for any storage pool. + Args: + cluster: Name of the cluster to query (e.g., 'Building 4') + Returns: - List of Content objects containing formatted storage information: - { - "storage": "storage-name", - "type": "storage-type", - "content": ["content-types"], - "status": "online/offline", - "used": bytes, - "total": bytes, - "available": bytes - } + List of Content objects containing formatted storage information Raises: + ValueError: If the cluster name is not found RuntimeError: If the cluster-wide storage query fails """ try: - result = self.proxmox.storage.get() + api = self.get_api(cluster) + result = api.storage.get() storage = [] - + for store in result: # Get detailed storage info including usage try: - status = self.proxmox.nodes(store.get("node", "localhost")).storage(store["storage"]).status.get() + status = api.nodes(store.get("node", "localhost")).storage(store["storage"]).status.get() storage.append({ "storage": store["storage"], "type": store["type"], @@ -88,7 +84,9 @@ def get_storage(self) -> List[Content]: "total": 0, "available": 0 }) - + return self._format_response(storage, "storage") + except ValueError: + raise except Exception as e: self._handle_error("get storage", e) diff --git a/src/proxmox_mcp/tools/vm.py b/src/proxmox_mcp/tools/vm.py index 97faeb3..5590323 100644 --- a/src/proxmox_mcp/tools/vm.py +++ b/src/proxmox_mcp/tools/vm.py @@ -21,29 +21,29 @@ class VMTools(ProxmoxTool): """Tools for managing Proxmox VMs. - + Provides functionality for: - Retrieving cluster-wide VM information - Getting detailed VM status and configuration - Executing commands within VMs - Managing VM console operations - + Implements fallback mechanisms for scenarios where detailed VM information might be temporarily unavailable. Integrates with QEMU guest agent for VM command execution. """ - def __init__(self, proxmox_api): + def __init__(self, cluster_manager): """Initialize VM tools. Args: - proxmox_api: Initialized ProxmoxAPI instance + cluster_manager: Initialized ProxmoxClusterManager instance """ - super().__init__(proxmox_api) - self.console_manager = VMConsoleManager(proxmox_api) + super().__init__(cluster_manager) + self.console_manager = VMConsoleManager(cluster_manager) - def get_vms(self) -> List[Content]: - """List all virtual machines across the cluster with detailed status. + def get_vms(self, cluster: str) -> List[Content]: + """List all virtual machines across a cluster with detailed status. Retrieves comprehensive information for each VM including: - Basic identification (ID, name) @@ -52,37 +52,31 @@ def get_vms(self) -> List[Content]: * CPU cores * Memory allocation and usage - Node placement - + Implements a fallback mechanism that returns basic information if detailed configuration retrieval fails for any VM. + Args: + cluster: Name of the cluster to query (e.g., 'Building 4') + Returns: - List of Content objects containing formatted VM information: - { - "vmid": "100", - "name": "vm-name", - "status": "running/stopped", - "node": "node-name", - "cpus": core_count, - "memory": { - "used": bytes, - "total": bytes - } - } + List of Content objects containing formatted VM information Raises: + ValueError: If the cluster name is not found RuntimeError: If the cluster-wide VM query fails """ try: + api = self.get_api(cluster) result = [] - for node in self.proxmox.nodes.get(): + for node in api.nodes.get(): node_name = node["node"] - vms = self.proxmox.nodes(node_name).qemu.get() + vms = api.nodes(node_name).qemu.get() for vm in vms: vmid = vm["vmid"] # Get VM config for CPU cores try: - config = self.proxmox.nodes(node_name).qemu(vmid).config.get() + config = api.nodes(node_name).qemu(vmid).config.get() result.append({ "vmid": vmid, "name": vm["name"], @@ -108,10 +102,12 @@ def get_vms(self) -> List[Content]: } }) return self._format_response(result, "vms") + except ValueError: + raise except Exception as e: self._handle_error("get VMs", e) - async def execute_command(self, node: str, vmid: str, command: str) -> List[Content]: + async def execute_command(self, cluster: str, node: str, vmid: str, command: str) -> List[Content]: """Execute a command in a VM via QEMU guest agent. Uses the QEMU guest agent to execute commands within a running VM. @@ -121,24 +117,20 @@ async def execute_command(self, node: str, vmid: str, command: str) -> List[Cont - Command execution permissions must be enabled Args: + cluster: Name of the cluster (e.g., 'Building 4') node: Host node name (e.g., 'pve1', 'proxmox-node2') vmid: VM ID number (e.g., '100', '101') command: Shell command to run (e.g., 'uname -a', 'systemctl status nginx') Returns: - List of Content objects containing formatted command output: - { - "success": true/false, - "output": "command output", - "error": "error message if any" - } + List of Content objects containing formatted command output Raises: - ValueError: If VM is not found, not running, or guest agent is not available + ValueError: If cluster/VM is not found, not running, or guest agent is not available RuntimeError: If command execution fails due to permissions or other issues """ try: - result = await self.console_manager.execute_command(node, vmid, command) + result = await self.console_manager.execute_command(cluster, node, vmid, command) # Use the command output formatter from ProxmoxFormatters from ..formatting import ProxmoxFormatters formatted = ProxmoxFormatters.format_command_output( @@ -148,5 +140,7 @@ async def execute_command(self, node: str, vmid: str, command: str) -> List[Cont error=result.get("error") ) return [Content(type="text", text=formatted)] + except ValueError: + raise except Exception as e: self._handle_error(f"execute command on VM {vmid}", e)