From 798972c72a21554b094210abf5001f0f1e7fbd58 Mon Sep 17 00:00:00 2001 From: Gaurav Rastogi Date: Wed, 8 Feb 2017 06:47:18 -0800 Subject: [PATCH] Avi Networks Ansible modules. (#20415) * Avi Networks Ansible modules. Avi Version: 16.3.4 * Fixed Review comments 1. Changed description to be full sentences 2. Fixed Pep8 warnings. 3. Fixed comments and descriptions. * 1. Fixed descriptions and messages as per review comments. 2. Added descriptions for the missing parameters. * Fixed the shippable break due to the incorrect description format * Removed the extra modules so that there is a single module for the first commit * Updated license to BSD based on review comments * updated comments based on review feedback * Refactored code to handle POST and PUT scenarios where playbook does not need to check whether object is present. Moved ansible helper utilities to module_utils as now roles can be patched with module_utils as well. * fixed pep8 warnings --- lib/ansible/module_utils/avi.py | 64 +++++ lib/ansible/modules/network/avi/__init__.py | 0 .../modules/network/avi/avi_api_session.py | 240 ++++++++++++++++++ .../utils/module_docs_fragments/avi.py | 45 ++++ test/compile/python2.4-skip.txt | 1 + 5 files changed, 350 insertions(+) create mode 100644 lib/ansible/module_utils/avi.py create mode 100644 lib/ansible/modules/network/avi/__init__.py create mode 100644 lib/ansible/modules/network/avi/avi_api_session.py create mode 100644 lib/ansible/utils/module_docs_fragments/avi.py diff --git a/lib/ansible/module_utils/avi.py b/lib/ansible/module_utils/avi.py new file mode 100644 index 00000000000..8aaa73bb735 --- /dev/null +++ b/lib/ansible/module_utils/avi.py @@ -0,0 +1,64 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c), Gaurav Rastogi , 2017 +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import os + + +def avi_common_argument_spec(): + """ + Returns common arguments for all Avi modules + :return: dict + """ + return dict( + controller=dict(default=os.environ.get('AVI_CONTROLLER', '')), + username=dict(default=os.environ.get('AVI_USERNAME', '')), + password=dict(default=os.environ.get('AVI_PASSWORD', ''), no_log=True), + tenant=dict(default='admin'), + tenant_uuid=dict(default='')) + + +def ansible_return(module, rsp, changed, req=None, existing_obj=None): + """ + Helper function to return the right ansible return based on the error code and + changed status. + :param module: AnsibleModule + :param rsp: ApiResponse object returned from ApiSession. + :param changed: Whether something changed in this module. + :param req: Dict data for Avi API call. + :param existing_obj: Dict representing current HTTP resource in Avi Controller. + + Returns: specific ansible module exit function + """ + if rsp.status_code > 299: + return module.fail_json(msg='Error %d Msg %s req: %s' % ( + rsp.status_code, rsp.text, req)) + if changed and existing_obj: + return module.exit_json( + changed=changed, obj=rsp.json(), old_obj=existing_obj) + return module.exit_json(changed=changed, obj=rsp.json()) diff --git a/lib/ansible/modules/network/avi/__init__.py b/lib/ansible/modules/network/avi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/network/avi/avi_api_session.py b/lib/ansible/modules/network/avi/avi_api_session.py new file mode 100644 index 00000000000..014e0b0f7f6 --- /dev/null +++ b/lib/ansible/modules/network/avi/avi_api_session.py @@ -0,0 +1,240 @@ +#!/usr/bin/python +""" +# Created on Aug 12, 2016 +# +# @author: Gaurav Rastogi (grastogi@avinetworks.com) GitHub ID: grastogi23 +# +# module_check: not supported +# +# 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 . +# +""" + +import json +import time +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.avi import avi_common_argument_spec, ansible_return +from copy import deepcopy + +HAS_AVI = True +try: + from avi.sdk.avi_api import ApiSession + from avi.sdk.utils.ansible_utils import avi_obj_cmp, cleanup_absent_fields +except ImportError: + HAS_AVI = False + + +DOCUMENTATION = ''' +--- +module: avi_api +author: Gaurav Rastogi (grastogi@avinetworks.com) + +short_description: Avi API Module +description: + - This module can be used for calling any resources defined in Avi REST API. + - This module is useful for invoking HTTP Patch methods and accessing resources that do not have an REST object associated with them. +version_added: 2.3 +requirements: [ avisdk ] +options: + http_method: + description: + - Allowed HTTP methods for RESTful services and are supported by Avi Controller. + choices: ["get", "put", "post", "patch", "delete"] + required: true + data: + description: + - HTTP body in YAML or JSON format. + params: + description: + - Query parameters passed to the HTTP API. + path: + description: + - Path for Avi API resource. For example, C(path: virtualservice) will translate to C(api/virtualserivce). + timeout: + description: + - Timeout (in seconds) for Avi API calls. +extends_documentation_fragment: + - avi +''' + +EXAMPLES = ''' + + - name: Get Pool Information using avi_api_session + avi_api_session: + controller: "{{ controller }}" + username: "{{ username }}" + password: "{{ password }}" + http_method: get + path: pool + params: + name: "{{ pool_name }}" + register: pool_results + + - name: Patch Pool with list of servers + avi_api_session: + controller: "{{ controller }}" + username: "{{ username }}" + password: "{{ password }}" + http_method: patch + path: "{{ pool_path }}" + data: + add: + servers: + - ip: + addr: 10.10.10.10 + type: V4 + - ip: + addr: 20.20.20.20 + type: V4 + register: updated_pool + + - name: Fetch Pool metrics bandwidth and connections rate + avi_api_session: + controller: "{{ controller }}" + username: "{{ username }}" + password: "{{ password }}" + http_method: get + path: analytics/metrics/pool + params: + name: "{{ pool_name }}" + metric_id: l4_server.avg_bandwidth,l4_server.avg_complete_conns + step: 300 + limit: 10 + register: pool_metrics + +''' + + +RETURN = ''' +obj: + description: Avi REST resource + returned: success, changed + type: dict +''' + +def main(): + argument_specs = dict( + http_method=dict(required=True, + choices=['get', 'put', 'post', 'patch', + 'delete']), + path=dict(type='str', required=True), + params=dict(type='dict'), + data=dict(type='jsonarg'), + timeout=dict(type='int', default=60) + ) + argument_specs.update(avi_common_argument_spec()) + module = AnsibleModule(argument_spec=argument_specs) + + if not HAS_AVI: + return module.fail_json(msg=( + 'Avi python API SDK (avisdk) is not installed. ' + 'For more details visit https://github.com/avinetworks/sdk.')) + tenant_uuid = module.params.get('tenant_uuid', None) + api = ApiSession.get_session( + module.params['controller'], module.params['username'], + module.params['password'], tenant=module.params['tenant'], + tenant_uuid=tenant_uuid) + + tenant = module.params.get('tenant', '') + timeout = int(module.params.get('timeout')) + # path is a required argument + path = module.params.get('path', '') + params = module.params.get('params', None) + data = module.params.get('data', None) + + if data is not None: + data = json.loads(data) + method = module.params['http_method'] + + existing_obj = None + changed = method != 'get' + gparams = deepcopy(params) if params else {} + gparams.update({'include_refs': '', 'include_name': ''}) + + if method == 'post': + # need to check if object already exists. In that case + # change the method to be put + gparams['name'] = data['name'] + rsp = api.get(path, tenant=tenant, tenant_uuid=tenant_uuid, + params=gparams) + try: + existing_obj = rsp.json()['results'][0] + except IndexError: + # object is not found + pass + else: + # object is present + method = 'put' + path += '/' + existing_obj['uuid'] + + if method == 'put': + # put can happen with when full path is specified or it is put + post + if existing_obj is None: + using_collection = False + if (len(path.split('/')) == 1) and ('name' in data): + gparams['name'] = data['name'] + using_collection = True + rsp = api.get(path, tenant=tenant, tenant_uuid=tenant_uuid, + params=gparams) + rsp_data = rsp.json() + if using_collection: + if rsp_data['results']: + existing_obj = rsp_data['results'][0] + path += '/' + existing_obj['uuid'] + else: + method = 'post' + else: + if rsp.status_code == 404: + method = 'post' + else: + existing_obj = rsp_data + if existing_obj: + changed = not avi_obj_cmp(data, existing_obj) + cleanup_absent_fields(data) + if method == 'patch': + rsp = api.get(path, tenant=tenant, tenant_uuid=tenant_uuid, + params=gparams) + existing_obj = rsp.json() + + if (method == 'put' and changed) or (method != 'put'): + fn = getattr(api, method) + rsp = fn(path, tenant=tenant, tenant_uuid=tenant, timeout=timeout, + params=params, data=data) + else: + rsp = None + if method == 'delete' and rsp.status_code == 404: + changed = False + rsp.status_code = 200 + if method == 'patch' and existing_obj and rsp.status_code < 299: + # Ideally the comparison should happen with the return values + # from the patch API call. However, currently Avi API are + # returning different hostname when GET is used vs Patch. + # tracked as AV-12561 + if path.startswith('pool'): + time.sleep(1) + gparams = deepcopy(params) if params else {} + gparams.update({'include_refs': '', 'include_name': ''}) + rsp = api.get(path, tenant=tenant, tenant_uuid=tenant_uuid, + params=gparams) + new_obj = rsp.json() + changed = not avi_obj_cmp(new_obj, existing_obj) + if rsp is None: + return module.exit_json(changed=changed, obj=existing_obj) + return ansible_return(module, rsp, changed, req=data) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/utils/module_docs_fragments/avi.py b/lib/ansible/utils/module_docs_fragments/avi.py new file mode 100644 index 00000000000..dfa8cf74950 --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/avi.py @@ -0,0 +1,45 @@ +# +# Created on December 12, 2016 +# @author: Gaurav Rastogi (grastogi@avinetworks.com) +# Avi Version: 16.3.4 +# +# +# 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 . +# + + +class ModuleDocFragment(object): + # Avi common documentation fragment + DOCUMENTATION = """ +options: + controller: + description: + - IP address or hostname of the controller. The default value is the environment variable C(AVI_CONTROLLER). + username: + description: + - Username used for accessing Avi controller. The default value is the environment variable C(AVI_USERNAME). + password: + description: + - Password of Avi user in Avi controller. The default value is the environment variable C(AVI_PASSWORD). + tenant: + description: + - Name of tenant used for all Avi API calls and context of object. + default: admin + tenant_uuid: + description: + - UUID of tenant used for all Avi API calls and context of object. + default: '' +""" diff --git a/test/compile/python2.4-skip.txt b/test/compile/python2.4-skip.txt index ec68ee9cf7a..76015cb2ce4 100644 --- a/test/compile/python2.4-skip.txt +++ b/test/compile/python2.4-skip.txt @@ -17,6 +17,7 @@ /lib/ansible/modules/database/mssql/ /lib/ansible/modules/remote_management/foreman/ /lib/ansible/modules/monitoring/zabbix.*.py +/lib/ansible/modules/network/avi/ /lib/ansible/modules/network/f5/ /lib/ansible/modules/network/nmcli.py /lib/ansible/modules/notification/pushbullet.py