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
This commit is contained in:
parent
f988c23f82
commit
1d82d25ea2
3 changed files with 322 additions and 0 deletions
126
lib/ansible/plugins/lookup/avi.py
Normal file
126
lib/ansible/plugins/lookup/avi.py
Normal file
|
@ -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 <sandeepb@avinetworks.com>
|
||||||
|
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 /<obj_type>/<obj_uuid> and /<obj_type>
|
||||||
|
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
|
104
test/units/plugins/lookup/fixtures/avi.json
Normal file
104
test/units/plugins/lookup/fixtures/avi.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
92
test/units/plugins/lookup/test_avi.py
Normal file
92
test/units/plugins/lookup/test_avi.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# (c) 2019, Sandeep Bandi <sandeepb@avinetworks.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# 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)
|
Loading…
Reference in a new issue