From 1da74f784ebe378205648b5b00a20e29ee19d546 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:15:54 +0200 Subject: [PATCH 1/9] feat(roles/apache_solr): EL10 support, backup pipeline, JVM heap, SecurityManager, allowPaths * Add Tomcat-9-style RHEL 10 support: pick java-21-openjdk-headless via a new vars/RedHat10.yml override (java-17 was dropped from EL10 AppStream). EL8/9 are unchanged. * Switch the Solr 9.x tarball download to dlcdn.apache.org first, fall back to archive.apache.org on 404 / failure. The archive server advertises `Vary: Slow,Glacial` and was making get_url hang for ~30 minutes; the CDN delivers a 371 MB tarball in seconds. Solr 8.x stays on the Lucene archive path (EOL, no longer mirrored by the CDN). * Add `become: false` to both `delegate_to: localhost` get_url tasks so the role works under ansible-navigator EEs (sudo without a password fails inside the EE container). * Expose three previously-buried-or-missing knobs as user-facing variables: - apache_solr__heap (default '512m', drives SOLR_HEAP) - apache_solr__security_manager_enabled (default true; disable for solr.solr.home outside Solr's permitted paths, e.g. Numishare) - apache_solr__allow_paths (default []; auto-augmented with apache_solr__dump_directory when backups are enabled, so Solr 9 can write its snapshots) * Add a mariadb-dump-style backup pipeline: - /usr/local/bin/apache-solr-dump (curl + replication?command=backup, polls command=details until status==success, status==exception fails loudly, status==unknown after 10 min per core fails loudly) - /etc/apache-solr-dump.conf (sourced by the script) - apache-solr-dump.service (Type=oneshot, After=solr.service) - apache-solr-dump.timer (default 22::00 daily) Wipe-and-refresh per run; retention is the surrounding backup tool's job. apache_solr__dump_cores empty (default) disables the timer. The dump directory's parent is created as 0o755 root:root so other dump pipelines (existdb-dump, mariadb-dump) sharing /backup/ can still traverse into their own subdirs. * Mark apache_solr as proven on RHEL 10 in COMPATIBILITY.md. * README documents the optional backup variables and the matching restore via replication?command=restore + restorestatus polling. --- CHANGELOG.md | 7 ++ COMPATIBILITY.md | 2 +- roles/apache_solr/README.md | 58 +++++++++ roles/apache_solr/defaults/main.yml | 11 ++ roles/apache_solr/tasks/main.yml | 116 ++++++++++++++++-- .../templates/etc/apache-solr-dump.conf.j2 | 8 ++ .../system/apache-solr-dump.service.j2 | 15 +++ .../systemd/system/apache-solr-dump.timer.j2 | 12 ++ .../templates/opt/solr/bin/solr.in.sh.j2 | 7 +- .../usr/local/bin/apache-solr-dump.j2 | 76 ++++++++++++ roles/apache_solr/vars/RedHat10.yml | 4 + 11 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 roles/apache_solr/templates/etc/apache-solr-dump.conf.j2 create mode 100644 roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.service.j2 create mode 100644 roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.timer.j2 create mode 100644 roles/apache_solr/templates/usr/local/bin/apache-solr-dump.j2 create mode 100644 roles/apache_solr/vars/RedHat10.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index d96cfaf64..d4d9a1d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:apache_solr**: Add `apache_solr__allow_paths` to expose `-Dsolr.allowPaths` from the inventory (default `[]`). Required when Solr 9 must read or write outside `solr.solr.home` — e.g. backup destinations that the new dump pipeline auto-injects, or cores whose data lives elsewhere. The empty default keeps the property unset, matching upstream behavior +* **role:apache_solr**: Add `apache_solr__heap` (default `'512m'`) so users can size the Solr JVM heap from the inventory. Previously the only knob was the upstream-shipped `#SOLR_HEAP="512m"` comment in `solr.in.sh`, leaving Solr permanently on the 512m default with no role-level override +* **role:apache_solr**: Add `apache_solr__security_manager_enabled` (default `true`, matching Solr 9's documented default) so deployments with a `solr.solr.home` outside Solr's permitted paths can disable the Java SecurityManager from the inventory. Required for Numishare, whose Solr core lives under `/opt/numishare/solr-home/` and otherwise hits `access denied ("java.io.FilePermission" ...)` errors +* **role:apache_solr**: Add a `mariadb-dump`-style backup pipeline: `apache-solr-dump.service` (oneshot) + `.timer` snapshot every core listed in `apache_solr__dump_cores` via Solr's `replication?command=backup` endpoint, polling `command=details` until status is `success`. Snapshots land in `apache_solr__dump_directory` (default `/backup/apache-solr-dump`). Wipe-and-refresh per run; retention is the surrounding backup tool's job. Empty `apache_solr__dump_cores` disables the timer (no-op default). The pipeline also adds `apache_solr__dump_directory` to `-Dsolr.allowPaths` automatically (Solr 9 rejects backup `location=` outside `solr.solr.home` with HTTP 400). The parent of `apache_solr__dump_directory` is created as `0o755 root:root` so other dump pipelines (existdb-dump, mariadb-dump) sharing `/backup/` can still traverse into their own subdirs. README documents the restore via `replication?command=restore` plus `restorestatus` polling * **role:graylog_datanode**: Add optional variable `graylog_datanode__raw`. * **role:graylog_datanode**: Add optional variables `graylog_datanode__path_repos`, `graylog_datanode__node_search_cache_size` to configure searchable snapshot locations and size of disk-based searchable snapshot cache. * **role:infomaniak_vm**: Add `keep_port_on_absent` subkey on `infomaniak_vm__networks` entries to preserve the port (and its fixed IP) when the VM is set to `infomaniak_vm__state: 'absent'`, so the same IP can be re-used @@ -25,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:apache_solr**: Fix `No package java-17-openjdk-headless available.` on RHEL 10. Red Hat dropped `java-17-openjdk` from EL10 AppStream (only `java-21-openjdk` and `java-25-openjdk` ship now). The role now picks `java-21-openjdk-headless` for Solr 9.x on EL10 via a new OS-specific `vars/RedHat10.yml`; EL8/9 keep `java-17-openjdk-headless` unchanged. Solr 9.x officially supports Java 11, 17, and 21. +* **role:apache_solr**: Add `become: false` to the `delegate_to: localhost` `get_url` tasks so the role works under ansible-navigator execution environments, where `become` would try to `sudo` inside the EE container without a password. * **playbooks/freeipa_client, playbooks/freeipa_server**: Set `strategy: 'linear'` explicitly so the playbooks work even when the user's `ansible.cfg` defaults to a strategy that reuses the target Python interpreter (e.g. `mitogen_linear`). The ansible-freeipa modules rely on `ipalib`'s global API singleton and otherwise fail with `API.bootstrap() already called` on the second module call. * **role:mariadb_server**: Fix MariaDB starting in the `unconfined_service_t` SELinux domain on RHEL 10, which leaves `/var/lib/mysql/mysql.sock` mislabeled and breaks `php-fpm`/`httpd_t` clients (e.g. Icinga Web 2 login: `SQLSTATE[HY000] [2002] Permission denied`). The unit drop-in's `ExecStartPre=-/bin/chcon -t mysqld_exec_t /usr/sbin/mariadbd` workaround for [MDEV-30520](https://jira.mariadb.org/browse/MDEV-30520) cannot relabel the binary on EL10+, where the packaged `mariadb.service` applies `ProtectSystem` that mounts `/usr` read-only inside the service sandbox. The role now sets the `mysqld_exec_t` file context for `/usr/sbin/mariadbd` persistently via `semanage fcontext` + `restorecon` (outside the systemd sandbox) and notifies a restart so the daemon comes up in `mysqld_t`. * **role:icinga2_master**: Fix `selinux` role failing on RHEL 10 with `SELinux boolean icinga2_can_connect_all is not defined in persistent policy` (and `[Errno 11]` for the other Icinga/Nagios booleans). The `icinga2-selinux` policy module references `nagios_*_plugin_t` types that were moved out of the EL10 base policy into the separate `nagios-selinux` package (EPEL), so without it the `icinga2-selinux` `%post` silently fails and the booleans never appear. The role now installs `nagios-selinux` as a separate pre-install task on RHEL 10 so its `%post` registers the required types before `icinga2-selinux`'s `%post` runs. @@ -48,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* **role:apache_solr**: Download Solr 9.x tarballs from `dlcdn.apache.org` first and only fall back to `archive.apache.org` on failure. The Apache CDN serves currently-supported versions at full speed, while the archive server is intentionally throttled (advertises `Vary: Slow,Glacial`) and previously made `get_url` appear to hang for ~30 minutes per run. The fallback keeps older 9.x versions reachable once they've rotated out of the CDN. Solr 8.x stays on `archive.apache.org` (Lucene path; EOL, CDN no longer mirrors it). * **role:system_update**: Change default of `system_update__update_time` from `'04:00 + 1 days'` to `'04:{{ 59 | random(seed=inventory_hostname) }} + 1 days'`, so updates are spread deterministically across 04:00–04:59 (minute derived from `inventory_hostname`) instead of all hosts firing at 04:00 sharp * **role:firewall**: Install `nftables` together with `iptables` for `firewall__firewall == "fwbuilder"` on all distros (previously only installed via per-distro task files on Fedora and RHEL 8/9). The redundant `tasks/Fedora.yml`, `tasks/RedHat8.yml` and `tasks/RedHat9.yml` were removed. * **role:graylog_server**: Update `server.conf` templates to include `telemetry_enabled = false`. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index f9eafa8a8..03aafab28 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -11,7 +11,7 @@ acme_sh | x | x | x | x | x | | x | alternatives | x | x | x | x | | x | x | ansible_init | | | | | | | | Fedora 35+ apache_httpd | x | x | x | x | x | | x | -apache_solr | | | x | x | | | | +apache_solr | | | x | x | x | | | apache_tomcat | | | x | x | | | | apps | | | x | x | x | | | at | | | x | x | | | | Fedora 35 diff --git a/roles/apache_solr/README.md b/roles/apache_solr/README.md index 473e79931..04a276982 100644 --- a/roles/apache_solr/README.md +++ b/roles/apache_solr/README.md @@ -259,6 +259,64 @@ apache_solr__users__host_var: ``` +## Backup and Restore + +The role can deploy a `mariadb-dump`-style backup pipeline for one or more Solr cores. It uses Solr's `replication?command=backup` endpoint, polls `command=details` until the backup status reports `success`, and writes the snapshot under `apache_solr__dump_directory`. On every run the directory is wiped and refreshed; retention is the responsibility of the surrounding backup tool (Borg, Restic, ...) which snapshots that directory. + +### Optional Backup Variables + +`apache_solr__dump_cores` + +* List of cores to back up. Empty disables the timer and stops the existing one. +* Type: List of strings. +* Default: `[]` + +`apache_solr__dump_directory` + +* Where the latest snapshot lands. Owned by `{{ apache_solr__user }}:{{ apache_solr__group }}` so Solr can write into it. +* Type: String. +* Default: `'/backup/apache-solr-dump'` + +`apache_solr__dump_on_calendar` + +* `OnCalendar=` value for `apache-solr-dump.timer`. Default seeds the minute by `inventory_hostname` so a fleet does not all hit Solr at the same second. +* Type: String. +* Default: `'*-*-* 22:{{ 59 | random(seed=inventory_hostname) }}:00'` + +`apache_solr__dump_url` + +* Base URL of the Solr instance the dumper hits. Defaults to localhost on the configured port. Override if Solr listens on a UNIX socket or the loopback alias differs. +* Type: String. +* Default: `'http://127.0.0.1:{{ apache_solr__http_bind_port }}/solr'` + +### Restoring a Core + +1. Stop Solr writes to the target core (route traffic away or stop the service). +2. Identify the snapshot directory written by the dumper: + + ```bash + ls /backup/apache-solr-dump/snapshot./ + ``` + +3. Hit the replication restore endpoint: + + ```bash + curl 'http://127.0.0.1:8983/solr//replication?command=restore&location=/backup/apache-solr-dump&name=' + ``` + +4. Poll until done: + + ```bash + curl 'http://127.0.0.1:8983/solr//replication?command=restorestatus&wt=json' + ``` + + `status` flips to `success` (or `failed`) when the restore completes. + +5. Re-enable traffic. + +For Numishare specifically: the `numishare` core is also reproducible from eXist-db (the Numishare publish pipeline rebuilds the index). The Solr snapshot exists to skip the (potentially long) rebuild during disaster recovery, not as the only authoritative source. + + ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/apache_solr/defaults/main.yml b/roles/apache_solr/defaults/main.yml index 34616963a..cfb741ee5 100644 --- a/roles/apache_solr/defaults/main.yml +++ b/roles/apache_solr/defaults/main.yml @@ -1,5 +1,7 @@ +apache_solr__allow_paths: [] # extra paths Solr is allowed to read/write (-Dsolr.allowPaths). Backup directory is auto-included when apache_solr__dump_cores is non-empty. apache_solr__data_dir: '/var/solr/data' apache_solr__group: 'solr' +apache_solr__heap: '512m' # SOLR_HEAP — keeps Solr's documented default apache_solr__http_bind_address: '0.0.0.0' apache_solr__http_bind_port: 8983 apache_solr__install_dir: '/opt' @@ -7,12 +9,21 @@ apache_solr__log4j_props: '/var/solr/log4j2.xml' apache_solr__log_level: 'INFO' apache_solr__logs_dir: '/var/log/solr' apache_solr__pid_dir: '/var/solr' +apache_solr__security_manager_enabled: true apache_solr__service: 'solr' apache_solr__service_enabled: true apache_solr__stop_wait: 15 apache_solr__user: 'solr' apache_solr__var_dir: '/var/solr' +# Backup (mariadb-dump-style: oneshot service + timer; rm -rf + fresh dump every +# run, retention is the external backup tool's job). Empty `apache_solr__dump_cores` +# disables the timer. +apache_solr__dump_cores: [] +apache_solr__dump_directory: '/backup/apache-solr-dump' +apache_solr__dump_on_calendar: '*-*-* 22:{{ 59 | random(seed=inventory_hostname) }}:00' +apache_solr__dump_url: 'http://127.0.0.1:{{ apache_solr__http_bind_port }}/solr' + apache_solr__roles__role_var: [] apache_solr__roles__dependent_var: [] apache_solr__roles__group_var: [] diff --git a/roles/apache_solr/tasks/main.yml b/roles/apache_solr/tasks/main.yml index c8d8bc695..ea710f351 100644 --- a/roles/apache_solr/tasks/main.yml +++ b/roles/apache_solr/tasks/main.yml @@ -1,3 +1,11 @@ +- name: 'Set platform/version specific variables' + ansible.builtin.import_role: + name: 'shared' + tasks_from: 'platform-variables.yml' + tags: + - 'always' + + - block: - name: 'install {{ __apache_solr__java_package[apache_solr__version.split(".")[0]] }}' @@ -5,14 +13,30 @@ name: '{{ __apache_solr__java_package[apache_solr__version.split(".")[0]] }}' state: 'present' - - name: 'Download and verify Apache Solr {{ apache_solr__version }} from Solr archive (the full binary package, for all operating systems)' - ansible.builtin.get_url: - url: 'https://archive.apache.org/dist/solr/solr/{{ apache_solr__version }}/solr-{{ apache_solr__version }}.tgz' - dest: '/tmp/solr-{{ apache_solr__version }}.tgz' - checksum: '{{ apache_solr__checksum }}' - delegate_to: 'localhost' - check_mode: false # run task even if `--check` is specified - changed_when: false # just gathering info, no actual change + # Try Apache CDN first (fast, but only hosts currently-supported versions); + # fall back to archive.apache.org (slow, archival; serves every version ever + # released). + - name: 'Download and verify Apache Solr {{ apache_solr__version }} (the full binary package, for all operating systems)' + block: + - name: 'Download and verify Apache Solr {{ apache_solr__version }} from Apache CDN' + ansible.builtin.get_url: + url: 'https://dlcdn.apache.org/solr/solr/{{ apache_solr__version }}/solr-{{ apache_solr__version }}.tgz' + dest: '/tmp/solr-{{ apache_solr__version }}.tgz' + checksum: '{{ apache_solr__checksum }}' + delegate_to: 'localhost' + become: false # writes to /tmp/ on the controller; ansible-navigator EE has no sudo + check_mode: false # run task even if `--check` is specified + changed_when: false # just gathering info, no actual change + rescue: + - name: 'Download and verify Apache Solr {{ apache_solr__version }} from Solr archive (fallback)' + ansible.builtin.get_url: + url: 'https://archive.apache.org/dist/solr/solr/{{ apache_solr__version }}/solr-{{ apache_solr__version }}.tgz' + dest: '/tmp/solr-{{ apache_solr__version }}.tgz' + checksum: '{{ apache_solr__checksum }}' + delegate_to: 'localhost' + become: false # writes to /tmp/ on the controller; ansible-navigator EE has no sudo + check_mode: false # run task even if `--check` is specified + changed_when: false # just gathering info, no actual change when: - 'apache_solr__version is version("9.0.0", ">=")' @@ -22,6 +46,7 @@ dest: '/tmp/solr-{{ apache_solr__version }}.tgz' checksum: '{{ apache_solr__checksum }}' delegate_to: 'localhost' + become: false # writes to /tmp/ on the controller; ansible-navigator EE has no sudo check_mode: false # run task even if `--check` is specified changed_when: false # just gathering info, no actual change when: @@ -198,3 +223,78 @@ tags: - 'apache_solr' - 'apache_solr:user' + + +# Backup (mariadb-dump-style): oneshot service + timer wipe and refresh +# /backup/apache-solr-dump/ on each run via Solr's replication?command=backup +# endpoint; an external backup tool snapshots that directory. Disabled when +# `apache_solr__dump_cores` is empty (no cores to back up). +- block: + + # Parent dir owned by root, world-traversable. Without this the implicit + # `mkdir -p` from the leaf task below would inherit the leaf's owner/mode on + # the parent too, making /backup/ unreadable for any other dump pipeline + # (e.g. mariadb-dump, existdb-dump) sharing the same root. + - name: 'mkdir -p {{ apache_solr__dump_directory | dirname }} (multi-tenant parent)' + ansible.builtin.file: + path: '{{ apache_solr__dump_directory | dirname }}' + state: 'directory' + owner: 'root' + group: 'root' + mode: 0o755 + + - name: 'mkdir -p {{ apache_solr__dump_directory }}' + ansible.builtin.file: + path: '{{ apache_solr__dump_directory }}' + state: 'directory' + owner: '{{ apache_solr__user }}' + group: '{{ apache_solr__group }}' + mode: 0o750 + + - name: 'Deploy /usr/local/bin/apache-solr-dump' + ansible.builtin.template: + backup: true + src: 'usr/local/bin/apache-solr-dump.j2' + dest: '/usr/local/bin/apache-solr-dump' + owner: 'root' + group: 'root' + mode: 0o755 + + - name: 'Deploy /etc/apache-solr-dump.conf' + ansible.builtin.template: + backup: true + src: 'etc/apache-solr-dump.conf.j2' + dest: '/etc/apache-solr-dump.conf' + owner: 'root' + group: 'root' + mode: 0o644 + + - name: 'Deploy /etc/systemd/system/apache-solr-dump.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/apache-solr-dump.service.j2' + dest: '/etc/systemd/system/apache-solr-dump.service' + owner: 'root' + group: 'root' + mode: 0o644 + + - name: 'Deploy /etc/systemd/system/apache-solr-dump.timer' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/apache-solr-dump.timer.j2' + dest: '/etc/systemd/system/apache-solr-dump.timer' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__apache_solr__dump_timer_result' + + - name: 'systemctl {{ (apache_solr__dump_cores | length > 0) | ternary("enable --now", "disable --now") }} apache-solr-dump.timer' + ansible.builtin.systemd: + name: 'apache-solr-dump.timer' + enabled: '{{ apache_solr__dump_cores | length > 0 }}' + state: '{{ (apache_solr__dump_cores | length > 0) | ternary("started", "stopped") }}' + daemon_reload: '{{ __apache_solr__dump_timer_result is changed }}' + + tags: + - 'apache_solr' + - 'apache_solr:backup' diff --git a/roles/apache_solr/templates/etc/apache-solr-dump.conf.j2 b/roles/apache_solr/templates/etc/apache-solr-dump.conf.j2 new file mode 100644 index 000000000..4a941c164 --- /dev/null +++ b/roles/apache_solr/templates/etc/apache-solr-dump.conf.j2 @@ -0,0 +1,8 @@ +# {{ ansible_managed }} +# 2026042801 + +BACKUP_DIR={{ apache_solr__dump_directory | quote }} +CORES={{ (apache_solr__dump_cores | join(' ')) | quote }} +SOLR_GROUP={{ apache_solr__group | quote }} +SOLR_URL={{ apache_solr__dump_url | quote }} +SOLR_USER={{ apache_solr__user | quote }} diff --git a/roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.service.j2 b/roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.service.j2 new file mode 100644 index 000000000..8e23c105a --- /dev/null +++ b/roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.service.j2 @@ -0,0 +1,15 @@ +# {{ ansible_managed }} +# 2026042801 + +[Unit] +Description=apache-solr-dump Service +After=solr.service +Requires=solr.service + +[Service] +ExecStart=/usr/local/bin/apache-solr-dump +Type=oneshot +User=root + +[Install] +WantedBy=basic.target diff --git a/roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.timer.j2 b/roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.timer.j2 new file mode 100644 index 000000000..1d1abcdfc --- /dev/null +++ b/roles/apache_solr/templates/etc/systemd/system/apache-solr-dump.timer.j2 @@ -0,0 +1,12 @@ +# {{ ansible_managed }} +# 2026042801 + +[Unit] +Description=apache-solr-dump Timer + +[Timer] +OnCalendar={{ apache_solr__dump_on_calendar }} +Unit=apache-solr-dump.service + +[Install] +WantedBy=timers.target diff --git a/roles/apache_solr/templates/opt/solr/bin/solr.in.sh.j2 b/roles/apache_solr/templates/opt/solr/bin/solr.in.sh.j2 index c0a2c4005..d6e944c5d 100644 --- a/roles/apache_solr/templates/opt/solr/bin/solr.in.sh.j2 +++ b/roles/apache_solr/templates/opt/solr/bin/solr.in.sh.j2 @@ -276,6 +276,10 @@ # Sometimes it may be necessary to place a core or a backup on a different location or a different disk # This parameter lets you specify file system path(s) to explicitly allow. The special value of '*' will allow any path #SOLR_OPTS="$SOLR_OPTS -Dsolr.allowPaths=/mnt/bigdisk,/other/path" +{% set __apache_solr__allow_paths_effective = (apache_solr__allow_paths + ([apache_solr__dump_directory] if apache_solr__dump_cores | length > 0 else [])) | unique %} +{% if __apache_solr__allow_paths_effective | length > 0 %} +SOLR_OPTS="$SOLR_OPTS -Dsolr.allowPaths={{ __apache_solr__allow_paths_effective | join(',') }}" +{% endif %} # Solr can attempt to take a heap dump on out of memory errors. To enable this, uncomment the line setting # SOLR_HEAP_DUMP below. Heap dumps will be saved to SOLR_LOG_DIR/dumps by default. Alternatively, you can specify any @@ -330,7 +334,8 @@ #SOLR_RECOMMENDED_MAX_PROCESSES= #SOLR_RECOMMENDED_OPEN_FILES= #SOLR_REQUESTLOG_ENABLED=true -#SOLR_SECURITY_MANAGER_ENABLED=true +SOLR_HEAP="{{ apache_solr__heap }}" +SOLR_SECURITY_MANAGER_ENABLED={{ apache_solr__security_manager_enabled | string | lower }} #SOLR_SOLRXML_REQUIRED=false #SOLR_SSL_CHECK_PEER_NAME=true #SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION=false diff --git a/roles/apache_solr/templates/usr/local/bin/apache-solr-dump.j2 b/roles/apache_solr/templates/usr/local/bin/apache-solr-dump.j2 new file mode 100644 index 000000000..a8ebe06dd --- /dev/null +++ b/roles/apache_solr/templates/usr/local/bin/apache-solr-dump.j2 @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +# {{ ansible_managed }} +# 2026042801 + +# Snapshot every core listed in /etc/apache-solr-dump.conf via Solr's +# replication?command=backup endpoint. Mirrors the mariadb-dump pattern: rm -rf +# $BACKUP_DIR + mkdir + fresh dump on every run; external backup tool handles +# retention. +# +# Solr's backup is async, so we trigger and then poll +# replication?command=details until status == "success" (or "exception"). Cap +# the wait at 10 minutes per core; raise it for very large indexes. + +set -e + +old_umask=$(umask) +umask 027 + +source /etc/apache-solr-dump.conf + +if [ -z "$CORES" ]; then + echo "apache-solr-dump: CORES is empty, nothing to back up." >&2 + exit 0 +fi + +rm -rf "$BACKUP_DIR" +mkdir -p "$BACKUP_DIR" +chown "$SOLR_USER:$SOLR_GROUP" "$BACKUP_DIR" + +deadline_per_core=600 # seconds + +for core in $CORES; do + echo "apache-solr-dump: backing up core '$core' to $BACKUP_DIR" + + # Trigger async backup. Solr writes to "$BACKUP_DIR/snapshot.$core/". + # On HTTP error print the response body so the failure mode is visible in + # the journal (typical: 400 with "Path X must be relative to SOLR_HOME ..." + # when solr.allowPaths does not include $BACKUP_DIR). + if ! trigger_response=$(curl --fail-with-body --silent --show-error \ + "$SOLR_URL/$core/replication?command=backup&location=$BACKUP_DIR&name=$core"); then + echo "apache-solr-dump: replication?command=backup failed for core '$core':" >&2 + echo "$trigger_response" >&2 + exit 1 + fi + + # Poll until done. + deadline=$(($(date +%s) + deadline_per_core)) + while [ "$(date +%s)" -lt "$deadline" ]; do + details=$(curl --fail --silent --show-error \ + "$SOLR_URL/$core/replication?command=details&wt=json") + + if echo "$details" | grep -q '"status":"success"'; then + break + fi + if echo "$details" | grep -q '"status":"exception"'; then + echo "apache-solr-dump: backup of core '$core' failed:" >&2 + echo "$details" >&2 + exit 1 + fi + sleep 5 + done + + # Final check: if we exited the loop on the deadline, fail loudly. + if ! echo "$details" | grep -q '"status":"success"'; then + echo "apache-solr-dump: backup of core '$core' did not finish within $deadline_per_core seconds." >&2 + exit 1 + fi +done + +umask "$old_umask" diff --git a/roles/apache_solr/vars/RedHat10.yml b/roles/apache_solr/vars/RedHat10.yml new file mode 100644 index 000000000..5b887a2b0 --- /dev/null +++ b/roles/apache_solr/vars/RedHat10.yml @@ -0,0 +1,4 @@ +# RHEL 10 dropped java-17-openjdk-headless from AppStream. Solr 9.x officially +# supports Java 11, 17, and 21, so use java-21-openjdk-headless on EL10. +__apache_solr__java_package: + '9': 'java-21-openjdk-headless' From 87ada3f198d8a5109d1e6532e4e99efa55cc12cf Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:16:24 +0200 Subject: [PATCH 2/9] feat(roles/apache_tomcat): support Tomcat 10.1 on RHEL 10, expose env_xms/xmx/xx defaults * Clone the existing 9.0 templates to a 10.1 set: etc/tomcat/10.1-server.xml.j2 etc/tomcat/10.1-context.xml.j2 etc/tomcat/10.1-logging.properties.j2 etc/tomcat/10.1-tomcat-users.xml.j2 etc/sysconfig/10.1-tomcat.j2 The role's `tomcat__installed_version` lookup picks them up automatically on RHEL 10 (which ships tomcat-10.1.x in AppStream). The cloned configs are byte-compatible enough for our use case; once concrete divergences surface they will be patched in place rather than re-derived from the vendor stock files. EL8/9 deployments using 9.0 are unchanged. * Expose the JVM heap knobs that lived as inline `| d('1024M')` fallbacks inside the sysconfig templates as first-class defaults: apache_tomcat__env_xms (default '1024M') apache_tomcat__env_xmx (default '1024M') apache_tomcat__env_xx (default '+UseParallelGC') User-visible behavior is unchanged; users who want to tune the Tomcat heap from their inventory now have documented variables. Both the 9.0 and 10.1 sysconfig templates drop the inline `| d(...)` and reference the defaults directly. * Mark apache_tomcat as proven on RHEL 10 in COMPATIBILITY.md. --- CHANGELOG.md | 2 + COMPATIBILITY.md | 2 +- roles/apache_tomcat/defaults/main.yml | 5 + .../templates/etc/sysconfig/10.1-tomcat.j2 | 13 ++ .../templates/etc/sysconfig/9.0-tomcat.j2 | 2 +- .../templates/etc/tomcat/10.1-context.xml.j2 | 37 ++++ .../etc/tomcat/10.1-logging.properties.j2 | 90 +++++++++ .../templates/etc/tomcat/10.1-server.xml.j2 | 189 ++++++++++++++++++ .../etc/tomcat/10.1-tomcat-users.xml.j2 | 76 +++++++ 9 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 roles/apache_tomcat/templates/etc/sysconfig/10.1-tomcat.j2 create mode 100644 roles/apache_tomcat/templates/etc/tomcat/10.1-context.xml.j2 create mode 100644 roles/apache_tomcat/templates/etc/tomcat/10.1-logging.properties.j2 create mode 100644 roles/apache_tomcat/templates/etc/tomcat/10.1-server.xml.j2 create mode 100644 roles/apache_tomcat/templates/etc/tomcat/10.1-tomcat-users.xml.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d9a1d50..087f45165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:apache_tomcat**: Add support for Tomcat 10.1 on RHEL 10. New `etc/tomcat/10.1-{server,context,logging.properties,tomcat-users}.xml.j2` and `etc/sysconfig/10.1-tomcat.j2` templates clone the existing 9.0 set; the `tomcat__installed_version` lookup picks them up automatically. Existing 9.0 deployments are unchanged +* **role:apache_tomcat**: Promote `apache_tomcat__env_xms` / `__env_xmx` / `__env_xx` from inline template defaults to first-class entries in `defaults/main.yml` (defaults `'1024M'` / `'1024M'` / `'+UseParallelGC'`). User-visible behavior is unchanged; users who want to tune the Tomcat heap from their inventory now have documented variables instead of having to chase the `| d(...)` fallback inside the `9.0-tomcat.j2` / `10.1-tomcat.j2` sysconfig templates * **role:apache_solr**: Add `apache_solr__allow_paths` to expose `-Dsolr.allowPaths` from the inventory (default `[]`). Required when Solr 9 must read or write outside `solr.solr.home` — e.g. backup destinations that the new dump pipeline auto-injects, or cores whose data lives elsewhere. The empty default keeps the property unset, matching upstream behavior * **role:apache_solr**: Add `apache_solr__heap` (default `'512m'`) so users can size the Solr JVM heap from the inventory. Previously the only knob was the upstream-shipped `#SOLR_HEAP="512m"` comment in `solr.in.sh`, leaving Solr permanently on the 512m default with no role-level override * **role:apache_solr**: Add `apache_solr__security_manager_enabled` (default `true`, matching Solr 9's documented default) so deployments with a `solr.solr.home` outside Solr's permitted paths can disable the Java SecurityManager from the inventory. Required for Numishare, whose Solr core lives under `/opt/numishare/solr-home/` and otherwise hits `access denied ("java.io.FilePermission" ...)` errors diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 03aafab28..939a3d66c 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -12,7 +12,7 @@ alternatives | x | x | x | x | | x | x | ansible_init | | | | | | | | Fedora 35+ apache_httpd | x | x | x | x | x | | x | apache_solr | | | x | x | x | | | -apache_tomcat | | | x | x | | | | +apache_tomcat | | | x | x | x | | | apps | | | x | x | x | | | at | | | x | x | | | | Fedora 35 audit | | | x | x | | | | diff --git a/roles/apache_tomcat/defaults/main.yml b/roles/apache_tomcat/defaults/main.yml index cb580c4e3..bf1f44d3b 100644 --- a/roles/apache_tomcat/defaults/main.yml +++ b/roles/apache_tomcat/defaults/main.yml @@ -14,8 +14,13 @@ apache_tomcat__roles__combined_var: '{{ ( apache_tomcat__context_xml_cache_max_size: 10240 +apache_tomcat__env_xms: '1024M' +apache_tomcat__env_xmx: '1024M' +apache_tomcat__env_xx: '+UseParallelGC' + apache_tomcat__server_xml_connector_compressable_mime_types: 'text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml' apache_tomcat__server_xml_connector_compression: 'on' +apache_tomcat__server_xml_connector_connection_timeout: 20000 apache_tomcat__server_xml_connector_max_threads: 200 apache_tomcat__server_xml_connector_min_spare_threads: 10 apache_tomcat__server_xml_connector_port: 8080 diff --git a/roles/apache_tomcat/templates/etc/sysconfig/10.1-tomcat.j2 b/roles/apache_tomcat/templates/etc/sysconfig/10.1-tomcat.j2 new file mode 100644 index 000000000..699446c19 --- /dev/null +++ b/roles/apache_tomcat/templates/etc/sysconfig/10.1-tomcat.j2 @@ -0,0 +1,13 @@ +# Service-specific configuration file for tomcat. This will be sourced by +# systemd for the default service (tomcat.service) +# If you want to customize named instance, make a similar file +# and name it tomcat@instancename. + +# You will not need to set this, usually. For default service it equals +# CATALINA_HOME. For named service, it equals ${TOMCATS_BASE}${NAME} +#CATALINA_BASE="/usr/share/tomcat" + +# Please take a look at /etc/tomcat/tomcat.conf to have an idea what you +# can override. + +CATALINA_OPTS=-Xms{{ apache_tomcat__env_xms }} -Xmx{{ apache_tomcat__env_xmx }} -server -XX:{{ apache_tomcat__env_xx }} diff --git a/roles/apache_tomcat/templates/etc/sysconfig/9.0-tomcat.j2 b/roles/apache_tomcat/templates/etc/sysconfig/9.0-tomcat.j2 index ab95f8739..699446c19 100644 --- a/roles/apache_tomcat/templates/etc/sysconfig/9.0-tomcat.j2 +++ b/roles/apache_tomcat/templates/etc/sysconfig/9.0-tomcat.j2 @@ -10,4 +10,4 @@ # Please take a look at /etc/tomcat/tomcat.conf to have an idea what you # can override. -CATALINA_OPTS=-Xms{{ apache_tomcat__env_xms | d('1024M') }} -Xmx{{ apache_tomcat__env_xmx | d('1024M') }} -server -XX:{{ apache_tomcat__env_xx | d('+UseParallelGC') }} +CATALINA_OPTS=-Xms{{ apache_tomcat__env_xms }} -Xmx{{ apache_tomcat__env_xmx }} -server -XX:{{ apache_tomcat__env_xx }} diff --git a/roles/apache_tomcat/templates/etc/tomcat/10.1-context.xml.j2 b/roles/apache_tomcat/templates/etc/tomcat/10.1-context.xml.j2 new file mode 100644 index 000000000..5ead83bae --- /dev/null +++ b/roles/apache_tomcat/templates/etc/tomcat/10.1-context.xml.j2 @@ -0,0 +1,37 @@ + + + + + + + + WEB-INF/web.xml + WEB-INF/tomcat-web.xml + ${catalina.base}/conf/web.xml + + + + {% if apache_tomcat__context_xml_cache_max_size is defined and apache_tomcat__context_xml_cache_max_size %} + + {% endif %} + diff --git a/roles/apache_tomcat/templates/etc/tomcat/10.1-logging.properties.j2 b/roles/apache_tomcat/templates/etc/tomcat/10.1-logging.properties.j2 new file mode 100644 index 000000000..7e4e3642c --- /dev/null +++ b/roles/apache_tomcat/templates/etc/tomcat/10.1-logging.properties.j2 @@ -0,0 +1,90 @@ +# {{ ansible_managed }} +# 2025082101 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +1catalina.org.apache.juli.AsyncFileHandler.level = FINE +1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina. +1catalina.org.apache.juli.AsyncFileHandler.maxDays = 90 +1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8 +# disabling rotatable since we use logrotate +1catalina.org.apache.juli.AsyncFileHandler.rotatable = false + +2localhost.org.apache.juli.AsyncFileHandler.level = FINE +2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost. +2localhost.org.apache.juli.AsyncFileHandler.maxDays = 90 +2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8 +# disabling rotatable since we use logrotate +2localhost.org.apache.juli.AsyncFileHandler.rotatable = false + +3manager.org.apache.juli.AsyncFileHandler.level = FINE +3manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +3manager.org.apache.juli.AsyncFileHandler.prefix = manager. +3manager.org.apache.juli.AsyncFileHandler.maxDays = 90 +3manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8 +# disabling rotatable since we use logrotate +3manager.org.apache.juli.AsyncFileHandler.rotatable = false + +4host-manager.org.apache.juli.AsyncFileHandler.level = FINE +4host-manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +4host-manager.org.apache.juli.AsyncFileHandler.prefix = host-manager. +4host-manager.org.apache.juli.AsyncFileHandler.maxDays = 90 +4host-manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8 +# disabling rotatable since we use logrotate +4host-manager.org.apache.juli.AsyncFileHandler.rotatable = false + +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter +java.util.logging.ConsoleHandler.encoding = UTF-8 + + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.AsyncFileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.AsyncFileHandler + +# For example, set the org.apache.catalina.util.LifecycleBase logger to log +# each component that extends LifecycleBase changing state: +#org.apache.catalina.util.LifecycleBase.level = FINE + +# To see debug messages in TldLocationsCache, uncomment the following line: +#org.apache.jasper.compiler.TldLocationsCache.level = FINE + +# To see debug messages for HTTP/2 handling, uncomment the following line: +#org.apache.coyote.http2.level = FINE + +# To see debug messages for WebSocket handling, uncomment the following line: +#org.apache.tomcat.websocket.level = FINE diff --git a/roles/apache_tomcat/templates/etc/tomcat/10.1-server.xml.j2 b/roles/apache_tomcat/templates/etc/tomcat/10.1-server.xml.j2 new file mode 100644 index 000000000..29a18d95b --- /dev/null +++ b/roles/apache_tomcat/templates/etc/tomcat/10.1-server.xml.j2 @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if apache_tomcat__server_xml_ajp_port is defined and apache_tomcat__server_xml_ajp_port %} + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/roles/apache_tomcat/templates/etc/tomcat/10.1-tomcat-users.xml.j2 b/roles/apache_tomcat/templates/etc/tomcat/10.1-tomcat-users.xml.j2 new file mode 100644 index 000000000..26dcaa32c --- /dev/null +++ b/roles/apache_tomcat/templates/etc/tomcat/10.1-tomcat-users.xml.j2 @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + +{% for role in apache_tomcat__roles__combined_var if role['state'] | d('present') != 'absent' %} + +{% endfor %} + +{% for user in apache_tomcat__users__combined_var if user['state'] | d('present') != 'absent' %} + +{% endfor %} + From 43de0029dd53c4e4fee1302bd36c91a778576494 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:16:57 +0200 Subject: [PATCH 3/9] Add roles/existdb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New role to install and operate eXist-db 6.x as a systemd-managed service. Targeted at the Numishare stack but standalone-usable. Install: * Extract upstream exist-distribution--unix.tar.bz2 to /opt/existdb * Create the existdb system user and group * Create /var/lib/existdb/data and /var/log/existdb with the right ownership * Rewrite Jetty HTTP/HTTPS ports off 8080/8443 to existdb__http_port (8888) / existdb__https_port (8444) so eXist-db can coexist with Tomcat or Wildfly on the same host * Redirect log4j2 output to existdb__log_dir (/var/log/existdb) * Force client.properties to 127.0.0.1 — eXist-db's Jetty binds IPv4 only, so on dual-stack hosts the bundled CLI tools (client.sh, backup.sh, restore.sh) hit the obscure "HTTP server returned unexpected status: null" when localhost resolves to ::1 first * Set the admin password on first install only, gated by a marker file (.linuxfabrik-admin-password-set) so re-runs don't clobber a manually rotated password * Drop a systemd unit, daemon-reload on changes, enable & start Backup pipeline (mariadb-dump-style): * /usr/local/bin/existdb-dump (calls bin/backup.sh; uses runuser instead of sudo because systemd's no-TTY oneshot context swallows sudo's stderr; passes -ouri=xmldb:exist://127.0.0.1:/exist/xmlrpc explicitly because bin/backup.sh ignores client.properties at runtime and falls back to the compiled-in default port 8080) * /etc/existdb-dump.conf (sourced by the script) * existdb-dump.service (Type=oneshot, After=existdb.service) * existdb-dump.timer (default 22::00 daily) * Wipe-and-refresh per run; retention is the surrounding backup tool's job * Toggle via existdb__dump_enabled (default true) * Dump directory's parent is created as 0o755 root:root so other dump pipelines (apache-solr-dump, mariadb-dump) sharing /backup/ can still traverse into their own subdirs README documents both the role and the matching bin/restore.sh invocation for disaster recovery. COMPATIBILITY: marked as proven on RHEL 10. --- CHANGELOG.md | 1 + COMPATIBILITY.md | 1 + roles/existdb/README.md | 176 +++++++++++++ roles/existdb/defaults/main.yml | 21 ++ roles/existdb/tasks/main.yml | 233 ++++++++++++++++++ .../templates/etc/existdb-dump.conf.j2 | 11 + .../systemd/system/existdb-dump.service.j2 | 15 ++ .../etc/systemd/system/existdb-dump.timer.j2 | 12 + .../etc/systemd/system/existdb.service.j2 | 18 ++ .../templates/usr/local/bin/existdb-dump.j2 | 54 ++++ 10 files changed, 542 insertions(+) create mode 100644 roles/existdb/README.md create mode 100644 roles/existdb/defaults/main.yml create mode 100644 roles/existdb/tasks/main.yml create mode 100644 roles/existdb/templates/etc/existdb-dump.conf.j2 create mode 100644 roles/existdb/templates/etc/systemd/system/existdb-dump.service.j2 create mode 100644 roles/existdb/templates/etc/systemd/system/existdb-dump.timer.j2 create mode 100644 roles/existdb/templates/etc/systemd/system/existdb.service.j2 create mode 100644 roles/existdb/templates/usr/local/bin/existdb-dump.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 087f45165..f6b8c418a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:existdb**: New role to install and operate eXist-db 6.x. Extracts the upstream `unix.tar.bz2` to `/opt/existdb`, creates the system user/group, shifts the Jetty HTTP/HTTPS ports off `8080`/`8443` (so eXist-db can coexist with Tomcat or Wildfly on the same host), redirects logs to `/var/log/existdb`, sets the admin password on first install only (marker-file gated), points `client.properties` at `127.0.0.1` (eXist-db's Jetty binds IPv4 only), and ships a `mariadb-dump`-style `existdb-dump.service` + `.timer`. README documents the matching `bin/restore.sh` invocation * **role:apache_tomcat**: Add support for Tomcat 10.1 on RHEL 10. New `etc/tomcat/10.1-{server,context,logging.properties,tomcat-users}.xml.j2` and `etc/sysconfig/10.1-tomcat.j2` templates clone the existing 9.0 set; the `tomcat__installed_version` lookup picks them up automatically. Existing 9.0 deployments are unchanged * **role:apache_tomcat**: Promote `apache_tomcat__env_xms` / `__env_xmx` / `__env_xx` from inline template defaults to first-class entries in `defaults/main.yml` (defaults `'1024M'` / `'1024M'` / `'+UseParallelGC'`). User-visible behavior is unchanged; users who want to tune the Tomcat heap from their inventory now have documented variables instead of having to chase the `| d(...)` fallback inside the `9.0-tomcat.j2` / `10.1-tomcat.j2` sysconfig templates * **role:apache_solr**: Add `apache_solr__allow_paths` to expose `-Dsolr.allowPaths` from the inventory (default `[]`). Required when Solr 9 must read or write outside `solr.solr.home` — e.g. backup destinations that the new dump pipeline auto-injects, or cores whose data lives elsewhere. The empty default keeps the property unset, matching upstream behavior diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 939a3d66c..b8e141b89 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -34,6 +34,7 @@ duplicity | | | x | x | x | | | Fe elastic_agent | | | | x | | | x | elastic_agent_fleet_server | | | | x | | | x | elasticsearch | | | x | x | | | x | +existdb | | | | | x | | | exoscale_vm | | | | | | | | Fedora 35+ fail2ban | | | x | x | | | | fangfrisch | | | | x | | | | diff --git a/roles/existdb/README.md b/roles/existdb/README.md new file mode 100644 index 000000000..b0ab9933f --- /dev/null +++ b/roles/existdb/README.md @@ -0,0 +1,176 @@ +# Ansible Role linuxfabrik.lfops.existdb + +This role installs and configures [eXist-db](https://exist-db.org/) — a native XML database — as a systemd-managed service. It is targeted at the Numishare stack but is independent of the other roles in the `setup_numishare` playbook and can be used standalone. + +The role: + +* Installs eXist-db from the upstream `unix.tar.bz2` release tarball into `existdb__install_dir` (default `/opt/existdb`). +* Creates the `existdb` system user and group, plus the data and log directories. +* Rewrites the shipped Jetty/log4j2 configuration so HTTP binds to `existdb__http_port` (default `8888`), HTTPS to `existdb__https_port` (default `8444`), data lives under `existdb__data_dir`, and logs under `existdb__log_dir`. Default ports are shifted off `8080`/`8443` to leave room for an application server (Tomcat, Wildfly). +* Sets the `admin` password on first install only (a marker file under the install dir prevents re-runs from overwriting a manually changed password). +* Drops a systemd unit file and starts the service. +* Optionally deploys a `mariadb-dump`-style backup pipeline (`existdb-dump.service` + `.timer`). + + +## Tags + +`existdb` + +* Installs and configures the whole eXist-db service, plus the backup units. + +`existdb:backup` + +* Limits the run to the backup pipeline (script, conf, service, timer). + + +## Mandatory Role Variables + +None. All variables have defaults; the admin password defaults to `'linuxfabrik'` and **must** be overridden in the inventory for any non-throwaway install. + + +## Optional Role Variables + +`existdb__admin_password` + +* Password for the eXist-db `admin` user. Set on first install only; subsequent runs do not touch it. +* Type: String. +* Default: `'linuxfabrik'` — change this. + +`existdb__data_dir` + +* Where eXist-db stores its index/journal files. +* Type: String. +* Default: `'/var/lib/existdb/data'` + +`existdb__group` + +* System group that owns the install dir and runs the service. +* Type: String. +* Default: `'existdb'` + +`existdb__http_port` + +* Jetty HTTP port. Shifted off the application-server-default `8080` so eXist-db can coexist with Tomcat or Wildfly on the same host. +* Type: Number. +* Default: `8888` + +`existdb__https_port` + +* Jetty HTTPS port. Shifted off `8443` for the same reason. +* Type: Number. +* Default: `8444` + +`existdb__install_dir` + +* Where the tarball is extracted to. +* Type: String. +* Default: `'/opt/existdb'` + +`existdb__java_opts` + +* JVM options passed to the eXist-db service via systemd `Environment=JAVA_OPTS`. +* Type: String. +* Default: `'-XX:+UseG1GC -XX:+UseStringDeduplication -XX:MaxRAMPercentage=75.0'` + +`existdb__log_dir` + +* Where eXist-db's log4j2 writes log files. +* Type: String. +* Default: `'/var/log/existdb'` + +`existdb__service_enabled` + +* Enables or disables the service, analogous to `systemctl enable/disable --now`. +* Type: Bool. +* Default: `true` + +`existdb__user` + +* System user that owns the install dir and runs the service. +* Type: String. +* Default: `'existdb'` + +`existdb__version` + +* Upstream release version. The role downloads `https://github.com/eXist-db/exist/releases/download/eXist-{{ existdb__version }}/exist-distribution-{{ existdb__version }}-unix.tar.bz2`. +* Type: String. +* Default: `'6.4.1'` + + +## Backup and Restore + +The role can deploy a `mariadb-dump`-style backup pipeline. On every run the dumper wipes `existdb__dump_directory` and writes a fresh snapshot using eXist-db's `bin/backup.sh` over XML-RPC to the running instance. Retention is the responsibility of the surrounding backup tool (Borg, Restic, ...) which snapshots that directory. + +### Optional Backup Variables + +`existdb__dump_collection` + +* Collection to back up. `/db` is the eXist-db root and covers everything. +* Type: String. +* Default: `'/db'` + +`existdb__dump_directory` + +* Where the latest snapshot lands. Owned by `{{ existdb__user }}:{{ existdb__group }}`. +* Type: String. +* Default: `'/backup/existdb-dump'` + +`existdb__dump_enabled` + +* Enables or disables the timer. +* Type: Bool. +* Default: `true` + +`existdb__dump_on_calendar` + +* `OnCalendar=` value for `existdb-dump.timer`. Default seeds the minute by `inventory_hostname` so a fleet does not all hit eXist-db at the same second. +* Type: String. +* Default: `'*-*-* 22:{{ 59 | random(seed=inventory_hostname) }}:00'` + +`existdb__dump_password` + +* Password the dumper uses to connect. Defaults to `existdb__admin_password`. +* Type: String. +* Default: `'{{ existdb__admin_password }}'` + +`existdb__dump_user` + +* User the dumper authenticates as. eXist-db has no separate "backup" role, so this typically stays `admin`. +* Type: String. +* Default: `'admin'` + +### Restoring a Backup + +1. The dumper writes a flat tree under `existdb__dump_directory` rooted at the backed-up collection name. With the default `existdb__dump_collection: '/db'` the layout is: + + ``` + /backup/existdb-dump/db/__contents__.xml + /backup/existdb-dump/db//__contents__.xml + ... + ``` + +2. With eXist-db running, replay the backup via `bin/restore.sh`: + + ```bash + runuser -u existdb -- /opt/existdb/bin/restore.sh \ + -u admin \ + -p '' \ + -r /backup/existdb-dump/db/__contents__.xml \ + -ouri=xmldb:exist://127.0.0.1:8888/exist/xmlrpc + ``` + + The path passed to `-r` must be the `__contents__.xml` at the root of the backup tree. The `-ouri=...` (no space, as in the eXist-db docs) is required because `bin/restore.sh` ignores `$EXIST_HOME/etc/client.properties` at runtime and falls back to the compiled-in default `xmldb:exist://localhost:8080/exist/xmlrpc`. On dual-stack hosts `localhost` resolves to `::1` first but eXist-db's Jetty binds IPv4 only, surfacing as `HTTP server returned unexpected status: null`. `runuser` is preferred over `sudo` for the same reason as in the dump script — `sudo`'s PAM session swallows the Java stderr without a TTY. + +3. The restore overwrites collections in place. Verify via the eXist-db admin UI at `http://:{{ existdb__http_port }}/exist/`. + +For full disaster recovery (host loss): re-run the role first to reinstall eXist-db at the same version with the same admin password, then run `restore.sh`. + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/existdb/defaults/main.yml b/roles/existdb/defaults/main.yml new file mode 100644 index 000000000..623f7e527 --- /dev/null +++ b/roles/existdb/defaults/main.yml @@ -0,0 +1,21 @@ +existdb__admin_password: 'linuxfabrik' +existdb__data_dir: '/var/lib/existdb/data' +existdb__group: 'existdb' +existdb__http_port: 8888 +existdb__https_port: 8444 +existdb__install_dir: '/opt/existdb' +existdb__java_opts: '-XX:+UseG1GC -XX:+UseStringDeduplication -XX:MaxRAMPercentage=75.0' +existdb__log_dir: '/var/log/existdb' +existdb__service_enabled: true +existdb__user: 'existdb' +existdb__version: '6.4.1' + +# Backup (mariadb-dump-style: oneshot service + timer; rm -rf + fresh dump every +# run, retention is the external backup tool's job). Set `existdb__dump_enabled` +# to false to skip the timer. +existdb__dump_collection: '/db' +existdb__dump_directory: '/backup/existdb-dump' +existdb__dump_enabled: true +existdb__dump_on_calendar: '*-*-* 22:{{ 59 | random(seed=inventory_hostname) }}:00' +existdb__dump_password: '{{ existdb__admin_password }}' +existdb__dump_user: 'admin' diff --git a/roles/existdb/tasks/main.yml b/roles/existdb/tasks/main.yml new file mode 100644 index 000000000..251181e75 --- /dev/null +++ b/roles/existdb/tasks/main.yml @@ -0,0 +1,233 @@ +- block: + + - name: 'Install bzip2 (needed to extract the eXist-db tarball)' + ansible.builtin.package: + name: + - 'bzip2' + state: 'present' + + - name: 'groupadd --system {{ existdb__group }}' + ansible.builtin.group: + name: '{{ existdb__group }}' + system: true + state: 'present' + + - name: 'useradd --system {{ existdb__user }}' + ansible.builtin.user: + name: '{{ existdb__user }}' + group: '{{ existdb__group }}' + home: '{{ existdb__install_dir }}' + create_home: false # {{ existdb__install_dir }} wird vom Tarball-Move angelegt; sonst kollidiert mv + shell: '/usr/sbin/nologin' + comment: 'eXist-db Service Account' + system: true + state: 'present' + + - name: 'mkdir -p {{ existdb__data_dir }} {{ existdb__log_dir }}' + ansible.builtin.file: + path: '{{ item }}' + state: 'directory' + owner: '{{ existdb__user }}' + group: '{{ existdb__group }}' + mode: 0o750 + loop: + - '{{ existdb__data_dir }}' + - '{{ existdb__log_dir }}' + + - name: 'curl https://github.com/eXist-db/exist/releases/download/eXist-{{ existdb__version }}/exist-distribution-{{ existdb__version }}-unix.tar.bz2 -> /tmp/' + ansible.builtin.get_url: + url: 'https://github.com/eXist-db/exist/releases/download/eXist-{{ existdb__version }}/exist-distribution-{{ existdb__version }}-unix.tar.bz2' + dest: '/tmp/ansible.exist-distribution-{{ existdb__version }}-unix.tar.bz2' + mode: 0o644 + + - name: 'tar -xjf /tmp/ansible.exist-distribution-{{ existdb__version }}-unix.tar.bz2 -C /opt/' + ansible.builtin.unarchive: + src: '/tmp/ansible.exist-distribution-{{ existdb__version }}-unix.tar.bz2' + dest: '/opt/' + remote_src: true + creates: '{{ existdb__install_dir }}' # Endzustand pruefen, nicht den Zwischenpfad - sonst doppelt extrahiert + + - name: 'mv /opt/exist-distribution-{{ existdb__version }} {{ existdb__install_dir }}' + ansible.builtin.command: 'mv /opt/exist-distribution-{{ existdb__version }} {{ existdb__install_dir }}' + args: + creates: '{{ existdb__install_dir }}' + removes: '/opt/exist-distribution-{{ existdb__version }}' + + - name: 'chown -R {{ existdb__user }}:{{ existdb__group }} {{ existdb__install_dir }}' + ansible.builtin.file: + path: '{{ existdb__install_dir }}' + owner: '{{ existdb__user }}' + group: '{{ existdb__group }}' + recurse: true + + # Configure paths and ports. eXist-db ships defaults that collide with Wildfly/Tomcat + # (8080/8443 vs 8888/8444). The role rewrites them in place via lineinfile-with-regex + # so re-runs are idempotent. + + - name: 'configure data dir in {{ existdb__install_dir }}/etc/conf.xml' + ansible.builtin.replace: + path: '{{ existdb__install_dir }}/etc/conf.xml' + regexp: 'files="\.\./data"' + replace: 'files="{{ existdb__data_dir }}"' + + - name: 'configure journal dir in {{ existdb__install_dir }}/etc/conf.xml' + ansible.builtin.replace: + path: '{{ existdb__install_dir }}/etc/conf.xml' + regexp: 'journal-dir="\.\./data"' + replace: 'journal-dir="{{ existdb__data_dir }}"' + + - name: 'set HTTP port to {{ existdb__http_port }} in jetty-http.xml' + ansible.builtin.replace: + path: '{{ existdb__install_dir }}/etc/jetty/jetty-http.xml' + regexp: 'default="8080"' + replace: 'default="{{ existdb__http_port }}"' + + - name: 'set HTTPS port to {{ existdb__https_port }} in jetty-ssl.xml' + ansible.builtin.replace: + path: '{{ existdb__install_dir }}/etc/jetty/jetty-ssl.xml' + regexp: 'default="8443"' + replace: 'default="{{ existdb__https_port }}"' + + - name: 'redirect logs to {{ existdb__log_dir }} in log4j2.xml' + ansible.builtin.replace: + path: '{{ existdb__install_dir }}/etc/log4j2.xml' + regexp: '.*' + replace: '{{ existdb__log_dir }}' + + # Point the bundled CLI tools (client.sh / backup.sh / restore.sh) at the + # configured HTTP port. They read the connection URI from + # $EXIST_HOME/etc/client.properties (default: localhost:8080). We also + # replace `localhost` with `127.0.0.1` because eXist-db's Jetty binds IPv4 + # only — when `localhost` resolves to `::1` first (Rocky 10 default + # gai.conf), apache-xmlrpc returns the cryptic `HTTP server returned + # unexpected status: null` for every CLI call. + - name: 'set client.properties URI to 127.0.0.1:{{ existdb__http_port }}' + ansible.builtin.replace: + path: '{{ existdb__install_dir }}/etc/client.properties' + regexp: '^uri=xmldb:exist://(localhost|127\.0\.0\.1):\d+/exist/xmlrpc' + replace: 'uri=xmldb:exist://127.0.0.1:{{ existdb__http_port }}/exist/xmlrpc' + + # Set the admin password on first install only. The marker prevents re-runs from + # overwriting a manually changed password. + + - name: 'Check whether eXist-db admin password has already been set' + ansible.builtin.stat: + path: '{{ existdb__install_dir }}/.linuxfabrik-admin-password-set' + register: '__existdb__admin_pw_marker' + + - name: 'Set eXist-db admin password' + ansible.builtin.command: >- + {{ existdb__install_dir }}/bin/client.sh + -l -u admin -P "" + -x "sm:passwd('admin', '{{ existdb__admin_password }}')" + become_user: '{{ existdb__user }}' + when: + - 'not __existdb__admin_pw_marker["stat"]["exists"]' + + - name: 'Mark eXist-db admin password as set' + ansible.builtin.file: + path: '{{ existdb__install_dir }}/.linuxfabrik-admin-password-set' + state: 'touch' + owner: '{{ existdb__user }}' + group: '{{ existdb__group }}' + mode: 0o600 + when: + - 'not __existdb__admin_pw_marker["stat"]["exists"]' + + - name: 'Deploy /etc/systemd/system/existdb.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/existdb.service.j2' + dest: '/etc/systemd/system/existdb.service' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__existdb__unit_result' + + - name: 'systemctl daemon-reload' # noqa no-handler + ansible.builtin.systemd: + daemon_reload: true + when: '__existdb__unit_result is changed' + + - name: 'systemctl {{ existdb__service_enabled | bool | ternary("enable", "disable") }} --now existdb.service' + ansible.builtin.systemd: + name: 'existdb.service' + enabled: '{{ existdb__service_enabled }}' + state: '{{ existdb__service_enabled | bool | ternary("started", "stopped") }}' + + tags: + - 'existdb' + + +# Backup (mariadb-dump-style): oneshot service + timer wipe and refresh +# /backup/existdb-dump/ on each run; an external backup tool snapshots that +# directory to provide retention. +- block: + + # Parent dir owned by root, world-traversable. Without this the implicit + # `mkdir -p` from the leaf task below would inherit the leaf's owner/mode on + # the parent too, making /backup/ unreadable for any other dump pipeline + # (e.g. mariadb-dump, apache-solr-dump) sharing the same root. + - name: 'mkdir -p {{ existdb__dump_directory | dirname }} (multi-tenant parent)' + ansible.builtin.file: + path: '{{ existdb__dump_directory | dirname }}' + state: 'directory' + owner: 'root' + group: 'root' + mode: 0o755 + + - name: 'mkdir -p {{ existdb__dump_directory }}' + ansible.builtin.file: + path: '{{ existdb__dump_directory }}' + state: 'directory' + owner: '{{ existdb__user }}' + group: '{{ existdb__group }}' + mode: 0o750 + + - name: 'Deploy /usr/local/bin/existdb-dump' + ansible.builtin.template: + backup: true + src: 'usr/local/bin/existdb-dump.j2' + dest: '/usr/local/bin/existdb-dump' + owner: 'root' + group: 'root' + mode: 0o755 + + - name: 'Deploy /etc/existdb-dump.conf' + ansible.builtin.template: + backup: true + src: 'etc/existdb-dump.conf.j2' + dest: '/etc/existdb-dump.conf' + owner: 'root' + group: 'root' + mode: 0o600 # contains the eXist-db admin password + + - name: 'Deploy /etc/systemd/system/existdb-dump.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/existdb-dump.service.j2' + dest: '/etc/systemd/system/existdb-dump.service' + owner: 'root' + group: 'root' + mode: 0o644 + + - name: 'Deploy /etc/systemd/system/existdb-dump.timer' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/existdb-dump.timer.j2' + dest: '/etc/systemd/system/existdb-dump.timer' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__existdb__dump_timer_result' + + - name: 'systemctl {{ existdb__dump_enabled | bool | ternary("enable --now", "disable --now") }} existdb-dump.timer' + ansible.builtin.systemd: + name: 'existdb-dump.timer' + enabled: '{{ existdb__dump_enabled }}' + state: '{{ existdb__dump_enabled | bool | ternary("started", "stopped") }}' + daemon_reload: '{{ __existdb__dump_timer_result is changed }}' + + tags: + - 'existdb' + - 'existdb:backup' diff --git a/roles/existdb/templates/etc/existdb-dump.conf.j2 b/roles/existdb/templates/etc/existdb-dump.conf.j2 new file mode 100644 index 000000000..fe1a52abe --- /dev/null +++ b/roles/existdb/templates/etc/existdb-dump.conf.j2 @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +# 2026042801 + +BACKUP_DIR={{ existdb__dump_directory | quote }} +COLLECTION={{ existdb__dump_collection | quote }} +EXISTDB_GROUP={{ existdb__group | quote }} +EXISTDB_INSTALL_DIR={{ existdb__install_dir | quote }} +EXISTDB_USER={{ existdb__user | quote }} +PASSWORD={{ existdb__dump_password | quote }} +URI={{ ('xmldb:exist://127.0.0.1:' ~ existdb__http_port ~ '/exist/xmlrpc') | quote }} +USERNAME={{ existdb__dump_user | quote }} diff --git a/roles/existdb/templates/etc/systemd/system/existdb-dump.service.j2 b/roles/existdb/templates/etc/systemd/system/existdb-dump.service.j2 new file mode 100644 index 000000000..fb6c47366 --- /dev/null +++ b/roles/existdb/templates/etc/systemd/system/existdb-dump.service.j2 @@ -0,0 +1,15 @@ +# {{ ansible_managed }} +# 2026042801 + +[Unit] +Description=existdb-dump Service +After=existdb.service +Requires=existdb.service + +[Service] +ExecStart=/usr/local/bin/existdb-dump +Type=oneshot +User=root + +[Install] +WantedBy=basic.target diff --git a/roles/existdb/templates/etc/systemd/system/existdb-dump.timer.j2 b/roles/existdb/templates/etc/systemd/system/existdb-dump.timer.j2 new file mode 100644 index 000000000..a6eb3b459 --- /dev/null +++ b/roles/existdb/templates/etc/systemd/system/existdb-dump.timer.j2 @@ -0,0 +1,12 @@ +# {{ ansible_managed }} +# 2026042801 + +[Unit] +Description=existdb-dump Timer + +[Timer] +OnCalendar={{ existdb__dump_on_calendar }} +Unit=existdb-dump.service + +[Install] +WantedBy=timers.target diff --git a/roles/existdb/templates/etc/systemd/system/existdb.service.j2 b/roles/existdb/templates/etc/systemd/system/existdb.service.j2 new file mode 100644 index 000000000..91156b9c7 --- /dev/null +++ b/roles/existdb/templates/etc/systemd/system/existdb.service.j2 @@ -0,0 +1,18 @@ +# {{ ansible_managed }} +# 2026042801 + +[Unit] +Description=eXist-db Server +Documentation=https://exist-db.org/exist/apps/doc/ +After=syslog.target + +[Service] +Type=simple +User={{ existdb__user }} +Group={{ existdb__group }} +Environment="JAVA_HOME=/usr/lib/jvm/jre" +Environment="JAVA_OPTS={{ existdb__java_opts }}" +ExecStart={{ existdb__install_dir }}/bin/startup.sh + +[Install] +WantedBy=multi-user.target diff --git a/roles/existdb/templates/usr/local/bin/existdb-dump.j2 b/roles/existdb/templates/usr/local/bin/existdb-dump.j2 new file mode 100644 index 000000000..f751a68a2 --- /dev/null +++ b/roles/existdb/templates/usr/local/bin/existdb-dump.j2 @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +# {{ ansible_managed }} +# 2026042801 + +# Online backup of an eXist-db instance via bin/backup.sh. Mirrors the +# mariadb-dump pattern: rm -rf $BACKUP_DIR + mkdir + fresh dump. Retention is +# the external backup tool's job. + +set -e + +old_umask=$(umask) +umask 027 + +source /etc/existdb-dump.conf + +rm -rf "$BACKUP_DIR" +mkdir -p "$BACKUP_DIR" +chown "$EXISTDB_USER:$EXISTDB_GROUP" "$BACKUP_DIR" + +# bin/backup.sh connects via XML-RPC to the running eXist-db instance and +# writes the backup tree under "$BACKUP_DIR/db/" (eXist-db 6.x writes the +# backup tree directly, no `full` prefix). +# +# We pass the connection URI as `-ouri=...` (no space — that is the syntax +# AppassemblerBooter expects). bin/backup.sh ignores +# $EXIST_HOME/etc/client.properties at runtime and otherwise falls back to +# its compiled-in default `xmldb:exist://localhost:8080/exist/xmlrpc`, which +# is wrong on two counts here: port 8080 (we shifted to existdb__http_port) +# and `localhost` resolves to ::1 first on dual-stack hosts but eXist-db's +# Jetty binds IPv4 only — apache-xmlrpc then returns the cryptic "HTTP +# server returned unexpected status: null". +# +# We use `runuser` rather than `sudo`: `sudo` requires PAM session setup +# that systemd's no-TTY oneshot context handles awkwardly, swallowing the +# Java process's stderr and leaving the service exiting `status=1` with no +# diagnostic in the journal. +runuser -u "$EXISTDB_USER" -- \ + "$EXISTDB_INSTALL_DIR/bin/backup.sh" \ + -u "$USERNAME" \ + -p "$PASSWORD" \ + -b "$COLLECTION" \ + -d "$BACKUP_DIR" \ + "-ouri=$URI" +backup_retc=$? + +umask "$old_umask" + +exit "$backup_retc" From 6d6ebeff8bddfb9e6ace031566c53592a1de50e9 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:17:31 +0200 Subject: [PATCH 4/9] Add roles/numishare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New role for Numishare (https://github.com/ewg118/numishare), the open-source numismatic-collection platform. The role wires Numishare into an already-deployed eXist-db / Solr / Tomcat / Orbeon stack rather than managing those services itself. Install: * Install git-core and shallow-clone numishare__git_url to numishare__install_dir (default /opt/numishare). update: false — the checkout is one-time, intentional updates happen out-of-band. * Deploy /opt/numishare/exist-config.xml so Numishare's XPL pipelines know how to authenticate against eXist-db. Mode 0640 (contains the eXist-db admin password in plaintext) and owned by the Tomcat user/group via numishare__app_{user,group}. * Deploy /opt/numishare/solr-home//core.properties and create the /var/solr/data/ -> /opt/numishare/solr-home/ symlink so Solr's core discovery picks up the Numishare core. Both notify the apache_solr restart handler so Solr reloads the core (Solr only scans for cores at startup, so without the notify the core would never load on first deploy). * chown -R numishare__solr_user:numishare__solr_group on the solr-home subtree. Themes: * mkdir -p numishare__themes_dir (default /opt/themes). * Symlink /default -> /ui to expose Numishare's bundled UI as the "default" theme via the same /orbeon/themes// delivery path as custom themes. * Deploy custom themes from inventory via the numishare__themes__* combined-var pattern. Each entry is keyed by `name`; source is either git_url (with optional git_version, git_update) or tarball_url (with optional tarball_strip_components). state: 'absent' removes the theme. README documents the variables and gives examples for both sources. COMPATIBILITY: marked as proven on RHEL 10. --- CHANGELOG.md | 1 + COMPATIBILITY.md | 1 + roles/numishare/README.md | 189 ++++++++++++++++++ roles/numishare/defaults/main.yml | 25 +++ roles/numishare/tasks/main.yml | 141 +++++++++++++ .../opt/numishare/exist-config.xml.j2 | 6 + .../solr/data/numishare/core.properties.j2 | 5 + 7 files changed, 368 insertions(+) create mode 100644 roles/numishare/README.md create mode 100644 roles/numishare/defaults/main.yml create mode 100644 roles/numishare/tasks/main.yml create mode 100644 roles/numishare/templates/opt/numishare/exist-config.xml.j2 create mode 100644 roles/numishare/templates/var/solr/data/numishare/core.properties.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b8c418a..5d9593895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:numishare**: New role for [Numishare](https://github.com/ewg118/numishare), the open-source numismatic-collection platform. Clones the repository, deploys `exist-config.xml`, wires the Solr `numishare` core (notifies a Solr restart so the core is loaded), exposes Numishare's bundled UI as the `default` theme via a `/opt/themes/default` symlink, and supports custom themes via `numishare__themes__*` (combined-var pattern, source can be `git_url` + optional `git_version`/`git_update`, or `tarball_url` + optional `tarball_strip_components`) * **role:existdb**: New role to install and operate eXist-db 6.x. Extracts the upstream `unix.tar.bz2` to `/opt/existdb`, creates the system user/group, shifts the Jetty HTTP/HTTPS ports off `8080`/`8443` (so eXist-db can coexist with Tomcat or Wildfly on the same host), redirects logs to `/var/log/existdb`, sets the admin password on first install only (marker-file gated), points `client.properties` at `127.0.0.1` (eXist-db's Jetty binds IPv4 only), and ships a `mariadb-dump`-style `existdb-dump.service` + `.timer`. README documents the matching `bin/restore.sh` invocation * **role:apache_tomcat**: Add support for Tomcat 10.1 on RHEL 10. New `etc/tomcat/10.1-{server,context,logging.properties,tomcat-users}.xml.j2` and `etc/sysconfig/10.1-tomcat.j2` templates clone the existing 9.0 set; the `tomcat__installed_version` lookup picks them up automatically. Existing 9.0 deployments are unchanged * **role:apache_tomcat**: Promote `apache_tomcat__env_xms` / `__env_xmx` / `__env_xx` from inline template defaults to first-class entries in `defaults/main.yml` (defaults `'1024M'` / `'1024M'` / `'+UseParallelGC'`). User-visible behavior is unchanged; users who want to tune the Tomcat heap from their inventory now have documented variables instead of having to chase the `| d(...)` fallback inside the `9.0-tomcat.j2` / `10.1-tomcat.j2` sysconfig templates diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index b8e141b89..0374acaa8 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -118,6 +118,7 @@ mount | | | x | x | | | | network | - | - | x | x | | | | nextcloud | | | x | x | | | | nfs_client | | | x | x | | | | +numishare | | | | | x | | | nfs_server | | | x | | | | | nodejs | | | x | x | | | | objectstore_backup | | | x | | | | | diff --git a/roles/numishare/README.md b/roles/numishare/README.md new file mode 100644 index 000000000..db81812a5 --- /dev/null +++ b/roles/numishare/README.md @@ -0,0 +1,189 @@ +# Ansible Role linuxfabrik.lfops.numishare + +This role checks out [Numishare](https://github.com/ewg118/numishare) — an open-source platform for managing and publishing numismatic collections — and wires it up against an existing eXist-db (XML database), Apache Solr (search backend) and Apache Tomcat / Orbeon Forms (web layer). + +The role: + +* Installs `git-core` and shallow-clones the Numishare repository into `numishare__install_dir` (default `/opt/numishare`). +* Deploys `exist-config.xml` so Numishare knows how to reach eXist-db. +* Deploys `solr-home//core.properties` so Solr can pick up the Numishare core, and creates the `/` symlink Solr's `solr.solr.home` scanner expects. Notifies a Solr restart so the core is loaded. +* Creates `numishare__themes_dir` (default `/opt/themes`) and exposes Numishare's bundled UI assets as the `default` theme via a symlink. +* Deploys custom themes from git or tarball sources via the `numishare__themes__*` variable family. + +Numishare itself is end-of-life upstream but stable; this role pins to the upstream `main` branch (one-time clone, updates handled out-of-band) and adapts the surrounding integration layer instead. + +This role is intended to be used together with [linuxfabrik.lfops.existdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/existdb), [linuxfabrik.lfops.apache_solr](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_solr), [linuxfabrik.lfops.apache_tomcat](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_tomcat), and [linuxfabrik.lfops.orbeon_forms](https://github.com/Linuxfabrik/lfops/tree/main/roles/orbeon_forms). The [setup_numishare](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_numishare.yml) playbook wires them all up in the right order. + + +## Mandatory Requirements + +* Apache Solr running with `solr.solr.home` set to `numishare__solr_data_dir` — done by [linuxfabrik.lfops.apache_solr](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_solr). +* eXist-db running and reachable at `numishare__exist_url` — done by [linuxfabrik.lfops.existdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/existdb). +* Apache Tomcat installed (creates the `tomcat` user that owns Numishare's config files) — done by [linuxfabrik.lfops.apache_tomcat](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_tomcat). + +If you use the [setup_numishare playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_numishare.yml), this is automatically done for you in the right order. + + +## Tags + +`numishare` + +* Installs `git-core`, clones Numishare, deploys `exist-config.xml`, wires the Solr core, creates `/opt/themes/` plus the `default` theme symlink, and deploys all custom themes listed in `numishare__themes__*`. +* Triggers: solr.service restart on Solr-core changes. + + +## Mandatory Role Variables + +None. All variables have defaults; the eXist-db admin password defaults to `'linuxfabrik'` and **must** be overridden via `numishare__exist_password` (or by setting `existdb__admin_password` and inheriting it) for any non-throwaway install. + + +## Optional Role Variables + +`numishare__app_group` + +* Group that owns Numishare config files (must match the application server user). +* Type: String. +* Default: `'tomcat'` + +`numishare__app_user` + +* User that owns Numishare config files (must match the application server user). +* Type: String. +* Default: `'tomcat'` + +`numishare__exist_password` + +* Password used by Numishare to authenticate against eXist-db. +* Type: String. +* Default: `'linuxfabrik'` — change this. + +`numishare__exist_url` + +* eXist-db REST endpoint Numishare connects to. `127.0.0.1` is preferred over `localhost` because eXist-db's Jetty binds IPv4 only. +* Type: String. +* Default: `'http://127.0.0.1:8888/exist/rest/db/'` + +`numishare__exist_username` + +* User Numishare authenticates as against eXist-db. Typically the eXist-db `admin`. +* Type: String. +* Default: `'admin'` + +`numishare__git_url` + +* Upstream Numishare repository to clone. +* Type: String. +* Default: `'https://github.com/ewg118/numishare.git'` + +`numishare__install_dir` + +* Where Numishare is checked out to. +* Type: String. +* Default: `'/opt/numishare'` + +`numishare__solr_core_name` + +* Name of the Solr core Numishare uses. +* Type: String. +* Default: `'numishare'` + +`numishare__solr_core_version` + +* Numishare's Solr schema version. Determines which `solr-home//` sub-directory is wired up. +* Type: String. +* Default: `'1.7'` + +`numishare__solr_data_dir` + +* Solr's `solr.solr.home`. The role creates `/` as a symlink to `/solr-home//`. +* Type: String. +* Default: `'/var/solr/data'` + +`numishare__solr_group` + +* Group that owns the Solr-related Numishare files. +* Type: String. +* Default: `'solr'` + +`numishare__solr_user` + +* User that owns the Solr-related Numishare files. +* Type: String. +* Default: `'solr'` + +`numishare__themes_dir` + +* Root directory for custom Numishare themes. Each direct subdirectory is a theme. The role auto-creates `/default` as a symlink to Numishare's bundled UI assets. +* Type: String. +* Default: `'/opt/themes'` + +`numishare__themes__host_var` / `numishare__themes__group_var` + +* List of themes to deploy under `numishare__themes_dir`. Each entry is one of two source types: `git_url` (clones the repo) or `tarball_url` (downloads + extracts). +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `name`: + + * Mandatory. Theme directory name under `numishare__themes_dir`. + * Type: String. + + * `state`: + + * Optional. Either `present` or `absent`. `absent` removes `/`. + * Type: String. + * Default: `'present'` + + * `git_url`: + + * Mandatory if the theme is git-sourced. Mutually exclusive with `tarball_url`. + * Type: String. + + * `git_version`: + + * Optional. Branch, tag, or commit to check out. + * Type: String. + * Default: HEAD of the cloned default branch + + * `git_update`: + + * Optional. Whether subsequent runs should `git pull`. + * Type: Bool. + * Default: `false` + + * `tarball_url`: + + * Mandatory if the theme is tarball-sourced. Mutually exclusive with `git_url`. `tar.gz`, `tgz`, `tar.bz2` and `zip` are auto-detected. ZIP requires `unzip` on the target. + * Type: String. + + * `tarball_strip_components`: + + * Optional. `--strip-components=N` for the unarchive step. Use `0` for tarballs without a wrapping top-level directory. + * Type: Number. + * Default: `1` + +Example: + +```yaml +numishare__themes__host_var: + - name: 'example' + git_url: 'https://git.example.com/themes/numishare-theme-example.git' + git_version: 'main' + state: 'present' + - name: 'example-tarball' + tarball_url: 'https://artifacts.example.com/numishare-theme-example-tarball-1.2.0.tar.gz' + state: 'present' + - name: 'example-old' + state: 'absent' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/numishare/defaults/main.yml b/roles/numishare/defaults/main.yml new file mode 100644 index 000000000..38fd56a2f --- /dev/null +++ b/roles/numishare/defaults/main.yml @@ -0,0 +1,25 @@ +numishare__app_group: 'tomcat' +numishare__app_user: 'tomcat' +numishare__exist_password: 'linuxfabrik' +numishare__exist_url: 'http://127.0.0.1:8888/exist/rest/db/' +numishare__exist_username: 'admin' +numishare__git_url: 'https://github.com/ewg118/numishare.git' +numishare__install_dir: '/opt/numishare' +numishare__solr_core_version: '1.7' +numishare__solr_core_name: 'numishare' +numishare__solr_data_dir: '/var/solr/data' +numishare__solr_group: 'solr' +numishare__solr_user: 'solr' +numishare__themes_dir: '/opt/themes' + +numishare__themes__role_var: [] +numishare__themes__dependent_var: [] +numishare__themes__group_var: [] +numishare__themes__host_var: [] +numishare__themes__combined_var: '{{ ( + numishare__themes__role_var + + numishare__themes__dependent_var + + numishare__themes__group_var + + numishare__themes__host_var + ) | linuxfabrik.lfops.combine_lod(unique_key="name") + }}' diff --git a/roles/numishare/tasks/main.yml b/roles/numishare/tasks/main.yml new file mode 100644 index 000000000..71d143c0f --- /dev/null +++ b/roles/numishare/tasks/main.yml @@ -0,0 +1,141 @@ +- block: + + - name: 'Install git-core' + ansible.builtin.package: + name: + - 'git-core' + state: 'present' + + - name: 'git clone --depth 1 {{ numishare__git_url }} {{ numishare__install_dir }}' + ansible.builtin.git: + repo: '{{ numishare__git_url }}' + dest: '{{ numishare__install_dir }}' + depth: 1 + update: false # Numishare is checked out once; updates handled out-of-band + + - name: 'Deploy {{ numishare__install_dir }}/exist-config.xml' + ansible.builtin.template: + backup: true + src: 'opt/numishare/exist-config.xml.j2' + dest: '{{ numishare__install_dir }}/exist-config.xml' + owner: '{{ numishare__app_user }}' + group: '{{ numishare__app_group }}' + mode: 0o640 # contains the eXist-db admin password in plaintext + + - name: 'Deploy {{ numishare__install_dir }}/solr-home/{{ numishare__solr_core_version }}/core.properties' + ansible.builtin.template: + backup: true + src: 'var/solr/data/numishare/core.properties.j2' + dest: '{{ numishare__install_dir }}/solr-home/{{ numishare__solr_core_version }}/core.properties' + owner: '{{ numishare__solr_user }}' + group: '{{ numishare__solr_group }}' + mode: 0o644 + notify: 'apache_solr: restart solr.service' + + - name: 'Symlink {{ numishare__solr_data_dir }}/{{ numishare__solr_core_name }} -> {{ numishare__install_dir }}/solr-home/{{ numishare__solr_core_version }}/' + ansible.builtin.file: + src: '{{ numishare__install_dir }}/solr-home/{{ numishare__solr_core_version }}/' + dest: '{{ numishare__solr_data_dir }}/{{ numishare__solr_core_name }}' + owner: '{{ numishare__solr_user }}' + group: '{{ numishare__solr_group }}' + state: 'link' + force: true + notify: 'apache_solr: restart solr.service' + + - name: 'chown -R {{ numishare__solr_user }}:{{ numishare__solr_group }} {{ numishare__install_dir }}/solr-home/{{ numishare__solr_core_version }}/' + ansible.builtin.file: + path: '{{ numishare__install_dir }}/solr-home/{{ numishare__solr_core_version }}/' + owner: '{{ numishare__solr_user }}' + group: '{{ numishare__solr_group }}' + recurse: true + + - name: 'mkdir -p {{ numishare__themes_dir }}' + ansible.builtin.file: + path: '{{ numishare__themes_dir }}' + state: 'directory' + owner: 'root' + group: 'root' + mode: 0o755 + + # Numishare's built-in "default" theme ships inside the repo under ui/, not + # under /opt/themes/. Expose it as `default` so that /orbeon/themes/default/... + # resolves through the same delivery path as custom themes. + - name: 'Symlink {{ numishare__themes_dir }}/default -> {{ numishare__install_dir }}/ui' + ansible.builtin.file: + src: '{{ numishare__install_dir }}/ui' + dest: '{{ numishare__themes_dir }}/default' + state: 'link' + force: true + + # ---------------------------------------------------------------------------- + # Custom themes — deployed from git_url or tarball_url. Each theme lands in + # {{ numishare__themes_dir }}// and is automatically picked up by + # Numishare's directory-scanner (Discovery) and the Orbeon `/themes/*` mapping. + # ---------------------------------------------------------------------------- + + - name: 'Combined Themes:' + ansible.builtin.debug: + var: 'numishare__themes__combined_var' + + - name: 'Remove absent themes from {{ numishare__themes_dir }}/' + ansible.builtin.file: + path: '{{ numishare__themes_dir }}/{{ item["name"] }}' + state: 'absent' + loop: '{{ numishare__themes__combined_var + | selectattr("state", "defined") + | selectattr("state", "eq", "absent") + | list }}' + loop_control: + label: '{{ item["name"] }}' + + - name: 'git clone present themes (git_url)' + ansible.builtin.git: + repo: '{{ item["git_url"] }}' + dest: '{{ numishare__themes_dir }}/{{ item["name"] }}' + version: '{{ item["git_version"] | d(omit) }}' + update: '{{ item["git_update"] | d(false) }}' + loop: '{{ (numishare__themes__combined_var | selectattr("state", "undefined") | selectattr("git_url", "defined") | list) + + (numishare__themes__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | selectattr("git_url", "defined") | list) + }}' + loop_control: + label: '{{ item["name"] }}' + + - name: 'mkdir -p {{ numishare__themes_dir }}/ for tarball-based themes' + ansible.builtin.file: + path: '{{ numishare__themes_dir }}/{{ item["name"] }}' + state: 'directory' + owner: 'root' + group: 'root' + mode: 0o755 + loop: '{{ (numishare__themes__combined_var | selectattr("state", "undefined") | selectattr("tarball_url", "defined") | list) + + (numishare__themes__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | selectattr("tarball_url", "defined") | list) + }}' + loop_control: + label: '{{ item["name"] }}' + + - name: 'Download tarball-based themes to /tmp/' + ansible.builtin.get_url: + url: '{{ item["tarball_url"] }}' + dest: '/tmp/ansible-numishare-theme-{{ item["name"] }}.archive' + mode: 0o644 + loop: '{{ (numishare__themes__combined_var | selectattr("state", "undefined") | selectattr("tarball_url", "defined") | list) + + (numishare__themes__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | selectattr("tarball_url", "defined") | list) + }}' + loop_control: + label: '{{ item["name"] }}' + + - name: 'Extract tarball-based themes into {{ numishare__themes_dir }}//' + ansible.builtin.unarchive: + src: '/tmp/ansible-numishare-theme-{{ item["name"] }}.archive' + dest: '{{ numishare__themes_dir }}/{{ item["name"] }}/' + remote_src: true + extra_opts: + - '--strip-components={{ item["tarball_strip_components"] | d(1) }}' + loop: '{{ (numishare__themes__combined_var | selectattr("state", "undefined") | selectattr("tarball_url", "defined") | list) + + (numishare__themes__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | selectattr("tarball_url", "defined") | list) + }}' + loop_control: + label: '{{ item["name"] }}' + + tags: + - 'numishare' diff --git a/roles/numishare/templates/opt/numishare/exist-config.xml.j2 b/roles/numishare/templates/opt/numishare/exist-config.xml.j2 new file mode 100644 index 000000000..12c055fc9 --- /dev/null +++ b/roles/numishare/templates/opt/numishare/exist-config.xml.j2 @@ -0,0 +1,6 @@ + + + {{ numishare__exist_username }} + {{ numishare__exist_password }} + {{ numishare__exist_url }} + diff --git a/roles/numishare/templates/var/solr/data/numishare/core.properties.j2 b/roles/numishare/templates/var/solr/data/numishare/core.properties.j2 new file mode 100644 index 000000000..64f9453cd --- /dev/null +++ b/roles/numishare/templates/var/solr/data/numishare/core.properties.j2 @@ -0,0 +1,5 @@ +# {{ ansible_managed }} +name={{ numishare__solr_core_name }} +config=solrconfig.xml +schema=schema.xml +dataDir=data From a9a817c40fc1bcad42cbaf31b217deb67e2a7c7d Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:18:11 +0200 Subject: [PATCH 5/9] Add roles/orbeon_forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New role to deploy Orbeon Forms (https://www.orbeon.com/) Community Edition into an already-deployed Tomcat for the Numishare stack. Heavily wired to Numishare's expectations rather than a generic Orbeon installer. Deployment: * Install unzip, fetch orbeon_forms__zip_url, extract orbeon.war from the CE zip, and unarchive it exploded into orbeon_forms__home (/var/lib/tomcat/webapps/orbeon). * Deploy /Catalina/localhost/orbeon.xml with . The `path` attribute is intentionally omitted — Tomcat ignores it for context descriptors and emits a warning during startup. Log path fix (RHEL 10 Tomcat): * Orbeon's bundled WEB-INF/resources/config/log4j2.xml references log files via the relative path `../logs/orbeon.log`. With CATALINA_BASE on /usr/share/tomcat (RHEL 10), this resolves to /usr/share/logs/, which the tomcat user cannot create. Every FileAppender silently fails to initialize, Orbeon's diagnostics disappear, and the only surface is a generic "Page Not Found" with no log trail. The role rewrites the prefix to orbeon_forms__log_dir (default /var/log/tomcat). Numishare wiring: * Symlink orbeon_forms__numishare_dir -> WEB-INF/resources/apps/numishare. * Copy /vendor/exist-xqj-api-1.0.1/*.jar into WEB-INF/lib/. * Place a Numishare-branded favicon at WEB-INF/resources/ops/images/ orbeon-icon-16.{ico,png}. Numishare's xforms templates hardcode that path; Orbeon 2023.1 removed the file from the WAR (the entire WEB-INF/resources/ops/ tree is gone), so without this every Numishare page emits a 404 for the favicon. * Copy properties-local.xml.template -> properties-local.xml (idempotent via force: false) and inject a Numishare block via blockinfile markers (oxf.epilogue.theme, oxf.fr.authentication.method=container, container roles). * Replace the shipped with the configured BASIC or FORM auth block (orbeon_forms__auth_method). * Replace the shipped with orbeon_forms__session_timeout (default 720 minutes; Numishare's admin forms benefit from a longer timeout). * Inject a Numishare block before (security-constraint for /numishare/admin/*, security-roles, /themes/* servlet-mapping for the Tomcat default servlet). Themes: * Symlink orbeon_forms__themes_dir (/opt/themes) into both the discovery path (apps/themes) and the delivery path (orbeon_home/themes), matching the structure the numishare role's apps/themes/default symlink expects. Final chown -R orbeon_forms__tomcat_user:orbeon_forms__tomcat_group on the deployment, then unconditional `systemctl restart tomcat.service`. orbeon_forms__roles default ships only `numishare-admin` (the universal Numishare role); per-collection container roles are added via the inventory. COMPATIBILITY: marked as proven on RHEL 10. --- CHANGELOG.md | 1 + COMPATIBILITY.md | 1 + roles/orbeon_forms/README.md | 133 ++++++++++++ roles/orbeon_forms/defaults/main.yml | 18 ++ roles/orbeon_forms/tasks/main.yml | 203 ++++++++++++++++++ .../conf/Catalina/localhost/orbeon.xml.j2 | 8 + .../templates/web-inf/login-config.xml.j2 | 10 + .../config/properties-local-numishare.xml.j2 | 14 ++ .../templates/web-inf/web-numishare.xml.j2 | 25 +++ 9 files changed, 413 insertions(+) create mode 100644 roles/orbeon_forms/README.md create mode 100644 roles/orbeon_forms/defaults/main.yml create mode 100644 roles/orbeon_forms/tasks/main.yml create mode 100644 roles/orbeon_forms/templates/usr/share/tomcat/conf/Catalina/localhost/orbeon.xml.j2 create mode 100644 roles/orbeon_forms/templates/web-inf/login-config.xml.j2 create mode 100644 roles/orbeon_forms/templates/web-inf/resources/config/properties-local-numishare.xml.j2 create mode 100644 roles/orbeon_forms/templates/web-inf/web-numishare.xml.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d9593895..c725fd096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:orbeon_forms**: New role to deploy [Orbeon Forms](https://www.orbeon.com/) Community Edition into an existing Tomcat for the Numishare stack. Extracts the WAR exploded, rewrites `log4j2.xml` to absolute log paths under `/var/log/tomcat` (Orbeon's relative `../logs/` resolves to `/usr/share/logs/` on RHEL 10 Tomcat and silently disables every FileAppender), deploys `Catalina/localhost/orbeon.xml` with `allowLinking="true"`, wires Numishare's `apps/` symlink and XQJ libraries, ships a Numishare-branded favicon at the legacy `/ops/images/orbeon-icon-16.{ico,png}` path (Orbeon 2023.1 dropped the upstream file), and injects managed Numishare blocks into `properties-local.xml` and `web.xml` (epilogue theme, container auth, security-constraint for `/numishare/admin/*`, `/themes/*` default-servlet mapping, longer session timeout) * **role:numishare**: New role for [Numishare](https://github.com/ewg118/numishare), the open-source numismatic-collection platform. Clones the repository, deploys `exist-config.xml`, wires the Solr `numishare` core (notifies a Solr restart so the core is loaded), exposes Numishare's bundled UI as the `default` theme via a `/opt/themes/default` symlink, and supports custom themes via `numishare__themes__*` (combined-var pattern, source can be `git_url` + optional `git_version`/`git_update`, or `tarball_url` + optional `tarball_strip_components`) * **role:existdb**: New role to install and operate eXist-db 6.x. Extracts the upstream `unix.tar.bz2` to `/opt/existdb`, creates the system user/group, shifts the Jetty HTTP/HTTPS ports off `8080`/`8443` (so eXist-db can coexist with Tomcat or Wildfly on the same host), redirects logs to `/var/log/existdb`, sets the admin password on first install only (marker-file gated), points `client.properties` at `127.0.0.1` (eXist-db's Jetty binds IPv4 only), and ships a `mariadb-dump`-style `existdb-dump.service` + `.timer`. README documents the matching `bin/restore.sh` invocation * **role:apache_tomcat**: Add support for Tomcat 10.1 on RHEL 10. New `etc/tomcat/10.1-{server,context,logging.properties,tomcat-users}.xml.j2` and `etc/sysconfig/10.1-tomcat.j2` templates clone the existing 9.0 set; the `tomcat__installed_version` lookup picks them up automatically. Existing 9.0 deployments are unchanged diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 0374acaa8..1c6bcdca1 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -123,6 +123,7 @@ nfs_server | | | x | | | | | nodejs | | | x | x | | | | objectstore_backup | | | x | | | | | opensearch | | | x | | | | | +orbeon_forms | | | | | x | | | open_vm_tools | | | x | x | | | | openvpn_server | | | x | x | x | | | php | x | x | x | x | | | | diff --git a/roles/orbeon_forms/README.md b/roles/orbeon_forms/README.md new file mode 100644 index 000000000..f3f57f8eb --- /dev/null +++ b/roles/orbeon_forms/README.md @@ -0,0 +1,133 @@ +# Ansible Role linuxfabrik.lfops.orbeon_forms + +This role deploys [Orbeon Forms](https://www.orbeon.com/) (Community Edition) into an existing Apache Tomcat and wires it up specifically for the Numishare stack: Numishare's `apps/` symlink, eXist-db XQJ libraries, container-managed authentication for Numishare's roles, the `/themes/*` servlet mapping, and the discovery + delivery symlinks against `/opt/themes`. + +The role: + +* Downloads the Orbeon CE release zip from `orbeon_forms__zip_url` and extracts `orbeon.war` exploded into `orbeon_forms__home` (default `/var/lib/tomcat/webapps/orbeon`). Tomcat then picks it up as the `/orbeon` webapp. +* Rewrites `WEB-INF/resources/config/log4j2.xml` to absolute log paths under `orbeon_forms__log_dir` (default `/var/log/tomcat`). Orbeon's relative `../logs/` paths resolve to `/usr/share/logs/` on RHEL 10 Tomcat, where the `tomcat` user can not create files; the FileAppenders silently fail and surface only as generic "Page Not Found" responses. +* Deploys `Catalina/localhost/orbeon.xml` with `allowLinking="true"` so Tomcat follows the symlinks the role creates. The `path` attribute is intentionally omitted (Tomcat ignores it for context descriptors and emits a warning). +* Symlinks `numishare` → `/WEB-INF/resources/apps/numishare` and copies the eXist-db XQJ libraries from `/vendor/exist-xqj-api-1.0.1/*.jar` into `/WEB-INF/lib/`. +* Provides a Numishare-branded favicon at `/WEB-INF/resources/ops/images/orbeon-icon-16.{ico,png}` (Numishare's xforms templates hardcode that path; Orbeon 2023.1 dropped the file from the WAR). +* Copies `properties-local.xml.template` to `properties-local.xml` and injects a managed Numishare block via `blockinfile` markers (epilogue theme, container auth method and roles). +* Replaces Orbeon's shipped `` with a configurable BASIC or FORM auth block, replaces `` with `orbeon_forms__session_timeout`, and injects a managed Numishare block before `` (security-constraint for `/numishare/admin/*`, security-roles, and the `/themes/*` default-servlet mapping). +* Wires `orbeon_forms__themes_dir` (default `/opt/themes`) into Orbeon as both the discovery path (`/WEB-INF/resources/apps/themes`) and the delivery path (`/themes`). +* Recursively `chown`s `` to the Tomcat user/group and restarts Tomcat. + + +## Mandatory Requirements + +* Apache Tomcat installed and running, with the `tomcat` user/group present — done by [linuxfabrik.lfops.apache_tomcat](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_tomcat). +* Numishare checked out at `orbeon_forms__numishare_dir` — done by [linuxfabrik.lfops.numishare](https://github.com/Linuxfabrik/lfops/tree/main/roles/numishare). +* Container roles configured in `apache_tomcat__roles__*` and matching `apache_tomcat__users__*` so the BASIC/FORM-auth login against `/numishare/admin/*` works. + +If you use the [setup_numishare playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_numishare.yml), Numishare and Tomcat are deployed in the right order before this role runs. + + +## Tags + +`orbeon_forms` + +* Installs `unzip`, deploys the WAR, fixes the log paths, deploys `orbeon.xml`, wires Numishare and themes, injects properties / web.xml blocks, and restarts Tomcat. + + +## Mandatory Role Variables + +None. All variables have defaults that match the Numishare stack layout. + + +## Optional Role Variables + +`orbeon_forms__auth_method` + +* HTTP authentication method written into the Orbeon `web.xml`. +* Type: String. One of `'BASIC'` or `'FORM'`. +* Default: `'BASIC'` + +`orbeon_forms__form_error_page` + +* Login-failure page when `auth_method == 'FORM'`. Numishare ships `/numishare/login-failed`. +* Type: String. +* Default: `'/numishare/login-failed'` + +`orbeon_forms__form_login_page` + +* Login page when `auth_method == 'FORM'`. Numishare ships `/numishare/login`. +* Type: String. +* Default: `'/numishare/login'` + +`orbeon_forms__home` + +* Exploded-WAR deployment directory. Tomcat must scan this path for webapps. +* Type: String. +* Default: `'/var/lib/tomcat/webapps/orbeon'` + +`orbeon_forms__log_dir` + +* Absolute path that replaces Orbeon's shipped `../logs/` references in `log4j2.xml`. Must be writable by the Tomcat user; the default matches the Tomcat-package convention so the role's logrotate config covers Orbeon logs as well. +* Type: String. +* Default: `'/var/log/tomcat'` + +`orbeon_forms__numishare_dir` + +* Where Numishare is checked out. Must match `numishare__install_dir`. +* Type: String. +* Default: `'/opt/numishare'` + +`orbeon_forms__roles` + +* Container roles allowed to access `/numishare/admin/*`. Each entry must have a matching `apache_tomcat__roles__*` and `apache_tomcat__users__*` entry so login actually works. The default ships only the universal `numishare-admin` role; per-collection roles are added via the inventory. +* Type: List of strings. +* Default: `['numishare-admin']` + +`orbeon_forms__session_timeout` + +* Replaces Orbeon's shipped `` value. Numishare's admin forms benefit from a higher timeout because configuration runs are long. +* Type: Number (minutes). +* Default: `720` + +`orbeon_forms__themes_dir` + +* Theme root directory exposed as the `apps/themes` discovery path and the `/themes` delivery path. +* Type: String. +* Default: `'/opt/themes'` + +`orbeon_forms__tomcat_conf_dir` + +* Tomcat configuration root. The role writes `/Catalina/localhost/orbeon.xml`. +* Type: String. +* Default: `'/usr/share/tomcat/conf'` + +`orbeon_forms__tomcat_group` + +* Group that owns Orbeon's deployment files. Must match the Tomcat group. +* Type: String. +* Default: `'tomcat'` + +`orbeon_forms__tomcat_user` + +* User that owns Orbeon's deployment files. Must match the Tomcat user. +* Type: String. +* Default: `'tomcat'` + +`orbeon_forms__version` + +* Orbeon CE version. Used to construct the download URL and identify the local copy under `/tmp`. +* Type: String. +* Default: `'2023.1.202312312000'` + +`orbeon_forms__zip_url` + +* Direct download URL of the Orbeon CE zip. The role unpacks `orbeon.war` from inside the zip and explodes it. +* Type: String. +* Default: `'https://github.com/orbeon/orbeon-forms/releases/download/tag-release-2023.1-ce/orbeon-2023.1.202312312000-CE.zip'` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/orbeon_forms/defaults/main.yml b/roles/orbeon_forms/defaults/main.yml new file mode 100644 index 000000000..9adbcb874 --- /dev/null +++ b/roles/orbeon_forms/defaults/main.yml @@ -0,0 +1,18 @@ +orbeon_forms__auth_method: 'BASIC' # 'BASIC' or 'FORM' +orbeon_forms__form_error_page: '/numishare/login-failed' +orbeon_forms__form_login_page: '/numishare/login' +orbeon_forms__home: '/var/lib/tomcat/webapps/orbeon' +orbeon_forms__log_dir: '/var/log/tomcat' +orbeon_forms__numishare_dir: '/opt/numishare' +orbeon_forms__themes_dir: '/opt/themes' +orbeon_forms__tomcat_conf_dir: '/usr/share/tomcat/conf' +orbeon_forms__tomcat_group: 'tomcat' +orbeon_forms__tomcat_user: 'tomcat' +orbeon_forms__version: '2023.1.202312312000' +orbeon_forms__zip_url: 'https://github.com/orbeon/orbeon-forms/releases/download/tag-release-2023.1-ce/orbeon-2023.1.202312312000-CE.zip' +# Container roles allowed to access /numishare/admin/*. Map 1:1 to +# apache_tomcat__roles__host_var. The default ships only the universal +# `numishare-admin` role; per-collection roles are added via the inventory. +orbeon_forms__roles: + - 'numishare-admin' +orbeon_forms__session_timeout: 720 diff --git a/roles/orbeon_forms/tasks/main.yml b/roles/orbeon_forms/tasks/main.yml new file mode 100644 index 000000000..ef803de18 --- /dev/null +++ b/roles/orbeon_forms/tasks/main.yml @@ -0,0 +1,203 @@ +- block: + + - name: 'Install unzip' + ansible.builtin.package: + name: + - 'unzip' + state: 'present' + + - name: 'Download Orbeon Forms zip to /tmp/' + ansible.builtin.get_url: + url: '{{ orbeon_forms__zip_url }}' + dest: '/tmp/ansible.orbeon-{{ orbeon_forms__version }}-CE.zip' + mode: 0o644 + + - name: 'Extract orbeon.war from the Orbeon zip into /tmp/' + ansible.builtin.command: >- + unzip -j -o + /tmp/ansible.orbeon-{{ orbeon_forms__version }}-CE.zip + */orbeon.war + -d /tmp/ + args: + creates: '/tmp/orbeon.war' + + - name: 'mkdir -p {{ orbeon_forms__home }}' + ansible.builtin.file: + path: '{{ orbeon_forms__home }}' + state: 'directory' + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o755 + + - name: 'Explode orbeon.war into {{ orbeon_forms__home }}' + ansible.builtin.unarchive: + src: '/tmp/orbeon.war' + dest: '{{ orbeon_forms__home }}/' + remote_src: true + creates: '{{ orbeon_forms__home }}/WEB-INF/web.xml' + + # Orbeon's log4j2.xml references log files via the relative path "../logs/orbeon.log", + # which is meant to resolve to $CATALINA_BASE/logs. On RHEL Tomcat 10.1 the working + # directory at log4j init is /usr/share/tomcat, so the relative path lands in + # /usr/share/logs/ — a path the tomcat user can't create. Every FileAppender fails + # and Orbeon's diagnostic output disappears, surfacing only as generic "Page Not Found" + # responses. Rewriting the prefix to an absolute log dir fixes both. (The rst guide + # documents the same workaround for Wildfly; on RHEL 10 Tomcat needs it too.) + - name: 'Rewrite Orbeon log4j2.xml relative paths to absolute (../logs/ -> {{ orbeon_forms__log_dir }}/)' + ansible.builtin.replace: + path: '{{ orbeon_forms__home }}/WEB-INF/resources/config/log4j2.xml' + regexp: '\.\./logs/' + replace: '{{ orbeon_forms__log_dir }}/' + + - name: 'Deploy {{ orbeon_forms__tomcat_conf_dir }}/Catalina/localhost/orbeon.xml' + ansible.builtin.template: + backup: true + src: 'usr/share/tomcat/conf/Catalina/localhost/orbeon.xml.j2' + dest: '{{ orbeon_forms__tomcat_conf_dir }}/Catalina/localhost/orbeon.xml' + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o644 + + - name: 'Symlink {{ orbeon_forms__numishare_dir }} -> {{ orbeon_forms__home }}/WEB-INF/resources/apps/numishare' + ansible.builtin.file: + src: '{{ orbeon_forms__numishare_dir }}' + dest: '{{ orbeon_forms__home }}/WEB-INF/resources/apps/numishare' + state: 'link' + force: true + + # Numishare's xhtml templates hardcode `/ops/images/orbeon-icon-16.{ico,png}`, + # a path Orbeon shipped in older versions but dropped in 2023.1 (the WAR no + # longer carries `WEB-INF/resources/ops/` at all). Wire Numishare's own favicon + # to the expected path so the references stop 404'ing without patching Numishare. + - name: 'mkdir -p {{ orbeon_forms__home }}/WEB-INF/resources/ops/images' + ansible.builtin.file: + path: '{{ orbeon_forms__home }}/WEB-INF/resources/ops/images' + state: 'directory' + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o755 + + - name: 'Provide Numishare favicon at the path the xforms templates expect (/ops/images/orbeon-icon-16.{ico,png})' + ansible.builtin.copy: + src: '{{ orbeon_forms__numishare_dir }}/ui/images/favicon.{{ item["ext"] }}' + dest: '{{ orbeon_forms__home }}/WEB-INF/resources/ops/images/orbeon-icon-16.{{ item["ext"] }}' + remote_src: true + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o644 + loop: + - {ext: 'ico'} + - {ext: 'png'} + loop_control: + label: '{{ item["ext"] }}' + + - name: 'Find eXist-db XQJ-API jars in {{ orbeon_forms__numishare_dir }}/vendor/' + ansible.builtin.find: + paths: '{{ orbeon_forms__numishare_dir }}/vendor/exist-xqj-api-1.0.1' + patterns: '*.jar' + register: '__orbeon_forms__xqj_jars' + + - name: 'Copy eXist-db XQJ-API jars into {{ orbeon_forms__home }}/WEB-INF/lib/' + ansible.builtin.copy: + src: '{{ item["path"] }}' + dest: '{{ orbeon_forms__home }}/WEB-INF/lib/{{ item["path"] | basename }}' + remote_src: true + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o644 + loop: '{{ __orbeon_forms__xqj_jars["files"] }}' + loop_control: + label: '{{ item["path"] | basename }}' + + # ---------------------------------------------------------------------------- + # properties-local.xml: copy template if missing, then inject Numishare block + # ---------------------------------------------------------------------------- + + - name: 'cp properties-local.xml.template -> properties-local.xml (if missing)' + ansible.builtin.copy: + src: '{{ orbeon_forms__home }}/WEB-INF/resources/config/properties-local.xml.template' + dest: '{{ orbeon_forms__home }}/WEB-INF/resources/config/properties-local.xml' + remote_src: true + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o644 + force: false + + - name: 'Inject the Numishare block into properties-local.xml (between ...)' + ansible.builtin.blockinfile: + path: '{{ orbeon_forms__home }}/WEB-INF/resources/config/properties-local.xml' + marker: '' + insertbefore: '' + block: "{{ lookup('ansible.builtin.template', 'web-inf/resources/config/properties-local-numishare.xml.j2') }}" + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o644 + + # ---------------------------------------------------------------------------- + # web.xml: inject Numishare login-config / security-constraint / themes mapping + # ---------------------------------------------------------------------------- + + # The Orbeon WAR ships with its own and . The web-app + # schema permits only one of each, so we replace those blocks in place instead of + # appending a second one. + - name: 'Replace existing in web.xml with the configured auth method ({{ orbeon_forms__auth_method }})' + ansible.builtin.replace: + path: '{{ orbeon_forms__home }}/WEB-INF/web.xml' + regexp: '[\s\S]*?' + replace: "{{ lookup('ansible.builtin.template', 'web-inf/login-config.xml.j2') }}" + + - name: 'Replace existing in web.xml with Numishare session-timeout' + ansible.builtin.replace: + path: '{{ orbeon_forms__home }}/WEB-INF/web.xml' + regexp: '[\s\S]*?' + replace: |- + + + {{ orbeon_forms__session_timeout }} + + + - name: 'Inject the Numishare block into web.xml (before )' + ansible.builtin.blockinfile: + path: '{{ orbeon_forms__home }}/WEB-INF/web.xml' + marker: '' + insertbefore: '' + block: "{{ lookup('ansible.builtin.template', 'web-inf/web-numishare.xml.j2') }}" + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + mode: 0o644 + + # ---------------------------------------------------------------------------- + # Themes: discovery (apps/themes -> /opt/themes) + delivery ($ORBEON/themes -> /opt/themes) + # ---------------------------------------------------------------------------- + + - name: 'Symlink {{ orbeon_forms__themes_dir }} -> {{ orbeon_forms__home }}/WEB-INF/resources/apps/themes' + ansible.builtin.file: + src: '{{ orbeon_forms__themes_dir }}' + dest: '{{ orbeon_forms__home }}/WEB-INF/resources/apps/themes' + state: 'link' + force: true + + - name: 'Symlink {{ orbeon_forms__themes_dir }} -> {{ orbeon_forms__home }}/themes' + ansible.builtin.file: + src: '{{ orbeon_forms__themes_dir }}' + dest: '{{ orbeon_forms__home }}/themes' + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + state: 'link' + force: true + + - name: 'chown -R {{ orbeon_forms__tomcat_user }}:{{ orbeon_forms__tomcat_group }} {{ orbeon_forms__home }}' + ansible.builtin.file: + path: '{{ orbeon_forms__home }}' + owner: '{{ orbeon_forms__tomcat_user }}' + group: '{{ orbeon_forms__tomcat_group }}' + recurse: true + follow: false + + - name: 'systemctl restart tomcat.service' + ansible.builtin.systemd: + name: 'tomcat.service' + state: 'restarted' + + tags: + - 'orbeon_forms' diff --git a/roles/orbeon_forms/templates/usr/share/tomcat/conf/Catalina/localhost/orbeon.xml.j2 b/roles/orbeon_forms/templates/usr/share/tomcat/conf/Catalina/localhost/orbeon.xml.j2 new file mode 100644 index 000000000..0a77b6e87 --- /dev/null +++ b/roles/orbeon_forms/templates/usr/share/tomcat/conf/Catalina/localhost/orbeon.xml.j2 @@ -0,0 +1,8 @@ + + + + diff --git a/roles/orbeon_forms/templates/web-inf/login-config.xml.j2 b/roles/orbeon_forms/templates/web-inf/login-config.xml.j2 new file mode 100644 index 000000000..5e5bcf1cf --- /dev/null +++ b/roles/orbeon_forms/templates/web-inf/login-config.xml.j2 @@ -0,0 +1,10 @@ + + + {{ orbeon_forms__auth_method | upper }} +{% if orbeon_forms__auth_method | upper == 'FORM' %} + + {{ orbeon_forms__form_login_page }} + {{ orbeon_forms__form_error_page }} + +{% endif %} + diff --git a/roles/orbeon_forms/templates/web-inf/resources/config/properties-local-numishare.xml.j2 b/roles/orbeon_forms/templates/web-inf/resources/config/properties-local-numishare.xml.j2 new file mode 100644 index 000000000..f36436b42 --- /dev/null +++ b/roles/orbeon_forms/templates/web-inf/resources/config/properties-local-numishare.xml.j2 @@ -0,0 +1,14 @@ + + + + + diff --git a/roles/orbeon_forms/templates/web-inf/web-numishare.xml.j2 b/roles/orbeon_forms/templates/web-inf/web-numishare.xml.j2 new file mode 100644 index 000000000..6d4ee86f0 --- /dev/null +++ b/roles/orbeon_forms/templates/web-inf/web-numishare.xml.j2 @@ -0,0 +1,25 @@ + + + + + Numishare + /numishare/admin/* + + +{% for role in orbeon_forms__roles %} + {{ role }} +{% endfor %} + + + +{% for role in orbeon_forms__roles %} + + {{ role }} + +{% endfor %} + + + + default + /themes/* + From b8fbd20a1b0cb1e0d472ef4741450aee3a74ac20 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:18:29 +0200 Subject: [PATCH 6/9] Add playbooks/setup_numishare New setup_* playbook that wires up the full Numishare stack on a single host, in the correct order: 1. linuxfabrik.lfops.apps (OS-level deps; injects apache_solr's bc / lsof / pwgen / tar via apache_solr__apps__apps__dependent_var) 2. linuxfabrik.lfops.apache_solr (numishare's solr-home is outside Solr's permitted paths, so we inject apache_solr__security_manager_enabled: false and apache_solr__dump_cores: ['numishare'] for the backup pipeline) 3. linuxfabrik.lfops.apache_tomcat (provides the tomcat user/group numishare and orbeon_forms chown to) 4. linuxfabrik.lfops.numishare (writes the eXist-config and Solr core wiring before existdb/orbeon reference them) 5. linuxfabrik.lfops.existdb (XML database; numishare's exist-config.xml points at it) 6. linuxfabrik.lfops.orbeon_forms (final WAR deployment + Numishare properties / web.xml / themes) Each role can be skipped via setup_numishare____skip_role (per the lfops setup_* convention). pre_tasks / post_tasks log start and end via the shared role's log-start.yml / log-end.yml so a run leaves a trail in /var/log/linuxfabrik-lfops.log. --- CHANGELOG.md | 1 + playbooks/setup_numishare.yml | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 playbooks/setup_numishare.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index c725fd096..6de009f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **playbooks/setup_numishare**: New playbook that wires up the full Numishare stack (`apps`, `apache_solr`, `apache_tomcat`, `numishare`, `existdb`, `orbeon_forms`) in the correct order, including the Solr-on-Numishare specifics (`apache_solr__security_manager_enabled: false`, `apache_solr__dump_cores: ['numishare']`, OS-level dependencies via `apache_solr__apps__apps__dependent_var`) * **role:orbeon_forms**: New role to deploy [Orbeon Forms](https://www.orbeon.com/) Community Edition into an existing Tomcat for the Numishare stack. Extracts the WAR exploded, rewrites `log4j2.xml` to absolute log paths under `/var/log/tomcat` (Orbeon's relative `../logs/` resolves to `/usr/share/logs/` on RHEL 10 Tomcat and silently disables every FileAppender), deploys `Catalina/localhost/orbeon.xml` with `allowLinking="true"`, wires Numishare's `apps/` symlink and XQJ libraries, ships a Numishare-branded favicon at the legacy `/ops/images/orbeon-icon-16.{ico,png}` path (Orbeon 2023.1 dropped the upstream file), and injects managed Numishare blocks into `properties-local.xml` and `web.xml` (epilogue theme, container auth, security-constraint for `/numishare/admin/*`, `/themes/*` default-servlet mapping, longer session timeout) * **role:numishare**: New role for [Numishare](https://github.com/ewg118/numishare), the open-source numismatic-collection platform. Clones the repository, deploys `exist-config.xml`, wires the Solr `numishare` core (notifies a Solr restart so the core is loaded), exposes Numishare's bundled UI as the `default` theme via a `/opt/themes/default` symlink, and supports custom themes via `numishare__themes__*` (combined-var pattern, source can be `git_url` + optional `git_version`/`git_update`, or `tarball_url` + optional `tarball_strip_components`) * **role:existdb**: New role to install and operate eXist-db 6.x. Extracts the upstream `unix.tar.bz2` to `/opt/existdb`, creates the system user/group, shifts the Jetty HTTP/HTTPS ports off `8080`/`8443` (so eXist-db can coexist with Tomcat or Wildfly on the same host), redirects logs to `/var/log/existdb`, sets the admin password on first install only (marker-file gated), points `client.properties` at `127.0.0.1` (eXist-db's Jetty binds IPv4 only), and ships a `mariadb-dump`-style `existdb-dump.service` + `.timer`. README documents the matching `bin/restore.sh` invocation diff --git a/playbooks/setup_numishare.yml b/playbooks/setup_numishare.yml new file mode 100644 index 000000000..0af315aaf --- /dev/null +++ b/playbooks/setup_numishare.yml @@ -0,0 +1,75 @@ +- name: 'Playbook linuxfabrik.lfops.setup_numishare' + hosts: 'all' + become: true + any_errors_fatal: true + serial: 1 + + vars: + + setup_numishare__apache_solr__skip_role__internal_var: '{{ setup_numishare__apache_solr__skip_role | d(false) }}' + setup_numishare__apache_tomcat__skip_role__internal_var: '{{ setup_numishare__apache_tomcat__skip_role | d(false) }}' + setup_numishare__apps__skip_role__internal_var: '{{ setup_numishare__apps__skip_role | d(false) }}' + setup_numishare__existdb__skip_role__internal_var: '{{ setup_numishare__existdb__skip_role | d(false) }}' + setup_numishare__numishare__skip_role__internal_var: '{{ setup_numishare__numishare__skip_role | d(false) }}' + setup_numishare__orbeon_forms__skip_role__internal_var: '{{ setup_numishare__orbeon_forms__skip_role | d(false) }}' + + pre_tasks: + + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + roles: + + # OS-level helpers (java, git, unzip, ...). Reduce to whatever is missing + # by overriding `apps__apps__host_var` in the inventory. + - role: 'linuxfabrik.lfops.apps' + apps__apps__dependent_var: '{{ + apache_solr__apps__apps__dependent_var + }}' + when: + - 'not setup_numishare__apps__skip_role__internal_var' + + # Search backend. Numishare's solr-home lives at /opt/numishare/solr-home + # (outside Solr's default permitted paths), so the Java SecurityManager + # blocks reads with `access denied ("java.io.FilePermission" ...)`. Disable it. + # The `numishare` core is fed from eXist-db and could be rebuilt from there, + # but we still snapshot it via apache-solr-dump for a fast restore path. + - role: 'linuxfabrik.lfops.apache_solr' + apache_solr__security_manager_enabled: false + apache_solr__dump_cores: + - '{{ numishare__solr_core_name | d("numishare") }}' + when: + - 'not setup_numishare__apache_solr__skip_role__internal_var' + + # Application server (required by Orbeon Forms). Runs before numishare so + # the `tomcat` user/group exist when numishare chowns its files. + - role: 'linuxfabrik.lfops.apache_tomcat' + when: + - 'not setup_numishare__apache_tomcat__skip_role__internal_var' + + # Numishare checkout, exist-config, Solr core wiring, themes dir. + # Runs before existdb/orbeon so its files exist when those reference them. + - role: 'linuxfabrik.lfops.numishare' + when: + - 'not setup_numishare__numishare__skip_role__internal_var' + + # XML database backing Numishare. + - role: 'linuxfabrik.lfops.existdb' + when: + - 'not setup_numishare__existdb__skip_role__internal_var' + + # Orbeon WAR deployment + Numishare-specific properties / web.xml / themes wiring. + - role: 'linuxfabrik.lfops.orbeon_forms' + when: + - 'not setup_numishare__orbeon_forms__skip_role__internal_var' + + post_tasks: + + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' From 2263366d3b8b158439ca51c76bb65c74d245bf7f Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:26:18 +0200 Subject: [PATCH 7/9] chore(roles): add meta/argument_specs.yml for existdb, numishare, orbeon_forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONTRIBUTING.md requires every role to ship `meta/argument_specs.yml` declaring all user-facing variables with types and (where applicable) defaults, so Ansible validates them at role entry without manual `assert + is defined` blocks in the tasks. Each spec covers what is documented in the role README: * mandatory variables (none of these three roles have any — the eXist-db admin password defaults to a non-secure placeholder and is documented as "must be overridden") * simple optional variables with their static defaults * the __host_var / __group_var halves of injection variables (numishare__themes__*) — internal __role_var, __dependent_var and __combined_var are intentionally excluded per CONTRIBUTING. `default` is omitted on entries whose `defaults/main.yml` value is a Jinja2 expression that argument_specs cannot evaluate (existdb__dump_on_calendar, existdb__dump_password). orbeon_forms__auth_method ships an explicit `choices: ['BASIC', 'FORM']` constraint matching the README documentation; `BASIC` and `FORM` are the only auth methods the login-config.xml.j2 template emits. --- roles/existdb/meta/argument_specs.yml | 106 +++++++++++++++++++++ roles/numishare/meta/argument_specs.yml | 98 +++++++++++++++++++ roles/orbeon_forms/meta/argument_specs.yml | 95 ++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 roles/existdb/meta/argument_specs.yml create mode 100644 roles/numishare/meta/argument_specs.yml create mode 100644 roles/orbeon_forms/meta/argument_specs.yml diff --git a/roles/existdb/meta/argument_specs.yml b/roles/existdb/meta/argument_specs.yml new file mode 100644 index 000000000..d0bb03ec4 --- /dev/null +++ b/roles/existdb/meta/argument_specs.yml @@ -0,0 +1,106 @@ +# argument_specs validates required variables and types automatically at role entry. +# use this for simple "is defined" / type checks. for complex validations +# (value ranges, cross-variable logic), use ansible.builtin.assert in the tasks. +argument_specs: + main: + options: + + existdb__admin_password: + type: 'str' + required: false + default: 'linuxfabrik' + description: 'Password for the eXist-db admin user. Set on first install only; subsequent runs do not touch it.' + + existdb__data_dir: + type: 'str' + required: false + default: '/var/lib/existdb/data' + description: 'Where eXist-db stores its index and journal files.' + + existdb__dump_collection: + type: 'str' + required: false + default: '/db' + description: 'Collection to back up.' + + existdb__dump_directory: + type: 'str' + required: false + default: '/backup/existdb-dump' + description: 'Where the latest backup snapshot lands.' + + existdb__dump_enabled: + type: 'bool' + required: false + default: true + description: 'Whether the existdb-dump.timer is enabled.' + + existdb__dump_on_calendar: + type: 'str' + required: false + description: 'OnCalendar= value for existdb-dump.timer.' + + existdb__dump_password: + type: 'str' + required: false + description: 'Password the dumper uses to authenticate against eXist-db. Defaults to existdb__admin_password.' + + existdb__dump_user: + type: 'str' + required: false + default: 'admin' + description: 'User the dumper authenticates as.' + + existdb__group: + type: 'str' + required: false + default: 'existdb' + description: 'System group that owns the install dir and runs the service.' + + existdb__http_port: + type: 'int' + required: false + default: 8888 + description: 'Jetty HTTP port. Shifted off the application-server-default 8080 so eXist-db can coexist with Tomcat or Wildfly on the same host.' + + existdb__https_port: + type: 'int' + required: false + default: 8444 + description: 'Jetty HTTPS port. Shifted off 8443 for the same reason as HTTP.' + + existdb__install_dir: + type: 'str' + required: false + default: '/opt/existdb' + description: 'Where the upstream tarball is extracted to.' + + existdb__java_opts: + type: 'str' + required: false + default: '-XX:+UseG1GC -XX:+UseStringDeduplication -XX:MaxRAMPercentage=75.0' + description: 'JVM options passed to the eXist-db service via systemd Environment=JAVA_OPTS.' + + existdb__log_dir: + type: 'str' + required: false + default: '/var/log/existdb' + description: 'Where eXist-db log4j2 writes log files.' + + existdb__service_enabled: + type: 'bool' + required: false + default: true + description: 'Whether the existdb.service is enabled, analogous to systemctl enable/disable --now.' + + existdb__user: + type: 'str' + required: false + default: 'existdb' + description: 'System user that owns the install dir and runs the service.' + + existdb__version: + type: 'str' + required: false + default: '6.4.1' + description: 'Upstream release version. The role downloads exist-distribution--unix.tar.bz2.' diff --git a/roles/numishare/meta/argument_specs.yml b/roles/numishare/meta/argument_specs.yml new file mode 100644 index 000000000..bb80ea85b --- /dev/null +++ b/roles/numishare/meta/argument_specs.yml @@ -0,0 +1,98 @@ +# argument_specs validates required variables and types automatically at role entry. +# use this for simple "is defined" / type checks. for complex validations +# (value ranges, cross-variable logic), use ansible.builtin.assert in the tasks. +argument_specs: + main: + options: + + numishare__app_group: + type: 'str' + required: false + default: 'tomcat' + description: 'Group that owns Numishare config files; must match the application server user.' + + numishare__app_user: + type: 'str' + required: false + default: 'tomcat' + description: 'User that owns Numishare config files; must match the application server user.' + + numishare__exist_password: + type: 'str' + required: false + default: 'linuxfabrik' + description: 'Password Numishare uses to authenticate against eXist-db.' + + numishare__exist_url: + type: 'str' + required: false + default: 'http://127.0.0.1:8888/exist/rest/db/' + description: 'eXist-db REST endpoint Numishare connects to. 127.0.0.1 is preferred over localhost because eXist-db Jetty binds IPv4 only.' + + numishare__exist_username: + type: 'str' + required: false + default: 'admin' + description: 'User Numishare authenticates as against eXist-db.' + + numishare__git_url: + type: 'str' + required: false + default: 'https://github.com/ewg118/numishare.git' + description: 'Upstream Numishare repository to clone.' + + numishare__install_dir: + type: 'str' + required: false + default: '/opt/numishare' + description: 'Where Numishare is checked out to.' + + numishare__solr_core_name: + type: 'str' + required: false + default: 'numishare' + description: 'Name of the Solr core Numishare uses.' + + numishare__solr_core_version: + type: 'str' + required: false + default: '1.7' + description: 'Numishare Solr schema version. Selects the solr-home// subtree.' + + numishare__solr_data_dir: + type: 'str' + required: false + default: '/var/solr/data' + description: 'Solr solr.solr.home directory. The role wires / as a symlink.' + + numishare__solr_group: + type: 'str' + required: false + default: 'solr' + description: 'Group that owns the Solr-related Numishare files.' + + numishare__solr_user: + type: 'str' + required: false + default: 'solr' + description: 'User that owns the Solr-related Numishare files.' + + numishare__themes__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Custom themes to deploy. Group-level override.' + + numishare__themes__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Custom themes to deploy. Host-level override.' + + numishare__themes_dir: + type: 'str' + required: false + default: '/opt/themes' + description: 'Root directory for custom themes. The role auto-creates /default as a symlink to Numishare bundled UI assets.' diff --git a/roles/orbeon_forms/meta/argument_specs.yml b/roles/orbeon_forms/meta/argument_specs.yml new file mode 100644 index 000000000..b8660769e --- /dev/null +++ b/roles/orbeon_forms/meta/argument_specs.yml @@ -0,0 +1,95 @@ +# argument_specs validates required variables and types automatically at role entry. +# use this for simple "is defined" / type checks. for complex validations +# (value ranges, cross-variable logic), use ansible.builtin.assert in the tasks. +argument_specs: + main: + options: + + orbeon_forms__auth_method: + type: 'str' + required: false + default: 'BASIC' + choices: + - 'BASIC' + - 'FORM' + description: 'HTTP authentication method written into the Orbeon web.xml.' + + orbeon_forms__form_error_page: + type: 'str' + required: false + default: '/numishare/login-failed' + description: 'Login-failure page when auth_method == FORM.' + + orbeon_forms__form_login_page: + type: 'str' + required: false + default: '/numishare/login' + description: 'Login page when auth_method == FORM.' + + orbeon_forms__home: + type: 'str' + required: false + default: '/var/lib/tomcat/webapps/orbeon' + description: 'Exploded-WAR deployment directory. Tomcat must scan this path for webapps.' + + orbeon_forms__log_dir: + type: 'str' + required: false + default: '/var/log/tomcat' + description: 'Absolute path that replaces Orbeon shipped ../logs/ references in log4j2.xml. Must be writable by the Tomcat user.' + + orbeon_forms__numishare_dir: + type: 'str' + required: false + default: '/opt/numishare' + description: 'Where Numishare is checked out. Must match numishare__install_dir.' + + orbeon_forms__roles: + type: 'list' + elements: 'str' + required: false + default: + - 'numishare-admin' + description: 'Container roles allowed to access /numishare/admin/*. Each entry must have a matching apache_tomcat__roles__* and apache_tomcat__users__* entry so login actually works.' + + orbeon_forms__session_timeout: + type: 'int' + required: false + default: 720 + description: 'Replaces Orbeon shipped value (in minutes). Numishare admin forms benefit from a higher timeout because configuration runs are long.' + + orbeon_forms__themes_dir: + type: 'str' + required: false + default: '/opt/themes' + description: 'Theme root directory exposed as the apps/themes discovery path and the /themes delivery path.' + + orbeon_forms__tomcat_conf_dir: + type: 'str' + required: false + default: '/usr/share/tomcat/conf' + description: 'Tomcat configuration root. The role writes /Catalina/localhost/orbeon.xml.' + + orbeon_forms__tomcat_group: + type: 'str' + required: false + default: 'tomcat' + description: 'Group that owns Orbeon deployment files; must match the Tomcat group.' + + orbeon_forms__tomcat_user: + type: 'str' + required: false + default: 'tomcat' + description: 'User that owns Orbeon deployment files; must match the Tomcat user.' + + orbeon_forms__version: + type: 'str' + required: false + default: '2023.1.202312312000' + description: 'Orbeon CE version. Used to construct the download URL and identify the local copy under /tmp.' + + orbeon_forms__zip_url: + type: 'str' + required: false + default: 'https://github.com/orbeon/orbeon-forms/releases/download/tag-release-2023.1-ce/orbeon-2023.1.202312312000-CE.zip' + description: 'Direct download URL of the Orbeon CE zip.' From e72d375fddf266eb75765a028307bc1c76c0e49e Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 14:30:15 +0200 Subject: [PATCH 8/9] chore: align setup_numishare and existdb with CONTRIBUTING (skip_injections, service split, playbooks docs) * playbooks/README.md: document setup_numishare.yml in alphabetical position between setup_nextcloud and setup_rocketchat (CONTRIBUTING: "After creating a new playbook, document it in playbooks/README.md"). * playbooks/all.yml: import setup_numishare.yml in the alphabetical slot between setup_nextcloud.yml and setup_rocketchat.yml (CONTRIBUTING: "and add it in the playbooks/all.yml"). * playbooks/setup_numishare.yml: introduce the setup_numishare__apache_solr__skip_injections__internal_var pattern exactly as CONTRIBUTING.md spells it out for setup_* playbooks. Defaults to the apache_solr skip_role state, can be overridden via setup_numishare__apache_solr__skip_injections to allow running the apache_solr role without injecting its OS deps into the apps role (e.g. when the user manages OS deps elsewhere). Apply via ternary on the apps role's apps__apps__dependent_var. * roles/existdb/tasks/main.yml: split the combined enable+state systemd call into two separate ansible.builtin.service tasks per CONTRIBUTING ("Split the service `enabled` and `state` into separate tasks. This is relevant for handlers that would restart the service"). Same split for existdb-dump.timer; the timer block now also has its own ansible.builtin.systemd `daemon_reload: true` task gated on the template-deploy result rather than piggybacking on the combined call. Register the existdb.service state task as __existdb__service_state_result for any future handler that needs to skip a redundant restart, matching the pattern in roles/example. --- playbooks/README.md | 12 ++++++++++++ playbooks/all.yml | 1 + playbooks/setup_numishare.yml | 19 +++++++++++-------- roles/existdb/tasks/main.yml | 25 +++++++++++++++++++------ 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/playbooks/README.md b/playbooks/README.md index 201b80e52..5db3c26ff 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -1231,6 +1231,18 @@ Calls the following roles (in order): * [icinga2_agent](https://github.com/Linuxfabrik/lfops/tree/main/roles/icinga2_agent) +## setup_numishare.yml + +Calls the following roles (in order): + +* [apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps): `setup_numishare__skip_apps` +* [apache_solr](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_solr): `setup_numishare__skip_apache_solr`, `setup_numishare__apache_solr__skip_injections` +* [apache_tomcat](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_tomcat): `setup_numishare__skip_apache_tomcat` +* [numishare](https://github.com/Linuxfabrik/lfops/tree/main/roles/numishare): `setup_numishare__skip_numishare` +* [existdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/existdb): `setup_numishare__skip_existdb` +* [orbeon_forms](https://github.com/Linuxfabrik/lfops/tree/main/roles/orbeon_forms): `setup_numishare__skip_orbeon_forms` + + ## setup_rocketchat.yml Calls the following roles (in order): diff --git a/playbooks/all.yml b/playbooks/all.yml index fcba354fa..a62abcf30 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -130,6 +130,7 @@ - import_playbook: 'setup_mastodon.yml' - import_playbook: 'setup_moodle.yml' - import_playbook: 'setup_nextcloud.yml' +- import_playbook: 'setup_numishare.yml' - import_playbook: 'setup_rocketchat.yml' - import_playbook: 'setup_wordpress.yml' - import_playbook: 'shell.yml' diff --git a/playbooks/setup_numishare.yml b/playbooks/setup_numishare.yml index 0af315aaf..1f7e6cfa2 100644 --- a/playbooks/setup_numishare.yml +++ b/playbooks/setup_numishare.yml @@ -6,12 +6,13 @@ vars: - setup_numishare__apache_solr__skip_role__internal_var: '{{ setup_numishare__apache_solr__skip_role | d(false) }}' - setup_numishare__apache_tomcat__skip_role__internal_var: '{{ setup_numishare__apache_tomcat__skip_role | d(false) }}' - setup_numishare__apps__skip_role__internal_var: '{{ setup_numishare__apps__skip_role | d(false) }}' - setup_numishare__existdb__skip_role__internal_var: '{{ setup_numishare__existdb__skip_role | d(false) }}' - setup_numishare__numishare__skip_role__internal_var: '{{ setup_numishare__numishare__skip_role | d(false) }}' - setup_numishare__orbeon_forms__skip_role__internal_var: '{{ setup_numishare__orbeon_forms__skip_role | d(false) }}' + setup_numishare__apache_solr__skip_injections__internal_var: '{{ setup_numishare__apache_solr__skip_injections | d(setup_numishare__apache_solr__skip_role__internal_var) }}' + setup_numishare__apache_solr__skip_role__internal_var: '{{ setup_numishare__apache_solr__skip_role | d(false) }}' + setup_numishare__apache_tomcat__skip_role__internal_var: '{{ setup_numishare__apache_tomcat__skip_role | d(false) }}' + setup_numishare__apps__skip_role__internal_var: '{{ setup_numishare__apps__skip_role | d(false) }}' + setup_numishare__existdb__skip_role__internal_var: '{{ setup_numishare__existdb__skip_role | d(false) }}' + setup_numishare__numishare__skip_role__internal_var: '{{ setup_numishare__numishare__skip_role | d(false) }}' + setup_numishare__orbeon_forms__skip_role__internal_var: '{{ setup_numishare__orbeon_forms__skip_role | d(false) }}' pre_tasks: @@ -24,10 +25,12 @@ roles: # OS-level helpers (java, git, unzip, ...). Reduce to whatever is missing - # by overriding `apps__apps__host_var` in the inventory. + # by overriding `apps__apps__host_var` in the inventory. apache_solr's OS + # dependencies (bc, lsof, pwgen, tar) are injected here unless the user + # opts out via setup_numishare__apache_solr__skip_injections. - role: 'linuxfabrik.lfops.apps' apps__apps__dependent_var: '{{ - apache_solr__apps__apps__dependent_var + (not setup_numishare__apache_solr__skip_injections__internal_var) | ternary(apache_solr__apps__apps__dependent_var, []) }}' when: - 'not setup_numishare__apps__skip_role__internal_var' diff --git a/roles/existdb/tasks/main.yml b/roles/existdb/tasks/main.yml index 251181e75..34b7435e9 100644 --- a/roles/existdb/tasks/main.yml +++ b/roles/existdb/tasks/main.yml @@ -149,11 +149,16 @@ daemon_reload: true when: '__existdb__unit_result is changed' - - name: 'systemctl {{ existdb__service_enabled | bool | ternary("enable", "disable") }} --now existdb.service' - ansible.builtin.systemd: + - name: 'systemctl {{ existdb__service_enabled | bool | ternary("enable", "disable") }} existdb.service' + ansible.builtin.service: + name: 'existdb.service' + enabled: '{{ existdb__service_enabled | bool }}' + + - name: 'systemctl {{ existdb__service_enabled | bool | ternary("start", "stop") }} existdb.service' + ansible.builtin.service: name: 'existdb.service' - enabled: '{{ existdb__service_enabled }}' state: '{{ existdb__service_enabled | bool | ternary("started", "stopped") }}' + register: '__existdb__service_state_result' tags: - 'existdb' @@ -221,12 +226,20 @@ mode: 0o644 register: '__existdb__dump_timer_result' - - name: 'systemctl {{ existdb__dump_enabled | bool | ternary("enable --now", "disable --now") }} existdb-dump.timer' + - name: 'systemctl daemon-reload (existdb-dump.timer changed)' # noqa no-handler ansible.builtin.systemd: + daemon_reload: true + when: '__existdb__dump_timer_result is changed' + + - name: 'systemctl {{ existdb__dump_enabled | bool | ternary("enable", "disable") }} existdb-dump.timer' + ansible.builtin.service: + name: 'existdb-dump.timer' + enabled: '{{ existdb__dump_enabled | bool }}' + + - name: 'systemctl {{ existdb__dump_enabled | bool | ternary("start", "stop") }} existdb-dump.timer' + ansible.builtin.service: name: 'existdb-dump.timer' - enabled: '{{ existdb__dump_enabled }}' state: '{{ existdb__dump_enabled | bool | ternary("started", "stopped") }}' - daemon_reload: '{{ __existdb__dump_timer_result is changed }}' tags: - 'existdb' From 231d198b6b7b9749dfacbc72ac43a8d45a2acac1 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 28 Apr 2026 16:27:49 +0200 Subject: [PATCH 9/9] fix(roles/numishare): rewrite hardcoded /usr/local/projects/numishare paths to numishare__install_dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Numishare upstream ships two files with the install path hardcoded to `/usr/local/projects/numishare` — the upstream maintainer's own layout: * `xforms/admin.xhtml` line 101: the `` element in the XForms instance for the Add-New-Collection form. The value pre-fills the "Installation Directory" field and gets persisted into eXist-db per collection at `/db/numishare//config.xml`. * `script/reindex-collection.php`: the `$eXist_config_path` constant points at `/usr/local/projects/numishare/exist-config.xml` for the CLI reindex helper. lfops installs Numishare to `/opt/numishare` by default. Without this rewrite the admin form defaults to a non-existent path on every fresh install (so the user has to clear and retype the field on every new collection), and the reindex script crashes on missing `exist-config.xml` until it is manually patched. Add an `ansible.builtin.replace` task right after the git clone, scoped to the two known files via a loop. We do not regex over the whole checkout to avoid accidentally rewriting `docker/docker-compose.yml`, which uses `/usr/local/projects/numishare` as a Docker volume mount path that is intentionally independent of the host layout. Re-runs are idempotent: replace finds nothing to change after the first pass since the literal `/usr/local/projects/numishare` is gone. --- CHANGELOG.md | 1 + roles/numishare/tasks/main.yml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de009f05..1098c23ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:numishare**: Rewrite the two upstream-hardcoded `/usr/local/projects/numishare` paths in the cloned source tree to `numishare__install_dir`. Affects `xforms/admin.xhtml` (pre-fills the "Installation Directory" field on the Add-New-Collection form, persisted into eXist-db per collection) and `script/reindex-collection.php` (`$eXist_config_path` for the CLI reindex). Without this rewrite the form default points at a non-existent path on every fresh install (lfops uses `/opt/numishare`) and the reindex script can't find `exist-config.xml` until manually patched. * **role:apache_solr**: Fix `No package java-17-openjdk-headless available.` on RHEL 10. Red Hat dropped `java-17-openjdk` from EL10 AppStream (only `java-21-openjdk` and `java-25-openjdk` ship now). The role now picks `java-21-openjdk-headless` for Solr 9.x on EL10 via a new OS-specific `vars/RedHat10.yml`; EL8/9 keep `java-17-openjdk-headless` unchanged. Solr 9.x officially supports Java 11, 17, and 21. * **role:apache_solr**: Add `become: false` to the `delegate_to: localhost` `get_url` tasks so the role works under ansible-navigator execution environments, where `become` would try to `sudo` inside the EE container without a password. * **playbooks/freeipa_client, playbooks/freeipa_server**: Set `strategy: 'linear'` explicitly so the playbooks work even when the user's `ansible.cfg` defaults to a strategy that reuses the target Python interpreter (e.g. `mitogen_linear`). The ansible-freeipa modules rely on `ipalib`'s global API singleton and otherwise fail with `API.bootstrap() already called` on the second module call. diff --git a/roles/numishare/tasks/main.yml b/roles/numishare/tasks/main.yml index 71d143c0f..e2edf2649 100644 --- a/roles/numishare/tasks/main.yml +++ b/roles/numishare/tasks/main.yml @@ -13,6 +13,22 @@ depth: 1 update: false # Numishare is checked out once; updates handled out-of-band + # Numishare ships two files with the install path hardcoded to + # `/usr/local/projects/numishare` (the upstream developer's own layout): + # * xforms/admin.xhtml — pre-fills the "Installation Directory" field on + # the Add-New-Collection form, persisted into eXist-db per collection + # * script/reindex-collection.php — `$eXist_config_path` for CLI reindex + # Rewrite both to numishare__install_dir so the form default is correct + # out of the box and the reindex CLI works without manual edits. + - name: 'Rewrite hardcoded /usr/local/projects/numishare paths to {{ numishare__install_dir }}' + ansible.builtin.replace: + path: '{{ numishare__install_dir }}/{{ item }}' + regexp: '/usr/local/projects/numishare' + replace: '{{ numishare__install_dir }}' + loop: + - 'script/reindex-collection.php' + - 'xforms/admin.xhtml' + - name: 'Deploy {{ numishare__install_dir }}/exist-config.xml' ansible.builtin.template: backup: true