Inventory¶
An inventory lists the machines a recipe runs on, groups them, and gives
groups and hosts their own vars and connection options. It is a file named
hosts.rb next to the recipe (or any file passed with --inventory), and
like a recipe it is plain Ruby: a static list is a few host lines, and a
dynamic inventory is code that calls host in a loop over data from a
file or a cloud provider's CLI. There is no plugin system to learn.
Minimal example¶
# Three servers in two roles. The names are placeholders: replace them with
# yours, or point them at real machines in ~/.ssh/config.
group :web do
host "web1.example.com", user: "deploy", sudo: true
host "web2.example.com", user: "deploy", sudo: true, vars: { workers: 8 }
vars workers: 4, server_name: "www.example.com"
end
group :db do
# sshd listens on a non-standard port here.
host "db1.example.com", user: "deploy", port: 2222, sudo: true
vars backup_hour: 3
end
group :all, include: [:web, :db] do
vars env: "production"
end
sadame hosts shows what sadame makes of it: every host, how it will be
reached, and each var with the layer it came from:
$ sadame hosts
sadame 0.5.1 hosts: 3 hosts from hosts.rb
web1.example.com
address web1.example.com
user deploy
port (ssh default)
sudo yes
ssh_opts (none)
groups web, all
vars
env "production" group all
workers 4 group web
server_name "www.example.com" group web
backup_hour 1 vars.rb
web2.example.com
address web2.example.com
user deploy
port (ssh default)
sudo yes
ssh_opts (none)
groups web, all
vars
env "production" group all
workers 8 host
server_name "www.example.com" group web
backup_hour 1 vars.rb
db1.example.com
address db1.example.com
user deploy
port 2222
sudo yes
ssh_opts (none)
groups db, all
vars
env "production" group all
workers 2 vars.rb
server_name "localhost" vars.rb
backup_hour 3 group db
The hosts.rb language¶
| Method | Where | Meaning |
|---|---|---|
group :name do ... end |
top level | Declares a group, or adds to one declared earlier. Groups cannot be nested. |
group :name, include: [:a, :b] |
top level | The group also contains every host of groups a and b. The block is optional. |
host "name", options |
in a group, or top level for an ungrouped host | Declares a host. A host may appear in several groups. |
vars key: value, ... |
in a group | Vars for every host of the group. |
command_output("command") |
anywhere | Runs a command on the machine running sadame and returns its output; fails when the command fails. |
JSON.parse and File.read are available, as in recipes.
Options of host:
| Option | Type | Default | Meaning |
|---|---|---|---|
address |
String | the host's name | Address to connect to, when the name is only a label. |
user |
String | ssh's default (~/.ssh/config or your user name) |
User to log in as. |
port |
Integer | ssh's default (~/.ssh/config or 22) |
SSH port. |
sudo |
true or false |
false |
Run sadame with sudo on this host. |
ssh_opts |
Array of Strings | [] |
Extra options for ssh and scp, such as ["-i", "~/.ssh/deploy"] or ["-F", "ssh_config"]. |
vars |
Hash | {} |
Vars for this host only. |
Connection options may be given in any one declaration of a host. Giving the same option two different values is an error; vars from several declarations are merged.
Vars precedence¶
Every target gets one vars Hash, merged from these layers. A higher layer
wins for the same key:
| Precedence | Layer | Shown by sadame hosts as |
|---|---|---|
| 1 (highest) | --var key=value on the command line |
--var |
| 2 | vars: of the host |
host |
| 3 | vars of the host's groups; a group overrides the groups it includes, and otherwise a group declared later overrides one declared earlier |
group NAME |
| 4 (lowest) | vars.rb next to the recipe |
vars.rb |
In the example, workers is 2 in vars.rb, 4 for the web group and 8
for web2; env is "staging" in vars.rb but "production" from
all, which includes web and db. --var overrides all of them:
$ sadame hosts --limit web2.example.com --var workers=16
sadame 0.5.1 hosts: 1 host from hosts.rb
web2.example.com
address web2.example.com
user deploy
port (ssh default)
sudo yes
ssh_opts (none)
groups web, all
vars
env "production" group all
workers 16 --var
server_name "www.example.com" group web
backup_hour 1 vars.rb
Selecting hosts¶
Without -h, -g or --limit, sadame runs on the machine it is started
on. With any of them, it runs on the selected hosts over SSH (see
Remote hosts).
| Flag | Selects |
|---|---|
-g web,db |
the hosts of these groups, including groups they include |
-h web1.example.com,admin@db9.example.com:2200 |
these hosts; a name found in the inventory keeps its options, with user and port from the flag when given; any other name works too |
--limit "web*" |
only the selected hosts whose name matches the pattern (* any characters, ? one); alone, it filters every host of the inventory |
Flags combine: -g db -h web1.example.com selects both, and
-g all --limit "web*" narrows all down to the web servers. Hosts are
taken in the order of the groups given, then the -h hosts; each runs once.
sadame hosts accepts the same flags, which makes it the way to check a
selection before running anything.
Using groups in recipes¶
A recipe learns the groups of the host it runs on from host_groups, a
list of symbols that includes groups reached through include:. The
example's site.rb picks roles that way:
# Every host gets the base below; the roles follow the host's groups in
# hosts.rb. Run it on all of them with: sadame apply site.rb -g all
file "/etc/motd", template: "motd.erb", locals: { name: host_name, groups: host_groups }
# Each role's resources are tagged with the role, so --tags web plans or
# applies only the web role, without the base.
include_recipe "roles/web", tags: [:web] if host_groups.include?(:web)
include_recipe "roles/db", tags: [:db] if host_groups.include?(:db)
host_name is the host's name from the inventory or -h. For local runs
they are "local" and [], unless set with --host-name and
--host-groups, which is a way to try a role on a test machine:
sudo sadame plan site.rb --host-groups web,all
Dynamic inventories¶
From a file¶
# A dynamic inventory: hosts.rb is Ruby, so it can build the host list from
# any data. This one reads servers.json, which another tool might write.
# Paths are relative to where you run sadame, so build them from __FILE__.
servers = JSON.parse(File.read(File.join(File.dirname(__FILE__), "servers.json")))
servers.group_by { |server| server["role"] }.each do |role, members|
group role.to_sym do
members.each do |server|
options = { address: server["address"], user: "deploy" }
options[:port] = server["port"] if server["port"]
host server["name"], options
end
end
end
group :all, include: [:web, :db] do
vars env: "production"
end
[
{ "name": "web1.example.com", "address": "203.0.113.11", "role": "web" },
{ "name": "web2.example.com", "address": "203.0.113.12", "role": "web" },
{ "name": "db1.example.com", "address": "203.0.113.21", "role": "db", "port": 2222 }
]
$ sadame hosts -g db
sadame 0.5.1 hosts: 1 host from hosts.rb
db1.example.com
address 203.0.113.21
user deploy
port 2222
sudo no
ssh_opts (none)
groups db, all
vars
env "production" group all
From a cloud provider's CLI¶
command_output runs the provider's CLI on the machine running sadame,
with whatever login that CLI already has. These three files are in the
example directory; pass one with --inventory.
# Hetzner Cloud: every server, grouped by its "role" label.
# sadame hosts --inventory hosts-hcloud.rb
servers = JSON.parse(command_output("hcloud server list -o json"))
servers.each do |server|
role = server["labels"]["role"] || "unassigned"
group role.to_sym do
host server["name"], address: server["public_net"]["ipv4"]["ip"], user: "root"
end
end
# DigitalOcean: every Droplet, in one group per tag.
# sadame hosts --inventory hosts-doctl.rb
droplets = JSON.parse(command_output("doctl compute droplet list --output json"))
droplets.each do |droplet|
public_ip = droplet["networks"]["v4"].find { |network| network["type"] == "public" }
next if public_ip.nil?
droplet["tags"].each do |tag|
group tag.to_sym do
host droplet["name"], address: public_ip["ip_address"], user: "root"
end
end
end
# AWS EC2: running instances, named and grouped by their Name and Role tags.
# sadame hosts --inventory hosts-aws.rb
reply = JSON.parse(command_output("aws ec2 describe-instances " \
"--filters Name=instance-state-name,Values=running --output json"))
reply["Reservations"].each do |reservation|
reservation["Instances"].each do |instance|
tags = {}
(instance["Tags"] || []).each { |tag| tags[tag["Key"]] = tag["Value"] }
next if instance["PublicIpAddress"].nil?
role = (tags["Role"] || "unassigned").to_sym
group role do
host tags["Name"] || instance["InstanceId"], address: instance["PublicIpAddress"], user: "ec2-user"
end
end
end
The tests run each of them against recorded CLI output, so a change in
sadame that breaks them is noticed. A change in a CLI's output format is
not; check sadame hosts --inventory ... after upgrading the CLI.
Common mistakes¶
- Relative paths in hosts.rb.
File.read("servers.json")is relative to the directory you run sadame from. Build paths fromFile.dirname(__FILE__), as the example does. varsoutside a group. Top-levelvarsis an error; usevars.rbfor defaults andhost ..., vars: {}for one host.- Expecting
-g allwithout a group namedall. Groups are only whathosts.rbdeclares.sadame hostswithout flags lists every host. - Slow commands in a dynamic inventory.
command_outputruns every time the inventory loads, including forplan. Cache the result in a file if the CLI is slow. - Secrets in hosts.rb. It is evaluated on the machine running sadame,
and its vars are sent to the targets. Read secrets from the environment
(
ENV["DB_PASSWORD"]) rather than committing them. - Facts in hosts.rb. For the same reason,
hosts.rbcannot see what a machine has. Read facts in the recipe, which runs on the target, and keep inhosts.rbwhat you decide.
See also¶
- Remote hosts: how sadame connects, sudo, jump hosts and troubleshooting.
- Recipe basics: vars and
include_recipe.