Description
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