Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* **role:glpi_agent**: Add optional scheduled database inventory. Set `glpi_agent__database_inventory_enabled` and `glpi_agent__database_inventory_login` to run `glpi-agent --partial=database` as a dedicated read-only database user via a systemd timer; the `database` inventory category is then disabled on the always-on daemon, which otherwise connects to the local database as root on every cycle. Also add `glpi_agent__conf_no_category` to disable arbitrary inventory categories and `glpi_agent__database_inventory_on_calendar` to set the schedule.
* **role:php**: Add `php__version` to declare which PHP version a Debian host runs, for example `'8.4'`. With the sury repo enabled, the unversioned metapackages (`php-cli`, `php-fpm`, `php-curl`, ...) follow whatever sury currently declares as its default, so an ordinary `apt upgrade` migrates the host to a new major PHP version without anyone deciding to, leaving the old version installed and running alongside. Declaring a version makes the role install the versioned packages instead, pin the `php`, `phar` and `phar.phar` alternatives to it, and purge the stacks of all other versions on `php:update`. A major version change is then an explicit act: raise `php__version` and run `php:update`. Leave it empty to keep the previous behaviour. Has no effect on RedHat, where the module stream pins the version at repo level.
* **playbook:php**: Now runs the `repo_sury` role on Debian (skip with `php__skip_repo_sury`), since sury is what makes any PHP version other than the distribution's own available in the first place.
* **role:fail2ban**: Add `fail2ban__filters__*_var` (combined-var pattern, `unique_key="filename"`) so users can deploy custom filter definitions from the inventory in the same generic shape as `fail2ban__jails__*_var`. Item subkeys mirror jails (`filename`, `template`, `state`, optional `raw`); a new `raw.conf.j2` filter template accepts arbitrary `[Definition]` blocks. The built-in `apache-dos` and `portscan` filters move into `fail2ban__filters__role_var` (no behavior change; they can now be opted out via `state: 'absent'`).
Expand Down
48 changes: 47 additions & 1 deletion roles/glpi_agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ This role installs and configures the [GLPI Agent](https://glpi-agent.readthedoc
*Available since LFOps `3.0.0`.*


## How the Role Behaves

The scheduled database inventory runs as a one-shot via a systemd timer instead of as part of the always-on daemon, because the agent refuses `--credentials` in daemon mode and offers no equivalent configuration directive.

Enabling the scheduled inventory also disables the `database` category on the always-on daemon. Without credentials the daemon falls back to its default database access (root via the local socket) on every cycle, which the dedicated account is meant to replace.

The one-shot must not inherit that `no-category` setting, otherwise the agent would discard its own `--partial=database` run. It therefore gets its own configuration file at `/etc/glpi-agent/db-inventory.cfg`, passed via `--conf-file`, which carries the same settings without the `no-category` directive. Both files are rendered from the same template, so they cannot drift apart. Note that the file lives outside `conf.d`, since the daemon would otherwise read it too.

The credentials are assembled from `glpi_agent__database_inventory_login` into the `0600` environment file `/etc/glpi-agent/db-inventory.env` and pulled into the unit via `EnvironmentFile`, so they are not part of the unit file itself. systemd expands the variable into the command line when it starts the job, so the password is readable in the process list for as long as the one-shot runs. Use a dedicated read-only database account, and keep the password in Ansible Vault.


## 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.
Expand All @@ -22,9 +33,14 @@ Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/RE

`glpi_agent:configure`

* Deploys the configuration file.
* Deploys the configuration files.
* Triggers: glpi-agent.service restart.

`glpi_agent:database_inventory`

* Deploys the optional database inventory timer, service and credentials file.
* Triggers: systemctl daemon-reload.

`glpi_agent:state`

* Manages the state of the systemd service.
Expand Down Expand Up @@ -54,6 +70,12 @@ glpi_agent__conf_server: 'https://glpi.example.com'
* Type: String.
* Default: `'/tmp'`

`glpi_agent__conf_no_category`

* List of inventory categories to disable via the `no-category` directive (for example `database` to stop the agent from inventorying local databases). When `glpi_agent__database_inventory_enabled` is `true`, the `database` category is added automatically.
* Type: List.
* Default: `[]`

`glpi_agent__conf_no_ssl_check`

* Ignore self-signed certificates of the server.
Expand All @@ -66,6 +88,24 @@ glpi_agent__conf_server: 'https://glpi.example.com'
* Type: String.
* Default: unset

`glpi_agent__database_inventory_enabled`

* Deploys a systemd timer that periodically runs `glpi-agent --partial=database`, and disables the `database` category on the always-on daemon. When `false`, the timer, service, configuration and environment file are removed.
* Type: Bool.
* Default: `false`

`glpi_agent__database_inventory_login`

* The database account used by the scheduled database inventory. Mandatory when `glpi_agent__database_inventory_enabled` is `true`.
* Type: Dictionary.
* Default: none

`glpi_agent__database_inventory_on_calendar`

* The `OnCalendar` schedule of the database inventory timer. The timer also applies a `RandomizedDelaySec` of 30 minutes, so the job does not collide with fixed maintenance windows such as system updates.
* Type: String.
* Default: `'*-*-* 00/6:17:00'`

`glpi_agent__service_enabled`

* Enables or disables the service, analogous to `systemctl enable/disable`.
Expand All @@ -88,8 +128,14 @@ Example:
```yaml
# optional
glpi_agent__conf_local: '/tmp'
glpi_agent__conf_no_category: []
glpi_agent__conf_no_ssl_check: false
glpi_agent__conf_ssl_fingerprint: 'sha256$...'
glpi_agent__database_inventory_enabled: false
glpi_agent__database_inventory_login:
username: 'glpi-reader'
password: 'linuxfabrik'
glpi_agent__database_inventory_on_calendar: '*-*-* 00/6:17:00'
glpi_agent__service_enabled: true
glpi_agent__service_state: 'started'
glpi_agent__version: 'latest'
Expand Down
3 changes: 3 additions & 0 deletions roles/glpi_agent/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
glpi_agent__conf_local: '/tmp'
glpi_agent__conf_no_category: []
glpi_agent__conf_no_ssl_check: false
glpi_agent__database_inventory_enabled: false
glpi_agent__database_inventory_on_calendar: '*-*-* 00/6:17:00'
glpi_agent__service_enabled: true
glpi_agent__service_state: '{{ glpi_agent__service_enabled | bool | ternary("started", "stopped") }}'
glpi_agent__version: 'latest'
5 changes: 5 additions & 0 deletions roles/glpi_agent/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@
- 'not (lfops__skip_restart_handlers | d(false) | bool)'
- '__glpi_agent__service_state_result is not defined or __glpi_agent__service_state_result is not changed'
- 'glpi_agent__service_state != "stopped"'


- name: 'glpi_agent: systemctl daemon-reload'
ansible.builtin.systemd:
daemon_reload: true
114 changes: 114 additions & 0 deletions roles/glpi_agent/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
- block:

- name: 'Check variable constraints'
ansible.builtin.assert:
that:
- 'glpi_agent__database_inventory_login is defined'
- 'glpi_agent__database_inventory_login["username"] | d("") | length > 0'
- 'glpi_agent__database_inventory_login["password"] | d("") | length > 0'
quiet: true
fail_msg: 'glpi_agent__database_inventory_login needs a "username" and a "password" when glpi_agent__database_inventory_enabled is true.'
when:
- 'glpi_agent__database_inventory_enabled | bool'

tags:
# use 'always' so the validation runs even when the role runs with a specific tag.
- 'always'


- block:

- name: 'Store the github release version'
Expand Down Expand Up @@ -67,13 +85,109 @@
owner: 'root'
group: 'root'
mode: 0o640
vars:
__glpi_agent__render_no_category: true
notify: 'glpi_agent: restart glpi-agent'

# the database inventory one-shot is started with --conf-file and therefore reads this file
# instead of agent.cfg and its conf.d include, so that it does not inherit
# "no-category = database". deployed here and not under 'glpi_agent:database_inventory', so
# that a configure run keeps both files in sync.
- name: 'Deploy /etc/glpi-agent/db-inventory.cfg'
ansible.builtin.template:
backup: true
src: 'etc/glpi-agent/conf.d/z00-linuxfabrik.cfg.j2'
dest: '/etc/glpi-agent/db-inventory.cfg'
owner: 'root'
group: 'root'
mode: 0o640
vars:
__glpi_agent__render_no_category: false
when:
- 'glpi_agent__database_inventory_enabled | bool'

tags:
- 'glpi_agent'
- 'glpi_agent:configure'


# glpi_agent__database_inventory_enabled == true
- block:

- name: 'Deploy /etc/glpi-agent/db-inventory.env'
ansible.builtin.template:
backup: false # contains secrets
src: 'etc/glpi-agent/db-inventory.env.j2'
dest: '/etc/glpi-agent/db-inventory.env'
owner: 'root'
group: 'root'
mode: 0o600 # contains secrets

- name: 'Deploy /etc/systemd/system/glpi-agent-database-inventory.service'
ansible.builtin.template:
backup: true
src: 'etc/systemd/system/glpi-agent-database-inventory.service.j2'
dest: '/etc/systemd/system/glpi-agent-database-inventory.service'
owner: 'root'
group: 'root'
mode: 0o644
notify: 'glpi_agent: systemctl daemon-reload'

- name: 'Deploy /etc/systemd/system/glpi-agent-database-inventory.timer'
ansible.builtin.template:
backup: true
src: 'etc/systemd/system/glpi-agent-database-inventory.timer.j2'
dest: '/etc/systemd/system/glpi-agent-database-inventory.timer'
owner: 'root'
group: 'root'
mode: 0o644
notify: 'glpi_agent: systemctl daemon-reload'

- name: 'systemctl enable --now glpi-agent-database-inventory.timer'
ansible.builtin.systemd:
daemon_reload: true
enabled: true
name: 'glpi-agent-database-inventory.timer'
state: 'started'

# block
when: 'glpi_agent__database_inventory_enabled | bool'
tags:
- 'glpi_agent'
- 'glpi_agent:database_inventory'


# glpi_agent__database_inventory_enabled == false
- block:

- name: 'systemctl disable --now glpi-agent-database-inventory.timer'
ansible.builtin.systemd:
enabled: false
name: 'glpi-agent-database-inventory.timer'
state: 'stopped'
register: '__glpi_agent__database_inventory_timer_result'
failed_when:
- '__glpi_agent__database_inventory_timer_result is failed'
- '"Could not find" not in (__glpi_agent__database_inventory_timer_result["msg"] | d(""))'

- name: 'rm -f {{ item }}'
ansible.builtin.file:
path: '{{ item }}'
state: 'absent'
loop:
- '/etc/glpi-agent/db-inventory.cfg'
- '/etc/glpi-agent/db-inventory.env'
- '/etc/systemd/system/glpi-agent-database-inventory.service'
- '/etc/systemd/system/glpi-agent-database-inventory.timer'
notify: 'glpi_agent: systemctl daemon-reload'

# block
when: 'not (glpi_agent__database_inventory_enabled | bool)'
tags:
- 'glpi_agent'
- 'glpi_agent:database_inventory'


- block:

- name: 'systemctl {{ glpi_agent__service_enabled | bool | ternary("enable", "disable") }} glpi-agent.service'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
# {{ ansible_managed }}
# 2024101701
# 2026072801
{#
This template is rendered to two destinations: conf.d/z00-linuxfabrik.cfg for the always-on
daemon, and db-inventory.cfg for the scheduled database inventory one-shot, which is started
with --conf-file so it reads the latter instead of agent.cfg and its conf.d include.
The one-shot must not inherit "no-category = database": the agent validates a partial
inventory against the disabled categories and would filter its own --partial=database run
away ("Nothing to inventory on partial inventory event"). Hence the render flag below.
#}

{% set __glpi_agent__no_category = (glpi_agent__conf_no_category + (['database'] if glpi_agent__database_inventory_enabled | bool else [])) | unique | sort %}
local = {{ glpi_agent__conf_local }}
{% if __glpi_agent__render_no_category | bool and __glpi_agent__no_category | length > 0 %}
no-category = {{ __glpi_agent__no_category | join(',') }}
{% endif %}
no-ssl-check = {{ glpi_agent__conf_no_ssl_check | ternary('1', '0') }}
server = {{ glpi_agent__conf_server }}
{% if glpi_agent__conf_ssl_fingerprint is defined and glpi_agent__conf_ssl_fingerprint | length %}
{% if glpi_agent__conf_ssl_fingerprint is defined and glpi_agent__conf_ssl_fingerprint | length > 0 %}
ssl-fingerprint = {{ glpi_agent__conf_ssl_fingerprint }}
{% endif %}
13 changes: 13 additions & 0 deletions roles/glpi_agent/templates/etc/glpi-agent/db-inventory.env.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# {{ ansible_managed }}
# 2026072801
{#
Two layers of escaping, applied in this order:
1. The agent parses --credentials as comma-separated "key:value" pairs, so an unquoted value
containing a comma is silently truncated. Single-quote both values and escape an embedded
single quote as \', which is what the agent's parser expects.
2. systemd unescapes backslashes when it reads this file, so every backslash has to be doubled
here to survive into the command line.
-#}
{% set __glpi_agent__username = glpi_agent__database_inventory_login['username'] | replace("'", "\\'") | replace("\\", "\\\\") %}
{% set __glpi_agent__password = glpi_agent__database_inventory_login['password'] | replace("'", "\\'") | replace("\\", "\\\\") %}
GLPI_DB_CRED=type:login_password,login:'{{ __glpi_agent__username }}',password:'{{ __glpi_agent__password }}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# {{ ansible_managed }}
# 2026072801

[Unit]
Description=GLPI Agent database inventory
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
EnvironmentFile=/etc/glpi-agent/db-inventory.env
ExecStart=/usr/bin/glpi-agent --conf-file=/etc/glpi-agent/db-inventory.cfg --credentials=${GLPI_DB_CRED} --partial=database
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# {{ ansible_managed }}
# 2026071301

[Unit]
Description=GLPI Agent database inventory timer

[Timer]
OnCalendar={{ glpi_agent__database_inventory_on_calendar }}
Persistent=true
RandomizedDelaySec=30m

[Install]
WantedBy=timers.target