Skip to content

community.general.nmcli Not clearing routes when empty list provided #10168

Open
@aakemon

Description

@aakemon

Summary

I am facing a problem where an earlier set route is not cleared when using the same nmcli command with an empty list. When using a simple task to set the route to an empty list it is cleared as expected. It seems that ansible is checking some of the differences between the current interface and the wanted changes and decides that no changes are needed. Then proceeds to say:
"Exists": "Connections already exist and no changes made",
"changed": false,

Even though we can clearly see in green that we wanted to give:
"routes4": null,
"routes4_extended": [],

And it doesnt matter if I simplyfy routes4_extended to routes4 as we might have policy based routing too, so the routes4_extended is needed.

Simple test playbook included to reproduce the error.

Issue Type

Bug Report

Component Name

nmcli

Ansible Version

$ ansible --version
ansible --version
ansible [core 2.14.17]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.9/site-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.9.19 (main, Sep 11 2024, 00:00:00) [GCC 11.5.0 20240719 (Red Hat 11.5.0-2)] (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = True

Community.general Version

$ ansible-galaxy collection list community.general

ansible-galaxy collection list community.general

# /usr/share/ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 7.1.0

# /usr/lib/python3.9/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 6.6.2

Configuration

[root@ ansible]# ansible-config dump --only-changed
CONFIG_FILE() = /etc/ansible/ansible.cfg
[root@ ansible]# cat /etc/ansible/ansible.cfg 
# Since Ansible 2.12 (core):
# To generate an example config file (a "disabled" one with all default settings, commented out):
#               $ ansible-config init --disabled > ansible.cfg
#
# Also you can now have a more complete file by including existing plugins:
# ansible-config init --disabled -t all > ansible.cfg

# For previous versions of Ansible you can check for examples in the 'stable' branches of each version
# Note that this file was always incomplete  and lagging changes to configuration settings

# for example, for 2.9: https://github.com/ansible/ansible/blob/stable-2.9/examples/ansible.cfg

OS / Environment

tested on almalinux 9.6 and 9.5
cat /etc/os-release 
NAME="AlmaLinux"
VERSION="9.5 (Teal Serval)"
ID="almalinux"
ID_LIKE="rhel centos fedora"
VERSION_ID="9.5"
PLATFORM_ID="platform:el9"
PRETTY_NAME="AlmaLinux 9.5 (Teal Serval)"
ANSI_COLOR="0;34"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:almalinux:almalinux:9::baseos"
HOME_URL="https://almalinux.org/"
DOCUMENTATION_URL="https://wiki.almalinux.org/"
BUG_REPORT_URL="https://bugs.almalinux.org/"

ALMALINUX_MANTISBT_PROJECT="AlmaLinux-9"
ALMALINUX_MANTISBT_PROJECT_VERSION="9.5"
REDHAT_SUPPORT_PRODUCT="AlmaLinux"
REDHAT_SUPPORT_PRODUCT_VERSION="9.5"
SUPPORT_END=2032-06-01

Steps to Reproduce

run the following ansible playbook on your local machine with nmcli installed and ansible.galaxy with nmcli module:
ansible-playbook -c local -i localhost, playbook.yml

<your_test_folder>/
├── playbook.yml
└── interface_configs/
    ├── set_static_ip_and_route.yml
    └── clear_routes_keep_ip.yml

Files:
playbook.yml:

# playbook.yml
- name: Configure Network Interface Test
  hosts: all # Takes hosts from inventory.ini (e.g., localhost or testhost)
  become: yes # Required for network configuration changes
  # You can choose which config to load by changing the path here
  vars_files: "interface_configs/set_static_ip_and_route.yml"
  #vars_files: "interface_configs/clear_routes_keep_ip.yml" # Defaulting to clear for testing your scenario

  tasks:
    - name: "Debug loaded variables (optional)"
      ansible.builtin.debug:
        msg:
          - "Connection Name: {{ conn_name }}"
          - "Interface Device: {{ interface.dev }}"
          - "IP Param: {{ ip4_param if ip4_param is iterable else 'omitted' }}"
          - "GW4: {{ ((interface.defroute | default(false)) | ternary(interface.gw4, 'omitted')) if ip4_param is iterable else 'omitted' }}"
          - "Method4: {{ ((interface.bootproto | default('')) == 'dhcp') | ternary('auto', 'manual') if is_ipv4 else 'disabled' }}"
          - "Never Default4: {{ (not interface.defroute|default(false))|bool if not is_gw_set|bool else 'omitted' }}"
          - "Routes4 Extended: {{ route_list if ip4_param is iterable else 'omitted' }}"
          - "is_gw_set: {{ is_gw_set }}"
          - "iftype: {{ iftype }}"

    - name: "Configure network interface {{ interface.dev }} for ethernet connection"
      community.general.nmcli: # Make sure you have community.general collection installed
        conn_name: "{{ conn_name }}"
        ifname: "{{ interface.dev }}"
        type: ethernet
        ip4: "{{ ip4_param if ip4_param is iterable else omit }}"
        gw4: "{{ ((interface.defroute | default(false)) | ternary(interface.gw4, omit)) if ip4_param is iterable else omit }}"
        dns4: "{{ dns_list if ip4_param is iterable else omit }}"
        dns4_search: "{{ search_list if ip4_param is iterable else omit }}"
        method4: "{{ ((interface.bootproto | default('')) == 'dhcp') | ternary('auto', 'manual') if is_ipv4 else 'disabled' }}"
        never_default4: false
        routes4_extended: "{{ route_list if ip4_param is iterable else omit }}" # This will be [] when using clear_routes_keep_ip.yml
        routing_rules4: "{{ rule_list if ip4_param is iterable else omit }}"
        method6: disabled # Explicitly disable IPv6 for simplicity here
        state: present
        zone: "{{ interface.zone | default('internal') }}"
      register: nmcli_result
      when: iftype == 'ethernet'

    - name: "Show nmcli task result"
      ansible.builtin.debug:
        var: nmcli_result

    - name: "Show current routes on {{ interface.dev }} (Linux)"
      ansible.builtin.command: "ip route show dev {{ interface.dev }}"
      register: current_routes
      changed_when: false
      when: nmcli_result is defined and (nmcli_result.changed or nmcli_result.ok)

    - name: "Display current routes"
      ansible.builtin.debug:
        var: current_routes.stdout_lines
      when: current_routes is defined and current_routes.stdout_lines is defined

    - name: "Show connection details using nmcli for {{ conn_name }}"
      ansible.builtin.command: "nmcli connection show \"{{ conn_name }}\""
      register: nmcli_show
      changed_when: false
      when: nmcli_result is defined and (nmcli_result.changed or nmcli_result.ok)

    - name: "Display connection details"
      ansible.builtin.debug:
        var: nmcli_show.stdout_lines
      when: nmcli_show is defined and nmcli_show.stdout_lines is defined

clear_routes_keep_ip.yml

interface_device: "dummy_test"
conn_name: "test-connection-{{ interface_device }}"
iftype: "ethernet"
is_ipv4: true

interface:
  dev: "{{ interface_device }}"
  bootproto: "static"
  zone: "public"
  defroute: false

ip4_param:
  - "192.168.123.100/24"
dns_list:
  - "8.8.8.8"
  - "1.1.1.1"
search_list:
  - "example.com"
  - "lab.local"
route_list: []
rule_list: []
is_gw_set: "{{ interface.defroute | default(false) and interface.gw4 is defined and interface.gw4 | trim != '' }}"

set_static_ip_and_route.yml:

interface_device: "dummy_test"
conn_name: "test-connection-{{ interface_device }}"
iftype: "ethernet"
is_ipv4: true
interface:
  dev: "{{ interface_device }}"
  bootproto: "static"
  zone: "public"
  defroute: false

ip4_param:
  - "192.168.123.100/24"

dns_list:
  - "8.8.8.8"
  - "1.1.1.1"
search_list:
  - "example.com"
  - "lab.local"
route_list:
  - ip: "12.100.10.12"
    next_hop: "12.100.45.97"
rule_list:
is_gw_set: "{{ interface.defroute | default(false) and interface.gw4 is defined and interface.gw4 | trim != '' }}"

Expected Results

I expect ansible to be able to pick up that the connection has routes defined as shown with:

 nmcli con show test-connection-dummy_test | grep routes
ipv4.routes:                            { ip = 12.100.10.12/32, nh = 12.100.45.97 }

And then after running the playbook the route is emptied:

TASK [Configure network interface dummy_test for ethernet connection] *******************************************************************************************************************************************************************task path: /tmp/ansible/playbook.yml:23
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: root
<localhost> EXEC /bin/sh -c 'echo ~root && sleep 0'
<localhost> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir "` echo /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751 `" && echo ansible-tmp-1747992451.261325-3057965-10737433201751="` echo /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751 `" ) && sleep 0'
Using module file /usr/share/ansible/collections/ansible_collections/community/general/plugins/modules/nmcli.py
<localhost> PUT /root/.ansible/tmp/ansible-local-3057882j4d4md9t/tmp1r8vb3wa TO /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/AnsiballZ_nmcli.py
<localhost> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/ /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/AnsiballZ_nmcli.py && sleep 0'
<localhost> EXEC /bin/sh -c '/usr/bin/python3 /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/AnsiballZ_nmcli.py && sleep 0'
<localhost> EXEC /bin/sh -c 'rm -f -r /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/ > /dev/null 2>&1 && sleep 0'
changed: [mgw1-test] => changed=true 
  Exists: Connections do exist so we are modifying them
    "conn_name": "test-connection-dummy_test",
    "invocation": {
        "module_args": {
            "addr_gen_mode6": null,
            "ageingtime": 300,
            "arp_interval": null,
            "arp_ip_target": null,
            "autoconnect": true,
            "conn_name": "test-connection-dummy_test",
            "dhcp_client_id": null,
            "dns4": [
                "8.8.8.8",
                "1.1.1.1"
            ],
            "dns4_ignore_auto": false,
            "dns4_search": [
                "example.com",
                "lab.local"
            ],
            "dns6": null,
            "dns6_ignore_auto": false,
            "dns6_search": null,
            "downdelay": null,
            "egress": null,
            "flags": null,
            "forwarddelay": 15,
            "gsm": null,
            "gw4": null,
            "gw4_ignore_auto": false,
            "gw6": null,
            "gw6_ignore_auto": false,
            "hairpin": false,
            "hellotime": 2,
            "ifname": "dummy_test",
            "ignore_unsupported_suboptions": false,
            "ingress": null,
            "ip4": [
                "192.168.123.100/24"
            ],
            "ip6": null,
            "ip_privacy6": null,
            "ip_tunnel_dev": null,
            "ip_tunnel_input_key": null,
            "ip_tunnel_local": null,
            "ip_tunnel_output_key": null,
            "ip_tunnel_remote": null,
            "mac": null,
            "macvlan": null,
            "master": null,
            "maxage": 20,
            "may_fail4": true,
            "method4": "manual",
            "method6": "disabled",
            "miimon": null,
            "mode": "balance-rr",
            "mtu": null,
            "never_default4": false,
            "path_cost": 100,
            "primary": null,
            "priority": 128,
            "route_metric4": null,
            "route_metric6": null,
            "routes4": null,
            "routes4_extended": [],
            "routes6": null,
            "routes6_extended": null,
            "routing_rules4": [],
            "runner": "roundrobin",
            "runner_fast_rate": null,
            "runner_hwaddr_policy": null,
            "slave_type": null,
            "slavepriority": 32,
            "ssid": null,
            "state": "present",
            "stp": true,
            "transport_mode": null,
            "type": "ethernet",
            "updelay": null,
            "vlandev": null,
            "vlanid": null,
            "vpn": null,
            "vxlan_id": null,
            "vxlan_local": null,
            "vxlan_remote": null,
            "wifi": null,
            "wifi_sec": null,
            "wireguard": null,
            "xmit_hash_policy": null,
            "zone": "public"
        }
    },
    "state": "present"

Actual Results

TASK [Configure network interface dummy_test for ethernet connection] *******************************************************************************************************************************************************************task path: /tmp/ansible/playbook.yml:23
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: root
<localhost> EXEC /bin/sh -c 'echo ~root && sleep 0'
<localhost> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir "` echo /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751 `" && echo ansible-tmp-1747992451.261325-3057965-10737433201751="` echo /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751 `" ) && sleep 0'
Using module file /usr/share/ansible/collections/ansible_collections/community/general/plugins/modules/nmcli.py
<localhost> PUT /root/.ansible/tmp/ansible-local-3057882j4d4md9t/tmp1r8vb3wa TO /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/AnsiballZ_nmcli.py
<localhost> EXEC /bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/ /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/AnsiballZ_nmcli.py && sleep 0'
<localhost> EXEC /bin/sh -c '/usr/bin/python3 /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/AnsiballZ_nmcli.py && sleep 0'
<localhost> EXEC /bin/sh -c 'rm -f -r /root/.ansible/tmp/ansible-tmp-1747992451.261325-3057965-10737433201751/ > /dev/null 2>&1 && sleep 0'
ok: [localhost] => {
    "Exists": "Connections already exist and no changes made",
    "changed": false,
    "conn_name": "test-connection-dummy_test",
    "invocation": {
        "module_args": {
            "addr_gen_mode6": null,
            "ageingtime": 300,
            "arp_interval": null,
            "arp_ip_target": null,
            "autoconnect": true,
            "conn_name": "test-connection-dummy_test",
            "dhcp_client_id": null,
            "dns4": [
                "8.8.8.8",
                "1.1.1.1"
            ],
            "dns4_ignore_auto": false,
            "dns4_search": [
                "example.com",
                "lab.local"
            ],
            "dns6": null,
            "dns6_ignore_auto": false,
            "dns6_search": null,
            "downdelay": null,
            "egress": null,
            "flags": null,
            "forwarddelay": 15,
            "gsm": null,
            "gw4": null,
            "gw4_ignore_auto": false,
            "gw6": null,
            "gw6_ignore_auto": false,
            "hairpin": false,
            "hellotime": 2,
            "ifname": "dummy_test",
            "ignore_unsupported_suboptions": false,
            "ingress": null,
            "ip4": [
                "192.168.123.100/24"
            ],
            "ip6": null,
            "ip_privacy6": null,
            "ip_tunnel_dev": null,
            "ip_tunnel_input_key": null,
            "ip_tunnel_local": null,
            "ip_tunnel_output_key": null,
            "ip_tunnel_remote": null,
            "mac": null,
            "macvlan": null,
            "master": null,
            "maxage": 20,
            "may_fail4": true,
            "method4": "manual",
            "method6": "disabled",
            "miimon": null,
            "mode": "balance-rr",
            "mtu": null,
            "never_default4": false,
            "path_cost": 100,
            "primary": null,
            "priority": 128,
            "route_metric4": null,
            "route_metric6": null,
            "routes4": null,
            "routes4_extended": [],
            "routes6": null,
            "routes6_extended": null,
            "routing_rules4": [],
            "runner": "roundrobin",
            "runner_fast_rate": null,
            "runner_hwaddr_policy": null,
            "slave_type": null,
            "slavepriority": 32,
            "ssid": null,
            "state": "present",
            "stp": true,
            "transport_mode": null,
            "type": "ethernet",
            "updelay": null,
            "vlandev": null,
            "vlanid": null,
            "vpn": null,
            "vxlan_id": null,
            "vxlan_local": null,
            "vxlan_remote": null,
            "wifi": null,
            "wifi_sec": null,
            "wireguard": null,
            "xmit_hash_policy": null,
            "zone": "public"
        }
    },
    "state": "present"

Code of Conduct

  • I agree to follow the Ansible Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue/PR relates to a bugmodulemodulepluginsplugin (any type)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions