Skip to main content

How I used Ansible to automate updates at home

Automation is not just for technology organizations anymore. You can use it anywhere, even at home.
Image
Laptop with sticky note, phone, notepad, and coffee

Image by DarkmoonArt_de from Pixabay

At home, I own some IT devices that run 24/7. To keep them up to date and install updates automatically, I leave the job to Ansible. In case you are completely new to Ansible, you'll find a good introduction in:

My home network includes the following devices:

  • Two Raspberry Pi running Raspbian OS
  • My KVM host running Debian 10 Buster
  • Two RHEL 8 hosts
  • A Synology DS213air
  • Four RHEL hosts on an isolated network
Image
Sample home network

The host marked by the red square is my Ansible Control Node. Its job is to update my Linux environment. I chose it because this host can reach all other hosts in the environment. Other hosts, for example, the Red Hat Enterprise Linux 7 (RHEL)-Ansible hosts, are only able to reach hosts inside the isolated network.

[ You might also like: How to create an Ansible Playbook ]

Prerequisites

On all my hosts, there is a user account that can use sudo to run commands with root privileges. For convenience, I created an SSH key-pair and distributed the SSH public key to the hosts I would like to update using Ansible. If you need some help generating the SSH keys, check out Using ssh-keygen and sharing for key-based authentication in Linux by Tyler Carrigan.

In order to use the host RHEL8-Squid as an Ansible Control Node, I'll have to enable a repo that provides Ansible and install it:

$ sudo subscription-manager repos --enable=ansible-2.9-for-rhel-8-x86_64-rpms
$ sudo dnf -y install ansible

For other distributions, please see the official documentation.

The Ansible default configuration file is found at /etc/ansible/ansible.cfg. Since this file is shipped and controlled by the RPM package, I like to create a custom config file at ~/.ansible.cfg by just copying the original one and editing it. I’ve only made a few changes:

$ egrep -v "^$|^#|^\[" ~/.ansible.cfg
inventory      = ~/ansible/hosts
private_key_file = /home/user/.ssh/ansible_id_rsa

As you can see, I've created an ansible directory in my HOME directory to store my host inventory file. I've also specified the path to the SSH private key Ansible should use to connect to the nodes on the network.

With this configuration, all the magic is controlled from my HOME directory, and I won't need any root privileges on my Ansible Control Node to get the following job done.

Create a static inventory file

In this use case, I use a static inventory file by putting my hosts with their FQDN into the ~/ansible/hosts file.

[special]
localhost
tower-pc.lan

[yum]
rhel7-ansible.private1
rhel7-t1.private1
rhel8-t1.private1
rpm-repo-r8.private1
podhost-r8-1.lan

[apt]
raspi-sht21.lan
pi-hole.lan

[ipkg]
diskstation.lan

As you can see, I grouped the hosts on my network by the package manager they use. This comes in handy when creating the playbook to update them. The group [special] contains my Ansible Control Node itself and my KVM hypervisor, where my Ansible Control Node runs.

For more information on Ansible's inventory, see: How to build your inventory.

Check the connectivity

Before I create the playbook that updates my hosts, I check if my Ansible Control Node RHEL8-Squid can connect to all of my hosts by using the following ad-hoc command:

$ ansible all -m ping -T 30
rhel7-t1.private1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
rhel7-ansible.private1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
rpm-repo-r8.private1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"
}
rhel8-t1.private1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"
}
podhost-r8-1.lan | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"
}
[WARNING]: Platform linux on host tower-pc.lan is using the discovered Python
interpreter at /usr/bin/python, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
tower-pc.lan | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
[WARNING]: sftp transfer mechanism failed on [diskstation.lan]. Use
ANSIBLE_DEBUG=1 to see detailed information
[WARNING]: Platform linux on host diskstation.lan is using the discovered
Python interpreter at /usr/bin/python, but future installation of another
Python interpreter could change this. See https://docs.ansible.com/ansible/2.9/
reference_appendices/interpreter_discovery.html for more information.
diskstation.lan | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
[WARNING]: Platform linux on host pi-hole.lan is using the discovered Python
interpreter at /usr/bin/python, but future installation of another Python
interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
ce_appendices/interpreter_discovery.html for more information.
pi-hole.lan | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
[WARNING]: Platform linux on host raspi-sht21.lan is using the discovered
Python interpreter at /usr/bin/python, but future installation of another
Python interpreter could change this. See https://docs.ansible.com/ansible/2.9/
reference_appendices/interpreter_discovery.html for more information.
raspi-sht21.lan | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}

The default timeout for an Ansible connection is 10 seconds. Because my Synology Diskstation is in standby mode most of the time, I'm using the option -T to specify a timeout of 30 seconds to give it some time to wake up.

The warnings don't bother me at this time, so I move on to create the playbook.

In case you would like to know more about the ad-hoc commands, read Introduction to ad-hoc commands in the official docs.

The playbook

My playbook contains three plays. Each play runs a task on hosts belonging to a certain group in my inventory except [special]. In this simple example, each play connects to a group of hosts, updates them, and reboots them afterward in case updates were installed.

To determine if updates were installed, I register variables that store the return values of the tasks where I registered them. I use these to check whether the status of a task is changed. If it is, the system will be rebooted. Here's the playbook:

---
- hosts: yum
  tasks:
  - name: Update all installed packages using YUM module
    yum:
      name: '*'
      state: latest
      update_cache: yes
      update_only: yes
    register: yum_update_status

  - name: Remove packates not needed anymore
    yum:
      autoremove: yes

  - name: Reboot when packages were updated
    reboot:
    when: yum_update_status.changed

- hosts: apt
  tasks:
  - name: Update all installed packages using APT module
    apt:
      name: '*'
      state: latest
      update_cache: yes
      only_upgrade: yes
    register: apt_update_status

  - name: Remove packages not needed anymore
    apt:
      autoremove: yes

  - name: Reboot when packages were updated
    reboot:
      post_reboot_delay: 60
    when: apt_update_status.changed

- hosts: ipkg
  tasks:
  - name: Update the Packages installed on Diskstation
    command: /opt/bin/ipkg update && /opt/bin/ipkg upgrade

As you may have noticed, the Diskstation wasn't rebooted. That's because only userspace tools will be updated, and there is no need for a reboot. If a new OS version is available for the Diskstation, I will update it manually because there is no Ansible module for it yet. It's a similar story with the hosts tower-pc.lan and rhel8-squid.lan. I left them out of this playbook on purpose. My Ansible Control Node and my KVM hypervisor are important enough to me that I'll update them manually.

Here's a look at the first playbook run:

$ ansible-playbook -T 30 -b --ask-become-pass pkg_update.yml
BECOME password:

PLAY [yum] **************************************************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************************************
ok: [podhost-r8-1.lan]
ok: [rhel7-t1.private1]
ok: [rhel8-t1.private1]
ok: [rpm-repo-r8.private1]
ok: [rhel7-ansible.private1]

TASK [Update all installed packages using YUM module] *******************************************************************************************
ok: [rhel8-t1.private1]
ok: [podhost-r8-1.lan]
ok: [rpm-repo-r8.private1]
ok: [rhel7-t1.private1]
ok: [rhel7-ansible.private1]

TASK [Remove packates not needed anymore] *******************************************************************************************************
ok: [rhel7-t1.private1]
ok: [rhel7-ansible.private1]
ok: [rhel8-t1.private1]
ok: [podhost-r8-1.lan]
ok: [rpm-repo-r8.private1]

TASK [Reboot when packages were updated] ********************************************************************************************************
skipping: [rhel7-ansible.private1]
skipping: [rhel7-t1.private1]
skipping: [rhel8-t1.private1]
skipping: [rpm-repo-r8.private1]
skipping: [podhost-r8-1.lan]

PLAY [apt] **************************************************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************************************
ok: [pi-hole.lan]
ok: [raspi-sht21.lan]

TASK [Update all installed packages using APT module] *******************************************************************************************
changed: [pi-hole.lan]
changed: [raspi-sht21.lan]

TASK [Remove packages not needed anymore] *******************************************************************************************************
ok: [pi-hole.lan]
ok: [raspi-sht21.lan]

TASK [Reboot when packages were updated] ********************************************************************************************************
changed: [pi-hole.lan]
changed: [raspi-sht21.lan]

PLAY [ipkg] *************************************************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************************************
ok: [diskstation.lan]

TASK [Update the Packages installed on Diskstation] *********************************************************************************************
changed: [diskstation.lan]

PLAY RECAP **************************************************************************************************************************************
diskstation.lan            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
pi-hole.lan                : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
podhost-r8-1.lan           : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
raspi-sht21.lan            : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
rhel7-ansible.private1     : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
rhel7-t1.private1          : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
rhel8-t1.private1          : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
rpm-repo-r8.private1       : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

As you can see, my RHEL machines were already up to date. Nothing to update or remove, and therefore no reboot required. My Raspberry Pis, on the other hand, had updates available and they were installed. Both devices were rebooted afterward. The Diskstation has its status changed, too. But be aware this is because I'm using the command module, which returns changed every time it runs, regardless of whether something on your node changed.

[ A free guide from Red Hat: 5 steps to automate your business. ] 

Wrap up

In this article, I showed you an easy but not very sophisticated example of how I keep my Linux devices at home up to date using Ansible automation. It shows you how to use the groups from your inventory in different plays of your playbook using some of the Ansible modules and a simple command.

Topics:   Linux   Linux administration   Ansible  
Author’s photo

Jörg Kastning

Jörg has been a Sysadmin for over ten years now. His fields of operation include Virtualization (VMware), Linux System Administration and Automation (RHEL), Firewalling (Forcepoint), and Loadbalancing (F5). More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.