I’ve been asked to provide a tutorial for using Ansible. Ansible has very good docs. This isn’t going to be a thorough tutorial. My goal is to provide just enough information that you will be able to use the Ansible docs to find out the details you need. I’m also not an expert and there are many anti-patterns and violations of best practice in the examples below.
The way I think about Ansible is as a bunch of scripts.
You have three main things to worry about with Ansible. The inventory, the playbook, and roles.
Inventory
This is a list of all your machines tagged with their purpose. For example, here is my inventory.
[desktops]
huginn
[media]
medusa
[homeauto]
argus
[nightscout]
fenrir
[cctv]
argus
[pis]
manticore
cerberos
hydra
norns-vpn
sibyl ansible_user=pi
sirens
[nas]
fafnir
[sensorReporter]
cerberos
hydra
manticore
[banana]
nidhogg
[tripwire]
manticore
cerberos
hydra
norns-vpn
sibyl ansible_user=pi
nidhogg
sirens
fenrir
argus
medusa
huginn
fafnir
[camera]
cerberos
[aiy]
sibyl ansible_user=pi
[music]
sirens
[remoteoh]
norns-vpn
[computers]
fafnir
fenrir
medusa
argus
manticore
cerberos
hydra
norns-vpn
sibyl ansible_user=pi
huginn
nidhogg
sirens
[nutclients]
fafnir
fenrir
medusa
argus
huginn
[shinobi]
argus
[docker]
argus
medusa
Yes, I have a Greek and Norse myth creature theme going. The main thing to notice is that machines can and do appear under more than one category. This is where I can, for example, put a RPi cam on one of my other RPis (let’s say hydra) and install and configure all the software just by adding hydra under the [camera] category. The next time I run those playbooks hydra will be set up and configured just like cerberos is. There are lots of places you can define this. I like to check this into git with the rest of my configs so I put this in a separate file. It can go under /etc/ansible as well.
The host names need to be resolvable from the Ansible machine (machine where you are running ansible-playbook). You may need to us FQDN or IP address depending on your network and local DNS configuration.
Playbook
The playbooks are where you define what configuration needs to be done on each of those categories. Here is my playbook for homeauto.
---
# Prerequisites:
# - install ssh
# - set up ssh for certificate only logins
# - install python
# - configure login account with sudo permissions
- hosts: homeauto
roles:
- common
- fish
# - mount-home
- vm
- docker
- ssmtp
- openzwave
- mosquitto
- influxdb
- openhab
# - grafana # not working for some reason
- screengrab
- share-ohconf
- multitail
- glances
- sensorreporter
- tripwire
Put in English, this file says “for all the hosts tagged as homeauto in the inventory, execute the following roles, common, fish, …”
At the top of the file you will see I’ve documented some prerequisites. Ansible requires ssh and python on the hosts you are trying to configure. To avoid having to type in my password a bunch of times or to provide a password to ansible-playbook, I configure all of my machines with ssh certificate login.
The way that Ansible executes a playbook is:
- ssh to the remote machine
- if required, sudo to another user
- download the Ansible python modules necessary to execute the roles
- execute those python modules in order with the defined parameters
- clean up
So you don’t need to have Ansible installed on the remote machine, just on the machine you are running Ansible on. However, some of the python modules will require python libraries to be installed on the remote machine. Luckily you can add a task to your playbooks to install the prerequisites just before they are needed.
Roles
Roles is where we get down to the fine detail of defining all of the steps necessary to configure a machine with a given role. I’ve split my roles up by application (i.e. each application has their own role). This is not the only approach but it works for me. You see the following roles in the playbook above:
- common: perform an apt-get update, install common apps like vim, htop, and curl, add the machine’s hostname to /etc/hosts, and install the nut client (this last one should probably be it’s own separate role.
- fish: install fish, make it my default shell, and download my fish configs from git
- mount-home: configure fstab to mount my home directory from my NAS (I only do this on my desktop now)
- vm: install OpenVMTools
- docker: all of the services I install are running in Docker, install the latest Docker and other ansillary stuff like py-docker so the later roles can download and run Images.
- ssmtp: install and configure ssmtp so I can get emails from the host (e.g. results of cron jobs)
- openzwave: on occasion I’ve found it useful to administer my controller
- openhab: download and configure the openHAB Docker image; roles can depend on other roles and this one depends on influxdb and mosquitto so both of those get downloaded and configured too; in addition to just installing openHAB, it also configures InfluxDB with the openhab database; OH configs are checked out of git
- sreengrab: installs and configures screengrab
- share-ohconf: sets up a samba share of the OH configs so I can edit them using VSCode (with the SSH Remote extension this is no longer needed and I plan on removing it)
- multitail: installs and configures it with openHAB log highlighting
- glances: installs and configures glances which is kind of like htop only with more information
- sensorReporter: my own python script
- tripwire: I find it useful to keep track of what my machines are doing when I’m not looking. This role requires you to manually install tripwire first because I’ve not figured out a way to do this automatically because tripwire uses a curses interface during installation.
Ansible has a defined file structure it likes you to have roles defined in. See https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html#role-directory-structure for details.
My structure is as follows:
ansible
koshak.net-hosts: Inventory
update: It is not best practice, but I have separate build and update playbooks
build
group_vars/all: place to define variables used across roles
home-auto.yml: file included above, also includes a bunch of other playbooks like media, nightscout, pis, etc.
roles: this is where the roles go
openhab: the openHAB role.
see the docs for the folders under this
Luckily you don’t have to create all these folders by hand. Ansible provides a tool called ansible-galaxy that can create these folders for you. From the build folder I’d run:
ansible-galaxy init roles/openhab
and the openhab folder and all the special Ansible subfolders will be created with a default (i.e. empty) yml file in each.
Let’s look at my openhab role:
--
- name: Change openhab group to 9001
group:
gid: 9001
name: openhab
state: present
system: yes
become: yes
- name: Create openhab user
user:
comment: 'openHAB'
createhome: no
name: openhab
shell: /bin/false
state: present
system: yes
uid: 9001 # uid of openhab user inside the official container
group: openhab
become: yes
- name: Add the openhab user to the dialout group
command: usermod -a -G dialout openhab
become: yes
- name: Add {{ share_user }} to the openhab group
command: usermod -a -G openhab {{ share_user }}
become: yes
- name: Set permissions on openhab data folder so we can check out into it
file:
path: "{{ openhab_data }}"
state: directory
owner: openhab
group: openhab
mode: u=rwx,g=rwx,o=rx
become: yes
- name: Checkout openhab config
git:
repo: "{{ openhab_conf_repo }}"
dest: "{{ openhab_data }}"
accept_hostkey: yes
become: yes
- name: Change ownership of openhab config
file:
path: "{{ openhab_data }}"
owner: openhab
group: openhab
recurse: yes
become: yes
- name: Create expected folders if they don't already exist
file:
path: "{{ item }}"
state: directory
owner: openhab
group: openhab
become: yes
become_user: openhab
with_items:
- "{{ openhab_data }}/conf"
- "{{ openhab_data }}/userdata"
- "{{ openhab_data }}/addons"
- "{{ openhab_data }}/.java"
- name: Create database
influxdb_database:
hostname: "{{ influxdb_ip_address }}"
database_name: "{{ openhab_influxdb_database_name }}"
state: present
username: admin
password: "{{ influxdb_admin_password }}"
# TODO there is currently a bug which prevents us from using influx in the container
- name: Create openhab user
# command: influx -username admin -password {{ influxdb_admin_password }} -database '{{ openhab_influxdb_database_name }}' -execute "CREATE USER {{ influx_openhab_user }} WITH PASSWORD '{{ influx_openhab_password }}'"
command: curl -XPOST http://localhost:8086/query?db={{ openhab_influxdb_database_name }}&u=admin&p={{ influxdb_admin_password }} --data-urlencode "q=CREATE USER {{ influx_openhab_user }} WITH PASSWORD '{{ influx_openhab_password }}'"
- name: Give openhab permissions on openhab db
# command: influx -username admin -password {{ influxdb_admin_password }} -database '{{ openhab_influxdb_database_name }}' -execute "GRANT ALL ON {{ openhab_influxdb_database_name }} TO {{ influx_openhab_user }}"
command: curl -XPOST http://localhost:8086/query?db={{ openhab_influxdb_database_name }}&u=admin&p={{ influxdb_admin_password }} --data-urlencode "q=GRANT ALL ON {{ openhab_influxdb_database_name }} TO {{ influx_openhab_user }}"
- name: Create grafana user
# command: influx -username admin -password {{ influxdb_admin_password }} -database '{{ openhab_influxdb_database_name }}' -execute "CREATE USER {{ influx_grafana_user }} WITH PASSWORD '{{ influx_grafana_password }}'"
command: curl -XPOST http://localhost:8086/query?db={{ openhab_influxdb_database_name }}&u=admin&p={{ influxdb_admin_password }} --data-urlencode "q=CREATE USER {{ influx_grafana_user }} WITH PASSWORD '{{ influx_grafana_password }}'"
- name: Give grafana read permissions on openhab_db
## command: influx -username admin -password {{ influxdb_admin_password }} -database '{{ openhab_influxdb_database_name }}' -execute "GRANT READ ON {{ openhab_influxdb_database_name }} TO {{ influx_grafana_user }}"
command: curl -XPOST http://localhost:8086/query?db={{ openhab_influxdb_database_name }}&u=admin&p={{ influxdb_admin_password }} --data-urlencode "q=GRANT READ ON {{ openhab_influxdb_database_name }} TO {{ influx_grafana_user }}"
- name: Install/update openHAB docker
docker_container:
detach: True
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0:rwm"
- "/dev/ttyUSB1:/dev/ttyUSB1:rwm"
env:
EXTRA_JAVA_OPTS: "-Xbootclasspath/a:/openhab/conf/automation/jython/jython-standalone-2.7.0.jar -Dpython.home=/ope
nhab/conf/automation/jython -Dpython.path=/openhab/conf/automation/lib/python"
CRYPTO_POLICY: unlimited
hostname: argus.koshak.net
image: "{{ openhab_version }}"
log_driver: syslog
name: openhab
network_mode: host
pull: True
recreate: True
restart: True
restart_policy: always
tty: yes
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- "{{ openhab_data }}/conf:/openhab/conf"
- "{{ openhab_data }}/userdata:/openhab/userdata"
- "{{ openhab_data }}/addons:/openhab/addons"
As you can see a role, just like a playbook, is in YAML format. Each section is a task. We give each task a name so we can more easily follow the execution when it runs. Then we call an Ansible module to perform some task. https://docs.ansible.com/ansible/latest/modules/modules_by_category.html lists all the hundreds of available modules. Just about any administration task you can think of has a module, and if no there is command which lets you execute an arbitrary command line command.
So, from the top of the above playbook we have the following tasks.
-
group: creates an openhab group and sets the gid to 9001 which matches the gid inside the container. The become:yes means run the command as another user, in this case root.
-
user: create an openhab user with uid 9001 which matches the uid inside the container.
-
command: execute the command usermod to add the openhab user to the dialout group
-
command: execute the command usermod to add my login ({{ }} denotes a variable, in this case {{ shared_user }} is my login and it’s defined in global_vars/all) to the openhab group
-
file: create a folder and set it’s permissions to allow group write access. {{ openhab_data }} is a variable defined in the ansible/build/roles/openhab/vars/main.yml.
-
git: checks out my OH config from git into the data directory.
-
file: change ownership of the checked out files to openhab:openhab
-
file: created required folders in case there was nothing in git to check out and this is a brand new install. The with_items is a deprecated way to run the same task on more than once with different values. There is a newer way to do it, see the Ansible docs or the warning that pops up when you run it for details.
-
influxdb_database: create the openhab database on InfluxDB
-
command: create the openhab user on InfluxDB
-
command: give the openhab user permissions on the openhab database on InfluxDB
-
command: create the grafan user on InfluxDB
-
command: give the grafana user read permissions on the openhab database on InfluxDB
-
docker_container: download the official openHAB Docker image and start a container running
One further note about roles. Roles should be idempotent. This means that you can run it repeatedly and if there is nothing to do, Ansible won’t do anything. Only if there is something to do does it run. This is where I’ve gone wrong in a few areas and why I have two different sets of Ansible roles and playbooks, one for building and another for updating. Eventually I’ll correct this problem.
Ansible Galaxy
One important thing to know is that you probably don’t even have to write most of your Ansible roles yourself. Ansible provides a service similar to dockerhub where users can check in and distribute their developed roles. Github is also another great place to look. There is at least one developed for openHAB (besides the one I posted above).
I’d be happy to answer any questions if there is anything not clear or provide further examples below.