Approve a machine's join with its assignment (bootstrap-admin).
curl --request POST \ --url https://stratum.example.com/v1/admin/cluster/joins/example/approve \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "assignment": { "name": "example", "role": "hybrid", "rack": "example", "az": "example", "mgmt": { "interface": "example", "address": "example", "gateway": "example", "dns": [ "example" ] }, "links": [ { "name": "storage", "kind": "ethernet", "interface": "vlan90", "parent": "example", "vlanId": 1, "controller": "example", "bond": { "mode": "active-backup", "xmitHashPolicy": "example", "miimonMs": 1, "lacpRate": "example" }, "addresses": [ "example" ], "mtu": 1, "purposes": [ "oob" ] } ], "disks": [ { "key": "/dev/disk/by-id/nvme-Samsung_SSD_980_PRO_2TB_S6B0NL0W123456", "role": "compute", "pool": "example", "storageClass": "example", "confirm": "example" } ], "ntpServers": [ "example" ], "provider": { "topology": "bridged", "upstreamInterface": "example", "vlan": 1 } }, "copyFrom": "example" }'. Checks the assignment
against the machine’s reported hardware, assigns its node id, picks
the addresses of links that name none from the cluster’s storage and
overlay networks (the next free host address; links[i].addresses
when a network is full or the cluster records none for the purpose),
builds its node.yaml (validated as firstboot --config validates it)
and queues it. The response’s assignment carries the picked addresses
(addressesAllocated). Joins run one at a time in approval order, only while the cluster is
green, no other join or removal runs and every member applied the
membership generation; waitReason says what a queued join waits for.
Every joiner is hybrid: a role other than hybrid (a compute-only
hypervisor, a storage-only node) is refused at role, those nodes are
not supported yet. The planned size is the install-time plan, not a
cap: a cluster takes joiners up to 5 control-plane nodes (an even count,
such as a 4th node, is allowed), and an approval that would pass 5 is
refused at role. Approve the machine
whose console shows the same pairing code. Audited
(cluster.join.approved). Gates on ha:ApproveNodeJoin.
A 400 (INVALID_INPUT, a body that does not decode) and a 409
(JOIN_APPROVAL_REFUSED, an assignment the machine’s hardware, the
cluster’s size or node.yaml’s rules refuse) list every problem in the
Problem’s errors, each at its field: the path in
assignment (role, name, rack, az, mgmt.interface,
mgmt.address, links[i].<field> such as links[0].interface or
links[1].bond.mode, disks[i].key, disks[i].role,
disks[i].confirm), copyFrom, or empty for the whole approval. An
index is the request’s; with copyFrom it is the merged assignment’s
(the copied links and disks, each replaced by the request’s of the same
name or key, then the request’s others). A 409’s details.reasons
keeps one line per problem, in the same order.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Request Body
Section titled “Request Body”An approval. copyFrom starts from another
node’s choices, matching disk models and sizes and the NICs by what they
are, and assignment overrides what it names. NIC names differ per machine
(PCI slot numbering), so a NIC keeps its name only when the joiner has one
of that name with the same driver and a compatible speed; the others are
matched by driver, speed and MTU class in name order and listed in
copyNotes (“check it”); a NIC with no match leaves its link out, with a
note. The management NIC is never matched by name: it is the one the join
registered from. Per-node addresses are never copied: they are picked from
the cluster’s networks (links[].addresses).
An approval that names no copyFrom and no assignment.links starts from
the first node’s links the same way, but only when every one of them maps
to a NIC of the joiner (copiedFrom then names the first node); otherwise
nothing is applied and copyNotes says why. Send copyFrom: "none" (or
assignment.links: []) to apply nothing.
object
Everything about a joiner, chosen on the founder. It becomes the joiner’s node.yaml and its member row. What the cluster decides (region, parent domain, upstream DNS, floating IPs, the first admin, services, base images) is not here: the joiner adopts it.
object
Display name. Default the hostname.
Every joiner is hybrid (control plane, compute and Ceph together),
the default. Any other role (a compute-only hypervisor, a
storage-only node, control-plane) is refused at role: those nodes
are not supported yet and are planned for a later release.
The planned size (3 by default, set at install) is the plan, not a
cap: a cluster takes joiners up to 5 control-plane nodes, and an
approval that would pass 5 is refused at role.
The topology.rack label.
The mgmt link. Default: the joiner’s NIC holding the address it
registered from, and that address.
object
An IP, or a CIDR when the address is new on that NIC (with gateway).
One link of a joiner: an ethernet NIC, a bond, or a VLAN
on either, and the purposes it carries. The same shape as a node.yaml
network.connections entry; a bond member is an ethernet link naming its
bond in controller. The oob link is the stage-1 NIC and is not listed.
object
Example
storageDefault ethernet.
Example
vlan90A VLAN’s parent device.
The bond this ethernet link is a member of.
object
Static CIDRs. Optional: a link that carries storage or overlay and
names none gets the next free host address of each of those cluster
networks (GET /v1/admin/cluster/networks), picked at approval and
returned here with addressesAllocated. Free means no member, VIP or
other approved, running or failed join holds it; a re-approval keeps
the address it had while it is still free, and a retry installs the
same one. A named address must be free and in the purpose’s network.
None on a bond member or a provider-only link.
The founder picked addresses from the cluster’s networks. Ignored on input.
Default the largest MTU of the cluster networks the link carries.
What a joiner’s disk becomes. The system disk(s) were
fixed by stage 1 and are not listed. A disk that holds data is wiped, so it
needs confirm set to its key.
object
The disk’s WWN or one of its /dev/disk/by-id paths.
Example
/dev/disk/by-id/nvme-Samsung_SSD_980_PRO_2TB_S6B0NL0W123456A compute disk’s pool name.
A compute pool’s storage class. Default the pool’s name.
The typed wipe confirmation, the disk’s key, for a disk that holds data.
Only when this node’s NTP servers differ from the cluster’s (the founder’s), which it gets otherwise.
The VM uplink: where the joiner’s floating IPs live and its
VM traffic leaves. Default the cluster’s provider choice
(GET /v1/admin/cluster/networks providerTopology, providerVlan,
providerUpstreamInterface), recorded from the founder. Topology and
VLAN are the cluster’s: a different value is refused at
provider.topology / provider.vlan. The uplink NIC defaults to the
interface of a link carrying provider, else the joiner’s NIC of the
founder’s uplink’s purpose (providerUpstreamPurpose mgmt: its mgmt
NIC), else the joiner’s NIC named like the founder’s. A bridged cluster
refuses, at provider.upstreamInterface, a joiner without that NIC or
whose uplink carries storage or overlay.
object
Must equal the cluster’s providerTopology when set.
The joiner’s NIC (or bond) that carries the provider network. Default by the founder’s uplink (above).
Default the cluster’s provider VLAN; any other value is refused.
The node the assignment was copied from (copyFrom).
What the copy could not match and left for the operator.
The node id to copy from: a node that joined with a join code (its
assignment), or a member with a hardware plan, such as the founder
(its links other than the lifelines, and its compute and ceph-osd
disks; the role is not copied). none copies nothing, and also turns
off the first-node default.
Responses
Section titled “Responses”Approved and queued.
A machine that registered to join, on the founder.
States: needs-upgrade (the machine runs an older release than the
cluster’s, within its upgrade rules, and upgrades itself before it can be
approved: upgrade says from and to, failedStep and failedError the
last failure of its upgrade, which it retries by itself; it becomes
pending when it registers again on the cluster’s release; approving it is
refused, rejecting it is not); pending (registered, waiting for approval); approved (queued:
one join runs at a time, only on a green cluster, and waitReason says
what it waits for); delivering (its member row is written and its
node.yaml and bundle are ready for it); installing (it reported
progress); joined; failed (with the step and the error; retry queues
it again); rejected; expired (nobody approved it within 24 hours of
its code’s expiry).
object
Shown on the joiner’s console too. Approve the machine whose console shows the same code.
The release the joiner was installed from.
The address it registered from.
SHA-256 of the joiner’s client key; every later call is pinned to it.
A joining machine’s hardware, as it reported it at registration (the inventory). The approval’s assignment is checked against it.
object
One whole disk of a joining machine, as its discovery reports it.
object
Live mounts on the disk or anything stacked on it. A mounted disk is the system disk.
What already holds data on the disk. Empty means blank.
object
The disk’s partitions. On an OS disk they say what stage 1 left free at its end (the compute tail, or nothing with the whole-disk layout).
object
Every md array and device-mapper device built on the disk, deepest first.
object
One network interface of a joining machine.
object
up with a carrier, down without one, unknown.
The IPv4 destinations the main routing table sends out of this NIC (CIDRs; a host route as /32), without the default route.
object
vmx, svm, or empty.
How stage 1 laid out the OS disks, from the joiner’s install
record: compute-tail keeps the end of each OS disk for the compute
pool, whole-disk gives the disks whole to the system, so compute is a
data disk (a compute pool, or Ceph on a disk marked ceph-osd). Absent
on a machine with no record; the founder then reads it off the disks’
partitions.
Everything about a joiner, chosen on the founder. It becomes the joiner’s node.yaml and its member row. What the cluster decides (region, parent domain, upstream DNS, floating IPs, the first admin, services, base images) is not here: the joiner adopts it.
object
Display name. Default the hostname.
Every joiner is hybrid (control plane, compute and Ceph together),
the default. Any other role (a compute-only hypervisor, a
storage-only node, control-plane) is refused at role: those nodes
are not supported yet and are planned for a later release.
The planned size (3 by default, set at install) is the plan, not a
cap: a cluster takes joiners up to 5 control-plane nodes, and an
approval that would pass 5 is refused at role.
The topology.rack label.
The mgmt link. Default: the joiner’s NIC holding the address it
registered from, and that address.
object
An IP, or a CIDR when the address is new on that NIC (with gateway).
One link of a joiner: an ethernet NIC, a bond, or a VLAN
on either, and the purposes it carries. The same shape as a node.yaml
network.connections entry; a bond member is an ethernet link naming its
bond in controller. The oob link is the stage-1 NIC and is not listed.
object
Default ethernet.
A VLAN’s parent device.
The bond this ethernet link is a member of.
object
Static CIDRs. Optional: a link that carries storage or overlay and
names none gets the next free host address of each of those cluster
networks (GET /v1/admin/cluster/networks), picked at approval and
returned here with addressesAllocated. Free means no member, VIP or
other approved, running or failed join holds it; a re-approval keeps
the address it had while it is still free, and a retry installs the
same one. A named address must be free and in the purpose’s network.
None on a bond member or a provider-only link.
The founder picked addresses from the cluster’s networks. Ignored on input.
Default the largest MTU of the cluster networks the link carries.
What a joiner’s disk becomes. The system disk(s) were
fixed by stage 1 and are not listed. A disk that holds data is wiped, so it
needs confirm set to its key.
object
The disk’s WWN or one of its /dev/disk/by-id paths.
A compute disk’s pool name.
A compute pool’s storage class. Default the pool’s name.
The typed wipe confirmation, the disk’s key, for a disk that holds data.
Only when this node’s NTP servers differ from the cluster’s (the founder’s), which it gets otherwise.
The VM uplink: where the joiner’s floating IPs live and its
VM traffic leaves. Default the cluster’s provider choice
(GET /v1/admin/cluster/networks providerTopology, providerVlan,
providerUpstreamInterface), recorded from the founder. Topology and
VLAN are the cluster’s: a different value is refused at
provider.topology / provider.vlan. The uplink NIC defaults to the
interface of a link carrying provider, else the joiner’s NIC of the
founder’s uplink’s purpose (providerUpstreamPurpose mgmt: its mgmt
NIC), else the joiner’s NIC named like the founder’s. A bridged cluster
refuses, at provider.upstreamInterface, a joiner without that NIC or
whose uplink carries storage or overlay.
object
Must equal the cluster’s providerTopology when set.
The joiner’s NIC (or bond) that carries the provider network. Default by the founder’s uplink (above).
Default the cluster’s provider VLAN; any other value is refused.
The node the assignment was copied from (copyFrom).
What the copy could not match and left for the operator.
Assigned at approval.
The upgrade a joiner on an older release makes before it can register: from the release it was installed with to
the release the cluster runs. Only a joiner that sent selfUpgrade: true
is ever offered one; any other joiner on another release is refused with
RELEASE_MISMATCH.
object
The release the joiner reported.
The release the cluster runs, which the joiner upgrades to.
Where the joiner fetches the release from (the release mirror’s URL of the release directory). Every file is checked against the release’s signed manifest. Empty when the founder names none.
The founder gives the release itself, when no release mirror can (a lab,
an air gap): fetch it from GET /v1/join/registrations/{joinId}/release/{path}
on the join listener, the release layout a mirror publishes
(payload/manifest.json, os/blobs/..., sysext/...). source is then
empty. Served only to this registration while it is needs-upgrade,
read only, a few files at once (503 RELEASE_BUSY: retry); ranges are
accepted so a download resumes. The joiner verifies the manifest’s
signature against the release keys of its own OS and every file against
the manifest, as it does from a mirror.
A running join whose joiner has been silent for 10 minutes.
The step that failed, as the joiner named it: a firstboot phase
(storage), a join step (join/patroni), upgrade or
upgrade/<step> while it upgrades itself, or join.
What a person is told failedStep is, e.g. “Upgrading to the
cluster’s release” for upgrade. Set whenever failedStep is, for
any step name, so a console needs no table of step names.
One step of a join, as the joiner reported it.
object
What a person is told the step is, e.g. “Joining the OpenBao raft”.
The last lines of the joiner’s install log (the founder keeps 2,000). Only on a single join.
When a pending join expires.
Example
{ "id": "nj-8w2k4m1p", "state": "needs-upgrade", "pairingCode": "4F7-K2Q", "inventory": { "disks": [ { "name": "nvme1n1", "path": "/dev/nvme1n1" } ], "nics": [ { "name": "enp87s0" } ], "osDiskLayout": "compute-tail" }, "assignment": { "role": "hybrid", "links": [ { "name": "storage", "kind": "ethernet", "interface": "vlan90", "bond": { "mode": "active-backup" }, "purposes": [ "oob" ] } ], "disks": [ { "key": "/dev/disk/by-id/nvme-Samsung_SSD_980_PRO_2TB_S6B0NL0W123456", "role": "compute" } ], "provider": { "topology": "bridged" } }, "upgrade": { "from": "0.5.0", "to": "0.6.0", "source": "https://mirror.example/releases/stratum/0.6.0/bundle" }, "steps": [ { "name": "join/raft-openbao", "title": "Joining the OpenBao raft", "status": "running" } ]}The body does not decode; errors names each field.
object
The findings of a rejected IAM policy document (custom and resource policies).
One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.
object
The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.
Index of the statement at fault; -1 for the document as a whole.
The offending action, where one is at fault (ACTION_NOT_APPLICABLE).
Field errors, for codes that carry them: each problem at its field
(see FieldError). Documented on the responses that set it, e.g.
approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED.
Absent otherwise.
One field’s problem, the stage-2 installer API’s shape.
field is the field’s path, message what is wrong with it.
object
The field’s path, e.g. links[0].interface, disks[1].confirm,
mgmt.address or role; empty for the whole request.
Structured facts about the refusal, for codes that carry them. The
shape is fixed per code and documented on the response that answers
it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and
IMAGE_IN_USE. Absent for codes that carry none.
object
Example
{ "issues": [ { "code": "ACTION_NOT_APPLICABLE", "action": "compute:CreateVpc" } ]}Authenticated but not authorized.
object
The findings of a rejected IAM policy document (custom and resource policies).
One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.
object
The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.
Index of the statement at fault; -1 for the document as a whole.
The offending action, where one is at fault (ACTION_NOT_APPLICABLE).
Field errors, for codes that carry them: each problem at its field
(see FieldError). Documented on the responses that set it, e.g.
approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED.
Absent otherwise.
One field’s problem, the stage-2 installer API’s shape.
field is the field’s path, message what is wrong with it.
object
The field’s path, e.g. links[0].interface, disks[1].confirm,
mgmt.address or role; empty for the whole request.
Structured facts about the refusal, for codes that carry them. The
shape is fixed per code and documented on the response that answers
it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and
IMAGE_IN_USE. Absent for codes that carry none.
object
Example
{ "issues": [ { "code": "ACTION_NOT_APPLICABLE", "action": "compute:CreateVpc" } ]}Resource not found.
object
The findings of a rejected IAM policy document (custom and resource policies).
One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.
object
The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.
Index of the statement at fault; -1 for the document as a whole.
The offending action, where one is at fault (ACTION_NOT_APPLICABLE).
Field errors, for codes that carry them: each problem at its field
(see FieldError). Documented on the responses that set it, e.g.
approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED.
Absent otherwise.
One field’s problem, the stage-2 installer API’s shape.
field is the field’s path, message what is wrong with it.
object
The field’s path, e.g. links[0].interface, disks[1].confirm,
mgmt.address or role; empty for the whole request.
Structured facts about the refusal, for codes that carry them. The
shape is fixed per code and documented on the response that answers
it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and
IMAGE_IN_USE. Absent for codes that carry none.
object
Example
{ "issues": [ { "code": "ACTION_NOT_APPLICABLE", "action": "compute:CreateVpc" } ]}The join is not pending or queued (JOIN_STATE_CONFLICT), or the
approval is refused (JOIN_APPROVAL_REFUSED; errors names each
field, details.reasons says each problem in a line).
object
The findings of a rejected IAM policy document (custom and resource policies).
One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.
object
The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.
Index of the statement at fault; -1 for the document as a whole.
The offending action, where one is at fault (ACTION_NOT_APPLICABLE).
Field errors, for codes that carry them: each problem at its field
(see FieldError). Documented on the responses that set it, e.g.
approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED.
Absent otherwise.
One field’s problem, the stage-2 installer API’s shape.
field is the field’s path, message what is wrong with it.
object
The field’s path, e.g. links[0].interface, disks[1].confirm,
mgmt.address or role; empty for the whole request.
Structured facts about the refusal, for codes that carry them. The
shape is fixed per code and documented on the response that answers
it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and
IMAGE_IN_USE. Absent for codes that carry none.
object
Example
{ "issues": [ { "code": "ACTION_NOT_APPLICABLE", "action": "compute:CreateVpc" } ]}