Deploying a Raspberry Pi Chromium Kiosk using Ansible

Posted on 2019-02-05 in sysadmin

Years ago, at a previous job, i was asked to put up a monitoring display showing details from our service desk. I was given a TV and a Raspberry Pi, and told to go at it.

I hacked around a bit, eventually had something that sort of worked, noted down how i'd done it, and moved on. When a friend asked me for more details i passed on my notes, and after a couple of years of poking around between a few of us, we have a pretty good idea how to implement this. Tas has documented it in great detail on her blog here and here.

So, at work i've set up a new monitoring system for our servers and services.
And i've found a nice dashboard that hooks into it and gives a ten-foot view.
And i have a spare raspberry pi and projector. Hmm.

I decided this time though, i'd do it properly. I've faffed about running through those instructions too many times in the past few years, and i'm now pretty handy with ansible for automation. Seems logical to implement it using that right?

So to start with, we have a raspberry pi running raspbian lite. We don't need all the full everything installed on it, just the few bits we want to display a browser full screen. We need to specify the user we plan on running as, and the url to display. I do this in host_vars/kiosk.example.com

1
2
3
kiosk:
  url: http://example.com/monitoring-dashboard
  user: pi

Then we have a role set up to actually do the thing. This is the playbook itself, at roles/chromium-kiosk/tasks/main.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
- name: Install X etc
  apt:
    name: "{{item}}"
    state: present
    install_recommends: no
  with_items:
    - xserver-xorg
    - xinit
    - xserver-xorg-video-fbdev
    - lightdm
    - chromium-browser
    - unclutter

- name: Enable auto-login to x for user
  lineinfile:
    regexp: "{{item.r}}"
    line: "{{item.l}}"
    path: /etc/lightdm/lightdm.conf
  with_items:
    - {'r': "^#?autologin-user=", 'l':"autologin-user={{kiosk.user}}"}
    - {'r': "^#?autologin-user-timeout=", 'l':"autologin-user-timeout=0"}
  notify: restart lightdm

- name: Set up xsession for user
  template:
    src: xsession.j2
    dest: "/home/{{kiosk.user}}/.xsession"
  notify: restart lightdm

- name: Make sure user has passwordless sudo for running fbset command
  copy:
    content: "{{kiosk.user}} ALL=(ALL:ALL) NOPASSWD: /bin/fbset\n"
    dest: "/etc/sudoers.d/kiosk-user"
    owner: root
    group: root
    mode: 0444
  notify: restart lightdm

- stat:
    path: /boot/config.txt
  register: pi_config

- name: disable overscan if we're on a pi
  lineinfile:
    regexp: "{{item.r}}"
    line: "{{item.l}}"
    path: /etc/lightdm/lightdm.conf
  with_items:
    - {'r': "^#?disable_overscan=", 'l':"disable_overscan=1"}
  notify: restart lightdm
  when: pi_config.stat.exists

We then have a handler to restart lightdm when we change any settings. This will cause the kiosk display to restart. It sits at roles/chromium-kiosk/handlers/main.yml

1
2
3
4
- name: restart lightdm
  service:
    name: lightdm
    state: restarted

And finally, the template for the xsession, this has a few modifications, it will automatically detect your screen size, and checks that it can see a http 200 code from the specified URL before starting the browser. It lives at roles/chromium-kiosk/templates/xsession.j2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Set this to your URL - it must return a 200 OK when called, not a redirect.
export URL={{kiosk.url}}

# Dont want screensavers or screen blanking
xset s off &
xset -dpms &
xset s noblank &

# Hide the mouse cursor
unclutter -idle 10 -noevents &

# Sit and wait until you can hit the URL you'll be showing in the kiosk
while ! curl -s -o /dev/null -w "%{http_code}" ${URL} | grep -q "200"; do
  sleep 1
done

# get screen resolution
WIDTH=`sudo fbset -s | grep "geometry" | cut -d " " -f6`
HEIGHT=`sudo fbset -s | grep "geometry" | cut -d " " -f7`

# Open chrome in incognito mode + kiosk mode
/usr/bin/chromium-browser --window-size=${WIDTH},${HEIGHT} --window-position=0,0 --hide-scrollbars --incognito --kiosk ${URL}

Automatically starting a Python script at boot on Raspbian Jessie

Posted on 2016-07-06 in sysadmin

As part of the Hackspace Manchester door control system, we have a raspberry pi running a little script that checks scanned cards against a database of members and opens the door if the card is known.  This has been humming along happily for around 3 years now, until recently it stopped updating card IDs when they were changed via the webui.

This led to a bit of a bug hunt, concluding with the fact the version of openssl on raspbian wheezy was waaaaay out of date, and we'd recently updated our members system to disable insecure cyphers on the HTTPS protocol.  We fixed it by upgrading to jessie, which as a side effect completely killed the auto-start of the door opening programme. Yay?

So.  Jessie.  Systemd.  Init system is a a bit different from sysvinit, but on the whole i find it a lot more sensible.  We want to run a script as the user 'alfred' (the door entry service user).  We also want to wait until the system is booted, and the serial port is available.

My script is called alfred, so i create the following file in /lib/systemd/system/alfred.service

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[Unit]
Description=alfred
After=dev-ttyAMA0.device multi-user.target

[Service]
Type=idle
ExecStart=/home/alfred/FRED/fred/fred.py
WorkingDirectory=/home/alfred/FRED/fred/
User=alfred

[Install]
WantedBy=multi-user.target

The After= says what services need to be up before this is run. In this case, it wants ttyAMA0 to be available, along with multi-user (this is the point where you could normally log in)

ExecStart= specifies the script I will be running, WorkingDirectory= is the directory to run the thing from (as i use relative paths in my python script, i need to set this), and User= says what unprivileged user to run the script as, since you don't want to be running random things as root if at all possible!

WantedBy= says that this should be started at the same time as multi-user.target, so at the end of the boot process, at the point you could normally log in.

We can then set up our new service to start on boot, and run it for the first time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ systemctl enable alfred
$ systemctl start alfred
No errors, so lets check the status:
$ sudo systemctl status alfred
● alfred.service - alfred
Loaded: loaded (/lib/systemd/system/alfred.service; enabled)
Active: active (running) since Wed 2016-07-06 16:42:59 BST; 30min ago
Main PID: 709 (fred.py)
CGroup: /system.slice/alfred.service
└─709 /usr/bin/python /home/alfred/FRED/fred/fred.py
Jul 06 16:42:59 alvin systemd[1]: Starting alfred...
Jul 06 16:42:59 alvin systemd[1]: Started alfred.
Jul 06 16:43:01 alvin fred.py[709]: 2016-07-06 16:43:01,577 FRED 0.7

Looks good, service is started, and we're getting some log output from it. Reboot to check everything comes up correctly and you're done!

... Though i wasnt. One gotcha I ran into is that because of the way raspbian's networking is set up, you can't get systemd to wait until after you have a network connection configured before starting the script (it will wait until the networking service is started, but not wait for the network to actually be up). This can be worked around pretty easily by setting the "Wait for network on boot" option in raspi-config, which will pause the whole boot process until it gets a DHCP lease.


Setting up a self-healing SSH tunnel for Raspberry Pi using Debian.

Posted on 2015-04-30 in sysadmin

I use quite a few raspberry pi's in locations that dont have the ability for me do incoming SSH to update / reboot / maintain them.  This is how i set up a reverse SSH tunnel to them, allowing me to access them from anywhere with internet access!

If the site has a proxy, first you'll need to install a proxy puncher to allow you to bypass it.

1
$ sudo http_proxy="http://proxy.ip.address:port" apt-get install corkscrew

Then install screen. Screen allows you to start a process running 'detached' from your current shell, so you dont have to be logged in to keep the tunnel up.

1
$ sudo apt-get install screen

Create a user for your ssh tunnel. You could just use the default pi user, but i prefer to use a dedicated tunnel user.

1
$ sudo adduser --system tunnel

Create tunnel config

1
2
$ sudo -u tunnel mkdir /home/tunnel/.ssh
$ sudo -u tunnel vim /home/tunnel/.ssh/config

Paste the following into the file. This forwards port 22 (ssh) and 80 (http) on the raspberry pi to ports 8022 and 8080 on your VPS.

1
2
3
4
5
6
7
8
9
ProxyCommand corkscrew 127.0.0.1 3128 %h %p
Host tunnel
  Hostname your.vps.hostname
  Remoteforward 8022 localhost:22
  Remoteforward 8080 localhost:80
  Port 443
  User tunnel
  ServerAliveInterval 10
  ServerAliveCountMax 3

NOTE: If you are not using a proxy, remove the corkscrew line and the port 443 line. The port 443 line above is only needed if you are behind a proxy and firewall that disallows port 22 outgoing. I have set up my SSH daemon on the VPS to listen to port 443 (the https port) as well as the normal 22 as this will manage to punch its way through most proxies.

The ServerAliveInterval and ServerAliveMax variables above basically say "send a packet across the tunnel every 10 seconds. If you don't get anything back after 3 tries, close the tunnel"

The above config also assumes you have set up a tunnel user on the machine you are SSHing to. If not, either create a tunnel user the same way we did above, or change the user line in the config to the username you will be using on the VPS side.

Generate your RSA key and upload to the VPS

1
2
3
4
$ sudo -su tunnel
$ export HOME=/home/tunnel
$ ssh-keygen
$ ssh-copy-id tunnel

At this point, you should be able to SSH to tunnel without typing in any passwords etc.

1
2
$ ssh tunnel
Linux thinkl33t 3.2.0-4-686-pae \#1 SMP Debian 3.2.65-1+deb7u2 i686

Set up our shell scripts to automatically start the tunnel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ sudo -su tunnel
$ mkdir /home/tunnel/bin
$ echo 'PATH="\$HOME/bin:\$PATH"' | tee /home/tunnel/.bashrc
$ echo '\#!/bin/bash' | tee /home/tunnel/bin/monitor\_tunnel.sh
$ echo 'APPCHK=\$(screen -ls | grep -c tunnel)' | tee -a /home/tunnel/bin/monitor\_tunnel.sh
$ echo 'if \[ \$APPCHK = "1" \]; then' | tee -a /home/tunnel/bin/monitor\_tunnel.sh
$ echo ' echo "Starting SSH tunnel"' | tee -a /home/tunnel/bin/monitor\_tunnel.sh
$ echo ' screen -mdS tunnel ssh -N tunnel' | tee -a /home/tunnel/bin/monitor\_tunnel.sh
$ echo 'fi' | tee -a /home/tunnel/bin/monitor\_tunnel.sh
$ echo 'exit' | tee -a /home/tunnel/bin/monitor\_tunnel.sh
$ chmod a+x /home/tunnel/bin/monitor\_tunnel.sh

The above script checks for an already open screen session with the name 'tunnel'. If it doesn't exist, it creates it. If it does exist it just ends. The screen session is launched in a detached state (in the background), and will automatically end when the SSH tunnel falls over.

Set up the crontab to automatically run our monitoring script once a minute

1
$ echo '\*/1 \* \* \* \* /home/tunnel/bin/monitor\_tunnel.sh' | crontab

Your tunnel should come up shortly, woo!

To test it out, from your VPS, type

1
2
3
$ ssh localhost -p 8022
parag0n@localhost's password:
Linux thinkl33t 3.2.0-4-686-pae \#1 SMP Debian 3.2.65-1+deb7u2 i686

Brill, you can now SSH in from your VPS... But by default SSH tunnel ports are only available from localhost, so you'd have to log into your VPS every time you wanted to get into the pi. So, lets edit the SSH config file on your local machine!

1
2
3
4
Host pi
  Hostname localhost
  Port 8022
  ProxyCommand ssh your.vps.hostname nc %h %p

This will allow you to type in ssh pi on your local machine, and will automatically ssh into your VPS, then SSH into localhost. Sorted.