1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# This file is part of Ansible 4# 5# Ansible is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Ansible is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 17 18from __future__ import absolute_import, division, print_function 19__metaclass__ = type 20 21DOCUMENTATION = ''' 22--- 23module: oneandone_load_balancer 24short_description: Configure 1&1 load balancer. 25description: 26 - Create, remove, update load balancers. 27 This module has a dependency on 1and1 >= 1.0 28options: 29 state: 30 description: 31 - Define a load balancer state to create, remove, or update. 32 type: str 33 required: false 34 default: 'present' 35 choices: [ "present", "absent", "update" ] 36 auth_token: 37 description: 38 - Authenticating API token provided by 1&1. 39 type: str 40 load_balancer: 41 description: 42 - The identifier (id or name) of the load balancer used with update state. 43 type: str 44 api_url: 45 description: 46 - Custom API URL. Overrides the 47 ONEANDONE_API_URL environment variable. 48 type: str 49 required: false 50 name: 51 description: 52 - Load balancer name used with present state. Used as identifier (id or name) when used with absent state. 53 maxLength=128 54 type: str 55 health_check_test: 56 description: 57 - Type of the health check. At the moment, HTTP is not allowed. 58 type: str 59 choices: [ "NONE", "TCP", "HTTP", "ICMP" ] 60 health_check_interval: 61 description: 62 - Health check period in seconds. minimum=5, maximum=300, multipleOf=1 63 type: str 64 health_check_path: 65 description: 66 - Url to call for checking. Required for HTTP health check. maxLength=1000 67 type: str 68 required: false 69 health_check_parse: 70 description: 71 - Regular expression to check. Required for HTTP health check. maxLength=64 72 type: str 73 required: false 74 persistence: 75 description: 76 - Persistence. 77 type: bool 78 persistence_time: 79 description: 80 - Persistence time in seconds. Required if persistence is enabled. minimum=30, maximum=1200, multipleOf=1 81 type: str 82 method: 83 description: 84 - Balancing procedure. 85 type: str 86 choices: [ "ROUND_ROBIN", "LEAST_CONNECTIONS" ] 87 datacenter: 88 description: 89 - ID or country code of the datacenter where the load balancer will be created. 90 - If not specified, it defaults to I(US). 91 type: str 92 choices: [ "US", "ES", "DE", "GB" ] 93 required: false 94 rules: 95 description: 96 - A list of rule objects that will be set for the load balancer. Each rule must contain protocol, 97 port_balancer, and port_server parameters, in addition to source parameter, which is optional. 98 type: list 99 elements: dict 100 description: 101 description: 102 - Description of the load balancer. maxLength=256 103 type: str 104 required: false 105 add_server_ips: 106 description: 107 - A list of server identifiers (id or name) to be assigned to a load balancer. 108 Used in combination with update state. 109 type: list 110 elements: str 111 required: false 112 remove_server_ips: 113 description: 114 - A list of server IP ids to be unassigned from a load balancer. Used in combination with update state. 115 type: list 116 elements: str 117 required: false 118 add_rules: 119 description: 120 - A list of rules that will be added to an existing load balancer. 121 It is syntax is the same as the one used for rules parameter. Used in combination with update state. 122 type: list 123 elements: dict 124 required: false 125 remove_rules: 126 description: 127 - A list of rule ids that will be removed from an existing load balancer. Used in combination with update state. 128 type: list 129 elements: str 130 required: false 131 wait: 132 description: 133 - wait for the instance to be in state 'running' before returning 134 required: false 135 default: "yes" 136 type: bool 137 wait_timeout: 138 description: 139 - how long before wait gives up, in seconds 140 type: int 141 default: 600 142 wait_interval: 143 description: 144 - Defines the number of seconds to wait when using the _wait_for methods 145 type: int 146 default: 5 147 148requirements: 149 - "1and1" 150 - "python >= 2.6" 151 152author: 153 - Amel Ajdinovic (@aajdinov) 154 - Ethan Devenport (@edevenport) 155''' 156 157EXAMPLES = ''' 158- name: Create a load balancer 159 community.general.oneandone_load_balancer: 160 auth_token: oneandone_private_api_key 161 name: ansible load balancer 162 description: Testing creation of load balancer with ansible 163 health_check_test: TCP 164 health_check_interval: 40 165 persistence: true 166 persistence_time: 1200 167 method: ROUND_ROBIN 168 datacenter: US 169 rules: 170 - 171 protocol: TCP 172 port_balancer: 80 173 port_server: 80 174 source: 0.0.0.0 175 wait: true 176 wait_timeout: 500 177 178- name: Destroy a load balancer 179 community.general.oneandone_load_balancer: 180 auth_token: oneandone_private_api_key 181 name: ansible load balancer 182 wait: true 183 wait_timeout: 500 184 state: absent 185 186- name: Update a load balancer 187 community.general.oneandone_load_balancer: 188 auth_token: oneandone_private_api_key 189 load_balancer: ansible load balancer 190 name: ansible load balancer updated 191 description: Testing the update of a load balancer with ansible 192 wait: true 193 wait_timeout: 500 194 state: update 195 196- name: Add server to a load balancer 197 community.general.oneandone_load_balancer: 198 auth_token: oneandone_private_api_key 199 load_balancer: ansible load balancer updated 200 description: Adding server to a load balancer with ansible 201 add_server_ips: 202 - server identifier (id or name) 203 wait: true 204 wait_timeout: 500 205 state: update 206 207- name: Remove server from a load balancer 208 community.general.oneandone_load_balancer: 209 auth_token: oneandone_private_api_key 210 load_balancer: ansible load balancer updated 211 description: Removing server from a load balancer with ansible 212 remove_server_ips: 213 - B2504878540DBC5F7634EB00A07C1EBD (server's ip id) 214 wait: true 215 wait_timeout: 500 216 state: update 217 218- name: Add rules to a load balancer 219 community.general.oneandone_load_balancer: 220 auth_token: oneandone_private_api_key 221 load_balancer: ansible load balancer updated 222 description: Adding rules to a load balancer with ansible 223 add_rules: 224 - 225 protocol: TCP 226 port_balancer: 70 227 port_server: 70 228 source: 0.0.0.0 229 - 230 protocol: TCP 231 port_balancer: 60 232 port_server: 60 233 source: 0.0.0.0 234 wait: true 235 wait_timeout: 500 236 state: update 237 238- name: Remove rules from a load balancer 239 community.general.oneandone_load_balancer: 240 auth_token: oneandone_private_api_key 241 load_balancer: ansible load balancer updated 242 description: Adding rules to a load balancer with ansible 243 remove_rules: 244 - rule_id #1 245 - rule_id #2 246 - ... 247 wait: true 248 wait_timeout: 500 249 state: update 250''' 251 252RETURN = ''' 253load_balancer: 254 description: Information about the load balancer that was processed 255 type: dict 256 sample: '{"id": "92B74394A397ECC3359825C1656D67A6", "name": "Default Balancer"}' 257 returned: always 258''' 259 260import os 261from ansible.module_utils.basic import AnsibleModule 262from ansible_collections.community.general.plugins.module_utils.oneandone import ( 263 get_load_balancer, 264 get_server, 265 get_datacenter, 266 OneAndOneResources, 267 wait_for_resource_creation_completion 268) 269 270HAS_ONEANDONE_SDK = True 271 272try: 273 import oneandone.client 274except ImportError: 275 HAS_ONEANDONE_SDK = False 276 277DATACENTERS = ['US', 'ES', 'DE', 'GB'] 278HEALTH_CHECK_TESTS = ['NONE', 'TCP', 'HTTP', 'ICMP'] 279METHODS = ['ROUND_ROBIN', 'LEAST_CONNECTIONS'] 280 281 282def _check_mode(module, result): 283 if module.check_mode: 284 module.exit_json( 285 changed=result 286 ) 287 288 289def _add_server_ips(module, oneandone_conn, load_balancer_id, server_ids): 290 """ 291 Assigns servers to a load balancer. 292 """ 293 try: 294 attach_servers = [] 295 296 for server_id in server_ids: 297 server = get_server(oneandone_conn, server_id, True) 298 attach_server = oneandone.client.AttachServer( 299 server_id=server['id'], 300 server_ip_id=next(iter(server['ips'] or []), None)['id'] 301 ) 302 attach_servers.append(attach_server) 303 304 if module.check_mode: 305 if attach_servers: 306 return True 307 return False 308 309 load_balancer = oneandone_conn.attach_load_balancer_server( 310 load_balancer_id=load_balancer_id, 311 server_ips=attach_servers) 312 return load_balancer 313 except Exception as ex: 314 module.fail_json(msg=str(ex)) 315 316 317def _remove_load_balancer_server(module, oneandone_conn, load_balancer_id, server_ip_id): 318 """ 319 Unassigns a server/IP from a load balancer. 320 """ 321 try: 322 if module.check_mode: 323 lb_server = oneandone_conn.get_load_balancer_server( 324 load_balancer_id=load_balancer_id, 325 server_ip_id=server_ip_id) 326 if lb_server: 327 return True 328 return False 329 330 load_balancer = oneandone_conn.remove_load_balancer_server( 331 load_balancer_id=load_balancer_id, 332 server_ip_id=server_ip_id) 333 return load_balancer 334 except Exception as ex: 335 module.fail_json(msg=str(ex)) 336 337 338def _add_load_balancer_rules(module, oneandone_conn, load_balancer_id, rules): 339 """ 340 Adds new rules to a load_balancer. 341 """ 342 try: 343 load_balancer_rules = [] 344 345 for rule in rules: 346 load_balancer_rule = oneandone.client.LoadBalancerRule( 347 protocol=rule['protocol'], 348 port_balancer=rule['port_balancer'], 349 port_server=rule['port_server'], 350 source=rule['source']) 351 load_balancer_rules.append(load_balancer_rule) 352 353 if module.check_mode: 354 lb_id = get_load_balancer(oneandone_conn, load_balancer_id) 355 if (load_balancer_rules and lb_id): 356 return True 357 return False 358 359 load_balancer = oneandone_conn.add_load_balancer_rule( 360 load_balancer_id=load_balancer_id, 361 load_balancer_rules=load_balancer_rules 362 ) 363 364 return load_balancer 365 except Exception as ex: 366 module.fail_json(msg=str(ex)) 367 368 369def _remove_load_balancer_rule(module, oneandone_conn, load_balancer_id, rule_id): 370 """ 371 Removes a rule from a load_balancer. 372 """ 373 try: 374 if module.check_mode: 375 rule = oneandone_conn.get_load_balancer_rule( 376 load_balancer_id=load_balancer_id, 377 rule_id=rule_id) 378 if rule: 379 return True 380 return False 381 382 load_balancer = oneandone_conn.remove_load_balancer_rule( 383 load_balancer_id=load_balancer_id, 384 rule_id=rule_id 385 ) 386 return load_balancer 387 except Exception as ex: 388 module.fail_json(msg=str(ex)) 389 390 391def update_load_balancer(module, oneandone_conn): 392 """ 393 Updates a load_balancer based on input arguments. 394 Load balancer rules and server ips can be added/removed to/from 395 load balancer. Load balancer name, description, health_check_test, 396 health_check_interval, persistence, persistence_time, and method 397 can be updated as well. 398 399 module : AnsibleModule object 400 oneandone_conn: authenticated oneandone object 401 """ 402 load_balancer_id = module.params.get('load_balancer') 403 name = module.params.get('name') 404 description = module.params.get('description') 405 health_check_test = module.params.get('health_check_test') 406 health_check_interval = module.params.get('health_check_interval') 407 health_check_path = module.params.get('health_check_path') 408 health_check_parse = module.params.get('health_check_parse') 409 persistence = module.params.get('persistence') 410 persistence_time = module.params.get('persistence_time') 411 method = module.params.get('method') 412 add_server_ips = module.params.get('add_server_ips') 413 remove_server_ips = module.params.get('remove_server_ips') 414 add_rules = module.params.get('add_rules') 415 remove_rules = module.params.get('remove_rules') 416 417 changed = False 418 419 load_balancer = get_load_balancer(oneandone_conn, load_balancer_id, True) 420 if load_balancer is None: 421 _check_mode(module, False) 422 423 if (name or description or health_check_test or health_check_interval or health_check_path or 424 health_check_parse or persistence or persistence_time or method): 425 _check_mode(module, True) 426 load_balancer = oneandone_conn.modify_load_balancer( 427 load_balancer_id=load_balancer['id'], 428 name=name, 429 description=description, 430 health_check_test=health_check_test, 431 health_check_interval=health_check_interval, 432 health_check_path=health_check_path, 433 health_check_parse=health_check_parse, 434 persistence=persistence, 435 persistence_time=persistence_time, 436 method=method) 437 changed = True 438 439 if add_server_ips: 440 if module.check_mode: 441 _check_mode(module, _add_server_ips(module, 442 oneandone_conn, 443 load_balancer['id'], 444 add_server_ips)) 445 446 load_balancer = _add_server_ips(module, oneandone_conn, load_balancer['id'], add_server_ips) 447 changed = True 448 449 if remove_server_ips: 450 chk_changed = False 451 for server_ip_id in remove_server_ips: 452 if module.check_mode: 453 chk_changed |= _remove_load_balancer_server(module, 454 oneandone_conn, 455 load_balancer['id'], 456 server_ip_id) 457 458 _remove_load_balancer_server(module, 459 oneandone_conn, 460 load_balancer['id'], 461 server_ip_id) 462 _check_mode(module, chk_changed) 463 load_balancer = get_load_balancer(oneandone_conn, load_balancer['id'], True) 464 changed = True 465 466 if add_rules: 467 load_balancer = _add_load_balancer_rules(module, 468 oneandone_conn, 469 load_balancer['id'], 470 add_rules) 471 _check_mode(module, load_balancer) 472 changed = True 473 474 if remove_rules: 475 chk_changed = False 476 for rule_id in remove_rules: 477 if module.check_mode: 478 chk_changed |= _remove_load_balancer_rule(module, 479 oneandone_conn, 480 load_balancer['id'], 481 rule_id) 482 483 _remove_load_balancer_rule(module, 484 oneandone_conn, 485 load_balancer['id'], 486 rule_id) 487 _check_mode(module, chk_changed) 488 load_balancer = get_load_balancer(oneandone_conn, load_balancer['id'], True) 489 changed = True 490 491 try: 492 return (changed, load_balancer) 493 except Exception as ex: 494 module.fail_json(msg=str(ex)) 495 496 497def create_load_balancer(module, oneandone_conn): 498 """ 499 Create a new load_balancer. 500 501 module : AnsibleModule object 502 oneandone_conn: authenticated oneandone object 503 """ 504 try: 505 name = module.params.get('name') 506 description = module.params.get('description') 507 health_check_test = module.params.get('health_check_test') 508 health_check_interval = module.params.get('health_check_interval') 509 health_check_path = module.params.get('health_check_path') 510 health_check_parse = module.params.get('health_check_parse') 511 persistence = module.params.get('persistence') 512 persistence_time = module.params.get('persistence_time') 513 method = module.params.get('method') 514 datacenter = module.params.get('datacenter') 515 rules = module.params.get('rules') 516 wait = module.params.get('wait') 517 wait_timeout = module.params.get('wait_timeout') 518 wait_interval = module.params.get('wait_interval') 519 520 load_balancer_rules = [] 521 522 datacenter_id = None 523 if datacenter is not None: 524 datacenter_id = get_datacenter(oneandone_conn, datacenter) 525 if datacenter_id is None: 526 module.fail_json( 527 msg='datacenter %s not found.' % datacenter) 528 529 for rule in rules: 530 load_balancer_rule = oneandone.client.LoadBalancerRule( 531 protocol=rule['protocol'], 532 port_balancer=rule['port_balancer'], 533 port_server=rule['port_server'], 534 source=rule['source']) 535 load_balancer_rules.append(load_balancer_rule) 536 537 _check_mode(module, True) 538 load_balancer_obj = oneandone.client.LoadBalancer( 539 health_check_path=health_check_path, 540 health_check_parse=health_check_parse, 541 name=name, 542 description=description, 543 health_check_test=health_check_test, 544 health_check_interval=health_check_interval, 545 persistence=persistence, 546 persistence_time=persistence_time, 547 method=method, 548 datacenter_id=datacenter_id 549 ) 550 551 load_balancer = oneandone_conn.create_load_balancer( 552 load_balancer=load_balancer_obj, 553 load_balancer_rules=load_balancer_rules 554 ) 555 556 if wait: 557 wait_for_resource_creation_completion(oneandone_conn, 558 OneAndOneResources.load_balancer, 559 load_balancer['id'], 560 wait_timeout, 561 wait_interval) 562 563 load_balancer = get_load_balancer(oneandone_conn, load_balancer['id'], True) # refresh 564 changed = True if load_balancer else False 565 566 _check_mode(module, False) 567 568 return (changed, load_balancer) 569 except Exception as ex: 570 module.fail_json(msg=str(ex)) 571 572 573def remove_load_balancer(module, oneandone_conn): 574 """ 575 Removes a load_balancer. 576 577 module : AnsibleModule object 578 oneandone_conn: authenticated oneandone object 579 """ 580 try: 581 lb_id = module.params.get('name') 582 load_balancer_id = get_load_balancer(oneandone_conn, lb_id) 583 if module.check_mode: 584 if load_balancer_id is None: 585 _check_mode(module, False) 586 _check_mode(module, True) 587 load_balancer = oneandone_conn.delete_load_balancer(load_balancer_id) 588 589 changed = True if load_balancer else False 590 591 return (changed, { 592 'id': load_balancer['id'], 593 'name': load_balancer['name'] 594 }) 595 except Exception as ex: 596 module.fail_json(msg=str(ex)) 597 598 599def main(): 600 module = AnsibleModule( 601 argument_spec=dict( 602 auth_token=dict( 603 type='str', no_log=True, 604 default=os.environ.get('ONEANDONE_AUTH_TOKEN')), 605 api_url=dict( 606 type='str', 607 default=os.environ.get('ONEANDONE_API_URL')), 608 load_balancer=dict(type='str'), 609 name=dict(type='str'), 610 description=dict(type='str'), 611 health_check_test=dict( 612 choices=HEALTH_CHECK_TESTS), 613 health_check_interval=dict(type='str'), 614 health_check_path=dict(type='str'), 615 health_check_parse=dict(type='str'), 616 persistence=dict(type='bool'), 617 persistence_time=dict(type='str'), 618 method=dict( 619 choices=METHODS), 620 datacenter=dict( 621 choices=DATACENTERS), 622 rules=dict(type='list', elements="dict", default=[]), 623 add_server_ips=dict(type='list', elements="str", default=[]), 624 remove_server_ips=dict(type='list', elements="str", default=[]), 625 add_rules=dict(type='list', elements="dict", default=[]), 626 remove_rules=dict(type='list', elements="str", default=[]), 627 wait=dict(type='bool', default=True), 628 wait_timeout=dict(type='int', default=600), 629 wait_interval=dict(type='int', default=5), 630 state=dict(type='str', default='present', choices=['present', 'absent', 'update']), 631 ), 632 supports_check_mode=True 633 ) 634 635 if not HAS_ONEANDONE_SDK: 636 module.fail_json(msg='1and1 required for this module') 637 638 if not module.params.get('auth_token'): 639 module.fail_json( 640 msg='auth_token parameter is required.') 641 642 if not module.params.get('api_url'): 643 oneandone_conn = oneandone.client.OneAndOneService( 644 api_token=module.params.get('auth_token')) 645 else: 646 oneandone_conn = oneandone.client.OneAndOneService( 647 api_token=module.params.get('auth_token'), api_url=module.params.get('api_url')) 648 649 state = module.params.get('state') 650 651 if state == 'absent': 652 if not module.params.get('name'): 653 module.fail_json( 654 msg="'name' parameter is required for deleting a load balancer.") 655 try: 656 (changed, load_balancer) = remove_load_balancer(module, oneandone_conn) 657 except Exception as ex: 658 module.fail_json(msg=str(ex)) 659 elif state == 'update': 660 if not module.params.get('load_balancer'): 661 module.fail_json( 662 msg="'load_balancer' parameter is required for updating a load balancer.") 663 try: 664 (changed, load_balancer) = update_load_balancer(module, oneandone_conn) 665 except Exception as ex: 666 module.fail_json(msg=str(ex)) 667 668 elif state == 'present': 669 for param in ('name', 'health_check_test', 'health_check_interval', 'persistence', 670 'persistence_time', 'method', 'rules'): 671 if not module.params.get(param): 672 module.fail_json( 673 msg="%s parameter is required for new load balancers." % param) 674 try: 675 (changed, load_balancer) = create_load_balancer(module, oneandone_conn) 676 except Exception as ex: 677 module.fail_json(msg=str(ex)) 678 679 module.exit_json(changed=changed, load_balancer=load_balancer) 680 681 682if __name__ == '__main__': 683 main() 684