ansible role cloudflared - remove unlisted services

Posted in automation on December 21, 2020 by Adrian Wyssmann ‐ 3 min read

One of the enhancements I want to do for my ansible role “cloudflared” is this:

The role should compare running cloudflare@xxx services against the ones listed in tunnels and if not listed anymore to the following:

  • stop and remove systemd service
  • remove unused config file
  • remove log file

The role does use systemd-unit-template cloudflared@{{ tunnel }}.service and start an instance for each service in the list of tunnels. So to implement the feature I have to figure out a way, of which related services are running. Well, one could use the shell-module and start with something like this:

systemctl list-units --type service | grep -oEi 'cloudflared@.+' | cut -d '@' -f 2

This single liner would already give me the information I am looking for. But executing shell commands is usually not the best, so I was looking for the ansible way. It appears service_facts_module is the thing I am looking for:

Return service state information as fact data for various service management utilities

I easily can get all services like this

- hosts: servers
  tasks:
    - name: Populate service facts
      service_facts:
    - name: Setting host facts
      set_fact:
        tunnels_available: "{{ ansible_facts.services | list }}"
    - debug: msg={{ tunnels_available }}

This returns something like this, including all services:

ok: [node001] => {
    "msg": [
        "apparmor",
        "cgroupfs-mount",
        "console-setup.sh",
        "cpufrequtils",
        ...
        "[email protected]",
        "[email protected]",
        ...

A good starting point, however, I am only interested in cloudflared services, thus the set_fact has to be enhanced accordingly, using filters. In addition, as the role requires only the names of the tunnels (e.g. ssh, k8s), I have to strip away unnecessary information:

    ...
    - name: Setting host facts
      set_fact:
        tunnels_available: "{{ ansible_facts.services | select('match', 'cloudflared@(.+).service') | map('regex_replace', 'cloudflared@', '') | map('regex_replace', '.service' '') | list }}
    ...

The result of this, is what I am looking for, I have a list of all available cloudflared services:

ok: [node001] => (item=k8s) => {
    "msg": "k8s"
}
ok: [node001] => (item=ssh) => {
    "msg": "ssh"
}

As a last thing, for the removal of services, I only need the ones installed i.e. in the list tunnels_available and not in the list of tunnels. Thus I need to get the difference of 2 lists (items in 1 that don’t exist in 2):

    ...
    - name: Setting tunnels to remove
      set_fact:
        tunnels_to_remove: "{{ tunnels_available | difference(tunnels) }}"
    ...

So assuming my list tunnels contains ssh and http and I have the tunnels ssh and k8s installed - see at the beginning of the post, I should get as a result of k8s to be removed

ok: [node001] => (item=k8s) => {
    "msg": "k8s"
}

It worked, so now I have a variable tunnels_to_remove which contains a list of all tunnels installed but to be removed. So that’s the staring point for the implementation of the feature. Have a look at the issue and the related code changes for details. Finally here you have a complete playbook which shows the essential steps as a proof-of-concept:

- hosts: servers
  vars:
    - tunnels:
      - ssh
      - http
  tasks:
    - debug: msg={{ item }}
      with_items: "{{ tunnels }}"
    - name: Populate service facts
      service_facts:
    - name: Setting tunnels_available
      set_fact:
        tunnels_available: "{{ ansible_facts.services | select('match', 'cloudflared@(.+).service') | map('regex_replace', 'cloudflared@', '') | map('regex_replace', '.service' '') | list }}"
    - debug: msg={{ item }}
      with_items: "{{ tunnels_available }}"
    - name: Setting tunnels to remove
      set_fact:
        tunnels_to_remove: "{{ tunnels_available | difference(tunnels) }}"
    - debug: msg={{ item }}
      with_items: "{{ tunnels_to_remove }}"