monotux.tech

Building Navidrome with Ansible

Ansible, FreeBSD, YAML

I used to be picky about not leaving custom built stuff around on my systems, as I quickly forget how they were setup, how to fix it and so forth. Stock packages from the distribution repositories, please.

Then Docker came along and changed barely anything. No snowflakey containers unless written in a docker-compose.yml.

But then I learned Ansible. It became my hammer, and there were a lot of nails around! And I was excited to have the cake of no custom hacks, and to eat it too, served with custom hacks!

My latest custom hack was to fix a broken FreeBSD port, Navidrome – all so I could listen to my music collection while at work. As I used Ansible this custom build is documented, automatically built and deployed so I just have to remember which playbook to run.

The role can be found on sr.ht, in my homelab repository. But I'll show the important bits below.

  $ tree
  .
  ├── defaults
  │   └── main.yml
  ├── files
  │   ├── config.toml.sample
  │   └── navidrome
  ├── handlers
  │   └── main.yml
  └── tasks
      └── main.yml

  4 directories, 5 files

default/main.yml

All variables needed to build Navidrome on FreeBSD. I just chose a uid/gid which shouldn't override some default system account.

  ---
  navidrome_dependencies:
    - npm
    - go
    - git-lite
    - taglib
    - ffmpeg

  navidrome_repo_path: "/root/navidrome"
  navidrome_repo_url: "https://github.com/navidrome/navidrome.git"
  navidrome_repo_version: "master"
  navidrome_repo_update: true
  navidrome_install: true
  navidrome_force_build: false

  navidrome_db_directory: "/var/db/navidrome"
  navidrome_config_directory: "/usr/local/etc/navidrome"
  navidrome_config: "{{ navidrome_config_directory }}/config.toml"

  navidrome_user: 'navidrome'
  navidrome_group: 'navidrome'
  navidrome_user_id: 2014
  navidrome_group_id: 2014

  navidrome_autostart: true
  navidrome_flags: ''

files

Two files, one is just a example configuration (config.toml.sample) and one is an init script. Below is files/navidrome:

  #!/bin/sh
  #
  # PROVIDE: navidrome
  # REQUIRE: NETWORKING
  # KEYWORD:
  #
  # Add the following lines to /etc/rc.conf to enable navidrome:
  # navidrome_enable="YES"
  #
  # navidrome_enable (bool):		Set to YES to enable navidrome
  #					Default: NO
  # navidrome_config (str):		navidrome configration file
  #					Default: /usr/local/etc/navidrome/config.toml
  # navidrome_datafolder (str):	navidrome Folder to store application data
  #					Default: navidrome
  # navidrome_user (str):		navidrome daemon user
  #					Default: navidrome
  # navidrome_group (str):		navidrome daemon group
  #					Default: navidrome

  . /etc/rc.subr

  name="navidrome"
  rcvar="navidrome_enable"
  load_rc_config $name

  : ${navidrome_user:="navidrome"}
  : ${navidrome_group:="navidrome"}
  : ${navidrome_enable:="NO"}
  : ${navidrome_config:="/usr/local/etc/navidrome/config.toml"}
  : ${navidrome_flags=""}
  : ${navidrome_facility:="daemon"}
  : ${navidrome_priority:="debug"}
  : ${navidrome_datafolder:="/var/db/${name}"}

  required_dirs=${navidrome_datafolder}
  required_files=${navidrome_config}
  procname="/usr/local/bin/${name}"
  pidfile="/var/run/${name}.pid"
  start_precmd="${name}_precmd"
  command=/usr/sbin/daemon
  command_args="-S -l ${navidrome_facility} -s ${navidrome_priority} -T ${name} -t ${name} -p ${pidfile} \
          ${procname} --configfile ${navidrome_config} --datafolder ${navidrome_datafolder} ${navidrome_flags}"

  navidrome_precmd()
  {
          install -o ${navidrome_user} /dev/null ${pidfile}
  }

  run_rc_command "$1"

handlers/main.yml

A simple handler to restart Navidrome:

  ---
  - name: Restart navidrome
    ansible.builtin.service:
      name: navidrome
      state: restarted

tasks/main.yml

Writing this down changes what we are doing from wrong to ok.

  ---
  - name: Install required packages
    ansible.builtin.package:
      name: "{{ item }}"
      state: present
    with_items: "{{ navidrome_dependencies }}"

  - name: Clone repository
    ansible.builtin.git:
      dest: "{{ navidrome_repo_path }}"
      repo: "{{ navidrome_repo_url }}"
      version: "{{ navidrome_repo_version }}"
      update: "{{ navidrome_repo_update }}"
    register: git_repo

  - name: Run make setup
    community.general.make:
      chdir: "{{ navidrome_repo_path }}"
      make: "/usr/local/bin/gmake"
      target: "setup"
    when: navidrome_force_build or git_repo.changed

  - name: Run make buildall
    community.general.make:
      chdir: "{{ navidrome_repo_path }}"
      make: "/usr/local/bin/gmake"
      target: "buildall"
    register: buildall
    when: navidrome_force_build or git_repo.changed

  - name: Install navidrome binary
    ansible.builtin.copy:
      src: "{{ navidrome_repo_path }}/navidrome"
      dest: "/usr/local/bin/navidrome"
      remote_src: true
      owner: root
      group: wheel
      mode: '0755'
    when: navidrome_install and buildall.changed
    notify: Restart navidrome

  - name: Install rc.d script
    ansible.builtin.copy:
      src: "navidrome"
      dest: "/usr/local/etc/rc.d/navidrome"
      owner: root
      group: wheel
      mode: '0755'
    when: navidrome_install

  - name: Create db directory
    ansible.builtin.file:
      path: "{{ navidrome_db_directory }}"
      state: directory
      owner: "{{ navidrome_user }}"
      group: "{{ navidrome_group }}"
    when: navidrome_install

  - name: Create config directory
    ansible.builtin.file:
      path: "{{ navidrome_config_directory }}"
      state: directory
      owner: "root"
      group: "wheel"
    when: navidrome_install

  - name: Copy sample configuration
    ansible.builtin.copy:
      src: "config.toml.sample"
      dest: "{{ navidrome_config }}.sample"
      owner: root
      group: wheel
      mode: '0750'
    when: navidrome_install

  - name: Create navidrome group
    ansible.builtin.group:
      name: "{{ navidrome_group }}"
      gid: "{{ navidrome_group_id }}"
      system: true
    when: navidrome_install

  - name: Create navidrome user
    ansible.builtin.user:
      name: "{{ navidrome_user }}"
      uid: "{{ navidrome_user_id }}"
      group: "{{ navidrome_group }}"
      system: true
    when: navidrome_install

  - name: Setup navidrome in sysrc
    community.general.sysrc:
      name: "navidrome_{{ item.name }}"
      state: present
      value: "{{ item.value }}"
    with_items:
      - {name: 'enable', value: '{{ navidrome_autostart }}'}
      - {name: 'datafolder', value: '{{ navidrome_db_directory }}'}
      - {name: 'config', value: '{{ navidrome_config }}'}
      - {name: 'user', value: '{{ navidrome_user }}'}
      - {name: 'group', value: '{{ navidrome_group }}'}

Conclusion

I've had too much cake to eat. And I will do it again.