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.

MethodPathPurposePermission
GET/api/v1/topology/graphGraph (nodes + edges), optional health overlaydevice: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 positionsdevice: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 layoutdevice:write
ParameterTypeDefaultDescription
site_idUUID-Scope the graph to one site. Omit for the full org graph.
include_healthbooltrueAttach health scores and status to each node.

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

ValueDescription
autoSystem chooses the most appropriate algorithm for the device count.
hierarchicalTop-down tree layout; useful for switch stacks with clear parent-leaf relationships.
force_directedPhysics-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.

OperationRequired permission
rebootdevice:reboot
push_configconfig:push
firmware_updatefirmware: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:

ScopeWhat it targetsAdditional fields
siteAll matching devices at one sitescope_id (site UUID)
device_listAn explicit list of device UUIDsdevice_ids
device_groupAll members of a device groupscope_id (group UUID)
tagAll devices with a specific tagtag (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
}
}
FieldDescription
stagesOrdered 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_percentPercentage of devices allowed to fail before the job aborts. Defaults to 5 if omitted.
rollback_on_failureIf 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.

MethodPathPurposePermission
POST/api/v1/enterprise/bulk-operationsCreate and dispatch a bulk jobPer-operation (see above)
GET/api/v1/enterprise/bulk-operationsList jobsconfig:read
GET/api/v1/enterprise/bulk-operations/{job_id}Job status and per-device resultsconfig:read
POST/api/v1/enterprise/bulk-operations/{job_id}/cancelCancel a pending or running jobPer-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:

FieldDescription
job_idUUID of the job
operationreboot, push_config, or firmware_update
statuspending, running, completed, failed, cancelled
devices_totalTotal devices targeted
devices_completedDevices finished successfully
devices_failedDevices that returned an error
devices_skippedDevices excluded (e.g. access-denied or offline)
current_stageIndex of the active rollout stage (0-indexed)
created_atISO-8601 timestamp
started_atWhen the Celery task began execution
completed_atWhen the job reached a terminal state
error_messageSet if the job failed at the job level (not per-device)

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

ParameterTypeDescription
statusstringFilter by job status (pending, running, completed, failed, cancelled).
limitintMaximum 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.

RoleCan view topologyCan save layoutsCan create bulk jobsCan cancel bulk jobs
viewerYesNoNoNo
operatorYesNoDepends on per-op permDepends on per-op perm
site_adminYesYes (own)Yes - reboot + config push only (no firmware_update)Yes - reboot + config push only (no firmware_update)
org_adminYesYes (own)Yes - reboot + config push only (no firmware_update)Yes - reboot + config push only (no firmware_update)
adminYesYes (own)YesYes
super_adminYesYes (own)YesYes

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.

EventSourceWhen
bulkop.createdBulk operationsJob accepted and queued
bulkop.cancelledBulk operationsJob cancelled
device.lifecycle.*Lifecycle FSM (POST /enterprise/devices/{id}/lifecycle)On each manual lifecycle state transition