diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-26 04:05:57 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-26 04:05:57 +0000 |
commit | 0dcbb2c58231264c2f0a0374733b5e9cf8747e1f (patch) | |
tree | 7f133117f9ebecefdc96e42e01ee7557247d5d8a /ansible_collections/cisco/mso/plugins | |
parent | Adding debian version 9.4.0+dfsg-1. (diff) | |
download | ansible-0dcbb2c58231264c2f0a0374733b5e9cf8747e1f.tar.xz ansible-0dcbb2c58231264c2f0a0374733b5e9cf8747e1f.zip |
Merging upstream version 9.5.1+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/cisco/mso/plugins')
73 files changed, 2573 insertions, 650 deletions
diff --git a/ansible_collections/cisco/mso/plugins/module_utils/constants.py b/ansible_collections/cisco/mso/plugins/module_utils/constants.py index 2f3e0d472..ff08019df 100644 --- a/ansible_collections/cisco/mso/plugins/module_utils/constants.py +++ b/ansible_collections/cisco/mso/plugins/module_utils/constants.py @@ -16,7 +16,7 @@ SERVICE_NODE_CONNECTOR_MAP = { # 'external_epg': {'id': 'externalEpg', 'connector_type': 'route-peering'} } -YES_OR_NO_TO_BOOL_STRING_MAP = {"yes": "true", "no": "false"} +YES_OR_NO_TO_BOOL_STRING_MAP = {"yes": "true", "no": "false", True: "yes", False: "no"} NDO_4_UNIQUE_IDENTIFIERS = ["templateID", "autoRouteTargetImport", "autoRouteTargetExport"] @@ -38,3 +38,40 @@ EPG_U_SEG_ATTR_TYPE_MAP = { } EPG_U_SEG_ATTR_OPERATOR_LIST = ["equals", "contains", "starts_with", "ends_with"] + +AZURE_L4L7_CONNECTOR_TYPE_MAP = { + "none": "none", + "redirect": "redir", + "source_nat": "snat", + "destination_nat": "dnat", + "source_and_destination_nat": "snat_dnat", +} + +LISTENER_PROTOCOLS = ["http", "https", "tcp", "udp", "tls", "inherit"] + +LISTENER_SECURITY_POLICY_MAP = { + "default": "default", + "elb_sec_2016_18": "eLBSecurityPolicy-2016-08", + "elb_sec_fs_2018_06": "eLBSecurityPolicy-FS-2018-06", + "elb_sec_tls_1_2_2017_01": "eLBSecurityPolicy-TLS-1-2-2017-01", + "elb_sec_tls_1_2_ext_2018_06": "eLBSecurityPolicy-TLS-1-2-Ext-2018-06", + "elb_sec_tls_1_1_2017_01": "eLBSecurityPolicy-TLS-1-1-2017-01", + "elb_sec_2015_05": "eLBSecurityPolicy-2015-05", + "elb_sec_tls_1_0_2015_04": "eLBSecurityPolicy-TLS-1-0-2015-04", + "app_gw_ssl_default": "AppGwSslPolicyDefault", + "app_gw_ssl_2015_501": "AppGwSslPolicy20150501", + "app_gw_ssl_2017_401": "AppGwSslPolicy20170401", + "app_gw_ssl_2017_401s": "AppGwSslPolicy20170401S", +} + +LISTENER_ACTION_TYPE_MAP = {"fixed_response": "fixedResponse", "forward": "forward", "redirect": "redirect", "ha_port": "haPort"} + +LISTENER_CONTENT_TYPE_MAP = {"text_plain": "textPlain", "text_css": "textCSS", "text_html": "textHtml", "app_js": "appJS", "app_json": "appJson"} + +LISTENER_REDIRECT_CODE_MAP = { + "unknown": "unknown", + "permanently_moved": "permMoved", + "found": "found", + "see_other": "seeOther", + "temporary_redirect": "temporary", +} diff --git a/ansible_collections/cisco/mso/plugins/module_utils/mso.py b/ansible_collections/cisco/mso/plugins/module_utils/mso.py index 4bc9053ef..0f346d79b 100644 --- a/ansible_collections/cisco/mso/plugins/module_utils/mso.py +++ b/ansible_collections/cisco/mso/plugins/module_utils/mso.py @@ -22,7 +22,16 @@ from ansible.module_utils.six.moves.urllib.parse import urlencode, urljoin from ansible.module_utils.urls import fetch_url from ansible.module_utils._text import to_native, to_text from ansible.module_utils.connection import Connection -from ansible_collections.cisco.mso.plugins.module_utils.constants import NDO_API_VERSION_PATH_FORMAT +from ansible_collections.cisco.mso.plugins.module_utils.constants import ( + NDO_API_VERSION_PATH_FORMAT, + AZURE_L4L7_CONNECTOR_TYPE_MAP, + LISTENER_REDIRECT_CODE_MAP, + LISTENER_CONTENT_TYPE_MAP, + LISTENER_ACTION_TYPE_MAP, + LISTENER_PROTOCOLS, +) +from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule + try: from requests_toolbelt.multipart.encoder import MultipartEncoder @@ -50,7 +59,7 @@ def issubset(subset, superset): return True # Both objects have a different type - if type(subset) != type(superset): + if type(subset) is not type(superset): return False for key, value in subset.items(): @@ -205,7 +214,11 @@ def mso_service_graph_node_spec(): def mso_service_graph_node_device_spec(): return dict( - name=dict(type="str", required=True), + device_name=dict(type="str", aliases=["name"], required=True), + provider_connector_type=dict(type="str", choices=list(AZURE_L4L7_CONNECTOR_TYPE_MAP.keys())), + provider_interface=dict(type="str"), + consumer_connector_type=dict(type="str", choices=["none", "redirect"]), + consumer_interface=dict(type="str"), ) @@ -239,6 +252,13 @@ def mso_site_anp_epg_bulk_staticport_spec(): ) +def ndo_remote_user_spec(): + return dict( + name=dict(type="str", required=True), + login_domain=dict(type="str", required=True), + ) + + # Copied from ansible's module uri.py (url): https://github.com/ansible/ansible/blob/cdf62edc65f564fff6b7e575e084026fa7faa409/lib/ansible/modules/uri.py def write_file(module, url, dest, content, resp, tmpsrc=None): # create a tempfile with some test content @@ -321,6 +341,8 @@ class MSOModule(object): self.status = None self.url = None self.httpapi_logs = list() + self.site_type = None # on-premise or cloud + self.cloud_provider_type = None # aws or azure or gcp if self.module._debug: self.module.warn("Enable debug output because ANSIBLE_DEBUG was set.") @@ -845,6 +867,13 @@ class MSOModule(object): ids.append(dict(roleId=r.get("id"), accessType=access_type)) return ids + def lookup_site_type(self, site_data): + """Get site type(AWS, AZURE or physical)""" + site_type = site_data.get("platform") + if site_type == "cloud": + self.cloud_provider_type = site_data.get("cloudProviders")[0] + self.site_type = site_type + def lookup_site(self, site, ignore_not_found_error=False): """Look up a site and return its id""" if site is None: @@ -858,6 +887,8 @@ class MSOModule(object): return None if "id" not in s: self.fail_json(msg="Site lookup failed for site '{0}': {1}".format(site, s)) + + self.lookup_site_type(s) return s.get("id") def lookup_sites(self, sites, ignore_not_found_error=False): @@ -916,26 +947,65 @@ class MSOModule(object): users.append("admin") ids = [] + if self.platform == "nd": + nd = NDModule(self.module) + remote_users = nd.request("/nexus/infra/api/aaa/v4/remoteusers", method="GET") + local_users = nd.request("/nexus/infra/api/aaa/v4/localusers", method="GET") + for user in users: + user_dict = dict() if self.platform == "nd": - u = self.get_obj("users", loginID=user, api_version="v2") + user_dict = self.get_user_from_list_of_users(user, local_users) + if user_dict is None: + user_dict = self.get_user_from_list_of_users(user, remote_users) else: - u = self.get_obj("users", username=user) - if not u and not ignore_not_found_error: + user_dict = self.get_obj("users", username=user) + if not user_dict 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: + elif (not user_dict or "id" not in user_dict) 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 '{0}': {1}".format(user, u)) - id = dict(userId=u.get("userID")) + if "id" not in user_dict: + if "userID" not in user_dict: + self.fail_json(msg="User lookup failed for user '{0}': {1}".format(user, user_dict)) + id = dict(userId=user_dict.get("userID")) else: - id = dict(userId=u.get("id")) + id = dict(userId=user_dict.get("id")) if id in ids: self.fail_json(msg="User '{0}' is duplicate.".format(user)) ids.append(id) + return ids + def get_user_from_list_of_users(self, user_name, list_of_users, login_domain=""): + """Get user from list of users""" + for user in list_of_users.get("items"): + if user.get("spec").get("loginID") == user_name and (login_domain == "" or user.get("spec").get("loginDomain") == login_domain): + return user.get("spec") + return None + + def lookup_remote_users(self, remote_users, ignore_not_found_error=False): + ids = [] + if self.platform == "nd": + nd = NDModule(self.module) + remote_users_data = nd.request("/nexus/infra/api/aaa/v4/remoteusers", method="GET") + for remote_user in remote_users: + user_dict = dict() + if self.platform == "nd": + user_dict = self.get_user_from_list_of_users(remote_user.get("name"), remote_users_data, remote_user.get("login_domain")) + if not user_dict and not ignore_not_found_error: + self.fail_json(msg="User '{0}' is not a valid user name.".format(remote_user.get("name"))) + elif (not user_dict or "id" not in user_dict) and ignore_not_found_error: + self.module.warn("User '{0}' is not a valid user name.".format(remote_user.get("name"))) + return ids + if "id" not in user_dict: + if "userID" not in user_dict: + self.fail_json(msg="User lookup failed for user '{0}': {1}".format(remote_user.get("name"), user_dict)) + id = dict(userId=user_dict.get("userID")) + else: + id = dict(userId=user_dict.get("id")) + if id in ids: + self.fail_json(msg="User '{0}' is duplicate.".format(remote_user.get("name"))) + ids.append(id) return ids def create_label(self, label, label_type): @@ -1112,7 +1182,7 @@ class MSOModule(object): """Create a DHCP policy from input""" if data is None: return None - if type(data) == list: + if isinstance(data, list): dhcps = [] for dhcp in data: if "dhcp_option_policy" in dhcp: @@ -1330,6 +1400,9 @@ class MSOModule(object): return node_objs def lookup_service_node_device(self, site_id, tenant, device_name=None, service_node_type=None, ignore_not_found_error=False): + if self.site_type == "cloud": + tenant = "{0}/{1}".format(tenant, self.site_type) + if service_node_type is None: node_devices = self.query_objs("sites/{0}/aci/tenants/{1}/devices".format(site_id, tenant), key="devices") else: @@ -1393,3 +1466,121 @@ class MSOModule(object): def validate_schema(self, schema_id): return self.request("schemas/{id}/validate".format(id=schema_id), method="GET") + + def input_validation(self, attr_name, attr_value, required_attributes, target_object, object_position=None, object_name=None): + if attr_name in (None, "") or attr_value in (None, ""): + self.module.fail_json(msg="The attribute and value must be set") + + empty_attributes = [attribute for attribute in required_attributes if target_object.get(attribute) in (None, "", [], {}, 0)] + + if object_position is not None and object_name is not None and empty_attributes: + self.module.fail_json( + msg="When the '{0}' is '{1}', the {2} attributes must be set at the object position: {3} and the object name: {4}".format( + attr_name, attr_value, empty_attributes, object_position, object_name + ) + ) + elif object_position is not None and object_name is None and empty_attributes: + self.module.fail_json( + msg="When the '{0}' is '{1}', the {2} attributes must be set at the object position: {3}".format( + attr_name, attr_value, empty_attributes, object_position + ) + ) + elif object_position is None and object_name is not None and empty_attributes: + self.module.fail_json( + msg="When the '{0}' is '{1}', the {2} attributes must be set and the object name: {3}".format( + attr_name, attr_value, empty_attributes, object_name + ) + ) + elif empty_attributes: + self.module.fail_json(msg="When the '{0}' is '{1}', the {2} attributes must be set".format(attr_name, attr_value, empty_attributes)) + + +def service_node_ref_str_to_dict(serviceNodeRefStr): + serviceNodeRefTokens = serviceNodeRefStr.split("/") + return dict( + schemaId=serviceNodeRefTokens[2], + serviceGraphName=serviceNodeRefTokens[6], + serviceNodeName=serviceNodeRefTokens[8], + templateName=serviceNodeRefTokens[4], + ) + + +def mso_schema_site_contract_service_graph_spec(): + return dict( + cluster_interface_device=dict(type="str", required=True, aliases=["cluster_device", "device", "device_name"]), + provider_connector_cluster_interface=dict( + type="str", required=True, aliases=["provider_cluster_interface", "provider_interface", "provider_interface_name"] + ), + provider_connector_redirect_policy_tenant=dict(type="str", aliases=["provider_redirect_policy_tenant", "provider_tenant"]), + provider_connector_redirect_policy=dict(type="str", aliases=["provider_redirect_policy", "provider_policy"]), + consumer_connector_cluster_interface=dict( + type="str", required=True, aliases=["consumer_cluster_interface", "consumer_interface", "consumer_interface_name"] + ), + consumer_connector_redirect_policy_tenant=dict(type="str", aliases=["consumer_redirect_policy_tenant", "consumer_tenant"]), + consumer_connector_redirect_policy=dict(type="str", aliases=["consumer_redirect_policy", "consumer_policy"]), + consumer_subnet_ips=dict(type="list", elements="str"), + ) + + +def listener_ssl_certificates_spec(): + return dict( + name=dict(type="str", required=True), + certificate_store=dict(type="str", choices=["default", "iam", "acm"], required=True), + ) + + +def listener_rules_provider_epg_ref_spec(): + return dict( + schema=dict(type="str"), + template=dict(type="str"), + anp_name=dict(type="str", required=True, aliases=["anp"]), + epg_name=dict(type="str", required=True, aliases=["epg"]), + ) + + +def listener_rules_health_check_spec(): + return dict( + port=dict(type="int"), + protocol=dict(type="str", choices=LISTENER_PROTOCOLS), + path=dict(type="str"), + interval=dict(type="int"), + timeout=dict(type="int"), + unhealthy_threshold=dict(type="int"), + use_host_from_rule=dict(type="bool"), + success_code=dict(type="str"), + host=dict(type="str"), + ) + + +def listener_rules_spec(): + return dict( + name=dict(type="str", required=True), + floating_ip=dict(type="str"), + priority=dict(type="int", required=True), + host=dict(type="str"), + path=dict(type="str"), + action=dict(type="str"), + action_type=dict(type="str", required=True, choices=list(LISTENER_ACTION_TYPE_MAP)), + content_type=dict(type="str", choices=list(LISTENER_CONTENT_TYPE_MAP)), + port=dict(type="int"), + protocol=dict(type="str", choices=LISTENER_PROTOCOLS), + provider_epg=dict( + type="dict", + options=listener_rules_provider_epg_ref_spec(), + ), + url_type=dict(type="str", choices=["original", "custom"]), + custom_url=dict(type="str"), + redirect_host_name=dict(type="str"), + redirect_path=dict(type="str"), + redirect_query=dict(type="str"), + response_code=dict(type="str"), + response_body=dict(type="str"), + redirect_protocol=dict(type="str", choices=LISTENER_PROTOCOLS), + redirect_port=dict(type="int"), + redirect_code=dict(type="str", choices=list(LISTENER_REDIRECT_CODE_MAP)), + health_check=dict( + type="dict", + options=listener_rules_health_check_spec(), + ), + target_ip_type=dict(type="str", choices=["unspecified", "primary", "secondary"]), + ) diff --git a/ansible_collections/cisco/mso/plugins/module_utils/schema.py b/ansible_collections/cisco/mso/plugins/module_utils/schema.py index ce1bd36c7..75da0d1de 100644 --- a/ansible_collections/cisco/mso/plugins/module_utils/schema.py +++ b/ansible_collections/cisco/mso/plugins/module_utils/schema.py @@ -112,6 +112,24 @@ class MSOSchema: self.mso.fail_json(msg=msg) self.schema_objects["template_anp_epg"] = match + def set_template_anp_epg_contract(self, contract_ref, relation_type, fail_module=True): + """ + Get template endpoint group contract item that matches the reference and type of an contract. + :param contract_ref: Reference of the contract to match. -> Str + :param relation_type: Relation_type of the contract to match. -> Str + :param fail_module: When match is not found fail the ansible module. -> Bool + :return: Template epg item. -> Item(Int, Dict) | None + """ + self.validate_schema_objects_present(["template_anp_epg"]) + kv_list = [KVPair("contractRef", contract_ref), KVPair("relationshipType", relation_type)] + match, existing = self.get_object_from_list(self.schema_objects["template_anp_epg"].details.get("contractRelationships"), kv_list) + if not match and fail_module: + msg = "Provided Contract Reference '{0}' with type '{1}' not matching existing contacts(s): {2}".format( + contract_ref, relation_type, ", ".join(existing) + ) + self.mso.fail_json(msg=msg) + self.schema_objects["template_anp_epg_contract"] = 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. @@ -237,3 +255,60 @@ class MSOSchema: 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 + + def set_site_contract(self, contract_name, fail_module=True): + """ + Get site contract item that matches the name of a contract. + :param contract_name: Name of the contract to match. -> Str + :param fail_module: When match is not found fail the ansible module. -> Bool + :return: Site contract item. -> Item(Int, Dict) | None + """ + self.validate_schema_objects_present(["template", "site"]) + kv_list = [ + KVPair( + "contractRef", self.mso.contract_ref(schema_id=self.id, template=self.schema_objects["template"].details.get("name"), contract=contract_name) + ) + ] + match, existing = self.get_object_from_list(self.schema_objects["site"].details.get("contracts"), kv_list) + if not match and fail_module: + msg = "Provided Contract '{0}' not matching existing site contract(s): {1}".format(contract_name, ", ".join(existing)) + self.mso.fail_json(msg=msg) + self.schema_objects["site_contract"] = match + + def set_site_service_graph(self, site_service_graph, fail_module=True): + """ + Get site item that matches the name of a service graph. + :param service_graph: Name of the service graph to match. -> Str + :param fail_module: When match is not found fail the ansible module. -> Bool + :return: Site service graph item. -> Item(Int, Dict) | None + """ + self.validate_schema_objects_present(["template", "site"]) + + kv_list = [ + KVPair( + "serviceGraphRef", + self.mso.service_graph_ref(schema_id=self.id, template=self.schema_objects["template"].details.get("name"), service_graph=site_service_graph), + ) + ] + + site_service_graph = self.schema_objects["site"].details.get("serviceGraphs") + match, existing = self.get_object_from_list(site_service_graph, kv_list) + if not match and fail_module: + msg = "Provided Site Service Graph '{0}' not matching existing site service graph(s): {1}".format(site_service_graph, ", ".join(existing)) + self.mso.fail_json(msg=msg) + self.schema_objects["site_service_graph"] = match + + def set_site_anp_epg_static_port(self, path, fail_module=True): + """ + Get site anp epg static port path item that matches the path of Static Port. + :param path: Path of the Static Port to match. -> Str + :param fail_module: When match is not found fail the ansible module. -> Bool + :return: Site anp epg item. -> Item(Int, Dict) | None + """ + self.validate_schema_objects_present(["site_anp_epg"]) + kv_list = [KVPair("path", path)] + match, existing = self.get_object_from_list(self.schema_objects["site_anp_epg"].details.get("staticPorts"), kv_list) + if not match and fail_module: + msg = "Provided Static Port Path '{0}' not matching existing static port path(s): {1}".format(path, ", ".join(existing)) + self.mso.fail_json(msg=msg) + self.schema_objects["site_anp_epg_static_port"] = match diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_backup.py b/ansible_collections/cisco/mso/plugins/modules/mso_backup.py index fc2564b82..d7ceab4fb 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_backup.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_backup.py @@ -83,7 +83,6 @@ EXAMPLES = r""" description: via Ansible location_type: local state: present - delegate_to: localhost - name: Create a new remote backup cisco.mso.mso_backup: @@ -95,7 +94,6 @@ EXAMPLES = r""" location_type: remote remote_location: ansible_test state: present - delegate_to: localhost - name: Move backup to remote location cisco.mso.mso_backup: @@ -106,7 +104,6 @@ EXAMPLES = r""" remote_location: ansible_test remote_path: test state: move - delegate_to: localhost - name: Download a backup cisco.mso.mso_backup: @@ -116,7 +113,6 @@ EXAMPLES = r""" backup: Backup destination: ./ state: download - delegate_to: localhost - name: Upload a backup cisco.mso.mso_backup: @@ -125,7 +121,6 @@ EXAMPLES = r""" password: SomeSecretPassword backup: ./Backup state: upload - delegate_to: localhost - name: Restore a backup cisco.mso.mso_backup: @@ -134,7 +129,6 @@ EXAMPLES = r""" password: SomeSecretPassword backup: Backup state: restore - delegate_to: localhost - name: Remove a Backup cisco.mso.mso_backup: @@ -143,7 +137,6 @@ EXAMPLES = r""" password: SomeSecretPassword backup: Backup state: absent - delegate_to: localhost - name: Query a backup cisco.mso.mso_backup: @@ -152,7 +145,6 @@ EXAMPLES = r""" password: SomeSecretPassword backup: Backup state: query - delegate_to: localhost register: query_result - name: Query a backup with its complete name @@ -162,7 +154,6 @@ EXAMPLES = r""" password: SomeSecretPassword backup: Backup_20200721220043 state: query - delegate_to: localhost register: query_result - name: Query all backups @@ -171,7 +162,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py b/ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py index e97b59b2e..a96c1da97 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_backup_schedule.py @@ -65,7 +65,7 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost + register: query_result - name: Set backup schedule cisco.mso.mso_backup_schedule: @@ -76,7 +76,6 @@ EXAMPLES = r""" 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: @@ -90,7 +89,6 @@ EXAMPLES = r""" start_time: 20:57:36 start_date: 2023-04-09 state: present - delegate_to: localhost - name: Delete backup schedule cisco.mso.mso_backup_schedule: @@ -98,7 +96,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: absent - delegate_to: localhost """ RETURN = r""" 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 index e9b7f23d3..7568c4704 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_option_policy.py @@ -56,7 +56,6 @@ EXAMPLES = r""" description: "My Test DHCP Policy" tenant: ansible_test state: present - delegate_to: localhost - name: Remove DHCP Option Policy cisco.mso.mso_dhcp_option_policy: @@ -65,7 +64,6 @@ EXAMPLES = r""" 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: @@ -74,7 +72,7 @@ EXAMPLES = r""" password: SomeSecretPassword dhcp_option_policy: my_test_dhcp_policy state: query - delegate_to: localhost + register: query_result - name: Query all DHCP Option Policies cisco.mso.mso_dhcp_option_policy: @@ -82,7 +80,7 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost + register: query_result """ RETURN = r""" 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 index f4c397c24..4d5edd14f 100644 --- 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 @@ -63,7 +63,6 @@ EXAMPLES = r""" 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: @@ -73,7 +72,6 @@ EXAMPLES = r""" 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: @@ -83,7 +81,7 @@ EXAMPLES = r""" dhcp_option_policy: my_test_dhcp_policy name: ansible_test state: query - delegate_to: localhost + register: query_result - name: Query all option of a DHCP Option Policy cisco.mso.mso_dhcp_option_policy_option: @@ -92,7 +90,7 @@ EXAMPLES = r""" password: SomeSecretPassword dhcp_option_policy: my_test_dhcp_policy state: query - delegate_to: localhost + register: query_result """ RETURN = r""" 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 index 394825feb..59fdf4db7 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_dhcp_relay_policy.py @@ -55,7 +55,6 @@ EXAMPLES = r""" description: "My Test DHCP Policy" tenant: ansible_test state: present - delegate_to: localhost - name: Remove DHCP Relay Policy cisco.mso.mso_dhcp_relay_policy: @@ -64,7 +63,6 @@ EXAMPLES = r""" 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: @@ -73,7 +71,7 @@ EXAMPLES = r""" password: SomeSecretPassword dhcp_relay_policy: my_test_dhcp_policy state: query - delegate_to: localhost + register: query_result - name: Query all DHCP Relay Policies cisco.mso.mso_dhcp_relay_policy: @@ -81,7 +79,7 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost + register: query_result """ RETURN = r""" 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 index 760d90430..e17ee92bc 100644 --- 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 @@ -82,7 +82,6 @@ EXAMPLES = r""" 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: @@ -96,7 +95,6 @@ EXAMPLES = r""" 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: @@ -110,7 +108,7 @@ EXAMPLES = r""" application_profile: ansible_test endpoint_group: ansible_test state: query - delegate_to: localhost + register: query_result - name: Query all provider of a DHCP Relay Policy cisco.mso.mso_dhcp_relay_policy_provider: @@ -119,7 +117,7 @@ EXAMPLES = r""" password: SomeSecretPassword dhcp_relay_policy: my_test_dhcp_policy state: query - delegate_to: localhost + register: query_result """ RETURN = r""" diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_label.py b/ansible_collections/cisco/mso/plugins/modules/mso_label.py index f4f05f7de..2b65de521 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_label.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_label.py @@ -49,7 +49,6 @@ EXAMPLES = r""" label: Belgium type: site state: present - delegate_to: localhost - name: Remove a label cisco.mso.mso_label: @@ -58,7 +57,6 @@ EXAMPLES = r""" password: SomeSecretPassword label: Belgium state: absent - delegate_to: localhost - name: Query a label cisco.mso.mso_label: @@ -67,7 +65,6 @@ EXAMPLES = r""" password: SomeSecretPassword label: Belgium state: query - delegate_to: localhost register: query_result - name: Query all labels @@ -76,7 +73,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py b/ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py index 10546563f..b3ec5e405 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_remote_location.py @@ -89,7 +89,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: backups - name: Query a remote location @@ -99,7 +98,7 @@ EXAMPLES = r""" password: SomeSecretPassword remote_location: ansible_test state: query - delegate_to: localhost + register: query_result - name: Configure a remote location cisco.mso.mso_remote_location: @@ -114,7 +113,6 @@ EXAMPLES = r""" remote_username: username remote_password: password state: present - delegate_to: localhost - name: Delete a remote location cisco.mso.mso_remote_location: @@ -123,7 +121,6 @@ EXAMPLES = r""" password: SomeSecretPassword remote_location: ansible_test state: absent - delegate_to: localhost """ RETURN = r""" diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_rest.py b/ansible_collections/cisco/mso/plugins/modules/mso_rest.py index ae05b093b..04387c505 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_rest.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_rest.py @@ -83,7 +83,6 @@ EXAMPLES = r""" "sites": [], "_updateVersion": 0 } - delegate_to: localhost - name: Query schema cisco.mso.mso_rest: @@ -92,7 +91,7 @@ EXAMPLES = r""" password: SomeSecretPassword path: /mso/api/v1/schemas method: get - delegate_to: localhost + register: query_result - name: Patch schema (YAML) cisco.mso.mso_rest: @@ -109,7 +108,6 @@ EXAMPLES = r""" displayName: AP2 epgs: [] _updateVersion: 0 - delegate_to: localhost - name: Add a tenant from a templated payload file from templates cisco.mso.mso_rest: @@ -119,7 +117,6 @@ EXAMPLES = r""" method: post path: /api/v1/tenants content: "{{ lookup('template', 'mso/tenant.json.j2') }}" - delegate_to: localhost """ RETURN = r""" diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_role.py b/ansible_collections/cisco/mso/plugins/modules/mso_role.py index cfa4483b0..a99df655a 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_role.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_role.py @@ -115,7 +115,6 @@ EXAMPLES = r""" - manage-tenant-schemas - manage-users state: present - delegate_to: localhost - name: Remove a role cisco.mso.mso_role: @@ -124,7 +123,6 @@ EXAMPLES = r""" password: SomeSecretPassword role: readOnly state: absent - delegate_to: localhost - name: Query a role cisco.mso.mso_role: @@ -133,7 +131,6 @@ EXAMPLES = r""" password: SomeSecretPassword role: readOnly state: query - delegate_to: localhost register: query_result - name: Query all roles @@ -142,7 +139,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema.py index 2eba13ac9..7ba2df609 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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) from __future__ import absolute_import, division, print_function @@ -18,21 +20,32 @@ description: - Manage schemas on Cisco ACI Multi-Site. author: - Dag Wieers (@dagwieers) +- Akini Ross (@akinross) options: schema: description: - The name of the schema. type: str aliases: [ name ] + id: + description: + - The id of the schema. + - This parameter is required when the C(schema) needs to be updated. + type: str + description: + description: + - The description of the schema. + type: str state: description: - Use C(absent) for removing. - Use C(query) for listing an object or multiple objects. + - Use C(present) for creating or updating. Only supported on versions of MSO that are 4.1 or greater. type: str - choices: [ absent, query ] + choices: [ absent, query, present ] default: query notes: -- Due to restrictions of the MSO REST API this module cannot create empty schemas (i.e. schemas without templates). +- Due to restrictions of the MSO REST API this module can only create empty schemas (i.e. schemas without templates) on versions of MSO that are 4.1 or greater. Use the M(cisco.mso.mso_schema_template) to automatically create schemas with templates. seealso: - module: cisco.mso.mso_schema_site @@ -41,6 +54,15 @@ extends_documentation_fragment: cisco.mso.modules """ EXAMPLES = r""" +- name: Create schema + cisco.mso.mso_schema: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + state: present + delegate_to: localhost + - name: Remove schemas cisco.mso.mso_schema: host: mso_host @@ -48,7 +70,6 @@ EXAMPLES = r""" password: SomeSecretPassword schema: Schema 1 state: absent - delegate_to: localhost - name: Query a schema cisco.mso.mso_schema: @@ -57,7 +78,6 @@ EXAMPLES = r""" password: SomeSecretPassword schema: Schema 1 state: query - delegate_to: localhost register: query_result - name: Query all schemas @@ -66,7 +86,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: query_result """ @@ -81,12 +100,9 @@ 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"]), + id=dict(type="str"), + description=dict(type="str"), + state=dict(type="str", default="query", choices=["absent", "query", "present"]), ) module = AnsibleModule( @@ -94,28 +110,48 @@ def main(): supports_check_mode=True, required_if=[ ["state", "absent", ["schema"]], + ["state", "present", ["schema"]], ], ) schema = module.params.get("schema") + schema_id = module.params.get("id") + description = module.params.get("description") 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 schema_id: + mso.existing = mso.get_obj(path, id=schema_id) + else: + mso.existing = mso.get_obj(path, displayName=schema) + if mso.existing: - schema_id = mso.existing.get("id") + if not schema_id: + schema_id = mso.existing.get("id") path = "schemas/{id}".format(id=schema_id) else: mso.existing = mso.query_objs(path) - if state == "query": - pass + mso.previous = mso.existing + if state == "present": + mso.sanitize(dict(displayName=schema, id=schema_id, description=description), collate=True) + if mso.existing: + ops = [] + if mso.existing.get("displayName") != schema: + ops.append(dict(op="replace", path="/displayName", value=schema)) + if mso.existing.get("description") != description and description is not None: + ops.append(dict(op="replace", path="/description", value=description)) + + if not module.check_mode: + mso.request(path, method="PATCH", data=ops) + else: + if not module.check_mode: + mso.request(path, method="POST", data=dict(displayName=schema, description=description)) + mso.existing = mso.proposed elif state == "absent": mso.previous = mso.existing diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py index 840fb12ca..f02322f9e 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_clone.py @@ -49,7 +49,6 @@ EXAMPLES = r""" source_schema: Source_Schema destination_schema: Destination_Schema state: clone - delegate_to: localhost """ RETURN = r""" diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py index 83a5213a2..b8e1a7cda 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site.py @@ -58,7 +58,6 @@ EXAMPLES = r""" site: Site1 template: Template 1 state: present - delegate_to: localhost - name: Remove a site from a schema cisco.mso.mso_schema_site: @@ -69,7 +68,6 @@ EXAMPLES = r""" site: Site1 template: Template 1 state: absent - delegate_to: localhost - name: Query a schema site cisco.mso.mso_schema_site: @@ -80,7 +78,6 @@ EXAMPLES = r""" site: Site1 template: Template 1 state: query - delegate_to: localhost register: query_result - name: Query all schema sites @@ -90,7 +87,6 @@ EXAMPLES = r""" password: SomeSecretPassword schema: Schema 1 state: query - delegate_to: localhost register: query_result """ 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 index 0552b6a31..39c5225cd 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_anp.py @@ -64,7 +64,6 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: present - delegate_to: localhost - name: Remove a site ANP cisco.mso.mso_schema_site_anp: @@ -76,7 +75,6 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: absent - delegate_to: localhost - name: Query a specific site ANP cisco.mso.mso_schema_site_anp: @@ -88,7 +86,6 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: query - delegate_to: localhost register: query_result - name: Query all site ANPs @@ -100,7 +97,6 @@ EXAMPLES = r""" site: Site1 template: Template1 state: query - delegate_to: localhost register: query_result """ 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 index 1c2ad6433..6c23a0ca0 100644 --- 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 @@ -75,7 +75,6 @@ EXAMPLES = r""" anp: ANP1 epg: EPG1 state: present - delegate_to: localhost - name: Remove a site EPG cisco.mso.mso_schema_site_anp_epg: @@ -88,7 +87,6 @@ EXAMPLES = r""" anp: ANP1 epg: EPG1 state: absent - delegate_to: localhost - name: Query a specific site EPGs cisco.mso.mso_schema_site_anp_epg: @@ -101,7 +99,6 @@ EXAMPLES = r""" anp: ANP1 epg: EPG1 state: query - delegate_to: localhost register: query_result - name: Query all site EPGs @@ -114,7 +111,6 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: query - delegate_to: localhost register: query_result """ 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 index eda60cfd1..aa302a991 100644 --- 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 @@ -164,6 +164,10 @@ notes: seealso: - module: cisco.mso.mso_schema_site_anp_epg - module: cisco.mso.mso_schema_template_anp_epg +deprecated: + removed_in: 3.0.0 + alternative: Use M(cisco.mso.mso_schema_site_anp_epg_staticport) with option `force_replace=true` instead. + why: The module has been merged to centralise all static port functionality into M(cisco.mso.mso_schema_site_anp_epg_staticport). extends_documentation_fragment: cisco.mso.modules """ @@ -190,7 +194,6 @@ EXAMPLES = r""" - 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: @@ -215,7 +218,6 @@ EXAMPLES = r""" 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: @@ -245,7 +247,6 @@ EXAMPLES = r""" 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: @@ -258,7 +259,6 @@ EXAMPLES = r""" 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: @@ -270,7 +270,6 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: query - delegate_to: localhost register: query_result """ 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 index c684a27be..4e6097f7f 100644 --- 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 @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2019, Nirav Katarmal (@nkatarmal-crest) <nirav.katarmal@crestdatasys.com> +# Copyright: (c) 2023, Mabille Florent (@fmabille09) <florent.mabille@smals.be> +# Copyright: (c) 2024, 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 @@ -18,6 +20,8 @@ description: - Manage site-local EPG domains in schema template on Cisco ACI Multi-Site. author: - Nirav Katarmal (@nkatarmal-crest) +- Mabille Florent (@fmabille09) +- Akini Ross (@akinross) options: schema: description: @@ -68,46 +72,95 @@ options: 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. + - Virtual LAN type for microsegmentation. This attribute can only be used with VMM domain association. type: str + choices: [ vlan ] micro_seg_vlan: description: - - Virtual LAN for microsegmentation. This attribute can only be used with vmmDomain domain association. + - Virtual LAN for microsegmentation. This attribute can only be used with VMM 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. + - Virtual LAN type for port encap. This attribute can only be used with VMM domain association. type: str + choices: [ vlan ] port_encap_vlan: description: - - Virtual LAN type for port encap. This attribute can only be used with vmmDomain domain association. + - Virtual LAN type for port encap. This attribute can only be used with VMM domain association. type: int vlan_encap_mode: description: - - Which VLAN enacap mode to use. This attribute can only be used with vmmDomain domain association. + - Which VLAN enacap mode to use. This attribute can only be used with VMM 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. + - Specifies microsegmentation is enabled or not. This attribute can only be used with VMM 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. + - Which switch type to use with this domain association. This attribute can only be used with VMM 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. + - Which switching mode to use with this domain association. This attribute can only be used with VMM domain association. type: str enhanced_lagpolicy_name: description: - - EPG enhanced lagpolicy name. This attribute can only be used with vmmDomain domain association. + - EPG enhanced lagpolicy name. This attribute can only be used with VMM domain association. type: str enhanced_lagpolicy_dn: description: - - Distinguished name of EPG lagpolicy. This attribute can only be used with vmmDomain domain association. + - Distinguished name of EPG lagpolicy. This attribute can only be used with VMM domain association. + type: str + delimiter: + description: + - Which delimiter to use with this domain association. This attribute can only be used with VMM domain association. + type: str + choices: [ '+', '|', '~', '!', '@', '^', '=' ] + binding_type: + description: + - Which binding_type to use with this domain association. This attribute can only be used with VMM domain association. + type: str + default: none + choices: [ static, dynamic, none, ephemeral ] + num_ports: + description: + - Number of ports for the binding type. This attribute can only be used with VMM domain association. + type: int + port_allocation: + description: + - Port allocation for the binding type. This attribute can only be used with VMM domain association and binding type in static. + - Required when O(binding_type=static). + type: str + choices: [ elastic, fixed ] + netflow_pref: + description: + - The netflow preference state of the domain association. This attribute can only be used with VMM domain association. + type: str + default: disabled + choices: [ enabled, disabled ] + allow_promiscuous: + description: + - The allow promiscuous state of the domain association. This attribute can only be used with VMM domain association. + type: str + default: reject + choices: [ accept, reject ] + forged_transmits: + description: + - The forged transmits state of the domain association. This attribute can only be used with VMM domain association. + type: str + default: reject + choices: [ accept, reject ] + mac_changes: + description: + - The mac changes state of the domain association. This attribute can only be used with VMM domain association. + type: str + default: reject + choices: [ accept, reject ] + custom_epg_name: + description: + - Which custom_epg_name to use with this domain association. This attribute can only be used with VMM domain association. type: str state: description: @@ -142,9 +195,8 @@ EXAMPLES = r""" deployment_immediacy: lazy resolution_immediacy: pre-provision state: present - delegate_to: localhost -- name: Remove a domain from a site EPG +- name: Add a new domain to a site EPG with all possible attributes set cisco.mso.mso_schema_site_anp_epg_domain: host: mso_host username: admin @@ -156,10 +208,28 @@ EXAMPLES = r""" epg: EPG1 domain_association_type: vmmDomain domain_profile: 'VMware-VMM' - deployment_immediacy: lazy - resolution_immediacy: pre-provision - state: absent - delegate_to: localhost + deployment_immediacy: immediate + resolution_immediacy: immediate + micro_seg_vlan_type: vlan + micro_seg_vlan: 100 + port_encap_vlan_type: vlan + port_encap_vlan: 100 + vlan_encap_mode: static + allow_micro_segmentation: true + switch_type: default + switching_mode: native + enhanced_lagpolicy_name: ansible_lag_name + enhanced_lagpolicy_dn: ansible_lag_dn + delimiter: '|' + binding_type: static + num_ports: 2 + port_allocation: elastic + netflow_pref: enabled + allow_promiscuous: accept + forged_transmits: accept + mac_changes: accept + custom_epg_name: ansible_custom_epg + state: present - name: Query a domain associated with a specific site EPG cisco.mso.mso_schema_site_anp_epg_domain: @@ -174,7 +244,6 @@ EXAMPLES = r""" 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 @@ -188,8 +257,23 @@ EXAMPLES = r""" anp: ANP1 epg: EPG1 state: query - delegate_to: localhost register: query_result + +- 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 """ RETURN = r""" @@ -212,9 +296,9 @@ def main(): 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_type=dict(type="str", choices=["vlan"]), micro_seg_vlan=dict(type="int"), - port_encap_vlan_type=dict(type="str"), + port_encap_vlan_type=dict(type="str", choices=["vlan"]), port_encap_vlan=dict(type="int"), vlan_encap_mode=dict(type="str", choices=["static", "dynamic"]), allow_micro_segmentation=dict(type="bool"), @@ -222,6 +306,15 @@ def main(): switching_mode=dict(type="str"), enhanced_lagpolicy_name=dict(type="str"), enhanced_lagpolicy_dn=dict(type="str"), + binding_type=dict(type="str", default="none", choices=["dynamic", "ephemeral", "none", "static"]), + port_allocation=dict(type="str", choices=["elastic", "fixed"]), + num_ports=dict(type="int"), + netflow_pref=dict(type="str", default="disabled", choices=["enabled", "disabled"]), + allow_promiscuous=dict(type="str", default="reject", choices=["accept", "reject"]), + forged_transmits=dict(type="str", default="reject", choices=["accept", "reject"]), + mac_changes=dict(type="str", default="reject", choices=["accept", "reject"]), + delimiter=dict(type="str", choices=["+", "|", "~", "!", "@", "^", "="]), + custom_epg_name=dict(type="str"), ) module = AnsibleModule( @@ -230,6 +323,12 @@ def main(): required_if=[ ["state", "absent", ["domain_association_type", "domain_profile", "deployment_immediacy", "resolution_immediacy"]], ["state", "present", ["domain_association_type", "domain_profile", "deployment_immediacy", "resolution_immediacy"]], + ["binding_type", "static", ["port_allocation"]], + ], + required_together=[ + ("micro_seg_vlan_type", "micro_seg_vlan"), + ("port_encap_vlan_type", "port_encap_vlan"), + ("enhanced_lagpolicy_name", "enhanced_lagpolicy_dn"), ], ) @@ -253,6 +352,15 @@ def main(): 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") + binding_type = module.params.get("binding_type") + port_allocation = module.params.get("port_allocation") + num_ports = module.params.get("num_ports") + netflow_pref = module.params.get("netflow_pref") + allow_promiscuous = module.params.get("allow_promiscuous") + forged_transmits = module.params.get("forged_transmits") + mac_changes = module.params.get("mac_changes") + delimiter = module.params.get("delimiter") + custom_epg_name = module.params.get("custom_epg_name") mso = MSOModule(module) @@ -397,46 +505,54 @@ def main(): 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.") + vmmDomainProperties["microSegVlan"] = dict(vlanType=micro_seg_vlan_type, vlan=micro_seg_vlan) 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.") + vmmDomainProperties["portEncapVlan"] = dict(vlanType=port_encap_vlan_type, vlan=port_encap_vlan) if vlan_encap_mode: vmmDomainProperties["vlanEncapMode"] = vlan_encap_mode - if allow_micro_segmentation: + if allow_micro_segmentation is not None: 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.") + vmmDomainProperties["epgLagPol"] = dict(enhancedLagPol=dict(name=enhanced_lagpolicy_name, dn=enhanced_lagpolicy_dn)) + + if delimiter: + vmmDomainProperties["delimiter"] = delimiter + + if binding_type: + vmmDomainProperties["bindingType"] = binding_type + vmmDomainProperties["numPorts"] = num_ports + + if port_allocation: + vmmDomainProperties["portAllocation"] = port_allocation + + if netflow_pref: + vmmDomainProperties["netflowPref"] = netflow_pref + + if allow_promiscuous: + vmmDomainProperties["allowPromiscuous"] = allow_promiscuous + + if forged_transmits: + vmmDomainProperties["forgedTransmits"] = forged_transmits + + if mac_changes: + vmmDomainProperties["macChanges"] = mac_changes + + if custom_epg_name: + vmmDomainProperties["customEpgName"] = custom_epg_name 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] + new_domain.update(vmmDomainProperties) # If payload is empty, anp and EPG already exist at site level if not payload: 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 index ffd3c682e..54780ceed 100644 --- 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 @@ -103,7 +103,6 @@ EXAMPLES = r""" 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: @@ -117,7 +116,6 @@ EXAMPLES = r""" epg: EPG 1 selector: selector_1 state: absent - delegate_to: localhost - name: Query a specific Selector cisco.mso.mso_schema_site_anp_epg_selector: @@ -131,7 +129,6 @@ EXAMPLES = r""" epg: EPG 1 selector: selector_1 state: query - delegate_to: localhost register: query_result - name: Query all Selectors @@ -145,7 +142,6 @@ EXAMPLES = r""" anp: ANP 1 epg: EPG 1 state: query - delegate_to: localhost register: query_result """ 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 index 01e2ac386..64a2bcddb 100644 --- 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 @@ -88,7 +88,6 @@ EXAMPLES = r""" 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: @@ -102,7 +101,6 @@ EXAMPLES = r""" 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: @@ -116,7 +114,6 @@ EXAMPLES = r""" epg: EPG1 leaf: Leaf1 state: query - delegate_to: localhost register: query_result - name: Query all site EPG static leafs @@ -129,7 +126,6 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: query - delegate_to: localhost register: query_result """ 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 index d05336c52..d22ae1fcf 100644 --- 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 @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2024, 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 @@ -18,6 +19,7 @@ description: - Manage site-local EPG static ports in schema template on Cisco ACI Multi-Site. author: - Dag Wieers (@dagwieers) +- Akini Ross (@akinross) options: schema: description: @@ -47,9 +49,9 @@ options: 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 + - C(vpc) is used for a Virtual Port Channel + - C(dpc) is used for a Direct Port Channel + - C(port) is used for a single interface type: str choices: [ port, vpc, dpc ] default: port @@ -94,6 +96,75 @@ options: description: - Primary micro-seg VLAN of static port. type: int + force_replace: + description: + - Replaces all the configured static port(s) with the provided static port(s). + - This option can only be used in combination with the O(static_ports) option. + - In combination with the O(state=absent) and without any static port configuration all configured static port(s) will be removed. + type: bool + static_ports: + description: + - A list of Static Ports associated to this EPG. + - All configured Static Ports will be replaced with the provided Static Ports when used with O(force_replace=true). + - Only the provided Static Ports will be added, updated or removed when used with O(force_replace=false). + - In combination with the O(state=query) all provided Static Ports must be found else the task will fail. + - When I(static_ports) attributes are not provided the module attributes will be used. + - For each Static Ports provided in the list, the following attributes must be resolved + - I(static_ports.type) + - I(static_ports.pod) + - I(static_ports.leaf) + - I(static_ports.path) + - I(static_ports.vlan) + type: list + elements: dict + suboptions: + type: + description: + - The path type of the static port + - C(vpc) is used for a Virtual Port Channel + - C(dpc) is used for a Direct Port Channel + - C(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. + type: str + vlan: + description: + - The port encapsulation 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 static port. + type: int state: description: - Use C(present) or C(absent) for adding or removing. @@ -129,7 +200,6 @@ EXAMPLES = r""" 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: @@ -149,7 +219,6 @@ EXAMPLES = r""" 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: @@ -169,9 +238,8 @@ EXAMPLES = r""" mode: untagged deployment_immediacy: lazy state: present - delegate_to: localhost -- name: Remove a static port from a site EPG +- name: Add two new static port to a site EPG cisco.mso.mso_schema_site_anp_epg_staticport: host: mso_host username: admin @@ -181,12 +249,40 @@ EXAMPLES = r""" template: Template1 anp: ANP1 epg: EPG1 - type: port - pod: pod-1 - leaf: 101 - path: eth1/1 - state: absent - delegate_to: localhost + static_ports: + - pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + - pod: pod-2 + leaf: 101 + path: eth2/1 + vlan: 128 + deployment_immediacy: immediate + state: present + +- name: Replace all existing static pors on a site EPG with 2 new 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 + epg: EPG1 + force_replace: true + static_ports: + - pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + - pod: pod-2 + leaf: 101 + path: eth2/1 + vlan: 128 + deployment_immediacy: immediate + state: present - name: Query a specific site EPG static port cisco.mso.mso_schema_site_anp_epg_staticport: @@ -203,7 +299,28 @@ EXAMPLES = r""" leaf: 101 path: eth1/1 state: query - delegate_to: localhost + register: query_result + +- name: Query a list of 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 + epg: EPG1 + static_ports: + - pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + - pod: pod-2 + leaf: 101 + path: eth2/1 + vlan: 128 + state: query register: query_result - name: Query all site EPG static ports @@ -216,15 +333,66 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: query - delegate_to: localhost register: query_result + +- 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 + +- name: Remove two static ports 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 + static_ports: + - pod: pod-1 + leaf: 101 + path: eth1/1 + vlan: 126 + - pod: pod-2 + leaf: 101 + path: eth2/1 + vlan: 128 + deployment_immediacy: immediate + state: absent + +- name: Remove all existing static pors 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 + force_replace: true + state: absent """ 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.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(): @@ -235,25 +403,23 @@ def main(): template=dict(type="str", required=True), anp=dict(type="str", required=True), epg=dict(type="str", required=True), + force_replace=dict(type="bool"), 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 + pod=dict(type="str"), + leaf=dict(type="str"), + fex=dict(type="str"), + path=dict(type="str"), + vlan=dict(type="int"), + primary_micro_segment_vlan=dict(type="int"), 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", "absent", ["type", "pod", "leaf", "path", "vlan"]], - ["state", "present", ["type", "pod", "leaf", "path", "vlan"]], - ], ) schema = module.params.get("schema") @@ -270,174 +436,247 @@ def main(): primary_micro_segment_vlan = module.params.get("primary_micro_segment_vlan") deployment_immediacy = module.params.get("deployment_immediacy") mode = module.params.get("mode") + force_replace = module.params.get("force_replace") + static_ports = module.params.get("static_ports") 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) + if not static_ports and state in ["present", "absent"]: + key_list = ["pod", "leaf", "path", "vlan"] + required_missing = [key for key in key_list if module.params.get(key)] + if len(required_missing) != 4 and not (len(required_missing) == 0 and state == "absent" and force_replace): + module.fail_json( + msg="state is present or absent but all of the following are missing: {0}.".format( + ", ".join([key for key in key_list if not module.params.get(key)]), + ) + ) 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) + mso_schema = MSOSchema(mso, schema, template, site) + mso_schema.set_template_anp(anp) + mso_schema.set_template_anp_epg(epg) + mso_schema.set_site_anp(anp, False) + mso_schema.set_site_anp_epg(epg, False) - 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) + # Create missing site anp and site epg if not present + # Logic is only needed for NDO version below 4.x when validate false flag was still available + # This did not trigger the auto creation of site anp and site epg during template anp and epg creation or ataching site to template + # Coverage misses this two conditionals when testing on 4.x and above + if state == "present" and not mso_schema.schema_objects.get("site_anp"): + ops.append( + dict( + op="add", + path="/sites/{0}-{1}/anps/-".format(mso_schema.schema_objects.get("site").details.get("siteId"), template), + value=dict(epgRef=dict(schemaId=mso_schema.id, templateName=template, anpName=anp, epgName=epg)), + ) + ) - # Get EPG - epg_ref = mso.epg_ref(schema_id=schema_id, template=template, anp=anp, epg=epg) + if state == "present" and not mso_schema.schema_objects.get("site_anp_epg"): + ops.append( + dict( + op="add", + path="/sites/{0}-{1}/anps/{2}/epgs/-".format(mso_schema.schema_objects.get("site").details.get("siteId"), template, anp), + value=dict(anpRef=dict(schemaId=mso_schema.id, templateName=template, anpName=anp)), + ) + ) - # 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"]] + static_ports_path = "/sites/{0}-{1}/anps/{2}/epgs/{3}/staticPorts".format( + mso_schema.schema_objects.get("site").details.get("siteId"), + template, + anp, + epg, + ) + static_port_path = "{0}/-".format(static_ports_path) + + full_paths = [] + if static_ports: + found_static_ports = [] + found_full_paths = [] + set_existing_static_ports(mso, mso_schema, full_paths) + for static_port in static_ports: + overwrite_static_path_unprovided_attributes( + mso, static_port, path_type, pod, leaf, fex, path, vlan, primary_micro_segment_vlan, deployment_immediacy, mode + ) + full_path = get_full_static_path( + static_port.get("type"), static_port.get("pod"), static_port.get("leaf"), static_port.get("fex"), static_port.get("path") + ) + mso_schema.set_site_anp_epg_static_port(full_path, False) + if mso_schema.schema_objects.get("site_anp_epg_static_port") is not None: + found_static_ports.append(mso_schema.schema_objects["site_anp_epg_static_port"].details) + found_full_paths.append(full_path) + + elif path_type and pod and leaf and path and vlan: + full_path = get_full_static_path(path_type, pod, leaf, fex, path) + mso_schema.set_site_anp_epg_static_port(full_path, False) + if mso_schema.schema_objects.get("site_anp_epg_static_port") is not None: + mso.existing = mso_schema.schema_objects["site_anp_epg_static_port"].details + static_port_path = "{0}/{1}".format(static_ports_path, mso_schema.schema_objects["site_anp_epg_static_port"].index) + else: + set_existing_static_ports(mso, mso_schema, full_paths) - # 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 state == "query": + if static_ports: + if len(found_static_ports) == len(static_ports): + mso.existing = found_static_ports + else: + not_found_static_ports = [ + "Provided Static Port Path '{0}' not found".format(full_paths[index]) + for index, static_port in enumerate(static_ports) + if full_paths[index] not in found_full_paths + ] + mso.fail_json(msg=not_found_static_ports) + elif not mso.existing and full_path: + mso.fail_json(msg="Provided Static Port Path '{0}' not found".format(full_path)) + mso.exit_json() - # 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)) + mso.previous = mso.existing - # EPG at template level but not at site level. Create payload at site level for EPG + if state == "absent" and mso.existing: + if static_ports and not force_replace: + mso.proposed = mso.existing.copy() + remove_index = [] + for found_full_path in found_full_paths: + if found_full_path in full_paths: + index = full_paths.index(found_full_path) + remove_index.append(index) + # The list index should not shift when removing static ports from the list + # By sorting the indexes found in reverse order, we assure that the highest index is removed first by the NDO backend + # This logic is to avoid removing the wrong static ports + for index in reversed(sorted(remove_index)): + mso.proposed.pop(index) + ops.append(dict(op="remove", path="{0}/{1}".format(static_ports_path, index))) + mso.sent = mso.proposed + elif not force_replace: + mso.sent = mso.existing = {} + ops.append(dict(op="remove", path=static_port_path)) else: - new_epg = dict( - epgRef=dict( - schemaId=schema_id, - templateName=template, - anpName=anp, - epgName=epg, - ) - ) + mso.sent = mso.proposed = mso.existing = [] + ops.append(dict(op="remove", path=static_ports_path)) - # 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 + elif state == "present": + if static_ports and force_replace: + mso.sent = mso.proposed = [ + get_static_port_payload( + get_full_static_path( + static_port.get("type"), + static_port.get("pod"), + static_port.get("leaf"), + static_port.get("fex"), + static_port.get("path"), + ), + static_port.get("deployment_immediacy"), + static_port.get("mode"), + static_port.get("vlan"), + static_port.get("type"), + static_port.get("primary_micro_segment_vlan"), + ) + for static_port in static_ports + ] + if mso.existing: + ops.append(dict(op="replace", path=static_ports_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=static_ports_path, value=mso.sent)) + elif static_ports: + mso.sent = mso.proposed = mso.existing.copy() + for static_port in static_ports: + full_path = get_full_static_path( + static_port.get("type"), static_port.get("pod"), static_port.get("leaf"), static_port.get("fex"), static_port.get("path") + ) + payload = get_static_port_payload( + full_path, + static_port.get("deployment_immediacy"), + static_port.get("mode"), + static_port.get("vlan"), + static_port.get("type"), + static_port.get("primary_micro_segment_vlan"), + ) + if full_path not in found_full_paths: + ops.append(dict(op="add", path=static_port_path, value=payload)) + mso.proposed.append(payload) + else: + index = found_full_paths.index(full_path) + mso.proposed[index] = payload + ops.append(dict(op="replace", path="{0}/{1}".format(static_ports_path, index), value=payload)) + else: + payload = get_static_port_payload(full_path, deployment_immediacy, mode, vlan, path_type, primary_micro_segment_vlan) + mso.sanitize(payload, collate=True) + if mso.existing: + ops.append(dict(op="replace", path=static_port_path, value=mso.sent)) else: - # If anp in payload, anp exists at site level. Update payload with EPG payload - payload["epgs"] = [new_epg] + ops.append(dict(op="add", path=static_port_path, value=mso.sent)) + mso.existing = payload - # Update index of EPG at site level - else: - epg_idx = epgs.index(epg_ref) + mso.existing = mso.proposed - # 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 not module.check_mode and mso.proposed != mso.previous: + mso.request(mso_schema.path, method="PATCH", data=ops) - 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() + mso.exit_json() - ports_path = "/sites/{0}/anps/{1}/epgs/{2}/staticPorts".format(site_template, anp, epg) - ops = [] - new_leaf = dict( + +def set_existing_static_ports(mso, mso_schema, full_paths): + mso.existing = [] + if mso_schema.schema_objects.get("site_anp_epg"): + for existing_static_port in mso_schema.schema_objects["site_anp_epg"].details.get("staticPorts"): + full_paths.append(existing_static_port.get("path")) + mso.existing.append(existing_static_port) + + +def get_static_port_payload(full_path, deployment_immediacy, mode, vlan, path_type, primary_micro_segment_vlan): + payload = dict( deploymentImmediacy=deployment_immediacy, mode=mode, - path=portpath, + path=full_path, portEncapVlan=vlan, type=path_type, ) if primary_micro_segment_vlan: - new_leaf.update(microSegVlan=primary_micro_segment_vlan) + payload.update(microSegVlan=primary_micro_segment_vlan) + return payload - # 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 +def get_full_static_path(path_type, pod, leaf, fex, path): + if path_type == "port" and fex is not None: + return "topology/{0}/paths-{1}/extpaths-{2}/pathep-[{3}]".format(pod, leaf, fex, path) + elif path_type == "vpc": + return "topology/{0}/protpaths-{1}/pathep-[{2}]".format(pod, leaf, path) 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() + return "topology/{0}/paths-{1}/pathep-[{2}]".format(pod, leaf, path) + + +def overwrite_static_path_unprovided_attributes(mso, static_path, path_type, pod, leaf, fex, path, vlan, micro_vlan, deployment_immediacy, mode): + required_overwrites = [] + if not static_path.get("type"): + static_path["type"] = path_type + if not static_path.get("pod"): + static_path["pod"] = pod + if not pod: + required_overwrites.append("pod") + if not static_path.get("leaf"): + static_path["leaf"] = leaf + if not leaf: + required_overwrites.append("leaf") + if not static_path.get("fex"): + static_path["fex"] = fex + if not static_path.get("path"): + static_path["path"] = path + if not path: + required_overwrites.append("path") + if not static_path.get("vlan"): + static_path["vlan"] = vlan + if not vlan: + required_overwrites.append("vlan") + if not static_path.get("primary_micro_segment_vlan"): + static_path["primary_micro_segment_vlan"] = micro_vlan + if not static_path.get("deployment_immediacy"): + static_path["deployment_immediacy"] = deployment_immediacy + if not static_path.get("mode"): + static_path["mode"] = mode + + if required_overwrites: + mso.fail_json(msg="state is present but all of the following are missing: {0}.".format(", ".join(required_overwrites))) if __name__ == "__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 index e5aaeba37..ba55e7c07 100644 --- 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 @@ -100,7 +100,6 @@ EXAMPLES = r""" 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: @@ -114,7 +113,6 @@ EXAMPLES = r""" 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: @@ -128,7 +126,6 @@ EXAMPLES = r""" epg: EPG1 subnet: 10.0.0.0/24 state: query - delegate_to: localhost register: query_result - name: Query all site EPG subnets @@ -141,7 +138,6 @@ EXAMPLES = r""" template: Template1 anp: ANP1 state: query - delegate_to: localhost register: query_result """ 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 index 3030852f5..7e69f5481 100644 --- 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 @@ -74,7 +74,7 @@ options: 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. + - Use C(true) to set the default uSeg Subnet IP address 0.0.0.0. type: bool state: description: @@ -104,7 +104,6 @@ EXAMPLES = r""" 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: @@ -118,7 +117,7 @@ EXAMPLES = r""" name: useg_attr_ip site: ansible_test state: query - delegate_to: localhost + register: query_result - name: Query all EPG uSeg attrs cisco.mso.mso_schema_site_anp_epg_useg_attribute: @@ -131,7 +130,7 @@ EXAMPLES = r""" epg: EPG 1 site: ansible_test state: query - delegate_to: localhost + register: query_result - name: Remove a uSeg attr from an EPG with name cisco.mso.mso_schema_site_anp_epg_useg_attribute: @@ -145,7 +144,6 @@ EXAMPLES = r""" site: ansible_test name: useg_attr_ip state: absent - delegate_to: localhost """ RETURN = r""" @@ -251,11 +249,13 @@ def main(): 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 + if useg_subnet: + if value != "" and value != "0.0.0.0" and value is not None: + mso.fail_json(msg="The value of uSeg subnet IP should be an empty string or 0.0.0.0, when the useg_subnet is set to true.") + payload["fvSubnet"] = useg_subnet payload["value"] = "0.0.0.0" + else: + payload["fvSubnet"] = useg_subnet mso.sanitize(payload, collate=True) 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 index 35c352fb6..64a798b1a 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_bd.py @@ -73,7 +73,6 @@ EXAMPLES = r""" template: Template1 bd: BD1 state: present - delegate_to: localhost - name: Remove a site BD cisco.mso.mso_schema_site_bd: @@ -85,7 +84,6 @@ EXAMPLES = r""" template: Template1 bd: BD1 state: absent - delegate_to: localhost - name: Query a specific site BD cisco.mso.mso_schema_site_bd: @@ -97,7 +95,6 @@ EXAMPLES = r""" template: Template1 bd: BD1 state: query - delegate_to: localhost register: query_result - name: Query all site BDs @@ -109,7 +106,6 @@ EXAMPLES = r""" site: Site1 template: Template1 state: query - delegate_to: localhost register: query_result """ 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 index 8f9581294..2f562a684 100644 --- 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 @@ -94,7 +94,6 @@ EXAMPLES = r""" 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: @@ -110,7 +109,6 @@ EXAMPLES = r""" schema: Schema2 template: Template2 state: present - delegate_to: localhost - name: Remove a site BD l3out cisco.mso.mso_schema_site_bd_l3out: @@ -124,7 +122,6 @@ EXAMPLES = r""" l3out: name: L3out1 state: absent - delegate_to: localhost - name: Query a specific site BD l3out cisco.mso.mso_schema_site_bd_l3out: @@ -138,7 +135,6 @@ EXAMPLES = r""" l3out: name: L3out1 state: query - delegate_to: localhost register: query_result - name: Query all site BD l3outs @@ -151,7 +147,6 @@ EXAMPLES = r""" template: Template1 bd: BD1 state: query - delegate_to: localhost register: query_result """ 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 index c4ab52eec..9bce8c2da 100644 --- 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 @@ -111,7 +111,6 @@ EXAMPLES = r""" 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: @@ -124,7 +123,6 @@ EXAMPLES = r""" 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: @@ -137,7 +135,6 @@ EXAMPLES = r""" bd: BD1 subnet: 11.11.11.0/24 state: query - delegate_to: localhost register: query_result - name: Query all site BD subnets @@ -150,7 +147,6 @@ EXAMPLES = r""" template: Template1 bd: BD1 state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_contract_service_graph.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_contract_service_graph.py new file mode 100644 index 000000000..231ee4284 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_contract_service_graph.py @@ -0,0 +1,376 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024, 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_contract_service_graph +short_description: Manage the service graph association with a contract in schema sites +description: +- Manage the service graph association with a contract in schema sites on Cisco ACI Multi-Site. +- This module is only compatible with NDO versions 3.7 and 4.2+. NDO versions 4.0 and 4.1 are not supported. +author: +- Sabari Jaganathan (@sajagana) +options: + tenant: + description: + - The name of the tenant. + type: str + 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 + site: + description: + - The name of the site. + type: str + required: true + service_graph_schema: + description: + - The name of the schema in which the service graph is located. + type: str + service_graph_template: + description: + - The name of the template in which the service graph is located. + type: str + service_graph: + description: + - The name of the service graph to associate with the site contract. + type: str + node_relationship: + description: + - A list of nodes and their connector details associated with the Service Graph. + type: list + elements: dict + suboptions: + cluster_interface_device: + description: + - The name of the cluster interface device. + type: str + required: true + aliases: [ cluster_device, device, device_name ] + provider_connector_cluster_interface: + description: + - The name of the cluster interface for the provider connector. + type: str + required: true + aliases: [ provider_cluster_interface, provider_interface, provider_interface_name ] + provider_connector_redirect_policy_tenant: + description: + - The name of the tenant for the provider connector redirect policy. + type: str + aliases: [ provider_redirect_policy_tenant, provider_tenant ] + provider_connector_redirect_policy: + description: + - The name of the redirect policy for the provider connector. + type: str + aliases: [ provider_redirect_policy, provider_policy ] + consumer_connector_cluster_interface: + description: + - The name of the cluster interface for the consumer connector. + type: str + required: true + aliases: [ consumer_cluster_interface, consumer_interface, consumer_interface_name ] + consumer_connector_redirect_policy_tenant: + description: + - The name of the tenant for the consumer connector redirect policy. + type: str + aliases: [ consumer_redirect_policy_tenant, consumer_tenant ] + consumer_connector_redirect_policy: + description: + - The name of the redirect policy for the consumer connector. + type: str + aliases: [ consumer_redirect_policy, consumer_policy ] + consumer_subnet_ips: + description: + - The list of subnet IPs for the consumer connector. + - The subnet IPs option is only available for the load balancer devices. + 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 +seealso: +- module: cisco.mso.mso_schema_template_contract_service_graph +extends_documentation_fragment: cisco.mso.modules +""" + + +EXAMPLES = r""" +- name: Associate a service graph with a site contract + cisco.mso.mso_schema_site_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: ansible_tenant + schema: ansible_schema + template: ansible_template1 + site: ansible_test + contract: Contract1 + service_graph_schema: ansible_schema + service_graph_template: ansible_template1 + service_graph: sg + node_relationship: + - cluster_interface_device: ansible_tenant_firewall1 + provider_connector_cluster_interface: clu_if1 + provider_connector_redirect_policy: redirect_policy1 + consumer_connector_cluster_interface: clu_if1 + consumer_connector_redirect_policy: redirect_policy1 + - cluster_interface_device: ansible_tenant_adc + provider_connector_cluster_interface: clu_if3 + provider_connector_redirect_policy: redirect_policy1 + consumer_connector_cluster_interface: clu_if3 + consumer_connector_redirect_policy: redirect_policy1 + consumer_subnet_ips: ["1.1.1.1/24", "4.4.4.4/24"] + - cluster_interface_device: ansible_tenant_other + provider_connector_cluster_interface: clu_if4 + provider_connector_redirect_policy: redirect_policy1 + consumer_connector_cluster_interface: clu_if4 + consumer_connector_redirect_policy: redirect_policy1 + state: present + +- name: Associate a service graph with a cloud site contract + cisco.mso.mso_schema_site_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + tenant: ansible_tenant + schema: ansible_schema + template: ansible_template1 + site: ansible_test + contract: Contract1 + service_graph_schema: ansible_schema + service_graph_template: ansible_template1 + service_graph: sg + state: present + +- name: Query a site contract service graph with contract name + cisco.mso.mso_schema_site_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: ansible_schema + template: ansible_template1 + contract: Contract1 + site: ansible_test + state: query + register: query_result + +- name: Query all site contract service graphs associated with a site template + cisco.mso.mso_schema_site_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: ansible_schema + template: ansible_template1 + site: ansible_test + state: query + register: query_result + +- name: Remove a site contract service graph with contract name + cisco.mso.mso_schema_site_contract_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: ansible_schema + template: ansible_template1 + site: ansible_test + contract: Contract1 + state: absent +""" + +RETURN = r""" +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec, mso_schema_site_contract_service_graph_spec + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + tenant=dict(type="str"), + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + contract=dict(type="str"), + site=dict(type="str", required=True), + service_graph_schema=dict(type="str"), + service_graph_template=dict(type="str"), + service_graph=dict(type="str"), + node_relationship=dict(type="list", elements="dict", options=mso_schema_site_contract_service_graph_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", "service_graph"]], + ], + ) + + tenant = module.params.get("tenant") + schema = module.params.get("schema") + template = module.params.get("template").replace(" ", "") + contract = module.params.get("contract") + site = module.params.get("site") + service_graph_schema = module.params.get("service_graph_schema") + service_graph_template = module.params.get("service_graph_template") + service_graph = module.params.get("service_graph") + node_relationship = module.params.get("node_relationship") + state = module.params.get("state") + + mso = MSOModule(module) + + mso_schema = MSOSchema(mso, schema, template) + mso_schema.set_template(template) + + service_graph_schema_id = schema_id = mso.lookup_schema(schema, True) + service_graph_reference_schema = service_graph_schema or schema + + # Get service graph reference schema id if the service graph schema is not matching with the schema + if service_graph_reference_schema != schema: + service_graph_schema_id = mso.lookup_schema(service_graph_reference_schema, True) + if service_graph_schema_id is None: + mso.fail_json(msg="Provided service_graph_schema: '{0}' does not exist.".format(service_graph_schema)) + + service_graph_reference_schema_id = service_graph_schema_id or schema_id + service_graph_reference_template = service_graph_template or template + + # Get site id + site_id = mso.lookup_site(site) + mso_schema.set_site(template, site) + mso_schema.set_site_contract(contract, False) + + if mso.site_type == "on-premise" and state == "present": + # When site type is on-premise, both node_relationship and tenant are required + if node_relationship is None and tenant is None: + mso.fail_json(msg="The node_relationship and tenant attributes are required when state is present and site type is on-premise.") + elif node_relationship is None: + mso.fail_json(msg="The node_relationship attribute is required when state is present and site type is on-premise.") + elif tenant is None: + mso.fail_json(msg="The tenant attribute is required when state is present and site type is on-premise.") + + if contract and mso_schema.schema_objects["site_contract"] is not None: + site_contract_service_graph = mso_schema.schema_objects["site_contract"].details.get("serviceGraphRelationship") + if site_contract_service_graph and service_graph: + site_contract_service_graph_name = site_contract_service_graph.get("serviceGraphRef").split("/")[-1] + if site_contract_service_graph_name == service_graph: + mso.existing = site_contract_service_graph + else: + mso.fail_json(msg="The service graph: {0} does not associated with the site contract: {1}.".format(service_graph, contract)) + elif site_contract_service_graph and service_graph is None: + mso.existing = site_contract_service_graph + elif contract is not None and mso_schema.schema_objects["site_contract"] is None: + mso.fail_json(msg="The site contract: {0} does not exist.".format(contract)) + elif contract is None and mso_schema.schema_objects["site"].details.get("contracts"): + mso.existing = [ + contract.get("serviceGraphRelationship") + for contract in mso_schema.schema_objects["site"].details.get("contracts") + if contract.get("serviceGraphRelationship") + ] + + if state == "query": + mso.exit_json() + + site_contract_service_graph_path = "/sites/{0}-{1}/contracts/{2}/serviceGraphRelationship".format(site_id, service_graph_reference_template, contract) + + ops = [] + mso.previous = mso.existing + + if state == "absent": + mso.existing = {} + ops.append(dict(op="remove", path=site_contract_service_graph_path)) + elif state == "present": + service_graph_ref = dict(schemaId=service_graph_reference_schema_id, serviceGraphName=service_graph, templateName=service_graph_reference_template) + service_node_relationship = [] + if mso.site_type == "on-premise" and node_relationship: + for node_index, node in enumerate(node_relationship, 1): + service_node_ref = dict( + schemaId=service_graph_reference_schema_id, + serviceGraphName=service_graph, + serviceNodeName="node{0}".format(node_index), + templateName=service_graph_reference_template, + ) + + consumer_subnet_ips_list = [dict(ip=subnet) for subnet in node.get("consumer_subnet_ips")] if node.get("consumer_subnet_ips") else [] + consumer_connector = dict( + clusterInterface=dict( + dn="uni/tn-{0}/lDevVip-{1}/lIf-{2}".format( + tenant, node.get("cluster_interface_device"), node.get("consumer_connector_cluster_interface") + ) + ), + redirectPolicy=dict( + dn="uni/tn-{0}/svcCont/svcRedirectPol-{1}".format( + node.get("consumer_connector_redirect_policy_tenant") or tenant, node.get("consumer_connector_redirect_policy") + ) + ), + subnets=consumer_subnet_ips_list, + ) + provider_connector = dict( + clusterInterface=dict( + dn="uni/tn-{0}/lDevVip-{1}/lIf-{2}".format( + tenant, node.get("cluster_interface_device"), node.get("provider_connector_cluster_interface") + ) + ), + redirectPolicy=dict( + dn="uni/tn-{0}/svcCont/svcRedirectPol-{1}".format( + node.get("provider_connector_redirect_policy_tenant") or tenant, node.get("provider_connector_redirect_policy") + ) + ), + ) + service_node_relationship.append( + dict(consumerConnector=consumer_connector, providerConnector=provider_connector, serviceNodeRef=service_node_ref) + ) + + payload = dict(serviceGraphRef=service_graph_ref, serviceNodesRelationship=service_node_relationship) + elif mso.cloud_provider_type == "azure": + mso_schema.set_site_service_graph(service_graph) + service_nodes = mso_schema.schema_objects["site_service_graph"].details.get("serviceNodes", []) + payload = dict( + serviceGraphRef=service_graph_ref, serviceNodesRelationship=[dict(serviceNodeRef=sg_node.get("serviceNodeRef")) for sg_node in service_nodes] + ) + + mso.sanitize(payload, collate=True) + + if mso.existing: + ops.append(dict(op="replace", path=site_contract_service_graph_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=site_contract_service_graph_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_contract_service_graph_listener.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_contract_service_graph_listener.py new file mode 100644 index 000000000..b854f0404 --- /dev/null +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_contract_service_graph_listener.py @@ -0,0 +1,739 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024, 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_contract_service_graph_listener +short_description: Manage the listener for Azure site contract service graph in schema sites +description: +- Manage the listener for Azure site contract service graph in schema sites on Cisco ACI Multi-Site. +- This module is only compatible with NDO versions 3.7 and 4.2+. NDO versions 4.0 and 4.1 are not supported. +author: +- Sabari Jaganathan (@sajagana) +options: + tenant: + description: + - The name of the tenant. + type: str + device: + description: + - The name of the device. + type: str + aliases: [ device_name ] + 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 + site: + description: + - The name of the site. + type: str + required: true + service_node_index: + description: + - The index of the service node in the site contract service graph. The value starts from 0. + type: int + listener: + description: + - The name of the listener. + type: str + aliases: [ name, listener_name ] + listener_protocol: + description: + - The protocol of the listener. + type: str + choices: [ http, https, tcp, udp, tls, inherit ] + listener_port: + description: + - The port of the listener. + type: int + security_policy: + description: + - The security policy of the listener. + type: str + choices: [ + default, + elb_sec_2016_18, + elb_sec_fs_2018_06, + elb_sec_tls_1_2_2017_01, + elb_sec_tls_1_2_ext_2018_06, + elb_sec_tls_1_1_2017_01, + elb_sec_2015_05, + elb_sec_tls_1_0_2015_04, + app_gw_ssl_default, + app_gw_ssl_2015_501, + app_gw_ssl_2017_401, + app_gw_ssl_2017_401s + ] + ssl_certificates: + description: + - The ssl certificates of the listener. + type: list + elements: dict + suboptions: + name: + description: + - The name of the ssl certificate. + type: str + required: true + certificate_store: + description: + - The certificate store of the ssl certificate. + type: str + required: true + choices: [ default, iam, acm ] + frontend_ip: + description: + - The frontend ip of the listener. Only supported for Network load balancers. + type: str + rules: + description: + - The rules of the listener. + type: list + elements: dict + suboptions: + name: + description: + - The name of the rule. + type: str + required: true + floating_ip: + description: + - The floating ip of the rule. + type: str + priority: + description: + - The priority of the rule. + type: int + required: true + host: + description: + - The host of the rule. + type: str + path: + description: + - The path of the rule. + type: str + action: + description: + - The action of the rule. + type: str + action_type: + description: + - The action type of the rule. + type: str + required: true + choices: [ fixed_response, forward, redirect, ha_port ] + content_type: + description: + - The content type of the rule. + type: str + choices: [ text_plain, text_css, text_html, app_js, app_json ] + port: + description: + - The port of the rule. + type: int + protocol: + description: + - The protocol of the rule. + type: str + choices: [ http, https, tcp, udp, tls, inherit ] + provider_epg: + description: + - The provider epg of the rule. + type: dict + suboptions: + schema: + description: + - The schema name of the provider epg reference. + type: str + template: + description: + - The template name of the provider epg reference. + type: str + anp_name: + description: + - The application profile name of the provider epg reference. + type: str + required: true + aliases: [ anp ] + epg_name: + description: + - The epg (Endpoint Group) name of the provider epg reference. + type: str + required: true + aliases: [ epg ] + url_type: + description: + - The url type of the rule. + type: str + choices: [ original, custom ] + custom_url: + description: + - The custom url of the rule. + type: str + redirect_host_name: + description: + - The redirect host name of the rule. + type: str + redirect_path: + description: + - The redirect path of the rule. + type: str + redirect_query: + description: + - The redirect query of the rule. + type: str + response_code: + description: + - The response code of the rule. + type: str + response_body: + description: + - The response body of the rule. + type: str + redirect_protocol: + description: + - The redirect protocol of the rule. + type: str + choices: [ http, https, tcp, udp, tls, inherit ] + redirect_port: + description: + - The redirect port of the rule. + type: int + redirect_code: + description: + - The redirect code of the rule. + type: str + choices: [ unknown, permanently_moved, found, see_other, temporary_redirect ] + health_check: + description: + - The health check of the rule. + type: dict + suboptions: + port: + description: + - The port of the health check. + type: int + protocol: + description: + - The protocol of the health check. + type: str + choices: [ http, https, tcp, udp, tls, inherit ] + path: + description: + - The path of the health check. + type: str + interval: + description: + - The interval of the health check. + type: int + timeout: + description: + - The timeout of the health check. + type: int + unhealthy_threshold: + description: + - The unhealthy threshold of the health check. + type: int + use_host_from_rule: + description: + - The use host from rule of the health check. + type: bool + host: + description: + - The host of the health check. The host attribute will be enabled when the I(use_host_from_rule) is false. + type: str + success_code: + description: + - The success code of the health check. + type: str + target_ip_type: + description: + - The target ip type of the rule. + type: str + choices: [ unspecified, primary, secondary ] + 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_service_graph +extends_documentation_fragment: cisco.mso.modules +""" + + +EXAMPLES = r""" +- name: Add a listener for Network Load-Balancer + cisco.mso.mso_schema_site_contract_service_graph_listener: + host: mso_host + username: admin + password: SomeSecretPassword + contract: "Contract2" + schema: mso_schema + template: ansible_template1 + site: mso_site + service_node_index: 0 + listener: nlb_li_tcp + listener_port: 80 + listener_protocol: tcp + tenant: mso_tenant + frontend_ip: "10.10.10.10" + device: ans_test_nlb + security_policy: default + rules: + - name: rule1 + priority: 1 + action_type: forward + port: 80 + protocol: tcp + health_check: + port: 80 + protocol: tcp + interval: 5 + unhealthy_threshold: 2 + success_code: 200-399 + +- name: Add a listener for Application Load-Balancer + cisco.mso.mso_schema_site_contract_service_graph_listener: + host: mso_host + username: admin + password: SomeSecretPassword + contract: "Contract2" + schema: mso_schema + template: ansible_template1 + site: mso_site + service_node_index: 1 + listener: aplb_li_https + tenant: mso_tenant + device: ans_test_aplb + listener_port: 443 + listener_protocol: https + security_policy: default + ssl_certificates: + - name: ans_test_keyring + certificate_store: default + rules: + - name: rule1 + priority: 1 + action_type: forward + port: 80 + protocol: http + provider_epg: + anp_name: AP1 + epg_name: EPG1 + health_check: + port: 80 + protocol: http + path: "health_check_path" + interval: 30 + timeout: 30 + unhealthy_threshold: 3 + use_host_from_rule: true + success_code: "200" + target_ip_type: unspecified + +- name: Query all listeners + cisco.mso.mso_schema_site_contract_service_graph_listener: + host: mso_host + username: admin + password: SomeSecretPassword + contract: "Contract2" + schema: mso_schema + template: ansible_template1 + site: mso_site + state: query + register: query_all_listeners + +- name: Query all listeners with name ans_li_common + cisco.mso.mso_schema_site_contract_service_graph_listener: + host: mso_host + username: admin + password: SomeSecretPassword + contract: "Contract2" + schema: mso_schema + template: ansible_template1 + site: mso_site + listener: ans_li_common + state: query + register: query_all_ans_li_common + +- name: Query a listener with name - aplb_li_https + cisco.mso.mso_schema_site_contract_service_graph_listener: + host: mso_host + username: admin + password: SomeSecretPassword + contract: "Contract2" + schema: mso_schema + template: ansible_template1 + site: mso_site + service_node_index: 1 + listener: aplb_li_https + state: query + register: query_aplb_li_https + +- name: Remove an existing listener - ans_li_common + cisco.mso.mso_schema_site_contract_service_graph_listener: + host: mso_host + username: admin + password: SomeSecretPassword + contract: "Contract2" + schema: mso_schema + template: ansible_template1 + site: mso_site + service_node_index: 1 + listener: aplb_li_https + state: absent +""" + +RETURN = r""" +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema +from ansible_collections.cisco.mso.plugins.module_utils.mso import ( + MSOModule, + mso_argument_spec, + listener_ssl_certificates_spec, + listener_rules_spec, +) +from ansible_collections.cisco.mso.plugins.module_utils.constants import ( + LISTENER_REDIRECT_CODE_MAP, + LISTENER_CONTENT_TYPE_MAP, + LISTENER_ACTION_TYPE_MAP, + LISTENER_SECURITY_POLICY_MAP, + LISTENER_PROTOCOLS, + YES_OR_NO_TO_BOOL_STRING_MAP, +) + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + tenant=dict(type="str"), + device=dict(type="str", aliases=["device_name"]), + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + contract=dict(type="str", required=True), + site=dict(type="str", required=True), + service_node_index=dict(type="int"), + listener=dict(type="str", aliases=["name", "listener_name"]), + listener_protocol=dict(type="str", choices=LISTENER_PROTOCOLS), + listener_port=dict(type="int"), + security_policy=dict(type="str", choices=list(LISTENER_SECURITY_POLICY_MAP)), + ssl_certificates=dict(type="list", elements="dict", options=listener_ssl_certificates_spec()), + frontend_ip=dict(type="str"), + rules=dict(type="list", elements="dict", options=listener_rules_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", ["listener"]], + ["state", "present", ["listener", "listener_protocol", "listener_port", "rules"]], + ], + ) + + mso = MSOModule(module) + site = module.params.get("site") + + # Get site id + site_id = mso.lookup_site(site) + + if mso.site_type == "on-premise" or mso.cloud_provider_type != "azure": + mso.fail_json(msg="The Site Contract Service Graph Listener is not supported for the site: {0}.".format(site)) + + schema = module.params.get("schema") + template = module.params.get("template") + contract = module.params.get("contract") + service_node_index = module.params.get("service_node_index") + listener = module.params.get("listener") + tenant = module.params.get("tenant") + device = module.params.get("device") + listener_protocol = module.params.get("listener_protocol") + listener_port = module.params.get("listener_port") + security_policy = LISTENER_SECURITY_POLICY_MAP.get(module.params.get("security_policy")) + ssl_certificates = module.params.get("ssl_certificates") + frontend_ip = module.params.get("frontend_ip") + rules = module.params.get("rules") + state = module.params.get("state") + + if listener_protocol == "https": + mso.input_validation("listener_protocol", "https", ["security_policy", "ssl_certificates"], module.params, None, object_name=listener) + + mso_schema = MSOSchema(mso, schema, template) + mso_schema.set_template(template) + schema_id = mso.lookup_schema(schema, True) + mso_schema.set_site(template, site) + mso_schema.set_site_contract(contract, False) + + service_graph_ref = mso_schema.schema_objects["site_contract"].details.get("serviceGraphRelationship", {}).get("serviceGraphRef") + service_nodes = mso_schema.schema_objects["site_contract"].details.get("serviceGraphRelationship", {}).get("serviceNodesRelationship", []) + + parent_present = False + + if service_graph_ref is None: + mso.fail_json(msg="The site contract: {0} is not associated with a service graph.".format(contract)) + # Parent object present check + else: + if service_node_index is not None and service_node_index >= 0: + if len(service_nodes) > 0 and len(service_nodes) > service_node_index: + service_node = service_nodes[service_node_index] + # Query all listeners under a contract service node + # The below condition was never false if the service graph was created properly, but condition is required to avoid error + if service_node.get("serviceNodeRef") and service_node.get("serviceNodeRef").split("/")[-1] == "node{0}".format(service_node_index + 1): + listeners = service_node.get("deviceConfiguration", {}).get("cloudLoadBalancer", {}).get("listeners", []) + if listeners: + parent_present = True + if listener: + for listener_data in listeners: + if listener_data.get("name") == listener: + mso.existing = listener_data + break + else: + mso.existing = listeners + else: + mso.fail_json( + msg="The service_node_index: {0} is not matching with the service node reference: {1}.".format( + service_node_index, service_node.get("serviceNodeRef") + ) + ) + else: + mso.fail_json(msg="The service_node_index: {0} is out of range.".format(service_node_index)) + + # Query all listeners under a contract does not require service_node_index, so the below condition is required + elif state == "query": + # Query all listeners under a contract + for service_node in service_nodes: + listeners = service_node.get("deviceConfiguration", {}).get("cloudLoadBalancer", {}).get("listeners", []) + if listener: + for listener_data in listeners: + # Query a listener under a contract service node + if listener_data.get("name") == listener: + mso.existing = ([listener_data] + mso.existing) if mso.existing else [listener_data] + break + else: + mso.existing = (listeners + mso.existing) if mso.existing else listeners + + else: + mso.fail_json(msg="The service_node_index: {0} is not valid.".format(service_node_index)) + + if state == "query": + mso.exit_json() + + ops = [] + mso.previous = mso.existing + + parent_object = {} + + if state == "present": + # Parent object creation logic begins + if device is None and parent_present is False: + mso.fail_json(msg="The 'device' name is required to initialize the parent object.") + + elif device is not None and parent_present is False: + query_device_data = mso.lookup_service_node_device(site_id, tenant, device_name=device) + + if query_device_data.get("deviceVendorType") == "NATIVELB" and ( + query_device_data.get("devType") == "application" or query_device_data.get("devType") == "network" + ): + mso_schema.set_site_service_graph(service_graph_ref.split("/")[-1]) + + for sg in mso_schema.schema_objects["site_service_graph"].details.get("serviceNodes", []): + if device == sg.get("device").get("dn").split("/")[-1].split("-")[-1]: + parent_object = dict(deviceConfiguration=dict(cloudLoadBalancer=dict(listeners=[])), serviceNodeRef=sg.get("serviceNodeRef")) + break + else: + mso.fail_json( + msg="Listener is not supported for the 'service_node_index': {0} is associated with the Third-Party {1} device.".format( + service_node_index, "Load Balancer" if query_device_data.get("deviceVendorType") == "ADC" else "Firewall" + ) + ) + # Parent object creation logic ends + + # Listener object creation logic begins + listener_object = dict( + name=listener, + protocol=listener_protocol, + port=listener_port, + secPolicy=security_policy, + ) + + if frontend_ip: + mso.input_validation("frontend_ip", frontend_ip, ["tenant", "device"], module.params, None, object_name=listener) + listener_object["nlbDevIp"] = dict(name=frontend_ip, dn="uni/tn-{0}/clb-{1}/vip-{2}".format(tenant, device, frontend_ip)) + + if ssl_certificates: + listener_object["certificates"] = [ + { + "name": ssl_certificate.get("name"), + "tDn": "uni/tn-{0}/certstore".format(tenant), + "default": True, + "store": ssl_certificate.get("certificate_store"), + } + for ssl_certificate in ssl_certificates + ] + + # Rules object creation logic + rules_data = [] + + for position, rule in enumerate(rules, 0): + if (listener_protocol == "http" and rule.get("protocol") == "http") or (listener_protocol == "https" and rule.get("protocol") == "https"): + mso.fail_json(msg="When the 'listener_protocol' is '{0}', the rule 'protocol' must be '{1}'".format(listener_protocol, rule.get("protocol"))) + + if rule.get("action_type") == "redirect": + mso.input_validation( + "action_type", "redirect", ["redirect_protocol", "redirect_port", "url_type", "redirect_code"], rule, position, rule.get("name") + ) + elif rule.get("action_type") == "forward": + mso.input_validation("action_type", "forward", ["protocol", "port", "health_check"], rule, position, rule.get("name")) + + if rule.get("url_type") == "custom": + mso.input_validation( + "url_type", "custom", ["redirect_host_name", "redirect_path", "redirect_query", "response_code"], rule, position, rule.get("name") + ) + + rule_data = dict( + name=rule.get("name"), + floatingIp=rule.get("floating_ip"), + index=rule.get("priority"), + host=rule.get("host"), + path=rule.get("path"), + action=rule.get("action"), + actionType=LISTENER_ACTION_TYPE_MAP.get(rule.get("action_type")), + contentType=LISTENER_CONTENT_TYPE_MAP.get(rule.get("content_type")), + port=rule.get("port"), + protocol=rule.get("protocol"), + urlType=rule.get("url_type"), + customURL=rule.get("custom_url"), + redirectHostName=rule.get("redirect_host_name"), + redirectPath=rule.get("redirect_path"), + redirectQuery=rule.get("redirect_query"), + responseCode=rule.get("response_code"), + responseBody=rule.get("response_body"), + redirectProtocol=rule.get("redirect_protocol"), + redirectPort=rule.get("redirect_port"), + redirectCode=LISTENER_REDIRECT_CODE_MAP.get(rule.get("redirect_code")), + targetIpType=rule.get("target_ip_type"), + ) + + if listener_protocol in ["tcp", "udp"]: + mso.input_validation("listener_protocol", "tcp/udp", ["health_check"], rule) + + provider_epg = rule.get("provider_epg") + if provider_epg: + rule_data["providerEpgRef"] = "/schemas/{0}/templates/{1}/anps/{2}/epgs/{3}".format( + provider_epg.get("schema") or schema_id, + provider_epg.get("template_name") or template, + provider_epg.get("anp_name"), + provider_epg.get("epg_name"), + ) + + health_check = rule.get("health_check") + if health_check: + if listener_protocol in ["tcp", "udp"]: + if health_check.get("protocol") == "tcp": + mso.input_validation("health_check - 'protocol'", "tcp", ["port", "unhealthy_threshold", "interval"], health_check) + elif health_check.get("protocol") in ["http", "https"]: + mso.input_validation("health_check - 'protocol'", "http/https", ["port", "path", "unhealthy_threshold", "interval"], health_check) + elif (listener_protocol == "http" and health_check.get("protocol") == "https") or ( + listener_protocol == "https" and health_check.get("protocol") == "http" + ): + mso.input_validation( + "health_check - 'protocol'", "http/https", ["port", "path", "unhealthy_threshold", "timeout", "interval"], health_check + ) + else: + mso.fail_json( + msg=( + "The 'listener_protocol': {0} and the health_check protocol: {1} " + + "is not a valid configuration at the object position: {2} and the object name: {3}" + ).format(listener_protocol, health_check.get("protocol"), position, rule.get("name")) + ) + + health_check_data = dict( + port=health_check.get("port"), + protocol=health_check.get("protocol"), + path=health_check.get("path"), + interval=health_check.get("interval"), + timeout=health_check.get("timeout"), + unhealthyThreshold=health_check.get("unhealthy_threshold"), + successCode=health_check.get("success_code"), + useHostFromRule=YES_OR_NO_TO_BOOL_STRING_MAP.get(health_check.get("use_host_from_rule")), + host=health_check.get("host"), + ) + + rule_data["healthCheck"] = health_check_data + + rules_data.append(rule_data) + + listener_object["rules"] = rules_data + + if parent_present: + # Update an existing listener + if mso.existing: + listener_path = ( + "/sites/{0}-{1}/contracts/{2}/serviceGraphRelationship/serviceNodesRelationship/{3}/deviceConfiguration/cloudLoadBalancer/listeners/{4}" + ).format(site_id, template, contract, service_node_index, listener) + op = "replace" + else: + # Create a new listener + listener_path = ( + "/sites/{0}-{1}/contracts/{2}/serviceGraphRelationship/serviceNodesRelationship/{3}/deviceConfiguration/cloudLoadBalancer/listeners/-" + ).format(site_id, template, contract, service_node_index) + op = "add" + parent_object = listener_object + else: + # Create a new listener with parent object + listener_path = "/sites/{0}-{1}/contracts/{2}/serviceGraphRelationship/serviceNodesRelationship/{3}".format( + site_id, template, contract, service_node_index + ) + op = "replace" + parent_object["deviceConfiguration"]["cloudLoadBalancer"]["listeners"].append(listener_object) + + mso.sanitize(parent_object, collate=True) + ops.append(dict(op=op, path=listener_path, value=mso.sent)) + + elif state == "absent": + if mso.existing: + listener_path = ( + "/sites/{0}-{1}/contracts/{2}/serviceGraphRelationship/serviceNodesRelationship/{3}/deviceConfiguration/cloudLoadBalancer/listeners/{4}" + ).format(site_id, template, contract, service_node_index, listener) + ops.append(dict(op="remove", path=listener_path)) + + 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 index dff4cf152..ee48df378 100644 --- 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 @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: (c) 2021, Anvitha Jain (@anvitha-jain) <anvjain@cisco.com> +# Copyright: (c) 2021, 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 @@ -13,7 +13,7 @@ ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported DOCUMENTATION = r""" --- module: mso_schema_site_external_epg -short_description: Manage External EPG in schema of sites +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. @@ -34,6 +34,19 @@ options: description: - The L3Out associated with the external epg. - Required when site is of type on-premise. + - In NDO versions over 4.2, the parameter is accessible only when an external EPG is + - linked to the current schema-template's VRF. + type: str + aliases: [ l3out_name ] + l3out_schema: + description: + - The schema that defines the referenced L3Out. + - If this parameter is unspecified, it defaults to the current schema. + type: str + l3out_template: + description: + - The template that defines the referenced L3Out. + - If this parameter is unspecified, it defaults to the current template. type: str external_epg: description: @@ -75,7 +88,6 @@ EXAMPLES = r""" 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: @@ -87,7 +99,6 @@ EXAMPLES = r""" 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: @@ -99,7 +110,7 @@ EXAMPLES = r""" external_epg: External EPG 1 l3out: L3out1 state: query - delegate_to: localhost + register: query_result - name: Query all Site External EPGs cisco.mso.mso_schema_site_external_epg: @@ -109,7 +120,7 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost + register: query_result """ RETURN = r""" @@ -126,7 +137,9 @@ def main(): schema=dict(type="str", required=True), template=dict(type="str", required=True), site=dict(type="str", required=True), - l3out=dict(type="str"), + l3out=dict(type="str", aliases=["l3out_name"]), + l3out_schema=dict(type="str"), + l3out_template=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"]), @@ -142,15 +155,21 @@ def main(): ) schema = module.params.get("schema") - template = module.params.get("template") + template = module.params.get("template").replace(" ", "") site = module.params.get("site") external_epg = module.params.get("external_epg") l3out = module.params.get("l3out") + l3out_schema = module.params.get("l3out_schema") + l3out_template = module.params.get("l3out_template") route_reachability = module.params.get("route_reachability") state = module.params.get("state") mso = MSOModule(module) + l3out_template = template if l3out_template is None else l3out_template.replace(" ", "") + l3out_schema = schema if l3out_schema is None else l3out_schema + l3out_schema_id = mso.lookup_schema(l3out_schema) + mso_schema = MSOSchema(mso, schema, template, site) mso_objects = mso_schema.schema_objects @@ -175,7 +194,6 @@ def main(): ops = [] l3out_dn = "" - if state == "query": if external_epg is None: mso.existing = mso_objects.get("site").details.get("externalEpgs") @@ -208,8 +226,8 @@ def main(): ), l3outDn=l3out_dn, l3outRef=dict( - schemaId=mso_schema.id, - templateName=template, + schemaId=l3out_schema_id, + templateName=l3out_template, l3outName=l3out, ), routeReachabilityInternetType=route_reachability, 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 index 001ebf2b5..e4081cebe 100644 --- 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 @@ -94,7 +94,6 @@ EXAMPLES = r""" operator: equals value: 10.0.0.0 state: present - delegate_to: localhost - name: Remove a Selector cisco.mso.mso_schema_site_external_epg_selector: @@ -107,7 +106,6 @@ EXAMPLES = r""" external_epg: ext1 selector: test state: absent - delegate_to: localhost - name: Query a specific Selector cisco.mso.mso_schema_site_external_epg_selector: @@ -120,7 +118,6 @@ EXAMPLES = r""" external_epg: ext1 selector: selector_1 state: query - delegate_to: localhost register: query_result - name: Query all Selectors @@ -133,7 +130,6 @@ EXAMPLES = r""" site: azure_ansible_test external_epg: ext1 state: query - delegate_to: localhost register: query_result """ 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 index 4741e4bd7..4293d7253 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_l3out.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_l3out.py @@ -89,7 +89,6 @@ EXAMPLES = r""" template: TemplateName schema: schemaName state: present - delegate_to: localhost - name: Remove a site L3Out cisco.mso.mso_schema_site_l3out: @@ -101,7 +100,6 @@ EXAMPLES = r""" template: Template1 l3out: L3out1 state: absent - delegate_to: localhost - name: Query a specific site L3Out cisco.mso.mso_schema_site_l3out: @@ -113,7 +111,6 @@ EXAMPLES = r""" template: Template1 l3out: L3out1 state: query - delegate_to: localhost register: query_result - name: Query all site l3outs @@ -125,7 +122,6 @@ EXAMPLES = r""" site: Site1 template: Template1 state: query - delegate_to: localhost register: query_result """ 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 index 9495a501d..29919152d 100644 --- 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 @@ -50,11 +50,32 @@ options: type: list elements: dict suboptions: - name: + device_name: description: - The name of the device required: true type: str + aliases: [ name ] + provider_interface: + description: + - The name of the provider interface for the Azure CNC L4-L7 device. + type: str + provider_connector_type: + description: + - The provider connector type for the Azure CNC site service graph. + - Defaults to C(none) when unset during creation. + type: str + choices: [ none, redirect, source_nat, destination_nat, source_and_destination_nat ] + consumer_interface: + description: + - The name of the consumer interface for the Azure CNC L4-L7 device. + type: str + consumer_connector_type: + description: + - The consumer connector type for the Azure CNC site service graph. + - Defaults to C(none) when unset during creation. + type: str + choices: [ none, redirect ] state: description: - Use C(present) or C(absent) for adding or removing. @@ -81,7 +102,28 @@ EXAMPLES = r""" - name: ansible_test_adc - name: ansible_test_other state: present - delegate_to: localhost + +- name: Add a Site service graph for the Azure cloud CNC + cisco.mso.mso_schema_site_service_graph: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema1 + template: Template1 + service_graph: SG1 + site: site1 + tenant: tenant1 + devices: + - name: ans_tnt_firewall1 + provider_connector_type: source_nat + provider_interface: TP_FW_Inf1 + consumer_connector_type: redirect + consumer_interface: TP_FW_Inf1 + - name: ans_tnt_app_lb + - name: ans_tnt_other + provider_connector_type: destination_nat + consumer_connector_type: redirect + state: present - name: Remove a Service Graph cisco.mso.mso_schema_site_service_graph_node: @@ -93,7 +135,6 @@ EXAMPLES = r""" service_graph: SG1 site: site1 state: absent - delegate_to: localhost - name: Query a specific Service Graph cisco.mso.mso_schema_site_service_graph_node: @@ -105,7 +146,7 @@ EXAMPLES = r""" service_graph: SG1 site: site1 state: query - delegate_to: localhost + register: query_result - name: Query all Service Graphs cisco.mso.mso_schema_site_service_graph_node: @@ -116,14 +157,21 @@ EXAMPLES = r""" template: Template1 site: site1 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_node_device_spec +from ansible_collections.cisco.mso.plugins.module_utils.mso import ( + MSOModule, + mso_argument_spec, + mso_service_graph_node_device_spec, + service_node_ref_str_to_dict, +) +from ansible_collections.cisco.mso.plugins.module_utils.constants import AZURE_L4L7_CONNECTOR_TYPE_MAP def main(): @@ -206,17 +254,25 @@ def main(): ops = [] mso.previous = mso.existing + if mso.previous.get("serviceNodes") is not None and len(mso.previous.get("serviceNodes")) > 0: + for node in mso.previous.get("serviceNodes"): + node["serviceNodeRef"] = service_node_ref_str_to_dict(node.get("serviceNodeRef")) + + devices_payload = [] + 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"] + service_node_types_from_template = [] for graph in service_graphs: if graph.get("name") == service_graph: service_node_types_from_template = graph["serviceNodes"] + break + 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: @@ -227,29 +283,77 @@ def main(): ) if devices is not None: - service_node_type_names_from_template = [type.get("name") for type in service_node_types_from_template] + query_device_data = mso.lookup_service_node_device(site_id, tenant, device_name=None, service_node_type=None) 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, - ), - ), - ) + if query_device_data: + for device_data in query_device_data: + if device.get("device_name") == device_data.get("dn").split("/")[-1].split("-")[-1]: + device_payload = dict() + device_payload["device"] = dict( + dn=device_data.get("dn"), + funcType=device_data.get("funcType"), + ) + device_payload["serviceNodeRef"] = dict( + serviceNodeName="node{0}".format(index + 1), + serviceGraphName=service_graph, + templateName=template, + schemaId=schema_id, + ) + + if mso.cloud_provider_type == "azure": + consumer_interface = device.get("consumer_interface") + provider_interface = device.get("provider_interface") + provider_connector_type = device.get("provider_connector_type") + consumer_connector_type = device.get("consumer_connector_type") + + if ( + device_data.get("deviceVendorType") == "NATIVELB" + and device_data.get("devType") == "application" + and ( + consumer_interface is not None + or provider_interface is not None + or provider_connector_type is not None + or consumer_connector_type is not None + ) + ): + # Application Load Balancer - Consumer Interface, Consumer Connector Type, + # Provider Interface, Provider Connector Type - not supported + mso.fail_json( + msg="Unsupported attributes: provider_connector_type, provider_interface, " + + "consumer_connector_type, consumer_interface should be 'None' for the " + + "Application Load Balancer device." + ) + elif ( + device_data.get("deviceVendorType") == "NATIVELB" + and device_data.get("devType") == "network" + and (consumer_interface is not None or provider_interface is not None) + ): + # Network Load Balancer - Consumer Interface, Provider Interface - not supported + mso.fail_json( + msg="Unsupported attributes: provider_interface and consumer_interface should " + + "be 'None' for the Network Load Balancer device." + ) + elif ( + device_data.get("deviceVendorType") == "ADC" + and device_data.get("devType") == "CLOUD" + and (provider_connector_type is not None or consumer_connector_type is not None) + ): + # Third-Party Load Balancer - Consumer Connector Type, + # Provider Connector Type - not supported + mso.fail_json( + msg="Unsupported attributes: provider_connector_type and " + + "consumer_connector_type should be 'None' for the " + + "Third-Party Load Balancer." + ) + + # (FW) Third-Party Firewall - Consumer Interface, Consumer Connector Type, + # Provider Interface, Provider Connector Type - supported + device_payload["consumerInterface"] = consumer_interface + device_payload["providerInterface"] = provider_interface + device_payload["providerConnectorType"] = AZURE_L4L7_CONNECTOR_TYPE_MAP.get(provider_connector_type) + device_payload["consumerConnectorType"] = AZURE_L4L7_CONNECTOR_TYPE_MAP.get(consumer_connector_type) + + devices_payload.append(device_payload) payload = dict( serviceGraphRef=dict( @@ -263,6 +367,8 @@ def main(): mso.sanitize(payload, collate=True) if not mso.existing: + # The site service graph reference will be added automatically when the site is associated with the template + # So the add(create) part will not be used for the NDO v4.2 ops.append(dict(op="add", path=service_graphs_path, value=payload)) else: ops.append(dict(op="replace", path=service_graph_path, value=mso.sent)) 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 index a0b864a8b..42de06ab7 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_site_vrf.py @@ -63,7 +63,6 @@ EXAMPLES = r""" template: Template1 vrf: VRF1 state: present - delegate_to: localhost - name: Remove a site VRF cisco.mso.mso_schema_site_vrf: @@ -75,7 +74,6 @@ EXAMPLES = r""" template: Template1 vrf: VRF1 state: absent - delegate_to: localhost - name: Query a specific site VRF cisco.mso.mso_schema_site_vrf: @@ -87,7 +85,6 @@ EXAMPLES = r""" template: Template1 vrf: VRF1 state: query - delegate_to: localhost register: query_result - name: Query all site VRFs @@ -99,7 +96,6 @@ EXAMPLES = r""" site: Site1 template: Template1 state: query - delegate_to: localhost register: query_result """ 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 index 5ac5804e6..6863426ad 100644 --- 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 @@ -101,7 +101,6 @@ EXAMPLES = r""" 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: @@ -114,7 +113,6 @@ EXAMPLES = r""" 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: @@ -127,7 +125,6 @@ EXAMPLES = r""" vrf: VRF1 region: us-west-1 state: query - delegate_to: localhost register: query_result - name: Query all site VRF regions @@ -140,7 +137,6 @@ EXAMPLES = r""" template: Template1 vrf: VRF1 state: query - delegate_to: localhost register: query_result """ 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 index ef409f710..b2a8e4f33 100644 --- 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 @@ -87,7 +87,6 @@ EXAMPLES = r""" 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: @@ -101,7 +100,6 @@ EXAMPLES = r""" 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: @@ -115,7 +113,6 @@ EXAMPLES = r""" 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 @@ -129,7 +126,6 @@ EXAMPLES = r""" vrf: VRF1 region: us-west-1 state: query - delegate_to: localhost register: query_result """ 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 index 85a00ea9c..a8cd42ca5 100644 --- 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 @@ -113,7 +113,6 @@ EXAMPLES = r""" 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: @@ -128,7 +127,6 @@ EXAMPLES = r""" 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: @@ -143,7 +141,6 @@ EXAMPLES = r""" 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 @@ -158,7 +155,6 @@ EXAMPLES = r""" region: us-west-1 cidr: 14.14.14.1/24 state: query - delegate_to: localhost register: query_result """ 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 index 9df7bab4f..3d0cf4f3c 100644 --- 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 @@ -93,7 +93,6 @@ EXAMPLES = r""" 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: @@ -105,7 +104,6 @@ EXAMPLES = r""" 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: @@ -118,7 +116,6 @@ EXAMPLES = r""" vrf: VRF1 region: us-west-1 state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py index 6f4ece9ca..bd0fa8fef 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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) from __future__ import absolute_import, division, print_function @@ -18,12 +20,12 @@ description: - Manage templates on Cisco ACI Multi-Site. author: - Dag Wieers (@dagwieers) +- Akini Ross (@akinross) options: tenant: description: - The tenant used for this template. type: str - required: true schema: description: - The name of the schema. @@ -50,6 +52,7 @@ options: description: - Use C(present) or C(absent) for adding or removing. - Use C(query) for listing an object or multiple objects. + - Using C(present) on empty schemas M(cisco.mso.mso_schema) is supported on versions of MSO that are 4.2 or greater. type: str choices: [ absent, present, query ] default: present @@ -71,7 +74,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: present - delegate_to: localhost - name: Remove a template from a schema cisco.mso.mso_schema_template: @@ -82,7 +84,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: absent - delegate_to: localhost - name: Query a template cisco.mso.mso_schema_template: @@ -93,7 +94,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result - name: Query all templates @@ -104,7 +104,6 @@ EXAMPLES = r""" tenant: Tenant 1 schema: Schema 1 state: query - delegate_to: localhost register: query_result """ @@ -118,7 +117,7 @@ from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, ms def main(): argument_spec = mso_argument_spec() argument_spec.update( - tenant=dict(type="str", required=True), + tenant=dict(type="str"), schema=dict(type="str", required=True), schema_description=dict(type="str"), template_description=dict(type="str"), @@ -132,7 +131,7 @@ def main(): supports_check_mode=True, required_if=[ ["state", "absent", ["template"]], - ["state", "present", ["template"]], + ["state", "present", ["template", "tenant"]], ], ) @@ -157,13 +156,11 @@ def main(): schema_path = "schemas/{id}".format(**schema_obj) # Get template - templates = [t.get("name") for t in schema_obj.get("templates")] + templates = schema_obj.get("templates") if schema_obj.get("templates") is not None else [] if template: - if template in templates: - template_idx = templates.index(template) - mso.existing = schema_obj.get("templates")[template_idx] + mso.existing = next((item for item in templates if item.get("name") == template), {}) else: - mso.existing = schema_obj.get("templates") + mso.existing = templates else: schema_path = "schemas" 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 index 9922750f8..f37b95237 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_anp.py @@ -65,7 +65,6 @@ EXAMPLES = r""" template: Template 1 anp: ANP 1 state: present - delegate_to: localhost - name: Remove an ANP cisco.mso.mso_schema_template_anp: @@ -76,7 +75,6 @@ EXAMPLES = r""" template: Template 1 anp: ANP 1 state: absent - delegate_to: localhost - name: Query a specific ANPs cisco.mso.mso_schema_template_anp: @@ -86,7 +84,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result - name: Query all ANPs @@ -97,7 +94,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index 6c021e7f7..b5a26f31c 100644 --- 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 @@ -212,7 +212,6 @@ EXAMPLES = r""" vrf: name: vrf1 state: present - delegate_to: localhost - name: Add a new EPG with preferred group. cisco.mso.mso_schema_template_anp_epg: @@ -225,7 +224,6 @@ EXAMPLES = r""" epg: EPG 1 state: present preferred_group: true - delegate_to: localhost - name: Remove an EPG cisco.mso.mso_schema_template_anp_epg: @@ -241,7 +239,6 @@ EXAMPLES = r""" vrf: name: vrf1 state: absent - delegate_to: localhost - name: Query a specific EPG cisco.mso.mso_schema_template_anp_epg: @@ -257,7 +254,6 @@ EXAMPLES = r""" vrf: name: vrf1 state: query - delegate_to: localhost register: query_result - name: Query all EPGs @@ -274,7 +270,6 @@ EXAMPLES = r""" vrf: name: vrf1 state: query - delegate_to: localhost register: query_result """ 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 index d8c881d5d..3563e78f9 100644 --- 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 @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Copyright: (c) 2024, 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 @@ -18,15 +19,16 @@ description: - Manage EPG contracts in schema templates on Cisco ACI Multi-Site. author: - Dag Wieers (@dagwieers) +- Akini Ross (@akinross) options: schema: description: - - The name of the schema. + - The name of the Schema. type: str required: true template: description: - - The name of the template to change. + - The name of the Template. type: str required: true anp: @@ -36,31 +38,70 @@ options: required: true epg: description: - - The name of the EPG to manage. + - The name of the EPG. type: str required: true + force_replace: + description: + - Replaces all the configured contract(s) with the provided contract(s). + - This option can only be used in combination with the O(contracts) option. + - In combination with the O(state=absent) and without any contract configuration all configured static port(s) will be removed. + type: bool contract: description: - - A contract associated to this EPG. + - The Contract associated to this EPG. + - This option can not be used in combination with the I(contracts) option. type: dict suboptions: name: description: - - The name of the Contract to associate with. + - The name of the Contract. + required: true + type: str + schema: + description: + - The name of the Schema that defines the referenced Contract. + - If this parameter is unspecified, it defaults to the current schema. + type: str + template: + description: + - The name of the Template that defines the referenced Contract. + - If this parameter is unspecified, it defaults to the current schema. + type: str + type: + description: + - The type of the Contract. + type: str + required: true + choices: [ consumer, provider ] + contracts: + description: + - A list of Contracts associated to this EPG. + - This option can not be used in combination with the O(contract) option. + - All configured contract(s) will be replaced with the provided contract(s) when used with O(force_replace=true). + - Only the provided contract(s) will be added, updated or removed when used with O(force_replace=false). + - In combination with the O(state=query) all provided contract(s) must be found else the task will fail. + type: list + elements: dict + suboptions: + name: + description: + - The name of the Contract. required: true type: str schema: description: - - The schema that defines the referenced BD. + - The name of 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 BD. + - The name of the Template that defines the referenced Contract. + - If this parameter is unspecified, it defaults to the current schema. type: str type: description: - - The type of contract. + - The type of the Contract. type: str required: true choices: [ consumer, provider ] @@ -91,9 +132,8 @@ EXAMPLES = r""" name: Contract 1 type: consumer state: present - delegate_to: localhost -- name: Remove a Contract +- name: Add 2 contracts to an EPG cisco.mso.mso_schema_template_anp_epg_contract: host: mso_host username: admin @@ -102,10 +142,29 @@ EXAMPLES = r""" template: Template 1 anp: ANP 1 epg: EPG 1 - contract: - name: Contract 1 - state: absent - delegate_to: localhost + contracts: + - name: Contract 1 + type: provider + - name: Contract 1 + type: consumer + state: present + +- name: Replace all existing contracts on an EPG with 2 new 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 + epg: EPG 1 + force_replace: true + contracts: + - name: Contract 2 + type: provider + - name: Contract 2 + type: consumer + state: present - name: Query a specific Contract cisco.mso.mso_schema_template_anp_epg_contract: @@ -118,8 +177,25 @@ EXAMPLES = r""" epg: EPG 1 contract: name: Contract 1 + type: consumer + state: query + register: query_result + +- name: Query a list of 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 + epg: EPG 1 + contracts: + - name: Contract 2 + type: provider + - name: Contract 2 + type: consumer state: query - delegate_to: localhost register: query_result - name: Query all Contracts @@ -131,8 +207,48 @@ EXAMPLES = r""" template: Template 1 anp: ANP 1 state: query - delegate_to: localhost register: query_result + +- 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 + +- name: Remove 2 contracts 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 + contracts: + - name: Contract 1 + type: provider + - name: Contract 1 + type: consumer + state: absent + +- name: Remove all existing contracts from 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 + force_replace: true + state: absent """ RETURN = r""" @@ -140,6 +256,7 @@ 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 +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema def main(): @@ -149,7 +266,9 @@ def main(): template=dict(type="str", required=True), anp=dict(type="str", required=True), epg=dict(type="str", required=True), + force_replace=dict(type="bool"), contract=dict(type="dict", options=mso_contractref_spec()), + contracts=dict(type="list", elements="dict", options=mso_contractref_spec()), state=dict(type="str", default="present", choices=["absent", "present", "query"]), ) @@ -157,8 +276,12 @@ def main(): argument_spec=argument_spec, supports_check_mode=True, required_if=[ - ["state", "absent", ["contract"]], - ["state", "present", ["contract"]], + ["state", "absent", ["contract", "contracts", "force_replace"], True], + ["state", "present", ["contract", "contracts"], True], + ], + mutually_exclusive=[ + ["contract", "contracts"], + ["contract", "force_replace"], ], ) @@ -166,97 +289,160 @@ def main(): template = module.params.get("template").replace(" ", "") anp = module.params.get("anp") epg = module.params.get("epg") + force_replace = module.params.get("force_replace") contract = module.params.get("contract") - if contract is not None and contract.get("template") is not None: - contract["template"] = contract.get("template").replace(" ", "") + contracts = module.params.get("contracts") state = module.params.get("state") mso = MSOModule(module) + mso_schema = MSOSchema(mso, schema, template) + mso_schema.set_template(template) + mso_schema.set_template_anp(anp) + mso_schema.set_template_anp_epg(epg) + + # Schema dict is used as a cache store for schema id lookups + # This is done to limit the amount of schema id lookups when schema is not specified for multiple contracts + schema_cache = {mso_schema.schema_name: mso_schema.id} 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 + overwrite_contract_schema_and_template(mso, contract, schema, schema_cache, template) + elif contracts: + for contract_dict in contracts: + overwrite_contract_schema_and_template(mso, contract_dict, schema, schema_cache, template) + + contracts_path = "/templates/{0}/anps/{1}/epgs/{2}/contractRelationships".format(template, anp, epg) + contract_path = "{0}/-".format(contracts_path) + ops = [] + 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] + mso_schema.set_template_anp_epg_contract(contract_ref, contract.get("type"), False) + if mso_schema.schema_objects.get("template_anp_epg_contract") is not None: + mso.existing = get_contract_payload_from_schema(mso, mso_schema) + contract_path = "{0}/{1}".format(contracts_path, mso_schema.schema_objects["template_anp_epg_contract"].index) + else: + found_contracts = [] + set_existing_contracts(mso, mso_schema) + if contracts: + for contract_details in contracts: + contract_ref = mso.contract_ref(**contract_details) + mso_schema.set_template_anp_epg_contract(contract_ref, contract_details.get("type"), False) + if mso_schema.schema_objects.get("template_anp_epg_contract") is not None: + found_contracts.append(get_contract_payload_from_schema(mso, mso_schema)) if state == "query": - if not contract: - mso.existing = schema_obj.get("templates")[template_idx]["anps"][anp_idx]["epgs"][epg_idx]["contractRelationships"] + if contracts: + if len(found_contracts) == len(contracts): + mso.existing = found_contracts + else: + not_found_contracts = [ + "Contract with Reference '{0}' and type '{1}' not found".format(mso.contract_ref(**contract), contract.get("type")) + for contract in contracts + if contract not in found_contracts + ] + mso.fail_json(msg=not_found_contracts) 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.fail_json(msg="Contract with Reference '{0}' and type '{1}' not found".format(contract_ref, contract.get("type"))) 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 = {} + + if state == "absent" and mso.existing: + if contract: + mso.sent = mso.proposed = {} ops.append(dict(op="remove", path=contract_path)) + elif force_replace: + mso.sent = mso.proposed = [] + ops.append(dict(op="remove", path=contracts_path)) + else: + mso.proposed = mso.existing.copy() + remove_index = [] + for contract in contracts: + payload = get_contract_payload(contract) + if any(True if payload == found_contract else False for found_contract in found_contracts): + mso.proposed.remove(payload) + remove_index.append(mso.existing.index(payload)) + # The list index should not shift when removing contracts from the list + # By sorting the indexes found in reverse order, we assure that the highest index is removed first by the NDO backend + # This logic is to avoid removing the wrong contract + for index in reversed(sorted(remove_index)): + ops.append(dict(op="remove", path="{0}/{1}".format(contracts_path, index))) + mso.sent = mso.proposed elif state == "present": - payload = dict( - relationshipType=contract.get("type"), - contractRef=dict( - contractName=contract.get("name"), - templateName=contract.get("template"), - schemaId=contract.get("schema_id"), - ), + if contract: + mso.sanitize(get_contract_payload(contract), collate=True) + if not mso.existing: + ops.append(dict(op="add", path=contract_path, value=mso.sent)) + elif force_replace: + mso.sent = mso.proposed = [get_contract_payload(contract) for contract in contracts] + if mso.existing: + ops.append(dict(op="replace", path=contracts_path, value=mso.sent)) + else: + ops.append(dict(op="add", path=contracts_path, value=mso.sent)) + else: + mso.sent = [] + mso.proposed = mso.existing.copy() + for contract in contracts: + payload = get_contract_payload(contract) + if payload not in mso.existing: + mso.proposed.append(payload) + # Only add the operation list if the contract is not already present + # This is to avoid adding the same contract multiple times + # Replace operation is not required because there are no attributes that can be changed except the contract itself + if not force_replace and not any(True if payload == found_contract else False for found_contract in found_contracts): + ops.append(dict(op="add", path=contract_path, value=payload)) + mso.sent.append(payload) + + 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() + + +def set_existing_contracts(mso, mso_schema): + mso.existing = [] + for existing_contract in mso_schema.schema_objects["template_anp_epg"].details.get("contractRelationships"): + mso.existing.append( + dict( + relationshipType=existing_contract.get("relationshipType"), + contractRef=mso.dict_from_ref(existing_contract.get("contractRef")), + ) ) - 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)) +def get_contract_payload_from_schema(mso, mso_schema): + return dict( + relationshipType=mso_schema.schema_objects["template_anp_epg_contract"].details.get("relationshipType"), + contractRef=mso.dict_from_ref(mso_schema.schema_objects["template_anp_epg_contract"].details.get("contractRef")), + ) - 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) +def get_contract_payload(contract): + return dict( + relationshipType=contract.get("type"), + contractRef=dict( + contractName=contract.get("name"), + templateName=contract.get("template"), + schemaId=contract.get("schema_id"), + ), + ) - mso.exit_json() + +def overwrite_contract_schema_and_template(mso, contract, epg_schema_name, schema_cache, epg_template_name): + if contract.get("schema") is None: + contract["schema"] = epg_schema_name + contract["schema_id"] = schema_cache.get(epg_schema_name) + else: + schema_id = schema_cache.get(contract.get("schema")) + contract["schema_id"] = schema_id if schema_id else mso.lookup_schema(contract.get("schema")) + + if contract.get("template") is None: + contract["template"] = epg_template_name + else: + contract["template"] = contract.get("template").replace(" ", "") if __name__ == "__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 index bd98fc321..f479fe807 100644 --- 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 @@ -94,7 +94,6 @@ EXAMPLES = r""" operator: in value: test state: present - delegate_to: localhost - name: Remove a Selector cisco.mso.mso_schema_template_anp_epg_selector: @@ -107,7 +106,6 @@ EXAMPLES = r""" epg: EPG 1 selector: selector_1 state: absent - delegate_to: localhost - name: Query a specific Selector cisco.mso.mso_schema_template_anp_epg_selector: @@ -120,7 +118,6 @@ EXAMPLES = r""" epg: EPG 1 selector: selector_1 state: query - delegate_to: localhost register: query_result - name: Query all Selectors @@ -133,7 +130,6 @@ EXAMPLES = r""" anp: ANP 1 epg: EPG 1 state: query - delegate_to: localhost register: query_result """ 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 index b060ddd5e..2a88803b3 100644 --- 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 @@ -89,7 +89,6 @@ EXAMPLES = r""" 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: @@ -102,7 +101,6 @@ EXAMPLES = r""" 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: @@ -115,7 +113,6 @@ EXAMPLES = r""" epg: EPG 1 subnet: 10.0.0.0/24 state: query - delegate_to: localhost register: query_result - name: Query all EPGs subnets @@ -127,7 +124,6 @@ EXAMPLES = r""" template: Template 1 anp: ANP 1 state: query - delegate_to: localhost register: query_result """ 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 index 1f61e95de..0e9b8178e 100644 --- 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 @@ -69,7 +69,7 @@ options: 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. + - Use C(true) to set the default uSeg Subnet IP address 0.0.0.0. type: bool state: description: @@ -98,7 +98,6 @@ EXAMPLES = r""" 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: @@ -111,7 +110,6 @@ EXAMPLES = r""" epg: EPG 1 name: useg_attr_ip state: query - delegate_to: localhost register: query_result - name: Query all EPG uSeg attrs @@ -124,7 +122,6 @@ EXAMPLES = r""" anp: ANP 1 epg: EPG 1 state: query - delegate_to: localhost register: query_result - name: Remove a uSeg attr from an EPG with name @@ -138,7 +135,6 @@ EXAMPLES = r""" epg: EPG 1 name: useg_attr_ip state: absent - delegate_to: localhost """ RETURN = r""" @@ -233,11 +229,13 @@ def main(): 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 + if useg_subnet: + if value != "" and value != "0.0.0.0" and value is not None: + mso.fail_json(msg="The value of uSeg subnet IP should be an empty string or 0.0.0.0, when the useg_subnet is set to true.") + payload["fvSubnet"] = useg_subnet payload["value"] = "0.0.0.0" + else: + payload["fvSubnet"] = useg_subnet mso.sanitize(payload, collate=True) 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 index 0793e844d..ca2f9445d 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_bd.py @@ -250,7 +250,6 @@ EXAMPLES = r""" vrf: name: VRF1 state: present - delegate_to: localhost - name: Add a new BD from another Schema mso_schema_template_bd: @@ -265,7 +264,6 @@ EXAMPLES = r""" 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: @@ -357,7 +355,6 @@ EXAMPLES = r""" name: ansible_test_option version: 1 state: present - delegate_to: localhost - name: Remove a BD cisco.mso.mso_schema_template_bd: @@ -368,7 +365,6 @@ EXAMPLES = r""" template: Template 1 bd: BD1 state: absent - delegate_to: localhost - name: Query a specific BD cisco.mso.mso_schema_template_bd: @@ -379,7 +375,6 @@ EXAMPLES = r""" template: Template 1 bd: BD1 state: query - delegate_to: localhost register: query_result - name: Query all BDs @@ -390,7 +385,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index 64fe360ea..095e5b668 100644 --- 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 @@ -85,7 +85,6 @@ EXAMPLES = r""" 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: @@ -98,7 +97,6 @@ EXAMPLES = r""" 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: @@ -110,7 +108,6 @@ EXAMPLES = r""" bd: BD 1 name: ansible_test state: query - delegate_to: localhost register: query_result - name: Query all BD DHCP Policies @@ -122,7 +119,6 @@ EXAMPLES = r""" template: Template 1 bd: BD 1 state: query - delegate_to: localhost register: query_result """ 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 index cc55eea7b..e4ae628e4 100644 --- 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 @@ -98,7 +98,6 @@ EXAMPLES = r""" 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: @@ -110,7 +109,6 @@ EXAMPLES = r""" 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: @@ -122,7 +120,6 @@ EXAMPLES = r""" bd: BD 1 subnet: 10.0.0.0/24 state: query - delegate_to: localhost register: query_result - name: Query all BD subnets @@ -134,7 +131,6 @@ EXAMPLES = r""" template: Template 1 bd: BD 1 state: query - delegate_to: localhost register: query_result """ 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 index 0cd41779f..dd1d1e067 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_clone.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_clone.py @@ -69,7 +69,6 @@ EXAMPLES = r""" 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: @@ -83,7 +82,6 @@ EXAMPLES = r""" 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: @@ -96,7 +94,6 @@ EXAMPLES = r""" source_template_name: Template1_clone destination_template_name: Template1_clone_2 state: clone - delegate_to: localhost """ RETURN = r""" 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 index 11bf08731..cb2ac1b0d 100644 --- 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 @@ -128,7 +128,6 @@ EXAMPLES = r""" contract_scope: global filter: Filter 1 state: present - delegate_to: localhost - name: Remove a contract filter cisco.mso.mso_schema_template_contract_filter: @@ -140,7 +139,6 @@ EXAMPLES = r""" contract: Contract 1 filter: Filter 1 state: absent - delegate_to: localhost - name: Query a specific contract filter cisco.mso.mso_schema_template_contract_filter: @@ -152,7 +150,6 @@ EXAMPLES = r""" contract: Contract 1 filter: Filter 1 state: query - delegate_to: localhost register: query_result - name: Query all contract filters @@ -164,7 +161,6 @@ EXAMPLES = r""" template: Template 1 contract: Contract 1 state: query - delegate_to: localhost register: query_result """ 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 index 0e398843b..deaa82e18 100644 --- 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 @@ -113,7 +113,6 @@ EXAMPLES = r""" consumer: b2 filter: Filter 1 state: present - delegate_to: localhost - name: Remove a contract service graph cisco.mso.mso_schema_template_contract_service_graph: @@ -125,7 +124,6 @@ EXAMPLES = r""" 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: @@ -136,7 +134,6 @@ EXAMPLES = r""" template: Template 1 contract: Contract 1 state: query - delegate_to: localhost register: query_result """ 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 index 49df465c5..d3714d184 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_deploy.py @@ -60,7 +60,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: deploy - delegate_to: localhost - name: Undeploy a schema template cisco.mso.mso_schema_template_deploy: @@ -71,7 +70,6 @@ EXAMPLES = r""" template: Template 1 site: Site 1 state: undeploy - delegate_to: localhost - name: Get deployment status cisco.mso.mso_schema: @@ -81,7 +79,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: status - delegate_to: localhost register: status_result """ 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 index 707ad7320..768694905 100644 --- 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 @@ -51,7 +51,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result - name: Query status of objects using site @@ -62,7 +61,6 @@ EXAMPLES = r""" 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 @@ -74,7 +72,6 @@ EXAMPLES = r""" template: Template 1 site: ansible_test state: query - delegate_to: localhost register: query_result - name: Query status of objects in all templates @@ -84,7 +81,6 @@ EXAMPLES = r""" password: SomeSecretPassword schema: Schema 1 state: query - delegate_to: localhost register: query_result """ 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 index ce201913f..3da65c7f5 100644 --- 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 @@ -138,7 +138,6 @@ EXAMPLES = r""" 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: @@ -158,7 +157,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: present - delegate_to: localhost - name: Remove an external EPG cisco.mso.mso_schema_template_external_epg: @@ -169,7 +167,6 @@ EXAMPLES = r""" 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: @@ -180,7 +177,6 @@ EXAMPLES = r""" template: Template 1 external_epg: external EPG1 state: query - delegate_to: localhost register: query_result - name: Query all external EPGs @@ -191,7 +187,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index 4029175bc..3b13c8020 100644 --- 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 @@ -85,7 +85,6 @@ EXAMPLES = r""" name: Contract 1 type: consumer state: present - delegate_to: localhost - name: Remove a Contract cisco.mso.mso_schema_template_external_epg_contract: @@ -98,7 +97,6 @@ EXAMPLES = r""" contract: name: Contract 1 state: absent - delegate_to: localhost - name: Query a specific Contract cisco.mso.mso_schema_template_external_epg_contract: @@ -111,7 +109,6 @@ EXAMPLES = r""" contract: name: Contract 1 state: query - delegate_to: localhost register: query_result - name: Query all Contracts @@ -122,7 +119,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index 7d4b93b27..679c4ee27 100644 --- 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 @@ -90,7 +90,6 @@ EXAMPLES = r""" operator: equals value: 10.0.0.0 state: present - delegate_to: localhost - name: Remove a Selector cisco.mso.mso_schema_template_external_epg_selector: @@ -102,7 +101,6 @@ EXAMPLES = r""" 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: @@ -114,7 +112,6 @@ EXAMPLES = r""" external_epg: extEPG 1 selector: selector_1 state: query - delegate_to: localhost register: query_result - name: Query all Selectors @@ -126,7 +123,6 @@ EXAMPLES = r""" template: Template 1 external_epg: extEPG 1 state: query - delegate_to: localhost register: query_result """ 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 index c7512c2ee..7339312a0 100644 --- 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 @@ -50,6 +50,7 @@ options: - The C(shared-security) for Shared Security Import can only be used when External Subnets for External EPG is present type: list elements: str + default: [] aggregate: description: - The aggregate option aggregates shared routes for the subnet. @@ -57,6 +58,7 @@ options: - 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 + default: [] state: description: - Use C(present) or C(absent) for adding or removing. @@ -80,7 +82,6 @@ EXAMPLES = r""" 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: @@ -92,7 +93,6 @@ EXAMPLES = r""" 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: @@ -104,7 +104,6 @@ EXAMPLES = r""" external_epg: EPG 1 subnet: 10.0.0.0/24 state: query - delegate_to: localhost register: query_result - name: Query all External EPGs subnets @@ -115,7 +114,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index ce201913f..3da65c7f5 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_externalepg.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_externalepg.py @@ -138,7 +138,6 @@ EXAMPLES = r""" 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: @@ -158,7 +157,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: present - delegate_to: localhost - name: Remove an external EPG cisco.mso.mso_schema_template_external_epg: @@ -169,7 +167,6 @@ EXAMPLES = r""" 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: @@ -180,7 +177,6 @@ EXAMPLES = r""" template: Template 1 external_epg: external EPG1 state: query - delegate_to: localhost register: query_result - name: Query all external EPGs @@ -191,7 +187,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index c0ab485a4..5bf69bde1 100644 --- 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 @@ -132,7 +132,6 @@ EXAMPLES = r""" template: Template 1 filter: Filter 1 state: present - delegate_to: localhost - name: Remove a filter entry cisco.mso.mso_schema_template_filter_entry: @@ -143,7 +142,6 @@ EXAMPLES = r""" template: Template 1 filter: Filter 1 state: absent - delegate_to: localhost - name: Query a specific filter entry cisco.mso.mso_schema_template_filter_entry: @@ -154,7 +152,6 @@ EXAMPLES = r""" template: Template 1 filter: Filter 1 state: query - delegate_to: localhost register: query_result - name: Query all filter entries @@ -165,7 +162,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index 4b8c1b66d..eb7857df8 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_l3out.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_l3out.py @@ -87,7 +87,6 @@ EXAMPLES = r""" schema: vrfSchema template: vrfTemplate state: present - delegate_to: localhost - name: Remove an L3out cisco.mso.mso_schema_template_l3out: @@ -98,7 +97,6 @@ EXAMPLES = r""" template: Template 1 l3out: L3out 1 state: absent - delegate_to: localhost - name: Query a specific L3outs cisco.mso.mso_schema_template_l3out: @@ -110,7 +108,6 @@ EXAMPLES = r""" template: Template 1 l3out: L3out 1 state: query - delegate_to: localhost register: query_result - name: Query all L3outs @@ -122,7 +119,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index d0e15b8d0..35fa0d205 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_migrate.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_migrate.py @@ -84,7 +84,6 @@ EXAMPLES = r""" - epg: EPG1 anp: ANP state: present - delegate_to: localhost - name: Migration of objects between templates of different schema mso_schema_template_migrate: @@ -101,7 +100,6 @@ EXAMPLES = r""" - epg: EPG1 anp: ANP state: present - delegate_to: localhost - name: Migration of BD object between templates of same schema mso_schema_template_migrate: @@ -116,7 +114,6 @@ EXAMPLES = r""" - BD - BD1 state: present - delegate_to: localhost - name: Migration of BD object between templates of different schema mso_schema_template_migrate: @@ -131,7 +128,6 @@ EXAMPLES = r""" - BD - BD1 state: present - delegate_to: localhost - name: Migration of EPG objects between templates of same schema mso_schema_template_migrate: @@ -148,7 +144,6 @@ EXAMPLES = r""" - epg: EPG2 anp: ANP2 state: present - delegate_to: localhost - name: Migration of EPG objects between templates of different schema mso_schema_template_migrate: @@ -165,7 +160,6 @@ EXAMPLES = r""" - epg: EPG2 anp: ANP2 state: present - delegate_to: localhost """ RETURN = r""" 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 index 70fadd804..d304673dc 100644 --- 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 @@ -83,7 +83,6 @@ EXAMPLES = r""" - type: other - type: load-balancer state: present - delegate_to: localhost - name: Remove a Service Graph cisco.mso.mso_schema_template_service_graph: @@ -94,7 +93,6 @@ EXAMPLES = r""" template: Template1 service_graph: graph1 state: absent - delegate_to: localhost - name: Query a specific Service Graph cisco.mso.mso_schema_template_service_graph: @@ -105,7 +103,7 @@ EXAMPLES = r""" template: Template1 service_graph: graph1 state: query - delegate_to: localhost + register: query_result - name: Query all Service Graphs cisco.mso.mso_schema_template_service_graph: @@ -115,7 +113,7 @@ EXAMPLES = r""" schema: Schema1 template: Template1 state: query - delegate_to: localhost + register: query_result """ RETURN = r""" @@ -215,14 +213,15 @@ def main(): node_name = node.get("type") if node_name in service_node_types: service_node_index = service_node_index + 1 + service_node_name = "node{0}".format(service_node_index) for node_data in query_node_data: if node_data["name"] == node_name: payload = dict( - name=node_name, + name=service_node_name, serviceNodeTypeId=node_data.get("id"), index=service_node_index, serviceNodeRef=dict( - serviceNodeName=node_name, + serviceNodeName=service_node_name, serviceGraphName=service_graph, templateName=template, schemaId=schema_id, 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 index efafd2387..84464c312 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_template_vrf.py @@ -79,7 +79,6 @@ EXAMPLES = r""" template: Template 1 vrf: VRF 1 state: present - delegate_to: localhost - name: Remove an VRF cisco.mso.mso_schema_template_vrf: @@ -90,7 +89,6 @@ EXAMPLES = r""" template: Template 1 vrf: VRF1 state: absent - delegate_to: localhost - name: Query a specific VRFs cisco.mso.mso_schema_template_vrf: @@ -101,7 +99,6 @@ EXAMPLES = r""" template: Template 1 vrf: VRF1 state: query - delegate_to: localhost register: query_result - name: Query all VRFs @@ -112,7 +109,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost register: query_result """ 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 index eaef8235c..fbfb96352 100644 --- 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 @@ -84,7 +84,6 @@ EXAMPLES = r""" name: Contract 1 type: consumer state: present - delegate_to: localhost - name: Remove a Contract cisco.mso.mso_schema_template_vrf_contract: @@ -98,7 +97,6 @@ EXAMPLES = r""" name: Contract 1 type: consumer state: absent - delegate_to: localhost - name: Query a specific Contract cisco.mso.mso_schema_template_vrf_contract: @@ -112,7 +110,6 @@ EXAMPLES = r""" name: Contract 1 type: consumer state: query - delegate_to: localhost register: query_result - name: Query all Contracts @@ -124,7 +121,6 @@ EXAMPLES = r""" template: Template 1 vrf: VRF 1 state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py b/ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py index a4a4f6cd0..35b082618 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_schema_validate.py @@ -35,11 +35,19 @@ options: default: query choices: [ query ] seealso: -- module: cisco.mso.mso_schema_template_external_epg +- module: cisco.mso.mso_schema extends_documentation_fragment: cisco.mso.modules """ EXAMPLES = r""" + - name: Get Validation status + mso_schema_validate: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + state: query + register: query_validate """ RETURN = r""" 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 index b8319256b..eace7bd26 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_service_node_type.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_service_node_type.py @@ -46,7 +46,6 @@ EXAMPLES = r""" name: ips display_name: ips state: present - delegate_to: localhost - name: Remove a Service Node Type cisco.mso.mso_schema_service_node: @@ -55,7 +54,6 @@ EXAMPLES = r""" password: SomeSecretPassword name: ips state: absent - delegate_to: localhost - name: Query a specific Service Node Type cisco.mso.mso_schema_service_node: @@ -64,7 +62,7 @@ EXAMPLES = r""" password: SomeSecretPassword name: ips state: query - delegate_to: localhost + register: query_result - name: Query all Service Node Types cisco.mso.mso_schema_service_node: @@ -72,7 +70,7 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost + register: query_result """ RETURN = r""" diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_site.py b/ansible_collections/cisco/mso/plugins/modules/mso_site.py index a3778d28a..d12f935bc 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_site.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_site.py @@ -110,7 +110,6 @@ EXAMPLES = r""" latitude: 50.887318 longitude: 4.447084 state: present - delegate_to: localhost - name: Remove a site cisco.mso.mso_site: @@ -119,7 +118,6 @@ EXAMPLES = r""" password: SomeSecretPassword site: north_europe state: absent - delegate_to: localhost - name: Query a site cisco.mso.mso_site: @@ -128,7 +126,6 @@ EXAMPLES = r""" password: SomeSecretPassword site: north_europe state: query - delegate_to: localhost register: query_result - name: Query all sites @@ -137,7 +134,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_tenant.py b/ansible_collections/cisco/mso/plugins/modules/mso_tenant.py index 17aa457e3..b1caf3cbe 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_tenant.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_tenant.py @@ -3,6 +3,7 @@ # Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> # Copyright: (c) 2020, Cindy Zhao (@cizhao) <cizhao@cisco.com> +# Copyright: (c) 2023, Anvitha Jain (@anvjain) <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 @@ -40,6 +41,22 @@ options: - Admin user is always added to the associated user list irrespective of this parameter being used. type: list elements: str + remote_users: + description: + - A list of associated remote users for this tenant. + type: list + elements: dict + suboptions: + name: + description: + - The name of the associated remote user for this tenant. + required: true + type: str + login_domain: + description: + - Domain name of the associated remote user for this tenant. + required: true + type: str sites: description: - A list of associated sites for this tenant. @@ -73,7 +90,6 @@ EXAMPLES = r""" 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: @@ -83,7 +99,6 @@ EXAMPLES = r""" tenant: north_europe orchestrator_only: no state: absent - delegate_to: localhost - name: Remove a tenant from MSO only cisco.mso.mso_tenant: @@ -93,7 +108,6 @@ EXAMPLES = r""" tenant: north_europe orchestrator_only: yes state: absent - delegate_to: localhost - name: Query a tenant cisco.mso.mso_tenant: @@ -102,7 +116,6 @@ EXAMPLES = r""" password: SomeSecretPassword tenant: north_europe state: query - delegate_to: localhost register: query_result - name: Query all tenants @@ -111,7 +124,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: query_result """ @@ -119,7 +131,7 @@ 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.mso import MSOModule, mso_argument_spec, ndo_remote_user_spec from ansible_collections.cisco.mso.plugins.module_utils.constants import YES_OR_NO_TO_BOOL_STRING_MAP @@ -130,6 +142,7 @@ def main(): display_name=dict(type="str"), tenant=dict(type="str", aliases=["name"]), users=dict(type="list", elements="str"), + remote_users=dict(type="list", elements="dict", options=ndo_remote_user_spec()), 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"]), @@ -149,13 +162,10 @@ def main(): tenant = module.params.get("tenant") orchestrator_only = module.params.get("orchestrator_only") state = module.params.get("state") + remote_users = module.params.get("remote_users") 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" @@ -182,6 +192,12 @@ def main(): mso.existing = mso.request(path, method="DELETE") elif state == "present": + # Convert sites and users + sites = mso.lookup_sites(module.params.get("sites")) + users = mso.lookup_users(module.params.get("users")) + if remote_users is not None: + users += mso.lookup_remote_users(remote_users) + mso.previous = mso.existing payload = dict( 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 4b9c2af56..895403e12 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_tenant_site.py @@ -114,7 +114,6 @@ EXAMPLES = r""" 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: @@ -126,7 +125,6 @@ EXAMPLES = r""" 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: @@ -141,7 +139,6 @@ EXAMPLES = r""" secret_key: '0' aws_account_org: false state: present - delegate_to: localhost - name: Associate Azure site in managed mode mso.cisco.mso_tenant_site: @@ -155,7 +152,6 @@ EXAMPLES = r""" 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: @@ -173,7 +169,6 @@ EXAMPLES = r""" azure_active_directory_id: '32' azure_active_directory_name: CiscoINSBUAd state: present - delegate_to: localhost - name: Dissociate a site cisco.mso.mso_tenant_site: @@ -183,7 +178,6 @@ EXAMPLES = r""" tenant: tenant_name site: site_name state: absent - delegate_to: localhost - name: Query a site cisco.mso.mso_tenant_site: @@ -193,7 +187,7 @@ EXAMPLES = r""" tenant: tenant_name site: site_name state: query - delegate_to: localhost + register: query_result - name: Query all sites of a tenant cisco.mso.mso_tenant_site: @@ -202,7 +196,6 @@ EXAMPLES = r""" password: SomeSecretPassword tenant: tenant_name state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_user.py b/ansible_collections/cisco/mso/plugins/modules/mso_user.py index 37127a7f1..d74088206 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_user.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_user.py @@ -87,7 +87,6 @@ EXAMPLES = r""" user: admin user_password: newPassword state: present - delegate_to: localhost - name: Add a new user cisco.mso.mso_user: @@ -107,7 +106,6 @@ EXAMPLES = r""" - name: schemaManager access_type: read state: present - delegate_to: localhost - name: Add a new user cisco.mso.mso_user: @@ -122,7 +120,6 @@ EXAMPLES = r""" phone: +32 478 436 299 roles: - powerUser - delegate_to: localhost - name: Remove a user cisco.mso.mso_user: @@ -132,7 +129,6 @@ EXAMPLES = r""" validate_certs: false user: dag state: absent - delegate_to: localhost - name: Query a user cisco.mso.mso_user: @@ -142,7 +138,6 @@ EXAMPLES = r""" validate_certs: false user: dag state: query - delegate_to: localhost register: query_result - name: Query all users @@ -152,7 +147,6 @@ EXAMPLES = r""" password: SomeSecretPassword validate_certs: false state: query - delegate_to: localhost register: query_result """ diff --git a/ansible_collections/cisco/mso/plugins/modules/mso_version.py b/ansible_collections/cisco/mso/plugins/modules/mso_version.py index 19668afb4..f2c0ce8a1 100644 --- a/ansible_collections/cisco/mso/plugins/modules/mso_version.py +++ b/ansible_collections/cisco/mso/plugins/modules/mso_version.py @@ -35,7 +35,6 @@ EXAMPLES = r""" username: admin password: SomeSecretPassword state: query - delegate_to: localhost register: query_result """ 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 index b8bdb63ae..f1971280e 100644 --- a/ansible_collections/cisco/mso/plugins/modules/ndo_schema_template_deploy.py +++ b/ansible_collections/cisco/mso/plugins/modules/ndo_schema_template_deploy.py @@ -61,7 +61,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: deploy - delegate_to: localhost - name: Redeploy a schema template cisco.mso.ndo_schema_template_deploy: @@ -71,7 +70,6 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: redeploy - delegate_to: localhost - name: Undeploy a schema template cisco.mso.ndo_schema_template_deploy: @@ -82,7 +80,6 @@ EXAMPLES = r""" template: Template 1 sites: [ Site1, Site2 ] state: undeploy - delegate_to: localhost - name: Query a schema template deploy status cisco.mso.ndo_schema_template_deploy: @@ -92,7 +89,7 @@ EXAMPLES = r""" schema: Schema 1 template: Template 1 state: query - delegate_to: localhost + register: query_result """ RETURN = r""" |