1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# 4# Copyright: (c) 2019, F5 Networks Inc. 5# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10DOCUMENTATION = r''' 11--- 12module: bigip_message_routing_route 13short_description: Manages static routes for routing message protocol messages 14description: 15 - Manages static routes for routing message protocol messages. 16version_added: "1.0.0" 17options: 18 name: 19 description: 20 - Specifies the name of the static route. 21 required: True 22 type: str 23 description: 24 description: 25 - The user-defined description of the static route. 26 type: str 27 type: 28 description: 29 - Parameter used to specify the type of the route to manage. 30 - Default setting is C(generic) with more options coming. 31 type: str 32 choices: 33 - generic 34 default: generic 35 src_address: 36 description: 37 - Specifies the source address of the route. 38 - Setting the attribute to an empty string will create a wildcard matching all message source-addresses, which is 39 the default when creating a new route. 40 type: str 41 dst_address: 42 description: 43 - Specifies the destination address of the route. 44 - Setting the attribute to an empty string will create a wildcard matching all message destination-addresses, 45 which is the default when creating a new route. 46 type: str 47 peer_selection_mode: 48 description: 49 - Specifies the method to use when selecting a peer from the provided list of C(peers). 50 type: str 51 choices: 52 - ratio 53 - sequential 54 peers: 55 description: 56 - Specifies a list of ltm messagerouting-peer objects. 57 - The specified peer must be on the same partition as the route. 58 type: list 59 elements: str 60 partition: 61 description: 62 - Device partition to create route object on. 63 type: str 64 default: Common 65 state: 66 description: 67 - When C(present), ensures the route exists. 68 - When C(absent), ensures the route is removed. 69 type: str 70 choices: 71 - present 72 - absent 73 default: present 74notes: 75 - Requires BIG-IP >= 14.0.0 76extends_documentation_fragment: f5networks.f5_modules.f5 77author: 78 - Wojciech Wypior (@wojtek0806) 79''' 80 81EXAMPLES = r''' 82- name: Create a simple generic route 83 bigip_message_routing_route: 84 name: foobar 85 provider: 86 password: secret 87 server: lb.mydomain.com 88 user: admin 89 delegate_to: localhost 90 91- name: Modify a generic route 92 bigip_message_routing_route: 93 name: foobar 94 peers: 95 - peer1 96 - peer2 97 peer_selection_mode: ratio 98 src_address: annoying_user 99 dst_address: blackhole 100 provider: 101 password: secret 102 server: lb.mydomain.com 103 user: admin 104 delegate_to: localhost 105 106- name: Remove a generic 107 bigip_message_routing_route: 108 name: foobar 109 state: absent 110 provider: 111 password: secret 112 server: lb.mydomain.com 113 user: admin 114 delegate_to: localhost 115''' 116 117RETURN = r''' 118description: 119 description: The user-defined description of the route. 120 returned: changed 121 type: str 122 sample: Some description 123src_address: 124 description: The source address of the route. 125 returned: changed 126 type: str 127 sample: annyoing_user 128dst_address: 129 description: The destination address of the route. 130 returned: changed 131 type: str 132 sample: blackhole 133peer_selection_mode: 134 description: The method to use when selecting a peer. 135 returned: changed 136 type: str 137 sample: ratio 138peers: 139 description: The list of ltm messagerouting-peer object. 140 returned: changed 141 type: list 142 sample: ['/Common/peer1', '/Common/peer2'] 143''' 144from datetime import datetime 145from ansible.module_utils.basic import ( 146 AnsibleModule, env_fallback 147) 148from distutils.version import LooseVersion 149 150from ..module_utils.bigip import F5RestClient 151from ..module_utils.common import ( 152 F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, is_empty_list, fq_name 153) 154from ..module_utils.compare import ( 155 cmp_simple_list, cmp_str_with_none 156) 157from ..module_utils.icontrol import tmos_version 158from ..module_utils.teem import send_teem 159 160 161class Parameters(AnsibleF5Parameters): 162 api_map = { 163 'peerSelectionMode': 'peer_selection_mode', 164 'sourceAddress': 'src_address', 165 'destinationAddress': 'dst_address', 166 } 167 168 api_attributes = [ 169 'description', 170 'peerSelectionMode', 171 'peers', 172 'sourceAddress', 173 'destinationAddress', 174 ] 175 176 returnables = [ 177 'peer_selection_mode', 178 'peers', 179 'description', 180 'src_address', 181 'dst_address' 182 ] 183 184 updatables = [ 185 'peer_selection_mode', 186 'peers', 187 'description', 188 'src_address', 189 'dst_address' 190 ] 191 192 193class ApiParameters(Parameters): 194 pass 195 196 197class ModuleParameters(Parameters): 198 @property 199 def peers(self): 200 if self._values['peers'] is None: 201 return None 202 if is_empty_list(self._values['peers']): 203 return "" 204 result = [fq_name(self.partition, peer) for peer in self._values['peers']] 205 return result 206 207 208class Changes(Parameters): 209 def to_return(self): 210 result = {} 211 try: 212 for returnable in self.returnables: 213 result[returnable] = getattr(self, returnable) 214 result = self._filter_params(result) 215 except Exception: 216 raise 217 return result 218 219 220class UsableChanges(Changes): 221 pass 222 223 224class ReportableChanges(Changes): 225 pass 226 227 228class Difference(object): 229 def __init__(self, want, have=None): 230 self.want = want 231 self.have = have 232 233 def compare(self, param): 234 try: 235 result = getattr(self, param) 236 return result 237 except AttributeError: 238 return self.__default(param) 239 240 def __default(self, param): 241 attr1 = getattr(self.want, param) 242 try: 243 attr2 = getattr(self.have, param) 244 if attr1 != attr2: 245 return attr1 246 except AttributeError: 247 return attr1 248 249 @property 250 def description(self): 251 result = cmp_str_with_none(self.want.description, self.have.description) 252 return result 253 254 @property 255 def dst_address(self): 256 result = cmp_str_with_none(self.want.dst_address, self.have.dst_address) 257 return result 258 259 @property 260 def src_address(self): 261 result = cmp_str_with_none(self.want.src_address, self.have.src_address) 262 return result 263 264 @property 265 def peers(self): 266 result = cmp_simple_list(self.want.peers, self.have.peers) 267 return result 268 269 270class BaseManager(object): 271 def __init__(self, *args, **kwargs): 272 self.module = kwargs.get('module', None) 273 self.client = F5RestClient(**self.module.params) 274 self.want = ModuleParameters(params=self.module.params) 275 self.have = ApiParameters() 276 self.changes = UsableChanges() 277 278 def _set_changed_options(self): 279 changed = {} 280 for key in Parameters.returnables: 281 if getattr(self.want, key) is not None: 282 changed[key] = getattr(self.want, key) 283 if changed: 284 self.changes = UsableChanges(params=changed) 285 286 def _update_changed_options(self): 287 diff = Difference(self.want, self.have) 288 updatables = Parameters.updatables 289 changed = dict() 290 for k in updatables: 291 change = diff.compare(k) 292 if change is None: 293 continue 294 else: 295 if isinstance(change, dict): 296 changed.update(change) 297 else: 298 changed[k] = change 299 if changed: 300 self.changes = UsableChanges(params=changed) 301 return True 302 return False 303 304 def _announce_deprecations(self, result): 305 warnings = result.pop('__warnings', []) 306 for warning in warnings: 307 self.client.module.deprecate( 308 msg=warning['msg'], 309 version=warning['version'] 310 ) 311 312 def exec_module(self): 313 start = datetime.now().isoformat() 314 version = tmos_version(self.client) 315 changed = False 316 result = dict() 317 state = self.want.state 318 319 if state == "present": 320 changed = self.present() 321 elif state == "absent": 322 changed = self.absent() 323 324 reportable = ReportableChanges(params=self.changes.to_return()) 325 changes = reportable.to_return() 326 result.update(**changes) 327 result.update(dict(changed=changed)) 328 self._announce_deprecations(result) 329 send_teem(start, self.client, self.module, version) 330 return result 331 332 def present(self): 333 if self.exists(): 334 return self.update() 335 else: 336 return self.create() 337 338 def absent(self): 339 if self.exists(): 340 return self.remove() 341 return False 342 343 def should_update(self): 344 result = self._update_changed_options() 345 if result: 346 return True 347 return False 348 349 def update(self): 350 self.have = self.read_current_from_device() 351 if not self.should_update(): 352 return False 353 if self.module.check_mode: 354 return True 355 self.update_on_device() 356 return True 357 358 def remove(self): 359 if self.module.check_mode: 360 return True 361 self.remove_from_device() 362 if self.exists(): 363 raise F5ModuleError("Failed to delete the resource.") 364 return True 365 366 def create(self): 367 self._set_changed_options() 368 if self.module.check_mode: 369 return True 370 self.create_on_device() 371 return True 372 373 374class GenericModuleManager(BaseManager): 375 def exists(self): 376 uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format( 377 self.client.provider['server'], 378 self.client.provider['server_port'], 379 transform_name(self.want.partition, self.want.name) 380 ) 381 resp = self.client.api.get(uri) 382 try: 383 response = resp.json() 384 except ValueError as ex: 385 raise F5ModuleError(str(ex)) 386 387 if resp.status == 404 or 'code' in response and response['code'] == 404: 388 return False 389 if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: 390 return True 391 392 errors = [401, 403, 409, 500, 501, 502, 503, 504] 393 394 if resp.status in errors or 'code' in response and response['code'] in errors: 395 if 'message' in response: 396 raise F5ModuleError(response['message']) 397 else: 398 raise F5ModuleError(resp.content) 399 400 def create_on_device(self): 401 params = self.changes.api_params() 402 params['name'] = self.want.name 403 params['partition'] = self.want.partition 404 uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/".format( 405 self.client.provider['server'], 406 self.client.provider['server_port'], 407 ) 408 resp = self.client.api.post(uri, json=params) 409 try: 410 response = resp.json() 411 except ValueError as ex: 412 raise F5ModuleError(str(ex)) 413 414 if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: 415 return True 416 raise F5ModuleError(resp.content) 417 418 def update_on_device(self): 419 params = self.changes.api_params() 420 uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format( 421 self.client.provider['server'], 422 self.client.provider['server_port'], 423 transform_name(self.want.partition, self.want.name) 424 ) 425 resp = self.client.api.patch(uri, json=params) 426 try: 427 response = resp.json() 428 except ValueError as ex: 429 raise F5ModuleError(str(ex)) 430 431 if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: 432 return True 433 raise F5ModuleError(resp.content) 434 435 def remove_from_device(self): 436 uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format( 437 self.client.provider['server'], 438 self.client.provider['server_port'], 439 transform_name(self.want.partition, self.want.name) 440 ) 441 response = self.client.api.delete(uri) 442 if response.status == 200: 443 return True 444 raise F5ModuleError(response.content) 445 446 def read_current_from_device(self): 447 uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format( 448 self.client.provider['server'], 449 self.client.provider['server_port'], 450 transform_name(self.want.partition, self.want.name) 451 ) 452 resp = self.client.api.get(uri) 453 try: 454 response = resp.json() 455 except ValueError as ex: 456 raise F5ModuleError(str(ex)) 457 458 if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: 459 return ApiParameters(params=response) 460 raise F5ModuleError(resp.content) 461 462 463class ModuleManager(object): 464 def __init__(self, *args, **kwargs): 465 self.module = kwargs.get('module', None) 466 self.client = F5RestClient(**self.module.params) 467 self.kwargs = kwargs 468 469 def version_less_than_14(self): 470 version = tmos_version(self.client) 471 if LooseVersion(version) < LooseVersion('14.0.0'): 472 return True 473 return False 474 475 def exec_module(self): 476 if self.version_less_than_14(): 477 raise F5ModuleError('Message routing is not supported on TMOS version below 14.x') 478 if self.module.params['type'] == 'generic': 479 manager = self.get_manager('generic') 480 else: 481 raise F5ModuleError( 482 "Unknown type specified." 483 ) 484 return manager.exec_module() 485 486 def get_manager(self, type): 487 if type == 'generic': 488 return GenericModuleManager(**self.kwargs) 489 490 491class ArgumentSpec(object): 492 def __init__(self): 493 self.supports_check_mode = True 494 argument_spec = dict( 495 name=dict(required=True), 496 description=dict(), 497 src_address=dict(), 498 dst_address=dict(), 499 peer_selection_mode=dict( 500 choices=['ratio', 'sequential'] 501 ), 502 peers=dict( 503 type='list', 504 elements='str', 505 ), 506 type=dict( 507 choices=['generic'], 508 default='generic' 509 ), 510 partition=dict( 511 default='Common', 512 fallback=(env_fallback, ['F5_PARTITION']) 513 ), 514 state=dict( 515 default='present', 516 choices=['present', 'absent'] 517 ) 518 519 ) 520 self.argument_spec = {} 521 self.argument_spec.update(f5_argument_spec) 522 self.argument_spec.update(argument_spec) 523 524 525def main(): 526 spec = ArgumentSpec() 527 528 module = AnsibleModule( 529 argument_spec=spec.argument_spec, 530 supports_check_mode=spec.supports_check_mode, 531 ) 532 533 try: 534 mm = ModuleManager(module=module) 535 results = mm.exec_module() 536 module.exit_json(**results) 537 except F5ModuleError as ex: 538 module.fail_json(msg=str(ex)) 539 540 541if __name__ == '__main__': 542 main() 543