--- - name: Install Java runtime (full — required for GUI mode) ansible.builtin.apt: name: - openjdk-21-jre - openjdk-21-jdk state: present update_cache: true - name: Create JMRI system group ansible.builtin.group: name: "{{ jmri_group }}" state: present system: true - name: Create JMRI service user ansible.builtin.user: name: "{{ jmri_user }}" group: "{{ jmri_group }}" home: "{{ jmri_home }}" shell: /bin/bash system: true create_home: true state: present - name: Deploy SSH authorized key for jmri user ansible.posix.authorized_key: user: "{{ jmri_user }}" key: "{{ jmri_ssh_authorized_key }}" state: present when: jmri_ssh_authorized_key | length > 0 - name: Add JMRI user to dialout group (serial device access) ansible.builtin.user: name: "{{ jmri_user }}" groups: dialout append: true # --------------------------------------------------------------------------- # Phase 1 — Stable udev device symlinks # Creates /dev/jmri/nce, /dev/jmri/loconet, /dev/jmri/lcc # --------------------------------------------------------------------------- - name: Create /dev/jmri symlink directory (static — survives reboots) ansible.builtin.file: path: /etc/jmri state: directory owner: root group: root mode: '0755' - name: Deploy udev rules for stable JMRI device symlinks ansible.builtin.template: src: 99-jmri-devices.rules.j2 dest: /etc/udev/rules.d/99-jmri-devices.rules owner: root group: root mode: '0644' notify: Reload udev # --------------------------------------------------------------------------- # Phase 2 — LocoNet traffic monitor # Installs jmri-monitor: watches /dev/jmri/loconet, starts/stops jmri.service # --------------------------------------------------------------------------- - name: Install python3-venv (monitor virtualenv support) ansible.builtin.apt: name: python3-venv state: present - name: Create virtualenv for jmri-monitor ansible.builtin.command: cmd: python3 -m venv /opt/jmri-monitor creates: /opt/jmri-monitor/bin/python3 - name: Install decora_wifi into jmri-monitor virtualenv ansible.builtin.pip: name: decora_wifi state: present virtualenv: /opt/jmri-monitor - name: Deploy jmri-monitor script ansible.builtin.template: src: jmri-monitor.py.j2 dest: /usr/local/bin/jmri-monitor owner: root group: root mode: '0755' notify: Restart jmri-monitor - name: Deploy jmri-monitor systemd unit ansible.builtin.template: src: jmri-monitor.service.j2 dest: /etc/systemd/system/jmri-monitor.service owner: root group: root mode: '0644' notify: - Reload systemd - Restart jmri-monitor - name: Enable jmri-monitor service ansible.builtin.systemd: name: jmri-monitor enabled: true state: started daemon_reload: true # --------------------------------------------------------------------------- # Phase 2b — LCRR config repo (clone/pull .jmri from Gitea) # --------------------------------------------------------------------------- - name: Ensure jmri .ssh directory exists ansible.builtin.file: path: /home/jmri/.ssh state: directory owner: "{{ jmri_user }}" group: "{{ jmri_group }}" mode: '0700' - name: Deploy jmri SSH config for Gitea ansible.builtin.copy: dest: /home/jmri/.ssh/config owner: "{{ jmri_user }}" group: "{{ jmri_group }}" mode: '0600' content: | Host gitea.mk-labs.cloud HostName gitea.mk-labs.cloud User git Port 2221 IdentityFile {{ jmri_gitea_key }} StrictHostKeyChecking accept-new - name: Clone LCRR config repo if not present ansible.builtin.git: repo: "{{ jmri_lcrr_repo }}" dest: "{{ jmri_home }}/LCRR" version: main accept_hostkey: true key_file: "{{ jmri_gitea_key }}" update: false become_user: "{{ jmri_user }}" when: jmri_lcrr_repo | length > 0 notify: Restart jmri - name: Remove auto-generated .jmri dir if it exists (will be replaced by symlink) ansible.builtin.file: path: "{{ jmri_home }}/.jmri" state: absent when: - jmri_lcrr_repo | length > 0 - name: Link .jmri config from LCRR repo ansible.builtin.file: src: "{{ jmri_home }}/LCRR/.jmri" dest: "{{ jmri_home }}/.jmri" state: link owner: "{{ jmri_user }}" group: "{{ jmri_group }}" force: true when: jmri_lcrr_repo | length > 0 notify: Restart jmri - name: Check if JMRI is already installed at correct version ansible.builtin.stat: path: "{{ jmri_install_dir }}/JmriFaceless" register: jmri_binary - name: Download JMRI release archive ansible.builtin.get_url: url: "{{ jmri_download_url }}" dest: /tmp/JMRI-{{ jmri_version }}.tgz mode: '0644' when: not jmri_binary.stat.exists - name: Create JMRI install directory ansible.builtin.file: path: "{{ jmri_install_dir }}" state: directory owner: "{{ jmri_user }}" group: "{{ jmri_group }}" mode: '0755' - name: Extract JMRI archive ansible.builtin.unarchive: src: /tmp/JMRI-{{ jmri_version }}.tgz dest: "{{ jmri_install_dir }}" remote_src: true owner: "{{ jmri_user }}" group: "{{ jmri_group }}" extra_opts: ['--strip-components=1'] when: not jmri_binary.stat.exists - name: Restore JMRI config from backup ansible.builtin.copy: src: "{{ jmri_config_src }}/" dest: "{{ jmri_home }}/.jmri/" owner: "{{ jmri_user }}" group: "{{ jmri_group }}" mode: '0644' directory_mode: '0755' when: jmri_config_src | length > 0 notify: Restart jmri - name: Deploy JmriFaceless systemd unit ansible.builtin.template: src: jmri.service.j2 dest: /etc/systemd/system/jmri.service owner: root group: root mode: '0644' notify: - Reload systemd - Restart jmri - name: Enable jmri service (monitor manages start/stop — do not start directly) ansible.builtin.systemd: name: jmri enabled: false daemon_reload: true # --------------------------------------------------------------------------- # Phase 3 — X11 remote GUI access # --------------------------------------------------------------------------- - name: Install xauth (required for SSH X11 forwarding) ansible.builtin.apt: name: xauth state: present - name: Remove old jmri X11 sshd drop-in if present (renamed) ansible.builtin.file: path: /etc/ssh/sshd_config.d/20-jmri-x11.conf state: absent notify: Restart sshd - name: Deploy sshd drop-in to enable X11 forwarding (must load before hardening) ansible.builtin.copy: dest: /etc/ssh/sshd_config.d/09-jmri-x11.conf owner: root group: root mode: '0644' content: | # Allow X11 forwarding for JMRI GUI sessions (jmri role) # Must be numbered below 10-mk-labs-hardening.conf — first match wins. X11Forwarding yes X11UseLocalhost yes notify: Restart sshd - name: Deploy jmri-gui script ansible.builtin.template: src: jmri-gui.j2 dest: /usr/local/bin/jmri-gui owner: root group: root mode: '0755' - name: Deploy sudoers drop-in for jmri-gui (wed can manage jmri service) ansible.builtin.copy: dest: /etc/sudoers.d/jmri-gui owner: root group: root mode: '0440' validate: 'visudo -cf %s' content: | # jmri user can manage its own service (for jmri-gui interactive sessions) jmri ALL=(root) NOPASSWD: /usr/bin/systemctl start jmri.service jmri ALL=(root) NOPASSWD: /usr/bin/systemctl stop jmri.service jmri ALL=(root) NOPASSWD: /opt/jmri-monitor/bin/python3 # wed retains service control for automation/admin use wed ALL=(root) NOPASSWD: /usr/bin/systemctl start jmri.service wed ALL=(root) NOPASSWD: /usr/bin/systemctl stop jmri.service wed ALL=(root) NOPASSWD: /opt/jmri-monitor/bin/python3