Skip to content

Commit 20a80ff

Browse files
authored
Merge pull request #304 from CiscoDevNet/dcnm-intf-fix-229
Fix for issue 229 to accept individual vlans in accepted-vlans parameter
2 parents 9499fa6 + 990582c commit 20a80ff

File tree

4 files changed

+243
-13
lines changed

4 files changed

+243
-13
lines changed

plugins/modules/dcnm_interface.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4085,6 +4085,7 @@ def dcnm_intf_get_diff_overridden(self, cfg):
40854085
== "DCNM_INTF_MATCH"
40864086
):
40874087
continue
4088+
40884089
if uelem is not None:
40894090
# Before defaulting ethernet interfaces, check if they are
40904091
# member of any port-channel. If so, do not default that
@@ -4886,6 +4887,66 @@ def dcnm_intf_send_message_to_dcnm(self):
48864887
else:
48874888
self.result["changed"] = False
48884889

4890+
def dcnm_intf_get_xlated_object(self, cfg, key):
4891+
4892+
"""
4893+
Routine to translate individual vlans like 45, 55 to 44-44 and 55-55 format
4894+
4895+
Parameters:
4896+
cfg (dict): Config element that includes the object idebtified by key to be translated
4897+
key (str): key identifying the object to be translated
4898+
4899+
Returns:
4900+
translated object
4901+
"""
4902+
4903+
citems = cfg["profile"][key].split(",")
4904+
4905+
for index in range(len(citems)):
4906+
if (
4907+
(citems[index].lower() == "none")
4908+
or (citems[index].lower() == "all")
4909+
or ("-" in citems[index])
4910+
):
4911+
continue
4912+
4913+
# Playbook config includes individual vlans in allowed_vlans object. Convert the elem to
4914+
# appropriate format i.e. vlaues in the form of 4, 7 to 4-4 and 7-7
4915+
citems[index] = citems[index].strip() + "-" + citems[index].strip()
4916+
return citems
4917+
4918+
def dcnm_intf_translate_allowed_vlans(self, cfg):
4919+
4920+
"""
4921+
Routine to translate xxx_allowed_vlans object in the config. 'xxx_allowed_vlans' object will
4922+
allow only 'none', 'all', or 'vlan-ranges like 1-5' values. It does not allow individual
4923+
vlans to be included. To enable user to include individual vlans in the playbook config, this
4924+
routine tranlates the individual vlans like 3, 5 etc to 3-3 and 5-5 format.
4925+
4926+
Parameters:
4927+
cfg (dict): Config element that needs to be translated
4928+
4929+
Returns:
4930+
None
4931+
"""
4932+
4933+
if cfg.get("profile", None) is None:
4934+
return
4935+
4936+
if cfg["profile"].get("allowed_vlans", None) is not None:
4937+
xlated_obj = self.dcnm_intf_get_xlated_object(cfg, "allowed_vlans")
4938+
cfg["profile"]["allowed_vlans"] = ",".join(xlated_obj)
4939+
if cfg["profile"].get("peer1_allowed_vlans", None) is not None:
4940+
xlated_obj = self.dcnm_intf_get_xlated_object(
4941+
cfg, "peer1_allowed_vlans"
4942+
)
4943+
cfg["profile"]["peer1_allowed_vlans"] = ",".join(xlated_obj)
4944+
if cfg["profile"].get("peer2_allowed_vlans", None) is not None:
4945+
xlated_obj = self.dcnm_intf_get_xlated_object(
4946+
cfg, "peer2_allowed_vlans"
4947+
)
4948+
cfg["profile"]["peer2_allowed_vlans"] = ",".join(xlated_obj)
4949+
48894950
def dcnm_intf_update_inventory_data(self):
48904951

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

5066+
# 'allowed-vlans' in the case of trunk interfaces accepts 'all', 'none' and 'vlan-ranges' which
5067+
# will be of the form 20-30 etc. There is not way to include individual vlans which are not contiguous.
5068+
# To include individual vlans like 3,6,20 etc. user must input them in the form 3-3, 6-6, 20-20 which is
5069+
# not very intuitive. To handle this scenario, we allow playbooks to include individual vlans and translate
5070+
# them here appropriately.
5071+
5072+
if cfg.get("profile", None) is not None:
5073+
if (
5074+
(
5075+
cfg["profile"].get("peer1_allowed_vlans", None)
5076+
is not None
5077+
)
5078+
or (
5079+
cfg["profile"].get("peer2_allowed_vlans", None)
5080+
is not None
5081+
)
5082+
or (cfg["profile"].get("allowed_vlans", None) is not None)
5083+
):
5084+
self.dcnm_intf_translate_allowed_vlans(cfg)
5085+
50055086

50065087
def main():
50075088

tests/integration/targets/dcnm_interface/tests/dcnm/dcnm_pc_merge.yaml

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
- name: Put the fabric to default state
99
cisco.dcnm.dcnm_interface:
1010
check_deploy: True
11-
fabric: "{{ ansible_it_fabric }}"
11+
fabric: "{{ ansible_it_fabric }}"
1212
state: overridden # only choose form [merged, replaced, deleted, overridden, query]
13-
register: result
13+
register: result
1414

1515
- assert:
1616
that:
17-
- 'item["RETURN_CODE"] == 200'
17+
- 'item["RETURN_CODE"] == 200'
1818
loop: '{{ result.response }}'
1919

2020
- block:
@@ -37,13 +37,13 @@
3737
profile:
3838
admin_state: true # choose from [true, false]
3939
mode: trunk # choose from [trunk, access, l3, monitor]
40-
members: # member interfaces
40+
members: # member interfaces
4141
- "{{ ansible_eth_intf13 }}"
4242
pc_mode: 'on' # choose from ['on', 'active', 'passive']
4343
bpdu_guard: true # choose from [true, false, no]
4444
port_type_fast: true # choose from [true, false]
4545
mtu: jumbo # choose from [default, jumbo]
46-
allowed_vlans: none # choose from [none, all, vlan range]
46+
allowed_vlans: none # choose from [none, all, vlan range]
4747
cmds: # Freeform config
4848
- no shutdown
4949
description: "port channel acting as trunk"
@@ -56,13 +56,13 @@
5656
profile:
5757
admin_state: true # choose from [true, false]
5858
mode: access # choose from [trunk, access, l3, monitor]
59-
members: # member interfaces
59+
members: # member interfaces
6060
- "{{ ansible_eth_intf14 }}"
6161
pc_mode: 'on' # choose from ['on', 'active', 'passive']
6262
bpdu_guard: true # choose from [true, false, no]
6363
port_type_fast: true # choose from [true, false]
6464
mtu: default # choose from [default, jumbo]
65-
access_vlan: 301 #
65+
access_vlan: 301 #
6666
cmds: # Freeform config
6767
- no shutdown
6868
description: "port channel acting as access"
@@ -75,11 +75,11 @@
7575
profile:
7676
admin_state: true # choose from [true, false]
7777
mode: l3 # choose from [trunk, access, l3, monitor]
78-
members: # member interfaces
78+
members: # member interfaces
7979
- "{{ ansible_eth_intf15 }}"
8080
pc_mode: 'on' # choose from ['on', 'active', 'passive']
8181
mtu: 9216 # choose between [min=576, max=9216]
82-
int_vrf: "" # interface VRF
82+
int_vrf: "" # interface VRF
8383
ipv4_addr: 192.168.20.1 # ipv4 address for the interface
8484
ipv4_mask_len: 24 # choose between [min:1, max:31]
8585
route_tag: "" # Route Tag
@@ -107,7 +107,7 @@
107107

108108
- assert:
109109
that:
110-
- 'item["RETURN_CODE"] == 200'
110+
- 'item["RETURN_CODE"] == 200'
111111
loop: '{{ result.response }}'
112112

113113
- name: Create port channel interfaces - Idempotence
@@ -125,7 +125,69 @@
125125

126126
- assert:
127127
that:
128-
- 'item["RETURN_CODE"] == 200'
128+
- 'item["RETURN_CODE"] == 200'
129+
loop: '{{ result.response }}'
130+
131+
##############################################
132+
## MERGE ##
133+
##############################################
134+
135+
- name: Create port channel interfaces with vlan ranges
136+
cisco.dcnm.dcnm_interface: &pc_merge2
137+
check_deploy: True
138+
fabric: "{{ ansible_it_fabric }}"
139+
state: merged # only choose form [merged, replaced, deleted, overridden, query]
140+
config:
141+
- name: po400 # should be of the form po<port-id>
142+
type: pc # choose from this list [pc, vpc, sub_int, lo, eth, svi]
143+
switch:
144+
- "{{ ansible_switch1 }}" # provide the switch information where the config is to be deployed
145+
deploy: true # choose from [true, false]
146+
profile:
147+
admin_state: true # choose from [true, false]
148+
mode: trunk # choose from [trunk, access, l3, monitor]
149+
members: # member interfaces
150+
- "{{ ansible_eth_intf21 }}"
151+
pc_mode: 'on' # choose from ['on', 'active', 'passive']
152+
bpdu_guard: true # choose from [true, false, no]
153+
port_type_fast: true # choose from [true, false]
154+
mtu: jumbo # choose from [default, jumbo]
155+
allowed_vlans: 10,20,30-40 # choose from [none, all, vlan range]
156+
cmds: # Freeform config
157+
- no shutdown
158+
description: "port channel acting as trunk"
159+
register: result
160+
161+
- assert:
162+
that:
163+
- 'result.changed == true'
164+
- '(result["diff"][0]["merged"] | length) == 1'
165+
- '(result["diff"][0]["deleted"] | length) == 0'
166+
- '(result["diff"][0]["replaced"] | length) == 0'
167+
- '(result["diff"][0]["overridden"] | length) == 0'
168+
- '(result["diff"][0]["deploy"] | length) == 1'
169+
170+
- assert:
171+
that:
172+
- 'item["RETURN_CODE"] == 200'
173+
loop: '{{ result.response }}'
174+
175+
- name: Create port channel interfaces with vlan ranges - Idempotence
176+
cisco.dcnm.dcnm_interface: *pc_merge2
177+
register: result
178+
179+
- assert:
180+
that:
181+
- 'result.changed == false'
182+
- '(result["diff"][0]["merged"] | length) == 0'
183+
- '(result["diff"][0]["deleted"] | length) == 0'
184+
- '(result["diff"][0]["replaced"] | length) == 0'
185+
- '(result["diff"][0]["overridden"] | length) == 0'
186+
- '(result["diff"][0]["deploy"] | length) == 0'
187+
188+
- assert:
189+
that:
190+
- 'item["RETURN_CODE"] == 200'
129191
loop: '{{ result.response }}'
130192

131193
##############################################
@@ -137,13 +199,13 @@
137199
- name: Put fabric to default state
138200
cisco.dcnm.dcnm_interface:
139201
check_deploy: True
140-
fabric: "{{ ansible_it_fabric }}"
202+
fabric: "{{ ansible_it_fabric }}"
141203
state: overridden # only choose form [merged, replaced, deleted, overridden, query]
142204
register: result
143205
when: IT_CONTEXT is not defined
144206

145207
- assert:
146208
that:
147-
- 'item["RETURN_CODE"] == 200'
209+
- 'item["RETURN_CODE"] == 200'
148210
loop: '{{ result.response }}'
149211
when: IT_CONTEXT is not defined

tests/unit/modules/dcnm/fixtures/dcnm_intf_pc_configs.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,36 @@
297297
"deploy": "True"
298298
}],
299299

300+
"pc_merged_vlan_range_config" : [
301+
{
302+
"switch": [
303+
"192.168.1.108"
304+
],
305+
"profile": {
306+
"description": "port channel acting as trunk",
307+
"bpdu_guard": "True",
308+
"sno": "SAL1819SAN8",
309+
"mtu": "jumbo",
310+
"pc_mode": "on",
311+
"mode": "trunk",
312+
"members": [
313+
"e1/9"
314+
],
315+
"port_type_fast": "True",
316+
"policy": "int_port_channel_trunk_host_11_1",
317+
"admin_state": "True",
318+
"allowed_vlans": "20,30,40,50-60,70,90-100",
319+
"cmds": [
320+
"no shutdown"
321+
],
322+
"ifname": "Port-channel300",
323+
"fabric": "test_fabric"
324+
},
325+
"type": "pc",
326+
"name": "po300",
327+
"deploy": "True"
328+
}],
329+
300330
"pc_deleted_config_deploy" : [
301331
{
302332
"switch": [

tests/unit/modules/dcnm/test_dcnm_intf.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,24 @@ def load_pc_fixtures(self):
11871187
self.playbook_mock_succ_resp,
11881188
self.playbook_mock_succ_resp,
11891189
]
1190+
1191+
if "_pc_merged_vlan_range_new" in self._testMethodName:
1192+
# No I/F exists case
1193+
playbook_pc_intf1 = []
1194+
playbook_have_all_data = self.have_all_payloads_data.get(
1195+
"payloads"
1196+
)
1197+
1198+
self.run_dcnm_send.side_effect = [
1199+
self.mock_monitor_false_resp,
1200+
self.playbook_mock_vpc_resp,
1201+
playbook_pc_intf1,
1202+
playbook_have_all_data,
1203+
playbook_have_all_data,
1204+
self.playbook_mock_succ_resp,
1205+
self.playbook_mock_succ_resp,
1206+
]
1207+
11901208
if "_pc_merged_policy_change" in self._testMethodName:
11911209
playbook_pc_intf1 = self.payloads_data.get(
11921210
"pc_merged_trunk_payloads"
@@ -2705,6 +2723,45 @@ def test_dcnm_intf_pc_merged_new(self):
27052723
True,
27062724
)
27072725

2726+
def test_dcnm_intf_pc_merged_vlan_range_new(self):
2727+
2728+
# load the json from playbooks
2729+
self.config_data = loadPlaybookData("dcnm_intf_pc_configs")
2730+
self.payloads_data = loadPlaybookData("dcnm_intf_pc_payloads")
2731+
self.have_all_payloads_data = loadPlaybookData(
2732+
"dcnm_intf_have_all_payloads"
2733+
)
2734+
2735+
# load required config data
2736+
self.playbook_config = self.config_data.get("pc_merged_vlan_range_config")
2737+
self.playbook_mock_succ_resp = self.config_data.get("mock_succ_resp")
2738+
self.mock_ip_sn = self.config_data.get("mock_ip_sn")
2739+
self.mock_fab_inv = self.config_data.get("mock_fab_inv_data")
2740+
self.mock_monitor_true_resp = self.config_data.get("mock_monitor_true_resp")
2741+
self.mock_monitor_false_resp = self.config_data.get("mock_monitor_false_resp")
2742+
self.playbook_mock_vpc_resp = self.config_data.get("mock_vpc_resp")
2743+
2744+
set_module_args(
2745+
dict(
2746+
state="merged",
2747+
fabric="test_fabric",
2748+
config=self.playbook_config,
2749+
)
2750+
)
2751+
result = self.execute_module(changed=True, failed=False)
2752+
self.assertEqual(len(result["diff"][0]["merged"]), 1)
2753+
for d in result["diff"][0]["merged"]:
2754+
for intf in d["interfaces"]:
2755+
self.assertEqual(
2756+
(
2757+
intf["ifName"]
2758+
in [
2759+
"Port-channel300"
2760+
]
2761+
),
2762+
True,
2763+
)
2764+
27082765
def test_dcnm_intf_pc_merged_idempotent(self):
27092766

27102767
# load the json from playbooks

0 commit comments

Comments
 (0)