diff --git a/plugins/module_utils/aci.py b/plugins/module_utils/aci.py index a9e666ec3..f64ca6eed 100644 --- a/plugins/module_utils/aci.py +++ b/plugins/module_utils/aci.py @@ -320,6 +320,7 @@ def __init__(self, module): self.obj_filter = None self.method = None self.path = None + self.parent_path = None self.response = None self.status = None self.url = None @@ -921,6 +922,7 @@ def _construct_url_1(self, obj, config_only=True): if self.module.params.get("state") in ("absent", "present"): # State is absent or present self.path = "api/mo/uni/{0}.json".format(obj_rn) + self.parent_path = "api/mo/uni.json" if config_only: self.update_qs({"rsp-prop-include": "config-only"}) self.obj_filter = obj_filter @@ -947,6 +949,7 @@ def _construct_url_2(self, parent, obj, config_only=True): if self.module.params.get("state") in ("absent", "present"): # State is absent or present self.path = "api/mo/uni/{0}/{1}.json".format(parent_rn, obj_rn) + self.parent_path = "api/mo/uni/{0}.json".format(parent_rn) if config_only: self.update_qs({"rsp-prop-include": "config-only"}) self.obj_filter = obj_filter @@ -984,6 +987,7 @@ def _construct_url_3(self, root, parent, obj, config_only=True): if self.module.params.get("state") in ("absent", "present"): # State is absent or present self.path = "api/mo/uni/{0}/{1}/{2}.json".format(root_rn, parent_rn, obj_rn) + self.parent_path = "api/mo/uni/{0}/{1}.json".format(root_rn, parent_rn) if config_only: self.update_qs({"rsp-prop-include": "config-only"}) self.obj_filter = obj_filter @@ -1053,6 +1057,7 @@ def _construct_url_4(self, root, sec, parent, obj, config_only=True): if self.module.params.get("state") in ("absent", "present"): # State is absent or present self.path = "api/mo/uni/{0}/{1}/{2}/{3}.json".format(root_rn, sec_rn, parent_rn, obj_rn) + self.parent_path = "api/mo/uni/{0}/{1}/{2}.json".format(root_rn, sec_rn, parent_rn) if config_only: self.update_qs({"rsp-prop-include": "config-only"}) self.obj_filter = obj_filter @@ -1108,6 +1113,7 @@ def _construct_url_5(self, root, ter, sec, parent, obj, config_only=True): if self.module.params.get("state") in ("absent", "present"): # State is absent or present self.path = "api/mo/uni/{0}/{1}/{2}/{3}/{4}.json".format(root_rn, ter_rn, sec_rn, parent_rn, obj_rn) + self.parent_path = "api/mo/uni/{0}/{1}/{2}/{3}.json".format(root_rn, ter_rn, sec_rn, parent_rn) if config_only: self.update_qs({"rsp-prop-include": "config-only"}) self.obj_filter = obj_filter @@ -1517,7 +1523,7 @@ def payload(self, aci_class, class_config, child_configs=None): if children: self.proposed[aci_class].update(dict(children=children)) - def post_config(self): + def post_config(self, parent_class=None): """ This method is used to handle the logic when the modules state is equal to present. The method only pushes a change if the object has differences than what exists on the APIC, and if check_mode is False. A successful change will mark the @@ -1527,12 +1533,19 @@ def post_config(self): return elif not self.module.check_mode: # Sign and encode request as to APIC's wishes + url = self.url + if parent_class is not None: + if self.params.get("port") is not None: + url = "{protocol}://{host}:{port}/{path}".format(path=self.parent_path, **self.module.params) + else: + url = "{protocol}://{host}/{path}".format(path=self.parent_path, **self.module.params) + self.config = {parent_class: {"attributes": {}, "children": [self.config]}} if self.params.get("private_key"): self.cert_auth(method="POST", payload=json.dumps(self.config)) resp, info = fetch_url( self.module, - self.url, + url, data=json.dumps(self.config), headers=self.headers, method="POST", diff --git a/plugins/module_utils/constants.py b/plugins/module_utils/constants.py index 9b273a75d..8788bd32a 100644 --- a/plugins/module_utils/constants.py +++ b/plugins/module_utils/constants.py @@ -31,3 +31,5 @@ NODE_TYPE_MAPPING = {"tier_2": "tier-2-leaf", "remote": "remote-leaf-wan", "virtual": "virtual", "unspecified": "unspecified"} SPAN_DIRECTION_MAP = {"incoming": "in", "outgoing": "out", "both": "both"} + +MATCH_TYPE_MAPPING = {"all": "All", "at_least_one": "AtleastOne", "at_most_one": "AtmostOne", "none": "None"} diff --git a/plugins/modules/aci_vrf_leak_internal_subnet.py b/plugins/modules/aci_vrf_leak_internal_subnet.py new file mode 100644 index 000000000..67432de61 --- /dev/null +++ b/plugins/modules/aci_vrf_leak_internal_subnet.py @@ -0,0 +1,397 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Abraham Mughal (@abmughal) abmughal@cisco.com +# 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": "certified"} + +DOCUMENTATION = r""" +--- +module: aci_vrf_leak_internal_subnet +short_description: Manage VRF leaking of subnets (fv:leakInternalSubnet) +description: +- Manage the leaking of internal subnets under the VRF. +options: + tenant: + description: + - The name of the Tenant the VRF belongs to. + type: str + aliases: [ tenant_name ] + vrf: + description: + - The name of the VRF. + type: str + aliases: [ context, name, vrf_name ] + description: + description: + - The description for the VRF Leak Internal Subnet. + type: str + aliases: [ descr ] + name_alias: + description: + - The alias for the current object. This relates to the nameAlias field in ACI. + type: str + scope: + description: + - Scope of the object. + type: str + choices: [ public, private, shared ] + default: private + leak_to: + description: + - The VRFs to leak the subnet routes into. + type: list + elements: dict + suboptions: + tenant: + description: + - Name of the tenant. + type: str + aliases: [ tenant_name ] + vrf: + description: + - Name of the VRF. + type: str + aliases: [ vrf_name ] + ip: + description: + - The IP address / subnet used to match routes to be leaked. + 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 +- cisco.aci.owner + +notes: +- The C(tenant) and C(vrf) used must exist before using this module in your playbook. + The M(cisco.aci.aci_tenant) and M(cisco.aci.aci_vrf) modules can be used for this. +seealso: +- module: cisco.aci.aci_tenant +- module: cisco.aci.aci_vrf +- name: APIC Management Information Model reference + description: More information about the internal APIC class B(leak:InternalSubnet). + link: https://developer.cisco.com/docs/apic-mim-ref/ +author: +- Abraham Mughal (@abmughal) +""" + +EXAMPLES = r""" +- name: Create leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + host: apic + username: admin + password: SomeSecretPassword + vrf: vrf_lab + tenant: lab_tenant + descr: Lab VRF + state: present + leak_to: + - vrf: "test" + tenant: "lab_tenant" + - vrf: "test2" + tenant: "lab_tenant" + description: Ansible Test + ip: 1.1.1.2 + delegate_to: localhost + +- name: Remove a subnet from leaking + cisco.aci.aci_vrf_leak_internal_subnet: + host: apic + username: admin + password: SomeSecretPassword + vrf: vrf_lab + tenant: lab_tenant + state: absent + leak_to: + - vrf: "test2" + tenant: "lab_tenant" + description: Ansible Test + ip: 1.1.1.2 + delegate_to: localhost + +- name: Delete leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + host: apic + username: admin + password: SomeSecretPassword + vrf: vrf_lab + tenant: lab_tenant + state: absent + description: Ansible Test + ip: 1.1.1.2 + delegate_to: localhost + +- name: Query all leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + host: apic + username: admin + password: SomeSecretPassword + state: query + ip: 1.1.1.2 + delegate_to: localhost + register: query_result + +- name: Query leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + host: apic + username: admin + password: SomeSecretPassword + vrf: vrf_lab + tenant: lab_tenant + state: query + ip: 1.1.1.2 + delegate_to: localhost + register: query_result +""" + +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, aci_owner_spec + + +def main(): + argument_spec = aci_argument_spec() + argument_spec.update(aci_annotation_spec()) + argument_spec.update(aci_owner_spec()) + argument_spec.update( + tenant=dict(type="str", aliases=["tenant_name"]), # Not required for querying all objects + vrf=dict(type="str", aliases=["context", "name", "vrf_name"]), # Not required for querying all objects + leak_to=dict( + type="list", + elements="dict", + options=dict( + vrf=dict(type="str", aliases=["vrf_name"]), + tenant=dict(type="str", aliases=["tenant_name"]), + ), + ), + description=dict(type="str", aliases=["descr"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + name_alias=dict(type="str"), + scope=dict(type="str", default="private", choices=["public", "private", "shared"]), + ip=dict(type="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["tenant", "vrf"]], + ["state", "present", ["tenant", "vrf", "leak_to"]], + ], + ) + + description = module.params.get("description") + state = module.params.get("state") + tenant = module.params.get("tenant") + vrf = module.params.get("vrf") + leak_to = module.params.get("leak_to") + name_alias = module.params.get("name_alias") + scope = module.params.get("scope") + ip = module.params.get("ip") + + 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="fvCtx", + aci_rn="ctx-{0}".format(vrf), + module_object=vrf, + target_filter={"name": vrf}, + ), + subclass_2=dict( + aci_class="leakRoutes", + aci_rn="leakroutes", + module_object=True, + ), + subclass_3=dict( + aci_class="leakInternalSubnet", + aci_rn="leakintsubnet-[{0}]".format(ip), + module_object=ip, + target_filter={"ip": ip}, + ), + child_classes=["leakTo"], + ) + + aci.get_existing() + + if state == "present": + child_configs = [] + + subnet_rn_list = [] + for subnet in leak_to: + subnet_rn_list.append("to-[{0}]-[{1}]".format(subnet.get("tenant"), subnet.get("vrf"))) + child_configs.append( + dict( + leakTo=dict( + attributes=dict( + ctxName=subnet.get("vrf"), + tenantName=subnet.get("tenant"), + scope=scope, + ) + ) + ) + ) + + if isinstance(aci.existing, list) and len(aci.existing) > 0: + for child in aci.existing[0].get("leakInternalSubnet", {}).get("children", {}): + child_attributes = child.get("leakTo", {}).get("attributes", {}) + if child_attributes and "to-[{0}]-[{1}]".format(child_attributes.get("tenantName"), child_attributes.get("ctxName")) not in subnet_rn_list: + child_configs.append( + dict( + leakTo=dict( + attributes=dict( + ctxName=child_attributes.get("ctxName"), + tenantName=child_attributes.get("tenantName"), + status="deleted", + ) + ) + ) + ) + + aci.payload( + aci_class="leakInternalSubnet", + class_config=dict( + descr=description, + ip=ip, + scope=scope, + nameAlias=name_alias, + ), + child_configs=child_configs, + ) + + aci.get_diff(aci_class="leakInternalSubnet") + + if aci.existing: + aci.post_config() + else: + aci.post_config(parent_class="leakRoutes") + + elif state == "absent": + aci.delete_config() + + aci.exit_json() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/aci_vrf_leak_internal_subnet/aliases b/tests/integration/targets/aci_vrf_leak_internal_subnet/aliases new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/targets/aci_vrf_leak_internal_subnet/tasks/main.yml b/tests/integration/targets/aci_vrf_leak_internal_subnet/tasks/main.yml new file mode 100644 index 000000000..2455f24e4 --- /dev/null +++ b/tests/integration/targets/aci_vrf_leak_internal_subnet/tasks/main.yml @@ -0,0 +1,233 @@ +# Test code for the ACI modules +# Copyright: (c) 2023, Abraham Mughal (@abmughal) + +# 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 + +- name: Verify Cloud and Non-Cloud Sites in use. + include_tasks: ../../../../../../integration/targets/aci_cloud_provider/tasks/main.yml + +- name: Execute tasks only for non-cloud sites + when: + - query_cloud.current == [] # This condition will execute only non-cloud sites + - version.current.0.topSystem.attributes.version is version('5', '>=') + block: # block specifies execution of tasks within, based on conditions + - 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("debug") }}' + + - name: delete existing tenant + cisco.aci.aci_tenant: + <<: *aci_info + state: absent + tenant: ansible_test3 + register: tenant_present + + - name: ensure tenant exists for tests to kick off + cisco.aci.aci_tenant: &aci_tenant_present + <<: *aci_info + state: present + tenant: ansible_test3 + register: tenant_present + + - name: create vrf + cisco.aci.aci_vrf: &aci_vrf_present + <<: *aci_tenant_present + vrf: test + description: Ansible Test + register: vrf_present + + - name: create second vrf - creation works + cisco.aci.aci_vrf: + <<: *aci_vrf_present + vrf: test2 + register: vrf_present + + - name: create third vrf - creation works + cisco.aci.aci_vrf: + <<: *aci_vrf_present + vrf: test3 + register: vrf_present + + - name: create leak internal subnet - check_mode + cisco.aci.aci_vrf_leak_internal_subnet: &aci_leak_internal_subnet + <<: *aci_info + state: present + tenant: ansible_test3 + vrf: test + ip: 1.1.1.2 + leak_to: + - vrf: "test2" + tenant: "ansible_test3" + - vrf: "test3" + tenant: "ansible_test3" + description: Ansible Test + check_mode: true + register: leak_sub_check_mode + + - name: create leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + <<: *aci_leak_internal_subnet + register: leak_sub_present + + - name: create leak internal subnet - idempotency check + cisco.aci.aci_vrf_leak_internal_subnet: + <<: *aci_info + state: present + tenant: ansible_test3 + vrf: test + ip: 1.1.1.2 + leak_to: + - vrf: "test2" + tenant: "ansible_test3" + - vrf: "test3" + tenant: "ansible_test3" + description: Ansible Test + register: leak_sub_idempotent + + - name: create second leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + <<: *aci_info + tenant: ansible_test3 + vrf: test2 + leak_to: + - vrf: "test" + tenant: "ansible_test3" + description: Ansible Test + ip: 1.1.1.2 + register: leak_sub_present_2 + + - name: Sort the list of leaked internal subnets for present + ansible.builtin.set_fact: + attributes_list_present: "{{ leak_sub_present.current.0.leakInternalSubnet.children | map(attribute='leakTo.attributes') | list | sort(attribute='ctxName') }}" + + - name: present asserts + assert: + that: + - vrf_present is changed + - leak_sub_check_mode is changed + - leak_sub_check_mode.proposed.leakInternalSubnet.attributes.ip == '1.1.1.2' + - leak_sub_check_mode.proposed.leakInternalSubnet.children.0.leakTo.attributes.ctxName == 'test2' + - leak_sub_check_mode.proposed.leakInternalSubnet.children.0.leakTo.attributes.tenantName == 'ansible_test3' + - leak_sub_check_mode.proposed.leakInternalSubnet.children.1.leakTo.attributes.ctxName == 'test3' + - leak_sub_check_mode.proposed.leakInternalSubnet.children.1.leakTo.attributes.tenantName == 'ansible_test3' + - leak_sub_present.current.0.leakInternalSubnet.attributes.ip == '1.1.1.2' + - attributes_list_present.0.tenantName == 'ansible_test3' + - attributes_list_present.0.ctxName == 'test2' + - attributes_list_present.1.tenantName == 'ansible_test3' + - attributes_list_present.1.ctxName == 'test3' + - leak_sub_idempotent is not changed + - leak_sub_present_2.current.0.leakInternalSubnet.attributes.ip == '1.1.1.2' + - leak_sub_present_2.current.0.leakInternalSubnet.children.0.leakTo.attributes.ctxName == 'test' + - leak_sub_present_2.current.0.leakInternalSubnet.children.0.leakTo.attributes.tenantName == 'ansible_test3' + + - name: query all + cisco.aci.aci_vrf_leak_internal_subnet: &aci_query + <<: *aci_info + state: query + ip: 1.1.1.2 + register: query_all + + - name: query one leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + <<: *aci_tenant_present + state: query + vrf: test + ip: 1.1.1.2 + register: query + + - name: absent case + cisco.aci.aci_vrf_leak_internal_subnet: + <<: *aci_info + tenant: ansible_test3 + vrf: test2 + leak_to: + - vrf: "test" + tenant: "ansible_test3" + description: Ansible Test + ip: 1.1.1.2 + state: absent + register: leak_sub_absent + + - name: Sort the list of leaked internal subnets for query + ansible.builtin.set_fact: + attributes_list_query_all: "{{ query_all.current.0.leakInternalSubnet.children | map(attribute='leakTo.attributes') | list | sort(attribute='ctxName') }}" + attributes_list_query: "{{ query.current.0.leakInternalSubnet.children | map(attribute='leakTo.attributes') | list | sort(attribute='ctxName') }}" + + - name: query asserts + assert: + that: + - query_all is not changed + - query is not changed + - query_all.current.0.leakInternalSubnet.attributes.ip == '1.1.1.2' + - attributes_list_query_all.0.ctxName == 'test2' + - attributes_list_query_all.0.tenantName == 'ansible_test3' + - attributes_list_query_all.1.ctxName == 'test3' + - attributes_list_query_all.1.tenantName == 'ansible_test3' + - query_all.current.1.leakInternalSubnet.attributes.ip == '1.1.1.2' + - query_all.current.1.leakInternalSubnet.children.0.leakTo.attributes.ctxName == 'test' + - query_all.current.1.leakInternalSubnet.children.0.leakTo.attributes.tenantName == 'ansible_test3' + - attributes_list_query.0.ctxName == 'test2' + - attributes_list_query.0.tenantName == 'ansible_test3' + - attributes_list_query.1.ctxName == 'test3' + - attributes_list_query.1.tenantName == 'ansible_test3' + - leak_sub_absent.proposed == {} + + - name: delete leak internal subnet - check_mode + cisco.aci.aci_vrf_leak_internal_subnet: &aci_delete + <<: *aci_vrf_present + vrf: test + leak_to: + - vrf: "test3" + tenant: "ansible_test3" + ip: 1.1.1.2 + register: leak_sub_delete_check_mode + + - name: delete leak internal subnet + cisco.aci.aci_vrf_leak_internal_subnet: + <<: *aci_delete + register: leak_sub_delete + + - name: delete leak internal subnet again + cisco.aci.aci_vrf_leak_internal_subnet: &aci_delete_again + <<: *aci_vrf_present + vrf: test + leak_to: + - vrf: "test2" + tenant: "ansible_test3" + ip: 1.1.1.2 + register: leak_sub_delete_2 + + - name: delete leak internal subnet idempotency check + cisco.aci.aci_vrf_leak_internal_subnet: + <<: *aci_delete_again + register: leak_sub_delete_idempotency + + - name: delete asserts + assert: + that: + - leak_sub_delete_check_mode is changed + - leak_sub_delete_check_mode.current.0.leakInternalSubnet.children.0.leakTo.attributes.ctxName == 'test3' + - leak_sub_delete_check_mode.current.0.leakInternalSubnet.children.0.leakTo.attributes.tenantName == 'ansible_test3' + - leak_sub_delete.previous != [] + - leak_sub_delete.current.0.leakInternalSubnet.children | length == 1 + - leak_sub_delete.current.0.leakInternalSubnet.children.0.leakTo.attributes.ctxName == 'test3' + - leak_sub_delete.current.0.leakInternalSubnet.children.0.leakTo.attributes.tenantName == 'ansible_test3' + - leak_sub_delete_2 is changed + - leak_sub_delete_2.current.0.leakInternalSubnet.children | length == 1 + - leak_sub_delete_2.current.0.leakInternalSubnet.children.0.leakTo.attributes.ctxName == 'test2' + - leak_sub_delete_2.current.0.leakInternalSubnet.children.0.leakTo.attributes.tenantName == 'ansible_test3' + - leak_sub_delete_idempotency is not changed + - leak_sub_delete_idempotency.current.0.leakInternalSubnet.children.0.leakTo.attributes.ctxName == 'test2' + - leak_sub_delete_idempotency.current.0.leakInternalSubnet.children.0.leakTo.attributes.tenantName == 'ansible_test3' \ No newline at end of file