From 490c483924ccfe6ba7b06a806ce33deed755cc96 Mon Sep 17 00:00:00 2001 From: Hermes Agent service account Date: Sat, 18 Jul 2026 19:35:01 -0500 Subject: [PATCH] feat: add JMRI headless server role and main-street-station host - New ansible/roles/jmri role: installs OpenJDK 21 headless, creates jmri service user, downloads JMRI 5.10, deploys JmriFaceless systemd unit - Handles dialout group membership for serial device access - Config restore task for post-reinstall recovery from GitHub backup - host_vars/main-street-station: profile_id and serial device (TODO: fill in) - Inventory: jmri_server group with main-street-station at 192.168.10.45 - Playbook: day1_deploy_jmri.yml (linux-baseline + jmri) --- .../host_vars/main-street-station/main.yml | 16 ++++ ansible/inventory.yml | 7 ++ ansible/playbooks/day1_deploy_jmri.yml | 24 +++++ ansible/roles/jmri/defaults/main.yml | 25 ++++++ ansible/roles/jmri/handlers/main.yml | 9 ++ ansible/roles/jmri/meta/main.yml | 13 +++ ansible/roles/jmri/tasks/main.yml | 87 +++++++++++++++++++ ansible/roles/jmri/templates/jmri.service.j2 | 35 ++++++++ 8 files changed, 216 insertions(+) create mode 100644 ansible/host_vars/main-street-station/main.yml create mode 100644 ansible/playbooks/day1_deploy_jmri.yml create mode 100644 ansible/roles/jmri/defaults/main.yml create mode 100644 ansible/roles/jmri/handlers/main.yml create mode 100644 ansible/roles/jmri/meta/main.yml create mode 100644 ansible/roles/jmri/tasks/main.yml create mode 100644 ansible/roles/jmri/templates/jmri.service.j2 diff --git a/ansible/host_vars/main-street-station/main.yml b/ansible/host_vars/main-street-station/main.yml new file mode 100644 index 0000000..cb01b57 --- /dev/null +++ b/ansible/host_vars/main-street-station/main.yml @@ -0,0 +1,16 @@ +--- +# Host-specific vars for main-street-station (JMRI headless server) +# LCRR - Lake Country Railroad, Milwaukee Road Oct 1956, HO scale + +# JMRI profile ID — find with: ls ~/.jmri/profiles/ on the old box +# Format: .<8-char-hex> e.g. LCRR.3d3f1dfc +# TODO: fill in after restoring config from GitHub backup +jmri_profile_id: "" + +# USB serial device for NCE command station +# Verify after install: ls -la /dev/ttyUSB* /dev/ttyACM* +jmri_serial_device: /dev/ttyUSB0 + +# Path to JMRI config backup for restore task (leave empty to skip) +# Point at a local checkout of the LCRR GitHub repo +jmri_config_src: "" diff --git a/ansible/inventory.yml b/ansible/inventory.yml index 703d77b..8c651d9 100755 --- a/ansible/inventory.yml +++ b/ansible/inventory.yml @@ -80,6 +80,13 @@ honcho_server: ansible_user: wed ansible_become: true +jmri_server: + hosts: + main-street-station: + ansible_host: 192.168.10.45 + ansible_user: wed + ansible_become: true + papermc_server: # ansible-galaxy role install engonzal.papermc hosts: diff --git a/ansible/playbooks/day1_deploy_jmri.yml b/ansible/playbooks/day1_deploy_jmri.yml new file mode 100644 index 0000000..acc9b20 --- /dev/null +++ b/ansible/playbooks/day1_deploy_jmri.yml @@ -0,0 +1,24 @@ +--- +# ============================================================================ +# day1_deploy_jmri.yml +# ---------------------------------------------------------------------------- +# Deploys JMRI JmriFaceless headless server on main-street-station. +# Applies linux-baseline first, then the jmri role. +# +# Usage: +# ansible-playbook playbooks/day1_deploy_jmri.yml +# ansible-playbook playbooks/day1_deploy_jmri.yml -e target=main-street-station +# +# Prerequisites: +# 1. Host is in inventory under jmri_server group +# 2. jmri_profile_id is set in host_vars/main-street-station.yml +# 3. SSH access as 'wed' with sudo +# ============================================================================ + +- name: Deploy JMRI headless server + hosts: "{{ target | default('jmri_server') }}" + become: true + gather_facts: true + roles: + - linux-baseline + - jmri diff --git a/ansible/roles/jmri/defaults/main.yml b/ansible/roles/jmri/defaults/main.yml new file mode 100644 index 0000000..0c5bfac --- /dev/null +++ b/ansible/roles/jmri/defaults/main.yml @@ -0,0 +1,25 @@ +--- +# JMRI version to install +jmri_version: "5.10" +jmri_install_dir: /opt/JMRI +jmri_download_url: "https://github.com/JMRI/JMRI/releases/download/v{{ jmri_version }}/JMRI.{{ jmri_version }}+Rca461bd266.tgz" + +# Service user +jmri_user: jmri +jmri_group: jmri +jmri_home: /home/jmri + +# Profile — override per-host in inventory +# Find on the old box: ls ~/.jmri/profiles/ +jmri_profile_id: "" + +# Config restore source (path to the backed-up .jmri directory) +jmri_config_src: "" + +# Ports (for documentation / firewall rules) +jmri_json_port: 12080 +jmri_withrottle_port: 12090 + +# USB serial device for NCE (set per-host in inventory) +# e.g. /dev/ttyUSB0 or /dev/ttyACM0 +jmri_serial_device: /dev/ttyUSB0 diff --git a/ansible/roles/jmri/handlers/main.yml b/ansible/roles/jmri/handlers/main.yml new file mode 100644 index 0000000..5e60bec --- /dev/null +++ b/ansible/roles/jmri/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + +- name: Restart jmri + ansible.builtin.systemd: + name: jmri + state: restarted diff --git a/ansible/roles/jmri/meta/main.yml b/ansible/roles/jmri/meta/main.yml new file mode 100644 index 0000000..f8705ec --- /dev/null +++ b/ansible/roles/jmri/meta/main.yml @@ -0,0 +1,13 @@ +--- +galaxy_info: + role_name: jmri + author: JARVIS + description: Deploy JMRI JmriFaceless headless server as a systemd service + license: MIT + min_ansible_version: "2.12" + platforms: + - name: Ubuntu + versions: + - jammy + - noble +dependencies: [] diff --git a/ansible/roles/jmri/tasks/main.yml b/ansible/roles/jmri/tasks/main.yml new file mode 100644 index 0000000..1fecc45 --- /dev/null +++ b/ansible/roles/jmri/tasks/main.yml @@ -0,0 +1,87 @@ +--- +- name: Install Java runtime (headless) + ansible.builtin.apt: + name: openjdk-21-jre-headless + 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: /usr/sbin/nologin + system: true + create_home: true + state: present + +- name: Add JMRI user to dialout group (serial device access) + ansible.builtin.user: + name: "{{ jmri_user }}" + groups: dialout + append: true + +- 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 and start JMRI service + ansible.builtin.systemd: + name: jmri + enabled: true + state: started + daemon_reload: true diff --git a/ansible/roles/jmri/templates/jmri.service.j2 b/ansible/roles/jmri/templates/jmri.service.j2 new file mode 100644 index 0000000..a79b32b --- /dev/null +++ b/ansible/roles/jmri/templates/jmri.service.j2 @@ -0,0 +1,35 @@ +[Unit] +Description=JMRI Server (JmriFaceless) +Documentation=https://www.jmri.org/ +After=network.target +# Give USB devices time to enumerate after boot +After=dev-{{ jmri_serial_device | basename }}.device +Wants=dev-{{ jmri_serial_device | basename }}.device + +[Service] +Type=simple +User={{ jmri_user }} +Group={{ jmri_group }} +WorkingDirectory={{ jmri_install_dir }} + +# JmriFaceless requires a profile ID +# Find yours: ls ~/.jmri/profiles/ +ExecStart={{ jmri_install_dir }}/JmriFaceless --profile={{ jmri_profile_id }} + +# Restart on failure, not on clean exit (e.g. intentional shutdown from Jython) +Restart=on-failure +RestartSec=10 + +# Give hardware time to settle before retry +TimeoutStartSec=30 + +# Logging — view with: journalctl -u jmri -f +StandardOutput=journal +StandardError=journal +SyslogIdentifier=jmri + +# Serial device access +SupplementaryGroups=dialout + +[Install] +WantedBy=multi-user.target