diff --git a/docs/features/compute-controller-setup.md b/docs/features/compute-controller-setup.md new file mode 100644 index 000000000..59dfff39e --- /dev/null +++ b/docs/features/compute-controller-setup.md @@ -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 diff --git a/docs/features/statistics-api.md b/docs/features/statistics-api.md new file mode 100644 index 000000000..d992b96c0 --- /dev/null +++ b/docs/features/statistics-api.md @@ -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 " +``` + +### 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). diff --git a/gns3server/api/routes/controller/controller.py b/gns3server/api/routes/controller/controller.py index bc0a249f8..58b89f2ea 100644 --- a/gns3server/api/routes/controller/controller.py +++ b/gns3server/api/routes/controller/controller.py @@ -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)])