mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
Merge pull request #2654 from yueguobin/feature/extended-statistics
Feature/extended statistics
This commit is contained in:
commit
9290b37292
96
docs/features/compute-controller-setup.md
Normal file
96
docs/features/compute-controller-setup.md
Normal file
@ -0,0 +1,96 @@
|
||||
# Controller + Compute Setup
|
||||
|
||||
This document describes the minimum configuration required to set up a GNS3 Controller with remote Compute nodes.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
- **Compute**: Runs individual nodes (QEMU, Docker, etc.) and provides resource monitoring
|
||||
- **Controller**: Manages multiple computes, projects, and provides the REST API
|
||||
- **Database**: Controller uses SQLite to store projects, nodes, and compute registration
|
||||
|
||||
## Minimum Configuration
|
||||
|
||||
### 1. Compute Node Configuration
|
||||
|
||||
Create the configuration file at `~/.config/GNS3/3.1/gns3_server.conf`:
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
host = 0.0.0.0
|
||||
port = 3080
|
||||
compute_username = gns3
|
||||
compute_password = gns3
|
||||
```
|
||||
|
||||
Start the Compute:
|
||||
```bash
|
||||
gns3server
|
||||
```
|
||||
|
||||
### 2. Controller Node Configuration
|
||||
|
||||
Create the configuration file at `~/.config/GNS3/3.1/gns3_server.conf`:
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
host = 192.168.1.140
|
||||
port = 3080
|
||||
compute_username = gns3
|
||||
compute_password = gns3
|
||||
```
|
||||
|
||||
Start the Controller:
|
||||
```bash
|
||||
gns3server
|
||||
```
|
||||
|
||||
### 3. Register Compute with Controller
|
||||
|
||||
Use the API to register a remote compute:
|
||||
|
||||
```bash
|
||||
POST /v3/computes
|
||||
{
|
||||
"protocol": "http",
|
||||
"host": "192.168.1.x",
|
||||
"port": 3080,
|
||||
"user": "gns3",
|
||||
"password": "gns3"
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Host Configuration
|
||||
|
||||
- **Controller `host`**: If set to `0.0.0.0`, it will be changed to `127.0.0.1`, which breaks cross-subnet link creation
|
||||
- **Solution**: Always use the actual IP address for Controller's `host` field
|
||||
|
||||
### Password Configuration
|
||||
|
||||
- If `compute_password` is not set, a random 16-character password is auto-generated on startup
|
||||
- The Controller must use the same credentials as the Compute's configuration
|
||||
|
||||
### Network Requirements
|
||||
|
||||
- Controller and Compute must be on the same LAN for cross-compute links to work
|
||||
- UDP tunnel is used for cross-compute links, requiring network connectivity on UDP ports
|
||||
|
||||
### Configuration File Location
|
||||
|
||||
- Default location: `~/.config/GNS3/3.1/gns3_server.conf`
|
||||
- Version `3.0` uses `~/.config/GNS3/3.0/`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 401 Unauthorized on Compute Connect
|
||||
|
||||
1. Check that Compute's `compute_username` and `compute_password` match what was passed to the API
|
||||
2. Verify the Compute's configuration file is correctly loaded
|
||||
3. Ensure the `[Server]` section is used (not `[Controller]`)
|
||||
|
||||
### No Common Subnet Error
|
||||
|
||||
1. Verify Controller's `host` is set to its actual IP, not `0.0.0.0`
|
||||
2. Ensure both machines are on the same network
|
||||
3. Check firewall rules allow UDP communication
|
||||
176
docs/features/statistics-api.md
Normal file
176
docs/features/statistics-api.md
Normal file
@ -0,0 +1,176 @@
|
||||
# Statistics API
|
||||
|
||||
## `GET /statistics`
|
||||
|
||||
Returns aggregated server statistics including compute resources, projects, nodes, and links.
|
||||
|
||||
**Authentication:** Requires active user session
|
||||
|
||||
**Method:** `GET`
|
||||
|
||||
**URL:** `http://server:3080/v1/statistics`
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"computes": [
|
||||
{
|
||||
"compute_id": "string",
|
||||
"compute_name": "string",
|
||||
"statistics": {
|
||||
"memory_total": 16777216000,
|
||||
"memory_free": 8000000000,
|
||||
"memory_used": 8777216000,
|
||||
"swap_total": 2147479552,
|
||||
"swap_free": 1500000000,
|
||||
"swap_used": 647279552,
|
||||
"cpu_usage_percent": 45,
|
||||
"memory_usage_percent": 52,
|
||||
"swap_usage_percent": 30,
|
||||
"disk_usage_percent": 67,
|
||||
"load_average_percent": [12, 8, 5]
|
||||
}
|
||||
}
|
||||
],
|
||||
"projects": {
|
||||
"total": 5,
|
||||
"opened": 3,
|
||||
"closed": 2
|
||||
},
|
||||
"nodes": {
|
||||
"total": 42,
|
||||
"open_project_nodes": 30,
|
||||
"closed_project_nodes": 12,
|
||||
"by_type": {
|
||||
"qemu": 20,
|
||||
"docker": 12,
|
||||
"dynamips": 6,
|
||||
"vpcs": 4
|
||||
},
|
||||
"by_status": {
|
||||
"started": 25,
|
||||
"stopped": 12,
|
||||
"suspended": 5
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"total": 38,
|
||||
"capturing": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
#### `computes`
|
||||
|
||||
Array of compute node statistics. Each compute reports:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `compute_id` | string | Unique identifier for the compute |
|
||||
| `compute_name` | string | Human-readable name |
|
||||
| `statistics` | object | Resource usage statistics |
|
||||
|
||||
#### `computes[].statistics`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `memory_total` | integer | Total physical memory in bytes |
|
||||
| `memory_free` | integer | Free memory in bytes |
|
||||
| `memory_used` | integer | Used memory in bytes |
|
||||
| `swap_total` | integer | Total swap space in bytes |
|
||||
| `swap_free` | integer | Free swap space in bytes |
|
||||
| `swap_used` | integer | Used swap space in bytes |
|
||||
| `cpu_usage_percent` | integer | CPU usage percentage (0-100) |
|
||||
| `memory_usage_percent` | integer | Memory usage percentage (0-100) |
|
||||
| `swap_usage_percent` | integer | Swap usage percentage (0-100) |
|
||||
| `disk_usage_percent` | integer | Disk usage percentage for project directory (0-100) |
|
||||
| `load_average_percent` | integer[] | Load average as percentage per CPU core (1/5/15 min) |
|
||||
|
||||
#### `projects`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total number of projects |
|
||||
| `opened` | integer | Number of projects currently opened |
|
||||
| `closed` | integer | Number of projects currently closed |
|
||||
|
||||
#### `nodes`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total number of nodes across all projects |
|
||||
| `open_project_nodes` | integer | Nodes in opened projects (has real status) |
|
||||
| `closed_project_nodes` | integer | Nodes in closed projects (loaded from topology JSON, no status) |
|
||||
| `by_type` | object | Node count grouped by node type (qemu, docker, dynamips, etc.) |
|
||||
| `by_status` | object | Node count grouped by status (only for `open_project_nodes`) |
|
||||
|
||||
**Note on `by_status`:** Status is a runtime attribute only available for nodes in opened projects. Closed projects store topology data in JSON format which does not include runtime status. Therefore `by_status` only reflects nodes from opened projects.
|
||||
|
||||
Valid node statuses: `started`, `stopped`, `suspended`
|
||||
|
||||
#### `links`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total number of links across all projects |
|
||||
| `capturing` | integer | Number of links currently capturing packets |
|
||||
|
||||
### Example Usage
|
||||
|
||||
```bash
|
||||
# Get statistics
|
||||
curl -X GET http://localhost:3080/v1/statistics \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### Dashboard Integration
|
||||
|
||||
This API is designed for monitoring dashboards that need:
|
||||
|
||||
- **System health**: CPU, memory, disk from `computes[].statistics`
|
||||
- **Project overview**: Project counts from `projects`
|
||||
- **Node inventory**: Node counts by type and status from `nodes`
|
||||
- **Capture monitoring**: Active capture sessions from `links.capturing`
|
||||
|
||||
### Error Responses
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 401 | Unauthorized - invalid or missing session |
|
||||
| 500 | Internal server error |
|
||||
|
||||
### Future Optimizations
|
||||
|
||||
#### Per-Compose Node Statistics
|
||||
|
||||
Currently `nodes` are aggregated globally. Future enhancement could add per-compute breakdown:
|
||||
|
||||
```json
|
||||
"nodes": {
|
||||
"total": 42,
|
||||
"by_compute": {
|
||||
"local": {
|
||||
"total": 30,
|
||||
"open_project_nodes": 20,
|
||||
"closed_project_nodes": 10,
|
||||
"by_type": { "qemu": 20, "docker": 10 }
|
||||
},
|
||||
"remote-server-1": {
|
||||
"total": 12,
|
||||
"open_project_nodes": 10,
|
||||
"closed_project_nodes": 2,
|
||||
"by_type": { "docker": 12 }
|
||||
}
|
||||
},
|
||||
"by_type": { "qemu": 20, "docker": 22 },
|
||||
"open_project_nodes": 30,
|
||||
"closed_project_nodes": 12,
|
||||
"by_type": { "qemu": 20, "docker": 22 },
|
||||
"by_status": { "started": 25, "stopped": 12, "suspended": 5 }
|
||||
}
|
||||
```
|
||||
|
||||
This requires tracking which compute each node runs on (Node._compute).
|
||||
@ -24,7 +24,7 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.routing import Mount
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.controller import Controller
|
||||
@ -162,19 +162,91 @@ async def update_iou_license(iou_license: schemas.IOULicense) -> schemas.IOULice
|
||||
|
||||
|
||||
@router.get("/statistics", dependencies=[Depends(get_current_active_user)])
|
||||
async def statistics() -> List[dict]:
|
||||
async def statistics() -> dict:
|
||||
"""
|
||||
Return server statistics.
|
||||
Return server statistics including compute resources, projects, and nodes.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
|
||||
# Compute statistics (existing behavior)
|
||||
compute_statistics = []
|
||||
for compute in list(Controller.instance().computes.values()):
|
||||
for compute in list(controller.computes.values()):
|
||||
try:
|
||||
r = await compute.get("/statistics")
|
||||
compute_statistics.append({"compute_id": compute.id, "compute_name": compute.name, "statistics": r.json})
|
||||
except ControllerError as e:
|
||||
log.error(f"Could not retrieve statistics on compute {compute.name}: {e}")
|
||||
return compute_statistics
|
||||
|
||||
# Project statistics
|
||||
projects = list(controller.projects.values())
|
||||
project_stats = {
|
||||
"total": len(projects),
|
||||
"opened": sum(1 for p in projects if p.status == "opened"),
|
||||
"closed": sum(1 for p in projects if p.status == "closed"),
|
||||
}
|
||||
|
||||
# Node statistics - distinguish open vs closed project nodes
|
||||
open_project_nodes = []
|
||||
closed_project_nodes = []
|
||||
node_by_type = {}
|
||||
node_by_status = {}
|
||||
|
||||
for project in projects:
|
||||
nodes = project.nodes.values()
|
||||
if project.status == "closed":
|
||||
closed_project_nodes.extend(nodes)
|
||||
else:
|
||||
open_project_nodes.extend(nodes)
|
||||
|
||||
# Open project nodes have real status
|
||||
for node in open_project_nodes:
|
||||
node_type = getattr(node, "node_type", "unknown")
|
||||
node_status = getattr(node, "status", "unknown")
|
||||
node_by_type[node_type] = node_by_type.get(node_type, 0) + 1
|
||||
node_by_status[node_status] = node_by_status.get(node_status, 0) + 1
|
||||
|
||||
# Closed project nodes don't have status, count them separately
|
||||
for node in closed_project_nodes:
|
||||
if isinstance(node, dict):
|
||||
node_type = node.get("node_type", "unknown")
|
||||
else:
|
||||
node_type = getattr(node, "node_type", "unknown")
|
||||
node_by_type[node_type] = node_by_type.get(node_type, 0) + 1
|
||||
|
||||
node_stats = {
|
||||
"total": len(open_project_nodes) + len(closed_project_nodes),
|
||||
"open_project_nodes": len(open_project_nodes),
|
||||
"closed_project_nodes": len(closed_project_nodes),
|
||||
"by_type": node_by_type,
|
||||
"by_status": node_by_status,
|
||||
}
|
||||
|
||||
# Link statistics
|
||||
all_links = []
|
||||
for project in projects:
|
||||
all_links.extend(project.links.values())
|
||||
|
||||
def is_capturing(link):
|
||||
if hasattr(link, "capturing"):
|
||||
return link.capturing
|
||||
if isinstance(link, dict):
|
||||
return link.get("capturing", False)
|
||||
return False
|
||||
|
||||
link_capturing = sum(1 for link in all_links if is_capturing(link))
|
||||
|
||||
link_stats = {
|
||||
"total": len(all_links),
|
||||
"capturing": link_capturing,
|
||||
}
|
||||
|
||||
return {
|
||||
"computes": compute_statistics,
|
||||
"projects": project_stats,
|
||||
"nodes": node_stats,
|
||||
"links": link_stats,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/notifications", dependencies=[Depends(get_current_active_user)])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user