summaryrefslogtreecommitdiffstats
path: root/ansible_collections/cisco/mso/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:27 +0000
commit3b0807ad7b283c46c21862eb826dcbb4ad04e5e2 (patch)
tree6461ea75f03eca87a5a90c86c3c9a787a6ad037e /ansible_collections/cisco/mso/plugins
parentAdding debian version 7.7.0+dfsg-3. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/cisco/mso/plugins/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--ansible_collections/cisco/mso/plugins/doc_fragments/modules.py3
-rw-r--r--ansible_collections/cisco/mso/plugins/httpapi/mso.py117
-rw-r--r--ansible_collections/cisco/mso/plugins/module_utils/constants.py16
-rw-r--r--ansible_collections/cisco/mso/plugins/module_utils/mso.py117
-rw-r--r--ansible_collections/cisco/mso/plugins/module_utils/schema.py30
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp_epg_useg_attribute.py276
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp_epg_useg_attribute.py258
-rw-r--r--ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py8
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
index 1ffed58d7..7c7d7d98b 100644
--- a/ansible_collections/cisco/mso/plugins/.DS_Store
+++ b/ansible_collections/cisco/mso/plugins/.DS_Store
Binary files differ
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))