From a06f06a9a33554f8b51ca54b6709f14aa0c603cb Mon Sep 17 00:00:00 2001 From: Evgeny Fedoruk Date: Wed, 8 Nov 2017 18:41:20 +0200 Subject: [PATCH] Module for committing pending configuration on Radware devices (#31776) With this module, pending configurations can be commited on Radware ADC devices. --- .../modules/network/radware/vdirect_commit.py | 358 ++++++++++++++++++ .../network/radware/test_vdirect_commit.py | 200 ++++++++++ 2 files changed, 558 insertions(+) create mode 100644 lib/ansible/modules/network/radware/vdirect_commit.py create mode 100644 test/units/modules/network/radware/test_vdirect_commit.py diff --git a/lib/ansible/modules/network/radware/vdirect_commit.py b/lib/ansible/modules/network/radware/vdirect_commit.py new file mode 100644 index 00000000000..c623b5d1707 --- /dev/null +++ b/lib/ansible/modules/network/radware/vdirect_commit.py @@ -0,0 +1,358 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2017 Radware LTD. +# +# 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 . + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.1'} + +DOCUMENTATION = ''' +module: vdirect_commit +author: Evgeny Fedoruk @ Radware LTD (@evgenyfedoruk) +short_description: Commits pending configuration changes on Radware devices +description: + - Commits pending configuration changes on one or more Radware devices via vDirect server. + - For Alteon ADC device, apply, sync and save actions will be performed by default. + Skipping of an action is possible by explicit parameter specifying. + - For Alteon VX Container device, no sync operation will be performed + since sync action is only relevant for Alteon ADC devices. + - For DefensePro and AppWall devices, a bulk commit action will be performed. + Explicit apply, sync and save actions specifying is not relevant. +notes: + - Requires the Radware vdirect-client Python package on the host. This is as easy as + C(pip install vdirect-client) +version_added: "2.5" +options: + vdirect_ip: + description: + - Primary vDirect server IP address, may be set as C(VDIRECT_IP) environment variable. + required: true + vdirect_user: + description: + - vDirect server username, may be set as C(VDIRECT_USER) environment variable. + required: true + default: None + vdirect_password: + description: + - vDirect server password, may be set as C(VDIRECT_PASSWORD) environment variable. + required: true + default: None + vdirect_secondary_ip: + description: + - Secondary vDirect server IP address, may be set as C(VDIRECT_SECONDARY_IP) environment variable. + required: false + default: None + vdirect_wait: + description: + - Wait for async operation to complete, may be set as C(VDIRECT_WAIT) environment variable. + required: false + type: bool + default: 'yes' + vdirect_https_port: + description: + - vDirect server HTTPS port number, may be set as C(VDIRECT_HTTPS_PORT) environment variable. + required: false + default: 2189 + vdirect_http_port: + description: + - vDirect server HTTP port number, may be set as C(VDIRECT_HTTP_PORT) environment variable. + required: false + default: 2188 + vdirect_timeout: + description: + - Amount of time to wait for async operation completion [seconds], + - may be set as C(VDIRECT_TIMEOUT) environment variable. + required: false + default: 60 + vdirect_use_ssl: + description: + - If C(no), an HTTP connection will be used instead of the default HTTPS connection, + - may be set as C(VDIRECT_HTTPS) or C(VDIRECT_USE_SSL) environment variable. + required: false + type: bool + default: 'yes' + vdirect_validate_certs: + description: + - If C(no), SSL certificates will not be validated, + - may be set as C(VDIRECT_VALIDATE_CERTS) or C(VDIRECT_VERIFY) environment variable. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + type: bool + default: 'yes' + devices: + description: + - List of Radware Alteon device names for commit operations. + required: true + apply: + description: + - If C(no), apply action will not be performed. Relevant for ADC devices only. + required: false + type: bool + default: 'yes' + save: + description: + - If C(no), save action will not be performed. Relevant for ADC devices only. + required: false + type: bool + default: 'yes' + sync: + description: + - If C(no), sync action will not be performed. Relevant for ADC devices only. + required: false + type: bool + default: 'yes' + +requirements: + - "vdirect-client >= 4.1.1" +''' + +EXAMPLES = ''' +- name: vdirect_commit + vdirect_commit: + vdirect_primary_ip: 10.10.10.10 + vdirect_user: vDirect + vdirect_password: radware + devices: ['dev1', 'dev2'] + sync: no +''' + +RETURN = ''' +result: + description: Message detailing actions result + returned: success + type: string + sample: "Requested actions were successfully performed on all devices." +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback + +try: + from vdirect_client import rest_client + HAS_REST_CLIENT = True +except ImportError: + HAS_REST_CLIENT = False + + +SUCCESS = 'Requested actions were successfully performed on all devices.' +FAILURE = 'Failure occurred while performing requested actions on devices. See details' + +ADC_DEVICE_TYPE = 'Adc' +CONTAINER_DEVICE_TYPE = 'Container' +PARTITIONED_CONTAINER_DEVICE_TYPE = 'AlteonPartitioned' +APPWALL_DEVICE_TYPE = 'AppWall' +DP_DEVICE_TYPE = 'DefensePro' + +SUCCEEDED = 'succeeded' +FAILED = 'failed' +NOT_PERFORMED = 'not performed' + +meta_args = dict( + vdirect_ip=dict( + required=True, fallback=(env_fallback, ['VDIRECT_IP']), + default=None), + vdirect_user=dict( + required=True, fallback=(env_fallback, ['VDIRECT_USER']), + default=None), + vdirect_password=dict( + required=True, fallback=(env_fallback, ['VDIRECT_PASSWORD']), + default=None, no_log=True, type='str'), + vdirect_secondary_ip=dict( + required=False, fallback=(env_fallback, ['VDIRECT_SECONDARY_IP']), + default=None), + vdirect_use_ssl=dict( + required=False, fallback=(env_fallback, ['VDIRECT_HTTPS', 'VDIRECT_USE_SSL']), + default=True, type='bool'), + vdirect_wait=dict( + required=False, fallback=(env_fallback, ['VDIRECT_WAIT']), + default=True, type='bool'), + vdirect_timeout=dict( + required=False, fallback=(env_fallback, ['VDIRECT_TIMEOUT']), + default=60, type='int'), + vdirect_validate_certs=dict( + required=False, fallback=(env_fallback, ['VDIRECT_VERIFY', 'VDIRECT_VALIDATE_CERTS']), + default=True, type='bool'), + vdirect_https_port=dict( + required=False, fallback=(env_fallback, ['VDIRECT_HTTPS_PORT']), + default=2189, type='int'), + vdirect_http_port=dict( + required=False, fallback=(env_fallback, ['VDIRECT_HTTP_PORT']), + default=2188, type='int'), + devices=dict( + required=True, type='list'), + apply=dict( + required=False, default=True, type='bool'), + save=dict( + required=False, default=True, type='bool'), + sync=dict( + required=False, default=True, type='bool'), +) + + +class CommitException(Exception): + def __init__(self, reason, details): + self.reason = reason + self.details = details + + def __str__(self): + return 'Reason: {0}. Details:{1}.'.format(self.reason, self.details) + + +class MissingDeviceException(CommitException): + def __init__(self, device_name): + super(MissingDeviceException, self).__init__( + 'Device missing', + 'Device ' + repr(device_name) + ' does not exist') + + +class VdirectCommit(object): + def __init__(self, params): + self.client = rest_client.RestClient(params['vdirect_ip'], + params['vdirect_user'], + params['vdirect_password'], + wait=params['vdirect_wait'], + secondary_vdirect_ip=params['vdirect_secondary_ip'], + https_port=params['vdirect_https_port'], + http_port=params['vdirect_http_port'], + timeout=params['vdirect_timeout'], + https=params['vdirect_use_ssl'], + verify=params['vdirect_validate_certs']) + self.devices = params['devices'] + self.apply = params['apply'] + self.save = params['save'] + self.sync = params['sync'] + self.devicesMap = {} + + def _validate_devices(self): + for device in self.devices: + try: + res = self.client.adc.get(device) + if res[rest_client.RESP_STATUS] == 200: + self.devicesMap.update({device: ADC_DEVICE_TYPE}) + continue + res = self.client.container.get(device) + if res[rest_client.RESP_STATUS] == 200: + if res[rest_client.RESP_DATA]['type'] == PARTITIONED_CONTAINER_DEVICE_TYPE: + self.devicesMap.update({device: CONTAINER_DEVICE_TYPE}) + continue + res = self.client.appWall.get(device) + if res[rest_client.RESP_STATUS] == 200: + self.devicesMap.update({device: APPWALL_DEVICE_TYPE}) + continue + res = self.client.defensePro.get(device) + if res[rest_client.RESP_STATUS] == 200: + self.devicesMap.update({device: DP_DEVICE_TYPE}) + continue + + except Exception as e: + raise CommitException('Failed to communicate with device ' + device, str(e)) + + raise MissingDeviceException(device) + + def _perform_action_and_update_result(self, device, action, perform, failure_occurred, actions_result): + + if not perform or failure_occurred: + actions_result[action] = NOT_PERFORMED + return True + + try: + if self.devicesMap[device] == ADC_DEVICE_TYPE: + res = self.client.adc.control_device(device, action) + elif self.devicesMap[device] == CONTAINER_DEVICE_TYPE: + res = self.client.container.control(device, action) + elif self.devicesMap[device] == APPWALL_DEVICE_TYPE: + res = self.client.appWall.control_device(device, action) + elif self.devicesMap[device] == DP_DEVICE_TYPE: + res = self.client.defensePro.control_device(device, action) + + if res[rest_client.RESP_STATUS] in [200, 204]: + actions_result[action] = SUCCEEDED + else: + actions_result[action] = FAILED + actions_result['failure_description'] = res[rest_client.RESP_STR] + return False + except Exception as e: + actions_result[action] = FAILED + actions_result['failure_description'] = 'Exception occurred while performing '\ + + action + ' action. Exception: ' + str(e) + return False + + return True + + def commit(self): + self._validate_devices() + + result_to_return = dict() + result_to_return['details'] = list() + + for device in self.devices: + failure_occurred = False + device_type = self.devicesMap[device] + actions_result = dict() + actions_result['device_name'] = device + actions_result['device_type'] = device_type + + if device_type in [DP_DEVICE_TYPE, APPWALL_DEVICE_TYPE]: + failure_occurred = not self._perform_action_and_update_result( + device, 'commit', True, failure_occurred, actions_result)\ + or failure_occurred + else: + failure_occurred = not self._perform_action_and_update_result( + device, 'apply', self.apply, failure_occurred, actions_result)\ + or failure_occurred + if device_type != CONTAINER_DEVICE_TYPE: + failure_occurred = not self._perform_action_and_update_result( + device, 'sync', self.sync, failure_occurred, actions_result)\ + or failure_occurred + failure_occurred = not self._perform_action_and_update_result( + device, 'save', self.save, failure_occurred, actions_result)\ + or failure_occurred + + result_to_return['details'].extend([actions_result]) + + if failure_occurred: + result_to_return['msg'] = FAILURE + + if 'msg' not in result_to_return: + result_to_return['msg'] = SUCCESS + + return result_to_return + + +def main(): + + if not HAS_REST_CLIENT: + raise ImportError("The python vdirect-client module is required") + + module = AnsibleModule(argument_spec=meta_args) + + try: + vdirect_commit = VdirectCommit(module.params) + result = vdirect_commit.commit() + result = dict(result=result) + module.exit_json(**result) + except Exception as e: + module.fail_json(msg=str(e)) + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/radware/test_vdirect_commit.py b/test/units/modules/network/radware/test_vdirect_commit.py new file mode 100644 index 00000000000..cbd8f551bfb --- /dev/null +++ b/test/units/modules/network/radware/test_vdirect_commit.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 Radware LTD. +# +# 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 os +from ansible.compat.tests.mock import patch, MagicMock + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch + +BASE_PARAMS = {'vdirect_ip': None, 'vdirect_user': None, 'vdirect_password': None, + 'vdirect_wait': None, 'vdirect_secondary_ip': None, + 'vdirect_https_port': None, 'vdirect_http_port': None, + 'vdirect_timeout': None, 'vdirect_use_ssl': None, 'vdirect_validate_certs': None} + +COMMIT_PARAMS = {'devices': ['adc', 'defensepro', 'vx', 'appwall'], 'apply': True, 'save': True, 'sync': True} + +COMMIT_GET_DEVICE_200_RESULT = [200, '', '', {'type': 'AlteonPartitioned'}] +COMMIT_GET_DEVICE_404_RESULT = [404, '', '', ''] + +COMMIT_RESULT_200 = [200, '', '', ''] +COMMIT_RESULT_204 = [204, '', '', ''] + +MODULE_RESULT = {"msg": "Requested actions were successfully performed on all devices.", + "details": [{'device_name': 'adc', 'device_type': 'Adc', + 'apply': 'succeeded', 'save': 'succeeded', 'sync': 'succeeded'}, + {'device_name': 'defensepro', 'device_type': 'DefensePro', + 'commit': 'succeeded'}, + {'device_name': 'vx', 'device_type': 'Container', + 'apply': 'succeeded', 'save': 'succeeded'}, + {'device_name': 'appwall', 'device_type': 'AppWall', + 'commit': 'succeeded'}]} + + +@patch('vdirect_client.rest_client.RestClient') +class RestClient (): + def __init__(self, vdirect_ip=None, vdirect_user=None, vdirect_password=None, wait=None, + secondary_vdirect_ip=None, https_port=None, http_port=None, + timeout=None, https=None, strict_http_results=None, + verify=None): + pass + + +class DeviceMock (): + + def __init__(self, name, client): + self.name = name + self.client = client + self.get_throw = False + self.control_throw = False + self.exception = Exception('exception message') + self.control_result = COMMIT_RESULT_200 + + def set_control_result(self, result): + self.control_result = result + + def throw_exception(self, get_throw=False, control_throw=False): + self.get_throw = get_throw + self.control_throw = control_throw + + def get(self, name): + if self.get_throw: + raise self.exception # pylint: disable=E0702 + if name == self.name: + return COMMIT_GET_DEVICE_200_RESULT + else: + return COMMIT_GET_DEVICE_404_RESULT + + def control_device(self, name, action): + if self.control_throw: + raise self.exception # pylint: disable=E0702 + return self.control_result + + def control(self, name, action): + return self.control_device(name, action) + + +class TestManager(unittest.TestCase): + + def setUp(self): + self.module_mock = MagicMock() + self.module_mock.rest_client.RESP_STATUS = 0 + self.module_mock.rest_client.RESP_REASON = 1 + self.module_mock.rest_client.RESP_STR = 2 + self.module_mock.rest_client.RESP_DATA = 3 + + def test_missing_parameter(self, *args): + with patch.dict('sys.modules', **{ + 'vdirect_client': self.module_mock, + 'vdirect_client.rest_client.RestClient': self.module_mock, + }): + from ansible.modules.network.radware import vdirect_commit + + try: + params = BASE_PARAMS.copy() + vdirect_commit.VdirectCommit(params) + self.fail("KeyError was not thrown for missing parameter") + except KeyError: + assert True + + def test_validate_devices(self, *args): + with patch.dict('sys.modules', **{ + 'vdirect_client': self.module_mock, + 'vdirect_client.rest_client.RestClient': self.module_mock, + }): + from ansible.modules.network.radware import vdirect_commit + + BASE_PARAMS.update(COMMIT_PARAMS) + vdirectcommit = vdirect_commit.VdirectCommit(BASE_PARAMS) + vdirectcommit.client.adc = DeviceMock('adc', vdirectcommit.client) + vdirectcommit.client.container = DeviceMock('vx', vdirectcommit.client) + vdirectcommit.client.appWall = DeviceMock('appwall', vdirectcommit.client) + vdirectcommit.client.defensePro = DeviceMock('defensepro', vdirectcommit.client) + + vdirectcommit._validate_devices() + assert True + + vdirectcommit.client.adc.throw_exception(True) + try: + vdirectcommit._validate_devices() + self.fail("CommitException was not thrown for device communication failure") + except vdirect_commit.CommitException: + assert True + + vdirectcommit.client.adc.throw_exception(False) + vdirectcommit.client.defensePro.throw_exception(True) + try: + vdirectcommit._validate_devices() + self.fail("CommitException was not thrown for device communication failure") + except vdirect_commit.CommitException: + assert True + + vdirectcommit.client.defensePro.throw_exception(False) + + vdirectcommit.client.adc.name = 'wrong' + try: + vdirectcommit._validate_devices() + self.fail("MissingDeviceException was not thrown for missing device") + except vdirect_commit.MissingDeviceException: + assert True + + def test_commit(self, *args): + with patch.dict('sys.modules', **{ + 'vdirect_client': self.module_mock, + 'vdirect_client.rest_client.RestClient': self.module_mock, + }): + from ansible.modules.network.radware import vdirect_commit + + BASE_PARAMS.update(COMMIT_PARAMS) + vdirectcommit = vdirect_commit.VdirectCommit(BASE_PARAMS) + vdirectcommit.client.adc = DeviceMock('adc', vdirectcommit.client) + vdirectcommit.client.container = DeviceMock('vx', vdirectcommit.client) + vdirectcommit.client.appWall = DeviceMock('appwall', vdirectcommit.client) + vdirectcommit.client.defensePro = DeviceMock('defensepro', vdirectcommit.client) + + res = vdirectcommit.commit() + assert res == MODULE_RESULT + + vdirectcommit.sync = False + for detail in MODULE_RESULT['details']: + if 'sync' in detail: + detail['sync'] = vdirect_commit.NOT_PERFORMED + res = vdirectcommit.commit() + assert res == MODULE_RESULT + + vdirectcommit.client.adc.control_result = COMMIT_RESULT_204 + vdirectcommit.client.adc.control_result[self.module_mock.rest_client.RESP_STATUS] = 500 + vdirectcommit.client.adc.control_result[self.module_mock.rest_client.RESP_STR] = 'Some Failure' + MODULE_RESULT['msg'] = 'Failure occurred while performing requested actions on devices. See details' + for detail in MODULE_RESULT['details']: + if detail['device_name'] == 'adc': + detail['apply'] = vdirect_commit.FAILED + detail['failure_description'] = 'Some Failure' + detail['save'] = vdirect_commit.NOT_PERFORMED + detail['sync'] = vdirect_commit.NOT_PERFORMED + res = vdirectcommit.commit() + assert res == MODULE_RESULT + + vdirectcommit.client.adc.throw_exception(control_throw=True) + for detail in MODULE_RESULT['details']: + if detail['device_name'] == 'adc': + detail['failure_description'] = 'Exception occurred while performing apply action. ' \ + 'Exception: exception message' + res = vdirectcommit.commit() + assert res == MODULE_RESULT