From f91a9b8fc982d437c77cf576edb9c8b27542bee9 Mon Sep 17 00:00:00 2001 From: Ruakij Date: Sun, 25 Oct 2026 15:04:56 +0000 Subject: [PATCH] host_exporters: IPMI exporter with sudo-pinned FreeIPMI tools for hosts with a BMC --- host_exporters/defaults/main.yml | 25 +++++++++ host_exporters/tasks/main.yml | 88 ++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/host_exporters/defaults/main.yml b/host_exporters/defaults/main.yml index fc35c48..1884b5c 100644 --- a/host_exporters/defaults/main.yml +++ b/host_exporters/defaults/main.yml @@ -2,3 +2,28 @@ # Adds prometheus-smartctl-exporter (trixie-backports) next to the node exporter # on hosts with physical disks. host_exporters_smart: false + +# Adds ipmitool and prometheus-ipmi-exporter (port 9290) on hosts with a BMC reachable through /dev/ipmi0. +host_exporters_ipmi: false +host_exporters_ipmi_collectors: + - ipmi + - bmc + - sel + - chassis +# FreeIPMI tool behind each collector and the fixed arguments the exporter (1.10) passes before +# --config-file . sudo admits exactly these, so a compromised exporter cannot +# run chassis control or clear the SEL. A different exporter version needs matching arguments +# (ipmi_up stays 0 until then). dcmi is not listed: the S1200RP BMC has no power monitoring. +host_exporters_ipmi_tools: + ipmi: + tool: ipmimonitoring + args: --quiet-cache --ignore-unrecognized-events --comma-separated-output --no-header-output --sdr-cache-recreate --output-event-bitmask --output-sensor-state + bmc: + tool: bmc-info + args: "" + sel: + tool: ipmi-sel + args: --info + chassis: + tool: ipmi-chassis + args: --get-chassis-status diff --git a/host_exporters/tasks/main.yml b/host_exporters/tasks/main.yml index 6c0b197..3f3d99e 100644 --- a/host_exporters/tasks/main.yml +++ b/host_exporters/tasks/main.yml @@ -69,3 +69,91 @@ enabled: true state: started ignore_errors: "{{ ansible_check_mode }}" + +# Reads the BMC through /dev/ipmi0, read-only: no raw commands, no configuration. +# FreeIPMI refuses in-band access for any uid but 0, so the exporter runs as the packaged +# unprivileged user prometheus and starts the FreeIPMI tools it needs through sudo. +- name: Install the IPMI exporter + when: host_exporters_ipmi + block: + - name: Install ipmitool and the IPMI exporter + ansible.builtin.apt: + name: + - ipmitool + - sudo + - prometheus-ipmi-exporter + install_recommends: false + state: present + + - name: Allow the exporter user to run the FreeIPMI tools + ansible.builtin.copy: + dest: /etc/sudoers.d/prometheus-ipmi-exporter + mode: "0440" + validate: visudo -cf %s + content: | + Defaults:prometheus !log_allowed, !pam_session + {% for collector in host_exporters_ipmi_collectors %} + prometheus ALL = (root) NOPASSWD: /usr/sbin/{{ host_exporters_ipmi_tools[collector].tool }} {{ host_exporters_ipmi_tools[collector].args }} --config-file /tmp/ipmi_exporter-{{ '[0-9a-f]' * 32 }} + {% endfor %} + register: host_exporters_ipmi_sudo + + # collector_cmd replaces the tool itself, so each collector gets a wrapper that adds sudo. + - name: Create the sudo wrappers directory + ansible.builtin.file: + path: /usr/local/libexec/ipmi-exporter + state: directory + mode: "0755" + + - name: Write the sudo wrappers + ansible.builtin.copy: + dest: /usr/local/libexec/ipmi-exporter/{{ item.key }} + mode: "0755" + content: | + #!/bin/sh + exec sudo -n /usr/sbin/{{ item.value.tool }} "$@" + loop: "{{ host_exporters_ipmi_tools | dict2items | selectattr('key', 'in', host_exporters_ipmi_collectors) | list }}" + loop_control: + label: "{{ item.key }}" + register: host_exporters_ipmi_wrappers + + - name: Write the IPMI exporter configuration + ansible.builtin.copy: + dest: /etc/prometheus/ipmi-local.yml + mode: "0644" + content: | + modules: + default: + collectors: + {% for collector in host_exporters_ipmi_collectors %} + - {{ collector }} + {% endfor %} + collector_cmd: + {% for collector in host_exporters_ipmi_collectors %} + {{ collector }}: /usr/local/libexec/ipmi-exporter/{{ collector }} + {% endfor %} + register: host_exporters_ipmi_config + + - name: Create the IPMI exporter unit drop-in directory + ansible.builtin.file: + path: /etc/systemd/system/prometheus-ipmi-exporter.service.d + state: directory + mode: "0755" + + # sudo needs setuid, so NoNewPrivileges stays off. + - name: Run the IPMI exporter with that configuration + ansible.builtin.copy: + dest: /etc/systemd/system/prometheus-ipmi-exporter.service.d/local.conf + mode: "0644" + content: | + [Service] + ExecStart= + ExecStart=/usr/bin/prometheus-ipmi-exporter --config.file=/etc/prometheus/ipmi-local.yml + register: host_exporters_ipmi_unit + + - name: Enable the IPMI exporter + ansible.builtin.systemd: + name: prometheus-ipmi-exporter + enabled: true + state: "{{ 'restarted' if (host_exporters_ipmi_config.changed or host_exporters_ipmi_unit.changed or host_exporters_ipmi_wrappers.changed) else 'started' }}" + daemon_reload: true + ignore_errors: "{{ ansible_check_mode }}"