diff --git a/docs/cisco.dcnm.dcnm_network_module.rst b/docs/cisco.dcnm.dcnm_network_module.rst index de22372e0..b5d972252 100644 --- a/docs/cisco.dcnm.dcnm_network_module.rst +++ b/docs/cisco.dcnm.dcnm_network_module.rst @@ -30,12 +30,12 @@ Parameters - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter Choices/Defaults Comments
+
config @@ -52,7 +52,7 @@ Parameters
+
arp_suppress @@ -73,7 +73,7 @@ Parameters
+
attach @@ -91,7 +91,7 @@ Parameters
+
deploy @@ -113,7 +113,7 @@ Parameters
+
ip_address @@ -131,7 +131,7 @@ Parameters
+
ports @@ -147,10 +147,69 @@ Parameters
List of switch interfaces where the network will be attached
+
+ tor_ports + +
+ list + / elements=dictionary +
+
+ +
List of interfaces in the paired TOR switch for this leaf where the network will be attached
+
Please attach the same set of TOR ports to both the VPC paired switches.
+
+
+ ip_address + +
+ string + / required +
+
+ +
IP address of the TOR switch where the network will be attached
+
+
+ ports + +
+ list + / elements=string + / required +
+
+ +
List of TOR switch interfaces where the network will be attached
+
deploy @@ -174,7 +233,7 @@ Parameters
+
dhcp_loopback_id @@ -191,7 +250,7 @@ Parameters
+
dhcp_srvr1_ip @@ -207,7 +266,7 @@ Parameters
+
dhcp_srvr1_vrf @@ -223,7 +282,7 @@ Parameters
+
dhcp_srvr2_ip @@ -239,7 +298,7 @@ Parameters
+
dhcp_srvr2_vrf @@ -255,7 +314,7 @@ Parameters
+
dhcp_srvr3_ip @@ -271,7 +330,7 @@ Parameters
+
dhcp_srvr3_vrf @@ -287,7 +346,7 @@ Parameters
+
gw_ip_subnet @@ -303,7 +362,7 @@ Parameters
+
gw_ipv6_subnet @@ -319,7 +378,7 @@ Parameters
+
int_desc @@ -335,7 +394,7 @@ Parameters
+
intfvlan_nf_monitor @@ -353,7 +412,7 @@ Parameters
+
is_l2only @@ -374,7 +433,7 @@ Parameters
+
l3gw_on_border @@ -394,7 +453,7 @@ Parameters
+
mtu_l3intf @@ -411,7 +470,7 @@ Parameters
+
multicast_group_address @@ -427,7 +486,7 @@ Parameters
+
net_extension_template @@ -444,7 +503,7 @@ Parameters
+
net_id @@ -461,7 +520,7 @@ Parameters
+
net_name @@ -478,7 +537,7 @@ Parameters
+
net_template @@ -495,7 +554,7 @@ Parameters
+
netflow_enable @@ -517,7 +576,7 @@ Parameters
+
route_target_both @@ -537,7 +596,7 @@ Parameters
+
routing_tag @@ -554,7 +613,7 @@ Parameters
+
secondary_ip_gw1 @@ -570,7 +629,7 @@ Parameters
+
secondary_ip_gw2 @@ -586,7 +645,7 @@ Parameters
+
secondary_ip_gw3 @@ -602,7 +661,7 @@ Parameters
+
secondary_ip_gw4 @@ -618,7 +677,7 @@ Parameters
+
trm_enable @@ -638,7 +697,7 @@ Parameters
+
vlan_id @@ -655,7 +714,7 @@ Parameters
+
vlan_name @@ -672,7 +731,7 @@ Parameters
+
vlan_nf_monitor @@ -690,7 +749,7 @@ Parameters
+
vrf_name @@ -707,7 +766,7 @@ Parameters
+
fabric @@ -723,7 +782,7 @@ Parameters
+
state @@ -817,6 +876,9 @@ Examples attach: - ip_address: 192.168.1.224 ports: [Ethernet1/11, Ethernet1/12] + tor_ports: + - ip_address: 192.168.1.120 + ports: [Ethernet1/14, Ethernet1/15] - ip_address: 192.168.1.225 ports: [Ethernet1/11, Ethernet1/12] deploy: false diff --git a/plugins/modules/dcnm_network.py b/plugins/modules/dcnm_network.py index c0e995e7e..15385cd27 100644 --- a/plugins/modules/dcnm_network.py +++ b/plugins/modules/dcnm_network.py @@ -256,6 +256,25 @@ There will not be any functional impact if specified in playbook. type: bool default: true + tor_ports: + description: + - List of interfaces in the paired TOR switch for this leaf where the network will be attached + - Please attach the same set of TOR ports to both the VPC paired switches. + type: list + elements: dict + required: false + suboptions: + ip_address: + description: + - IP address of the TOR switch where the network will be attached + type: str + required: true + ports: + description: + - List of TOR switch interfaces where the network will be attached + type: list + elements: str + required: true deploy: description: - Global knob to control whether to deploy the attachment @@ -332,6 +351,9 @@ attach: - ip_address: 192.168.1.224 ports: [Ethernet1/11, Ethernet1/12] + tor_ports: + - ip_address: 192.168.1.120 + ports: [Ethernet1/14, Ethernet1/15] - ip_address: 192.168.1.225 ports: [Ethernet1/11, Ethernet1/12] deploy: false @@ -551,6 +573,74 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): if want.get("isAttached") is not None: if bool(have["isAttached"]) and bool(want["isAttached"]): + torports_configured = False + + # Handle tor ports first if configured. + if want.get("torports"): + for tor_w in want["torports"]: + torports_present = False + if have.get("torports"): + for tor_h in have["torports"]: + if tor_w["switch"] == tor_h["switch"]: + atch_tor_ports = [] + torports_present = True + h_tor_ports = ( + tor_h["torPorts"].split(",") + if tor_h["torPorts"] + else [] + ) + w_tor_ports = ( + tor_w["torPorts"].split(",") + if tor_w["torPorts"] + else [] + ) + + if sorted(h_tor_ports) != sorted(w_tor_ports): + atch_tor_ports = list( + set(w_tor_ports) - set(h_tor_ports) + ) + + if replace: + atch_tor_ports = w_tor_ports + else: + atch_tor_ports.extend(h_tor_ports) + + torconfig = tor_w["switch"] + "(" + ",".join(atch_tor_ports) + ")" + want.update({"torPorts": torconfig}) + # Update torports_configured to True. If there is no other config change for attach + # We will still append this attach to attach_list as there is tor port change + if sorted(atch_tor_ports) != sorted(h_tor_ports): + torports_configured = True + + if not torports_present: + torconfig = tor_w["switch"] + "(" + tor_w["torPorts"] + ")" + want.update({"torPorts": torconfig}) + # Update torports_configured to True. If there is no other config change for attach + # We will still append this attach to attach_list as there is tor port change + torports_configured = True + + if have.get("torports"): + del have["torports"] + + elif have.get("torports"): + if replace: + # There are tor ports configured, but it has to be removed as want tor ports are not present + # and state is replaced/overridden. Update torports_configured to True to remove tor ports + want.update({"torPorts": ""}) + torports_configured = True + + else: + # Dont update torports_configured to True. + # If at all there is any other config change, this attach to will be appended attach_list there + for tor_h in have.get("torports"): + torconfig = tor_h["switch"] + "(" + tor_h["torPorts"] + ")" + want.update({"torPorts": torconfig}) + + del have["torports"] + + if want.get("torports"): + del want["torports"] + h_sw_ports = ( have["switchPorts"].split(",") if have["switchPorts"] @@ -580,6 +670,12 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): ) if not atch_sw_ports and not dtach_sw_ports: + if torports_configured: + del want["isAttached"] + attach_list.append(want) + if bool(want["is_deploy"]): + dep_net = True + continue want.update( @@ -608,11 +704,25 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): if not atch_sw_ports: # The attachments in the have consist of attachments in want and more. + if torports_configured: + del want["isAttached"] + attach_list.append(want) + if bool(want["is_deploy"]): + dep_net = True + continue + else: + want.update( + {"switchPorts": ",".join(atch_sw_ports)} + ) - want.update( - {"switchPorts": ",".join(atch_sw_ports)} - ) + del want["isAttached"] + attach_list.append(want) + if bool(want["is_deploy"]): + dep_net = True + continue + + elif torports_configured: del want["isAttached"] attach_list.append(want) if bool(want["is_deploy"]): @@ -631,6 +741,11 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): dep_net = True continue del want["isAttached"] + if want.get("torports"): + for tor_w in want["torports"]: + torconfig = tor_w["switch"] + "(" + tor_w["torPorts"] + ")" + want.update({"torPorts": torconfig}) + del want["torports"] want.update({"deployment": True}) attach_list.append(want) if bool(want["is_deploy"]): @@ -650,6 +765,11 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): if not found: if bool(want["isAttached"]): + if want.get("torports"): + for tor_w in want["torports"]: + torconfig = tor_w["switch"] + "(" + tor_w["torPorts"] + ")" + want.update({"torPorts": torconfig}) + del want["torports"] del want["isAttached"] want["deployment"] = True attach_list.append(want) @@ -680,10 +800,13 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): attach_list.append(havtoattach) break + # self.module.fail_json(msg="attach done") + return attach_list, dep_net def update_attach_params(self, attach, net_name, deploy): + torlist = [] if not attach: return {} @@ -728,6 +851,20 @@ def update_attach_params(self, attach, net_name, deploy): attach.update({"instanceValues": ""}) attach.update({"freeformConfig": ""}) attach.update({"is_deploy": deploy}) + if attach.get("tor_ports"): + torports = {} + if role.lower() != "leaf": + msg = "tor_ports for Networks cannot be attached to switch {0} with role {1}".format( + attach["ip_address"], role + ) + self.module.fail_json(msg=msg) + for tor in attach.get("tor_ports"): + torports.update({"switch": self.inventory_data[tor["ip_address"]].get("logicalName")}) + torports.update({"torPorts": ",".join(tor["ports"])}) + torlist.append(torports) + del attach["tor_ports"] + attach.update({"torports": torlist}) + if "deploy" in attach: del attach["deploy"] del attach["ports"] @@ -1351,6 +1488,7 @@ def get_have(self): attach_list = net_attach["lanAttachList"] dep_net = "" for attach in attach_list: + torlist = [] attach_state = False if attach["lanAttachState"] == "NA" else True deploy = attach["isLanAttached"] deployed = False @@ -1367,7 +1505,21 @@ def get_have(self): sn = attach["switchSerialNo"] vlan = attach["vlanId"] - ports = attach["portNames"] + + if attach["portNames"] and re.match(r"\S+\(\S+\d+\/\d+\)", attach["portNames"]): + for idx, sw_list in enumerate(re.findall(r"\S+\(\S+\d+\/\d+\)", attach["portNames"])): + torports = {} + sw = sw_list.split("(") + eth_list = sw[1].split(")") + if idx == 0: + ports = eth_list[0] + continue + torports.update({"switch": sw[0]}) + torports.update({"torPorts": eth_list[0]}) + torlist.append(torports) + attach.update({"torports": torlist}) + else: + ports = attach["portNames"] # The deletes and updates below are done to update the incoming dictionary format to # match to what the outgoing payload requirements mandate. @@ -1962,6 +2114,11 @@ def get_diff_merge(self, replace=False): for attach in want_a["lanAttachList"]: # Saftey check if attach.get("isAttached"): + if attach.get("torports"): + for tor_w in attach["torports"]: + torconfig = tor_w["switch"] + "(" + tor_w["torPorts"] + ")" + attach.update({"torPorts": torconfig}) + del attach["torports"] del attach["isAttached"] atch_list.append(attach) if atch_list: @@ -2092,6 +2249,8 @@ def format_diff(self): found_c["attach"].append(detach_d) attach_d.update({"ports": a_w["switchPorts"]}) attach_d.update({"deploy": a_w["deployment"]}) + if a_w.get("torPorts"): + attach_d.update({"tor_ports": a_w["torPorts"]}) found_c["attach"].append(attach_d) diff.append(found_c) @@ -2118,6 +2277,8 @@ def format_diff(self): new_attach_list.append(detach_d) attach_d.update({"ports": a_w["switchPorts"]}) attach_d.update({"deploy": a_w["deployment"]}) + if a_w.get("torPorts"): + attach_d.update({"tor_ports": a_w["torPorts"]}) new_attach_list.append(attach_d) if new_attach_list: @@ -2613,6 +2774,11 @@ def validate_input(self): ip_address=dict(required=True, type="str"), ports=dict(required=True, type="list"), deploy=dict(type="bool", default=True), + tor_ports=dict(required=False, type="list", elements="dict"), + ) + tor_att_spec = dict( + ip_address=dict(required=True, type="str"), + ports=dict(required=False, type="list", default=[]), ) if self.config: @@ -2631,6 +2797,19 @@ def validate_input(self): attach["deploy"] = net["deploy"] if attach.get("ports"): attach["ports"] = [port.capitalize() for port in attach["ports"]] + if attach.get("tor_ports"): + if self.dcnm_version == 11: + msg = "Invalid parameters in playbook: tor_ports configurations are supported only on NDFC" + self.module.fail_json(msg=msg) + + valid_tor_att, invalid_tor_att = validate_list_of_dicts( + attach["tor_ports"], tor_att_spec + ) + attach["tor_ports"] = valid_tor_att + for tor in attach["tor_ports"]: + if tor.get("ports"): + tor["ports"] = [port.capitalize() for port in tor["ports"]] + invalid_params.extend(invalid_tor_att) invalid_params.extend(invalid_att) if state != "deleted": diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/tor_ports_networks.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/tor_ports_networks.yaml new file mode 100644 index 000000000..648861cac --- /dev/null +++ b/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/tor_ports_networks.yaml @@ -0,0 +1,366 @@ +############################################## +## SETUP ## +############################################## + +- block: + - set_fact: + rest_path: "/rest/control/fabrics/{{ test_fabric }}" + when: controller_version == "11" + + - set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + when: controller_version >= "12" + + - name: TOR - Verify if fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + + - assert: + that: + - 'result.response.DATA != None' + + - name: TOR - Clean up any existing networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## TESTS ## +############################################## + + - name: TOR - Create, Attach and Deploy Single Network with Multiple Switch and TOR Attach + cisco.dcnm.dcnm_network: &conf1 + fabric: "{{ test_fabric }}" + state: merged + config: + - net_name: ansible-net13 + vrf_name: Tenant-1 + net_id: 7005 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + vlan_id: 1500 + gw_ip_subnet: '192.168.30.1/24' + attach: + - ip_address: "{{ ansible_switch1 }}" + ports: ["{{ ansible_sw1_int1 }}", "{{ ansible_sw1_int2 }}"] + tor_ports: + - ip_address: "{{ ansible_tor_switch1 }}" + ports: ["{{ ansible_tor_int1 }}", "{{ ansible_tor_int2 }}"] + - ip_address: "{{ ansible_switch2 }}" + ports: ["{{ ansible_sw2_int5 }}", "{{ ansible_sw2_int6 }}"] + deploy: True + register: result + + - name: Query fabric state until networkStatus transitions to DEPLOYED state + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: query + register: query_result + until: + - "query_result.response[0].parent.networkStatus is search('DEPLOYED|PENDING')" + retries: 30 + delay: 2 + + - assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - 'result.response[2].RETURN_CODE == 200' + - '(result.response[1].DATA|dict2items)[0].value == "SUCCESS"' + - '(result.response[1].DATA|dict2items)[1].value == "SUCCESS"' + - 'result.diff[0].attach[0].deploy == true' + - '"{{ ansible_switch1 }}" in result.diff[0].attach[0].ip_address' + - '"{{ ansible_tor_int1 }}" in result.diff[0].attach[0].tor_ports' + - '"{{ ansible_tor_int2 }}" in result.diff[0].attach[0].tor_ports' + - 'result.diff[0].attach[1].deploy == true' + - '"{{ ansible_switch2 }}" in result.diff[0].attach[1].ip_address' + - 'result.diff[0].net_name == "ansible-net13"' + - 'result.diff[0].net_id == 7005' + - 'result.diff[0].vrf_name == "Tenant-1"' + + - name: TOR - conf1 - Idempotence + cisco.dcnm.dcnm_network: *conf1 + register: result + + - assert: + that: + - 'result.changed == false' + - 'result.response|length == 0' + + - name: TOR - Attach new TOR ports to already Attached TOR ports + cisco.dcnm.dcnm_network: &conf2 + fabric: "{{ test_fabric }}" + state: merged + config: + - net_name: ansible-net13 + vrf_name: Tenant-1 + net_id: 7005 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + vlan_id: 1500 + gw_ip_subnet: '192.168.30.1/24' + attach: + - ip_address: "{{ ansible_switch1 }}" + ports: ["{{ ansible_sw1_int1 }}", "{{ ansible_sw1_int2 }}"] + tor_ports: + - ip_address: "{{ ansible_tor_switch1 }}" + ports: ["{{ ansible_tor_int3 }}"] + - ip_address: "{{ ansible_switch2 }}" + ports: ["{{ ansible_sw2_int5 }}", "{{ ansible_sw2_int6 }}"] + deploy: True + register: result + + - name: Query fabric state until networkStatus transitions to DEPLOYED state + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: query + register: query_result + until: + - "query_result.response[0].parent.networkStatus is search('DEPLOYED|PENDING')" + retries: 30 + delay: 2 + + - assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - '(result.response[0].DATA|dict2items)[0].value == "SUCCESS"' + - 'result.diff[0].attach[0].deploy == true' + - '"{{ ansible_switch1 }}" in result.diff[0].attach[0].ip_address' + - '"{{ ansible_tor_int1 }}" in result.diff[0].attach[0].tor_ports' + - '"{{ ansible_tor_int2 }}" in result.diff[0].attach[0].tor_ports' + - '"{{ ansible_tor_int3 }}" in result.diff[0].attach[0].tor_ports' + - 'result.diff[0].net_name == "ansible-net13"' + + - name: TOR - conf2 - Idempotence + cisco.dcnm.dcnm_network: *conf2 + register: result + + - assert: + that: + - 'result.changed == false' + - 'result.response|length == 0' + + - name: TOR - setup - Clean up any existing network + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted + + - name: TOR - Create, Attach and Deploy Single Network with Multiple Switch Attach + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: merged + config: + - net_name: ansible-net13 + vrf_name: Tenant-1 + net_id: 7005 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + vlan_id: 1500 + gw_ip_subnet: '192.168.30.1/24' + attach: + - ip_address: "{{ ansible_switch1 }}" + ports: ["{{ ansible_sw1_int1 }}", "{{ ansible_sw1_int2 }}"] + - ip_address: "{{ ansible_switch2 }}" + ports: ["{{ ansible_sw2_int5 }}", "{{ ansible_sw2_int6 }}"] + deploy: True + register: result + + - name: Query fabric state until networkStatus transitions to DEPLOYED state + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: query + register: query_result + until: + - "query_result.response[0].parent.networkStatus is search('DEPLOYED|PENDING')" + retries: 30 + delay: 2 + + - assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - 'result.response[2].RETURN_CODE == 200' + - '(result.response[1].DATA|dict2items)[0].value == "SUCCESS"' + - '(result.response[1].DATA|dict2items)[1].value == "SUCCESS"' + - 'result.diff[0].attach[0].deploy == true' + - '"{{ ansible_switch1 }}" in result.diff[0].attach[0].ip_address' + - 'result.diff[0].attach[1].deploy == true' + - '"{{ ansible_switch2 }}" in result.diff[0].attach[1].ip_address' + - 'result.diff[0].net_name == "ansible-net13"' + - 'result.diff[0].net_id == 7005' + - 'result.diff[0].vrf_name == "Tenant-1"' + + - name: TOR - Attach new TOR ports to already Attached network ports + cisco.dcnm.dcnm_network: &conf3 + fabric: "{{ test_fabric }}" + state: merged + config: + - net_name: ansible-net13 + vrf_name: Tenant-1 + net_id: 7005 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + vlan_id: 1500 + gw_ip_subnet: '192.168.30.1/24' + attach: + - ip_address: "{{ ansible_switch1 }}" + ports: ["{{ ansible_sw1_int1 }}", "{{ ansible_sw1_int2 }}"] + tor_ports: + - ip_address: "{{ ansible_tor_switch1 }}" + ports: ["{{ ansible_tor_int3 }}"] + - ip_address: "{{ ansible_switch2 }}" + ports: ["{{ ansible_sw2_int5 }}", "{{ ansible_sw2_int6 }}"] + deploy: True + register: result + + - name: Query fabric state until networkStatus transitions to DEPLOYED state + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: query + register: query_result + until: + - "query_result.response[0].parent.networkStatus is search('DEPLOYED|PENDING')" + retries: 30 + delay: 2 + + - assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - '(result.response[0].DATA|dict2items)[0].value == "SUCCESS"' + - 'result.diff[0].attach[0].deploy == true' + - '"{{ ansible_switch1 }}" in result.diff[0].attach[0].ip_address' + - '"{{ ansible_tor_int3 }}" in result.diff[0].attach[0].tor_ports' + - 'result.diff[0].net_name == "ansible-net13"' + + - name: TOR - conf3 - Idempotence + cisco.dcnm.dcnm_network: *conf3 + register: result + + - assert: + that: + - 'result.changed == false' + - 'result.response|length == 0' + + - name: TOR - Replace new TOR ports to already Attached network ports + cisco.dcnm.dcnm_network: &conf4 + fabric: "{{ test_fabric }}" + state: replaced + config: + - net_name: ansible-net13 + vrf_name: Tenant-1 + net_id: 7005 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + vlan_id: 1500 + gw_ip_subnet: '192.168.30.1/24' + attach: + - ip_address: "{{ ansible_switch1 }}" + ports: ["{{ ansible_sw1_int1 }}", "{{ ansible_sw1_int2 }}"] + tor_ports: + - ip_address: "{{ ansible_tor_switch1 }}" + ports: ["{{ ansible_tor_int1 }}", "{{ ansible_tor_int2 }}"] + - ip_address: "{{ ansible_switch2 }}" + ports: ["{{ ansible_sw2_int5 }}", "{{ ansible_sw2_int6 }}"] + deploy: True + register: result + + - name: Query fabric state until networkStatus transitions to DEPLOYED state + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: query + register: query_result + until: + - "query_result.response[0].parent.networkStatus is search('DEPLOYED|PENDING')" + retries: 30 + delay: 2 + + - assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - '(result.response[0].DATA|dict2items)[0].value == "SUCCESS"' + - 'result.diff[0].attach[0].deploy == true' + - '"{{ ansible_switch1 }}" in result.diff[0].attach[0].ip_address' + - '"{{ ansible_tor_int1 }}" in result.diff[0].attach[0].tor_ports' + - '"{{ ansible_tor_int2 }}" in result.diff[0].attach[0].tor_ports' + - 'result.diff[0].net_name == "ansible-net13"' + + - name: TOR - conf4 - Idempotence + cisco.dcnm.dcnm_network: *conf4 + register: result + + - assert: + that: + - 'result.changed == false' + - 'result.response|length == 0' + + - name: TOR - Override new TOR ports to already Attached network ports + cisco.dcnm.dcnm_network: &conf5 + fabric: "{{ test_fabric }}" + state: overridden + config: + - net_name: ansible-net13 + vrf_name: Tenant-1 + net_id: 7005 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + vlan_id: 1500 + gw_ip_subnet: '192.168.30.1/24' + attach: + - ip_address: "{{ ansible_switch1 }}" + ports: ["{{ ansible_sw1_int1 }}", "{{ ansible_sw1_int2 }}"] + tor_ports: + - ip_address: "{{ ansible_tor_switch1 }}" + ports: ["{{ ansible_tor_int3 }}", "{{ ansible_tor_int4 }}"] + deploy: True + register: result + + - name: Query fabric state until networkStatus transitions to DEPLOYED state + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: query + register: query_result + until: + - "query_result.response[0].parent.networkStatus is search('DEPLOYED|PENDING')" + retries: 30 + delay: 2 + + - assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - '(result.response[0].DATA|dict2items)[0].value == "SUCCESS"' + - 'result.diff[0].attach[0].deploy == true' + - '"{{ ansible_switch1 }}" in result.diff[0].attach[0].ip_address' + - '"{{ ansible_tor_int3 }}" in result.diff[0].attach[0].tor_ports' + - '"{{ ansible_tor_int4 }}" in result.diff[0].attach[0].tor_ports' + - 'result.diff[0].net_name == "ansible-net13"' + + - name: TOR - conf5 - Idempotence + cisco.dcnm.dcnm_network: *conf5 + register: result + + - assert: + that: + - 'result.changed == false' + - 'result.response|length == 0' + +############################################## +## CLEAN-UP ## +############################################## + + - name: TOR - setup - remove any networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted + when: test_tor_pair is defined diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_network.json b/tests/unit/modules/dcnm/fixtures/dcnm_network.json index df426a4da..e212c90a9 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_network.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_network.json @@ -1,7 +1,9 @@ { "mock_ip_sn" : { "10.10.10.217": "9NN7E41N16A", - "10.10.10.218": "9YO9A29F27U", + "10.10.10.218": "9YO9A29F27U", + "10.10.10.219": "9YO9A29F28C", + "10.10.10.220": "9YO9A29F29D", "10.10.10.226": "XYZKSJHSMK3", "10.10.10.227": "XYZKSJHSMK4", "10.10.10.228": "XYZKSJHSMK5" @@ -17,7 +19,7 @@ "gw_ip_subnet": "192.168.30.1/24", "attach": [ { - "ip_address": "10.10.10.217", + "ip_address": "10.10.10.217", "ports": ["Ethernet1/13", "Ethernet1/14"], "deploy": true }, @@ -30,6 +32,68 @@ "deploy": true } ], + "playbook_tor_config" : [ + { + "net_name": "test_network", + "vrf_name": "ansible-vrf-int1", + "net_id": "9008011", + "net_template": "Default_Network_Universal", + "net_extension_template": "Default_Network_Extension_Universal", + "vlan_id": "202", + "gw_ip_subnet": "192.168.30.1/24", + "attach": [ + { + "ip_address": "10.10.10.217", + "ports": ["Ethernet1/13", "Ethernet1/14"], + "tor_ports": [ + { + "ip_address": "10.10.10.219", + "ports": ["Ethernet1/13", "Ethernet1/14"] + } + ] + }, + { + "ip_address": "10.10.10.218", + "ports": ["Ethernet1/13", "Ethernet1/14"], + "tor_ports": [ + { + "ip_address": "10.10.10.220", + "ports": ["Ethernet1/15", "Ethernet1/16"] + } + ] + } + ], + "deploy": true + } + ], + "playbook_tor_roleerr_config" : [ + { + "net_name": "test_network", + "vrf_name": "ansible-vrf-int1", + "net_id": "9008011", + "net_template": "Default_Network_Universal", + "net_extension_template": "Default_Network_Extension_Universal", + "vlan_id": "202", + "gw_ip_subnet": "192.168.30.1/24", + "attach": [ + { + "ip_address": "10.10.10.228", + "ports": ["Ethernet1/13", "Ethernet1/14"], + "tor_ports": [ + { + "ip_address": "10.10.10.220", + "ports": ["Ethernet1/13", "Ethernet1/14"] + } + ] + }, + { + "ip_address": "10.10.10.218", + "ports": ["Ethernet1/13", "Ethernet1/14"] + } + ], + "deploy": true + } + ], "playbook_config_incorrect_netid" : [ { "net_name": "test_network", @@ -102,6 +166,42 @@ "deploy": true } ], + "playbook_tor_config_update" : [ + { + "net_name": "test_network", + "vrf_name": "ansible-vrf-int1", + "net_id": "9008011", + "net_template": "Default_Network_Universal", + "net_extension_template": "Default_Network_Extension_Universal", + "vlan_id": "203", + "gw_ip_subnet": "192.168.30.1/24", + "attach": [ + { + "ip_address": "10.10.10.218", + "ports": ["Ethernet1/13", "Ethernet1/14"], + "deploy": true, + "tor_ports": [ + { + "ip_address": "10.10.10.219", + "ports": ["Ethernet1/13", "Ethernet1/14"] + } + ] + }, + { + "ip_address": "10.10.10.217", + "ports": ["Ethernet1/13", "Ethernet1/14"], + "deploy": true, + "tor_ports": [ + { + "ip_address": "10.10.10.220", + "ports": ["Ethernet1/13", "Ethernet1/14"] + } + ] + } + ], + "deploy": true + } + ], "playbook_config_replace" : [ { "net_name": "test_network", @@ -335,6 +435,50 @@ } ] }, + "mock_net_attach_tor_object" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "networkName": "test_network", + "lanAttachList": [ + { + "networkName": "test_network", + "displayName": "test_network", + "switchName": "n9kv-218", + "switchRole": "leaf", + "fabricName": "test_net", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "portNames": "dt-n9k2(Ethernet1/13,Ethernet1/14) dt-n9k6(Ethernet1/12)", + "switchSerialNo": "9YO9A29F27U", + "switchDbId": 4191270, + "ipAddress": "10.10.10.218", + "networkId": 9008011, + "vlanId": 202, + "interfaceGroups": null + }, + { + "networkName": "test_network", + "displayName": "test_network", + "switchName": "n9kv-217", + "switchRole": "leaf", + "fabricName": "test_net", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "portNames": "dt-n9k1(Ethernet1/13,Ethernet1/14) dt-n9k7(Ethernet1/12)", + "switchSerialNo": "9NN7E41N16A", + "switchDbId": 4195850, + "ipAddress": "10.10.10.217", + "networkId": 9008011, + "vlanId": 202, + "interfaceGroups": null + } + ] + } + ] + }, "mock_net_attach_object_del_ready": { "ERROR": "", "RETURN_CODE": 200, @@ -468,6 +612,18 @@ "serialNumber": "9YO9A29F27U", "switchRole": "leaf" }, + "10.10.10.219":{ + "ipAddress": "10.10.10.219", + "logicalName": "dt-n9k6", + "serialNumber": "9YO9A29F28C", + "switchRole": "tor" + }, + "10.10.10.220":{ + "ipAddress": "10.10.10.220", + "logicalName": "dt-n9k7", + "serialNumber": "9YO9A29F29D", + "switchRole": "tor" + }, "10.10.10.226":{ "ipAddress": "10.10.10.226", "logicalName": "dt-n9k3", diff --git a/tests/unit/modules/dcnm/test_dcnm_network.py b/tests/unit/modules/dcnm/test_dcnm_network.py index 3b0bc2980..88d635a9e 100644 --- a/tests/unit/modules/dcnm/test_dcnm_network.py +++ b/tests/unit/modules/dcnm/test_dcnm_network.py @@ -47,6 +47,9 @@ class TestDcnmNetworkModule(TestDcnmModule): playbook_config_incorrect_vrf = test_data.get("playbook_config_incorrect_vrf") playbook_config_update = test_data.get("playbook_config_update") playbook_config_novlan = test_data.get("playbook_config_novlan") + playbook_tor_config = test_data.get("playbook_tor_config") + playbook_tor_roleerr_config = test_data.get("playbook_tor_roleerr_config") + playbook_tor_config_update = test_data.get("playbook_tor_config_update") playbook_config_replace = test_data.get("playbook_config_replace") playbook_config_replace_no_atch = test_data.get("playbook_config_replace_no_atch") @@ -79,6 +82,7 @@ def init_data(self): ) self.mock_net_query_object = copy.deepcopy(self.test_data.get("mock_net_query_object")) self.mock_vlan_get = copy.deepcopy(self.test_data.get("mock_vlan_get")) + self.mock_net_attach_tor_object = copy.deepcopy(self.test_data.get("mock_net_attach_tor_object")) def setUp(self): super(TestDcnmNetworkModule, self).setUp() @@ -370,6 +374,54 @@ def load_fixtures(self, response=None, device=""): self.mock_net_attach_object ] + elif "_merged_torport_new" in self._testMethodName: + self.init_data() + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.blank_data, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_merged_torport_vererror" in self._testMethodName: + self.init_data() + + elif "_merged_torport_roleerror" in self._testMethodName: + self.init_data() + + elif "_merged_tor_with_update" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_net_attach_tor_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_net_object, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_replace_tor_ports" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_net_attach_tor_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_net_object, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] + + elif "_override_tor_ports" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_net_attach_tor_object] + self.run_dcnm_send.side_effect = [ + self.mock_vrf_object, + self.mock_net_object, + self.blank_data, + self.attach_success_resp, + self.deploy_success_resp, + ] else: pass @@ -735,3 +787,107 @@ def test_dcnm_net_query_without_config(self): result.get("response")[0]["attach"][1]["vlan"], 202, ) + + def test_dcnm_net_merged_torport_new(self): + self.version = 12 + set_module_args( + dict(state="merged", fabric="test_network", config=self.playbook_tor_config) + ) + result = self.execute_module(changed=True, failed=False) + self.version = 11 + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual( + result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.217" + ) + + def test_dcnm_net_merged_torport_vererror(self): + set_module_args( + dict(state="merged", fabric="test_network", config=self.playbook_tor_config) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get("msg"), + "Invalid parameters in playbook: tor_ports configurations are supported only on NDFC", + ) + + def test_dcnm_net_merged_torport_roleerror(self): + self.version = 12 + set_module_args( + dict(state="merged", fabric="test_network", config=self.playbook_tor_roleerr_config) + ) + result = self.execute_module(changed=False, failed=True) + self.version = 11 + self.assertEqual( + result.get("msg"), + "tor_ports for Networks cannot be attached to switch 10.10.10.228 with role border", + ) + + def test_dcnm_net_merged_tor_with_update(self): + self.version = 12 + set_module_args( + dict( + state="merged", fabric="test_network", config=self.playbook_tor_config_update + ) + ) + result = self.execute_module(changed=True, failed=False) + self.version = 11 + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual( + result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.218" + ) + self.assertEqual( + result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.217" + ) + self.assertEqual(result.get("diff")[0]["vrf_name"], "ansible-vrf-int1") + + def test_dcnm_net_replace_tor_ports(self): + self.version = 12 + set_module_args( + dict( + state="replaced", fabric="test_network", config=self.playbook_tor_config_update + ) + ) + result = self.execute_module(changed=True, failed=False) + self.version = 11 + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual( + result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.218" + ) + self.assertEqual( + result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.217" + ) + self.assertEqual( + result.get("diff")[0]["attach"][0]["tor_ports"], "dt-n9k6(Ethernet1/13,Ethernet1/14)" + ) + self.assertEqual( + result.get("diff")[0]["attach"][1]["tor_ports"], "dt-n9k7(Ethernet1/13,Ethernet1/14)" + ) + self.assertEqual(result.get("diff")[0]["vrf_name"], "ansible-vrf-int1") + + def test_dcnm_net_override_tor_ports(self): + self.version = 12 + set_module_args( + dict( + state="overridden", fabric="test_network", config=self.playbook_tor_config_update + ) + ) + result = self.execute_module(changed=True, failed=False) + self.version = 11 + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) + self.assertEqual( + result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.218" + ) + self.assertEqual( + result.get("diff")[0]["attach"][1]["ip_address"], "10.10.10.217" + ) + self.assertEqual( + result.get("diff")[0]["attach"][0]["tor_ports"], "dt-n9k6(Ethernet1/13,Ethernet1/14)" + ) + self.assertEqual( + result.get("diff")[0]["attach"][1]["tor_ports"], "dt-n9k7(Ethernet1/13,Ethernet1/14)" + ) + self.assertEqual(result.get("diff")[0]["vrf_name"], "ansible-vrf-int1")