Cisco Playbook sents commands multiple times

I’ve got a playbook that is triggered by a Netbox Interface change and it’s almost working as expected. When the playbook runs and connected to the Cisco device if it needs to push the block of changes it will push it multiple times which in some cases causes errors. And it will push the commands for every command in the list. For example if there are 3 commands in the task is will send all 3 commands 3 times to the switch. I’m at a loss as to why :frowning: it causes issues with something like “auto qos voip cisco-phone” because it can only be run once so it errors out.

Running Ansibe 2.20.2 on Ubuntu 24.04

---
- name: Configure switch interface based on NetBox
  hosts: "{{ switch_name }}"
  gather_facts: no
  run_once: true

  vars:
    netbox_url: "https://localhost"
    netbox_token: Removed
    # default values (can override via --extra-vars)

  tasks:
    - name: Get switch info from NetBox
      set_fact:
        switch_info: >-
          {{ query('netbox.netbox.nb_lookup', 'devices',
                   api_endpoint=netbox_url,
                   api_filter='name=' ~ switch_name,
                   token=netbox_token)[0].value }}

    - name: Get Virtual Chassis Information from stacked switch
      set_fact:
        vc_info: >-
          {{ query('netbox.netbox.nb_lookup', 'virtual-chassis',
                   api_endpoint=netbox_url,
                   api_filter='master_id=' ~ switch_info['virtual_chassis']['master']['id'],
                   token=netbox_token)[0].value }}
      when: switch_info.virtual_chassis is defined and switch_info.virtual_chassis is not none

    - name: Loop thru Virtual Chassis members to find Interface Data from stacked switch
      set_fact:
        found_interfaces: >-
          {{ lookup(
               'netbox.netbox.nb_lookup',
               'interfaces',
               api_endpoint=netbox_url,
               token=netbox_token,
               api_filter='device_id=' ~ item.id  ~ ' name=' ~ interface_name
             ) | default([]) }}
      loop: "{{ vc_info.members }}"
      loop_control:
        label: "{{ item.name }}"
        break_when: found_interfaces | length > 0

      when: vc_info is defined

    - name: Get Interface Data from non-stacked switch
      set_fact:
        found_interfaces: >-
          {{ lookup(
               'netbox.netbox.nb_lookup',
               'interfaces',
               api_endpoint=netbox_url,
               token=netbox_token,
               api_filter='device=' ~ switch_info['name']  ~ ' name=' ~ interface_name
             ) | default([]) }}
      when: vc_info is not defined


    - name: Fail if interface not found in Netbox
      fail:
        msg: "Interface {{ interface_name }} was not found on device {{ switch_info['name'] }} in Netbox."
      when: found_interfaces | length == 0

    - name: Extract tag slugs
      set_fact:
        interface_tag_slugs: "{{ found_interfaces.value.tags | map(attribute='slug') | list }}"

    - name: Build interface_info object
      set_fact:
        interface_info:
          description: "{{ found_interfaces['value']['description'] | default('') }}"
          vlan: "{{ found_interfaces['value']['untagged_vlan']['vid'] | default(None) }}"
          vlan_mode: "{{ found_interfaces['value']['mode']['value'] }}"
          is_unmanaged: "{{ 'ansible-unmanaged' in interface_tag_slugs }}"
          voice: "{{ found_interfaces['value']['custom_fields']['Voice_VLAN'] }}"
          enabled: "{{ found_interfaces['value']['enabled'] }}"

#    - name: Show interface summary
#      debug:
#        var: interface_info

    - name: Stop playbook if unmanaged
      meta: end_play
      when: interface_info.is_unmanaged | bool

    - name: Get interface config
      cisco.ios.ios_command:
        commands:
          - show running-config interface {{ interface_name }}
      register: interface_config

    - name: Configure - Interface Description
      cisco.ios.ios_config:
        lines:
          - description {{ interface_info.description }}
        parents: "interface {{ interface_name }}"

    - name: Configure - Interface VLAN
      cisco.ios.ios_config:
        lines:
          - switchport access vlan {{ interface_info.vlan }}
          - switchport mode access
          - spanning-tree portfast
        parents: "interface {{ interface_name }}"
      when: interface_info.vlan_mode == "access"

    - name: Configure - Enable CDP and LLDP
      cisco.ios.ios_config:
        lines:
          - cdp enable
          - lldp transmit
        parents: "interface {{ interface_name }}"
      when:
        - interface_info.voice == true
        - "'cdp' in interface_config.stdout[0]"

    - name: Configure - Disable CDP and LLDP
      cisco.ios.ios_config:
        lines:
          - no cdp enable
          - no lldp transmit
        parents: "interface {{ interface_name }}"
      when:
        - interface_info.voice == false


    - name: Configure - Enable VOIP Phone
      cisco.ios.ios_config:
        commands:
          - switchport voice vlan 100
          - auto qos voip cisco-phone
        parents: "interface {{ interface_name }}"
        match: none
      ignore_errors: yes
      when:
        - interface_info.voice == true
        - "'switchport voice vlan 100' not in interface_config.stdout[0]"

    - name: Configure - Disable VOIP Phone
      cisco.ios.ios_config:
        commands:
          - no switchport voice vlan 100
          - no auto qos voip cisco-phone
        parents: "interface {{ interface_name }}"
        match: none
      ignore_errors: yes
      when:
        - interface_info.voice == false
        - "'switchport voice vlan ' in interface_config.stdout[0]"