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