diff --git a/changelogs/fragments/route_maps_acl_fix.yaml b/changelogs/fragments/route_maps_acl_fix.yaml new file mode 100644 index 000000000..ab03ffdea --- /dev/null +++ b/changelogs/fragments/route_maps_acl_fix.yaml @@ -0,0 +1,3 @@ +--- +bugfixes: + - "ios_route_maps - Fix removal of ACLs in replaced state to properly remove unspecified ACLs while leaving specified ones intact." diff --git a/plugins/module_utils/network/ios/config/route_maps/route_maps.py b/plugins/module_utils/network/ios/config/route_maps/route_maps.py index f02fbfdfb..9e9ac0686 100644 --- a/plugins/module_utils/network/ios/config/route_maps/route_maps.py +++ b/plugins/module_utils/network/ios/config/route_maps/route_maps.py @@ -217,16 +217,34 @@ def list_type_compare(self, compare_type, want, have): if val != have_val: if have_val: if self.state == "overridden" or self.state == "replaced": - self.compare( - parsers=parsers, - want=dict(), - have={compare_type: {k: {key: have_val}}}, + list_type = next(iter(val)) + # Get sets of values for comparison + want_values = set(str(v) for v in val[list_type].values()) + have_values = ( + set(str(v) for v in have_val[list_type].values()) + if have_val + else set() ) - self.compare( - parsers=parsers, - want={compare_type: {k: {key: val}}}, - have={compare_type: {k: {key: have_val}}}, - ) + # Find values that need to be removed (in have but not in want) + to_remove = have_values - want_values + # Find values that need to be added (in want but not in have) + to_add = want_values - have_values + # Handle removals first + if to_remove: + diff = {f"acl_{v}": v for v in to_remove} + self.compare( + parsers=parsers, + want=dict(), + have={compare_type: {k: {key: {list_type: diff}}}}, + ) + # Then handle additions + if to_add: + diff = {f"acl_{v}": v for v in to_add} + self.compare( + parsers=parsers, + want={compare_type: {k: {key: {list_type: diff}}}}, + have=dict(), + ) else: self.compare( parsers=parsers, diff --git a/tests/unit/modules/network/ios/fixtures/ios_route_maps.cfg b/tests/unit/modules/network/ios/fixtures/ios_route_maps.cfg index 8b0620dff..fa91735e5 100644 --- a/tests/unit/modules/network/ios/fixtures/ios_route_maps.cfg +++ b/tests/unit/modules/network/ios/fixtures/ios_route_maps.cfg @@ -43,3 +43,6 @@ route-map test_1 deny 20 set global set interface GigabitEthernet0/2 GigabitEthernet0/1 set lisp locator-set test_lisp +route-map TO_OUT permit 10 + match ip address 185 186 + set as-path prepend 1321 diff --git a/tests/unit/modules/network/ios/test_ios_route_maps.py b/tests/unit/modules/network/ios/test_ios_route_maps.py index 27c5aa7f6..7d35bcb4c 100644 --- a/tests/unit/modules/network/ios/test_ios_route_maps.py +++ b/tests/unit/modules/network/ios/test_ios_route_maps.py @@ -259,7 +259,7 @@ def test_ios_route_maps_replaced(self): exact_match=True, name=["new_replace"], ), - ip=dict(address=dict(acls=[10, 100])), + ip=dict(address=dict(acls=[10, 20, 30])), length=dict(maximum=50000, minimum=5000), mpls_label=True, policy_lists=["ip_policy"], @@ -332,7 +332,9 @@ def test_ios_route_maps_replaced(self): "continue 20", "description this is replace test", "match community new_replace exact-match", + "match ip address 20 30", "match length 5000 50000", + "no match ip address 100", "no match mdt-group 25 30", "no match community 98 99 test_1 test_2 exact-match", "no match extcommunity 110 130", @@ -573,6 +575,7 @@ def test_ios_route_maps_overridden(self): "match security-group source tag 10 20", "match local-preference 105 55", "match mpls-label", + "no route-map TO_OUT", ] result = self.execute_module(changed=True) self.assertEqual(sorted(result["commands"]), sorted(commands)) @@ -657,6 +660,29 @@ def test_ios_route_maps_overridden_idempotent(self): ], route_map="test_1", ), + dict( + route_map="TO_OUT", + entries=[ + dict( + sequence=10, + action="permit", + match=dict( + ip=dict( + address=dict( + acls=["185", "186"], + ), + ), + ), + set=dict( + as_path=dict( + prepend=dict( + as_number=["1321"], + ), + ), + ), + ), + ], + ), ], state="overridden", ), @@ -671,7 +697,10 @@ def test_ios_route_maps_deleted(self): def test_ios_route_maps_delete_without_config(self): set_module_args(dict(state="deleted")) - commands = ["no route-map test_1"] + commands = [ + "no route-map test_1", + "no route-map TO_OUT", + ] result = self.execute_module(changed=True) self.assertEqual(sorted(result["commands"]), sorted(commands)) @@ -819,3 +848,41 @@ def test_ios_route_maps_rendered(self): ] result = self.execute_module(changed=False) self.assertEqual(sorted(result["rendered"]), sorted(commands)) + + def test_ios_route_maps_replaced_acl_removal(self): + set_module_args( + dict( + config=[ + dict( + route_map="TO_OUT", + entries=[ + dict( + sequence=10, + action="permit", + match=dict( + ip=dict( + address=dict( + acls=["185"], + ), + ), + ), + set=dict( + as_path=dict( + prepend=dict( + as_number=["1321"], + ), + ), + ), + ), + ], + ), + ], + state="replaced", + ), + ) + commands = [ + "route-map TO_OUT permit 10", + "no match ip address 186", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands))