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:
Adolfo Delorenzo 2025-07-18 23:59:56 -03:00
parent d7055a912e
commit 7a1abbe243
8 changed files with 1789 additions and 30 deletions

312
README_EDGE.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,12 @@ Metadata-Version: 2.4
Name: portainer-core-mcp Name: portainer-core-mcp
Version: 0.1.0 Version: 0.1.0
Summary: Portainer Core MCP Server - Authentication and User Management 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 License: MIT
Project-URL: Homepage, https://github.com/yourusername/portainer-core-mcp Project-URL: Homepage, https://github.com/portainer/portainer-mcp-core
Project-URL: Documentation, https://github.com/yourusername/portainer-core-mcp#readme Project-URL: Documentation, https://github.com/portainer/portainer-mcp-core#readme
Project-URL: Repository, https://github.com/yourusername/portainer-core-mcp Project-URL: Repository, https://github.com/portainer/portainer-mcp-core
Project-URL: Issues, https://github.com/yourusername/portainer-core-mcp/issues Project-URL: Issues, https://github.com/portainer/portainer-mcp-core/issues
Classifier: Development Status :: 3 - Alpha Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License Classifier: License :: OSI Approved :: MIT License
@ -41,62 +41,145 @@ Requires-Dist: pre-commit>=3.0.0; extra == "dev"
# Portainer Core MCP Server # 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 ## Features
- **Authentication & Session Management**: JWT token handling and user authentication - **Authentication**: JWT token-based authentication with Portainer API
- **User Management**: Create, read, update, and delete users - **User Management**: Complete CRUD operations for users
- **Settings Management**: Retrieve and update Portainer settings - **Settings Management**: Portainer instance configuration
- **Secure Token Handling**: Automatic token refresh and secure storage - **Health Monitoring**: Server and service health checks
- **Error Handling**: Comprehensive error handling with retry logic - **Fault Tolerance**: Circuit breaker pattern with automatic recovery
- **Circuit Breaker**: Fault tolerance for external API calls - **Structured Logging**: JSON-formatted logs with correlation IDs
## Requirements
- Python 3.8+
- Portainer Business Edition instance
- Valid Portainer API key
## Installation ## Installation
### Using pip
```bash ```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 ## Configuration
Set the following environment variables: ### Environment Variables
Create a `.env` file or set environment variables:
```bash ```bash
# Required
PORTAINER_URL=https://your-portainer-instance.com PORTAINER_URL=https://your-portainer-instance.com
PORTAINER_API_KEY=your-api-token # Optional, for API key authentication PORTAINER_API_KEY=your-api-key-here
PORTAINER_USERNAME=admin # For username/password authentication
PORTAINER_PASSWORD=your-password # For username/password authentication # 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 ## Usage
### As MCP Server ### Start the Server
#### Using Python
```bash ```bash
portainer-core-mcp python run_server.py
``` ```
### Programmatic Usage #### Using uv
```python ```bash
from portainer_core.server import PortainerCoreMCPServer uv run python run_server.py
server = PortainerCoreMCPServer()
# Use server instance
``` ```
## 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 - `authenticate` - Login with username/password
- `generate_token` - Generate API token - `generate_token` - Generate API tokens
- `get_current_user` - Get authenticated user info - `get_current_user` - Get current user info
### User Management
- `list_users` - List all users - `list_users` - List all users
- `create_user` - Create new user - `create_user` - Create new user
- `update_user` - Update user details - `update_user` - Update user details
- `delete_user` - Delete user - `delete_user` - Delete user
### Settings
- `get_settings` - Get Portainer 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 ## Development

View File

@ -4,11 +4,18 @@ src/portainer_core/__init__.py
src/portainer_core/config.py src/portainer_core/config.py
src/portainer_core/server.py src/portainer_core/server.py
src/portainer_core/models/__init__.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/__init__.py
src/portainer_core/services/auth.py
src/portainer_core/services/base.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/__init__.py
src/portainer_core/utils/errors.py src/portainer_core/utils/errors.py
src/portainer_core/utils/logging.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/PKG-INFO
src/portainer_core_mcp.egg-info/SOURCES.txt src/portainer_core_mcp.egg-info/SOURCES.txt
src/portainer_core_mcp.egg-info/dependency_links.txt src/portainer_core_mcp.egg-info/dependency_links.txt

View File

@ -1,2 +1,2 @@
[console_scripts] [console_scripts]
portainer-core-mcp = portainer_core.server:main portainer-core-mcp = portainer_core.server:main_sync

44
test_edge_api.sh Executable file
View 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
View 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())