curl --request POST \
--url https://api.example.com/api/v1/devices/reassign-slots/ \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"assignments": [
{
"device_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"rack_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"position": "<string>"
}
]
}
'import requests
url = "https://api.example.com/api/v1/devices/reassign-slots/"
payload = { "assignments": [
{
"device_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"rack_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"position": "<string>"
}
] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
assignments: [
{
device_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
rack_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
position: '<string>'
}
]
})
};
fetch('https://api.example.com/api/v1/devices/reassign-slots/', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/devices/reassign-slots/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'assignments' => [
[
'device_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'rack_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'position' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/devices/reassign-slots/"
payload := strings.NewReader("{\n \"assignments\": [\n {\n \"device_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"rack_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"position\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/devices/reassign-slots/")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"assignments\": [\n {\n \"device_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"rack_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"position\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/devices/reassign-slots/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"assignments\": [\n {\n \"device_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"rack_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"position\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"devices_changed": 123
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}Reassign device rack slots atomically
Move devices to new rack slots as one unit, so they can cross over.
The single-device update path cannot express a swap. (rack, position) is
unique, so whichever of the two devices moves first lands on the other — there is
no ordering that avoids it, and the netbox agent consequently deadlocked on every
tray swap and served the pre-swap location indefinitely.
This applies the whole batch in one transaction, with the constraint DEFERRED so
it is judged once at COMMIT rather than per row. QuerySet.update() is used
deliberately: full_clean would reject the intermediate overlap in Python before
the database ever saw it (BaseModel.validate_constraints refuses to exclude
fields, by design), so nothing outside the transaction observes the crossing.
Bypassing full_clean means the invariants it would have applied are the
serializer’s job instead: it derives location/site from the target rack (they
are non-editable and normally re-derived by Device.save()) and rejects a batch
that no ordering could satisfy. Only those four columns are written — name, Ironic
reference and status are untouched, because a physical move is all this represents.
The occupancy check covers only (rack, position), not NVSwitch slots. A bad
NVSwitch slot swap therefore fails at commit with HTTP 409. NVSwitch rack moves
are manual in the M1.5 milestone. Revisit this check before automating them in a
later milestone.
Every member is locked in device-id order before anything is written. Two agent workers reconciling the two halves of one swap concurrently each put their own device first, which would be a lock cycle and a Postgres deadlock; taking the locks in a stable order makes the loser wait instead, and it is then a no-op because the winner already committed the layout it was going to write. The same lock closes the window in which a member deleted after validation would leave its partner’s move committed on its own — that returns 409 rather than a half-applied swap.
curl --request POST \
--url https://api.example.com/api/v1/devices/reassign-slots/ \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"assignments": [
{
"device_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"rack_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"position": "<string>"
}
]
}
'import requests
url = "https://api.example.com/api/v1/devices/reassign-slots/"
payload = { "assignments": [
{
"device_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"rack_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"position": "<string>"
}
] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
assignments: [
{
device_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
rack_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
position: '<string>'
}
]
})
};
fetch('https://api.example.com/api/v1/devices/reassign-slots/', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/devices/reassign-slots/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'assignments' => [
[
'device_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'rack_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'position' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/devices/reassign-slots/"
payload := strings.NewReader("{\n \"assignments\": [\n {\n \"device_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"rack_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"position\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/devices/reassign-slots/")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"assignments\": [\n {\n \"device_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"rack_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"position\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/devices/reassign-slots/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"assignments\": [\n {\n \"device_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"rack_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"position\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"devices_changed": 123
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}{
"detail": "<string>",
"code": "error",
"field_errors": [
{
"field": "<string>",
"messages": [
"<string>"
]
}
]
}Authorizations
The access token received from the authorization server in the OAuth 2.0 flow.
Body
Input payload for the atomic slot reassignment action.
Every assignment is applied together, so a set that merely passes through an occupied slot — the swap case, where each device wants the other's — is legal even though no single assignment in it is.
max_length documents the intent as much as it bounds the work: the netbox agent
submits one 2-device swap, and each assignment costs its own UPDATE inside a single
transaction holding row locks until COMMIT.
1 - 64 elementsShow child attributes
Show child attributes
Response
Result of an atomic slot reassignment.