summaryrefslogtreecommitdiffstats
path: root/ansible_collections/cisco/mso/plugins/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/cisco/mso/plugins/modules
parentInitial commit. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_backup.py331
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py219
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy.py167
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy_option.py193
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy.py166
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy_provider.py256
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_label.py164
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py243
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_rest.py186
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_role.py285
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema.py132
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py125
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py194
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp.py224
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg.py301
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_bulk_staticport.py464
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_domain.py476
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_selector.py392
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticleaf.py258
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_staticport.py444
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_subnet.py281
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd.py236
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_l3out.py255
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd_subnet.py290
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg.py234
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_external_epg_selector.py291
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_l3out.py246
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_service_graph.py279
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf.py207
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region.py274
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr.py304
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_cidr_subnet.py320
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf_region_hub_network.py245
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py263
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp.py210
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg.py471
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_contract.py263
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_selector.py277
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_subnet.py256
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd.py566
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_dhcp_policy.py245
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd_subnet.py262
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_clone.py222
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_filter.py396
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_contract_service_graph.py317
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy.py147
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy_status.py163
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg.py337
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_contract.py247
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_selector.py250
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_external_epg_subnet.py224
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_externalepg.py337
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_filter_entry.py369
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_l3out.py233
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_migrate.py246
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_service_graph.py270
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf.py228
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf_contract.py263
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py74
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_service_node_type.py162
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_site.py290
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_tenant.py218
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py387
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_user.py283
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_version.py65
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/ndo_schema_template_deploy.py153
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()