Files
homelab/ansible/roles/jmri/tasks/main.yml
Hermes Agent service account 265d3f8fd6 jmri: remove one-shot xpra migration task (idempotency fix)
Migration from Ubuntu 3.x to upstream 6.x is complete. The explicit
removal task was firing changed on every run. state: latest on the
install task handles upgrades going forward.
2026-08-01 22:15:11 -05:00

389 lines
11 KiB
YAML

---
- 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: Deploy extra SSH authorized keys for jmri user
ansible.posix.authorized_key:
user: "{{ jmri_user }}"
key: "{{ item }}"
state: present
loop: "{{ jmri_ssh_authorized_keys_extra }}"
- name: Add JMRI user to dialout group (serial device access)
ansible.builtin.user:
name: "{{ jmri_user }}"
groups: dialout
append: true
# ---------------------------------------------------------------------------
# Phase 1 — Stable USB device symlinks
# Creates /dev/jmri/loconet and /dev/jmri/nce via udev ID_SERIAL matching.
# ---------------------------------------------------------------------------
- name: Deploy udev rules for JMRI USB devices
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
- Trigger 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: "{{ jmri_lcrr_branch }}"
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
# ---------------------------------------------------------------------------
# JMRI install / upgrade — version-marker pattern
# Writes {{ jmri_install_dir }}/.jmri_installed_version after each install.
# On subsequent runs: read the marker, skip everything if it matches
# jmri_version. If it differs (or is absent), stop JMRI cleanly, wipe the
# old install, download the new archive, extract, and write the new marker.
# To upgrade: bump jmri_version + jmri_build_hash in defaults/main.yml (or
# host_vars) and re-run the playbook.
# ---------------------------------------------------------------------------
- name: Read installed JMRI version marker (if present)
ansible.builtin.slurp:
src: "{{ jmri_install_dir }}/.jmri_installed_version"
register: jmri_version_marker
ignore_errors: true
- name: Determine whether JMRI install/upgrade is needed
ansible.builtin.set_fact:
jmri_needs_install: >-
{{
jmri_version_marker is failed or
(jmri_version_marker.content | b64decode | trim) != jmri_version
}}
- name: Stop JMRI service before upgrade (if running)
ansible.builtin.systemd:
name: jmri
state: stopped
failed_when: false # service may not exist yet on first install
when: jmri_needs_install
- name: Remove existing JMRI install directory (upgrade path)
ansible.builtin.file:
path: "{{ jmri_install_dir }}"
state: absent
when: jmri_needs_install
- name: Remove stale JMRI archive from /tmp (if version changed)
ansible.builtin.file:
path: "/tmp/JMRI-{{ jmri_version }}.tgz"
state: absent
when: jmri_needs_install
- name: Download JMRI release archive
ansible.builtin.get_url:
url: "{{ jmri_download_url }}"
dest: /tmp/JMRI-{{ jmri_version }}.tgz
mode: '0644'
when: jmri_needs_install
- 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: jmri_needs_install
- name: Write installed version marker
ansible.builtin.copy:
content: "{{ jmri_version }}\n"
dest: "{{ jmri_install_dir }}/.jmri_installed_version"
owner: "{{ jmri_user }}"
group: "{{ jmri_group }}"
mode: '0644'
when: jmri_needs_install
- 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 (retained for fallback; VNC preferred)
# ---------------------------------------------------------------------------
- name: Install xauth (required for SSH X11 forwarding fallback)
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
# ---------------------------------------------------------------------------
# Phase 4 — Xpra virtual display
# Replaces TigerVNC. Rootless mode: each JMRI window appears as a native
# window on the client. Sessions are persistent across disconnects.
# Connect: xpra attach ssh://jmri@main-street-station/{{ jmri_xpra_display }}
# ---------------------------------------------------------------------------
- name: Remove TigerVNC (replaced by Xpra)
ansible.builtin.apt:
name: tigervnc-standalone-server
state: absent
notify: Reload systemd
- name: Disable and stop jmri-vnc service if present
ansible.builtin.systemd:
name: jmri-vnc
enabled: false
state: stopped
failed_when: false
- name: Remove jmri-vnc systemd unit if present
ansible.builtin.file:
path: /etc/systemd/system/jmri-vnc.service
state: absent
notify: Reload systemd
- name: Remove jmri VNC password directory if present
ansible.builtin.file:
path: "{{ jmri_home }}/.vnc"
state: absent
- name: Install xpra.org apt signing key
ansible.builtin.get_url:
url: https://xpra.org/gpg.asc
dest: /usr/share/keyrings/xpra.asc
mode: '0644'
- name: Add xpra.org upstream apt repository
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/xpra.list
owner: root
group: root
mode: '0644'
content: |
deb [arch=amd64 signed-by=/usr/share/keyrings/xpra.asc] https://xpra.org/ noble main
- name: Install Xpra from upstream repo (v6.x)
ansible.builtin.apt:
name: xpra
state: latest
update_cache: true
- name: Deploy jmri-xpra systemd unit
ansible.builtin.template:
src: jmri-xpra.service.j2
dest: /etc/systemd/system/jmri-xpra.service
owner: root
group: root
mode: '0644'
notify:
- Reload systemd
- Restart jmri-xpra
- name: Enable and start jmri-xpra service
ansible.builtin.systemd:
name: jmri-xpra
enabled: true
state: started
daemon_reload: true
- 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