Skip to content

Topology & Bulk Operations

FreeSDN builds a live topology graph of every device it knows about and lets you act on many devices at once through bulk operations. This page covers both features: how the topology graph works, how to save and restore layouts, and how bulk jobs are dispatched, tracked, and optionally rolled back automatically.


GET /api/v1/topology/graph returns a graph object whose nodes are devices and whose edges are links between them. You can request the graph at site scope or across the whole organisation. The optional health overlay (enabled by default) annotates each node with the device’s current health_score (0-100) and health_status (HEALTHY / WARNING / DEGRADED / CRITICAL).

The frontend at /topology renders this graph interactively. Node positions can be set manually by drag-and-drop or computed automatically by one of three built-in layout algorithms.

Method Path Purpose Permission
GET /api/v1/topology/graph Graph (nodes + edges), optional health overlay device:read
GET /api/v1/topology/layout/{site_id} Saved layout for a site (null if none saved) device:read
PUT /api/v1/topology/layout/{site_id} Save or update layout positions device:write
POST /api/v1/topology/auto-layout/{site_id} Compute an auto-layout (does not persist) device:read
DELETE /api/v1/topology/layout/{site_id} Delete a saved layout device:write
Parameter Type Default Description
site_id UUID - Scope the graph to one site. Omit for the full org graph.
include_health bool true Attach health scores and status to each node.

Pass the algorithm query parameter to POST /api/v1/topology/auto-layout/{site_id}:

Value Description
auto System chooses the most appropriate algorithm for the device count.
hierarchical Top-down tree layout; useful for switch stacks with clear parent-leaf relationships.
force_directed Physics-based spring layout; tends to work better for mesh or partially-connected topologies.

Auto-layout does not persist. It returns new x/y values for each node. If you are satisfied with the result, follow with a PUT /api/v1/topology/layout/{site_id} to save it.

Layouts are per-user and per-site. Two operators viewing the same site can have independent layouts. Saving a layout stores the x/y position of every node. On the next page load the frontend retrieves the saved layout and places nodes accordingly.

To reset to an unsaved state, call DELETE /api/v1/topology/layout/{site_id}.

Every site-scoped topology request verifies the site_id against the caller’s organisation before responding. A foreign site_id returns 404, not an empty graph, to avoid leaking the existence of other organisations’ sites. Users with site-limited grants (operator or viewer scoped to specific sites) are also checked via per-user site access before the graph is returned.


The bulk-operations engine lets you dispatch a single job - reboot, config push, or firmware upgrade - to many devices at once. You target devices by site, explicit list, device group, or tag. The job runs asynchronously on the sync Celery queue. You can watch progress through the status endpoint or the /bulk-operations page in the UI.

Optionally, you can supply a staged rollout strategy: the job processes devices in percentage- based waves, pausing between waves, and can automatically roll back all completed operations if the failure rate exceeds a threshold.

Operation Required permission
reboot device:reboot
push_config config:push
firmware_update firmware:upgrade

The permission check runs twice: once at job creation (403 if the caller lacks the permission entirely) and again at job execution for each device (ensuring a permission change between submission and execution is caught).

Use the target object in the create request body. Exactly one scope must be provided:

Scope What it targets Additional fields
site All matching devices at one site scope_id (site UUID)
device_list An explicit list of device UUIDs device_ids
device_group All members of a device group scope_id (group UUID)
tag All devices with a specific tag tag (string)

All scopes accept an optional device_type filter to restrict the operation to, for example, only access_point or switch devices within the target.

Bulk jobs support an optional rollout object:

{
"rollout": {
"strategy": "staged",
"stages": [
{ "percent": 10, "wait_minutes": 15 },
{ "percent": 40, "wait_minutes": 30 },
{ "percent": 50, "wait_minutes": 0 }
],
"failure_threshold_percent": 20,
"rollback_on_failure": true
}
}
Field Description
stages Ordered list of wave definitions. percent is the share of total devices in that wave; wait_minutes is the pause before the next wave starts.
failure_threshold_percent Percentage of devices allowed to fail before the job aborts. Defaults to 5 if omitted.
rollback_on_failure If true and the failure rate exceeds the threshold, the engine aborts the job and skips all remaining devices. Operations already completed on earlier-wave devices are not reversed - rollback_on_failure controls job-abort behaviour, not device-state restoration.

If you omit rollout, all devices are processed concurrently with no staged pausing.

Method Path Purpose Permission
POST /api/v1/enterprise/bulk-operations Create and dispatch a bulk job Per-operation (see above)
GET /api/v1/enterprise/bulk-operations List jobs config:read
GET /api/v1/enterprise/bulk-operations/{job_id} Job status and per-device results config:read
POST /api/v1/enterprise/bulk-operations/{job_id}/cancel Cancel a pending or running job Per-operation
{
"operation": "firmware_update",
"target": {
"scope": "device_group",
"scope_id": "<device-group-uuid>"
},
"config": {
"firmware_url": "https://dl.example.com/fw-8.4.59.bin",
"firmware_version": "8.4.59"
},
"rollout": {
"strategy": "staged",
"stages": [
{ "percent": 25, "wait_minutes": 10 },
{ "percent": 75, "wait_minutes": 0 }
],
"failure_threshold_percent": 10,
"rollback_on_failure": false
}
}

The GET /api/v1/enterprise/bulk-operations/{job_id} response includes:

Field Description
job_id UUID of the job
operation reboot, push_config, or firmware_update
status pending, running, completed, failed, cancelled
devices_total Total devices targeted
devices_completed Devices finished successfully
devices_failed Devices that returned an error
devices_skipped Devices excluded (e.g. access-denied or offline)
current_stage Index of the active rollout stage (0-indexed)
created_at ISO-8601 timestamp
started_at When the Celery task began execution
completed_at When the job reached a terminal state
error_message Set if the job failed at the job level (not per-device)

GET /api/v1/enterprise/bulk-operations accepts:

Parameter Type Description
status string Filter by job status (pending, running, completed, failed, cancelled).
limit int Maximum results (up to 200).

POST /api/v1/enterprise/bulk-operations/{job_id}/cancel stops a job if it is still pending or running. The same per-operation permission check applies to cancellation. Devices already processed before cancellation are not automatically reversed.


The UI page /bulk-operations is gated on the frontend device:update permission. The backend checks the more granular per-operation permission at both create and cancel time.

Role Can view topology Can save layouts Can create bulk jobs Can cancel bulk jobs
viewer Yes No No No
operator Yes No Depends on per-op perm Depends on per-op perm
site_admin Yes Yes (own) Yes - reboot + config push only (no firmware_update) Yes - reboot + config push only (no firmware_update)
org_admin Yes Yes (own) Yes - reboot + config push only (no firmware_update) Yes - reboot + config push only (no firmware_update)
admin Yes Yes (own) Yes Yes
super_admin Yes Yes (own) Yes Yes

Layout saves are always scoped to the individual user - site_admin saving a layout does not overwrite another user’s layout for the same site.


Both subsystems publish events to the internal event bus, which downstream automation rules, notifications, and WebSocket clients can subscribe to.

Event Source When
bulkop.created Bulk operations Job accepted and queued
bulkop.cancelled Bulk operations Job cancelled
device.lifecycle.* Lifecycle FSM (POST /enterprise/devices/{id}/lifecycle) On each manual lifecycle state transition


All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.