feat(jmri): headless JMRI server with Leviton layout power monitor and X11 GUI mode
- Stable udev device symlinks (/dev/jmri/nce, /dev/jmri/loconet, /dev/jmri/lcc) - jmri-monitor: polls Leviton Decora Smart switch to start/stop JMRI automatically - Quiet hours 1-10 AM (no polling) - 30s off-delay before shutdown - LCRR config cloned from Gitea (ssh://gitea.mk-labs.cloud:2221/rblundon/LCRR.git) - ~/.jmri symlinked to LCRR repo for GitOps config management - jmri-gui: X11 remote GUI access (PanelPro/DecoderPro) via ssh -X as jmri user - Stops daemon, launches GUI, restarts daemon on exit if layout still on - jmri user gets login shell + SSH key for GUI sessions - Full JRE installed (openjdk-21-jre) for AWT/X11 support
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
---
|
||||
- name: Install Java runtime (headless)
|
||||
- name: Install Java runtime (full — required for GUI mode)
|
||||
ansible.builtin.apt:
|
||||
name: openjdk-21-jre-headless
|
||||
name:
|
||||
- openjdk-21-jre
|
||||
- openjdk-21-jdk
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
@@ -16,17 +18,147 @@
|
||||
name: "{{ jmri_user }}"
|
||||
group: "{{ jmri_group }}"
|
||||
home: "{{ jmri_home }}"
|
||||
shell: /usr/sbin/nologin
|
||||
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"
|
||||
@@ -79,9 +211,60 @@
|
||||
- Reload systemd
|
||||
- Restart jmri
|
||||
|
||||
- name: Enable and start JMRI service
|
||||
- name: Enable jmri service (monitor manages start/stop — do not start directly)
|
||||
ansible.builtin.systemd:
|
||||
name: jmri
|
||||
enabled: true
|
||||
state: started
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user