Suil data and runs
Suil provisions Linux hosts from declarative data. This page describes the data a workspace holds, how suil resolves it into the configuration of a run, and how a run applies that configuration node by node.
Data layer
A workspace is the directory suil is started in. Suil reads and writes only four directories and one file under it:
<workspace>/
data/ site data, read only
common.yaml connection defaults and the hierarchy
os/ one file per OS family and release
modules/ one file per module
roles/ one file per role
nodes/ one file per node, named by its FQDN
modules/ module code and module defaults, read only
<module>/data/ defaults: *.yaml, then os/<family>/<release>.yaml
modules.yaml the modules of the workspace and where they come from
facts/ what suil last read on each node, written by suil
.runs/ the resolved configuration of each run, written by suil
The layout under data/ other than common.yaml is not fixed: it is whatever
the hierarchy in common.yaml names. The tree above follows the default
hierarchy. The smallest workspace that runs is common.yaml, one role file
and one node file:
# data/common.yaml
deployment:
ssh_port: 22
ssh_user: deploy
ssh_accept_unknown_hosts: yes
hierarchy:
- os/{family}/{release}.yaml
- modules/{module}.yaml
- roles/{role}.yaml
- nodes/{node}.yaml
# data/roles/web.yaml
modules:
- base
- nginx
# data/nodes/web-01.example.com.yaml
role: web
deployment:
ssh_host: 192.0.2.10
Common
data/common.yaml holds two keys.
deployment- how suil connects to every node. Each key goes to the pyinfra SSH connector as host data.ssh_hostis the address and defaults to the node name,ssh_accept_unknown_hoststurns intossh_strict_host_key_checking. A role or a node overrides any of them in its owndeploymentkey.hierarchy- the ordered list of layer patterns, lowest first. A pattern is a path underdata/with placeholders:{family}and{release}from the OS of the node,{module}for each module of the node,{role}and{node}.
The pattern with {role} is where suil looks for roles, the pattern with
{node} is where it looks for nodes. Both must be in the hierarchy.
Hierarchy
Suil builds the configuration of every node of a run the same way.
- It reads the node file. The file must exist, and its
rolenames the role of the node. - It reads the role file, which must exist too. The modules of the node are
the role's
modulesfollowed by the node's ownmodules, without duplicates. - When the OS of the node is known, the modules are put in run order: every
module after the modules its
requires.yamlnames, depth first, in the order of each list. A cycle is an error. - It merges the layers, each one over the previous:
deploymentfromcommon.yaml, thedata/*.yamlof every module followed by that module's owndata/os/{family}/{release}.yaml, then every hierarchy pattern in order. A pattern with{module}is read once per module. - It expands the lookups, see Lookup.
Every top-level key of the result is a module name: ssh: in any layer is the
configuration of the ssh module. A missing layer file is an empty layer,
except the node file and the role file.
Dicts merge key by key and lists concatenate. A tag on a value changes that for the value:
| Tag | Effect |
|---|---|
!replace |
Replaces the value below instead of merging with it |
!append |
Concatenates a list, the default made explicit |
!merge |
Merges a list of dicts item by item, matched by name |
!delete |
Removes the key from the result |
# data/nodes/web-01.example.com.yaml
example:
packages: !replace
- htop
The OS layer is the one only the node knows. Suil probes the OS when it
connects and caches the family and release in facts/<node>/suil.yaml. A node
with no cache is resolved without the OS layer, and its module list is marked
provisional until the run probes it.
Secrets
A secret is stored encrypted in place, in any data file, as an !ENC[...]
value made with age:
suil encrypt 'the value'
Suil keeps the ciphertext while it resolves the data. It decrypts a secret
only while it applies the node that uses it, with the key in SUIL_AGE_KEY.
Everything that is printed, written to .runs/ or sent to a node for fact
collection carries a sha256: digest in place of the secret.
Lookup
!LOOKUP reads a field from the nodes of the run instead of repeating it in
the data. It takes these keys:
| Key | Meaning |
|---|---|
field |
The dotted path to read on each node; required |
nodes |
A glob over node names; * by default |
role |
Only nodes of this role |
ip |
v4, v6 or any (default); keeps addresses of that family |
format |
How each value is written, with {value}, {node} and {role}; {value} by default |
self |
include (default), exclude or only the node that asks |
Inside a list the values found are spliced in place, beside the literal items. Anywhere else the marker becomes the list of values. The values are sorted by node name, so a rendered file does not change between runs.
# data/modules/unbound.yaml
unbound:
local_zones:
- name: example.com
type: transparent
data:
- "gw.example.com. A 192.0.2.1"
- !LOOKUP
field: unbound.host_address
format: "{node}. A {value}"
A lookup sees only the nodes of the run, the way a Hiera lookup sees only the data of the catalogue being compiled. A run for one role sees the nodes of that role, and a run for one node sees that node alone. A lookup that finds nothing fails the run with the node and the field. A node whose field is missing or empty is skipped.
A lookup reads the other nodes as merged, before their own lookups are expanded, so one lookup never reads another.
Run
A run is one role or one node. Suil resolves every node of it into a
Directory before anything connects. The Directory does not change after
that, except for the OS layer each node gets when it is probed. Modules never
see it: a module gets its configuration, its facts and force.
Commands
suil apply --role web
suil apply --role web --confirm
suil apply --node web-01.example.com --confirm --dry-run
suil config --role web
suil facts --node web-01.example.com
suil nodes
suil modules
suil init
suil --version
suil
applyshows the nodes of the run and their modules. It applies them only with--confirm.--dry-runconnects and shows what would change, changing nothing.--forceruns every module even when nothing changed.configshows the run and the public configuration of every module, without connecting.factsconnects, probes and collects facts, and changes nothing.nodeslists the nodes of the workspace and their roles.moduleslists the modules, theirrequiresand their run order.validatechecksmodules.yamland every module it declares and connects to nothing;config,factsandapplyrun it first.installclones the Git repositories into the suil cache, andautoupdatemoves eachversionto the newest tag.initlays out an empty workspace in the current directory:data/common.yamlwith the default hierarchy, the layer directories underdata/andmodules/.modules.yamlis yours to write. It writes nothing wheredata/,modules/ormodules.yamlalready is.--versionprints the version of the installed package.suilwith no command opens the interactive mode. It asks for one role, shows the run and asks before it applies.
Give --role or --node, not both. The command line never asks a question:
--confirm is the only way to apply from it.
Node by node
Suil applies the nodes of a run one after another. A node that fails stops the run, and the nodes after it are not touched.
connect SSH to the node
probe read the OS, cache it, resolve the node again with the OS layer
configs validate every module configuration, decrypt its secrets
facts every module's collector reads the node
queue every module queues the operations that close its diff
apply pyinfra runs the queue; --dry-run stops here
facts again only for the modules that queued operations
check module problems and a remaining diff fail the run
record .runs/<timestamp>/<node>/ with the public configuration
A module queues operations only when its expected state differs from the
facts, or when --force is given or its code changed since the last run. A
node that already matches gets no operations. The record is written after the
node, also when the apply or the check failed.
Settings
Suil reads its settings from the environment and from .env in the
workspace, when it starts:
| Variable | Purpose |
|---|---|
SUIL_AGE_KEY |
age private key; decrypts !ENC[...] values |
SUIL_AGE_RECIPIENT |
age public key; suil encrypt encrypts to it |
SUIL_SUDO_PASSWORD |
sudo password on the nodes |
SUIL_LOG_COLOR |
false turns colored output off |
A variable set in the environment wins over the same one in .env.