-
+ |
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")
|