diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index 08ed31bb..8e7f0abc 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -129,6 +129,9 @@ jobs: - name: Install the collection tarball run: ansible-galaxy collection install .cache/collection-tarballs/*.tar.gz + - name: Install the ND collection (NDO dependency) + run: ansible-galaxy collection install cisco.nd + - name: Run sanity tests run: ansible-test sanity --docker -v --color --truncate 0 --coverage working-directory: /home/runner/.ansible/collections/ansible_collections/cisco/mso diff --git a/plugins/module_utils/mso.py b/plugins/module_utils/mso.py index 4bc9053e..cc448020 100644 --- a/plugins/module_utils/mso.py +++ b/plugins/module_utils/mso.py @@ -23,6 +23,7 @@ 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.nd.plugins.module_utils.nd import NDModule try: from requests_toolbelt.multipart.encoder import MultipartEncoder @@ -50,7 +51,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(): @@ -239,6 +240,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 @@ -916,26 +924,65 @@ def lookup_users(self, users, ignore_not_found_error=False): 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 +1159,7 @@ def make_dhcp_label(self, data): """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: diff --git a/plugins/modules/mso_tenant.py b/plugins/modules/mso_tenant.py index 17aa457e..4cbc58ae 100644 --- a/plugins/modules/mso_tenant.py +++ b/plugins/modules/mso_tenant.py @@ -3,6 +3,7 @@ # Copyright: (c) 2018, Dag Wieers (@dagwieers) # Copyright: (c) 2020, Cindy Zhao (@cizhao) +# Copyright: (c) 2023, Anvitha Jain (@anvjain) # 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 @@ - 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. @@ -119,7 +136,7 @@ """ 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 +147,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 +167,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 +197,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/tests/integration/targets/mso_tenant/tasks/main.yml b/tests/integration/targets/mso_tenant/tasks/main.yml index e5cc5a0e..5eeb223d 100644 --- a/tests/integration/targets/mso_tenant/tasks/main.yml +++ b/tests/integration/targets/mso_tenant/tasks/main.yml @@ -211,6 +211,41 @@ - nm_add_tenant2_again.current.userAssociations | length == 1 when: mso_username == 'admin' +# ADD TENANT WITH REMOTE USERS +- name: Add tenant 3 (check_mode) + mso_tenant: + <<: *tenant_present + tenant: ansible_test3 + display_name: null + remote_users: + - name: ansible_github_ci + login_domain: test + state: present + check_mode: true + register: cm_add_rmt_usr_tenant3 + +- name: Verify cm_add_rmt_usr_tenant3 + assert: + that: + - cm_add_rmt_usr_tenant3 is changed + +- name: Add tenant 3 (normal_mode) + mso_tenant: + <<: *tenant_present + tenant: ansible_test3 + display_name: null + remote_users: + - name: ansible_github_ci + login_domain: test + state: present + register: nm_add_rmt_usr_tenant3 + +- name: Verify cm_add_rmt_usr_tenant3 + assert: + that: + - nm_add_rmt_usr_tenant3 is changed + - nm_add_rmt_usr_tenant3.current.name == 'ansible_test3' + - name: Add tenant 3 with duplicate admin user (normal mode) mso_tenant: <<: *tenant_present @@ -226,7 +261,7 @@ - name: Verify nm_add_tenant3_with_duplicate_admin assert: that: - - nm_add_tenant3_with_duplicate_admin is not changed + - nm_add_tenant3_with_duplicate_admin is changed - nm_add_tenant3_with_duplicate_admin.msg == "User 'admin' is duplicate." - name: Add tenant 3 with invalid user (normal mode) @@ -252,6 +287,9 @@ tenant: ansible_test3 users: - '{{ mso_username }}' + remote_users: + - name: ansible_github_ci + login_domain: test display_name: null state: present register: nm_add_tenant3 @@ -264,7 +302,7 @@ - name: Verify nm_add_tenant3 (when mso_username != admin) assert: that: - - nm_add_tenant3.current.userAssociations | length == 2 + - nm_add_tenant3.current.userAssociations | length == 3 when: mso_username != 'admin' - name: Verify nm_add_tenant3 (when mso_username == admin)