Using saltext-incus¶
This extension manages Incus instances from Salt. It has two surfaces:
Instance lifecycle (create, start, stop, configure, delete) driven through the
incuscommand line.Agentless in-instance state application: apply SLS inside an instance over
incus execwith no resident minion, the waydocker.slsworks for containers.
Requirements¶
Run these modules on the minion that is the Incus host (the machine where the
incus client talks to the local daemon). That minion needs the incus
binary on its PATH. The control node runs Salt 3007 or 3008.
For the in-instance apply functions (incus.call, incus.sls, and the
sls_applied state) with the default thin transport, the target instance
needs only a Python 3 interpreter. No Salt installation in the instance is
required. If the instance already has Salt installed, you can use
transport: baked to run its own salt-call instead of shipping the thin.
Configuration¶
Optional settings live under the incus: namespace in the minion config,
grains or pillar, and are read with config.get:
incus:
# Extra Python modules to fold into the shipped thin, same meaning as in
# salt-ssh. Usually unnecessary.
thin_extra_mods: ""
thin_so_mods: ""
Most calls take an optional project argument to scope the operation to a
non-default Incus project. Projects, networks, storage pools and profiles are
referenced by name; this version does not manage them as their own resources.
Execution module¶
All examples target a single Incus host minion. Replace 'incus-host' with
your own target, or use salt-call locally on the host.
Lifecycle¶
# Create from an image and start it
salt 'incus-host' incus.create web01 images:alpine/edge start=True
# Create with config and a profile, leave it stopped
salt 'incus-host' incus.create web01 images:alpine/edge \
profiles='[default, web]' \
config='{limits.cpu: "2", boot.autostart: true}'
# Start, stop, restart
salt 'incus-host' incus.start web01
salt 'incus-host' incus.stop web01 timeout=30
salt 'incus-host' incus.restart web01
# Inspect: returns the full instance dict (status, config, devices, profiles)
salt 'incus-host' incus.info web01
salt 'incus-host' incus.exists web01
salt 'incus-host' incus.list
# Config keys
salt 'incus-host' incus.config_set web01 boot.autostart true
salt 'incus-host' incus.config_unset web01 boot.autostart
# Devices
salt 'incus-host' incus.device_add web01 data disk source=/srv/web01 path=/data
salt 'incus-host' incus.device_remove web01 data
# Delete
salt 'incus-host' incus.delete web01
Running a command inside an instance¶
incus.call runs a single Salt execution function inside the instance:
salt 'incus-host' incus.call web01 test.ping
salt 'incus-host' incus.call web01 cmd.run 'id -un'
salt 'incus-host' incus.call web01 pkg.install nginx
Applying SLS inside an instance¶
incus.sls applies one or more SLS modules inside the instance and returns
the highstate result. The SLS and any pillar are resolved on the control node;
the instance does not need a master or a minion.
# Apply two SLS modules
salt 'incus-host' incus.sls web01 mods=access.users,access.sshd
# Dry run (nothing changes inside the instance)
salt 'incus-host' incus.sls web01 mods=hardening test=True
# Precompiled mode: no SLS source or Jinja ever reaches the instance
salt 'incus-host' incus.sls web01 mods=hardening precompiled=True
Key arguments:
mods: SLS modules, as a comma-separated string on the CLI or a list in a state.pillar: pillar data, already resolved on the control node. It is shipped as a root-only file and never placed on the command line.test: run in test mode.transport:thin(default) ships the thin per run;bakeduses the instance’s ownsalt-call.precompiled:False(default) ships SLS source and renders in-instance;Truecompiles on the control node and applies a self-contained tarball. See Choosing a render strategy below.cleanup:True(default) removes the in-instance staging directory after the run. SetFalseto leave it for debugging.
Building an image¶
incus.sls_build launches a throwaway instance from a base image, applies SLS
inside it, stops it, publishes it as a local image, and always deletes the
throwaway instance afterward:
# Build and publish an image aliased mycorp/web
salt 'incus-host' incus.sls_build mycorp/web base=images:alpine/edge mods=web,hardening
# Dry run: apply in test mode, do not publish
salt 'incus-host' incus.sls_build mycorp/web base=images:alpine/edge mods=web test=True
State module¶
incus.present¶
present ensures an instance exists with the declared config and devices. It
reconciles additively: it sets the config keys and devices you declare and
leaves everything else alone. It does not remove the volatile.* and
image.* keys Incus injects, and it does not remove devices inherited from
profiles. Profiles are attached at creation and are not reconciled afterward.
web01 present:
incus.present:
- name: web01
- image: images:alpine/edge
- running: true
- profiles:
- default
- web
- config:
limits.cpu: "2"
limits.memory: 2GiB
boot.autostart: true
- devices:
data:
type: disk
source: /srv/web01-data
path: /data
running: true ensures the instance is started, running: false ensures it
is stopped, and omitting it leaves the run state untouched. image is required
only when the instance must be created.
incus.running, incus.stopped, incus.absent¶
web01 running:
incus.running:
- name: web01
batch01 stopped:
incus.stopped:
- name: batch01
old01 absent:
incus.absent:
- name: old01
running and stopped fail if the instance does not exist. absent is a
no-op if it is already gone.
incus.sls_applied¶
sls_applied applies SLS inside a running instance through the agentless thin
path. The instance must already exist and be running, so compose it after
present or running with a requisite. The in-instance highstate result is
mapped onto this state: any failed inner state fails this state, test=True
yields a None result, and the inner changes are reported through
changes.
web01 present:
incus.present:
- name: web01
- image: images:alpine/edge
- running: true
web01 configured:
incus.sls_applied:
- name: web01
- mods:
- access.users
- access.sshd
- nginx
- require:
- incus: web01 present
A complete sls_applied example: nginx on Alpine¶
If you have not used sls_applied before, here is the whole chain end to end.
The goal: stand up an Alpine instance and, inside it, install nginx and enable it
as a boot service. Nothing runs an agent inside the instance; Salt ships itself
in for the duration of the run and cleans up after.
There are two pieces: the ordinary SLS that describes the in-instance state, and the orchestrating SLS that runs on the Incus host minion.
1. The in-instance SLS lives in your control node’s file_roots, exactly like
any other SLS you would apply to a minion. Save this as nginx/init.sls (so it
is referenced by the mod name nginx):
# salt://nginx/init.sls
nginx:
pkg.installed: []
nginx service:
service.running:
- name: nginx
- enable: true
- require:
- pkg: nginx
This is not Incus-specific in any way. pkg.installed installs the nginx
package, and service.running with enable: true starts it and adds it to
the boot runlevel. The require makes sure the package is installed before
Salt tries to start the service.
2. The orchestrating SLS runs on the minion that is the Incus host. Save it
as incus_web.sls:
# salt://incus_web.sls (applied on the Incus host minion)
web01 present:
incus.present:
- name: web01
- image: images:alpine/3.20
- running: true
web01 has python3:
cmd.run:
- name: incus exec web01 -- apk add --no-cache python3
- unless: incus exec web01 -- /bin/sh -c "command -v python3"
- require:
- incus: web01 present
web01 nginx configured:
incus.sls_applied:
- name: web01
- mods:
- nginx
- require:
- cmd: web01 has python3
The middle state is the one Alpine-specific wrinkle: stock Alpine images do not
ship a Python interpreter, and the default thin transport needs Python 3
inside the instance to run salt-call. This cmd.run installs it once (the
unless guard skips it on later runs), using the instance’s outbound network,
which the default Incus bridge provides. An image that already includes Python 3
would not need this step.
Apply it on the host:
salt 'incus-host' state.apply incus_web
What happens, in order:
web01 presentruns on the host. Salt calls theincusCLI to createweb01fromimages:alpine/3.20and start it. On the first run this reports the instance as created; on later runs it finds the instance already matches and reports no changes.web01 has python3runsapk add python3inside the instance so the thin has an interpreter to run under.web01 nginx configuredis thesls_appliedcall. Under the hood it:Confirms
web01exists and is running (it fails fast if not, which is why the requisites above start it first).On the control node, gathers the
nginxSLS source from your file_roots.Ships the Salt thin, that SLS source, and a small masterless minion config into a root-only directory under
/runinside the instance.Runs
salt-call --local state.apply nginxinside the instance. Salt detects the OS as Alpine, sopkg.installedresolves toapk add nginxandservice.runninguses OpenRC (rc-service nginx startplusrc-update add nginx).Removes the staging directory from the instance.
Returns the in-instance highstate to the host.
sls_appliedmaps it onto its own result: every inner state succeeded, so this state’s result isTrue, and the inner changes surface underchanges.
Reading the result. The host-side output for the last state looks roughly like this on the first run:
----------
ID: web01 nginx configured
Function: incus.sls_applied
Result: True
Comment: applied 2 in-instance states (2 changed)
Changes:
----------
nginx:
----------
new:
<installed nginx version>
old:
nginx service:
----------
nginx:
True
The Changes block is the in-instance highstate folded up: the nginx
package was installed, and the nginx service state reports the service is now
running and enabled.
Idempotency. Apply the same SLS again and nothing changes. web01 present
finds the instance already in its declared state, web01 has python3 is
skipped by its unless, and web01 nginx configured runs the in-instance
apply again but finds nginx already installed and the service already running and
enabled. sls_applied then reports Result: True with applied 2
in-instance states (0 changed) and an empty Changes block. It is safe to
run on every highstate.
Test mode. To preview without changing anything, add test=True:
salt 'incus-host' state.apply incus_web test=True
incus.present reports what it would create or change, and sls_applied
runs the in-instance apply in test mode so each inner state reports what it would
do with a None result, while nothing is actually installed or started inside
the instance.
Passing pillar to sls_applied¶
Pillar is resolved on the control node and passed through the pillar
argument. The idiomatic way to forward a pillar subtree is the json Jinja
filter, which emits a JSON object that is also valid YAML, so the state receives
a real dict:
web01 configured:
incus.sls_applied:
- name: web01
- mods:
- access.users
- pillar:
access: {{ salt['pillar.get']('access', {}) | json }}
- require:
- incus: web01 present
Note
Decrypt any GPG or Vault values before passing them, since the resolved values are what gets shipped (as a root-only file) into the instance.
Precompiled application in a state¶
Set precompiled: true to compile on the control node and ship low state
instead of SLS source:
web01 hardened:
incus.sls_applied:
- name: web01
- mods:
- hardening
- precompiled: true
- require:
- incus: web01 present
Choosing a render strategy¶
incus.sls and sls_applied support two strategies, selected by
precompiled.
Use the default (precompiled: false) for most cases. The requested SLS
source is shipped into the instance and state.apply runs there, so Jinja and
map.jinja render against the instance’s own grains. The one limitation is
that only the requested mods’ top-level component directories are shipped
(applying access.users ships all of salt://access/). A salt:// file
source that lives in a different component is not shipped by this strategy.
Use precompiled: true when you need files from other components shipped, or
when you want no SLS source, Jinja or macros to reach the instance at all. The
low state is compiled on the control node using the instance’s grains (gathered
first), referenced files are gathered, and a self-contained tarball is applied
with state.pkg. The trade-off is that the state set must compile cleanly
off-instance.
Debugging¶
Set cleanup: false (state) or cleanup=False (CLI) to leave the
in-instance staging directory in place after a run so you can inspect it. It
lives under /run/salt.incus.<random> and is owned by root with mode 0700.
If the instance already has Salt installed and you want to use it instead of
shipping the thin, pass transport: baked.