diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/cisco/mso/plugins/modules | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/cisco/mso/plugins/modules')
66 files changed, 17376 insertions, 0 deletions
diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_backup.py b/ansible_collections/cisco/mso/plugins/modules/mso_backup.py new file mode 100644 index 000000000..fc2564b82 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_backup.py @@ -0,0 +1,331 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Shreyas Srish (@shrsr) <ssrish@cisco.com> +# Copyright: (c) 2023, Lionel Hercot (@lhercot) <lhercot@cisco.com> +# Copyright: (c) 2023, Sabari Jaganathan (@sajagana) <sajagana@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_backup +short_description: Manages backups +description: +- Manage backups on Cisco ACI Multi-Site. +author: +- Shreyas Srish (@shrsr) +- Lionel Hercot (@lhercot) +- Sabari Jaganathan (@sajagana) +options: + location_type: + description: + - The type of location for the backup to be stored + type: str + choices: [ local, remote] + default: local + backup: + description: + - The name given to the backup + - C(backup) is mutually exclusive with C(backup_id). Only use one of the two. + type: str + aliases: [ name ] + backup_id: + description: + - The id of a specific backup + - C(backup_id) is mutually exclusive with C(backup). Only use one of the two. + type: str + aliases: [ id ] + remote_location: + description: + - The remote location's name where the backup should be stored + type: str + remote_path: + description: + - This path is relative to the remote location. + - A '/' is automatically added between the remote location folder and this path. + - This folder structure should already exist on the remote location. + type: str + description: + description: + - Brief information about the backup. + type: str + destination: + description: + - Location where to download the backup to + type: str + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + - Use C(upload) for uploading backup. + - Use C(restore) for restoring backup. + - Use C(download) for downloading backup. + - Use C(move) for moving backup from local to remote location. + type: str + choices: [ absent, present, query, upload, restore, download, move ] + default: present +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Create a new local backup + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup + description: via Ansible + location_type: local + state: present + delegate_to: localhost + +- name: Create a new remote backup + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup + description: via Ansible + location_type: remote + remote_location: ansible_test + state: present + delegate_to: localhost + +- name: Move backup to remote location + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup0 + remote_location: ansible_test + remote_path: test + state: move + delegate_to: localhost + +- name: Download a backup + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup + destination: ./ + state: download + delegate_to: localhost + +- name: Upload a backup + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: ./Backup + state: upload + delegate_to: localhost + +- name: Restore a backup + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup + state: restore + delegate_to: localhost + +- name: Remove a Backup + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup + state: absent + delegate_to: localhost + +- name: Query a backup + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup + state: query + delegate_to: localhost + register: query_result + +- name: Query a backup with its complete name + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + backup: Backup_20200721220043 + state: query + delegate_to: localhost + register: query_result + +- name: Query all backups + cisco.mso.mso_backup: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +import os + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + location_type=dict(type="str", default="local", choices=["local", "remote"]), + description=dict(type="str"), + backup=dict(type="str", aliases=["name"]), + backup_id=dict(type="str", aliases=["id"]), + remote_location=dict(type="str"), + remote_path=dict(type="str"), + state=dict(type="str", default="present", choices=["absent", "present", "query", "upload", "restore", "download", "move"]), + destination=dict(type="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["location_type", "remote", ["remote_location"]], + ["state", "absent", ["backup", "backup_id"], True], + ["state", "present", ["backup"]], + ["state", "upload", ["backup", "backup_id"], True], + ["state", "restore", ["backup", "backup_id"], True], + ["state", "download", ["backup", "backup_id"], True], + ["state", "download", ["destination"]], + ["state", "move", ["backup", "backup_id"], True], + ["state", "move", ["remote_location", "remote_path"]], + ], + mutually_exclusive=[ + ("backup", "backup_id"), + ], + ) + + description = module.params.get("description") + location_type = module.params.get("location_type") + state = module.params.get("state") + backup = module.params.get("backup") + backup_id = module.params.get("backup_id") + remote_location = module.params.get("remote_location") + remote_path = module.params.get("remote_path") + destination = module.params.get("destination") + + mso = MSOModule(module) + + backup_names = [] + mso.existing = mso.query_objs("backups/backupRecords", key="backupRecords") + if backup or backup_id: + if mso.existing: + data = mso.existing + mso.existing = [] + for backup_info in data: + if (backup_id and backup_id == backup_info.get("id")) or ( + backup and (backup == backup_info.get("name").split("_")[0] or backup == backup_info.get("name")) + ): + mso.existing.append(backup_info) + backup_names.append(backup_info.get("name")) + + if state == "query": + mso.exit_json() + + elif state == "absent": + mso.previous = mso.existing + if len(mso.existing) > 1: + mso.module.fail_json(msg="Multiple backups with same name found. Existing backups with similar names: {0}".format(", ".join(backup_names))) + elif len(mso.existing) == 1: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request("backups/backupRecords/{id}".format(id=mso.existing[0].get("id")), method="DELETE") + mso.exit_json() + + elif state == "present": + mso.previous = mso.existing + + payload = dict(name=backup, description=description, locationType=location_type) + + if location_type == "remote": + remote_location_info = mso.lookup_remote_location(remote_location) + payload.update(remoteLocationId=remote_location_info.get("id")) + if remote_path: + remote_path = "{0}/{1}".format(remote_location_info.get("path"), remote_path) + payload.update(remotePath=remote_path) + + mso.proposed = payload + + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request("backups", method="POST", data=payload) + mso.exit_json() + + elif state == "upload": + mso.previous = mso.existing + + if module.check_mode: + mso.existing = mso.proposed + else: + try: + request_url = "backups/upload" + payload = dict() + if mso.platform == "nd": + if remote_location is None or remote_path is None: + mso.module.fail_json(msg="NDO backup upload failed: remote_location and remote_path are required for NDO backup upload") + remote_location_info = mso.lookup_remote_location(remote_location) + request_url = "backups/remoteUpload/{0}".format(remote_location_info.get("id")) + else: + payload = dict(name=(os.path.basename(backup), open(backup, "rb"), "application/x-gzip")) + + mso.existing = mso.request_upload(request_url, fields=payload) + except Exception as error: + mso.module.fail_json(msg="Upload failed due to: {0}, Backup file: '{1}'".format(error, ", ".join(backup.split("/")[-1:]))) + mso.exit_json() + + if len(mso.existing) == 0: + mso.module.fail_json(msg="Backup '{0}' does not exist".format(backup)) + elif len(mso.existing) > 1: + mso.module.fail_json(msg="Multiple backups with same name found. Existing backups with similar names: {0}".format(", ".join(backup_names))) + + elif state == "restore": + mso.previous = mso.existing + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request("backups/{id}/restore".format(id=mso.existing[0].get("id")), method="PUT") + + elif state == "download": + mso.previous = mso.existing + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request_download("backups/{id}/download".format(id=mso.existing[0].get("id")), destination=destination) + + elif state == "move": + mso.previous = mso.existing + remote_location_info = mso.lookup_remote_location(remote_location) + remote_path = "{0}/{1}".format(remote_location_info.get("path"), remote_path) + payload = dict(remoteLocationId=remote_location_info.get("id"), remotePath=remote_path, backupRecordId=mso.existing[0].get("id")) + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request("backups/remote-location", method="POST", data=payload) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py b/ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py new file mode 100644 index 000000000..e97b59b2e --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py @@ -0,0 +1,219 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_backup_schedule +short_description: Manages backup schedules +description: +- Manage backup schedules on Cisco ACI Multi-Site. +author: +- Akini Ross (@akinross) +options: + start_date: + description: + - The date to start the scheduler in format YYYY-MM-DD + - If no date is provided, the current date will be used. + type: str + start_time: + description: + - The time to start the scheduler in format HH:MM:SS + - If no time is provided, midnight "00:00:00" will be used. + type: str + frequency_unit: + description: + - The interval unit type + choices: [ hours, days ] + type: str + frequency_length: + description: + - Amount of hours or days for the schedule trigger frequency + type: int + remote_location: + description: + - The remote location's name where the backup should be stored + type: str + remote_path: + description: + - This path is relative to the remote location. + - A '/' is automatically added between the remote location folder and this path. + - This folder structure should already exist on the remote location. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Get current backup schedule + cisco.mso.mso_backup_schedule: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + +- name: Set backup schedule + cisco.mso.mso_backup_schedule: + host: mso_host + username: admin + password: SomeSecretPassword + frequency_unit: hours + frequency_length: 7 + remote_location: ansible_test + state: present + delegate_to: localhost + +- name: Set backup schedule with date and time + cisco.mso.mso_backup_schedule: + host: mso_host + username: admin + password: SomeSecretPassword + frequency_unit: days + frequency_length: 1 + remote_location: ansible_test + remote_path: test + start_time: 20:57:36 + start_date: 2023-04-09 + state: present + delegate_to: localhost + +- name: Delete backup schedule + cisco.mso.mso_backup_schedule: + host: mso_host + username: admin + password: SomeSecretPassword + state: absent + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from datetime import datetime, tzinfo, timedelta + +# UTC Timezone implementation as datetime.timezone is not supported in Python 2.7 + + +class UTC(tzinfo): + """UTC""" + + def utcoffset(self, dt): + return timedelta(0) + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return timedelta(0) + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + start_date=dict(type="str"), + start_time=dict(type="str"), + frequency_unit=dict(type="str", choices=["hours", "days"]), + frequency_length=dict(type="int"), + remote_location=dict(type="str"), + remote_path=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", "present", ["frequency_unit", "frequency_length", "remote_location"]]] + ) + + start_date = module.params.get("start_date") + start_time = module.params.get("start_time") + frequency_unit = module.params.get("frequency_unit") + frequency_length = module.params.get("frequency_length") + remote_location = module.params.get("remote_location") + remote_path = module.params.get("remote_path") + state = module.params.get("state") + + mso = MSOModule(module) + api_path = "backups/schedule" + mso.existing = mso.request(api_path, method="GET") + + if state == "absent": + mso.previous = mso.existing + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(api_path, method="DELETE") + + elif state == "present": + mso.previous = mso.existing + + remote_location_info = mso.lookup_remote_location(remote_location) + + if start_date: + try: + y, m, d = start_date.split("-") + year = int(y) + month = int(m) + day = int(d) + except Exception as e: + module.fail_json(msg="Failed to parse date format 'YYYY-MM-DD' %s, %s" % (start_date, e)) + else: + current_date = datetime.now(UTC()).date() + year = current_date.year + month = current_date.month + day = current_date.day + + if start_time: + try: + h, m, s = start_time.split(":") + hours = int(h) + minutes = int(m) + seconds = int(s) + except Exception as e: + module.fail_json(msg="Failed to parse time format 'HH:MM:SS' %s, %s" % (start_time, e)) + else: + hours = minutes = seconds = 0 + + try: + set_date = datetime(year, month, day, hours, minutes, seconds) + except Exception as e: + module.fail_json(msg="Failed to create datetime object with date '%s', and time '%s'. Error: %s" % (start_date, start_time, e)) + + payload = dict( + startDate="{0}.000Z".format(set_date.isoformat()), + intervalTimeUnit=frequency_unit.upper(), + intervalLength=frequency_length, + remoteLocationId=remote_location_info.get("id"), + locationType="remote", + ) + + if remote_path: + payload.update(remotePath="{0}/{1}".format(remote_location_info.get("path"), remote_path)) + + mso.proposed = payload + + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(api_path, method="POST", data=payload) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy.py b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy.py new file mode 100644 index 000000000..e9b7f23d3 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Lionel Hercot (@lhercot) <lhercot@cisco.com> +# Copyright: (c) 2020, Jorge Gomez (@jgomezve) <jgomezve@cisco.com> (based on mso_dhcp_relay_policy module) +# 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: mso_dhcp_option_policy +short_description: Manage DHCP Option policies. +description: +- Manage DHCP Option policies on Cisco Multi-Site Orchestrator. +author: +- Lionel Hercot (@lhercot) +options: + dhcp_option_policy: + description: + - Name of the DHCP Option Policy + type: str + aliases: [ name ] + description: + description: + - Description of the DHCP Option Policy + type: str + tenant: + description: + - Tenant where the DHCP Option Policy is located. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new DHCP Option Policy + cisco.mso.mso_dhcp_option_policy: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_option_policy: my_test_dhcp_policy + description: "My Test DHCP Policy" + tenant: ansible_test + state: present + delegate_to: localhost + +- name: Remove DHCP Option Policy + cisco.mso.mso_dhcp_option_policy: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_option_policy: my_test_dhcp_policy + state: absent + delegate_to: localhost + +- name: Query a DHCP Option Policy + cisco.mso.mso_dhcp_option_policy: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_option_policy: my_test_dhcp_policy + state: query + delegate_to: localhost + +- name: Query all DHCP Option Policies + cisco.mso.mso_dhcp_option_policy: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + dhcp_option_policy=dict(type="str", aliases=["name"]), + description=dict(type="str"), + tenant=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", ["dhcp_option_policy"]], + ["state", "present", ["dhcp_option_policy", "tenant"]], + ], + ) + + dhcp_option_policy = module.params.get("dhcp_option_policy") + description = module.params.get("description") + tenant = module.params.get("tenant") + state = module.params.get("state") + + mso = MSOModule(module) + + path = "policies/dhcp/option" + + # Query for existing object(s) + if dhcp_option_policy: + mso.existing = mso.get_obj(path, name=dhcp_option_policy, key="DhcpRelayPolicies") + if mso.existing: + policy_id = mso.existing.get("id") + # If we found an existing object, continue with it + path = "{0}/{1}".format(path, policy_id) + else: + mso.existing = mso.query_objs(path, key="DhcpRelayPolicies") + + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(path, method="DELETE", data=mso.sent) + + elif state == "present": + tenant_id = mso.lookup_tenant(tenant) + payload = dict( + name=dhcp_option_policy, + desc=description, + policyType="dhcp", + policySubtype="option", + tenantId=tenant_id, + ) + mso.sanitize(payload, collate=True) + + if mso.existing: + if mso.check_changed(): + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="PUT", data=mso.sent) + else: + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy_option.py b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy_option.py new file mode 100644 index 000000000..f4c397c24 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy_option.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Lionel Hercot (@lhercot) <lhercot@cisco.com> +# Copyright: (c) 2020, Jorge Gomez (@jgomezve) <jgomezve@cisco.com> (based on mso_dhcp_relay_policy module) +# 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: mso_dhcp_option_policy_option +short_description: Manage DHCP options in a DHCP Option policy. +description: +- Manage DHCP options in a DHCP Option policy on Cisco Multi-Site Orchestrator. +author: +- Lionel Hercot (@lhercot) +options: + dhcp_option_policy: + description: + - Name of the DHCP Option Policy + type: str + required: true + aliases: [ name ] + name: + description: + - Name of the option in the DHCP Option Policy + type: str + aliases: [ option ] + id: + description: + - Id of the option in the DHCP Option Policy + type: int + data: + description: + - Data of the DHCP option in the DHCP Option Policy + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new option to a DHCP Option Policy + cisco.mso.mso_dhcp_option_policy_option: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_option_policy: my_test_dhcp_policy + name: ansible_test + id: 1 + data: Data stored in the option + state: present + delegate_to: localhost + +- name: Remove a option to a DHCP Option Policy + cisco.mso.mso_dhcp_option_policy_option: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_option_policy: my_test_dhcp_policy + name: ansible_test + state: absent + delegate_to: localhost + +- name: Query a option to a DHCP Option Policy + cisco.mso.mso_dhcp_option_policy_option: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_option_policy: my_test_dhcp_policy + name: ansible_test + state: query + delegate_to: localhost + +- name: Query all option of a DHCP Option Policy + cisco.mso.mso_dhcp_option_policy_option: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_option_policy: my_test_dhcp_policy + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import ( + MSOModule, + mso_argument_spec, +) + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + dhcp_option_policy=dict(type="str", required=True), + name=dict(type="str", aliases=["option"]), + id=dict(type="int"), + data=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", "present", ["name", "id", "data"]], + ["state", "absent", ["name"]], + ], + ) + + dhcp_option_policy = module.params.get("dhcp_option_policy") + option_id = module.params.get("id") + name = module.params.get("name") + data = module.params.get("data") + state = module.params.get("state") + + mso = MSOModule(module) + + path = "policies/dhcp/option" + + option_index = None + previous_option = {} + + # Query for existing object(s) + dhcp_option_obj = mso.get_obj(path, name=dhcp_option_policy, key="DhcpRelayPolicies") + if "id" not in dhcp_option_obj: + mso.fail_json(msg="DHCP Option Policy '{0}' is not a valid DHCP Option Policy name.".format(dhcp_option_policy)) + policy_id = dhcp_option_obj.get("id") + options = [] + if "dhcpOption" in dhcp_option_obj: + options = dhcp_option_obj.get("dhcpOption") + for index, opt in enumerate(options): + if opt.get("name") == name: + previous_option = opt + option_index = index + + # If we found an existing object, continue with it + path = "{0}/{1}".format(path, policy_id) + + if state == "query": + mso.existing = options + if name is not None: + mso.existing = previous_option + mso.exit_json() + + mso.previous = previous_option + if state == "absent": + option = {} + if previous_option and option_index is not None: + options.pop(option_index) + + elif state == "present": + option = dict( + id=str(option_id), + name=name, + data=data, + ) + if option_index is not None: + options[option_index] = option + else: + options.append(option) + + if module.check_mode: + mso.existing = option + else: + mso.existing = dhcp_option_obj + dhcp_option_obj["dhcpOption"] = options + mso.sanitize(dhcp_option_obj, collate=True) + new_dhcp_option_obj = mso.request(path, method="PUT", data=mso.sent) + mso.existing = {} + for index, opt in enumerate(new_dhcp_option_obj.get("dhcpOption")): + if opt.get("name") == name: + mso.existing = opt + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy.py b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy.py new file mode 100644 index 000000000..394825feb --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Jorge Gomez Velasquez <jgomezve@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": "community", +} + +DOCUMENTATION = r""" +--- +module: mso_dhcp_relay_policy +short_description: Manage DHCP Relay policies. +description: +- Manage DHCP Relay policies on Cisco Multi-Site Orchestrator. +author: +- Jorge Gomez (@jorgegome2307) +options: + dhcp_relay_policy: + description: + - Name of the DHCP Relay Policy + type: str + aliases: [ name ] + description: + description: + - Description of the DHCP Relay Policy + type: str + tenant: + description: + - Tenant where the DHCP Relay Policy is located. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new DHCP Relay Policy + cisco.mso.mso_dhcp_relay_policy: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_relay_policy: my_test_dhcp_policy + description: "My Test DHCP Policy" + tenant: ansible_test + state: present + delegate_to: localhost + +- name: Remove DHCP Relay Policy + cisco.mso.mso_dhcp_relay_policy: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_relay_policy: my_test_dhcp_policy + state: absent + delegate_to: localhost + +- name: Query a DHCP Relay Policy + cisco.mso.mso_dhcp_relay_policy: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_relay_policy: my_test_dhcp_policy + state: query + delegate_to: localhost + +- name: Query all DHCP Relay Policies + cisco.mso.mso_dhcp_relay_policy: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + dhcp_relay_policy=dict(type="str", aliases=["name"]), + description=dict(type="str"), + tenant=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", ["dhcp_relay_policy"]], + ["state", "present", ["dhcp_relay_policy", "tenant"]], + ], + ) + + dhcp_relay_policy = module.params.get("dhcp_relay_policy") + description = module.params.get("description") + tenant = module.params.get("tenant") + state = module.params.get("state") + + mso = MSOModule(module) + + path = "policies/dhcp/relay" + + # Query for existing object(s) + if dhcp_relay_policy: + mso.existing = mso.get_obj(path, name=dhcp_relay_policy, key="DhcpRelayPolicies") + if mso.existing: + policy_id = mso.existing.get("id") + # If we found an existing object, continue with it + path = "{0}/{1}".format(path, policy_id) + else: + mso.existing = mso.query_objs(path, key="DhcpRelayPolicies") + + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(path, method="DELETE", data=mso.sent) + + elif state == "present": + tenant_id = mso.lookup_tenant(tenant) + payload = dict( + name=dhcp_relay_policy, + desc=description, + policyType="dhcp", + policySubtype="relay", + tenantId=tenant_id, + ) + mso.sanitize(payload, collate=True) + + if mso.existing: + if mso.check_changed(): + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="PUT", data=mso.sent) + else: + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy_provider.py b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy_provider.py new file mode 100644 index 000000000..760d90430 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy_provider.py @@ -0,0 +1,256 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Jorge Gomez Velasquez <jgomezve@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": "community", +} + +DOCUMENTATION = r""" +--- +module: mso_dhcp_relay_policy_provider +short_description: Manage DHCP providers in a DHCP Relay policy. +description: +- Manage DHCP providers in a DHCP Relay policy on Cisco Multi-Site Orchestrator. +author: +- Jorge Gomez (@jorgegome2307) +options: + dhcp_relay_policy: + description: + - Name of the DHCP Relay Policy + type: str + required: true + aliases: [ name ] + ip: + description: + - IP address of the DHCP Server + type: str + tenant: + description: + - Tenant where the DHCP provider is located. + type: str + schema: + description: + - Schema where the DHCP provider is configured + type: str + template: + description: + - template where the DHCP provider is configured + type: str + application_profile: + description: + - Application Profile where the DHCP provider is configured + type: str + aliases: [ anp ] + endpoint_group: + description: + - EPG where the DHCP provider is configured + type: str + aliases: [ epg ] + external_endpoint_group: + description: + - External EPG where the DHCP provider is configured + type: str + aliases: [ ext_epg, external_epg ] + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new provider to a DHCP Relay Policy + cisco.mso.mso_dhcp_relay_policy_provider: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_relay_policy: my_test_dhcp_policy + tenant: ansible_test + schema: ansible_test + template: Template 1 + application_profile: ansible_test + endpoint_group: ansible_test + state: present + delegate_to: localhost + +- name: Remove a provider to a DHCP Relay Policy + cisco.mso.mso_dhcp_relay_policy_provider: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_relay_policy: my_test_dhcp_policy + tenant: ansible_test + schema: ansible_test + template: Template 1 + application_profile: ansible_test + endpoint_group: ansible_test + state: absent + delegate_to: localhost + +- name: Query a provider to a DHCP Relay Policy + cisco.mso.mso_dhcp_relay_policy_provider: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_relay_policy: my_test_dhcp_policy + tenant: ansible_test + schema: ansible_test + template: Template 1 + application_profile: ansible_test + endpoint_group: ansible_test + state: query + delegate_to: localhost + +- name: Query all provider of a DHCP Relay Policy + cisco.mso.mso_dhcp_relay_policy_provider: + host: mso_host + username: admin + password: SomeSecretPassword + dhcp_relay_policy: my_test_dhcp_policy + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import ( + MSOModule, + mso_argument_spec, +) + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + dhcp_relay_policy=dict(type="str", required=True, aliases=["name"]), + ip=dict(type="str"), + tenant=dict(type="str"), + schema=dict(type="str"), + template=dict(type="str"), + application_profile=dict(type="str", aliases=["anp"]), + endpoint_group=dict(type="str", aliases=["epg"]), + external_endpoint_group=dict(type="str", aliases=["ext_epg", "external_epg"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["ip", "tenant", "schema", "template"]], + ["state", "absent", ["tenant", "schema", "template"]], + ], + ) + + dhcp_relay_policy = module.params.get("dhcp_relay_policy") + ip = module.params.get("ip") + tenant = module.params.get("tenant") + schema = module.params.get("schema") + template = module.params.get("template") + if template is not None: + template = template.replace(" ", "") + application_profile = module.params.get("application_profile") + endpoint_group = module.params.get("endpoint_group") + external_endpoint_group = module.params.get("external_endpoint_group") + state = module.params.get("state") + + mso = MSOModule(module) + + path = "policies/dhcp/relay" + + tenant_id = mso.lookup_tenant(tenant) + # Get schema_id + schema_id = mso.lookup_schema(schema) + + provider = dict( + addr=ip, + externalEpgRef="", + epgRef="", + l3Ref="", + tenantId=tenant_id, + ) + provider_index = None + previous_provider = {} + + if application_profile is not None and endpoint_group is not None: + provider["epgRef"] = "/schemas/{schemaId}/templates/{templateName}/anps/{app}/epgs/{epg}".format( + schemaId=schema_id, + templateName=template, + app=application_profile, + epg=endpoint_group, + ) + elif external_endpoint_group is not None: + provider["externalEpgRef"] = "/schemas/{schemaId}/templates/{templateName}/externalEpgs/{ext_epg}".format( + schemaId=schema_id, templateName=template, ext_epg=external_endpoint_group + ) + + # Query for existing object(s) + dhcp_relay_obj = mso.get_obj(path, name=dhcp_relay_policy, key="DhcpRelayPolicies") + if "id" not in dhcp_relay_obj: + mso.fail_json(msg="DHCP Relay Policy '{0}' is not a valid DHCP Relay Policy name.".format(dhcp_relay_policy)) + policy_id = dhcp_relay_obj.get("id") + providers = [] + if "provider" in dhcp_relay_obj: + providers = dhcp_relay_obj.get("provider") + for index, prov in enumerate(providers): + if (provider.get("epgRef") != "" and prov.get("epgRef") == provider.get("epgRef")) or ( + provider.get("externalEpgRef") != "" and prov.get("externalEpgRef") == provider.get("externalEpgRef") + ): + previous_provider = prov + provider_index = index + + # If we found an existing object, continue with it + path = "{0}/{1}".format(path, policy_id) + + if state == "query": + mso.existing = providers + if endpoint_group is not None or external_endpoint_group is not None: + mso.existing = previous_provider + mso.exit_json() + + if endpoint_group is None and external_endpoint_group is None: + mso.fail_json(msg="Missing either endpoint_group or external_endpoint_group required attribute.") + + mso.previous = previous_provider + if state == "absent": + provider = {} + if previous_provider: + if provider_index is not None: + providers.pop(provider_index) + + elif state == "present": + if provider_index is not None: + providers[provider_index] = provider + else: + providers.append(provider) + + if module.check_mode: + mso.existing = provider + else: + mso.existing = dhcp_relay_obj + dhcp_relay_obj["provider"] = providers + mso.sanitize(dhcp_relay_obj, collate=True) + new_dhcp_relay_obj = mso.request(path, method="PUT", data=mso.sent) + mso.existing = {} + for index, prov in enumerate(new_dhcp_relay_obj.get("provider")): + if (provider.get("epgRef") != "" and prov.get("epgRef") == provider.get("epgRef")) or ( + provider.get("externalEpgRef") != "" and prov.get("externalEpgRef") == provider.get("externalEpgRef") + ): + mso.existing = prov + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_label.py b/ansible_collections/cisco/mso/plugins/modules/mso_label.py new file mode 100644 index 000000000..f4f05f7de --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_label.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_label +short_description: Manage labels +description: +- Manage labels on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + label: + description: + - The name of the label. + type: str + aliases: [ name ] + type: + description: + - The type of the label. + type: str + choices: [ site ] + default: site + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new label + cisco.mso.mso_label: + host: mso_host + username: admin + password: SomeSecretPassword + label: Belgium + type: site + state: present + delegate_to: localhost + +- name: Remove a label + cisco.mso.mso_label: + host: mso_host + username: admin + password: SomeSecretPassword + label: Belgium + state: absent + delegate_to: localhost + +- name: Query a label + cisco.mso.mso_label: + host: mso_host + username: admin + password: SomeSecretPassword + label: Belgium + state: query + delegate_to: localhost + register: query_result + +- name: Query all labels + cisco.mso.mso_label: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + label=dict(type="str", aliases=["name"]), + type=dict(type="str", default="site", choices=["site"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["label"]], + ["state", "present", ["label"]], + ], + ) + + label = module.params.get("label") + label_type = module.params.get("type") + state = module.params.get("state") + + mso = MSOModule(module) + + label_id = None + path = "labels" + + # Query for existing object(s) + if label: + mso.existing = mso.get_obj(path, displayName=label) + if mso.existing: + label_id = mso.existing.get("id") + # If we found an existing object, continue with it + path = "labels/{id}".format(id=label_id) + else: + mso.existing = mso.query_objs(path) + + if state == "query": + pass + + elif state == "absent": + mso.previous = mso.existing + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(path, method="DELETE") + + elif state == "present": + mso.previous = mso.existing + + payload = dict( + id=label_id, + displayName=label, + type=label_type, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + if mso.check_changed(): + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="PUT", data=mso.sent) + else: + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py b/ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py new file mode 100644 index 000000000..10546563f --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Akini Ross (@akinross) <akinross@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_remote_location +short_description: Manages remote locations +description: +- Manage remote locations on Cisco ACI Multi-Site. +author: +- Akini Ross (@akinross) +options: + remote_location: + description: + - The remote location's name. + type: str + aliases: [ name ] + description: + description: + - The remote location's description. + type: str + remote_protocol: + description: + - The protocol used to export to the remote server. + - If the remote location is a Windows server, you must use the C(sftp) protocol. + choices: [ scp, sftp ] + type: str + remote_host: + description: + - The host name or IP address of the remote server. + type: str + remote_path: + description: + - The full path to a directory on the remote server where backups are saved. + - The path must start with a slash (/) character and must not contain periods (.) or backslashes (\). + - The directory must already exist on the server. + type: str + remote_port: + description: + - The port used to connect to the remote server. + default: 22 + type: int + authentication_type: + description: + - The authentication method used to connect to the remote server. + choices: [ password, ssh ] + type: str + remote_username: + description: + - The username used to log in to the remote server. + type: str + remote_password: + description: + - The password used to log in to the remote server. + type: str + remote_ssh_key: + description: + - The private ssh key used to log in to the remote server. + - The private ssh key must be provided in PEM format. + - The private ssh key must be a single line string with linebreaks represent as "\n". + type: str + remote_ssh_passphrase: + description: + - The private ssh key passphrase used to log in to the remote server. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Query all remote locations + cisco.mso.mso_remote_location: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: backups + +- name: Query a remote location + cisco.mso.mso_remote_location: + host: mso_host + username: admin + password: SomeSecretPassword + remote_location: ansible_test + state: query + delegate_to: localhost + +- name: Configure a remote location + cisco.mso.mso_remote_location: + host: mso_host + username: admin + password: SomeSecretPassword + remote_location: ansible_test + remote_protocol: scp + remote_host: 10.0.0.1 + remote_path: /username/backup + remote_authentication_type: password + remote_username: username + remote_password: password + state: present + delegate_to: localhost + +- name: Delete a remote location + cisco.mso.mso_remote_location: + host: mso_host + username: admin + password: SomeSecretPassword + remote_location: ansible_test + state: absent + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + remote_location=dict(type="str", aliases=["name"]), + description=dict(type="str"), + remote_protocol=dict(type="str", choices=["scp", "sftp"]), + remote_host=dict(type="str"), + remote_path=dict(type="str"), + remote_port=dict(type="int", default=22), + authentication_type=dict(type="str", choices=["password", "ssh"]), + remote_username=dict(type="str"), + remote_password=dict(type="str", no_log=True), + remote_ssh_key=dict(type="str", no_log=True), + remote_ssh_passphrase=dict(type="str", no_log=True), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["remote_location", "remote_protocol", "remote_host", "remote_path", "authentication_type"]], + ["state", "absent", ["remote_location"]], + ["authentication_type", "password", ["remote_username", "remote_password"]], + ["authentication_type", "ssh", ["remote_ssh_key"]], + ], + ) + + location_name = module.params.get("remote_location") + description = module.params.get("description") + protocol = module.params.get("remote_protocol") + host = module.params.get("remote_host") + path = module.params.get("remote_path") + port = module.params.get("remote_port") + authentication_type = module.params.get("authentication_type") + username = module.params.get("remote_username") + password = module.params.get("remote_password") + ssh_key = module.params.get("remote_ssh_key") + passphrase = module.params.get("remote_ssh_passphrase") + state = module.params.get("state") + + mso = MSOModule(module) + api_path = "platform/remote-locations" + mso.existing = mso.query_objs(api_path, key="remoteLocations") + + remote_location_obj = None + if location_name and mso.existing: + remote_location_obj = next((item for item in mso.existing if item.get("name") == location_name), None) + if remote_location_obj: + mso.existing = remote_location_obj + + if state == "query": + if location_name and not remote_location_obj: + existing_location_list = ", ".join([item.get("name") for item in mso.existing]) + mso.module.fail_json(msg="Remote location {0} not found. Remote locations configured: {1}".format(location_name, existing_location_list)) + + elif state == "absent": + mso.previous = mso.existing + + if module.check_mode: + mso.existing = {} + elif remote_location_obj: + mso.existing = mso.request("{0}/{1}".format(api_path, remote_location_obj.get("id")), method="DELETE") + + elif state == "present": + mso.previous = mso.existing + + credential = dict( + authType=authentication_type if authentication_type == "password" else "sshKey", + hostname=host, + port=port, + protocolType=protocol, + remotePath=path, + username=username, + ) + + if authentication_type == "password": + credential.update(password=password) + else: + credential.update(sshKey=ssh_key) + if passphrase: + credential.update(passPhrase=passphrase) + + payload = dict(name=location_name, credential=credential) + + if description: + payload.update(description=description) + + mso.proposed = payload + + if module.check_mode: + mso.existing = mso.proposed + else: + if remote_location_obj: + payload.update(id=remote_location_obj.get("id")) + mso.existing = mso.request("{0}/{1}".format(api_path, remote_location_obj.get("id")), method="PUT", data=payload) + else: + mso.existing = mso.request(api_path, method="POST", data=payload) + + mso.existing["credential"].pop("password", None) + mso.existing["credential"].pop("sshKey", None) + mso.existing["credential"].pop("passPhrase", None) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_rest.py b/ansible_collections/cisco/mso/plugins/modules/mso_rest.py new file mode 100644 index 000000000..ae05b093b --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_rest.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_rest +short_description: Direct access to the Cisco MSO REST API +description: +- Enables the management of the Cisco MSO fabric through direct access to the Cisco MSO REST API. +- This module is not idempotent and does not report changes. +options: + method: + description: + - The HTTP method of the request. + - Using C(delete) is typically used for deleting objects. + - Using C(get) is typically used for querying objects. + - Using C(post) is typically used for modifying objects. + - Using C(put) is typically used for modifying existing objects. + - Using C(patch) is typically also used for modifying existing objects. + type: str + choices: [ delete, get, post, put, patch ] + default: get + aliases: [ action ] + path: + description: + - URI being used to execute API calls. + type: str + required: true + aliases: [ uri ] + content: + description: + - Sets the payload of the API request directly. + - This may be convenient to template simple requests. + - For anything complex use the C(template) lookup plugin (see examples). + type: raw + aliases: [ payload ] +extends_documentation_fragment: +- cisco.mso.modules + +notes: +- Most payloads are known not to be idempotent, so be careful when constructing payloads. +seealso: +- module: cisco.mso.mso_tenant +author: +- Anvitha Jain (@anvitha-jain) +""" + +EXAMPLES = r""" +- name: Add schema (JSON) + cisco.mso.mso_rest: + host: mso + username: admin + password: SomeSecretPassword + path: /mso/api/v1/schemas + method: post + content: + { + "displayName": "{{ mso_schema | default('ansible_test') }}", + "templates": [{ + "name": "Template_1", + "tenantId": "{{ add_tenant.jsondata.id }}", + "displayName": "Template_1", + "templateSubType": [], + "templateType": "stretched-template", + "anps": [], + "contracts": [], + "vrfs": [], + "bds": [], + "filters": [], + "externalEpgs": [], + "serviceGraphs": [], + "intersiteL3outs": [] + }], + "sites": [], + "_updateVersion": 0 + } + delegate_to: localhost + +- name: Query schema + cisco.mso.mso_rest: + host: mso + username: admin + password: SomeSecretPassword + path: /mso/api/v1/schemas + method: get + delegate_to: localhost + +- name: Patch schema (YAML) + cisco.mso.mso_rest: + host: mso + username: admin + password: SomeSecretPassword + path: "/mso/api/v1/schemas/{{ add_schema.jsondata.id }}" + method: patch + content: + - op: add + path: /templates/Template_1/anps/- + value: + name: AP2 + displayName: AP2 + epgs: [] + _updateVersion: 0 + delegate_to: localhost + +- name: Add a tenant from a templated payload file from templates + cisco.mso.mso_rest: + host: mso + username: admin + password: SomeSecretPassword + method: post + path: /api/v1/tenants + content: "{{ lookup('template', 'mso/tenant.json.j2') }}" + delegate_to: localhost +""" + +RETURN = r""" +""" + +# Optional, only used for YAML validation +try: + import yaml + + HAS_YAML = True +except Exception: + HAS_YAML = False + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible.module_utils._text import to_text + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + path=dict(type="str", required=True, aliases=["uri"]), + method=dict(type="str", default="get", choices=["delete", "get", "post", "put", "patch"], aliases=["action"]), + content=dict(type="raw", aliases=["payload"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + ) + + content = module.params.get("content") + path = module.params.get("path") + + mso = MSOModule(module) + + # Validate content/payload + if content and isinstance(content, str) and HAS_YAML: + try: + # Validate YAML/JSON string + content = yaml.safe_load(content) + except Exception as e: + module.fail_json(msg="Failed to parse provided JSON/YAML payload: %s" % to_text(e), exception=to_text(e), payload=content) + + mso.method = mso.params.get("method").upper() + + # Perform request + if module.check_mode: + mso.result["jsondata"] = content + else: + mso.result["jsondata"] = mso.request(path, method=mso.method, data=content, api_version=None) + + mso.result["status"] = mso.status + + if mso.method != "GET": + mso.result["changed"] = True + if mso.method == "DELETE": + mso.result["jsondata"] = None + + # Report success + mso.exit_json(**mso.result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_role.py b/ansible_collections/cisco/mso/plugins/modules/mso_role.py new file mode 100644 index 000000000..cfa4483b0 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_role.py @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_role +short_description: Manage roles +description: +- Manage roles on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + role: + description: + - The name of the role. + type: str + aliases: [ name ] + display_name: + description: + - The name of the role to be displayed in the web UI. + type: str + description: + description: + - The description of the role. + type: str + read_permissions: + description: + - A list of read permissions tied to this role. + type: list + elements: str + choices: + - backup-db + - manage-audit-records + - manage-labels + - manage-roles + - manage-schemas + - manage-sites + - manage-tenants + - manage-tenant-schemas + - manage-users + - platform-logs + - view-all-audit-records + - view-labels + - view-roles + - view-schemas + - view-sites + - view-tenants + - view-tenant-schemas + - view-users + write_permissions: + description: + - A list of write permissions tied to this role. + type: list + elements: str + aliases: [ permissions ] + choices: + - backup-db + - manage-audit-records + - manage-labels + - manage-roles + - manage-schemas + - manage-sites + - manage-tenants + - manage-tenant-schemas + - manage-users + - platform-logs + - view-all-audit-records + - view-labels + - view-roles + - view-schemas + - view-sites + - view-tenants + - view-tenant-schemas + - view-users + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new role + cisco.mso.mso_role: + host: mso_host + username: admin + password: SomeSecretPassword + role: readOnly + display_name: Read Only + description: Read-only access for troubleshooting + read_permissions: + - view-roles + - view-schemas + - view-sites + - view-tenants + - view-tenant-schemas + - view-users + write_permissions: + - manage-roles + - manage-schemas + - manage-sites + - manage-tenants + - manage-tenant-schemas + - manage-users + state: present + delegate_to: localhost + +- name: Remove a role + cisco.mso.mso_role: + host: mso_host + username: admin + password: SomeSecretPassword + role: readOnly + state: absent + delegate_to: localhost + +- name: Query a role + cisco.mso.mso_role: + host: mso_host + username: admin + password: SomeSecretPassword + role: readOnly + state: query + delegate_to: localhost + register: query_result + +- name: Query all roles + cisco.mso.mso_role: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + role=dict(type="str", aliases=["name"]), + display_name=dict(type="str"), + description=dict(type="str"), + read_permissions=dict( + type="list", + elements="str", + choices=[ + "backup-db", + "manage-audit-records", + "manage-labels", + "manage-roles", + "manage-schemas", + "manage-sites", + "manage-tenants", + "manage-tenant-schemas", + "manage-users", + "platform-logs", + "view-all-audit-records", + "view-labels", + "view-roles", + "view-schemas", + "view-sites", + "view-tenants", + "view-tenant-schemas", + "view-users", + ], + ), + write_permissions=dict( + type="list", + elements="str", + aliases=["permissions"], + choices=[ + "backup-db", + "manage-audit-records", + "manage-labels", + "manage-roles", + "manage-schemas", + "manage-sites", + "manage-tenants", + "manage-tenant-schemas", + "manage-users", + "platform-logs", + "view-all-audit-records", + "view-labels", + "view-roles", + "view-schemas", + "view-sites", + "view-tenants", + "view-tenant-schemas", + "view-users", + ], + ), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["role"]], + ["state", "present", ["role"]], + ], + ) + + role = module.params.get("role") + description = module.params.get("description") + read_permissions = module.params.get("read_permissions") + write_permissions = module.params.get("write_permissions") + state = module.params.get("state") + + mso = MSOModule(module) + + role_id = None + path = "roles" + + # Query for existing object(s) + if role: + mso.existing = mso.get_obj(path, name=role) + if mso.existing: + role_id = mso.existing.get("id") + # If we found an existing object, continue with it + path = "roles/{id}".format(id=role_id) + else: + mso.existing = mso.query_objs(path) + + if state == "query": + pass + + elif state == "absent": + mso.previous = mso.existing + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(path, method="DELETE") + + elif state == "present": + mso.previous = mso.existing + + payload = dict( + id=role_id, + name=role, + displayName=role, + description=description, + readPermissions=read_permissions, + writePermissions=write_permissions, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + if mso.check_changed(): + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="PUT", data=mso.sent) + else: + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema.py new file mode 100644 index 000000000..2eba13ac9 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema.py @@ -0,0 +1,132 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema +short_description: Manage schemas +description: +- Manage schemas on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + aliases: [ name ] + state: + description: + - Use C(absent) for removing. + - Use C(query) for listing an object or multiple objects. + type: str + choices: [ absent, query ] + default: query +notes: +- Due to restrictions of the MSO REST API this module cannot create empty schemas (i.e. schemas without templates). + Use the M(cisco.mso.mso_schema_template) to automatically create schemas with templates. +seealso: +- module: cisco.mso.mso_schema_site +- module: cisco.mso.mso_schema_template +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Remove schemas + cisco.mso.mso_schema: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + state: absent + delegate_to: localhost + +- name: Query a schema + cisco.mso.mso_schema: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all schemas + cisco.mso.mso_schema: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", aliases=["name"]), + # messages=dict(type='dict'), + # associations=dict(type='list'), + # health_faults=dict(type='list'), + # references=dict(type='dict'), + # policy_states=dict(type='list'), + state=dict(type="str", default="query", choices=["absent", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["schema"]], + ], + ) + + schema = module.params.get("schema") + state = module.params.get("state") + + mso = MSOModule(module) + + schema_id = None + path = "schemas" + + # Query for existing object(s) + if schema: + mso.existing = mso.get_obj(path, displayName=schema) + if mso.existing: + schema_id = mso.existing.get("id") + path = "schemas/{id}".format(id=schema_id) + else: + mso.existing = mso.query_objs(path) + + if state == "query": + pass + + elif state == "absent": + mso.previous = mso.existing + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(path, method="DELETE") + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py new file mode 100644 index 000000000..840fb12ca --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py @@ -0,0 +1,125 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_clone +short_description: Clone schemas +description: +- Clone schemas on Cisco ACI Multi-Site. +- Clones only template objects and not site objects. +- This module can only be used on versions of MSO that are 3.3 or greater. +author: +- Anvitha Jain (@anvitha-jain) +options: + source_schema: + description: + - The name of the source_schema. + type: str + destination_schema: + description: + - The name of the destination_schema. + type: str + state: + description: + - Use C(clone) for adding. + type: str + choices: [ clone ] + default: clone +seealso: +- module: cisco.mso.mso_schema +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Clone schema + cisco.mso.mso_schema_clone: + host: mso_host + username: admin + password: SomeSecretPassword + source_schema: Source_Schema + destination_schema: Destination_Schema + state: clone + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible_collections.cisco.mso.plugins.module_utils.constants import NDO_4_UNIQUE_IDENTIFIERS +import json + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + source_schema=dict(type="str"), + destination_schema=dict(type="str"), + state=dict(type="str", default="clone", choices=["clone"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "clone", ["destination_schema"]], + ], + ) + + source_schema = module.params.get("source_schema") + destination_schema = module.params.get("destination_schema") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get source schema details + source_schema_path = "schemas/{0}".format(mso.lookup_schema(source_schema)) + source_schema_obj = mso.query_obj(source_schema_path, displayName=source_schema) + + source_data = source_schema_obj.get("templates") + source_data = json.loads(json.dumps(source_data).replace("/{0}".format(source_schema_path), "")) + # certain unique identifiers are present in NDO4.0> source which need to be deleted from source_data prior to POST + for template in source_data: + mso.delete_keys_from_dict(template, NDO_4_UNIQUE_IDENTIFIERS) + + path = "schemas" + + # Check if source and destination schema are named differently + if source_schema == destination_schema: + mso.fail_json(msg="Source and Destination schema cannot have same names.") + # Query for existing object(s) + if destination_schema: + mso.existing = mso.get_obj(path, displayName=destination_schema) + if mso.existing: + mso.fail_json(msg="Schema with the name '{0}' already exists. Please use another name.".format(destination_schema)) + + if state == "clone": + mso.previous = mso.existing + payload = dict( + displayName=destination_schema, + templates=source_data, + ) + mso.sanitize(payload, collate=True) + + if not mso.existing: + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py new file mode 100644 index 000000000..83a5213a2 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2020, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site +short_description: Manage sites in schemas +description: +- Manage sites on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +- Shreyas Srish (@shrsr) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site to manage. + type: str + template: + description: + - The name of the template. + type: str + aliases: [ name ] + 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 +seealso: +- module: cisco.mso.mso_schema_template +- module: cisco.mso.mso_site +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site to a schema + cisco.mso.mso_schema_site: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: Site1 + template: Template 1 + state: present + delegate_to: localhost + +- name: Remove a site from a schema + cisco.mso.mso_schema_site: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: Site1 + template: Template 1 + state: absent + delegate_to: localhost + +- name: Query a schema site + cisco.mso.mso_schema_site: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: Site1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all schema sites + cisco.mso.mso_schema_site: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", aliases=["name"]), + template=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", ["site", "template"]], + ["state", "present", ["site", "template"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template") + if template is not None: + template = template.replace(" ", "") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get site + site_id = mso.lookup_site(site) + + mso.existing = {} + if "sites" in schema_obj: + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if template: + if (site_id, template) in sites: + site_idx = sites.index((site_id, template)) + site_path = "/sites/{0}".format(site_idx) + mso.existing = schema_obj.get("sites")[site_idx] + else: + mso.existing = schema_obj.get("sites") + + if state == "query": + if not mso.existing: + if template: + mso.fail_json(msg="Template '{0}' not found".format(template)) + else: + mso.existing = [] + mso.exit_json() + + sites_path = "/sites" + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + # Remove existing site + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=site_path)) + + elif state == "present": + if not mso.existing: + # Add new site + payload = dict( + siteId=site_id, + templateName=template, + anps=[], + bds=[], + contracts=[], + externalEpgs=[], + intersiteL3outs=[], + serviceGraphs=[], + vrfs=[], + ) + + mso.sanitize(payload, collate=True) + + ops.append(dict(op="add", path=sites_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp.py new file mode 100644 index 000000000..0552b6a31 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp +short_description: Manage site-local Application Network Profiles (ANPs) in schema template +description: +- Manage site-local ANPs in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP to manage. + type: str + aliases: [ name ] + 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 +seealso: +- module: cisco.mso.mso_schema_site +- module: cisco.mso.mso_schema_site_anp_epg +- module: cisco.mso.mso_schema_template_anp +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site ANP + cisco.mso.mso_schema_site_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: present + delegate_to: localhost + +- name: Remove a site ANP + cisco.mso.mso_schema_site_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: absent + delegate_to: localhost + +- name: Query a specific site ANP + cisco.mso.mso_schema_site_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site ANPs + cisco.mso.mso_schema_site_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["anp"]], + ["state", "present", ["anp"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + sites_list = [s.get("siteId") + "/" + s.get("templateName") for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json( + msg="Provided site/siteId/template '{0}/{1}/{2}' does not exist. " + "Existing siteIds/templates: {3}".format(site, site_id, template, ", ".join(sites_list)) + ) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get ANP + anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp) + anps = [a.get("anpRef") for a in schema_obj.get("sites")[site_idx]["anps"]] + + if anp is not None and anp_ref in anps: + anp_idx = anps.index(anp_ref) + anp_path = "/sites/{0}/anps/{1}".format(site_template, anp) + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx] + + if state == "query": + if anp is None: + mso.existing = schema_obj.get("sites")[site_idx]["anps"] + elif not mso.existing: + mso.fail_json(msg="ANP '{anp}' not found".format(anp=anp)) + mso.exit_json() + + anps_path = "/sites/{0}/anps".format(site_template) + ops = [] + + # Workaround due to inconsistency in attributes REQUEST/RESPONSE API + # FIX for MSO Error 400: Bad Request: (0)(0)(0)(0)/deploymentImmediacy error.path.missing + mso.replace_keys_in_dict("deployImmediacy", "deploymentImmediacy") + if mso.existing.get("anpRef"): + anp_ref = mso.dict_from_ref(mso.existing.get("anpRef")) + mso.existing["anpRef"] = anp_ref + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=anp_path)) + + elif state == "present": + payload = dict( + anpRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + ), + ) + + if "epgs" in mso.existing: + for epg in mso.existing.get("epgs"): + epg = mso.recursive_dict_from_ref(epg) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=anp_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=anps_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg.py new file mode 100644 index 000000000..1c2ad6433 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp_epg +short_description: Manage site-local Endpoint Groups (EPGs) in schema template +description: +- Manage site-local EPGs in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG to manage. + type: str + aliases: [ name ] + private_link_label: + description: + - The private link label used to represent this subnet. + - This parameter is available for MSO version greater than 3.3. + 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 +seealso: +- module: cisco.mso.mso_schema_site_anp +- module: cisco.mso.mso_schema_site_anp_epg_subnet +- module: cisco.mso.mso_schema_template_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site EPG + cisco.mso.mso_schema_site_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + state: present + delegate_to: localhost + +- name: Remove a site EPG + cisco.mso.mso_schema_site_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + state: absent + delegate_to: localhost + +- name: Query a specific site EPGs + cisco.mso.mso_schema_site_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site EPGs + cisco.mso.mso_schema_site_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + private_link_label=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", ["epg"]], + ["state", "present", ["epg"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + private_link_label = module.params.get("private_link_label") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + sites_list = [s.get("siteId") + "/" + s.get("templateName") for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json( + msg="Provided site/siteId/template '{0}/{1}/{2}' does not exist. " + "Existing siteIds/templates: {3}".format(site, site_id, template, ", ".join(sites_list)) + ) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + payload = {} + ops = [] + op_path = "" + + # Get ANP + anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp) + anps = [a.get("anpRef") for a in schema_obj["sites"][site_idx]["anps"]] + anps_in_temp = [a.get("name") for a in schema_obj["templates"][template_idx]["anps"]] + if anp not in anps_in_temp: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps_in_temp))) + else: + # Get anp index at template level + template_anp_idx = anps_in_temp.index(anp) + + # If anp not at site level but exists at template level + if anp_ref not in anps: + op_path = "/sites/{0}/anps".format(site_template) + payload = dict( + anpRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + ), + ) + else: + # Get anp index at site level + anp_idx = anps.index(anp_ref) + + if epg is not None: + # Get EPG + epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg) + new_epg = dict( + epgRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + epgName=epg, + ) + ) + + # If anp exists at site level + if "anpRef" not in payload: + epgs = [e.get("epgRef") for e in schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"]] + + # If anp already at site level AND if epg not at site level (or) anp not at site level? + if ("anpRef" not in payload and epg_ref not in epgs) or "anpRef" in payload: + epgs_in_temp = [e.get("name") for e in schema_obj["templates"][template_idx]["anps"][template_anp_idx]["epgs"]] + + # If EPG not at template level - Fail + if epg not in epgs_in_temp: + mso.fail_json(msg="Provided EPG '{0}' does not exist. Existing EPGs: {1}".format(epg, ", ".join(epgs_in_temp))) + + # EPG at template level but not at site level. Create payload at site level for EPG + else: + # If anp not in payload then, anp already exists at site level. New payload will only have new EPG payload + if "anpRef" not in payload: + op_path = "/sites/{0}/anps/{1}/epgs".format(site_template, anp) + payload = new_epg + else: + # If anp in payload, anp exists at site level. Update payload with EPG payload + payload["epgs"] = [new_epg] + + # Get index of EPG at site level + else: + epg_idx = epgs.index(epg_ref) + epg_path = "/sites/{0}/anps/{1}/epgs/{2}".format(site_template, anp, epg) + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx] + payload = new_epg + + ops = [] + + if state == "query": + if anp_ref not in anps: + mso.fail_json(msg="Provided anp '{0}' does not exist at site level.".format(anp)) + if epg is None: + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"] + elif not mso.existing: + mso.fail_json(msg="EPG '{epg}' not found".format(epg=epg)) + mso.exit_json() + + # Workaround due to inconsistency in attributes REQUEST/RESPONSE API + # FIX for MSO Error 400: Bad Request: (0)(0)(0)(0)/deploymentImmediacy error.path.missing + mso.replace_keys_in_dict("deployImmediacy", "deploymentImmediacy") + if mso.existing.get("epgRef"): + epg_ref = mso.dict_from_ref(mso.existing.get("epgRef")) + mso.existing["epgRef"] = epg_ref + + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=epg_path)) + + elif state == "present": + if private_link_label is not None: + payload["privateLinkLabel"] = dict(name=private_link_label) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=epg_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_bulk_staticport.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_bulk_staticport.py new file mode 100644 index 000000000..eda60cfd1 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_bulk_staticport.py @@ -0,0 +1,464 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp_epg_bulk_staticport +short_description: Manage site-local EPG static ports in bulk in schema template +description: +- Manage site-local EPG static ports in bulk in schema template on Cisco ACI Multi-Site. +author: +- Anvitha Jain (@anvjain) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG. + type: str + required: true + type: + description: + - The path type of the static port + - vpc is used for a Virtual Port Channel + - dpc is used for a Direct Port Channel + - port is used for a single interface + type: str + choices: [ port, vpc, dpc ] + default: port + pod: + description: + - The pod of the static port. + type: str + leaf: + description: + - The leaf of the static port. + type: str + fex: + description: + - The fex id of the static port. + type: str + path: + description: + - The path of the static port. + type: str + vlan: + description: + - The port encap VLAN id of the static port. + type: int + deployment_immediacy: + description: + - The deployment immediacy of the static port. + - C(immediate) means B(Deploy immediate). + - C(lazy) means B(deploy on demand). + type: str + choices: [ immediate, lazy ] + default: lazy + mode: + description: + - The mode of the static port. + - C(native) means B(Access (802.1p)). + - C(regular) means B(Trunk). + - C(untagged) means B(Access (untagged)). + type: str + choices: [ native, regular, untagged ] + default: untagged + primary_micro_segment_vlan: + description: + - Primary micro-seg VLAN of static port. + type: int + static_ports: + description: + - List of static port configurations and elements in the form of a dictionary. + - Module level attributes will be overridden by the path level attributes. + - Making changes to an item in the list will update the whole payload. + type: list + elements: dict + suboptions: + type: + description: + - The path type of the static port + - vpc is used for a Virtual Port Channel + - dpc is used for a Direct Port Channel + - port is used for a single interface + type: str + choices: [ port, vpc, dpc ] + pod: + description: + - The pod of the static port. + type: str + leaf: + description: + - The leaf of the static port. + type: str + fex: + description: + - The fex id of the static port. + type: str + path: + description: + - The path of the static port. + - Path has to be unique for each static port in a particular leaf. + type: str + vlan: + description: + - The port encap VLAN id of the static port. + type: int + deployment_immediacy: + description: + - The deployment immediacy of the static port. + - C(immediate) means B(Deploy immediate). + - C(lazy) means B(deploy on demand). + type: str + choices: [ immediate, lazy ] + mode: + description: + - The mode of the static port. + - C(native) means B(Access (802.1p)). + - C(regular) means B(Trunk). + - C(untagged) means B(Access (untagged)). + type: str + choices: [ native, regular, untagged ] + primary_micro_segment_vlan: + description: + - Primary micro-seg VLAN of the static port. + type: int + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing an object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_anp_epg +- module: cisco.mso.mso_schema_template_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new static port to a site EPG + cisco.mso.mso_schema_site_anp_epg_bulk_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + type: port + pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + deployment_immediacy: immediate + static_ports: + - path: eth1/2 + leaf: 102 + - path: eth1/3 + vlan: 124 + state: present + delegate_to: localhost + +- name: Add a new static fex port to a site EPG + cisco.mso.mso_schema_site_anp_epg_bulk_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + type: port + pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + deployment_immediacy: lazy + static_ports: + - path: eth1/2 + leaf: 102 + - path: eth1/3 + vlan: 124 + - fex: 151 + state: present + delegate_to: localhost + +- name: Add a new static VPC to a site EPG + cisco.mso.mso_schema_site_anp_epg_bulk_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + type: port + pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + static_ports: + - path: eth1/2 + leaf: 102 + - path: eth1/3 + vlan: 124 + - fex: 151 + - leaf: 101-102 + path: ansible_polgrp + vlan: 127 + type: vpc + mode: untagged + deployment_immediacy: lazy + state: present + delegate_to: localhost + +- name: Remove static ports from a site EPG + cisco.mso.mso_schema_site_anp_epg_bulk_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + state: absent + delegate_to: localhost + +- name: Query all site EPG static ports + cisco.mso.mso_schema_site_anp_epg_bulk_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_site_anp_epg_bulk_staticport_spec +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + type=dict(type="str", default="port", choices=["port", "vpc", "dpc"]), + pod=dict(type="str"), # This parameter is not required for querying all objects + leaf=dict(type="str"), # This parameter is not required for querying all objects + fex=dict(type="str"), # This parameter is not required for querying all objects + path=dict(type="str"), # This parameter is not required for querying all objects + vlan=dict(type="int"), # This parameter is not required for querying all objects + primary_micro_segment_vlan=dict(type="int"), # This parameter is not required for querying all objects + deployment_immediacy=dict(type="str", default="lazy", choices=["immediate", "lazy"]), + mode=dict(type="str", default="untagged", choices=["native", "regular", "untagged"]), + static_ports=dict(type="list", elements="dict", options=mso_site_anp_epg_bulk_staticport_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["static_ports"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + module_path_type = module.params.get("type") + module_pod = module.params.get("pod") + module_leaf = module.params.get("leaf") + module_fex = module.params.get("fex") + module_path = module.params.get("path") + module_vlan = module.params.get("vlan") + module_primary_micro_segment_vlan = module.params.get("primary_micro_segment_vlan") + module_deployment_immediacy = module.params.get("deployment_immediacy") + module_mode = module.params.get("mode") + static_ports = module.params.get("static_ports") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + mso_schema = MSOSchema(mso, schema, template, site) + mso_objects = mso_schema.schema_objects + + # Verifies ANP and EPG exists at template level + mso_schema.set_template_anp(anp) + mso_schema.set_template_anp_epg(epg) + + # Verifies if ANP exists at site level + mso_schema.set_site_anp(anp, fail_module=False) + + payload = dict() + ops = [] + op_path = "/sites/{0}-{1}/anps".format(mso_objects.get("site").details.get("siteId"), template) + mso.existing = [] + + # If anp not at site level but exists at template level + if not mso_objects.get("site_anp"): + op_path = op_path + "/-" + payload.update( + anpRef=dict( + schemaId=mso_schema.id, + templateName=template, + anpName=anp, + ), + ) + else: + mso_schema.set_site_anp_epg(epg, fail_module=False) + + # If epg not at site level (or) anp not at site level payload + if not mso_objects.get("site_anp_epg") or "anpRef" in payload: + # EPG at template level but not at site level. Create payload at site level for EPG + new_epg = dict( + epgRef=dict( + schemaId=mso_schema.id, + templateName=template, + anpName=anp, + epgName=epg, + ) + ) + + # If anp not in payload then, anp already exists at site level. New payload will only have new EPG payload + if "anpRef" not in payload: + op_path = "{0}/{1}/epgs/-".format(op_path, anp) + payload = new_epg + else: + # If anp in payload, anp exists at site level. Update payload with EPG payload + payload["epgs"] = [new_epg] + else: + # If anp and epg exists at site level + op_path = "{0}/{1}/epgs/{2}/staticPorts".format(op_path, anp, epg) + mso.existing = mso_objects.get("site_anp_epg").details.get("staticPorts", []) + + staticport_list = [] + unique_paths = [] + + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + mso.sent = mso.existing = [] + ops.append(dict(op="remove", path=op_path)) + + elif state == "present": + for static_port in static_ports: + path_type = static_port.get("type") or module_path_type + pod = static_port.get("pod") or module_pod + leaf = static_port.get("leaf") or module_leaf + fex = static_port.get("fex") or module_fex + path = static_port.get("path") or module_path # Note :path has to be diffent in each leaf for every static port in the list. + vlan = static_port.get("vlan") or module_vlan + primary_micro_segment_vlan = static_port.get("primary_micro_segment_vlan") or module_primary_micro_segment_vlan + deployment_immediacy = static_port.get("deployment_immediacy") or module_deployment_immediacy + mode = static_port.get("mode") or module_mode + + required_dict = {"pod": pod, "leaf": leaf, "path": path, "vlan": vlan} + if None in required_dict.values(): + res = [key for key in required_dict.keys() if required_dict[key] is None] + mso.fail_json(msg="state is present but all of the following are missing: {0}.".format(", ".join(res))) + else: + if path_type == "port" and fex is not None: + # Select port path for fex if fex param is used + portpath = "topology/{0}/paths-{1}/extpaths-{2}/pathep-[{3}]".format(pod, leaf, fex, path) + elif path_type == "vpc": + portpath = "topology/{0}/protpaths-{1}/pathep-[{2}]".format(pod, leaf, path) + else: + portpath = "topology/{0}/paths-{1}/pathep-[{2}]".format(pod, leaf, path) + + new_leaf = dict( + deploymentImmediacy=deployment_immediacy, + mode=mode, + path=portpath, + portEncapVlan=vlan, + type=path_type, + ) + + if primary_micro_segment_vlan: + new_leaf.update(microSegVlan=primary_micro_segment_vlan) + + # validate and append staticports to staticport_list if path variable is different + if portpath in unique_paths: + mso.fail_json(msg="Each leaf in a pod of a static port should have an unique path.") + else: + unique_paths.append(portpath) + staticport_list.append(new_leaf) + + # If payload is empty, anp and EPG already exist at site level + if not payload: + payload = staticport_list + elif "anpRef" not in payload: # If anp already exists at site level + payload["staticPorts"] = staticport_list + else: + payload["epgs"][0]["staticPorts"] = staticport_list + + mso.proposed = staticport_list + mso.sent = payload + + if mso.existing: + ops.append(dict(op="replace", path=op_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(mso_schema.path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_domain.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_domain.py new file mode 100644 index 000000000..c684a27be --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_domain.py @@ -0,0 +1,476 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Nirav Katarmal (@nkatarmal-crest) <nirav.katarmal@crestdatasys.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp_epg_domain +short_description: Manage site-local EPG domains in schema template +description: +- Manage site-local EPG domains in schema template on Cisco ACI Multi-Site. +author: +- Nirav Katarmal (@nkatarmal-crest) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG. + type: str + required: true + domain_association_type: + description: + - The type of domain to associate. + type: str + choices: [ vmmDomain, l3ExtDomain, l2ExtDomain, physicalDomain, fibreChannelDomain ] + domain_profile: + description: + - The domain profile name. + type: str + deployment_immediacy: + description: + - The deployment immediacy of the domain. + - C(immediate) means B(Deploy immediate). + - C(lazy) means B(deploy on demand). + type: str + choices: [ immediate, lazy ] + resolution_immediacy: + description: + - Determines when the policies should be resolved and available. + - Defaults to C(lazy) when unset during creation. + type: str + choices: [ immediate, lazy, pre-provision ] + micro_seg_vlan_type: + description: + - Virtual LAN type for microsegmentation. This attribute can only be used with vmmDomain domain association. + - vlan is currently the only accepted value. + type: str + micro_seg_vlan: + description: + - Virtual LAN for microsegmentation. This attribute can only be used with vmmDomain domain association. + type: int + port_encap_vlan_type: + description: + - Virtual LAN type for port encap. This attribute can only be used with vmmDomain domain association. + - vlan is currently the only accepted value. + type: str + port_encap_vlan: + description: + - Virtual LAN type for port encap. This attribute can only be used with vmmDomain domain association. + type: int + vlan_encap_mode: + description: + - Which VLAN enacap mode to use. This attribute can only be used with vmmDomain domain association. + type: str + choices: [ static, dynamic ] + allow_micro_segmentation: + description: + - Specifies microsegmentation is enabled or not. This attribute can only be used with vmmDomain domain association. + type: bool + switch_type: + description: + - Which switch type to use with this domain association. This attribute can only be used with vmmDomain domain association. + type: str + switching_mode: + description: + - Which switching mode to use with this domain association. This attribute can only be used with vmmDomain domain association. + type: str + enhanced_lagpolicy_name: + description: + - EPG enhanced lagpolicy name. This attribute can only be used with vmmDomain domain association. + type: str + enhanced_lagpolicy_dn: + description: + - Distinguished name of EPG lagpolicy. This attribute can only be used with vmmDomain domain association. + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_anp_epg +- module: cisco.mso.mso_schema_template_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new domain to a site EPG + cisco.mso.mso_schema_site_anp_epg_domain: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + domain_association_type: vmmDomain + domain_profile: 'VMware-VMM' + deployment_immediacy: lazy + resolution_immediacy: pre-provision + state: present + delegate_to: localhost + +- name: Remove a domain from a site EPG + cisco.mso.mso_schema_site_anp_epg_domain: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + domain_association_type: vmmDomain + domain_profile: 'VMware-VMM' + deployment_immediacy: lazy + resolution_immediacy: pre-provision + state: absent + delegate_to: localhost + +- name: Query a domain associated with a specific site EPG + cisco.mso.mso_schema_site_anp_epg_domain: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + domain_association_type: vmmDomain + domain_profile: 'VMware-VMM' + state: query + delegate_to: localhost + register: query_result + +- name: Query all domains associated with a site EPG + cisco.mso.mso_schema_site_anp_epg_domain: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + domain_association_type=dict(type="str", choices=["vmmDomain", "l3ExtDomain", "l2ExtDomain", "physicalDomain", "fibreChannelDomain"]), + domain_profile=dict(type="str"), + deployment_immediacy=dict(type="str", choices=["immediate", "lazy"]), + resolution_immediacy=dict(type="str", choices=["immediate", "lazy", "pre-provision"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + micro_seg_vlan_type=dict(type="str"), + micro_seg_vlan=dict(type="int"), + port_encap_vlan_type=dict(type="str"), + port_encap_vlan=dict(type="int"), + vlan_encap_mode=dict(type="str", choices=["static", "dynamic"]), + allow_micro_segmentation=dict(type="bool"), + switch_type=dict(type="str"), + switching_mode=dict(type="str"), + enhanced_lagpolicy_name=dict(type="str"), + enhanced_lagpolicy_dn=dict(type="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["domain_association_type", "domain_profile", "deployment_immediacy", "resolution_immediacy"]], + ["state", "present", ["domain_association_type", "domain_profile", "deployment_immediacy", "resolution_immediacy"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + domain_association_type = module.params.get("domain_association_type") + domain_profile = module.params.get("domain_profile") + deployment_immediacy = module.params.get("deployment_immediacy") + resolution_immediacy = module.params.get("resolution_immediacy") + state = module.params.get("state") + micro_seg_vlan_type = module.params.get("micro_seg_vlan_type") + micro_seg_vlan = module.params.get("micro_seg_vlan") + port_encap_vlan_type = module.params.get("port_encap_vlan_type") + port_encap_vlan = module.params.get("port_encap_vlan") + vlan_encap_mode = module.params.get("vlan_encap_mode") + allow_micro_segmentation = module.params.get("allow_micro_segmentation") + switch_type = module.params.get("switch_type") + switching_mode = module.params.get("switching_mode") + enhanced_lagpolicy_name = module.params.get("enhanced_lagpolicy_name") + enhanced_lagpolicy_dn = module.params.get("enhanced_lagpolicy_dn") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + sites_list = [s.get("siteId") + "/" + s.get("templateName") for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json( + msg="Provided site/siteId/template '{0}/{1}/{2}' does not exist. " + "Existing siteIds/templates: {3}".format(site, site_id, template, ", ".join(sites_list)) + ) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + payload = dict() + ops = [] + op_path = "" + + # Get ANP + anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp) + anps = [a.get("anpRef") for a in schema_obj["sites"][site_idx]["anps"]] + anps_in_temp = [a.get("name") for a in schema_obj["templates"][template_idx]["anps"]] + if anp not in anps_in_temp: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps))) + else: + # Update anp index at template level + template_anp_idx = anps_in_temp.index(anp) + + # If anp not at site level but exists at template level + if anp_ref not in anps: + op_path = "/sites/{0}/anps/-".format(site_template) + payload.update( + anpRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + ), + ) + + else: + # Update anp index at site level + anp_idx = anps.index(anp_ref) + + # Get EPG + epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg) + + # If anp exists at site level + if "anpRef" not in payload: + epgs = [e.get("epgRef") for e in schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"]] + + # If anp already at site level AND if epg not at site level (or) anp not at site level? + if ("anpRef" not in payload and epg_ref not in epgs) or "anpRef" in payload: + epgs_in_temp = [e.get("name") for e in schema_obj["templates"][template_idx]["anps"][template_anp_idx]["epgs"]] + + # If EPG not at template level - Fail + if epg not in epgs_in_temp: + mso.fail_json(msg="Provided EPG '{0}' does not exist. Existing EPGs: {1} epgref {2}".format(epg, ", ".join(epgs_in_temp), epg_ref)) + + # EPG at template level but not at site level. Create payload at site level for EPG + else: + new_epg = dict( + epgRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + epgName=epg, + ) + ) + + # If anp not in payload then, anp already exists at site level. New payload will only have new EPG payload + if "anpRef" not in payload: + op_path = "/sites/{0}/anps/{1}/epgs/-".format(site_template, anp) + payload = new_epg + else: + # If anp in payload, anp exists at site level. Update payload with EPG payload + payload["epgs"] = [new_epg] + + # Update index of EPG at site level + else: + epg_idx = epgs.index(epg_ref) + + if domain_association_type == "vmmDomain": + domain_dn = "uni/vmmp-VMware/dom-{0}".format(domain_profile) + elif domain_association_type == "l3ExtDomain": + domain_dn = "uni/l3dom-{0}".format(domain_profile) + elif domain_association_type == "l2ExtDomain": + domain_dn = "uni/l2dom-{0}".format(domain_profile) + elif domain_association_type == "physicalDomain": + domain_dn = "uni/phys-{0}".format(domain_profile) + elif domain_association_type == "fibreChannelDomain": + domain_dn = "uni/fc-{0}".format(domain_profile) + else: + domain_dn = "" + + # Get Domains + # If anp at site level and epg is at site level + if "anpRef" not in payload and "epgRef" not in payload: + domains = [dom.get("dn") for dom in schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"][epg_idx]["domainAssociations"]] + if domain_dn in domains: + domain_idx = domains.index(domain_dn) + domain_path = "/sites/{0}/anps/{1}/epgs/{2}/domainAssociations/{3}".format(site_template, anp, epg, domain_idx) + mso.existing = schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"][epg_idx]["domainAssociations"][domain_idx] + + if state == "query": + if domain_association_type is None or domain_profile is None: + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["domainAssociations"] + elif not mso.existing: + mso.fail_json( + msg="Domain association '{domain_association_type}/{domain_profile}' not found".format( + domain_association_type=domain_association_type, domain_profile=domain_profile + ) + ) + mso.exit_json() + + domains_path = "/sites/{0}/anps/{1}/epgs/{2}/domainAssociations".format(site_template, anp, epg) + ops = [] + new_domain = dict( + dn=domain_dn, + domainType=domain_association_type, + deploymentImmediacy=deployment_immediacy, # keeping for backworths compatibility + deployImmediacy=deployment_immediacy, # rename of deploymentImmediacy + resolutionImmediacy=resolution_immediacy, + ) + + if domain_association_type == "vmmDomain": + vmmDomainProperties = {} + if micro_seg_vlan_type and micro_seg_vlan: + microSegVlan = dict(vlanType=micro_seg_vlan_type, vlan=micro_seg_vlan) + vmmDomainProperties["microSegVlan"] = microSegVlan + elif not micro_seg_vlan_type and micro_seg_vlan: + mso.fail_json(msg="micro_seg_vlan_type is required when micro_seg_vlan is provided.") + elif micro_seg_vlan_type and not micro_seg_vlan: + mso.fail_json(msg="micro_seg_vlan is required when micro_seg_vlan_type is provided.") + + if port_encap_vlan_type and port_encap_vlan: + portEncapVlan = dict(vlanType=port_encap_vlan_type, vlan=port_encap_vlan) + vmmDomainProperties["portEncapVlan"] = portEncapVlan + elif not port_encap_vlan_type and port_encap_vlan: + mso.fail_json(msg="port_encap_vlan_type is required when port_encap_vlan is provided.") + elif port_encap_vlan_type and not port_encap_vlan: + mso.fail_json(msg="port_encap_vlan is required when port_encap_vlan_type is provided.") + + if vlan_encap_mode: + vmmDomainProperties["vlanEncapMode"] = vlan_encap_mode + + if allow_micro_segmentation: + vmmDomainProperties["allowMicroSegmentation"] = allow_micro_segmentation + if switch_type: + vmmDomainProperties["switchType"] = switch_type + if switching_mode: + vmmDomainProperties["switchingMode"] = switching_mode + + if enhanced_lagpolicy_name and enhanced_lagpolicy_dn: + enhancedLagPol = dict(name=enhanced_lagpolicy_name, dn=enhanced_lagpolicy_dn) + epgLagPol = dict(enhancedLagPol=enhancedLagPol) + vmmDomainProperties["epgLagPol"] = epgLagPol + elif not enhanced_lagpolicy_name and enhanced_lagpolicy_dn: + mso.fail_json(msg="enhanced_lagpolicy_name is required when enhanced_lagpolicy_dn is provided.") + elif enhanced_lagpolicy_name and not enhanced_lagpolicy_dn: + mso.fail_json(msg="enhanced_lagpolicy_dn is required when enhanced_lagpolicy_name is provided.") + + if vmmDomainProperties: + new_domain["vmmDomainProperties"] = vmmDomainProperties + properties = ["allowMicroSegmentation", "epgLagPol", "switchType", "switchingMode", "vlanEncapMode", "portEncapVlan", "microSegVlan"] + for property in properties: + if property in vmmDomainProperties: + new_domain[property] = vmmDomainProperties[property] + + # If payload is empty, anp and EPG already exist at site level + if not payload: + op_path = domains_path + "/-" + payload = new_domain + + # If payload exists + else: + # If anp already exists at site level...(AND payload != epg as well?) + if "anpRef" not in payload: + payload["domainAssociations"] = [new_domain] + else: + payload["epgs"][0]["domainAssociations"] = [new_domain] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=domain_path)) + elif state == "present": + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=domain_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path, value=mso.sent)) + + mso.existing = new_domain + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_selector.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_selector.py new file mode 100644 index 000000000..ffd3c682e --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_selector.py @@ -0,0 +1,392 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Cindy Zhao (@cizhao) <cizhao@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp_epg_selector +short_description: Manage site-local EPG selector in schema templates +description: +- Manage EPG selector in schema template on Cisco ACI Multi-Site. +author: +- Cindy Zhao (@cizhao) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG to manage. + type: str + required: true + selector: + description: + - The name of the selector. + type: str + expressions: + description: + - Expressions associated to this selector. + type: list + elements: dict + suboptions: + type: + description: + - The type of the expression. + - The type is custom or is one of region, zone and ip_address + - The type can be zone only when the site is AWS. + required: true + type: str + aliases: [ tag ] + operator: + description: + - The operator associated to the expression. + - Operator has_key or does_not_have_key is only available for custom type / tag + required: true + type: str + choices: [ not_in, in, equals, not_equals, has_key, does_not_have_key ] + value: + description: + - The value associated to the expression. + - If the operator is in or not_in, the value should be a comma separated string. + 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 +seealso: +- module: cisco.mso.mso_schema_site_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a selector to a site EPG + cisco.mso.mso_schema_site_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: Site 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + selector: selector_1 + expressions: + - type: expression_1 + operator: in + value: test + state: present + delegate_to: localhost + +- name: Remove a Selector from a site EPG + cisco.mso.mso_schema_site_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: Site 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + selector: selector_1 + state: absent + delegate_to: localhost + +- name: Query a specific Selector + cisco.mso.mso_schema_site_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: Site 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + selector: selector_1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all Selectors + cisco.mso.mso_schema_site_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: Site 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_expression_spec + +EXPRESSION_KEYS = { + "ip_address": "ipAddress", + "region": "region", + "zone": "zone", +} + +EXPRESSION_OPERATORS = { + "not_in": "notIn", + "not_equals": "notEquals", + "has_key": "keyExist", + "does_not_have_key": "keyNotExist", + "in": "in", + "equals": "equals", +} + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + selector=dict(type="str"), + expressions=dict(type="list", elements="dict", options=mso_expression_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["selector"]], + ["state", "present", ["selector"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + selector = module.params.get("selector") + expressions = module.params.get("expressions") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get site + site_id = mso.lookup_site(site) + + # Get cloud type + site_type = mso.get_obj("sites", name=site).get("cloudProviders")[0] + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site-template association '{0}-{1}' does not exist.".format(site, template)) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + payload = dict() + ops = [] + op_path = "" + + # Get ANP + anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp) + anps = [a.get("anpRef") for a in schema_obj["sites"][site_idx]["anps"]] + anps_in_temp = [a.get("name") for a in schema_obj["templates"][template_idx]["anps"]] + if anp not in anps_in_temp: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps_in_temp))) + else: + # Get anp index at template level + template_anp_idx = anps_in_temp.index(anp) + + # If anp not at site level but exists at template level + if anp_ref not in anps: + op_path = "/sites/{0}/anps/-".format(site_template) + payload.update( + anpRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + ), + ) + + else: + # Get anp index at site level + anp_idx = anps.index(anp_ref) + + # Get EPG + epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg) + + # If anp exists at site level + if "anpRef" not in payload: + epgs = [e.get("epgRef") for e in schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"]] + + # If anp already at site level AND if epg not at site level (or) anp not at site level? + if ("anpRef" not in payload and epg_ref not in epgs) or "anpRef" in payload: + epgs_in_temp = [e.get("name") for e in schema_obj["templates"][template_idx]["anps"][template_anp_idx]["epgs"]] + + # If EPG not at template level - Fail + if epg not in epgs_in_temp: + mso.fail_json(msg="Provided EPG '{0}' does not exist. Existing EPGs: {1}".format(epg, ", ".join(epgs_in_temp))) + + # EPG at template level but not at site level. Create payload at site level for EPG + else: + new_epg = dict( + epgRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + epgName=epg, + ) + ) + + # If anp not in payload then, anp already exists at site level. New payload will only have new EPG payload + if "anpRef" not in payload: + op_path = "/sites/{0}/anps/{1}/epgs/-".format(site_template, anp) + payload = new_epg + else: + # If anp in payload, anp exists at site level. Update payload with EPG payload + payload["epgs"] = [new_epg] + + # Get index of EPG at site level + else: + epg_idx = epgs.index(epg_ref) + + # Get selectors + # If anp at site level and epg is at site level + if "anpRef" not in payload and "epgRef" not in payload: + if selector and " " in selector: + mso.fail_json(msg="There should not be any space in selector name.") + selectors = [s.get("name") for s in schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["selectors"]] + if selector in selectors: + selector_idx = selectors.index(selector) + selector_path = "/sites/{0}/anps/{1}/epgs/{2}/selectors/{3}".format(site_template, anp, epg, selector_idx) + mso.existing = schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"][epg_idx]["selectors"][selector_idx] + + if state == "query": + if "anpRef" in payload: + mso.fail_json(msg="Anp '{anp}' does not exist in site level.".format(anp=anp)) + if "epgRef" in payload: + mso.fail_json(msg="Epg '{epg}' does not exist in site level.".format(epg=epg)) + if selector is None: + mso.existing = schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"][epg_idx]["selectors"] + elif not mso.existing: + mso.fail_json(msg="Selector '{selector}' not found".format(selector=selector)) + mso.exit_json() + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=selector_path)) + elif state == "present": + # Get expressions + all_expressions = [] + if expressions: + for expression in expressions: + type = expression.get("type") + operator = expression.get("operator") + value = expression.get("value") + if " " in type: + mso.fail_json(msg="There should not be any space in 'type' attribute of expression '{0}'".format(type)) + if operator in ["has_key", "does_not_have_key"] and value: + mso.fail_json(msg="Attribute 'value' is not supported for operator '{0}' in expression '{1}'".format(operator, type)) + if operator in ["not_in", "in", "equals", "not_equals"] and not value: + mso.fail_json(msg="Attribute 'value' needed for operator '{0}' in expression '{1}'".format(operator, type)) + if type in ["region", "zone", "ip_address"]: + if type == "zone" and site_type != "aws": + mso.fail_json(msg="Type 'zone' is only supported for aws") + if operator in ["has_key", "does_not_have_key"]: + mso.fail_json(msg="Operator '{0}' is not supported when expression type is '{1}'".format(operator, type)) + type = EXPRESSION_KEYS.get(type) + else: + type = "Custom:" + type + all_expressions.append( + dict( + key=type, + operator=EXPRESSION_OPERATORS.get(operator), + value=value, + ) + ) + new_selector = dict( + name=selector, + expressions=all_expressions, + ) + + selectors_path = "/sites/{0}/anps/{1}/epgs/{2}/selectors/-".format(site_template, anp, epg) + + # if payload is empty, anp and epg already exist at site level + if not payload: + op_path = selectors_path + payload = new_selector + # if payload exist + else: + # if anp already exists at site level + if "anpRef" not in payload: + payload["selectors"] = [new_selector] + else: + payload["epgs"][0]["selectors"] = [new_selector] + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=selector_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path, value=mso.sent)) + + mso.existing = new_selector + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticleaf.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticleaf.py new file mode 100644 index 000000000..01e2ac386 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticleaf.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp_epg_staticleaf +short_description: Manage site-local EPG static leafs in schema template +description: +- Manage site-local EPG static leafs in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG. + type: str + required: true + pod: + description: + - The pod of the static leaf. + type: str + leaf: + description: + - The path of the static leaf. + type: str + aliases: [ name ] + vlan: + description: + - The VLAN id of the static leaf. + type: int + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_anp_epg +- module: cisco.mso.mso_schema_template_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new static leaf to a site EPG + cisco.mso.mso_schema_site_anp_epg_staticleaf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + leaf: Leaf1 + vlan: 123 + state: present + delegate_to: localhost + +- name: Remove a static leaf from a site EPG + cisco.mso.mso_schema_site_anp_epg_staticleaf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + leaf: Leaf1 + state: absent + delegate_to: localhost + +- name: Query a specific site EPG static leaf + cisco.mso.mso_schema_site_anp_epg_staticleaf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + leaf: Leaf1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site EPG static leafs + cisco.mso.mso_schema_site_anp_epg_staticleaf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + pod=dict(type="str"), # This parameter is not required for querying all objects + leaf=dict(type="str", aliases=["name"]), + vlan=dict(type="int"), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["pod", "leaf", "vlan"]], + ["state", "present", ["pod", "leaf", "vlan"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + pod = module.params.get("pod") + leaf = module.params.get("leaf") + vlan = module.params.get("vlan") + state = module.params.get("state") + + leafpath = "topology/{0}/node-{1}".format(pod, leaf) + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site/template '{0}-{1}' does not exist. Existing sites/templates: {2}".format(site, template, ", ".join(sites))) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get ANP + anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp) + anps = [a.get("anpRef") for a in schema_obj.get("sites")[site_idx]["anps"]] + if anp_ref not in anps: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps))) + anp_idx = anps.index(anp_ref) + + # Get EPG + epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg) + epgs = [e.get("epgRef") for e in schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"]] + if epg_ref not in epgs: + mso.fail_json(msg="Provided epg '{0}' does not exist. Existing epgs: {1}".format(epg, ", ".join(epgs))) + epg_idx = epgs.index(epg_ref) + + # Get Leaf + leafs = [(leaf.get("path"), leaf.get("portEncapVlan")) for leaf in schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["staticLeafs"]] + if (leafpath, vlan) in leafs: + leaf_idx = leafs.index((leafpath, vlan)) + # FIXME: Changes based on index are DANGEROUS + leaf_path = "/sites/{0}/anps/{1}/epgs/{2}/staticLeafs/{3}".format(site_template, anp, epg, leaf_idx) + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["staticLeafs"][leaf_idx] + + if state == "query": + if leaf is None or vlan is None: + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["staticLeafs"] + elif not mso.existing: + mso.fail_json(msg="Static leaf '{leaf}/{vlan}' not found".format(leaf=leaf, vlan=vlan)) + mso.exit_json() + + leafs_path = "/sites/{0}/anps/{1}/epgs/{2}/staticLeafs".format(site_template, anp, epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=leaf_path)) + + elif state == "present": + payload = dict( + path=leafpath, + portEncapVlan=vlan, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=leaf_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=leafs_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticport.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticport.py new file mode 100644 index 000000000..d05336c52 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticport.py @@ -0,0 +1,444 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp_epg_staticport +short_description: Manage site-local EPG static ports in schema template +description: +- Manage site-local EPG static ports in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG. + type: str + required: true + type: + description: + - The path type of the static port + - vpc is used for a Virtual Port Channel + - dpc is used for a Direct Port Channel + - port is used for a single interface + type: str + choices: [ port, vpc, dpc ] + default: port + pod: + description: + - The pod of the static port. + type: str + leaf: + description: + - The leaf of the static port. + type: str + fex: + description: + - The fex id of the static port. + type: str + path: + description: + - The path of the static port. + type: str + vlan: + description: + - The port encap VLAN id of the static port. + type: int + deployment_immediacy: + description: + - The deployment immediacy of the static port. + - C(immediate) means B(Deploy immediate). + - C(lazy) means B(deploy on demand). + type: str + choices: [ immediate, lazy ] + default: lazy + mode: + description: + - The mode of the static port. + - C(native) means B(Access (802.1p)). + - C(regular) means B(Trunk). + - C(untagged) means B(Access (untagged)). + type: str + choices: [ native, regular, untagged ] + default: untagged + primary_micro_segment_vlan: + description: + - Primary micro-seg VLAN of static port. + type: int + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing an object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_anp_epg +- module: cisco.mso.mso_schema_template_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new static port to a site EPG + cisco.mso.mso_schema_site_anp_epg_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + type: port + pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + deployment_immediacy: immediate + state: present + delegate_to: localhost + +- name: Add a new static fex port to a site EPG + mso_schema_site_anp_epg_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + type: port + pod: pod-1 + leaf: 101 + fex: 151 + path: eth1/1 + vlan: 126 + deployment_immediacy: lazy + state: present + delegate_to: localhost + +- name: Add a new static VPC to a site EPG + mso_schema_site_anp_epg_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + pod: pod-1 + leaf: 101-102 + path: ansible_polgrp + vlan: 127 + type: vpc + mode: untagged + deployment_immediacy: lazy + state: present + delegate_to: localhost + +- name: Remove a static port from a site EPG + cisco.mso.mso_schema_site_anp_epg_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + type: port + pod: pod-1 + leaf: 101 + path: eth1/1 + state: absent + delegate_to: localhost + +- name: Query a specific site EPG static port + cisco.mso.mso_schema_site_anp_epg_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + type: port + pod: pod-1 + leaf: 101 + path: eth1/1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site EPG static ports + cisco.mso.mso_schema_site_anp_epg_staticport: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + type=dict(type="str", default="port", choices=["port", "vpc", "dpc"]), + pod=dict(type="str"), # This parameter is not required for querying all objects + leaf=dict(type="str"), # This parameter is not required for querying all objects + fex=dict(type="str"), # This parameter is not required for querying all objects + path=dict(type="str"), # This parameter is not required for querying all objects + vlan=dict(type="int"), # This parameter is not required for querying all objects + primary_micro_segment_vlan=dict(type="int"), # This parameter is not required for querying all objects + deployment_immediacy=dict(type="str", default="lazy", choices=["immediate", "lazy"]), + mode=dict(type="str", default="untagged", choices=["native", "regular", "untagged"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["type", "pod", "leaf", "path", "vlan"]], + ["state", "present", ["type", "pod", "leaf", "path", "vlan"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + path_type = module.params.get("type") + pod = module.params.get("pod") + leaf = module.params.get("leaf") + fex = module.params.get("fex") + path = module.params.get("path") + vlan = module.params.get("vlan") + primary_micro_segment_vlan = module.params.get("primary_micro_segment_vlan") + deployment_immediacy = module.params.get("deployment_immediacy") + mode = module.params.get("mode") + state = module.params.get("state") + + if path_type == "port" and fex is not None: + # Select port path for fex if fex param is used + portpath = "topology/{0}/paths-{1}/extpaths-{2}/pathep-[{3}]".format(pod, leaf, fex, path) + elif path_type == "vpc": + portpath = "topology/{0}/protpaths-{1}/pathep-[{2}]".format(pod, leaf, path) + else: + portpath = "topology/{0}/paths-{1}/pathep-[{2}]".format(pod, leaf, path) + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + sites_list = [s.get("siteId") + "/" + s.get("templateName") for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json( + msg="Provided site/siteId/template '{0}/{1}/{2}' does not exist. " + "Existing siteIds/templates: {3}".format(site, site_id, template, ", ".join(sites_list)) + ) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + payload = dict() + ops = [] + op_path = "" + + # Get ANP + anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp) + anps = [a.get("anpRef") for a in schema_obj["sites"][site_idx]["anps"]] + anps_in_temp = [a.get("name") for a in schema_obj["templates"][template_idx]["anps"]] + if anp not in anps_in_temp: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps))) + else: + # Update anp index at template level + template_anp_idx = anps_in_temp.index(anp) + + # If anp not at site level but exists at template level + if anp_ref not in anps: + op_path = "/sites/{0}/anps/-".format(site_template) + payload.update( + anpRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + ), + ) + + else: + # Update anp index at site level + anp_idx = anps.index(anp_ref) + + # Get EPG + epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg) + + # If anp exists at site level + if "anpRef" not in payload: + epgs = [e.get("epgRef") for e in schema_obj["sites"][site_idx]["anps"][anp_idx]["epgs"]] + + # If anp already at site level AND if epg not at site level (or) anp not at site level + if ("anpRef" not in payload and epg_ref not in epgs) or "anpRef" in payload: + epgs_in_temp = [e.get("name") for e in schema_obj["templates"][template_idx]["anps"][template_anp_idx]["epgs"]] + + # If EPG not at template level - Fail + if epg not in epgs_in_temp: + mso.fail_json(msg="Provided EPG '{0}' does not exist. Existing EPGs: {1} epgref {2}".format(epg, ", ".join(epgs_in_temp), epg_ref)) + + # EPG at template level but not at site level. Create payload at site level for EPG + else: + new_epg = dict( + epgRef=dict( + schemaId=schema_id, + templateName=template, + anpName=anp, + epgName=epg, + ) + ) + + # If anp not in payload then, anp already exists at site level. New payload will only have new EPG payload + if "anpRef" not in payload: + op_path = "/sites/{0}/anps/{1}/epgs/-".format(site_template, anp) + payload = new_epg + else: + # If anp in payload, anp exists at site level. Update payload with EPG payload + payload["epgs"] = [new_epg] + + # Update index of EPG at site level + else: + epg_idx = epgs.index(epg_ref) + + # Get Leaf + # If anp at site level and epg is at site level + if "anpRef" not in payload and "epgRef" not in payload: + portpaths = [p.get("path") for p in schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["staticPorts"]] + if portpath in portpaths: + portpath_idx = portpaths.index(portpath) + port_path = "/sites/{0}/anps/{1}/epgs/{2}/staticPorts/{3}".format(site_template, anp, epg, portpath_idx) + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["staticPorts"][portpath_idx] + + if state == "query": + if leaf is None or vlan is None: + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["staticPorts"] + elif not mso.existing: + mso.fail_json(msg="Static port '{portpath}' not found".format(portpath=portpath)) + mso.exit_json() + + ports_path = "/sites/{0}/anps/{1}/epgs/{2}/staticPorts".format(site_template, anp, epg) + ops = [] + new_leaf = dict( + deploymentImmediacy=deployment_immediacy, + mode=mode, + path=portpath, + portEncapVlan=vlan, + type=path_type, + ) + if primary_micro_segment_vlan: + new_leaf.update(microSegVlan=primary_micro_segment_vlan) + + # If payload is empty, anp and EPG already exist at site level + if not payload: + op_path = ports_path + "/-" + payload = new_leaf + + # If payload exists + else: + # If anp already exists at site level + if "anpRef" not in payload: + payload["staticPorts"] = [new_leaf] + else: + payload["epgs"][0]["staticPorts"] = [new_leaf] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=port_path)) + + elif state == "present": + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=port_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path, value=mso.sent)) + + mso.existing = new_leaf + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_subnet.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_subnet.py new file mode 100644 index 000000000..e5aaeba37 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_subnet.py @@ -0,0 +1,281 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_anp_epg_subnet +short_description: Manage site-local EPG subnets in schema template +description: +- Manage site-local EPG subnets in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG. + type: str + required: true + subnet: + description: + - The IP range in CIDR notation. + type: str + required: true + aliases: [ ip ] + description: + description: + - The description of this subnet. + type: str + scope: + description: + - The scope of the subnet. + type: str + default: private + choices: [ private, public ] + shared: + description: + - Whether this subnet is shared between VRFs. + type: bool + default: false + no_default_gateway: + description: + - Whether this subnet has a default gateway. + type: bool + default: false + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_anp_epg +- module: cisco.mso.mso_schema_template_anp_epg_subnet +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new subnet to a site EPG + cisco.mso.mso_schema_site_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + subnet: 10.0.0.0/24 + state: present + delegate_to: localhost + +- name: Remove a subnet from a site EPG + cisco.mso.mso_schema_site_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + subnet: 10.0.0.0/24 + state: absent + delegate_to: localhost + +- name: Query a specific site EPG subnet + cisco.mso.mso_schema_site_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + epg: EPG1 + subnet: 10.0.0.0/24 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site EPG subnets + cisco.mso.mso_schema_site_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + anp: ANP1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_epg_subnet_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + argument_spec.update(mso_epg_subnet_spec()) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["subnet"]], + ["state", "present", ["subnet"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + subnet = module.params.get("subnet") + description = module.params.get("description") + scope = module.params.get("scope") + shared = module.params.get("shared") + no_default_gateway = module.params.get("no_default_gateway") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site/template '{0}-{1}' does not exist. Existing sites/templates: {2}".format(site, template, ", ".join(sites))) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get ANP + anp_ref = mso.anp_ref(schema_id=schema_id, template=template, anp=anp) + anps = [a.get("anpRef") for a in schema_obj.get("sites")[site_idx]["anps"]] + if anp_ref not in anps: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps))) + anp_idx = anps.index(anp_ref) + + # Get EPG + epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg) + epgs = [e.get("epgRef") for e in schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"]] + if epg_ref not in epgs: + mso.fail_json(msg="Provided epg '{0}' does not exist. Existing epgs: {1}".format(epg, ", ".join(epgs))) + epg_idx = epgs.index(epg_ref) + + # Get Subnet + subnets = [s.get("ip") for s in schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["subnets"]] + if subnet in subnets: + subnet_idx = subnets.index(subnet) + # FIXME: Changes based on index are DANGEROUS + subnet_path = "/sites/{0}/anps/{1}/epgs/{2}/subnets/{3}".format(site_template, anp, epg, subnet_idx) + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["subnets"][subnet_idx] + + if state == "query": + if subnet is None: + mso.existing = schema_obj.get("sites")[site_idx]["anps"][anp_idx]["epgs"][epg_idx]["subnets"] + elif not mso.existing: + mso.fail_json(msg="Subnet '{subnet}' not found".format(subnet=subnet)) + mso.exit_json() + + subnets_path = "/sites/{0}/anps/{1}/epgs/{2}/subnets".format(site_template, anp, epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=subnet_path)) + + elif state == "present": + if not mso.existing: + if description is None: + description = subnet + if scope is None: + scope = "private" + if shared is None: + shared = False + if no_default_gateway is None: + no_default_gateway = False + + payload = dict( + ip=subnet, + description=description, + scope=scope, + shared=shared, + noDefaultGateway=no_default_gateway, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=subnet_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=subnets_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd.py new file mode 100644 index 000000000..35c352fb6 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_bd +short_description: Manage site-local Bridge Domains (BDs) in schema template +description: +- Manage site-local BDs in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + bd: + description: + - The name of the BD to manage. + type: str + aliases: [ name ] + host_route: + description: + - Whether host-based routing is enabled. + type: bool + svi_mac: + description: + - SVI MAC Address + 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 +seealso: +- module: cisco.mso.mso_schema_site +- module: cisco.mso.mso_schema_site_bd_l3out +- module: cisco.mso.mso_schema_site_bd_subnet +- module: cisco.mso.mso_schema_template_bd +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site BD + cisco.mso.mso_schema_site_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + state: present + delegate_to: localhost + +- name: Remove a site BD + cisco.mso.mso_schema_site_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + state: absent + delegate_to: localhost + +- name: Query a specific site BD + cisco.mso.mso_schema_site_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site BDs + cisco.mso.mso_schema_site_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + bd=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + host_route=dict(type="bool"), + svi_mac=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", ["bd"]], + ["state", "present", ["bd"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + bd = module.params.get("bd") + host_route = module.params.get("host_route") + svi_mac = module.params.get("svi_mac") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site-template association '{0}-{1}' does not exist.".format(site, template)) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get BD + bd_ref = mso.bd_ref(schema_id=schema_id, template=template, bd=bd) + bds = [v.get("bdRef") for v in schema_obj.get("sites")[site_idx]["bds"]] + if bd is not None and bd_ref in bds: + bd_idx = bds.index(bd_ref) + bd_path = "/sites/{0}/bds/{1}".format(site_template, bd) + mso.existing = schema_obj.get("sites")[site_idx]["bds"][bd_idx] + mso.existing["bdRef"] = mso.dict_from_ref(mso.existing.get("bdRef")) + + if state == "query": + if bd is None: + mso.existing = schema_obj.get("sites")[site_idx]["bds"] + for bd in mso.existing: + bd["bdRef"] = mso.dict_from_ref(bd.get("bdRef")) + elif not mso.existing: + mso.fail_json(msg="BD '{bd}' not found".format(bd=bd)) + mso.exit_json() + + bds_path = "/sites/{0}/bds".format(site_template) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=bd_path)) + + elif state == "present": + if not mso.existing: + if host_route is None: + host_route = False + + payload = dict( + bdRef=dict( + schemaId=schema_id, + templateName=template, + bdName=bd, + ), + hostBasedRouting=host_route, + ) + if svi_mac is not None: + payload.update(mac=svi_mac) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=bd_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=bds_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_l3out.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_l3out.py new file mode 100644 index 000000000..8f9581294 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_l3out.py @@ -0,0 +1,255 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@cisco.com> +# Copyright: (c) 2022, Akini Ross (@akinross) <akinross@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_bd_l3out +short_description: Manage site-local BD l3out's in schema template +description: +- Manage site-local BDs l3out's in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +- Anvitha Jain (@anvitha-jain) +- Akini Ross (@akinross) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + bd: + description: + - The name of the BD. + type: str + required: true + aliases: [ name ] + l3out: + description: + - The l3out associated to this BD. + type: dict + suboptions: + name: + description: + - The name of the l3out to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced l3out. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced l3out. + - If this parameter is unspecified, it defaults to the current schema. + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_bd +- module: cisco.mso.mso_schema_template_bd +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site BD l3out + cisco.mso.mso_schema_site_bd_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + l3out: + name: L3out1 + state: present + delegate_to: localhost + +- name: Add a new site BD l3out with different schema and template + cisco.mso.mso_schema_site_bd_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + l3out: + name: L3out1 + schema: Schema2 + template: Template2 + state: present + delegate_to: localhost + +- name: Remove a site BD l3out + cisco.mso.mso_schema_site_bd_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + l3out: + name: L3out1 + state: absent + delegate_to: localhost + +- name: Query a specific site BD l3out + cisco.mso.mso_schema_site_bd_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + l3out: + name: L3out1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site BD l3outs + cisco.mso.mso_schema_site_bd_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_reference_spec +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + bd=dict(type="str", required=True), + l3out=dict(type="dict", options=mso_reference_spec(), aliases=["name"]), # This parameter is not required for querying all objects + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["l3out"]], + ["state", "present", ["l3out"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + bd = module.params.get("bd") + l3out = module.params.get("l3out") + state = module.params.get("state") + + mso = MSOModule(module) + + mso_schema = MSOSchema(mso, schema, template, site) + mso_objects = mso_schema.schema_objects + + mso_schema.set_template_bd(bd) + mso_schema.set_site_bd(bd, fail_module=False) + + bd_path = "/sites/{0}-{1}/bds".format(mso_objects.get("site").details.get("siteId"), template) + ops = [] + payload = dict() + + if l3out: + l3out_schema_id = mso.lookup_schema(l3out.get("schema")) if l3out.get("schema") else mso_schema.id + l3out_template = l3out.get("template") if l3out.get("template") else template + l3out_ref = mso.l3out_ref(schema_id=l3out_schema_id, template=l3out_template, l3out=l3out.get("name")) + if not mso_objects.get("site_bd"): + payload = dict(bdRef=dict(schemaId=mso_schema.id, templateName=template, bdName=bd), l3Outs=[l3out.get("name")], l3OutRefs=[l3out_ref]) + else: + mso_objects.get("site_bd").details["bdRef"] = dict(schemaId=mso_schema.id, templateName=template, bdName=bd) + l3out_refs = mso_objects.get("site_bd").details.get("l3OutRefs", []) + l3outs = mso_objects.get("site_bd").details.get("l3Outs", []) + # check on name because refs are handled differently between versions + if l3out.get("name") in l3outs: + mso.existing = mso.make_reference(l3out, "l3out", l3out_schema_id, l3out_template) + + if state == "query": + if l3out is None: + if "l3OutRefs" in mso_objects.get("site_bd", {}).details.keys(): + mso.existing = [mso.dict_from_ref(l3) for l3 in mso_objects.get("site_bd", {}).details.get("l3OutRefs", [])] + else: + mso.existing = [dict(l3outName=l3) for l3 in mso_objects.get("site_bd", {}).details.get("l3Outs", [])] + elif not mso.existing: + mso.fail_json(msg="L3out '{0}' not found".format(l3out.get("name"))) + mso.exit_json() + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + if l3out.get("name") in l3outs: + del l3outs[l3outs.index(l3out.get("name"))] + if l3out_ref in l3out_refs: + del l3out_refs[l3out_refs.index(l3out_ref)] + ops.append(dict(op="replace", path="{0}/{1}".format(bd_path, bd), value=mso_objects.get("site_bd").details)) + + elif state == "present": + if payload: + ops.append(dict(op="add", path="{0}/-".format(bd_path), value=payload)) + elif not mso.existing: + l3outs.append(l3out.get("name")) + l3out_refs.append(l3out_ref) + ops.append(dict(op="replace", path="{0}/{1}".format(bd_path, bd), value=mso_objects.get("site_bd").details)) + mso.existing = mso.make_reference(l3out, "l3out", l3out_schema_id, l3out_template) + + if not module.check_mode: + mso.request(mso_schema.path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_subnet.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_subnet.py new file mode 100644 index 000000000..c4ab52eec --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_subnet.py @@ -0,0 +1,290 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Akini Ross (@akinross) <akinross@cisco.com> +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_bd_subnet +short_description: Manage site-local BD subnets in schema template +description: +- Manage site-local BD subnets in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + bd: + description: + - The name of the BD. + type: str + required: true + aliases: [ name ] + subnet: + description: + - The IP range in CIDR notation. + type: str + aliases: [ ip ] + description: + description: + - The description of this subnet. + type: str + scope: + description: + - The scope of the subnet. + type: str + choices: [ private, public ] + shared: + description: + - Whether this subnet is shared between VRFs. + type: bool + default: false + no_default_gateway: + description: + - Whether this subnet has a default gateway. + type: bool + default: false + querier: + description: + - Whether this subnet is an IGMP querier. + type: bool + default: false + primary: + description: + - Treat as Primary Subnet. + - There can be only one primary subnet per address family under a BD. + - This option can only be used on versions of MSO that are 3.1.1h or greater. + type: bool + default: false + is_virtual_ip: + description: + - Treat as Virtual IP Address + type: bool + default: false + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_bd +- module: cisco.mso.mso_schema_template_bd +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site BD subnet + cisco.mso.mso_schema_site_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + subnet: 11.11.11.0/24 + state: present + delegate_to: localhost + +- name: Remove a site BD subnet + cisco.mso.mso_schema_site_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + subnet: 11.11.11.0/24 + state: absent + delegate_to: localhost + +- name: Query a specific site BD subnet + cisco.mso.mso_schema_site_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + subnet: 11.11.11.0/24 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site BD subnets + cisco.mso.mso_schema_site_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + bd: BD1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_subnet_spec +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update(mso_subnet_spec()) + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + bd=dict(type="str", aliases=["name"], required=True), + subnet=dict(type="str", aliases=["ip"]), + description=dict(type="str"), + scope=dict(type="str", choices=["private", "public"]), + shared=dict(type="bool", default=False), + no_default_gateway=dict(type="bool", default=False), + querier=dict(type="bool", default=False), + primary=dict(type="bool", default=False), + is_virtual_ip=dict(type="bool", default=False), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["subnet"]], + ["state", "present", ["subnet"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + bd = module.params.get("bd") + ip = module.params.get("subnet") + description = module.params.get("description") + scope = module.params.get("scope") + shared = module.params.get("shared") + no_default_gateway = module.params.get("no_default_gateway") + querier = module.params.get("querier") + primary = module.params.get("primary") + is_virtual_ip = module.params.get("is_virtual_ip") + state = module.params.get("state") + + mso = MSOModule(module) + + mso_schema = MSOSchema(mso, schema, template, site) + mso_objects = mso_schema.schema_objects + + mso_schema.set_template_bd(bd) + if mso_objects.get("template_bd") and mso_objects.get("template_bd").details.get("l2Stretch") is True and state == "present": + mso.fail_json( + msg="The l2Stretch of template bd should be false in order to create a site bd subnet. " "Set l2Stretch as false using mso_schema_template_bd" + ) + + if state == "query": + mso_schema.set_site_bd(bd) + if not ip: + mso.existing = mso_objects.get("site_bd").details.get("subnets") + else: + mso_schema.set_site_bd_subnet(ip) + mso.existing = mso_objects.get("site_bd_subnet").details + mso.exit_json() + + mso_schema.set_site_bd(bd, fail_module=False) + + subnet = None + if mso_objects.get("site_bd"): + mso_schema.set_site_bd_subnet(ip, fail_module=False) + subnet = mso_objects.get("site_bd_subnet") + + mso.previous = mso.existing = subnet.details if subnet else mso.existing + + bd_path = "/sites/{0}-{1}/bds".format(mso_objects.get("site").details.get("siteId"), template) + subnet_path = "{0}/{1}/subnets".format(bd_path, bd) + ops = [] + + if state == "absent": + if subnet: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=subnet_path)) + + elif state == "present": + if not mso_objects.get("site_bd"): + bd_payload = dict( + bdRef=dict( + schemaId=mso_schema.id, + templateName=template, + bdName=bd, + ), + hostBasedRouting=False, + ) + ops.append(dict(op="add", path=bd_path + "/-", value=bd_payload)) + + if not subnet: + if description is None: + description = ip + if scope is None: + scope = "private" + + subnet_payload = dict( + ip=ip, + description=description, + scope=scope, + shared=shared, + noDefaultGateway=no_default_gateway, + virtual=is_virtual_ip, + querier=querier, + primary=primary, + ) + + mso.sanitize(subnet_payload, collate=True) + + if subnet: + ops.append(dict(op="replace", path="{0}/{1}".format(subnet_path, subnet.index), value=mso.sent)) + else: + ops.append(dict(op="add", path="{0}/-".format(subnet_path), value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(mso_schema.path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg.py new file mode 100644 index 000000000..dff4cf152 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg.py @@ -0,0 +1,234 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_external_epg +short_description: Manage External EPG in schema of sites +description: +- Manage External EPG in schema of sites on Cisco ACI Multi-Site. +- This module can only be used on versions of MSO that are 3.3 or greater. +author: +- Anvitha Jain (@anvitha-jain) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + l3out: + description: + - The L3Out associated with the external epg. + - Required when site is of type on-premise. + type: str + external_epg: + description: + - The name of the External EPG to be managed. + type: str + aliases: [ name ] + site: + description: + - The name of the site. + type: str + required: true + route_reachability: + description: + - Configures if an external EPG route is pointing to the internet or to an external remote network. + - Only available when associated with an azure site. + type: str + choices: [ internet, site-ext ] + default: internet + 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 +seealso: +- module: cisco.mso.mso_schema_template_external_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a Site External EPG + cisco.mso.mso_schema_site_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: External EPG 1 + l3out: L3out1 + state: present + delegate_to: localhost + +- name: Remove a Site External EPG + cisco.mso.mso_schema_site_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: External EPG 1 + l3out: L3out1 + state: absent + delegate_to: localhost + +- name: Query a Site External EPG + cisco.mso.mso_schema_site_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: External EPG 1 + l3out: L3out1 + state: query + delegate_to: localhost + +- name: Query all Site External EPGs + cisco.mso.mso_schema_site_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + site=dict(type="str", required=True), + l3out=dict(type="str"), + external_epg=dict(type="str", aliases=["name"]), + route_reachability=dict(type="str", default="internet", choices=["internet", "site-ext"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["external_epg"]], + ["state", "present", ["external_epg"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template") + site = module.params.get("site") + external_epg = module.params.get("external_epg") + l3out = module.params.get("l3out") + route_reachability = module.params.get("route_reachability") + state = module.params.get("state") + + mso = MSOModule(module) + + mso_schema = MSOSchema(mso, schema, template, site) + mso_objects = mso_schema.schema_objects + + mso_schema.set_template_external_epg(external_epg, fail_module=False) + + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(mso_objects.get("site").details.get("siteId"), template) + + payload = dict() + op_path = "/sites/{0}/externalEpgs/-".format(site_template) + + # Get template External EPG + if mso_objects.get("template_external_epg") is not None: + ext_epg_ref = mso_objects.get("template_external_epg").details.get("externalEpgRef") + external_epgs = [e.get("externalEpgRef") for e in mso_objects.get("site").details.get("externalEpgs")] + + # Get Site External EPG + if ext_epg_ref in external_epgs: + external_epg_idx = external_epgs.index(ext_epg_ref) + mso.existing = mso_objects.get("site").details.get("externalEpgs")[external_epg_idx] + op_path = "/sites/{0}/externalEpgs/{1}".format(site_template, external_epg) + + ops = [] + l3out_dn = "" + + if state == "query": + if external_epg is None: + mso.existing = mso_objects.get("site").details.get("externalEpgs") + elif not mso.existing: + mso.fail_json(msg="External EPG '{external_epg}' not found".format(external_epg=external_epg)) + mso.exit_json() + + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=op_path)) + + elif state == "present": + # Get external EPGs type from template level and verify template_external_epg type. + if mso_objects.get("template_external_epg").details.get("extEpgType") != "cloud": + if l3out is not None: + path = "tenants/{0}".format(mso_objects.get("template").details.get("tenantId")) + tenant_name = mso.request(path, method="GET").get("name") + l3out_dn = "uni/tn-{0}/out-{1}".format(tenant_name, l3out) + else: + mso.fail_json(msg="L3Out cannot be empty when template external EPG type is 'on-premise'.") + + payload = dict( + externalEpgRef=dict( + schemaId=mso_schema.id, + templateName=template, + externalEpgName=external_epg, + ), + l3outDn=l3out_dn, + l3outRef=dict( + schemaId=mso_schema.id, + templateName=template, + l3outName=l3out, + ), + routeReachabilityInternetType=route_reachability, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=op_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(mso_schema.path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg_selector.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg_selector.py new file mode 100644 index 000000000..001ebf2b5 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg_selector.py @@ -0,0 +1,291 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_external_epg_selector +short_description: Manage External EPG selector in schema of cloud sites +description: +- Manage External EPG selector in schema of cloud sites on Cisco ACI Multi-Site. +author: +- Shreyas Srish (@shrsr) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + external_epg: + description: + - The name of the External EPG to be managed. + type: str + required: true + site: + description: + - The name of the cloud site. + type: str + required: true + selector: + description: + - The name of the selector. + type: str + expressions: + description: + - Expressions associated to this selector. + type: list + elements: dict + suboptions: + type: + description: + - The name of the expression which in this case is always IP address. + required: true + type: str + choices: [ ip_address ] + operator: + description: + - The operator associated with the expression which in this case is always equals. + required: true + type: str + choices: [ equals ] + value: + description: + - The value of the IP Address / Subnet associated with the expression. + required: true + 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 +seealso: +- module: cisco.mso.mso_schema_template_external_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a selector to an External EPG + cisco.mso.mso_schema_site_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: ansible_test + template: Template1 + site: azure_ansible_test + external_epg: ext1 + selector: test + expressions: + - type: ip_address + operator: equals + value: 10.0.0.0 + state: present + delegate_to: localhost + +- name: Remove a Selector + cisco.mso.mso_schema_site_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: ansible_test + template: Template1 + site: azure_ansible_test + external_epg: ext1 + selector: test + state: absent + delegate_to: localhost + +- name: Query a specific Selector + cisco.mso.mso_schema_site_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: ansible_test + template: Template1 + site: azure_ansible_test + external_epg: ext1 + selector: selector_1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all Selectors + cisco.mso.mso_schema_site_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: ansible_test + template: Template1 + site: azure_ansible_test + external_epg: ext1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_expression_spec_ext_epg + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + site=dict(type="str", required=True), + external_epg=dict(type="str", required=True), + selector=dict(type="str"), + expressions=dict(type="list", elements="dict", options=mso_expression_spec_ext_epg()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + site = module.params.get("site") + external_epg = module.params.get("external_epg") + selector = module.params.get("selector") + expressions = module.params.get("expressions") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(templates)) + ) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + sites_list = [s.get("siteId") + "/" + s.get("templateName") for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json( + msg="Provided site/siteId/template '{0}/{1}/{2}' does not exist. " + "Existing siteIds/templates: {3}".format(site, site_id, template, ", ".join(sites_list)) + ) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + payload = dict() + op_path = "" + + # Get External EPG + ext_epg_ref = mso.ext_epg_ref(schema_id=schema_id, template=template, external_epg=external_epg) + external_epgs = [e.get("externalEpgRef") for e in schema_obj.get("sites")[site_idx]["externalEpgs"]] + + if ext_epg_ref not in external_epgs: + op_path = "/sites/{0}/externalEpgs/-".format(site_template) + payload = dict( + externalEpgRef=dict( + schemaId=schema_id, + templateName=template, + externalEpgName=external_epg, + ), + l3outDn="", + ) + + else: + external_epg_idx = external_epgs.index(ext_epg_ref) + + # Get Selector + selectors = [s.get("name") for s in schema_obj["sites"][site_idx]["externalEpgs"][external_epg_idx]["subnets"]] + if selector in selectors: + selector_idx = selectors.index(selector) + selector_path = "/sites/{0}/externalEpgs/{1}/subnets/{2}".format(site_template, external_epg, selector_idx) + mso.existing = schema_obj["sites"][site_idx]["externalEpgs"][external_epg_idx]["subnets"][selector_idx] + + selectors_path = "/sites/{0}/externalEpgs/{1}/subnets/-".format(site_template, external_epg) + ops = [] + + if state == "query": + if selector is None: + mso.existing = schema_obj["sites"][site_idx]["externalEpgs"][external_epg_idx]["subnets"] + elif not mso.existing: + mso.fail_json(msg="Selector '{selector}' not found".format(selector=selector)) + mso.exit_json() + + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=selector_path)) + + elif state == "present": + # Get expressions + types = dict(ip_address="ipAddress") + all_expressions = [] + if expressions: + for expression in expressions: + type_val = expression.get("type") + operator = expression.get("operator") + value = expression.get("value") + all_expressions.append( + dict( + key=types.get(type_val), + operator=operator, + value=value, + ) + ) + else: + mso.fail_json(msg="Missing expressions in selector") + + subnets = dict(name=selector, ip=all_expressions[0]["value"]) + + if not external_epgs: + payload["subnets"] = [subnets] + else: + payload = subnets + op_path = selectors_path + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=selector_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_l3out.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_l3out.py new file mode 100644 index 000000000..4741e4bd7 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_l3out.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_l3out +short_description: Manage site-local layer3 Out (L3Outs) in schema template +description: +- Manage site-local L3Outs in schema template on Cisco ACI Multi-Site. +- This module can only be used on versions of MSO that are 3.0 or greater. +- NOTE - Usage of this module for version lesser than 3.0 might break the MSO. +author: +- Anvitha Jain (@anvitha-jain) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + vrf: + description: + - The VRF associated to this L3out. + type: dict + suboptions: + name: + description: + - The name of the VRF to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + type: str + l3out: + description: + - The name of the l3out to manage. + type: str + aliases: [ name ] + 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 +seealso: +- module: cisco.mso.mso_schema_site +- module: cisco.mso.mso_schema_template_l3out +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site L3Out + cisco.mso.mso_schema_site_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + l3out: L3out1 + vrf: + name: vrfName + template: TemplateName + schema: schemaName + state: present + delegate_to: localhost + +- name: Remove a site L3Out + cisco.mso.mso_schema_site_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + l3out: L3out1 + state: absent + delegate_to: localhost + +- name: Query a specific site L3Out + cisco.mso.mso_schema_site_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + l3out: L3out1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site l3outs + cisco.mso.mso_schema_site_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_reference_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="dict", options=mso_reference_spec()), + l3out=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["l3out"]], + ["state", "present", ["l3out", "vrf"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + l3out = module.params.get("l3out") + vrf = module.params.get("vrf") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided template '{0}' is not associated to site".format(template)) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get l3out + l3out_ref = mso.l3out_ref(schema_id=schema_id, template=template, l3out=l3out) + l3outs = [v.get("l3outRef") for v in schema_obj.get("sites")[site_idx]["intersiteL3outs"]] + + if l3out is not None and l3out_ref in l3outs: + l3out_idx = l3outs.index(l3out_ref) + l3out_path = "/sites/{0}/intersiteL3outs/{1}".format(site_template, l3out) + mso.existing = schema_obj.get("sites")[site_idx]["intersiteL3outs"][l3out_idx] + + if state == "query": + if l3out is None: + mso.existing = schema_obj.get("sites")[site_idx]["intersiteL3outs"] + for l3out in mso.existing: + l3out["l3outRef"] = mso.dict_from_ref(l3out.get("l3outRef")) + elif not mso.existing: + mso.fail_json(msg="L3Out '{l3out}' not found".format(l3out=l3out)) + mso.exit_json() + + l3outs_path = "/sites/{0}/intersiteL3outs".format(site_template) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=l3out_path)) + + elif state == "present": + vrf_ref = mso.make_reference(vrf, "vrf", schema_id, template) + + payload = dict( + l3outRef=dict( + schemaId=schema_id, + templateName=template, + l3outName=l3out, + ), + vrfRef=vrf_ref, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=l3out_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=l3outs_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_service_graph.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_service_graph.py new file mode 100644 index 000000000..9495a501d --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_service_graph.py @@ -0,0 +1,279 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_service_graph +short_description: Manage Service Graph in schema sites +description: +- Manage Service Graph in schema sites on Cisco ACI Multi-Site. +- This module is only supported in MSO/NDO version 3.3 and above. +author: +- Shreyas Srish (@shrsr) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + tenant: + description: + - The name of the tenant. + type: str + service_graph: + description: + - The name of the Service Graph to manage. + type: str + aliases: [ name ] + devices: + description: + - A list of devices to be associated with the Service Graph. + type: list + elements: dict + suboptions: + name: + description: + - The name of the device + required: true + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a Service Graph + cisco.mso.mso_schema_site_service_graph_node: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + service_graph: SG1 + site: site1 + tenant: tenant1 + devices: + - name: ansible_test_firewall + - name: ansible_test_adc + - name: ansible_test_other + state: present + delegate_to: localhost + +- name: Remove a Service Graph + cisco.mso.mso_schema_site_service_graph_node: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + service_graph: SG1 + site: site1 + state: absent + delegate_to: localhost + +- name: Query a specific Service Graph + cisco.mso.mso_schema_site_service_graph_node: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + service_graph: SG1 + site: site1 + state: query + delegate_to: localhost + +- name: Query all Service Graphs + cisco.mso.mso_schema_site_service_graph_node: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + site: site1 + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_service_graph_node_device_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + service_graph=dict(type="str", aliases=["name"]), + tenant=dict(type="str"), + site=dict(type="str", required=True), + devices=dict(type="list", elements="dict", options=mso_service_graph_node_device_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["service_graph"]], + ["state", "present", ["service_graph", "devices"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + service_graph = module.params.get("service_graph") + devices = module.params.get("devices") + site = module.params.get("site") + tenant = module.params.get("tenant") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = schema_obj.get("templates") + template_names = [t.get("name") for t in templates] + if template not in template_names: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(template_names)) + ) + template_idx = template_names.index(template) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site-template association '{0}-{1}' does not exist.".format(site, template)) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + mso.existing = {} + service_graph_idx = None + + # Get Service Graph + service_graph_ref = mso.service_graph_ref(schema_id=schema_id, template=template, service_graph=service_graph) + service_graph_refs = [f.get("serviceGraphRef") for f in schema_obj.get("sites")[site_idx]["serviceGraphs"]] + if service_graph is not None and service_graph_ref in service_graph_refs: + service_graph_idx = service_graph_refs.index(service_graph_ref) + mso.existing = schema_obj.get("sites")[site_idx]["serviceGraphs"][service_graph_idx] + + if state == "query": + if service_graph is None: + mso.existing = schema_obj.get("sites")[site_idx]["serviceGraphs"] + elif service_graph is not None and service_graph_idx is None: + mso.fail_json(msg="Service Graph '{service_graph}' not found".format(service_graph=service_graph)) + mso.exit_json() + + service_graphs_path = "/sites/{0}/serviceGraphs/-".format(site_template) + service_graph_path = "/sites/{0}/serviceGraphs/{1}".format(site_template, service_graph) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=service_graph_path)) + + elif state == "present": + devices_payload = [] + service_graphs = templates[template_idx]["serviceGraphs"] + for graph in service_graphs: + if graph.get("name") == service_graph: + service_node_types_from_template = graph["serviceNodes"] + user_number_devices = len(devices) + number_of_nodes_in_template = len(service_node_types_from_template) + if user_number_devices != number_of_nodes_in_template: + mso.fail_json( + msg="Service Graph '{0}' has '{1}' service node type(s) but '{2}' service node(s) were given for the service graph".format( + service_graph, number_of_nodes_in_template, user_number_devices + ) + ) + + if devices is not None: + service_node_type_names_from_template = [type.get("name") for type in service_node_types_from_template] + for index, device in enumerate(devices): + template_node_type = service_node_type_names_from_template[index] + apic_type = "OTHERS" + if template_node_type == "firewall": + apic_type = "FW" + elif template_node_type == "load-balancer": + apic_type = "ADC" + query_device_data = mso.lookup_service_node_device(site_id, tenant, device.get("name"), apic_type) + devices_payload.append( + dict( + device=dict( + dn=query_device_data.get("dn"), + funcTyp=query_device_data.get("funcType"), + ), + serviceNodeRef=dict( + serviceNodeName=template_node_type, + serviceGraphName=service_graph, + templateName=template, + schemaId=schema_id, + ), + ), + ) + + payload = dict( + serviceGraphRef=dict( + serviceGraphName=service_graph, + templateName=template, + schemaId=schema_id, + ), + serviceNodes=devices_payload, + ) + + mso.sanitize(payload, collate=True) + + if not mso.existing: + ops.append(dict(op="add", path=service_graphs_path, value=payload)) + else: + ops.append(dict(op="replace", path=service_graph_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf.py new file mode 100644 index 000000000..a0b864a8b --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_vrf +short_description: Manage site-local VRFs in schema template +description: +- Manage site-local VRFs in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + vrf: + description: + - The name of the VRF to manage. + type: str + aliases: [ name ] + 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 +seealso: +- module: cisco.mso.mso_schema_site +- module: cisco.mso.mso_schema_template_vrf +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site VRF + cisco.mso.mso_schema_site_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + state: present + delegate_to: localhost + +- name: Remove a site VRF + cisco.mso.mso_schema_site_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + state: absent + delegate_to: localhost + +- name: Query a specific site VRF + cisco.mso.mso_schema_site_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site VRFs + cisco.mso.mso_schema_site_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["vrf"]], + ["state", "present", ["vrf"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + vrf = module.params.get("vrf") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site/template '{0}-{1}' does not exist. Existing sites/templates: {2}".format(site, template, ", ".join(sites))) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get VRF + vrf_ref = mso.vrf_ref(schema_id=schema_id, template=template, vrf=vrf) + vrfs = [v.get("vrfRef") for v in schema_obj.get("sites")[site_idx]["vrfs"]] + if vrf is not None and vrf_ref in vrfs: + vrf_idx = vrfs.index(vrf_ref) + vrf_path = "/sites/{0}/vrfs/{1}".format(site_template, vrf) + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx] + + if state == "query": + if vrf is None: + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"] + elif not mso.existing: + mso.fail_json(msg="VRF '{vrf}' not found".format(vrf=vrf)) + mso.exit_json() + + vrfs_path = "/sites/{0}/vrfs".format(site_template) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=vrf_path)) + + elif state == "present": + payload = dict( + vrfRef=dict( + schemaId=schema_id, + templateName=template, + vrfName=vrf, + ), + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=vrf_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=vrfs_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region.py new file mode 100644 index 000000000..5ac5804e6 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region.py @@ -0,0 +1,274 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_vrf_region +short_description: Manage site-local VRF regions in schema template +description: +- Manage site-local VRF regions in schema template on Cisco ACI Multi-Site. +author: +- Anvitha Jain (@anvitha-jain) +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + vrf: + description: + - The name of the VRF. + type: str + required: true + region: + description: + - The name of the region to manage. + type: str + aliases: [ name ] + vpn_gateway_router: + description: + - Whether VPN Gateway Router is enabled or not. + type: bool + container_overlay: + description: + - The name of the context profile type. + - This is supported on versions of MSO that are 3.3 or greater. + type: bool + underlay_context_profile: + description: + - The name of the context profile type. + - This parameter can only be added when container_overlay is True. + - This is supported on versions of MSO that are 3.3 or greater. + type: dict + suboptions: + vrf: + description: + - The name of the VRF to associate with underlay context profile. + type: str + required: true + region: + description: + - The name of the region associated with underlay context profile VRF. + type: str + required: true + 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 +notes: +- Due to restrictions of the MSO REST API, this module cannot create empty region (i.e. regions without cidrs) + Use the M(cisco.mso.mso_schema_site_vrf_region_cidr) to automatically create regions with cidrs. +seealso: +- module: cisco.mso.mso_schema_site_vrf +- module: cisco.mso.mso_schema_template_vrf +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Remove VPN Gateway Router at site VRF Region + cisco.mso.mso_schema_site_vrf_region: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + vpn_gateway_router: false + state: present + delegate_to: localhost + +- name: Remove a site VRF region + cisco.mso.mso_schema_site_vrf_region: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + state: absent + delegate_to: localhost + +- name: Query a specific site VRF region + cisco.mso.mso_schema_site_vrf_region: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site VRF regions + cisco.mso.mso_schema_site_vrf_region: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="str", required=True), + region=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + vpn_gateway_router=dict(type="bool"), + container_overlay=dict(type="bool"), + underlay_context_profile=dict( + type="dict", + options=dict( + vrf=dict(type="str", required=True), + region=dict(type="str", required=True), + ), + ), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["region"]], + ["state", "present", ["region"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + vrf = module.params.get("vrf") + region = module.params.get("region") + vpn_gateway_router = module.params.get("vpn_gateway_router") + container_overlay = module.params.get("container_overlay") + underlay_context_profile = module.params.get("underlay_context_profile") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site-template association '{0}-{1}' does not exist.".format(site, template)) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get VRF + vrf_ref = mso.vrf_ref(schema_id=schema_id, template=template, vrf=vrf) + vrfs = [v.get("vrfRef") for v in schema_obj.get("sites")[site_idx]["vrfs"]] + vrfs_name = [mso.dict_from_ref(v).get("vrfName") for v in vrfs] + if vrf_ref not in vrfs: + mso.fail_json(msg="Provided vrf '{0}' does not exist. Existing vrfs: {1}".format(vrf, ", ".join(vrfs_name))) + vrf_idx = vrfs.index(vrf_ref) + + # Get Region + regions = [r.get("name") for r in schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"]] + if region is not None and region in regions: + region_idx = regions.index(region) + region_path = "/sites/{0}/vrfs/{1}/regions/{2}".format(site_template, vrf, region) + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx] + + if state == "query": + if region is None: + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"] + elif not mso.existing: + mso.fail_json(msg="Region '{region}' not found".format(region=region)) + mso.exit_json() + + regions_path = "/sites/{0}/vrfs/{1}/regions".format(site_template, vrf) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=region_path)) + + elif state == "present": + payload = dict( + name=region, + isVpnGatewayRouter=vpn_gateway_router, + ) + + if container_overlay: + payload["contextProfileType"] = "container-overlay" + if mso.existing: + underlay_dict = dict( + vrfRef=dict(schemaId=schema_id, templateName=template, vrfName=underlay_context_profile["vrf"]), + regionName=underlay_context_profile["region"], + ) + payload["underlayCtxProfile"] = underlay_dict + + mso.sanitize(payload, collate=True) + if mso.existing: + ops.append(dict(op="replace", path=region_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=regions_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr.py new file mode 100644 index 000000000..ef409f710 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr.py @@ -0,0 +1,304 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2020, Lionel Hercot (@lhercot) <lhercot@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_vrf_region_cidr +short_description: Manage site-local VRF region CIDRs in schema template +description: +- Manage site-local VRF region CIDRs in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +- Lionel Hercot (@lhercot) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + vrf: + description: + - The name of the VRF. + type: str + required: true + region: + description: + - The name of the region. + type: str + required: true + cidr: + description: + - The name of the region CIDR to manage. + type: str + aliases: [ ip ] + primary: + description: + - Whether this is the primary CIDR. + type: bool + default: true + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_vrf_region +- module: cisco.mso.mso_schema_site_vrf_region_cidr_subnet +- module: cisco.mso.mso_schema_template_vrf +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site VRF region CIDR + cisco.mso.mso_schema_site_vrf_region_cidr: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + cidr: 14.14.14.1/24 + state: present + delegate_to: localhost + +- name: Remove a site VRF region CIDR + cisco.mso.mso_schema_site_vrf_region_cidr: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + cidr: 14.14.14.1/24 + state: absent + delegate_to: localhost + +- name: Query a specific site VRF region CIDR + cisco.mso.mso_schema_site_vrf_region_cidr: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + cidr: 14.14.14.1/24 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site VRF region CIDR + cisco.mso.mso_schema_site_vrf_region_cidr: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="str", required=True), + region=dict(type="str", required=True), + cidr=dict(type="str", aliases=["ip"]), # This parameter is not required for querying all objects + primary=dict(type="bool", default=True), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["cidr"]], + ["state", "present", ["cidr"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + vrf = module.params.get("vrf") + region = module.params.get("region") + cidr = module.params.get("cidr") + primary = module.params.get("primary") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + payload = dict() + op_path = "" + new_cidr = dict( + ip=cidr, + primary=primary, + ) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + all_sites = schema_obj.get("sites") + sites = [] + if all_sites is not None: + sites = [(s.get("siteId"), s.get("templateName")) for s in all_sites] + + # Get VRF + vrf_ref = mso.vrf_ref(schema_id=schema_id, template=template, vrf=vrf) + template_vrfs = [a.get("name") for a in schema_obj["templates"][template_idx]["vrfs"]] + if vrf not in template_vrfs: + mso.fail_json(msg="Provided vrf '{0}' does not exist. Existing vrfs: {1}".format(vrf, ", ".join(template_vrfs))) + + # if site-template does not exist, create it + if (site_id, template) not in sites: + op_path = "/sites/-" + payload.update( + siteId=site_id, + templateName=template, + vrfs=[ + dict( + vrfRef=dict( + schemaId=schema_id, + templateName=template, + vrfName=vrf, + ), + regions=[dict(name=region, cidrs=[new_cidr])], + ) + ], + ) + + else: + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # If vrf not at site level but exists at template level + vrfs = [v.get("vrfRef") for v in schema_obj.get("sites")[site_idx]["vrfs"]] + if vrf_ref not in vrfs: + op_path = "/sites/{0}/vrfs/-".format(site_template) + payload.update( + vrfRef=dict( + schemaId=schema_id, + templateName=template, + vrfName=vrf, + ), + regions=[dict(name=region, cidrs=[new_cidr])], + ) + else: + # Update vrf index at site level + vrf_idx = vrfs.index(vrf_ref) + + # Get Region + regions = [r.get("name") for r in schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"]] + if region not in regions: + op_path = "/sites/{0}/vrfs/{1}/regions/-".format(site_template, vrf) + payload.update(name=region, cidrs=[new_cidr]) + else: + region_idx = regions.index(region) + + # Get CIDR + cidrs = [c.get("ip") for c in schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx]["cidrs"]] + if cidr is not None: + if cidr in cidrs: + cidr_idx = cidrs.index(cidr) + # FIXME: Changes based on index are DANGEROUS + cidr_path = "/sites/{0}/vrfs/{1}/regions/{2}/cidrs/{3}".format(site_template, vrf, region, cidr_idx) + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx]["cidrs"][cidr_idx] + op_path = "/sites/{0}/vrfs/{1}/regions/{2}/cidrs/-".format(site_template, vrf, region) + payload = new_cidr + + if state == "query": + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site-template association '{0}-{1}' does not exist.".format(site, template)) + elif vrf_ref not in vrfs: + mso.fail_json(msg="Provided vrf '{0}' does not exist at site level.".format(vrf)) + elif not regions or region not in regions: + mso.fail_json(msg="Provided region '{0}' does not exist. Existing regions: {1}".format(region, ", ".join(regions))) + elif cidr is None and not payload: + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx]["cidrs"] + elif not mso.existing: + mso.fail_json(msg="CIDR IP '{cidr}' not found".format(cidr=cidr)) + mso.exit_json() + + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=cidr_path)) + + elif state == "present": + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=cidr_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=op_path, value=mso.sent)) + + mso.existing = new_cidr + + if not module.check_mode and mso.previous != mso.existing: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr_subnet.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr_subnet.py new file mode 100644 index 000000000..85a00ea9c --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr_subnet.py @@ -0,0 +1,320 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@cisco.com> +# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2020, Lionel Hercot (@lhercot) <lhercot@cisco.com> +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_vrf_region_cidr_subnet +short_description: Manage site-local VRF regions in schema template +description: +- Manage site-local VRF regions in schema template on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +- Lionel Hercot (@lhercot) +- Anvitha Jain (@anvitha-jain) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + vrf: + description: + - The name of the VRF. + type: str + required: true + region: + description: + - The name of the region. + type: str + required: true + cidr: + description: + - The IP range of for the region CIDR. + type: str + required: true + subnet: + description: + - The IP subnet of this region CIDR. + type: str + aliases: [ ip ] + private_link_label: + description: + - The private link label used to represent this subnet. + - This parameter is available for MSO version greater than 3.3. + type: str + zone: + description: + - The name of the zone for the region CIDR subnet. + - This argument is required for AWS sites. + type: str + aliases: [ name ] + vgw: + description: + - Whether this subnet is used for the Azure Gateway in Azure. + - Whether this subnet is used for the Transit Gateway Attachment in AWS. + type: bool + aliases: [ hub_network ] + hosted_vrf: + description: + - The name of hosted vrf associated with region CIDR subnet. + - This is supported on versions of MSO that are 3.3 or greater. + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_vrf_region_cidr +- module: cisco.mso.mso_schema_template_vrf +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site VRF region CIDR subnet + cisco.mso.mso_schema_site_vrf_region_cidr_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + cidr: 14.14.14.1/24 + subnet: 14.14.14.2/24 + zone: us-west-1a + state: present + delegate_to: localhost + +- name: Remove a site VRF region CIDR subnet + cisco.mso.mso_schema_site_vrf_region_cidr_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + cidr: 14.14.14.1/24 + subnet: 14.14.14.2/24 + state: absent + delegate_to: localhost + +- name: Query a specific site VRF region CIDR subnet + cisco.mso.mso_schema_site_vrf_region_cidr_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + cidr: 14.14.14.1/24 + subnet: 14.14.14.2/24 + state: query + delegate_to: localhost + register: query_result + +- name: Query all site VRF region CIDR subnet + cisco.mso.mso_schema_site_vrf_region_cidr_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + cidr: 14.14.14.1/24 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="str", required=True), + region=dict(type="str", required=True), + cidr=dict(type="str", required=True), + subnet=dict(type="str", aliases=["ip"]), # This parameter is not required for querying all objects + private_link_label=dict(type="str"), + zone=dict(type="str", aliases=["name"]), + vgw=dict(type="bool", aliases=["hub_network"]), + hosted_vrf=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", ["subnet"]], + ["state", "present", ["subnet"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + vrf = module.params.get("vrf") + region = module.params.get("region") + cidr = module.params.get("cidr") + subnet = module.params.get("subnet") + private_link_label = module.params.get("private_link_label") + zone = module.params.get("zone") + hosted_vrf = module.params.get("hosted_vrf") + vgw = module.params.get("vgw") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + sites_list = [s.get("siteId") + "/" + s.get("templateName") for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json( + msg="Provided site/siteId/template '{0}/{1}/{2}' does not exist. " + "Existing siteIds/templates: {3}".format(site, site_id, template, ", ".join(sites_list)) + ) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get VRF at site level + vrf_ref = mso.vrf_ref(schema_id=schema_id, template=template, vrf=vrf) + vrfs = [v.get("vrfRef") for v in schema_obj.get("sites")[site_idx]["vrfs"]] + + # If vrf not at site level but exists at template level + if vrf_ref not in vrfs: + mso.fail_json(msg="Provided vrf '{0}' does not exist at site level." " Use mso_schema_site_vrf_region_cidr to create it.".format(vrf)) + vrf_idx = vrfs.index(vrf_ref) + + # Get Region + regions = [r.get("name") for r in schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"]] + if region not in regions: + mso.fail_json( + msg="Provided region '{0}' does not exist. Existing regions: {1}." + " Use mso_schema_site_vrf_region_cidr to create it.".format(region, ", ".join(regions)) + ) + region_idx = regions.index(region) + + # Get CIDR + cidrs = [c.get("ip") for c in schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx]["cidrs"]] + if cidr not in cidrs: + mso.fail_json( + msg="Provided CIDR IP '{0}' does not exist. Existing CIDR IPs: {1}." + " Use mso_schema_site_vrf_region_cidr to create it.".format(cidr, ", ".join(cidrs)) + ) + cidr_idx = cidrs.index(cidr) + + # Get Subnet + subnets = [s.get("ip") for s in schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx]["cidrs"][cidr_idx]["subnets"]] + if subnet is not None and subnet in subnets: + subnet_idx = subnets.index(subnet) + # FIXME: Changes based on index are DANGEROUS + subnet_path = "/sites/{0}/vrfs/{1}/regions/{2}/cidrs/{3}/subnets/{4}".format(site_template, vrf, region, cidr_idx, subnet_idx) + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx]["cidrs"][cidr_idx]["subnets"][subnet_idx] + + if state == "query": + if subnet is None: + mso.existing = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx]["cidrs"][cidr_idx]["subnets"] + elif not mso.existing: + mso.fail_json(msg="Subnet IP '{subnet}' not found".format(subnet=subnet)) + mso.exit_json() + + subnets_path = "/sites/{0}/vrfs/{1}/regions/{2}/cidrs/{3}/subnets".format(site_template, vrf, region, cidr_idx) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=subnet_path)) + + elif state == "present": + payload = dict(ip=subnet, zone="") + + if zone is not None: + payload["zone"] = zone + if vgw is True: + payload["usage"] = "gateway" + if private_link_label is not None: + payload["privateLinkLabel"] = dict(name=private_link_label) + if hosted_vrf is not None: + payload["vrfRef"] = dict(schemaId=schema_id, templateName=template, vrfName=hosted_vrf) + payload["inEditing"] = "false" + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=subnet_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=subnets_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_hub_network.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_hub_network.py new file mode 100644 index 000000000..9df7bab4f --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_hub_network.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Cindy Zhao (@cizhao) <cizhao@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_site_vrf_region_hub_network +short_description: Manage site-local VRF region hub network in schema template +description: +- Manage site-local VRF region hub network in schema template on Cisco ACI Multi-Site. +- The 'Hub Network' feature was introduced in Multi-Site Orchestrator (MSO) version 3.0(1) for AWS and version 3.0(2) for Azure. +author: +- Cindy Zhao (@cizhao) +options: + schema: + description: + - The name of the schema. + type: str + required: true + site: + description: + - The name of the site. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + vrf: + description: + - The name of the VRF. + type: str + required: true + region: + description: + - The name of the region. + type: str + required: true + hub_network: + description: + - The hub network to be managed. + type: dict + suboptions: + name: + description: + - The name of the hub network. + - The hub-default is the default created hub network. + type: str + required: true + tenant: + description: + - The tenant name of the hub network. + type: str + required: true + 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 +notes: +- The ACI MultiSite PATCH API has a deficiency requiring some objects to be referenced by index. + This can cause silent corruption on concurrent access when changing/removing on object as + the wrong object may be referenced. This module is affected by this deficiency. +seealso: +- module: cisco.mso.mso_schema_site_vrf_region +- module: cisco.mso.mso_schema_template_vrf +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site VRF region hub network + cisco.mso.mso_schema_site_vrf_region_hub_network: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + hub_network: + name: hub-default + tenant: infra + state: present + delegate_to: localhost + +- name: Remove a site VRF region hub network + cisco.mso.mso_schema_site_vrf_region_hub_network: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + state: absent + delegate_to: localhost + +- name: Query site VRF region hub network + cisco.mso.mso_schema_site_vrf_region_hub_network: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + site: Site1 + template: Template1 + vrf: VRF1 + region: us-west-1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_hub_network_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + site=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="str", required=True), + region=dict(type="str", required=True), + hub_network=dict(type="dict", options=mso_hub_network_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["hub_network"]], + ], + ) + + schema = module.params.get("schema") + site = module.params.get("site") + template = module.params.get("template").replace(" ", "") + vrf = module.params.get("vrf") + region = module.params.get("region") + hub_network = module.params.get("hub_network") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + + # Get site + site_id = mso.lookup_site(site) + + # Get site_idx + if not schema_obj.get("sites"): + mso.fail_json(msg="No site associated with template '{0}'. Associate the site with the template using mso_schema_site.".format(template)) + sites = [(s.get("siteId"), s.get("templateName")) for s in schema_obj.get("sites")] + if (site_id, template) not in sites: + mso.fail_json(msg="Provided site-template association '{0}-{1}' does not exist.".format(site, template)) + + # Schema-access uses indexes + site_idx = sites.index((site_id, template)) + # Path-based access uses site_id-template + site_template = "{0}-{1}".format(site_id, template) + + # Get VRF + vrf_ref = mso.vrf_ref(schema_id=schema_id, template=template, vrf=vrf) + vrfs = [v.get("vrfRef") for v in schema_obj.get("sites")[site_idx]["vrfs"]] + vrfs_name = [mso.dict_from_ref(v).get("vrfName") for v in vrfs] + if vrf_ref not in vrfs: + mso.fail_json(msg="Provided vrf '{0}' does not exist. Existing vrfs: {1}".format(vrf, ", ".join(vrfs_name))) + vrf_idx = vrfs.index(vrf_ref) + + # Get Region + regions = [r.get("name") for r in schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"]] + if region not in regions: + mso.fail_json(msg="Provided region '{0}' does not exist. Existing regions: {1}".format(region, ", ".join(regions))) + region_idx = regions.index(region) + # Get Region object + region_obj = schema_obj.get("sites")[site_idx]["vrfs"][vrf_idx]["regions"][region_idx] + region_path = "/sites/{0}/vrfs/{1}/regions/{2}".format(site_template, vrf, region) + + # Get hub network + existing_hub_network = region_obj.get("cloudRsCtxProfileToGatewayRouterP") + if existing_hub_network is not None: + mso.existing = existing_hub_network + + if state == "query": + if not mso.existing: + mso.fail_json(msg="Hub network not found") + mso.exit_json() + + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=region_path + "/cloudRsCtxProfileToGatewayRouterP")) + ops.append(dict(op="replace", path=region_path + "/isTGWAttachment", value=False)) + + elif state == "present": + new_hub_network = dict( + name=hub_network.get("name"), + tenantName=hub_network.get("tenant"), + ) + payload = region_obj + payload.update( + cloudRsCtxProfileToGatewayRouterP=new_hub_network, + isTGWAttachment=True, + ) + + mso.sanitize(payload, collate=True) + + ops.append(dict(op="replace", path=region_path, value=mso.sent)) + + mso.existing = new_hub_network + + if not module.check_mode and mso.previous != mso.existing: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py new file mode 100644 index 000000000..6f4ece9ca --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template +short_description: Manage templates in schemas +description: +- Manage templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + tenant: + description: + - The tenant used for this template. + type: str + required: true + schema: + description: + - The name of the schema. + type: str + required: true + schema_description: + description: + - The description of Schema is supported on versions of MSO that are 3.3 or greater. + type: str + template_description: + description: + - The description of template is supported on versions of MSO that are 3.3 or greater. + type: str + template: + description: + - The name of the template. + type: str + aliases: [ name ] + display_name: + description: + - The name as displayed on the MSO web 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 +notes: +- Due to restrictions of the MSO REST API this module creates schemas when needed, and removes them when the last template has been removed. +seealso: +- module: cisco.mso.mso_schema +- module: cisco.mso.mso_schema_site +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new template to a schema + cisco.mso.mso_schema_template: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: Tenant 1 + schema: Schema 1 + template: Template 1 + state: present + delegate_to: localhost + +- name: Remove a template from a schema + cisco.mso.mso_schema_template: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: Tenant 1 + schema: Schema 1 + template: Template 1 + state: absent + delegate_to: localhost + +- name: Query a template + cisco.mso.mso_schema_template: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: Tenant 1 + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all templates + cisco.mso.mso_schema_template: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: Tenant 1 + schema: Schema 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + tenant=dict(type="str", required=True), + schema=dict(type="str", required=True), + schema_description=dict(type="str"), + template_description=dict(type="str"), + template=dict(type="str", aliases=["name"]), + display_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", ["template"]], + ["state", "present", ["template"]], + ], + ) + + tenant = module.params.get("tenant") + schema = module.params.get("schema") + schema_description = module.params.get("schema_description") + template_description = module.params.get("template_description") + template = module.params.get("template") + if template is not None: + template = template.replace(" ", "") + display_name = module.params.get("display_name") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_obj = mso.get_obj("schemas", displayName=schema) + + mso.existing = {} + if schema_obj: + # Schema exists + schema_path = "schemas/{id}".format(**schema_obj) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template: + if template in templates: + template_idx = templates.index(template) + mso.existing = schema_obj.get("templates")[template_idx] + else: + mso.existing = schema_obj.get("templates") + else: + schema_path = "schemas" + + if state == "query": + if not mso.existing: + if template: + mso.fail_json(msg="Template '{0}' not found".format(template)) + else: + mso.existing = [] + mso.exit_json() + + template_path = "/templates/{0}".format(template) + ops = [] + + mso.previous = mso.existing + if state == "absent": + mso.proposed = mso.sent = {} + if not schema_obj: + # There was no schema to begin with + pass + elif len(templates) == 1 and mso.existing: + # There is only one tenant, remove schema + mso.existing = {} + if not module.check_mode: + mso.request(schema_path, method="DELETE") + elif mso.existing: + # Remove existing template + mso.existing = {} + ops.append(dict(op="remove", path=template_path)) + + elif state == "present": + tenant_id = mso.lookup_tenant(tenant) + + if display_name is None: + display_name = mso.existing.get("displayName", template) + + if not schema_obj: + # Schema does not exist, so we have to create it + payload = dict( + displayName=schema, + templates=[ + dict( + name=template, + displayName=display_name, + tenantId=tenant_id, + ) + ], + sites=[], + ) + + if schema_description is not None: + payload.update(description=schema_description) + if template_description is not None: + payload["templates"][0].update(description=template_description) + + mso.existing = payload.get("templates")[0] + + if not module.check_mode: + mso.request(schema_path, method="POST", data=payload) + + elif mso.existing: + # Template exists, so we have to update it + payload = dict( + name=template, + displayName=display_name, + description=template_description, + tenantId=tenant_id, + ) + + mso.sanitize(payload, collate=True) + + ops.append(dict(op="replace", path=template_path + "/displayName", value=display_name)) + ops.append(dict(op="replace", path=template_path + "/tenantId", value=tenant_id)) + + mso.existing = mso.proposed + else: + # Template does not exist, so we have to add it + payload = dict( + name=template, + displayName=display_name, + tenantId=tenant_id, + ) + + mso.sanitize(payload, collate=True) + + ops.append(dict(op="add", path="/templates/-", value=payload)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp.py new file mode 100644 index 000000000..9922750f8 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_anp +short_description: Manage Application Network Profiles (ANPs) in schema templates +description: +- Manage ANPs in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP to manage. + type: str + aliases: [ name ] + description: + description: + - The description of ANP is supported on versions of MSO that are 3.3 or greater. + type: str + display_name: + description: + - The name as displayed on the MSO web 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 +seealso: +- module: cisco.mso.mso_schema_template +- module: cisco.mso.mso_schema_template_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new ANP + cisco.mso.mso_schema_template_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + state: present + delegate_to: localhost + +- name: Remove an ANP + cisco.mso.mso_schema_template_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + state: absent + delegate_to: localhost + +- name: Query a specific ANPs + cisco.mso.mso_schema_template_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all ANPs + cisco.mso.mso_schema_template_anp: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + description=dict(type="str"), + display_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", ["anp"]], + ["state", "present", ["anp"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + description = module.params.get("description") + display_name = module.params.get("display_name") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get ANP + anps = [a.get("name") for a in schema_obj.get("templates")[template_idx]["anps"]] + + if anp is not None and anp in anps: + anp_idx = anps.index(anp) + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx] + + if state == "query": + if anp is None: + mso.existing = schema_obj.get("templates")[template_idx]["anps"] + elif not mso.existing: + mso.fail_json(msg="ANP '{anp}' not found".format(anp=anp)) + mso.exit_json() + + anps_path = "/templates/{0}/anps".format(template) + anp_path = "/templates/{0}/anps/{1}".format(template, anp) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=anp_path)) + + elif state == "present": + if display_name is None and not mso.existing: + display_name = anp + + epgs = [] + if mso.existing: + epgs = None + + payload = dict( + name=anp, + displayName=display_name, + epgs=epgs, + ) + + if description is not None: + payload.update(description=description) + + mso.sanitize(payload, collate=True) + + if mso.existing: + if display_name is not None: + ops.append(dict(op="replace", path=anp_path + "/displayName", value=display_name)) + else: + ops.append(dict(op="add", path=anps_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if "anpRef" in mso.previous: + del mso.previous["anpRef"] + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg.py new file mode 100644 index 000000000..6c021e7f7 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg.py @@ -0,0 +1,471 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_anp_epg +short_description: Manage Endpoint Groups (EPGs) in schema templates +description: +- Manage EPGs in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +- Anvitha Jain (@anvitha-jain) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG to manage. + type: str + aliases: [ name ] + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + description: + description: + - The description as displayed on the MSO web interface. + - The description is supported on versions of MSO that are 3.3 or greater. + type: str +# contracts: +# description: +# - A list of contracts associated to this ANP. +# type: list + bd: + description: + - The BD associated to this ANP. + type: dict + suboptions: + name: + description: + - The name of the BD to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced BD. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced BD. + type: str + vrf: + description: + - The VRF associated to this ANP. + type: dict + suboptions: + name: + description: + - The name of the VRF to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced VRF. + type: str + subnets: + description: + - The subnets associated to this ANP. + type: list + elements: dict + suboptions: + subnet: + description: + - The IP range in CIDR notation. + type: str + required: true + aliases: [ ip ] + description: + description: + - The description of this subnet. + type: str + scope: + description: + - The scope of the subnet. + type: str + default: private + choices: [ private, public ] + shared: + description: + - Whether this subnet is shared between VRFs. + type: bool + default: false + no_default_gateway: + description: + - Whether this subnet has a default gateway. + type: bool + default: false + useg_epg: + description: + - Whether this is a USEG EPG. + type: bool +# useg_epg_attributes: +# description: +# - A dictionary consisting of USEG attributes. +# type: dict + intra_epg_isolation: + description: + - Whether intra EPG isolation is enforced. + - When not specified, this parameter defaults to C(unenforced). + type: str + choices: [ enforced, unenforced ] + intersite_multicast_source: + description: + - Whether intersite multicast source is enabled. + - When not specified, this parameter defaults to C(no). + type: bool + proxy_arp: + description: + - Whether proxy arp is enabled. + - When not specified, this parameter defaults to C(no). + type: bool + preferred_group: + description: + - Whether this EPG is added to preferred group or not. + - When not specified, this parameter defaults to C(no). + type: bool + qos_level: + description: + - Quality of Service (QoS) allows you to classify the network traffic in the fabric. + - It helps prioritize and police the traffic flow to help avoid congestion in the network. + - The Contract QoS Level parameter is supported on versions of MSO that are 3.1 or greater. + type: str + epg_type: + description: + - The EPG type parameter is supported on versions of MSO that are 3.3 or greater. + type: str + choices: [ application, service ] + deployment_type: + description: + - The deployment_type parameter indicates how and where the service is deployed. + - This parameter is available only when epg_type is service. + type: str + choices: [ cloud_native, cloud_native_managed, third_party ] + access_type: + description: + - This parameter indicates how the service will be accessed. + - It is only available when epg_type is service. + type: str + choices: [ private, public, public_and_private ] + service_type: + description: + - The service_type parameter refers to the type of cloud services. + - Only certain deployment types, and certain access types within each deployment type, are supported for each service type. + - This parameter is available only when epg_type is service. + 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 +seealso: +- module: cisco.mso.mso_schema_template_anp +- module: cisco.mso.mso_schema_template_anp_epg_subnet +- module: cisco.mso.mso_schema_template_bd +- module: cisco.mso.mso_schema_template_contract_filter +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new EPG + cisco.mso.mso_schema_template_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + bd: + name: bd1 + vrf: + name: vrf1 + state: present + delegate_to: localhost + +- name: Add a new EPG with preferred group. + cisco.mso.mso_schema_template_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + state: present + preferred_group: true + delegate_to: localhost + +- name: Remove an EPG + cisco.mso.mso_schema_template_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + bd: + name: bd1 + vrf: + name: vrf1 + state: absent + delegate_to: localhost + +- name: Query a specific EPG + cisco.mso.mso_schema_template_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + bd: + name: bd1 + vrf: + name: vrf1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all EPGs + cisco.mso.mso_schema_template_anp_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + bd: + name: bd1 + vrf: + name: vrf1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_reference_spec, mso_epg_subnet_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + description=dict(type="str"), + bd=dict(type="dict", options=mso_reference_spec()), + vrf=dict(type="dict", options=mso_reference_spec()), + display_name=dict(type="str"), + useg_epg=dict(type="bool"), + intra_epg_isolation=dict(type="str", choices=["enforced", "unenforced"]), + intersite_multicast_source=dict(type="bool"), + proxy_arp=dict(type="bool"), + subnets=dict(type="list", elements="dict", options=mso_epg_subnet_spec()), + qos_level=dict(type="str"), + epg_type=dict(type="str", choices=["application", "service"]), + deployment_type=dict(type="str", choices=["cloud_native", "cloud_native_managed", "third_party"]), + service_type=dict(type="str"), + access_type=dict(type="str", choices=["private", "public", "public_and_private"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + preferred_group=dict(type="bool"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["epg"]], + ["state", "present", ["epg"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + description = module.params.get("description") + display_name = module.params.get("display_name") + bd = module.params.get("bd") + if bd is not None and bd.get("template") is not None: + bd["template"] = bd.get("template").replace(" ", "") + vrf = module.params.get("vrf") + if vrf is not None and vrf.get("template") is not None: + vrf["template"] = vrf.get("template").replace(" ", "") + useg_epg = module.params.get("useg_epg") + intra_epg_isolation = module.params.get("intra_epg_isolation") + intersite_multicast_source = module.params.get("intersite_multicast_source") + proxy_arp = module.params.get("proxy_arp") + subnets = module.params.get("subnets") + qos_level = module.params.get("qos_level") + epg_type = module.params.get("epg_type") + deployment_type = module.params.get("deployment_type") + service_type = module.params.get("service_type") + access_type = module.params.get("access_type") + state = module.params.get("state") + preferred_group = module.params.get("preferred_group") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get ANP + anps = [a.get("name") for a in schema_obj.get("templates")[template_idx]["anps"]] + if anp not in anps: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps))) + anp_idx = anps.index(anp) + + # Get EPG + epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"]] + if epg is not None and epg in epgs: + epg_idx = epgs.index(epg) + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx] + + if state == "query": + if epg is None: + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"] + elif not mso.existing: + mso.fail_json(msg="EPG '{epg}' not found".format(epg=epg)) + + if "bdRef" in mso.existing: + mso.existing["bdRef"] = mso.dict_from_ref(mso.existing["bdRef"]) + if "vrfRef" in mso.existing: + mso.existing["vrfRef"] = mso.dict_from_ref(mso.existing["vrfRef"]) + mso.exit_json() + + epgs_path = "/templates/{0}/anps/{1}/epgs".format(template, anp) + epg_path = "/templates/{0}/anps/{1}/epgs/{2}".format(template, anp, epg) + service_path = "{0}/cloudServiceEpgConfig".format(epg_path) + ops = [] + cloud_service_epg_config = {} + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=epg_path)) + + elif state == "present": + bd_ref = mso.make_reference(bd, "bd", schema_id, template) + vrf_ref = mso.make_reference(vrf, "vrf", schema_id, template) + subnets = mso.make_subnets(subnets, is_bd_subnet=False) + + if display_name is None and not mso.existing: + display_name = epg + + payload = dict( + name=epg, + displayName=display_name, + uSegEpg=useg_epg, + intraEpg=intra_epg_isolation, + mCastSource=intersite_multicast_source, + proxyArp=proxy_arp, + # FIXME: Missing functionality + # uSegAttrs=[], + subnets=subnets, + bdRef=bd_ref, + preferredGroup=preferred_group, + vrfRef=vrf_ref, + ) + if description is not None: + payload.update(description=description) + if qos_level is not None: + payload.update(prio=qos_level) + if epg_type is not None: + payload.update(epgType=epg_type) + + mso.sanitize(payload, collate=True) + + if mso.existing: + # Clean contractRef to fix api issue + for contract in mso.sent.get("contractRelationships"): + contract["contractRef"] = mso.dict_from_ref(contract.get("contractRef")) + ops.append(dict(op="replace", path=epg_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=epgs_path + "/-", value=mso.sent)) + + if epg_type == "service": + access_type_map = { + "private": "Private", + "public": "Public", + "public_and_private": "PublicAndPrivate", + } + deployment_type_map = { + "cloud_native": "CloudNative", + "cloud_native_managed": "CloudNativeManaged", + "third_party": "Third-party", + } + if cloud_service_epg_config != {}: + cloud_service_epg_config.update( + dict(deploymentType=deployment_type_map[deployment_type], serviceType=service_type, accessType=access_type_map[access_type]) + ) + ops.append(dict(op="replace", path=service_path, value=cloud_service_epg_config)) + else: + cloud_service_epg_config.update( + dict(deploymentType=deployment_type_map[deployment_type], serviceType=service_type, accessType=access_type_map[access_type]) + ) + ops.append(dict(op="add", path=service_path, value=cloud_service_epg_config)) + + mso.existing = mso.proposed + + if "epgRef" in mso.previous: + del mso.previous["epgRef"] + if "bdRef" in mso.previous and mso.previous["bdRef"] != "": + mso.previous["bdRef"] = mso.dict_from_ref(mso.previous["bdRef"]) + if "vrfRef" in mso.previous and mso.previous["bdRef"] != "": + mso.previous["vrfRef"] = mso.dict_from_ref(mso.previous["vrfRef"]) + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_contract.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_contract.py new file mode 100644 index 000000000..d8c881d5d --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_contract.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_anp_epg_contract +short_description: Manage EPG contracts in schema templates +description: +- Manage EPG contracts in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG to manage. + type: str + required: true + contract: + description: + - A contract associated to this EPG. + type: dict + suboptions: + name: + description: + - The name of the Contract to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced BD. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced BD. + type: str + type: + description: + - The type of contract. + type: str + required: true + choices: [ consumer, provider ] + 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 +seealso: +- module: cisco.mso.mso_schema_template_anp_epg +- module: cisco.mso.mso_schema_template_contract_filter +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a contract to an EPG + cisco.mso.mso_schema_template_anp_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + contract: + name: Contract 1 + type: consumer + state: present + delegate_to: localhost + +- name: Remove a Contract + cisco.mso.mso_schema_template_anp_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + contract: + name: Contract 1 + state: absent + delegate_to: localhost + +- name: Query a specific Contract + cisco.mso.mso_schema_template_anp_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + contract: + name: Contract 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all Contracts + cisco.mso.mso_schema_template_anp_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_contractref_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + contract=dict(type="dict", options=mso_contractref_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["contract"]], + ["state", "present", ["contract"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + contract = module.params.get("contract") + if contract is not None and contract.get("template") is not None: + contract["template"] = contract.get("template").replace(" ", "") + state = module.params.get("state") + + mso = MSOModule(module) + + if contract: + if contract.get("schema") is None: + contract["schema"] = schema + contract["schema_id"] = mso.lookup_schema(contract.get("schema")) + if contract.get("template") is None: + contract["template"] = template + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get ANP + anps = [a.get("name") for a in schema_obj.get("templates")[template_idx]["anps"]] + if anp not in anps: + mso.fail_json(msg="Provided anp '{0}' does not exist. Existing anps: {1}".format(anp, ", ".join(anps))) + anp_idx = anps.index(anp) + + # Get EPG + epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"]] + if epg not in epgs: + mso.fail_json(msg="Provided epg '{epg}' does not exist. Existing epgs: {epgs}".format(epg=epg, epgs=", ".join(epgs))) + epg_idx = epgs.index(epg) + + # Get Contract + if contract: + contracts = [ + (c.get("contractRef"), c.get("relationshipType")) + for c in schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["contractRelationships"] + ] + contract_ref = mso.contract_ref(**contract) + if (contract_ref, contract.get("type")) in contracts: + contract_idx = contracts.index((contract_ref, contract.get("type"))) + contract_path = "/templates/{0}/anps/{1}/epgs/{2}/contractRelationships/{3}".format(template, anp, epg, contract_idx) + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["contractRelationships"][contract_idx] + + if state == "query": + if not contract: + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["contractRelationships"] + elif not mso.existing: + mso.fail_json(msg="Contract '{0}' not found".format(contract_ref)) + + if "contractRef" in mso.existing: + mso.existing["contractRef"] = mso.dict_from_ref(mso.existing.get("contractRef")) + mso.exit_json() + + contracts_path = "/templates/{0}/anps/{1}/epgs/{2}/contractRelationships".format(template, anp, epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=contract_path)) + + elif state == "present": + payload = dict( + relationshipType=contract.get("type"), + contractRef=dict( + contractName=contract.get("name"), + templateName=contract.get("template"), + schemaId=contract.get("schema_id"), + ), + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=contract_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=contracts_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if "contractRef" in mso.previous: + mso.previous["contractRef"] = mso.dict_from_ref(mso.previous.get("contractRef")) + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_selector.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_selector.py new file mode 100644 index 000000000..bd98fc321 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_selector.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Cindy Zhao (@cizhao) <cizhao@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_anp_epg_selector +short_description: Manage EPG selector in schema templates +description: +- Manage EPG selector in schema templates on Cisco ACI Multi-Site. +author: +- Cindy Zhao (@cizhao) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG to manage. + type: str + required: true + selector: + description: + - The name of the selector. + type: str + expressions: + description: + - Expressions associated to this selector. + type: list + elements: dict + suboptions: + type: + description: + - The name of the expression. + required: true + type: str + aliases: [ tag ] + operator: + description: + - The operator associated to the expression. + required: true + type: str + choices: [ not_in, in, equals, not_equals, has_key, does_not_have_key ] + value: + description: + - The value associated to the expression. + - If the operator is in or not_in, the value should be a comma separated str. + 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 +seealso: +- module: cisco.mso.mso_schema_template_anp_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a selector to an EPG + cisco.mso.mso_schema_template_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + selector: selector_1 + expressions: + - type: expression_1 + operator: in + value: test + state: present + delegate_to: localhost + +- name: Remove a Selector + cisco.mso.mso_schema_template_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + selector: selector_1 + state: absent + delegate_to: localhost + +- name: Query a specific Selector + cisco.mso.mso_schema_template_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + selector: selector_1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all Selectors + cisco.mso.mso_schema_template_anp_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_expression_spec + +EXPRESSION_KEYS = { + "not_in": "notIn", + "not_equals": "notEquals", + "has_key": "keyExist", + "does_not_have_key": "keyNotExist", + "in": "in", + "equals": "equals", +} + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + selector=dict(type="str"), + expressions=dict(type="list", elements="dict", options=mso_expression_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["selector"]], + ["state", "present", ["selector"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + selector = module.params.get("selector") + expressions = module.params.get("expressions") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(templates)) + ) + template_idx = templates.index(template) + + # Get ANP + anps = [a.get("name") for a in schema_obj.get("templates")[template_idx]["anps"]] + if anp not in anps: + mso.fail_json(msg="Provided anp '{anp}' does not exist. Existing anps: {anps}".format(anp=anp, anps=", ".join(anps))) + anp_idx = anps.index(anp) + + # Get EPG + epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"]] + if epg not in epgs: + mso.fail_json(msg="Provided epg '{epg}' does not exist. Existing epgs: {epgs}".format(epg=epg, epgs=", ".join(epgs))) + epg_idx = epgs.index(epg) + + # Get Selector + if selector and " " in selector: + mso.fail_json(msg="There should not be any space in selector name.") + selectors = [s.get("name") for s in schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["selectors"]] + if selector in selectors: + selector_idx = selectors.index(selector) + selector_path = "/templates/{0}/anps/{1}/epgs/{2}/selectors/{3}".format(template, anp, epg, selector_idx) + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["selectors"][selector_idx] + + if state == "query": + if selector is None: + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["selectors"] + elif not mso.existing: + mso.fail_json(msg="Selector '{selector}' not found".format(selector=selector)) + mso.exit_json() + + selectors_path = "/templates/{0}/anps/{1}/epgs/{2}/selectors/-".format(template, anp, epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=selector_path)) + + elif state == "present": + # Get expressions + all_expressions = [] + if expressions: + for expression in expressions: + tag = expression.get("type") + operator = expression.get("operator") + value = expression.get("value") + if " " in tag: + mso.fail_json(msg="There should not be any space in 'type' attribute of expression '{0}'".format(tag)) + if operator in ["has_key", "does_not_have_key"] and value: + mso.fail_json(msg="Attribute 'value' is not supported for operator '{0}' in expression '{1}'".format(operator, tag)) + if operator in ["not_in", "in", "equals", "not_equals"] and not value: + mso.fail_json(msg="Attribute 'value' needed for operator '{0}' in expression '{1}'".format(operator, tag)) + all_expressions.append( + dict( + key="Custom:" + tag, + operator=EXPRESSION_KEYS.get(operator), + value=value, + ) + ) + + payload = dict( + name=selector, + expressions=all_expressions, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=selector_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=selectors_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_subnet.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_subnet.py new file mode 100644 index 000000000..b060ddd5e --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_subnet.py @@ -0,0 +1,256 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_anp_epg_subnet +short_description: Manage EPG subnets in schema templates +description: +- Manage EPG subnets in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + anp: + description: + - The name of the ANP. + type: str + required: true + epg: + description: + - The name of the EPG to manage. + type: str + required: true + subnet: + description: + - The IP range in CIDR notation. + type: str + required: true + aliases: [ ip ] + description: + description: + - The description of this subnet. + type: str + scope: + description: + - The scope of the subnet. + type: str + default: private + choices: [ private, public ] + shared: + description: + - Whether this subnet is shared between VRFs. + type: bool + default: false + no_default_gateway: + description: + - Whether this subnet has a default gateway. + type: bool + default: false + 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 +notes: +- Due to restrictions of the MSO REST API concurrent modifications to EPG subnets can be dangerous and corrupt data. +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new subnet to an EPG + cisco.mso.mso_schema_template_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + subnet: 10.0.0.0/24 + state: present + delegate_to: localhost + +- name: Remove a subnet from an EPG + cisco.mso.mso_schema_template_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + subnet: 10.0.0.0/24 + state: absent + delegate_to: localhost + +- name: Query a specific EPG subnet + cisco.mso.mso_schema_template_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + subnet: 10.0.0.0/24 + state: query + delegate_to: localhost + register: query_result + +- name: Query all EPGs subnets + cisco.mso.mso_schema_template_anp_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_epg_subnet_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + argument_spec.update(mso_epg_subnet_spec()) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["subnet"]], + ["state", "present", ["subnet"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + subnet = module.params.get("subnet") + description = module.params.get("description") + scope = module.params.get("scope") + shared = module.params.get("shared") + no_default_gateway = module.params.get("no_default_gateway") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(templates)) + ) + template_idx = templates.index(template) + + # Get ANP + anps = [a.get("name") for a in schema_obj.get("templates")[template_idx]["anps"]] + if anp not in anps: + mso.fail_json(msg="Provided anp '{anp}' does not exist. Existing anps: {anps}".format(anp=anp, anps=", ".join(anps))) + anp_idx = anps.index(anp) + + # Get EPG + epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"]] + if epg not in epgs: + mso.fail_json(msg="Provided epg '{epg}' does not exist. Existing epgs: {epgs}".format(epg=epg, epgs=", ".join(epgs))) + epg_idx = epgs.index(epg) + + # Get Subnet + subnets = [s.get("ip") for s in schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["subnets"]] + if subnet in subnets: + subnet_idx = subnets.index(subnet) + # FIXME: Changes based on index are DANGEROUS + subnet_path = "/templates/{0}/anps/{1}/epgs/{2}/subnets/{3}".format(template, anp, epg, subnet_idx) + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["subnets"][subnet_idx] + + if state == "query": + if subnet is None: + mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["subnets"] + elif not mso.existing: + mso.fail_json(msg="Subnet '{subnet}' not found".format(subnet=subnet)) + mso.exit_json() + + subnets_path = "/templates/{0}/anps/{1}/epgs/{2}/subnets".format(template, anp, epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.existing = {} + ops.append(dict(op="remove", path=subnet_path)) + + elif state == "present": + if not mso.existing: + if description is None: + description = subnet + if scope is None: + scope = "private" + if shared is None: + shared = False + if no_default_gateway is None: + no_default_gateway = False + + payload = dict( + ip=subnet, + description=description, + scope=scope, + shared=shared, + noDefaultGateway=no_default_gateway, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=subnet_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=subnets_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd.py new file mode 100644 index 000000000..0793e844d --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd.py @@ -0,0 +1,566 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2021, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_bd +short_description: Manage Bridge Domains (BDs) in schema templates +description: +- Manage BDs in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +- Shreyas Srish (@shrsr) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + - Display Name of template for operations can only be used in some versions of mso. + - Use the name of template instead of Display Name to avoid discrepency. + type: str + required: true + bd: + description: + - The name of the BD to manage. + type: str + aliases: [ name ] + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + description: + description: + - The description of BD is supported on versions of MSO that are 3.3 or greater. + type: str + vrf: + description: + - The VRF associated to this BD. This is required only when creating a new BD. + type: dict + suboptions: + name: + description: + - The name of the VRF to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current template. + type: str + dhcp_policy: + description: + - The DHCP Policy + type: dict + suboptions: + name: + description: + - The name of the DHCP Relay Policy + type: str + required: true + version: + description: + - The version of DHCP Relay Policy + type: int + required: true + dhcp_option_policy: + description: + - The DHCP Option Policy + type: dict + suboptions: + name: + description: + - The name of the DHCP Option Policy + type: str + required: true + version: + description: + - The version of the DHCP Option Policy + type: int + required: true + dhcp_policies: + description: + - A list DHCP Policies to be assciated with the BD + - This option can only be used on versions of MSO that are 3.1.1h or greater. + type: list + elements: dict + suboptions: + name: + description: + - The name of the DHCP Relay Policy + type: str + required: true + version: + description: + - The version of DHCP Relay Policy + type: int + required: true + dhcp_option_policy: + description: + - The DHCP Option Policy + type: dict + suboptions: + name: + description: + - The name of the DHCP Option Policy + type: str + required: true + version: + description: + - The version of the DHCP Option Policy + type: int + required: true + subnets: + description: + - The subnets associated to this BD. + type: list + elements: dict + suboptions: + subnet: + description: + - The IP range in CIDR notation. + type: str + required: true + aliases: [ ip ] + description: + description: + - The description of this subnet. + type: str + scope: + description: + - The scope of the subnet. + type: str + default: private + choices: [ private, public ] + shared: + description: + - Whether this subnet is shared between VRFs. + type: bool + default: false + no_default_gateway: + description: + - Whether this subnet has a default gateway. + type: bool + default: false + querier: + description: + - Whether this subnet is an IGMP querier. + type: bool + default: false + virtual: + description: + - Treat as Virtual IP Address. + type: bool + default: false + primary: + description: + - Treat as Primary Subnet. + - There can be only one primary subnet per address family under a BD. + - This option can only be used on versions of MSO that are 3.1.1h or greater. + type: bool + default: false + intersite_bum_traffic: + description: + - Whether to allow intersite BUM traffic. + type: bool + optimize_wan_bandwidth: + description: + - Whether to optimize WAN bandwidth. + type: bool + layer2_stretch: + description: + - Whether to enable L2 stretch. + type: bool + default: true + layer2_unknown_unicast: + description: + - Layer2 unknown unicast. + type: str + choices: [ flood, proxy ] + layer3_multicast: + description: + - Whether to enable L3 multicast. + type: bool + unknown_multicast_flooding: + description: + - Unknown Multicast Flooding can either be Flood or Optimized Flooding. + type: str + choices: [ flood, optimized_flooding ] + multi_destination_flooding: + description: + - Multi-Destination Flooding can either be Flood in BD, Drop or Flood in Encapsulation. + - Flood in Encapsulation is only supported on versions of MSO that are 3.3 or greater. + type: str + choices: [ flood_in_bd, drop, encap-flood ] + ipv6_unknown_multicast_flooding: + description: + - IPv6 Unknown Multicast Flooding can either be Flood or Optimized Flooding + type: str + choices: [ flood, optimized_flooding ] + arp_flooding: + description: + - ARP Flooding + type: bool + virtual_mac_address: + description: + - Virtual MAC Address + type: str + unicast_routing: + description: + - Unicast Routing + - This option can only be used on versions of MSO that are 3.1.1h or greater. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new BD + cisco.mso.mso_schema_template_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + vrf: + name: VRF1 + state: present + delegate_to: localhost + +- name: Add a new BD from another Schema + mso_schema_template_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + vrf: + name: VRF1 + schema: Schema Origin + template: Template Origin + state: present + delegate_to: localhost + +- name: Add bd with options available on version 3.1 + mso_schema_template_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + intersite_bum_traffic: true + optimize_wan_bandwidth: false + layer2_stretch: true + layer2_unknown_unicast: flood + layer3_multicast: false + unknown_multicast_flooding: flood + multi_destination_flooding: drop + ipv6_unknown_multicast_flooding: flood + arp_flooding: false + virtual_mac_address: 00:00:5E:00:01:3C + subnets: + - subnet: 10.0.0.128/24 + - subnet: 10.0.1.254/24 + description: 1234567890 + - ip: 192.168.0.254/24 + description: "My description for a subnet" + scope: private + shared: false + no_default_gateway: true + vrf: + name: vrf1 + schema: Test + template: Template1 + dhcp_policy: + name: ansible_test + version: 1 + dhcp_option_policy: + name: ansible_test_option + version: 1 + state: present + +- name: Add bd with options available on version 3.1.1h or greater + mso_schema_template_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + intersite_bum_traffic: true + optimize_wan_bandwidth: false + layer2_stretch: true + layer2_unknown_unicast: flood + layer3_multicast: false + unknown_multicast_flooding: flood + multi_destination_flooding: drop + ipv6_unknown_multicast_flooding: flood + arp_flooding: false + virtual_mac_address: 00:00:5E:00:01:3C + unicast_routing: true + subnets: + - subnet: 10.0.0.128/24 + primary: true + - subnet: 10.0.1.254/24 + description: 1234567890 + virtual: true + - ip: 192.168.0.254/24 + description: "My description for a subnet" + scope: private + shared: false + no_default_gateway: true + vrf: + name: vrf1 + schema: Schema1 + template: Template1 + dhcp_policies: + - name: ansible_test + version: 1 + dhcp_option_policy: + name: ansible_test_option + version: 1 + - name: ansible_test2 + version: 1 + dhcp_option_policy: + name: ansible_test_option2 + version: 1 + - name: ansible_test3 + version: 1 + dhcp_option_policy: + name: ansible_test_option + version: 1 + state: present + delegate_to: localhost + +- name: Remove a BD + cisco.mso.mso_schema_template_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD1 + state: absent + delegate_to: localhost + +- name: Query a specific BD + cisco.mso.mso_schema_template_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all BDs + cisco.mso.mso_schema_template_bd: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_reference_spec, mso_bd_subnet_spec, mso_dhcp_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + bd=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + display_name=dict(type="str"), + description=dict(type="str"), + intersite_bum_traffic=dict(type="bool"), + optimize_wan_bandwidth=dict(type="bool"), + layer2_stretch=dict(type="bool", default="true"), + layer2_unknown_unicast=dict(type="str", choices=["flood", "proxy"]), + layer3_multicast=dict(type="bool"), + vrf=dict(type="dict", options=mso_reference_spec()), + dhcp_policy=dict(type="dict", options=mso_dhcp_spec()), + dhcp_policies=dict(type="list", elements="dict", options=mso_dhcp_spec()), + subnets=dict(type="list", elements="dict", options=mso_bd_subnet_spec()), + unknown_multicast_flooding=dict(type="str", choices=["optimized_flooding", "flood"]), + multi_destination_flooding=dict(type="str", choices=["flood_in_bd", "drop", "encap-flood"]), + ipv6_unknown_multicast_flooding=dict(type="str", choices=["optimized_flooding", "flood"]), + arp_flooding=dict(type="bool"), + virtual_mac_address=dict(type="str"), + unicast_routing=dict(type="bool"), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["bd"]], + ["state", "present", ["bd", "vrf"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + bd = module.params.get("bd") + display_name = module.params.get("display_name") + description = module.params.get("description") + intersite_bum_traffic = module.params.get("intersite_bum_traffic") + optimize_wan_bandwidth = module.params.get("optimize_wan_bandwidth") + layer2_stretch = module.params.get("layer2_stretch") + layer2_unknown_unicast = module.params.get("layer2_unknown_unicast") + layer3_multicast = module.params.get("layer3_multicast") + vrf = module.params.get("vrf") + if vrf is not None and vrf.get("template") is not None: + vrf["template"] = vrf.get("template").replace(" ", "") + dhcp_policy = module.params.get("dhcp_policy") + dhcp_policies = module.params.get("dhcp_policies") + subnets = module.params.get("subnets") + unknown_multicast_flooding = module.params.get("unknown_multicast_flooding") + multi_destination_flooding = module.params.get("multi_destination_flooding") + ipv6_unknown_multicast_flooding = module.params.get("ipv6_unknown_multicast_flooding") + arp_flooding = module.params.get("arp_flooding") + virtual_mac_address = module.params.get("virtual_mac_address") + unicast_routing = module.params.get("unicast_routing") + state = module.params.get("state") + + mso = MSOModule(module) + + # Map choices + if unknown_multicast_flooding == "optimized_flooding": + unknown_multicast_flooding = "opt-flood" + if ipv6_unknown_multicast_flooding == "optimized_flooding": + ipv6_unknown_multicast_flooding = "opt-flood" + if multi_destination_flooding == "flood_in_bd": + multi_destination_flooding = "bd-flood" + + if layer2_unknown_unicast == "flood": + arp_flooding = True + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get BDs + bds = [b.get("name") for b in schema_obj.get("templates")[template_idx]["bds"]] + + if bd is not None and bd in bds: + bd_idx = bds.index(bd) + mso.existing = schema_obj.get("templates")[template_idx]["bds"][bd_idx] + + if state == "query": + if bd is None: + mso.existing = schema_obj.get("templates")[template_idx]["bds"] + elif not mso.existing: + mso.fail_json(msg="BD '{bd}' not found".format(bd=bd)) + mso.exit_json() + + bds_path = "/templates/{0}/bds".format(template) + bd_path = "/templates/{0}/bds/{1}".format(template, bd) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=bd_path)) + + elif state == "present": + vrf_ref = mso.make_reference(vrf, "vrf", schema_id, template) + subnets = mso.make_subnets(subnets) + dhcp_label = mso.make_dhcp_label(dhcp_policy) + dhcp_labels = mso.make_dhcp_label(dhcp_policies) + + if display_name is None and not mso.existing: + display_name = bd + if subnets is None and not mso.existing: + subnets = [] + + payload = dict( + name=bd, + displayName=display_name, + intersiteBumTrafficAllow=intersite_bum_traffic, + optimizeWanBandwidth=optimize_wan_bandwidth, + l2UnknownUnicast=layer2_unknown_unicast, + l2Stretch=layer2_stretch, + l3MCast=layer3_multicast, + subnets=subnets, + vrfRef=vrf_ref, + dhcpLabel=dhcp_label, + unkMcastAct=unknown_multicast_flooding, + multiDstPktAct=multi_destination_flooding, + v6unkMcastAct=ipv6_unknown_multicast_flooding, + vmac=virtual_mac_address, + arpFlood=arp_flooding, + ) + + if dhcp_labels: + payload.update(dhcpLabels=dhcp_labels) + + if unicast_routing is not None: + payload.update(unicastRouting=unicast_routing) + + if description: + payload.update(description=description) + + mso.sanitize(payload, collate=True, required=["dhcpLabel", "dhcpLabels"]) + if mso.existing: + ops.append(dict(op="replace", path=bd_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=bds_path + "/-", value=mso.sent)) + mso.existing = mso.proposed + + if "bdRef" in mso.previous: + del mso.previous["bdRef"] + if "vrfRef" in mso.previous: + mso.previous["vrfRef"] = mso.vrf_dict_from_ref(mso.previous.get("vrfRef")) + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_dhcp_policy.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_dhcp_policy.py new file mode 100644 index 000000000..64fe360ea --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_dhcp_policy.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_bd_dhcp_policy +short_description: Manage BD DHCP Policy in schema templates +description: +- Manage BD DHCP policies in schema templates on Cisco ACI Multi-Site. +author: +- Shreyas Srish (@shrsr) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + bd: + description: + - The name of the BD to manage. + type: str + required: true + dhcp_policy: + description: + - The DHCP Policy + type: str + aliases: [ name ] + version: + description: + - The version of DHCP Relay Policy. + type: int + dhcp_option_policy: + description: + - The DHCP Option Policy. + type: dict + suboptions: + name: + description: + - The name of the DHCP Option Policy. + type: str + required: true + version: + description: + - The version of the DHCP Option Policy. + type: int + required: true + 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 +notes: +- This module can only be used on versions of MSO that are 3.1.1h or greater. +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new DHCP policy to a BD + cisco.mso.mso_schema_template_bd_dhcp_policy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + name: ansible_test + version: 1 + dhcp_option_policy: + name: ansible_test_option + version: 1 + state: present + delegate_to: localhost + +- name: Remove a DHCP policy from a BD + cisco.mso.mso_schema_template_bd_dhcp_policy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + name: ansible_test + version: 1 + state: absent + delegate_to: localhost + +- name: Query a specific BD DHCP Policy + cisco.mso.mso_schema_template_bd_dhcp_policy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + name: ansible_test + state: query + delegate_to: localhost + register: query_result + +- name: Query all BD DHCP Policies + cisco.mso.mso_schema_template_bd_dhcp_policy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_dhcp_option_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + bd=dict(type="str", required=True), + dhcp_policy=dict(type="str", aliases=["name"]), + version=dict(type="int"), + dhcp_option_policy=dict(type="dict", options=mso_dhcp_option_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["dhcp_policy"]], + ["state", "present", ["dhcp_policy", "version"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + bd = module.params.get("bd") + dhcp_policy = module.params.get("dhcp_policy") + dhcp_option_policy = module.params.get("dhcp_option_policy") + version = module.params.get("version") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get BD + bds = [b.get("name") for b in schema_obj.get("templates")[template_idx]["bds"]] + if bd not in bds: + mso.fail_json(msg="Provided BD '{0}' does not exist. Existing BDs: {1}".format(bd, ", ".join(bds))) + bd_idx = bds.index(bd) + + # Check if DHCP policy already exists + if dhcp_policy: + check_policy = mso.get_obj("policies/dhcp/relay", name=dhcp_policy, key="DhcpRelayPolicies") + if check_policy: + pass + else: + mso.fail_json(msg="DHCP policy '{dhcp_policy}' does not exist".format(dhcp_policy=dhcp_policy)) + + # Check if DHCP option policy already exists + if dhcp_option_policy: + check_option_policy = mso.get_obj("policies/dhcp/option", name=dhcp_option_policy.get("name"), key="DhcpRelayPolicies") + if check_option_policy: + pass + else: + mso.fail_json(msg="DHCP option policy '{dhcp_option_policy}' does not exist".format(dhcp_option_policy=dhcp_option_policy.get("name"))) + + # Get DHCP policies + dhcp_policies = [s.get("name") for s in schema_obj.get("templates")[template_idx]["bds"][bd_idx]["dhcpLabels"]] + if dhcp_policy in dhcp_policies: + dhcp_idx = dhcp_policies.index(dhcp_policy) + # FIXME: Changes based on index are DANGEROUS + dhcp_policy_path = "/templates/{0}/bds/{1}/dhcpLabels/{2}".format(template, bd, dhcp_idx) + mso.existing = schema_obj.get("templates")[template_idx]["bds"][bd_idx]["dhcpLabels"][dhcp_idx] + + if state == "query": + if dhcp_policy is None: + mso.existing = schema_obj.get("templates")[template_idx]["bds"][bd_idx]["dhcpLabels"] + elif not mso.existing: + mso.fail_json(msg="DHCP policy not associated with the bd") + mso.exit_json() + + dhcp_policy_paths = "/templates/{0}/bds/{1}/dhcpLabels".format(template, bd) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=dhcp_policy_path)) + + elif state == "present": + payload = dict( + name=dhcp_policy, + version=version, + dhcpOptionLabel=dhcp_option_policy, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=dhcp_policy_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=dhcp_policy_paths + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_subnet.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_subnet.py new file mode 100644 index 000000000..cc55eea7b --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_subnet.py @@ -0,0 +1,262 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_bd_subnet +short_description: Manage BD subnets in schema templates +description: +- Manage BD subnets in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + bd: + description: + - The name of the BD to manage. + type: str + required: true + subnet: + description: + - The IP range in CIDR notation. + type: str + aliases: [ ip ] + description: + description: + - The description of this subnet. + type: str + is_virtual_ip: + description: + - Treat as Virtual IP Address + type: bool + default: false + scope: + description: + - The scope of the subnet. + type: str + choices: [ private, public ] + shared: + description: + - Whether this subnet is shared between VRFs. + type: bool + default: false + no_default_gateway: + description: + - Whether this subnet has a default gateway. + type: bool + default: false + querier: + description: + - Whether this subnet is an IGMP querier. + type: bool + default: false + primary: + description: + - Treat as Primary Subnet. + - There can be only one primary subnet per address family under a BD. + - This option can only be used on versions of MSO that are 3.1.1h or greater. + type: bool + default: false + 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 +notes: +- Due to restrictions of the MSO REST API concurrent modifications to BD subnets can be dangerous and corrupt data. +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new subnet to a BD + cisco.mso.mso_schema_template_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + subnet: 10.0.0.0/24 + state: present + delegate_to: localhost + +- name: Remove a subset from a BD + cisco.mso.mso_schema_template_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + subnet: 10.0.0.0/24 + state: absent + delegate_to: localhost + +- name: Query a specific BD subnet + cisco.mso.mso_schema_template_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + subnet: 10.0.0.0/24 + state: query + delegate_to: localhost + register: query_result + +- name: Query all BD subnets + cisco.mso.mso_schema_template_bd_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + bd: BD 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + bd=dict(type="str", required=True), + subnet=dict(type="str", aliases=["ip"]), + description=dict(type="str"), + is_virtual_ip=dict(type="bool", default=False), + scope=dict(type="str", choices=["private", "public"]), + shared=dict(type="bool", default=False), + no_default_gateway=dict(type="bool", default=False), + querier=dict(type="bool", default=False), + primary=dict(type="bool", default=False), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["subnet"]], + ["state", "present", ["subnet"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + bd = module.params.get("bd") + subnet = module.params.get("subnet") + description = module.params.get("description") + is_virtual_ip = module.params.get("is_virtual_ip") + scope = module.params.get("scope") + shared = module.params.get("shared") + no_default_gateway = module.params.get("no_default_gateway") + querier = module.params.get("querier") + primary = module.params.get("primary") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get BD + bds = [b.get("name") for b in schema_obj.get("templates")[template_idx]["bds"]] + if bd not in bds: + mso.fail_json(msg="Provided BD '{0}' does not exist. Existing BDs: {1}".format(bd, ", ".join(bds))) + bd_idx = bds.index(bd) + + # Get Subnet + subnets = [s.get("ip") for s in schema_obj.get("templates")[template_idx]["bds"][bd_idx]["subnets"]] + if subnet in subnets: + subnet_idx = subnets.index(subnet) + # FIXME: Changes based on index are DANGEROUS + subnet_path = "/templates/{0}/bds/{1}/subnets/{2}".format(template, bd, subnet_idx) + mso.existing = schema_obj.get("templates")[template_idx]["bds"][bd_idx]["subnets"][subnet_idx] + + if state == "query": + if subnet is None: + mso.existing = schema_obj.get("templates")[template_idx]["bds"][bd_idx]["subnets"] + elif not mso.existing: + mso.fail_json(msg="Subnet IP '{subnet}' not found".format(subnet=subnet)) + mso.exit_json() + + subnets_path = "/templates/{0}/bds/{1}/subnets".format(template, bd) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=subnet_path)) + + elif state == "present": + if not mso.existing: + if description is None: + description = subnet + if scope is None: + scope = "private" + + payload = dict( + ip=subnet, + description=description, + virtual=is_virtual_ip, + scope=scope, + shared=shared, + noDefaultGateway=no_default_gateway, + querier=querier, + primary=primary, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=subnet_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=subnets_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_clone.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_clone.py new file mode 100644 index 000000000..0cd41779f --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_clone.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_clone +short_description: Clone templates +description: +- Clone templates on Cisco ACI Multi-Site. +- Clones only template objects and not site objects. +author: +- Anvitha Jain (@anvitha-jain) +options: + source_schema: + description: + - The name of the source_schema. + type: str + destination_schema: + description: + - The name of the destination_schema. + type: str + destination_tenant: + description: + - The name of the destination_schema. + type: str + source_template_name: + description: + - The name of the source template. + type: str + destination_template_name: + description: + - The name of the destination template. + type: str + destination_template_display_name: + description: + - The display name of the destination template. + type: str + state: + description: + - Use C(clone) for adding. + type: str + choices: [ clone ] + default: clone +seealso: +- module: cisco.mso.mso_schema +- module: cisco.mso.mso_schema_clone +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Clone template in the same schema + cisco.mso.mso_schema_template_clone: + host: mso_host + username: admin + password: SomeSecretPassword + source_schema: Schema1 + destination_schema: Schema1 + destination_tenant: ansible_test + source_template_name: Template1 + destination_template_name: Template1_clone + destination_template_display_name: Template1_clone + state: clone + delegate_to: localhost + +- name: Clone template to different schema + cisco.mso.mso_schema_template_clone: + host: mso_host + username: admin + password: SomeSecretPassword + source_schema: Schema1 + destination_schema: Schema2 + destination_tenant: ansible_test + source_template_name: Template2 + destination_template_name: Cloned_template_1 + destination_template_display_name: Cloned_template_1 + state: clone + delegate_to: localhost + +- name: Clone template in the same schema but different tenant attached + cisco.mso.mso_schema_template_clone: + host: mso_host + username: admin + password: SomeSecretPassword + source_schema: Schema1 + destination_schema: Schema1 + destination_tenant: common + source_template_name: Template1_clone + destination_template_name: Template1_clone_2 + state: clone + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible_collections.cisco.mso.plugins.module_utils.constants import NDO_4_UNIQUE_IDENTIFIERS +import json + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + source_schema=dict(type="str"), + destination_schema=dict(type="str"), + destination_tenant=dict(type="str"), + source_template_name=dict(type="str"), + destination_template_name=dict(type="str"), + destination_template_display_name=dict(type="str"), + state=dict(type="str", default="clone", choices=["clone"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "clone", ["source_schema", "source_template_name"]], + ], + ) + + source_schema = module.params.get("source_schema") + destination_schema = module.params.get("destination_schema") + destination_tenant = module.params.get("destination_tenant") + source_template_name = module.params.get("source_template_name") + destination_template_name = module.params.get("destination_template_name") + destination_template_display_name = module.params.get("destination_template_display_name") + state = module.params.get("state") + + mso = MSOModule(module) + + source_schema_id = None + destination_schema_id = None + destination_tenant_id = None + ops = [] + + if destination_schema is None: + destination_schema = source_schema + + if destination_template_name is None: + destination_template_name = source_template_name + + if destination_template_display_name is None: + destination_template_display_name = destination_template_name + + # Check if source and destination template are named differently if in same schema + if source_schema == destination_schema: + if source_template_name == destination_template_name: + mso.fail_json(msg="Source and destination templates in the same schema cannot have same names.") + + # Get source schema id and destination schema id + schema_summary = mso.query_objs("schemas/list-identity", key="schemas") + + for schema in schema_summary: + if schema.get("displayName") == source_schema: + source_schema_id = schema.get("id") + + if schema.get("displayName") == destination_schema: + destination_schema_id = schema.get("id") + for template in schema.get("templates"): + if template.get("name") == destination_template_name: + mso.fail_json(msg="Template with the name '{0}' already exists. Please use another name.".format(destination_template_name)) + + if source_schema_id is None: + mso.fail_json(msg="Schema with the name '{0}' does not exist.".format(source_schema)) + elif destination_schema_id is None: + mso.fail_json(msg="Schema with the name '{0}' does not exist.".format(destination_schema)) + + # Get destination schema details before change + destination_schema_path = "schemas/{0}".format(destination_schema_id) + mso.existing = mso.query_obj(destination_schema_path, displayName=destination_schema) + + if state == "clone": + # Get destination tenant id + if destination_tenant is not None: + destination_tenant_id = mso.lookup_tenant(destination_tenant) + + # Get source schema details + source_schema_path = "schemas/{0}".format(source_schema_id) + source_schema_obj = mso.query_obj(source_schema_path, displayName=source_schema) + + source_template_path = "/{0}/templates/{1}".format(source_schema_path, source_template_name) + destination_template_path = "/{0}/templates/{1}".format(destination_schema_path, destination_template_name) + + source_templates = source_schema_obj.get("templates") + new_template = None + for template in source_templates: + if template.get("name") == source_template_name: + new_template = json.loads(json.dumps(template).replace(source_template_path, destination_template_path)) + new_template["name"] = destination_template_name + new_template["displayName"] = destination_template_display_name + if destination_tenant_id is not None: + new_template["tenantId"] = destination_tenant_id + mso.delete_keys_from_dict(new_template, NDO_4_UNIQUE_IDENTIFIERS) + break + + if new_template is None: + mso.fail_json(msg="Source template with the name '{0}' does not exist.".format(source_template_name)) + + new_template = mso.recursive_dict_from_ref(new_template) + mso.previous = mso.existing + + ops.append(dict(op="add", path="/templates/-", value=new_template)) + if not module.check_mode: + mso.request(destination_schema_path, method="PATCH", data=ops) + + mso.existing = mso.query_obj(destination_schema_path, displayName=destination_schema) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_filter.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_filter.py new file mode 100644 index 000000000..11bf08731 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_filter.py @@ -0,0 +1,396 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Akini Ross (@akinross) <akinross@cisco.com> +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@cisco.com> +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_contract_filter +short_description: Manage contract filters in schema templates +description: +- Manage contract filters in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + contract: + description: + - The name of the contract to manage. + type: str + required: true + description: + description: + - The description of contract is supported on versions of MSO/NDO that are 3.3 or greater. + type: str + contract_display_name: + description: + - The name as displayed on the MSO web interface. + - This defaults to the contract name when unset on creation. + type: str + contract_filter_type: + description: + - DEPRECATION WARNING, contract_filter_type will not be used anymore and is deduced from filter_type. + - The type of filters defined in this contract. + - This defaults to C(both-way) when unset on creation. + default: both-way + type: str + choices: [ both-way, one-way ] + contract_scope: + description: + - The scope of the contract. + - This defaults to C(vrf) when unset on creation. + type: str + choices: [ application-profile, global, tenant, vrf ] + filter: + description: + - The filter to associate with this contract. + type: str + aliases: [ name ] + filter_template: + description: + - The template name in which the filter is located. + type: str + filter_schema: + description: + - The schema name in which the filter is located. + type: str + filter_type: + description: + - The type of filter to manage. + - Prior to MSO/NDO 3.3 remove and re-apply contract to change the filter type. + type: str + choices: [ both-way, consumer-to-provider, provider-to-consumer ] + default: both-way + aliases: [ type ] + filter_directives: + description: + - A list of filter directives. + type: list + elements: str + choices: [ log, none, policy_compression ] + qos_level: + description: + - The Contract QoS Level parameter is supported on versions of MSO/NDO that are 3.3 or greater. + type: str + choices: [ unspecified, level1, level2, level3, level4, level5, level6 ] + action: + description: + - The filter action parameter is supported on versions of MSO/NDO that are 3.3 or greater. + type: str + choices: [ permit, deny ] + priority: + description: + - The filter priority override parameter is supported on versions of MSO/NDO that are 3.3 or greater. + type: str + choices: [ default, lowest_priority, medium_priority, highest_priority ] + 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 +seealso: +- module: cisco.mso.mso_schema_template_filter_entry +notes: +- Due to restrictions of the MSO/NDO REST API this module creates contracts when needed, and removes them when the last filter has been removed. +- Due to restrictions of the MSO/NDO REST API concurrent modifications to contract filters can be dangerous and corrupt data. +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new contract filter + cisco.mso.mso_schema_template_contract_filter: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + contract: Contract 1 + contract_scope: global + filter: Filter 1 + state: present + delegate_to: localhost + +- name: Remove a contract filter + cisco.mso.mso_schema_template_contract_filter: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + contract: Contract 1 + filter: Filter 1 + state: absent + delegate_to: localhost + +- name: Query a specific contract filter + cisco.mso.mso_schema_template_contract_filter: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + contract: Contract 1 + filter: Filter 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all contract filters + cisco.mso.mso_schema_template_contract_filter: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + contract: Contract 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible_collections.cisco.mso.plugins.module_utils.constants import FILTER_KEY_MAP, PRIORITY_MAP + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + contract=dict(type="str", required=True), + description=dict(type="str"), + contract_display_name=dict(type="str"), + contract_scope=dict(type="str", choices=["application-profile", "global", "tenant", "vrf"]), + # Deprecated input: contract_filter_type is deduced from filter_type + contract_filter_type=dict(type="str", default="both-way", choices=["both-way", "one-way"]), + filter=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + filter_directives=dict(type="list", elements="str", choices=["log", "none", "policy_compression"]), + filter_template=dict(type="str"), + filter_schema=dict(type="str"), + filter_type=dict(type="str", default="both-way", choices=list(FILTER_KEY_MAP), aliases=["type"]), + qos_level=dict(type="str", choices=["unspecified", "level1", "level2", "level3", "level4", "level5", "level6"]), + action=dict(type="str", choices=["permit", "deny"]), + priority=dict(type="str", choices=["default", "lowest_priority", "medium_priority", "highest_priority"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["filter"]], + ["state", "present", ["filter"]], + ], + ) + + schema = module.params.get("schema") + template_name = module.params.get("template").replace(" ", "") + contract_name = module.params.get("contract") + contract_display_name = module.params.get("contract_display_name") + description = module.params.get("description") + # Deprecated input: contract_filter_type is deduced from filter_type. + # contract_filter_type = module.params.get('contract_filter_type') + contract_scope = module.params.get("contract_scope") + filter_name = module.params.get("filter") + filter_directives = module.params.get("filter_directives") + filter_template = module.params.get("filter_template") + filter_schema = module.params.get("filter_schema") + filter_type = module.params.get("filter_type") + filter_action = module.params.get("action") + filter_priority = module.params.get("priority") + qos_level = module.params.get("qos_level") + + state = module.params.get("state") + + mso = MSOModule(module) + + # Initialize variables + ops = [] + filter_obj = None + filter_key = FILTER_KEY_MAP.get(filter_type) + filter_template = template_name if filter_template is None else filter_template.replace(" ", "") + filter_schema = schema if filter_schema is None else filter_schema + filter_schema_id = mso.lookup_schema(filter_schema) + contract_filter_type = "bothWay" if filter_type == "both-way" else "oneWay" + + # Set path defaults, when object (contract or filter) is found append /{name} to base paths + base_contract_path = "/templates/{0}/contracts".format(template_name) + base_filter_path = "{0}/{1}/{2}".format(base_contract_path, contract_name, filter_key) + contract_path = "{0}/-".format(base_contract_path) + filter_path = "{0}/-".format(base_filter_path) + + # Get schema information. + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template by unique identifier "name". + template_obj = next((item for item in schema_obj.get("templates") if item.get("name") == template_name), None) + if not template_obj: + existing_templates = [t.get("name") for t in schema_obj.get("templates")] + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template_name, ", ".join(existing_templates))) + + filter_ref = mso.filter_ref(schema_id=filter_schema_id, template=filter_template, filter=filter_name) + + # Get contract by unique identifier "name". + contract_obj = next((item for item in template_obj.get("contracts") if item.get("name") == contract_name), None) + if contract_obj: + if contract_obj.get("filterType") != contract_filter_type: + mso.fail_json( + msg="Current filter type '{0}' for contract '{1}' is not allowed to change to '{2}'.".format( + contract_obj.get("filterType"), contract_name, contract_filter_type + ) + ) + contract_path = "{0}/{1}".format(base_contract_path, contract_name) + if filter_name: + # Get filter by unique identifier "filterRef". + filter_obj = next((item for item in contract_obj.get(filter_key) if item.get("filterRef") == filter_ref), None) + if filter_obj: + filter_path = "{0}/{1}".format(base_filter_path, filter_name) + mso.update_filter_obj(contract_obj, filter_obj, filter_type) + mso.existing = filter_obj + + if state == "query": + if not contract_obj: + existing_contracts = [c.get("name") for c in template_obj.get("contracts")] + mso.fail_json(msg="Provided contract '{0}' does not exist. Existing contracts: {1}".format(contract_name, ", ".join(existing_contracts))) + + # If filter name is not provided, provide overview of all filter objects for the filter type. + if not filter_name: + mso.existing = contract_obj.get(filter_key) + for filter_obj in mso.existing: + mso.update_filter_obj(contract_obj, filter_obj, filter_type) + + elif not mso.existing: + mso.fail_json(msg="FilterRef '{filter_ref}' not found".format(filter_ref=filter_ref)) + + mso.exit_json() + + mso.previous = mso.existing + + if state == "absent": + # Contracts need at least one filter left, remove contract if remove would lead 0 filters remaining. + if contract_obj: + if len(contract_obj.get(filter_key)) == 1: + mso.existing = {} + ops.append(dict(op="remove", path=contract_path)) + elif len(contract_obj.get(filter_key)) > 1: + mso.existing = {} + ops.append(dict(op="remove", path=filter_path)) + + elif state == "present": + contract_scope = "context" if contract_scope == "vrf" else contract_scope + + # Initialize "present" state filter variables + if not filter_directives: + # Avoid validation error: "Bad Request: (0)(1)(0) 'directives' is undefined on object + if not filter_obj: + filter_directives = ["none"] + else: + filter_directives = filter_obj.get("directives", ["none"]) + + elif "policy_compression" in filter_directives: + filter_directives[filter_directives.index("policy_compression")] = "no_stats" + filter_payload = dict( + filterRef=dict( + filterName=filter_name, + templateName=filter_template, + schemaId=filter_schema_id, + ), + directives=filter_directives, + ) + if filter_action: + filter_payload.update(action=filter_action) + if filter_action == "deny" and filter_priority: + filter_payload.update(priorityOverride=PRIORITY_MAP.get(filter_priority)) + + # If contract exist the operation should be set to replace else operation is add to create new contract. + if contract_obj: + if contract_display_name: + ops.append(dict(op="replace", path=contract_path + "/displayName", value=contract_display_name)) + # Conditional statement 'description == ""' is needed to allow setting the description back to empty string. + if description or description == "": + ops.append(dict(op="replace", path=contract_path + "/description", value=description)) + if qos_level: + # Conditional statement is needed to determine if "prio" exist in contract object. + # An object can be created in 3.3 higher version without prio via the API. + # In the GUI a default is set to "unspecified" and thus prio is always configured via GUI. + # We can't set a default of "unspecified" because prior to version 3.3 qos_level is not supported, + # thus the logic is needed for both add and replace operation + if contract_obj.get("prio"): + ops.append(dict(op="replace", path=contract_path + "/prio", value=qos_level)) + else: + ops.append(dict(op="add", path=contract_path + "/prio", value=qos_level)) + if contract_scope: + ops.append(dict(op="replace", path=contract_path + "/scope", value=contract_scope)) + + # If filter exist the operation should be set to replace else operation is add to create new filter. + if filter_obj: + ops.append(dict(op="replace", path=filter_path, value=filter_payload)) + else: + ops.append(dict(op="add", path=filter_path, value=filter_payload)) + + else: + contract_display_name = contract_display_name if contract_display_name else contract_name + # If contract_scope is not provided default to context to match GUI behaviour on create new contract. + contract_scope = "context" if contract_scope is None else contract_scope + contract_payload = dict(name=contract_name, displayName=contract_display_name, filterType=contract_filter_type, scope=contract_scope) + if description: + contract_payload.update(description=description) + if qos_level: + contract_payload.update(prio=qos_level) + if filter_key == "filterRelationships": + contract_payload.update(filterRelationships=[filter_payload]) + elif filter_key == "filterRelationshipsConsumerToProvider": + contract_payload.update(filterRelationshipsConsumerToProvider=[filter_payload]) + elif filter_key == "filterRelationshipsProviderToConsumer": + contract_payload.update(filterRelationshipsProviderToConsumer=[filter_payload]) + ops.append(dict(op="add", path=contract_path, value=contract_payload)) + + mso.sanitize(filter_payload, collate=True, unwanted=["filterType", "contractScope", "contractFilterType"]) + + # Update existing with filter (mso.sent) and contract information. + mso.existing = mso.sent + mso.existing["displayName"] = contract_display_name if contract_display_name else contract_obj.get("displayName") + mso.existing["filterType"] = filter_type + mso.existing["contractScope"] = contract_scope if contract_scope else contract_obj.get("scope") + mso.existing["contractFilterType"] = contract_filter_type + # Conditional statement 'description == ""' is needed to allow setting the description back to empty string. + if description or (contract_obj and (contract_obj.get("description") or contract_obj.get("description") == "")): + mso.existing["description"] = description if description or description == "" else contract_obj.get("description") + # Conditional statement to check qos_level is defined or is present in the contract object. + # qos_level is not supported prior to 3.3 thus this check in place, GUI uses default of "unspecified" from 3.3. + # When default of "unspecified" is set, conditional statement can be simplified since "prio" always present. + if qos_level or (contract_obj and contract_obj.get("prio")): + mso.existing["prio"] = qos_level if qos_level else contract_obj.get("prio") + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_service_graph.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_service_graph.py new file mode 100644 index 000000000..0e398843b --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_service_graph.py @@ -0,0 +1,317 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Akini Ross (@akinross) <akinross@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_contract_service_graph +short_description: Manage the service graph association with a contract in schema template +description: +- Manage the service graph association with a contract in schema template on Cisco ACI Multi-Site. +- The Contract Service Graph parameter is supported on versions of MSO/NDO that are 3.3 or greater. +author: +- Akini Ross (@akinross) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + contract: + description: + - The name of the contract. + type: str + required: true + service_graph: + description: + - The service graph to associate with this contract. + type: str + service_graph_template: + description: + - The template name in which the service graph is located. + type: str + service_graph_schema: + description: + - The schema name in which the service graph is located. + type: str + service_nodes: + description: + - A list of nodes and their connector details associated with the Service Graph. + - The order of the list matches the node id ordering in GUI, so first entry in list will be match node 1. + type: list + elements: dict + suboptions: + provider: + description: + - The name of the Bridge Domain. + required: true + type: str + consumer: + description: + - The name of the Bridge Domain. + required: true + type: str + connector_object_type: + description: + - The connector ACI object type of the node. + type: str + default: bd + choices: [ bd ] + provider_schema: + description: + - The schema name in which the provider is located. + type: str + provider_template: + description: + - The template name in which the provider is located. + type: str + consumer_schema: + description: + - The schema name in which the consumer is located. + type: str + consumer_template: + description: + - The template name in which the consumer is located. + 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 +seealso: +- module: cisco.mso.mso_schema_template_contract_filter +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new contract service graph + cisco.mso.mso_schema_template_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + contract: Contract 1 + service_graph: SG1 + service_graph_nodes: + - provider: b1 + consumer: b2 + filter: Filter 1 + state: present + delegate_to: localhost + +- name: Remove a contract service graph + cisco.mso.mso_schema_template_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + contract: Contract 1 + service_graph: SG1 + state: absent + delegate_to: localhost + +- name: Query a contract service graph + cisco.mso.mso_schema_template_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + contract: Contract 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_service_graph_connector_spec +from ansible_collections.cisco.mso.plugins.module_utils.constants import SERVICE_NODE_CONNECTOR_MAP + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + contract=dict(type="str", required=True), + service_graph=dict(type="str"), + service_graph_template=dict(type="str"), + service_graph_schema=dict(type="str"), + service_nodes=dict(type="list", elements="dict", options=mso_service_graph_connector_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["service_graph"]], + ["state", "present", ["service_graph", "service_nodes"]], + ], + ) + + schema = module.params.get("schema") + template_name = module.params.get("template").replace(" ", "") + contract_name = module.params.get("contract") + service_graph_name = module.params.get("service_graph") + service_graph_template = module.params.get("service_graph_template") + service_graph_schema = module.params.get("service_graph_schema") + service_nodes = module.params.get("service_nodes") + + state = module.params.get("state") + + mso = MSOModule(module) + + # Initialize variables + ops = [] + service_graph_obj = None + + # Set path defaults for create logic, if object (contract or filter) is found replace the "-" for specific value + base_contract_path = "/templates/{0}/contracts".format(template_name) + service_graph_path = "{0}/{1}/serviceGraphRelationship".format(base_contract_path, contract_name) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + template_obj = next((item for item in schema_obj.get("templates") if item.get("name") == template_name), None) + if not template_obj: + mso.fail_json( + msg="Provided template '{0}' does not exist. Existing templates: {1}".format( + template_name, ", ".join([t.get("name") for t in schema_obj.get("templates")]) + ) + ) + + # Get contract + contract_obj = next((item for item in template_obj.get("contracts") if item.get("name") == contract_name), None) + if contract_obj: + # Get service graph if it exists in contract + if contract_obj.get("serviceGraphRelationship"): + service_graph_obj = contract_obj.get("serviceGraphRelationship") + mso.update_service_graph_obj(service_graph_obj) + mso.existing = service_graph_obj + else: + mso.fail_json( + msg="Provided contract '{0}' does not exist. Existing contracts: {1}".format( + contract_name, ", ".join([c.get("name") for c in template_obj.get("contracts")]) + ) + ) + + if state == "query": + mso.exit_json() + + mso.previous = mso.existing + + if state == "absent": + if contract_obj.get("serviceGraphRelationship"): + mso.existing = {} + ops.append(dict(op="remove", path=service_graph_path)) + + elif state == "present": + service_nodes_relationship = [] + service_graph_template = service_graph_template.replace(" ", "") if service_graph_template else template_name + service_graph_schema = service_graph_schema if service_graph_schema else schema + service_graph_schema_id, service_graph_schema_path, service_graph_schema_obj = mso.query_schema(service_graph_schema) + + # Validation to check if amount of service graph nodes provided is matching the service graph template. + # The API allows providing more or less service graph nodes behaviour but the GUI does not. + service_graph_template_obj = next((item for item in service_graph_schema_obj.get("templates") if item.get("name") == service_graph_template), None) + if not service_graph_template_obj: + mso.fail_json( + msg="Provided template '{0}' does not exist. Existing templates: {1}".format( + template_name, ", ".join([t.get("name") for t in service_graph_schema_obj.get("templates")]) + ) + ) + service_graph_schema_obj = next((item for item in service_graph_template_obj.get("serviceGraphs") if item.get("name") == service_graph_name), None) + if service_graph_schema_obj: + if len(service_nodes) < len(service_graph_schema_obj.get("serviceNodes")): + mso.fail_json( + msg="Not enough service nodes defined, {0} service node(s) provided when {1} needed.".format( + len(service_nodes), len(service_graph_schema_obj.get("serviceNodes")) + ) + ) + elif len(service_nodes) > len(service_graph_schema_obj.get("serviceNodes")): + mso.fail_json( + msg="Too many service nodes defined, {0} service nodes provided when {1} needed.".format( + len(service_nodes), len(service_graph_schema_obj.get("serviceNodes")) + ) + ) + else: + mso.fail_json(msg="Provided service graph '{0}' does not exist.".format(service_graph_name)) + + for node_id, service_node in enumerate(service_nodes, 0): + # Consumer and provider share connector details (so provider/consumer could have separate details in future) + connector_details = SERVICE_NODE_CONNECTOR_MAP.get(service_node.get("connector_object_type")) + provider_schema = mso.lookup_schema(service_node.get("provider_schema")) if service_node.get("provider_schema") else schema_id + provider_template = service_node.get("provider_template").replace(" ", "") if service_node.get("provider_template") else template_name + consumer_schema = mso.lookup_schema(service_node.get("consumer_schema")) if service_node.get("consumer_schema") else schema_id + consumer_template = service_node.get("consumer_template").replace(" ", "") if service_node.get("consumer_template") else template_name + + service_nodes_relationship.append( + { + "serviceNodeRef": dict( + schemaId=service_graph_schema_id, + templateName=service_graph_template, + serviceGraphName=service_graph_name, + serviceNodeName=service_graph_schema_obj.get("serviceNodes")[node_id].get("name"), + ), + "providerConnector": { + "connectorType": connector_details.get("connector_type"), + "{0}Ref".format(connector_details.get("id")): { + "schemaId": provider_schema, + "templateName": provider_template, + "{0}Name".format(connector_details.get("id")): service_node.get("provider"), + }, + }, + "consumerConnector": { + "connectorType": connector_details.get("connector_type"), + "{0}Ref".format(connector_details.get("id")): { + "schemaId": consumer_schema, + "templateName": consumer_template, + "{0}Name".format(connector_details.get("id")): service_node.get("consumer"), + }, + }, + } + ) + + service_graph_payload = dict( + serviceGraphRef=dict(serviceGraphName=service_graph_name, templateName=service_graph_template, schemaId=service_graph_schema_id), + serviceNodesRelationship=service_nodes_relationship, + ) + + # If service graph exist the operation should be set to "replace" else operation is "add" to create new + if service_graph_obj: + ops.append(dict(op="replace", path=service_graph_path, value=service_graph_payload)) + else: + ops.append(dict(op="add", path=service_graph_path, value=service_graph_payload)) + + mso.existing = mso.sent = service_graph_payload + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy.py new file mode 100644 index 000000000..49df465c5 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_deploy +short_description: Deploy schema templates to sites +description: +- Deploy schema templates to sites. +- Prior to deploy a schema validation is executed for MSO releases running on the ND platform. +- When schema validation fails, M(cisco.mso.mso_schema_template_deploy) fails and deploy will not be executed. +- DEPRECATED for NDO v4.1 and later. Use M(cisco.mso.ndo_schema_template_deploy) on NDO v4.1 and later. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + aliases: [ name ] + site: + description: + - The name of the site B(to undeploy). + type: str + state: + description: + - Use C(deploy) to deploy schema template. + - Use C(status) to get deployment status. + - Use C(undeploy) to deploy schema template from a site. + type: str + choices: [ deploy, status, undeploy ] + default: deploy +seealso: +- module: cisco.mso.mso_schema_site +- module: cisco.mso.mso_schema_template +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Deploy a schema template + cisco.mso.mso_schema_template_deploy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: deploy + delegate_to: localhost + +- name: Undeploy a schema template + cisco.mso.mso_schema_template_deploy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + site: Site 1 + state: undeploy + delegate_to: localhost + +- name: Get deployment status + cisco.mso.mso_schema: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: status + delegate_to: localhost + register: status_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True, aliases=["name"]), + site=dict(type="str"), + state=dict(type="str", default="deploy", choices=["deploy", "status", "undeploy"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "undeploy", ["site"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + site = module.params.get("site") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema id + schema_id = mso.lookup_schema(schema) + + payload = dict( + schemaId=schema_id, + templateName=template, + ) + + qs = None + if state == "deploy": + if mso.platform == "nd": + mso.validate_schema(schema_id) + path = "execute/schema/{0}/template/{1}".format(schema_id, template) + elif state == "status": + path = "status/schema/{0}/template/{1}".format(schema_id, template) + elif state == "undeploy": + path = "execute/schema/{0}/template/{1}".format(schema_id, template) + site_id = mso.lookup_site(site) + qs = dict(undeploy=site_id) + + if not module.check_mode: + status = mso.request(path, method="GET", data=payload, qs=qs) + mso.exit_json(**status) + else: + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy_status.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy_status.py new file mode 100644 index 000000000..707ad7320 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy_status.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_deploy_status +short_description: Check query of objects before deployment to site +description: +- Check query of objects in a template of a schema +author: +- Shreyas Srish (@shrsr) +options: + schema: + description: + - The name of the schema. + type: str + aliases: [ name ] + template: + description: + - The name of the template. + type: str + site: + description: + - The name of the site. + type: str + state: + description: + - Use C(query) for listing query of objects. + type: str + choices: [ query ] + default: query +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" + +- name: Query status of objects in a template + cisco.mso.mso_schema_template_deploy_status: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query status of objects using site + cisco.mso.mso_schema_template_deploy_status: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + site: ansible_test + state: query + delegate_to: localhost + register: query_result + +- name: Query status of objects in a template associated with a site + cisco.mso.mso_schema_template_deploy_status: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + site: ansible_test + state: query + delegate_to: localhost + register: query_result + +- name: Query status of objects in all templates + cisco.mso.mso_schema_template_deploy_status: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", aliases=["name"]), + template=dict(type="str"), + site=dict(type="str"), + state=dict(type="str", default="query", choices=["query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "query", ["schema"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template") + if template is not None: + template = template.replace(" ", "") + site = module.params.get("site") + state = module.params.get("state") + + mso = MSOModule(module) + + schema_id = None + path = "schemas" + + get_schema = mso.get_obj(path, displayName=schema) + if get_schema: + schema_id = get_schema.get("id") + path = "schemas/{id}/policy-states".format(id=schema_id) + else: + mso.fail_json(msg="Schema '{0}' not found.".format(schema)) + + if state == "query": + get_data = mso.request(path, method="GET") + mso.existing = [] + if template: + for configuration_objects in get_data.get("policyStates"): + if configuration_objects.get("templateName") == template: + mso.existing.append(configuration_objects) + if not mso.existing: + mso.fail_json(msg="Template '{0}' not found.".format(template)) + + if site: + mso.existing.clear() + for configuration_objects in get_data.get("policyStates"): + if configuration_objects.get("siteId") == mso.lookup_site(site): + if template: + if configuration_objects.get("templateName") == template: + mso.existing = configuration_objects + else: + mso.existing.append(configuration_objects) + if template is not None and not mso.existing: + mso.fail_json(msg="Provided Template '{0}' not associated with Site '{1}'.".format(template, site)) + + if template is None and site is None: + mso.existing = get_data + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg.py new file mode 100644 index 000000000..ce201913f --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg.py @@ -0,0 +1,337 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_external_epg +short_description: Manage external EPGs in schema templates +description: +- Manage external EPGs in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + external_epg: + description: + - The name of the external EPG to manage. + type: str + aliases: [ name, externalepg ] + description: + description: + - The description of external EPG is supported on versions of MSO that are 3.3 or greater. + type: str + type: + description: + - The type of external epg. + - anp needs to be associated with external epg when the type is cloud. + - l3out can be associated with external epg when the type is on-premise. + type: str + choices: [ on-premise, cloud ] + default: on-premise + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + vrf: + description: + - The VRF associated with the external epg. + type: dict + suboptions: + name: + description: + - The name of the VRF to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current template. + type: str + l3out: + description: + - The L3Out associated with the external epg. + type: dict + suboptions: + name: + description: + - The name of the L3Out to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced L3Out. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced L3Out. + - If this parameter is unspecified, it defaults to the current template. + type: str + anp: + description: + - The anp associated with the external epg. + type: dict + suboptions: + name: + description: + - The name of the anp to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced anp. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced anp. + - If this parameter is unspecified, it defaults to the current template. + type: str + preferred_group: + description: + - Preferred Group is enabled for this External EPG or not. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new external EPG + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: External EPG 1 + vrf: + name: VRF + schema: Schema 1 + template: Template 1 + state: present + delegate_to: localhost + +- name: Add a new external EPG with external epg in cloud + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: External EPG 1 + type: cloud + vrf: + name: VRF + schema: Schema 1 + template: Template 1 + anp: + name: ANP1 + schema: Schema 1 + template: Template 1 + state: present + delegate_to: localhost + +- name: Remove an external EPG + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: external EPG1 + state: absent + delegate_to: localhost + +- name: Query a specific external EPGs + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: external EPG1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all external EPGs + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_reference_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + external_epg=dict(type="str", aliases=["name", "externalepg"]), # This parameter is not required for querying all objects + description=dict(type="str"), + display_name=dict(type="str"), + vrf=dict(type="dict", options=mso_reference_spec()), + l3out=dict(type="dict", options=mso_reference_spec()), + anp=dict(type="dict", options=mso_reference_spec()), + preferred_group=dict(type="bool"), + type=dict(type="str", default="on-premise", choices=["on-premise", "cloud"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["external_epg"]], + ["state", "present", ["external_epg", "vrf"]], + ["type", "cloud", ["anp"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + external_epg = module.params.get("external_epg") + description = module.params.get("description") + display_name = module.params.get("display_name") + vrf = module.params.get("vrf") + if vrf is not None and vrf.get("template") is not None: + vrf["template"] = vrf.get("template").replace(" ", "") + l3out = module.params.get("l3out") + if l3out is not None and l3out.get("template") is not None: + l3out["template"] = l3out.get("template").replace(" ", "") + anp = module.params.get("anp") + if anp is not None and anp.get("template") is not None: + anp["template"] = anp.get("template").replace(" ", "") + preferred_group = module.params.get("preferred_group") + type_ext_epg = module.params.get("type") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get external EPGs + external_epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["externalEpgs"]] + + if external_epg is not None and external_epg in external_epgs: + external_epg_idx = external_epgs.index(external_epg) + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][external_epg_idx] + if "externalEpgRef" in mso.existing: + del mso.existing["externalEpgRef"] + if "vrfRef" in mso.existing: + mso.existing["vrfRef"] = mso.dict_from_ref(mso.existing.get("vrfRef")) + if "l3outRef" in mso.existing: + mso.existing["l3outRef"] = mso.dict_from_ref(mso.existing.get("l3outRef")) + if "anpRef" in mso.existing: + mso.existing["anpRef"] = mso.dict_from_ref(mso.existing.get("anpRef")) + + if state == "query": + if external_epg is None: + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"] + elif not mso.existing: + mso.fail_json(msg="External EPG '{external_epg}' not found".format(external_epg=external_epg)) + mso.exit_json() + + eepgs_path = "/templates/{0}/externalEpgs".format(template) + eepg_path = "/templates/{0}/externalEpgs/{1}".format(template, external_epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=eepg_path)) + + elif state == "present": + vrf_ref = mso.make_reference(vrf, "vrf", schema_id, template) + l3out_ref = mso.make_reference(l3out, "l3out", schema_id, template) + anp_ref = mso.make_reference(anp, "anp", schema_id, template) + if display_name is None and not mso.existing: + display_name = external_epg + + payload = dict( + name=external_epg, + displayName=display_name, + vrfRef=vrf_ref, + preferredGroup=preferred_group, + ) + + if description is not None: + payload.update(description=description) + + if type_ext_epg == "cloud": + payload["extEpgType"] = "cloud" + payload["anpRef"] = anp_ref + else: + payload["l3outRef"] = l3out_ref + + mso.sanitize(payload, collate=True) + + if mso.existing: + # clean anpRef when anpRef is null + if "anpRef" in mso.existing and mso.existing.get("anpRef") is None: + del mso.existing["anpRef"] + # clean contractRef to fix api issue + for contract in mso.sent.get("contractRelationships"): + contract["contractRef"] = mso.dict_from_ref(contract.get("contractRef")) + ops.append(dict(op="replace", path=eepg_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=eepgs_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_contract.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_contract.py new file mode 100644 index 000000000..4029175bc --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_contract.py @@ -0,0 +1,247 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING 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: mso_schema_template_external_epg_contract +short_description: Manage Extrnal EPG contracts in schema templates +description: +- Manage External EPG contracts in schema templates on Cisco ACI Multi-Site. +author: +- Devarshi Shah (@devarshishah3) +version_added: '0.0.8' +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + external_epg: + description: + - The name of the EPG to manage. + type: str + required: true + contract: + description: + - A contract associated to this EPG. + type: dict + suboptions: + name: + description: + - The name of the Contract to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced BD. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced BD. + type: str + type: + description: + - The type of contract. + type: str + required: true + choices: [ consumer, provider ] + 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 +seealso: +- module: cisco.mso.mso_schema_template_external_epg +- module: cisco.mso.mso_schema_template_contract_filter +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a contract to an EPG + cisco.mso.mso_schema_template_external_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + epg: EPG 1 + contract: + name: Contract 1 + type: consumer + state: present + delegate_to: localhost + +- name: Remove a Contract + cisco.mso.mso_schema_template_external_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + epg: EPG 1 + contract: + name: Contract 1 + state: absent + delegate_to: localhost + +- name: Query a specific Contract + cisco.mso.mso_schema_template_external_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + epg: EPG 1 + contract: + name: Contract 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all Contracts + cisco.mso.mso_schema_template_external_epg_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_contractref_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + external_epg=dict(type="str", required=True), + contract=dict(type="dict", options=mso_contractref_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["contract"]], + ["state", "present", ["contract"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + external_epg = module.params.get("external_epg") + contract = module.params.get("contract") + if contract is not None and contract.get("template") is not None: + contract["template"] = contract.get("template").replace(" ", "") + state = module.params.get("state") + + mso = MSOModule(module) + + if contract: + if contract.get("schema") is None: + contract["schema"] = schema + contract["schema_id"] = mso.lookup_schema(contract.get("schema")) + if contract.get("template") is None: + contract["template"] = template + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get EPG + epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["externalEpgs"]] + if external_epg not in epgs: + mso.fail_json(msg="Provided epg '{epg}' does not exist. Existing epgs: {epgs}".format(epg=external_epg, epgs=", ".join(epgs))) + epg_idx = epgs.index(external_epg) + + # Get Contract + if contract: + contracts = [ + (c.get("contractRef"), c.get("relationshipType")) + for c in schema_obj.get("templates")[template_idx]["externalEpgs"][epg_idx]["contractRelationships"] + ] + contract_ref = mso.contract_ref(**contract) + if (contract_ref, contract.get("type")) in contracts: + contract_idx = contracts.index((contract_ref, contract.get("type"))) + contract_path = "/templates/{0}/externalEpgs/{1}/contractRelationships/{2}".format(template, external_epg, contract_idx) + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][epg_idx]["contractRelationships"][contract_idx] + + if state == "query": + if not contract: + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][epg_idx]["contractRelationships"] + elif not mso.existing: + mso.fail_json(msg="Contract '{0}' not found".format(contract_ref)) + + if "contractRef" in mso.existing: + mso.existing["contractRef"] = mso.dict_from_ref(mso.existing.get("contractRef")) + mso.exit_json() + + contracts_path = "/templates/{0}/externalEpgs/{1}/contractRelationships".format(template, external_epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=contract_path)) + + elif state == "present": + payload = dict( + relationshipType=contract.get("type"), + contractRef=dict( + contractName=contract.get("name"), + templateName=contract.get("template"), + schemaId=contract.get("schema_id"), + ), + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=contract_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=contracts_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if "contractRef" in mso.previous: + mso.previous["contractRef"] = mso.dict_from_ref(mso.previous.get("contractRef")) + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_selector.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_selector.py new file mode 100644 index 000000000..7d4b93b27 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_selector.py @@ -0,0 +1,250 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Shreyas Srish (@shrsr) <ssrish@cisco.com> +# Copyright: (c) 2020, Cindy Zhao (@cizhao) <cizhao@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_external_epg_selector +short_description: Manage External EPG selector in schema templates +description: +- Manage External EPG selector in schema templates on Cisco ACI Multi-Site. +author: +- Shreyas Srish (@shrsr) +- Cindy Zhao (@cizhao) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + external_epg: + description: + - The name of the External EPG to be managed. + type: str + required: true + selector: + description: + - The name of the selector. + type: str + expressions: + description: + - Expressions associated to this selector. + type: list + elements: dict + suboptions: + type: + description: + - The name of the expression which in this case is always IP address. + required: true + type: str + choices: [ ip_address ] + operator: + description: + - The operator associated with the expression which in this case is always equals. + required: true + type: str + choices: [ equals ] + value: + description: + - The value of the IP Address / Subnet associated with the expression. + required: true + 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 +seealso: +- module: cisco.mso.mso_schema_template_external_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a selector to an External EPG + cisco.mso.mso_schema_template_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: extEPG 1 + selector: selector_1 + expressions: + - type: ip_address + operator: equals + value: 10.0.0.0 + state: present + delegate_to: localhost + +- name: Remove a Selector + cisco.mso.mso_schema_template_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: extEPG 1 + selector: selector_1 + state: absent + delegate_to: localhost + +- name: Query a specific Selector + cisco.mso.mso_schema_template_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: extEPG 1 + selector: selector_1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all Selectors + cisco.mso.mso_schema_template_external_epg_selector: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: extEPG 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_expression_spec_ext_epg + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + external_epg=dict(type="str", required=True), + selector=dict(type="str"), + expressions=dict(type="list", elements="dict", options=mso_expression_spec_ext_epg()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["selector"]], + ["state", "present", ["selector"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + external_epg = module.params.get("external_epg") + selector = module.params.get("selector") + expressions = module.params.get("expressions") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(templates)) + ) + template_idx = templates.index(template) + + # Get External EPG + external_epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["externalEpgs"]] + if external_epg not in external_epgs: + mso.fail_json( + msg="Provided external epg '{external_epg}' does not exist. Existing epgs: {external_epgs}".format( + external_epg=external_epg, external_epgs=", ".join(external_epgs) + ) + ) + external_epg_idx = external_epgs.index(external_epg) + + # Get Selector + selectors = [s.get("name") for s in schema_obj.get("templates")[template_idx]["externalEpgs"][external_epg_idx]["selectors"]] + if selector in selectors: + selector_idx = selectors.index(selector) + selector_path = "/templates/{0}/externalEpgs/{1}/selectors/{2}".format(template, external_epg, selector_idx) + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][external_epg_idx]["selectors"][selector_idx] + + if state == "query": + if selector is None: + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][external_epg_idx]["selectors"] + elif not mso.existing: + mso.fail_json(msg="Selector '{selector}' not found".format(selector=selector)) + mso.exit_json() + + selectors_path = "/templates/{0}/externalEpgs/{1}/selectors/-".format(template, external_epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=selector_path)) + + elif state == "present": + # Get expressions + types = dict(ip_address="ipAddress") + all_expressions = [] + if expressions: + for expression in expressions: + type_val = expression.get("type") + operator = expression.get("operator") + value = expression.get("value") + all_expressions.append( + dict( + key=types.get(type_val), + operator=operator, + value=value, + ) + ) + + payload = dict( + name=selector, + expressions=all_expressions, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=selector_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=selectors_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.existing != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_subnet.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_subnet.py new file mode 100644 index 000000000..c7512c2ee --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_subnet.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_external_epg_subnet +short_description: Manage External EPG subnets in schema templates +description: +- Manage External EPG subnets in schema templates on Cisco ACI Multi-Site. +author: +- Devarshi Shah (@devarshishah3) +- Anvitha Jain (@anvitha-jain) +version_added: '0.0.8' +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + external_epg: + description: + - The name of the External EPG to manage. + type: str + required: true + subnet: + description: + - The IP range in CIDR notation. + type: str + scope: + description: + - The scope parameter contains two sections 1. Route Control and 2. External EPG Classification. + - The existing Route Control parameters are C(export-rtctrl) for Export Route Control, C(import-rtctrl) for Import Route Control + - and C(shared-rtctrl) for Shared Route Control + - The existing External EPG Classification parameters are C(import-security) for External Subnets for External EPG + - and C(shared-security) for Shared Security Import + - The C(shared-security) for Shared Security Import can only be used when External Subnets for External EPG is present + type: list + elements: str + aggregate: + description: + - The aggregate option aggregates shared routes for the subnet. + - Use C(shared-rtctrl) to add Aggregate Shared Routes + - The C(shared-rtctrl) option can only be used when scope parameter Shared Route Control in the Route Control section is selected. + type: list + elements: 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 +notes: +- Due to restrictions of the MSO REST API concurrent modifications to EPG subnets can be dangerous and corrupt data. +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new subnet to an External EPG + cisco.mso.mso_schema_template_external_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: EPG 1 + subnet: 10.0.0.0/24 + state: present + delegate_to: localhost + +- name: Remove a subnet from an External EPG + cisco.mso.mso_schema_template_external_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: EPG 1 + subnet: 10.0.0.0/24 + state: absent + delegate_to: localhost + +- name: Query a specific External EPG subnet + cisco.mso.mso_schema_template_external_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: EPG 1 + subnet: 10.0.0.0/24 + state: query + delegate_to: localhost + register: query_result + +- name: Query all External EPGs subnets + cisco.mso.mso_schema_template_external_epg_subnet: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + external_epg=dict(type="str", required=True), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + subnet=dict(type="str"), + scope=dict(type="list", elements="str", default=[]), + aggregate=dict(type="list", elements="str", default=[]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["subnet"]], + ["state", "present", ["subnet"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + external_epg = module.params.get("external_epg") + subnet = module.params.get("subnet") + scope = module.params.get("scope") + aggregate = module.params.get("aggregate") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(templates)) + ) + template_idx = templates.index(template) + + # Get EPG + external_epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["externalEpgs"]] + if external_epg not in external_epgs: + mso.fail_json(msg="Provided External EPG '{epg}' does not exist. Existing epgs: {epgs}".format(epg=external_epg, epgs=", ".join(external_epgs))) + epg_idx = external_epgs.index(external_epg) + + # Get Subnet + subnets = [s.get("ip") for s in schema_obj.get("templates")[template_idx]["externalEpgs"][epg_idx]["subnets"]] + if subnet in subnets: + subnet_idx = subnets.index(subnet) + # FIXME: Changes based on index are DANGEROUS + subnet_path = "/templates/{0}/externalEpgs/{1}/subnets/{2}".format(template, external_epg, subnet_idx) + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][epg_idx]["subnets"][subnet_idx] + + if state == "query": + if subnet is None: + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][epg_idx]["subnets"] + elif not mso.existing: + mso.fail_json(msg="Subnet '{subnet}' not found".format(subnet=subnet)) + mso.exit_json() + + subnets_path = "/templates/{0}/externalEpgs/{1}/subnets".format(template, external_epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.existing = {} + ops.append(dict(op="remove", path=subnet_path)) + + elif state == "present": + payload = dict( + ip=subnet, + scope=scope, + aggregate=aggregate, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=subnet_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=subnets_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_externalepg.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_externalepg.py new file mode 100644 index 000000000..ce201913f --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_externalepg.py @@ -0,0 +1,337 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_external_epg +short_description: Manage external EPGs in schema templates +description: +- Manage external EPGs in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + external_epg: + description: + - The name of the external EPG to manage. + type: str + aliases: [ name, externalepg ] + description: + description: + - The description of external EPG is supported on versions of MSO that are 3.3 or greater. + type: str + type: + description: + - The type of external epg. + - anp needs to be associated with external epg when the type is cloud. + - l3out can be associated with external epg when the type is on-premise. + type: str + choices: [ on-premise, cloud ] + default: on-premise + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + vrf: + description: + - The VRF associated with the external epg. + type: dict + suboptions: + name: + description: + - The name of the VRF to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current template. + type: str + l3out: + description: + - The L3Out associated with the external epg. + type: dict + suboptions: + name: + description: + - The name of the L3Out to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced L3Out. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced L3Out. + - If this parameter is unspecified, it defaults to the current template. + type: str + anp: + description: + - The anp associated with the external epg. + type: dict + suboptions: + name: + description: + - The name of the anp to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced anp. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced anp. + - If this parameter is unspecified, it defaults to the current template. + type: str + preferred_group: + description: + - Preferred Group is enabled for this External EPG or not. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new external EPG + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: External EPG 1 + vrf: + name: VRF + schema: Schema 1 + template: Template 1 + state: present + delegate_to: localhost + +- name: Add a new external EPG with external epg in cloud + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: External EPG 1 + type: cloud + vrf: + name: VRF + schema: Schema 1 + template: Template 1 + anp: + name: ANP1 + schema: Schema 1 + template: Template 1 + state: present + delegate_to: localhost + +- name: Remove an external EPG + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: external EPG1 + state: absent + delegate_to: localhost + +- name: Query a specific external EPGs + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + external_epg: external EPG1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all external EPGs + cisco.mso.mso_schema_template_external_epg: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_reference_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + external_epg=dict(type="str", aliases=["name", "externalepg"]), # This parameter is not required for querying all objects + description=dict(type="str"), + display_name=dict(type="str"), + vrf=dict(type="dict", options=mso_reference_spec()), + l3out=dict(type="dict", options=mso_reference_spec()), + anp=dict(type="dict", options=mso_reference_spec()), + preferred_group=dict(type="bool"), + type=dict(type="str", default="on-premise", choices=["on-premise", "cloud"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["external_epg"]], + ["state", "present", ["external_epg", "vrf"]], + ["type", "cloud", ["anp"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + external_epg = module.params.get("external_epg") + description = module.params.get("description") + display_name = module.params.get("display_name") + vrf = module.params.get("vrf") + if vrf is not None and vrf.get("template") is not None: + vrf["template"] = vrf.get("template").replace(" ", "") + l3out = module.params.get("l3out") + if l3out is not None and l3out.get("template") is not None: + l3out["template"] = l3out.get("template").replace(" ", "") + anp = module.params.get("anp") + if anp is not None and anp.get("template") is not None: + anp["template"] = anp.get("template").replace(" ", "") + preferred_group = module.params.get("preferred_group") + type_ext_epg = module.params.get("type") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get external EPGs + external_epgs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["externalEpgs"]] + + if external_epg is not None and external_epg in external_epgs: + external_epg_idx = external_epgs.index(external_epg) + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"][external_epg_idx] + if "externalEpgRef" in mso.existing: + del mso.existing["externalEpgRef"] + if "vrfRef" in mso.existing: + mso.existing["vrfRef"] = mso.dict_from_ref(mso.existing.get("vrfRef")) + if "l3outRef" in mso.existing: + mso.existing["l3outRef"] = mso.dict_from_ref(mso.existing.get("l3outRef")) + if "anpRef" in mso.existing: + mso.existing["anpRef"] = mso.dict_from_ref(mso.existing.get("anpRef")) + + if state == "query": + if external_epg is None: + mso.existing = schema_obj.get("templates")[template_idx]["externalEpgs"] + elif not mso.existing: + mso.fail_json(msg="External EPG '{external_epg}' not found".format(external_epg=external_epg)) + mso.exit_json() + + eepgs_path = "/templates/{0}/externalEpgs".format(template) + eepg_path = "/templates/{0}/externalEpgs/{1}".format(template, external_epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=eepg_path)) + + elif state == "present": + vrf_ref = mso.make_reference(vrf, "vrf", schema_id, template) + l3out_ref = mso.make_reference(l3out, "l3out", schema_id, template) + anp_ref = mso.make_reference(anp, "anp", schema_id, template) + if display_name is None and not mso.existing: + display_name = external_epg + + payload = dict( + name=external_epg, + displayName=display_name, + vrfRef=vrf_ref, + preferredGroup=preferred_group, + ) + + if description is not None: + payload.update(description=description) + + if type_ext_epg == "cloud": + payload["extEpgType"] = "cloud" + payload["anpRef"] = anp_ref + else: + payload["l3outRef"] = l3out_ref + + mso.sanitize(payload, collate=True) + + if mso.existing: + # clean anpRef when anpRef is null + if "anpRef" in mso.existing and mso.existing.get("anpRef") is None: + del mso.existing["anpRef"] + # clean contractRef to fix api issue + for contract in mso.sent.get("contractRelationships"): + contract["contractRef"] = mso.dict_from_ref(contract.get("contractRef")) + ops.append(dict(op="replace", path=eepg_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=eepgs_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_filter_entry.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_filter_entry.py new file mode 100644 index 000000000..c0ab485a4 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_filter_entry.py @@ -0,0 +1,369 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_filter_entry +short_description: Manage filter entries in schema templates +description: +- Manage filter entries in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +- Anvitha Jain (@anvitha-jain) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + filter: + description: + - The name of the filter to manage. + - There should be no space in the filter name. APIC will throw an error if a space is provided in the filter name. + - See the C(filter_display_name) attribute if you want the display name of the filter to contain a space. + type: str + required: true + filter_description: + description: + - The description of this filter is supported on versions of MSO that are 3.3 or greater. + type: str + default: '' + filter_display_name: + description: + - The name as displayed on the MSO web interface. + type: str + entry: + description: + - The filter entry name to manage. + type: str + aliases: [ name ] + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + aliases: [ entry_display_name ] + filter_entry_description: + description: + - The description of this filter entry. + type: str + aliases: [ entry_description, description ] + default: '' + ethertype: + description: + - The ethernet type to use for this filter entry. + type: str + choices: [ arp, fcoe, ip, ipv4, ipv6, mac-security, mpls-unicast, trill, unspecified ] + ip_protocol: + description: + - The IP protocol to use for this filter entry. + type: str + choices: [ eigrp, egp, icmp, icmpv6, igmp, igp, l2tp, ospfigp, pim, tcp, udp, unspecified ] + tcp_session_rules: + description: + - A list of TCP session rules. + type: list + elements: str + choices: [ acknowledgement, established, finish, synchronize, reset, unspecified ] + source_from: + description: + - The source port range from. + type: str + source_to: + description: + - The source port range to. + type: str + destination_from: + description: + - The destination port range from. + type: str + destination_to: + description: + - The destination port range to. + type: str + arp_flag: + description: + - The ARP flag to use for this filter entry. + type: str + choices: [ reply, request, unspecified ] + stateful: + description: + - Whether this filter entry is stateful. + type: bool + fragments_only: + description: + - Whether this filter entry only matches fragments. + 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 +seealso: +- module: cisco.mso.mso_schema_template_contract_filter +notes: +- Due to restrictions of the MSO REST API this module creates filters when needed, and removes them when the last entry has been removed. +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new filter entry + cisco.mso.mso_schema_template_filter_entry: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + filter: Filter 1 + state: present + delegate_to: localhost + +- name: Remove a filter entry + cisco.mso.mso_schema_template_filter_entry: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + filter: Filter 1 + state: absent + delegate_to: localhost + +- name: Query a specific filter entry + cisco.mso.mso_schema_template_filter_entry: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + filter: Filter 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all filter entries + cisco.mso.mso_schema_template_filter_entry: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + filter=dict(type="str", required=True), + filter_description=dict(type="str", default=""), + filter_display_name=dict(type="str"), + entry=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + filter_entry_description=dict(type="str", default="", aliases=["entry_description", "description"]), + display_name=dict(type="str", aliases=["entry_display_name"]), + ethertype=dict(type="str", choices=["arp", "fcoe", "ip", "ipv4", "ipv6", "mac-security", "mpls-unicast", "trill", "unspecified"]), + ip_protocol=dict(type="str", choices=["eigrp", "egp", "icmp", "icmpv6", "igmp", "igp", "l2tp", "ospfigp", "pim", "tcp", "udp", "unspecified"]), + tcp_session_rules=dict(type="list", elements="str", choices=["acknowledgement", "established", "finish", "synchronize", "reset", "unspecified"]), + source_from=dict(type="str"), + source_to=dict(type="str"), + destination_from=dict(type="str"), + destination_to=dict(type="str"), + arp_flag=dict(type="str", choices=["reply", "request", "unspecified"]), + stateful=dict(type="bool"), + fragments_only=dict(type="bool"), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["entry"]], + ["state", "present", ["entry"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + filter_name = module.params.get("filter") + filter_display_name = module.params.get("filter_display_name") + filter_description = module.params.get("filter_description") + entry = module.params.get("entry") + display_name = module.params.get("display_name") + filter_entry_description = module.params.get("filter_entry_description") + ethertype = module.params.get("ethertype") + ip_protocol = module.params.get("ip_protocol") + tcp_session_rules = module.params.get("tcp_session_rules") + source_from = module.params.get("source_from") + source_to = module.params.get("source_to") + destination_from = module.params.get("destination_from") + destination_to = module.params.get("destination_to") + arp_flag = module.params.get("arp_flag") + if arp_flag == "request": + arp_flag = "req" + stateful = module.params.get("stateful") + fragments_only = module.params.get("fragments_only") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(templates)) + ) + template_idx = templates.index(template) + + # Get filters + mso.existing = {} + filter_idx = None + entry_idx = None + filters = [f.get("name") for f in schema_obj.get("templates")[template_idx]["filters"]] + if filter_name in filters: + filter_idx = filters.index(filter_name) + + entries = [f.get("name") for f in schema_obj.get("templates")[template_idx]["filters"][filter_idx]["entries"]] + if entry in entries: + entry_idx = entries.index(entry) + mso.existing = schema_obj.get("templates")[template_idx]["filters"][filter_idx]["entries"][entry_idx] + + if state == "query": + if entry is None: + if filter_idx is None: + mso.fail_json(msg="Filter '{filter}' not found".format(filter=filter_name)) + mso.existing = schema_obj.get("templates")[template_idx]["filters"][filter_idx]["entries"] + elif not mso.existing: + mso.fail_json(msg="Entry '{entry}' not found".format(entry=entry)) + mso.exit_json() + + filters_path = "/templates/{0}/filters".format(template) + filter_path = "/templates/{0}/filters/{1}".format(template, filter_name) + entries_path = "/templates/{0}/filters/{1}/entries".format(template, filter_name) + entry_path = "/templates/{0}/filters/{1}/entries/{2}".format(template, filter_name, entry) + ops = [] + + mso.previous = mso.existing + if state == "absent": + mso.proposed = mso.sent = {} + + if filter_idx is None: + # There was no filter to begin with + pass + elif entry_idx is None: + # There was no entry to begin with + pass + elif len(entries) == 1: + # There is only one entry, remove filter + mso.existing = {} + ops.append(dict(op="remove", path=filter_path)) + + else: + mso.existing = {} + ops.append(dict(op="remove", path=entry_path)) + + elif state == "present": + if not mso.existing: + if display_name is None: + display_name = entry + if ethertype is None: + ethertype = "unspecified" + if ip_protocol is None: + ip_protocol = "unspecified" + if tcp_session_rules is None: + tcp_session_rules = ["unspecified"] + if source_from is None: + source_from = "unspecified" + if source_to is None: + source_to = "unspecified" + if destination_from is None: + destination_from = "unspecified" + if destination_to is None: + destination_to = "unspecified" + if arp_flag is None: + arp_flag = "unspecified" + if stateful is None: + stateful = False + if fragments_only is None: + fragments_only = False + + payload = dict( + name=entry, + displayName=display_name, + description=filter_entry_description, + etherType=ethertype, + ipProtocol=ip_protocol, + tcpSessionRules=tcp_session_rules, + sourceFrom=source_from, + sourceTo=source_to, + destinationFrom=destination_from, + destinationTo=destination_to, + arpFlag=arp_flag, + stateful=stateful, + matchOnlyFragments=fragments_only, + ) + + mso.sanitize(payload, collate=True) + + if filter_idx is None: + # Filter does not exist, so we have to create it + if filter_display_name is None: + filter_display_name = filter_name + + payload = dict( + name=filter_name, + displayName=filter_display_name, + description=filter_description, + entries=[mso.sent], + ) + + ops.append(dict(op="add", path=filters_path + "/-", value=payload)) + + elif entry_idx is None: + # Entry does not exist, so we have to add it + ops.append(dict(op="add", path=entries_path + "/-", value=mso.sent)) + + else: + # Entry exists, we have to update it + for key, value in mso.sent.items(): + ops.append(dict(op="replace", path=entry_path + "/" + key, value=value)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_l3out.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_l3out.py new file mode 100644 index 000000000..4b8c1b66d --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_l3out.py @@ -0,0 +1,233 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_l3out +short_description: Manage l3outs in schema templates +description: +- Manage l3outs in schema templates on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + l3out: + description: + - The name of the l3out to manage. + type: str + aliases: [ name ] + description: + description: + - The description of l3out is supported on versions of MSO that are 3.3 or greater. + type: str + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + vrf: + description: + - The VRF associated to this L3out. + type: dict + suboptions: + name: + description: + - The name of the VRF to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced VRF. + - If this parameter is unspecified, it defaults to the current schema. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new L3out + cisco.mso.mso_schema_template_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + schema: Schema 1 + template: Template 1 + l3out: L3out 1 + vrf: + name: vrfName + schema: vrfSchema + template: vrfTemplate + state: present + delegate_to: localhost + +- name: Remove an L3out + cisco.mso.mso_schema_template_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + l3out: L3out 1 + state: absent + delegate_to: localhost + +- name: Query a specific L3outs + cisco.mso.mso_schema_template_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + schema: Schema 1 + template: Template 1 + l3out: L3out 1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all L3outs + cisco.mso.mso_schema_template_l3out: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_reference_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + l3out=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + description=dict(type="str"), + display_name=dict(type="str"), + vrf=dict(type="dict", options=mso_reference_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["l3out"]], + ["state", "present", ["l3out", "vrf"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + l3out = module.params.get("l3out") + description = module.params.get("description") + display_name = module.params.get("display_name") + vrf = module.params.get("vrf") + if vrf is not None and vrf.get("template") is not None: + vrf["template"] = vrf.get("template").replace(" ", "") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema objects + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get L3out + l3outs = [l3.get("name") for l3 in schema_obj.get("templates")[template_idx]["intersiteL3outs"]] + + if l3out is not None and l3out in l3outs: + l3out_idx = l3outs.index(l3out) + mso.existing = schema_obj.get("templates")[template_idx]["intersiteL3outs"][l3out_idx] + + if state == "query": + if l3out is None: + mso.existing = schema_obj.get("templates")[template_idx]["intersiteL3outs"] + elif not mso.existing: + mso.fail_json(msg="L3out '{l3out}' not found".format(l3out=l3out)) + mso.exit_json() + + l3outs_path = "/templates/{0}/intersiteL3outs".format(template) + l3out_path = "/templates/{0}/intersiteL3outs/{1}".format(template, l3out) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=l3out_path)) + + elif state == "present": + vrf_ref = mso.make_reference(vrf, "vrf", schema_id, template) + + if display_name is None and not mso.existing: + display_name = l3out + + payload = dict( + name=l3out, + displayName=display_name, + vrfRef=vrf_ref, + ) + + if description is not None: + payload.update(description=description) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=l3out_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=l3outs_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_migrate.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_migrate.py new file mode 100644 index 000000000..d0e15b8d0 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_migrate.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_migrate +short_description: Migrate Bridge Domains (BDs) and EPGs between templates +description: +- Migrate BDs and EPGs between templates of same and different schemas. +author: +- Anvitha Jain (@anvitha-jain) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + bds: + description: + - The name of the BDs to migrate. + type: list + elements: str + epgs: + description: + - The name of the EPGs and the ANP it is in to migrate. + type: list + elements: dict + suboptions: + epg: + description: + - The name of the EPG to migrate. + type: str + required: true + anp: + description: + - The name of the anp to migrate. + type: str + required: true + target_schema: + description: + - The name of the target_schema. + type: str + required: true + target_template: + description: + - The name of the target_template. + type: str + required: true + state: + description: + - Use C(present) for adding. + type: str + default: present +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Migration of objects between templates of same schema + mso_schema_template_migrate: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + target_schema: Schema 1 + target_template: Template 2 + bds: + - BD + epgs: + - epg: EPG1 + anp: ANP + state: present + delegate_to: localhost + +- name: Migration of objects between templates of different schema + mso_schema_template_migrate: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + target_schema: Schema 2 + target_template: Template 2 + bds: + - BD + epgs: + - epg: EPG1 + anp: ANP + state: present + delegate_to: localhost + +- name: Migration of BD object between templates of same schema + mso_schema_template_migrate: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + target_schema: Schema 1 + target_template: Template 2 + bds: + - BD + - BD1 + state: present + delegate_to: localhost + +- name: Migration of BD object between templates of different schema + mso_schema_template_migrate: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + target_schema: Schema 2 + target_template: Template 2 + bds: + - BD + - BD1 + state: present + delegate_to: localhost + +- name: Migration of EPG objects between templates of same schema + mso_schema_template_migrate: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + target_schema: Schema 2 + target_template: Template 2 + epgs: + - epg: EPG1 + anp: ANP + - epg: EPG2 + anp: ANP2 + state: present + delegate_to: localhost + +- name: Migration of EPG objects between templates of different schema + mso_schema_template_migrate: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + target_schema: Schema 2 + target_template: Template 2 + epgs: + - epg: EPG1 + anp: ANP + - epg: EPG2 + anp: ANP2 + state: present + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_object_migrate_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + bds=dict(type="list", elements="str"), + epgs=dict(type="list", elements="dict", options=mso_object_migrate_spec()), + target_schema=dict(type="str", required=True), + target_template=dict(type="str", required=True), + state=dict(type="str", default="present"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + target_schema = module.params.get("target_schema") + target_template = module.params.get("target_template").replace(" ", "") + bds = module.params.get("bds") + epgs = module.params.get("epgs") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema_id + schema_id = mso.lookup_schema(schema) + + target_schema_id = mso.lookup_schema(target_schema) + + if state == "present": + if schema_id is not None: + bds_payload = [] + if bds is not None: + for bd in bds: + bds_payload.append(dict(name=bd)) + + anp_dict = {} + if epgs is not None: + for epg in epgs: + if epg.get("anp") in anp_dict: + anp_dict[epg.get("anp")].append(dict(name=epg.get("epg"))) + else: + anp_dict[epg.get("anp")] = [dict(name=epg.get("epg"))] + + anps_payload = [] + for anp, epgs_payload in anp_dict.items(): + anps_payload.append(dict(name=anp, epgs=epgs_payload)) + + payload = dict( + targetSchemaId=target_schema_id, + targetTemplateName=target_template, + bds=bds_payload, + anps=anps_payload, + ) + + template = template.replace(" ", "%20") + + target_template = target_template.replace(" ", "%20") # removes API error for extra space + + mso.existing = mso.request(path="migrate/schema/{0}/template/{1}".format(schema_id, template), method="POST", data=payload) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_service_graph.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_service_graph.py new file mode 100644 index 000000000..70fadd804 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_service_graph.py @@ -0,0 +1,270 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_service_graph +short_description: Manage Service Graph in schema templates +description: +- Manage Service Graph in schema templates on Cisco ACI Multi-Site. +author: +- Shreyas Srish (@shrsr) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + service_graph: + description: + - The name of the Service Graph to manage. + type: str + aliases: [ name ] + description: + description: + - The description of Service Graph. + type: str + default: '' + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + service_nodes: + description: + - A list of node types to be associated with the Service Graph. + type: list + elements: dict + suboptions: + type: + description: + - The type of node + required: true + type: str + filter_after_first_node: + description: + - The filter applied after the first node. + type: str + choices: [ allow_all, filters_from_contract ] + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new Service Graph + cisco.mso.mso_schema_template_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + service_graph: graph1 + service_nodes: + - type: firewall + - type: other + - type: load-balancer + state: present + delegate_to: localhost + +- name: Remove a Service Graph + cisco.mso.mso_schema_template_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + service_graph: graph1 + state: absent + delegate_to: localhost + +- name: Query a specific Service Graph + cisco.mso.mso_schema_template_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + service_graph: graph1 + state: query + delegate_to: localhost + +- name: Query all Service Graphs + cisco.mso.mso_schema_template_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_service_graph_node_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + service_graph=dict(type="str", aliases=["name"]), + description=dict(type="str", default=""), + display_name=dict(type="str"), + service_nodes=dict(type="list", elements="dict", options=mso_service_graph_node_spec()), + filter_after_first_node=dict(type="str", choices=["allow_all", "filters_from_contract"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["service_graph"]], + ["state", "present", ["service_graph", "service_nodes"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + service_graph = module.params.get("service_graph") + display_name = module.params.get("display_name") + description = module.params.get("description") + service_nodes = module.params.get("service_nodes") + filter_after_first_node = module.params.get("filter_after_first_node") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json( + msg="Provided template '{template}' does not exist. Existing templates: {templates}".format(template=template, templates=", ".join(templates)) + ) + template_idx = templates.index(template) + + mso.existing = {} + service_graph_idx = None + + # Get Service Graphs + service_graphs = [f.get("name") for f in schema_obj.get("templates")[template_idx]["serviceGraphs"]] + if service_graph in service_graphs: + service_graph_idx = service_graphs.index(service_graph) + mso.existing = schema_obj.get("templates")[template_idx]["serviceGraphs"][service_graph_idx] + + if state == "query": + if service_graph is None: + mso.existing = schema_obj.get("templates")[template_idx]["serviceGraphs"] + if service_graph is not None and service_graph_idx is None: + mso.fail_json(msg="Service Graph '{service_graph}' not found".format(service_graph=service_graph)) + mso.exit_json() + + service_graphs_path = "/templates/{0}/serviceGraphs/-".format(template) + service_graph_path = "/templates/{0}/serviceGraphs/{1}".format(template, service_graph) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=service_graph_path)) + + elif state == "present": + nodes_payload = [] + service_node_index = 0 + + if filter_after_first_node == "allow_all": + filter_after_first_node = "allow-all" + elif filter_after_first_node == "filters_from_contract": + filter_after_first_node = "filters-from-contract" + + if display_name is None: + display_name = service_graph + + # Get service nodes + query_node_data = mso.query_service_node_types() + service_node_types = [f.get("name") for f in query_node_data] + if service_nodes is not None: + for node in service_nodes: + node_name = node.get("type") + if node_name in service_node_types: + service_node_index = service_node_index + 1 + for node_data in query_node_data: + if node_data["name"] == node_name: + payload = dict( + name=node_name, + serviceNodeTypeId=node_data.get("id"), + index=service_node_index, + serviceNodeRef=dict( + serviceNodeName=node_name, + serviceGraphName=service_graph, + templateName=template, + schemaId=schema_id, + ), + ) + if node_data.get("uuid"): + payload.update(uuid=node_data.get("uuid")) + nodes_payload.append(payload) + else: + mso.fail_json( + "Provided service node type '{node_name}' does not exist. Existing service node types include: {node_types}".format( + node_name=node_name, node_types=", ".join(service_node_types) + ) + ) + + payload = dict( + name=service_graph, + displayName=display_name, + description=description, + nodeFilter=filter_after_first_node, + serviceGraphRef=dict( + serviceGraphName=service_graph, + templateName=template, + schemaId=schema_id, + ), + serviceNodes=nodes_payload, + ) + + mso.sanitize(payload, collate=True) + + if not mso.existing: + ops.append(dict(op="add", path=service_graphs_path, value=payload)) + else: + ops.append(dict(op="replace", path=service_graph_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf.py new file mode 100644 index 000000000..efafd2387 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Anvitha Jain (@anvitha-jain) <anvjain@cisco.com> +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_template_vrf +short_description: Manage VRFs in schema templates +description: +- Manage VRFs in schema templates on Cisco ACI Multi-Site. +author: +- Anvitha Jain (@anvitha-jain) +- Dag Wieers (@dagwieers) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + vrf: + description: + - The name of the VRF to manage. + type: str + aliases: [ name ] + display_name: + description: + - The name as displayed on the MSO web interface. + type: str + layer3_multicast: + description: + - Whether to enable L3 multicast. + type: bool + vzany: + description: + - Whether to enable vzAny. + type: bool + ip_data_plane_learning: + description: + - Whether IP data plane learning is enabled or disabled. + - The APIC defaults to C(enabled) when unset during creation. + type: str + choices: [ disabled, enabled ] + preferred_group: + description: + - Whether to enable preferred Endpoint Group. + - The APIC defaults to C(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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new VRF + cisco.mso.mso_schema_template_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + vrf: VRF 1 + state: present + delegate_to: localhost + +- name: Remove an VRF + cisco.mso.mso_schema_template_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + vrf: VRF1 + state: absent + delegate_to: localhost + +- name: Query a specific VRFs + cisco.mso.mso_schema_template_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + vrf: VRF1 + state: query + delegate_to: localhost + register: query_result + +- name: Query all VRFs + cisco.mso.mso_schema_template_vrf: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="str", aliases=["name"]), # This parameter is not required for querying all objects + display_name=dict(type="str"), + layer3_multicast=dict(type="bool"), + vzany=dict(type="bool"), + preferred_group=dict(type="bool"), + ip_data_plane_learning=dict(type="str", choices=["enabled", "disabled"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["vrf"]], + ["state", "present", ["vrf"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + vrf = module.params.get("vrf") + display_name = module.params.get("display_name") + layer3_multicast = module.params.get("layer3_multicast") + vzany = module.params.get("vzany") + ip_data_plane_learning = module.params.get("ip_data_plane_learning") + preferred_group = module.params.get("preferred_group") + state = module.params.get("state") + + mso = MSOModule(module) + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get ANP + vrfs = [v.get("name") for v in schema_obj.get("templates")[template_idx]["vrfs"]] + + if vrf is not None and vrf in vrfs: + vrf_idx = vrfs.index(vrf) + mso.existing = schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx] + + if state == "query": + if vrf is None: + mso.existing = schema_obj.get("templates")[template_idx]["vrfs"] + elif not mso.existing: + mso.fail_json(msg="VRF '{vrf}' not found".format(vrf=vrf)) + mso.exit_json() + + vrfs_path = "/templates/{0}/vrfs".format(template) + vrf_path = "/templates/{0}/vrfs/{1}".format(template, vrf) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=vrf_path)) + + elif state == "present": + if display_name is None and not mso.existing: + display_name = vrf + + payload = dict( + name=vrf, + displayName=display_name, + l3MCast=layer3_multicast, + vzAnyEnabled=vzany, + preferredGroup=preferred_group, + ipDataPlaneLearning=ip_data_plane_learning, + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + # clean contractRef to fix api issue + for contract in mso.sent.get("vzAnyConsumerContracts"): + contract["contractRef"] = mso.dict_from_ref(contract.get("contractRef")) + for contract in mso.sent.get("vzAnyProviderContracts"): + contract["contractRef"] = mso.dict_from_ref(contract.get("contractRef")) + ops.append(dict(op="replace", path=vrf_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=vrfs_path + "/-", value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf_contract.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf_contract.py new file mode 100644 index 000000000..eaef8235c --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf_contract.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING 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: mso_schema_template_vrf_contract +short_description: Manage vrf contracts in schema templates +description: +- Manage vrf contracts in schema templates on Cisco ACI Multi-Site. +author: +- Cindy Zhao (@cizhao) +version_added: '0.0.8' +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template to change. + type: str + required: true + vrf: + description: + - The name of the VRF. + type: str + required: true + contract: + description: + - A contract associated to this VRF. + type: dict + suboptions: + name: + description: + - The name of the Contract to associate with. + required: true + type: str + schema: + description: + - The schema that defines the referenced contract. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The template that defines the referenced contract. + type: str + type: + description: + - The type of contract. + type: str + required: true + choices: [ consumer, provider ] + 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 +seealso: +- module: cisco.mso.mso_schema_template_vrf +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Add a contract to a VRF + cisco.mso.mso_schema_template_vrf_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + vrf: VRF 1 + contract: + name: Contract 1 + type: consumer + state: present + delegate_to: localhost + +- name: Remove a Contract + cisco.mso.mso_schema_template_vrf_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + vrf: VRF 1 + contract: + name: Contract 1 + type: consumer + state: absent + delegate_to: localhost + +- name: Query a specific Contract + cisco.mso.mso_schema_template_vrf_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + vrf: VRF 1 + contract: + name: Contract 1 + type: consumer + state: query + delegate_to: localhost + register: query_result + +- name: Query all Contracts + cisco.mso.mso_schema_template_vrf_contract: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + vrf: VRF 1 + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_contractref_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + vrf=dict(type="str", required=True), + contract=dict(type="dict", options=mso_contractref_spec()), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["contract"]], + ["state", "present", ["contract"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + vrf = module.params.get("vrf") + contract = module.params.get("contract") + if contract is not None and contract.get("template") is not None: + contract["template"] = contract.get("template").replace(" ", "") + state = module.params.get("state") + + mso = MSOModule(module) + if contract: + if contract.get("schema") is None: + contract["schema"] = schema + contract["schema_id"] = mso.lookup_schema(contract.get("schema")) + if contract.get("template") is None: + contract["template"] = template + + # Get schema + schema_id, schema_path, schema_obj = mso.query_schema(schema) + + # Get template + templates = [t.get("name") for t in schema_obj.get("templates")] + if template not in templates: + mso.fail_json(msg="Provided template '{0}' does not exist. Existing templates: {1}".format(template, ", ".join(templates))) + template_idx = templates.index(template) + + # Get VRF + vrfs = [e.get("name") for e in schema_obj.get("templates")[template_idx]["vrfs"]] + if vrf not in vrfs: + mso.fail_json(msg="Provided vrf '{vrf}' does not exist. Existing vrfs: {vrfs}".format(vrf=vrf, vrfs=", ".join(vrfs))) + vrf_idx = vrfs.index(vrf) + vrf_obj = schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx] + + if not vrf_obj.get("vzAnyEnabled"): + mso.fail_json(msg="vzAny attribute on vrf '{0}' is disabled.".format(vrf)) + + # Get Contract + if contract: + provider_contracts = [c.get("contractRef") for c in schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx]["vzAnyProviderContracts"]] + consumer_contracts = [c.get("contractRef") for c in schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx]["vzAnyConsumerContracts"]] + contract_ref = mso.contract_ref(**contract) + if contract_ref in provider_contracts and contract.get("type") == "provider": + contract_idx = provider_contracts.index(contract_ref) + contract_path = "/templates/{0}/vrfs/{1}/vzAnyProviderContracts/{2}".format(template, vrf, contract_idx) + mso.existing = schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx]["vzAnyProviderContracts"][contract_idx] + if contract_ref in consumer_contracts and contract.get("type") == "consumer": + contract_idx = consumer_contracts.index(contract_ref) + contract_path = "/templates/{0}/vrfs/{1}/vzAnyConsumerContracts/{2}".format(template, vrf, contract_idx) + mso.existing = schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx]["vzAnyConsumerContracts"][contract_idx] + if mso.existing.get("contractRef"): + mso.existing["contractRef"] = mso.dict_from_ref(mso.existing.get("contractRef")) + mso.existing["relationshipType"] = contract.get("type") + + if state == "query": + if not contract: + provider_contracts = [ + dict(contractRef=mso.dict_from_ref(c.get("contractRef")), relationshipType="provider") + for c in schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx]["vzAnyProviderContracts"] + ] + consumer_contracts = [ + dict(contractRef=mso.dict_from_ref(c.get("contractRef")), relationshipType="consumer") + for c in schema_obj.get("templates")[template_idx]["vrfs"][vrf_idx]["vzAnyConsumerContracts"] + ] + mso.existing = provider_contracts + consumer_contracts + elif not mso.existing: + mso.fail_json(msg="Contract '{0}' not found".format(contract.get("name"))) + + mso.exit_json() + + if contract.get("type") == "provider": + contracts_path = "/templates/{0}/vrfs/{1}/vzAnyProviderContracts/-".format(template, vrf) + if contract.get("type") == "consumer": + contracts_path = "/templates/{0}/vrfs/{1}/vzAnyConsumerContracts/-".format(template, vrf) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=contract_path)) + + elif state == "present": + payload = dict( + contractRef=dict( + contractName=contract.get("name"), + templateName=contract.get("template"), + schemaId=contract.get("schema_id"), + ), + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=contract_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=contracts_path, value=mso.sent)) + + mso.existing = mso.proposed + mso.existing["relationshipType"] = contract.get("type") + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(schema_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py new file mode 100644 index 000000000..a4a4f6cd0 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_schema_validate +short_description: Validate the schema before deploying it to site +description: +- This module is used to verify if a schema can be deployed to site without any error. +- This module can only be used on versions of MSO that are 3.3 or greater. +- Starting with MSO 3.3, the schema modules in this collection will skip some validation checks to allow part of the schema to be updated more easily. +- This module will check those validation after all changes have been made. +author: +- Anvitha Jain (@anvitha-jain) +version_added: "1.3.0" +options: + schema: + description: + - The name of the schema. + type: str + required: true + state: + description: + - Use C(query) to validate deploying the schema. + type: str + default: query + choices: [ query ] +seealso: +- module: cisco.mso.mso_schema_template_external_epg +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + state=dict(type="str", default="query", choices=["query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + schema = module.params.get("schema") + + mso = MSOModule(module) + + mso.existing = mso.validate_schema(schema_id=mso.lookup_schema(schema)) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_service_node_type.py b/ansible_collections/cisco/mso/plugins/modules/mso_service_node_type.py new file mode 100644 index 000000000..b8319256b --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_service_node_type.py @@ -0,0 +1,162 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_service_node_type +short_description: Manage Service Node Types +description: +- Manage Service Node Types on Cisco ACI Multi-Site. +author: +- Shreyas Srish (@shrsr) +options: + name: + description: + - The name of the node type. + type: str + display_name: + description: + - The name of the node type as displayed on the MSO web 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new Service Node Type + cisco.mso.mso_schema_service_node: + host: mso_host + username: admin + password: SomeSecretPassword + name: ips + display_name: ips + state: present + delegate_to: localhost + +- name: Remove a Service Node Type + cisco.mso.mso_schema_service_node: + host: mso_host + username: admin + password: SomeSecretPassword + name: ips + state: absent + delegate_to: localhost + +- name: Query a specific Service Node Type + cisco.mso.mso_schema_service_node: + host: mso_host + username: admin + password: SomeSecretPassword + name: ips + state: query + delegate_to: localhost + +- name: Query all Service Node Types + cisco.mso.mso_schema_service_node: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + name=dict(type="str"), + display_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", ["name"]], + ["state", "present", ["name"]], + ], + ) + + name = module.params.get("name") + display_name = module.params.get("display_name") + state = module.params.get("state") + + mso = MSOModule(module) + + mso.existing = {} + service_node_id = None + + # Get service node id + query_node_data = mso.query_service_node_types() + service_nodes = [f.get("name") for f in query_node_data] + if name in service_nodes: + for node_data in query_node_data: + if node_data.get("name") == name: + service_node_id = node_data.get("id") + mso.existing = node_data + + if state == "query": + if name is None: + mso.existing = query_node_data + if name is not None and service_node_id is None: + mso.fail_json(msg="Service Node Type '{service_node_type}' not found".format(service_node_type=name)) + mso.exit_json() + + service_nodes_path = "schemas/service-node-types" + service_node_path = "schemas/service-node-types/{0}".format(service_node_id) + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(service_node_path, method="DELETE") + + elif state == "present": + if display_name is None: + display_name = name + + payload = dict( + name=name, + displayName=display_name, + ) + mso.sanitize(payload, collate=True) + if not module.check_mode: + if not mso.existing: + mso.request(service_nodes_path, method="POST", data=payload) + elif mso.existing.get("displayName") != display_name: + mso.fail_json( + msg="Service Node Type '{0}' already exists with display name '{1}' which is different from provided display name '{2}'.".format( + name, mso.existing.get("displayName"), display_name + ) + ) + mso.existing = mso.proposed + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_site.py b/ansible_collections/cisco/mso/plugins/modules/mso_site.py new file mode 100644 index 000000000..a3778d28a --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_site.py @@ -0,0 +1,290 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_site +short_description: Manage sites +description: +- Manage sites on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + apic_password: + description: + - The password for the APICs. + - The apic_password attribute is not supported when using with ND platform. + - See the ND collection for complete site management. + type: str + apic_site_id: + description: + - The site ID of the APICs. + type: str + apic_username: + description: + - The username for the APICs. + - The apic_username attribute is not supported when using with ND platform. + - See the ND collection for complete site management. + type: str + default: admin + apic_login_domain: + description: + - The AAA login domain for the username for the APICs. + - The apic_login_domain attribute is not supported when using with ND platform. + - See the ND collection for complete site management. + type: str + site: + description: + - The name of the site. + type: str + aliases: [ name ] + labels: + description: + - The labels for this site. + - Labels that do not already exist will be automatically created. + - The labels attribute is not supported when using with ND platform. + - See the ND collection for complete site management. + type: list + elements: str + location: + description: + - Location of the site. + - The location attribute is not supported when using with ND platform. + - See the ND collection for complete site management. + type: dict + suboptions: + latitude: + description: + - The latitude of the location of the site. + type: float + longitude: + description: + - The longitude of the location of the site. + type: float + urls: + description: + - A list of URLs to reference the APICs. + - The urls attribute is not supported when using with ND platform. + - See the ND collection for complete site management. + type: list + elements: 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new site + cisco.mso.mso_site: + host: mso_host + username: admin + password: SomeSecretPassword + site: north_europe + description: North European Datacenter + apic_username: mso_admin + apic_password: AnotherSecretPassword + apic_site_id: 12 + urls: + - 10.2.3.4 + - 10.2.4.5 + - 10.3.5.6 + labels: + - NEDC + - Europe + - Diegem + location: + latitude: 50.887318 + longitude: 4.447084 + state: present + delegate_to: localhost + +- name: Remove a site + cisco.mso.mso_site: + host: mso_host + username: admin + password: SomeSecretPassword + site: north_europe + state: absent + delegate_to: localhost + +- name: Query a site + cisco.mso.mso_site: + host: mso_host + username: admin + password: SomeSecretPassword + site: north_europe + state: query + delegate_to: localhost + register: query_result + +- name: Query all sites + cisco.mso.mso_site: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + location_arg_spec = dict( + latitude=dict(type="float"), + longitude=dict(type="float"), + ) + + argument_spec = mso_argument_spec() + argument_spec.update( + apic_password=dict(type="str", no_log=True), + apic_site_id=dict(type="str"), + apic_username=dict(type="str", default="admin"), + apic_login_domain=dict(type="str"), + labels=dict(type="list", elements="str"), + location=dict(type="dict", options=location_arg_spec), + site=dict(type="str", aliases=["name"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + urls=dict(type="list", elements="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["site"]], + ["state", "present", ["apic_site_id", "site"]], + ], + ) + + apic_username = module.params.get("apic_username") + apic_password = module.params.get("apic_password") + apic_site_id = module.params.get("apic_site_id") + site = module.params.get("site") + location = module.params.get("location") + if location is not None: + latitude = module.params.get("location")["latitude"] + longitude = module.params.get("location")["longitude"] + state = module.params.get("state") + urls = module.params.get("urls") + apic_login_domain = module.params.get("apic_login_domain") + + mso = MSOModule(module) + + site_id = None + path = "sites" + api_version = "v1" + if mso.platform == "nd": + api_version = "v2" + + # Convert labels + labels = mso.lookup_labels(module.params.get("labels"), "site") + + # Query for mso.existing object(s) + if site: + if mso.platform == "nd": + site_info = mso.get_obj(path, api_version=api_version, common=dict(name=site)) + path = "sites/manage" + if site_info: + # If we found an existing object, continue with it + site_id = site_info.get("id") + if site_id is not None and site_id != "": + # Checking if site is managed by MSO + mso.existing = site_info + path = "sites/manage/{id}".format(id=site_id) + else: + mso.existing = mso.get_obj(path, name=site) + if mso.existing: + # If we found an existing object, continue with it + site_id = mso.existing.get("id") + path = "sites/{id}".format(id=site_id) + + else: + mso.existing = mso.query_objs(path, api_version=api_version) + + if state == "query": + pass + + elif state == "absent": + mso.previous = mso.existing + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.request(path, method="DELETE", qs=dict(force="true"), api_version=api_version) + mso.existing = {} + + elif state == "present": + mso.previous = mso.existing + + if mso.platform == "nd": + if mso.existing: + payload = mso.existing + else: + if site_info: + payload = site_info + payload["common"]["siteId"] = apic_site_id + else: + mso.fail_json(msg="Site '{0}' is not a valid Site configured at ND-level. Add Site to ND first.".format(site)) + + else: + payload = dict( + apicSiteId=apic_site_id, + id=site_id, + name=site, + urls=urls, + labels=labels, + username=apic_username, + password=apic_password, + ) + + if location is not None: + payload["location"] = dict( + lat=latitude, + long=longitude, + ) + + if apic_login_domain is not None and apic_login_domain not in ["", "local", "Local"]: + payload["username"] = "apic#{0}\\{1}".format(apic_login_domain, apic_username) + + mso.sanitize(payload, collate=True) + + if mso.existing: + if mso.check_changed(): + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="PUT", data=mso.sent, api_version=api_version) + else: + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent, api_version=api_version) + + if "password" in mso.existing: + mso.existing["password"] = "******" + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_tenant.py b/ansible_collections/cisco/mso/plugins/modules/mso_tenant.py new file mode 100644 index 000000000..17aa457e3 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_tenant.py @@ -0,0 +1,218 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2020, Cindy Zhao (@cizhao) <cizhao@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_tenant +short_description: Manage tenants +description: +- Manage tenants on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + tenant: + description: + - The name of the tenant. + type: str + aliases: [ name ] + display_name: + description: + - The name of the tenant to be displayed in the web UI. + type: str + description: + description: + - The description for this tenant. + type: str + users: + description: + - A list of associated users for this tenant. + - Using this property will replace any existing associated users. + - Admin user is always added to the associated user list irrespective of this parameter being used. + type: list + elements: str + sites: + description: + - A list of associated sites for this tenant. + - Using this property will replace any existing associated sites. + type: list + elements: str + orchestrator_only: + description: + - Orchestrator Only C(no) is used to delete the tenant from the MSO and Sites/APIC. + - C(yes) is used to remove the tenant only from the MSO. + type: str + choices: [ 'yes', 'no' ] + default: 'yes' + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Add a new tenant + cisco.mso.mso_tenant: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: north_europe + display_name: North European Datacenter + description: This tenant manages the NEDC environment. + state: present + delegate_to: localhost + +- name: Remove a tenant from MSO and Site/APIC + cisco.mso.mso_tenant: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: north_europe + orchestrator_only: no + state: absent + delegate_to: localhost + +- name: Remove a tenant from MSO only + cisco.mso.mso_tenant: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: north_europe + orchestrator_only: yes + state: absent + delegate_to: localhost + +- name: Query a tenant + cisco.mso.mso_tenant: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: north_europe + state: query + delegate_to: localhost + register: query_result + +- name: Query all tenants + cisco.mso.mso_tenant: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible_collections.cisco.mso.plugins.module_utils.constants import YES_OR_NO_TO_BOOL_STRING_MAP + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + description=dict(type="str"), + display_name=dict(type="str"), + tenant=dict(type="str", aliases=["name"]), + users=dict(type="list", elements="str"), + sites=dict(type="list", elements="str"), + orchestrator_only=dict(type="str", default="yes", choices=["yes", "no"]), + 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"]], + ["state", "present", ["tenant"]], + ], + ) + + description = module.params.get("description") + display_name = module.params.get("display_name") + tenant = module.params.get("tenant") + orchestrator_only = module.params.get("orchestrator_only") + state = module.params.get("state") + + mso = MSOModule(module) + + # Convert sites and users + sites = mso.lookup_sites(module.params.get("sites")) + users = mso.lookup_users(module.params.get("users")) + + tenant_id = None + path = "tenants" + + # Query for existing object(s) + if tenant: + mso.existing = mso.get_obj(path, name=tenant) + if mso.existing: + tenant_id = mso.existing.get("id") + # If we found an existing object, continue with it + path = "tenants/{id}".format(id=tenant_id) + else: + mso.existing = mso.query_objs(path) + + if state == "query": + pass + + elif state == "absent": + mso.previous = mso.existing + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + path = "{0}?msc-only={1}".format(path, YES_OR_NO_TO_BOOL_STRING_MAP.get(orchestrator_only)) + mso.existing = mso.request(path, method="DELETE") + + elif state == "present": + mso.previous = mso.existing + + payload = dict( + description=description, + id=tenant_id, + name=tenant, + displayName=display_name, + siteAssociations=sites, + userAssociations=users, + ) + + mso.sanitize(payload, collate=True) + + # Ensure displayName is not undefined + if mso.sent.get("displayName") is None: + mso.sent["displayName"] = tenant + + if mso.existing: + if mso.check_changed(): + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="PUT", data=mso.sent) + else: + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py b/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py new file mode 100644 index 000000000..735f85b13 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py @@ -0,0 +1,387 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2020, Shreyas Srish (@shrsr) <ssrish@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_tenant_site +short_description: Manage tenants with cloud sites. +description: +- Manage tenants with cloud sites on Cisco ACI Multi-Site. +author: +- Shreyas Srish (@shrsr) +options: + tenant: + description: + - The name of the tenant. + type: str + required: true + site: + description: + - The name of the site. + - This can either be cloud site or non-cloud site. + type: str + aliases: [ name ] + cloud_account: + description: + - Required for cloud site. + - Account id of AWS in the form '000000000000'. + - Account id of Azure in the form 'uni/tn-(tenant_name)/act-[(subscription_id)]-azure_vendor-azure'. + - Example values inside account id of Azure '(tenant_name)=tenant_test and (subscription_id)=10'. + type: str + security_domains: + description: + - List of security domains for sites. + type: list + elements: str + default: [] + aws_account_org: + description: + - AWS account for organization. + default: false + type: bool + aws_trusted: + description: + - AWS account's access in trusted mode. Credentials are required, when set to false. + type: bool + aws_access_key: + description: + - AWS account's access key id. This is required when aws_trusted is set to false. + type: str + azure_access_type: + description: + - Managed mode for Azure. + - Unmanaged mode for Azure. + - Shared mode if the attribute is not specified. + choices: [ managed, unmanaged, shared ] + default: shared + type: str + azure_active_directory_id: + description: + - Azure account's active directory id. + - This attribute is required when azure_access_type is in unmanaged mode. + type: str + azure_active_directory_name: + description: + - Azure account's active directory name. Example being 'CiscoINSBUAd' as active directory name. + - This attribute is required when azure_access_type is in unmanaged mode. + type: str + azure_subscription_id: + description: + - Azure account's subscription id. + - This attribute is required when azure_access_type is either in managed mode or unmanaged mode. + type: str + azure_application_id: + description: + - Azure account's application id. + - This attribute is required when azure_access_type is either in managed mode or unmanaged mode. + type: str + azure_credential_name: + description: + - Azure account's credential name. + - This attribute is required when azure_access_type is in unmanaged mode. + type: str + secret_key: + description: + - secret key of AWS for untrusted account. Required when aws_trusted is set to false. + - secret key of Azure account for unmanaged identity. Required in unmanaged mode of Azure account. + 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.mso.modules +""" + +EXAMPLES = r""" +- name: Associate a non-cloud site with a tenant + cisco.mso.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + site: site_name + state: present + delegate_to: localhost + +- name: Associate AWS site with a tenant, with aws_trusted set to true + cisco.mso.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + site: site_name + cloud_account: '000000000000' + aws_trusted: true + state: present + delegate_to: localhost + +- name: Associate AWS site with a tenant, with aws_trusted set to false + cisco.mso.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + site: AWS + cloud_account: '000000000000' + aws_trusted: false + aws_access_key: '1' + secret_key: '0' + aws_account_org: false + state: present + delegate_to: localhost + +- name: Associate Azure site in managed mode + mso.cisco.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + site: site_name + cloud_account: uni/tn-ansible_test/act-[9]-azure_vendor-azure + azure_access_type: managed + azure_subscription_id: '9' + azure_application_id: '100' + state: present + delegate_to: localhost + +- name: Associate Azure site in unmanaged mode + mso.cisco.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + site: site_name + cloud_account: uni/tn-ansible_test/act-[9]-azure_vendor-azure + azure_access_type: unmanaged + azure_subscription_id: '9' + azure_application_id: '100' + azure_credential_name: cApicApp + secret_key: iins + azure_active_directory_id: '32' + azure_active_directory_name: CiscoINSBUAd + state: present + delegate_to: localhost + +- name: Dissociate a site + cisco.mso.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + site: site_name + state: absent + delegate_to: localhost + +- name: Query a site + cisco.mso.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + site: site_name + state: query + delegate_to: localhost + +- name: Query all sites of a tenant + cisco.mso.mso_tenant_site: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: tenant_name + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + tenant=dict(type="str", aliases=["name"], required=True), + site=dict(type="str", aliases=["name"]), + cloud_account=dict(type="str"), + security_domains=dict(type="list", elements="str", default=[]), + aws_trusted=dict(type="bool"), + azure_access_type=dict(type="str", default="shared", choices=["managed", "unmanaged", "shared"]), + azure_active_directory_id=dict(type="str"), + aws_access_key=dict(type="str", no_log=True), + aws_account_org=dict(type="bool", default="false"), + azure_active_directory_name=dict(type="str"), + azure_subscription_id=dict(type="str"), + azure_application_id=dict(type="str"), + azure_credential_name=dict(type="str"), + secret_key=dict(type="str", no_log=True), + 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", "site"]], + ["state", "present", ["tenant", "site"]], + ], + ) + + state = module.params.get("state") + security_domains = module.params.get("security_domains") + cloud_account = module.params.get("cloud_account") + azure_access_type = module.params.get("azure_access_type") + azure_credential_name = module.params.get("azure_credential_name") + azure_application_id = module.params.get("azure_application_id") + azure_active_directory_id = module.params.get("azure_active_directory_id") + azure_active_directory_name = module.params.get("azure_active_directory_name") + azure_subscription_id = module.params.get("azure_subscription_id") + secret_key = module.params.get("secret_key") + aws_account_org = module.params.get("aws_account_org") + aws_access_key = module.params.get("aws_access_key") + aws_trusted = module.params.get("aws_trusted") + + mso = MSOModule(module) + + # Get tenant_id and site_id + tenant_id = mso.lookup_tenant(module.params.get("tenant")) + site_id = mso.lookup_site(module.params.get("site")) + tenants = [(t.get("id")) for t in mso.query_objs("tenants")] + tenant_idx = tenants.index((tenant_id)) + + # set tenent and port paths + tenant_path = "tenants/{0}".format(tenant_id) + ops = [] + ports_path = "/siteAssociations/-" + port_path = "/siteAssociations/{0}".format(site_id) + + payload = dict( + siteId=site_id, + securityDomains=security_domains, + cloudAccount=cloud_account, + ) + + if cloud_account: + if "azure" in cloud_account: + azure_account = dict( + accessType=azure_access_type, + securityDomains=security_domains, + vendor="azure", + ) + + payload["azureAccount"] = [azure_account] + + cloudSubscription = dict( + cloudSubscriptionId=azure_subscription_id, + cloudApplicationId=azure_application_id, + ) + + payload["azureAccount"][0]["cloudSubscription"] = cloudSubscription + + if azure_access_type == "shared": + payload["azureAccount"] = [] + + if azure_access_type == "managed": + if not azure_subscription_id: + mso.fail_json(msg="azure_susbscription_id is required when in managed mode.") + if not azure_application_id: + mso.fail_json(msg="azure_application_id is required when in managed mode.") + payload["azureAccount"][0]["cloudApplication"] = [] + payload["azureAccount"][0]["cloudActiveDirectory"] = [] + + if azure_access_type == "unmanaged": + if not azure_subscription_id: + mso.fail_json(msg="azure_subscription_id is required when in unmanaged mode.") + if not azure_application_id: + mso.fail_json(msg="azure_application_id is required when in unmanaged mode.") + if not secret_key: + mso.fail_json(msg="secret_key is required when in unmanaged mode.") + if not azure_active_directory_id: + mso.fail_json(msg="azure_active_directory_id is required when in unmanaged mode.") + if not azure_active_directory_name: + mso.fail_json(msg="azure_active_directory_name is required when in unmanaged mode.") + if not azure_credential_name: + mso.fail_json(msg="azure_credential_name is required when in unmanaged mode.") + azure_account.update( + accessType="credentials", + ) + cloudApplication = dict( + cloudApplicationId=azure_application_id, + cloudCredentialName=azure_credential_name, + secretKey=secret_key, + cloudActiveDirectoryId=azure_active_directory_id, + ) + cloudActiveDirectory = dict(cloudActiveDirectoryId=azure_active_directory_id, cloudActiveDirectoryName=azure_active_directory_name) + payload["azureAccount"][0]["cloudApplication"] = [cloudApplication] + payload["azureAccount"][0]["cloudActiveDirectory"] = [cloudActiveDirectory] + + else: + aws_account = dict( + accountId=cloud_account, + isTrusted=aws_trusted, + accessKeyId=aws_access_key, + secretKey=secret_key, + isAccountInOrg=aws_account_org, + ) + + if not aws_trusted: + if not aws_access_key: + mso.fail_json(msg="aws_access_key is a required field in untrusted mode.") + if not secret_key: + mso.fail_json(msg="secret_key is a required field in untrusted mode.") + payload["awsAccount"] = [aws_account] + + sites = [(s.get("siteId")) for s in mso.query_objs("tenants")[tenant_idx]["siteAssociations"]] + + if site_id in sites: + site_idx = sites.index((site_id)) + mso.existing = mso.query_objs("tenants")[tenant_idx]["siteAssociations"][site_idx] + + if state == "query": + if len(sites) == 0: + mso.fail_json(msg="No site associated with tenant Id {0}".format(tenant_id)) + elif site_id not in sites and site_id is not None: + mso.fail_json(msg="Site Id {0} not associated with tenant Id {1}".format(site_id, tenant_id)) + elif site_id is None: + mso.existing = mso.query_objs("tenants")[tenant_idx]["siteAssociations"] + mso.exit_json() + + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=port_path)) + if state == "present": + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=port_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=ports_path, value=mso.sent)) + + mso.existing = mso.proposed + + if not module.check_mode and mso.proposed != mso.previous: + mso.request(tenant_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_user.py b/ansible_collections/cisco/mso/plugins/modules/mso_user.py new file mode 100644 index 000000000..37127a7f1 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_user.py @@ -0,0 +1,283 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_user +short_description: Manage users +description: +- Manage users on Cisco ACI Multi-Site. +author: +- Dag Wieers (@dagwieers) +options: + user: + description: + - The name of the user. + type: str + aliases: [ name ] + user_password: + description: + - The password of the user. + type: str + first_name: + description: + - The first name of the user. + - This parameter is required when creating new users. + type: str + last_name: + description: + - The last name of the user. + - This parameter is required when creating new users. + type: str + email: + description: + - The email address of the user. + - This parameter is required when creating new users. + type: str + phone: + description: + - The phone number of the user. + - This parameter is required when creating new users. + type: str + account_status: + description: + - The status of the user account. + type: str + choices: [ active, inactive ] + domain: + description: + - The domain this user belongs to. + - When creating new users, this defaults to C(Local). + type: str + roles: + description: + - The roles for this user and their access types (read or write). + - Access type defaults to C(write). + type: list + elements: 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 +notes: +- A default installation of ACI Multi-Site ships with admin password 'we1come!' which requires a password change on first login. + See the examples of how to change the 'admin' password using Ansible. +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Update initial admin password + cisco.mso.mso_user: + host: mso_host + username: admin + password: initialPassword + validate_certs: false + user: admin + user_password: newPassword + state: present + delegate_to: localhost + +- name: Add a new user + cisco.mso.mso_user: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + user: dag + user_password: userPassword + first_name: Dag + last_name: Wieers + email: dag@wieers.com + phone: +32 478 436 299 + roles: + - name: siteManager + access_type: write + - name: schemaManager + access_type: read + state: present + delegate_to: localhost + +- name: Add a new user + cisco.mso.mso_user: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + user: dag + first_name: Dag + last_name: Wieers + email: dag@wieers.com + phone: +32 478 436 299 + roles: + - powerUser + delegate_to: localhost + +- name: Remove a user + cisco.mso.mso_user: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + user: dag + state: absent + delegate_to: localhost + +- name: Query a user + cisco.mso.mso_user: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + user: dag + state: query + delegate_to: localhost + register: query_result + +- name: Query all users + cisco.mso.mso_user: + host: mso_host + username: admin + password: SomeSecretPassword + validate_certs: false + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" # """ + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, issubset + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + user=dict(type="str", aliases=["name"]), + user_password=dict(type="str", no_log=True), + first_name=dict(type="str"), + last_name=dict(type="str"), + email=dict(type="str"), + phone=dict(type="str"), + # TODO: What possible options do we have ? + account_status=dict(type="str", choices=["active", "inactive"]), + domain=dict(type="str"), + roles=dict(type="list", elements="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", ["user"]], + ["state", "present", ["user"]], + ], + ) + + user_name = module.params.get("user") + user_password = module.params.get("user_password") + first_name = module.params.get("first_name") + last_name = module.params.get("last_name") + email = module.params.get("email") + phone = module.params.get("phone") + account_status = module.params.get("account_status") + state = module.params.get("state") + + mso = MSOModule(module) + + roles = mso.lookup_roles(module.params.get("roles")) + domain = mso.lookup_domain(module.params.get("domain")) + + user_id = None + path = "users" + + # Query for existing object(s) + if user_name: + if mso.module._socket_path and mso.connection.get_platform() == "cisco.nd": + mso.existing = mso.get_obj(path, loginID=user_name, api_version="v2") + if mso.existing: + mso.existing["id"] = mso.existing.get("userID") + mso.existing["username"] = mso.existing.get("loginID") + else: + mso.existing = mso.get_obj(path, username=user_name) + if mso.existing: + user_id = mso.existing.get("id") + # If we found an existing object, continue with it + path = "users/{id}".format(id=user_id) + else: + mso.existing = mso.query_objs(path) + + if state == "query": + pass + + elif state == "absent": + mso.previous = mso.existing + if mso.existing: + if module.check_mode: + mso.existing = {} + else: + mso.existing = mso.request(path, method="DELETE") + + elif state == "present": + mso.previous = mso.existing + + payload = dict( + id=user_id, + username=user_name, + firstName=first_name, + lastName=last_name, + emailAddress=email, + phoneNumber=phone, + accountStatus=account_status, + domainId=domain, + roles=roles, + # active=True, + # remote=True, + ) + + if user_password is not None: + payload.update(password=user_password) + + mso.sanitize(payload, collate=True) + + if mso.sent.get("accountStatus") is None: + mso.sent["accountStatus"] = "active" + + if mso.existing: + if not issubset(mso.sent, mso.existing): + # NOTE: Since MSO always returns '******' as password, we need to assume a change + if "password" in mso.proposed: + mso.module.warn("A password change is assumed, as the MSO REST API does not return passwords we do not know.") + mso.result["changed"] = True + + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="PUT", data=mso.sent) + + else: + if user_password is None: + mso.fail_json("The user {0} does not exist. The 'user_password' attribute is required to create a new user.".format(user_name)) + if module.check_mode: + mso.existing = mso.proposed + else: + mso.existing = mso.request(path, method="POST", data=mso.sent) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_version.py b/ansible_collections/cisco/mso/plugins/modules/mso_version.py new file mode 100644 index 000000000..19668afb4 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_version.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Lionel Hercot (@lhercot) <lhercot@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": "community"} + +DOCUMENTATION = r""" +--- +module: mso_version +short_description: Get version of MSO +description: +- Retrieve the code version of Cisco Multi-Site Orchestrator. +author: +- Lionel Hercot (@lhercot) +options: + state: + description: + - Use C(query) for retrieving the version object. + type: str + choices: [ query ] + default: query +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Get MSO version + cisco.mso.mso_version: + host: mso_host + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update(state=dict(type="str", default="query", choices=["query"])) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + mso = MSOModule(module) + + path = "platform/version" + + # Query for mso.existing object + mso.existing = mso.query_obj(path) + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/mso/plugins/modules/ndo_schema_template_deploy.py b/ansible_collections/cisco/mso/plugins/modules/ndo_schema_template_deploy.py new file mode 100644 index 000000000..b8bdb63ae --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/ndo_schema_template_deploy.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Akini Ross (@akinross) <akinross@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": "community"} + +DOCUMENTATION = r""" +--- +module: ndo_schema_template_deploy +short_description: Deploy schema templates to sites for NDO v3.7 and higher +description: +- Deploy schema templates to sites. +- Prior to deploy or redeploy a schema validation is executed. +- When schema validation fails, M(cisco.mso.ndo_schema_template_deploy) fails and deploy or redeploy will not be executed. +- Only supports NDO v3.7 and higher +author: +- Akini Ross (@akinross) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + sites: + description: + - The name of the site(s). + type: list + elements: str + state: + description: + - Use C(deploy) to deploy schema template. + - Use C(redeploy) to redeploy schema template. + - Use C(undeploy) to undeploy schema template from a site. + - Use C(query) to get deployment status. + type: str + choices: [ deploy, redeploy, undeploy, query ] + default: deploy +seealso: +- module: cisco.mso.mso_schema_site +- module: cisco.mso.mso_schema_template +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Deploy a schema template + cisco.mso.ndo_schema_template_deploy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: deploy + delegate_to: localhost + +- name: Redeploy a schema template + cisco.mso.ndo_schema_template_deploy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: redeploy + delegate_to: localhost + +- name: Undeploy a schema template + cisco.mso.ndo_schema_template_deploy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + sites: [ Site1, Site2 ] + state: undeploy + delegate_to: localhost + +- name: Query a schema template deploy status + cisco.mso.ndo_schema_template_deploy: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + state: query + delegate_to: localhost +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + sites=dict(type="list", elements="str"), + state=dict(type="str", default="deploy", choices=["deploy", "redeploy", "undeploy", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "undeploy", ["sites"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + sites = module.params.get("sites") + state = module.params.get("state") + + mso = MSOModule(module) + schema_id = mso.lookup_schema(schema) + + if state == "query": + path = "status/schema/{0}/template/{1}".format(schema_id, template) + method = "GET" + payload = None + else: + path = "task" + method = "POST" + payload = dict(schemaId=schema_id, templateName=template) + if state == "deploy": + mso.validate_schema(schema_id) + payload.update(isRedeploy=False) + elif state == "redeploy": + mso.validate_schema(schema_id) + payload.update(isRedeploy=True) + elif state == "undeploy": + payload.update(undeploy=[site.get("siteId") for site in mso.lookup_sites(sites)]) + + if not module.check_mode: + mso.existing = mso.request(path, method=method, data=payload) + mso.exit_json() + + +if __name__ == "__main__": + main() |