Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c7236e4
Add roles/repo_google_chrome
danyalberchtoldlf May 12, 2026
c510b8b
Add roles/google_chrome
danyalberchtoldlf May 12, 2026
9d16d8a
fix(roles/icingaweb2_module_pdfexport): wire to chrome-headless service
danyalberchtoldlf May 12, 2026
d8b5add
docs(roles/google_chrome): explain why systemd-socket-proxyd is neede…
danyalberchtoldlf May 13, 2026
ffaf007
Merge branch 'main' into feat/google-chrome-roles
danyalberchtoldlf May 13, 2026
0262606
fix(roles/google_chrome): also set systemd_socket_proxyd_bind_any boo…
danyalberchtoldlf May 13, 2026
9bc8266
Merge remote-tracking branch 'origin/main' into feat/google-chrome-roles
danyalberchtoldlf May 13, 2026
5123f26
refactor(roles/google_chrome): drop migration-specific handler logic
danyalberchtoldlf May 13, 2026
584eefe
feat(roles/repo_google_chrome): add meta/argument_specs.yml
danyalberchtoldlf May 13, 2026
32b9ade
refactor(roles/google_chrome): tighten handler flow and tag boundaries
danyalberchtoldlf May 15, 2026
41bdbbc
Merge remote-tracking branch 'origin/main' into feat/google-chrome-roles
danyalberchtoldlf May 15, 2026
f524c8c
refactor(roles/google_chrome): rename systemd units and wire CRB repo
NavidSassan May 20, 2026
49efef4
refactor(roles/chromium_headless): replace google_chrome with EPEL ch…
NavidSassan May 21, 2026
1415db0
fix(roles/chromium_headless): keep Chromium debugging port bound to l…
NavidSassan May 21, 2026
2f55fdf
style(roles/chromium_headless): align defaults order and internal nam…
NavidSassan May 21, 2026
490d0af
docs(changelog): condense chromium_headless and pdfexport entries
NavidSassan May 21, 2026
bb96efe
docs(contributing): list chromium_headless under roles with special f…
NavidSassan May 21, 2026
48dcb21
style(roles/icingaweb2_module_pdfexport): silence risky-file-permissi…
NavidSassan May 21, 2026
7a9e009
feat(roles/chromium_headless): add Debian support
NavidSassan May 21, 2026
2c0661a
fix(roles/redis): add missing vars for Debian
NavidSassan May 21, 2026
b58f771
docs(compatibility): correct chromium_headless tested platforms
NavidSassan May 22, 2026
7804934
Merge branch 'main' into feat/google-chrome-roles
NavidSassan May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* **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:graylog_datanode, role:graylog_server**: Add template for Graylog 7.1.
* **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.
Expand Down Expand Up @@ -67,6 +68,8 @@ 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:graylog_datanode**: Fix the `Conditional result ... was of type 'str'` deprecation warning.
* **role:graylog_server**: Validate that each `graylog_server__system_inputs` entry sets `global: true` or assigns a `node`. Key was marked as mandatory but not enforced. The role now aborts the `graylog_server:configure_defaults` run with a clear message.
* **role:graylog_server**: Fix the `graylog_server:configure_defaults` run aborting on Graylog 7.0+ with `Status code was 400 and not [200]` / `Unable to map property can_be_default` while creating the default index set, by removing the property from the role. Graylog 7.x dropped it and 6.x ignored it.
Expand Down
1 change: 1 addition & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | x | x | | | | |
| chrony | | | x | x | x | | | | |
| clamav | | | x | x | (x) | | | | |
| cloud_init | (x) | (x) | x | x | x | (x) | (x) | (x) | |
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,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`.
Expand Down
11 changes: 11 additions & 0 deletions playbooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -447,6 +455,8 @@ 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`
* [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)


Expand Down Expand Up @@ -1105,6 +1115,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`)
* [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`
Expand Down
1 change: 1 addition & 0 deletions playbooks/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
28 changes: 28 additions & 0 deletions playbooks/chromium_headless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
- name: 'Playbook linuxfabrik.lfops.chromium_headless'
hosts:
- 'lfops_chromium_headless'

pre_tasks:
- ansible.builtin.import_role:
name: 'shared'
tasks_from: 'log-start.yml'
tags:
- 'always'


roles:

- 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'


post_tasks:
- ansible.builtin.import_role:
name: 'shared'
tasks_from: 'log-end.yml'
tags:
- 'always'
9 changes: 9 additions & 0 deletions playbooks/icingaweb2_module_pdfexport.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@

roles:

- 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'
when:
- 'not icingaweb2_module_pdfexport__skip_chromium_headless | d(false) | bool'

- role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport'


Expand Down
5 changes: 5 additions & 0 deletions playbooks/setup_icinga2_master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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__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) }}'
Expand Down Expand Up @@ -313,6 +314,10 @@
when:
- 'not setup_icinga2_master__icingaweb2_module_jira__skip_role__internal_var'

- role: 'linuxfabrik.lfops.chromium_headless'
when:
- 'not setup_icinga2_master__chromium_headless__skip_role__internal_var'

- role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport'
when:
- 'not setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var'
Expand Down
135 changes: 135 additions & 0 deletions roles/chromium_headless/README.md
Original file line number Diff line number Diff line change
@@ -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 in the next LFOps release.*


## 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.


## Dependent Roles

Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables.

* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). It provides the `chromium-headless` package. On Debian, the `chromium-headless-shell` package ships in the default repositories, so no extra repository is required.


## 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 `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'`

`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)
18 changes: 18 additions & 0 deletions roles/chromium_headless/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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 +
chromium_headless__extra_args__group_var +
chromium_headless__extra_args__host_var
) | linuxfabrik.lfops.combine_lod
}}'
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'
17 changes: 17 additions & 0 deletions roles/chromium_headless/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -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"'
Loading
Loading