diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:27 +0000 |
commit | 3b0807ad7b283c46c21862eb826dcbb4ad04e5e2 (patch) | |
tree | 6461ea75f03eca87a5a90c86c3c9a787a6ad037e /ansible_collections/cisco/mso/plugins | |
parent | Adding debian version 7.7.0+dfsg-3. (diff) | |
download | ansible-3b0807ad7b283c46c21862eb826dcbb4ad04e5e2.tar.xz ansible-3b0807ad7b283c46c21862eb826dcbb4ad04e5e2.zip |
Merging upstream version 9.4.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/cisco/mso/plugins')
9 files changed, 716 insertions, 109 deletions
diff --git a/ansible_collections/cisco/mso/plugins/.DS_Store b/ansible_collections/cisco/mso/plugins/.DS_Store Binary files differindex 1ffed58d7..7c7d7d98b 100644 --- a/ansible_collections/cisco/mso/plugins/.DS_Store +++ b/ansible_collections/cisco/mso/plugins/.DS_Store diff --git a/ansible_collections/cisco/mso/plugins/doc_fragments/modules.py b/ansible_collections/cisco/mso/plugins/doc_fragments/modules.py index c7d3d81c0..36e4a84ac 100644 --- a/ansible_collections/cisco/mso/plugins/doc_fragments/modules.py +++ b/ansible_collections/cisco/mso/plugins/doc_fragments/modules.py @@ -47,9 +47,9 @@ options: timeout: description: - The socket level timeout in seconds. + - The default value is 30 seconds. - If the value is not specified in the task, the value of environment variable C(MSO_TIMEOUT) will be used instead. type: int - default: 30 use_proxy: description: - If C(false), it will not use a proxy, even if one is defined in an environment variable on the target hosts. @@ -75,6 +75,7 @@ options: - The login domain name to use for authentication. - The default value is Local. - If the value is not specified in the task, the value of environment variable C(MSO_LOGIN_DOMAIN) will be used instead. + - When using a HTTPAPI connection plugin the inventory variable C(ansible_httpapi_login_domain) will be used if this attribute is not specified. type: str requirements: - Multi Site Orchestrator v2.1 or newer diff --git a/ansible_collections/cisco/mso/plugins/httpapi/mso.py b/ansible_collections/cisco/mso/plugins/httpapi/mso.py index 5d69c8a64..286e9dbd0 100644 --- a/ansible_collections/cisco/mso/plugins/httpapi/mso.py +++ b/ansible_collections/cisco/mso/plugins/httpapi/mso.py @@ -1,5 +1,6 @@ # Copyright: (c) 2020, Lionel Hercot (@lhercot) <lhercot@cisco.com> # Copyright: (c) 2020, Cindy Zhao (@cizhao) <cizhao@cisco.com> +# Copyright: (c) 2023, Akini Ross (@akinross) <akinross@cisco.com> # # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -16,19 +17,32 @@ description: a connection to MSO, send API requests and process the response. version_added: "1.2.0" +options: + login_domain: + description: + - The login domain name to use for authentication. + - The default value is Local. + type: string + env: + - name: ANSIBLE_HTTPAPI_LOGIN_DOMAIN + vars: + - name: ansible_httpapi_login_domain """ import json import re -import pickle - -# import ipaddress import traceback from ansible.module_utils.six import PY3 from ansible.module_utils._text import to_text from ansible.module_utils.connection import ConnectionError from ansible.plugins.httpapi import HttpApiBase +from copy import copy + + +CONNECTION_MAP = {"username": "remote_user", "timeout": "persistent_command_timeout"} +RESET_KEYS = ["username", "password", "login_domain", "host", "port"] +CONNECTION_KEYS = RESET_KEYS + ["use_proxy", "use_ssl", "timeout", "validate_certs"] class HttpApi(HttpApiBase): @@ -47,6 +61,8 @@ class HttpApi(HttpApiBase): self.status = -1 self.info = {} + self.connection_parameters = {} + def get_platform(self): return self.platform @@ -70,20 +86,14 @@ class HttpApi(HttpApiBase): path = "/mso/api/v1/auth/login" full_path = self.connection.get_option("host") + path - if (self.params.get("login_domain") is not None) and (self.params.get("login_domain") != "Local"): - domain_id = self._get_login_domain_id(self.params.get("login_domain")) - payload = {"username": self.connection.get_option("remote_user"), "password": self.connection.get_option("password"), "domainId": domain_id} - else: - payload = {"username": self.connection.get_option("remote_user"), "password": self.connection.get_option("password")} - - # Override the global username/password with the ones specified per task - if self.params.get("username") is not None: - payload["username"] = self.params.get("username") - if self.params.get("password") is not None: - payload["password"] = self.params.get("password") + payload = {"username": username, "password": password} + if self.connection_parameters["login_domain"] is not None and self.connection_parameters["login_domain"] != "Local": + payload["domainId"] = self._get_login_domain_id(self.connection_parameters["login_domain"]) + data = json.dumps(payload) try: - self.connection.queue_message("vvvv", "login() - connection.send({0}, {1}, {2}, {3})".format(path, data, method, self.headers)) + payload.pop("password") + self.connection.queue_message("vvvv", "login() - connection.send({0}, {1}, {2}, {3})".format(path, payload, method, self.headers)) response, response_data = self.connection.send(path, data, method=method, headers=self.headers) # Handle MSO response self.status = response.getcode() @@ -107,7 +117,7 @@ class HttpApi(HttpApiBase): path = "/mso/api/v1/auth/logout" try: - response, response_data = self.connection.send(path, {}, method=method, headers=self.headers) + self.connection.send(path, {}, method=method, headers=self.headers) except Exception as e: self.error = dict(code=self.status, message="Error on attempt to logout from MSO. {0}".format(e)) raise ConnectionError(json.dumps(self._verify_response(None, method, self.connection.get_option("host") + path, None))) @@ -126,47 +136,8 @@ class HttpApi(HttpApiBase): data = {} self.connection.queue_message("vvvv", "send_request method called") - # # Case1: List of hosts is provided - # self.backup_hosts = self.set_backup_hosts() - # if not self.backup_hosts: - if self.connection._connected is True and self.params.get("host") != self.connection.get_option("host"): - self.connection._connected = False - self.connection.queue_message( - "vvvv", - "send_request reseting connection as host has changed from {0} to {1}".format(self.connection.get_option("host"), self.params.get("host")), - ) - - if self.params.get("host") is not None: - self.connection.set_option("host", self.params.get("host")) - - else: - try: - with open("my_hosts.pk", "rb") as fi: - self.host_counter = pickle.load(fi) - except FileNotFoundError: - pass - try: - self.connection.set_option("host", self.backup_hosts[self.host_counter]) - except (IndexError, TypeError): - pass - - if self.params.get("port") is not None: - self.connection.set_option("port", self.params.get("port")) - - if self.params.get("username") is not None: - self.connection.set_option("remote_user", self.params.get("username")) - - if self.params.get("password") is not None: - self.connection.set_option("password", self.params.get("password")) - - if self.params.get("use_proxy") is not None: - self.connection.set_option("use_proxy", self.params.get("use_proxy")) - - if self.params.get("use_ssl") is not None: - self.connection.set_option("use_ssl", self.params.get("use_ssl")) - - if self.params.get("validate_certs") is not None: - self.connection.set_option("validate_certs", self.params.get("validate_certs")) + + self.set_connection_parameters() # Perform some very basic path input validation. path = str(path) @@ -187,18 +158,26 @@ class HttpApi(HttpApiBase): raise ConnectionError(json.dumps(self._verify_response(None, method, full_path, None))) return self._verify_response(response, method, full_path, rdata) - def handle_error(self): - self.host_counter += 1 - if self.host_counter == len(self.backup_hosts): - raise ConnectionError("No hosts left in cluster to continue operation") - with open("my_hosts.pk", "wb") as host_file: - pickle.dump(self.host_counter, host_file) - try: - self.connection.set_option("host", self.backup_hosts[self.host_counter]) - except IndexError: - pass - self.login(self.connection.get_option("remote_user"), self.connection.get_option("password")) - return True + def set_connection_parameters(self): + connection_parameters = {} + for key in CONNECTION_KEYS: + if key == "login_domain": + value = self.params.get(key) if self.params.get(key) is not None else self.get_option(CONNECTION_MAP.get(key, key)) + self.set_option(key, value) + else: + value = self.params.get(key) if self.params.get(key) is not None else self.connection.get_option(CONNECTION_MAP.get(key, key)) + self.connection.set_option(CONNECTION_MAP.get(key, key), value) + + connection_parameters[key] = value + if value != self.connection_parameters.get(key) and key in RESET_KEYS: + self.connection._connected = False + self.connection.queue_message("vvvv", "set_connection_parameters() - resetting connection due to '{0}' change".format(key)) + + if self.connection_parameters != connection_parameters: + self.connection_parameters = copy(connection_parameters) + connection_parameters.pop("password") + msg = "set_connection_parameters() - changed connection parameters {0}".format(connection_parameters) + self.connection.queue_message("vvvv", msg) def _verify_response(self, response, method, path, data): """Process the return code and response object from MSO""" diff --git a/ansible_collections/cisco/mso/plugins/module_utils/constants.py b/ansible_collections/cisco/mso/plugins/module_utils/constants.py index ea461f76c..2f3e0d472 100644 --- a/ansible_collections/cisco/mso/plugins/module_utils/constants.py +++ b/ansible_collections/cisco/mso/plugins/module_utils/constants.py @@ -22,3 +22,19 @@ NDO_4_UNIQUE_IDENTIFIERS = ["templateID", "autoRouteTargetImport", "autoRouteTar NDO_API_VERSION_FORMAT = "/mso/api/{api_version}" NDO_API_VERSION_PATH_FORMAT = "/mso/api/{api_version}/{path}" + +EPG_U_SEG_ATTR_TYPE_MAP = { + "ip": "ip", + "mac": "mac", + "dns": "dns", + "vm_datacenter": "rootContName", + "vm_hypervisor_identifier": "hv", + "vm_operating_system": "guest-os", + "vm_tag": "tag", + "vm_identifier": "vm", + "vmm_domain": "domain", + "vm_name": "vm-name", + "vnic_dn": "vnic", +} + +EPG_U_SEG_ATTR_OPERATOR_LIST = ["equals", "contains", "starts_with", "ends_with"] diff --git a/ansible_collections/cisco/mso/plugins/module_utils/mso.py b/ansible_collections/cisco/mso/plugins/module_utils/mso.py index 91475336b..4bc9053ef 100644 --- a/ansible_collections/cisco/mso/plugins/module_utils/mso.py +++ b/ansible_collections/cisco/mso/plugins/module_utils/mso.py @@ -103,7 +103,7 @@ def mso_argument_spec(): username=dict(type="str", required=False, fallback=(env_fallback, ["MSO_USERNAME", "ANSIBLE_NET_USERNAME"])), password=dict(type="str", required=False, no_log=True, fallback=(env_fallback, ["MSO_PASSWORD", "ANSIBLE_NET_PASSWORD"])), output_level=dict(type="str", default="normal", choices=["debug", "info", "normal"], fallback=(env_fallback, ["MSO_OUTPUT_LEVEL"])), - timeout=dict(type="int", default=30, fallback=(env_fallback, ["MSO_TIMEOUT"])), + timeout=dict(type="int", fallback=(env_fallback, ["MSO_TIMEOUT"])), use_proxy=dict(type="bool", fallback=(env_fallback, ["MSO_USE_PROXY"])), use_ssl=dict(type="bool", fallback=(env_fallback, ["MSO_USE_SSL"])), validate_certs=dict(type="bool", fallback=(env_fallback, ["MSO_VALIDATE_CERTS"])), @@ -296,7 +296,7 @@ class MSOModule(object): self.params = module.params self.result = dict(changed=False) self.headers = {"Content-Type": "text/json"} - self.platform = "mso" + self.platform = "local" # normal output self.existing = dict() @@ -333,6 +333,8 @@ class MSOModule(object): self.params["use_proxy"] = True if self.params.get("validate_certs") is None: self.params["validate_certs"] = True + if self.params.get("timeout") is None: + self.params["timeout"] = 30 # Ensure protocol is set self.params["protocol"] = "https" if self.params.get("use_ssl", True) else "http" @@ -357,6 +359,10 @@ class MSOModule(object): self.connection = Connection(self.module._socket_path) if self.connection.get_platform() == "cisco.nd": self.platform = "nd" + elif self.connection.get_platform() == "cisco.mso": + self.platform = "mso" + else: + self.fail_json(msg="Connection must be identified as platform 'cisco.nd' or 'cisco.mso'") def get_login_domain_id(self, domain): """Get a domain and return its id""" @@ -608,7 +614,10 @@ class MSOModule(object): if self.module._socket_path: self.connection.set_params(self.params) if api_version is not None: - uri = NDO_API_VERSION_PATH_FORMAT.format(api_version=api_version, path=self.path) + if self.platform == "nd": + uri = NDO_API_VERSION_PATH_FORMAT.format(api_version=api_version, path=self.path) + else: + uri = "/api/{0}/{1}".format(api_version, self.path) else: uri = self.path @@ -628,6 +637,7 @@ class MSOModule(object): error=dict(code=-1, message="Unable to parse error output as JSON. Raw error message: {0}".format(e), exception=to_text(e)) ) pass + self.httpapi_logs.extend(self.connection.pop_messages()) self.fail_json(msg=error_obj["error"]["message"]) else: @@ -776,32 +786,38 @@ class MSOModule(object): self.fail_json(msg="More than one object matches unique filter: {0}".format(kwargs)) return objs[0] - def lookup_schema(self, schema): + def lookup_schema(self, schema, ignore_not_found_error=False): """Look up schema and return its id""" if schema is None: return schema schema_summary = self.query_objs("schemas/list-identity", key="schemas", displayName=schema) - if not schema_summary: + if not schema_summary and not ignore_not_found_error: self.fail_json(msg="Provided schema '{0}' does not exist.".format(schema)) + elif (not schema_summary or not schema_summary[0].get("id")) and ignore_not_found_error: + self.module.warn("Provided schema '{0}' does not exist.".format(schema)) + return None schema_id = schema_summary[0].get("id") if not schema_id: self.fail_json(msg="Schema lookup failed for schema '{0}': '{1}'".format(schema, schema_id)) return schema_id - def lookup_domain(self, domain): + def lookup_domain(self, domain, ignore_not_found_error=False): """Look up a domain and return its id""" if domain is None: return domain d = self.get_obj("auth/domains", key="domains", name=domain) - if not d: - self.fail_json(msg="Domain '%s' is not a valid domain name." % domain) + if not d and not ignore_not_found_error: + self.fail_json(msg="Domain '{0}' is not a valid domain name.".format(domain)) + elif (not d or "id" not in d) and ignore_not_found_error: + self.module.warn("Domain '{0}' is not a valid domain name.".format(domain)) + return None if "id" not in d: - self.fail_json(msg="Domain lookup failed for domain '%s': %s" % (domain, d)) + self.fail_json(msg="Domain lookup failed for domain '{0}': {1}".format(domain, d)) return d.get("id") - def lookup_roles(self, roles): + def lookup_roles(self, roles, ignore_not_found_error=False): """Look up roles and return their ids""" if roles is None: return roles @@ -819,26 +835,32 @@ class MSOModule(object): name = role r = self.get_obj("roles", name=name) - if not r: - self.fail_json(msg="Role '%s' is not a valid role name." % name) + if not r and not ignore_not_found_error: + self.fail_json(msg="Role '{0}' is not a valid role name.".format(name)) + elif (not r or "id" not in r) and ignore_not_found_error: + self.module.warn("Role '{0}' is not a valid role name.".format(name)) + return ids if "id" not in r: - self.fail_json(msg="Role lookup failed for role '%s': %s" % (name, r)) + self.fail_json(msg="Role lookup failed for role '{0}': {1}".format(name, r)) ids.append(dict(roleId=r.get("id"), accessType=access_type)) return ids - def lookup_site(self, site): + def lookup_site(self, site, ignore_not_found_error=False): """Look up a site and return its id""" if site is None: return site s = self.get_obj("sites", name=site) - if not s: - self.fail_json(msg="Site '%s' is not a valid site name." % site) + if not s and not ignore_not_found_error: + self.fail_json(msg="Site '{0}' is not a valid site name.".format(site)) + elif (not s or "id" not in s) and ignore_not_found_error: + self.module.warn("Site '{0}' is not a valid site name.".format(site)) + return None if "id" not in s: - self.fail_json(msg="Site lookup failed for site '%s': %s" % (site, s)) + self.fail_json(msg="Site lookup failed for site '{0}': {1}".format(site, s)) return s.get("id") - def lookup_sites(self, sites): + def lookup_sites(self, sites, ignore_not_found_error=False): """Look up sites and return their ids""" if sites is None: return sites @@ -846,37 +868,46 @@ class MSOModule(object): ids = [] for site in sites: s = self.get_obj("sites", name=site) - if not s: - self.fail_json(msg="Site '%s' is not a valid site name." % site) + if not s and not ignore_not_found_error: + self.fail_json(msg="Site '{0}' is not a valid site name.".format(site)) + elif (not s or "id" not in s) and ignore_not_found_error: + self.module.warn("Site '{0}' is not a valid site name.".format(site)) + return ids if "id" not in s: - self.fail_json(msg="Site lookup failed for site '%s': %s" % (site, s)) + self.fail_json(msg="Site lookup failed for site '{0}': {1}".format(site, s)) ids.append(dict(siteId=s.get("id"), securityDomains=[])) return ids - def lookup_tenant(self, tenant): + def lookup_tenant(self, tenant, ignore_not_found_error=False): """Look up a tenant and return its id""" if tenant is None: return tenant t = self.get_obj("tenants", key="tenants", name=tenant) - if not t: - self.fail_json(msg="Tenant '%s' is not valid tenant name." % tenant) + if not t and not ignore_not_found_error: + self.fail_json(msg="Tenant '{0}' is not valid tenant name.".format(tenant)) + elif (not t or "id" not in t) and ignore_not_found_error: + self.module.warn("Tenant '{0}' is not valid tenant name.".format(tenant)) + return None if "id" not in t: - self.fail_json(msg="Tenant lookup failed for tenant '%s': %s" % (tenant, t)) + self.fail_json(msg="Tenant lookup failed for tenant '{0}': {1}".format(tenant, t)) return t.get("id") - def lookup_remote_location(self, remote_location): + def lookup_remote_location(self, remote_location, ignore_not_found_error=False): """Look up a remote location and return its path and id""" if remote_location is None: return None remote = self.get_obj("platform/remote-locations", key="remoteLocations", name=remote_location) - if "id" not in remote: - self.fail_json(msg="No remote location found for remote '%s'" % (remote_location)) + if "id" not in remote and not ignore_not_found_error: + self.fail_json(msg="No remote location found for remote '{0}'".format(remote_location)) + elif "id" not in remote and ignore_not_found_error: + self.module.warn("No remote location found for remote '{0}'".format(remote_location)) + return dict() remote_info = dict(id=remote.get("id"), path=remote.get("credential")["remotePath"]) return remote_info - def lookup_users(self, users): + def lookup_users(self, users, ignore_not_found_error=False): """Look up users and return their ids""" # Ensure tenant has at least admin user if users is None: @@ -890,16 +921,19 @@ class MSOModule(object): u = self.get_obj("users", loginID=user, api_version="v2") else: u = self.get_obj("users", username=user) - if not u: - self.fail_json(msg="User '%s' is not a valid user name." % user) + if not u and not ignore_not_found_error: + self.fail_json(msg="User '{0}' is not a valid user name.".format(user)) + elif (not u or "id" not in u) and ignore_not_found_error: + self.module.warn("User '{0}' is not a valid user name.".format(user)) + return ids if "id" not in u: if "userID" not in u: - self.fail_json(msg="User lookup failed for user '%s': %s" % (user, u)) + self.fail_json(msg="User lookup failed for user '{0}': {1}".format(user, u)) id = dict(userId=u.get("userID")) else: id = dict(userId=u.get("id")) if id in ids: - self.fail_json(msg="User '%s' is duplicate." % user) + self.fail_json(msg="User '{0}' is duplicate.".format(user)) ids.append(id) return ids @@ -908,7 +942,7 @@ class MSOModule(object): """Create a new label""" return self.request("labels", method="POST", data=dict(displayName=label, type=label_type)) - def lookup_labels(self, labels, label_type): + def lookup_labels(self, labels, label_type, ignore_not_found_error=False): """Look up labels and return their ids (create if necessary)""" if labels is None: return None @@ -918,8 +952,11 @@ class MSOModule(object): label_obj = self.get_obj("labels", displayName=label) if not label_obj: label_obj = self.create_label(label, label_type) - if "id" not in label_obj: - self.fail_json(msg="Label lookup failed for label '%s': %s" % (label, label_obj)) + if "id" not in label_obj and not ignore_not_found_error: + self.fail_json(msg="Label lookup failed for label '{0}': {1}".format(label, label_obj)) + elif "id" not in label_obj and ignore_not_found_error: + self.module.warn("Label lookup failed for label '{0}': {1}".format(label, label_obj)) + return ids ids.append(label_obj.get("id")) return ids @@ -1292,7 +1329,7 @@ class MSOModule(object): self.module.fail_json(msg="Service node types do not exist") return node_objs - def lookup_service_node_device(self, site_id, tenant, device_name=None, service_node_type=None): + def lookup_service_node_device(self, site_id, tenant, device_name=None, service_node_type=None, ignore_not_found_error=False): if service_node_type is None: node_devices = self.query_objs("sites/{0}/aci/tenants/{1}/devices".format(site_id, tenant), key="devices") else: @@ -1301,7 +1338,11 @@ class MSOModule(object): for device in node_devices: if device_name == device.get("name"): return device - self.module.fail_json(msg="Provided device '{0}' of type '{1}' does not exist.".format(device_name, service_node_type)) + if ignore_not_found_error: + self.module.warn("Provided device '{0}' of type '{1}' does not exist.".format(device_name, service_node_type)) + return node_devices + else: + self.module.fail_json(msg="Provided device '{0}' of type '{1}' does not exist.".format(device_name, service_node_type)) return node_devices # Workaround function due to inconsistency in attributes REQUEST/RESPONSE API diff --git a/ansible_collections/cisco/mso/plugins/module_utils/schema.py b/ansible_collections/cisco/mso/plugins/module_utils/schema.py index ca08ab10b..ce1bd36c7 100644 --- a/ansible_collections/cisco/mso/plugins/module_utils/schema.py +++ b/ansible_collections/cisco/mso/plugins/module_utils/schema.py @@ -112,6 +112,21 @@ class MSOSchema: self.mso.fail_json(msg=msg) self.schema_objects["template_anp_epg"] = match + def set_template_anp_epg_useg_attr(self, useg_attr, fail_module=True): + """ + Get template endpoint group item that matches the name of an EPG uSeg Attribute. + :param useg_attr: Name of the EPG uSeg Attribute to match. -> Str + :param fail_module: When match is not found fail the ansible module. -> Bool + :return: Template EPG uSeg Attribute item. -> Item(Int, Dict) | None + """ + self.validate_schema_objects_present(["template_anp_epg"]) + kv_list = [KVPair("name", useg_attr)] + match, existing = self.get_object_from_list(self.schema_objects["template_anp_epg"].details.get("uSegAttrs"), kv_list) + if not match and fail_module: + msg = "Provided uSeg Attribute '{0}' does not match the existing uSeg Attribute(s): {1}".format(useg_attr, ", ".join(existing)) + self.mso.fail_json(msg=msg) + self.schema_objects["template_anp_epg_useg_attribute"] = match + def set_template_external_epg(self, external_epg, fail_module=True): """ Get template external epg item that matches the name of an anp. @@ -207,3 +222,18 @@ class MSOSchema: msg = "Provided EPG '{0}' not matching existing site anp epg(s): {1}".format(epg_name, ", ".join(existing)) self.mso.fail_json(msg=msg) self.schema_objects["site_anp_epg"] = match + + def set_site_anp_epg_useg_attr(self, useg_attr, fail_module=True): + """ + Get site endpoint group item that matches the name of an EPG uSeg Attribute. + :param useg_attr: Name of the EPG uSeg Attribute to match. -> Str + :param fail_module: When match is not found fail the ansible module. -> Bool + :return: Site EPG uSeg Attribute item. -> Item(Int, Dict) | None + """ + self.validate_schema_objects_present(["site_anp_epg"]) + kv_list = [KVPair("name", useg_attr)] + match, existing = self.get_object_from_list(self.schema_objects["site_anp_epg"].details.get("uSegAttrs"), kv_list) + if not match and fail_module: + msg = "Provided Site uSeg Attribute '{0}' does not match the existing Site uSeg Attribute(s): {1}".format(useg_attr, ", ".join(existing)) + self.mso.fail_json(msg=msg) + self.schema_objects["site_anp_epg_useg_attribute"] = match diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_useg_attribute.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_useg_attribute.py new file mode 100644 index 000000000..3030852f5 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_useg_attribute.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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_schema_site_anp_epg_useg_attribute +short_description: Manage EPG Site uSeg Attributes in schema sites +description: +- Manage Site uSeg Attributes in the schema site EPGs on Cisco ACI Multi-Site. +author: +- Sabari Jaganathan (@sajagana) +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 + anp: + description: + - The name of the Application Profile. + type: str + required: true + epg: + description: + - The name of the EPG. + type: str + required: true + name: + description: + - The name and display name of the uSeg Attribute. + type: str + aliases: [ useg ] + description: + description: + - The description of the uSeg Attribute. + type: str + aliases: [ descr ] + type: + description: + - The type of the uSeg Attribute. + type: str + choices: [ vm_name, ip, mac, vmm_domain, vm_operating_system, vm_tag, vm_hypervisor_identifier, dns, vm_datacenter, vm_identifier, vnic_dn ] + aliases: [ attribute_type ] + value: + description: + - The value of the uSeg Attribute. + type: str + aliases: [ attribute_value ] + operator: + description: + - The operator type of the uSeg Attribute. + type: str + choices: [ equals, contains, starts_with, ends_with ] + useg_subnet: + description: + - The uSeg Subnet can only be used when the I(attribute_type) is IP. + - Use C(false) to set the custom uSeg Subnet IP address to the uSeg Attribute. + - Use C(true) to set the uSeg Subnet IP address to 0.0.0.0. + 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 +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 an uSeg attr with attribute_type - ip + cisco.mso.mso_schema_site_anp_epg_useg_attribute: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + site: ansible_test + name: useg_attr_ip + attribute_type: ip + useg_subnet: false + value: 10.0.0.0/24 + state: present + delegate_to: localhost + +- name: Query a specific EPG uSeg attr with name + cisco.mso.mso_schema_site_anp_epg_useg_attribute: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + name: useg_attr_ip + site: ansible_test + state: query + delegate_to: localhost + +- name: Query all EPG uSeg attrs + cisco.mso.mso_schema_site_anp_epg_useg_attribute: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + site: ansible_test + state: query + delegate_to: localhost + +- name: Remove a uSeg attr from an EPG with name + cisco.mso.mso_schema_site_anp_epg_useg_attribute: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + site: ansible_test + name: useg_attr_ip + 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 ansible_collections.cisco.mso.plugins.module_utils.constants import EPG_U_SEG_ATTR_TYPE_MAP, EPG_U_SEG_ATTR_OPERATOR_LIST +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), + name=dict(type="str", aliases=["useg"]), + description=dict(type="str", aliases=["descr"]), + type=dict(type="str", aliases=["attribute_type"], choices=list(EPG_U_SEG_ATTR_TYPE_MAP.keys())), + value=dict(type="str", aliases=["attribute_value"]), + operator=dict(type="str", choices=EPG_U_SEG_ATTR_OPERATOR_LIST), + useg_subnet=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", ["name"]], + ["state", "present", ["name", "type"]], + ["useg_subnet", False, ["value"]], + ], + ) + + 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") + name = module.params.get("name") + description = module.params.get("description") + attribute_type = module.params.get("type") + value = module.params.get("value") + operator = module.params.get("operator") + useg_subnet = module.params.get("useg_subnet") + state = module.params.get("state") + mso = MSOModule(module) + + if state == "present": + if attribute_type in ["mac", "dns"] and value is None: + mso.fail_json(msg="Failed due to invalid 'value' and the attribute_type is: {0}.".format(attribute_type)) + elif attribute_type not in ["mac", "dns", "ip"] and (value is None or operator is None): + mso.fail_json(msg="Failed due to invalid 'value' or 'operator' and the attribute_type is: {0}.".format(attribute_type)) + + mso_schema = MSOSchema(mso, schema, template) + mso_schema.set_template(template) + mso_schema.set_template_anp(anp) + mso_schema.set_template_anp_epg(epg) + + mso_schema.set_site(template, site) + mso_schema.set_site_anp(anp, False) + mso_schema.set_site_anp_epg(epg, False) + + # Only for NDO less than or equal to 3.7 + if mso_schema.schema_objects["site_anp_epg"] is None: + mso_schema.schema_objects["site_anp_epg_useg_attribute"] = None + + if mso_schema.schema_objects["template_anp_epg"].details.get("uSegEpg"): + mso_schema.set_site_anp_epg_useg_attr(name, fail_module=False) + + if mso_schema.schema_objects["site_anp_epg_useg_attribute"] is not None: + site_useg_attr_path = "/sites/{0}-{1}/anps/{2}/epgs/{3}/uSegAttrs/{4}".format( + mso_schema.schema_objects["site"].details.get("siteId"), template, anp, epg, mso_schema.schema_objects["site_anp_epg_useg_attribute"].index + ) + mso.existing = mso_schema.schema_objects["site_anp_epg_useg_attribute"].details + else: + mso.fail_json(msg="{0}: is not a valid uSeg EPG.".format(epg)) + + if state == "query": + if name is None and mso_schema.schema_objects["site_anp_epg"] is not None: + mso.existing = mso_schema.schema_objects["site_anp_epg"].details.get("uSegAttrs") + elif not mso.existing: + mso.fail_json(msg="The uSeg Attribute: {0} not found.".format(name)) + mso.exit_json() + + site_useg_attrs_path = "/sites/{0}-{1}/anps/{2}/epgs/{3}/uSegAttrs".format(mso_schema.schema_objects["site"].details.get("siteId"), template, anp, epg) + ops = [] + mso.previous = mso.existing + + if state == "absent": + if mso.existing: + mso.existing = {} + ops.append(dict(op="remove", path=site_useg_attr_path)) + + if state == "present": + if not mso.existing and description is None: + description = name + + payload = dict(name=name, displayName=name, description=description, type=EPG_U_SEG_ATTR_TYPE_MAP[attribute_type], value=value) + + if attribute_type == "ip": + if useg_subnet is False: + payload["fvSubnet"] = True + else: + payload["fvSubnet"] = False + payload["value"] = "0.0.0.0" + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=site_useg_attr_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=site_useg_attrs_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_template_anp_epg_useg_attribute.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_useg_attribute.py new file mode 100644 index 000000000..1f61e95de --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_useg_attribute.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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_schema_template_anp_epg_useg_attribute +short_description: Manage EPG uSeg Attributes in schema templates +description: +- Manage uSeg Attributes in the schema template EPGs on Cisco ACI Multi-Site. +author: +- Sabari Jaganathan (@sajagana) +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 Application Profile. + type: str + required: true + epg: + description: + - The name of the EPG. + type: str + required: true + name: + description: + - The name and display name of the uSeg Attribute. + type: str + aliases: [ useg ] + description: + description: + - The description of the uSeg Attribute. + type: str + aliases: [ descr ] + type: + description: + - The type of the uSeg Attribute. + type: str + choices: [ vm_name, ip, mac, vmm_domain, vm_operating_system, vm_tag, vm_hypervisor_identifier, dns, vm_datacenter, vm_identifier, vnic_dn ] + aliases: [ attribute_type ] + value: + description: + - The value of the uSeg Attribute. + type: str + aliases: [ attribute_value ] + operator: + description: + - The operator type of the uSeg Attribute. + type: str + choices: [ equals, contains, starts_with, ends_with ] + useg_subnet: + description: + - The uSeg Subnet can only be used when the I(attribute_type) is IP. + - Use C(false) to set the custom uSeg Subnet IP address to the uSeg Attribute. + - Use C(true) to set the uSeg Subnet IP address to 0.0.0.0. + 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 +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 an uSeg attr with attribute_type - ip + cisco.mso.mso_schema_template_anp_epg_useg_attribute: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + name: useg_attr_ip + attribute_type: ip + useg_subnet: false + value: 10.0.0.0/24 + state: present + delegate_to: localhost + +- name: Query a specific EPG uSeg attr with name + cisco.mso.mso_schema_template_anp_epg_useg_attribute: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + name: useg_attr_ip + state: query + delegate_to: localhost + register: query_result + +- name: Query all EPG uSeg attrs + cisco.mso.mso_schema_template_anp_epg_useg_attribute: + 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 + +- name: Remove a uSeg attr from an EPG with name + cisco.mso.mso_schema_template_anp_epg_useg_attribute: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + name: useg_attr_ip + 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 ansible_collections.cisco.mso.plugins.module_utils.constants import EPG_U_SEG_ATTR_TYPE_MAP, EPG_U_SEG_ATTR_OPERATOR_LIST +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), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + name=dict(type="str", aliases=["useg"]), + description=dict(type="str", aliases=["descr"]), + type=dict(type="str", aliases=["attribute_type"], choices=list(EPG_U_SEG_ATTR_TYPE_MAP.keys())), + value=dict(type="str", aliases=["attribute_value"]), + operator=dict(type="str", choices=EPG_U_SEG_ATTR_OPERATOR_LIST), + useg_subnet=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", ["name"]], + ["state", "present", ["name", "type"]], + ["useg_subnet", False, ["value"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + anp = module.params.get("anp") + epg = module.params.get("epg") + name = module.params.get("name") + description = module.params.get("description") + attribute_type = module.params.get("type") + value = module.params.get("value") + operator = module.params.get("operator") + useg_subnet = module.params.get("useg_subnet") + state = module.params.get("state") + mso = MSOModule(module) + + if state == "present": + if attribute_type in ["mac", "dns"] and value is None: + mso.fail_json(msg="Failed due to invalid 'value' and the attribute_type is: {0}.".format(attribute_type)) + elif attribute_type not in ["mac", "dns", "ip"] and (value is None or operator is None): + mso.fail_json(msg="Failed due to invalid 'value' or 'operator' and the attribute_type is: {0}.".format(attribute_type)) + + mso_schema = MSOSchema(mso, schema, template) + mso_schema.set_template(template) + mso_schema.set_template_anp(anp) + mso_schema.set_template_anp_epg(epg) + + if mso_schema.schema_objects["template_anp_epg"].details.get("uSegEpg"): + mso_schema.set_template_anp_epg_useg_attr(name, fail_module=False) + if mso_schema.schema_objects["template_anp_epg_useg_attribute"] is not None: + useg_attr_path = "/templates/{0}/anps/{1}/epgs/{2}/uSegAttrs/{3}".format( + template, anp, epg, mso_schema.schema_objects["template_anp_epg_useg_attribute"].index + ) + mso.existing = mso_schema.schema_objects["template_anp_epg_useg_attribute"].details + else: + mso.fail_json(msg="{0}: is not a valid uSeg EPG.".format(epg)) + + if state == "query": + if name is None: + mso.existing = mso_schema.schema_objects["template_anp_epg"].details.get("uSegAttrs") + elif not mso.existing: + mso.fail_json(msg="The uSeg Attribute: {0} not found.".format(name)) + mso.exit_json() + + useg_attrs_path = "/templates/{0}/anps/{1}/epgs/{2}/uSegAttrs".format(template, anp, epg) + ops = [] + + mso.previous = mso.existing + if state == "absent": + if mso.existing: + mso.existing = {} + ops.append(dict(op="remove", path=useg_attr_path)) + + if state == "present": + if not mso.existing and description is None: + description = name + + payload = dict(name=name, displayName=name, description=description, type=EPG_U_SEG_ATTR_TYPE_MAP[attribute_type], value=value) + + if attribute_type == "ip": + if useg_subnet is False: + payload["fvSubnet"] = True + else: + payload["fvSubnet"] = False + payload["value"] = "0.0.0.0" + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=useg_attr_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=useg_attrs_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_tenant_site.py b/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py index 735f85b13..4b9c2af56 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py @@ -260,7 +260,13 @@ def main(): # Get tenant_id and site_id tenant_id = mso.lookup_tenant(module.params.get("tenant")) - site_id = mso.lookup_site(module.params.get("site")) + + # To ignore the object not found issue for the lookup methods + site_id = mso.lookup_site(module.params.get("site"), True) + + if state == "absent" and not site_id: + mso.exit_json() + tenants = [(t.get("id")) for t in mso.query_objs("tenants")] tenant_idx = tenants.index((tenant_id)) |