diff --git a/CHANGELOG.md b/CHANGELOG.md index f4994fb3..929b76d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,8 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. +* **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). * **role:repo_remi**: Add `meta/argument_specs.yml` declaring the four user-facing variables (`repo_remi__basic_auth_login`, `repo_remi__enabled_php_version`, `repo_remi__enabled_redis_version`, `repo_remi__mirror_url`) so role-entry validation catches type mismatches and unknown variables. `repo_remi__basic_auth_login` is declared as `type: 'raw'` because its default in `defaults/main.yml` resolves to an empty string when no Bitwarden lookup is configured. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). @@ -63,6 +65,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/COMPATIBILITY.md b/COMPATIBILITY.md index 021c3f69..c25a6004 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) | | | | | @@ -128,6 +129,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 3d95e807..6ff59812 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): @@ -445,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) @@ -840,6 +852,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): @@ -1097,6 +1116,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/all.yml b/playbooks/all.yml index 067d14fd..96cda61d 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' @@ -101,6 +102,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/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/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/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/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/google_chrome/README.md b/roles/google_chrome/README.md new file mode 100644 index 00000000..b5077002 --- /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`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. + * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. +* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. +* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. + + +## 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_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. +* Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). +* Ensures the `chrome-headless-proxy.socket` is in the desired state. +* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. + +`google_chrome:configure` + +* Deploys the three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). +* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. + +`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..dbf1b84b --- /dev/null +++ b/roles/google_chrome/defaults/main.yml @@ -0,0 +1,33 @@ +# Chrome's own listening port. The proxy connects to it on demand; clients never +# talk to it directly. +google_chrome__backend_port: 9223 + +# --- list of dicts injection pattern --- +# Extra Chrome CLI flags appended to the chrome-headless systemd unit. +google_chrome__extra_args__dependent_var: [] +google_chrome__extra_args__group_var: [] +google_chrome__extra_args__host_var: [] +google_chrome__extra_args__role_var: [] +google_chrome__extra_args__combined_var: '{{ ( + google_chrome__extra_args__role_var + + google_chrome__extra_args__dependent_var + + google_chrome__extra_args__group_var + + google_chrome__extra_args__host_var + ) | linuxfabrik.lfops.combine_lod + }}' + +# Idle timeout for systemd-socket-proxyd in seconds. After this much time without +# active connections, the proxy exits — and Chrome stops with it via BindsTo. +google_chrome__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..f48793dd --- /dev/null +++ b/roles/google_chrome/handlers/main.yml @@ -0,0 +1,17 @@ +# Only socket-template changes trigger an immediate restart, because the socket unit +# is what binds the externally-visible listen_address:listen_port. Changes to the +# proxy or 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_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 new file mode 100644 index 00000000..b4d2c15a --- /dev/null +++ b/roles/google_chrome/meta/argument_specs.yml @@ -0,0 +1,71 @@ +argument_specs: + main: + options: + + google_chrome__backend_port: + type: 'int' + required: false + default: 9223 + description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' + + google_chrome__extra_args__dependent_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Dependent-role injection.' + + google_chrome__extra_args__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Group-level override.' + + google_chrome__extra_args__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Host-level override.' + + google_chrome__idle_timeout: + type: 'int' + required: false + default: 300 + description: 'Seconds the systemd-socket-proxyd waits without traffic before exiting (and stopping Chrome via BindsTo).' + + google_chrome__listen_address: + type: 'str' + required: false + default: '127.0.0.1' + description: 'Listen address for the Chrome remote debugging interface.' + + google_chrome__listen_port: + type: 'int' + required: false + default: 9222 + description: 'Listen port for the Chrome remote debugging interface.' + + google_chrome__service_enabled: + type: 'bool' + required: false + default: true + description: 'Enables or disables the chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' + + google_chrome__service_state: + type: 'str' + required: false + default: 'started' + choices: + - 'reloaded' + - 'restarted' + - 'started' + - 'stopped' + description: 'Desired state of the chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' + + google_chrome__user_data_dir: + type: 'str' + required: false + default: '/var/lib/chrome-headless' + 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..2f75df89 --- /dev/null +++ b/roles/google_chrome/tasks/main.yml @@ -0,0 +1,177 @@ +- 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' + + +# 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. +# Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chrome's non-standard backend port, which has no matching SELinux port type. +- block: + + - name: 'setsebool -P systemd_socket_proxyd_bind_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_bind_any' + persistent: true + state: true + + - name: 'setsebool -P systemd_socket_proxyd_connect_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_connect_any' + persistent: true + state: true + + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + + tags: + - 'google_chrome' + + +- block: + + - 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 + register: '__google_chrome__deploy_socket_result' + notify: + - '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 + register: '__google_chrome__deploy_proxy_result' + + - 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 + 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: + 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: + + - 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..f79f18d3 --- /dev/null +++ b/roles/google_chrome/vars/RedHat.yml @@ -0,0 +1,5 @@ +__google_chrome__binary_path: '/usr/bin/google-chrome-stable' +__google_chrome__packages: + - 'gnu-free-sans-fonts' + - 'google-chrome-stable' + - 'mesa-libOSMesa' diff --git a/roles/icingaweb2_module_pdfexport/README.md b/roles/icingaweb2_module_pdfexport/README.md index 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') }}" 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/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. 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 %}