Validate a config, or the stored draft.
curl --request POST \ --url https://192.0.2.10:9443/v1/installer/config:validate \ --header 'Authorization: <Authorization>' \ --header 'Content-Type: application/json' \ --data '{ "mode": "single-node", "profile": "single-node", "region": "example", "admin": { "email": "admin@cloud.internal" }, "dns": { "parentDomain": "cloud.internal", "lanAddress": "example", "upstreamServers": [ "example" ], "upstreamFallback": true }, "network": { "uplinkInterface": "example", "topology": "routed-host", "floatingIpCidrs": [ "example" ], "defaultVpcCidr": "example" }, "storage": { "disks": [ { "byId": "/dev/disk/by-id/nvme-Samsung_SSD_990_PRO_S7DNNJ0X123456", "use": "compute", "poolName": "fast", "storageClassName": "nvme" } ], "mdArrays": [ { "name": "md-data0", "level": 0, "members": [ "example" ] } ], "dataDisks": [ "example" ], "wipeConfirm": [ "example" ] }, "services": { "additionalProperty": true }, "time": { "ntpServers": [ "example" ] }, "hostReservation": { "vcpus": 1, "memoryBytes": 1 }, "advanced": { "devMode": true, "upgradesMirrorUrl": "https://mirror.example.com", "releaseToken": "rel_0123456789abcdef" }, "cluster": { "name": "lab", "plannedSize": 3 }, "networks": { "storage": { "cidr": "192.0.2.0/24", "vlan": 90, "mtu": 9000 }, "overlay": { "cidr": "192.0.2.0/24", "vlan": 90, "mtu": 9000 }, "providerVlan": 1, "vips": { "api": "192.0.2.240", "postgres": "192.0.2.241", "openbao": "192.0.2.242", "ovnNb": "192.0.2.243", "ovnSb": "192.0.2.244", "vmselect": "192.0.2.245", "providerGw": "192.0.2.246" } }, "links": [ { "name": "example", "kind": "ethernet", "interface": "bond0", "members": [ "example" ], "bond": { "mode": "active-backup", "xmitHashPolicy": "layer2", "miimonMs": 1, "lacpRate": "slow" }, "parent": "bond0", "vlanId": 1, "mtu": 1, "purposes": [ "storage" ], "addresses": [ "example" ] } ] }'With a body, validates that structured config without storing it;
without one, validates the stored draft. Errors name the structured
field they belong to (mode, dns.parentDomain,
storage.dataDisks[0], storage.disks[1].poolName); an error with
an empty field applies to the whole config. A config whose mode is
not available yet gets an error on mode saying why, and only the
mode’s own checks (its profile, the fields that do not belong to
it); the rest of its answers are judged once the mode is available.
Authorizations
Section titled “Authorizations”Request Body
Section titled “Request Body”The product answers for an install. The first is mode,
the install mode, which decides which of the others apply (see
GET /v1/installer/options, modes). Every field maps onto
node.yaml; GET /v1/installer/config.yaml shows the
result.
mode is optional so that clients that predate it keep working: a
config with no mode is a single-node one, as every config was
before it existed. A field the mode does not list is refused with
an error on that field that names the mode. A config whose mode is
not available yet is refused at mode.
object
How this node is installed: the first answer of stage 2. Each mode is a deployment profile of the release model and the node’s part in it.
single-node: one node runs everything (profilesingle-node; node.yamlmode: single-node).hyperconverged-init: the first node of a hyperconverged cluster, the one that founds it (“HA init”; node.yamlmode: ha-init).hyperconverged-join: a node that joins a hyperconverged cluster (“HA join”; node.yamlmode: join).disaggregated-control-plane: a control-plane-only node that compute and storage nodes join later (profiledisaggregated, a preview whose design is open,).
GET /v1/installer/options says which are available; the others
are refused at mode, with the reason.
The deployment profile of mode, which it must match:
single-node for single-node (and for a config with no mode),
hyperconverged for both hyperconverged modes, disaggregated
for the disaggregated one. node.yaml has no profile; it follows
from the mode.
Node.yaml cluster.region. Baked into every ARN and DNS name; fixed once installed.
object
The first admin’s email, node.yaml iam.admin_email. The password is generated and written to the credential file the report names. A new draft starts with admin@cloud.internal.
Example
admin@cloud.internalobject
Node.yaml dns.parent_domain, the domain the install serves its
names under. It need not be public, and a new draft starts with
cloud.internal. It may not be, or sit under, stratum.internal
or compute.internal: the control plane creates those zones
itself. Avoid .local, which macOS and most Linux desktops send
to multicast DNS.
Example
cloud.internalThe node’s LAN address its authoritative DNS answers on, node.yaml identity.self_ip. Defaults to the OOB
NIC’s address; empty lets the install use the default route’s
source address.
The resolvers the cloud’s DNS forwards every name it does not
serve to, node.yaml dns.upstream_servers: IPv4
addresses, not this node’s own. Optional: with none, tenant VMs
resolve public names from the root servers, which needs DNS out
to the internet; a site that only allows its own resolvers must
list them. Not the host’s own resolvers (stage 1 set those), though
they are often the same: hardware.resolvers lists them, as a
suggestion. A new draft starts with none.
Node.yaml dns.upstream_fallback: when every upstream
server fails, tenant VMs resolve names from the root servers
instead of getting a failure. Off by default: the fallback needs
DNS out to the internet, hides a dead upstream, and cannot resolve
names only your own resolvers serve. No effect without
upstreamServers. Changeable after the install.
object
The NIC tenant traffic leaves through, node.yaml provider.upstream_interface. Defaults to the OOB NIC. Another NIC must already have an IPv4 address; stage 1 configures only the OOB NIC.
Node.yaml provider.topology: one of the mode’s
(options.modes[].topologies, with the copy in
options.modes[].provider). A single node is routed-host. A
founder chooses: bridged (its uplink is the link that carries
provider, else the OOB NIC on networks.providerVlan; the
first floating-IP range is then the VPC transit range, node.yaml
network.transit_cidr) or routed-host (no provider VLAN and no
provider link; the uplink is the OOB NIC, and the founder routes
the floating IPs, as their only gateway).
Node.yaml network.floating_ip_cidrs. Required, at least one;
route them to the node on the LAN router. A new draft starts with
the range the options recommend for this node
(modes[].network.floatingIpCidrs.suggested,).
Node.yaml network.default_vpc_cidr.
The node’s disks. By default the OS disk’s free space at
its end is the node’s first pool, stratum-compute, with the
storage class stratum-compute, the install’s default. When stage
1 gave the OS disks whole to the system (options.modes[].storage.osDiskLayout
is whole-disk,) there is no such pool: disks must have
a compute disk, and the first one is the node’s first pool and its
class the default. Every other disk is left as it is unless it is
listed in disks (or the older dataDisks), and every listed disk
is wiped. storagePlan on the draft says what the install does to
each disk of the machine.
object
The data disks, each with what it is for: an LVM-thin
pool for instance volumes (compute), or, on a node of a
cluster, a disk reserved for a later Ceph bootstrap (ceph; not
on a single node,). node.yaml storage.compute_pools
and storage.ceph_osd_disks. A disk appears once. Use this or
dataDisks, not both. A new draft starts with none.
An entry may name an array of mdArrays instead of a disk:
byId is then /dev/md/<name>, and only use: compute is
accepted.
One data disk and what it is for. Field errors name
storage.disks[i].byId, .use, .poolName and .storageClassName.
object
The disk, by the first of its byId paths in the hardware list.
It must be dataDiskEligible, and is WIPED. Or an array of
storage.mdArrays, by /dev/md/<name> (compute only).
Example
/dev/disk/by-id/nvme-Samsung_SSD_990_PRO_S7DNNJ0X123456compute: the disk becomes an LVM-thin pool on this node, which
holds instance volumes (instance root disks and the volumes
tenants create). Name the pool and its storage class.
ceph: the disk is wiped now and reserved for Ceph. Nothing Ceph
runs at install. The node records the disk as a Ceph OSD
candidate, and a later Ceph bootstrap (POST /v1/admin/storage/ceph/bootstrap, or Ceph setup in the console)
starts with it selected as an OSD. Only where the mode offers it
(options.modes[].storage.cephOffered): Ceph needs a cluster, so
a single-node install refuses it here.
Compute only, and required for it. The pool’s name: this node’s
LVM-thin space on this disk, holding instance volumes. Pool names
are unique on a node and follow the storage-pools API’s rule: 3
to 40 characters, lowercase letters, digits and hyphens, starting
with a letter and ending with a letter or digit. stratum-compute,
data, stratum and stratum-block are taken by the install’s
own storage. node.yaml storage.compute_pools[].name.
Example
fastCompute only. The storage class: what tenants pick when they
create a volume. A class names one pool name, and the pools of
that name on every node are its capacity, so one class can span
pools on several nodes. Empty means the same as poolName, which
is what a single node needs; a separate name keeps the class
(what tenants see) apart from the pools (where it lives). Same
name rule as a pool; not stratum-compute, the OS disk’s class;
two pools on one node cannot share a class. node.yaml
storage.compute_pools[].storage_class.
Example
nvmeMd RAID arrays to build from data disks, node.yaml
storage.md_arrays. Optional. Each array is then ONE device: an
entry of disks names it by /dev/md/<name> to make it a
compute pool, with its own pool name and storage class; an array
no entry names is built and left unused. Disks in no array stay
separate disks. Its members are data disks (the rules of
disks[].byId), each in at most one array and not listed in
disks themselves; every member is wiped, and one that holds
data must be in wipeConfirm. storagePlan lists each member
(use: md-member) and each array (mdMembers). Field errors
name storage.mdArrays[i].name, .level, .members and
.members[k].
One md RAID array of data disks. level is required; a
left-out level is refused, never read as 0.
object
The array’s name: its device is /dev/md/<name>. 3 to 32
characters, lowercase letters, digits and hyphens, starting with
a letter and ending with a letter or digit; unique; not boot,
pv or stratum-compute (the install’s own arrays). Empty
means md-data<i>, with i the entry’s index in mdArrays.
Example
md-data0The RAID level (options.modes[].storage.mdLevels). 1: a mirror
of 2 or more members, the size of the smallest. 10: mirrored
pairs, an even number of members, at least 4, half the total
size. 0: striped over 2 or more members, the total size, and no
redundancy (a warning).
The member disks, by the first of their byId paths in the hardware list. Wiped.
The older form of disks, kept for clients that predate it:
whole disks (by-id paths from the hardware list) that each become
a compute pool named compute-2, compute-3,…, with a storage
class of the same name (node.yaml storage.compute_disks). Send
an empty list when you use disks.
The listed disks (in disks or dataDisks) that hold data and
may be wiped, by the same by-id path. A disk that holds data and
is not listed here is refused, whether it is for compute or for
Ceph. node.yaml storage.force_wipe.
Service key to enabled, for the switchable services the profile
offers (see options). A service left out keeps its default.
Disabled ones become node.yaml services.disabled.
object
Where the node gets its time. node.yaml time.
object
NTP servers, as host names or IP addresses, one chrony server
line each. node.yaml time.ntp_servers. A new draft starts with
options.time.suggested: DHCP’s NTP servers, else the
public NTP pool. Left out (no time, or no ntpServers), the
install uses the same servers. An empty list is accepted, with a
validation warning: the node then syncs only from NTP servers
DHCP offers, and keeps its own clock when there are none (an
air-gapped site with no NTP server).
The host reservation the operator chose, node.yaml
host_reservation. Left out, the node computes its default from
its size and keeps it in step (modes[].hostReservation.default);
send it only when the operator changed the value. Both values are
required, between minimum and maximum, memory a whole number of
MiB. Errors name hostReservation.vcpus and
hostReservation.memoryBytes. Only in a mode that lists the field
(single-node). The control plane takes it at the node’s first
registration; after that stratumctl nodes set-reservation
changes it.
object
A whole number of MiB.
Settings most installs leave as they are. Left out, each takes the default given below.
object
Node.yaml dev_mode: the OpenBao web UI, the
substrate’s web UIs (OpenBao, VictoriaMetrics, VictoriaLogs,
vmagent, OTel zPages) published on the node’s loopback, and the
OpenBao root token and Postgres password shown in the admin
substrate-tools response. Left out, it is the mode’s default
(options.modes[].defaultDevMode,): on for
single-node, off for the others; a fresh draft carries the
default mode’s. The install always writes the key. Changed later
by editing node.yaml, running stratum-bootstrap render-quadlets, and restarting the units it changes.
The release mirror the cluster checks for newer releases, node.yaml upgrades.mirror_url: an http(s) URL
without credentials, query or fragment. A new draft starts with
the default this installer was built with
(options.limits.defaultUpgradesMirrorUrl).
Left out, or the default, the cluster follows the default of the
release it runs. Empty switches the update check off, for an
offline install upgraded from media. The founder stores it once;
after the install it changes with stratumctl admin upgrade mirror set.
Example
https://mirror.example.comA release token from the Stratum Account, node.yaml
upgrades.release_token: it opens the stable release channel
(releases.stratum-labs.cloud), which the update check and the
release downloads then read, sending the token there and to no
other host. Printable ASCII without spaces. Left out or empty,
the cluster reads the default mirror. The founder stores it in
OpenBao at the end of the install, unless the cluster already
holds one; after that it is set, replaced or removed in the
console’s upgrade settings (PUT/DELETE /v1/admin/upgrades/release-token) and never shown again. The
draft and config.yaml echo it to this install session only.
Example
rel_0123456789abcdefThe cluster itself: node.yaml cluster.name and
cluster.planned_size. hyperconverged-init only.
object
The cluster’s name: 1 to 63 lowercase letters, digits and hyphens, starting and ending with a letter or digit. Optional.
Example
labThe planned number of control-plane members (default 3). It drives the formation findings; nothing is rendered from it. It is the install-time plan, not a cap: a cluster takes joiners up to 5 control-plane nodes.
The cluster’s networks, defined once on
the founder: node.yaml cluster.networks, cluster.vips and
provider.vlan. The management network is the OOB NIC’s subnet
(options.modes[].cluster.mgmt). hyperconverged-init only.
object
One purpose’s network. Optional: without it the purpose rides mgmt.
A link must carry it. Errors name networks.<purpose>.cidr, .vlan
and .mtu.
object
An IPv4 network, /30 at most, overlapping no other cluster network.
Example
192.0.2.0/24Its 802.1Q tag; 0 or absent is untagged. A tagged network is
carried by a vlan link with this vlanId.
Example
90The least MTU a link carrying it must have. A link that sets none gets it.
Example
9000One purpose’s network. Optional: without it the purpose rides mgmt.
A link must carry it. Errors name networks.<purpose>.cidr, .vlan
and .mtu.
object
An IPv4 network, /30 at most, overlapping no other cluster network.
Example
192.0.2.0/24Its 802.1Q tag; 0 or absent is untagged. A tagged network is
carried by a vlan link with this vlanId.
Example
90The least MTU a link carrying it must have. A link that sets none gets it.
Example
9000The VLAN the VM uplink trunks (node.yaml provider.vlan); 0 is
untagged. Bridged only: required (1 to 4094) when no link carries
provider (the uplink is then the OOB NIC, and its untagged
traffic is the node’s own); refused under routed-host.
The service VIPs (node.yaml cluster.vips), on the management
network. Each must be in its subnet, not the node’s own address nor
another VIP, outside the floating-IP ranges, and free: the draft’s
validation probes each with ARP on the OOB NIC, and an address that
answers is refused. Errors name networks.vips.<name>.
object
Example
192.0.2.240Example
192.0.2.241Example
192.0.2.242Example
192.0.2.243Example
192.0.2.244Example
192.0.2.245The routed-host floating-IP next hop: the LAN router routes every floating range to it, and
it follows the node whose chassis holds the VPC gateways. Required
when network.topology is routed-host, refused when bridged
(the router is the floating IPs’ gateway there).
Example
192.0.2.246The founder’s links beyond the OOB NIC, each with the purposes it carries: ethernet NICs, bonds
of NICs, and VLANs on either (or on the OOB NIC). node.yaml
network.connections, a bond’s members as their own entries.
hyperconverged-init only.
The OOB NIC is not listed: it carries oob and mgmt. A purpose
no link carries falls back: storage and overlay ride mgmt,
and provider the OOB NIC on networks.providerVlan. At most
one link per purpose. Field errors name links[i].<field>.
One link of the founder: an ethernet NIC, a bond of NICs, or a VLAN on a NIC or a bond, and what it carries.
object
The NetworkManager connection id. Empty means interface.
Ethernet: the NIC, from the hardware list (not the OOB NIC). bond:
the bond’s name, e.g. bond0. vlan: the VLAN interface’s name;
empty means vlan<vlanId>. At most 15 characters.
Example
bond0Bond only. The NICs it bonds, from the hardware list; each with a carrier, in no other link, not the OOB NIC.
A bond’s options (node.yaml network.connections[].bond).
object
Empty means active-backup, which needs no switch configuration.
802.3ad only. Empty means layer3+4, which spreads Geneve tunnels over the members.
The link-monitor interval. Empty means 100.
802.3ad only.
Vlan only. The NIC (the OOB NIC included) or the interface of another link it is on.
Example
bond0Vlan only. The tag; it must be the network’s (networks.<purpose>.vlan).
Empty means the largest MTU of the networks it carries, and for a link VLANs ride on, the largest of theirs.
What it carries: storage, overlay, provider. provider only
on an ethernet or bond link, the trunk networks.providerVlan
arrives on. A link that carries nothing is only a parent of VLANs.
Its addresses, as CIDRs in the networks it carries (storage,
overlay). Empty means one per carried network: its first host
address. provider takes none.
Responses
Section titled “Responses”The validation result.
object
object
The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.
Example generated
{ "valid": true, "errors": [ { "field": "example", "message": "example" } ], "warnings": [ "example" ]}The request is malformed.
RFC 7807 problem details, the control plane’s shape.
object
Field errors, for INVALID_CONFIG.
object
The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.
Example generated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "code": "example", "requestId": "example", "errors": [ { "field": "example", "message": "example" } ]}No install code, or the wrong one.
RFC 7807 problem details, the control plane’s shape.
object
Field errors, for INVALID_CONFIG.
object
The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.
Example generated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "code": "example", "requestId": "example", "errors": [ { "field": "example", "message": "example" } ]}Too many wrong install codes; wait for Retry-After.
RFC 7807 problem details, the control plane’s shape.
object
Field errors, for INVALID_CONFIG.
object
The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.
Example generated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "code": "example", "requestId": "example", "errors": [ { "field": "example", "message": "example" } ]}Headers
Section titled “Headers”Seconds until the next attempt is considered.
The installer failed to answer.
RFC 7807 problem details, the control plane’s shape.
object
Field errors, for INVALID_CONFIG.
object
The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.
Example generated
{ "type": "example", "title": "example", "status": 1, "detail": "example", "code": "example", "requestId": "example", "errors": [ { "field": "example", "message": "example" } ]}