Ansible musicplayers project

Ter ‘lering ende vermaeck‘ hieronder een uiteenzetting van mijn Ansible-project voor de Musicplayers die ik gebruik. De MusicPlayers zijn gebaseerd op Raspberry PI’s en werken onder Debian Linux. De software die ik gebruik is in eerste instantie een kant-en-klare image van Volumio, een distri speciaal voor muziek-doeleinden gemaakt. Standaard ziet dat er zo uit (m.b.v. een web-browser):

Nu wil ik dat wijzigen in andere kleuren en mijn eigen logo. Ook een andere motd en de mogelijkheid om berichten via de mail te kunnen versturen. Het lijstje ziet er alsvolgt uit:

common:

  • NTP service installeren
  • Juiste tijdzone instellen voor NL
  • SSH login beperken tot authenticated key-pair
  • Crontab jobs kunnen maken
  • ssmtp/mailutils installeren
  • ssmtp configureren voor mijn mail-provider
  • IP adres laten mail na opstarten
  • player instellen als Roon-device
  • player instellen als SqueezeBox (LMS) device

volumio:

  • kleuren van de stylesheet aanpassen
  • logo wijzigen in Digital Hifi logo
  • NL webradio stations toevoegen
  • wachtwoord instellen op AirPlay

Stap 1: Inventory

De eerste stap is het aanmaken van de inventory. In een directory genaamd /apps/ansible/musicplayers/ ziet het bestand ‘inventory‘ er zo uit:

[musicplayers]
huiskamer  ansible_host=192.168.1.31 ansible_user=volumio
demoruimte ansible_host=192.168.1.21 ansible_user=volumio
badkamer   ansible_host=192.168.1.41 ansible_user=volumio
tuin       ansible_host=192.168.1.51 ansible_user=volumio

Stap 2: ansible.cfg

Dit bestand komt in dezelfde directory en bevat verwijzingen naar o.a. de inventory file. Ook wil ik voorkomen dat er retry-bestanden aangemaakt worden als een play op een bepaalde host niet lukt en het resultaat hoeft niet in saycows balloons te worden weergegeven. Het bestand ansible.cfg ziet er zo uit:

[defaults]
nocows = 1
inventory = ./inventory
retry_files_enabled = false

Stap 3: aanmaken van de variabelen

De Ansible-scripts gebruiken variabelen en die plaatsen we in een directory ‘defaults‘. Hierin komt dan een bestand genaamd ‘main.yml‘ met daarin de variabelen. Aangezien we twee roles gebruiken, maak ik voor elke role een aparte default-vars file.

roles/common/defaults/main.yml:

---
# Variables listed here are applicable to common
ntpserver0: 0.nl.pool.ntp.org
ntpserver1: 1.nl.pool.ntp.org
ntpserver2: 2.nl.pool.ntp.org
ntpserver3: 3.nl.pool.ntp.org

ssmtp_mail_to: "wiedanook@hotmail.com"
ssmtp_mail_from: "iemand.bij@ziggo.nl"

squeezelite_server_address: 192.168.1.35
airplay_password: "wijzig_dit_wachtwoord"

roles/volumio/defaults/main.yml:

---
# Variables listed here are applicable to volumio

stylesheet_digitalhifi_color: "#136F9A"
stylesheet_volumio_color1: "#54c698"
stylesheet_volumio_color2: "#3c763d"
stylesheet_volumio_color3: "#54c688"

JazzFM_uri: "http://tx.sharp-stream.com/icecast.php?i=jazzfmmobile.mp3"
RadioM_uri: "http://icecast.omroep.nl/rtvutrecht-radio-m-bb-mp3"
Radio10_uri: "http://playerservices.streamtheworld.com/api/livestream-redirect/RADIO10.mp3"
Radio3_uri: "http://icecast.omroep.nl/3fm-sb-mp3.m3u"
Radio2_uri: "http://icecast.omroep.nl/radio2-bb-mp3.m3u"
Radio4_uri: "http://icecast.omroep.nl/radio4-sb-mp3.m3u"
Radio6_uri: "http://icecast.omroep.nl/radio6-sb-mp3.m3u"
Arrow_uri: "http://www.arrow.nl/streams/Rock128kmp3.pls"
BNR_uri: "http://icecast-bnr.cdp.triple-it.nl/bnr_mp3_96_06.m3u"
Classic_uri: "http://provisioning.streamtheworld.com/pls/classicfm.pls"
Concert_uri: "http://streams.greenhost.nl:8080/live.m3u"
QMusic_uri: "http://icecast-qmusic.cdp.triple-it.nl/Qmusic_nl_live_96.mp3.m3u"
Radio10GP_uri: "http://playerservices.streamtheworld.com/api/livestream-redirect/TLPSTR12.mp3"
Radio1080_uri: "http://playerservices.streamtheworld.com/api/livestream-redirect/TLPSTR20.mp3"
Radio1060_uri: "http://playerservices.streamtheworld.com/api/livestream-redirect/TLPSTR18.mp3"
Radio10DC_uri: "http://playerservices.streamtheworld.com/api/livestream-redirect/TLPSTR23.mp3"
Radio10LS_uri: "http://playerservices.streamtheworld.com/api/livestream-redirect/TLPSTR04.mp3"
Radio538_uri: "http://vip-icecast.538.lw.triple-it.nl/RADIO538_MP3.m3u"
Veronica_uri: "http://provisioning.streamtheworld.com/pls/VERONICA.pls"
VeronicaT1000_uri: "http://provisioning.streamtheworld.com/pls/TOP1000.pls"
SkyRadio_uri: "http://provisioning.streamtheworld.com/pls/skyradio.pls"
SlamFM_uri: "http://vip-icecast.538.lw.triple-it.nl/SLAMFM_MP3.m3u"
Sublime_uri: "http://stream.sublimefm.nl/SublimeFM_mp3.m3u"
Bingo_uri: "http://icecast.omroep.nl/rtvutrecht-bingo-fm-bb-mp3"
Radio2T2000_uri: "http://icecast.omroep.nl/radio2-top2000-mp3.m3u"

Verderop in dit artikel, bij de templates, zal de aanroep van de variabelen duidelijk worden. In ieder geval is het handig om één plek te hebben waar je dit bij kunt houden voor alle tasks in de role! In feite is ‘defaults’ de laagste rangorde en de variabelen kunnen later nog overschreven worden in bv. de playbooks zelf of op de CLI.

Stap 4: musicplayers.yml

Nog steeds in de directory /apps/ansible/musicplayers wordt een yaml-bestand gemaakt dat het algemene script is. Dit zal, gezien de takenlijst, twee ‘roles‘ aanroepen, common en volumio.

---
- hosts: musicplayers
  roles:
    - { role: common, tags: 'common' }
    - { role: volumio, tags: 'volumio' }

De hosts-group ‘musicplayers’ (uit de inventory) wordt gebruikt om te bepalen welke nodes er ingericht gaan worden.

Stap 5: aanmaken roles

De twee roles maken we aan door de volgende directory structuur aan te maken in /apps/ansible/musicplayers/:

.
└── roles
    ├── common
    │   ├── defaults
    │   ├── files
    │   ├── handlers
    │   ├── tasks
    │   └── templates
    └── volumio
        ├── defaults
        ├── files
        ├── tasks
        └── templates

common

Stap 6: common templates

In de common-role worden templates gebruikt voor o.a. motd en mail-berichten. Templates maken gebruik van variabelen uit de ansible-facts of uit de door ons gedefinieerde variabelen (in defaults). Ansible gebruikt hiervoor Jinja2 en de extensie van templates is daarom .j2. Bijvoorbeeld de motd-template die na inloggen met ssh laat zien wat de FQDN is van de node, motd.j2:

        ############## {{ ansible_fqdn }} ###############
        __  ___           _      ____  __
       /  |/  /_  _______(_)____/ __ \/ /___ ___  _____  _____
      / /|_/ / / / / ___/ / ___/ /_/ / / __ `/ / / / _ \/ ___/
     / /  / / /_/ (__  ) / /__/ ____/ / /_/ / /_/ /  __/ /
    /_/  /_/\__,_/____/_/\___/_/   /_/\__,_/\__, /\___/_/
                                           /____/

Voor de ntp-service plaatsen we de NTP-servers in de config-file aan de hand van de variabelen uit defaults/main.yml. Deze template noemen we ntp.conf.j2:

driftfile /var/lib/ntp/drift

restrict 127.0.0.1
restrict -6 ::1

server {{ ntpserver0 }}
server {{ ntpserver1 }}
server {{ ntpserver2 }}
server {{ ntpserver3 }}

includefile /etc/ntp/crypto/pw

keys /etc/ntp/keys

Zodra er een interactieve login via ssh plaats vindt op één van de Musicplayers, wordt een E-mail bericht verstuurd. De template voor dit bericht is ialogin.txt.j2:

To:{{ ssmtp_mail_to }}
From:{{ ssmtp_mail_from }}
Subject: Interactive Login detected!

*********************************************
* Interactive login detected at MusicPlayer *
*********************************************

Logged on hostname: {{ ansible_fqdn }}

Stap 7: aanmaken van de handlers

Handlers worden gebruikt om bv. services te herstarten nadat een conf-file is aangepast. Zo ook voor de cron- en ntp-services. In de ‘handlers‘ directory komt een bestand genaamd main.yml:

---
# Handler to handle common notifications. Handlers are called by other plays.
# See http://docs.ansible.com/playbooks_intro.html for more information about handlers.

- name: restart ntp
  service: name=ntp state=restarted
- name: restart cron
  service: name=cron state=restarted

De handlers kunnen in playbooks aangeroepen worden via de (exacte) name van de handler.

Stap 8: common files

Voor het installeren van SqueezeLite is een bestand nodig en dat zetten we klaar in de files directory:

  1. squeezelite-armv6hf

Stap 9: common playbooks

Als eerste wordt een bestand genaamd main.yml aangemaakt waarin de basis wordt gedefinieerd. De playbooks staan in de directory tasks. Het eerste gedeelte van main.yml zorgt voor de aanroep van andere playbooks:

---
- name: Install and start the NTP services
  include_tasks: ntp.yml
  tags: ntp
- name: Prevent ssh login without key-pair
  include_tasks: nopwlogon.yml
  tags: ssh
- name: Install and start the cron service
  include_tasks: cron.yml
  tags: cron
- name: Add additional services
  include_tasks: additional_services.yml

In dezelfde directory (tasks) staan deze playbooks, bijvoorbeeld ntp.yml:

---
- name: Install ntp
  apt: name=ntp state=present
  tags: ntp

- name: set timezone to Europe/Amsterdam
  command: timedatectl set-timezone Europe/Amsterdam
  tags: ntp

- name: Configure ntp file
  template: src=ntp.conf.j2 dest=/etc/ntp.conf
  tags: ntp
  notify: restart ntp

- name: Start the ntp service
  service: name=ntp state=started enabled=yes
  tags: ntp

Dit zorgt ervoor dat de ntp-service aanwezig is en gestart, ook na een eventuele reboot van het systeem. Nadat de template is aangepast wordt de handler ge-notified en zal de ntp-service herstarten. We geven deze taken de tags: ntp

Een ander playbook zorgt ervoor dat inloggen via ssh beperkt is tot gebruik van een key-pair. Deze playbook heb ik nopwlogon.yml genoemd:

---
- name: Restrict ssh logon to authorized keys
  lineinfile: path='/etc/ssh/sshd_config'
              insertafter='^UsePAM yes'
              line='PasswordAuthentication no'
              state=present

Het bestand /etc/ssh/ssh_config wordt aangepast zodat plain-tekst-wachtwoorden niet gebruikt kunnen worden om in te loggen via ssh.

De overige playbooks installeren dan nog cron, vim, mailutils, ssmtp en bzip2. Als laatste stappen in de common-role installeren we de Roon en SqueezeLite clients:

- name: Install musicplayer as Roon device 
  include_tasks: roon.yml 
  tags: roon 
- name: Install musicplayer as Squeezebox device 
  include_tasks: squeezelite.yml 
  tags: squeezelite

Hiervoor dient dus het playbook roon.yml:

---
- name: Check if Roon is already installed
  stat:
    path: /opt/RoonBridge/VERSION
  register: Roon
- block:
  - name: get Roon software for ARMv7
    get_url:
      url: http://download.roonlabs.com/builds/roonbridge-installer-linuxarmv7hf.sh
      dest: /home/volumio/
  - name: set execution flag
    file:
      path: /home/volumio/roonbridge-installer-linuxarmv7hf.sh
      mode: 0755
  - name: install roon software on ARMv7
    become: yes
    shell: 'echo Y | /home/volumio/roonbridge-installer-linuxarmv7hf.sh'
  tags: roon
  when: ansible_architecture == "armv7l" and Roon.stat.exists == False

- block:
  - name: get Roon software for ARMv8
    get_url:
      url: http://download.roonlabs.com/builds/roonbridge-installer-linuxarmv8hf.sh
      dest: /home/volumio/
  - name: set execution flag
    file:
      path: /home/volumio/roonbridge-installer-linuxarmv8hf.sh
      mode: 0755
  - name: install roon software on ARMv8
    become: yes
    shell: 'echo Y | /home/volumio/roonbridge-installer-linuxarmv8hf.sh'
  tags: roon
  when: ansible_architecture == "armv8l" and Roon.stat.exists == False

De te installeren package is alleen nodig als die er nog niet is, daarvoor registreren we de variabele ‘Roon’. De package is afhankelijk van de processor van de Raspberry PI en aangezien de RPi 2 een ARM7 en de RPi 3 een ARM8 heeft, gaan we die opvragen via de ansible fact ‘ansible_architecture‘. Zodoende wordt de juiste package opgehaald en geïnstalleerd. De tags: en de when: clausule hoeven we slechts éénmaal te plaatsen omdat de verschillende tasks in een block: gezet zijn.

Tenslotte installeren we de squeezelite service als deze nog niet aanwezig is. Hiervoor hadden we dus squeezelite.yml ge-include in main.yml:

---
- name: Check if Squeezelite is already installed
  stat:
    path: /usr/bin/squeezelite-armv6hf
  register: squeeze

- block:
  - name: Get the squeezelite file from template
    become: yes
    template:
      src: squeezelite.j2
      dest: /etc/init.d/squeezelite
      owner: volumio
      group: volumio
      mode: 0755
  - name: Get the squeezelite-armv6hf command file
    become: yes
    file:
      src: squeezelite-armv6hf
      dest: /usr/bin/squeezelite-armv6hf
      owner: volumio
      group: volumio
      mode: 0755
  - name: Start and Enable the squeezelite service
    systemd:
      name: squeezelite
      enabled: True
      state: started
  tags: squeezelite
  when: squeeze.stat.exists == False

Eerst wordt gecontroleerd of de service al aanwezig is. Mocht dat een ‘False‘ opleveren dan gebruiken we een block: om deze te installeren. Hierbij wordt de executable gekopieerd vanuit de ‘files‘ directory.

Volumio

Stap 10: volumio templates

Voor web-radiostations wordt een bestand met hierin de Uri’s van de stations aangegeven. Die Uri’s halen we uit de default-variabelen en daarom is er een template voor roles/volumio/templates/my-web-radio,js2. Deze ziet er dan zo uit:

[
 {
   "service": "webradio",
   "name": "JazzFM UK",
   "uri": "{{ JazzFM_uri }}"
 },
 {
   "service": "webradio",
   "name": "Radio M",
   "uri": "{{ RadioM_uri }}"
 },
 {
   "service": "webradio",
   "name": "Radio 10",
   "uri": "{{ Radio10_uri }}"
 }
]

Je ziet bij de Uri’s dat er variabelen gebruikt worden.

Voor de squeezelite client is er een configuratie bestand waarin de Logitech Media Server wordt aangegeven. Deze staat ook een een default variabele en we gebruiken een template squeezelite.j2 waarin o.a. dit staat:

SB_SERVER_IP="{{ squeezelite_server_address }}"

Stap 11: volumio files

In de ‘files‘ directory worden bestanden geplaatst die één-op-één gekopieerd kunnen worden naar de nodes. In dit geval is dat:

  1. volumio-logo.png

Beide bestanden worden met playbooks gekopieerd naar de nodes.

Stap 12: volumio tasks

De playbooks staan in tasks en dit is het eerste gedeelte van de main.yml playbook:

---
- name: Replace Volumio logo in assets/variants/volumio/
  stat: path=/volumio/http/www/app/themes/volumio/assets/variants/volumio/graphics/volumio-logo.png
  register: variants_path
  tags: logo
- file:
    src: volumio-logo.png
    dest: /volumio/http/www/app/themes/volumio/assets/variants/volumio/graphics/volumio-logo.png
    owner: volumio
    group: volumio
    mode: 0644
  when: variants_path.stat.exists == True
  tags: logo

- name: Replace volumio logo in assets
  stat: path=/volumio/http/www/app/themes/volumio/assets/graphics/volumio-logo.png
  register: assets_path
  tags: logo
- file:
    src: volumio-logo.png
    dest: /volumio/http/www/app/themes/volumio/assets/graphics/volumio-logo.png
    owner: volumio
    group: volumio
    mode: 0644
  when: assets_path.stat.exists == True
  tags: logo

Nadat gecontroleerd wordt (m.b.v. stat:) of een bepaalde directory bestaat (variants) wordt een nieuw logo op de node geplaatst in dit geval dan in twee verschillende directories. De beide taken krijgen de tags: logo. Hierna zal main.yml de overige playbooks includen, elk met hun eigen tags:

- name: Changing the values in the stylesheet(s)
  include_tasks: stylesheet.yml
  tags: css

- name: Add web-radio station to favourites
  include_tasks: webradio.yml
  tags: radio

- name: Put a password on Airplay
  include_tasks: airplayww.yml
  tags: airplay

Bijvoorbeeld om bepaalde waaarde uit een stylesheet-bestand (.css) te vervangen door een andere waarde. Deze waardes komen uit default-variabelen. stylesheet.yml:

---
- name: define the stylesheet
  find:
    paths: "/volumio/http/www/styles/"
    patterns: "*.css"
  register: files
  #debug: var=files
  tags: css

- name: replace color1 in stylesheet
  replace: dest={{item.path}} replace="{{stylesheet_digitalhifi_color}}" regexp="{{stylesheet_volumio_color1}}"
  with_items: "{{ files.files }}"
  tags: css

- name: replace color2 in stylesheets
  replace: dest={{item.path}} replace="{{stylesheet_digitalhifi_color}}" regexp="{{stylesheet_volumio_color2}}"
  with_items: "{{ files.files }}"
  tags: css

Hiermee wordt de standaard groene kleur van Volumio vervangen door de blauwe tint van Digital Hifi. Omdat de naam van het css-bestand wijzigt per versie van Volumio, zoeken we eerst deze naam op en registreren die in de var: files waarna we deze als item kunnen gebruiken in de replace: module. We geven de tags: css mee.

Om te voorkomen dat iedereen met een i-device via Airplay zijn of haar muziek naar de Musicplayer stuurt, zetten we een wachtwoord op de Airplay-service met airplayww.yml:

---
- name: Put a password on AirPlay
  lineinfile: path='/volumio/app/plugins/music_service/airplay_emulation/shairport-sync.conf.tmpl'
              regexp='password'
              insertafter='log_verbosity = 0;'
              line='    password = "{{ airplay_password }}"'
              state=present
  tags: airplay

Het wachtwoord komt uit de defaults-variabelen maar kan uiteraard overschreven worden, bv. op de CLI met de ‘-e’ optie.

Hiermee zijn de scripts gereed en kunnen deze getest worden. In de hoofd-directory /apps/ansible/musicplayers/ gebruiken we het volgende commando’s:

ansible-playbook musicplayers.yml --syntax-check

Als de script in orde zijn kunnen we ze echt uitvoeren. Voor een aantal taken is het noodzakelijk om deze met ‘sudo’ uit te voeren en daarvoor is het commando ‘become: yes’ opgenomen. Hiermee wordt wel een sudo-wachtwoord gewenst en die geven we op met de optie ‘-K’. We kunnen tevens het aantal nodes beperken tot bv. één test-node om zodoende eerst alles te testen op juiste werking. Hiervoor is de ‘-l’ optie (limit)

ansible-playbook musicplayers.yml  -l huiskamer -K

Het gebruik van de tags: stelt ons in staat om de playbooks te runnen voor specifieke taken, bijvoorbeeld om het logo te kopieren op de node demoruimte:

ansible-playbook musicplayers.yml  -l demoruimte -K --tags=logo

Als alle taken succesvol uitgevoerd zijn zal de MusicPlayer GUI er anders uit zien, namelijk: