From c7236e4596999a3d73f21e1dde50a3f147f1b200 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 12 May 2026 17:12:31 +0200 Subject: [PATCH 01/18] Add roles/repo_google_chrome --- CHANGELOG.md | 1 + COMPATIBILITY.md | 1 + playbooks/README.md | 7 +++ playbooks/all.yml | 1 + playbooks/repo_google_chrome.yml | 23 +++++++++ roles/repo_google_chrome/README.md | 48 +++++++++++++++++++ roles/repo_google_chrome/defaults/main.yml | 2 + roles/repo_google_chrome/tasks/RedHat.yml | 40 ++++++++++++++++ roles/repo_google_chrome/tasks/main.yml | 18 +++++++ .../etc/yum.repos.d/google-chrome.repo.j2 | 20 ++++++++ 10 files changed, 161 insertions(+) create mode 100644 playbooks/repo_google_chrome.yml create mode 100644 roles/repo_google_chrome/README.md create mode 100644 roles/repo_google_chrome/defaults/main.yml create mode 100644 roles/repo_google_chrome/tasks/RedHat.yml create mode 100644 roles/repo_google_chrome/tasks/main.yml create mode 100644 roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d4b885..fb97e997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). * **role:alternatives, role:elastic_agent, role:elastic_agent_fleet_server, role:icinga_kubernetes_web, role:lvm, role:mailto_root, role:motd, role:proxysql**: (Re-)introduce `meta/argument_specs.yml` so role-entry validation catches type mismatches and missing required variables. The originally proposed specs were correct for these roles (no strict-options login dicts, no `__dependent_var` injections from `setup_*` playbooks), so they are restored unchanged. * **role:apps, role:example, role:kernel_settings**: (Re-)introduce `meta/argument_specs.yml`, with the `__dependent_var` slot declared so `setup_*` playbooks that inject these via `vars:` (e.g. `setup_icinga2_master`, `setup_moodle`, `setup_nextcloud`) pass validation. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index f2ada57d..84f54665 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -130,6 +130,7 @@ Which Ansible role is proven to run on which OS? | repo_epel | | | x | x | x | | | | | | repo_gitlab_ce | | | x | (x) | (x) | | | | | | repo_gitlab_runner | | | x | (x) | (x) | | | | | +| repo_google_chrome | | | x | x | (x) | | | | | | repo_grafana | x | x | x | x | (x) | (x) | (x) | (x) | | | repo_graylog | x | x | x | (x) | (x) | (x) | (x) | (x) | | | repo_icinga | x | x | x | x | x | x | (x) | (x) | | diff --git a/playbooks/README.md b/playbooks/README.md index 3f44cad1..f1b7a6ac 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -855,6 +855,13 @@ Calls the following roles (in order): * [repo_gitlab_runner](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_gitlab_runner) +## repo_google_chrome.yml + +Calls the following roles (in order): + +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) + + ## repo_grafana.yml Calls the following roles (in order): diff --git a/playbooks/all.yml b/playbooks/all.yml index fcba354f..f925e09f 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -103,6 +103,7 @@ - import_playbook: 'repo_epel.yml' - import_playbook: 'repo_gitlab_ce.yml' - import_playbook: 'repo_gitlab_runner.yml' +- import_playbook: 'repo_google_chrome.yml' - import_playbook: 'repo_grafana.yml' - import_playbook: 'repo_graylog.yml' - import_playbook: 'repo_icinga.yml' diff --git a/playbooks/repo_google_chrome.yml b/playbooks/repo_google_chrome.yml new file mode 100644 index 00000000..f19a1b89 --- /dev/null +++ b/playbooks/repo_google_chrome.yml @@ -0,0 +1,23 @@ +- name: 'Playbook linuxfabrik.lfops.repo_google_chrome' + hosts: + - 'lfops_repo_google_chrome' + + pre_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + + roles: + + - role: 'linuxfabrik.lfops.repo_google_chrome' + + + post_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' diff --git a/roles/repo_google_chrome/README.md b/roles/repo_google_chrome/README.md new file mode 100644 index 00000000..7da86d69 --- /dev/null +++ b/roles/repo_google_chrome/README.md @@ -0,0 +1,48 @@ +# Ansible Role linuxfabrik.lfops.repo_google_chrome + +This role deploys the package repository for [Google Chrome](https://www.google.com/chrome/) on RHEL-based distributions. + + +*Available since LFOps `6.0.2`.* + + +## Tags + +`repo_google_chrome` + +* Deploys the Google Chrome Repository. +* Triggers: none. + + +## Optional Role Variables + +`repo_google_chrome__basic_auth_login` + +* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Type: String. +* Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` + +`repo_google_chrome__mirror_url` + +* Set the URL to a custom mirror server providing the repository. Defaults to `lfops__repo_mirror_url` to allow easily setting the same URL for all `repo_*` roles. If `lfops__repo_mirror_url` is not set, the default mirrors of the repo are used. +* Type: String. +* Default: `'{{ lfops__repo_mirror_url | default("") }}'` + +Example: +```yaml +# optional +repo_google_chrome__basic_auth_login: + username: 'my-username' + password: 'linuxfabrik' +repo_google_chrome__mirror_url: 'https://mirror.example.com' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/repo_google_chrome/defaults/main.yml b/roles/repo_google_chrome/defaults/main.yml new file mode 100644 index 00000000..69f976e3 --- /dev/null +++ b/roles/repo_google_chrome/defaults/main.yml @@ -0,0 +1,2 @@ +repo_google_chrome__basic_auth_login: '{{ lfops__repo_basic_auth_login | default("") }}' +repo_google_chrome__mirror_url: '{{ lfops__repo_mirror_url | default("") }}' diff --git a/roles/repo_google_chrome/tasks/RedHat.yml b/roles/repo_google_chrome/tasks/RedHat.yml new file mode 100644 index 00000000..83a5f6cf --- /dev/null +++ b/roles/repo_google_chrome/tasks/RedHat.yml @@ -0,0 +1,40 @@ +- block: + + - name: 'curl https://dl-ssl.google.com/linux/linux_signing_key.pub --output /tmp/ansible.google-chrome.key' + ansible.builtin.get_url: + url: 'https://dl-ssl.google.com/linux/linux_signing_key.pub' + dest: '/tmp/ansible.google-chrome.key' + mode: 0o644 + delegate_to: 'localhost' + become: false + run_once: true + changed_when: false # not an actual config change on the server + check_mode: false # run task even if `--check` is specified + + - name: 'copy /tmp/ansible.google-chrome.key to /etc/pki/rpm-gpg/google-chrome.key' + ansible.builtin.copy: + src: '/tmp/ansible.google-chrome.key' + dest: '/etc/pki/rpm-gpg/google-chrome.key' + owner: 'root' + group: 'root' + mode: 0o644 + + # https://www.google.com/linuxrepositories/ + - name: 'deploy the Google Chrome repo (mirror: {{ repo_google_chrome__mirror_url }})' + ansible.builtin.template: + backup: true + src: 'etc/yum.repos.d/google-chrome.repo.j2' + dest: '/etc/yum.repos.d/google-chrome.repo' + owner: 'root' + group: 'root' + mode: 0o644 + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '/etc/yum.repos.d/google-chrome.repo' + + tags: + - 'repo_google_chrome' diff --git a/roles/repo_google_chrome/tasks/main.yml b/roles/repo_google_chrome/tasks/main.yml new file mode 100644 index 00000000..4f290d1f --- /dev/null +++ b/roles/repo_google_chrome/tasks/main.yml @@ -0,0 +1,18 @@ +- name: 'Perform platform/version specific tasks' + ansible.builtin.include_tasks: '{{ __task_file }}' + when: '__task_file | length > 0' + vars: + __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' + __first_found_options: + files: + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}.yml' + paths: + - '{{ role_path }}/tasks' + skip: true + tags: + - 'always' diff --git a/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 b/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 new file mode 100644 index 00000000..ff8375bc --- /dev/null +++ b/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 @@ -0,0 +1,20 @@ +# {{ ansible_managed }} +# 2026051201 + +[google-chrome] +name=google-chrome +{% if repo_google_chrome__mirror_url is defined and repo_google_chrome__mirror_url | length %} +baseurl={{ repo_google_chrome__mirror_url }}/linux/chrome/rpm/stable/$basearch +{% else %} +baseurl=https://dl.google.com/linux/chrome/rpm/stable/$basearch +{% endif %} +enabled=1 +gpgcheck=1 +repo_gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/google-chrome.key +sslverify=1 +sslcacert=/etc/pki/tls/certs/ca-bundle.crt +{% if repo_google_chrome__basic_auth_login is defined and repo_google_chrome__basic_auth_login | length %} +username={{ repo_google_chrome__basic_auth_login["username"] }} +password={{ repo_google_chrome__basic_auth_login["password"] }} +{% endif %} From c510b8b9527549dce5dd7a3f3b5bc170546b6f8d Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 12 May 2026 17:13:31 +0200 Subject: [PATCH 02/18] Add roles/google_chrome --- CHANGELOG.md | 1 + COMPATIBILITY.md | 1 + playbooks/README.md | 9 + playbooks/all.yml | 1 + playbooks/google_chrome.yml | 31 ++++ roles/google_chrome/README.md | 136 ++++++++++++++ roles/google_chrome/defaults/main.yml | 33 ++++ roles/google_chrome/handlers/main.yml | 20 ++ roles/google_chrome/meta/argument_specs.yml | 71 ++++++++ roles/google_chrome/tasks/main.yml | 171 ++++++++++++++++++ .../system/chrome-headless-proxy.service.j2 | 13 ++ .../system/chrome-headless-proxy.socket.j2 | 11 ++ .../systemd/system/chrome-headless.service.j2 | 54 ++++++ roles/google_chrome/vars/RedHat.yml | 6 + 14 files changed, 558 insertions(+) create mode 100644 playbooks/google_chrome.yml create mode 100644 roles/google_chrome/README.md create mode 100644 roles/google_chrome/defaults/main.yml create mode 100644 roles/google_chrome/handlers/main.yml create mode 100644 roles/google_chrome/meta/argument_specs.yml create mode 100644 roles/google_chrome/tasks/main.yml create mode 100644 roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 create mode 100644 roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 create mode 100644 roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 create mode 100644 roles/google_chrome/vars/RedHat.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index fb97e997..fdf25170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips the `systemd_socket_proxyd_connect_any` SELinux boolean on enforcing hosts so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. * **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). * **role:alternatives, role:elastic_agent, role:elastic_agent_fleet_server, role:icinga_kubernetes_web, role:lvm, role:mailto_root, role:motd, role:proxysql**: (Re-)introduce `meta/argument_specs.yml` so role-entry validation catches type mismatches and missing required variables. The originally proposed specs were correct for these roles (no strict-options login dicts, no `__dependent_var` injections from `setup_*` playbooks), so they are restored unchanged. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 84f54665..2ca55211 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -42,6 +42,7 @@ Which Ansible role is proven to run on which OS? | gitlab_ce | | | x | (x) | (x) | | | | | | glances | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | glpi_agent | | | x | x | (x) | | | | | +| google_chrome | | | x | x | (x) | | | | | | grafana | | | x | x | x | | | | | | grafana_grizzly | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | grav | | | x | (x) | (x) | | | | | diff --git a/playbooks/README.md b/playbooks/README.md index f1b7a6ac..6ab15919 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -338,6 +338,15 @@ Calls the following roles (in order): * [glpi_agent](https://github.com/Linuxfabrik/lfops/tree/main/roles/glpi_agent) +## google_chrome.yml + +Calls the following roles (in order): + +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `google_chrome__skip_repo_epel` +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `google_chrome__skip_repo_google_chrome` +* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) + + ## grafana.yml Calls the following roles (in order): diff --git a/playbooks/all.yml b/playbooks/all.yml index f925e09f..2d2467d1 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -36,6 +36,7 @@ - import_playbook: 'gitlab_ce.yml' - import_playbook: 'glances.yml' - import_playbook: 'glpi_agent.yml' +- import_playbook: 'google_chrome.yml' - import_playbook: 'grafana.yml' - import_playbook: 'grafana_grizzly.yml' - import_playbook: 'haveged.yml' diff --git a/playbooks/google_chrome.yml b/playbooks/google_chrome.yml new file mode 100644 index 00000000..f959bf4c --- /dev/null +++ b/playbooks/google_chrome.yml @@ -0,0 +1,31 @@ +- name: 'Playbook linuxfabrik.lfops.google_chrome' + hosts: + - 'lfops_google_chrome' + + pre_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + + roles: + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'not google_chrome__skip_repo_epel | d(false) | bool' + + - role: 'linuxfabrik.lfops.repo_google_chrome' + when: + - 'not google_chrome__skip_repo_google_chrome | d(false) | bool' + + - role: 'linuxfabrik.lfops.google_chrome' + + + post_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md new file mode 100644 index 00000000..c4997988 --- /dev/null +++ b/roles/google_chrome/README.md @@ -0,0 +1,136 @@ +# Ansible Role linuxfabrik.lfops.google_chrome + +This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. + +The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). + + +*Available since LFOps `6.0.2`.* + + +## How the Role Behaves + +* Three systemd units are deployed: + * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). + * `chrome-headless-proxy.service` runs `systemd-socket-proxyd` and forwards traffic to Chrome on `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. + * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. +* On SELinux-enforcing hosts, the `systemd_socket_proxyd_connect_any` boolean is enabled so the proxy may connect to Chrome's non-standard backend port. +* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. + + +## Mandatory Requirements + +* Enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. +* Enable the Google Chrome repository. This can be done using the [linuxfabrik.lfops.repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) role. + +If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/google_chrome.yml), this is automatically done for you. + + +## Tags + +`google_chrome` + +* Creates the `chrome` system user and group. +* Installs Google Chrome along with the required runtime libraries and fonts. +* Sets the `systemd_socket_proxyd_connect_any` SELinux boolean. +* Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). +* Ensures the `chrome-headless-proxy.socket` is in the desired state. +* Triggers: daemon-reload, socket restart, Chrome service restart. + +`google_chrome:configure` + +* Deploys the three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). +* Triggers: daemon-reload, socket restart, Chrome service restart. + +`google_chrome:state` + +* Manages the `chrome-headless-proxy.socket` state (start, stop, enable, disable). +* Triggers: none. + + +## Optional Role Variables + +`google_chrome__backend_port` + +* Internal port Chrome itself listens on. The proxy forwards traffic from `listen_port` to this port. Only meaningful to change if `listen_port` and `backend_port` would otherwise collide. +* Type: Number. +* Default: `9223` + +`google_chrome__extra_args__host_var` / `google_chrome__extra_args__group_var` + +* Additional Chrome CLI flags appended to the `ExecStart` line of `chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `name`: + + * Mandatory. The CLI flag, including any leading dashes and value (e.g. `--window-size=1920,1080`). + * Type: String. + + * `state`: + + * Optional. `present` or `absent`. + * Type: String. + * Default: `'present'` + +`google_chrome__idle_timeout` + +* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1–2 seconds of cold-start latency. +* Type: Number. +* Default: `300` + +`google_chrome__listen_address` + +* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chrome enforces TLS or authentication. +* Type: String. +* Default: `'127.0.0.1'` + +`google_chrome__listen_port` + +* Port the proxy socket listens on. This is the endpoint clients connect to. +* Type: Number. +* Default: `9222` + +`google_chrome__service_enabled` + +* Enables or disables the `chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. +* Type: Bool. +* Default: `true` + +`google_chrome__service_state` + +* Changes the state of the `chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. +* Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. +* Default: `'started'` + +`google_chrome__user_data_dir` + +* Home directory of the `chrome` system user and Chrome user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chrome, and as the writable path exposed via systemd `ReadWritePaths=`. +* Type: String. +* Default: `'/var/lib/chrome-headless'` + +Example: +```yaml +# optional +google_chrome__backend_port: 9223 +google_chrome__extra_args__host_var: + - name: '--window-size=1920,1080' + - name: '--lang=de-CH' +google_chrome__idle_timeout: 600 +google_chrome__listen_address: '127.0.0.1' +google_chrome__listen_port: 9222 +google_chrome__service_enabled: true +google_chrome__service_state: 'started' +google_chrome__user_data_dir: '/var/lib/chrome-headless' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml new file mode 100644 index 00000000..7cd77812 --- /dev/null +++ b/roles/google_chrome/defaults/main.yml @@ -0,0 +1,33 @@ +# --- list of dicts injection pattern --- +# Extra Chrome CLI flags appended to the chrome-headless systemd unit. +google_chrome__extra_args__dependent_var: [] +google_chrome__extra_args__group_var: [] +google_chrome__extra_args__host_var: [] +google_chrome__extra_args__role_var: [] +google_chrome__extra_args__combined_var: '{{ ( + google_chrome__extra_args__role_var + + google_chrome__extra_args__dependent_var + + google_chrome__extra_args__group_var + + google_chrome__extra_args__host_var + ) | linuxfabrik.lfops.combine_lod + }}' + +# Chrome's own listening port. The proxy connects to it on demand; clients never +# talk to it directly. +google_chrome__backend_port: 9223 + +# Idle timeout for systemd-socket-proxyd in seconds. After this much time without +# active connections, the proxy exits — and Chrome stops with it via BindsTo. +google_chrome__idle_timeout: 300 + +# External listening endpoint exposed by the chrome-headless-proxy.socket unit. +# This is what clients (Apache, the pdfexport module, ...) connect to. +google_chrome__listen_address: '127.0.0.1' +google_chrome__listen_port: 9222 + +# Lifecycle of the chrome-headless-proxy.socket unit. The Chrome service itself +# is triggered on demand by the proxy and is not managed directly. +google_chrome__service_enabled: true +google_chrome__service_state: 'started' + +google_chrome__user_data_dir: '/var/lib/chrome-headless' diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml new file mode 100644 index 00000000..741892ce --- /dev/null +++ b/roles/google_chrome/handlers/main.yml @@ -0,0 +1,20 @@ +- name: 'google_chrome: systemctl daemon-reload' + ansible.builtin.systemd: + daemon_reload: true + +# Restart chrome before the socket: in a migration from the old, non-socket-activated +# layout the running Chrome still binds the listen port. The socket can only bind +# after Chrome has been re-execed on the backend port. +- name: 'google_chrome: restart chrome-headless' + ansible.builtin.service: + name: 'chrome-headless.service' + state: 'restarted' + when: + - 'google_chrome__service_state != "stopped"' + +- name: 'google_chrome: restart chrome-headless-proxy.socket' + ansible.builtin.service: + name: 'chrome-headless-proxy.socket' + state: 'restarted' + when: + - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml new file mode 100644 index 00000000..b5fd5ff4 --- /dev/null +++ b/roles/google_chrome/meta/argument_specs.yml @@ -0,0 +1,71 @@ +argument_specs: + main: + options: + + google_chrome__extra_args__dependent_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Dependent-role injection.' + + google_chrome__extra_args__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Group-level override.' + + google_chrome__extra_args__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Host-level override.' + + google_chrome__backend_port: + type: 'int' + required: false + default: 9223 + description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' + + google_chrome__idle_timeout: + type: 'int' + required: false + default: 300 + description: 'Seconds the systemd-socket-proxyd waits without traffic before exiting (and stopping Chrome via BindsTo).' + + google_chrome__listen_address: + type: 'str' + required: false + default: '127.0.0.1' + description: 'Listen address for the Chrome remote debugging interface.' + + google_chrome__listen_port: + type: 'int' + required: false + default: 9222 + description: 'Listen port for the Chrome remote debugging interface.' + + google_chrome__service_enabled: + type: 'bool' + required: false + default: true + description: 'Enables or disables the chrome-headless.service.' + + google_chrome__service_state: + type: 'str' + required: false + default: 'started' + choices: + - 'reloaded' + - 'restarted' + - 'started' + - 'stopped' + description: 'Desired state of the chrome-headless.service.' + + google_chrome__user_data_dir: + type: 'str' + required: false + default: '/var/lib/chrome-headless' + description: 'Home directory of the chrome system user and Chrome user data directory.' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml new file mode 100644 index 00000000..00d9fa0e --- /dev/null +++ b/roles/google_chrome/tasks/main.yml @@ -0,0 +1,171 @@ +- block: + + - name: 'Set platform/version specific variables' + ansible.builtin.import_role: + name: 'shared' + tasks_from: 'platform-variables.yml' + + tags: + - 'always' + + +- name: 'Perform platform/version specific tasks' + ansible.builtin.include_tasks: '{{ __task_file }}' + when: '__task_file | length > 0' + vars: + __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' + __first_found_options: + files: + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}.yml' + paths: + - '{{ role_path }}/tasks' + skip: true + tags: + - 'always' + + +- block: + + - name: 'groupadd --system chrome' + ansible.builtin.group: + name: 'chrome' + state: 'present' + system: true + + - name: 'useradd --system chrome' + ansible.builtin.user: + name: 'chrome' + comment: 'Headless Google Chrome' + group: 'chrome' + home: '{{ google_chrome__user_data_dir }}' + shell: '/sbin/nologin' + system: true + state: 'present' + + - name: 'install --directory --owner chrome --group chrome --mode 0750 {{ google_chrome__user_data_dir }}' + ansible.builtin.file: + path: '{{ google_chrome__user_data_dir }}' + state: 'directory' + owner: 'chrome' + group: 'chrome' + mode: 0o750 + + - name: 'Install required packages' + ansible.builtin.package: + name: '{{ __google_chrome__packages }}' + state: 'present' + + tags: + - 'google_chrome' + + +- block: + + - name: 'setsebool -P systemd_socket_proxyd_connect_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_connect_any' + persistent: true + state: true + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + + - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.socket' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chrome-headless-proxy.socket.j2' + dest: '/etc/systemd/system/chrome-headless-proxy.socket' + owner: 'root' + group: 'root' + mode: 0o644 + notify: + - 'google_chrome: systemctl daemon-reload' + - 'google_chrome: restart chrome-headless-proxy.socket' + + - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chrome-headless-proxy.service.j2' + dest: '/etc/systemd/system/chrome-headless-proxy.service' + owner: 'root' + group: 'root' + mode: 0o644 + notify: + - 'google_chrome: systemctl daemon-reload' + - 'google_chrome: restart chrome-headless-proxy.socket' + + - name: 'Deploy /etc/systemd/system/chrome-headless.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chrome-headless.service.j2' + dest: '/etc/systemd/system/chrome-headless.service' + owner: 'root' + group: 'root' + mode: 0o644 + notify: + - 'google_chrome: systemctl daemon-reload' + - 'google_chrome: restart chrome-headless' + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' + loop: + - '/etc/systemd/system/chrome-headless-proxy.socket' + - '/etc/systemd/system/chrome-headless-proxy.service' + - '/etc/systemd/system/chrome-headless.service' + + tags: + - 'google_chrome' + - 'google_chrome:configure' + + +- block: + + # Force the handlers (daemon-reload, restart chrome on the new backend port, + # restart the socket) to run before the state block tries to enable the socket. + # Without this the socket would fail to load when migrating from the old + # non-socket-activated layout, because Chrome would still be holding the listen + # port at enable-time. + - name: 'Flush handlers' + ansible.builtin.meta: 'flush_handlers' + + tags: + - 'google_chrome' + - 'google_chrome:configure' + - 'google_chrome:state' + + +- block: + + - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} chrome-headless-proxy.socket' + ansible.builtin.service: + name: 'chrome-headless-proxy.socket' + enabled: '{{ google_chrome__service_enabled | bool }}' + + - name: 'systemctl {{ google_chrome__service_state }} chrome-headless-proxy.socket' + ansible.builtin.service: + name: 'chrome-headless-proxy.socket' + state: '{{ google_chrome__service_state }}' + register: '__google_chrome__service_state_result' + + tags: + - 'google_chrome' + - 'google_chrome:state' + + +- block: + + - name: 'Flush handlers so that the service is ready for dependent roles' + ansible.builtin.meta: 'flush_handlers' + + tags: + - 'google_chrome' + - 'google_chrome:configure' + - 'google_chrome:state' diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 new file mode 100644 index 00000000..35e1fe33 --- /dev/null +++ b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 @@ -0,0 +1,13 @@ +# {{ ansible_managed }} +# 2026051201 + +[Unit] +Description=Proxy to on-demand Headless Google Chrome +Requires=chrome-headless.service +After=chrome-headless.service + +[Service] +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ google_chrome__idle_timeout }} {{ google_chrome__listen_address }}:{{ google_chrome__backend_port }} +PrivateTmp=true +Restart=on-failure +RestartSec=5 diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 new file mode 100644 index 00000000..4bbbb54a --- /dev/null +++ b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +# 2026051201 + +[Unit] +Description=Socket for on-demand Headless Google Chrome + +[Socket] +ListenStream={{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} + +[Install] +WantedBy=sockets.target diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 b/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 new file mode 100644 index 00000000..73062de4 --- /dev/null +++ b/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 @@ -0,0 +1,54 @@ +# {{ ansible_managed }} +# 2026051201 + +[Unit] +Description=Headless Google Chrome +BindsTo=chrome-headless-proxy.service + +[Service] +Type=simple +User=chrome +Group=chrome +ExecStart={{ __google_chrome__binary_path }} \ + --headless=new \ + --disable-gpu \ + --no-first-run \ + --no-default-browser-check \ + --hide-scrollbars \ + --disable-dev-shm-usage \ + --remote-debugging-address={{ google_chrome__listen_address }} \ + --remote-debugging-port={{ google_chrome__backend_port }} \ + --remote-allow-origins=http://{{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} \ +{% for arg in google_chrome__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} + {{ arg['name'] }} \ +{% endfor %} + --user-data-dir={{ google_chrome__user_data_dir }} +Restart=on-failure +RestartSec=5 + +PrivateDevices=true +ProtectClock=true +NoNewPrivileges=true +RemoveIPC=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths={{ google_chrome__user_data_dir }} +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=~cgroup uts ipc +LockPersonality=true +MemoryDenyWriteExecute=false +CapabilityBoundingSet= +AmbientCapabilities= +SystemCallArchitectures=native +ProtectKernelLogs=true +ProtectHostname=true +ProtectClockSetting=true +ProtectProc=invisible +ProcSubset=pid +RestrictSUIDSGID=true +RestrictRealtime=true +UMask=0077 diff --git a/roles/google_chrome/vars/RedHat.yml b/roles/google_chrome/vars/RedHat.yml new file mode 100644 index 00000000..5f780776 --- /dev/null +++ b/roles/google_chrome/vars/RedHat.yml @@ -0,0 +1,6 @@ +__google_chrome__binary_path: '/usr/bin/google-chrome-stable' +__google_chrome__packages: + - 'gnu-free-sans-fonts' + - 'google-chrome-stable' + - 'mesa-libOSMesa' + - 'mesa-libOSMesa-devel' From 9d16d8af5e69481b2b1d92b3ce3edb4da381922b Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 12 May 2026 17:14:30 +0200 Subject: [PATCH 03/18] fix(roles/icingaweb2_module_pdfexport): wire to chrome-headless service Deploy /etc/icingaweb2/modules/pdfexport/config.ini so the module talks to the chrome-headless service over the Chrome DevTools Protocol by default (host/port), with an optional fall-back to a local Chrome binary. Move the platform-variables import into an always-tagged block so the new icingaweb2_module_pdfexport:configure tag can be run on its own. Wire the repo_epel, repo_google_chrome and google_chrome roles into both the standalone playbook and setup_icinga2_master.yml, with *__skip_* opt-outs tracking the existing pdfexport skip flag. --- CHANGELOG.md | 1 + playbooks/README.md | 5 ++ playbooks/icingaweb2_module_pdfexport.yml | 12 +++++ playbooks/setup_icinga2_master.yml | 10 ++++ roles/icingaweb2_module_pdfexport/README.md | 50 ++++++++++++++++++- .../defaults/main.yml | 9 ++++ .../meta/argument_specs.yml | 30 +++++++++++ .../tasks/main.yml | 37 ++++++++++++++ .../modules/pdfexport/config.ini.j2 | 11 ++++ 9 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 roles/icingaweb2_module_pdfexport/defaults/main.yml create mode 100644 roles/icingaweb2_module_pdfexport/meta/argument_specs.yml create mode 100644 roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf25170..5db22552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `google_chrome` and `repo_google_chrome` roles now provide a hardened `chrome-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire them up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chrome-headless.service` over the Chrome DevTools Protocol, falling back to a local Chrome binary only if `chrome_binary` is set explicitly. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/playbooks/README.md b/playbooks/README.md index 6ab15919..88d6f1cf 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -454,6 +454,9 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `icingaweb2_module_pdfexport__skip_repo_epel` +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `icingaweb2_module_pdfexport__skip_repo_google_chrome` +* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `icingaweb2_module_pdfexport__skip_google_chrome` * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport) @@ -1128,6 +1131,8 @@ Calls the following roles (in order): * [icingaweb2_theme_linuxfabrik](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_theme_linuxfabrik): `setup_icinga2_master__icingaweb2_theme_linuxfabrik__skip_role` * [icingaweb2_module_incubator](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_incubator): `setup_icinga2_master__icingaweb2_module_incubator__skip_role` * [icingaweb2_module_jira](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_jira): `setup_icinga2_master__icingaweb2_module_jira__skip_role` (default: `true`) +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `setup_icinga2_master__repo_google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) +* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `setup_icinga2_master__google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport): `setup_icinga2_master__icingaweb2_module_pdfexport__skip_role` (default: `true`) * [icingaweb2_module_vspheredb](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_vspheredb): `setup_icinga2_master__icingaweb2_module_vspheredb__skip_role` (default: `true`) * [icingaweb2_module_director](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_director): `setup_icinga2_master__icingaweb2_module_director__skip_role` diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index 82ab5a9d..ef912403 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -12,6 +12,18 @@ roles: + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' + + - role: 'linuxfabrik.lfops.repo_google_chrome' + when: + - 'not icingaweb2_module_pdfexport__skip_repo_google_chrome | d(false) | bool' + + - role: 'linuxfabrik.lfops.google_chrome' + when: + - 'not icingaweb2_module_pdfexport__skip_google_chrome | d(false) | bool' + - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' diff --git a/playbooks/setup_icinga2_master.yml b/playbooks/setup_icinga2_master.yml index c795bdd4..4b2d6324 100644 --- a/playbooks/setup_icinga2_master.yml +++ b/playbooks/setup_icinga2_master.yml @@ -7,6 +7,7 @@ setup_icinga2_master__apache_httpd__skip_injections__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_injections | d(setup_icinga2_master__apache_httpd__skip_role__internal_var) }}' setup_icinga2_master__apache_httpd__skip_role__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_role | d(false) }}' + setup_icinga2_master__google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__grafana__skip_role__internal_var: '{{ setup_icinga2_master__grafana__skip_role | d(false) }}' setup_icinga2_master__grafana_grizzly__skip_injections__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_injections | d(setup_icinga2_master__grafana_grizzly__skip_role__internal_var) }}' setup_icinga2_master__grafana_grizzly__skip_role__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_role | d(false) }}' @@ -58,6 +59,7 @@ setup_icinga2_master__redis__skip_injections__internal_var: '{{ setup_icinga2_master__redis__skip_injections | d(setup_icinga2_master__redis__skip_role__internal_var) }}' setup_icinga2_master__redis__skip_role__internal_var: '{{ setup_icinga2_master__redis__skip_role | d(false) }}' setup_icinga2_master__repo_epel__skip_role__internal_var: '{{ setup_icinga2_master__repo_epel__skip_role | d(false) }}' + setup_icinga2_master__repo_google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__repo_google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__repo_grafana__skip_role__internal_var: '{{ setup_icinga2_master__repo_grafana__skip_role | d(false) }}' setup_icinga2_master__repo_icinga__skip_role__internal_var: '{{ setup_icinga2_master__repo_icinga__skip_role | d(false) }}' setup_icinga2_master__repo_influxdb__skip_role__internal_var: '{{ setup_icinga2_master__repo_influxdb__skip_role | d(false) }}' @@ -312,6 +314,14 @@ when: - 'not setup_icinga2_master__icingaweb2_module_jira__skip_role__internal_var' + - role: 'linuxfabrik.lfops.repo_google_chrome' + when: + - 'not setup_icinga2_master__repo_google_chrome__skip_role__internal_var' + + - role: 'linuxfabrik.lfops.google_chrome' + when: + - 'not setup_icinga2_master__google_chrome__skip_role__internal_var' + - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' when: - 'not setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var' diff --git a/roles/icingaweb2_module_pdfexport/README.md b/roles/icingaweb2_module_pdfexport/README.md index 2e077d81..fcbf5a78 100644 --- a/roles/icingaweb2_module_pdfexport/README.md +++ b/roles/icingaweb2_module_pdfexport/README.md @@ -15,14 +15,17 @@ This role is tested with the following IcingaWeb2 PDF Export Module versions: * The Tarball for `icingaweb2_module_pdfexport__version` is downloaded on the Ansible controller (`delegate_to: 'localhost'`, `run_once: true`), then copied to the target. The controller therefore needs Internet access to GitHub; the target does not. * On every role run the directory `/usr/share/icingaweb2/modules/pdfexport` is overwritten with the contents of the configured version. To upgrade or downgrade the module, change `icingaweb2_module_pdfexport__version` and re-run the role. * `icingacli module enable pdfexport` is only invoked when `/etc/icingaweb2/enabledModules/pdfexport` does not yet exist (idempotent). -* This role only installs the IcingaWeb2 module itself. Any runtime dependencies of the module (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) have to be installed and configured separately. +* `/etc/icingaweb2/modules/pdfexport/config.ini` is deployed on every run. By default the module is wired to a running headless Chrome over the Chrome DevTools Protocol (CDP); set `icingaweb2_module_pdfexport__chrome_binary` to fall back to spawning Chrome locally on every export. +* This role only installs and configures the IcingaWeb2 module itself. The headless browser backend it talks to (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) is provided separately by the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. ## Mandatory Requirements * A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. * Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-pdfexport/archive/`). -* The runtime dependencies listed in the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements) (typically a headless browser binary). Install and configure them separately. +* A running headless Chrome instance providing the remote debugging interface this module talks to. This can be done using the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. + +If you use the [IcingaWeb2 PDF Export Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml), the headless Chrome backend is automatically installed for you. ## Tags @@ -30,6 +33,12 @@ This role is tested with the following IcingaWeb2 PDF Export Module versions: `icingaweb2_module_pdfexport` * Installs and enables the IcingaWeb2 PDF Export Module. +* Deploys `/etc/icingaweb2/modules/pdfexport/config.ini`. +* Triggers: none. + +`icingaweb2_module_pdfexport:configure` + +* Deploys `/etc/icingaweb2/modules/pdfexport/config.ini`. * Triggers: none. @@ -48,6 +57,43 @@ icingaweb2_module_pdfexport__version: 'v0.11.0' ``` +## Optional Role Variables + +`icingaweb2_module_pdfexport__chrome_binary` + +* Path to a local Chrome / Chromium binary. If set, the module spawns Chrome locally on every PDF export and the `chrome_host` / `chrome_port` settings are ignored. Leave empty (the default) to use the remote CDP mode. +* Type: String. +* Default: `''` + +`icingaweb2_module_pdfexport__chrome_host` + +* Address of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Type: String. +* Default: `'{{ google_chrome__listen_address | d("127.0.0.1") }}'` + +`icingaweb2_module_pdfexport__chrome_port` + +* Port of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Type: Number. +* Default: `'{{ google_chrome__listen_port | d(9222) }}'` + +`icingaweb2_module_pdfexport__force_temp_storage` + +* When `true`, the module renders every PDF to a temporary file on disk before sending it to the browser instead of streaming it directly. Useful as a workaround on memory-constrained hosts. +* Type: Bool. +* Default: `false` + +Example: + +```yaml +# optional +icingaweb2_module_pdfexport__chrome_binary: '/usr/bin/google-chrome-stable' +icingaweb2_module_pdfexport__chrome_host: '127.0.0.1' +icingaweb2_module_pdfexport__chrome_port: 9222 +icingaweb2_module_pdfexport__force_temp_storage: false +``` + + ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/icingaweb2_module_pdfexport/defaults/main.yml b/roles/icingaweb2_module_pdfexport/defaults/main.yml new file mode 100644 index 00000000..0995bfed --- /dev/null +++ b/roles/icingaweb2_module_pdfexport/defaults/main.yml @@ -0,0 +1,9 @@ +# If `icingaweb2_module_pdfexport__chrome_binary` is set, the module spawns chrome +# locally on every PDF export. Otherwise it talks to a running headless Chrome via +# the Chrome DevTools Protocol on `chrome_host` / `chrome_port` (default mode). +# Defaults pull from the linuxfabrik.lfops.google_chrome role so a single change +# there propagates here. +icingaweb2_module_pdfexport__chrome_binary: '' +icingaweb2_module_pdfexport__chrome_host: '{{ google_chrome__listen_address | d("127.0.0.1") }}' +icingaweb2_module_pdfexport__chrome_port: '{{ google_chrome__listen_port | d(9222) }}' +icingaweb2_module_pdfexport__force_temp_storage: false diff --git a/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml new file mode 100644 index 00000000..469ff4a1 --- /dev/null +++ b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml @@ -0,0 +1,30 @@ +argument_specs: + main: + options: + + icingaweb2_module_pdfexport__chrome_binary: + type: 'str' + required: false + default: '' + description: 'Path to a local Chrome/Chromium binary. If set, the module spawns Chrome locally on every export and the host/port settings are ignored.' + + icingaweb2_module_pdfexport__chrome_host: + type: 'str' + required: false + description: 'Listen address of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_address.' + + icingaweb2_module_pdfexport__chrome_port: + type: 'raw' + required: false + description: 'Listen port of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_port.' + + icingaweb2_module_pdfexport__force_temp_storage: + type: 'bool' + required: false + default: false + description: 'When true, the module renders every PDF to a temporary file on disk before sending it to the browser instead of streaming it directly. Useful as a workaround on memory-constrained hosts.' + + icingaweb2_module_pdfexport__version: + type: 'str' + required: true + description: 'The IcingaWeb2 PDF Export Module version to install. See https://github.com/Icinga/icingaweb2-module-pdfexport/releases.' diff --git a/roles/icingaweb2_module_pdfexport/tasks/main.yml b/roles/icingaweb2_module_pdfexport/tasks/main.yml index e486d0a7..333ce09f 100644 --- a/roles/icingaweb2_module_pdfexport/tasks/main.yml +++ b/roles/icingaweb2_module_pdfexport/tasks/main.yml @@ -5,6 +5,12 @@ name: 'shared' tasks_from: 'platform-variables.yml' + tags: + - 'always' + + +- block: + - name: 'mkdir -p /usr/share/icingaweb2/modules/pdfexport' ansible.builtin.file: path: '/usr/share/icingaweb2/modules/pdfexport' @@ -47,3 +53,34 @@ tags: - 'icingaweb2_module_pdfexport' + + +- block: + + - name: 'mkdir -p /etc/icingaweb2/modules/pdfexport' + ansible.builtin.file: + path: '/etc/icingaweb2/modules/pdfexport' + state: 'directory' + owner: '{{ __icingaweb2_module_pdfexport__icingaweb2_owner }}' + group: 'icingaweb2' + mode: 0o2770 + + - name: 'Deploy /etc/icingaweb2/modules/pdfexport/config.ini' + ansible.builtin.template: + backup: true + src: 'etc/icingaweb2/modules/pdfexport/config.ini.j2' + dest: '/etc/icingaweb2/modules/pdfexport/config.ini' + owner: '{{ __icingaweb2_module_pdfexport__icingaweb2_owner }}' + group: 'icingaweb2' + mode: 0o660 + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '/etc/icingaweb2/modules/pdfexport/config.ini' + + tags: + - 'icingaweb2_module_pdfexport' + - 'icingaweb2_module_pdfexport:configure' diff --git a/roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 b/roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 new file mode 100644 index 00000000..d12b6162 --- /dev/null +++ b/roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 @@ -0,0 +1,11 @@ +; {{ ansible_managed }} +; 2026051201 + +[chrome] +{% if icingaweb2_module_pdfexport__chrome_binary | length > 0 %} +binary = "{{ icingaweb2_module_pdfexport__chrome_binary }}" +{% else %} +host = "{{ icingaweb2_module_pdfexport__chrome_host }}" +port = "{{ icingaweb2_module_pdfexport__chrome_port }}" +{% endif %} +force_temp_storage = "{{ icingaweb2_module_pdfexport__force_temp_storage | bool | ternary('1', '0') }}" From d8b5add0a01d81baad6cce3e29c51e996f8ddfbc Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 10:45:20 +0200 Subject: [PATCH 04/18] docs(roles/google_chrome): explain why systemd-socket-proxyd is needed in front of Chrome --- roles/google_chrome/README.md | 2 +- roles/google_chrome/tasks/main.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index c4997988..d2417f75 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -12,7 +12,7 @@ The setup is used as a headless browser backend for tools such as the [Icinga We * Three systemd units are deployed: * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). - * `chrome-headless-proxy.service` runs `systemd-socket-proxyd` and forwards traffic to Chrome on `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. + * `chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. * On SELinux-enforcing hosts, the `systemd_socket_proxyd_connect_any` boolean is enabled so the proxy may connect to Chrome's non-standard backend port. * The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index 00d9fa0e..a29b1b52 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -64,6 +64,8 @@ - 'google_chrome' +# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. +# The SELinux boolean is required because the proxy connects to Chrome's non-standard backend port, which has no matching SELinux port type. - block: - name: 'setsebool -P systemd_socket_proxyd_connect_any on' From 0262606afe818e5244754ef586580aefbd48e902 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 11:44:37 +0200 Subject: [PATCH 05/18] fix(roles/google_chrome): also set systemd_socket_proxyd_bind_any boolean Without bind_any the chrome-headless-proxy.socket cannot bind the listen port on hosts where the port carries an unexpected SELinux port type (on Rocky/RHEL 9 the default 9222 is registered as hplip_port_t). --- CHANGELOG.md | 2 +- roles/google_chrome/README.md | 4 ++-- roles/google_chrome/tasks/main.yml | 10 +++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede2f0f3..cd4f8f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips the `systemd_socket_proxyd_connect_any` SELinux boolean on enforcing hosts so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. +* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. * **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). * **role:repo_remi**: Add `meta/argument_specs.yml` declaring the four user-facing variables (`repo_remi__basic_auth_login`, `repo_remi__enabled_php_version`, `repo_remi__enabled_redis_version`, `repo_remi__mirror_url`) so role-entry validation catches type mismatches and unknown variables. `repo_remi__basic_auth_login` is declared as `type: 'raw'` because its default in `defaults/main.yml` resolves to an empty string when no Bitwarden lookup is configured. diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index d2417f75..29d510c7 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -14,7 +14,7 @@ The setup is used as a headless browser backend for tools such as the [Icinga We * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). * `chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. -* On SELinux-enforcing hosts, the `systemd_socket_proxyd_connect_any` boolean is enabled so the proxy may connect to Chrome's non-standard backend port. +* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. * The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. @@ -32,7 +32,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Creates the `chrome` system user and group. * Installs Google Chrome along with the required runtime libraries and fonts. -* Sets the `systemd_socket_proxyd_connect_any` SELinux boolean. +* Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. * Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). * Ensures the `chrome-headless-proxy.socket` is in the desired state. * Triggers: daemon-reload, socket restart, Chrome service restart. diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index a29b1b52..2327269c 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -65,9 +65,17 @@ # Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. -# The SELinux boolean is required because the proxy connects to Chrome's non-standard backend port, which has no matching SELinux port type. +# Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chrome's non-standard backend port, which has no matching SELinux port type. - block: + - name: 'setsebool -P systemd_socket_proxyd_bind_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_bind_any' + persistent: true + state: true + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + - name: 'setsebool -P systemd_socket_proxyd_connect_any on' ansible.posix.seboolean: name: 'systemd_socket_proxyd_connect_any' From 5123f26b03edac2a6e4c1c65ea77e86e0b7ab1e3 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 17:23:07 +0200 Subject: [PATCH 06/18] refactor(roles/google_chrome): drop migration-specific handler logic Remove the comments and the chrome-headless-before-socket ordering that only existed to handle the cut-over from a pre-existing, non-socket- activated chrome service. With no such legacy unit in the wild, the regular notify chain (daemon-reload, restart socket, restart chrome on template change) is sufficient. --- roles/google_chrome/handlers/main.yml | 11 ++++------- roles/google_chrome/tasks/main.yml | 7 ++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml index 741892ce..84033af2 100644 --- a/roles/google_chrome/handlers/main.yml +++ b/roles/google_chrome/handlers/main.yml @@ -2,19 +2,16 @@ ansible.builtin.systemd: daemon_reload: true -# Restart chrome before the socket: in a migration from the old, non-socket-activated -# layout the running Chrome still binds the listen port. The socket can only bind -# after Chrome has been re-execed on the backend port. -- name: 'google_chrome: restart chrome-headless' +- name: 'google_chrome: restart chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless.service' + name: 'chrome-headless-proxy.socket' state: 'restarted' when: - 'google_chrome__service_state != "stopped"' -- name: 'google_chrome: restart chrome-headless-proxy.socket' +- name: 'google_chrome: restart chrome-headless' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'chrome-headless.service' state: 'restarted' when: - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index 2327269c..dbd123dc 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -138,11 +138,8 @@ - block: - # Force the handlers (daemon-reload, restart chrome on the new backend port, - # restart the socket) to run before the state block tries to enable the socket. - # Without this the socket would fail to load when migrating from the old - # non-socket-activated layout, because Chrome would still be holding the listen - # port at enable-time. + # Run daemon-reload before the state block, so systemctl enable/start operates + # on the freshly deployed unit definitions. - name: 'Flush handlers' ansible.builtin.meta: 'flush_handlers' From 584eefeb4471f08de9568f6891e8a55df52f3e60 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 17:26:20 +0200 Subject: [PATCH 07/18] feat(roles/repo_google_chrome): add meta/argument_specs.yml Declare the two user-facing variables (basic_auth_login as 'raw', mirror_url as 'str'), matching the pattern repo_remi established. Also sort entries in roles/google_chrome/{meta/argument_specs.yml, defaults/main.yml} alphabetically per CONTRIBUTING.md. --- roles/google_chrome/defaults/main.yml | 8 +++---- roles/google_chrome/meta/argument_specs.yml | 12 +++++----- .../meta/argument_specs.yml | 23 +++++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 roles/repo_google_chrome/meta/argument_specs.yml diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml index 7cd77812..dbf1b84b 100644 --- a/roles/google_chrome/defaults/main.yml +++ b/roles/google_chrome/defaults/main.yml @@ -1,3 +1,7 @@ +# Chrome's own listening port. The proxy connects to it on demand; clients never +# talk to it directly. +google_chrome__backend_port: 9223 + # --- list of dicts injection pattern --- # Extra Chrome CLI flags appended to the chrome-headless systemd unit. google_chrome__extra_args__dependent_var: [] @@ -12,10 +16,6 @@ google_chrome__extra_args__combined_var: '{{ ( ) | linuxfabrik.lfops.combine_lod }}' -# Chrome's own listening port. The proxy connects to it on demand; clients never -# talk to it directly. -google_chrome__backend_port: 9223 - # Idle timeout for systemd-socket-proxyd in seconds. After this much time without # active connections, the proxy exits — and Chrome stops with it via BindsTo. google_chrome__idle_timeout: 300 diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml index b5fd5ff4..f2c22db9 100644 --- a/roles/google_chrome/meta/argument_specs.yml +++ b/roles/google_chrome/meta/argument_specs.yml @@ -2,6 +2,12 @@ argument_specs: main: options: + google_chrome__backend_port: + type: 'int' + required: false + default: 9223 + description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' + google_chrome__extra_args__dependent_var: type: 'list' elements: 'dict' @@ -23,12 +29,6 @@ argument_specs: default: [] description: 'Extra Google Chrome CLI flags. Host-level override.' - google_chrome__backend_port: - type: 'int' - required: false - default: 9223 - description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' - google_chrome__idle_timeout: type: 'int' required: false diff --git a/roles/repo_google_chrome/meta/argument_specs.yml b/roles/repo_google_chrome/meta/argument_specs.yml new file mode 100644 index 00000000..2a67df11 --- /dev/null +++ b/roles/repo_google_chrome/meta/argument_specs.yml @@ -0,0 +1,23 @@ +argument_specs: + main: + options: + + repo_google_chrome__basic_auth_login: + # 'raw' rather than 'dict', because the default in defaults/main.yml + # resolves to '' (empty string) when lfops__repo_basic_auth_login is + # not set; a strict 'dict' spec would reject the empty default. + type: 'raw' + required: false + description: >- + HTTP basic auth credentials for the Google Chrome repository. + Expected as a dict with `username` and `password` keys. Typically + fed by `linuxfabrik.lfops.bitwarden_item`, which returns the full + Bitwarden item with additional keys. + + repo_google_chrome__mirror_url: + type: 'str' + required: false + description: >- + URL of a custom mirror server providing the repository. Defaults + to `lfops__repo_mirror_url`; if that is also unset, the default + upstream mirrors are used. From 32b9adeea9e1cab5278af1b2a11ac01c5f3f5270 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 15 May 2026 10:43:38 +0200 Subject: [PATCH 08/18] refactor(roles/google_chrome): tighten handler flow and tag boundaries - Split SELinux booleans into their own block, scoped to `google_chrome` only, so `google_chrome:configure` is limited to unit-file deployment as documented in the README. - Move daemon-reload from a handler into a regular task, gated by `is changed` on the three deploy tasks. The state block now runs with the freshly reloaded unit definitions without needing an intermediate `flush_handlers`, and the restart-socket handler can rely on `__google_chrome__service_state_result is not changed` (with an `is not defined` fallback for tag-restricted runs) to skip the redundant restart right after a fresh service start. - Drop the `restart chrome-headless` handler. Changes to the proxy or Chrome service unit only need a daemon-reload now; they take effect on the next socket-activation cycle. Only socket-template changes still trigger an immediate restart, because that unit holds the externally-visible listen port. - Fix descriptions for `google_chrome__service_enabled` and `google_chrome__service_state` in `meta/argument_specs.yml`: both manage the `chrome-headless-proxy.socket` unit, not `chrome-headless.service`. - Drop `mesa-libOSMesa-devel` from the runtime package list; the runtime library `mesa-libOSMesa` stays. --- roles/google_chrome/README.md | 4 +- roles/google_chrome/handlers/main.yml | 22 +++++----- roles/google_chrome/meta/argument_specs.yml | 4 +- roles/google_chrome/tasks/main.yml | 47 ++++++++++----------- roles/google_chrome/vars/RedHat.yml | 1 - 5 files changed, 38 insertions(+), 40 deletions(-) diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index 29d510c7..b5077002 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -35,12 +35,12 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. * Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). * Ensures the `chrome-headless-proxy.socket` is in the desired state. -* Triggers: daemon-reload, socket restart, Chrome service restart. +* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:configure` * Deploys the three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). -* Triggers: daemon-reload, socket restart, Chrome service restart. +* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:state` diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml index 84033af2..f48793dd 100644 --- a/roles/google_chrome/handlers/main.yml +++ b/roles/google_chrome/handlers/main.yml @@ -1,17 +1,17 @@ -- name: 'google_chrome: systemctl daemon-reload' - ansible.builtin.systemd: - daemon_reload: true - +# Only socket-template changes trigger an immediate restart, because the socket unit +# is what binds the externally-visible listen_address:listen_port. Changes to the +# proxy or Chrome service templates only need daemon-reload: the running proxy and +# Chrome process keep going with the old settings until the next idle timeout, and +# the next socket-activation cycle re-spawns them with the updated unit files. +# +# `is not defined` covers the `--tags google_chrome:configure` run, where the state +# block is skipped and __google_chrome__service_state_result is never registered. +# `is not changed` covers the normal flow: skip the restart if the state task just +# (re-)started the socket. - name: 'google_chrome: restart chrome-headless-proxy.socket' ansible.builtin.service: name: 'chrome-headless-proxy.socket' state: 'restarted' when: - - 'google_chrome__service_state != "stopped"' - -- name: 'google_chrome: restart chrome-headless' - ansible.builtin.service: - name: 'chrome-headless.service' - state: 'restarted' - when: + - '__google_chrome__service_state_result is not defined or __google_chrome__service_state_result is not changed' - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml index f2c22db9..b4d2c15a 100644 --- a/roles/google_chrome/meta/argument_specs.yml +++ b/roles/google_chrome/meta/argument_specs.yml @@ -51,7 +51,7 @@ argument_specs: type: 'bool' required: false default: true - description: 'Enables or disables the chrome-headless.service.' + description: 'Enables or disables the chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__service_state: type: 'str' @@ -62,7 +62,7 @@ argument_specs: - 'restarted' - 'started' - 'stopped' - description: 'Desired state of the chrome-headless.service.' + description: 'Desired state of the chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__user_data_dir: type: 'str' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index dbd123dc..2f75df89 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -73,16 +73,21 @@ name: 'systemd_socket_proxyd_bind_any' persistent: true state: true - when: - - 'ansible_facts["selinux"]["status"] != "disabled"' - name: 'setsebool -P systemd_socket_proxyd_connect_any on' ansible.posix.seboolean: name: 'systemd_socket_proxyd_connect_any' persistent: true state: true - when: - - 'ansible_facts["selinux"]["status"] != "disabled"' + + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + + tags: + - 'google_chrome' + + +- block: - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.socket' ansible.builtin.template: @@ -92,8 +97,8 @@ owner: 'root' group: 'root' mode: 0o644 + register: '__google_chrome__deploy_socket_result' notify: - - 'google_chrome: systemctl daemon-reload' - 'google_chrome: restart chrome-headless-proxy.socket' - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.service' @@ -104,9 +109,7 @@ owner: 'root' group: 'root' mode: 0o644 - notify: - - 'google_chrome: systemctl daemon-reload' - - 'google_chrome: restart chrome-headless-proxy.socket' + register: '__google_chrome__deploy_proxy_result' - name: 'Deploy /etc/systemd/system/chrome-headless.service' ansible.builtin.template: @@ -116,9 +119,18 @@ owner: 'root' group: 'root' mode: 0o644 - notify: - - 'google_chrome: systemctl daemon-reload' - - 'google_chrome: restart chrome-headless' + register: '__google_chrome__deploy_chrome_result' + + # Run daemon-reload as a regular task (not as a handler), so it runs before the + # state block below and so the restart-socket handler can rely on the registered + # __google_chrome__service_state_result to skip redundant restarts. + - name: 'systemctl daemon-reload' + ansible.builtin.systemd: + daemon_reload: true + when: + - '__google_chrome__deploy_socket_result is changed or + __google_chrome__deploy_proxy_result is changed or + __google_chrome__deploy_chrome_result is changed' - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' ansible.builtin.include_role: @@ -136,19 +148,6 @@ - 'google_chrome:configure' -- block: - - # Run daemon-reload before the state block, so systemctl enable/start operates - # on the freshly deployed unit definitions. - - name: 'Flush handlers' - ansible.builtin.meta: 'flush_handlers' - - tags: - - 'google_chrome' - - 'google_chrome:configure' - - 'google_chrome:state' - - - block: - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} chrome-headless-proxy.socket' diff --git a/roles/google_chrome/vars/RedHat.yml b/roles/google_chrome/vars/RedHat.yml index 5f780776..f79f18d3 100644 --- a/roles/google_chrome/vars/RedHat.yml +++ b/roles/google_chrome/vars/RedHat.yml @@ -3,4 +3,3 @@ __google_chrome__packages: - 'gnu-free-sans-fonts' - 'google-chrome-stable' - 'mesa-libOSMesa' - - 'mesa-libOSMesa-devel' From f524c8c149d5b7628217824409a20844c8d13f76 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Wed, 20 May 2026 10:54:45 +0200 Subject: [PATCH 09/18] refactor(roles/google_chrome): rename systemd units and wire CRB repo --- playbooks/README.md | 2 + playbooks/google_chrome.yml | 12 +++ playbooks/icingaweb2_module_pdfexport.yml | 12 +++ roles/google_chrome/README.md | 37 +++++----- roles/google_chrome/defaults/main.yml | 25 ++----- roles/google_chrome/handlers/main.yml | 4 +- roles/google_chrome/meta/argument_specs.yml | 6 +- roles/google_chrome/tasks/main.yml | 74 +++++++------------ ...> google-chrome-headless-proxy.service.j2} | 6 +- ...=> google-chrome-headless-proxy.socket.j2} | 2 +- ...e.j2 => google-chrome-headless.service.j2} | 4 +- .../defaults/main.yml | 5 -- 12 files changed, 88 insertions(+), 101 deletions(-) rename roles/google_chrome/templates/etc/systemd/system/{chrome-headless-proxy.service.j2 => google-chrome-headless-proxy.service.j2} (77%) rename roles/google_chrome/templates/etc/systemd/system/{chrome-headless-proxy.socket.j2 => google-chrome-headless-proxy.socket.j2} (94%) rename roles/google_chrome/templates/etc/systemd/system/{chrome-headless.service.j2 => google-chrome-headless.service.j2} (96%) diff --git a/playbooks/README.md b/playbooks/README.md index 6ff59812..0841b4be 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -342,6 +342,7 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `google_chrome__skip_repo_baseos` * [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `google_chrome__skip_repo_epel` * [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `google_chrome__skip_repo_google_chrome` * [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) @@ -454,6 +455,7 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `icingaweb2_module_pdfexport__skip_repo_baseos` * [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `icingaweb2_module_pdfexport__skip_repo_epel` * [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `icingaweb2_module_pdfexport__skip_repo_google_chrome` * [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `icingaweb2_module_pdfexport__skip_google_chrome` diff --git a/playbooks/google_chrome.yml b/playbooks/google_chrome.yml index f959bf4c..c029f420 100644 --- a/playbooks/google_chrome.yml +++ b/playbooks/google_chrome.yml @@ -12,6 +12,18 @@ roles: + # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB + # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the + # equivalent (PowerTools) ships with the repo_epel repo file. + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky"' + - 'ansible_facts["distribution_major_version"] in ["9", "10"]' + - 'not google_chrome__skip_repo_baseos | d(false) | bool' + - role: 'linuxfabrik.lfops.repo_epel' when: - 'not google_chrome__skip_repo_epel | d(false) | bool' diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index ef912403..fea35a6a 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -12,6 +12,18 @@ roles: + # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB + # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the + # equivalent (PowerTools) ships with the repo_epel repo file. + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky"' + - 'ansible_facts["distribution_major_version"] in ["9", "10"]' + - 'not icingaweb2_module_pdfexport__skip_repo_baseos | d(false) | bool' + - role: 'linuxfabrik.lfops.repo_epel' when: - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index b5077002..286dc6ec 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -1,6 +1,6 @@ # Ansible Role linuxfabrik.lfops.google_chrome -This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. +This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `google-chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). @@ -11,15 +11,16 @@ The setup is used as a headless browser backend for tools such as the [Icinga We ## How the Role Behaves * Three systemd units are deployed: - * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). - * `chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. - * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. -* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. -* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. + * `google-chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). + * `google-chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. + * `google-chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly. The proxy triggers it via `Requires=`. +* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `google-chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. +* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `google-chrome-headless-proxy.socket` unit, not the Chrome service directly. ## Mandatory Requirements +* Enable the CRB repository (PowerTools on EL8), which provides `mesa-libOSMesa`. On EL9 this can be done using the [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) role; on EL8 it ships with the EPEL repository file. * Enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. * Enable the Google Chrome repository. This can be done using the [linuxfabrik.lfops.repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) role. @@ -33,18 +34,18 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Creates the `chrome` system user and group. * Installs Google Chrome along with the required runtime libraries and fonts. * Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. -* Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). -* Ensures the `chrome-headless-proxy.socket` is in the desired state. -* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. +* Deploys all three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). +* Ensures the `google-chrome-headless-proxy.socket` is in the desired state. +* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:configure` -* Deploys the three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). -* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. +* Deploys the three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). +* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:state` -* Manages the `chrome-headless-proxy.socket` state (start, stop, enable, disable). +* Manages the `google-chrome-headless-proxy.socket` state (start, stop, enable, disable). * Triggers: none. @@ -58,7 +59,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo `google_chrome__extra_args__host_var` / `google_chrome__extra_args__group_var` -* Additional Chrome CLI flags appended to the `ExecStart` line of `chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. +* Additional Chrome CLI flags appended to the `ExecStart` line of `google-chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. * Type: List of dictionaries. * Default: `[]` * Subkeys: @@ -76,7 +77,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo `google_chrome__idle_timeout` -* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1–2 seconds of cold-start latency. +* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `google-chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1-2 seconds of cold-start latency. * Type: Number. * Default: `300` @@ -94,13 +95,13 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo `google_chrome__service_enabled` -* Enables or disables the `chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. +* Enables or disables the `google-chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. * Type: Bool. * Default: `true` `google_chrome__service_state` -* Changes the state of the `chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. +* Changes the state of the `google-chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. * Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. * Default: `'started'` @@ -108,7 +109,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Home directory of the `chrome` system user and Chrome user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chrome, and as the writable path exposed via systemd `ReadWritePaths=`. * Type: String. -* Default: `'/var/lib/chrome-headless'` +* Default: `'/var/lib/google-chrome-headless'` Example: ```yaml @@ -122,7 +123,7 @@ google_chrome__listen_address: '127.0.0.1' google_chrome__listen_port: 9222 google_chrome__service_enabled: true google_chrome__service_state: 'started' -google_chrome__user_data_dir: '/var/lib/chrome-headless' +google_chrome__user_data_dir: '/var/lib/google-chrome-headless' ``` diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml index dbf1b84b..f8c539e7 100644 --- a/roles/google_chrome/defaults/main.yml +++ b/roles/google_chrome/defaults/main.yml @@ -1,13 +1,4 @@ -# Chrome's own listening port. The proxy connects to it on demand; clients never -# talk to it directly. google_chrome__backend_port: 9223 - -# --- list of dicts injection pattern --- -# Extra Chrome CLI flags appended to the chrome-headless systemd unit. -google_chrome__extra_args__dependent_var: [] -google_chrome__extra_args__group_var: [] -google_chrome__extra_args__host_var: [] -google_chrome__extra_args__role_var: [] google_chrome__extra_args__combined_var: '{{ ( google_chrome__extra_args__role_var + google_chrome__extra_args__dependent_var + @@ -15,19 +6,13 @@ google_chrome__extra_args__combined_var: '{{ ( google_chrome__extra_args__host_var ) | linuxfabrik.lfops.combine_lod }}' - -# Idle timeout for systemd-socket-proxyd in seconds. After this much time without -# active connections, the proxy exits — and Chrome stops with it via BindsTo. +google_chrome__extra_args__dependent_var: [] +google_chrome__extra_args__group_var: [] +google_chrome__extra_args__host_var: [] +google_chrome__extra_args__role_var: [] google_chrome__idle_timeout: 300 - -# External listening endpoint exposed by the chrome-headless-proxy.socket unit. -# This is what clients (Apache, the pdfexport module, ...) connect to. google_chrome__listen_address: '127.0.0.1' google_chrome__listen_port: 9222 - -# Lifecycle of the chrome-headless-proxy.socket unit. The Chrome service itself -# is triggered on demand by the proxy and is not managed directly. google_chrome__service_enabled: true google_chrome__service_state: 'started' - -google_chrome__user_data_dir: '/var/lib/chrome-headless' +google_chrome__user_data_dir: '/var/lib/google-chrome-headless' diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml index f48793dd..11272b6b 100644 --- a/roles/google_chrome/handlers/main.yml +++ b/roles/google_chrome/handlers/main.yml @@ -8,9 +8,9 @@ # block is skipped and __google_chrome__service_state_result is never registered. # `is not changed` covers the normal flow: skip the restart if the state task just # (re-)started the socket. -- name: 'google_chrome: restart chrome-headless-proxy.socket' +- name: 'google_chrome: restart google-chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'google-chrome-headless-proxy.socket' state: 'restarted' when: - '__google_chrome__service_state_result is not defined or __google_chrome__service_state_result is not changed' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml index b4d2c15a..cccab91a 100644 --- a/roles/google_chrome/meta/argument_specs.yml +++ b/roles/google_chrome/meta/argument_specs.yml @@ -51,7 +51,7 @@ argument_specs: type: 'bool' required: false default: true - description: 'Enables or disables the chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' + description: 'Enables or disables the google-chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__service_state: type: 'str' @@ -62,10 +62,10 @@ argument_specs: - 'restarted' - 'started' - 'stopped' - description: 'Desired state of the chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' + description: 'Desired state of the google-chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__user_data_dir: type: 'str' required: false - default: '/var/lib/chrome-headless' + default: '/var/lib/google-chrome-headless' description: 'Home directory of the chrome system user and Chrome user data directory.' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index 2f75df89..21373f9d 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -9,26 +9,6 @@ - 'always' -- name: 'Perform platform/version specific tasks' - ansible.builtin.include_tasks: '{{ __task_file }}' - when: '__task_file | length > 0' - vars: - __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' - __first_found_options: - files: - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}.yml' - paths: - - '{{ role_path }}/tasks' - skip: true - tags: - - 'always' - - - block: - name: 'groupadd --system chrome' @@ -64,7 +44,7 @@ - 'google_chrome' -# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. +# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on google-chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. # Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chrome's non-standard backend port, which has no matching SELinux port type. - block: @@ -80,47 +60,58 @@ persistent: true state: true + # block when: - 'ansible_facts["selinux"]["status"] != "disabled"' - tags: - 'google_chrome' - block: - - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.socket' + - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.socket' ansible.builtin.template: backup: true - src: 'etc/systemd/system/chrome-headless-proxy.socket.j2' - dest: '/etc/systemd/system/chrome-headless-proxy.socket' + src: 'etc/systemd/system/google-chrome-headless-proxy.socket.j2' + dest: '/etc/systemd/system/google-chrome-headless-proxy.socket' owner: 'root' group: 'root' mode: 0o644 register: '__google_chrome__deploy_socket_result' notify: - - 'google_chrome: restart chrome-headless-proxy.socket' + - 'google_chrome: restart google-chrome-headless-proxy.socket' - - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.service' + - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.service' ansible.builtin.template: backup: true - src: 'etc/systemd/system/chrome-headless-proxy.service.j2' - dest: '/etc/systemd/system/chrome-headless-proxy.service' + src: 'etc/systemd/system/google-chrome-headless-proxy.service.j2' + dest: '/etc/systemd/system/google-chrome-headless-proxy.service' owner: 'root' group: 'root' mode: 0o644 register: '__google_chrome__deploy_proxy_result' - - name: 'Deploy /etc/systemd/system/chrome-headless.service' + - name: 'Deploy /etc/systemd/system/google-chrome-headless.service' ansible.builtin.template: backup: true - src: 'etc/systemd/system/chrome-headless.service.j2' - dest: '/etc/systemd/system/chrome-headless.service' + src: 'etc/systemd/system/google-chrome-headless.service.j2' + dest: '/etc/systemd/system/google-chrome-headless.service' owner: 'root' group: 'root' mode: 0o644 register: '__google_chrome__deploy_chrome_result' + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' + loop: + - '/etc/systemd/system/google-chrome-headless-proxy.socket' + - '/etc/systemd/system/google-chrome-headless-proxy.service' + - '/etc/systemd/system/google-chrome-headless.service' + # Run daemon-reload as a regular task (not as a handler), so it runs before the # state block below and so the restart-socket handler can rely on the registered # __google_chrome__service_state_result to skip redundant restarts. @@ -132,17 +123,6 @@ __google_chrome__deploy_proxy_result is changed or __google_chrome__deploy_chrome_result is changed' - - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' - ansible.builtin.include_role: - name: 'shared' - tasks_from: 'remove-rpmnew-rpmsave.yml' - vars: - shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' - loop: - - '/etc/systemd/system/chrome-headless-proxy.socket' - - '/etc/systemd/system/chrome-headless-proxy.service' - - '/etc/systemd/system/chrome-headless.service' - tags: - 'google_chrome' - 'google_chrome:configure' @@ -150,14 +130,14 @@ - block: - - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} chrome-headless-proxy.socket' + - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} google-chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'google-chrome-headless-proxy.socket' enabled: '{{ google_chrome__service_enabled | bool }}' - - name: 'systemctl {{ google_chrome__service_state }} chrome-headless-proxy.socket' + - name: 'systemctl {{ google_chrome__service_state }} google-chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'google-chrome-headless-proxy.socket' state: '{{ google_chrome__service_state }}' register: '__google_chrome__service_state_result' diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 similarity index 77% rename from roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 rename to roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 index 35e1fe33..a51e3b47 100644 --- a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 +++ b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 @@ -1,10 +1,10 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052001 [Unit] Description=Proxy to on-demand Headless Google Chrome -Requires=chrome-headless.service -After=chrome-headless.service +Requires=google-chrome-headless.service +After=google-chrome-headless.service [Service] ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ google_chrome__idle_timeout }} {{ google_chrome__listen_address }}:{{ google_chrome__backend_port }} diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 similarity index 94% rename from roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 rename to roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 index 4bbbb54a..eb7e5c64 100644 --- a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 +++ b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052001 [Unit] Description=Socket for on-demand Headless Google Chrome diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 similarity index 96% rename from roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 rename to roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 index 73062de4..cfff2f55 100644 --- a/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 +++ b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 @@ -1,9 +1,9 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052001 [Unit] Description=Headless Google Chrome -BindsTo=chrome-headless-proxy.service +BindsTo=google-chrome-headless-proxy.service [Service] Type=simple diff --git a/roles/icingaweb2_module_pdfexport/defaults/main.yml b/roles/icingaweb2_module_pdfexport/defaults/main.yml index 0995bfed..1bd3a3ef 100644 --- a/roles/icingaweb2_module_pdfexport/defaults/main.yml +++ b/roles/icingaweb2_module_pdfexport/defaults/main.yml @@ -1,8 +1,3 @@ -# If `icingaweb2_module_pdfexport__chrome_binary` is set, the module spawns chrome -# locally on every PDF export. Otherwise it talks to a running headless Chrome via -# the Chrome DevTools Protocol on `chrome_host` / `chrome_port` (default mode). -# Defaults pull from the linuxfabrik.lfops.google_chrome role so a single change -# there propagates here. icingaweb2_module_pdfexport__chrome_binary: '' icingaweb2_module_pdfexport__chrome_host: '{{ google_chrome__listen_address | d("127.0.0.1") }}' icingaweb2_module_pdfexport__chrome_port: '{{ google_chrome__listen_port | d(9222) }}' From 49efef411c003bd451cdb252e7d7eed1730a25dd Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 16:26:33 +0200 Subject: [PATCH 10/18] refactor(roles/chromium_headless): replace google_chrome with EPEL chromium-headless --- CHANGELOG.md | 5 +- COMPATIBILITY.md | 3 +- playbooks/README.md | 32 ++-- playbooks/all.yml | 3 +- ...oogle_chrome.yml => chromium_headless.yml} | 10 +- playbooks/google_chrome.yml | 43 ----- playbooks/icingaweb2_module_pdfexport.yml | 20 +-- playbooks/setup_icinga2_master.yml | 11 +- roles/chromium_headless/README.md | 135 +++++++++++++++ roles/chromium_headless/defaults/main.yml | 18 ++ roles/chromium_headless/handlers/main.yml | 17 ++ .../chromium_headless/meta/argument_specs.yml | 71 ++++++++ roles/chromium_headless/tasks/main.yml | 157 ++++++++++++++++++ .../system/chromium-headless-proxy.service.j2 | 18 ++ .../system/chromium-headless-proxy.socket.j2 | 11 ++ .../system/chromium-headless.service.j2 | 57 +++++++ roles/chromium_headless/vars/RedHat.yml | 4 + roles/google_chrome/README.md | 137 --------------- roles/google_chrome/defaults/main.yml | 18 -- roles/google_chrome/handlers/main.yml | 17 -- roles/google_chrome/meta/argument_specs.yml | 71 -------- roles/google_chrome/tasks/main.yml | 157 ------------------ .../google-chrome-headless-proxy.service.j2 | 13 -- .../google-chrome-headless-proxy.socket.j2 | 11 -- .../system/google-chrome-headless.service.j2 | 54 ------ roles/google_chrome/vars/RedHat.yml | 5 - roles/icingaweb2_module_pdfexport/README.md | 18 +- .../defaults/main.yml | 4 +- .../meta/argument_specs.yml | 6 +- roles/repo_google_chrome/README.md | 48 ------ roles/repo_google_chrome/defaults/main.yml | 2 - .../meta/argument_specs.yml | 23 --- roles/repo_google_chrome/tasks/RedHat.yml | 40 ----- roles/repo_google_chrome/tasks/main.yml | 18 -- .../etc/yum.repos.d/google-chrome.repo.j2 | 20 --- 35 files changed, 528 insertions(+), 749 deletions(-) rename playbooks/{repo_google_chrome.yml => chromium_headless.yml} (53%) delete mode 100644 playbooks/google_chrome.yml create mode 100644 roles/chromium_headless/README.md create mode 100644 roles/chromium_headless/defaults/main.yml create mode 100644 roles/chromium_headless/handlers/main.yml create mode 100644 roles/chromium_headless/meta/argument_specs.yml create mode 100644 roles/chromium_headless/tasks/main.yml create mode 100644 roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 create mode 100644 roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 create mode 100644 roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 create mode 100644 roles/chromium_headless/vars/RedHat.yml delete mode 100644 roles/google_chrome/README.md delete mode 100644 roles/google_chrome/defaults/main.yml delete mode 100644 roles/google_chrome/handlers/main.yml delete mode 100644 roles/google_chrome/meta/argument_specs.yml delete mode 100644 roles/google_chrome/tasks/main.yml delete mode 100644 roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 delete mode 100644 roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 delete mode 100644 roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 delete mode 100644 roles/google_chrome/vars/RedHat.yml delete mode 100644 roles/repo_google_chrome/README.md delete mode 100644 roles/repo_google_chrome/defaults/main.yml delete mode 100644 roles/repo_google_chrome/meta/argument_specs.yml delete mode 100644 roles/repo_google_chrome/tasks/RedHat.yml delete mode 100644 roles/repo_google_chrome/tasks/main.yml delete mode 100644 roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 929b76d2..553fb093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,10 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. +* **role:chromium_headless**: New role. Installs the headless Chromium shell (`chromium-headless` from EPEL) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chromium-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chromium service, wired with `BindsTo`). Chromium is started on the first incoming connection and stopped again after `chromium_headless__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chromium on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to, without pulling in Google's proprietary repository. * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. -* **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). * **role:repo_remi**: Add `meta/argument_specs.yml` declaring the four user-facing variables (`repo_remi__basic_auth_login`, `repo_remi__enabled_php_version`, `repo_remi__enabled_redis_version`, `repo_remi__mirror_url`) so role-entry validation catches type mismatches and unknown variables. `repo_remi__basic_auth_login` is declared as `type: 'raw'` because its default in `defaults/main.yml` resolves to an empty string when no Bitwarden lookup is configured. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). @@ -65,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `google_chrome` and `repo_google_chrome` roles now provide a hardened `chrome-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire them up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chrome-headless.service` over the Chrome DevTools Protocol, falling back to a local Chrome binary only if `chrome_binary` is set explicitly. +* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `chromium_headless` role now provides a hardened `chromium-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire it up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chromium-headless.service` over the Chrome DevTools Protocol, falling back to a local Chromium binary only if `chrome_binary` is set explicitly. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index c25a6004..99413fbd 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -16,6 +16,7 @@ Which Ansible role is proven to run on which OS? | bind | | | x | x | x | | | | | | blocky | | | x | x | (x) | | | | | | borg_local | | | x | (x) | (x) | | | | | +| chromium_headless | | | x | x | (x) | | | | | | chrony | | | x | x | x | | | | | | clamav | | | x | x | (x) | | | | | | cloud_init | (x) | (x) | x | x | x | (x) | (x) | (x) | | @@ -42,7 +43,6 @@ Which Ansible role is proven to run on which OS? | gitlab_ce | | | x | (x) | (x) | | | | | | glances | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | glpi_agent | | | x | x | (x) | | | | | -| google_chrome | | | x | x | (x) | | | | | | grafana | | | x | x | x | | | | | | grafana_grizzly | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | grav | | | x | (x) | (x) | | | | | @@ -129,7 +129,6 @@ Which Ansible role is proven to run on which OS? | repo_epel | | | x | x | x | | | | | | repo_gitlab_ce | | | x | (x) | (x) | | | | | | repo_gitlab_runner | | | x | (x) | (x) | | | | | -| repo_google_chrome | | | x | x | (x) | | | | | | repo_grafana | x | x | x | x | (x) | (x) | (x) | (x) | | | repo_graylog | x | x | x | (x) | (x) | (x) | (x) | (x) | | | repo_icinga | x | x | x | x | x | x | (x) | (x) | | diff --git a/playbooks/README.md b/playbooks/README.md index 0841b4be..810a660f 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -109,6 +109,14 @@ Calls the following roles (in order): * [borg_local](https://github.com/Linuxfabrik/lfops/tree/main/roles/borg_local) +## chromium_headless.yml + +Calls the following roles (in order): + +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `chromium_headless__skip_repo_epel` +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) + + ## chrony.yml Calls the following roles (in order): @@ -338,16 +346,6 @@ Calls the following roles (in order): * [glpi_agent](https://github.com/Linuxfabrik/lfops/tree/main/roles/glpi_agent) -## google_chrome.yml - -Calls the following roles (in order): - -* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `google_chrome__skip_repo_baseos` -* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `google_chrome__skip_repo_epel` -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `google_chrome__skip_repo_google_chrome` -* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) - - ## grafana.yml Calls the following roles (in order): @@ -455,10 +453,8 @@ Calls the following roles (in order): Calls the following roles (in order): -* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `icingaweb2_module_pdfexport__skip_repo_baseos` * [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `icingaweb2_module_pdfexport__skip_repo_epel` -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `icingaweb2_module_pdfexport__skip_repo_google_chrome` -* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `icingaweb2_module_pdfexport__skip_google_chrome` +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless): `icingaweb2_module_pdfexport__skip_chromium_headless` * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport) @@ -854,13 +850,6 @@ Calls the following roles (in order): * [repo_gitlab_runner](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_gitlab_runner) -## repo_google_chrome.yml - -Calls the following roles (in order): - -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) - - ## repo_grafana.yml Calls the following roles (in order): @@ -1118,8 +1107,7 @@ Calls the following roles (in order): * [icingaweb2_theme_linuxfabrik](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_theme_linuxfabrik): `setup_icinga2_master__icingaweb2_theme_linuxfabrik__skip_role` * [icingaweb2_module_incubator](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_incubator): `setup_icinga2_master__icingaweb2_module_incubator__skip_role` * [icingaweb2_module_jira](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_jira): `setup_icinga2_master__icingaweb2_module_jira__skip_role` (default: `true`) -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `setup_icinga2_master__repo_google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) -* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `setup_icinga2_master__google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless): `setup_icinga2_master__chromium_headless__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport): `setup_icinga2_master__icingaweb2_module_pdfexport__skip_role` (default: `true`) * [icingaweb2_module_vspheredb](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_vspheredb): `setup_icinga2_master__icingaweb2_module_vspheredb__skip_role` (default: `true`) * [icingaweb2_module_director](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_director): `setup_icinga2_master__icingaweb2_module_director__skip_role` diff --git a/playbooks/all.yml b/playbooks/all.yml index 96cda61d..5d66638c 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -10,6 +10,7 @@ - import_playbook: 'bind.yml' - import_playbook: 'blocky.yml' - import_playbook: 'borg_local.yml' +- import_playbook: 'chromium_headless.yml' - import_playbook: 'chrony.yml' - import_playbook: 'clamav.yml' - import_playbook: 'cloud_init.yml' @@ -36,7 +37,6 @@ - import_playbook: 'gitlab_ce.yml' - import_playbook: 'glances.yml' - import_playbook: 'glpi_agent.yml' -- import_playbook: 'google_chrome.yml' - import_playbook: 'grafana.yml' - import_playbook: 'grafana_grizzly.yml' - import_playbook: 'haveged.yml' @@ -102,7 +102,6 @@ - import_playbook: 'repo_epel.yml' - import_playbook: 'repo_gitlab_ce.yml' - import_playbook: 'repo_gitlab_runner.yml' -- import_playbook: 'repo_google_chrome.yml' - import_playbook: 'repo_grafana.yml' - import_playbook: 'repo_graylog.yml' - import_playbook: 'repo_icinga.yml' diff --git a/playbooks/repo_google_chrome.yml b/playbooks/chromium_headless.yml similarity index 53% rename from playbooks/repo_google_chrome.yml rename to playbooks/chromium_headless.yml index f19a1b89..0500b191 100644 --- a/playbooks/repo_google_chrome.yml +++ b/playbooks/chromium_headless.yml @@ -1,6 +1,6 @@ -- name: 'Playbook linuxfabrik.lfops.repo_google_chrome' +- name: 'Playbook linuxfabrik.lfops.chromium_headless' hosts: - - 'lfops_repo_google_chrome' + - 'lfops_chromium_headless' pre_tasks: - ansible.builtin.import_role: @@ -12,7 +12,11 @@ roles: - - role: 'linuxfabrik.lfops.repo_google_chrome' + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'not chromium_headless__skip_repo_epel | d(false) | bool' + + - role: 'linuxfabrik.lfops.chromium_headless' post_tasks: diff --git a/playbooks/google_chrome.yml b/playbooks/google_chrome.yml deleted file mode 100644 index c029f420..00000000 --- a/playbooks/google_chrome.yml +++ /dev/null @@ -1,43 +0,0 @@ -- name: 'Playbook linuxfabrik.lfops.google_chrome' - hosts: - - 'lfops_google_chrome' - - pre_tasks: - - ansible.builtin.import_role: - name: 'shared' - tasks_from: 'log-start.yml' - tags: - - 'always' - - - roles: - - # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB - # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the - # equivalent (PowerTools) ships with the repo_epel repo file. - - role: 'linuxfabrik.lfops.repo_baseos' - repo_baseos__crb_repo_enabled__dependent_var: '{{ - repo_epel__repo_baseos__crb_repo_enabled__dependent_var - }}' - when: - - 'ansible_facts["distribution"] == "Rocky"' - - 'ansible_facts["distribution_major_version"] in ["9", "10"]' - - 'not google_chrome__skip_repo_baseos | d(false) | bool' - - - role: 'linuxfabrik.lfops.repo_epel' - when: - - 'not google_chrome__skip_repo_epel | d(false) | bool' - - - role: 'linuxfabrik.lfops.repo_google_chrome' - when: - - 'not google_chrome__skip_repo_google_chrome | d(false) | bool' - - - role: 'linuxfabrik.lfops.google_chrome' - - - post_tasks: - - ansible.builtin.import_role: - name: 'shared' - tasks_from: 'log-end.yml' - tags: - - 'always' diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index fea35a6a..57ce3f4e 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -12,29 +12,13 @@ roles: - # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB - # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the - # equivalent (PowerTools) ships with the repo_epel repo file. - - role: 'linuxfabrik.lfops.repo_baseos' - repo_baseos__crb_repo_enabled__dependent_var: '{{ - repo_epel__repo_baseos__crb_repo_enabled__dependent_var - }}' - when: - - 'ansible_facts["distribution"] == "Rocky"' - - 'ansible_facts["distribution_major_version"] in ["9", "10"]' - - 'not icingaweb2_module_pdfexport__skip_repo_baseos | d(false) | bool' - - role: 'linuxfabrik.lfops.repo_epel' when: - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' - - role: 'linuxfabrik.lfops.repo_google_chrome' - when: - - 'not icingaweb2_module_pdfexport__skip_repo_google_chrome | d(false) | bool' - - - role: 'linuxfabrik.lfops.google_chrome' + - role: 'linuxfabrik.lfops.chromium_headless' when: - - 'not icingaweb2_module_pdfexport__skip_google_chrome | d(false) | bool' + - 'not icingaweb2_module_pdfexport__skip_chromium_headless | d(false) | bool' - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' diff --git a/playbooks/setup_icinga2_master.yml b/playbooks/setup_icinga2_master.yml index 4b2d6324..68a66a9a 100644 --- a/playbooks/setup_icinga2_master.yml +++ b/playbooks/setup_icinga2_master.yml @@ -7,7 +7,7 @@ setup_icinga2_master__apache_httpd__skip_injections__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_injections | d(setup_icinga2_master__apache_httpd__skip_role__internal_var) }}' setup_icinga2_master__apache_httpd__skip_role__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_role | d(false) }}' - setup_icinga2_master__google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' + setup_icinga2_master__chromium_headless__skip_role__internal_var: '{{ setup_icinga2_master__chromium_headless__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__grafana__skip_role__internal_var: '{{ setup_icinga2_master__grafana__skip_role | d(false) }}' setup_icinga2_master__grafana_grizzly__skip_injections__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_injections | d(setup_icinga2_master__grafana_grizzly__skip_role__internal_var) }}' setup_icinga2_master__grafana_grizzly__skip_role__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_role | d(false) }}' @@ -59,7 +59,6 @@ setup_icinga2_master__redis__skip_injections__internal_var: '{{ setup_icinga2_master__redis__skip_injections | d(setup_icinga2_master__redis__skip_role__internal_var) }}' setup_icinga2_master__redis__skip_role__internal_var: '{{ setup_icinga2_master__redis__skip_role | d(false) }}' setup_icinga2_master__repo_epel__skip_role__internal_var: '{{ setup_icinga2_master__repo_epel__skip_role | d(false) }}' - setup_icinga2_master__repo_google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__repo_google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__repo_grafana__skip_role__internal_var: '{{ setup_icinga2_master__repo_grafana__skip_role | d(false) }}' setup_icinga2_master__repo_icinga__skip_role__internal_var: '{{ setup_icinga2_master__repo_icinga__skip_role | d(false) }}' setup_icinga2_master__repo_influxdb__skip_role__internal_var: '{{ setup_icinga2_master__repo_influxdb__skip_role | d(false) }}' @@ -314,13 +313,9 @@ when: - 'not setup_icinga2_master__icingaweb2_module_jira__skip_role__internal_var' - - role: 'linuxfabrik.lfops.repo_google_chrome' + - role: 'linuxfabrik.lfops.chromium_headless' when: - - 'not setup_icinga2_master__repo_google_chrome__skip_role__internal_var' - - - role: 'linuxfabrik.lfops.google_chrome' - when: - - 'not setup_icinga2_master__google_chrome__skip_role__internal_var' + - 'not setup_icinga2_master__chromium_headless__skip_role__internal_var' - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' when: diff --git a/roles/chromium_headless/README.md b/roles/chromium_headless/README.md new file mode 100644 index 00000000..f83f377e --- /dev/null +++ b/roles/chromium_headless/README.md @@ -0,0 +1,135 @@ +# Ansible Role linuxfabrik.lfops.chromium_headless + +This role installs the headless [Chromium](https://www.chromium.org/) shell together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `chromium-headless` systemd service. Clients connect to a configurable TCP socket; Chromium is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. + +The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). + + +*Available since LFOps `6.0.2`.* + + +## How the Role Behaves + +* Three systemd units are deployed: + * `chromium-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). + * `chromium-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chromium (Chromium itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. On EL8 (systemd 239) the underlying `--exit-idle-time` option does not exist, so the idle shutdown is skipped there and the backend stays resident once activated; on-demand start still works. + * `chromium-headless.service` runs the actual Chromium process under the `chromium` system user. Its start job is held until Chromium's debugging port actually accepts connections, so the proxy (ordered after it) never races ahead and fails the first request. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chromium stops too; the SIGTERM-triggered exit is treated as clean so this does not mark the unit failed. It is **not** enabled on boot and must not be started directly. The proxy triggers it via `Requires=`. +* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `chromium-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chromium's non-standard backend port. +* The service-lifecycle variables (`chromium_headless__service_enabled`, `__service_state`) manage the `chromium-headless-proxy.socket` unit, not the Chromium service directly. + + +## Mandatory Requirements + +* Enable the EPEL repository, which provides `chromium-headless`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. + +If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/chromium_headless.yml), this is automatically done for you. + + +## Tags + +`chromium_headless` + +* Creates the `chromium` system user and group. +* Installs Chromium along with the required runtime libraries and fonts. +* Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. +* Deploys all three systemd units (`chromium-headless-proxy.socket`, `chromium-headless-proxy.service`, `chromium-headless.service`). +* Ensures the `chromium-headless-proxy.socket` is in the desired state. +* Triggers: daemon-reload on any unit-file change; socket restart only on `chromium-headless-proxy.socket` changes. Changes to the proxy or Chromium service unit file take effect on the next socket-activation cycle. + +`chromium_headless:configure` + +* Deploys the three systemd units (`chromium-headless-proxy.socket`, `chromium-headless-proxy.service`, `chromium-headless.service`). +* Triggers: daemon-reload on any unit-file change; socket restart only on `chromium-headless-proxy.socket` changes. Changes to the proxy or Chromium service unit file take effect on the next socket-activation cycle. + +`chromium_headless:state` + +* Manages the `chromium-headless-proxy.socket` state (start, stop, enable, disable). +* Triggers: none. + + +## Optional Role Variables + +`chromium_headless__backend_port` + +* Internal port Chromium itself listens on. The proxy forwards traffic from `listen_port` to this port. Only meaningful to change if `listen_port` and `backend_port` would otherwise collide. +* Type: Number. +* Default: `9223` + +`chromium_headless__extra_args__host_var` / `chromium_headless__extra_args__group_var` + +* Additional Chromium CLI flags appended to the `ExecStart` line of `chromium-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `name`: + + * Mandatory. The CLI flag, including any leading dashes and value (e.g. `--window-size=1920,1080`). + * Type: String. + + * `state`: + + * Optional. `present` or `absent`. + * Type: String. + * Default: `'present'` + +`chromium_headless__idle_timeout` + +* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `chromium-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1-2 seconds of cold-start latency. Has no effect on EL8, whose systemd (239) lacks the `--exit-idle-time` option; the backend stays resident there. +* Type: Number. +* Default: `300` + +`chromium_headless__listen_address` + +* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chromium enforces TLS or authentication. +* Type: String. +* Default: `'127.0.0.1'` + +`chromium_headless__listen_port` + +* Port the proxy socket listens on. This is the endpoint clients connect to. +* Type: Number. +* Default: `9222` + +`chromium_headless__service_enabled` + +* Enables or disables the `chromium-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. +* Type: Bool. +* Default: `true` + +`chromium_headless__service_state` + +* Changes the state of the `chromium-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. +* Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. +* Default: `'started'` + +`chromium_headless__user_data_dir` + +* Home directory of the `chromium` system user and Chromium user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chromium, and as the writable path exposed via systemd `ReadWritePaths=`. +* Type: String. +* Default: `'/var/lib/chromium-headless'` + +Example: +```yaml +# optional +chromium_headless__backend_port: 9223 +chromium_headless__extra_args__host_var: + - name: '--window-size=1920,1080' + - name: '--lang=de-CH' +chromium_headless__idle_timeout: 600 +chromium_headless__listen_address: '127.0.0.1' +chromium_headless__listen_port: 9222 +chromium_headless__service_enabled: true +chromium_headless__service_state: 'started' +chromium_headless__user_data_dir: '/var/lib/chromium-headless' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/chromium_headless/defaults/main.yml b/roles/chromium_headless/defaults/main.yml new file mode 100644 index 00000000..7073336b --- /dev/null +++ b/roles/chromium_headless/defaults/main.yml @@ -0,0 +1,18 @@ +chromium_headless__backend_port: 9223 +chromium_headless__extra_args__combined_var: '{{ ( + chromium_headless__extra_args__role_var + + chromium_headless__extra_args__dependent_var + + chromium_headless__extra_args__group_var + + chromium_headless__extra_args__host_var + ) | linuxfabrik.lfops.combine_lod + }}' +chromium_headless__extra_args__dependent_var: [] +chromium_headless__extra_args__group_var: [] +chromium_headless__extra_args__host_var: [] +chromium_headless__extra_args__role_var: [] +chromium_headless__idle_timeout: 300 +chromium_headless__listen_address: '127.0.0.1' +chromium_headless__listen_port: 9222 +chromium_headless__service_enabled: true +chromium_headless__service_state: 'started' +chromium_headless__user_data_dir: '/var/lib/chromium-headless' diff --git a/roles/chromium_headless/handlers/main.yml b/roles/chromium_headless/handlers/main.yml new file mode 100644 index 00000000..ef8cb21b --- /dev/null +++ b/roles/chromium_headless/handlers/main.yml @@ -0,0 +1,17 @@ +# Only socket-template changes trigger an immediate restart, because the socket unit +# is what binds the externally-visible listen_address:listen_port. Changes to the +# proxy or Chromium service templates only need daemon-reload: the running proxy and +# Chromium process keep going with the old settings until the next idle timeout, and +# the next socket-activation cycle re-spawns them with the updated unit files. +# +# `is not defined` covers the `--tags chromium_headless:configure` run, where the state +# block is skipped and __chromium_headless__service_state_result is never registered. +# `is not changed` covers the normal flow: skip the restart if the state task just +# (re-)started the socket. +- name: 'chromium_headless: restart chromium-headless-proxy.socket' + ansible.builtin.service: + name: 'chromium-headless-proxy.socket' + state: 'restarted' + when: + - '__chromium_headless__service_state_result is not defined or __chromium_headless__service_state_result is not changed' + - 'chromium_headless__service_state != "stopped"' diff --git a/roles/chromium_headless/meta/argument_specs.yml b/roles/chromium_headless/meta/argument_specs.yml new file mode 100644 index 00000000..f77d9561 --- /dev/null +++ b/roles/chromium_headless/meta/argument_specs.yml @@ -0,0 +1,71 @@ +argument_specs: + main: + options: + + chromium_headless__backend_port: + type: 'int' + required: false + default: 9223 + description: 'Internal port Chromium itself listens on. The proxy forwards traffic from listen_port to this port.' + + chromium_headless__extra_args__dependent_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Chromium CLI flags. Dependent-role injection.' + + chromium_headless__extra_args__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Chromium CLI flags. Group-level override.' + + chromium_headless__extra_args__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Chromium CLI flags. Host-level override.' + + chromium_headless__idle_timeout: + type: 'int' + required: false + default: 300 + description: 'Seconds the systemd-socket-proxyd waits without traffic before exiting (and stopping Chromium via BindsTo).' + + chromium_headless__listen_address: + type: 'str' + required: false + default: '127.0.0.1' + description: 'Listen address for the Chromium remote debugging interface.' + + chromium_headless__listen_port: + type: 'int' + required: false + default: 9222 + description: 'Listen port for the Chromium remote debugging interface.' + + chromium_headless__service_enabled: + type: 'bool' + required: false + default: true + description: 'Enables or disables the chromium-headless-proxy.socket unit at boot. The Chromium service itself is triggered on demand by the proxy and is not managed directly.' + + chromium_headless__service_state: + type: 'str' + required: false + default: 'started' + choices: + - 'reloaded' + - 'restarted' + - 'started' + - 'stopped' + description: 'Desired state of the chromium-headless-proxy.socket unit. The Chromium service itself is triggered on demand by the proxy and is not managed directly.' + + chromium_headless__user_data_dir: + type: 'str' + required: false + default: '/var/lib/chromium-headless' + description: 'Home directory of the chromium system user and Chromium user data directory.' diff --git a/roles/chromium_headless/tasks/main.yml b/roles/chromium_headless/tasks/main.yml new file mode 100644 index 00000000..7cd20d98 --- /dev/null +++ b/roles/chromium_headless/tasks/main.yml @@ -0,0 +1,157 @@ +- block: + + - name: 'Set platform/version specific variables' + ansible.builtin.import_role: + name: 'shared' + tasks_from: 'platform-variables.yml' + + tags: + - 'always' + + +- block: + + - name: 'groupadd --system chromium' + ansible.builtin.group: + name: 'chromium' + state: 'present' + system: true + + - name: 'useradd --system chromium' + ansible.builtin.user: + name: 'chromium' + comment: 'Headless Chromium' + group: 'chromium' + home: '{{ chromium_headless__user_data_dir }}' + shell: '/sbin/nologin' + system: true + state: 'present' + + - name: 'install --directory --owner chromium --group chromium --mode 0750 {{ chromium_headless__user_data_dir }}' + ansible.builtin.file: + path: '{{ chromium_headless__user_data_dir }}' + state: 'directory' + owner: 'chromium' + group: 'chromium' + mode: 0o750 + + - name: 'Install required packages' + ansible.builtin.package: + name: '{{ __chromium_headless__packages }}' + state: 'present' + + tags: + - 'chromium_headless' + + +# Chromium itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chromium: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chromium opens itself). --exit-idle-time on the proxy plus BindsTo= on chromium-headless.service ties Chromium's lifecycle to the proxy, so Chromium stops together with the proxy after the idle timeout. +# Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chromium's non-standard backend port, which has no matching SELinux port type. +- block: + + - name: 'setsebool -P systemd_socket_proxyd_bind_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_bind_any' + persistent: true + state: true + + - name: 'setsebool -P systemd_socket_proxyd_connect_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_connect_any' + persistent: true + state: true + + # block + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + tags: + - 'chromium_headless' + + +- block: + + - name: 'Deploy /etc/systemd/system/chromium-headless-proxy.socket' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chromium-headless-proxy.socket.j2' + dest: '/etc/systemd/system/chromium-headless-proxy.socket' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__chromium_headless__deploy_socket_result' + notify: + - 'chromium_headless: restart chromium-headless-proxy.socket' + + - name: 'Deploy /etc/systemd/system/chromium-headless-proxy.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chromium-headless-proxy.service.j2' + dest: '/etc/systemd/system/chromium-headless-proxy.service' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__chromium_headless__deploy_proxy_result' + + - name: 'Deploy /etc/systemd/system/chromium-headless.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chromium-headless.service.j2' + dest: '/etc/systemd/system/chromium-headless.service' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__chromium_headless__deploy_chrome_result' + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' + loop: + - '/etc/systemd/system/chromium-headless-proxy.socket' + - '/etc/systemd/system/chromium-headless-proxy.service' + - '/etc/systemd/system/chromium-headless.service' + + # Run daemon-reload as a regular task (not as a handler), so it runs before the + # state block below and so the restart-socket handler can rely on the registered + # __chromium_headless__service_state_result to skip redundant restarts. + - name: 'systemctl daemon-reload' + ansible.builtin.systemd: + daemon_reload: true + when: + - '__chromium_headless__deploy_socket_result is changed or + __chromium_headless__deploy_proxy_result is changed or + __chromium_headless__deploy_chrome_result is changed' + + tags: + - 'chromium_headless' + - 'chromium_headless:configure' + + +- block: + + - name: 'systemctl {{ chromium_headless__service_enabled | bool | ternary("enable", "disable") }} chromium-headless-proxy.socket' + ansible.builtin.service: + name: 'chromium-headless-proxy.socket' + enabled: '{{ chromium_headless__service_enabled | bool }}' + + - name: 'systemctl {{ chromium_headless__service_state }} chromium-headless-proxy.socket' + ansible.builtin.service: + name: 'chromium-headless-proxy.socket' + state: '{{ chromium_headless__service_state }}' + register: '__chromium_headless__service_state_result' + + tags: + - 'chromium_headless' + - 'chromium_headless:state' + + +- block: + + - name: 'Flush handlers so that the service is ready for dependent roles' + ansible.builtin.meta: 'flush_handlers' + + tags: + - 'chromium_headless' + - 'chromium_headless:configure' + - 'chromium_headless:state' diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 new file mode 100644 index 00000000..3a7343b5 --- /dev/null +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 @@ -0,0 +1,18 @@ +# {{ ansible_managed }} +# 2026052101 + +[Unit] +Description=Proxy to on-demand Headless Chromium +Requires=chromium-headless.service +After=chromium-headless.service + +[Service] +{# --exit-idle-time was added in systemd 246. EL8 ships systemd 239 and rejects the option, so we omit it there; the proxy (and the Chromium service bound to it) then stays resident once activated. EL9 (systemd 252) and newer support it. #} +{% if ansible_facts['distribution_major_version'] | int >= 9 %} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +{% else %} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +{% endif %} +PrivateTmp=true +Restart=on-failure +RestartSec=5 diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 new file mode 100644 index 00000000..f20198f2 --- /dev/null +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +# 2026052002 + +[Unit] +Description=Socket for on-demand Headless Chromium + +[Socket] +ListenStream={{ chromium_headless__listen_address }}:{{ chromium_headless__listen_port }} + +[Install] +WantedBy=sockets.target diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 new file mode 100644 index 00000000..837df4ef --- /dev/null +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 @@ -0,0 +1,57 @@ +# {{ ansible_managed }} +# 2026052101 + +[Unit] +Description=Headless Chromium +BindsTo=chromium-headless-proxy.service + +[Service] +Type=simple +User=chromium +Group=chromium +ExecStart={{ __chromium_headless__binary_path }} \ + --headless=new \ + --disable-gpu \ + --no-first-run \ + --no-default-browser-check \ + --hide-scrollbars \ + --disable-dev-shm-usage \ + --remote-debugging-address={{ chromium_headless__listen_address }} \ + --remote-debugging-port={{ chromium_headless__backend_port }} \ + --remote-allow-origins=http://{{ chromium_headless__listen_address }}:{{ chromium_headless__listen_port }} \ +{% for arg in chromium_headless__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} + {{ arg['name'] }} \ +{% endfor %} + --user-data-dir={{ chromium_headless__user_data_dir }} +# Chromium binds its debugging port a moment after the process starts and does not implement systemd socket activation. Block the start job until the port accepts connections, otherwise systemd-socket-proxyd (ordered After= this unit) connects too early and the first request fails with "Connection refused". +ExecStartPost=/bin/bash -c 'until (echo > /dev/tcp/{{ chromium_headless__listen_address }}/{{ chromium_headless__backend_port }}) 2>/dev/null; do sleep 0.1; done' +Restart=on-failure +RestartSec=5 +# Chromium traps SIGTERM and exits 143 (128 + SIGTERM); treat that as a clean shutdown so stopping the bound proxy on idle does not mark this unit failed. +SuccessExitStatus=143 + +PrivateDevices=true +ProtectClock=true +NoNewPrivileges=true +RemoveIPC=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths={{ chromium_headless__user_data_dir }} +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=~cgroup uts ipc +LockPersonality=true +MemoryDenyWriteExecute=false +CapabilityBoundingSet= +AmbientCapabilities= +SystemCallArchitectures=native +ProtectKernelLogs=true +ProtectHostname=true +ProtectProc=invisible +ProcSubset=pid +RestrictSUIDSGID=true +RestrictRealtime=true +UMask=0077 diff --git a/roles/chromium_headless/vars/RedHat.yml b/roles/chromium_headless/vars/RedHat.yml new file mode 100644 index 00000000..d31bf926 --- /dev/null +++ b/roles/chromium_headless/vars/RedHat.yml @@ -0,0 +1,4 @@ +__chromium_headless__binary_path: '/usr/lib64/chromium-browser/headless_shell' +__chromium_headless__packages: + - 'chromium-headless' + - 'gnu-free-sans-fonts' diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md deleted file mode 100644 index 286dc6ec..00000000 --- a/roles/google_chrome/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# Ansible Role linuxfabrik.lfops.google_chrome - -This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `google-chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. - -The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). - - -*Available since LFOps `6.0.2`.* - - -## How the Role Behaves - -* Three systemd units are deployed: - * `google-chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). - * `google-chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. - * `google-chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly. The proxy triggers it via `Requires=`. -* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `google-chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. -* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `google-chrome-headless-proxy.socket` unit, not the Chrome service directly. - - -## Mandatory Requirements - -* Enable the CRB repository (PowerTools on EL8), which provides `mesa-libOSMesa`. On EL9 this can be done using the [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) role; on EL8 it ships with the EPEL repository file. -* Enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Enable the Google Chrome repository. This can be done using the [linuxfabrik.lfops.repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) role. - -If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/google_chrome.yml), this is automatically done for you. - - -## Tags - -`google_chrome` - -* Creates the `chrome` system user and group. -* Installs Google Chrome along with the required runtime libraries and fonts. -* Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. -* Deploys all three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). -* Ensures the `google-chrome-headless-proxy.socket` is in the desired state. -* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. - -`google_chrome:configure` - -* Deploys the three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). -* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. - -`google_chrome:state` - -* Manages the `google-chrome-headless-proxy.socket` state (start, stop, enable, disable). -* Triggers: none. - - -## Optional Role Variables - -`google_chrome__backend_port` - -* Internal port Chrome itself listens on. The proxy forwards traffic from `listen_port` to this port. Only meaningful to change if `listen_port` and `backend_port` would otherwise collide. -* Type: Number. -* Default: `9223` - -`google_chrome__extra_args__host_var` / `google_chrome__extra_args__group_var` - -* Additional Chrome CLI flags appended to the `ExecStart` line of `google-chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. -* Type: List of dictionaries. -* Default: `[]` -* Subkeys: - - * `name`: - - * Mandatory. The CLI flag, including any leading dashes and value (e.g. `--window-size=1920,1080`). - * Type: String. - - * `state`: - - * Optional. `present` or `absent`. - * Type: String. - * Default: `'present'` - -`google_chrome__idle_timeout` - -* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `google-chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1-2 seconds of cold-start latency. -* Type: Number. -* Default: `300` - -`google_chrome__listen_address` - -* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chrome enforces TLS or authentication. -* Type: String. -* Default: `'127.0.0.1'` - -`google_chrome__listen_port` - -* Port the proxy socket listens on. This is the endpoint clients connect to. -* Type: Number. -* Default: `9222` - -`google_chrome__service_enabled` - -* Enables or disables the `google-chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. -* Type: Bool. -* Default: `true` - -`google_chrome__service_state` - -* Changes the state of the `google-chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. -* Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. -* Default: `'started'` - -`google_chrome__user_data_dir` - -* Home directory of the `chrome` system user and Chrome user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chrome, and as the writable path exposed via systemd `ReadWritePaths=`. -* Type: String. -* Default: `'/var/lib/google-chrome-headless'` - -Example: -```yaml -# optional -google_chrome__backend_port: 9223 -google_chrome__extra_args__host_var: - - name: '--window-size=1920,1080' - - name: '--lang=de-CH' -google_chrome__idle_timeout: 600 -google_chrome__listen_address: '127.0.0.1' -google_chrome__listen_port: 9222 -google_chrome__service_enabled: true -google_chrome__service_state: 'started' -google_chrome__user_data_dir: '/var/lib/google-chrome-headless' -``` - - -## License - -[The Unlicense](https://unlicense.org/) - - -## Author Information - -[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml deleted file mode 100644 index f8c539e7..00000000 --- a/roles/google_chrome/defaults/main.yml +++ /dev/null @@ -1,18 +0,0 @@ -google_chrome__backend_port: 9223 -google_chrome__extra_args__combined_var: '{{ ( - google_chrome__extra_args__role_var + - google_chrome__extra_args__dependent_var + - google_chrome__extra_args__group_var + - google_chrome__extra_args__host_var - ) | linuxfabrik.lfops.combine_lod - }}' -google_chrome__extra_args__dependent_var: [] -google_chrome__extra_args__group_var: [] -google_chrome__extra_args__host_var: [] -google_chrome__extra_args__role_var: [] -google_chrome__idle_timeout: 300 -google_chrome__listen_address: '127.0.0.1' -google_chrome__listen_port: 9222 -google_chrome__service_enabled: true -google_chrome__service_state: 'started' -google_chrome__user_data_dir: '/var/lib/google-chrome-headless' diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml deleted file mode 100644 index 11272b6b..00000000 --- a/roles/google_chrome/handlers/main.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Only socket-template changes trigger an immediate restart, because the socket unit -# is what binds the externally-visible listen_address:listen_port. Changes to the -# proxy or Chrome service templates only need daemon-reload: the running proxy and -# Chrome process keep going with the old settings until the next idle timeout, and -# the next socket-activation cycle re-spawns them with the updated unit files. -# -# `is not defined` covers the `--tags google_chrome:configure` run, where the state -# block is skipped and __google_chrome__service_state_result is never registered. -# `is not changed` covers the normal flow: skip the restart if the state task just -# (re-)started the socket. -- name: 'google_chrome: restart google-chrome-headless-proxy.socket' - ansible.builtin.service: - name: 'google-chrome-headless-proxy.socket' - state: 'restarted' - when: - - '__google_chrome__service_state_result is not defined or __google_chrome__service_state_result is not changed' - - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml deleted file mode 100644 index cccab91a..00000000 --- a/roles/google_chrome/meta/argument_specs.yml +++ /dev/null @@ -1,71 +0,0 @@ -argument_specs: - main: - options: - - google_chrome__backend_port: - type: 'int' - required: false - default: 9223 - description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' - - google_chrome__extra_args__dependent_var: - type: 'list' - elements: 'dict' - required: false - default: [] - description: 'Extra Google Chrome CLI flags. Dependent-role injection.' - - google_chrome__extra_args__group_var: - type: 'list' - elements: 'dict' - required: false - default: [] - description: 'Extra Google Chrome CLI flags. Group-level override.' - - google_chrome__extra_args__host_var: - type: 'list' - elements: 'dict' - required: false - default: [] - description: 'Extra Google Chrome CLI flags. Host-level override.' - - google_chrome__idle_timeout: - type: 'int' - required: false - default: 300 - description: 'Seconds the systemd-socket-proxyd waits without traffic before exiting (and stopping Chrome via BindsTo).' - - google_chrome__listen_address: - type: 'str' - required: false - default: '127.0.0.1' - description: 'Listen address for the Chrome remote debugging interface.' - - google_chrome__listen_port: - type: 'int' - required: false - default: 9222 - description: 'Listen port for the Chrome remote debugging interface.' - - google_chrome__service_enabled: - type: 'bool' - required: false - default: true - description: 'Enables or disables the google-chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' - - google_chrome__service_state: - type: 'str' - required: false - default: 'started' - choices: - - 'reloaded' - - 'restarted' - - 'started' - - 'stopped' - description: 'Desired state of the google-chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' - - google_chrome__user_data_dir: - type: 'str' - required: false - default: '/var/lib/google-chrome-headless' - description: 'Home directory of the chrome system user and Chrome user data directory.' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml deleted file mode 100644 index 21373f9d..00000000 --- a/roles/google_chrome/tasks/main.yml +++ /dev/null @@ -1,157 +0,0 @@ -- block: - - - name: 'Set platform/version specific variables' - ansible.builtin.import_role: - name: 'shared' - tasks_from: 'platform-variables.yml' - - tags: - - 'always' - - -- block: - - - name: 'groupadd --system chrome' - ansible.builtin.group: - name: 'chrome' - state: 'present' - system: true - - - name: 'useradd --system chrome' - ansible.builtin.user: - name: 'chrome' - comment: 'Headless Google Chrome' - group: 'chrome' - home: '{{ google_chrome__user_data_dir }}' - shell: '/sbin/nologin' - system: true - state: 'present' - - - name: 'install --directory --owner chrome --group chrome --mode 0750 {{ google_chrome__user_data_dir }}' - ansible.builtin.file: - path: '{{ google_chrome__user_data_dir }}' - state: 'directory' - owner: 'chrome' - group: 'chrome' - mode: 0o750 - - - name: 'Install required packages' - ansible.builtin.package: - name: '{{ __google_chrome__packages }}' - state: 'present' - - tags: - - 'google_chrome' - - -# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on google-chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. -# Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chrome's non-standard backend port, which has no matching SELinux port type. -- block: - - - name: 'setsebool -P systemd_socket_proxyd_bind_any on' - ansible.posix.seboolean: - name: 'systemd_socket_proxyd_bind_any' - persistent: true - state: true - - - name: 'setsebool -P systemd_socket_proxyd_connect_any on' - ansible.posix.seboolean: - name: 'systemd_socket_proxyd_connect_any' - persistent: true - state: true - - # block - when: - - 'ansible_facts["selinux"]["status"] != "disabled"' - tags: - - 'google_chrome' - - -- block: - - - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.socket' - ansible.builtin.template: - backup: true - src: 'etc/systemd/system/google-chrome-headless-proxy.socket.j2' - dest: '/etc/systemd/system/google-chrome-headless-proxy.socket' - owner: 'root' - group: 'root' - mode: 0o644 - register: '__google_chrome__deploy_socket_result' - notify: - - 'google_chrome: restart google-chrome-headless-proxy.socket' - - - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.service' - ansible.builtin.template: - backup: true - src: 'etc/systemd/system/google-chrome-headless-proxy.service.j2' - dest: '/etc/systemd/system/google-chrome-headless-proxy.service' - owner: 'root' - group: 'root' - mode: 0o644 - register: '__google_chrome__deploy_proxy_result' - - - name: 'Deploy /etc/systemd/system/google-chrome-headless.service' - ansible.builtin.template: - backup: true - src: 'etc/systemd/system/google-chrome-headless.service.j2' - dest: '/etc/systemd/system/google-chrome-headless.service' - owner: 'root' - group: 'root' - mode: 0o644 - register: '__google_chrome__deploy_chrome_result' - - - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' - ansible.builtin.include_role: - name: 'shared' - tasks_from: 'remove-rpmnew-rpmsave.yml' - vars: - shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' - loop: - - '/etc/systemd/system/google-chrome-headless-proxy.socket' - - '/etc/systemd/system/google-chrome-headless-proxy.service' - - '/etc/systemd/system/google-chrome-headless.service' - - # Run daemon-reload as a regular task (not as a handler), so it runs before the - # state block below and so the restart-socket handler can rely on the registered - # __google_chrome__service_state_result to skip redundant restarts. - - name: 'systemctl daemon-reload' - ansible.builtin.systemd: - daemon_reload: true - when: - - '__google_chrome__deploy_socket_result is changed or - __google_chrome__deploy_proxy_result is changed or - __google_chrome__deploy_chrome_result is changed' - - tags: - - 'google_chrome' - - 'google_chrome:configure' - - -- block: - - - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} google-chrome-headless-proxy.socket' - ansible.builtin.service: - name: 'google-chrome-headless-proxy.socket' - enabled: '{{ google_chrome__service_enabled | bool }}' - - - name: 'systemctl {{ google_chrome__service_state }} google-chrome-headless-proxy.socket' - ansible.builtin.service: - name: 'google-chrome-headless-proxy.socket' - state: '{{ google_chrome__service_state }}' - register: '__google_chrome__service_state_result' - - tags: - - 'google_chrome' - - 'google_chrome:state' - - -- block: - - - name: 'Flush handlers so that the service is ready for dependent roles' - ansible.builtin.meta: 'flush_handlers' - - tags: - - 'google_chrome' - - 'google_chrome:configure' - - 'google_chrome:state' diff --git a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 deleted file mode 100644 index a51e3b47..00000000 --- a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 +++ /dev/null @@ -1,13 +0,0 @@ -# {{ ansible_managed }} -# 2026052001 - -[Unit] -Description=Proxy to on-demand Headless Google Chrome -Requires=google-chrome-headless.service -After=google-chrome-headless.service - -[Service] -ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ google_chrome__idle_timeout }} {{ google_chrome__listen_address }}:{{ google_chrome__backend_port }} -PrivateTmp=true -Restart=on-failure -RestartSec=5 diff --git a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 deleted file mode 100644 index eb7e5c64..00000000 --- a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 +++ /dev/null @@ -1,11 +0,0 @@ -# {{ ansible_managed }} -# 2026052001 - -[Unit] -Description=Socket for on-demand Headless Google Chrome - -[Socket] -ListenStream={{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} - -[Install] -WantedBy=sockets.target diff --git a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 deleted file mode 100644 index cfff2f55..00000000 --- a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 +++ /dev/null @@ -1,54 +0,0 @@ -# {{ ansible_managed }} -# 2026052001 - -[Unit] -Description=Headless Google Chrome -BindsTo=google-chrome-headless-proxy.service - -[Service] -Type=simple -User=chrome -Group=chrome -ExecStart={{ __google_chrome__binary_path }} \ - --headless=new \ - --disable-gpu \ - --no-first-run \ - --no-default-browser-check \ - --hide-scrollbars \ - --disable-dev-shm-usage \ - --remote-debugging-address={{ google_chrome__listen_address }} \ - --remote-debugging-port={{ google_chrome__backend_port }} \ - --remote-allow-origins=http://{{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} \ -{% for arg in google_chrome__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} - {{ arg['name'] }} \ -{% endfor %} - --user-data-dir={{ google_chrome__user_data_dir }} -Restart=on-failure -RestartSec=5 - -PrivateDevices=true -ProtectClock=true -NoNewPrivileges=true -RemoveIPC=true -PrivateTmp=true -ProtectSystem=strict -ProtectHome=true -ReadWritePaths={{ google_chrome__user_data_dir }} -ProtectKernelTunables=true -ProtectKernelModules=true -ProtectControlGroups=true -RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX -RestrictNamespaces=~cgroup uts ipc -LockPersonality=true -MemoryDenyWriteExecute=false -CapabilityBoundingSet= -AmbientCapabilities= -SystemCallArchitectures=native -ProtectKernelLogs=true -ProtectHostname=true -ProtectClockSetting=true -ProtectProc=invisible -ProcSubset=pid -RestrictSUIDSGID=true -RestrictRealtime=true -UMask=0077 diff --git a/roles/google_chrome/vars/RedHat.yml b/roles/google_chrome/vars/RedHat.yml deleted file mode 100644 index f79f18d3..00000000 --- a/roles/google_chrome/vars/RedHat.yml +++ /dev/null @@ -1,5 +0,0 @@ -__google_chrome__binary_path: '/usr/bin/google-chrome-stable' -__google_chrome__packages: - - 'gnu-free-sans-fonts' - - 'google-chrome-stable' - - 'mesa-libOSMesa' diff --git a/roles/icingaweb2_module_pdfexport/README.md b/roles/icingaweb2_module_pdfexport/README.md index fcbf5a78..51a028bc 100644 --- a/roles/icingaweb2_module_pdfexport/README.md +++ b/roles/icingaweb2_module_pdfexport/README.md @@ -15,17 +15,17 @@ This role is tested with the following IcingaWeb2 PDF Export Module versions: * The Tarball for `icingaweb2_module_pdfexport__version` is downloaded on the Ansible controller (`delegate_to: 'localhost'`, `run_once: true`), then copied to the target. The controller therefore needs Internet access to GitHub; the target does not. * On every role run the directory `/usr/share/icingaweb2/modules/pdfexport` is overwritten with the contents of the configured version. To upgrade or downgrade the module, change `icingaweb2_module_pdfexport__version` and re-run the role. * `icingacli module enable pdfexport` is only invoked when `/etc/icingaweb2/enabledModules/pdfexport` does not yet exist (idempotent). -* `/etc/icingaweb2/modules/pdfexport/config.ini` is deployed on every run. By default the module is wired to a running headless Chrome over the Chrome DevTools Protocol (CDP); set `icingaweb2_module_pdfexport__chrome_binary` to fall back to spawning Chrome locally on every export. -* This role only installs and configures the IcingaWeb2 module itself. The headless browser backend it talks to (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) is provided separately by the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. +* `/etc/icingaweb2/modules/pdfexport/config.ini` is deployed on every run. By default the module is wired to a running headless Chromium over the Chrome DevTools Protocol (CDP); set `icingaweb2_module_pdfexport__chrome_binary` to fall back to spawning Chromium locally on every export. +* This role only installs and configures the IcingaWeb2 module itself. The headless browser backend it talks to (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) is provided separately by the [linuxfabrik.lfops.chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) role. ## Mandatory Requirements * A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. * Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-pdfexport/archive/`). -* A running headless Chrome instance providing the remote debugging interface this module talks to. This can be done using the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. +* A running headless Chromium instance providing the remote debugging interface this module talks to. This can be done using the [linuxfabrik.lfops.chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) role. -If you use the [IcingaWeb2 PDF Export Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml), the headless Chrome backend is automatically installed for you. +If you use the [IcingaWeb2 PDF Export Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml), the headless Chromium backend is automatically installed for you. ## Tags @@ -67,15 +67,15 @@ icingaweb2_module_pdfexport__version: 'v0.11.0' `icingaweb2_module_pdfexport__chrome_host` -* Address of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Address of the headless Chromium instance the module connects to via the Chrome DevTools Protocol. * Type: String. -* Default: `'{{ google_chrome__listen_address | d("127.0.0.1") }}'` +* Default: `'{{ chromium_headless__listen_address | d("127.0.0.1") }}'` `icingaweb2_module_pdfexport__chrome_port` -* Port of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Port of the headless Chromium instance the module connects to via the Chrome DevTools Protocol. * Type: Number. -* Default: `'{{ google_chrome__listen_port | d(9222) }}'` +* Default: `'{{ chromium_headless__listen_port | d(9222) }}'` `icingaweb2_module_pdfexport__force_temp_storage` @@ -87,7 +87,7 @@ Example: ```yaml # optional -icingaweb2_module_pdfexport__chrome_binary: '/usr/bin/google-chrome-stable' +icingaweb2_module_pdfexport__chrome_binary: '/usr/lib64/chromium-browser/headless_shell' icingaweb2_module_pdfexport__chrome_host: '127.0.0.1' icingaweb2_module_pdfexport__chrome_port: 9222 icingaweb2_module_pdfexport__force_temp_storage: false diff --git a/roles/icingaweb2_module_pdfexport/defaults/main.yml b/roles/icingaweb2_module_pdfexport/defaults/main.yml index 1bd3a3ef..bc0b8ba7 100644 --- a/roles/icingaweb2_module_pdfexport/defaults/main.yml +++ b/roles/icingaweb2_module_pdfexport/defaults/main.yml @@ -1,4 +1,4 @@ icingaweb2_module_pdfexport__chrome_binary: '' -icingaweb2_module_pdfexport__chrome_host: '{{ google_chrome__listen_address | d("127.0.0.1") }}' -icingaweb2_module_pdfexport__chrome_port: '{{ google_chrome__listen_port | d(9222) }}' +icingaweb2_module_pdfexport__chrome_host: '{{ chromium_headless__listen_address | d("127.0.0.1") }}' +icingaweb2_module_pdfexport__chrome_port: '{{ chromium_headless__listen_port | d(9222) }}' icingaweb2_module_pdfexport__force_temp_storage: false diff --git a/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml index 469ff4a1..593bfc3e 100644 --- a/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml +++ b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml @@ -6,17 +6,17 @@ argument_specs: type: 'str' required: false default: '' - description: 'Path to a local Chrome/Chromium binary. If set, the module spawns Chrome locally on every export and the host/port settings are ignored.' + description: 'Path to a local Chrome/Chromium binary. If set, the module spawns Chromium locally on every export and the host/port settings are ignored.' icingaweb2_module_pdfexport__chrome_host: type: 'str' required: false - description: 'Listen address of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_address.' + description: 'Listen address of the headless Chromium instance the module connects to. Defaults to chromium_headless__listen_address.' icingaweb2_module_pdfexport__chrome_port: type: 'raw' required: false - description: 'Listen port of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_port.' + description: 'Listen port of the headless Chromium instance the module connects to. Defaults to chromium_headless__listen_port.' icingaweb2_module_pdfexport__force_temp_storage: type: 'bool' diff --git a/roles/repo_google_chrome/README.md b/roles/repo_google_chrome/README.md deleted file mode 100644 index 7da86d69..00000000 --- a/roles/repo_google_chrome/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Ansible Role linuxfabrik.lfops.repo_google_chrome - -This role deploys the package repository for [Google Chrome](https://www.google.com/chrome/) on RHEL-based distributions. - - -*Available since LFOps `6.0.2`.* - - -## Tags - -`repo_google_chrome` - -* Deploys the Google Chrome Repository. -* Triggers: none. - - -## Optional Role Variables - -`repo_google_chrome__basic_auth_login` - -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. -* Type: String. -* Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` - -`repo_google_chrome__mirror_url` - -* Set the URL to a custom mirror server providing the repository. Defaults to `lfops__repo_mirror_url` to allow easily setting the same URL for all `repo_*` roles. If `lfops__repo_mirror_url` is not set, the default mirrors of the repo are used. -* Type: String. -* Default: `'{{ lfops__repo_mirror_url | default("") }}'` - -Example: -```yaml -# optional -repo_google_chrome__basic_auth_login: - username: 'my-username' - password: 'linuxfabrik' -repo_google_chrome__mirror_url: 'https://mirror.example.com' -``` - - -## License - -[The Unlicense](https://unlicense.org/) - - -## Author Information - -[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/repo_google_chrome/defaults/main.yml b/roles/repo_google_chrome/defaults/main.yml deleted file mode 100644 index 69f976e3..00000000 --- a/roles/repo_google_chrome/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -repo_google_chrome__basic_auth_login: '{{ lfops__repo_basic_auth_login | default("") }}' -repo_google_chrome__mirror_url: '{{ lfops__repo_mirror_url | default("") }}' diff --git a/roles/repo_google_chrome/meta/argument_specs.yml b/roles/repo_google_chrome/meta/argument_specs.yml deleted file mode 100644 index 2a67df11..00000000 --- a/roles/repo_google_chrome/meta/argument_specs.yml +++ /dev/null @@ -1,23 +0,0 @@ -argument_specs: - main: - options: - - repo_google_chrome__basic_auth_login: - # 'raw' rather than 'dict', because the default in defaults/main.yml - # resolves to '' (empty string) when lfops__repo_basic_auth_login is - # not set; a strict 'dict' spec would reject the empty default. - type: 'raw' - required: false - description: >- - HTTP basic auth credentials for the Google Chrome repository. - Expected as a dict with `username` and `password` keys. Typically - fed by `linuxfabrik.lfops.bitwarden_item`, which returns the full - Bitwarden item with additional keys. - - repo_google_chrome__mirror_url: - type: 'str' - required: false - description: >- - URL of a custom mirror server providing the repository. Defaults - to `lfops__repo_mirror_url`; if that is also unset, the default - upstream mirrors are used. diff --git a/roles/repo_google_chrome/tasks/RedHat.yml b/roles/repo_google_chrome/tasks/RedHat.yml deleted file mode 100644 index 83a5f6cf..00000000 --- a/roles/repo_google_chrome/tasks/RedHat.yml +++ /dev/null @@ -1,40 +0,0 @@ -- block: - - - name: 'curl https://dl-ssl.google.com/linux/linux_signing_key.pub --output /tmp/ansible.google-chrome.key' - ansible.builtin.get_url: - url: 'https://dl-ssl.google.com/linux/linux_signing_key.pub' - dest: '/tmp/ansible.google-chrome.key' - mode: 0o644 - delegate_to: 'localhost' - become: false - run_once: true - changed_when: false # not an actual config change on the server - check_mode: false # run task even if `--check` is specified - - - name: 'copy /tmp/ansible.google-chrome.key to /etc/pki/rpm-gpg/google-chrome.key' - ansible.builtin.copy: - src: '/tmp/ansible.google-chrome.key' - dest: '/etc/pki/rpm-gpg/google-chrome.key' - owner: 'root' - group: 'root' - mode: 0o644 - - # https://www.google.com/linuxrepositories/ - - name: 'deploy the Google Chrome repo (mirror: {{ repo_google_chrome__mirror_url }})' - ansible.builtin.template: - backup: true - src: 'etc/yum.repos.d/google-chrome.repo.j2' - dest: '/etc/yum.repos.d/google-chrome.repo' - owner: 'root' - group: 'root' - mode: 0o644 - - - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' - ansible.builtin.include_role: - name: 'shared' - tasks_from: 'remove-rpmnew-rpmsave.yml' - vars: - shared__remove_rpmnew_rpmsave_config_file: '/etc/yum.repos.d/google-chrome.repo' - - tags: - - 'repo_google_chrome' diff --git a/roles/repo_google_chrome/tasks/main.yml b/roles/repo_google_chrome/tasks/main.yml deleted file mode 100644 index 4f290d1f..00000000 --- a/roles/repo_google_chrome/tasks/main.yml +++ /dev/null @@ -1,18 +0,0 @@ -- name: 'Perform platform/version specific tasks' - ansible.builtin.include_tasks: '{{ __task_file }}' - when: '__task_file | length > 0' - vars: - __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' - __first_found_options: - files: - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}.yml' - paths: - - '{{ role_path }}/tasks' - skip: true - tags: - - 'always' diff --git a/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 b/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 deleted file mode 100644 index ff8375bc..00000000 --- a/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 +++ /dev/null @@ -1,20 +0,0 @@ -# {{ ansible_managed }} -# 2026051201 - -[google-chrome] -name=google-chrome -{% if repo_google_chrome__mirror_url is defined and repo_google_chrome__mirror_url | length %} -baseurl={{ repo_google_chrome__mirror_url }}/linux/chrome/rpm/stable/$basearch -{% else %} -baseurl=https://dl.google.com/linux/chrome/rpm/stable/$basearch -{% endif %} -enabled=1 -gpgcheck=1 -repo_gpgcheck=1 -gpgkey=file:///etc/pki/rpm-gpg/google-chrome.key -sslverify=1 -sslcacert=/etc/pki/tls/certs/ca-bundle.crt -{% if repo_google_chrome__basic_auth_login is defined and repo_google_chrome__basic_auth_login | length %} -username={{ repo_google_chrome__basic_auth_login["username"] }} -password={{ repo_google_chrome__basic_auth_login["password"] }} -{% endif %} From 1415db0b3256ef1f3a40f4dfecaae22af3caeca0 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:08:22 +0200 Subject: [PATCH 11/18] fix(roles/chromium_headless): keep Chromium debugging port bound to localhost Chromium's --remote-debugging-address now always binds 127.0.0.1 instead of chromium_headless__listen_address. Only the proxy socket is meant to be the public endpoint; binding the backend to a routable listen_address exposed Chromium's unauthenticated CDP port off-host and let clients bypass the idle-managed proxy. The proxy and the ExecStartPost health check connect to 127.0.0.1 accordingly. Also documents why MemoryDenyWriteExecute must stay false (V8 JIT) and bumps the two unit-template timestamps. --- roles/chromium_headless/README.md | 2 +- .../etc/systemd/system/chromium-headless-proxy.service.j2 | 7 ++++--- .../etc/systemd/system/chromium-headless.service.j2 | 8 +++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/roles/chromium_headless/README.md b/roles/chromium_headless/README.md index f83f377e..b2935150 100644 --- a/roles/chromium_headless/README.md +++ b/roles/chromium_headless/README.md @@ -81,7 +81,7 @@ If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops `chromium_headless__listen_address` -* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chromium enforces TLS or authentication. +* Address the `chromium-headless-proxy.socket` binds to. Chromium's own debugging port always stays on `127.0.0.1` regardless of this setting, so only the proxy is reachable here. Keep this on `127.0.0.1` unless you intentionally want to expose the proxy to other hosts; it enforces neither TLS nor authentication. * Type: String. * Default: `'127.0.0.1'` diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 index 3a7343b5..cf7c5238 100644 --- a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026052101 +# 2026052102 [Unit] Description=Proxy to on-demand Headless Chromium @@ -7,11 +7,12 @@ Requires=chromium-headless.service After=chromium-headless.service [Service] +{# The proxy always connects to the backend on 127.0.0.1, never chromium_headless__listen_address: Chromium binds its unauthenticated debugging port locally only (see chromium-headless.service). #} {# --exit-idle-time was added in systemd 246. EL8 ships systemd 239 and rejects the option, so we omit it there; the proxy (and the Chromium service bound to it) then stays resident once activated. EL9 (systemd 252) and newer support it. #} {% if ansible_facts['distribution_major_version'] | int >= 9 %} -ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} 127.0.0.1:{{ chromium_headless__backend_port }} {% else %} -ExecStart=/usr/lib/systemd/systemd-socket-proxyd {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:{{ chromium_headless__backend_port }} {% endif %} PrivateTmp=true Restart=on-failure diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 index 837df4ef..92b06af0 100644 --- a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026052101 +# 2026052102 [Unit] Description=Headless Chromium @@ -9,6 +9,7 @@ BindsTo=chromium-headless-proxy.service Type=simple User=chromium Group=chromium +# The backend always binds 127.0.0.1, never chromium_headless__listen_address. Only the proxy socket is the public endpoint (it may bind a routable listen_address); Chromium's raw remote-debugging port has no authentication, so it must never be reachable off-host. The proxy connects to it locally. ExecStart={{ __chromium_headless__binary_path }} \ --headless=new \ --disable-gpu \ @@ -16,7 +17,7 @@ ExecStart={{ __chromium_headless__binary_path }} \ --no-default-browser-check \ --hide-scrollbars \ --disable-dev-shm-usage \ - --remote-debugging-address={{ chromium_headless__listen_address }} \ + --remote-debugging-address=127.0.0.1 \ --remote-debugging-port={{ chromium_headless__backend_port }} \ --remote-allow-origins=http://{{ chromium_headless__listen_address }}:{{ chromium_headless__listen_port }} \ {% for arg in chromium_headless__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} @@ -24,7 +25,7 @@ ExecStart={{ __chromium_headless__binary_path }} \ {% endfor %} --user-data-dir={{ chromium_headless__user_data_dir }} # Chromium binds its debugging port a moment after the process starts and does not implement systemd socket activation. Block the start job until the port accepts connections, otherwise systemd-socket-proxyd (ordered After= this unit) connects too early and the first request fails with "Connection refused". -ExecStartPost=/bin/bash -c 'until (echo > /dev/tcp/{{ chromium_headless__listen_address }}/{{ chromium_headless__backend_port }}) 2>/dev/null; do sleep 0.1; done' +ExecStartPost=/bin/bash -c 'until (echo > /dev/tcp/127.0.0.1/{{ chromium_headless__backend_port }}) 2>/dev/null; do sleep 0.1; done' Restart=on-failure RestartSec=5 # Chromium traps SIGTERM and exits 143 (128 + SIGTERM); treat that as a clean shutdown so stopping the bound proxy on idle does not mark this unit failed. @@ -44,6 +45,7 @@ ProtectControlGroups=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=~cgroup uts ipc LockPersonality=true +# Must stay false: Chromium's V8 JIT maps writable+executable pages, which MemoryDenyWriteExecute=true would kill at startup. MemoryDenyWriteExecute=false CapabilityBoundingSet= AmbientCapabilities= From 2f55fdf26d997a3c7908242cb114e44ef4411468 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:08:53 +0200 Subject: [PATCH 12/18] style(roles/chromium_headless): align defaults order and internal naming with example --- roles/chromium_headless/defaults/main.yml | 8 ++++---- roles/chromium_headless/tasks/main.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/roles/chromium_headless/defaults/main.yml b/roles/chromium_headless/defaults/main.yml index 7073336b..52fa488d 100644 --- a/roles/chromium_headless/defaults/main.yml +++ b/roles/chromium_headless/defaults/main.yml @@ -1,4 +1,8 @@ chromium_headless__backend_port: 9223 +chromium_headless__extra_args__dependent_var: [] +chromium_headless__extra_args__group_var: [] +chromium_headless__extra_args__host_var: [] +chromium_headless__extra_args__role_var: [] chromium_headless__extra_args__combined_var: '{{ ( chromium_headless__extra_args__role_var + chromium_headless__extra_args__dependent_var + @@ -6,10 +10,6 @@ chromium_headless__extra_args__combined_var: '{{ ( chromium_headless__extra_args__host_var ) | linuxfabrik.lfops.combine_lod }}' -chromium_headless__extra_args__dependent_var: [] -chromium_headless__extra_args__group_var: [] -chromium_headless__extra_args__host_var: [] -chromium_headless__extra_args__role_var: [] chromium_headless__idle_timeout: 300 chromium_headless__listen_address: '127.0.0.1' chromium_headless__listen_port: 9222 diff --git a/roles/chromium_headless/tasks/main.yml b/roles/chromium_headless/tasks/main.yml index 7cd20d98..c7294aaa 100644 --- a/roles/chromium_headless/tasks/main.yml +++ b/roles/chromium_headless/tasks/main.yml @@ -99,7 +99,7 @@ owner: 'root' group: 'root' mode: 0o644 - register: '__chromium_headless__deploy_chrome_result' + register: '__chromium_headless__deploy_service_result' - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' ansible.builtin.include_role: @@ -121,7 +121,7 @@ when: - '__chromium_headless__deploy_socket_result is changed or __chromium_headless__deploy_proxy_result is changed or - __chromium_headless__deploy_chrome_result is changed' + __chromium_headless__deploy_service_result is changed' tags: - 'chromium_headless' From 490d0afeecb11996f581f958c084afa4cb96c0dc Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:09:20 +0200 Subject: [PATCH 13/18] docs(changelog): condense chromium_headless and pdfexport entries --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 553fb093..8a93e9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* **role:chromium_headless**: New role. Installs the headless Chromium shell (`chromium-headless` from EPEL) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chromium-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chromium service, wired with `BindsTo`). Chromium is started on the first incoming connection and stopped again after `chromium_headless__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chromium on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to, without pulling in Google's proprietary repository. +* **role:chromium_headless**: New role. Provides a hardened, socket-activated headless Chromium backend (started on the first request, stopped again after an idle timeout, so it uses no RAM while unused) for tools such as the Icinga Web 2 PDF Export Module. Installs `chromium-headless` from EPEL instead of Google's proprietary repository. * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). @@ -64,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `chromium_headless` role now provides a hardened `chromium-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire it up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chromium-headless.service` over the Chrome DevTools Protocol, falling back to a local Chromium binary only if `chrome_binary` is set explicitly. +* **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). From bb96efe08021aab5383a99d5a373b17d0f53872e Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:10:22 +0200 Subject: [PATCH 14/18] docs(contributing): list chromium_headless under roles with special features --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71196ae8..a76985b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -672,6 +672,11 @@ The following roles use techniques that are unusual within LFOps. Roles not in t * [grav](https://github.com/Linuxfabrik/lfops/tree/main/roles/grav): Four separate `chmod` passes (files `664`, `bin/` `775`, directories `775`, plus a setgid pass on directories), each registered with `changed_when` based on the `--changes` output for idempotency. +#### systemd socket activation with an on-demand backend + +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless): Fronts a long-running daemon (Chromium, which does not implement the systemd socket-activation protocol) with a `systemd-socket-proxyd`. A `.socket` unit binds the public port, the proxy forwards to the backend on `127.0.0.1` and exits after an idle timeout, and `BindsTo=` ties the backend's lifecycle to the proxy so it starts on the first request and stops when idle. + + #### Other * [apache_solr](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_solr): Picks the matching OpenJDK package for the configured Solr major version (Solr 9 → OpenJDK 17, Solr 8 → OpenJDK 8) via a per-major-version lookup in `vars/main.yml`. From 48dcb2101c7ab03510c4bf21605e70d407377503 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:21:53 +0200 Subject: [PATCH 15/18] style(roles/icingaweb2_module_pdfexport): silence risky-file-permissions on temp-file tasks --- roles/icingaweb2_module_pdfexport/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/icingaweb2_module_pdfexport/tasks/main.yml b/roles/icingaweb2_module_pdfexport/tasks/main.yml index 333ce09f..60b519e2 100644 --- a/roles/icingaweb2_module_pdfexport/tasks/main.yml +++ b/roles/icingaweb2_module_pdfexport/tasks/main.yml @@ -19,7 +19,7 @@ group: 'icingaweb2' mode: 0o755 - - name: 'curl https://github.com/Icinga/icingaweb2-module-pdfexport/archive/{{ icingaweb2_module_pdfexport__version }}.tar.gz --output /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' + - name: 'curl https://github.com/Icinga/icingaweb2-module-pdfexport/archive/{{ icingaweb2_module_pdfexport__version }}.tar.gz --output /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' # noqa risky-file-permissions (temporary file) ansible.builtin.get_url: url: 'https://github.com/Icinga/icingaweb2-module-pdfexport/archive/{{ icingaweb2_module_pdfexport__version }}.tar.gz' dest: '/tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' @@ -28,7 +28,7 @@ run_once: true check_mode: false # run task even if `--check` is specified - - name: 'copy /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz to the server' + - name: 'copy /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz to the server' # noqa risky-file-permissions (temporary file) ansible.builtin.copy: src: '/tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' dest: '/tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' From 7a9e009af3419b5cca9e9dd3a41d55774e6b0be7 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 18:32:53 +0200 Subject: [PATCH 16/18] feat(roles/chromium_headless): add Debian support --- COMPATIBILITY.md | 2 +- playbooks/chromium_headless.yml | 1 + playbooks/icingaweb2_module_pdfexport.yml | 1 + roles/chromium_headless/README.md | 4 ++-- .../etc/systemd/system/chromium-headless-proxy.service.j2 | 6 +++--- roles/chromium_headless/vars/Debian.yml | 6 ++++++ roles/chromium_headless/vars/RedHat.yml | 2 ++ roles/chromium_headless/vars/RedHat8.yml | 2 ++ 8 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 roles/chromium_headless/vars/Debian.yml create mode 100644 roles/chromium_headless/vars/RedHat8.yml diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 99413fbd..de868c1f 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -16,7 +16,7 @@ Which Ansible role is proven to run on which OS? | bind | | | x | x | x | | | | | | blocky | | | x | x | (x) | | | | | | borg_local | | | x | (x) | (x) | | | | | -| chromium_headless | | | x | x | (x) | | | | | +| chromium_headless | x | x | x | x | (x) | | | | | | chrony | | | x | x | x | | | | | | clamav | | | x | x | (x) | | | | | | cloud_init | (x) | (x) | x | x | x | (x) | (x) | (x) | | diff --git a/playbooks/chromium_headless.yml b/playbooks/chromium_headless.yml index 0500b191..ce4ca69a 100644 --- a/playbooks/chromium_headless.yml +++ b/playbooks/chromium_headless.yml @@ -14,6 +14,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: + - 'ansible_facts["os_family"] == "RedHat"' - 'not chromium_headless__skip_repo_epel | d(false) | bool' - role: 'linuxfabrik.lfops.chromium_headless' diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index 57ce3f4e..4724eee3 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -14,6 +14,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: + - 'ansible_facts["os_family"] == "RedHat"' - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' - role: 'linuxfabrik.lfops.chromium_headless' diff --git a/roles/chromium_headless/README.md b/roles/chromium_headless/README.md index b2935150..4c292e95 100644 --- a/roles/chromium_headless/README.md +++ b/roles/chromium_headless/README.md @@ -20,9 +20,9 @@ The setup is used as a headless browser backend for tools such as the [Icinga We ## Mandatory Requirements -* Enable the EPEL repository, which provides `chromium-headless`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. +* On Red Hat-family systems, enable the EPEL repository, which provides `chromium-headless`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. On Debian, the `chromium-headless-shell` package ships in the default repositories, so no extra repository is required. -If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/chromium_headless.yml), this is automatically done for you. +If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/chromium_headless.yml), the EPEL repository is enabled for you on Red Hat-family systems. ## Tags diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 index cf7c5238..8e773f99 100644 --- a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026052102 +# 2026052103 [Unit] Description=Proxy to on-demand Headless Chromium @@ -8,8 +8,8 @@ After=chromium-headless.service [Service] {# The proxy always connects to the backend on 127.0.0.1, never chromium_headless__listen_address: Chromium binds its unauthenticated debugging port locally only (see chromium-headless.service). #} -{# --exit-idle-time was added in systemd 246. EL8 ships systemd 239 and rejects the option, so we omit it there; the proxy (and the Chromium service bound to it) then stays resident once activated. EL9 (systemd 252) and newer support it. #} -{% if ansible_facts['distribution_major_version'] | int >= 9 %} +{# --exit-idle-time was added in systemd 246. Platforms whose systemd predates it (EL8 ships 239) omit the option via __chromium_headless__proxy_exit_idle_time_supported; the proxy (and the Chromium service bound to it) then stays resident once activated. #} +{% if __chromium_headless__proxy_exit_idle_time_supported | bool %} ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} 127.0.0.1:{{ chromium_headless__backend_port }} {% else %} ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:{{ chromium_headless__backend_port }} diff --git a/roles/chromium_headless/vars/Debian.yml b/roles/chromium_headless/vars/Debian.yml new file mode 100644 index 00000000..4f995b7a --- /dev/null +++ b/roles/chromium_headless/vars/Debian.yml @@ -0,0 +1,6 @@ +__chromium_headless__binary_path: '/usr/bin/chromium-headless-shell' +__chromium_headless__packages: + - 'chromium-headless-shell' + - 'fonts-freefont-ttf' +# Debian 12 (systemd 252) and newer support systemd-socket-proxyd --exit-idle-time (added in systemd 246). +__chromium_headless__proxy_exit_idle_time_supported: true diff --git a/roles/chromium_headless/vars/RedHat.yml b/roles/chromium_headless/vars/RedHat.yml index d31bf926..f045ac30 100644 --- a/roles/chromium_headless/vars/RedHat.yml +++ b/roles/chromium_headless/vars/RedHat.yml @@ -2,3 +2,5 @@ __chromium_headless__binary_path: '/usr/lib64/chromium-browser/headless_shell' __chromium_headless__packages: - 'chromium-headless' - 'gnu-free-sans-fonts' +# systemd-socket-proxyd --exit-idle-time (systemd 246+) is available on EL9 and newer; EL8 overrides this to false in RedHat8.yml. +__chromium_headless__proxy_exit_idle_time_supported: true diff --git a/roles/chromium_headless/vars/RedHat8.yml b/roles/chromium_headless/vars/RedHat8.yml new file mode 100644 index 00000000..24046187 --- /dev/null +++ b/roles/chromium_headless/vars/RedHat8.yml @@ -0,0 +1,2 @@ +# EL8 ships systemd 239, which lacks systemd-socket-proxyd --exit-idle-time (added in systemd 246). +__chromium_headless__proxy_exit_idle_time_supported: false From 2c0661a675df2d0af2ae8714ddba0e286e566365 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 18:41:21 +0200 Subject: [PATCH 17/18] fix(roles/redis): add missing vars for Debian --- CHANGELOG.md | 1 + roles/redis/vars/Debian.yml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a93e9a8..5f08bff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. diff --git a/roles/redis/vars/Debian.yml b/roles/redis/vars/Debian.yml index dcbd8acc..86f76a62 100644 --- a/roles/redis/vars/Debian.yml +++ b/roles/redis/vars/Debian.yml @@ -1,2 +1,9 @@ +__redis__config_dir: '/etc/redis' +__redis__config_file: 'redis.conf' +__redis__data_dir: '/var/lib/redis' +__redis__module_dir: '/etc/redis/modules' +__redis__package: 'redis-server' +__redis__runtime_dir: '/var/run/redis' + redis__conf_logfile: '/var/log/redis/redis-server.log' redis__service_name: 'redis-server.service' From b58f771881526d22c58ea56c09ede21612c901b7 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 22 May 2026 11:22:44 +0200 Subject: [PATCH 18/18] docs(compatibility): correct chromium_headless tested platforms --- COMPATIBILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index de868c1f..132f6645 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -16,7 +16,7 @@ Which Ansible role is proven to run on which OS? | bind | | | x | x | x | | | | | | blocky | | | x | x | (x) | | | | | | borg_local | | | x | (x) | (x) | | | | | -| chromium_headless | x | x | x | x | (x) | | | | | +| chromium_headless | x | (x) | x | x | x | | | | | | chrony | | | x | x | x | | | | | | clamav | | | x | x | (x) | | | | | | cloud_init | (x) | (x) | x | x | x | (x) | (x) | (x) | |