Skip to content

Commit

Permalink
Merge pull request #304 from CiscoDevNet/dcnm-intf-fix-229
Browse files Browse the repository at this point in the history
Fix for issue 229 to accept individual vlans in accepted-vlans parameter
  • Loading branch information
mikewiebe committed Sep 5, 2024
2 parents 9499fa6 + 990582c commit 20a80ff
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 13 deletions.
81 changes: 81 additions & 0 deletions plugins/modules/dcnm_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -4085,6 +4085,7 @@ def dcnm_intf_get_diff_overridden(self, cfg):
== "DCNM_INTF_MATCH"
):
continue

if uelem is not None:
# Before defaulting ethernet interfaces, check if they are
# member of any port-channel. If so, do not default that
Expand Down Expand Up @@ -4886,6 +4887,66 @@ def dcnm_intf_send_message_to_dcnm(self):
else:
self.result["changed"] = False

def dcnm_intf_get_xlated_object(self, cfg, key):

"""
Routine to translate individual vlans like 45, 55 to 44-44 and 55-55 format
Parameters:
cfg (dict): Config element that includes the object idebtified by key to be translated
key (str): key identifying the object to be translated
Returns:
translated object
"""

citems = cfg["profile"][key].split(",")

for index in range(len(citems)):
if (
(citems[index].lower() == "none")
or (citems[index].lower() == "all")
or ("-" in citems[index])
):
continue

# Playbook config includes individual vlans in allowed_vlans object. Convert the elem to
# appropriate format i.e. vlaues in the form of 4, 7 to 4-4 and 7-7
citems[index] = citems[index].strip() + "-" + citems[index].strip()
return citems

def dcnm_intf_translate_allowed_vlans(self, cfg):

"""
Routine to translate xxx_allowed_vlans object in the config. 'xxx_allowed_vlans' object will
allow only 'none', 'all', or 'vlan-ranges like 1-5' values. It does not allow individual
vlans to be included. To enable user to include individual vlans in the playbook config, this
routine tranlates the individual vlans like 3, 5 etc to 3-3 and 5-5 format.
Parameters:
cfg (dict): Config element that needs to be translated
Returns:
None
"""

if cfg.get("profile", None) is None:
return

if cfg["profile"].get("allowed_vlans", None) is not None:
xlated_obj = self.dcnm_intf_get_xlated_object(cfg, "allowed_vlans")
cfg["profile"]["allowed_vlans"] = ",".join(xlated_obj)
if cfg["profile"].get("peer1_allowed_vlans", None) is not None:
xlated_obj = self.dcnm_intf_get_xlated_object(
cfg, "peer1_allowed_vlans"
)
cfg["profile"]["peer1_allowed_vlans"] = ",".join(xlated_obj)
if cfg["profile"].get("peer2_allowed_vlans", None) is not None:
xlated_obj = self.dcnm_intf_get_xlated_object(
cfg, "peer2_allowed_vlans"
)
cfg["profile"]["peer2_allowed_vlans"] = ",".join(xlated_obj)

def dcnm_intf_update_inventory_data(self):

"""
Expand Down Expand Up @@ -5002,6 +5063,26 @@ def dcnm_translate_playbook_info(self, config, ip_sn, hn_sn):
cfg["switch"].remove(sw_elem)
index = index + 1

# 'allowed-vlans' in the case of trunk interfaces accepts 'all', 'none' and 'vlan-ranges' which
# will be of the form 20-30 etc. There is not way to include individual vlans which are not contiguous.
# To include individual vlans like 3,6,20 etc. user must input them in the form 3-3, 6-6, 20-20 which is
# not very intuitive. To handle this scenario, we allow playbooks to include individual vlans and translate
# them here appropriately.

if cfg.get("profile", None) is not None:
if (
(
cfg["profile"].get("peer1_allowed_vlans", None)
is not None
)
or (
cfg["profile"].get("peer2_allowed_vlans", None)
is not None
)
or (cfg["profile"].get("allowed_vlans", None) is not None)
):
self.dcnm_intf_translate_allowed_vlans(cfg)


def main():

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
- name: Put the fabric to default state
cisco.dcnm.dcnm_interface:
check_deploy: True
fabric: "{{ ansible_it_fabric }}"
fabric: "{{ ansible_it_fabric }}"
state: overridden # only choose form [merged, replaced, deleted, overridden, query]
register: result
register: result

- assert:
that:
- 'item["RETURN_CODE"] == 200'
- 'item["RETURN_CODE"] == 200'
loop: '{{ result.response }}'

- block:
Expand All @@ -37,13 +37,13 @@
profile:
admin_state: true # choose from [true, false]
mode: trunk # choose from [trunk, access, l3, monitor]
members: # member interfaces
members: # member interfaces
- "{{ ansible_eth_intf13 }}"
pc_mode: 'on' # choose from ['on', 'active', 'passive']
bpdu_guard: true # choose from [true, false, no]
port_type_fast: true # choose from [true, false]
mtu: jumbo # choose from [default, jumbo]
allowed_vlans: none # choose from [none, all, vlan range]
allowed_vlans: none # choose from [none, all, vlan range]
cmds: # Freeform config
- no shutdown
description: "port channel acting as trunk"
Expand All @@ -56,13 +56,13 @@
profile:
admin_state: true # choose from [true, false]
mode: access # choose from [trunk, access, l3, monitor]
members: # member interfaces
members: # member interfaces
- "{{ ansible_eth_intf14 }}"
pc_mode: 'on' # choose from ['on', 'active', 'passive']
bpdu_guard: true # choose from [true, false, no]
port_type_fast: true # choose from [true, false]
mtu: default # choose from [default, jumbo]
access_vlan: 301 #
access_vlan: 301 #
cmds: # Freeform config
- no shutdown
description: "port channel acting as access"
Expand All @@ -75,11 +75,11 @@
profile:
admin_state: true # choose from [true, false]
mode: l3 # choose from [trunk, access, l3, monitor]
members: # member interfaces
members: # member interfaces
- "{{ ansible_eth_intf15 }}"
pc_mode: 'on' # choose from ['on', 'active', 'passive']
mtu: 9216 # choose between [min=576, max=9216]
int_vrf: "" # interface VRF
int_vrf: "" # interface VRF
ipv4_addr: 192.168.20.1 # ipv4 address for the interface
ipv4_mask_len: 24 # choose between [min:1, max:31]
route_tag: "" # Route Tag
Expand Down Expand Up @@ -107,7 +107,7 @@

- assert:
that:
- 'item["RETURN_CODE"] == 200'
- 'item["RETURN_CODE"] == 200'
loop: '{{ result.response }}'

- name: Create port channel interfaces - Idempotence
Expand All @@ -125,7 +125,69 @@

- assert:
that:
- 'item["RETURN_CODE"] == 200'
- 'item["RETURN_CODE"] == 200'
loop: '{{ result.response }}'

##############################################
## MERGE ##
##############################################

- name: Create port channel interfaces with vlan ranges
cisco.dcnm.dcnm_interface: &pc_merge2
check_deploy: True
fabric: "{{ ansible_it_fabric }}"
state: merged # only choose form [merged, replaced, deleted, overridden, query]
config:
- name: po400 # should be of the form po<port-id>
type: pc # choose from this list [pc, vpc, sub_int, lo, eth, svi]
switch:
- "{{ ansible_switch1 }}" # provide the switch information where the config is to be deployed
deploy: true # choose from [true, false]
profile:
admin_state: true # choose from [true, false]
mode: trunk # choose from [trunk, access, l3, monitor]
members: # member interfaces
- "{{ ansible_eth_intf21 }}"
pc_mode: 'on' # choose from ['on', 'active', 'passive']
bpdu_guard: true # choose from [true, false, no]
port_type_fast: true # choose from [true, false]
mtu: jumbo # choose from [default, jumbo]
allowed_vlans: 10,20,30-40 # choose from [none, all, vlan range]
cmds: # Freeform config
- no shutdown
description: "port channel acting as trunk"
register: result

- assert:
that:
- 'result.changed == true'
- '(result["diff"][0]["merged"] | length) == 1'
- '(result["diff"][0]["deleted"] | length) == 0'
- '(result["diff"][0]["replaced"] | length) == 0'
- '(result["diff"][0]["overridden"] | length) == 0'
- '(result["diff"][0]["deploy"] | length) == 1'

- assert:
that:
- 'item["RETURN_CODE"] == 200'
loop: '{{ result.response }}'

- name: Create port channel interfaces with vlan ranges - Idempotence
cisco.dcnm.dcnm_interface: *pc_merge2
register: result

- assert:
that:
- 'result.changed == false'
- '(result["diff"][0]["merged"] | length) == 0'
- '(result["diff"][0]["deleted"] | length) == 0'
- '(result["diff"][0]["replaced"] | length) == 0'
- '(result["diff"][0]["overridden"] | length) == 0'
- '(result["diff"][0]["deploy"] | length) == 0'

- assert:
that:
- 'item["RETURN_CODE"] == 200'
loop: '{{ result.response }}'

##############################################
Expand All @@ -137,13 +199,13 @@
- name: Put fabric to default state
cisco.dcnm.dcnm_interface:
check_deploy: True
fabric: "{{ ansible_it_fabric }}"
fabric: "{{ ansible_it_fabric }}"
state: overridden # only choose form [merged, replaced, deleted, overridden, query]
register: result
when: IT_CONTEXT is not defined

- assert:
that:
- 'item["RETURN_CODE"] == 200'
- 'item["RETURN_CODE"] == 200'
loop: '{{ result.response }}'
when: IT_CONTEXT is not defined
30 changes: 30 additions & 0 deletions tests/unit/modules/dcnm/fixtures/dcnm_intf_pc_configs.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,36 @@
"deploy": "True"
}],

"pc_merged_vlan_range_config" : [
{
"switch": [
"192.168.1.108"
],
"profile": {
"description": "port channel acting as trunk",
"bpdu_guard": "True",
"sno": "SAL1819SAN8",
"mtu": "jumbo",
"pc_mode": "on",
"mode": "trunk",
"members": [
"e1/9"
],
"port_type_fast": "True",
"policy": "int_port_channel_trunk_host_11_1",
"admin_state": "True",
"allowed_vlans": "20,30,40,50-60,70,90-100",
"cmds": [
"no shutdown"
],
"ifname": "Port-channel300",
"fabric": "test_fabric"
},
"type": "pc",
"name": "po300",
"deploy": "True"
}],

"pc_deleted_config_deploy" : [
{
"switch": [
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/modules/dcnm/test_dcnm_intf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,24 @@ def load_pc_fixtures(self):
self.playbook_mock_succ_resp,
self.playbook_mock_succ_resp,
]

if "_pc_merged_vlan_range_new" in self._testMethodName:
# No I/F exists case
playbook_pc_intf1 = []
playbook_have_all_data = self.have_all_payloads_data.get(
"payloads"
)

self.run_dcnm_send.side_effect = [
self.mock_monitor_false_resp,
self.playbook_mock_vpc_resp,
playbook_pc_intf1,
playbook_have_all_data,
playbook_have_all_data,
self.playbook_mock_succ_resp,
self.playbook_mock_succ_resp,
]

if "_pc_merged_policy_change" in self._testMethodName:
playbook_pc_intf1 = self.payloads_data.get(
"pc_merged_trunk_payloads"
Expand Down Expand Up @@ -2705,6 +2723,45 @@ def test_dcnm_intf_pc_merged_new(self):
True,
)

def test_dcnm_intf_pc_merged_vlan_range_new(self):

# load the json from playbooks
self.config_data = loadPlaybookData("dcnm_intf_pc_configs")
self.payloads_data = loadPlaybookData("dcnm_intf_pc_payloads")
self.have_all_payloads_data = loadPlaybookData(
"dcnm_intf_have_all_payloads"
)

# load required config data
self.playbook_config = self.config_data.get("pc_merged_vlan_range_config")
self.playbook_mock_succ_resp = self.config_data.get("mock_succ_resp")
self.mock_ip_sn = self.config_data.get("mock_ip_sn")
self.mock_fab_inv = self.config_data.get("mock_fab_inv_data")
self.mock_monitor_true_resp = self.config_data.get("mock_monitor_true_resp")
self.mock_monitor_false_resp = self.config_data.get("mock_monitor_false_resp")
self.playbook_mock_vpc_resp = self.config_data.get("mock_vpc_resp")

set_module_args(
dict(
state="merged",
fabric="test_fabric",
config=self.playbook_config,
)
)
result = self.execute_module(changed=True, failed=False)
self.assertEqual(len(result["diff"][0]["merged"]), 1)
for d in result["diff"][0]["merged"]:
for intf in d["interfaces"]:
self.assertEqual(
(
intf["ifName"]
in [
"Port-channel300"
]
),
True,
)

def test_dcnm_intf_pc_merged_idempotent(self):

# load the json from playbooks
Expand Down

0 comments on commit 20a80ff

Please sign in to comment.