diff --git a/roles/pve_exporter/README.md b/roles/pve_exporter/README.md new file mode 100644 index 000000000..c1fac072b --- /dev/null +++ b/roles/pve_exporter/README.md @@ -0,0 +1,47 @@ +# Ansible Role: pve exporter + +## Description + +Deploy prometheus [pve exporter](https://github.com/prometheus-pve/prometheus-pve-exporter), written in python, using ansible. + +Possible Grafana dashboard: https://grafana.com/grafana/dashboards/10347-proxmox-via-prometheus/ + +## Requirements + +- Ansible >= 2.9 (It might work on previous versions, but we cannot guarantee it) + +## Role Variables + +All variables which can be overridden are stored in [defaults/main.yml](defaults/main.yml) file as well as in [meta/argument_specs.yml](meta/argument_specs.yml). +Please refer to the [collection docs](https://prometheus-community.github.io/ansible/branch/main/pve_exporter_role.html) for description and default values of the variables. + +## Example + +### Playbook + +Use it in a playbook as follows: +```yaml +- hosts: all + roles: + - prometheus.prometheus.pve_exporter +``` + +## Local Testing + +The preferred way of locally testing the role is to use Docker and [molecule](https://github.com/ansible-community/molecule) (v3.x). You will have to install Docker on your system. See "Get started" for a Docker package suitable for your system. Running your tests is as simple as executing `molecule test`. + +## Continuous Integration + +Combining molecule and circle CI allows us to test how new PRs will behave when used with multiple ansible versions and multiple operating systems. This also allows use to create test scenarios for different role configurations. As a result we have quite a large test matrix which can take more time than local testing, so please be patient. + +## Contributing + +See [contributor guideline](CONTRIBUTING.md). + +## Troubleshooting + +See [troubleshooting](TROUBLESHOOTING.md). + +## License + +This project is licensed under MIT License. See [LICENSE](/LICENSE) for more details. diff --git a/roles/pve_exporter/TROUBLESHOOTING.md b/roles/pve_exporter/TROUBLESHOOTING.md new file mode 100644 index 000000000..4f341277d --- /dev/null +++ b/roles/pve_exporter/TROUBLESHOOTING.md @@ -0,0 +1 @@ +# Troubleshooting diff --git a/roles/pve_exporter/defaults/main.yml b/roles/pve_exporter/defaults/main.yml new file mode 100644 index 000000000..4b58bae1b --- /dev/null +++ b/roles/pve_exporter/defaults/main.yml @@ -0,0 +1,37 @@ +--- +pve_exporter_version: 3.5.0 +# not go, but python pip. placeholders for _common +pve_exporter_textfile_dir: "" +pve_exporter_tls_server_config: {} +pve_exporter_http_server_config: {} +pve_exporter_basic_auth_users: {} +# just for "prometheus.prometheus._common : Naive assertion of proper listen address" else not used +pve_exporter_web_listen_address: "127.0.0.1:9221" + +# cluster collectors +pve_exporter_enabled_collectors: [] +# pve_exporter_enabled_collectors: +# - status +# - version +# - node +# - cluster +# - resources +pve_exporter_disabled_collectors: [] + +pve_exporter_home: "/var/lib/pve_exporter" +pve_exporter_virtualenv: "/var/lib/pve_exporter/venv" + +pve_exporter_auth_user: prometheus@pve +pve_exporter_auth_pass: +pve_exporter_auth_verify_ssl: true +pve_exporter_auth_token_name: your-token-id +pve_exporter_auth_token_value: + +pve_exporter_config_template: pve_exporter.yml.j2 + +pve_exporter_system_group: "pve-exp" +pve_exporter_system_user: "{{ pve_exporter_system_group }}" + +pve_exporter_config_dir: "/etc/pve_exporter" +pve_exporter_set_credential: false +pve_exporter_no_log: true diff --git a/roles/pve_exporter/handlers/main.yml b/roles/pve_exporter/handlers/main.yml new file mode 100644 index 000000000..102878b30 --- /dev/null +++ b/roles/pve_exporter/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Restart pve_exporter + listen: "restart pve_exporter" + become: true + ansible.builtin.systemd: + daemon_reload: true + name: pve_exporter + state: restarted + when: + - not ansible_check_mode diff --git a/roles/pve_exporter/meta/argument_specs.yml b/roles/pve_exporter/meta/argument_specs.yml new file mode 100644 index 000000000..c4482080e --- /dev/null +++ b/roles/pve_exporter/meta/argument_specs.yml @@ -0,0 +1,98 @@ +--- +# yamllint disable rule:line-length +argument_specs: + main: + short_description: "Prometheus Proxmox VE Exporter" + description: + - "Deploy prometheus L(pve exporter,https://github.com/prometheus-pve/prometheus-pve-exporter) using ansible" + author: + - "Prometheus Community" + options: + pve_exporter_home: + description: "Home of pve_exporter user and base file locations" + default: "/var/lib/pve_exporter" + pve_exporter_virtualenv: + description: "Python virtualenv filepath for pve_exporter" + default: "/var/lib/pve_exporter/venv" + pve_exporter_auth_user: + description: "Authentication PVE user name if using user/password auth" + default: "prometheus@pve" + pve_exporter_auth_pass: + description: "Authentication PVE user password if using user/password auth" + pve_exporter_auth_verify_ssl: + description: "Validate proxmox certificate or not" + type: bool + default: true + pve_exporter_auth_token_name: + description: "Authentication PVE user token name if using token auth" + default: your-token-id + pve_exporter_auth_token_value: + description: "Authentication PVE user token value from `pveum user token add ...`" + pve_exporter_config_template: + description: "Configuration template for pve_exporter" + default: "pve_exporter.yml.j2" + pve_exporter_version: + description: "Proxmox VE Exporter package version. Also accepts latest as parameter." + default: "1.0.0" + pve_exporter_web_listen_address: + description: "Address on which pve exporter will listen" + default: "0.0.0.0:15353" + pve_exporter_enabled_collectors: + description: + - "List of dicts defining additionally enabled collectors and their configuration." + - "It adds collectors to L(those enabled by default,https://github.com/prometheus-pve/prometheus-pve-exporter#enabled-by-default)." + type: "list" + default: + - systemd + - textfile: + directory: "{{ pve_exporter_textfile_dir }}" + pve_exporter_disabled_collectors: + description: + - "List of disabled collectors." + - "By default pve_exporter disables collectors listed L(here,https://github.com/prometheus-pve/prometheus-pve-exporter#disabled-by-default)." + type: "list" + elements: "str" + pve_exporter_textfile_dir: + description: + - "Directory used by the L(Textfile Collector,https://github.com/prometheus-pve/prometheus-pve-exporter#textfile-collector)." + - "To get permissions to write metrics in this directory, users must be in C(pve-exp) system group." + - "B(Note:) More information in TROUBLESHOOTING.md guide." + default: "/var/lib/pve_exporter" + pve_exporter_tls_server_config: + description: + - "Configuration for TLS authentication." + - "Keys and values are the same as in L(pve_exporter docs,https://prometheus.io/docs/prometheus/latest/configuration/https/)." + type: "dict" + pve_exporter_http_server_config: + description: + - "Config for HTTP/2 support." + - "Keys and values are the same as in L(pve_exporter docs,https://prometheus.io/docs/prometheus/latest/configuration/https/)." + type: "dict" + pve_exporter_basic_auth_users: + description: "Dictionary of users and password for basic authentication. Passwords are automatically hashed with bcrypt." + type: "dict" + pve_exporter_system_group: + description: + - "I(Advanced)" + - "System group for pve exporter" + default: "pve-exp" + pve_exporter_system_user: + description: + - "I(Advanced)" + - "Proxmox VE Exporter user" + default: "pve-exp" + pve_exporter_config_dir: + description: "Path to directory with pve_exporter configuration" + default: "/etc/pve_exporter" + pve_exporter_set_credential: + description: + - "I(Advanced)" + - "Create pve_exporter user in proxmox" + type: bool + default: false + pve_exporter_no_log: + description: + - "I(Advanced)" + - "Display/Log ansible tasks containing secrets" + type: bool + default: true diff --git a/roles/pve_exporter/meta/main.yml b/roles/pve_exporter/meta/main.yml new file mode 100644 index 000000000..cc004c4e5 --- /dev/null +++ b/roles/pve_exporter/meta/main.yml @@ -0,0 +1,25 @@ +--- +galaxy_info: + author: "Prometheus Community" + description: "Prometheus Proxmox VE Exporter" + license: "Apache" + min_ansible_version: "2.9" + platforms: + - name: "Ubuntu" + versions: + - "focal" + - "jammy" + - "noble" + - name: "Debian" + versions: + - "bullseye" + - name: "EL" + versions: + - "8" + - "9" + galaxy_tags: + - "monitoring" + - "prometheus" + - "exporter" + - "metrics" + - "system" diff --git a/roles/pve_exporter/molecule/alternative/molecule.yml b/roles/pve_exporter/molecule/alternative/molecule.yml new file mode 100644 index 000000000..8cc22dc20 --- /dev/null +++ b/roles/pve_exporter/molecule/alternative/molecule.yml @@ -0,0 +1,23 @@ +--- +provisioner: + playbooks: + prepare: "${MOLECULE_PROJECT_DIRECTORY}/../../.config/molecule/alternative/prepare.yml" + inventory: + group_vars: + all: + pve_exporter_web_listen_address: + - '127.0.0.1:8080' + - '127.0.1.1:8080' + pve_exporter_textfile_dir: "" + pve_exporter_enabled_collectors: + - entropy + pve_exporter_disabled_collectors: + - diskstats + pve_exporter_tls_server_config: + cert_file: /etc/pve_exporter/tls.cert + key_file: /etc/pve_exporter/tls.key + pve_exporter_http_server_config: + http2: true + pve_exporter_basic_auth_users: + randomuser: examplepassword + pve_exporter_version: 3.5.0 diff --git a/roles/pve_exporter/molecule/alternative/tests/test_alternative.py b/roles/pve_exporter/molecule/alternative/tests/test_alternative.py new file mode 100644 index 000000000..f3e79e70a --- /dev/null +++ b/roles/pve_exporter/molecule/alternative/tests/test_alternative.py @@ -0,0 +1,43 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from testinfra_helpers import get_target_hosts +import pytest + +testinfra_hosts = get_target_hosts() + + +def test_directories(host): + dirs = [ + "/var/lib/pve_exporter" + ] + for dir in dirs: + d = host.file(dir) + assert not d.exists + + +def test_service(host): + s = host.service("pve_exporter") + try: + assert s.is_running + except AssertionError: + # Capture service logs + journal_output = host.run('journalctl -u pve_exporter --since "1 hour ago"') + print("\n==== journalctl -u pve_exporter Output ====\n") + print(journal_output) + print("\n============================================\n") + raise # Re-raise the original assertion error + + +def test_protecthome_property(host): + s = host.service("pve_exporter") + p = s.systemd_properties + assert p.get("ProtectHome") == "yes" + + +@pytest.mark.parametrize("sockets", [ + "tcp://127.0.0.1:8080", + "tcp://127.0.1.1:8080", +]) +def test_socket(host, sockets): + assert host.socket(sockets).is_listening diff --git a/roles/pve_exporter/molecule/default/molecule.yml b/roles/pve_exporter/molecule/default/molecule.yml new file mode 100644 index 000000000..fea738e2b --- /dev/null +++ b/roles/pve_exporter/molecule/default/molecule.yml @@ -0,0 +1,6 @@ +--- +provisioner: + inventory: + group_vars: + all: + pve_exporter_web_listen_address: "127.0.0.1:9221" diff --git a/roles/pve_exporter/molecule/default/tests/test_default.py b/roles/pve_exporter/molecule/default/tests/test_default.py new file mode 100644 index 000000000..3f70321b2 --- /dev/null +++ b/roles/pve_exporter/molecule/default/tests/test_default.py @@ -0,0 +1,76 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from testinfra_helpers import get_target_hosts + +testinfra_hosts = get_target_hosts() + + +def test_directories(host): + dirs = [ + "/var/lib/pve_exporter" + ] + for dir in dirs: + d = host.file(dir) + assert d.is_directory + assert d.exists + + +def test_files(host): + files = [ + "/etc/systemd/system/pve_exporter.service", + "/usr/local/bin/pve_exporter" + ] + for file in files: + f = host.file(file) + assert f.exists + assert f.is_file + + +def test_permissions_didnt_change(host): + dirs = [ + "/etc", + "/root", + "/usr", + "/var" + ] + for file in dirs: + f = host.file(file) + assert f.exists + assert f.is_directory + assert f.user == "root" + assert f.group == "root" + + +def test_user(host): + assert host.group("pve-exp").exists + assert "pve-exp" in host.user("pve-exp").groups + assert host.user("pve-exp").shell == "/usr/sbin/nologin" + + +def test_service(host): + s = host.service("pve_exporter") + try: + assert s.is_running + except AssertionError: + # Capture service logs + journal_output = host.run('journalctl -u pve_exporter --since "1 hour ago"') + print("\n==== journalctl -u pve_exporter Output ====\n") + print(journal_output) + print("\n============================================\n") + raise # Re-raise the original assertion error + + +def test_protecthome_property(host): + s = host.service("pve_exporter") + p = s.systemd_properties + assert p.get("ProtectHome") == "yes" + + +def test_socket(host): + sockets = [ + "tcp://127.0.0.1:9221" + ] + for socket in sockets: + s = host.socket(socket) + assert s.is_listening diff --git a/roles/pve_exporter/molecule/latest/molecule.yml b/roles/pve_exporter/molecule/latest/molecule.yml new file mode 100644 index 000000000..afb8c1d86 --- /dev/null +++ b/roles/pve_exporter/molecule/latest/molecule.yml @@ -0,0 +1,7 @@ +--- +provisioner: + inventory: + group_vars: + all: + pve_exporter_version: latest + pve_exporter_textfile_dir: /home/pve_exporter diff --git a/roles/pve_exporter/molecule/latest/tests/test_latest.py b/roles/pve_exporter/molecule/latest/tests/test_latest.py new file mode 100644 index 000000000..426ee277a --- /dev/null +++ b/roles/pve_exporter/molecule/latest/tests/test_latest.py @@ -0,0 +1,51 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from testinfra_helpers import get_target_hosts +import pytest + +testinfra_hosts = get_target_hosts() + + +@pytest.mark.parametrize("files", [ + "/etc/systemd/system/pve_exporter.service", + "/usr/local/bin/pve_exporter" +]) +def test_files(host, files): + f = host.file(files) + assert f.exists + assert f.is_file + + +def test_directories(host): + dirs = [ + "/home/pve_exporter" + ] + for dir in dirs: + d = host.file(dir) + assert d.is_directory + assert d.exists + + +def test_service(host): + s = host.service("pve_exporter") + try: + assert s.is_running + except AssertionError: + # Capture service logs + journal_output = host.run('journalctl -u pve_exporter --since "1 hour ago"') + print("\n==== journalctl -u pve_exporter Output ====\n") + print(journal_output) + print("\n============================================\n") + raise # Re-raise the original assertion error + + +def test_protecthome_property(host): + s = host.service("pve_exporter") + p = s.systemd_properties + assert p.get("ProtectHome") == "read-only" + + +def test_socket(host): + s = host.socket("tcp://0.0.0.0:9221") + assert s.is_listening diff --git a/roles/pve_exporter/tasks/configure.yml b/roles/pve_exporter/tasks/configure.yml new file mode 100644 index 000000000..a73ce3d94 --- /dev/null +++ b/roles/pve_exporter/tasks/configure.yml @@ -0,0 +1,30 @@ +--- +- name: Configure + ansible.builtin.include_role: + name: prometheus.prometheus._common + tasks_from: configure.yml + vars: + _common_system_user: "{{ pve_exporter_system_user }}" + _common_system_group: "{{ pve_exporter_system_group }}" + _common_config_dir: "{{ pve_exporter_config_dir }}" + _common_tls_server_config: "{{ pve_exporter_tls_server_config }}" + _common_http_server_config: "{{ pve_exporter_http_server_config }}" + _common_basic_auth_users: "{{ pve_exporter_basic_auth_users }}" + tags: + - pve_exporter + - configure + - pve_exporter_configure + +- name: Create textfile collector dir + ansible.builtin.file: + path: "{{ pve_exporter_textfile_dir }}" + state: directory + owner: "{{ pve_exporter_system_user }}" + group: "{{ pve_exporter_system_group }}" + mode: u+rwX,g+rwX,o=rX + become: true + when: pve_exporter_textfile_dir | length > 0 + tags: + - pve_exporter + - configure + - pve_exporter_configure diff --git a/roles/pve_exporter/tasks/credentials.yml b/roles/pve_exporter/tasks/credentials.yml new file mode 100644 index 000000000..933a9f2fd --- /dev/null +++ b/roles/pve_exporter/tasks/credentials.yml @@ -0,0 +1,72 @@ +--- + +- name: Get PVE users + ansible.builtin.command: + cmd: pveum user list + changed_when: false + register: pve_user_list + +- name: Ensure user prometheus@pve + ansible.builtin.command: # noqa no-changed-when + cmd: "pveum user add prometheus@pve --comment 'prometheus user' --password \"{{ lookup('community.general.random_string', length=64, special=false) }}\"" + # changed_when: false + when: + - "'prometheus@pve' not in pve_user_list.stdout" + no_log: "{{ pve_exporter_no_log }}" + +- name: Get prometheus@pve user tokens + ansible.builtin.command: + cmd: pveum user token list prometheus@pve + changed_when: false + register: pve_usertoken_list + +- name: Check if /etc/pve_exporter/pve_exporter.yml exists + ansible.builtin.stat: + path: /etc/pve_exporter/pve_exporter.yml + register: pve_exporter_config + +- name: Delete existing user prometheus@pve token if no pve_exporter.yml + ansible.builtin.command: # noqa no-changed-when + cmd: "pveum user token delete prometheus@pve prometheus-pve-exporter-token" + when: + - not pve_exporter_config.stat.exists + - "'prometheus-pve-exporter-token' in pve_usertoken_list.stdout" + +- name: Create user token + when: > + ("'prometheus-pve-exporter-token' not in pve_usertoken_list.stdout") and + not pve_exporter_config.stat.exists + block: + - name: Ensure user prometheus@pve has a token + ansible.builtin.command: # noqa no-changed-when + cmd: "pveum user token add prometheus@pve prometheus-pve-exporter-token" + register: pve_exporter_token + # changed_when: false + no_log: "{{ pve_exporter_no_log }}" + - name: Ensure user and token have right acl + ansible.builtin.command: # noqa no-changed-when + cmd: "{{ item }}" + loop: + - "pveum acl modify / -user 'prometheus@pve' -role PVEAuditor" + - "pveum acl modify / -token 'prometheus@pve!prometheus-pve-exporter-token' -role PVEAuditor" + - name: Set facts for pve_exporter role + ansible.builtin.set_fact: # noqa no-changed-when + pve_exporter_auth_token_name: "prometheus-pve-exporter-token" + pve_exporter_auth_token_value: "{{ pve_exporter_token.stdout | regex_findall('value .* ([0-9a-f-]+) ', multiline=True, ignorecase=True) | first }}" + no_log: "{{ pve_exporter_no_log }}" + +- name: Get existing user token + when: + - "'prometheus-pve-exporter-token' in pve_usertoken_list.stdout" + - pve_exporter_config.stat.exists + block: + - name: Get existing user token + ansible.builtin.command: # noqa no-changed-when + cmd: "grep token_value /etc/pve_exporter/pve_exporter.yml" + changed_when: false + register: pve_exporter_existing_token + - name: Set facts for pve_exporter role - existing token + ansible.builtin.set_fact: # noqa no-changed-when + pve_exporter_auth_token_name: "prometheus-pve-exporter-token" + pve_exporter_auth_token_value: "{{ pve_exporter_existing_token.stdout | regex_findall('token_value: ([0-9a-f-]+)', ignorecase=True) | first }}" + no_log: "{{ pve_exporter_no_log }}" diff --git a/roles/pve_exporter/tasks/install.yml b/roles/pve_exporter/tasks/install.yml new file mode 100644 index 000000000..e6ae396ec --- /dev/null +++ b/roles/pve_exporter/tasks/install.yml @@ -0,0 +1,51 @@ +--- + +- name: Gather python facts + ansible.builtin.setup: + gather_subset: + - "!all" + - "python" + when: + "('python' not in ansible_facts) or + ('discovered_interpreter_python' not in ansible_facts)" + +- name: Install packages dependencies + ansible.builtin.package: + name: "{{ _pve_exporter_pkgs }}" + state: present + become: true +- name: "Create system group {{ pve_exporter_system_group }}" + ansible.builtin.group: + name: "{{ pve_exporter_system_group }}" + state: present + become: true +- name: "Create system user {{ pve_exporter_system_user }}" + ansible.builtin.user: + name: "{{ pve_exporter_system_user }}" + system: true + shell: "/usr/sbin/nologin" + group: "{{ pve_exporter_system_group }}" + home: "{{ pve_exporter_home }}" + create_home: true + become: true +- name: Install pve_exporter from pip + ansible.builtin.pip: + name: prometheus-pve-exporter + version: "{{ pve_exporter_version }}" + state: present + virtualenv: "{{ pve_exporter_virtualenv }}" + virtualenv_command: "{% if ansible_facts['os_family'] != 'RedHat' %}{{ ansible_facts['discovered_interpreter_python'] }} -m venv{% endif %}" +- name: Ensure config dir exists + ansible.builtin.file: + path: "{{ pve_exporter_config_dir }}" + state: directory + mode: '0775' # like _common + owner: root +- name: Configure pve_exporter + ansible.builtin.template: + src: "{{ pve_exporter_config_template }}" + dest: "{{ pve_exporter_config_dir }}/pve_exporter.yml" + mode: '0644' + owner: root + notify: + - Restart pve_exporter diff --git a/roles/pve_exporter/tasks/main.yml b/roles/pve_exporter/tasks/main.yml new file mode 100644 index 000000000..384a7769f --- /dev/null +++ b/roles/pve_exporter/tasks/main.yml @@ -0,0 +1,50 @@ +--- + +- name: RedHat | Import redhat-epel + ansible.builtin.import_tasks: redhat-epel.yml + when: ansible_facts['os_family'] == "RedHat" + +- name: Preflight + ansible.builtin.include_tasks: + file: preflight.yml + tags: + - pve_exporter_install + - pve_exporter_configure + - pve_exporter_run + +- name: Credentials + ansible.builtin.import_tasks: credentials.yml + when: pve_exporter_set_credential | bool + +- name: Install + ansible.builtin.import_tasks: install.yml + tags: + - pve_exporter_install + +- name: SELinux + ansible.builtin.include_role: + name: prometheus.prometheus._common + tasks_from: selinux.yml + vars: + _common_selinux_port: "{{ pve_exporter_web_listen_address | urlsplit('port') }}" + when: ansible_facts.selinux.status == "enabled" + tags: + - pve_exporter_configure + +- name: Configure + ansible.builtin.include_tasks: + file: configure.yml + tags: + - pve_exporter_configure + +- name: Ensure Proxmox VE Exporter is enabled on boot + become: true + ansible.builtin.systemd: + daemon_reload: true + name: pve_exporter + enabled: true + state: started + when: + - not ansible_check_mode + tags: + - pve_exporter_run diff --git a/roles/pve_exporter/tasks/preflight.yml b/roles/pve_exporter/tasks/preflight.yml new file mode 100644 index 000000000..5dfdc0f1a --- /dev/null +++ b/roles/pve_exporter/tasks/preflight.yml @@ -0,0 +1,65 @@ +--- +- name: Common preflight + ansible.builtin.include_role: + name: prometheus.prometheus._common + tasks_from: preflight.yml + vars: + _common_web_listen_address: "{{ pve_exporter_web_listen_address }}" + +- name: Assert that used version supports listen address type + ansible.builtin.assert: + that: + - >- + pve_exporter_web_listen_address is string + or + ( + pve_exporter_version is version('1.5.0', '>=') and + pve_exporter_web_listen_address | type_debug == "list" + ) + +- name: Assert collectors are not both disabled and enabled at the same time + ansible.builtin.assert: + that: + - "item not in pve_exporter_enabled_collectors" + with_items: "{{ pve_exporter_disabled_collectors }}" + +- name: Assert that TLS config is correct + when: pve_exporter_tls_server_config | length > 0 + block: + - name: Assert that TLS key and cert path are set + ansible.builtin.assert: + that: + - "pve_exporter_tls_server_config.cert_file is defined" + - "pve_exporter_tls_server_config.key_file is defined" + + - name: Check existence of TLS cert file + ansible.builtin.stat: + path: "{{ pve_exporter_tls_server_config.cert_file }}" + register: __pve_exporter_cert_file + + - name: Check existence of TLS key file + ansible.builtin.stat: + path: "{{ pve_exporter_tls_server_config.key_file }}" + register: __pve_exporter_key_file + + - name: Assert that TLS key and cert are present + ansible.builtin.assert: + that: + - "__pve_exporter_cert_file.stat.exists" + - "__pve_exporter_key_file.stat.exists" + +- name: Discover latest version + ansible.builtin.set_fact: + pve_exporter_version: "{{ (lookup('url', 'https://api.github.com/repos/{{ _pve_exporter_repo }}/releases/latest', headers=_github_api_headers, + split_lines=False) | from_json).get('tag_name') | replace('v', '') }}" + run_once: true + until: pve_exporter_version is version('0.0.0', '>=') + retries: 10 + when: + - pve_exporter_version == "latest" + tags: + - pve_exporter + - install + - pve_exporter_install + - download + - pve_exporter_download diff --git a/roles/pve_exporter/tasks/redhat-epel.yml b/roles/pve_exporter/tasks/redhat-epel.yml new file mode 100644 index 000000000..8afd1422d --- /dev/null +++ b/roles/pve_exporter/tasks/redhat-epel.yml @@ -0,0 +1,76 @@ +--- + +- name: Include version-specific variables for RedHat + ansible.builtin.include_vars: "RedHat-{{ ansible_facts['distribution_major_version'] }}.yml" +- name: Include AlmaLinux-9 specific variables + ansible.builtin.include_vars: "AlmaLinux-9.yml" + when: + - ansible_facts['distribution'] == 'AlmaLinux' + - ansible_facts['distribution_major_version'] | int == 9 + +- name: RedHat | Install dependencies packages + ansible.builtin.package: + name: "{{ redhat_epel_pkgs | default([]) }}" + state: present + register: pkg_result + until: pkg_result is success + +- name: Check if epel key already retrieved + ansible.builtin.stat: + path: "/var/tmp/{{ epel_rpm_key | basename }}" + register: epelkey +- name: Download epel rpm key + ansible.builtin.get_url: + url: "{{ epel_rpm_key }}" + dest: "/var/tmp/{{ epel_rpm_key | basename }}" + mode: '0600' + checksum: "sha256:{{ epel_rpm_key_hash }}" + validate_certs: false + register: dl_result + until: dl_result is success + when: + - not epelkey.stat.exists + - epel_rpm_key | length > 0 +- name: Installing epel rpm key + # rpm_key: key={{ epel_rpm_key }} + ansible.builtin.rpm_key: + key: "/var/tmp/{{ epel_rpm_key | basename }}" + state: present + when: + - epel_rpm_key | length > 0 + register: pkg_result + until: pkg_result is success + become: true + +- name: Gather package facts + ansible.builtin.package_facts: + manager: auto + +- name: RedHat | Install Fedora epel + ansible.builtin.package: + name: "{{ redhat_epel_rpm }}" + state: present + when: "'epel-release' not in ansible_facts.packages" + register: pkg_result + until: pkg_result is success + become: true + +- name: RedHat | Ensure dnf config-manager is present + ansible.builtin.package: + name: 'dnf-command(config-manager)' + state: present + register: pkg_result + until: pkg_result is success + become: true + when: + - ansible_facts['distribution_major_version'] | int >= 9 + +- name: RedHat9+ | Enable CRB repo + community.general.ini_file: + path: "{{ redhatepel_crb_file }}" + section: crb + option: enabled + value: '1' + mode: '0644' + when: + - ansible_facts['distribution_major_version'] | int >= 9 diff --git a/roles/pve_exporter/templates/pve_exporter.service.j2 b/roles/pve_exporter/templates/pve_exporter.service.j2 new file mode 100644 index 000000000..d3125fc5b --- /dev/null +++ b/roles/pve_exporter/templates/pve_exporter.service.j2 @@ -0,0 +1,113 @@ +{{ ansible_managed | comment }} + +# Exposure level `systemd-analyze security`: 2.7 OK + +[Unit] +Description=Prometheus Proxmox VE Exporter +After=network-online.target + +[Service] +Type=simple +User={{ pve_exporter_system_user }} +Group={{ pve_exporter_system_group }} +ExecStart={{ pve_exporter_virtualenv }}/bin/pve_exporter \ +{% for collector in pve_exporter_enabled_collectors -%} +{% if not collector is mapping %} + '--collector.{{ collector }}' \ +{% else -%} +{% set name, options = (collector.items()|list)[0] -%} + '--collector.{{ name }}' \ +{% for k,v in options|dictsort %} + '--collector.{{ name }}.{{ k }}={{ v }}' \ +{% endfor -%} +{% endif -%} +{% endfor -%} +{% for collector in pve_exporter_disabled_collectors %} + '--no-collector.{{ collector }}' \ +{% endfor %} + --web.listen-address {{ pve_exporter_web_listen_address }} \ + --config.file {{ pve_exporter_config_dir }}/pve_exporter.yml + + +SyslogIdentifier=pve_exporter +Restart=always +RestartSec=1 +StartLimitInterval=0 + +{% set ns = namespace(protect_home = 'yes') %} +{% for m in ansible_facts.mounts if m.mount.startswith('/home') %} +{% set ns.protect_home = 'read-only' %} +{% endfor %} +{% if pve_exporter_textfile_dir.startswith('/home') %} +{% set ns.protect_home = 'read-only' %} +{% endif %} +ProtectHome={{ ns.protect_home }} +NoNewPrivileges=yes +PrivateTmp=true +UMask=077 + +{% if (ansible_facts.packages.systemd | first).version is version('232', '>=') %} +ProtectSystem=strict +ProtectProc=noaccess +ProtectControlGroups=true +ProtectKernelModules=true +ProtectKernelTunables=yes +ProtectHostname=yes +ProtectClock=yes +LockPersonality=true +RestrictRealtime=true +RestrictNamespaces=yes +RestrictSUIDSGID=yes +MemoryDenyWriteExecute=yes +RemoveIPC=yes +{% else %} +ProtectSystem=full +{% endif %} + +IPAccounting=yes +IPAddressAllow=localhost link-local multicast 10.0.0.0/8 192.168.0.0/16 +# IPAddressDeny= + +CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_DAC_READ_SEARCH +CapabilityBoundingSet=~CAP_BLOCK_SUSPEND +CapabilityBoundingSet=~CAP_BPF +CapabilityBoundingSet=~CAP_DAC_* CAP_FOWNER CAP_IPC_OWNER +CapabilityBoundingSet=~CAP_FSETID CAP_SETFCAP +CapabilityBoundingSet=~CAP_IPC_LOCK +CapabilityBoundingSet=~CAP_KILL +CapabilityBoundingSet=~CAP_LEASE +CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE +CapabilityBoundingSet=~CAP_MKNOD +CapabilityBoundingSet=~CAP_NET_ADMIN +CapabilityBoundingSet=~CAP_NET_BIND_SERVICE CAP_NET_BROADCAST +CapabilityBoundingSet=~CAP_NET_RAW +CapabilityBoundingSet=~CAP_SETUID CAP_SETGID CAP_SETPCAP +CapabilityBoundingSet=~CAP_SYS_CHROOT +CapabilityBoundingSet=~CAP_SYS_PACCT +CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG +CapabilityBoundingSet=~CAP_SYS_ADMIN +CapabilityBoundingSet=~CAP_SYS_NICE CAP_SYS_RESOURCE +CapabilityBoundingSet=~CAP_SYS_BOOT +CapabilityBoundingSet=~CAP_SYS_RAWIO +CapabilityBoundingSet=~CAP_SYS_PTRACE + +{% if not (ansible_facts.virtualization_type is defined and + (ansible_facts.virtualization_type == "lxc" or ansible_facts.virtualization_type == "docker") + ) +%} +SystemCallFilter=@system-service @privileged @resources +{% endif %} +SystemCallFilter=~@clock @cpu-emulation @debug @mount @obsolete @privileged @raw-io @reboot @resources @swap @module +SystemCallArchitectures=native +# When system call is disallowed, return error code instead of killing process +SystemCallErrorNumber=EPERM + +{% if pve_exporter_cgroups_restriction_enable is defined and pve_exporter_cgroups_restriction_enable|bool %} +CPUWeight={{ pve_exporter_cgroups_cpushares | default('80') }} +CPUQuota={{ pve_exporter_cgroups_cpuquota | default('40%') }} +MemoryMax={{ pve_exporter_cgroups_memorylimit | default('2G') }} +IOWeight={{ pve_exporter_cgroups_ioweight | default('80') }} +{% endif %} + +[Install] +WantedBy=multi-user.target diff --git a/roles/pve_exporter/templates/pve_exporter.yml.j2 b/roles/pve_exporter/templates/pve_exporter.yml.j2 new file mode 100644 index 000000000..26fb7ea8a --- /dev/null +++ b/roles/pve_exporter/templates/pve_exporter.yml.j2 @@ -0,0 +1,13 @@ +--- +{{ ansible_managed | comment }} + +default: + user: {{ pve_exporter_auth_user }} +{% if pve_exporter_auth_pass != None %} + password: "{{ pve_exporter_auth_pass }}" +{% elif pve_exporter_auth_token_name != None and pve_exporter_auth_token_value != None %} + token_name: {{ pve_exporter_auth_token_name }} + token_value: {{ pve_exporter_auth_token_value }} +{% endif %} + # Optional: set to false to skip SSL/TLS verification + verify_ssl: {{ pve_exporter_auth_verify_ssl | bool | lower }} diff --git a/roles/pve_exporter/test-requirements.txt b/roles/pve_exporter/test-requirements.txt new file mode 100644 index 000000000..7f0b6e759 --- /dev/null +++ b/roles/pve_exporter/test-requirements.txt @@ -0,0 +1 @@ +bcrypt diff --git a/roles/pve_exporter/vars/AlmaLinux-9.yml b/roles/pve_exporter/vars/AlmaLinux-9.yml new file mode 100644 index 000000000..9bcc5e20f --- /dev/null +++ b/roles/pve_exporter/vars/AlmaLinux-9.yml @@ -0,0 +1,3 @@ +--- + +redhatepel_crb_file: "/etc/yum.repos.d/almalinux-crb.repo" diff --git a/roles/pve_exporter/vars/RedHat-10.yml b/roles/pve_exporter/vars/RedHat-10.yml new file mode 100644 index 000000000..c9b016337 --- /dev/null +++ b/roles/pve_exporter/vars/RedHat-10.yml @@ -0,0 +1,14 @@ +--- + +epel_rpm_key: 'https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-10' +epel_rpm_key_hash: de390fc168eae5ab2852e9e93d34a0b9ddf05cf9ce90ee28d97de26a4b1f6b93 + +redhat_epel_rpm: "https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_facts['distribution_major_version'] }}.noarch.rpm" +redhat_epel_pkgs: + - gnupg2 + +redhatepel_crb_file: "/etc/yum.repos.d/{{ ansible_facts['distribution'] | lower }}-crb.repo" + +_pve_exporter_pkgs: + - "python{{ ansible_facts['python']['version']['major'] }}-setuptools" + - "python{{ ansible_facts['python']['version']['major'] }}-virtualenv" diff --git a/roles/pve_exporter/vars/RedHat-8.yml b/roles/pve_exporter/vars/RedHat-8.yml new file mode 100644 index 000000000..b41a2e74d --- /dev/null +++ b/roles/pve_exporter/vars/RedHat-8.yml @@ -0,0 +1,10 @@ +--- + +epel_rpm_key: 'https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-8' +epel_rpm_key_hash: cd1db21a863185127f2e3b264c97fb1c6c44c316385707999041ea475c110d1c + +redhat_epel_rpm: "https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_facts['distribution_major_version'] }}.noarch.rpm" + +_pve_exporter_pkgs: + - "python{{ ansible_facts['python']['version']['major'] }}-setuptools" + - "python{{ ansible_facts['python']['version']['major'] }}-virtualenv" diff --git a/roles/pve_exporter/vars/RedHat-9.yml b/roles/pve_exporter/vars/RedHat-9.yml new file mode 100644 index 000000000..2e5d660f2 --- /dev/null +++ b/roles/pve_exporter/vars/RedHat-9.yml @@ -0,0 +1,12 @@ +--- + +epel_rpm_key: 'https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-9' +epel_rpm_key_hash: fcf0eab4f05a1c0de6363ac4b707600a27a9d774e9b491059e59e6921b255a84 + +redhat_epel_rpm: "https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_facts['distribution_major_version'] }}.noarch.rpm" + +redhatepel_crb_file: "/etc/yum.repos.d/{{ ansible_facts['distribution'] | lower }}.repo" + +_pve_exporter_pkgs: + - "python{{ ansible_facts['python']['version']['major'] }}-setuptools" + - "python{{ ansible_facts['python']['version']['major'] }}-virtualenv" diff --git a/roles/pve_exporter/vars/main.yml b/roles/pve_exporter/vars/main.yml new file mode 100644 index 000000000..655579822 --- /dev/null +++ b/roles/pve_exporter/vars/main.yml @@ -0,0 +1,6 @@ +--- +_pve_exporter_repo: "prometheus-pve/prometheus-pve-exporter" +_github_api_headers: "{{ {'GITHUB_TOKEN': lookup('ansible.builtin.env', 'GITHUB_TOKEN')} if (lookup('ansible.builtin.env', 'GITHUB_TOKEN')) else {} }}" +_pve_exporter_pkgs: + - "python{{ ansible_facts['python']['version']['major'] }}-setuptools" + - "python{{ ansible_facts['python']['version']['major'] }}-venv" diff --git a/tests/integration/targets/molecule-pve_exporter-alternative/runme.sh b/tests/integration/targets/molecule-pve_exporter-alternative/runme.sh new file mode 100755 index 000000000..d094c3e1b --- /dev/null +++ b/tests/integration/targets/molecule-pve_exporter-alternative/runme.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +collection_root=$(pwd | grep -oP ".+\/ansible_collections\/\w+?\/\w+") +source "$collection_root/tests/integration/molecule.sh" diff --git a/tests/integration/targets/molecule-pve_exporter-default/runme.sh b/tests/integration/targets/molecule-pve_exporter-default/runme.sh new file mode 100755 index 000000000..d094c3e1b --- /dev/null +++ b/tests/integration/targets/molecule-pve_exporter-default/runme.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +collection_root=$(pwd | grep -oP ".+\/ansible_collections\/\w+?\/\w+") +source "$collection_root/tests/integration/molecule.sh" diff --git a/tests/integration/targets/molecule-pve_exporter-latest/runme.sh b/tests/integration/targets/molecule-pve_exporter-latest/runme.sh new file mode 100755 index 000000000..d094c3e1b --- /dev/null +++ b/tests/integration/targets/molecule-pve_exporter-latest/runme.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +collection_root=$(pwd | grep -oP ".+\/ansible_collections\/\w+?\/\w+") +source "$collection_root/tests/integration/molecule.sh"