From 1d82d25ea299f1c6f7a1343ff92c3ff6b332c156 Mon Sep 17 00:00:00 2001 From: Sandeep Bandi Date: Wed, 24 Jul 2019 21:05:04 +0530 Subject: [PATCH] Adding Avi ansible lookup module (#58667) * Adding Avi ansible lookup module (cherry picked from commit 77b8951f68cbc889e6595b2a359ca27b84a43c0d) * Added description for examples * Added debug logs and unit tests * Fix __builtin__ import and restting super * Fix pep8 errors * Updated as per review comments on IP address --- lib/ansible/plugins/lookup/avi.py | 126 ++++++++++++++++++++ test/units/plugins/lookup/fixtures/avi.json | 104 ++++++++++++++++ test/units/plugins/lookup/test_avi.py | 92 ++++++++++++++ 3 files changed, 322 insertions(+) create mode 100644 lib/ansible/plugins/lookup/avi.py create mode 100644 test/units/plugins/lookup/fixtures/avi.json create mode 100644 test/units/plugins/lookup/test_avi.py diff --git a/lib/ansible/plugins/lookup/avi.py b/lib/ansible/plugins/lookup/avi.py new file mode 100644 index 00000000000..1c2b087cffd --- /dev/null +++ b/lib/ansible/plugins/lookup/avi.py @@ -0,0 +1,126 @@ +# python 3 headers, required if submitting to Ansible +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ +lookup: avi +author: Sandeep Bandi +version_added: 2.9 +short_description: Look up ``Avi`` objects. +description: + - Given an object_type, fetch all the objects of that type or fetch + the specific object that matches the name/uuid given via options. + - For single object lookup. If you want the output to be a list, you may + want to pass option wantlist=True to the plugin. + +options: + obj_type: + description: + - type of object to query + required: True + obj_name: + description: + - name of the object to query + obj_uuid: + description: + - UUID of the object to query +extends_documentation_fragment: avi +""" + +EXAMPLES = """ +# Lookup query for all the objects of a specific type. +- debug: msg="{{ lookup('avi', avi_credentials=avi_credentials, obj_type='virtualservice') }}" +# Lookup query for an object with the given name and type. +- debug: msg="{{ lookup('avi', avi_credentials=avi_credentials, obj_name='vs1', obj_type='virtualservice', wantlist=True) }}" +# Lookup query for an object with the given UUID and type. +- debug: msg="{{ lookup('avi', obj_uuid='virtualservice-5c0e183a-690a-45d8-8d6f-88c30a52550d', obj_type='virtualservice') }}" +# We can replace lookup with query function to always the get the output as list. +# This is helpful for looping. +- debug: msg="{{ query('avi', obj_uuid='virtualservice-5c0e183a-690a-45d8-8d6f-88c30a52550d', obj_type='virtualservice') }}" +""" + +RETURN = """ + _raw: + description: + - One ore more objects returned from ``Avi`` API. + type: list + elements: dictionary +""" + +from ansible.module_utils._text import to_native +from ansible.errors import AnsibleError, AnsibleParserError +from ansible.plugins.lookup import LookupBase +from ansible.utils.display import Display +from ansible.module_utils.network.avi.avi_api import (ApiSession, + AviCredentials, + AviServerError, + ObjectNotFound, + APIError) + +display = Display() + + +def _api(avi_session, path, **kwargs): + ''' + Generic function to handle both // and / + API resource endpoints. + ''' + rsp = [] + try: + rsp_data = avi_session.get(path, **kwargs).json() + if 'results' in rsp_data: + rsp = rsp_data['results'] + else: + rsp.append(rsp_data) + except ObjectNotFound as e: + display.warning('Resource not found. Please check obj_name/' + 'obj_uuid/obj_type are spelled correctly.') + display.v(to_native(e)) + except (AviServerError, APIError) as e: + raise AnsibleError(to_native(e)) + except Exception as e: + # Generic excption handling for connection failures + raise AnsibleError('Unable to communicate with controller' + 'due to error: %s' % to_native(e)) + + return rsp + + +class LookupModule(LookupBase): + def run(self, terms, variables=None, avi_credentials=None, **kwargs): + + api_creds = AviCredentials(**avi_credentials) + # Create the session using avi_credentials + try: + avi = ApiSession(avi_credentials=api_creds) + except Exception as e: + raise AnsibleError(to_native(e)) + + # Return an empty list if the object is not found + rsp = [] + try: + path = kwargs.pop('obj_type') + except KeyError: + raise AnsibleError("Please pass the obj_type for lookup") + + if kwargs.get('obj_name', None): + name = kwargs.pop('obj_name') + try: + display.v("Fetching obj: %s of type: %s" % (name, path)) + rsp_data = avi.get_object_by_name(path, name, **kwargs) + if rsp_data: + # Append the return data only if it is not None. i.e object + # with specified name is present + rsp.append(rsp_data) + except AviServerError as e: + raise AnsibleError(to_native(e)) + elif kwargs.get('obj_uuid', None): + obj_uuid = kwargs.pop('obj_uuid') + obj_path = "%s/%s" % (path, obj_uuid) + display.v("Fetching obj: %s of type: %s" % (obj_uuid, path)) + rsp = _api(avi, obj_path, **kwargs) + else: + display.v("Fetching all objects of type: %s" % path) + rsp = _api(avi, path, **kwargs) + + return rsp diff --git a/test/units/plugins/lookup/fixtures/avi.json b/test/units/plugins/lookup/fixtures/avi.json new file mode 100644 index 00000000000..ae89ca689c3 --- /dev/null +++ b/test/units/plugins/lookup/fixtures/avi.json @@ -0,0 +1,104 @@ +{ + "mock_single_obj": { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "PG-123", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + "mock_multiple_obj": { + "results": [ + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0682", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0231", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0535", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0094", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0437", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0673", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + } + ] + } +} diff --git a/test/units/plugins/lookup/test_avi.py b/test/units/plugins/lookup/test_avi.py new file mode 100644 index 00000000000..4619cba7350 --- /dev/null +++ b/test/units/plugins/lookup/test_avi.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# (c) 2019, Sandeep Bandi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import pytest +import json + +from units.compat.mock import patch, MagicMock + +from ansible.errors import AnsibleError +from ansible.plugins.loader import lookup_loader +from ansible.plugins.lookup import avi + + +try: + import builtins as __builtin__ +except ImportError: + import __builtin__ + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') + +with open(fixture_path + '/avi.json') as json_file: + data = json.load(json_file) + + +@pytest.fixture +def dummy_credentials(): + dummy_credentials = {} + dummy_credentials['controller'] = "192.0.2.13" + dummy_credentials['username'] = "admin" + dummy_credentials['password'] = "password" + dummy_credentials['api_version'] = "17.2.14" + dummy_credentials['tenant'] = 'admin' + return dummy_credentials + + +@pytest.fixture +def super_switcher(scope="function", autouse=True): + # Mocking the inbuilt super as it is used in ApiSession initialization + original_super = __builtin__.super + __builtin__.super = MagicMock() + yield + # Revert the super to default state + __builtin__.super = original_super + + +def test_lookup_multiple_obj(dummy_credentials): + avi_lookup = lookup_loader.get('avi') + avi_mock = MagicMock() + avi_mock.return_value.get.return_value.json.return_value = data["mock_multiple_obj"] + with patch.object(avi, 'ApiSession', avi_mock): + retval = avi_lookup.run([], {}, avi_credentials=dummy_credentials, + obj_type="network") + assert retval == data["mock_multiple_obj"]["results"] + + +def test_lookup_single_obj(dummy_credentials): + avi_lookup = lookup_loader.get('avi') + avi_mock = MagicMock() + avi_mock.return_value.get_object_by_name.return_value = data["mock_single_obj"] + with patch.object(avi, 'ApiSession', avi_mock): + retval = avi_lookup.run([], {}, avi_credentials=dummy_credentials, + obj_type="network", obj_name='PG-123') + assert retval[0] == data["mock_single_obj"] + + +def test_invalid_lookup(dummy_credentials): + avi_lookup = lookup_loader.get('avi') + avi_mock = MagicMock() + with pytest.raises(AnsibleError): + with patch.object(avi, 'ApiSession', avi_mock): + avi_lookup.run([], {}, avi_credentials=dummy_credentials)