1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# Copyright: (c) 2017, 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 10 11ANSIBLE_METADATA = {'metadata_version': '1.1', 12 'status': ['preview'], 13 'supported_by': 'certified'} 14 15DOCUMENTATION = r''' 16--- 17module: bigip_gtm_monitor_firepass 18short_description: Manages F5 BIG-IP GTM FirePass monitors 19description: 20 - Manages F5 BIG-IP GTM FirePass monitors. 21version_added: 2.6 22options: 23 name: 24 description: 25 - Monitor name. 26 type: str 27 required: True 28 parent: 29 description: 30 - The parent template of this monitor template. Once this value has 31 been set, it cannot be changed. By default, this value is the C(tcp) 32 parent on the C(Common) partition. 33 type: str 34 default: /Common/firepass_gtm 35 ip: 36 description: 37 - IP address part of the IP/port definition. If this parameter is not 38 provided when creating a new monitor, then the default value will be 39 '*'. 40 - If this value is an IP address, then a C(port) number must be specified. 41 type: str 42 port: 43 description: 44 - Port address part of the IP/port definition. If this parameter is not 45 provided when creating a new monitor, then the default value will be 46 '*'. Note that if specifying an IP address, a value between 1 and 65535 47 must be specified. 48 type: str 49 interval: 50 description: 51 - The interval specifying how frequently the monitor instance of this 52 template will run. 53 - If this parameter is not provided when creating a new monitor, then 54 the default value will be 30. 55 - This value B(must) be less than the C(timeout) value. 56 type: int 57 timeout: 58 description: 59 - The number of seconds in which the node or service must respond to 60 the monitor request. If the target responds within the set time 61 period, it is considered up. If the target does not respond within 62 the set time period, it is considered down. You can change this 63 number to any number you want, however, it should be 3 times the 64 interval number of seconds plus 1 second. 65 - If this parameter is not provided when creating a new monitor, then 66 the default value will be 90. 67 type: int 68 partition: 69 description: 70 - Device partition to manage resources on. 71 type: str 72 default: Common 73 state: 74 description: 75 - When C(present), ensures that the monitor exists. 76 - When C(absent), ensures the monitor is removed. 77 type: str 78 choices: 79 - present 80 - absent 81 default: present 82 probe_timeout: 83 description: 84 - Specifies the number of seconds after which the system times out the probe request 85 to the system. 86 - When creating a new monitor, if this parameter is not provided, then the default 87 value will be C(5). 88 type: int 89 ignore_down_response: 90 description: 91 - Specifies that the monitor allows more than one probe attempt per interval. 92 - When C(yes), specifies that the monitor ignores down responses for the duration of 93 the monitor timeout. Once the monitor timeout is reached without the system receiving 94 an up response, the system marks the object down. 95 - When C(no), specifies that the monitor immediately marks an object down when it 96 receives a down response. 97 - When creating a new monitor, if this parameter is not provided, then the default 98 value will be C(no). 99 type: bool 100 target_username: 101 description: 102 - Specifies the user name, if the monitored target requires authentication. 103 type: str 104 target_password: 105 description: 106 - Specifies the password, if the monitored target requires authentication. 107 type: str 108 update_password: 109 description: 110 - C(always) will update passwords if the C(target_password) is specified. 111 - C(on_create) will only set the password for newly created monitors. 112 type: str 113 choices: 114 - always 115 - on_create 116 default: always 117 cipher_list: 118 description: 119 - Specifies the list of ciphers for this monitor. 120 - The items in the cipher list are separated with the colon C(:) symbol. 121 - When creating a new monitor, if this parameter is not specified, the default 122 list is C(HIGH:!ADH). 123 type: str 124 max_load_average: 125 description: 126 - Specifies the number that the monitor uses to mark the Secure Access Manager 127 system up or down. 128 - The system compares the Max Load Average setting against a one-minute average 129 of the Secure Access Manager system load. 130 - When the Secure Access Manager system-load average falls within the specified 131 Max Load Average, the monitor marks the Secure Access Manager system up. 132 - When the average exceeds the setting, the monitor marks the system down. 133 - When creating a new monitor, if this parameter is not specified, the default 134 is C(12). 135 type: int 136 concurrency_limit: 137 description: 138 - Specifies the maximum percentage of licensed connections currently in use under 139 which the monitor marks the Secure Access Manager system up. 140 - As an example, a setting of 95 percent means that the monitor marks the Secure 141 Access Manager system up until 95 percent of licensed connections are in use. 142 - When the number of in-use licensed connections exceeds 95 percent, the monitor 143 marks the Secure Access Manager system down. 144 - When creating a new monitor, if this parameter is not specified, the default is C(95). 145 type: int 146extends_documentation_fragment: f5 147author: 148 - Tim Rupp (@caphrim007) 149 - Wojciech Wypior (@wojtek0806) 150''' 151 152EXAMPLES = r''' 153- name: Create a GTM FirePass monitor 154 bigip_gtm_monitor_firepass: 155 name: my_monitor 156 ip: 1.1.1.1 157 port: 80 158 state: present 159 provider: 160 user: admin 161 password: secret 162 server: lb.mydomain.com 163 delegate_to: localhost 164 165- name: Remove FirePass Monitor 166 bigip_gtm_monitor_firepass: 167 name: my_monitor 168 state: absent 169 provider: 170 user: admin 171 password: secret 172 server: lb.mydomain.com 173 delegate_to: localhost 174 175- name: Add FirePass monitor for all addresses, port 514 176 bigip_gtm_monitor_firepass: 177 name: my_monitor 178 port: 514 179 provider: 180 user: admin 181 password: secret 182 server: lb.mydomain.com 183 delegate_to: localhost 184''' 185 186RETURN = r''' 187parent: 188 description: New parent template of the monitor. 189 returned: changed 190 type: str 191 sample: firepass_gtm 192ip: 193 description: The new IP of IP/port definition. 194 returned: changed 195 type: str 196 sample: 10.12.13.14 197port: 198 description: The new port the monitor checks the resource on. 199 returned: changed 200 type: str 201 sample: 8080 202interval: 203 description: The new interval in which to run the monitor check. 204 returned: changed 205 type: int 206 sample: 2 207timeout: 208 description: The new timeout in which the remote system must respond to the monitor. 209 returned: changed 210 type: int 211 sample: 10 212ignore_down_response: 213 description: Whether to ignore the down response or not. 214 returned: changed 215 type: bool 216 sample: True 217probe_timeout: 218 description: The new timeout in which the system will timeout the monitor probe. 219 returned: changed 220 type: int 221 sample: 10 222cipher_list: 223 description: The new value for the cipher list. 224 returned: changed 225 type: str 226 sample: +3DES:+kEDH 227max_load_average: 228 description: The new value for the max load average. 229 returned: changed 230 type: int 231 sample: 12 232concurrency_limit: 233 description: The new value for the concurrency limit. 234 returned: changed 235 type: int 236 sample: 95 237''' 238 239from ansible.module_utils.basic import AnsibleModule 240from ansible.module_utils.basic import env_fallback 241 242try: 243 from library.module_utils.network.f5.bigip import F5RestClient 244 from library.module_utils.network.f5.common import F5ModuleError 245 from library.module_utils.network.f5.common import AnsibleF5Parameters 246 from library.module_utils.network.f5.common import fq_name 247 from library.module_utils.network.f5.common import f5_argument_spec 248 from library.module_utils.network.f5.common import transform_name 249 from library.module_utils.network.f5.icontrol import module_provisioned 250 from library.module_utils.network.f5.ipaddress import is_valid_ip 251except ImportError: 252 from ansible.module_utils.network.f5.bigip import F5RestClient 253 from ansible.module_utils.network.f5.common import F5ModuleError 254 from ansible.module_utils.network.f5.common import AnsibleF5Parameters 255 from ansible.module_utils.network.f5.common import fq_name 256 from ansible.module_utils.network.f5.common import f5_argument_spec 257 from ansible.module_utils.network.f5.common import transform_name 258 from ansible.module_utils.network.f5.icontrol import module_provisioned 259 from ansible.module_utils.network.f5.ipaddress import is_valid_ip 260 261 262class Parameters(AnsibleF5Parameters): 263 api_map = { 264 'defaultsFrom': 'parent', 265 'ignoreDownResponse': 'ignore_down_response', 266 'probeTimeout': 'probe_timeout', 267 'username': 'target_username', 268 'password': 'target_password', 269 'cipherlist': 'cipher_list', 270 'concurrencyLimit': 'concurrency_limit', 271 'maxLoadAverage': 'max_load_average', 272 } 273 274 api_attributes = [ 275 'defaultsFrom', 276 'interval', 277 'timeout', 278 'destination', 279 'probeTimeout', 280 'ignoreDownResponse', 281 'username', 282 'password', 283 'cipherlist', 284 'concurrencyLimit', 285 'maxLoadAverage', 286 ] 287 288 returnables = [ 289 'parent', 290 'ip', 291 'port', 292 'interval', 293 'timeout', 294 'probe_timeout', 295 'ignore_down_response', 296 'cipher_list', 297 'max_load_average', 298 'concurrency_limit', 299 ] 300 301 updatables = [ 302 'destination', 303 'interval', 304 'timeout', 305 'probe_timeout', 306 'ignore_down_response', 307 'ip', 308 'port', 309 'target_username', 310 'target_password', 311 'cipher_list', 312 'max_load_average', 313 'concurrency_limit', 314 ] 315 316 317class ApiParameters(Parameters): 318 @property 319 def ip(self): 320 ip, port = self._values['destination'].split(':') 321 return ip 322 323 @property 324 def port(self): 325 ip, port = self._values['destination'].split(':') 326 try: 327 return int(port) 328 except ValueError: 329 return port 330 331 @property 332 def ignore_down_response(self): 333 if self._values['ignore_down_response'] is None: 334 return None 335 if self._values['ignore_down_response'] == 'disabled': 336 return False 337 return True 338 339 340class ModuleParameters(Parameters): 341 @property 342 def interval(self): 343 if self._values['interval'] is None: 344 return None 345 if 1 > int(self._values['interval']) > 86400: 346 raise F5ModuleError( 347 "Interval value must be between 1 and 86400" 348 ) 349 return int(self._values['interval']) 350 351 @property 352 def timeout(self): 353 if self._values['timeout'] is None: 354 return None 355 return int(self._values['timeout']) 356 357 @property 358 def ip(self): # lgtm [py/similar-function] 359 if self._values['ip'] is None: 360 return None 361 if self._values['ip'] in ['*', '0.0.0.0']: 362 return '*' 363 elif is_valid_ip(self._values['ip']): 364 return self._values['ip'] 365 else: 366 raise F5ModuleError( 367 "The provided 'ip' parameter is not an IP address." 368 ) 369 370 @property 371 def parent(self): 372 if self._values['parent'] is None: 373 return None 374 result = fq_name(self.partition, self._values['parent']) 375 return result 376 377 @property 378 def port(self): 379 if self._values['port'] is None: 380 return None 381 elif self._values['port'] == '*': 382 return '*' 383 return int(self._values['port']) 384 385 @property 386 def destination(self): 387 if self.ip is None and self.port is None: 388 return None 389 destination = '{0}:{1}'.format(self.ip, self.port) 390 return destination 391 392 @destination.setter 393 def destination(self, value): 394 ip, port = value.split(':') 395 self._values['ip'] = ip 396 self._values['port'] = port 397 398 @property 399 def probe_timeout(self): 400 if self._values['probe_timeout'] is None: 401 return None 402 return int(self._values['probe_timeout']) 403 404 @property 405 def max_load_average(self): 406 if self._values['max_load_average'] is None: 407 return None 408 return int(self._values['max_load_average']) 409 410 @property 411 def concurrency_limit(self): 412 if self._values['concurrency_limit'] is None: 413 return None 414 return int(self._values['concurrency_limit']) 415 416 417class Changes(Parameters): 418 def to_return(self): 419 result = {} 420 try: 421 for returnable in self.returnables: 422 result[returnable] = getattr(self, returnable) 423 result = self._filter_params(result) 424 except Exception: 425 pass 426 return result 427 428 429class UsableChanges(Changes): 430 @property 431 def ignore_down_response(self): 432 if self._values['ignore_down_response'] is None: 433 return None 434 elif self._values['ignore_down_response'] is True: 435 return 'enabled' 436 return 'disabled' 437 438 439class ReportableChanges(Changes): 440 @property 441 def ip(self): 442 ip, port = self._values['destination'].split(':') 443 return ip 444 445 @property 446 def port(self): 447 ip, port = self._values['destination'].split(':') 448 return int(port) 449 450 @property 451 def ignore_down_response(self): 452 if self._values['ignore_down_response'] == 'enabled': 453 return True 454 return False 455 456 457class Difference(object): 458 def __init__(self, want, have=None): 459 self.want = want 460 self.have = have 461 462 def compare(self, param): 463 try: 464 result = getattr(self, param) 465 return result 466 except AttributeError: 467 return self.__default(param) 468 469 def __default(self, param): 470 attr1 = getattr(self.want, param) 471 try: 472 attr2 = getattr(self.have, param) 473 if attr1 != attr2: 474 return attr1 475 except AttributeError: 476 return attr1 477 478 @property 479 def parent(self): 480 if self.want.parent != self.have.parent: 481 raise F5ModuleError( 482 "The parent monitor cannot be changed" 483 ) 484 485 @property 486 def destination(self): 487 if self.want.ip is None and self.want.port is None: 488 return None 489 if self.want.port is None: 490 self.want.update({'port': self.have.port}) 491 if self.want.ip is None: 492 self.want.update({'ip': self.have.ip}) 493 494 if self.want.port in [None, '*'] and self.want.ip != '*': 495 raise F5ModuleError( 496 "Specifying an IP address requires that a port number be specified" 497 ) 498 499 if self.want.destination != self.have.destination: 500 return self.want.destination 501 502 @property 503 def interval(self): 504 if self.want.timeout is not None and self.want.interval is not None: 505 if self.want.interval >= self.want.timeout: 506 raise F5ModuleError( 507 "Parameter 'interval' must be less than 'timeout'." 508 ) 509 elif self.want.timeout is not None: 510 if self.have.interval >= self.want.timeout: 511 raise F5ModuleError( 512 "Parameter 'interval' must be less than 'timeout'." 513 ) 514 elif self.want.interval is not None: 515 if self.want.interval >= self.have.timeout: 516 raise F5ModuleError( 517 "Parameter 'interval' must be less than 'timeout'." 518 ) 519 if self.want.interval != self.have.interval: 520 return self.want.interval 521 522 @property 523 def target_password(self): 524 if self.want.target_password != self.have.target_password: 525 if self.want.update_password == 'always': 526 result = self.want.target_password 527 return result 528 529 530class ModuleManager(object): 531 def __init__(self, *args, **kwargs): 532 self.module = kwargs.get('module', None) 533 self.client = F5RestClient(**self.module.params) 534 self.want = ModuleParameters(params=self.module.params) 535 self.have = ApiParameters() 536 self.changes = UsableChanges() 537 538 def _set_changed_options(self): 539 changed = {} 540 for key in Parameters.returnables: 541 if getattr(self.want, key) is not None: 542 changed[key] = getattr(self.want, key) 543 if changed: 544 self.changes = UsableChanges(params=changed) 545 546 def _update_changed_options(self): 547 diff = Difference(self.want, self.have) 548 updatables = Parameters.updatables 549 changed = dict() 550 for k in updatables: 551 change = diff.compare(k) 552 if change is None: 553 continue 554 else: 555 if isinstance(change, dict): 556 changed.update(change) 557 else: 558 changed[k] = change 559 if changed: 560 self.changes = UsableChanges(params=changed) 561 return True 562 return False 563 564 def _announce_deprecations(self, result): 565 warnings = result.pop('__warnings', []) 566 for warning in warnings: 567 self.client.module.deprecate( 568 msg=warning['msg'], 569 version=warning['version'] 570 ) 571 572 def _set_default_creation_values(self): 573 if self.want.timeout is None: 574 self.want.update({'timeout': 90}) 575 if self.want.interval is None: 576 self.want.update({'interval': 30}) 577 if self.want.probe_timeout is None: 578 self.want.update({'probe_timeout': 5}) 579 if self.want.ip is None: 580 self.want.update({'ip': '*'}) 581 if self.want.port is None: 582 self.want.update({'port': '*'}) 583 if self.want.ignore_down_response is None: 584 self.want.update({'ignore_down_response': False}) 585 if self.want.cipher_list is None: 586 self.want.update({'cipher_list': 'HIGH:!ADH'}) 587 if self.want.max_load_average is None: 588 self.want.update({'max_load_average': 12}) 589 if self.want.concurrency_limit is None: 590 self.want.update({'concurrency_limit': 95}) 591 592 def exec_module(self): 593 if not module_provisioned(self.client, 'gtm'): 594 raise F5ModuleError( 595 "GTM must be provisioned to use this module." 596 ) 597 changed = False 598 result = dict() 599 state = self.want.state 600 601 if state == "present": 602 changed = self.present() 603 elif state == "absent": 604 changed = self.absent() 605 606 reportable = ReportableChanges(params=self.changes.to_return()) 607 changes = reportable.to_return() 608 result.update(**changes) 609 result.update(dict(changed=changed)) 610 self._announce_deprecations(result) 611 return result 612 613 def present(self): 614 if self.exists(): 615 return self.update() 616 else: 617 return self.create() 618 619 def absent(self): 620 if self.exists(): 621 return self.remove() 622 return False 623 624 def should_update(self): 625 result = self._update_changed_options() 626 if result: 627 return True 628 return False 629 630 def update(self): 631 self.have = self.read_current_from_device() 632 if not self.should_update(): 633 return False 634 if self.module.check_mode: 635 return True 636 self.update_on_device() 637 return True 638 639 def remove(self): 640 if self.module.check_mode: 641 return True 642 self.remove_from_device() 643 if self.exists(): 644 raise F5ModuleError("Failed to delete the resource.") 645 return True 646 647 def create(self): 648 self._set_default_creation_values() 649 self._set_changed_options() 650 if self.module.check_mode: 651 return True 652 self.create_on_device() 653 return True 654 655 def exists(self): 656 uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format( 657 self.client.provider['server'], 658 self.client.provider['server_port'], 659 transform_name(self.want.partition, self.want.name), 660 ) 661 resp = self.client.api.get(uri) 662 try: 663 response = resp.json() 664 except ValueError: 665 return False 666 if resp.status == 404 or 'code' in response and response['code'] == 404: 667 return False 668 return True 669 670 def create_on_device(self): 671 params = self.changes.api_params() 672 params['name'] = self.want.name 673 params['partition'] = self.want.partition 674 uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/".format( 675 self.client.provider['server'], 676 self.client.provider['server_port'], 677 ) 678 resp = self.client.api.post(uri, json=params) 679 try: 680 response = resp.json() 681 except ValueError as ex: 682 raise F5ModuleError(str(ex)) 683 684 if 'code' in response and response['code'] in [400, 403]: 685 if 'message' in response: 686 raise F5ModuleError(response['message']) 687 else: 688 raise F5ModuleError(resp.content) 689 return response['selfLink'] 690 691 def update_on_device(self): 692 params = self.changes.api_params() 693 uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format( 694 self.client.provider['server'], 695 self.client.provider['server_port'], 696 transform_name(self.want.partition, self.want.name), 697 ) 698 resp = self.client.api.patch(uri, json=params) 699 try: 700 response = resp.json() 701 except ValueError as ex: 702 raise F5ModuleError(str(ex)) 703 704 if 'code' in response and response['code'] == 400: 705 if 'message' in response: 706 raise F5ModuleError(response['message']) 707 else: 708 raise F5ModuleError(resp.content) 709 710 def remove_from_device(self): 711 uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format( 712 self.client.provider['server'], 713 self.client.provider['server_port'], 714 transform_name(self.want.partition, self.want.name), 715 ) 716 response = self.client.api.delete(uri) 717 if response.status == 200: 718 return True 719 raise F5ModuleError(response.content) 720 721 def read_current_from_device(self): 722 uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format( 723 self.client.provider['server'], 724 self.client.provider['server_port'], 725 transform_name(self.want.partition, self.want.name), 726 ) 727 resp = self.client.api.get(uri) 728 try: 729 response = resp.json() 730 except ValueError as ex: 731 raise F5ModuleError(str(ex)) 732 733 if 'code' in response and response['code'] == 400: 734 if 'message' in response: 735 raise F5ModuleError(response['message']) 736 else: 737 raise F5ModuleError(resp.content) 738 return ApiParameters(params=response) 739 740 741class ArgumentSpec(object): 742 def __init__(self): 743 self.supports_check_mode = True 744 argument_spec = dict( 745 name=dict(required=True), 746 parent=dict(default='/Common/firepass_gtm'), 747 ip=dict(), 748 port=dict(), 749 interval=dict(type='int'), 750 timeout=dict(type='int'), 751 ignore_down_response=dict(type='bool'), 752 probe_timeout=dict(type='int'), 753 target_username=dict(), 754 target_password=dict(no_log=True), 755 cipher_list=dict(), 756 update_password=dict( 757 default='always', 758 choices=['always', 'on_create'] 759 ), 760 max_load_average=dict(type='int'), 761 concurrency_limit=dict(type='int'), 762 state=dict( 763 default='present', 764 choices=['present', 'absent'] 765 ), 766 partition=dict( 767 default='Common', 768 fallback=(env_fallback, ['F5_PARTITION']) 769 ) 770 ) 771 self.argument_spec = {} 772 self.argument_spec.update(f5_argument_spec) 773 self.argument_spec.update(argument_spec) 774 775 776def main(): 777 spec = ArgumentSpec() 778 779 module = AnsibleModule( 780 argument_spec=spec.argument_spec, 781 supports_check_mode=spec.supports_check_mode, 782 ) 783 784 try: 785 mm = ModuleManager(module=module) 786 results = mm.exec_module() 787 module.exit_json(**results) 788 except F5ModuleError as ex: 789 module.fail_json(msg=str(ex)) 790 791 792if __name__ == '__main__': 793 main() 794