1#!/usr/bin/python
2
3# Copyright (c) 2018 Catalyst Cloud Ltd.
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7
8__metaclass__ = type
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14DOCUMENTATION = '''
15---
16module: os_loadbalancer
17short_description: Add/Delete load balancer from OpenStack Cloud
18extends_documentation_fragment: openstack
19version_added: "2.7"
20author: "Lingxian Kong (@lingxiankong)"
21description:
22  - Add or Remove load balancer from the OpenStack load-balancer
23    service(Octavia). Load balancer update is not supported for now.
24options:
25  name:
26    description:
27      - Name that has to be given to the load balancer
28    required: true
29  state:
30    description:
31      - Should the resource be present or absent.
32    choices: [present, absent]
33    default: present
34  vip_network:
35    description:
36      - The name or id of the network for the virtual IP of the load balancer.
37        One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified
38        for creation.
39  vip_subnet:
40    description:
41      - The name or id of the subnet for the virtual IP of the load balancer.
42        One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified
43        for creation.
44  vip_port:
45    description:
46      - The name or id of the load balancer virtual IP port. One of
47        I(vip_network), I(vip_subnet), or I(vip_port) must be specified for
48        creation.
49  vip_address:
50    description:
51      - IP address of the load balancer virtual IP.
52  public_ip_address:
53    description:
54      - Public IP address associated with the VIP.
55  auto_public_ip:
56    description:
57      - Allocate a public IP address and associate with the VIP automatically.
58    type: bool
59    default: 'no'
60  public_network:
61    description:
62      - The name or ID of a Neutron external network.
63  delete_public_ip:
64    description:
65      - When C(state=absent) and this option is true, any public IP address
66        associated with the VIP will be deleted along with the load balancer.
67    type: bool
68    default: 'no'
69  listeners:
70    description:
71      - A list of listeners that attached to the load balancer.
72    suboptions:
73      name:
74        description:
75          - The listener name or ID.
76      protocol:
77        description:
78          - The protocol for the listener.
79        default: HTTP
80      protocol_port:
81        description:
82          - The protocol port number for the listener.
83        default: 80
84      pool:
85        description:
86          - The pool attached to the listener.
87        suboptions:
88          name:
89            description:
90              - The pool name or ID.
91          protocol:
92            description:
93              - The protocol for the pool.
94            default: HTTP
95          lb_algorithm:
96            description:
97              - The load balancing algorithm for the pool.
98            default: ROUND_ROBIN
99          members:
100            description:
101              - A list of members that added to the pool.
102            suboptions:
103              name:
104                description:
105                  - The member name or ID.
106              address:
107                description:
108                  - The IP address of the member.
109              protocol_port:
110                description:
111                  - The protocol port number for the member.
112                default: 80
113              subnet:
114                description:
115                  - The name or ID of the subnet the member service is
116                    accessible from.
117  wait:
118    description:
119      - If the module should wait for the load balancer to be created or
120        deleted.
121    type: bool
122    default: 'yes'
123  timeout:
124    description:
125      - The amount of time the module should wait.
126    default: 180
127  availability_zone:
128    description:
129      - Ignored. Present for backwards compatibility
130requirements: ["openstacksdk"]
131'''
132
133RETURN = '''
134id:
135    description: The load balancer UUID.
136    returned: On success when C(state=present)
137    type: str
138    sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69"
139loadbalancer:
140    description: Dictionary describing the load balancer.
141    returned: On success when C(state=present)
142    type: complex
143    contains:
144        id:
145            description: Unique UUID.
146            type: str
147            sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69"
148        name:
149            description: Name given to the load balancer.
150            type: str
151            sample: "lingxian_test"
152        vip_network_id:
153            description: Network ID the load balancer virtual IP port belongs in.
154            type: str
155            sample: "f171db43-56fd-41cf-82d7-4e91d741762e"
156        vip_subnet_id:
157            description: Subnet ID the load balancer virtual IP port belongs in.
158            type: str
159            sample: "c53e3c70-9d62-409a-9f71-db148e7aa853"
160        vip_port_id:
161            description: The load balancer virtual IP port ID.
162            type: str
163            sample: "2061395c-1c01-47ab-b925-c91b93df9c1d"
164        vip_address:
165            description: The load balancer virtual IP address.
166            type: str
167            sample: "192.168.2.88"
168        public_vip_address:
169            description: The load balancer public VIP address.
170            type: str
171            sample: "10.17.8.254"
172        provisioning_status:
173            description: The provisioning status of the load balancer.
174            type: str
175            sample: "ACTIVE"
176        operating_status:
177            description: The operating status of the load balancer.
178            type: str
179            sample: "ONLINE"
180        is_admin_state_up:
181            description: The administrative state of the load balancer.
182            type: bool
183            sample: true
184        listeners:
185            description: The associated listener IDs, if any.
186            type: list
187            sample: [{"id": "7aa1b380-beec-459c-a8a7-3a4fb6d30645"}, {"id": "692d06b8-c4f8-4bdb-b2a3-5a263cc23ba6"}]
188        pools:
189            description: The associated pool IDs, if any.
190            type: list
191            sample: [{"id": "27b78d92-cee1-4646-b831-e3b90a7fa714"}, {"id": "befc1fb5-1992-4697-bdb9-eee330989344"}]
192'''
193
194EXAMPLES = '''
195# Create a load balancer by specifying the VIP subnet.
196- os_loadbalancer:
197    auth:
198      auth_url: https://identity.example.com
199      username: admin
200      password: passme
201      project_name: admin
202    state: present
203    name: my_lb
204    vip_subnet: my_subnet
205    timeout: 150
206
207# Create a load balancer by specifying the VIP network and the IP address.
208- os_loadbalancer:
209    auth:
210      auth_url: https://identity.example.com
211      username: admin
212      password: passme
213      project_name: admin
214    state: present
215    name: my_lb
216    vip_network: my_network
217    vip_address: 192.168.0.11
218
219# Create a load balancer together with its sub-resources in the 'all in one'
220# way. A public IP address is also allocated to the load balancer VIP.
221- os_loadbalancer:
222    auth:
223      auth_url: https://identity.example.com
224      username: admin
225      password: passme
226      project_name: admin
227    name: lingxian_test
228    state: present
229    vip_subnet: kong_subnet
230    auto_public_ip: yes
231    public_network: public
232    listeners:
233      - name: lingxian_80
234        protocol: TCP
235        protocol_port: 80
236        pool:
237          name: lingxian_80_pool
238          protocol: TCP
239          members:
240            - name: mywebserver1
241              address: 192.168.2.81
242              protocol_port: 80
243              subnet: webserver_subnet
244      - name: lingxian_8080
245        protocol: TCP
246        protocol_port: 8080
247        pool:
248          name: lingxian_8080-pool
249          protocol: TCP
250          members:
251            - name: mywebserver2
252              address: 192.168.2.82
253              protocol_port: 8080
254    wait: yes
255    timeout: 600
256
257# Delete a load balancer(and all its related resources)
258- os_loadbalancer:
259    auth:
260      auth_url: https://identity.example.com
261      username: admin
262      password: passme
263      project_name: admin
264    state: absent
265    name: my_lb
266
267# Delete a load balancer(and all its related resources) together with the
268# public IP address(if any) attached to it.
269- os_loadbalancer:
270    auth:
271      auth_url: https://identity.example.com
272      username: admin
273      password: passme
274      project_name: admin
275    state: absent
276    name: my_lb
277    delete_public_ip: yes
278'''
279
280import time
281
282from ansible.module_utils.basic import AnsibleModule
283from ansible.module_utils.openstack import openstack_full_argument_spec, \
284    openstack_module_kwargs, openstack_cloud_from_module
285
286
287def _wait_for_lb(module, cloud, lb, status, failures, interval=5):
288    """Wait for load balancer to be in a particular provisioning status."""
289    timeout = module.params['timeout']
290
291    total_sleep = 0
292    if failures is None:
293        failures = []
294
295    while total_sleep < timeout:
296        lb = cloud.load_balancer.find_load_balancer(lb.id)
297
298        if lb:
299            if lb.provisioning_status == status:
300                return None
301            if lb.provisioning_status in failures:
302                module.fail_json(
303                    msg="Load Balancer %s transitioned to failure state %s" %
304                        (lb.id, lb.provisioning_status)
305                )
306        else:
307            if status == "DELETED":
308                return None
309            else:
310                module.fail_json(
311                    msg="Load Balancer %s transitioned to DELETED" % lb.id
312                )
313
314        time.sleep(interval)
315        total_sleep += interval
316
317    module.fail_json(
318        msg="Timeout waiting for Load Balancer %s to transition to %s" %
319            (lb.id, status)
320    )
321
322
323def main():
324    argument_spec = openstack_full_argument_spec(
325        name=dict(required=True),
326        state=dict(default='present', choices=['absent', 'present']),
327        vip_network=dict(required=False),
328        vip_subnet=dict(required=False),
329        vip_port=dict(required=False),
330        vip_address=dict(required=False),
331        listeners=dict(type='list', default=[]),
332        public_ip_address=dict(required=False, default=None),
333        auto_public_ip=dict(required=False, default=False, type='bool'),
334        public_network=dict(required=False),
335        delete_public_ip=dict(required=False, default=False, type='bool'),
336    )
337    module_kwargs = openstack_module_kwargs()
338    module = AnsibleModule(argument_spec, **module_kwargs)
339    sdk, cloud = openstack_cloud_from_module(module)
340
341    vip_network = module.params['vip_network']
342    vip_subnet = module.params['vip_subnet']
343    vip_port = module.params['vip_port']
344    listeners = module.params['listeners']
345    public_vip_address = module.params['public_ip_address']
346    allocate_fip = module.params['auto_public_ip']
347    delete_fip = module.params['delete_public_ip']
348    public_network = module.params['public_network']
349
350    vip_network_id = None
351    vip_subnet_id = None
352    vip_port_id = None
353
354    try:
355        changed = False
356        lb = cloud.load_balancer.find_load_balancer(
357            name_or_id=module.params['name'])
358
359        if module.params['state'] == 'present':
360            if not lb:
361                if not (vip_network or vip_subnet or vip_port):
362                    module.fail_json(
363                        msg="One of vip_network, vip_subnet, or vip_port must "
364                            "be specified for load balancer creation"
365                    )
366
367                if vip_network:
368                    network = cloud.get_network(vip_network)
369                    if not network:
370                        module.fail_json(
371                            msg='network %s is not found' % vip_network
372                        )
373                    vip_network_id = network.id
374                if vip_subnet:
375                    subnet = cloud.get_subnet(vip_subnet)
376                    if not subnet:
377                        module.fail_json(
378                            msg='subnet %s is not found' % vip_subnet
379                        )
380                    vip_subnet_id = subnet.id
381                if vip_port:
382                    port = cloud.get_port(vip_port)
383                    if not port:
384                        module.fail_json(
385                            msg='port %s is not found' % vip_port
386                        )
387                    vip_port_id = port.id
388
389                lb = cloud.load_balancer.create_load_balancer(
390                    name=module.params['name'],
391                    vip_network_id=vip_network_id,
392                    vip_subnet_id=vip_subnet_id,
393                    vip_port_id=vip_port_id,
394                    vip_address=module.params['vip_address'],
395                )
396                changed = True
397
398            if not listeners and not module.params['wait']:
399                module.exit_json(
400                    changed=changed,
401                    loadbalancer=lb.to_dict(),
402                    id=lb.id
403                )
404
405            _wait_for_lb(module, cloud, lb, "ACTIVE", ["ERROR"])
406
407            for listener_def in listeners:
408                listener_name = listener_def.get("name")
409                pool_def = listener_def.get("pool")
410
411                if not listener_name:
412                    module.fail_json(msg='listener name is required')
413
414                listener = cloud.load_balancer.find_listener(
415                    name_or_id=listener_name
416                )
417
418                if not listener:
419                    _wait_for_lb(module, cloud, lb, "ACTIVE", ["ERROR"])
420
421                    protocol = listener_def.get("protocol", "HTTP")
422                    protocol_port = listener_def.get("protocol_port", 80)
423
424                    listener = cloud.load_balancer.create_listener(
425                        name=listener_name,
426                        loadbalancer_id=lb.id,
427                        protocol=protocol,
428                        protocol_port=protocol_port,
429                    )
430                    changed = True
431
432                # Ensure pool in the listener.
433                if pool_def:
434                    pool_name = pool_def.get("name")
435                    members = pool_def.get('members', [])
436
437                    if not pool_name:
438                        module.fail_json(msg='pool name is required')
439
440                    pool = cloud.load_balancer.find_pool(name_or_id=pool_name)
441
442                    if not pool:
443                        _wait_for_lb(module, cloud, lb, "ACTIVE", ["ERROR"])
444
445                        protocol = pool_def.get("protocol", "HTTP")
446                        lb_algorithm = pool_def.get("lb_algorithm",
447                                                    "ROUND_ROBIN")
448
449                        pool = cloud.load_balancer.create_pool(
450                            name=pool_name,
451                            listener_id=listener.id,
452                            protocol=protocol,
453                            lb_algorithm=lb_algorithm
454                        )
455                        changed = True
456
457                    # Ensure members in the pool
458                    for member_def in members:
459                        member_name = member_def.get("name")
460                        if not member_name:
461                            module.fail_json(msg='member name is required')
462
463                        member = cloud.load_balancer.find_member(member_name,
464                                                                 pool.id)
465
466                        if not member:
467                            _wait_for_lb(module, cloud, lb, "ACTIVE",
468                                         ["ERROR"])
469
470                            address = member_def.get("address")
471                            if not address:
472                                module.fail_json(
473                                    msg='member address for member %s is '
474                                        'required' % member_name
475                                )
476
477                            subnet_id = member_def.get("subnet")
478                            if subnet_id:
479                                subnet = cloud.get_subnet(subnet_id)
480                                if not subnet:
481                                    module.fail_json(
482                                        msg='subnet %s for member %s is not '
483                                            'found' % (subnet_id, member_name)
484                                    )
485                                subnet_id = subnet.id
486
487                            protocol_port = member_def.get("protocol_port", 80)
488
489                            member = cloud.load_balancer.create_member(
490                                pool,
491                                name=member_name,
492                                address=address,
493                                protocol_port=protocol_port,
494                                subnet_id=subnet_id
495                            )
496                            changed = True
497
498            # Associate public ip to the load balancer VIP. If
499            # public_vip_address is provided, use that IP, otherwise, either
500            # find an available public ip or create a new one.
501            fip = None
502            orig_public_ip = None
503            new_public_ip = None
504            if public_vip_address or allocate_fip:
505                ips = cloud.network.ips(
506                    port_id=lb.vip_port_id,
507                    fixed_ip_address=lb.vip_address
508                )
509                ips = list(ips)
510                if ips:
511                    orig_public_ip = ips[0]
512                    new_public_ip = orig_public_ip.floating_ip_address
513
514            if public_vip_address and public_vip_address != orig_public_ip:
515                fip = cloud.network.find_ip(public_vip_address)
516                if not fip:
517                    module.fail_json(
518                        msg='Public IP %s is unavailable' % public_vip_address
519                    )
520
521                # Release origin public ip first
522                cloud.network.update_ip(
523                    orig_public_ip,
524                    fixed_ip_address=None,
525                    port_id=None
526                )
527
528                # Associate new public ip
529                cloud.network.update_ip(
530                    fip,
531                    fixed_ip_address=lb.vip_address,
532                    port_id=lb.vip_port_id
533                )
534
535                new_public_ip = public_vip_address
536                changed = True
537            elif allocate_fip and not orig_public_ip:
538                fip = cloud.network.find_available_ip()
539                if not fip:
540                    if not public_network:
541                        module.fail_json(msg="Public network is not provided")
542
543                    pub_net = cloud.network.find_network(public_network)
544                    if not pub_net:
545                        module.fail_json(
546                            msg='Public network %s not found' %
547                                public_network
548                        )
549                    fip = cloud.network.create_ip(
550                        floating_network_id=pub_net.id
551                    )
552
553                cloud.network.update_ip(
554                    fip,
555                    fixed_ip_address=lb.vip_address,
556                    port_id=lb.vip_port_id
557                )
558
559                new_public_ip = fip.floating_ip_address
560                changed = True
561
562            # Include public_vip_address in the result.
563            lb = cloud.load_balancer.find_load_balancer(name_or_id=lb.id)
564            lb_dict = lb.to_dict()
565            lb_dict.update({"public_vip_address": new_public_ip})
566
567            module.exit_json(
568                changed=changed,
569                loadbalancer=lb_dict,
570                id=lb.id
571            )
572        elif module.params['state'] == 'absent':
573            changed = False
574            public_vip_address = None
575
576            if lb:
577                if delete_fip:
578                    ips = cloud.network.ips(
579                        port_id=lb.vip_port_id,
580                        fixed_ip_address=lb.vip_address
581                    )
582                    ips = list(ips)
583                    if ips:
584                        public_vip_address = ips[0]
585
586                # Deleting load balancer with `cascade=False` does not make
587                # sense because the deletion will always fail if there are
588                # sub-resources.
589                cloud.load_balancer.delete_load_balancer(lb, cascade=True)
590                changed = True
591
592                if module.params['wait']:
593                    _wait_for_lb(module, cloud, lb, "DELETED", ["ERROR"])
594
595            if delete_fip and public_vip_address:
596                cloud.network.delete_ip(public_vip_address)
597                changed = True
598
599            module.exit_json(changed=changed)
600    except sdk.exceptions.OpenStackCloudException as e:
601        module.fail_json(msg=str(e), extra_data=e.extra_data)
602
603
604if __name__ == "__main__":
605    main()
606