11. Subnets and Attachments
11.1 Subnets
Endpoint Summary
| Path | Summary | Method |
|---|---|---|
/api/v1/subnets |
List every subnet in the realm | GET |
/api/v1/subnets/segment-type |
Get the network segment type new subnets are placed on | GET |
/api/v1/subnets/segment-type |
Set the network segment type new subnets are placed on | PUT |
/api/v1/tenants/{tenant_id}/subnets |
List a tenant’s subnets | GET |
/api/v1/tenants/{tenant_id}/subnets |
Create a new subnet | POST |
/api/v1/tenants/{tenant_id}/subnets/{subnet_id} |
Get subnet info | GET |
/api/v1/tenants/{tenant_id}/subnets/{subnet_id} |
Update subnet | PATCH |
/api/v1/tenants/{tenant_id}/subnets/{subnet_id} |
Delete subnet | DELETE |
/api/v1/tenants/{tenant_id}/subnets/{subnet_id}/mount-info |
Get the mount targets on a subnet | GET |
GET /api/v1/subnets
List every subnet in the realm
Returns every subnet the realm holds, whatever tenant owns it. To read one tenant instead, name it in the path: GET /tenants/{tenant_id}/subnets.
Authentication: required (PasswordLogin or BearerJWT).
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful response containing the list of subnets. | SubnetListResponse |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 500 | Internal server error. | - |
Response Body (200, SubnetListResponse)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
subnets |
array of Subnet object |
no | n/a | Array of subnet objects. Empty array if none are found. | |
total |
integer | yes | n/a | 0, 1, 12 |
Total count of subnets returned. |
Example
{
"subnets" : [
{
"addr_count" : 12,
"addr_external" : false,
"created_at" : "2026-06-29T12:00:00Z",
"gateway" : "192.168.50.1",
"id" : "36a8995f-026b-47e1-88bd-7fb9e7239995",
"ip_range_base" : "192.168.50.0",
"ip_range_prefix" : 24,
"mtu" : 1450,
"name" : "storage-net",
"range_end" : "192.168.50.200",
"range_start" : "192.168.50.10",
"segment_id" : 10,
"segment_type" : "vlan",
"tenant_id" : "00000000-0000-0000-0000-000000000001",
"tunnel_udp_port" : 0,
"updated_at" : "2026-06-29T12:30:00Z"
}
],
"total" : 1
}GET /api/v1/subnets/segment-type
Get the network segment type new subnets are placed on
Returns the realm-wide network segment type new subnets are placed on.
Authentication: required (PasswordLogin or BearerJWT).
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The network segment type new subnets are placed on. | RealmSegmentTypeResponse |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 500 | Internal server error. | - |
| 503 | Service unavailable. A backend dependency the request needs is unreachable. The request was well formed and changed nothing, so the same call is worth retrying once the dependency is back; which one failed is recorded server-side, not in the response. | - |
Response Body (200, RealmSegmentTypeResponse)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
segment_type |
SubnetSegmentType (none, vlan, vxlan) |
yes | n/a | vxlan, vlan, none |
The segment type a new subnet is placed on. vlan carries a VLAN ID in segment_id and vxlan a VNI. none is the realm naming no segment at all, which is a state to report rather than an error: a create on such a realm is refused, with a reason. |
Example
{
"segment_type" : "vxlan"
}PUT /api/v1/subnets/segment-type
Set the network segment type new subnets are placed on
Sets the realm-wide network segment type. Refused while any tenant has a subnet.
Authentication: required (PasswordLogin or BearerJWT).
Request Body (application/json, required)
Schema: RealmSegmentTypeRequest
Set the network segment type new subnets are placed on.
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
segment_type |
one of: vlan, vxlan |
yes | n/a | vlan, vxlan |
The segment type new subnets are placed on. |
Example
{
"segment_type" : "vlan"
}Responses
| Status | Description | Schema |
|---|---|---|
| 204 | Success. No content is returned. | - |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 409 | Conflict. The request conflicts with the current state of the resource. | - |
| 422 | Validation error. | - |
| 500 | Internal server error. | - |
| 503 | Service unavailable. A backend dependency the request needs is unreachable. The request was well formed and changed nothing, so the same call is worth retrying once the dependency is back; which one failed is recorded server-side, not in the response. | - |
GET /api/v1/tenants/{tenant_id}/subnets
List a tenant’s subnets
Returns the subnets the tenant named in the path owns.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful response containing the list of subnets. | SubnetListResponse |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Tenant not found. | - |
| 422 | Validation Error | HTTPValidationError |
| 500 | Internal server error. | - |
Response Body (200, SubnetListResponse)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
subnets |
array of Subnet object |
no | n/a | Array of subnet objects. Empty array if none are found. | |
total |
integer | yes | n/a | 0, 1, 12 |
Total count of subnets returned. |
Example
{
"subnets" : [
{
"addr_count" : 12,
"addr_external" : false,
"created_at" : "2026-06-29T12:00:00Z",
"gateway" : "192.168.50.1",
"id" : "36a8995f-026b-47e1-88bd-7fb9e7239995",
"ip_range_base" : "192.168.50.0",
"ip_range_prefix" : 24,
"mtu" : 1450,
"name" : "storage-net",
"range_end" : "192.168.50.200",
"range_start" : "192.168.50.10",
"segment_id" : 10,
"segment_type" : "vlan",
"tenant_id" : "00000000-0000-0000-0000-000000000001",
"tunnel_udp_port" : 0,
"updated_at" : "2026-06-29T12:30:00Z"
}
],
"total" : 1
}POST /api/v1/tenants/{tenant_id}/subnets
Create a new subnet
Creates a subnet owned by the tenant named in the path. Two things answer 404 here, with different detail text: a tenant the realm does not hold (“Owning tenant not found”), and a tenant the caller may not reach (“Tenant not found”), which is refused before the handler so the code cannot be used to establish which tenants exist. The platform tenant itself cannot own a subnet: it is given one with the realm and no other, so a create addressed to it is answered 409. The window must hold at least as many addresses as the realm primary pool, the node ipranges. A node takes the window address at its own position in that pool. A smaller window is answered 400, however few nodes the realm has.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
Request Body (application/json, required)
Schema: SubnetCreateRequest
Request payload for creating a subnet.
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
addr_external |
boolean | no | false |
false, true |
Selects the address-allocation model. False leaves PanFS managing the addresses, true hands that to an outside allocator. Fixed at create and cannot be changed afterwards. |
gateway |
string, nullable | no | null |
192.168.50.1, null |
Optional default gateway address, in the same family as ip_range_base. |
ip_range_base |
string | yes | n/a | 192.168.50.0, fd00:dead:beef:: |
Base IP address of the subnet range (IPv4 or IPv6). |
ip_range_prefix |
integer | yes | n/a | 24, 64 |
CIDR prefix length. 1-32 for IPv4, 1-128 for IPv6. |
mtu |
integer | no | 0 |
0, 1450, 8950 |
The MTU of the subnet’s tenant interface on every realm node and every attached host. 0 selects the default, which is the realm’s primary network MTU less the encapsulation overhead (50 bytes on vxlan, 4 bytes on vlan): 1450 on vxlan over a 1500-byte primary network. Any other value, from 68 to 9216, has to leave that overhead within the primary network MTU. A larger one is refused with 400 naming the largest value that fits. |
name |
string | yes | n/a | storage-net, tenant_a-vlan10 |
Subnet name, unique per tenant. 3-31 chars from [A-Za-z0-9_-]. |
range_end |
string, nullable | no | null |
192.168.50.200, null |
Upper bound of the usable-address window, which is the range the realm allocates its own node addresses from. Supply it together with range_start, or omit both and let the realm derive the window, as range_start describes. |
range_start |
string, nullable | no | null |
192.168.50.10, null |
Lower bound of the usable-address window, which is the range the realm allocates its own node addresses from. The window must hold at least as many addresses as the realm primary pool. A host attached from outside the realm is placed outside this window. Supply it together with range_end, or omit both and the realm derives the window from the first usable host, as deep as its primary pool, so the subnet as stored reports a window this request did not carry. The realm refuses with 400 when it cannot read that pool, and then the window has to be named. Nothing is derived for a subnet whose addresses an outside allocator owns. |
segment_id |
integer | no | 0 |
0, 10, 100100 |
Network segment identifier: a VLAN ID when the realm runs 802.1Q and a VNI when it runs VXLAN. 0 leaves it unassigned. Otherwise the bound here is the wider of the two spaces, 1-16777215. The realm manager owns the narrower range that follows the realm segment type, and the uniqueness of the identifier across the realm. Fixed at create and cannot be changed afterwards. |
tunnel_udp_port |
integer | no | 0 |
0, 4789 |
UDP port carrying a tunnelled segment, which is meaningful when the realm runs VXLAN. 0 leaves it unset and the realm manager applies its own default. Otherwise the bound is 1-65535. Fixed at create and cannot be changed afterwards. |
Example
{
"addr_external" : false,
"gateway" : "192.168.50.1",
"ip_range_base" : "192.168.50.0",
"ip_range_prefix" : 24,
"mtu" : 1450,
"name" : "storage-net",
"range_end" : "192.168.50.200",
"range_start" : "192.168.50.10",
"segment_id" : 10,
"tunnel_udp_port" : 0
}Responses
| Status | Description | Schema |
|---|---|---|
| 201 | Subnet created successfully. | SubnetCreateResponse |
| 400 | Bad request. The realm manager refused the subnet: a gateway outside the range, a window the stored CIDR does not hold, a window smaller than the realm primary pool, no window named on a realm whose primary pool cannot be read to derive one, an unknown allocation flag, the default tenant as owner, a segment identifier offered while the realm names no segment type, or a segment identifier outside the space that type defines. An mtu larger than the realm’s primary network MTU less the segment’s encapsulation overhead is refused here too, before the realm manager sees it, with the largest value that fits named in the detail. Input the request schema can judge for itself is refused before the realm manager sees it and comes back as 422. | - |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Tenant not found. | - |
| 409 | Conflict. The name is already taken, the segment identifier is already held by another subnet, or the realm already carries subnets on a different network segment type. The name is unique within the owning tenant, while the segment identifier is unique across the whole realm, because the identifier space belongs to the physical interface every tenant on a node shares. One realm carries one segment type, so a create lands on the type the realm already holds. A create addressed to the platform tenant answers this too: it owns its subnet with the realm and is given no other. A create also answers this when the realm could not record an address for the new subnet on every node, because every node takes one address on every tenant subnet and the realm caps how many it holds in all. | - |
| 422 | Validation error. The request schema settles the name charset and length, the address family, the prefix, the MTU bounds, the segment-identifier and encapsulation-port bounds, and every usable-window rule a single request can judge on its own. A cross-field refusal reports body as its location and names the offending field in the message. segment_type and segment_flags land here too, because the realm owns them and the create schema forbids fields it does not declare. |
- |
| 500 | Internal server error. | - |
| 503 | Service unavailable. A backend dependency the request needs is unreachable. The request was well formed and changed nothing, so the same call is worth retrying once the dependency is back; which one failed is recorded server-side, not in the response. | - |
Response Body (201, SubnetCreateResponse)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
id |
string (uuid) | yes | n/a | Server-assigned unique identifier of the created subnet. |
Example
{
"id" : "36a8995f-026b-47e1-88bd-7fb9e7239995"
}GET /api/v1/tenants/{tenant_id}/subnets/{subnet_id}
Get subnet info
Returns details for a specific subnet by its UUID. A subnet outside the tenant named in the path is answered 404, so the code cannot be used to establish that one exists.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successfully retrieved detailed information about the subnet. | SubnetGetResponse |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Subnet not found. | - |
| 422 | Validation Error | HTTPValidationError |
| 500 | Internal server error. | - |
Response Body (200, SubnetGetResponse)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
subnet |
Subnet object |
yes | n/a | Subnet entity with all available details. |
Example
{
"subnet" : {
"addr_count" : 12,
"addr_external" : false,
"created_at" : "2026-06-29T12:00:00Z",
"gateway" : "192.168.50.1",
"id" : "36a8995f-026b-47e1-88bd-7fb9e7239995",
"ip_range_base" : "192.168.50.0",
"ip_range_prefix" : 24,
"mtu" : 1450,
"name" : "storage-net",
"range_end" : "192.168.50.200",
"range_start" : "192.168.50.10",
"segment_id" : 10,
"segment_type" : "vlan",
"tenant_id" : "00000000-0000-0000-0000-000000000001",
"tunnel_udp_port" : 0,
"updated_at" : "2026-06-29T12:30:00Z"
}
}PATCH /api/v1/tenants/{tenant_id}/subnets/{subnet_id}
Update subnet
Updates an existing subnet using PATCH semantics. Only the fields provided in the request are modified, and a field provided as null is cleared. An empty patch is rejected with HTTP 422. The subnet must belong to the tenant named in the path: one owned by another tenant is answered 404.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet |
Request Body (application/json, required)
Schema: SubnetPatchRequest
Request payload for updating a subnet via PATCH (partial update).
Only the supplied fields are modified, and a patch carrying no field at all is refused here, so it answers 422 with the same envelope every other patch schema uses. A field supplied as null is cleared, which is how a gateway is removed and how a usable window is withdrawn by clearing both of its endpoints. name and mtu carry no meaning when cleared, so an explicit null is refused for them.
addr_external is absent by design: the flag is fixed at create, and extra='forbid' refuses a request that carries it. The four network segment values are absent for the same reason, and a patch naming any of them is refused with that field in the error location.
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
gateway |
string, nullable | no | null |
192.168.50.254, null |
New default gateway address (IPv4 or IPv6). Null clears the gateway. |
mtu |
integer, nullable | no | null |
1450, 8950 |
New MTU. The MTU of the subnet’s tenant interface on every realm node and every attached host. 0 selects the default, which is the realm’s primary network MTU less the encapsulation overhead (50 bytes on vxlan, 4 bytes on vlan): 1450 on vxlan over a 1500-byte primary network. Any other value, from 68 to 9216, has to leave that overhead within the primary network MTU. A larger one is refused with 400 naming the largest value that fits. |
name |
string, nullable | no | null |
||
range_end |
string, nullable | no | null |
192.168.50.220, null |
New upper bound of the usable-address window. Patchable on its own. Null clears it, and clearing both bounds together has the realm derive the window anew, as range_start describes. |
range_start |
string, nullable | no | null |
192.168.50.20, null |
New lower bound of the usable-address window. Patchable on its own, and the realm manager validates the resulting pair against the subnet CIDR. Null clears it. Clearing both bounds together has the realm derive the window anew, on the same terms a create naming none gets one, and is refused with 409 while a subnet the realm addresses itself has an attached host. |
Example
{
"gateway" : "192.168.50.254",
"mtu" : 1500,
"name" : "storage-net-2",
"range_end" : "192.168.50.220",
"range_start" : "192.168.50.20"
}Responses
| Status | Description | Schema |
|---|---|---|
| 204 | Subnet updated successfully. | - |
| 400 | Bad request. The realm manager refused the result of moving one window endpoint against the record it stores, including a window left smaller than the realm primary pool, or a window cleared on a realm whose primary pool cannot be read to derive a new one. An mtu larger than the realm’s primary network MTU less the subnet’s encapsulation overhead is refused before the realm manager sees it, with the largest value that fits named in the detail. A patch the request schema can judge for itself comes back as 422. | - |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Subnet not found. | - |
| 409 | Conflict. The name is already taken, the window overlaps another subnet of the same tenant, or it would strand an address already bound to the subnet. The platform tenant’s own subnet answers this too: it comes with the realm. A write of the window or the gateway answers this when the subnet it leaves behind would reserve an address one of the subnet’s attached hosts already holds. Clearing range_start and range_end has the realm derive the window anew, which cannot be known in advance, so it answers this whenever the subnet has an attached host. A subnet whose addresses an outside allocator owns reserves nothing for the realm, so a write to it never answers this for an attached host. | - |
| 422 | Validation error. A patch carrying no field at all, a field that is fixed at create, a null on a field that cannot be cleared, or a window whose own two endpoints contradict each other. addr_external and the four network segment values land here because the patch schema forbids fields it does not declare. |
- |
| 500 | Internal server error. | - |
| 503 | Service unavailable. A backend dependency the request needs is unreachable. The request was well formed and changed nothing, so the same call is worth retrying once the dependency is back; which one failed is recorded server-side, not in the response. | - |
DELETE /api/v1/tenants/{tenant_id}/subnets/{subnet_id}
Delete subnet
Deletes the specified subnet by its UUID. The realm manager removes every address bound to the subnet in the same transaction, so bound addresses do not refuse the delete. The subnet must belong to the tenant named in the path: one owned by another tenant is answered 404.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | Subnet deleted successfully. | - |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Subnet not found. | - |
| 409 | Conflict. This is the last subnet of a tenant that still owns volumes, whose addresses it carries. Delete those volumes, or create another subnet for the tenant, first. The platform tenant’s own subnet answers this too: it comes with the realm. | - |
| 422 | Validation Error | HTTPValidationError |
| 500 | Internal server error. | - |
GET /api/v1/tenants/{tenant_id}/subnets/{subnet_id}/mount-info
Get the mount targets on a subnet
Returns the realm directors holding an address on this subnet, together with the address list a client puts in its mount options as rmlist=(
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successfully retrieved the realm directors a client on this subnet can mount to. An empty directors array with an empty rmlist is a legitimate answer: the subnet exists and the realm holds no address on it yet, which is one of the states a subnet is in. A realm provisioning operation records those addresses. An empty rmlist beside a populated directors array is legitimate too, and it says the recorded addresses are ones mount_panfs cannot bootstrap against. |
MountInfoResponse |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Subnet not found. | - |
| 422 | Validation Error | HTTPValidationError |
| 500 | Internal server error. | - |
| 503 | Service unavailable. The realm manager could not be reached, or it kept changing its address table for long enough that no consistent snapshot could be read. | - |
Response Body (200, MountInfoResponse)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
directors |
array of DirectorEntry object |
no | n/a | The realm directors holding an address on this subnet, nodes carrying a realm-database replica first and then ordered by address. Every such director appears here, in either address family. An empty array means the subnet exists and carries no address records yet. | |
rmlist |
string | yes | n/a | 10.20.0.11;10.20.0.12 |
The addresses for mount_panfs, joined with “;” in the order above and wrapped by the caller as rmlist=( |
subnet_id |
string (uuid) | yes | n/a | UUID of the subnet the addresses belong to. | |
tenant_id |
string (uuid) | yes | n/a | UUID of the tenant owning the subnet. |
Example
{
"directors" : [
{
"dataserial" : "SN-D01",
"ip" : "10.20.0.11",
"repset" : true,
"status" : "online"
},
{
"dataserial" : "SN-D02",
"ip" : "10.20.0.12",
"repset" : true,
"status" : "online"
},
{
"dataserial" : "SN-D03",
"ip" : "10.20.0.13",
"repset" : false,
"status" : "online"
}
],
"rmlist" : "10.20.0.11;10.20.0.12",
"subnet_id" : "36a8995f-026b-47e1-88bd-7fb9e7239995",
"tenant_id" : "e97b94d6-9cc1-4325-a267-1064ae538ccd"
}11.2 Host Attachments
Attaching and detaching individual hosts to a subnet’s tenant network.
Endpoint Summary
| Path | Summary | Method |
|---|---|---|
/api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments |
List the subnet’s host attachments | GET |
/api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments |
Attach a host to the subnet’s tenant network | POST |
/api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments/{attachment_id} |
Get attachment detail | GET |
/api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments/{attachment_id} |
Detach a host from the subnet | DELETE |
GET /api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments
List the subnet’s host attachments
Returns the hosts attached to the subnet. Supply the underlay_ip query parameter to narrow the listing to one host, which is how a host that knows only its own address finds its record in a single call.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet the host attaches to |
underlay_ip |
query | string, nullable | no | Narrow the listing to the host with this address on the realm underlay. |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful response containing the subnet’s attachments. Narrowed to a single host when the underlay_ip query parameter is given, which is how a host that knows only its own address finds its record in one call. |
AttachmentListResponse |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Not found. No such subnet or no such attachment, and equally a subnet or an attachment owned by another tenant. The two are one answer on purpose, so a response code cannot be used to establish that a record exists where the caller cannot see it. | - |
| 422 | Validation Error | HTTPValidationError |
| 500 | Internal server error. | - |
| 503 | Service unavailable. The attachment store is unreachable, so the record could neither be read nor written. The request itself was well formed, which is why this is a 5xx. | - |
Response Body (200, AttachmentListResponse)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
attachments |
array of Attachment object |
no | n/a | Array of attachment objects. Empty array if none are found. | |
total |
integer | yes | n/a | 0, 1, 12 |
Total count of attachments returned. |
Example
{
"attachments" : [
{
"hostname" : "client-0",
"id" : "8f14e45f-ceea-467a-9575-5c4bcbdcd2f1",
"joined_at" : "2026-08-28T12:00:00Z",
"mtu" : 1450,
"overlay_ip" : "192.168.50.100",
"overlay_prefix" : 24,
"segment_id" : 4257216,
"segment_type" : "vxlan",
"subnet_id" : "36a8995f-026b-47e1-88bd-7fb9e7239995",
"tenant_id" : "00000000-0000-0000-0000-000000000001",
"tunnel" : {
"mac" : "92:b2:99:59:f3:fa",
"udp_port" : 4789,
"vteps" : [
"10.128.1.0",
"10.128.1.1",
"10.128.1.2"
]
},
"underlay_ip" : "10.128.9.0"
}
],
"total" : 1
}POST /api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments
Attach a host to the subnet’s tenant network
Records the host and allocates its address on the tenant network. The response carries the whole record, so the host builds its interface from it with no further call. A host already attached to this subnet answers 200 with the stored record and an unchanged address.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet the host attaches to |
Request Body (application/json, required)
Schema: AttachmentCreateRequest
Request body for attaching one host to a subnet.
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
hostname |
string | no | (empty string) | client-0 |
Informational host name, carried for the operator and never interpreted. |
overlay_ip |
string, nullable | no | null |
192.168.50.100 |
Requested address on the tenant network. Must sit inside the subnet, outside the realm node pool and be unallocated. Omit to let the backend allocate one. Applied on first attach only, so a repeated create never moves a host. |
underlay_ip |
string | yes | n/a | 10.128.9.0 |
The host’s address on the realm underlay. Identifies the attachment inside its subnet, and on a tunnelled segment it is the endpoint the realm sends the host traffic to. |
Example
{
"hostname" : "client-0",
"overlay_ip" : "192.168.50.100",
"underlay_ip" : "10.128.9.0"
}Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The host is attached already, and the stored record is returned unchanged. A repeated create never moves a host, so an address named in the request is not applied to a record that exists: changing a host address is a delete followed by a create. | - |
| 201 | Host attached. The response carries the whole record, so the host builds its interface from it with no further call. tunnel is null on a segment the fabric carries itself. |
Attachment |
| 400 | Bad request. The named address sits outside the subnet or, on a subnet the realm addresses itself, inside the range_start-range_end window the realm allocates its own node addresses from or on the gateway the subnet’s router holds. It is equally a request that named no address at all on a subnet whose addresses an outside allocator owns, where the realm has none to give. Input the request schema can judge for itself is refused before this and comes back as 422. | - |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Not found. No such subnet or no such attachment, and equally a subnet or an attachment owned by another tenant. The two are one answer on purpose, so a response code cannot be used to establish that a record exists where the caller cannot see it. | - |
| 409 | Conflict. The named address is already allocated in this subnet, the subnet has no free address left outside the window the realm allocates its own node addresses from, or it declares no window at all, so any usable address may be a node’s. It may also already hold the maximum number of attachments. | - |
| 422 | Validation error. The request schema settles that each address parses and that the body carries no field it does not declare. The link-layer address, the segment identifier and the realm node list are assigned by the backend, so naming any of them lands here. | - |
| 500 | Internal server error. | - |
| 503 | Service unavailable. The attachment store is unreachable, so the record could neither be read nor written. The request itself was well formed, which is why this is a 5xx. | - |
Response Body (201, Attachment)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
hostname |
string | no | (empty string) | Host name as supplied at create. Informational. | |
id |
string (uuid) | yes | n/a | Unique identifier of the attachment. | |
joined_at |
string (date-time) | yes | n/a | First attach time (UTC). | |
mtu |
integer | yes | n/a | The MTU the host sets on its tenant interface, which is the MTU the realm nodes run the subnet at: the subnet’s declared MTU, or the realm’s primary network MTU less the encapsulation overhead when the subnet declares 0. Never 0. | |
overlay_ip |
string | yes | n/a | The host’s address on the tenant network. | |
overlay_prefix |
integer | yes | n/a | CIDR prefix length of the subnet, which the host applies to its own address. | |
segment_id |
integer | yes | n/a | The subnet VLAN ID under one segment type and its VNI under the other. 0 means unassigned. | |
segment_type |
SubnetSegmentType (none, vlan, vxlan) |
yes | n/a | Kind of network segment the subnet is placed on, as the realm has it configured. | |
subnet_id |
string (uuid) | yes | n/a | UUID of the subnet the host is attached to. | |
tenant_id |
string (uuid) | yes | n/a | UUID of the tenant owning the subnet. | |
tunnel |
AttachmentTunnel object, nullable |
no | null |
Tunnel recipe, present only when the segment is tunnelled and null otherwise. | |
underlay_ip |
string | yes | n/a | 10.128.9.0 |
The host’s address on the realm underlay. Identifies the attachment inside its subnet, and on a tunnelled segment it is the endpoint the realm sends the host traffic to. |
Example
{
"hostname" : "client-0",
"id" : "8f14e45f-ceea-467a-9575-5c4bcbdcd2f1",
"joined_at" : "2026-08-28T12:00:00Z",
"mtu" : 1450,
"overlay_ip" : "192.168.50.100",
"overlay_prefix" : 24,
"segment_id" : 4257216,
"segment_type" : "vxlan",
"subnet_id" : "36a8995f-026b-47e1-88bd-7fb9e7239995",
"tenant_id" : "00000000-0000-0000-0000-000000000001",
"tunnel" : {
"mac" : "92:b2:99:59:f3:fa",
"udp_port" : 4789,
"vteps" : [
"10.128.1.0",
"10.128.1.1",
"10.128.1.2"
]
},
"underlay_ip" : "10.128.9.0"
}GET /api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments/{attachment_id}
Get attachment detail
Returns one attachment of the subnet by its UUID.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet the host attaches to |
attachment_id |
path | string (uuid) | yes | UUID of the attachment |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successfully retrieved detailed information about the attachment. | Attachment |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Not found. No such subnet or no such attachment, and equally a subnet or an attachment owned by another tenant. The two are one answer on purpose, so a response code cannot be used to establish that a record exists where the caller cannot see it. | - |
| 422 | Validation Error | HTTPValidationError |
| 500 | Internal server error. | - |
| 503 | Service unavailable. The attachment store is unreachable, so the record could neither be read nor written. The request itself was well formed, which is why this is a 5xx. | - |
Response Body (200, Attachment)
| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
hostname |
string | no | (empty string) | Host name as supplied at create. Informational. | |
id |
string (uuid) | yes | n/a | Unique identifier of the attachment. | |
joined_at |
string (date-time) | yes | n/a | First attach time (UTC). | |
mtu |
integer | yes | n/a | The MTU the host sets on its tenant interface, which is the MTU the realm nodes run the subnet at: the subnet’s declared MTU, or the realm’s primary network MTU less the encapsulation overhead when the subnet declares 0. Never 0. | |
overlay_ip |
string | yes | n/a | The host’s address on the tenant network. | |
overlay_prefix |
integer | yes | n/a | CIDR prefix length of the subnet, which the host applies to its own address. | |
segment_id |
integer | yes | n/a | The subnet VLAN ID under one segment type and its VNI under the other. 0 means unassigned. | |
segment_type |
SubnetSegmentType (none, vlan, vxlan) |
yes | n/a | Kind of network segment the subnet is placed on, as the realm has it configured. | |
subnet_id |
string (uuid) | yes | n/a | UUID of the subnet the host is attached to. | |
tenant_id |
string (uuid) | yes | n/a | UUID of the tenant owning the subnet. | |
tunnel |
AttachmentTunnel object, nullable |
no | null |
Tunnel recipe, present only when the segment is tunnelled and null otherwise. | |
underlay_ip |
string | yes | n/a | 10.128.9.0 |
The host’s address on the realm underlay. Identifies the attachment inside its subnet, and on a tunnelled segment it is the endpoint the realm sends the host traffic to. |
Example
{
"hostname" : "client-0",
"id" : "8f14e45f-ceea-467a-9575-5c4bcbdcd2f1",
"joined_at" : "2026-08-28T12:00:00Z",
"mtu" : 1450,
"overlay_ip" : "192.168.50.100",
"overlay_prefix" : 24,
"segment_id" : 4257216,
"segment_type" : "vxlan",
"subnet_id" : "36a8995f-026b-47e1-88bd-7fb9e7239995",
"tenant_id" : "00000000-0000-0000-0000-000000000001",
"tunnel" : {
"mac" : "92:b2:99:59:f3:fa",
"udp_port" : 4789,
"vteps" : [
"10.128.1.0",
"10.128.1.1",
"10.128.1.2"
]
},
"underlay_ip" : "10.128.9.0"
}DELETE /api/v1/tenants/{tenant_id}/subnets/{subnet_id}/attachments/{attachment_id}
Detach a host from the subnet
Removes the attachment record and frees the address it held. The call programs nothing, so there is no outcome to report: the realm withdraws the forwarding state on its own, after which the host is unreachable on the tenant network even if it keeps its interface.
Authentication: required (PasswordLogin or BearerJWT).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id |
path | string (uuid) | yes | Tenant identifier (UUID). |
subnet_id |
path | string (uuid) | yes | UUID of the subnet the host attaches to |
attachment_id |
path | string (uuid) | yes | UUID of the attachment |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | Host detached. The call removes the record and programs nothing, so there is no outcome to report. The realm withdraws the forwarding state on its own. | - |
| 401 | Unauthorized. Authentication required. | - |
| 403 | Forbidden. Insufficient permissions. | - |
| 404 | Not found. No such subnet or no such attachment, and equally a subnet or an attachment owned by another tenant. The two are one answer on purpose, so a response code cannot be used to establish that a record exists where the caller cannot see it. | - |
| 422 | Validation Error | HTTPValidationError |
| 500 | Internal server error. | - |
| 503 | Service unavailable. The attachment store is unreachable, so the record could neither be read nor written. The request itself was well formed, which is why this is a 5xx. | - |