From c7236e4596999a3d73f21e1dde50a3f147f1b200 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 12 May 2026 17:12:31 +0200 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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'