state: restarted is unconditional, so every run reported a change and bounced smbd - which is the share the music is copied over, so a run during a transfer would interrupt it. Start/enable it as a task, restart it from a handler the config copy notifies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
232 lines
8.6 KiB
YAML
232 lines
8.6 KiB
YAML
---
|
|
- name: Packages
|
|
ansible.builtin.apt:
|
|
name:
|
|
- git
|
|
- libvlc5 # runtime lib python-vlc binds against
|
|
- vlc-plugin-base
|
|
- alsa-utils
|
|
- samba
|
|
- python3
|
|
- python3-venv
|
|
- python3-pip
|
|
cache_valid_time: 7200
|
|
|
|
- name: Checkout Musicmouse repo
|
|
ansible.builtin.git:
|
|
repo: "{{ pi_musicmouse_repo }}"
|
|
dest: /opt/musicmouse
|
|
version: "{{ musicmouse_version }}"
|
|
accept_hostkey: true
|
|
force: true
|
|
register: pi_musicmouse_checkout
|
|
|
|
- name: Check the checkout has the layout this role knows
|
|
ansible.builtin.stat:
|
|
path: /opt/musicmouse/python-backend/pyproject.toml
|
|
register: pi_musicmouse_pyproject
|
|
# Meaningless under --check on a host that has never been deployed: the git task
|
|
# above reports "changed" without writing anything, so there is nothing to stat.
|
|
when: not ansible_check_mode
|
|
|
|
- name: Fail on a pre-rearchitecture checkout
|
|
ansible.builtin.fail:
|
|
msg: >-
|
|
{{ musicmouse_version }} has no python-backend/pyproject.toml, so it predates the
|
|
rearchitecture and this role cannot install it - the venv, the systemd unit and
|
|
the web/ build all live somewhere else on those commits. Point
|
|
musicmouse_version at a branch that has it (deploy/musicdolphin), or set
|
|
mediapi_install_kidsmusic to false for this host and leave whatever is installed
|
|
alone. Stopping here rather than failing three tasks deeper with the real reason
|
|
buried.
|
|
when: not ansible_check_mode and not pi_musicmouse_pyproject.stat.exists
|
|
|
|
- name: Find leftover uv installs
|
|
# This role used to install uv here to get a Python the distribution does not ship.
|
|
# It manages neither any more, and a stray uv on PATH is a confusing thing to find
|
|
# when debugging the venv, so take them back out. Nothing else put them there.
|
|
ansible.builtin.find:
|
|
paths: /opt
|
|
patterns: "uv-*"
|
|
file_type: directory
|
|
register: pi_musicmouse_stale_uv
|
|
|
|
- name: Remove leftover uv installs
|
|
ansible.builtin.file:
|
|
path: "{{ item }}"
|
|
state: absent
|
|
loop: >-
|
|
{{ (pi_musicmouse_stale_uv.files | map(attribute='path') | list)
|
|
+ ['/usr/local/bin/uv', '/usr/local/bin/uvx'] }}
|
|
|
|
- name: Get the system Python version
|
|
ansible.builtin.command:
|
|
cmd: python3 -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
|
register: pi_musicmouse_system_python
|
|
changed_when: false
|
|
|
|
- name: Get the version the venv was built with
|
|
ansible.builtin.command:
|
|
cmd: /opt/musicmouse/.venv/bin/python -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
|
register: pi_musicmouse_venv_python
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Discard a venv built by a different Python
|
|
# `creates:` below would otherwise keep an old interpreter forever - which is exactly
|
|
# what a switch away from a separately-installed Python leaves behind. A venv holds
|
|
# absolute paths into the interpreter that made it, so it cannot be migrated; the
|
|
# cheap and correct move is to rebuild it.
|
|
ansible.builtin.file:
|
|
path: /opt/musicmouse/.venv
|
|
state: absent
|
|
when: >-
|
|
pi_musicmouse_venv_python.rc == 0
|
|
and pi_musicmouse_venv_python.stdout | trim != pi_musicmouse_system_python.stdout | trim
|
|
|
|
- name: Create the virtualenv from the system Python
|
|
ansible.builtin.command:
|
|
cmd: "python3 -m venv /opt/musicmouse/.venv"
|
|
creates: /opt/musicmouse/.venv/bin/python
|
|
|
|
# Installing the backend is deliberately not a handler hanging off the checkout.
|
|
# Tied to the checkout, one failed run strands the device: the notification is dropped
|
|
# when the play fails, the next run finds the checkout already current and notifies
|
|
# nothing, and the venv stays empty while the service crash-loops on
|
|
# ModuleNotFoundError. Instead, decide from what is actually on the device - the commit
|
|
# it has checked out, and whether the venv can import the package at all.
|
|
#
|
|
# pip's own output cannot answer this either: `musicmouse` is a local path dependency,
|
|
# so every run rebuilds and reinstalls it. Keying off that restarts the service on
|
|
# every run and rescans the whole library for nothing.
|
|
- name: Read which commit the venv was built from
|
|
ansible.builtin.command:
|
|
cmd: cat /opt/musicmouse/.venv/.installed-commit
|
|
register: pi_musicmouse_stamp
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Check the venv can actually import the backend
|
|
ansible.builtin.command:
|
|
cmd: /opt/musicmouse/.venv/bin/python -c "import musicmouse"
|
|
register: pi_musicmouse_importable
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Install the backend into the venv
|
|
ansible.builtin.command:
|
|
cmd: "/opt/musicmouse/.venv/bin/pip install /opt/musicmouse/python-backend"
|
|
when: >-
|
|
pi_musicmouse_importable.rc != 0
|
|
or pi_musicmouse_stamp.stdout | trim != pi_musicmouse_commit
|
|
changed_when: true # guarded by `when`, so reaching this means it installed something
|
|
notify: Restart musicmouse
|
|
|
|
- name: Record which commit the venv was built from
|
|
ansible.builtin.copy:
|
|
content: "{{ pi_musicmouse_commit }}\n"
|
|
dest: /opt/musicmouse/.venv/.installed-commit
|
|
mode: "0644"
|
|
|
|
- name: Create media directory
|
|
ansible.builtin.file:
|
|
path: /media/musicmouse
|
|
state: directory
|
|
mode: "0755"
|
|
|
|
- name: Install config file
|
|
ansible.builtin.copy:
|
|
src: "{{ pi_musicmouse_config_file }}"
|
|
dest: /media/musicmouse/config.yml
|
|
mode: "0644"
|
|
# Install-once: the app writes this file itself (parent mode's volume keys, the
|
|
# remote-control mapping). See pi_musicmouse_force_config in defaults.
|
|
force: "{{ pi_musicmouse_force_config }}"
|
|
notify: Restart musicmouse
|
|
|
|
- name: Install tippen curriculum
|
|
ansible.builtin.copy:
|
|
src: "{{ pi_musicmouse_curriculum_file }}"
|
|
dest: /media/musicmouse/tippen-curriculum.yml
|
|
mode: "0644"
|
|
when: pi_musicmouse_curriculum_file | length > 0
|
|
notify: Restart musicmouse
|
|
|
|
- name: Install systemd service file (from the checked-out repo, not a copy kept here)
|
|
ansible.builtin.copy:
|
|
src: /opt/musicmouse/python-backend/musicmouse.service
|
|
dest: /etc/systemd/system/musicmouse.service
|
|
mode: "0644"
|
|
remote_src: true
|
|
notify: Restart musicmouse
|
|
|
|
- name: Add script to autostart and start now
|
|
ansible.builtin.systemd:
|
|
name: musicmouse
|
|
state: started
|
|
enabled: "yes"
|
|
daemon_reload: "yes"
|
|
|
|
- name: Ensure local frontend build cache directory exists
|
|
ansible.builtin.file:
|
|
path: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
|
|
state: directory
|
|
mode: "0755"
|
|
delegate_to: localhost
|
|
become: false
|
|
vars:
|
|
# `become: false` above is not enough on its own. pi_standard_setup's defaults set
|
|
# `ansible_become: true`, role defaults are play-wide host variables, and the
|
|
# `ansible_become` *variable* outranks the `become` *keyword* in Ansible's
|
|
# precedence - so every task here tried to sudo on the control machine. Those
|
|
# defaults are needed to bootstrap a fresh Pi (connect as `pi`, become root, enable
|
|
# root login), so override the variable here rather than removing them.
|
|
ansible_become: false
|
|
|
|
- name: Checkout Musicmouse repo for frontend build (control machine)
|
|
ansible.builtin.git:
|
|
repo: "{{ pi_musicmouse_repo }}"
|
|
dest: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
|
|
version: "{{ musicmouse_version }}"
|
|
accept_hostkey: true
|
|
force: true
|
|
delegate_to: localhost
|
|
become: false
|
|
vars:
|
|
ansible_become: false # see the first delegated task in tasks/main.yml
|
|
notify: Build frontend
|
|
|
|
- name: Check whether the device has a built frontend
|
|
ansible.builtin.stat:
|
|
path: /opt/musicmouse/web/dist/index.html
|
|
register: pi_musicmouse_dist
|
|
|
|
- name: Queue a frontend build when the device has none
|
|
# The build chain hangs off the control machine's checkout changing, which has the
|
|
# same hole the backend install had: a run that fails after that checkout leaves the
|
|
# Pi with no dist, and every later run finds the checkout current and rebuilds
|
|
# nothing. Cheap to check, and it only fires when the device really has no UI.
|
|
ansible.builtin.debug:
|
|
msg: "No frontend at /opt/musicmouse/web/dist - queueing a build"
|
|
when: not pi_musicmouse_dist.stat.exists
|
|
changed_when: true
|
|
notify: Build frontend
|
|
|
|
- name: Samba setup
|
|
ansible.builtin.copy:
|
|
src: smb.conf
|
|
dest: /etc/samba/
|
|
mode: "0644"
|
|
notify: Restart samba
|
|
|
|
- name: Enable samba
|
|
ansible.builtin.systemd:
|
|
name: smbd
|
|
state: started
|
|
enabled: true
|
|
# manual steps:
|
|
# - set samba passwords with smbpasswd
|
|
# - copy music into /media/musicmouse: scripts/sync-library.sh in the app repo, which
|
|
# also carries the cover and analysis cache over (or use the samba share, and let
|
|
# the device spend the DSP time itself)
|