Raspberry Pi provisioning with ansible by Grzegorz Godlewski on 2019-09-08

Most common storage for Rasperry Pi are SD Cards. Unfortunately they are not a very reliable memory. From time to time you need to replace one for another. To make this process easier you can create your own customized raspbian image.

Recently one of my rpis stop working properly. Reason unknown but dmesg showed few fs errors. Usually I would download raspbian image and install it from scratch. Unfortunately it is not very pleasant experience:

  • if you use wifi the default raspbian has no config and is not able to connect to it
  • raspbian has ssh turned off
  • you need to plug keyboard and hdmi to fix above issues
  • then you can install your software, config the machine

Most of my servers, cloud services, workstation I provision with ansible so the solution for the problem was obvious. I only needed to find a way to alter raspian image with my own configuration, add some provisioning script and record it to an SD.

Here is my sample script. Read comments for explanation:

playbook.yml

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/ansible-playbook -i 'localhost,'
---

- hosts: localhost
  connection: local

  tasks:
  - name: Download raspbian
    get_url:
      url: https://downloads.raspberrypi.org/raspbian_lite_latest
      dest: raspbian-lite.zip
      mode: '0440'

  - name: Extract raspbian-lite.zip
    unarchive:
      src: raspbian-lite.zip
      dest: .

  - name: Rename
    raw: mv *-raspbian-*-lite.img raspbian-lite.img

  - name: Unmount all mount dirs just in case
    raw: umount ./mount_boot ; umount ./mount_root
    failed_when: false
    become: true

# Modifying Boot partition
# Using method from https://learnaddict.com/2016/02/23/modifying-a-raspberry-pi-raspbian-image-on-linux/ to calculate partition offsets

  - name: Get boot offset
    raw: fdisk -l raspbian-lite.img | grep raspbian-lite.img1 | sed  's/\s\{1,\}/\t/g' | cut -f2
    register: "offset_boot"

  - name: Create mount_boot
    file: path=./mount_boot state=directory

  - name: Mount boot
    raw: mount -v -o offset={{offset_boot.stdout | int * 512}} -t vfat raspbian-lite.img ./mount_boot
    become: true

  - name: Copy boot config.txt
    copy:
      src: ./files/config.txt
      dest: ./mount_boot/config.txt
      owner: root
      group: root
      mode: '0644'
    become: true

  - name: Unmount boot
    raw: umount ./mount_boot
    failed_when: false
    become: true

# Modifying Root partition

  - name: Get root offset
    raw: fdisk -l raspbian-lite.img | grep raspbian-lite.img2 | sed  's/\s\{1,\}/\t/g' | cut -f2
    register: "offset_root"

  - name: Unmount root
    raw: umount ./mount_root
    failed_when: false
    become: true

  - name: Create mount_root
    file: path=./mount_boot state=directory

  - name: Mount root
    raw: mount -v -o offset={{offset_root.stdout | int * 512}} -t ext4 raspbian-lite.img ./mount_root
    become: true

# Provision script
  - name: Create flag file which indicates that rpi need provisioning
    file:
      path: ./mount_root/root/requires_provisioning
      state: touch
      owner: root
      group: root
      mode: '0644'
    become: true

  - name: Copy provisioning service
    copy:
      src: ./files/provision.service
      dest: ./mount_root/lib/systemd/system
      owner: root
      group: root
      mode: '0644'
    become: true

  - name: Create symbolic link
    file:
      src: "/lib/systemd/system/provision.service"
      dest: "./mount_root/etc/systemd/system/multi-user.target.wants/provision.service"
      state: link
      force: yes
    become: true

  - name: Copy provisioning script
    copy:
      src: ./files/provision.sh
      dest: ./mount_root/root/provision.sh
      owner: root
      group: root
      mode: '0744'
    become: true

# Config WiFi

  - name: Get WiFi SSID in Ubuntu
    raw: iwgetid -r
    register: "wifi_ssid"
    when: config_wifi == 'y'

  - name: Get WiFi password in Ubuntu
    raw: cat /etc/NetworkManager/system-connections/{{wifi_ssid.stdout | trim}} | grep ^psk= | sed s/^psk=//g
    register: "wifi_pass"
    become: true
    when: config_wifi == 'y'

  - name: Write WiFi credentials to Root partition
    template:
      src: wpa_supplicant.conf
      dest: ./mount_root/etc/wpa_supplicant/wpa_supplicant.conf
      owner: root
      group: root
      mode: '0600'
    become: true
    when: config_wifi == 'y'

  - name: Unmount root
    raw: umount ./mount_root
    failed_when: false
    become: true

  - name: Write to SD
    raw: dd if="raspbian-lite.img" of={{dest}} bs=4M oflag=dsync status=progress
    become: true

  vars:
    config_wifi: 'n'

  vars_prompt:
    - name: dest
      prompt: "Destination"
      default: "/dev/mmcblk0"
      private: no

files/provision.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/sh

set -e
set -u

if [ ! -f /root/requires_provisioning ]; then
    exit 0
fi

sudo systemctl enable ssh || {
    echo "Unable to enable ssh"
    exit 1
}

sudo systemctl start ssh || {
    echo "Unable to start ssh"
    exit 1
}

sudo apt update || {
    echo "Unable to apt update"
    exit 2
}

sudo apt upgrade -y || {
    echo "Unable to apt upgrade"
    exit 2
}

sudo apt install ansible -y || {
    echo "Unable to install ansible"
    exit 2
}

rm -f /root/requires_provisioning

templates/wpa_supplicant.conf

network={
    ssid="{{wifi_ssid.stdout | trim}}"
    psk="{{wifi_pass.stdout | trim}}"
}