diff --git a/meta/runtime.yml b/meta/runtime.yml index 0bdcb52eb..4b462e205 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -263,3 +263,16 @@ action_groups: - aci_vrf_leak_internal_subnet - aci_vrf_multicast - aci_vzany_to_contract + +plugin_routing: + modules: + aci_l4l7_concrete_interface_attach: + redirect: cisco.aci.aci_l4l7_concrete_interface_attachment + deprecation: + removal_version: 3.0.0 + warning_text: Use cisco.aci.aci_l4l7_concrete_interface_attachment instead. + aci_l4l7_policy_based_redirect_dest: + redirect: cisco.aci.aci_l4l7_policy_based_redirect_destination + deprecation: + removal_version: 3.0.0 + warning_text: Use cisco.aci.aci_l4l7_policy_based_redirect_destination instead. diff --git a/plugins/modules/aci_l4l7_concrete_device.py b/plugins/modules/aci_l4l7_concrete_device.py new file mode 100644 index 000000000..b708899c0 --- /dev/null +++ b/plugins/modules/aci_l4l7_concrete_device.py @@ -0,0 +1,296 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: aci_l4l7_concrete_device +short_description: Manage L4-L7 Concrete Devices (vns:CDev) +description: +- Manage Layer 4 to Layer 7 (L4-L7) Concrete Devices. +options: + tenant: + description: + - The name of an existing tenant. + type: str + aliases: [ tenant_name ] + logical_device: + description: + - The name of the logical device (vns:lDevVip) the concrete device is attached to. + type: str + aliases: [ device_name, device, logical_device_name ] + name: + description: + - The name of the concrete device. + type: str + aliases: [ concrete_device, concrete_device_name ] + vcenter_name: + description: + - The name of the vCenter hosting the L4-L7 device. + type: str + vm_name: + description: + - The VM name within the vCenter for the L4-L7 device. + type: str + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + type: str + choices: [ absent, present, query ] + default: present +extends_documentation_fragment: +- cisco.aci.aci +- cisco.aci.annotation +notes: +- The I(tenant) and I(logical_device) must exist before using this module in your playbook. + The M(cisco.aci.aci_tenant) and M(cisco.aci.aci_l4l7_device) modules can be used for this. +seealso: +- module: cisco.aci.aci_tenant +- module: cisco.aci.aci_l4l7_device +- name: APIC Management Information Model reference + description: More information about the internal APIC class B(vns:CDev) + link: https://developer.cisco.com/docs/apic-mim-ref/ +author: +- Tim Cragg (@timcragg) +- Shreyas Srish (@shrsr) +""" + +EXAMPLES = r""" +- name: Add a new concrete device + cisco.aci.aci_l4l7_concrete_device: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + concrete_device: my_concrete_device + state: present + delegate_to: localhost + +- name: Query a concrete device + cisco.aci.aci_l4l7_concrete_device: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + concrete_device: my_concrete_device + state: query + delegate_to: localhost + register: query_result + +- name: Query all concrete devices + cisco.aci.aci_l4l7_concrete_device: + host: apic + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result + +- name: Delete a concrete device + cisco.aci.aci_l4l7_concrete_device: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + concrete_device: my_concrete_device + state: absent + delegate_to: localhost +""" + +RETURN = r""" +current: + description: The existing configuration from the APIC after the module has finished + returned: success + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +error: + description: The error information as returned from the APIC + returned: failure + type: dict + sample: + { + "code": "122", + "text": "unknown managed object class foo" + } +raw: + description: The raw output returned by the APIC REST API (xml or json) + returned: parse error + type: str + sample: '' +sent: + description: The actual/minimal configuration pushed to the APIC + returned: info + type: list + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment" + } + } + } +previous: + description: The original configuration from the APIC before the module has started + returned: info + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +proposed: + description: The assembled configuration from the user-provided parameters + returned: info + type: dict + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "name": "production" + } + } + } +filter_string: + description: The filter string used for the request + returned: failure or debug + type: str + sample: ?rsp-prop-include=config-only +method: + description: The HTTP method used for the request to the APIC + returned: failure or debug + type: str + sample: POST +response: + description: The HTTP response from the APIC + returned: failure or debug + type: str + sample: OK (30 bytes) +status: + description: The HTTP status from the APIC + returned: failure or debug + type: int + sample: 200 +url: + description: The HTTP url used for the request to the APIC + returned: failure or debug + type: str + sample: https://10.11.12.13/api/mo/uni/tn-production.json +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec, aci_annotation_spec + + +def main(): + argument_spec = aci_argument_spec() + argument_spec.update(aci_annotation_spec()) + argument_spec.update( + tenant=dict(type="str", aliases=["tenant_name"]), + logical_device=dict(type="str", aliases=["device_name", "device", "logical_device_name"]), + name=dict(type="str", aliases=["concrete_device", "concrete_device_name"]), + vcenter_name=dict(type="str"), + vm_name=dict(type="str"), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["tenant", "logical_device", "name"]], + ["state", "present", ["tenant", "logical_device", "name"]], + ], + ) + + tenant = module.params.get("tenant") + state = module.params.get("state") + logical_device = module.params.get("logical_device") + name = module.params.get("name") + vcenter_name = module.params.get("vcenter_name") + vm_name = module.params.get("vm_name") + + aci = ACIModule(module) + + aci.construct_url( + root_class=dict( + aci_class="fvTenant", + aci_rn="tn-{0}".format(tenant), + module_object=tenant, + target_filter={"name": tenant}, + ), + subclass_1=dict( + aci_class="vnsLDevVip", + aci_rn="lDevVip-{0}".format(logical_device), + module_object=logical_device, + target_filter={"name": logical_device}, + ), + subclass_2=dict( + aci_class="vnsCDev", + aci_rn="cDev-{0}".format(name), + module_object=name, + target_filter={"name": name}, + ), + ) + + aci.get_existing() + + if state == "present": + aci.payload( + aci_class="vnsCDev", + class_config=dict( + name=name, + vcenterName=vcenter_name, + vmName=vm_name, + ), + ) + aci.get_diff(aci_class="vnsCDev") + + aci.post_config() + + elif state == "absent": + aci.delete_config() + + aci.exit_json() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/aci_l4l7_concrete_interface.py b/plugins/modules/aci_l4l7_concrete_interface.py new file mode 100644 index 000000000..eac42c5ea --- /dev/null +++ b/plugins/modules/aci_l4l7_concrete_interface.py @@ -0,0 +1,358 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: aci_l4l7_concrete_interface +short_description: Manage L4-L7 Concrete Interfaces (vns:CIf) +description: +- Manage Layer 4 to Layer 7 (L4-L7) Concrete Interfaces. +options: + tenant: + description: + - The name of an existing tenant. + type: str + aliases: [ tenant_name ] + logical_device: + description: + - The name of an existing logical device. + type: str + aliases: [ device_name, device, logical_device_name ] + concrete_device: + description: + - The name of an existing concrete device. + type: str + aliases: [ concrete_device_name ] + name: + description: + - The name of the concrete interface. + type: str + aliases: [ concrete_interface ] + pod_id: + description: + - The unique identifier for the pod where the concrete interface is located. + type: int + node_id: + description: + - The unique identifier for the node where the concrete interface is located. + - For Ports and Port-channels, this is represented as a single node ID. + - For virtual Port Channels (vPCs), this is represented as a hyphen-separated pair of node IDs, such as "201-202". + type: str + interface: + description: + - The path to the physical interface. + - For single ports, this is the port name, e.g. "eth1/15". + - For Port-channels and vPCs, this is the Interface Policy Group name. + type: str + aliases: [ path_ep, interface_name, interface_policy_group, interface_policy_group_name ] + vnic_name: + description: + - The concrete interface vNIC name. + type: str + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + type: str + choices: [ absent, present, query ] + default: present +extends_documentation_fragment: +- cisco.aci.aci +- cisco.aci.annotation +notes: +- The I(tenant), I(logical_device) and I(concrete_device) must exist before using this module in your playbook. + The M(cisco.aci.aci_tenant), M(cisco.aci.aci_l4l7_device) and M(cisco.aci.aci_l4l7_concrete_device) modules can be used for this. +seealso: +- module: cisco.aci.aci_l4l7_device +- module: cisco.aci.aci_l4l7_concrete_device +- name: APIC Management Information Model reference + description: More information about the internal APIC class B(vns:CIf) + link: https://developer.cisco.com/docs/apic-mim-ref/ +author: +- Tim Cragg (@timcragg) +- Shreyas Srish (@shrsr) +""" + +EXAMPLES = r""" +- name: Add a new concrete interface on a single port + cisco.aci.aci_l4l7_concrete_interface: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + concrete_device: my_concrete_device + name: my_concrete_interface + pod_id: 1 + node_id: 201 + interface: eth1/16 + state: present + delegate_to: localhost + +- name: Add a new concrete interface on a vPC + cisco.aci.aci_l4l7_concrete_interface: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + concrete_device: my_concrete_device + name: my_concrete_interface + pod_id: 1 + node_id: 201-202 + interface: my_vpc_ipg + state: present + delegate_to: localhost + +- name: Query a concrete interface + cisco.aci.aci_l4l7_concrete_interface: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + concrete_device: my_concrete_device + name: my_concrete_interface + pod_id: 1 + node_id: 201-202 + interface: my_vpc_ipg + state: query + delegate_to: localhost + register: query_result + +- name: Query all concrete interfaces + cisco.aci.aci_l4l7_concrete_interface: + host: apic + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result + +- name: Delete a concrete interface + cisco.aci.aci_l4l7_concrete_interface: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + concrete_device: my_concrete_device + name: my_concrete_interface + state: absent + delegate_to: localhost +""" + +RETURN = r""" +current: + description: The existing configuration from the APIC after the module has finished + returned: success + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +error: + description: The error information as returned from the APIC + returned: failure + type: dict + sample: + { + "code": "122", + "text": "unknown managed object class foo" + } +raw: + description: The raw output returned by the APIC REST API (xml or json) + returned: parse error + type: str + sample: '' +sent: + description: The actual/minimal configuration pushed to the APIC + returned: info + type: list + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment" + } + } + } +previous: + description: The original configuration from the APIC before the module has started + returned: info + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +proposed: + description: The assembled configuration from the user-provided parameters + returned: info + type: dict + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "name": "production" + } + } + } +filter_string: + description: The filter string used for the request + returned: failure or debug + type: str + sample: ?rsp-prop-include=config-only +method: + description: The HTTP method used for the request to the APIC + returned: failure or debug + type: str + sample: POST +response: + description: The HTTP response from the APIC + returned: failure or debug + type: str + sample: OK (30 bytes) +status: + description: The HTTP status from the APIC + returned: failure or debug + type: int + sample: 200 +url: + description: The HTTP url used for the request to the APIC + returned: failure or debug + type: str + sample: https://10.11.12.13/api/mo/uni/tn-production.json +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec, aci_annotation_spec + + +def main(): + argument_spec = aci_argument_spec() + argument_spec.update(aci_annotation_spec()) + argument_spec.update( + tenant=dict(type="str", aliases=["tenant_name"]), + logical_device=dict(type="str", aliases=["device_name", "device", "logical_device_name"]), + concrete_device=dict(type="str", aliases=["concrete_device_name"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + name=dict(type="str", aliases=["concrete_interface"]), + pod_id=dict(type="int"), + node_id=dict(type="str"), + interface=dict(type="str", aliases=["path_ep", "interface_name", "interface_policy_group", "interface_policy_group_name"]), + vnic_name=dict(type="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["tenant", "logical_device", "concrete_device", "name"]], + ["state", "present", ["tenant", "logical_device", "concrete_device", "name", "pod_id", "node_id", "interface"]], + ], + ) + + tenant = module.params.get("tenant") + state = module.params.get("state") + logical_device = module.params.get("logical_device") + concrete_device = module.params.get("concrete_device") + name = module.params.get("name") + pod_id = module.params.get("pod_id") + node_id = module.params.get("node_id") + interface = module.params.get("interface") + vnic_name = module.params.get("vnic_name") + + aci = ACIModule(module) + + aci.construct_url( + root_class=dict( + aci_class="fvTenant", + aci_rn="tn-{0}".format(tenant), + module_object=tenant, + target_filter={"name": tenant}, + ), + subclass_1=dict( + aci_class="vnsLDevVip", + aci_rn="lDevVip-{0}".format(logical_device), + module_object=logical_device, + target_filter={"name": logical_device}, + ), + subclass_2=dict( + aci_class="vnsCDev", + aci_rn="cDev-{0}".format(concrete_device), + module_object=concrete_device, + target_filter={"name": concrete_device}, + ), + subclass_3=dict( + aci_class="vnsCIf", + aci_rn="cIf-[{0}]".format(name), + module_object=name, + target_filter={"name": name}, + ), + child_classes=["vnsRsCIfPathAtt"], + ) + + aci.get_existing() + + if state == "present": + path_dn = "topology/pod-{0}/{1}-{2}/pathep-[{3}]".format(pod_id, "protpaths" if "-" in node_id else "paths", node_id, interface) + aci.payload( + aci_class="vnsCIf", + class_config=dict( + name=name, + vnicName=vnic_name, + ), + child_configs=[ + dict( + vnsRsCIfPathAtt=dict( + attributes=dict(tDn=path_dn), + ), + ), + ], + ) + aci.get_diff(aci_class="vnsCIf") + + aci.post_config() + + elif state == "absent": + aci.delete_config() + + aci.exit_json() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/aci_l4l7_concrete_interface_attachment.py b/plugins/modules/aci_l4l7_concrete_interface_attachment.py new file mode 100644 index 000000000..22b94a48a --- /dev/null +++ b/plugins/modules/aci_l4l7_concrete_interface_attachment.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: aci_l4l7_concrete_interface_attachment +short_description: Manage L4-L7 Concrete Interface Attachment (vns:RsCIfAttN) +description: +- Manage Layer 4 to Layer 7 (L4-L7) Concrete Interface Attachment to Logical Interfaces. +options: + tenant: + description: + - The name of an existing tenant. + type: str + aliases: [ tenant_name ] + logical_device: + description: + - The name of an existing Logical Device. + type: str + aliases: [ device_name, device, logical_device_name ] + logical_interface: + description: + - The name of an existing Logical Interface. + type: str + concrete_device: + description: + - The name of an existing Concrete Device. + type: str + aliases: [ concrete_device_name ] + concrete_interface: + description: + - The name of an existing Concrete Interface. + type: str + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + type: str + choices: [ absent, present, query ] + default: present +extends_documentation_fragment: +- cisco.aci.aci +- cisco.aci.annotation +notes: +- The I(tenant), I(logical_device), I(logical_interface) and I(concrete_device) must exist before using this module in your playbook. + The M(cisco.aci.aci_tenant), M(cisco.aci.aci_l4l7_device), M(cisco.aci.aci_l4l7_logical_interface) + and M(cisco.aci.aci_l4l7_concrete_device) modules can be used for this. +seealso: +- module: cisco.aci.aci_tenant +- module: cisco.aci.aci_l4l7_device +- module: cisco.aci.aci_l4l7_logical_interface +- module: cisco.aci.aci_l4l7_concrete_device +- module: cisco.aci.aci_l4l7_concrete_interface +- name: APIC Management Information Model reference + description: More information about the internal APIC class B(vns:RsCIfAttN) + link: https://developer.cisco.com/docs/apic-mim-ref/ +author: +- Tim Cragg (@timcragg) +- Shreyas Srish (@shrsr) +""" + +EXAMPLES = r""" +- name: Add a new concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attachment: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_log_device + logical_interface: my_log_intf + concrete_device: my_conc_device + concrete_interface: my_conc_intf + state: present + delegate_to: localhost + +- name: Query a concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attachment: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_log_device + logical_interface: my_log_intf + concrete_device: my_conc_device + concrete_interface: my_conc_intf + state: query + delegate_to: localhost + register: query_result + +- name: Query all concrete interface attachments + cisco.aci.aci_l4l7_concrete_interface_attachment: + host: apic + username: admin + password: SomeSecretPassword + delegate_to: localhost + register: query_result + +- name: Delete a concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attachment: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_log_device + logical_interface: my_log_intf + concrete_device: my_conc_device + concrete_interface: my_conc_intf + state: absent + delegate_to: localhost +""" + +RETURN = r""" +current: + description: The existing configuration from the APIC after the module has finished + returned: success + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +error: + description: The error information as returned from the APIC + returned: failure + type: dict + sample: + { + "code": "122", + "text": "unknown managed object class foo" + } +raw: + description: The raw output returned by the APIC REST API (xml or json) + returned: parse error + type: str + sample: '' +sent: + description: The actual/minimal configuration pushed to the APIC + returned: info + type: list + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment" + } + } + } +previous: + description: The original configuration from the APIC before the module has started + returned: info + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +proposed: + description: The assembled configuration from the user-provided parameters + returned: info + type: dict + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "name": "production" + } + } + } +filter_string: + description: The filter string used for the request + returned: failure or debug + type: str + sample: ?rsp-prop-include=config-only +method: + description: The HTTP method used for the request to the APIC + returned: failure or debug + type: str + sample: POST +response: + description: The HTTP response from the APIC + returned: failure or debug + type: str + sample: OK (30 bytes) +status: + description: The HTTP status from the APIC + returned: failure or debug + type: int + sample: 200 +url: + description: The HTTP url used for the request to the APIC + returned: failure or debug + type: str + sample: https://10.11.12.13/api/mo/uni/tn-production.json +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec, aci_annotation_spec + + +def main(): + argument_spec = aci_argument_spec() + argument_spec.update(aci_annotation_spec()) + argument_spec.update( + tenant=dict(type="str", aliases=["tenant_name"]), + logical_device=dict(type="str", aliases=["device_name", "device", "logical_device_name"]), + logical_interface=dict(type="str"), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + concrete_device=dict(type="str", aliases=["concrete_device_name"]), + concrete_interface=dict(type="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["tenant", "logical_device", "logical_interface", "concrete_device", "concrete_interface"]], + ["state", "present", ["tenant", "logical_device", "logical_interface", "concrete_device", "concrete_interface"]], + ], + required_together=[ + ["tenant", "logical_device", "concrete_device", "concrete_interface"], + ], + ) + + tenant = module.params.get("tenant") + state = module.params.get("state") + logical_device = module.params.get("logical_device") + logical_interface = module.params.get("logical_interface") + concrete_device = module.params.get("concrete_device") + concrete_interface = module.params.get("concrete_interface") + + aci = ACIModule(module) + + tdn = ( + "uni/tn-{0}/lDevVip-{1}/cDev-{2}/cIf-[{3}]".format(tenant, logical_device, concrete_device, concrete_interface) + if concrete_interface is not None + else None + ) + + aci.construct_url( + root_class=dict( + aci_class="fvTenant", + aci_rn="tn-{0}".format(tenant), + module_object=tenant, + target_filter={"name": tenant}, + ), + subclass_1=dict( + aci_class="vnsLDevVip", + aci_rn="lDevVip-{0}".format(logical_device), + module_object=logical_device, + target_filter={"name": logical_device}, + ), + subclass_2=dict( + aci_class="vnsLIf", + aci_rn="lIf-{0}".format(logical_interface), + module_object=logical_interface, + target_filter={"name": logical_interface}, + ), + subclass_3=dict( + aci_class="vnsRsCIfAttN", + aci_rn="rscIfAttN-[{0}]".format(tdn), + module_object=tdn, + target_filter={"tDn": tdn}, + ), + ) + + aci.get_existing() + + if state == "present": + aci.payload( + aci_class="vnsRsCIfAttN", + class_config=dict(tDn=tdn), + ) + aci.get_diff(aci_class="vnsRsCIfAttN") + + aci.post_config() + + elif state == "absent": + aci.delete_config() + + aci.exit_json() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/aci_l4l7_device.py b/plugins/modules/aci_l4l7_device.py new file mode 100644 index 000000000..c3db71d7e --- /dev/null +++ b/plugins/modules/aci_l4l7_device.py @@ -0,0 +1,366 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: aci_l4l7_device +short_description: Manage L4-L7 Devices (vns:LDevVip) +description: +- Manage Layer 4 to Layer 7 (L4-L7) Devices. +options: + tenant: + description: + - The name of an existing tenant. + type: str + aliases: [ tenant_name ] + name: + description: + - The name of the L4-L7 device. + type: str + aliases: [ device, logical_device, device_name, logical_device_name ] + context_aware: + description: + - Is device Single or Multi context aware. + - The APIC defaults to C(single) when unset during creation. + type: str + choices: [ multi, single ] + device_type: + description: + - The type of the device. + - The APIC defaults to C(physical) when unset during creation. + type: str + choices: [ physical, virtual ] + aliases: [ dev_type ] + function_type: + description: + - The function type of the device. + - The APIC defaults to C(go_to) when unset during creation. + type: str + choices: [ go_to, go_through, l1, l2 ] + aliases: [ func_type ] + managed: + description: + - Is the device a managed device. + - The APIC defaults to true when unset during creation. + type: bool + promiscuous_mode: + description: + - Enable promiscuous mode. + - The APIC defaults to false when unset during creation. + type: bool + aliases: [ prom_mode ] + service_type: + description: + - The service type running on the device. + - The APIC defaults to C(others) when unset during creation. + type: str + choices: [ adc, fw, others ] + aliases: [ svc_type ] + trunking: + description: + - Enable trunking. + - The APIC defaults to false when unset during creation. + type: bool + domain: + description: + - The domain to bind to the device. + - The type of domain is controlled by the device_type setting. + type: str + active_active_mode: + description: + - The active active mode on the device. + - This is only applicable when C(function_type="l1") or C(function_type="l2"). + - The APIC defaults to false when unset during creation. + type: bool + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + type: str + choices: [ absent, present, query ] + default: present +extends_documentation_fragment: +- cisco.aci.aci +- cisco.aci.annotation +notes: +- The I(tenant) must exist before using this module in your playbook. + The M(cisco.aci.aci_tenant) modules can be used for this. +seealso: +- module: cisco.aci.aci_tenant +- name: APIC Management Information Model reference + description: More information about the internal APIC class B(vns:LDevVip) + link: https://developer.cisco.com/docs/apic-mim-ref/ +author: +- Tim Cragg (@timcragg) +- Shreyas Srish (@shrsr) +""" + +EXAMPLES = r""" +- name: Add a new L4-L7 device + cisco.aci.aci_l4l7_device: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + name: my_device + state: present + domain: phys + function_type: go_to + context_aware: single + managed: false + device_type: physical + service_type: adc + trunking: false + promiscuous_mode: true + delegate_to: localhost + +- name: Query an L4-L7 device + cisco.aci.aci_l4l7_device: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + name: my_device + state: query + delegate_to: localhost + register: query_result + +- name: Query all L4-L7 devices + cisco.aci.aci_l4l7_device: + host: apic + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result + +- name: Delete an existing L4-L7 device + cisco.aci.aci_l4l7_device: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + name: my_device + state: absent + delegate_to: localhost +""" + +RETURN = r""" +current: + description: The existing configuration from the APIC after the module has finished + returned: success + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +error: + description: The error information as returned from the APIC + returned: failure + type: dict + sample: + { + "code": "122", + "text": "unknown managed object class foo" + } +raw: + description: The raw output returned by the APIC REST API (xml or json) + returned: parse error + type: str + sample: '' +sent: + description: The actual/minimal configuration pushed to the APIC + returned: info + type: list + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment" + } + } + } +previous: + description: The original configuration from the APIC before the module has started + returned: info + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +proposed: + description: The assembled configuration from the user-provided parameters + returned: info + type: dict + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "name": "production" + } + } + } +filter_string: + description: The filter string used for the request + returned: failure or debug + type: str + sample: ?rsp-prop-include=config-only +method: + description: The HTTP method used for the request to the APIC + returned: failure or debug + type: str + sample: POST +response: + description: The HTTP response from the APIC + returned: failure or debug + type: str + sample: OK (30 bytes) +status: + description: The HTTP status from the APIC + returned: failure or debug + type: int + sample: 200 +url: + description: The HTTP url used for the request to the APIC + returned: failure or debug + type: str + sample: https://10.11.12.13/api/mo/uni/tn-production.json +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec, aci_annotation_spec +from ansible_collections.cisco.aci.plugins.module_utils.constants import L4L7_FUNC_TYPES_MAPPING + + +def main(): + argument_spec = aci_argument_spec() + argument_spec.update(aci_annotation_spec()) + argument_spec.update( + tenant=dict(type="str", aliases=["tenant_name"]), + name=dict(type="str", aliases=["device", "device_name", "logical_device", "logical_device_name"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + context_aware=dict(type="str", choices=["single", "multi"]), + device_type=dict(type="str", aliases=["dev_type"], choices=["physical", "virtual"]), + function_type=dict(type="str", aliases=["func_type"], choices=list(L4L7_FUNC_TYPES_MAPPING)), + managed=dict(type="bool"), + promiscuous_mode=dict(type="bool", aliases=["prom_mode"]), + service_type=dict(type="str", aliases=["svc_type"], choices=["adc", "fw", "others"]), + trunking=dict(type="bool"), + domain=dict(type="str"), + active_active_mode=dict(type="bool"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["tenant", "name"]], + ["state", "present", ["tenant", "name", "device_type", "domain"]], + ], + ) + + aci = ACIModule(module) + + tenant = module.params.get("tenant") + state = module.params.get("state") + name = module.params.get("name") + context_aware = module.params.get("context_aware") + device_type = module.params.get("device_type") + function_type = L4L7_FUNC_TYPES_MAPPING.get(module.params.get("function_type")) + managed = aci.boolean(module.params.get("managed")) + promiscuous_mode = aci.boolean(module.params.get("promiscuous_mode")) + service_type = module.params.get("service_type") + trunking = aci.boolean(module.params.get("trunking")) + domain = module.params.get("domain") + active_active_mode = aci.boolean(module.params.get("active_active_mode")) + + aci.construct_url( + root_class=dict( + aci_class="fvTenant", + aci_rn="tn-{0}".format(tenant), + module_object=tenant, + target_filter={"name": tenant}, + ), + subclass_1=dict( + aci_class="vnsLDevVip", + aci_rn="lDevVip-{0}".format(name), + module_object=name, + target_filter={"name": name}, + ), + child_classes=["vnsRsALDevToPhysDomP", "vnsCDev", "vnsRsALDevToDomP"], + ) + + aci.get_existing() + + if state == "present": + child_configs = [] + if device_type == "virtual": + dom_class = "vnsRsALDevToDomP" + tdn = "uni/vmmp-VMware/dom-{0}".format(domain) + else: + dom_class = "vnsRsALDevToPhysDomP" + tdn = "uni/phys-{0}".format(domain) + child_configs.append({dom_class: {"attributes": {"tDn": tdn}}}) + + aci.payload( + aci_class="vnsLDevVip", + class_config=dict( + name=name, + contextAware="{0}-Context".format(context_aware), + devtype=device_type.upper(), + funcType=function_type, + managed=managed, + promMode=promiscuous_mode, + svcType=service_type.upper(), + trunking=trunking, + activeActive=active_active_mode, + ), + child_configs=child_configs, + ) + + aci.get_diff(aci_class="vnsLDevVip") + + aci.post_config() + + elif state == "absent": + aci.delete_config() + + aci.exit_json() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/aci_l4l7_logical_interface.py b/plugins/modules/aci_l4l7_logical_interface.py new file mode 100644 index 000000000..5a6bd3843 --- /dev/null +++ b/plugins/modules/aci_l4l7_logical_interface.py @@ -0,0 +1,295 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: aci_l4l7_logical_interface +short_description: Manage L4-L7 Logical Interface (vns:LIf) +description: +- Manage Layer 4 to Layer 7 (L4-L7) Logical Interfaces. +options: + tenant: + description: + - The name of an existing tenant. + type: str + aliases: [ tenant_name ] + logical_device: + description: + - The name of an existing Logical Device. + type: str + aliases: [ device_name, device, logical_device_name ] + logical_interface: + description: + - The name of an existing Logical Interface. + type: str + aliases: [ name ] + encap: + description: + - The encapsulation of the Logical Interface. + - It requires the VLAN to be prepended, for example, 'vlan-987'. + type: str + enhanced_lag_policy: + description: + - Name of the VMM Domain Enhanced Lag Policy. + type: str + aliases: [ lag_policy ] + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + type: str + choices: [ absent, present, query ] + default: present +extends_documentation_fragment: +- cisco.aci.aci +- cisco.aci.annotation +notes: +- The I(tenant) and I(logical_device) must exist before using this module in your playbook. + The M(cisco.aci.aci_tenant) and M(cisco.aci.aci_l4l7_device) modules can be used for this. +seealso: +- module: cisco.aci.aci_tenant +- module: cisco.aci.aci_l4l7_device +- name: APIC Management Information Model reference + description: More information about the internal APIC class B(vns:LIf) + link: https://developer.cisco.com/docs/apic-mim-ref/ +author: +- Tim Cragg (@timcragg) +- Shreyas Srish (@shrsr) +""" + +EXAMPLES = r""" +- name: Add a new logical interface + cisco.aci.aci_l4l7_logical_interface: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + logical_interface: my_log_intf + encap: vlan-987 + state: present + delegate_to: localhost + +- name: Query a logical interface + cisco.aci.aci_l4l7_logical_interface: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + logical_interface: my_log_intf + state: query + delegate_to: localhost + register: query_result + +- name: Query all logical interfaces + cisco.aci.aci_l4l7_logical_interface: + host: apic + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result + +- name: Delete a logical interface + cisco.aci.aci_l4l7_logical_interface: + host: apic + username: admin + password: SomeSecretPassword + tenant: my_tenant + device: my_device + logical_interface: my_log_intf + state: absent + delegate_to: localhost +""" + +RETURN = r""" +current: + description: The existing configuration from the APIC after the module has finished + returned: success + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +error: + description: The error information as returned from the APIC + returned: failure + type: dict + sample: + { + "code": "122", + "text": "unknown managed object class foo" + } +raw: + description: The raw output returned by the APIC REST API (xml or json) + returned: parse error + type: str + sample: '' +sent: + description: The actual/minimal configuration pushed to the APIC + returned: info + type: list + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment" + } + } + } +previous: + description: The original configuration from the APIC before the module has started + returned: info + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +proposed: + description: The assembled configuration from the user-provided parameters + returned: info + type: dict + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "name": "production" + } + } + } +filter_string: + description: The filter string used for the request + returned: failure or debug + type: str + sample: ?rsp-prop-include=config-only +method: + description: The HTTP method used for the request to the APIC + returned: failure or debug + type: str + sample: POST +response: + description: The HTTP response from the APIC + returned: failure or debug + type: str + sample: OK (30 bytes) +status: + description: The HTTP status from the APIC + returned: failure or debug + type: int + sample: 200 +url: + description: The HTTP url used for the request to the APIC + returned: failure or debug + type: str + sample: https://10.11.12.13/api/mo/uni/tn-production.json +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec, aci_annotation_spec + + +def main(): + argument_spec = aci_argument_spec() + argument_spec.update(aci_annotation_spec()) + argument_spec.update( + tenant=dict(type="str", aliases=["tenant_name"]), + logical_device=dict(type="str", aliases=["device_name", "device", "logical_device_name"]), + logical_interface=dict(type="str", aliases=["name"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + encap=dict(type="str"), + enhanced_lag_policy=dict(type="str", aliases=["lag_policy"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["tenant", "logical_device", "logical_interface"]], + ["state", "present", ["tenant", "logical_device", "logical_interface"]], + ], + ) + + tenant = module.params.get("tenant") + state = module.params.get("state") + logical_device = module.params.get("logical_device") + logical_interface = module.params.get("logical_interface") + encap = module.params.get("encap") + enhanced_lag_policy = module.params.get("enhanced_lag_policy") + + aci = ACIModule(module) + + aci.construct_url( + root_class=dict( + aci_class="fvTenant", + aci_rn="tn-{0}".format(tenant), + module_object=tenant, + target_filter={"name": tenant}, + ), + subclass_1=dict( + aci_class="vnsLDevVip", + aci_rn="lDevVip-{0}".format(logical_device), + module_object=logical_device, + target_filter={"name": logical_device}, + ), + subclass_2=dict( + aci_class="vnsLIf", + aci_rn="lIf-{0}".format(logical_interface), + module_object=logical_interface, + target_filter={"name": logical_interface}, + ), + ) + + aci.get_existing() + + if state == "present": + aci.payload( + aci_class="vnsLIf", + class_config=dict(name=logical_interface, encap=encap, lagPolicyName=enhanced_lag_policy), + ) + aci.get_diff(aci_class="vnsLIf") + + aci.post_config() + + elif state == "absent": + aci.delete_config() + + aci.exit_json() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/aci_l4l7_concrete_device/aliases b/tests/integration/targets/aci_l4l7_concrete_device/aliases new file mode 100644 index 000000000..209b793f9 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_concrete_device/aliases @@ -0,0 +1,2 @@ +# No ACI simulator yet, so not enabled +# unsupported diff --git a/tests/integration/targets/aci_l4l7_concrete_device/tasks/main.yml b/tests/integration/targets/aci_l4l7_concrete_device/tasks/main.yml new file mode 100644 index 000000000..1373b5a5d --- /dev/null +++ b/tests/integration/targets/aci_l4l7_concrete_device/tasks/main.yml @@ -0,0 +1,197 @@ +# Test code for the ACI modules +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have an ACI APIC host, ACI username and ACI password + fail: + msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.' + when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined + +# GET Credentials from the inventory +- name: Set vars + set_fact: + aci_info: &aci_info + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: '{{ aci_validate_certs | default(false) }}' + use_ssl: '{{ aci_use_ssl | default(true) }}' + use_proxy: '{{ aci_use_proxy | default(true) }}' + output_level: '{{ aci_output_level | default("info") }}' + +# CLEAN ENVIRONMENT +- name: Remove ansible_tenant if it already exists + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent + +# CREATE TENANT +- name: Create ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: present + +# CREATE DOMAIN +- name: Create ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: present + +# CREATE L4-L7 LOGICAL DEVICE +- name: Create L4-L7 Device + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + domain: ansible_phys_dom + func_type: go_to + context_aware: single + managed: false + dev_type: physical + svc_type: adc + trunking: false + prom_mode: true + state: present + +# ADD L4-L7 CONCRETE DEVICE +- name: Create L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: &l4l7_concrete_device + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + state: present + check_mode: true + register: add_l4l7_concrete_device_cm + +- name: Create L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: + <<: *l4l7_concrete_device + register: add_l4l7_concrete_device + +- name: Verify L4-L7 Concrete Device Attributes + ansible.builtin.assert: + that: + - add_l4l7_concrete_device_cm is changed + - add_l4l7_concrete_device is changed + - add_l4l7_concrete_device_cm.proposed.vnsCDev.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device" + - add_l4l7_concrete_device_cm.proposed.vnsCDev.attributes.name == "ansible_concrete_device" + - add_l4l7_concrete_device.current.0.vnsCDev.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device" + - add_l4l7_concrete_device.current.0.vnsCDev.attributes.name == "ansible_concrete_device" + - add_l4l7_concrete_device.previous == [] + +# ADD L4-L7 CONCRETE DEVICE AGAIN TO CHECK IDEMPOTENCE +- name: Add L4-L7 Concrete Device again + cisco.aci.aci_l4l7_concrete_device: + <<: *l4l7_concrete_device + register: add_l4l7_concrete_device_again + +- name: Verify L4-L7 Concrete Device Attributes + ansible.builtin.assert: + that: + - add_l4l7_concrete_device_again.previous == add_l4l7_concrete_device_again.current == add_l4l7_concrete_device.current + - add_l4l7_concrete_device_again is not changed + +- name: Create another L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device2 + state: present + register: add_l4l7_concrete_device_2 + +# QUERY L4-L7 CONCRETE DEVICE +- name: Query L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + state: query + register: query_l4l7_concrete_device + +- name: Verify L4-L7 Concrete Device Attributes + ansible.builtin.assert: + that: + - query_l4l7_concrete_device is not changed + - query_l4l7_concrete_device.current.0.vnsCDev.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device" + - query_l4l7_concrete_device.current.0.vnsCDev.attributes.name == "ansible_concrete_device" + +- name: Query All L4-L7 Concrete Devices + cisco.aci.aci_l4l7_concrete_device: + <<: *aci_info + state: query + register: query_l4l7_concrete_device_all + +- name: Verify L4-L7 Concrete Device Attributes + ansible.builtin.assert: + that: + - query_l4l7_concrete_device_all is not changed + - query_l4l7_concrete_device_all.current | length >= 2 + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device' in query_l4l7_concrete_device_all.current | map(attribute='vnsCDev.attributes.dn') | list" + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device2' in query_l4l7_concrete_device_all.current | map(attribute='vnsCDev.attributes.dn') | list" + +# DELETE L4-L7 CONCRETE DEVICE +- name: Remove L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: &remove_l4l7_concrete_device + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + state: absent + check_mode: true + register: delete_l4l7_concrete_device_cm + +- name: Remove L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: + <<: *remove_l4l7_concrete_device + register: delete_l4l7_concrete_device + +- name: Verify L4-L7 Concrete Device Deletion + ansible.builtin.assert: + that: + - delete_l4l7_concrete_device_cm is changed + - delete_l4l7_concrete_device is changed + - delete_l4l7_concrete_device.current == [] + - delete_l4l7_concrete_device_cm.proposed == {} + - delete_l4l7_concrete_device_cm.previous == delete_l4l7_concrete_device.previous == add_l4l7_concrete_device.current + +# DELETE L4-L7 CONCRETE DEVICE AGAIN TO TEST IDEMPOTENCE +- name: Remove L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: + <<: *remove_l4l7_concrete_device + register: delete_l4l7_concrete_device_again + +- name: Verify L4-L7 Concrete Device Deletion idempotence + ansible.builtin.assert: + that: + - delete_l4l7_concrete_device_again.previous == delete_l4l7_concrete_device_again.current == [] + - delete_l4l7_concrete_device_again is not changed + +# CLEAN UP +- name: Remove ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent diff --git a/tests/integration/targets/aci_l4l7_concrete_interface/aliases b/tests/integration/targets/aci_l4l7_concrete_interface/aliases new file mode 100644 index 000000000..209b793f9 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_concrete_interface/aliases @@ -0,0 +1,2 @@ +# No ACI simulator yet, so not enabled +# unsupported diff --git a/tests/integration/targets/aci_l4l7_concrete_interface/tasks/main.yml b/tests/integration/targets/aci_l4l7_concrete_interface/tasks/main.yml new file mode 100644 index 000000000..cbfb504c0 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_concrete_interface/tasks/main.yml @@ -0,0 +1,217 @@ +# Test code for the ACI modules +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have an ACI APIC host, ACI username and ACI password + fail: + msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.' + when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined + +# GET Credentials from the inventory +- name: Set vars + set_fact: + aci_info: &aci_info + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: '{{ aci_validate_certs | default(false) }}' + use_ssl: '{{ aci_use_ssl | default(true) }}' + use_proxy: '{{ aci_use_proxy | default(true) }}' + output_level: '{{ aci_output_level | default("info") }}' + +# CLEAN ENVIRONMENT +- name: Remove ansible_tenant if it already exists + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent + +# CREATE TENANT +- name: Create ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: present + +# CREATE DOMAIN +- name: Create ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: present + +# CREATE L4-L7 LOGICAL DEVICE +- name: Create L4-L7 Device + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + domain: ansible_phys_dom + func_type: go_to + context_aware: single + managed: false + dev_type: physical + svc_type: adc + trunking: false + prom_mode: true + state: present + +# ADD L4-L7 CONCRETE DEVICE +- name: Create L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + state: present + +# ADD L4-L7 CONCRETE INTERFACE +- name: Create L4-L7 Concrete Interface in check mode + cisco.aci.aci_l4l7_concrete_interface: &l4l7_concrete_interface + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface + pod_id: 1 + node_id: 201 + path_ep: eth1/16 + state: present + check_mode: true + register: add_concrete_interface_cm + +- name: Create L4-L7 Concrete Interface + cisco.aci.aci_l4l7_concrete_interface: + <<: *l4l7_concrete_interface + register: add_concrete_interface + +- name: Create L4-L7 Concrete Interface again + cisco.aci.aci_l4l7_concrete_interface: + <<: *l4l7_concrete_interface + register: add_concrete_interface_again + +- name: Update L4-L7 Concrete Interface + cisco.aci.aci_l4l7_concrete_interface: + <<: *l4l7_concrete_interface + pod_id: 2 + register: add_concrete_interface_update + +- name: Verify L4-L7 Concrete Interface Attributes + ansible.builtin.assert: + that: + - add_concrete_interface_cm is changed + - add_concrete_interface is changed + - add_concrete_interface_again is not changed + - add_concrete_interface_update is changed + - add_concrete_interface_cm.proposed.vnsCIf.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]" + - add_concrete_interface_cm.proposed.vnsCIf.attributes.name == "ansible_concrete_interface" + - add_concrete_interface.current.0.vnsCIf.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]" + - add_concrete_interface.current.0.vnsCIf.attributes.name == "ansible_concrete_interface" + - add_concrete_interface_again.previous == add_concrete_interface_again.current == add_concrete_interface.current + - add_concrete_interface.current.0.vnsCIf.children.0.vnsRsCIfPathAtt.attributes.tDn == "topology/pod-1/paths-201/pathep-[eth1/16]" + - add_concrete_interface_again.current.0.vnsCIf.children.0.vnsRsCIfPathAtt.attributes.tDn == "topology/pod-1/paths-201/pathep-[eth1/16]" + - add_concrete_interface_update.current.0.vnsCIf.children.0.vnsRsCIfPathAtt.attributes.tDn == "topology/pod-2/paths-201/pathep-[eth1/16]" + +# QUERY L4-L7 CONCRETE INTERFACE +- name: Create another L4-L7 Concrete Interface for query all + cisco.aci.aci_l4l7_concrete_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface2 + pod_id: 1 + node_id: 202 + path_ep: eth1/16 + state: present + register: add_concrete_interface_another + +- name: Query L4-L7 Concrete Interface + cisco.aci.aci_l4l7_concrete_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface + pod_id: 1 + node_id: 201 + path_ep: eth1/16 + state: query + register: query_concrete_interface + +- name: Query all L4-L7 Concrete Interfaces + cisco.aci.aci_l4l7_concrete_interface: + <<: *aci_info + state: query + register: query_all + +- name: Verify L4-L7 Concrete Interface Attributes + ansible.builtin.assert: + that: + - query_concrete_interface is not changed + - query_concrete_interface.current.0.vnsCIf.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]" + - query_concrete_interface.current.0.vnsCIf.attributes.name == "ansible_concrete_interface" + - query_all is not changed + - query_all.current | length >= 2 + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]' in query_all.current | map(attribute='vnsCIf.attributes.dn') | list" + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface2]' in query_all.current | map(attribute='vnsCIf.attributes.dn') | list" + - query_concrete_interface.current.0.vnsCIf.children.0.vnsRsCIfPathAtt.attributes.tDn == "topology/pod-2/paths-201/pathep-[eth1/16]" + +# DELETE L4-L7 CONCRETE INTERFACE +- name: Remove L4-L7 Concrete Interface in check mode + cisco.aci.aci_l4l7_concrete_interface: &remove_l4l7_concrete_interface + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface + pod_id: 1 + node_id: 201 + path_ep: eth1/16 + state: absent + check_mode: true + register: delete_concrete_interface_cm + +- name: Remove L4-L7 Concrete Interface + cisco.aci.aci_l4l7_concrete_interface: + <<: *remove_l4l7_concrete_interface + register: delete_concrete_interface + +- name: Remove L4-L7 Concrete Interface again + cisco.aci.aci_l4l7_concrete_interface: + <<: *remove_l4l7_concrete_interface + register: delete_concrete_interface_again + +- name: Verify L4-L7 Concrete Interface Deletion + ansible.builtin.assert: + that: + - delete_concrete_interface_cm is changed + - delete_concrete_interface is changed + - delete_concrete_interface_again is not changed + - delete_concrete_interface.current == [] == delete_concrete_interface_again.current + - delete_concrete_interface_cm.proposed == {} + - delete_concrete_interface_cm.previous == delete_concrete_interface.previous == add_concrete_interface_update.current + +# CLEAN UP +- name: Remove ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent diff --git a/tests/integration/targets/aci_l4l7_concrete_interface_attachment/aliases b/tests/integration/targets/aci_l4l7_concrete_interface_attachment/aliases new file mode 100644 index 000000000..209b793f9 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_concrete_interface_attachment/aliases @@ -0,0 +1,2 @@ +# No ACI simulator yet, so not enabled +# unsupported diff --git a/tests/integration/targets/aci_l4l7_concrete_interface_attachment/tasks/main.yml b/tests/integration/targets/aci_l4l7_concrete_interface_attachment/tasks/main.yml new file mode 100644 index 000000000..3246dbea8 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_concrete_interface_attachment/tasks/main.yml @@ -0,0 +1,251 @@ +# Test code for the ACI modules +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have an ACI APIC host, ACI username and ACI password + fail: + msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.' + when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined + +# GET Credentials from the inventory +- name: Set vars + set_fact: + aci_info: &aci_info + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: '{{ aci_validate_certs | default(false) }}' + use_ssl: '{{ aci_use_ssl | default(true) }}' + use_proxy: '{{ aci_use_proxy | default(true) }}' + output_level: '{{ aci_output_level | default("info") }}' + +# CLEAN ENVIRONMENT +- name: Remove ansible_tenant if it already exists + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent + +# CREATE TENANT +- name: Create ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: present + +# CREATE DOMAIN +- name: Create ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: present + +# CREATE L4-L7 LOGICAL DEVICE +- name: Create L4-L7 Device + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + domain: ansible_phys_dom + func_type: go_to + context_aware: single + managed: false + dev_type: physical + svc_type: adc + trunking: false + prom_mode: true + state: present + +# CREATE L4-L7 LOGICAL INTERFACE +- name: Add Logical Interface + cisco.aci.aci_l4l7_logical_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + encap: vlan-987 + state: present + +- name: Add Second Logical Interface + cisco.aci.aci_l4l7_logical_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: second_log_intf + encap: vlan-988 + state: present + +# CREATE L4-L7 CONCRETE INTERFACE +- name: Create L4-L7 Concrete Device + cisco.aci.aci_l4l7_concrete_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + state: present + +# CREATE L4-L7 CONCRETE INTERFACE +- name: Create L4-L7 Concrete Interface + cisco.aci.aci_l4l7_concrete_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface + pod_id: 1 + node_id: 201 + path_ep: eth1/16 + state: present + +- name: Create Second L4-L7 Concrete Interface + cisco.aci.aci_l4l7_concrete_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + concrete_device: ansible_concrete_device + concrete_interface: ansible_second_concrete_interface + pod_id: 1 + node_id: 201 + path_ep: eth1/17 + state: present + +- name: Add a new concrete interface attachment in check mode + cisco.aci.aci_l4l7_concrete_interface_attach: &concrete_interface_attach + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface + state: present + check_mode: true + register: add_concrete_intf_attach_cm + +- name: Add a new concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *concrete_interface_attach + register: add_concrete_intf_attach + +- name: Add a new concrete interface attachment again + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *concrete_interface_attach + register: add_concrete_intf_attach_again + +- name: Update concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *concrete_interface_attach + concrete_interface: ansible_second_concrete_interface + register: add_concrete_intf_attach_update + +- name: Verify interface attachment + ansible.builtin.assert: + that: + - add_concrete_intf_attach_cm is changed + - add_concrete_intf_attach is changed + - add_concrete_intf_attach_update is changed + - add_concrete_intf_attach.previous == [] + - add_concrete_intf_attach_cm.proposed.vnsRsCIfAttN.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf/rscIfAttN-[uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]]" + - add_concrete_intf_attach_cm.proposed.vnsRsCIfAttN.attributes.tDn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]" + - add_concrete_intf_attach.current.0.vnsRsCIfAttN.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf/rscIfAttN-[uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]]" + - add_concrete_intf_attach.current.0.vnsRsCIfAttN.attributes.tDn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]" + - add_concrete_intf_attach_again is not changed + - add_concrete_intf_attach_again.current == add_concrete_intf_attach.current == add_concrete_intf_attach_again.previous + - add_concrete_intf_attach_again.current.0.vnsRsCIfAttN.attributes.tDn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]" + - add_concrete_intf_attach_update.current.0.vnsRsCIfAttN.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf/rscIfAttN-[uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_second_concrete_interface]]" + +- name: Add a second concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: second_log_intf + concrete_device: ansible_concrete_device + concrete_interface: ansible_second_concrete_interface + state: present + +# QUERY CONCRETE INTERFACE ATTACHMENT +- name: Query concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface + state: query + register: query_concrete_intf_attach + +- name: Query all concrete interface attachments + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *aci_info + state: query + register: query_all_attachments + +- name: Verify interface attachment + ansible.builtin.assert: + that: + - query_concrete_intf_attach is not changed + - query_concrete_intf_attach.current.0.vnsRsCIfAttN.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf/rscIfAttN-[uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]]" + - query_concrete_intf_attach.current.0.vnsRsCIfAttN.attributes.tDn == "uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]" + - query_all_attachments is not changed + - query_all_attachments.current | length >= 2 + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf/rscIfAttN-[uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_second_concrete_interface]]' in query_all_attachments.current | map(attribute='vnsRsCIfAttN.attributes.dn') | list" + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf/rscIfAttN-[uni/tn-ansible_tenant/lDevVip-ansible_device/cDev-ansible_concrete_device/cIf-[ansible_concrete_interface]]' in query_all_attachments.current | map(attribute='vnsRsCIfAttN.attributes.dn') | list" + +# DELETE CONCRETE INTERFACE ATTACHMENT +- name: Remove concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attach: &remove_concrete_interface_attach + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + concrete_device: ansible_concrete_device + concrete_interface: ansible_concrete_interface + state: absent + check_mode: true + register: delete_concrete_intf_attach_cm + +- name: Remove concrete interface attachment + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *remove_concrete_interface_attach + register: delete_concrete_intf_attach + +- name: Remove concrete interface attachment again + cisco.aci.aci_l4l7_concrete_interface_attach: + <<: *remove_concrete_interface_attach + register: delete_concrete_intf_attach_again + +- name: Verify interface attachment removal + ansible.builtin.assert: + that: + - delete_concrete_intf_attach_cm is changed + - delete_concrete_intf_attach_cm.proposed == {} + - delete_concrete_intf_attach is changed + - delete_concrete_intf_attach.previous == add_concrete_intf_attach.current + - delete_concrete_intf_attach.current == [] + - delete_concrete_intf_attach_again is not changed + - delete_concrete_intf_attach_again.current == [] + +# CLEAN UP +- name: Remove ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent diff --git a/tests/integration/targets/aci_l4l7_device/aliases b/tests/integration/targets/aci_l4l7_device/aliases new file mode 100644 index 000000000..209b793f9 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_device/aliases @@ -0,0 +1,2 @@ +# No ACI simulator yet, so not enabled +# unsupported diff --git a/tests/integration/targets/aci_l4l7_device/tasks/main.yml b/tests/integration/targets/aci_l4l7_device/tasks/main.yml new file mode 100644 index 000000000..8d6f2cb67 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_device/tasks/main.yml @@ -0,0 +1,327 @@ +# Test code for the ACI modules +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have an ACI APIC host, ACI username and ACI password + fail: + msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.' + when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined + +# GET Credentials from the inventory +- name: Set vars + set_fact: + aci_info: &aci_info + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: '{{ aci_validate_certs | default(false) }}' + use_ssl: '{{ aci_use_ssl | default(true) }}' + use_proxy: '{{ aci_use_proxy | default(true) }}' + output_level: '{{ aci_output_level | default("info") }}' + +# CLEAN ENVIRONMENT +- name: Remove ansible_tenant if it already exists + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent + +- name: Remove ansible_vmm_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_vmm_dom + domain_type: vmm + vm_provider: vmware + state: absent + +# CREATE TENANT +- name: Create ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: present + +# CREATE DOMAINS +- name: Create ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: present + +- name: Create ansible_vmm_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_vmm_dom + domain_type: vmm + vm_provider: vmware + state: present + +# ADD L4-L7 DEVICE +- name: Create L4-L7 Physical Device in check mode + cisco.aci.aci_l4l7_device: &l4_l7_physical + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + domain: ansible_phys_dom + func_type: go_to + context_aware: single + managed: false + dev_type: physical + svc_type: adc + trunking: false + prom_mode: true + state: present + check_mode: true + register: create_l4l7_device_cm + +- name: Create L4-L7 Physical Device + cisco.aci.aci_l4l7_device: + <<: *l4_l7_physical + register: create_l4l7_device + +- name: Create L4-L7 Physical Device again + cisco.aci.aci_l4l7_device: + <<: *l4_l7_physical + register: create_l4l7_device_again + +- name: Create L4-L7 Virtual Device + cisco.aci.aci_l4l7_device: &l4_l7_virtual + <<: *aci_info + tenant: ansible_tenant + device: ansible_virt_device + domain: ansible_vmm_dom + func_type: go_to + context_aware: single + managed: false + dev_type: virtual + svc_type: adc + trunking: false + prom_mode: true + state: present + register: create_virt_l4l7_device + +- name: Create L4-L7 Virtual Device again + cisco.aci.aci_l4l7_device: + <<: *l4_l7_virtual + register: create_virt_l4l7_device_again + +- name: Verify L4-L7 device has been created + ansible.builtin.assert: + that: + - create_l4l7_device_cm is changed + - create_l4l7_device is changed + - create_l4l7_device_again is not changed + - create_virt_l4l7_device is changed + - create_virt_l4l7_device_again is not changed + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.name == "ansible_device" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.contextAware == "single-Context" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.devtype == "PHYSICAL" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.funcType == "GoTo" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.managed == "no" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.promMode == "yes" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.svcType == "ADC" + - create_l4l7_device_cm.proposed.vnsLDevVip.attributes.trunking == "no" + - create_l4l7_device.current.0.vnsLDevVip.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device" + - create_l4l7_device.current.0.vnsLDevVip.attributes.name == "ansible_device" + - create_l4l7_device.current.0.vnsLDevVip.attributes.contextAware == "single-Context" + - create_l4l7_device.current.0.vnsLDevVip.attributes.devtype == "PHYSICAL" + - create_l4l7_device.current.0.vnsLDevVip.attributes.funcType == "GoTo" + - create_l4l7_device.current.0.vnsLDevVip.attributes.isCopy == "no" + - create_l4l7_device.current.0.vnsLDevVip.attributes.managed == "no" + - create_l4l7_device.current.0.vnsLDevVip.attributes.promMode == "yes" + - create_l4l7_device.current.0.vnsLDevVip.attributes.svcType == "ADC" + - create_l4l7_device.current.0.vnsLDevVip.attributes.trunking == "no" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_virt_device" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.name == "ansible_virt_device" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.contextAware == "single-Context" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.devtype == "VIRTUAL" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.funcType == "GoTo" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.isCopy == "no" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.managed == "no" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.promMode == "yes" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.svcType == "ADC" + - create_virt_l4l7_device.current.0.vnsLDevVip.attributes.trunking == "no" + - create_l4l7_device.previous == [] + - create_virt_l4l7_device.previous == [] + - create_l4l7_device_again.previous == create_l4l7_device_again.current == create_l4l7_device.current + - create_virt_l4l7_device_again.previous == create_virt_l4l7_device_again.current == create_virt_l4l7_device.current + +- name: Verify domain binding object has been created + ansible.builtin.assert: + that: + - create_l4l7_device.current.0.vnsLDevVip.children.0.vnsRsALDevToPhysDomP.attributes.tDn == "uni/phys-ansible_phys_dom" + - create_virt_l4l7_device.current.0.vnsLDevVip.children.0.vnsRsALDevToDomP.attributes.tDn == "uni/vmmp-VMware/dom-ansible_vmm_dom" + +- name: Verify domain binding object is still correct + ansible.builtin.assert: + that: + - create_l4l7_device_again.current.0.vnsLDevVip.children.0.vnsRsALDevToPhysDomP.attributes.tDn == "uni/phys-ansible_phys_dom" + - create_virt_l4l7_device_again.current.0.vnsLDevVip.children.0.vnsRsALDevToDomP.attributes.tDn == "uni/vmmp-VMware/dom-ansible_vmm_dom" + +# MODIFY L4-L7 Device +- name: Update L4-L7 device + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + domain: ansible_phys_dom + func_type: go_through + context_aware: multi + managed: false + dev_type: physical + svc_type: fw + trunking: true + prom_mode: false + state: present + register: update_l4l7_device + +- name: Verify L4-L7 device has been updated + ansible.builtin.assert: + that: + - update_l4l7_device is changed + - update_l4l7_device.current.0.vnsLDevVip.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device" + - update_l4l7_device.current.0.vnsLDevVip.attributes.name == "ansible_device" + - update_l4l7_device.current.0.vnsLDevVip.attributes.contextAware == "multi-Context" + - update_l4l7_device.current.0.vnsLDevVip.attributes.devtype == "PHYSICAL" + - update_l4l7_device.current.0.vnsLDevVip.attributes.funcType == "GoThrough" + - update_l4l7_device.current.0.vnsLDevVip.attributes.isCopy == "no" + - update_l4l7_device.current.0.vnsLDevVip.attributes.managed == "no" + - update_l4l7_device.current.0.vnsLDevVip.attributes.promMode == "no" + - update_l4l7_device.current.0.vnsLDevVip.attributes.svcType == "FW" + - update_l4l7_device.current.0.vnsLDevVip.attributes.trunking == "yes" + +- name: Verify domain binding object + ansible.builtin.assert: + that: + - update_l4l7_device.current.0.vnsLDevVip.children.0.vnsRsALDevToPhysDomP.attributes.tDn == "uni/phys-ansible_phys_dom" + +- name: Create another L4-L7 device to test active active mode + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device2 + domain: ansible_phys_dom + func_type: l1 + context_aware: multi + managed: false + dev_type: physical + svc_type: others + trunking: true + prom_mode: false + active_active_mode: true + state: present + register: another_l4l7_device_active + +- name: Verify domain binding object + ansible.builtin.assert: + that: + - another_l4l7_device_active is changed + - another_l4l7_device_active.current.0.vnsLDevVip.attributes.funcType == "L1" + - another_l4l7_device_active.current.0.vnsLDevVip.attributes.activeActive == "yes" + +# QUERY DEVICE +- name: Query L4-L7 Device + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + state: query + register: query_l4l7_device + +- name: Verify L4-L7 Device attributes + ansible.builtin.assert: + that: + - query_l4l7_device is not changed + - query_l4l7_device.current.0.vnsLDevVip.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device" + - query_l4l7_device.current.0.vnsLDevVip.attributes.name == "ansible_device" + - query_l4l7_device.current.0.vnsLDevVip.attributes.contextAware == "multi-Context" + - query_l4l7_device.current.0.vnsLDevVip.attributes.devtype == "PHYSICAL" + - query_l4l7_device.current.0.vnsLDevVip.attributes.funcType == "GoThrough" + - query_l4l7_device.current.0.vnsLDevVip.attributes.isCopy == "no" + - query_l4l7_device.current.0.vnsLDevVip.attributes.managed == "no" + - query_l4l7_device.current.0.vnsLDevVip.attributes.promMode == "no" + - query_l4l7_device.current.0.vnsLDevVip.attributes.svcType == "FW" + - query_l4l7_device.current.0.vnsLDevVip.attributes.trunking == "yes" + +- name: Query all L4-L7 Devices + cisco.aci.aci_l4l7_device: + <<: *aci_info + state: query + register: query_l4l7_device_all + +- name: Verify L4-L7 Device query idempotence + ansible.builtin.assert: + that: + - query_l4l7_device_all is not changed + - query_l4l7_device_all.current | length >= 3 + - "'uni/tn-ansible_tenant/lDevVip-ansible_device' in query_l4l7_device_all.current | map(attribute='vnsLDevVip.attributes.dn') | list" + - "'uni/tn-ansible_tenant/lDevVip-ansible_virt_device' in query_l4l7_device_all.current | map(attribute='vnsLDevVip.attributes.dn') | list" + +# DELETE DEVICE +- name: Delete L4-L7 Device + cisco.aci.aci_l4l7_device: &remove_l4l7_device + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + state: absent + check_mode: true + register: remove_l4l7_device_cm + +- name: Delete L4-L7 Device + cisco.aci.aci_l4l7_device: + <<: *remove_l4l7_device + register: remove_l4l7_device + +- name: Verify L4-L7 Device deletion + ansible.builtin.assert: + that: + - remove_l4l7_device_cm is changed + - remove_l4l7_device is changed + - remove_l4l7_device_cm.proposed == {} + - remove_l4l7_device_cm.previous == remove_l4l7_device.previous == update_l4l7_device.current + - remove_l4l7_device.current == [] + +# DELETE DEVICE AGAIN TO TEST IDEMPOTENCE +- name: Delete L4-L7 Device again + cisco.aci.aci_l4l7_device: + <<: *remove_l4l7_device + register: remove_l4l7_device_again + +- name: Verify L4-L7 Device deletion idempotence + ansible.builtin.assert: + that: + - remove_l4l7_device_again is not changed + - remove_l4l7_device_again.current == [] == remove_l4l7_device_again.previous + +# CLEAN UP +- name: Remove ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent + +- name: Remove ansible_vmm_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_vmm_dom + domain_type: vmm + vm_provider: vmware + state: absent diff --git a/tests/integration/targets/aci_l4l7_logical_interface/aliases b/tests/integration/targets/aci_l4l7_logical_interface/aliases new file mode 100644 index 000000000..209b793f9 --- /dev/null +++ b/tests/integration/targets/aci_l4l7_logical_interface/aliases @@ -0,0 +1,2 @@ +# No ACI simulator yet, so not enabled +# unsupported diff --git a/tests/integration/targets/aci_l4l7_logical_interface/tasks/main.yml b/tests/integration/targets/aci_l4l7_logical_interface/tasks/main.yml new file mode 100644 index 000000000..5af74785e --- /dev/null +++ b/tests/integration/targets/aci_l4l7_logical_interface/tasks/main.yml @@ -0,0 +1,288 @@ +# Test code for the ACI modules +# Copyright: (c) 2025, Tim Cragg (@timcragg) +# Copyright: (c) 2025, Shreyas Srish (@shrsr) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have an ACI APIC host, ACI username and ACI password + fail: + msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.' + when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined + +# GET Credentials from the inventory +- name: Set vars + set_fact: + aci_info: &aci_info + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: '{{ aci_validate_certs | default(false) }}' + use_ssl: '{{ aci_use_ssl | default(true) }}' + use_proxy: '{{ aci_use_proxy | default(true) }}' + output_level: '{{ aci_output_level | default("info") }}' + +# CLEAN ENVIRONMENT +- name: Remove ansible_tenant if it already exists + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent + +- name: Remove ansible_vmm_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_vmm_dom + domain_type: vmm + vm_provider: vmware + state: absent + +# CREATE TENANT +- name: Create ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: present + +# CREATE DOMAIN +- name: Create ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: present + +- name: Create ansible_vmm_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_vmm_dom + domain_type: vmm + vm_provider: vmware + state: present + +# CREATE L4-L7 LOGICAL DEVICE +- name: Create L4-L7 Device + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + domain: ansible_phys_dom + func_type: go_to + context_aware: single + managed: false + dev_type: physical + svc_type: adc + trunking: false + prom_mode: true + state: present + +- name: Create L4-L7 Virtual Device + cisco.aci.aci_l4l7_device: + <<: *aci_info + tenant: ansible_tenant + device: ansible_virt_device + domain: ansible_vmm_dom + func_type: go_to + context_aware: single + managed: false + dev_type: virtual + svc_type: adc + trunking: false + prom_mode: true + state: present + +# CREATE L4-L7 LOGICAL INTERFACE +- name: Add Logical Interface in check mode + cisco.aci.aci_l4l7_logical_interface: &l4l7_logical_interface + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + encap: vlan-987 + state: present + check_mode: true + register: add_logical_interface_cm + +- name: Add Logical Interface + cisco.aci.aci_l4l7_logical_interface: + <<: *l4l7_logical_interface + register: add_logical_interface + +- name: Add Logical Interface again + cisco.aci.aci_l4l7_logical_interface: + <<: *l4l7_logical_interface + register: add_logical_interface_again + +- name: Verify Logical Interface Attributes + ansible.builtin.assert: + that: + - add_logical_interface_cm is changed + - add_logical_interface is changed + - add_logical_interface.previous == [] + - add_logical_interface_cm.proposed.vnsLIf.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf" + - add_logical_interface_cm.proposed.vnsLIf.attributes.name == "ansible_log_intf" + - add_logical_interface_cm.proposed.vnsLIf.attributes.encap == "vlan-987" + - add_logical_interface.current.0.vnsLIf.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf" + - add_logical_interface.current.0.vnsLIf.attributes.name == "ansible_log_intf" + - add_logical_interface.current.0.vnsLIf.attributes.encap == "vlan-987" + - add_logical_interface_again is not changed + - add_logical_interface_again.current == add_logical_interface_again.previous == add_logical_interface.current + +# MODIFY L4-L7 LOGICAL INTERFACE +- name: Update L4-L7 Logical Interface + cisco.aci.aci_l4l7_logical_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + encap: vlan-988 + state: present + register: update_logical_interface + +- name: Verify Logical Interface update + ansible.builtin.assert: + that: + - update_logical_interface is changed + - update_logical_interface.current.0.vnsLIf.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf" + - update_logical_interface.current.0.vnsLIf.attributes.name == "ansible_log_intf" + - update_logical_interface.current.0.vnsLIf.attributes.encap == "vlan-988" + +# Test to verify enhanced_lag_policy +- name: Create enhanced lag policy + cisco.aci.aci_rest: + <<: *aci_info + path: api/node/mo/uni/vmmp-VMware/dom-ansible_vmm_dom/vswitchpolcont.json + method: post + content: | + { + "vmmVSwitchPolicyCont": { + "attributes": { + "dn": "uni/vmmp-VMware/dom-ansible_vmm_dom/vswitchpolcont", + "status": "created,modified" + }, + "children": [{ + "lacpEnhancedLagPol": { + "attributes": { + "lbmode": "src-dst-ip", + "mode": "active", + "name": "enhanced", + "numLinks": "2" + } + } + }] + } + } + +- name: Add Logical Interface for virtual device + cisco.aci.aci_l4l7_logical_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_virt_device + logical_interface: ansible_virt + lag_policy: enhanced + state: present + register: add_logical_interface_virtual + +- name: Verify Logical Interface Virtual + ansible.builtin.assert: + that: + - add_logical_interface_virtual is changed + - add_logical_interface_virtual.current.0.vnsLIf.attributes.lagPolicyName == "enhanced" + +# QUERY L4-L7 LOGICAL INTERFACE +- name: Create a second L4-L7 Logical Interface for query all + cisco.aci.aci_l4l7_logical_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_int2 + encap: vlan-989 + state: present + +- name: Query L4-L7 Logical Interface + cisco.aci.aci_l4l7_logical_interface: + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + state: query + register: query_logical_interface + +- name: Query All L4-L7 Logical Interfaces + cisco.aci.aci_l4l7_logical_interface: + <<: *aci_info + state: query + register: query_all + +- name: Verify Logical Interface Attributes have not changed + ansible.builtin.assert: + that: + - query_logical_interface is not changed + - query_logical_interface.current.0.vnsLIf.attributes.dn == "uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf" + - query_logical_interface.current.0.vnsLIf.attributes.name == "ansible_log_intf" + - query_all.current | length >= 3 + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_intf' in query_all.current | map(attribute='vnsLIf.attributes.dn') | list" + - "'uni/tn-ansible_tenant/lDevVip-ansible_device/lIf-ansible_log_int2' in query_all.current | map(attribute='vnsLIf.attributes.dn') | list" + - query_logical_interface.current.0.vnsLIf.attributes.encap == "vlan-988" + +# DELETE L4-L7 LOGICAL INTERFACE +- name: Remove L4-L7 Logical Interface + cisco.aci.aci_l4l7_logical_interface: &remove_l4l7_logical_interface + <<: *aci_info + tenant: ansible_tenant + device: ansible_device + logical_interface: ansible_log_intf + state: absent + check_mode: true + register: delete_logical_interface_cm + +- name: Remove L4-L7 Logical Interface + cisco.aci.aci_l4l7_logical_interface: + <<: *remove_l4l7_logical_interface + register: delete_logical_interface + +- name: Remove L4-L7 Logical Interface + cisco.aci.aci_l4l7_logical_interface: + <<: *remove_l4l7_logical_interface + register: delete_logical_interface_again + +- name: Verify Logical Interface Deletion + ansible.builtin.assert: + that: + - delete_logical_interface_cm is changed + - delete_logical_interface_cm.proposed == {} + - delete_logical_interface is changed + - delete_logical_interface.current == [] + - delete_logical_interface.previous == update_logical_interface.current + - delete_logical_interface.previous.0.vnsLIf.attributes.name == "ansible_log_intf" + - delete_logical_interface.previous.0.vnsLIf.attributes.encap == "vlan-988" + - delete_logical_interface_again is not changed + - delete_logical_interface_again.current == [] == delete_logical_interface_again.previous + +# CLEAN UP +- name: Remove ansible_tenant + cisco.aci.aci_tenant: + <<: *aci_info + name: ansible_tenant + state: absent + +- name: Remove ansible_phys_domain + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_phys_dom + domain_type: phys + state: absent + +- name: Remove ansible_vmm_domain if it already exists + cisco.aci.aci_domain: + <<: *aci_info + domain: ansible_vmm_dom + domain_type: vmm + vm_provider: vmware + state: absent