Skip to content

Facts

facts tells a recipe what the machine it runs on has: CPUs, memory, disk, addresses, whether it is a virtual machine or a container. Use facts to size configuration to the machine (worker counts, caches, buffers), to bind to the right address, and to leave out what makes no sense in a container. sadame facts shows what a recipe will see on each host.

New in 0.3.0

Minimal example

file "/etc/demo-app/app.conf", content: "workers = #{facts[:cpus] * 2}\n"

Templates see the same facts:

listen <%= facts[:ipv4] %>:443;

The facts

Fact Type Example Read from (Linux)
hostname String "web1" uname -n
fqdn String "web1.example.com" hostname -f; hostname when that fails
arch String "x86_64", "aarch64" uname -m
kernel String "6.1.0-26-amd64" uname -r
cpus Integer 4 nproc: the CPUs sadame may run on
memory_mb Integer 7941 MemTotal in /proc/meminfo
swap_mb Integer 2047 SwapTotal in /proc/meminfo
root_disk_gb Integer 39 size of the file system holding /, from df -Pk /
ipv4 String or nil "203.0.113.10" source address of the route to the internet (ip -4 route get); nothing is sent
default_interface String or nil "eth0" interface of that route
ipv4_addresses Array of Strings ["203.0.113.10", "10.8.0.1"] every IPv4 address except 127.0.0.0/8 (ip -4 -o addr show)
ipv6_addresses Array of Strings ["2001:db8::10"] global IPv6 addresses, from /proc/net/if_inet6
virtualization String or nil "kvm", "docker" systemd-detect-virt; nil on bare metal
container true or false false systemd-detect-virt --container
service_manager String or nil "systemd", "openrc" what the service resource would use
machine_id String or nil "4c2a0f5e9b1d..." /etc/machine-id: unique per installation, never changes

Where ip is not installed, as in many container images, the addresses come from /proc/net/fib_trie and /proc/net/route; where systemd-detect-virt is missing, as on Alpine, /.dockerenv, /run/.containerenv and the DMI vendor in /sys/class/dmi/id stand in. For local runs on macOS, sadame reads sysctl, route and ifconfig; virtualization and machine_id are nil there.

How facts are read

  • When asked for. Each fact is read the first time the recipe, or a template, asks for it, then kept for the run. A recipe that uses no facts runs no extra commands.
  • On the machine itself. With -h or -g, the recipe runs on the target, so the facts are the target's. hosts.rb is evaluated on the machine running sadame and has no facts.
  • Never failing. A fact that cannot be read is nil (an empty Array for the address lists). Write facts[:ipv4] || "127.0.0.1" when a recipe must work without it.
  • Spelled right. facts[:cpu] is an error that lists every fact, so a typo stops plan instead of becoming nil.

Facts are read on every run, so a configuration made from them follows the machine: after a memory upgrade, plan shows the new value and drift reports it until it is applied.

Seeing the facts

sadame facts prints what recipes see on this machine; with -h, -g or --limit it connects to those hosts, as plan does, and prints theirs. --json prints them as JSON (see JSON output).

$ sadame facts
sadame 0.5.1 facts on local (Debian GNU/Linux 12 (bookworm))

hostname           3207ad2a2e73
fqdn               3207ad2a2e73
arch               x86_64
kernel             7.2.12-orbstack-00394-gbecdd6130068
cpus               10
memory_mb          8003
swap_mb            9027
root_disk_gb       140
ipv4               192.168.215.2
default_interface  eth0
ipv4_addresses     192.168.215.2
ipv6_addresses     (none)
virtualization     docker
container          true
service_manager    (none)
machine_id         (none)
$ sadame facts -h web1.example.com
[web1.example.com] sadame 0.5.1 facts on web1.example.com (Debian GNU/Linux 12 (bookworm))
[web1.example.com]
[web1.example.com] hostname           web1
[web1.example.com] fqdn               web1
[web1.example.com] arch               aarch64
[web1.example.com] kernel             7.2.12-orbstack-00394-gbecdd6130068
[web1.example.com] cpus               10
[web1.example.com] memory_mb          8003
[web1.example.com] swap_mb            9027
[web1.example.com] root_disk_gb       140
[web1.example.com] ipv4               192.168.107.3
[web1.example.com] default_interface  eth0
[web1.example.com] ipv4_addresses     192.168.107.3
[web1.example.com] ipv6_addresses     (none)
[web1.example.com] virtualization     docker
[web1.example.com] container          true
[web1.example.com] service_manager    systemd
[web1.example.com] machine_id         d9eb31414e07441d850ab7344644b841

Hosts
  web1.example.com  ok

facts on 1 host: 1 ok. Finished in 0.22s.

Scenarios

examples/facts/recipe.rb
# What a recipe can know about the machine it runs on. Each fact is read on
# that machine the first time the recipe asks for it.

# A login banner that says what this machine is; the template reads facts too.
file "/etc/motd", template: "motd.erb", mode: "0644"

# An application sized to the machine: two workers per CPU, an eighth of the
# memory for its cache, listening on the address of the default route.
directory "/etc/demo-app", mode: "0755"
file "/etc/demo-app/app.conf",
     content: "workers = #{facts[:cpus] * 2}\n" \
              "cache_mb = #{facts[:memory_mb] / 8}\n" \
              "listen = #{facts[:ipv4] || "127.0.0.1"}:8080\n",
     mode: "0644"

# A nightly job at a minute of its own, so that a fleet does not start it at
# the same moment. machine_id does not change, so neither does the minute.
seed = facts[:machine_id] || facts[:hostname]
minute = seed.bytes.inject(0) { |sum, byte| sum + byte } % 60
file "/etc/demo-app/nightly.schedule", content: "#{minute} 4 * * *\n", mode: "0644"

# Kernel settings belong to the host, not to a container that shares its kernel.
unless facts[:container]
  directory "/etc/sysctl.d", mode: "0755"
  file "/etc/sysctl.d/90-demo-app.conf", content: "net.core.somaxconn = 4096\n", mode: "0644"
end
examples/facts/templates/motd.erb
<%= facts[:fqdn] %>, <%= facts[:virtualization] || "no virtualization detected" %>
<%= facts[:cpus] %> CPUs, <%= facts[:memory_mb] %> MB of memory, <%= facts[:root_disk_gb] %> GB on /
<%= facts[:ipv4] || "no IPv4 address" %> on <%= facts[:default_interface] || "no default route" %>
Managed by sadame.

Planned in a container

The container has no /etc/machine-id, so the minute comes from the hostname, and container is true, so the kernel setting is left out:

$ sudo sadame plan examples/facts
sadame 0.5.1 plan on local (Debian GNU/Linux 12 (bookworm))

~ file[/etc/motd]
    --- /etc/motd
    +++ /etc/motd (desired)
    @@ -1,7 +1,4 @@
    -
    -The programs included with the Debian GNU/Linux system are free software;
    -the exact distribution terms for each program are described in the
    -individual files in /usr/share/doc/*/copyright.
    -
    -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
    -permitted by applicable law.
    +3207ad2a2e73, docker
    +10 CPUs, 8003 MB of memory, 140 GB on /
    +192.168.215.2 on eth0
    +Managed by sadame.

+ directory[/etc/demo-app]
    state: absent -> present
    mode: 0755

+ file[/etc/demo-app/app.conf]
    state: absent -> present
    mode: 0644
    --- /dev/null
    +++ /etc/demo-app/app.conf (desired)
    @@ -0,0 +1,3 @@
    +workers = 20
    +cache_mb = 1000
    +listen = 192.168.215.2:8080

+ file[/etc/demo-app/nightly.schedule]
    state: absent -> present
    mode: 0644
    --- /dev/null
    +++ /etc/demo-app/nightly.schedule (desired)
    @@ -0,0 +1 @@
    +25 4 * * *

Plan: 3 to create, 1 to update, 0 to remove, 0 to run, 0 unchanged.

Sizing PostgreSQL to the machine

examples/05-postgres sets shared_buffers to a quarter of the memory unless vars set it:

shared_buffers = vars[:shared_buffers] || "#{facts[:memory_mb] / 4}MB"

Common mistakes

  • Facts in hosts.rb. The inventory is evaluated before sadame connects to anything; decide per host in the recipe instead.
  • Memory and CPUs in a container. /proc/meminfo in a container shows the host's memory, not the container's limit, and cpus counts the CPUs it may be scheduled on, which ignores CPU quotas such as Docker's --cpus. Size containers with their limits in mind.
  • Assuming an address. A machine without a default route has no ipv4; one with several networks has several ipv4_addresses. Pick the one you mean, or set it in vars.
  • Using hostname as an ID. Hostnames get reused and changed; machine_id stays the same for the life of the installation.

See also

  • Recipe basics for vars, platform and templates.
  • Inventory for per-host vars, set in hosts.rb rather than read from the machine.