'How do I do the equivalent of combining a with_subelements loop and a with_nested loop in Ansible?

I'm writing a playbook to get a list of services deployed to a Mirantis' UCP/MKE cluster and check on all the Docker workers in that cluster, that each external swarm service port is open.

The playbook makes an API call from localhost to get an extensive JSON object of services deployed to the cluster, which is simplified using jmespath to just name, ID, ports.

As another play, my playbook runs a shell command on each worker in the cluster to obtain a list of open ports.

I'd like to loop through each port for each service and confirm if the port is open on each and every worker node.

My services/ports data object can look like this:

[
    {
        "ID": "aefgergergergergerg",
        "Name": "application1_service",
        "Ports": [
            [
                30950,
                "tcp"
            ],
            [
                30951,
                "tcp"
            ]
        ]
    },
    {
        "ID": "sdfthtrhrthrthrthrthtrh",
        "Name": "application2_service",
        "Ports": [
            [
                31190,
                "tcp"
            ]
        ]
    },
...
]

(obtained via an API call and can be simplified with a jmespath query:

'[?Endpoint.Ports].{ ID: ID, Name: Spec.Name, Ports: Endpoint.Ports[?contains(@.PublishMode,`ingress`)].[PublishedPort, PublishMode, Protocol] }'

And my worker's open ports objects look like this:

ok: [worker1] => {
    "msg": [
        "tcp:31557",
        "tcp:31501",
        "tcp:31556",
        "tcp:31500",
        "tcp:30231",
        "tcp:30230",
        "tcp:30651",
        "tcp:30650"
    ]
}
ok: [worker2] => {
    "msg": [
        "tcp:31557",
        "tcp:31501",
        "tcp:31556",
        "tcp:31500",
        "tcp:30231",
        "tcp:30230",
        "tcp:30651",
        "tcp:30650"
    ]
}
ok: [worker3] => {
    "msg": [
        "tcp:31557",
        "tcp:31501",
        "tcp:31556",
        "tcp:31500",
        "tcp:30231",
        "tcp:30230",
        "tcp:30651",
        "tcp:30650"
    ]
}

obtained with

iptables -L DOCKER-INGRESS | awk -F ' {2,}' '($1 == "ACCEPT") && ($6 ~ /dpt/) {print $6}' | sed 's/ dpt//g')

In my head, I want to combine a with_subelements loop (ports for each given service) with a with_nested loop (my subelements as the first list, and my open ports as the nested list), but I am sure this isn't quite possible.

This is the relevant part of my playbook (I've cut out the auth logic as it's not relevant)

- name: Ensure secrets are in the required collections
  hosts: localhost
  gather_facts: false
  vars_files: vars.yaml

[SNIP]

    - name: "Get a list of services from https://{{ endpoint }}/services"
      ansible.builtin.uri:
        url: "https://{{ endpoint }}/services"
        body_format: json
        headers:
          Authorization: "Bearer {{ auth.json.auth_token }}"
        validate_certs: "{{ validate_ssl_certs | default('yes') }}"
      register: services

    - name: "Create a simplified JSON object of services and ports"
      ansible.builtin.set_fact:
        services_ports: "{{ services.json | json_query(jmesquery) }}"
      vars:
        jmesquery: "{{ jmesquery_services }}"

- name: See what ports are open on which workers
  hosts: workers
  gather_facts: false
  become: true
  vars_files: vars.yaml
  tasks:
    - name: Get the list of open ports
      shell: iptables -L DOCKER-INGRESS | awk -F ' {2,}' '($1 == "ACCEPT") && ($6 ~ /dpt/) {print $6}' | sed 's/ dpt//g'
      register: iptables_rules

    - name: debug
      debug:
        msg: "{{ iptables_rules.stdout_lines }}"

And relevant bit of vars.yaml:

---
jmesquery_services: '[?Endpoint.Ports].{ ID: ID, Name: Spec.Name, Ports: Endpoint.Ports[?contains(@.PublishMode,`ingress`)].[PublishedPort, PublishMode, Protocol] }'

How best to check each of these ports for a service against each open port on each worker?



Solution 1:[1]

You are on a right track with subelements lookup plugin, you should simply loop over the service/port pairs and check if such port exists in iptables_rules.stdout_lines.

Here is an example playbook with dummy data on how to so:

- hosts: localhost
  gather_facts: false
  become: false
  tasks:
    - name: check if all service ports are open
      # Loop over service/port pairs
      loop: "{{ lookup('subelements', services_ports, 'Ports') }}"
      # Set variables for clarity
      vars:
        service_name: "{{ item[0]['Name'] }}"
        iptables_port: "{{ item[1][1] ~ ':' ~ item[1][0] }}"
        iptables_port_exists: "{{ iptables_port in iptables_rules.stdout_lines }}"
      # Fail the module if port is not found
      failed_when: "not iptables_port_exists"
      # For demo, print out the service status
      debug:
        msg: "Service {{ service_name }} is {{ 'up' if iptables_port_exists else 'down' }} on port {{ iptables_port }}"
  # Example data
  vars:
    services_ports:
      - ID: aefgergergergergerg
        Name: application1_service
        Ports:
          - ["30950", "tcp"]
          - ["30951", "tcp"]
      - ID: sdfthtrhrthrthrthrthtrh
        Name: application2_service
        Ports:
          - ["31190", "tcp"]
    iptables_rules:
      stdout_lines: [
        "tcp:30950",
        "tcp:31190",
      ]

This produces output like so:

TASK [check if all service ports are open] *************************************
ok: [localhost] => (item=[{'ID': 'aefgergergergergerg', 'Name': 'application1_service'}, ['30950', 'tcp']]) => {
    "msg": "Service application1_service is up on port tcp:30950"
}
failed: [localhost] (item=[{'ID': 'aefgergergergergerg', 'Name': 'application1_service'}, ['30951', 'tcp']]) => {
    "msg": "Service application1_service is down on port tcp:30951"
}
ok: [localhost] => (item=[{'ID': 'sdfthtrhrthrthrthrthtrh', 'Name': 'application2_service'}, ['31190', 'tcp']]) => {
    "msg": "Service application2_service is up on port tcp:31190"
}
fatal: [localhost]: FAILED! => {"msg": "One or more items failed"}

PS! I'm not familiar with jmespath, but you might need to use hostvars['localhost']['services_ports'] on the workers in order to access variables created on localhost

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1