Agent-deployment on RAW the Ansible-way (no checkmk collection)and my journey with it

Hi all,

I just wanted to share my experiences regarding having/getting agent-deployments done in the Ansible-way.
But i wanted to do this without having the requirement/need to import the Ansible checkmk class, just a/the basic Ansible modules at my disposal.

Some details:

  • Ansible controller is running off a RockyLinux 9.6 machine.
  • Ansible version/installtion was pulled from EPEL-repository ( ansible v 2.14.18)

This worked fine with the playbook i devised previously:

---
- hosts: all
  vars:
    monitor_host: 'monitor.myhost.tld'
    monitor_site: 'mysite'
    download_path: '/usr/local/install/packages'
    plugin_path: '/usr/lib/check_mk_agent/plugins'

  tasks:
    - name: Query monitoring host for latest CheckMK version
      ansible.builtin.shell: omd versions
      register: cmk_versions
      delegate_to: "{{ monitor_host }}"
      run_once: true

    - name: Set latest version of CheckMK package as fact
      ansible.builtin.set_fact:
        cmk_latest: "{{ cmk_versions.stdout_lines | community.general.version_sort | last | regex_search('[0-9]+.[0-9]+.[0-9]+[a-z][0-9]+') }}"
      delegate_to: "{{ monitor_host }}"
      run_once: true

    - name: Find latest rpm agents on monitoring host
      ansible.builtin.find:
        path: "/opt/omd/versions/{{ cmk_latest }}.cre/share/check_mk/agents"
        patterns: '*.rpm'
      register: rpm_agent_result
      delegate_to: "{{ monitor_host }}"
      run_once: true

    - name: Find latest deb agents on monitoring host
      ansible.builtin.find:
        path: "/opt/omd/versions/{{ cmk_latest }}.cre/share/check_mk/agents"
        patterns: '*.deb'
      register: deb_agent_result
      delegate_to: "{{ monitor_host }}"
      run_once: true

    - name: Set rpm agent filename as fact
      ansible.builtin.set_fact:
        rpm_latest: "{{ rpm_agent_result.files[0].path | basename }}"
      delegate_to: "{{ monitor_host }}"
      run_once: true

    - name: Propogate rpm fact to applicable hosts
      ansible.builtin.set_fact:
        rpm_basename: "{{ hostvars[monitor_host]['rpm_latest'] }}"
      when:
        - ansible_pkg_mgr == "yum" or ansible_pkg_mgr == "dnf" or ansible_pkg_mgr == "zypper"

    - name: Set fact for rpm agent
      ansible.builtin.set_fact:
        rpm_download: "https://{{ hostvars[monitor_host]['ansible_fqdn'] }}/{{ monitor_site }}/check_mk/agents/{{ hostvars[monitor_host]['rpm_latest'] }}"
      when:
        - ansible_pkg_mgr == "yum" or ansible_pkg_mgr == "dnf" or ansible_pkg_mgr == "zypper"

    - name: Set deb agent filename as fact
      ansible.builtin.set_fact:
        deb_latest: "{{ deb_agent_result.files[0].path | basename }}"
      delegate_to: "{{ monitor_host }}"
      run_once: true

    - name: Propogate deb fact to applicable hosts
      ansible.builtin.set_fact:
        deb_basename: "{{ hostvars[monitor_host]['deb_latest'] }}"
      when:
        - ansible_pkg_mgr == "apt"

    - name: Set fact for deb agent
      ansible.builtin.set_fact:
        deb_download: "https://{{ hostvars[monitor_host]['ansible_fqdn'] }}/{{ monitor_site }}/check_mk/agents/{{ hostvars[monitor_host]['deb_latest'] }}"
      when:
        - ansible_pkg_mgr == "apt"

    - name: Cleanup packages directory before installing (if it exists)
      ansible.builtin.file:
        path: "{{ download_path }}"
        state: absent

    - name: Create default directories for installing
      ansible.builtin.file:
        path: "{{ download_path }}"
        state: directory
        owner: root
        group: root
        mode: 0775

    - name: Handle Agent download and install of found rpm packages
      block:
        - name: Download found rpm agent with version from monitoring host
          nsible.builtin.get_url:
            url: "{{ rpm_download }}"
            dest: "{{ download_path }}/{{ rpm_basename }}"

        - name: Install rpm agent via YUM
          ansible.builtin.yum:
            name: "{{ download_path }}/{{ rpm_basename }}"
            allow_downgrade: true
            state: present
            disable_gpg_check: yes
          when:
            - ansible_pkg_mgr == "yum"

        - name: Install rpm agent via ZYPPER
          community.general.zypper:
            name: "{{ download_path }}/{{ rpm_basename }}"
            force: true
            state: present
            disable_gpg_check: yes
          environment:
            ZYPP_LOCK_TIMEOUT: 30
          when:
            - ansible_pkg_mgr == "zypper"

        - name: Install rpm agent via DNF
          ansible.builtin.dnf:
            name: "{{ download_path }}/{{ rpm_basename }}"
            allow_downgrade: true
            state: present
            disable_gpg_check: yes
          when:
            - ansible_pkg_mgr == "dnf"
      when:
        - ansible_pkg_mgr == "yum" or ansible_pkg_mgr == "dnf" or ansible_pkg_mgr == "zypper"

    - name: Download found deb agent with version from monitoring host
      ansible.builtin.get_url:
        url: "{{ deb_download }}"
        dest: "{{ download_path }}/{{ deb_basename }}"
      when:
        - ansible_pkg_mgr == "apt"

    - name: Install deb Agent package
      ansible.builtin.apt:
        deb: "{{ download_path }}/{{ deb_basename }}"
        allow_downgrade: yes
      become: yes

      when:
        - ansible_pkg_mgr == "apt"

    - name: find installed plugins in main plugins directory
      ansible.builtin.find:
        paths: "{{ plugin_path }}"
        file_type: file
        recurse: yes
      register: plugins_main

    - name: Set paths of found plugins as fact
      ansible.builtin.set_fact:
        plugins_main_found: "{{ plugins_main | json_query('files[*].path') }}"

    - name: Update found plugins with versions from monitoring host
      ansible.builtin.get_url:
        url: "https://{{ hostvars[monitor_host]['ansible_fqdn'] }}/{{ monitor_site }}/check_mk/agents/plugins/{{ item | basename }}"
        dest: "{{ item }}"
        force: true
      with_items:
           - "{{ plugins_main_found }}"
      ignore_errors: true

    - name: Check presense of mk_apt
      ansible.builtin.stat:
        path: /usr/lib/check_mk_agent/plugins/3600/mk_apt
      register: apt_plugin

    - name: update setting in mk_apt script to use dist-upgrade
      ansible.builtin.lineinfile:
        path: /usr/lib/check_mk_agent/plugins/3600/mk_apt
        regexp: '^UPGRADE=.*'
        line: UPGRADE=dist-upgrade
      when:
        - apt_plugin.stat.exists

    - name: Disable Unwanted CheckMK Services
      ansible.builtin.systemd:
        state: stopped
        enabled: no
        name: cmk-agent-ctl-daemon

The above worked fine up until i had (debian-flavoured) hosts started complaining after updates:

An unknown error occurred: HTTPSConnection.__init__() got an unexpected keyword argument 'cert_file'

A bit of research revealed that this has to do with a/the version of Ansible distributed by default in RockyLinux by default.

So to try and avoid/coap this issue i started to look into grabbing the agent over the API on Ansible and created a new playbook for it:

---
- hosts: all
  vars:
    monitor_host: 'monitor.myhost.tld'
    monitor_site: 'mysite'
    download_path: '/usr/local/install/packages'
    plugin_path: '/usr/lib/check_mk_agent/plugins'
    api_user: 'your_automation_user'
    api_password: 'your_automation_user_password'

  tasks:
    - name: Cleanup packages directory before installing (if it exists)
      ansible.builtin.file:
        path: "{{ download_path }}"
        state: absent

    - name: Create default directories for installing
      ansible.builtin.file:
        path: "{{ download_path }}"
        state: directory
        owner: root
        group: root
        mode: 0775

    - name: Handle CMK Agent download via API and install of found rpm packages
      block:
        - name: Query rpm agent information via API
          ansible.builtin.uri:
            url: "https://{{ monitor_host }}/{{ monitor_site }}/check_mk/api/1.0/domain-types/agent/actions/download/invoke?os_type=linux_rpm"
            validate_certs: false
            headers:
              Content-Type: "application/json"
              Accept: "*/*"
              Authorization: "Bearer {{ api_user }} {{ api_password }}"
            method: GET
            return_content: true
          register: rpm_response

        - name: Save .rpm CMK Agent to file
          ansible.builtin.copy:
            content: "{{ rpm_response.content }}"
            dest: "{{ download_path }}/{{ rpm_response.content_disposition | regex_findall('filename=\"([^\"]+)\"') | first }}"

        - name: Install rpm agent via YUM
          ansible.builtin.yum:
            name: "{{ download_path }}/{{ rpm_response.content_disposition | regex_findall('filename=\"([^\"]+)\"') | first }}"
            allow_downgrade: true
            state: present
            disable_gpg_check: yes
          when:
            - ansible_pkg_mgr == "yum"

        - name: Install rpm agent via ZYPPER
          community.general.zypper:
            name: "{{ download_path }}/{{ rpm_response.content_disposition | regex_findall('filename=\"([^\"]+)\"') | first }}"
            force: true
            state: present
            disable_gpg_check: yes
          environment:
            ZYPP_LOCK_TIMEOUT: 30
          when:
            - ansible_pkg_mgr == "zypper"

        - name: Install rpm agent via DNF
          ansible.builtin.dnf:
            name: "{{ download_path }}/{{ rpm_response.content_disposition | regex_findall('filename=\"([^\"]+)\"') | first }}"
            allow_downgrade: true
            state: present
            disable_gpg_check: yes
          when:
            - ansible_pkg_mgr == "dnf"
      when:
        - ansible_pkg_mgr == "yum" or ansible_pkg_mgr == "dnf" or ansible_pkg_mgr == "zypper"

    - name: Handle CMK Agent download via API and install of found deb packages
      block:
        - name: Query deb agent information via API
          ansible.builtin.uri:
            url: "https://{{ monitor_host }}/{{ monitor_site }}/check_mk/api/1.0/domain-types/agent/actions/download/invoke?os_type=linux_deb"
            validate_certs: false
            headers:
              Content-Type: "application/json"
              Accept: "*/*"
              Authorization: "Bearer {{ api_user }} {{ api_password }}"
            method: GET
            return_content: true
          register: deb_response

        - name: Save .deb CMK Agent to file
          ansible.builtin.copy:
            content: "{{ deb_response.content }}"
            dest: "{{ download_path }}/{{ deb_response.content_disposition | regex_findall('filename=\"([^\"]+)\"') | first }}"

        - name: Install deb Agent package
          ansible.builtin.apt:
            deb: "{{ download_path }}/{{ deb_response.content_disposition | regex_findall('filename=\"([^\"]+)\"') | first }}"
            allow_downgrade: yes
          become: yes
      when:
        - ansible_pkg_mgr == "apt"

    - name: find installed plugins in main plugins directory
      ansible.builtin.find:
        paths: "{{ plugin_path }}"
        file_type: file
        recurse: yes
      register: plugins_main

    - name: Set paths of found plugins as fact
      ansible.builtin.set_fact:
        plugins_main_found: "{{ plugins_main | json_query('files[*].path') }}"

    - name: Update found plugins with versions from monitoring host
      ansible.builtin.get_url:
        url: "https://{{ hostvars[monitor_host]['ansible_fqdn'] }}/{{ monitor_site }}/check_mk/agents/plugins/{{ item | basename }}"
        dest: "{{ item }}"
        force: true
      with_items:
           - "{{ plugins_main_found }}"
      ignore_errors: true

    - name: Check presense of mk_apt
      ansible.builtin.stat:
        path: /usr/lib/check_mk_agent/plugins/3600/mk_apt
      register: apt_plugin

    - name: update setting in mk_apt script to use dist-upgrade
      ansible.builtin.lineinfile:
        path: /usr/lib/check_mk_agent/plugins/3600/mk_apt
        regexp: '^UPGRADE=.*'
        line: UPGRADE=dist-upgrade
      when:
        - apt_plugin.stat.exists

    - name: Disable Unwanted CheckMK Services
      ansible.builtin.systemd:
        state: stopped
        enabled: no
        name: cmk-agent-ctl-daemon

The way of grabbing the agent this way works, but when you have a few legacy systems they are going to complain ( example is from SLES12.3 systems) next to the above issue with i was trying to get rid of.

fatal: [sles12.3.myhost.tld]: FAILED! => {"changed": false, "msg": "could not write content temp file: 'utf-8' codec can't encode character '\\uda5b' in position 1257: surrogates not allowed"}

So in changing the way i was trying to grab the Agent, instead of making it work i now introduced another issue.

Then i dug into the original error, and there seems to be an issue with the Ansible version being supplied via EPEL / RockyLinux.
However there was no newer version available, or i would have to go the ‘pip’-way of deploying a newer version.

Pip however is annoying as it is user-specific.

So instead ( after watching a video https://www.youtube.com/watch?v=9eHtelvVi6o, the alternative was to install a/the newer version via GIT.

For me this worked perfectly, as after that i was able to deploy my agents again ( apart from the SLES12.3 -hosts) with a notice tho:

[DEPRECATION WARNING]: Module "ansible.builtin.uri" returned non UTF-8 data in the JSON response. This will become an error in the future. This feature will
be removed in version 2.18. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.

So yes it works, but in retrospect, the api-method ( most likely can be refined) is slower, and breaks legacy systems.
So the way to go is imho update your Ansible version (to atleast 2.15.x) via GIT, and then use the initial method of deploying your agents, not triggering depreciation -stuff/messages in Ansible, and not breaking legacy host-compatibillity.

  • Glowsome

When I read this I am glad that we have checkmk enterprise and RedHat as a standard :slight_smile:

I wrote this to show that things are possible, but not as ‘smooth’ as when you just throw money over the counter to get subscription/enterprise products.

Most of the times it all works, but just as shown above requires some additional effort.

  • Glowsome

You could have used the Checkmk Ansible collection and just use the role checkmk.general.agent to install and register the agent.

https://galaxy.ansible.com/ui/repo/published/checkmk/general/

My playbooks now look like

- name: Install Checkmk agent on all hosts
  hosts: all
  debugger: on_failed
  roles:
    - checkmk.general.agent

You need to set some variables, but these are all explained in the collection documentation.

Hello Glowsome,

thank you very much for sharing all the work you’ve done – I truly appreciate the effort and dedication that went into it.

In an enterprise environment with around 20k hosts and all their different plugin combinations, we simply couldn’t manage without tools like the agent bakery in Checkmk Enterprise.
Operating a fleet of 280 monitoring servers without solutions such as Red Hat Satellite and Ansible would create an enormous workload.

Your contribution shows once again that every investment we make in these tools is worthwhile – saving us a great deal of time, resources, and ultimately money.

Please keep up the great work!

Best regards,
Mike

1 Like

I knew of the collection, but again, i wanted to prove that things can be handled with ‘the basics’.
I myself find it important to first understand the basics, and then use additional resources ( like the CMK collection) to make it easier.

  • Glowsome
2 Likes

In my humble experience, installing Ansible as a Python package is the best way to go.

Let me elaborate:
When I started using Ansible I also stuck to the version the Linux Distribution I used would ship. And I did not trust the whole pip-thingy.
Now, roughly ten years later, I am using uv (like pip but on steroids) to manage my Ansible projects and install Ansible versions based on the project (along with other dependencies).
It was a long journey with intermediate steps, cries of frustration and more than one broken Linux system because of broken Python stuff.

Why am I sharing this? I think there is no alternative to installing Ansible as a Python package. Which packaging you use is up to you of course. But distribution packages will always be rather old and building from git seems more complicated than necessary.

As explained, the delivered Ansible version on RockyLinux (9.6 now), via EPEL repository was giving me - the longer time went ahead and no updates were available more and more issues.

  • first a Kali Linux box was complaining when downloading a/the new agent (after a CMK-update/upgrade)
  • Then some Ubuntu 22.04 (after upgrading from 20.04) - boxes started acting up in the same behaviour.
  • Then after upgrading Proxmox VE to v9, based on Debian Trixie started complaining in the same way.
  • And last my Proxmox Backup Server(s) also started complaining after upgrading to v4.x + Debian Trixie.

So yes, i do prefer running repository-based packages, but yeah, functionality was going down the drain the more i upgraded boxes on my end.

As you i also do not like the pip-way.
So as the least of evil i chose for the GIT-way to get the direly needed functionality back.
… .and that payed off for me :slight_smile:

  • Glowsome
1 Like

I always run Ansible from Python venv. Makes it trivial to

  1. install & upgrade multiple versions,
  2. test newer versions without having to f*** up my system setup,
  3. provide the exact version numbers I use & test to all of my colleages in an easy-to-use installation script,
  4. not mess with stuff managed by package managers.

This is a simple script I currently use to install all versions I want to keep around in the directory I place this script in:

#!/bin/zsh

set -e

typeset -A ansible_versions

ansible_versions=(
  2.16 9.0
  2.17 10.0
  2.18 11.0
)

cd ${0:a:h}

for core_version in ${@:-${(pk)ansible_versions}}; do
  test -d ${core_version} || python3 -m venv ${core_version}

  next_core_version=${core_version%.*}.$(( ${core_version#*.} + 1 ))
  ansible_version=${ansible_versions[${core_version}]}
  next_ansible_version=$(( ${ansible_version%.*} + 1)).0

  ${core_version}/bin/pip install "ansible-core >= ${core_version}, < ${next_core_version}" "ansible >= ${ansible_version}, < ${next_ansible_version}" future jmespath netaddr passlib
done
2 Likes