feat: add Portainer Edge MCP server
- Implement comprehensive edge computing functionality - Add edge environment management (list, get, status, generate keys) - Add edge stack operations (list, get, create, update, delete) - Add edge group management (list, get, create, update, delete) - Add edge job scheduling (list, get, create, delete) - Add edge settings configuration (get, update) - Create test scripts for edge API validation - Add comprehensive README documentation for edge server - Include nginx stack creation script from earlier testing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d7055a912e
commit
7a1abbe243
312
README_EDGE.md
Normal file
312
README_EDGE.md
Normal file
@ -0,0 +1,312 @@
|
||||
# Portainer Edge MCP Server
|
||||
|
||||
This MCP server provides edge computing functionality through Portainer's API, managing edge environments, edge stacks, edge groups, and edge jobs.
|
||||
|
||||
## Features
|
||||
|
||||
- **Edge Environment Management**: List and monitor edge environments
|
||||
- **Edge Stack Deployment**: Deploy and manage stacks across edge devices
|
||||
- **Edge Group Organization**: Create and manage groups of edge endpoints
|
||||
- **Edge Job Scheduling**: Schedule and run jobs on edge devices
|
||||
- **Edge Settings Configuration**: Configure global edge settings
|
||||
|
||||
## Installation
|
||||
|
||||
1. Ensure you have the Portainer MCP servers repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/portainer-mcp.git
|
||||
cd portainer-mcp
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Configure environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your Portainer URL and API key
|
||||
```
|
||||
|
||||
4. Make the server executable:
|
||||
```bash
|
||||
chmod +x portainer_edge_server.py
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to your Claude Desktop configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"portainer-edge": {
|
||||
"command": "python",
|
||||
"args": ["/path/to/portainer-mcp/portainer_edge_server.py"],
|
||||
"env": {
|
||||
"PORTAINER_URL": "https://your-portainer-instance.com",
|
||||
"PORTAINER_API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Edge Environment Management
|
||||
|
||||
#### list_edge_environments
|
||||
List all edge environments.
|
||||
- **Parameters**:
|
||||
- `status` (optional): Filter by status - "connected" or "disconnected"
|
||||
|
||||
#### get_edge_environment
|
||||
Get details of a specific edge environment.
|
||||
- **Parameters**:
|
||||
- `environment_id` (required): Environment ID
|
||||
|
||||
#### get_edge_status
|
||||
Get edge environment status and check-in information.
|
||||
- **Parameters**:
|
||||
- `environment_id` (required): Environment ID
|
||||
|
||||
#### generate_edge_key
|
||||
Generate an edge key for adding new edge agents.
|
||||
- **Parameters**:
|
||||
- `name` (required): Environment name
|
||||
- `group_ids` (optional): List of edge group IDs
|
||||
|
||||
### Edge Stack Management
|
||||
|
||||
#### list_edge_stacks
|
||||
List all edge stacks.
|
||||
|
||||
#### get_edge_stack
|
||||
Get details of a specific edge stack.
|
||||
- **Parameters**:
|
||||
- `edge_stack_id` (required): Edge Stack ID
|
||||
|
||||
#### create_edge_stack
|
||||
Create a new edge stack.
|
||||
- **Parameters**:
|
||||
- `name` (required): Stack name
|
||||
- `stack_content` (required): Stack file content (Docker Compose)
|
||||
- `edge_groups` (required): List of edge group IDs
|
||||
- `deploy_type` (optional): Deployment type (0: compose, 1: kubernetes)
|
||||
- `edge_id_list` (optional): Specific edge IDs for deployment
|
||||
- `registries` (optional): List of registry IDs
|
||||
|
||||
#### update_edge_stack
|
||||
Update an existing edge stack.
|
||||
- **Parameters**:
|
||||
- `edge_stack_id` (required): Edge Stack ID
|
||||
- `stack_content` (optional): Updated stack content
|
||||
- `edge_groups` (optional): Updated edge group IDs
|
||||
- `deploy_type` (optional): Updated deployment type
|
||||
|
||||
#### delete_edge_stack
|
||||
Delete an edge stack.
|
||||
- **Parameters**:
|
||||
- `edge_stack_id` (required): Edge Stack ID
|
||||
|
||||
### Edge Group Management
|
||||
|
||||
#### list_edge_groups
|
||||
List all edge groups.
|
||||
|
||||
#### get_edge_group
|
||||
Get details of a specific edge group.
|
||||
- **Parameters**:
|
||||
- `edge_group_id` (required): Edge Group ID
|
||||
|
||||
#### create_edge_group
|
||||
Create a new edge group.
|
||||
- **Parameters**:
|
||||
- `name` (required): Group name
|
||||
- `dynamic` (optional): Enable dynamic membership (default: false)
|
||||
- `tag_ids` (optional): Tag IDs for dynamic groups
|
||||
- `endpoints` (optional): Endpoint IDs for static groups
|
||||
|
||||
#### update_edge_group
|
||||
Update an existing edge group.
|
||||
- **Parameters**:
|
||||
- `edge_group_id` (required): Edge Group ID
|
||||
- `name` (optional): Updated name
|
||||
- `dynamic` (optional): Update dynamic membership
|
||||
- `tag_ids` (optional): Updated tag IDs
|
||||
- `endpoints` (optional): Updated endpoint IDs
|
||||
|
||||
#### delete_edge_group
|
||||
Delete an edge group.
|
||||
- **Parameters**:
|
||||
- `edge_group_id` (required): Edge Group ID
|
||||
|
||||
### Edge Job Management
|
||||
|
||||
#### list_edge_jobs
|
||||
List all edge jobs.
|
||||
|
||||
#### get_edge_job
|
||||
Get details of a specific edge job.
|
||||
- **Parameters**:
|
||||
- `edge_job_id` (required): Edge Job ID
|
||||
|
||||
#### create_edge_job
|
||||
Create a new edge job.
|
||||
- **Parameters**:
|
||||
- `name` (required): Job name
|
||||
- `edge_groups` (required): Target edge group IDs
|
||||
- `script_content` (required): Script content to execute
|
||||
- `recurring` (optional): Enable recurring execution
|
||||
- `cron_expression` (optional): Cron expression for scheduling
|
||||
|
||||
#### delete_edge_job
|
||||
Delete an edge job.
|
||||
- **Parameters**:
|
||||
- `edge_job_id` (required): Edge Job ID
|
||||
|
||||
### Edge Settings
|
||||
|
||||
#### get_edge_settings
|
||||
Get global edge settings.
|
||||
|
||||
#### update_edge_settings
|
||||
Update global edge settings.
|
||||
- **Parameters**:
|
||||
- `check_in_interval` (optional): Check-in interval in seconds
|
||||
- `command_interval` (optional): Command interval in seconds
|
||||
- `ping_interval` (optional): Ping interval in seconds
|
||||
- `snapshot_interval` (optional): Snapshot interval in seconds
|
||||
- `tunnel_server_address` (optional): Tunnel server address
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### List Edge Environments
|
||||
|
||||
```javascript
|
||||
await use_mcp_tool("portainer-edge", "list_edge_environments", {
|
||||
status: "connected"
|
||||
});
|
||||
```
|
||||
|
||||
### Create Edge Stack
|
||||
|
||||
```javascript
|
||||
await use_mcp_tool("portainer-edge", "create_edge_stack", {
|
||||
name: "nginx-edge",
|
||||
stack_content: `version: '3'
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"`,
|
||||
edge_groups: ["1", "2"]
|
||||
});
|
||||
```
|
||||
|
||||
### Create Edge Group
|
||||
|
||||
```javascript
|
||||
// Static group with specific endpoints
|
||||
await use_mcp_tool("portainer-edge", "create_edge_group", {
|
||||
name: "production-edge",
|
||||
dynamic: false,
|
||||
endpoints: ["10", "11", "12"]
|
||||
});
|
||||
|
||||
// Dynamic group based on tags
|
||||
await use_mcp_tool("portainer-edge", "create_edge_group", {
|
||||
name: "tagged-devices",
|
||||
dynamic: true,
|
||||
tag_ids: ["1", "3"]
|
||||
});
|
||||
```
|
||||
|
||||
### Schedule Edge Job
|
||||
|
||||
```javascript
|
||||
await use_mcp_tool("portainer-edge", "create_edge_job", {
|
||||
name: "system-update",
|
||||
edge_groups: ["1"],
|
||||
script_content: "apt update && apt upgrade -y",
|
||||
recurring: true,
|
||||
cron_expression: "0 2 * * *" // Daily at 2 AM
|
||||
});
|
||||
```
|
||||
|
||||
## Edge Computing Concepts
|
||||
|
||||
### Edge Environments
|
||||
Edge environments are remote Docker or Kubernetes environments that connect to Portainer via the Edge Agent. They can be:
|
||||
- **Connected**: Currently connected and checking in
|
||||
- **Disconnected**: Not currently reachable
|
||||
|
||||
### Edge Stacks
|
||||
Edge stacks are Docker Compose or Kubernetes deployments that can be deployed to multiple edge environments simultaneously through edge groups.
|
||||
|
||||
### Edge Groups
|
||||
Edge groups organize edge environments for bulk operations:
|
||||
- **Static Groups**: Manually selected endpoints
|
||||
- **Dynamic Groups**: Automatically populated based on tags
|
||||
|
||||
### Edge Jobs
|
||||
Edge jobs execute scripts on edge devices:
|
||||
- **One-time Jobs**: Execute once immediately
|
||||
- **Recurring Jobs**: Execute on a schedule (cron)
|
||||
|
||||
## Testing
|
||||
|
||||
Use the provided test script to verify edge functionality:
|
||||
|
||||
```bash
|
||||
python test_edge_server.py
|
||||
```
|
||||
|
||||
This will test:
|
||||
- Listing edge environments
|
||||
- Creating and managing edge groups
|
||||
- Listing edge stacks
|
||||
- Viewing edge jobs
|
||||
- Checking edge settings
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Group Organization**: Use edge groups to organize devices by location, purpose, or capability
|
||||
2. **Stack Templates**: Create reusable stack templates for common deployments
|
||||
3. **Job Scheduling**: Use recurring jobs for maintenance tasks
|
||||
4. **Monitoring**: Regularly check edge environment status
|
||||
5. **Security**: Use proper authentication for edge agents
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Edge agents use unique keys for authentication
|
||||
- Communication is encrypted between edge agents and Portainer
|
||||
- Edge jobs execute with the permissions of the edge agent
|
||||
- Limit edge job permissions appropriately
|
||||
- Regularly rotate edge keys
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Edge Environment Not Connecting
|
||||
- Check network connectivity from edge device
|
||||
- Verify edge key is correct
|
||||
- Check firewall rules
|
||||
- Review edge agent logs
|
||||
|
||||
### Stack Deployment Failures
|
||||
- Verify stack syntax
|
||||
- Check image availability on edge devices
|
||||
- Review resource constraints
|
||||
- Check edge agent permissions
|
||||
|
||||
### Edge Group Issues
|
||||
- Verify tag configuration for dynamic groups
|
||||
- Check endpoint assignments for static groups
|
||||
- Review group membership rules
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- Portainer Business Edition 2.19+ (for full edge features)
|
||||
- Valid Portainer API token
|
||||
- Edge agents deployed on target devices
|
131
create_nginx_stack.py
Normal file
131
create_nginx_stack.py
Normal file
@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create nginx02 stack from Git repository"""
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Configuration
|
||||
PORTAINER_URL = "https://partner.portainer.live"
|
||||
PORTAINER_API_KEY = "ptr_uMqreULEo44qvuszgG8oZWdjkDx3K9HBXSmjd+F/vDE="
|
||||
|
||||
# Stack configuration
|
||||
STACK_NAME = "nginx02"
|
||||
ENVIRONMENT_ID = 6 # docker03
|
||||
REPOSITORY_URL = "https://git.oe74.net/adelorenzo/portainer-yaml"
|
||||
REPOSITORY_REF = "main" # or master
|
||||
COMPOSE_PATH = "nginx-cmpose.yaml"
|
||||
GIT_USERNAME = "adelorenzo"
|
||||
GIT_PASSWORD = "dimi2014"
|
||||
|
||||
def create_stack_from_git():
|
||||
"""Create a stack from Git repository"""
|
||||
|
||||
# Build request data
|
||||
data = {
|
||||
"Name": STACK_NAME,
|
||||
"EndpointId": ENVIRONMENT_ID,
|
||||
"GitConfig": {
|
||||
"URL": REPOSITORY_URL,
|
||||
"ReferenceName": REPOSITORY_REF,
|
||||
"ComposeFilePathInRepository": COMPOSE_PATH,
|
||||
"Authentication": {
|
||||
"Username": GIT_USERNAME,
|
||||
"Password": GIT_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Headers
|
||||
headers = {
|
||||
"X-API-Key": PORTAINER_API_KEY,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# API endpoint
|
||||
url = f"{PORTAINER_URL}/api/stacks"
|
||||
|
||||
print(f"Creating stack '{STACK_NAME}' from Git repository...")
|
||||
print(f"Repository: {REPOSITORY_URL}")
|
||||
print(f"Compose file: {COMPOSE_PATH}")
|
||||
print(f"Environment ID: {ENVIRONMENT_ID}")
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers)
|
||||
|
||||
if response.status_code == 200 or response.status_code == 201:
|
||||
result = response.json()
|
||||
print(f"\n✅ Stack created successfully!")
|
||||
print(f"Stack ID: {result['Id']}")
|
||||
print(f"Stack Name: {result['Name']}")
|
||||
return result
|
||||
else:
|
||||
print(f"\n❌ Error creating stack: {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
# Try to parse error message
|
||||
try:
|
||||
error_data = response.json()
|
||||
if "message" in error_data:
|
||||
print(f"Error message: {error_data['message']}")
|
||||
elif "details" in error_data:
|
||||
print(f"Error details: {error_data['details']}")
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Exception occurred: {str(e)}")
|
||||
return None
|
||||
|
||||
def list_existing_stacks():
|
||||
"""List existing stacks to check for references"""
|
||||
|
||||
headers = {
|
||||
"X-API-Key": PORTAINER_API_KEY
|
||||
}
|
||||
|
||||
url = f"{PORTAINER_URL}/api/stacks"
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
stacks = response.json()
|
||||
print("\n📚 Existing stacks:")
|
||||
for stack in stacks:
|
||||
if stack.get("EndpointId") == ENVIRONMENT_ID:
|
||||
print(f" - {stack['Name']} (ID: {stack['Id']})")
|
||||
if stack.get("GitConfig"):
|
||||
print(f" Git: {stack['GitConfig']['URL']}")
|
||||
print(f" Path: {stack['GitConfig'].get('ComposeFilePathInRepository', 'N/A')}")
|
||||
return stacks
|
||||
else:
|
||||
print(f"Error listing stacks: {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Exception listing stacks: {str(e)}")
|
||||
return []
|
||||
|
||||
if __name__ == "__main__":
|
||||
# First, list existing stacks
|
||||
print("Checking existing stacks on environment docker03...")
|
||||
existing_stacks = list_existing_stacks()
|
||||
|
||||
# Check if stack already exists
|
||||
stack_exists = any(s['Name'] == STACK_NAME and s.get('EndpointId') == ENVIRONMENT_ID for s in existing_stacks)
|
||||
|
||||
if stack_exists:
|
||||
print(f"\n⚠️ Stack '{STACK_NAME}' already exists on this environment!")
|
||||
response = input("Do you want to continue anyway? (y/n): ")
|
||||
if response.lower() != 'y':
|
||||
print("Aborting...")
|
||||
sys.exit(0)
|
||||
|
||||
# Create the stack
|
||||
print("\n" + "="*50)
|
||||
result = create_stack_from_git()
|
||||
|
||||
if result:
|
||||
print("\n🎉 Stack deployment completed!")
|
||||
else:
|
||||
print("\n😞 Stack deployment failed!")
|
1019
portainer_edge_server.py
Normal file
1019
portainer_edge_server.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,12 +2,12 @@ Metadata-Version: 2.4
|
||||
Name: portainer-core-mcp
|
||||
Version: 0.1.0
|
||||
Summary: Portainer Core MCP Server - Authentication and User Management
|
||||
Author-email: Your Name <your.email@example.com>
|
||||
Author-email: Portainer MCP Team <support@portainer.io>
|
||||
License: MIT
|
||||
Project-URL: Homepage, https://github.com/yourusername/portainer-core-mcp
|
||||
Project-URL: Documentation, https://github.com/yourusername/portainer-core-mcp#readme
|
||||
Project-URL: Repository, https://github.com/yourusername/portainer-core-mcp
|
||||
Project-URL: Issues, https://github.com/yourusername/portainer-core-mcp/issues
|
||||
Project-URL: Homepage, https://github.com/portainer/portainer-mcp-core
|
||||
Project-URL: Documentation, https://github.com/portainer/portainer-mcp-core#readme
|
||||
Project-URL: Repository, https://github.com/portainer/portainer-mcp-core
|
||||
Project-URL: Issues, https://github.com/portainer/portainer-mcp-core/issues
|
||||
Classifier: Development Status :: 3 - Alpha
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
@ -41,62 +41,145 @@ Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
||||
|
||||
# Portainer Core MCP Server
|
||||
|
||||
A Model Context Protocol (MCP) server for Portainer Business Edition authentication and user management.
|
||||
A Model Context Protocol (MCP) server that provides authentication and user management functionality for Portainer Business Edition.
|
||||
|
||||
## Features
|
||||
|
||||
- **Authentication & Session Management**: JWT token handling and user authentication
|
||||
- **User Management**: Create, read, update, and delete users
|
||||
- **Settings Management**: Retrieve and update Portainer settings
|
||||
- **Secure Token Handling**: Automatic token refresh and secure storage
|
||||
- **Error Handling**: Comprehensive error handling with retry logic
|
||||
- **Circuit Breaker**: Fault tolerance for external API calls
|
||||
- **Authentication**: JWT token-based authentication with Portainer API
|
||||
- **User Management**: Complete CRUD operations for users
|
||||
- **Settings Management**: Portainer instance configuration
|
||||
- **Health Monitoring**: Server and service health checks
|
||||
- **Fault Tolerance**: Circuit breaker pattern with automatic recovery
|
||||
- **Structured Logging**: JSON-formatted logs with correlation IDs
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- Portainer Business Edition instance
|
||||
- Valid Portainer API key
|
||||
|
||||
## Installation
|
||||
|
||||
### Using pip
|
||||
|
||||
```bash
|
||||
pip install portainer-core-mcp
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Using uv (recommended)
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
### Using uvx (run without installing)
|
||||
|
||||
```bash
|
||||
# No installation needed - runs directly
|
||||
uvx --from . portainer-core-mcp
|
||||
```
|
||||
|
||||
### Using npm/npx
|
||||
|
||||
```bash
|
||||
npm install -g portainer-core-mcp
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file or set environment variables:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
PORTAINER_URL=https://your-portainer-instance.com
|
||||
PORTAINER_API_KEY=your-api-token # Optional, for API key authentication
|
||||
PORTAINER_USERNAME=admin # For username/password authentication
|
||||
PORTAINER_PASSWORD=your-password # For username/password authentication
|
||||
PORTAINER_API_KEY=your-api-key-here
|
||||
|
||||
# Optional
|
||||
HTTP_TIMEOUT=30
|
||||
MAX_RETRIES=3
|
||||
LOG_LEVEL=INFO
|
||||
DEBUG=false
|
||||
```
|
||||
|
||||
### Generate API Key
|
||||
|
||||
1. Log in to your Portainer instance
|
||||
2. Go to **User Settings** > **API Tokens**
|
||||
3. Click **Add API Token**
|
||||
4. Copy the generated token
|
||||
|
||||
## Usage
|
||||
|
||||
### As MCP Server
|
||||
### Start the Server
|
||||
|
||||
#### Using Python
|
||||
|
||||
```bash
|
||||
portainer-core-mcp
|
||||
python run_server.py
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
#### Using uv
|
||||
|
||||
```python
|
||||
from portainer_core.server import PortainerCoreMCPServer
|
||||
|
||||
server = PortainerCoreMCPServer()
|
||||
# Use server instance
|
||||
```bash
|
||||
uv run python run_server.py
|
||||
```
|
||||
|
||||
## Available MCP Tools
|
||||
#### Using uvx
|
||||
|
||||
```bash
|
||||
uvx --from . portainer-core-mcp
|
||||
```
|
||||
|
||||
#### Using npm/npx
|
||||
|
||||
```bash
|
||||
npx portainer-core-mcp
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy example environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit configuration
|
||||
nano .env
|
||||
|
||||
# Start server (choose your preferred method)
|
||||
python run_server.py
|
||||
# OR
|
||||
uvx --from . portainer-core-mcp
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
The MCP server provides the following tools:
|
||||
|
||||
### Authentication
|
||||
- `authenticate` - Login with username/password
|
||||
- `generate_token` - Generate API token
|
||||
- `get_current_user` - Get authenticated user info
|
||||
- `generate_token` - Generate API tokens
|
||||
- `get_current_user` - Get current user info
|
||||
|
||||
### User Management
|
||||
- `list_users` - List all users
|
||||
- `create_user` - Create new user
|
||||
- `update_user` - Update user details
|
||||
- `delete_user` - Delete user
|
||||
|
||||
### Settings
|
||||
- `get_settings` - Get Portainer settings
|
||||
- `update_settings` - Update settings
|
||||
- `update_settings` - Update configuration
|
||||
|
||||
### Health
|
||||
- `health_check` - Server health status
|
||||
|
||||
## Available Resources
|
||||
|
||||
- `portainer://users` - User management data
|
||||
- `portainer://settings` - Configuration settings
|
||||
- `portainer://health` - Server health status
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -4,11 +4,18 @@ src/portainer_core/__init__.py
|
||||
src/portainer_core/config.py
|
||||
src/portainer_core/server.py
|
||||
src/portainer_core/models/__init__.py
|
||||
src/portainer_core/models/auth.py
|
||||
src/portainer_core/models/settings.py
|
||||
src/portainer_core/models/users.py
|
||||
src/portainer_core/services/__init__.py
|
||||
src/portainer_core/services/auth.py
|
||||
src/portainer_core/services/base.py
|
||||
src/portainer_core/services/settings.py
|
||||
src/portainer_core/services/users.py
|
||||
src/portainer_core/utils/__init__.py
|
||||
src/portainer_core/utils/errors.py
|
||||
src/portainer_core/utils/logging.py
|
||||
src/portainer_core/utils/tokens.py
|
||||
src/portainer_core_mcp.egg-info/PKG-INFO
|
||||
src/portainer_core_mcp.egg-info/SOURCES.txt
|
||||
src/portainer_core_mcp.egg-info/dependency_links.txt
|
||||
|
@ -1,2 +1,2 @@
|
||||
[console_scripts]
|
||||
portainer-core-mcp = portainer_core.server:main
|
||||
portainer-core-mcp = portainer_core.server:main_sync
|
||||
|
44
test_edge_api.sh
Executable file
44
test_edge_api.sh
Executable file
@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test Portainer Edge API endpoints
|
||||
|
||||
echo "🚀 Testing Portainer Edge API"
|
||||
echo "URL: $PORTAINER_URL"
|
||||
echo "API Key: ***${PORTAINER_API_KEY: -4}"
|
||||
echo ""
|
||||
|
||||
# Test edge environments
|
||||
echo "🌐 Testing Edge Environments..."
|
||||
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||
"$PORTAINER_URL/api/endpoints?types=4" | jq '.[] | select(.Type == 4) | {name: .Name, id: .Id, status: .Status}' 2>/dev/null || echo "No edge environments found or jq not installed"
|
||||
|
||||
echo ""
|
||||
|
||||
# Test edge groups
|
||||
echo "👥 Testing Edge Groups..."
|
||||
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||
"$PORTAINER_URL/api/edge_groups" | jq '.[0:3] | .[] | {name: .Name, id: .Id, dynamic: .Dynamic}' 2>/dev/null || echo "No edge groups found or jq not installed"
|
||||
|
||||
echo ""
|
||||
|
||||
# Test edge stacks
|
||||
echo "📚 Testing Edge Stacks..."
|
||||
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||
"$PORTAINER_URL/api/edge_stacks" | jq '.[0:3] | .[] | {name: .Name, id: .Id, groups: .EdgeGroups}' 2>/dev/null || echo "No edge stacks found or jq not installed"
|
||||
|
||||
echo ""
|
||||
|
||||
# Test edge jobs
|
||||
echo "💼 Testing Edge Jobs..."
|
||||
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||
"$PORTAINER_URL/api/edge_jobs" | jq '.[0:3] | .[] | {name: .Name, id: .Id, recurring: .Recurring}' 2>/dev/null || echo "No edge jobs found or jq not installed"
|
||||
|
||||
echo ""
|
||||
|
||||
# Test edge settings
|
||||
echo "⚙️ Testing Edge Settings..."
|
||||
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||
"$PORTAINER_URL/api/settings" | jq '.Edge | {checkin: .CheckinInterval, command: .CommandInterval, ping: .PingInterval}' 2>/dev/null || echo "Failed to get edge settings or jq not installed"
|
||||
|
||||
echo ""
|
||||
echo "✅ Edge API test completed!"
|
163
test_edge_server.py
Executable file
163
test_edge_server.py
Executable file
@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Portainer Edge MCP Server
|
||||
Tests edge environments, stacks, groups, and jobs functionality
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import os
|
||||
import json
|
||||
|
||||
PORTAINER_URL = os.getenv("PORTAINER_URL", "").rstrip("/")
|
||||
PORTAINER_API_KEY = os.getenv("PORTAINER_API_KEY", "")
|
||||
|
||||
async def make_request(method: str, endpoint: str, json_data=None, params=None):
|
||||
"""Make a request to Portainer API"""
|
||||
url = f"{PORTAINER_URL}{endpoint}"
|
||||
headers = {"X-API-Key": PORTAINER_API_KEY}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(
|
||||
method,
|
||||
url,
|
||||
json=json_data,
|
||||
params=params,
|
||||
headers=headers
|
||||
) as response:
|
||||
text = await response.text()
|
||||
if response.status >= 400:
|
||||
print(f"❌ Error {response.status}: {text}")
|
||||
return None
|
||||
return json.loads(text) if text else {}
|
||||
|
||||
async def test_edge_environments():
|
||||
"""Test edge environment operations"""
|
||||
print("\n🌐 Testing Edge Environments...")
|
||||
|
||||
# List all endpoints/environments
|
||||
endpoints = await make_request("GET", "/api/endpoints")
|
||||
if endpoints:
|
||||
edge_envs = [e for e in endpoints if e.get("Type") == 4] # Type 4 is Edge
|
||||
print(f"✅ Found {len(edge_envs)} edge environments")
|
||||
|
||||
if edge_envs:
|
||||
# Get details of first edge environment
|
||||
env = edge_envs[0]
|
||||
print(f" • {env['Name']} (ID: {env['Id']})")
|
||||
print(f" Status: {env.get('Status', 'Unknown')}")
|
||||
print(f" Edge ID: {env.get('EdgeID', 'N/A')}")
|
||||
|
||||
# Get edge status
|
||||
status = await make_request("GET", f"/api/endpoints/{env['Id']}/edge/status")
|
||||
if status:
|
||||
print(f" Check-in: {status.get('CheckinTime', 'Never')}")
|
||||
else:
|
||||
print("❌ Failed to list environments")
|
||||
|
||||
async def test_edge_groups():
|
||||
"""Test edge group operations"""
|
||||
print("\n👥 Testing Edge Groups...")
|
||||
|
||||
# List edge groups
|
||||
groups = await make_request("GET", "/api/edge_groups")
|
||||
if groups:
|
||||
print(f"✅ Found {len(groups)} edge groups")
|
||||
for group in groups[:3]: # Show first 3
|
||||
print(f" • {group['Name']} (ID: {group['Id']})")
|
||||
print(f" Dynamic: {'Yes' if group.get('Dynamic') else 'No'}")
|
||||
print(f" Endpoints: {len(group.get('Endpoints', []))}")
|
||||
else:
|
||||
print("❌ Failed to list edge groups")
|
||||
|
||||
# Try to create a test edge group
|
||||
print("\n📝 Creating test edge group...")
|
||||
test_group_data = {
|
||||
"Name": "test-edge-group",
|
||||
"Dynamic": False,
|
||||
"TagIds": []
|
||||
}
|
||||
|
||||
new_group = await make_request("POST", "/api/edge_groups", json_data=test_group_data)
|
||||
if new_group:
|
||||
print(f"✅ Created edge group: {new_group['Name']} (ID: {new_group['Id']})")
|
||||
|
||||
# Clean up - delete the test group
|
||||
await make_request("DELETE", f"/api/edge_groups/{new_group['Id']}")
|
||||
print("🗑️ Cleaned up test edge group")
|
||||
else:
|
||||
print("❌ Failed to create edge group")
|
||||
|
||||
async def test_edge_stacks():
|
||||
"""Test edge stack operations"""
|
||||
print("\n📚 Testing Edge Stacks...")
|
||||
|
||||
# List edge stacks
|
||||
stacks = await make_request("GET", "/api/edge_stacks")
|
||||
if stacks:
|
||||
print(f"✅ Found {len(stacks)} edge stacks")
|
||||
for stack in stacks[:3]: # Show first 3
|
||||
print(f" • {stack['Name']} (ID: {stack['Id']})")
|
||||
print(f" Type: {stack.get('StackType', 'Unknown')}")
|
||||
print(f" Groups: {len(stack.get('EdgeGroups', []))}")
|
||||
|
||||
# Check if it has GitOps
|
||||
if stack.get("GitConfig") and stack.get("AutoUpdate"):
|
||||
print(f" GitOps: Enabled ({stack['AutoUpdate'].get('Interval', 'N/A')})")
|
||||
else:
|
||||
print("❌ Failed to list edge stacks")
|
||||
|
||||
async def test_edge_jobs():
|
||||
"""Test edge job operations"""
|
||||
print("\n💼 Testing Edge Jobs...")
|
||||
|
||||
# List edge jobs
|
||||
jobs = await make_request("GET", "/api/edge_jobs")
|
||||
if jobs:
|
||||
print(f"✅ Found {len(jobs)} edge jobs")
|
||||
for job in jobs[:3]: # Show first 3
|
||||
print(f" • {job['Name']} (ID: {job['Id']})")
|
||||
print(f" Recurring: {'Yes' if job.get('Recurring') else 'No'}")
|
||||
if job.get('CronExpression'):
|
||||
print(f" Schedule: {job['CronExpression']}")
|
||||
print(f" Target Groups: {len(job.get('EdgeGroups', []))}")
|
||||
else:
|
||||
print("❌ Failed to list edge jobs")
|
||||
|
||||
async def test_edge_settings():
|
||||
"""Test edge settings"""
|
||||
print("\n⚙️ Testing Edge Settings...")
|
||||
|
||||
# Get settings
|
||||
settings = await make_request("GET", "/api/settings")
|
||||
if settings and settings.get("Edge"):
|
||||
edge_settings = settings["Edge"]
|
||||
print("✅ Edge Settings:")
|
||||
print(f" • Check-in Interval: {edge_settings.get('CheckinInterval', 'N/A')} seconds")
|
||||
print(f" • Command Interval: {edge_settings.get('CommandInterval', 'N/A')} seconds")
|
||||
print(f" • Ping Interval: {edge_settings.get('PingInterval', 'N/A')} seconds")
|
||||
print(f" • Tunnel Server: {edge_settings.get('TunnelServerAddress', 'Not configured')}")
|
||||
else:
|
||||
print("❌ Failed to get edge settings")
|
||||
|
||||
async def main():
|
||||
"""Run all edge tests"""
|
||||
print("🚀 Portainer Edge API Tests")
|
||||
print(f"URL: {PORTAINER_URL}")
|
||||
print(f"API Key: {'***' + PORTAINER_API_KEY[-4:] if PORTAINER_API_KEY else 'Not set'}")
|
||||
|
||||
if not PORTAINER_URL or not PORTAINER_API_KEY:
|
||||
print("\n❌ Please set PORTAINER_URL and PORTAINER_API_KEY environment variables")
|
||||
return
|
||||
|
||||
# Run tests
|
||||
await test_edge_environments()
|
||||
await test_edge_groups()
|
||||
await test_edge_stacks()
|
||||
await test_edge_jobs()
|
||||
await test_edge_settings()
|
||||
|
||||
print("\n✅ Edge API tests completed!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
Loading…
Reference in New Issue
Block a user