Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions plugins/module_utils/network/nxos/config/interfaces/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.facts.facts import (
Facts,
)
from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import (
default_intf_enabled,
)
from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.rm_templates.interfaces import (
InterfacesTemplate,
)
Expand Down Expand Up @@ -138,22 +141,6 @@ def _compare(self, want, have):
begin = len(self.commands)
self.compare(parsers=self.parsers, want=want, have=have)

# Handle the 'enabled' state separately
want_enabled = want.get("enabled")
have_enabled = have.get("enabled")
if want_enabled is not None:
if want_enabled != have_enabled:
if want_enabled is True:
self.addcmd(want, "enabled", True)
else:
self.addcmd(want, "enabled", False)
elif not want and self.state == "overridden":
if have_enabled is not None:
self.addcmd(have, "enabled", False)
elif not want and self.state == "deleted":
if have_enabled:
self.addcmd(have, "enabled", False)

# Handle the 'mode' state separately
want_mode = want.get("mode")
have_mode = have.get("mode", self.defaults.get("default_mode"))
Expand All @@ -172,6 +159,26 @@ def _compare(self, want, have):
no_cmd = True if self.defaults.get("default_mode") == "layer3" else False
self.addcmd(have, "mode", no_cmd)

# Handle the 'enabled' state separately
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moves the resolution of the state to after the interface mode, so that we can feed the final have_mode into the enabled state resolution.

want_enabled = want.get("enabled")
have_enabled = have.get("enabled")

if have_enabled is None and have.get("name", None) is not None:
have_enabled = default_intf_enabled(have["name"], self.defaults, have_mode)

if want_enabled is not None:
if want_enabled != have_enabled:
if want_enabled is True:
self.addcmd(want, "enabled", True)
else:
self.addcmd(want, "enabled", False)
elif not want and self.state == "overridden":
if have_enabled is not None:
self.addcmd(have, "enabled", False)
elif not want and self.state == "deleted":
if have_enabled:
self.addcmd(have, "enabled", False)

if len(self.commands) != begin:
self.commands.insert(begin, self._tmplt.render(want or have, "name", False))

Expand Down Expand Up @@ -217,4 +224,7 @@ def get_interface_defaults(self):
if default_enabled:
interface_defs["L2_enabled"] = True if "no " in default_enabled.groups() else False

# Layer 3 interfaces default to shutdown (disabled)
interface_defs["L3_enabled"] = False

return interface_defs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def __init__(self, lines=None, module=None):
"setval": "shutdown",
"result": {
'{{ name }}': {
'enabled': "{{ False if shutdown is defined and negate is not defined else True }}",
'enabled': "{{ True if negate is defined and shutdown is defined else False if shutdown is defined else None }}",
Copy link
Author

@ChrisPortman ChrisPortman Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont assume at this point that the absence of (no)? shutdown implies either enabled or disabled.

This change means we only set enabled True|False if it is explicit in the show run output, other wise its None.

Copy link
Author

@ChrisPortman ChrisPortman Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the logic that resolves facts should handle the system default shutdown logic such that the facts are complete. But that's not the previous convention.

},
},
},
Expand Down
18 changes: 10 additions & 8 deletions tests/unit/modules/network/nxos/test_nxos_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,16 +451,13 @@ def test_3(self):
)
merged = [
"interface Ethernet1/1",
"no shutdown",
"no switchport",
"interface Ethernet1/2",
"shutdown",
"interface loopback1",
"shutdown",
"interface loopback2",
"no shutdown",
"interface loopback3",
"no shutdown",
"interface port-channel3",
"no shutdown",
"interface loopback4",
Expand All @@ -474,6 +471,11 @@ def test_3(self):
self.assertEqual(sorted(result["commands"]), sorted(merged))

# Test with an older image version which has different defaults
self.exec_get_defaults.return_value = {
"default_mode": "layer2",
"L2_enabled": False,
}

merged_legacy = [
"interface Ethernet1/1",
"no shutdown",
Expand All @@ -484,22 +486,24 @@ def test_3(self):
"shutdown",
"interface loopback2",
"no shutdown",
"interface loopback3",
"no shutdown",
"interface port-channel3",
"no shutdown",
"interface loopback4",
"no shutdown",
"interface port-channel4",
"no shutdown",
]
result = self.execute_module(changed=True, device="legacy")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually appears to be ineffective. The parent class does not process the "device" arg at all from what I can see. Or load any fixtures one way or another

result = self.execute_module(changed=True)
self.assertEqual(sorted(result["commands"]), sorted(merged_legacy))

deleted = [
"interface Ethernet1/2",
"switchport",
"shutdown",
"interface loopback1",
"shutdown",
"interface loopback3",
"shutdown",
]
playbook["state"] = "deleted"
set_module_args(playbook)
Expand All @@ -516,8 +520,6 @@ def test_3(self):
"shutdown",
"interface loopback2",
"no shutdown",
"interface loopback3",
"no shutdown",
"interface port-channel3",
"no shutdown",
"interface loopback4",
Expand Down
Loading