--- - 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: 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" - name: Restart samba ansible.builtin.systemd: name: smbd state: restarted enabled: "yes" daemon_reload: "yes" # 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)