Skip to content

Validate a config, or the stored draft.

POST
/v1/installer/config:validate
Code sample: Shell / cURL
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.

Media typeapplication/json

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
mode

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 (profile single-node; node.yaml mode: single-node).
  • hyperconverged-init: the first node of a hyperconverged cluster, the one that founds it (“HA init”; node.yaml mode: ha-init).
  • hyperconverged-join: a node that joins a hyperconverged cluster (“HA join”; node.yaml mode: join).
  • disaggregated-control-plane: a control-plane-only node that compute and storage nodes join later (profile disaggregated, a preview whose design is open,).

GET /v1/installer/options says which are available; the others are refused at mode, with the reason.

string
Allowed values: single-node hyperconverged-init hyperconverged-join disaggregated-control-plane
profile
required

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.

string
Allowed values: single-node hyperconverged disaggregated
region
required

Node.yaml cluster.region. Baked into every ARN and DNS name; fixed once installed.

string
admin
required
object
email
required

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.

string

Example

admin@cloud.internal
dns
required
object
parentDomain
required

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.

string

Example

cloud.internal
lanAddress

The 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.

string
upstreamServers

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.

Array<string>
<= 4 items
upstreamFallback

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.

boolean
network
required
object
uplinkInterface
required

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.

string
topology
required

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).

string
Allowed values: routed-host bridged
floatingIpCidrs
required

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,).

Array<string>
defaultVpcCidr
required

Node.yaml network.default_vpc_cidr.

string
storage
required

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
disks

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.

Array<object>

One data disk and what it is for. Field errors name storage.disks[i].byId, .use, .poolName and .storageClassName.

object
byId
required

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).

string

Example

/dev/disk/by-id/nvme-Samsung_SSD_990_PRO_S7DNNJ0X123456
use
required

compute: 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.

string
Allowed values: compute ceph
poolName

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.

string

Example

fast
storageClassName

Compute 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.

string

Example

nvme
mdArrays

Md 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].

Array<object>

One md RAID array of data disks. level is required; a left-out level is refused, never read as 0.

object
name

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.

string

Example

md-data0
level

The 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).

integer
Allowed values: 0 1 10
members
required

The member disks, by the first of their byId paths in the hardware list. Wiped.

Array<string>
dataDisks
required

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.

Array<string>
wipeConfirm
required

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.

Array<string>
services
required

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
key
additional properties
boolean
time

Where the node gets its time. node.yaml time.

object
ntpServers

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).

Array<string>
<= 8 items
hostReservation

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
vcpus
required
integer
>= 1
memoryBytes
required

A whole number of MiB.

integer format: int64
>= 4294967296
advanced

Settings most installs leave as they are. Left out, each takes the default given below.

object
devMode

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.

boolean
upgradesMirrorUrl

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.

string

Example

https://mirror.example.com
releaseToken

A 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.

string
<= 512 characters

Example

rel_0123456789abcdef
cluster

The cluster itself: node.yaml cluster.name and cluster.planned_size. hyperconverged-init only.

object
name

The cluster’s name: 1 to 63 lowercase letters, digits and hyphens, starting and ending with a letter or digit. Optional.

string

Example

lab
plannedSize

The 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.

integer
Allowed values: 3 5
networks

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
storage

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
cidr
required

An IPv4 network, /30 at most, overlapping no other cluster network.

string

Example

192.0.2.0/24
vlan

Its 802.1Q tag; 0 or absent is untagged. A tagged network is carried by a vlan link with this vlanId.

integer
<= 4094

Example

90
mtu

The least MTU a link carrying it must have. A link that sets none gets it.

integer
>= 1280 <= 9216

Example

9000
overlay

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
cidr
required

An IPv4 network, /30 at most, overlapping no other cluster network.

string

Example

192.0.2.0/24
vlan

Its 802.1Q tag; 0 or absent is untagged. A tagged network is carried by a vlan link with this vlanId.

integer
<= 4094

Example

90
mtu

The least MTU a link carrying it must have. A link that sets none gets it.

integer
>= 1280 <= 9216

Example

9000
providerVlan

The 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.

integer
<= 4094
vips

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
api
required
string

Example

192.0.2.240
postgres
required
string

Example

192.0.2.241
openbao
required
string

Example

192.0.2.242
ovnNb
required
string

Example

192.0.2.243
ovnSb
required
string

Example

192.0.2.244
vmselect
required
string

Example

192.0.2.245
providerGw

The 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).

string

Example

192.0.2.246
links

The 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>.

Array<object>
<= 32 items

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
name

The NetworkManager connection id. Empty means interface.

string
kind
required
string
Allowed values: ethernet bond vlan
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.

string

Example

bond0
members

Bond only. The NICs it bonds, from the hardware list; each with a carrier, in no other link, not the OOB NIC.

Array<string>
bond

A bond’s options (node.yaml network.connections[].bond).

object
mode

Empty means active-backup, which needs no switch configuration.

string
Allowed values: active-backup 802.3ad
xmitHashPolicy

802.3ad only. Empty means layer3+4, which spreads Geneve tunnels over the members.

string
Allowed values: layer2 layer2+3 layer3+4 encap2+3 encap3+4
miimonMs

The link-monitor interval. Empty means 100.

integer
lacpRate

802.3ad only.

string
Allowed values: slow fast
parent

Vlan only. The NIC (the OOB NIC included) or the interface of another link it is on.

string

Example

bond0
vlanId

Vlan only. The tag; it must be the network’s (networks.<purpose>.vlan).

integer
>= 1 <= 4094
mtu

Empty means the largest MTU of the networks it carries, and for a link VLANs ride on, the largest of theirs.

integer
>= 1280 <= 9216
purposes
required

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.

Array<string>
Allowed values: storage overlay provider
addresses

Its addresses, as CIDRs in the networks it carries (storage, overlay). Empty means one per carried network: its first host address. provider takes none.

Array<string>

The validation result.

Media typeapplication/json
object
valid
required
boolean
errors
required
Array<object>
object
field
required

The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.

string
message
required
string
warnings
required
Array<string>

Example generated

{
"valid": true,
"errors": [
{
"field": "example",
"message": "example"
}
],
"warnings": [
"example"
]
}

The request is malformed.

Media typeapplication/problem+json

RFC 7807 problem details, the control plane’s shape.

object
type
required
string
title
required
string
status
required
integer
>= 400 <= 599
detail
string
code
required
string
requestId
required
string
errors

Field errors, for INVALID_CONFIG.

Array<object>
object
field
required

The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.

string
message
required
string

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.

Media typeapplication/problem+json

RFC 7807 problem details, the control plane’s shape.

object
type
required
string
title
required
string
status
required
integer
>= 400 <= 599
detail
string
code
required
string
requestId
required
string
errors

Field errors, for INVALID_CONFIG.

Array<object>
object
field
required

The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.

string
message
required
string

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.

Media typeapplication/problem+json

RFC 7807 problem details, the control plane’s shape.

object
type
required
string
title
required
string
status
required
integer
>= 400 <= 599
detail
string
code
required
string
requestId
required
string
errors

Field errors, for INVALID_CONFIG.

Array<object>
object
field
required

The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.

string
message
required
string

Example generated

{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"code": "example",
"requestId": "example",
"errors": [
{
"field": "example",
"message": "example"
}
]
}
Retry-After
integer

Seconds until the next attempt is considered.

The installer failed to answer.

Media typeapplication/problem+json

RFC 7807 problem details, the control plane’s shape.

object
type
required
string
title
required
string
status
required
integer
>= 400 <= 599
detail
string
code
required
string
requestId
required
string
errors

Field errors, for INVALID_CONFIG.

Array<object>
object
field
required

The structured field (network.floatingIpCidrs[1]); empty for the whole config. For a raw YAML draft, the node.yaml key.

string
message
required
string

Example generated

{
"type": "example",
"title": "example",
"status": 1,
"detail": "example",
"code": "example",
"requestId": "example",
"errors": [
{
"field": "example",
"message": "example"
}
]
}