Infrastructure@Home: Infrastructure Management

  • IM1: A task to update the operating systems
  • IM2: A generic task to install additional software
  • IM3: A task to install and update SSH keys

Prerequisites and new Ansible Futures

  • raspi-3-* and raspi-4 -*use the news Debian release Buster
  • linux-workstation uses an Arch Linux Distribution called Manjaro
- name: Update packages on all nodes
hosts:
- raspis
- server
serial: 1
become: true
tasks:
- block:
- name: Update packages on Debian
apt:
update_cache: true
upgrade: yes
register: result
- name: Print results
debug:
var=result.stdout_lines
when: ansible_facts[‘distribution’] == ‘Debian’
- block:
- name: Update packages on Archlinux
pacman:
update_cache: yes
upgrade: yes
register: result
- name: Print results
debug:
var=result.stdout_lines
when: ansible_facts[‘distribution’] == ‘Archlinux’
  • With serial = 1 we limit the execution to one host at a time. This is a safety measure: If the update fails on one host, we should stop to investigate the issue before continuing with another host
  • The option become = true means that the remote user issues sudo before attempting package updates - without this switch, the task would fail.
  • With block and when we define a conditional set of tasks: Only when the condition is true, then the tasks will be run.
  • The first block updates all nodes for Debian with the apt module.
  • The second block updates all nodes with the distribution Arch Linux by using the pacman module
  • After each step, we print the results to the console.
$: ansible-playbook -I hosts system/update_pacakges —limit "raspi-4-1"PLAY [Update packages on all nodes] **********************************************************************************TASK [Gathering Facts] ********************************************************************************
ok: [raspi-4-1]
TASK [Update packages] ********************************************************************
ok: [raspi-4-1]
TASK [Print results] ******************************************************************************************
ok: [raspi-4-1] => \{
"result.stdout_lines": [
"Reading package lists...",
"Building dependency tree...",
"Reading state information...",
"Calculating upgrade...",
"The following packages will be upgraded:",
" libpam-systemd libsystemd0 libudev1 systemd systemd-sysv udev",
"6 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.",

Install Packages

- name: Install software on Debian
hosts:
- raspis
become: true
vars:
package: bash
tasks:
- name: Install package
apt:
update_cache: true
name: "\{\{ package \}\}"
state: present
register: result
- name: Print results
debug:
var=result
ansible-playbook install_package.yml -e package=ntp —limit "raspi-4-2"PLAY [Install software on raspi] **********************************************************************TASK [Gathering Facts] ********************************************************************************
ok: [raspi-4-1]
TASK [Install package] ********************************************************************************
changed: [raspi-4-1]
TASK [Print results] ******************************************************************************************
ok: [raspi-4-1] => \{
"stdout_lines": [
"Reading package lists...",
"Building dependency tree...",
"Reading state information...",
"The following additional packages will be installed:",
" libevent-core-2.1-6 libevent-pthreads-2.1-6 libopts25 sntp",
"Suggested packages:",
" ntp-doc",
"The following NEW packages will be installed:",
" libevent-core-2.1-6 libevent-pthreads-2.1-6 libopts25 ntp sntp",
"0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded.",
"Need to get 1081 kB of archives.",
- name: Install software on Arch Linux
hosts:
- server
become: true
vars:
package: bash
tasks:
- name: Install package
pacman:
update_cache: yes
name: "\{\{ package \}\}"
state: present
register: result
- name: Print results
debug:
var=result

Rotate SSH Keys

- name: Add new public key
hosts: raspi-4-1
serial: 1
become: true
vars:
keyname: new_id_rsa.pub
new_key_file: "\{\{ lookup('env', 'HOME') + '/.ssh/' + lookup('vars', 'keyname') \}\}"
new_key: "\{\{ lookup('file', lookup('vars', 'new_key_file'), errors='ignore') \}\}"
old_key: "\{\{ lookup('file', lookup('env', 'HOME') + '/.ssh/id_rsa.pub') \}\}"
local_user: "\{\{ lookup('env', 'USER') \}\}"
tasks:
- block:
- local_action:
stat path="\{\{ new_key_file \}\}"
become_user: "\{\{ local_user \}\}"
register: file
- name: Check that the new key file exists and is not empty
assert:
that: file.stat.isreg and file.stat.isreg
- authorized_key:
user: "\{\{ ansible_ssh_user \}\}"
state: present
key: "\{\{new_key\}\}"
exclusive: true
register: result
- debug:
var=result
- name: pause for 10 seconds, then reconnect
wait_for:
delay: 10
- name: Connect with new key
ping:
- name: Delete old key
authorized_key:
user: "\{\{ ansible_ssh_user \}\}"
state: present
key: "\{\{ new_key \}\}"
exclusive: true
rescue:
- name: Error occured, restoring old key
authorized_key:
user: "\{\{ ansible_ssh_user \}\}"
state: present
exclusive: true
key: "\{\{ old_key \}\}"
  • The var keyname is defined - it can be passed as an argument. Then, with env lookup we construct the path to this file, and with the file lookup, we read its content
  • With stat we are accessing the filesystem to read the key file. Two assert: then: statement check that the file exists and its content is not empty
  • We then copy the new key with authorized_keys and all other keys with the exclusive: true statement.
  • The tasks are stopped for 10 seconds, then we reconnect with the new key to execute a ping. If the ping fails, the rescue statement applies: We copy the old key again and remove the new key.
PLAY [Add new public key] *************************************************************TASK [Gathering Facts] ****************************************************************
ok: [raspi-4-1]
TASK [debug] **************************************************************************
ok: [raspi-4-1] => \{
"new_key_file": "/Users/sguenther/.ssh/new_id_rsa.pub"
\}
TASK [stat] ***************************************************************************
ok: [raspi-4-1 -> localhost]
TASK [Check that the new key file exists and is not empty] ****************************
ok: [raspi-4-1] => \{
"changed": false
\}
MSG:All assertions passed
TASK [authorized_key] *****************************************************************
ok: [raspi-4-1]
TASK [debug] **************************************************************************
ok: [raspi-4-1] => \{
"result": \{
"changed": false,
"comment": null,
"exclusive": false,
"failed": false,
"follow": false,
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAD9QDABTzcYz3+c0SZQBHfXjDMaE/sRBB0L1zaBGEss1xu...

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store