121 lines
4.8 KiB
YAML
121 lines
4.8 KiB
YAML
---
|
|
# Audiobookshelf as a rootless Podman Quadlet unit.
|
|
#
|
|
# Everything here runs as {{ podman_user }} — the connection user. No become, no
|
|
# XDG_RUNTIME_DIR: the SSH connection gave us a real PAM session, so systemd's
|
|
# user manager is reachable through the standard environment.
|
|
|
|
- name: Ensure the Audiobookshelf data directories exist
|
|
ansible.builtin.file:
|
|
path: "{{ item }}"
|
|
state: directory
|
|
owner: "{{ podman_user }}"
|
|
group: "{{ podman_user }}"
|
|
mode: '0755'
|
|
loop:
|
|
# config/ and metadata/ are the irreplaceable 22 MB — absdatabase.sqlite,
|
|
# migrations, backups, cache, logs. audiobooks/ and podcasts/ are the
|
|
# library, which the app reads and podcast downloads are written into.
|
|
- "{{ config_dir }}"
|
|
- "{{ metadata_dir }}"
|
|
- "{{ audiobooks_dir }}"
|
|
- "{{ podcasts_dir }}"
|
|
|
|
- name: Install the Audiobookshelf Quadlet unit
|
|
ansible.builtin.template:
|
|
src: audiobookshelf.container.j2
|
|
dest: "{{ quadlet_dir }}/{{ container_name }}.container"
|
|
owner: "{{ podman_user }}"
|
|
group: "{{ podman_user }}"
|
|
mode: '0644'
|
|
notify:
|
|
- Reload systemd user units
|
|
- Restart Audiobookshelf
|
|
|
|
- name: Start Audiobookshelf
|
|
block:
|
|
- name: Start Audiobookshelf
|
|
ansible.builtin.systemd_service:
|
|
name: "{{ unit_name }}"
|
|
scope: user
|
|
state: started
|
|
daemon_reload: true
|
|
rescue:
|
|
# systemd reports "control process exited with error code" and nothing
|
|
# else, which is useless for the usual failures (an unattached network, an
|
|
# unwritable /config, a bad volume). Pull the real reason out.
|
|
- name: Collect Audiobookshelf diagnostics
|
|
ansible.builtin.command: "{{ item }}"
|
|
loop:
|
|
- "systemctl --user status {{ unit_name }} --no-pager"
|
|
- "journalctl --user -u {{ unit_name }} --no-pager -n 40"
|
|
- "podman logs --tail 40 {{ container_name }}"
|
|
register: audiobookshelf_diag
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Show Audiobookshelf diagnostics
|
|
ansible.builtin.debug:
|
|
msg: "{{ audiobookshelf_diag.results | map(attribute='stdout') | list }}"
|
|
|
|
- name: Fail
|
|
ansible.builtin.fail:
|
|
msg: "Audiobookshelf did not start — see the diagnostics above."
|
|
|
|
# Quadlet honours [Install] WantedBy= when it generates the unit, so
|
|
# `systemctl --user enable` is neither needed nor possible on a generated unit.
|
|
- name: Verify Audiobookshelf is running
|
|
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
|
|
register: audiobookshelf_active
|
|
changed_when: false
|
|
failed_when: audiobookshelf_active.stdout != 'active'
|
|
|
|
# `is-active` says the container is running; it does not say the app can serve.
|
|
# `Restart=always` means even a crash-looping container is reported active. The
|
|
# image ships no HEALTHCHECK, so the unit defines one (HealthCmd=, see the
|
|
# template) and this runs exactly that command once, synchronously.
|
|
- name: Wait for Audiobookshelf to pass its health check
|
|
ansible.builtin.command: "podman healthcheck run {{ container_name }}"
|
|
register: audiobookshelf_health
|
|
until: audiobookshelf_health.rc == 0
|
|
retries: 24
|
|
delay: 5
|
|
changed_when: false
|
|
|
|
- name: Confirm the container sees all four mount targets
|
|
ansible.builtin.command: "podman exec {{ container_name }} ls -d /config /metadata /audiobooks /podcasts"
|
|
register: audiobookshelf_mounts
|
|
changed_when: false
|
|
failed_when: audiobookshelf_mounts.rc != 0
|
|
|
|
# The health check proves the server started — it exits 1 with EROFS if /config
|
|
# is not writable — but not that /config is the *bind mount*. This does: the
|
|
# database the app has just created has to appear on the host, not inside the
|
|
# container's writable layer.
|
|
- name: Check the database landed on the host
|
|
ansible.builtin.stat:
|
|
path: "{{ config_dir }}/absdatabase.sqlite"
|
|
register: audiobookshelf_db
|
|
|
|
- name: Report where the database landed
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
{{ config_dir }}/absdatabase.sqlite is
|
|
{{ audiobookshelf_db.stat.pw_name | default('?') }}:{{ audiobookshelf_db.stat.gr_name | default('?') }},
|
|
{{ audiobookshelf_db.stat.size | default(0) }} bytes — created by the
|
|
container, visible on the host, so the bind mount is real.
|
|
when: audiobookshelf_db.stat.exists
|
|
|
|
# This is the check that catches a broken or missing Volume= line, which is the
|
|
# one failure here that is silent: the app would run perfectly well with its
|
|
# data in the container's writable layer, and lose all of it on the next
|
|
# recreate.
|
|
- name: Fail if the database is not on the host
|
|
ansible.builtin.assert:
|
|
that: audiobookshelf_db.stat.exists
|
|
fail_msg: >-
|
|
{{ config_dir }}/absdatabase.sqlite does not exist, so /config is not the
|
|
bind mount it should be. The app's state is inside the container's
|
|
writable layer and will be lost when the container is recreated — check
|
|
the Volume= lines in the generated unit.
|