|
| 1 | +#!/usr/bin/python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +# |
| 4 | +# Copyright 2017 Radware LTD. |
| 5 | +# |
| 6 | +# This file is part of Ansible |
| 7 | +# |
| 8 | +# Ansible is free software: you can redistribute it and/or modify |
| 9 | +# it under the terms of the GNU General Public License as published by |
| 10 | +# the Free Software Foundation, either version 3 of the License, or |
| 11 | +# (at your option) any later version. |
| 12 | +# |
| 13 | +# Ansible is distributed in the hope that it will be useful, |
| 14 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | +# GNU General Public License for more details. |
| 17 | +# |
| 18 | +# You should have received a copy of the GNU General Public License |
| 19 | +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. |
| 20 | + |
| 21 | +from __future__ import absolute_import, division, print_function |
| 22 | + |
| 23 | +__metaclass__ = type |
| 24 | + |
| 25 | +ANSIBLE_METADATA = {'status': ['preview'], |
| 26 | + 'supported_by': 'community', |
| 27 | + 'metadata_version': '1.1'} |
| 28 | + |
| 29 | +DOCUMENTATION = ''' |
| 30 | +module: vdirect_commit |
| 31 | +author: Evgeny Fedoruk @ Radware LTD (@evgenyfedoruk) |
| 32 | +short_description: Commits pending configuration changes on Radware devices |
| 33 | +description: |
| 34 | + - Commits pending configuration changes on one or more Radware devices via vDirect server. |
| 35 | + - For Alteon ADC device, apply, sync and save actions will be performed by default. |
| 36 | + Skipping of an action is possible by explicit parameter specifying. |
| 37 | + - For Alteon VX Container device, no sync operation will be performed |
| 38 | + since sync action is only relevant for Alteon ADC devices. |
| 39 | + - For DefensePro and AppWall devices, a bulk commit action will be performed. |
| 40 | + Explicit apply, sync and save actions specifying is not relevant. |
| 41 | +notes: |
| 42 | + - Requires the Radware vdirect-client Python package on the host. This is as easy as |
| 43 | + C(pip install vdirect-client) |
| 44 | +version_added: "2.5" |
| 45 | +options: |
| 46 | + vdirect_ip: |
| 47 | + description: |
| 48 | + - Primary vDirect server IP address, may be set as C(VDIRECT_IP) environment variable. |
| 49 | + required: true |
| 50 | + vdirect_user: |
| 51 | + description: |
| 52 | + - vDirect server username, may be set as C(VDIRECT_USER) environment variable. |
| 53 | + required: true |
| 54 | + default: None |
| 55 | + vdirect_password: |
| 56 | + description: |
| 57 | + - vDirect server password, may be set as C(VDIRECT_PASSWORD) environment variable. |
| 58 | + required: true |
| 59 | + default: None |
| 60 | + vdirect_secondary_ip: |
| 61 | + description: |
| 62 | + - Secondary vDirect server IP address, may be set as C(VDIRECT_SECONDARY_IP) environment variable. |
| 63 | + required: false |
| 64 | + default: None |
| 65 | + vdirect_wait: |
| 66 | + description: |
| 67 | + - Wait for async operation to complete, may be set as C(VDIRECT_WAIT) environment variable. |
| 68 | + required: false |
| 69 | + type: bool |
| 70 | + default: 'yes' |
| 71 | + vdirect_https_port: |
| 72 | + description: |
| 73 | + - vDirect server HTTPS port number, may be set as C(VDIRECT_HTTPS_PORT) environment variable. |
| 74 | + required: false |
| 75 | + default: 2189 |
| 76 | + vdirect_http_port: |
| 77 | + description: |
| 78 | + - vDirect server HTTP port number, may be set as C(VDIRECT_HTTP_PORT) environment variable. |
| 79 | + required: false |
| 80 | + default: 2188 |
| 81 | + vdirect_timeout: |
| 82 | + description: |
| 83 | + - Amount of time to wait for async operation completion [seconds], |
| 84 | + - may be set as C(VDIRECT_TIMEOUT) environment variable. |
| 85 | + required: false |
| 86 | + default: 60 |
| 87 | + vdirect_use_ssl: |
| 88 | + description: |
| 89 | + - If C(no), an HTTP connection will be used instead of the default HTTPS connection, |
| 90 | + - may be set as C(VDIRECT_HTTPS) or C(VDIRECT_USE_SSL) environment variable. |
| 91 | + required: false |
| 92 | + type: bool |
| 93 | + default: 'yes' |
| 94 | + vdirect_validate_certs: |
| 95 | + description: |
| 96 | + - If C(no), SSL certificates will not be validated, |
| 97 | + - may be set as C(VDIRECT_VALIDATE_CERTS) or C(VDIRECT_VERIFY) environment variable. |
| 98 | + - This should only set to C(no) used on personally controlled sites using self-signed certificates. |
| 99 | + required: false |
| 100 | + type: bool |
| 101 | + default: 'yes' |
| 102 | + devices: |
| 103 | + description: |
| 104 | + - List of Radware Alteon device names for commit operations. |
| 105 | + required: true |
| 106 | + apply: |
| 107 | + description: |
| 108 | + - If C(no), apply action will not be performed. Relevant for ADC devices only. |
| 109 | + required: false |
| 110 | + type: bool |
| 111 | + default: 'yes' |
| 112 | + save: |
| 113 | + description: |
| 114 | + - If C(no), save action will not be performed. Relevant for ADC devices only. |
| 115 | + required: false |
| 116 | + type: bool |
| 117 | + default: 'yes' |
| 118 | + sync: |
| 119 | + description: |
| 120 | + - If C(no), sync action will not be performed. Relevant for ADC devices only. |
| 121 | + required: false |
| 122 | + type: bool |
| 123 | + default: 'yes' |
| 124 | +
|
| 125 | +requirements: |
| 126 | + - "vdirect-client >= 4.1.1" |
| 127 | +''' |
| 128 | + |
| 129 | +EXAMPLES = ''' |
| 130 | +- name: vdirect_commit |
| 131 | + vdirect_commit: |
| 132 | + vdirect_primary_ip: 10.10.10.10 |
| 133 | + vdirect_user: vDirect |
| 134 | + vdirect_password: radware |
| 135 | + devices: ['dev1', 'dev2'] |
| 136 | + sync: no |
| 137 | +''' |
| 138 | + |
| 139 | +RETURN = ''' |
| 140 | +result: |
| 141 | + description: Message detailing actions result |
| 142 | + returned: success |
| 143 | + type: string |
| 144 | + sample: "Requested actions were successfully performed on all devices." |
| 145 | +''' |
| 146 | + |
| 147 | +from ansible.module_utils.basic import AnsibleModule |
| 148 | +from ansible.module_utils.basic import env_fallback |
| 149 | + |
| 150 | +try: |
| 151 | + from vdirect_client import rest_client |
| 152 | + HAS_REST_CLIENT = True |
| 153 | +except ImportError: |
| 154 | + HAS_REST_CLIENT = False |
| 155 | + |
| 156 | + |
| 157 | +SUCCESS = 'Requested actions were successfully performed on all devices.' |
| 158 | +FAILURE = 'Failure occurred while performing requested actions on devices. See details' |
| 159 | + |
| 160 | +ADC_DEVICE_TYPE = 'Adc' |
| 161 | +CONTAINER_DEVICE_TYPE = 'Container' |
| 162 | +PARTITIONED_CONTAINER_DEVICE_TYPE = 'AlteonPartitioned' |
| 163 | +APPWALL_DEVICE_TYPE = 'AppWall' |
| 164 | +DP_DEVICE_TYPE = 'DefensePro' |
| 165 | + |
| 166 | +SUCCEEDED = 'succeeded' |
| 167 | +FAILED = 'failed' |
| 168 | +NOT_PERFORMED = 'not performed' |
| 169 | + |
| 170 | +meta_args = dict( |
| 171 | + vdirect_ip=dict( |
| 172 | + required=True, fallback=(env_fallback, ['VDIRECT_IP']), |
| 173 | + default=None), |
| 174 | + vdirect_user=dict( |
| 175 | + required=True, fallback=(env_fallback, ['VDIRECT_USER']), |
| 176 | + default=None), |
| 177 | + vdirect_password=dict( |
| 178 | + required=True, fallback=(env_fallback, ['VDIRECT_PASSWORD']), |
| 179 | + default=None, no_log=True, type='str'), |
| 180 | + vdirect_secondary_ip=dict( |
| 181 | + required=False, fallback=(env_fallback, ['VDIRECT_SECONDARY_IP']), |
| 182 | + default=None), |
| 183 | + vdirect_use_ssl=dict( |
| 184 | + required=False, fallback=(env_fallback, ['VDIRECT_HTTPS', 'VDIRECT_USE_SSL']), |
| 185 | + default=True, type='bool'), |
| 186 | + vdirect_wait=dict( |
| 187 | + required=False, fallback=(env_fallback, ['VDIRECT_WAIT']), |
| 188 | + default=True, type='bool'), |
| 189 | + vdirect_timeout=dict( |
| 190 | + required=False, fallback=(env_fallback, ['VDIRECT_TIMEOUT']), |
| 191 | + default=60, type='int'), |
| 192 | + vdirect_validate_certs=dict( |
| 193 | + required=False, fallback=(env_fallback, ['VDIRECT_VERIFY', 'VDIRECT_VALIDATE_CERTS']), |
| 194 | + default=True, type='bool'), |
| 195 | + vdirect_https_port=dict( |
| 196 | + required=False, fallback=(env_fallback, ['VDIRECT_HTTPS_PORT']), |
| 197 | + default=2189, type='int'), |
| 198 | + vdirect_http_port=dict( |
| 199 | + required=False, fallback=(env_fallback, ['VDIRECT_HTTP_PORT']), |
| 200 | + default=2188, type='int'), |
| 201 | + devices=dict( |
| 202 | + required=True, type='list'), |
| 203 | + apply=dict( |
| 204 | + required=False, default=True, type='bool'), |
| 205 | + save=dict( |
| 206 | + required=False, default=True, type='bool'), |
| 207 | + sync=dict( |
| 208 | + required=False, default=True, type='bool'), |
| 209 | +) |
| 210 | + |
| 211 | + |
| 212 | +class CommitException(Exception): |
| 213 | + def __init__(self, reason, details): |
| 214 | + self.reason = reason |
| 215 | + self.details = details |
| 216 | + |
| 217 | + def __str__(self): |
| 218 | + return 'Reason: {0}. Details:{1}.'.format(self.reason, self.details) |
| 219 | + |
| 220 | + |
| 221 | +class MissingDeviceException(CommitException): |
| 222 | + def __init__(self, device_name): |
| 223 | + super(MissingDeviceException, self).__init__( |
| 224 | + 'Device missing', |
| 225 | + 'Device ' + repr(device_name) + ' does not exist') |
| 226 | + |
| 227 | + |
| 228 | +class VdirectCommit(object): |
| 229 | + def __init__(self, params): |
| 230 | + self.client = rest_client.RestClient(params['vdirect_ip'], |
| 231 | + params['vdirect_user'], |
| 232 | + params['vdirect_password'], |
| 233 | + wait=params['vdirect_wait'], |
| 234 | + secondary_vdirect_ip=params['vdirect_secondary_ip'], |
| 235 | + https_port=params['vdirect_https_port'], |
| 236 | + http_port=params['vdirect_http_port'], |
| 237 | + timeout=params['vdirect_timeout'], |
| 238 | + https=params['vdirect_use_ssl'], |
| 239 | + verify=params['vdirect_validate_certs']) |
| 240 | + self.devices = params['devices'] |
| 241 | + self.apply = params['apply'] |
| 242 | + self.save = params['save'] |
| 243 | + self.sync = params['sync'] |
| 244 | + self.devicesMap = {} |
| 245 | + |
| 246 | + def _validate_devices(self): |
| 247 | + for device in self.devices: |
| 248 | + try: |
| 249 | + res = self.client.adc.get(device) |
| 250 | + if res[rest_client.RESP_STATUS] == 200: |
| 251 | + self.devicesMap.update({device: ADC_DEVICE_TYPE}) |
| 252 | + continue |
| 253 | + res = self.client.container.get(device) |
| 254 | + if res[rest_client.RESP_STATUS] == 200: |
| 255 | + if res[rest_client.RESP_DATA]['type'] == PARTITIONED_CONTAINER_DEVICE_TYPE: |
| 256 | + self.devicesMap.update({device: CONTAINER_DEVICE_TYPE}) |
| 257 | + continue |
| 258 | + res = self.client.appWall.get(device) |
| 259 | + if res[rest_client.RESP_STATUS] == 200: |
| 260 | + self.devicesMap.update({device: APPWALL_DEVICE_TYPE}) |
| 261 | + continue |
| 262 | + res = self.client.defensePro.get(device) |
| 263 | + if res[rest_client.RESP_STATUS] == 200: |
| 264 | + self.devicesMap.update({device: DP_DEVICE_TYPE}) |
| 265 | + continue |
| 266 | + |
| 267 | + except Exception as e: |
| 268 | + raise CommitException('Failed to communicate with device ' + device, str(e)) |
| 269 | + |
| 270 | + raise MissingDeviceException(device) |
| 271 | + |
| 272 | + def _perform_action_and_update_result(self, device, action, perform, failure_occurred, actions_result): |
| 273 | + |
| 274 | + if not perform or failure_occurred: |
| 275 | + actions_result[action] = NOT_PERFORMED |
| 276 | + return True |
| 277 | + |
| 278 | + try: |
| 279 | + if self.devicesMap[device] == ADC_DEVICE_TYPE: |
| 280 | + res = self.client.adc.control_device(device, action) |
| 281 | + elif self.devicesMap[device] == CONTAINER_DEVICE_TYPE: |
| 282 | + res = self.client.container.control(device, action) |
| 283 | + elif self.devicesMap[device] == APPWALL_DEVICE_TYPE: |
| 284 | + res = self.client.appWall.control_device(device, action) |
| 285 | + elif self.devicesMap[device] == DP_DEVICE_TYPE: |
| 286 | + res = self.client.defensePro.control_device(device, action) |
| 287 | + |
| 288 | + if res[rest_client.RESP_STATUS] in [200, 204]: |
| 289 | + actions_result[action] = SUCCEEDED |
| 290 | + else: |
| 291 | + actions_result[action] = FAILED |
| 292 | + actions_result['failure_description'] = res[rest_client.RESP_STR] |
| 293 | + return False |
| 294 | + except Exception as e: |
| 295 | + actions_result[action] = FAILED |
| 296 | + actions_result['failure_description'] = 'Exception occurred while performing '\ |
| 297 | + + action + ' action. Exception: ' + str(e) |
| 298 | + return False |
| 299 | + |
| 300 | + return True |
| 301 | + |
| 302 | + def commit(self): |
| 303 | + self._validate_devices() |
| 304 | + |
| 305 | + result_to_return = dict() |
| 306 | + result_to_return['details'] = list() |
| 307 | + |
| 308 | + for device in self.devices: |
| 309 | + failure_occurred = False |
| 310 | + device_type = self.devicesMap[device] |
| 311 | + actions_result = dict() |
| 312 | + actions_result['device_name'] = device |
| 313 | + actions_result['device_type'] = device_type |
| 314 | + |
| 315 | + if device_type in [DP_DEVICE_TYPE, APPWALL_DEVICE_TYPE]: |
| 316 | + failure_occurred = not self._perform_action_and_update_result( |
| 317 | + device, 'commit', True, failure_occurred, actions_result)\ |
| 318 | + or failure_occurred |
| 319 | + else: |
| 320 | + failure_occurred = not self._perform_action_and_update_result( |
| 321 | + device, 'apply', self.apply, failure_occurred, actions_result)\ |
| 322 | + or failure_occurred |
| 323 | + if device_type != CONTAINER_DEVICE_TYPE: |
| 324 | + failure_occurred = not self._perform_action_and_update_result( |
| 325 | + device, 'sync', self.sync, failure_occurred, actions_result)\ |
| 326 | + or failure_occurred |
| 327 | + failure_occurred = not self._perform_action_and_update_result( |
| 328 | + device, 'save', self.save, failure_occurred, actions_result)\ |
| 329 | + or failure_occurred |
| 330 | + |
| 331 | + result_to_return['details'].extend([actions_result]) |
| 332 | + |
| 333 | + if failure_occurred: |
| 334 | + result_to_return['msg'] = FAILURE |
| 335 | + |
| 336 | + if 'msg' not in result_to_return: |
| 337 | + result_to_return['msg'] = SUCCESS |
| 338 | + |
| 339 | + return result_to_return |
| 340 | + |
| 341 | + |
| 342 | +def main(): |
| 343 | + |
| 344 | + if not HAS_REST_CLIENT: |
| 345 | + raise ImportError("The python vdirect-client module is required") |
| 346 | + |
| 347 | + module = AnsibleModule(argument_spec=meta_args) |
| 348 | + |
| 349 | + try: |
| 350 | + vdirect_commit = VdirectCommit(module.params) |
| 351 | + result = vdirect_commit.commit() |
| 352 | + result = dict(result=result) |
| 353 | + module.exit_json(**result) |
| 354 | + except Exception as e: |
| 355 | + module.fail_json(msg=str(e)) |
| 356 | + |
| 357 | +if __name__ == '__main__': |
| 358 | + main() |
0 commit comments