bigip_pool 18.5 KB
Newer Older
1
#!/usr/bin/python
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# -*- coding: utf-8 -*-

# (c) 2013, Matt Hite <mhite@hotmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

DOCUMENTATION = '''
---
module: bigip_pool
24
short_description: "Manages F5 BIG-IP LTM pools"
25
description:
26
    - "Manages F5 BIG-IP LTM pools via iControl SOAP API"
27 28 29 30 31 32 33 34 35 36 37
version_added: "1.2"
author: Matt Hite
notes:
    - "Requires BIG-IP software version >= 11"
    - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)"
    - "Best run as a local_action in your playbook"
requirements:
    - bigsuds
options:
    server:
        description:
38
            - BIG-IP host
39 40 41 42 43 44
        required: true
        default: null
        choices: []
        aliases: []
    user:
        description:
45
            - BIG-IP username
46 47 48 49 50 51
        required: true
        default: null
        choices: []
        aliases: []
    password:
        description:
52
            - BIG-IP password
53 54 55 56 57 58
        required: true
        default: null
        choices: []
        aliases: []
    state:
        description:
59
            - Pool/pool member state
60 61 62 63 64 65
        required: false
        default: present
        choices: ['present', 'absent']
        aliases: []
    name:
        description:
66
            - Pool name
67 68 69 70 71 72
        required: true
        default: null
        choices: []
        aliases: ['pool']
    partition:
        description:
73
            - Partition of pool/pool member
74 75 76 77
        required: false
        default: 'Common'
        choices: []
        aliases: []
78
    lb_method:
79
        description:
80
            - Load balancing method
81
        version_added: "1.3"
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
        required: False
        default: 'round_robin'
        choices: ['round_robin', 'ratio_member', 'least_connection_member',
                  'observed_member', 'predictive_member', 'ratio_node_address',
                  'least_connection_node_address', 'fastest_node_address',
                  'observed_node_address', 'predictive_node_address',
                  'dynamic_ratio', 'fastest_app_response', 'least_sessions',
                  'dynamic_ratio_member', 'l3_addr', 'unknown',
                  'weighted_least_connection_member',
                  'weighted_least_connection_node_address',
                  'ratio_session', 'ratio_least_connection_member',
                  'ratio_least_connection_node_address']
        aliases: []
    monitor_type:
        description:
            - Monitor rule type when monitors > 1
98
        version_added: "1.3"
99 100 101 102 103 104 105
        required: False
        default: null
        choices: ['and_list', 'm_of_n']
        aliases: []
    quorum:
        description:
            - Monitor quorum value when monitor_type is m_of_n
106
        version_added: "1.3"
107 108 109 110 111 112
        required: False
        default: null
        choices: []
        aliases: []
    monitors:
        description:
113 114
            - Monitor template name list. Always use the full path to the monitor.
        version_added: "1.3"
115 116 117 118 119 120
        required: False
        default: null
        choices: []
        aliases: []
    slow_ramp_time:
        description:
121 122
            - Sets the ramp-up time (in seconds) to gradually ramp up the load on newly added or freshly detected up pool members
        version_added: "1.3"
123
        required: False
124 125 126
        default: null
        choices: []
        aliases: []
127 128 129
    service_down_action:
        description:
            - Sets the action to take when node goes down in pool
130
        version_added: "1.3"
131 132 133 134 135 136 137 138 139 140 141
        required: False
        default: null
        choices: ['none', 'reset', 'drop', 'reselect']
        aliases: []
    host:
        description:
            - "Pool member IP"
        required: False
        default: null
        choices: []
        aliases: ['address']
142 143
    port:
        description:
144 145
            - "Pool member port"
        required: False
146 147 148 149 150 151 152
        default: null
        choices: []
        aliases: []
'''

EXAMPLES = '''

153
## playbook task examples:
154

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
---
# file bigip-test.yml
# ...
- hosts: localhost
  tasks:
  - name: Create pool
    local_action: >
      bigip_pool
      server=lb.mydomain.com
      user=admin
      password=mysecret
      state=present
      name=matthite-pool
      partition=matthite
      lb_method=least_connection_member
      slow_ramp_time=120
171

172 173 174 175 176 177 178 179 180 181
  - name: Modify load balancer method
    local_action: >
      bigip_pool
      server=lb.mydomain.com
      user=admin
      password=mysecret
      state=present
      name=matthite-pool
      partition=matthite
      lb_method=round_robin
182

183 184 185 186 187 188 189 190 191 192 193 194 195
- hosts: bigip-test
  tasks:
  - name: Add pool member
    local_action: >
      bigip_pool
      server=lb.mydomain.com
      user=admin
      password=mysecret
      state=present
      name=matthite-pool
      partition=matthite
      host="{{ ansible_default_ipv4["address"] }}"
      port=80
196

197 198 199 200 201 202 203 204 205 206 207
  - name: Remove pool member from pool
    local_action: >
      bigip_pool
      server=lb.mydomain.com
      user=admin
      password=mysecret
      state=absent
      name=matthite-pool
      partition=matthite
      host="{{ ansible_default_ipv4["address"] }}"
      port=80
208

209
- hosts: localhost
210
  tasks:
211 212 213 214 215 216 217 218 219 220
  - name: Delete pool
    local_action: >
      bigip_pool
      server=lb.mydomain.com
      user=admin
      password=mysecret
      state=absent
      name=matthite-pool
      partition=matthite

221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
'''

try:
    import bigsuds
except ImportError:
    bigsuds_found = False
else:
    bigsuds_found = True

# ===========================================
# bigip_pool module specific support methods.
#

def bigip_api(bigip, user, password):
    api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
    return api

def pool_exists(api, pool):
    # hack to determine if pool exists
    result = False
    try:
        api.LocalLB.Pool.get_object_status(pool_names=[pool])
        result = True
    except bigsuds.OperationFailed, e:
        if "was not found" in str(e):
            result = False
        else:
            # genuine exception
            raise
    return result

252
def create_pool(api, pool, lb_method):
253 254 255 256
    # create requires lb_method but we don't want to default
    # to a value on subsequent runs
    if not lb_method:
        lb_method = 'round_robin'
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
    lb_method = "LB_METHOD_%s" % lb_method.strip().upper()
    api.LocalLB.Pool.create_v2(pool_names=[pool], lb_methods=[lb_method],
                               members=[[]])

def remove_pool(api, pool):
    api.LocalLB.Pool.delete_pool(pool_names=[pool])

def get_lb_method(api, pool):
    lb_method = api.LocalLB.Pool.get_lb_method(pool_names=[pool])[0]
    lb_method = lb_method.strip().replace('LB_METHOD_', '').lower()
    return lb_method

def set_lb_method(api, pool, lb_method):
    lb_method = "LB_METHOD_%s" % lb_method.strip().upper()
    api.LocalLB.Pool.set_lb_method(pool_names=[pool], lb_methods=[lb_method])

def get_monitors(api, pool):
    result = api.LocalLB.Pool.get_monitor_association(pool_names=[pool])[0]['monitor_rule']
    monitor_type = result['type'].split("MONITOR_RULE_TYPE_")[-1].lower()
    quorum = result['quorum']
    monitor_templates = result['monitor_templates']
    return (monitor_type, quorum, monitor_templates)

def set_monitors(api, pool, monitor_type, quorum, monitor_templates):
    monitor_type = "MONITOR_RULE_TYPE_%s" % monitor_type.strip().upper()
    monitor_rule = {'type': monitor_type, 'quorum': quorum, 'monitor_templates': monitor_templates}
    monitor_association = {'pool_name': pool, 'monitor_rule': monitor_rule}
    api.LocalLB.Pool.set_monitor_association(monitor_associations=[monitor_association])

def get_slow_ramp_time(api, pool):
    result = api.LocalLB.Pool.get_slow_ramp_time(pool_names=[pool])[0]
    return result

def set_slow_ramp_time(api, pool, seconds):
    api.LocalLB.Pool.set_slow_ramp_time(pool_names=[pool], values=[seconds])

def get_action_on_service_down(api, pool):
    result = api.LocalLB.Pool.get_action_on_service_down(pool_names=[pool])[0]
    result = result.split("SERVICE_DOWN_ACTION_")[-1].lower()
    return result

def set_action_on_service_down(api, pool, action):
    action = "SERVICE_DOWN_ACTION_%s" % action.strip().upper()
    api.LocalLB.Pool.set_action_on_service_down(pool_names=[pool], actions=[action])

302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
def member_exists(api, pool, address, port):
    # hack to determine if member exists
    result = False
    try:
        members = [{'address': address, 'port': port}]
        api.LocalLB.Pool.get_member_object_status(pool_names=[pool],
                                                  members=[members])
        result = True
    except bigsuds.OperationFailed, e:
        if "was not found" in str(e):
            result = False
        else:
            # genuine exception
            raise
    return result

def delete_node_address(api, address):
    result = False
    try:
        api.LocalLB.NodeAddressV2.delete_node_address(nodes=[address])
        result = True
    except bigsuds.OperationFailed, e:
        if "is referenced by a member of pool" in str(e):
            result = False
        else:
            # genuine exception
            raise
    return result

def remove_pool_member(api, pool, address, port):
    members = [{'address': address, 'port': port}]
    api.LocalLB.Pool.remove_member_v2(pool_names=[pool], members=[members])

def add_pool_member(api, pool, address, port):
    members = [{'address': address, 'port': port}]
    api.LocalLB.Pool.add_member_v2(pool_names=[pool], members=[members])

def main():
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
    lb_method_choices = ['round_robin', 'ratio_member',
                         'least_connection_member', 'observed_member',
                         'predictive_member', 'ratio_node_address',
                         'least_connection_node_address',
                         'fastest_node_address', 'observed_node_address',
                         'predictive_node_address', 'dynamic_ratio',
                         'fastest_app_response', 'least_sessions',
                         'dynamic_ratio_member', 'l3_addr', 'unknown',
                         'weighted_least_connection_member',
                         'weighted_least_connection_node_address',
                         'ratio_session', 'ratio_least_connection_member',
                         'ratio_least_connection_node_address']

    monitor_type_choices = ['and_list', 'm_of_n']

    service_down_choices = ['none', 'reset', 'drop', 'reselect']

357 358
    module = AnsibleModule(
        argument_spec = dict(
359 360 361 362 363 364 365 366 367 368 369 370 371 372
            server = dict(type='str', required=True),
            user = dict(type='str', required=True),
            password = dict(type='str', required=True),
            state = dict(type='str', default='present', choices=['present', 'absent']),
            name = dict(type='str', required=True, aliases=['pool']),
            partition = dict(type='str', default='Common'),
            lb_method = dict(type='str', choices=lb_method_choices),
            monitor_type = dict(type='str', choices=monitor_type_choices),
            quorum = dict(type='int'),
            monitors = dict(type='list'),
            slow_ramp_time = dict(type='int'),
            service_down_action = dict(type='str', choices=service_down_choices),
            host = dict(type='str', aliases=['address']),
            port = dict(type='int')
373 374 375 376 377 378 379 380 381 382 383 384 385
        ),
        supports_check_mode=True
    )

    if not bigsuds_found:
        module.fail_json(msg="the python bigsuds module is required")

    server = module.params['server']
    user = module.params['user']
    password = module.params['password']
    state = module.params['state']
    name = module.params['name']
    partition = module.params['partition']
386 387 388 389 390 391 392 393 394
    pool = "/%s/%s" % (partition, name)
    lb_method = module.params['lb_method']
    if lb_method:
        lb_method = lb_method.lower()
    monitor_type = module.params['monitor_type']
    if monitor_type:
        monitor_type = monitor_type.lower()
    quorum = module.params['quorum']
    monitors = module.params['monitors']
395 396 397 398 399 400 401
    if monitors:
        monitors = []
        for monitor in module.params['monitors']:
            if "/" not in monitor:
                monitors.append("/%s/%s" % (partition, monitor))
            else:
                monitors.append(monitor)
402 403 404 405
    slow_ramp_time = module.params['slow_ramp_time']
    service_down_action = module.params['service_down_action']
    if service_down_action:
        service_down_action = service_down_action.lower()
406 407
    host = module.params['host']
    address = "/%s/%s" % (partition, host)
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
    port = module.params['port']

    # sanity check user supplied values

    if (host and not port) or (port and not host):
        module.fail_json(msg="both host and port must be supplied")

    if 1 > port > 65535:
        module.fail_json(msg="valid ports must be in range 1 - 65535")

    if monitors:
        if len(monitors) == 1:
            # set default required values for single monitor
            quorum = 0
            monitor_type = 'single'
        elif len(monitors) > 1:
            if not monitor_type:
                module.fail_json(msg="monitor_type required for monitors > 1")
            if monitor_type == 'm_of_n' and not quorum:
                module.fail_json(msg="quorum value required for monitor_type m_of_n")
            if monitor_type != 'm_of_n':
                quorum = 0
    elif monitor_type:
        # no monitors specified but monitor_type exists
        module.fail_json(msg="monitor_type require monitors parameter")
    elif quorum is not None:
        # no monitors specified but quorum exists
        module.fail_json(msg="quorum requires monitors parameter")
436 437 438

    try:
        api = bigip_api(server, user, password)
439 440
        result = {'changed': False}  # default

441
        if state == 'absent':
442 443 444 445 446 447 448 449 450 451 452 453
            if host and port and pool:
                # member removal takes precedent
                if pool_exists(api, pool) and member_exists(api, pool, address, port):
                    if not module.check_mode:
                        remove_pool_member(api, pool, address, port)
                        deleted = delete_node_address(api, address)
                        result = {'changed': True, 'deleted': deleted}
                    else:
                        result = {'changed': True}
            elif pool_exists(api, pool):
                # no host/port supplied, must be pool removal
                if not module.check_mode:
454 455 456 457 458 459 460 461 462 463 464 465 466 467
                    # hack to handle concurrent runs of module
                    # pool might be gone before we actually remove it
                    try:
                        remove_pool(api, pool)
                        result = {'changed': True}
                    except bigsuds.OperationFailed, e:
                        if "was not found" in str(e):
                            result = {'changed': False}
                        else:
                            # genuine exception
                            raise
                else:
                    # check-mode return value
                    result = {'changed': True}
468

469
        elif state == 'present':
470
            update = False
471 472 473
            if not pool_exists(api, pool):
                # pool does not exist -- need to create it
                if not module.check_mode:
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
                    # a bit of a hack to handle concurrent runs of this module.
                    # even though we've checked the pool doesn't exist,
                    # it may exist by the time we run create_pool().
                    # this catches the exception and does something smart
                    # about it!
                    try:
                        create_pool(api, pool, lb_method)
                        result = {'changed': True}
                    except bigsuds.OperationFailed, e:
                        if "already exists" in str(e):
                            update = True
                        else:
                            # genuine exception
                            raise
                    else:
                        if monitors:
                            set_monitors(api, pool, monitor_type, quorum, monitors)
                        if slow_ramp_time:
                            set_slow_ramp_time(api, pool, slow_ramp_time)
                        if service_down_action:
                            set_action_on_service_down(api, pool, service_down_action)
                        if host and port:
                            add_pool_member(api, pool, address, port)
                else:
                    # check-mode return value
                    result = {'changed': True}
500
            else:
501
                # pool exists -- potentially modify attributes
502 503 504
                update = True

            if update:
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
                if lb_method and lb_method != get_lb_method(api, pool):
                    if not module.check_mode:
                        set_lb_method(api, pool, lb_method)
                    result = {'changed': True}
                if monitors:
                    t_monitor_type, t_quorum, t_monitor_templates = get_monitors(api, pool)
                    if (t_monitor_type != monitor_type) or (t_quorum != quorum) or (set(t_monitor_templates) != set(monitors)):
                        if not module.check_mode:
                            set_monitors(api, pool, monitor_type, quorum, monitors)
                        result = {'changed': True}
                if slow_ramp_time and slow_ramp_time != get_slow_ramp_time(api, pool):
                    if not module.check_mode:
                        set_slow_ramp_time(api, pool, slow_ramp_time)
                    result = {'changed': True}
                if service_down_action and service_down_action != get_action_on_service_down(api, pool):
                    if not module.check_mode:
                        set_action_on_service_down(api, pool, service_down_action)
                    result = {'changed': True}
                if (host and port) and not member_exists(api, pool, address, port):
                    if not module.check_mode:
                        add_pool_member(api, pool, address, port)
                    result = {'changed': True}

528 529 530 531 532
    except Exception, e:
        module.fail_json(msg="received exception: %s" % e)

    module.exit_json(**result)

533 534
# import module snippets
from ansible.module_utils.basic import *
535 536
main()