summaryrefslogtreecommitdiff
path: root/network/a10/a10_service_group.py
blob: 486fcb0b3e19e5de426fe541fedbbc036f9cd4ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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
252
253
254
255
256
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
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
#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Ansible module to manage A10 Networks slb service-group objects
(c) 2014, Mischa Peters <mpeters@a10networks.com>,
Eric Chou <ericc@a10networks.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/>.
"""

ANSIBLE_METADATA = {'status': ['preview'],
                    'supported_by': 'community',
                    'version': '1.0'}

DOCUMENTATION = '''
---
module: a10_service_group
version_added: 1.8
short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' service groups.
description:
    - Manage SLB (Server Load Balancing) service-group objects on A10 Networks devices via aXAPIv2.
author: "Eric Chou (@ericchou) 2016, Mischa Peters (@mischapeters) 2014"
notes:
    - Requires A10 Networks aXAPI 2.1.
    - When a server doesn't exist and is added to the service-group the server will be created.
extends_documentation_fragment: a10
options:
  partition:
    version_added: "2.3"
    description:
      - set active-partition
    required: false
    default: null
  service_group:
    description:
      - The SLB (Server Load Balancing) service-group name
    required: true
    default: null
    aliases: ['service', 'pool', 'group']
  service_group_protocol:
    description:
      - The SLB service-group protocol of TCP or UDP.
    required: false
    default: tcp
    aliases: ['proto', 'protocol']
    choices: ['tcp', 'udp']
  service_group_method:
    description:
      - The SLB service-group load balancing method, such as round-robin or weighted-rr.
    required: false
    default: round-robin
    aliases: ['method']
    choices: ['round-robin', 'weighted-rr', 'least-connection', 'weighted-least-connection', 'service-least-connection', 'service-weighted-least-connection', 'fastest-response', 'least-request', 'round-robin-strict', 'src-ip-only-hash', 'src-ip-hash']
  servers:
    description:
      - A list of servers to add to the service group. Each list item should be a
        dictionary which specifies the C(server:) and C(port:), but can also optionally
        specify the C(status:). See the examples below for details.
    required: false
    default: null
  validate_certs:
    description:
      - If C(no), SSL certificates will not be validated. This should only be used
        on personally controlled devices using self-signed certificates.
    required: false
    default: 'yes'
    choices: ['yes', 'no']

'''

RETURN = '''
#
'''

EXAMPLES = '''
# Create a new service-group
- a10_service_group: 
    host: a10.mydomain.com
    username: myadmin
    password: mypassword
    partition: mypartition
    service_group: sg-80-tcp
    servers:
      - server: foo1.mydomain.com
        port: 8080
      - server: foo2.mydomain.com
        port: 8080
      - server: foo3.mydomain.com
        port: 8080
      - server: foo4.mydomain.com
        port: 8080
        status: disabled

'''

RETURN = '''
content:
  description: the full info regarding the slb_service_group
  returned: success
  type: string
  sample: "mynewservicegroup"
'''

VALID_SERVICE_GROUP_FIELDS = ['name', 'protocol', 'lb_method']
VALID_SERVER_FIELDS = ['server', 'port', 'status']

def validate_servers(module, servers):
    for item in servers:
        for key in item:
            if key not in VALID_SERVER_FIELDS:
                module.fail_json(msg="invalid server field (%s), must be one of: %s" % (key, ','.join(VALID_SERVER_FIELDS)))

        # validate the server name is present
        if 'server' not in item:
            module.fail_json(msg="server definitions must define the server field")

        # validate the port number is present and an integer
        if 'port' in item:
            try:
                item['port'] = int(item['port'])
            except:
                module.fail_json(msg="server port definitions must be integers")
        else:
            module.fail_json(msg="server definitions must define the port field")

        # convert the status to the internal API integer value
        if 'status' in item:
            item['status'] = axapi_enabled_disabled(item['status'])
        else:
            item['status'] = 1


def main():
    argument_spec = a10_argument_spec()
    argument_spec.update(url_argument_spec())
    argument_spec.update(
        dict(
            state=dict(type='str', default='present', choices=['present', 'absent']),
            service_group=dict(type='str', aliases=['service', 'pool', 'group'], required=True),
            service_group_protocol=dict(type='str', default='tcp', aliases=['proto', 'protocol'], choices=['tcp', 'udp']),
            service_group_method=dict(type='str', default='round-robin',
                                      aliases=['method'],
                                      choices=['round-robin',
                                               'weighted-rr',
                                               'least-connection',
                                               'weighted-least-connection',
                                               'service-least-connection',
                                               'service-weighted-least-connection',
                                               'fastest-response',
                                               'least-request',
                                               'round-robin-strict',
                                               'src-ip-only-hash',
                                               'src-ip-hash']),
            servers=dict(type='list', aliases=['server', 'member'], default=[]),
            partition=dict(type='str', default=[]),
        )
    )

    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=False
    )

    host = module.params['host']
    username = module.params['username']
    password = module.params['password']
    partition = module.params['partition']
    state = module.params['state']
    write_config = module.params['write_config']
    slb_service_group = module.params['service_group']
    slb_service_group_proto = module.params['service_group_protocol']
    slb_service_group_method = module.params['service_group_method']
    slb_servers = module.params['servers']

    if slb_service_group is None:
        module.fail_json(msg='service_group is required')

    axapi_base_url = 'https://' + host + '/services/rest/V2.1/?format=json'
    load_balancing_methods = {'round-robin': 0,
                              'weighted-rr': 1,
                              'least-connection': 2,
                              'weighted-least-connection': 3,
                              'service-least-connection': 4,
                              'service-weighted-least-connection': 5,
                              'fastest-response': 6,
                              'least-request': 7,
                              'round-robin-strict': 8,
                              'src-ip-only-hash': 14,
                              'src-ip-hash': 15}

    if not slb_service_group_proto or slb_service_group_proto.lower() == 'tcp':
        protocol = 2
    else:
        protocol = 3

    # validate the server data list structure
    validate_servers(module, slb_servers)

    json_post = {
        'service_group': {
            'name': slb_service_group,
            'protocol': protocol,
            'lb_method': load_balancing_methods[slb_service_group_method],
        }
    }

    # first we authenticate to get a session id
    session_url = axapi_authenticate(module, axapi_base_url, username, password)
    # then we select the active-partition
    slb_server_partition = axapi_call(module, session_url + '&method=system.partition.active', json.dumps({'name': partition}))
    # then we check to see if the specified group exists
    slb_result = axapi_call(module, session_url + '&method=slb.service_group.search', json.dumps({'name': slb_service_group}))
    slb_service_group_exist = not axapi_failure(slb_result)

    changed = False
    if state == 'present':
        # before creating/updating we need to validate that servers
        # defined in the servers list exist to prevent errors
        checked_servers = []
        for server in slb_servers:
            result = axapi_call(module, session_url + '&method=slb.server.search', json.dumps({'name': server['server']}))
            if axapi_failure(result):
                module.fail_json(msg="the server %s specified in the servers list does not exist" % server['server'])
            checked_servers.append(server['server'])

        if not slb_service_group_exist:
            result = axapi_call(module, session_url + '&method=slb.service_group.create', json.dumps(json_post))
            if axapi_failure(result):
                module.fail_json(msg=result['response']['err']['msg'])
            changed = True
        else:
            # check to see if the service group definition without the
            # server members is different, and update that individually
            # if it needs it
            do_update = False
            for field in VALID_SERVICE_GROUP_FIELDS:
                if json_post['service_group'][field] != slb_result['service_group'][field]:
                    do_update = True
                    break

            if do_update:
                result = axapi_call(module, session_url + '&method=slb.service_group.update', json.dumps(json_post))
                if axapi_failure(result):
                    module.fail_json(msg=result['response']['err']['msg'])
                changed = True

        # next we pull the defined list of servers out of the returned
        # results to make it a bit easier to iterate over
        defined_servers = slb_result.get('service_group', {}).get('member_list', [])

        # next we add/update new member servers from the user-specified
        # list if they're different or not on the target device
        for server in slb_servers:
            found = False
            different = False
            for def_server in defined_servers:
                if server['server'] == def_server['server']:
                    found = True
                    for valid_field in VALID_SERVER_FIELDS:
                        if server[valid_field] != def_server[valid_field]:
                            different = True
                            break
                    if found or different:
                        break
            # add or update as required
            server_data = {
                "name": slb_service_group,
                "member": server,
            }
            if not found:
                result = axapi_call(module, session_url + '&method=slb.service_group.member.create', json.dumps(server_data))
                changed = True
            elif different:
                result = axapi_call(module, session_url + '&method=slb.service_group.member.update', json.dumps(server_data))
                changed = True

        # finally, remove any servers that are on the target
        # device but were not specified in the list given
        for server in defined_servers:
            found = False
            for slb_server in slb_servers:
                if server['server'] == slb_server['server']:
                    found = True
                    break
            # remove if not found
            server_data = {
                "name": slb_service_group,
                "member": server,
            }
            if not found:
                result = axapi_call(module, session_url + '&method=slb.service_group.member.delete', json.dumps(server_data))
                changed = True

        # if we changed things, get the full info regarding
        # the service group for the return data below
        if changed:
            result = axapi_call(module, session_url + '&method=slb.service_group.search', json.dumps({'name': slb_service_group}))
        else:
            result = slb_result
    elif state == 'absent':
        if slb_service_group_exist:
            result = axapi_call(module, session_url + '&method=slb.service_group.delete', json.dumps({'name': slb_service_group}))
            changed = True
        else:
            result = dict(msg="the service group was not present")

    # if the config has changed, save the config unless otherwise requested
    if changed and write_config:
        write_result = axapi_call(module, session_url + '&method=system.action.write_memory')
        if axapi_failure(write_result):
            module.fail_json(msg="failed to save the configuration: %s" % write_result['response']['err']['msg'])

    # log out of the session nicely and exit
    axapi_call(module, session_url + '&method=session.close')
    module.exit_json(changed=changed, content=result)

# standard ansible module imports
import json
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import url_argument_spec
from ansible.module_utils.a10 import axapi_call, a10_argument_spec, axapi_authenticate, axapi_failure, axapi_enabled_disabled


if __name__ == '__main__':
    main()