From 4949ba81e12955308fb446193e5253835b723c4a Mon Sep 17 00:00:00 2001
From: James Cammarata <jimi@sngx.net>
Date: Thu, 28 Aug 2014 20:23:48 -0500
Subject: [PATCH] A10 module improvements

* moved common code to an module_util snippet
* rewrote logic to make each module idempotent
* added new capabilities like the write_config option
---
 net_infrastructure/a10_server         | 260 +++++++++++--------
 net_infrastructure/a10_service_group  | 322 ++++++++++++++---------
 net_infrastructure/a10_virtual        | 352 --------------------------
 net_infrastructure/a10_virtual_server | 299 ++++++++++++++++++++++
 4 files changed, 649 insertions(+), 584 deletions(-)
 delete mode 100644 net_infrastructure/a10_virtual
 create mode 100644 net_infrastructure/a10_virtual_server

diff --git a/net_infrastructure/a10_server b/net_infrastructure/a10_server
index 993ca7e8ce3..65410536eef 100644
--- a/net_infrastructure/a10_server
+++ b/net_infrastructure/a10_server
@@ -24,16 +24,13 @@ along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 DOCUMENTATION = '''
 ---
 module: a10_server
-version_added: 1.0
+version_added: 1.8
 short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices
 description:
     - Manage slb server objects on A10 Networks devices via aXAPI
 author: Mischa Peters
 notes:
     - Requires A10 Networks aXAPI 2.1
-requirements:
-    - urllib2
-    - re
 options:
   host:
     description:
@@ -70,27 +67,23 @@ options:
     default: null
     aliases: ['ip', 'address']
     choices: []
-  server_port:
-    description:
-      - slb server port
-    required: false
-    default: null
-    aliases: ['port']
-    choices: []
-  server_protocol:
-    description:
-      - slb server protocol
-    required: false
-    default: null
-    aliases: ['proto', 'protocol']
-    choices: ['tcp', 'udp']
   server_status:
     description:
-      - slb server status
+      - slb virtual server status
     required: false
-    default: enabled
+    default: enable
     aliases: ['status']
-    choices: ['enable', 'disable']
+    choices: ['enabled', 'disabled']
+  server_ports:
+    description:
+      - A list of ports to create for the server. Each list item should be a
+        dictionary which specifies the C(port:) and C(protocol:), but can also optionally
+        specify the C(status:). See the examples below for details. This parameter is
+        required when C(state) is C(present).
+    required: false
+    default: null
+    aliases: []
+    choices: []
   state:
     description:
       - create, update or remove slb server
@@ -102,124 +95,175 @@ options:
 
 EXAMPLES = '''
 # Create a new server
-ansible host -m a10_server -a "host=a10adc.example.com username=axapiuser password=axapipass server_name=realserver1 server_ip=192.168.1.23"
+- a10_server: 
+    host: a10.mydomain.com
+    username: myadmin
+    password: mypassword
+    server: test
+    server_ip: 1.1.1.100
+    server_ports:
+      - port_num: 8080
+        protocol: tcp
+      - port_num: 8443
+        protocol: TCP
 
-# Add a port
-ansible host -m a10_server -a "host=a10adc.example.com username=axapiuser password=axapipass server_name=realserver1 server_port=80 server_protocol=tcp"
-
-# Disable a server
-ansible host -m a10_server -a "host=a10adc.example.com username=axapiuser password=axapipass server_name=realserver1 server_status=disable"
 '''
 
-import urllib2
+VALID_PORT_FIELDS = ['port_num', 'protocol', 'status']
 
+def validate_ports(module, ports):
+    for item in ports:
+        for key in item:
+            if key not in VALID_PORT_FIELDS:
+                module.fail_json(msg="invalid port field (%s), must be one of: %s" % (key, ','.join(VALID_PORT_FIELDS)))
 
-def axapi_call(url, post=None):
-    result = urllib2.urlopen(url, post).read()
-    return result
+        # validate the port number is present and an integer
+        if 'port_num' in item:
+            try:
+                item['port_num'] = int(item['port_num'])
+            except:
+                module.fail_json(msg="port_num entries in the port definitions must be integers")
+        else:
+            module.fail_json(msg="port definitions must define the port_num field")
 
+        # validate the port protocol is present, and convert it to
+        # the internal API integer value (and validate it)
+        if 'protocol' in item:
+            protocol = axapi_get_port_protocol(item['protocol'])
+            if not protocol:
+                module.fail_json(msg="invalid port protocol, must be one of: %s" % ','.join(AXAPI_PORT_PROTOCOLS))
+            else:
+                item['protocol'] = protocol
+        else:
+            module.fail_json(msg="port definitions must define the port protocol (%s)" % ','.join(AXAPI_PORT_PROTOCOLS))
 
-def axapi_authenticate(base_url, user, pwd):
-    url = base_url + '&method=authenticate&username=' + user + \
-        '&password=' + pwd
-    result = json.loads(axapi_call(url))
-    if 'response' in result:
-        return module.fail_json(msg=result['response']['err']['msg'])
-    sessid = result['session_id']
-    return base_url + '&session_id=' + sessid
+        # 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():
-    global module
-    module = AnsibleModule(
-        argument_spec=dict(
-            host=dict(type='str', required=True),
-            username=dict(type='str', aliases=['user', 'admin'],
-                          required=True),
-            password=dict(type='str', aliases=['pass', 'pwd'], required=True),
+    argument_spec = a10_argument_spec()
+    argument_spec.update(url_argument_spec())
+    argument_spec.update(
+        dict(
+            state=dict(type='str', default='present', choices=['present', 'absent']),
             server_name=dict(type='str', aliases=['server'], required=True),
             server_ip=dict(type='str', aliases=['ip', 'address']),
-            server_port=dict(type='int', aliases=['port']),
-            server_protocol=dict(type='str', aliases=['proto', 'protocol'],
-                                 choices=['tcp', 'udp']),
-            server_status=dict(type='str', default='enable',
-                               aliases=['status'],
-                               choices=['enable', 'disable']),
-            state=dict(type='str', default='present',
-                       choices=['present', 'absent']),
-        ),
+            server_status=dict(type='str', default='enabled', aliases=['status'], choices=['enabled', 'disabled']),
+            server_ports=dict(type='list', aliases=['port'], default=[]),
+        )
+    )
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
         supports_check_mode=False
     )
 
     host = module.params['host']
-    user = module.params['username']
-    pwd = module.params['password']
+    username = module.params['username']
+    password = module.params['password']
+    state = module.params['state']
+    write_config = module.params['write_config']
     slb_server = module.params['server_name']
     slb_server_ip = module.params['server_ip']
-    slb_server_port = module.params['server_port']
-    slb_server_proto = module.params['server_protocol']
     slb_server_status = module.params['server_status']
-    state = module.params['state']
-
-    axapi_base_url = 'https://' + host + '/services/rest/V2.1/?format=json'
-
-    if slb_server_proto == 'tcp' or slb_server_proto == 'TCP' or \
-            slb_server_proto is None:
-        protocol = '2'
-    else:
-        protocol = '3'
-
-    if slb_server_status == 'enable':
-        status = '1'
-    else:
-        status = '0'
+    slb_server_ports = module.params['server_ports']
 
     if slb_server is None:
         module.fail_json(msg='server_name is required')
 
-    if slb_server_port is None:
-        json_post = {'server': {'name': slb_server,
-                                'host': slb_server_ip, 'status': status}}
-    else:
-        json_post = {'server': {'name': slb_server, 'host': slb_server_ip,
-                                'status': status, 'port_list':
-                                [{'port_num': slb_server_port,
-                                  'protocol': protocol}]}}
+    axapi_base_url = 'https://%s/services/rest/V2.1/?format=json' % host
+    session_url = axapi_authenticate(module, axapi_base_url, username, password)
 
-    try:
-        session_url = axapi_authenticate(axapi_base_url, user, pwd)
+    # validate the ports data structure
+    validate_ports(module, slb_server_ports)
 
-        if state == 'present':
-            response = axapi_call(session_url + '&method=slb.server.search',
-                                  json.dumps({'name': slb_server}))
-            slb_server_exist = re.search(slb_server, response, re.I)
+    json_post = {
+        'server': {
+            'name': slb_server, 
+            'host': slb_server_ip, 
+            'status': axapi_enabled_disabled(slb_server_status),
+            'port_list': slb_server_ports,
+        }
+    }
 
-            if slb_server_exist is None:
-                if slb_server_ip is None:
-                    module.fail_json(msg='IP address is required')
-                response = axapi_call(session_url +
-                                      '&method=slb.server.create',
-                                      json.dumps(json_post))
-            else:
-                response = axapi_call(session_url +
-                                      '&method=slb.server.update',
-                                      json.dumps(json_post))
+    slb_server_data = axapi_call(module, session_url + '&method=slb.server.search', json.dumps({'name': slb_server}))
+    slb_server_exists = not axapi_failure(slb_server_data)
 
-        if state == 'absent':
-            response = axapi_call(session_url +
-                                  '&method=slb.server.delete',
-                                  json.dumps({'name': slb_server}))
+    changed = False
+    if state == 'present':
+        if not slb_server_ip:
+            module.fail_json(msg='you must specify an IP address when creating a server')
 
-        result = json.loads(response)
-        axapi_call(session_url + '&method=session.close')
+        if not slb_server_exists:
+            result = axapi_call(module, session_url + '&method=slb.server.create', json.dumps(json_post))
+            if axapi_failure(result):
+                module.fail_json(msg="failed to create the server: %s" % result['response']['err']['msg'])
+            changed = True
+        else:
+            def needs_update(src_ports, dst_ports):
+                '''
+                Checks to determine if the port definitions of the src_ports
+                array are in or different from those in dst_ports. If there is
+                a difference, this function returns true, otherwise false.
+                '''
+                for src_port in src_ports:
+                    found = False
+                    different = False
+                    for dst_port in dst_ports:
+                        if src_port['port_num'] == dst_port['port_num']:
+                            found = True
+                            for valid_field in VALID_PORT_FIELDS:
+                                if src_port[valid_field] != dst_port[valid_field]:
+                                    different = True
+                                    break
+                            if found or different:
+                                break
+                    if not found or different:
+                        return True
+                # every port from the src exists in the dst, and none of them were different
+                return False
 
-    except Exception, e:
-        return module.fail_json(msg='received exception: %s' % e)
+            defined_ports = slb_server_data.get('server', {}).get('port_list', [])
 
-    if 'respone' in result and 'err' in result['response']:
-        return module.fail_json(msg=result['response']['err']['msg'])
+            # we check for a needed update both ways, in case ports
+            # are missing from either the ones specified by the user
+            # or from those on the device
+            if needs_update(defined_ports, slb_server_ports) or needs_update(slb_server_ports, defined_ports):
+                result = axapi_call(module, session_url + '&method=slb.server.update', json.dumps(json_post))
+                if axapi_failure(result):
+                    module.fail_json(msg="failed to update the server: %s" % result['response']['err']['msg'])
+                changed = True
 
-    module.exit_json(changed=True, content=result)
+        # 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.server.search', json.dumps({'name': slb_server}))
+        else:
+            result = slb_server_data
+    elif state == 'absent':
+        if slb_server_exists:
+            result = axapi_call(module, session_url + '&method=slb.server.delete', json.dumps({'name': slb_server}))
+            changed = True
+        else:
+            result = dict(msg="the  server 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
 from ansible.module_utils.basic import *
+from ansible.module_utils.urls import *
+from ansible.module_utils.a10 import *
+
 main()
diff --git a/net_infrastructure/a10_service_group b/net_infrastructure/a10_service_group
index 0a9f02f76a1..3627e2d12b8 100644
--- a/net_infrastructure/a10_service_group
+++ b/net_infrastructure/a10_service_group
@@ -24,7 +24,7 @@ along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 DOCUMENTATION = '''
 ---
 module: a10_service_group
-version_added: 1.0
+version_added: 1.8
 short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices
 description:
     - Manage slb service-group objects on A10 Networks devices via aXAPI
@@ -32,9 +32,6 @@ author: Mischa Peters
 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
-requirements:
-    - urllib2
-    - re
 options:
   host:
     description:
@@ -78,79 +75,91 @@ options:
     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']
-  server_name:
+  servers:
     description:
-      - slb server name
+      - 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
-    aliases: ['server', 'member']
-    choices: []
-  server_port:
-    description:
-      - slb server port
-    required: false
-    default: null
-    aliases: ['port']
-    choices: []
-  server_status:
-    description:
-      - slb server status
-    required: false
-    default: enabled
-    aliases: ['status']
-    choices: ['enable', 'disable']
-  state:
-    description:
-      - create, remove or update slb service-group
-    required: false
-    default: present
     aliases: []
-    choices: ['present', 'absent']
+    choices: []
+  write_config:
+    description:
+      - If C(yes), any changes will cause a write of the running configuration
+        to non-volatile memory. This will save I(all) configuration changes,
+        including those that may have been made manually or through other modules,
+        so care should be taken when specifying C(yes).
+    required: false
+    default: "no"
+    choices: ["yes", "no"]
+  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']
+
 '''
 
 EXAMPLES = '''
 # Create a new service-group
-ansible host -m a10_service_group -a "host=a10adc.example.com username=axapiuser password=axapipass service_group=sg-80-tcp"
+- a10_service_group: 
+    host: a10.mydomain.com
+    username: myadmin
+    password: mypassword
+    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
 
-# Add a server
-ansible host -m a10_service_group -a "host=a10adc.example.com username=axapiuser password=axapipass service_group=sg-80-tcp server_name=realserver1 server_port=80"
-
-# Disable a server
-ansible host -m a10_service_group -a "host=a10adc.example.com username=axapiuser password=axapipass service_group=sg-80-tcp server_name=realserver1 server_port=80 status=disable"
 '''
 
-import urllib2
+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)))
 
-def axapi_call(url, post=None):
-    result = urllib2.urlopen(url, post).read()
-    return result
+        # 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")
 
-def axapi_authenticate(base_url, user, pwd):
-    url = base_url + '&method=authenticate&username=' + user + \
-        '&password=' + pwd
-    result = json.loads(axapi_call(url))
-    if 'response' in result:
-        return module.fail_json(msg=result['response']['err']['msg'])
-    sessid = result['session_id']
-    return base_url + '&session_id=' + sessid
+        # 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():
-    global module
-    module = AnsibleModule(
-        argument_spec=dict(
-            host=dict(type='str', required=True),
-            username=dict(type='str', aliases=['user', 'admin'],
-                          required=True),
-            password=dict(type='str', aliases=['pass', 'pwd'], required=True),
-            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']),
+    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',
@@ -164,27 +173,27 @@ def main():
                                                'round-robin-strict',
                                                'src-ip-only-hash',
                                                'src-ip-hash']),
-            server_name=dict(type='str', aliases=['server', 'member']),
-            server_port=dict(type='int', aliases=['port']),
-            server_status=dict(type='str', default='enable',
-                               aliases=['status'],
-                               choices=['enable', 'disable']),
-            state=dict(type='str', default='present',
-                       choices=['present', 'absent']),
-        ),
+            servers=dict(type='list', aliases=['server', 'member'], default=[]),
+        )
+    )
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
         supports_check_mode=False
     )
 
     host = module.params['host']
-    user = module.params['username']
-    pwd = module.params['password']
+    username = module.params['username']
+    password = module.params['password']
+    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_server = module.params['server_name']
-    slb_server_port = module.params['server_port']
-    slb_server_status = module.params['server_status']
-    state = module.params['state']
+    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,
@@ -199,69 +208,134 @@ def main():
                               'src-ip-only-hash': 14,
                               'src-ip-hash': 15}
 
-    if slb_service_group_proto == 'tcp' or slb_service_group_proto == 'TCP':
-        protocol = '2'
+    if not slb_service_group_proto or slb_service_group_proto.lower() == 'tcp':
+        protocol = 2
     else:
-        protocol = '3'
+        protocol = 3
 
-    if slb_server_status == 'enable':
-        status = '1'
-    else:
-        status = '0'
+    # validate the server data list structure
+    validate_servers(module, slb_servers)
 
-    if slb_service_group is None:
-        module.fail_json(msg='service_group is required')
+    json_post = {
+        'service_group': {
+            'name': slb_service_group,
+            'protocol': protocol,
+            'lb_method': load_balancing_methods[slb_service_group_method],
+        }
+    }
 
-    if slb_server is None and slb_server_port is None:
-        json_post = {'service_group': {'name': slb_service_group,
-                                       'protocol': protocol,
-                                       'lb_method': load_balancing_methods[slb_service_group_method]}}
-    elif slb_server is not None and slb_server_port is not None:
-        json_post = {'service_group': {'name': slb_service_group,
-                                       'protocol': protocol,
-                                       'lb_method': load_balancing_methods[slb_service_group_method],
-                                       'member_list':
-                                       [{'server': slb_server,
-                                         'port': slb_server_port,
-                                         'status': status}]}}
-    else:
-        module.fail_json(msg='server_name and server_name_port are \
-            required to add to the service-group')
+    # first we authenticate to get a session id
+    session_url = axapi_authenticate(module, axapi_base_url, username, password)
 
-    try:
-        session_url = axapi_authenticate(axapi_base_url, user, pwd)
+    # 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)
 
-        if state == 'present':
-            response = axapi_call(session_url +
-                                  '&method=slb.service_group.search',
-                                  json.dumps({'name': slb_service_group}))
-            slb_service_group_exist = re.search(slb_service_group,
-                                                response, re.I)
+    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 slb_service_group_exist is None:
-                response = axapi_call(session_url +
-                                      '&method=slb.service_group.create',
-                                      json.dumps(json_post))
-            else:
-                response = axapi_call(session_url +
-                                      '&method=slb.service_group.update',
-                                      json.dumps(json_post))
+        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 state == 'absent':
-            response = axapi_call(session_url +
-                                  '&method=slb.service_group.delete',
-                                  json.dumps({'name': slb_service_group}))
+            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
 
-        result = json.loads(response)
-        axapi_call(session_url + '&method=session.close')
+        # 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', [])
 
-    except Exception, e:
-        return module.fail_json(msg='received exception: %s' % e)
+        # 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
 
-    if 'respone' in result and 'err' in result['response']:
-        return module.fail_json(msg=result['response']['err']['msg'])
+        # 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
 
-    module.exit_json(changed=True, content=result)
+        # 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
 from ansible.module_utils.basic import *
+from ansible.module_utils.urls import *
+from ansible.module_utils.a10 import *
+
 main()
diff --git a/net_infrastructure/a10_virtual b/net_infrastructure/a10_virtual
deleted file mode 100644
index aed49ce3ea5..00000000000
--- a/net_infrastructure/a10_virtual
+++ /dev/null
@@ -1,352 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-"""
-Ansible module to manage A10 Networks slb virtual server objects
-(c) 2014, Mischa Peters <mpeters@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/>.
-"""
-
-DOCUMENTATION = '''
----
-module: a10_virtual_server
-version_added: 1.0
-short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices
-description:
-    - Manage slb virtual server objects on A10 Networks devices via aXAPI
-author: Mischa Peters
-notes:
-    - Requires A10 Networks aXAPI 2.1
-requirements:
-    - urllib2
-    - re
-options:
-  host:
-    description:
-      - hostname or ip of your A10 Networks device
-    required: true
-    default: null
-    aliases: []
-    choices: []
-  username:
-    description:
-      - admin account of your A10 Networks device
-    required: true
-    default: null
-    aliases: ['user', 'admin']
-    choices: []
-  password:
-    description:
-      - admin password of your A10 Networks device
-    required: true
-    default: null
-    aliases: ['pass', 'pwd']
-    choices: []
-  virtual_server:
-    description:
-      - slb virtual server name
-    required: true
-    default: null
-    aliases: ['vip', 'virtual']
-    choices: []
-  virtual_server_ip:
-    description:
-      - slb virtual server ip address
-    required: false
-    default: null
-    aliases: ['ip', 'address']
-    choices: []
-  virtual_server_status:
-    description:
-      - slb virtual server status
-    required: false
-    default: enable
-    aliases: ['status']
-    choices: ['enabled', 'disabled']
-  virtual_server_port:
-    description:
-      - slb virtual server port
-    required: false
-    default: round-robin
-    aliases: ['port', 'vport']
-    choices: []
-  virtual_server_type:
-    description:
-      - slb virtual server port type
-    required: false
-    default: null
-    aliases: ['proto', 'protocol']
-    choices: ['tcp', 'udp', 'fast-http', 'http', 'https']
-  virtual_server_port_status:
-    description:
-      - slb virtual server port status
-    required: false
-    default: enable
-    aliases: ['status']
-    choices: ['enabled', 'disabled']
-  service_group:
-    description:
-      - slb virtual server service-group
-    required: false
-    default: enabled
-    aliases: ['pool', 'group']
-    choices: []
-  state:
-    description:
-      - create, update or remove slb virtual server
-    required: false
-    default: present
-    aliases: []
-    choices: ['present', 'absent']
-'''
-
-EXAMPLES = '''
-# Create a new virtual server
-ansible host -m a10_virtual -a "host=a10adc.example.com username=axapiuser password=axapipass virtual_server=vip1 virtual_server_ip=192.168.1.20"
-
-# Add a virtual port
-ansible host -m a10_virtual -a "host=a10adc.example.com username=axapiuser password=axapipass virtual_server=vip1 virtual_server_ip=192.168.1.20 virtual_server_port=80 virtual_server_port_type=http service_group=sg-80-tcp"
-
-# Disable a virtual server
-ansible host -m a10_virtual -a "host=a10adc.example.com username=axapiuser password=axapipass virtual_server=vip1 status=disable"
-
-# Disable a virtual server port
-ansible host -m a10_virtual -a "host=a10adc.example.com username=axapiuser password=axapipass virtual_server=vip1 virtual_server_port=80 virtual_server_port_type=http virtual_server_port_status=disable"
-'''
-
-import urllib2
-
-
-def axapi_call(url, post=None):
-    result = urllib2.urlopen(url, post).read()
-    return result
-
-
-def axapi_authenticate(base_url, user, pwd):
-    url = base_url + '&method=authenticate&username=' + user + \
-        '&password=' + pwd
-    result = json.loads(axapi_call(url))
-    if 'response' in result:
-        return module.fail_json(msg=result['response']['err']['msg'])
-    sessid = result['session_id']
-    return base_url + '&session_id=' + sessid
-
-
-def main():
-    global module
-    module = AnsibleModule(
-        argument_spec=dict(
-            host=dict(type='str', required=True),
-            username=dict(type='str', aliases=['user', 'admin'],
-                          required=True),
-            password=dict(type='str', aliases=['pass', 'pwd'],
-                          required=True),
-            virtual_server=dict(type='str', aliases=['vip', 'virtual'],
-                                required=True),
-            virtual_server_ip=dict(type='str',
-                                   aliases=['ip', 'address']),
-            virtual_server_status=dict(type='str', default='enabled',
-                                       aliases=['status'],
-                                       choices=['enabled', 'disabled']),
-            virtual_server_port=dict(type='int',
-                                     aliases=['port', 'vport']),
-            virtual_server_port_type=dict(type='str',
-                                          aliases=['proto', 'protocol'],
-                                          choices=['tcp', 'udp', 'fast-http',
-                                                   'http', 'https']),
-            virtual_server_port_status=dict(type='str', default='enabled',
-                                            aliases=['portstatus',
-                                                     'port_status'],
-                                            choices=['enabled', 'disabled']),
-            service_group=dict(type='str', aliases=['pool', 'group']),
-            state=dict(type='str', default='present',
-                       choices=['present', 'absent']),
-        ),
-        supports_check_mode=False
-    )
-
-    host = module.params['host']
-    user = module.params['username']
-    pwd = module.params['password']
-    slb_virtual = module.params['virtual_server']
-    slb_virtual_ip = module.params['virtual_server_ip']
-    slb_virtual_status = module.params['virtual_server_status']
-    slb_virtual_port = module.params['virtual_server_port']
-    slb_virtual_port_type = module.params['virtual_server_port_type']
-    slb_virtual_port_status = module.params['virtual_server_port_status']
-    slb_service_group = module.params['service_group']
-    state = module.params['state']
-
-    axapi_base_url = 'https://' + host + '/services/rest/V2.1/?format=json'
-    vport_types = {'tcp': 2,
-                   'udp': 3,
-                   'fast-http': 9,
-                   'http': 11,
-                   'https': 12}
-
-    if slb_virtual_status == 'enabled':
-        status = '1'
-    else:
-        status = '0'
-
-    if slb_virtual_port_status == 'enabled':
-        port_status = '1'
-    else:
-        port_status = '0'
-
-    if slb_virtual is None:
-        module.fail_json(msg='virtual_server is required')
-
-    try:
-        session_url = axapi_authenticate(axapi_base_url, user, pwd)
-
-        if state == 'present':
-            find_slb_virtual = axapi_call(session_url +
-                                          '&method=slb.virtual_server.search',
-                                          json.dumps({'name': slb_virtual}))
-            slb_virtual_fail = re.search('status": "fail',
-                                         find_slb_virtual, re.I)
-
-            if slb_virtual_fail:
-                if slb_virtual_port is None and slb_virtual_port_type is None \
-                        and slb_service_group is None:
-                    json_post = {'virtual_server': {'name': slb_virtual,
-                                                    'address': slb_virtual_ip,
-                                                    'status': status}}
-                elif slb_virtual_port is not None and \
-                        slb_virtual_port_type is not None and \
-                        slb_service_group is None:
-                    json_post = {'virtual_server':
-                                 {'name': slb_virtual,
-                                  'address': slb_virtual_ip,
-                                  'status': status,
-                                  'vport_list':
-                                  [{'protocol':
-                                   vport_types[slb_virtual_port_type],
-                                   'port': slb_virtual_port}]}}
-                elif slb_virtual_port is not None and \
-                        slb_virtual_port_type is not None and \
-                        slb_service_group is not None:
-                    json_post = {'virtual_server':
-                                 {'name': slb_virtual,
-                                  'address': slb_virtual_ip,
-                                  'status': status, 'vport_list':
-                                  [{'protocol':
-                                   vport_types[slb_virtual_port_type],
-                                   'port': slb_virtual_port,
-                                    'service_group': slb_service_group}]}}
-                else:
-                    module.fail_json(msg='virtual_server_port and
-                                     virtual_server_type are required to
-                                     create the virtual port')
-
-                response = axapi_call(session_url +
-                                      '&method=slb.virtual_server.create',
-                                      json.dumps(json_post))
-            else:
-                response = axapi_call(session_url +
-                                      '&method=slb.virtual_server.search',
-                                      json.dumps({'name': slb_virtual}))
-                slb_virtual_port_exist = re.search('"port":' +
-                                                   str(slb_virtual_port)
-                                                   response, re.I)
-                current_status = json.loads(response)['virtual_server']['status']
-                current_port_status = 1
-
-                if slb_virtual_port_exist:
-                    vport_list = json.loads(response)['virtual_server']['vport_list']
-                    if vport_list:
-                        for port in range(len(vport_list)):
-                            if slb_virtual_port == str(vport_list[port]['port']):
-                                current_port_status = vport_list[port]['port']
-
-                    json_post = {'address': slb_virtual_ip,
-                                 'vport':
-                                 {'protocol':
-                                  vport_types[slb_virtual_port_type],
-                                  'port': slb_virtual_port,
-                                  'service_group': slb_service_group},
-                                 'status': port_status}
-                    response = axapi_call(session_url +
-                                          '&method=slb.virtual_server.\
-                                          vport.update', json.dumps(json_post))
-                else:
-                    if slb_service_group is None:
-                        module.fail_json(msg='service_group is required')
-                    json_post = {'name': slb_virtual,
-                                 'vport':
-                                 {'protocol':
-                                  vport_types[slb_virtual_port_type],
-                                  'port': slb_virtual_port,
-                                  'service_group': slb_service_group},
-                                 'status': port_status}
-                    response = axapi_call(session_url +
-                                          '&method=slb.virtual_server.\
-                                          vport.create', json.dumps(json_post))
-
-                if current_status != status:
-                    json_post = {'virtual_server':
-                                 {'name': slb_virtual,
-                                  'address': slb_virtual_ip,
-                                  'status': status}}
-                    response = axapi_call(session_url +
-                                          '&method=slb.virtual_server.update',
-                                          json.dumps(json_post))
-
-                if current_port_status != port_status:
-                    json_post = {'address': slb_virtual_ip, 'vport':
-                                 {'protocol':
-                                  vport_types[slb_virtual_port_type],
-                                  'port': slb_virtual_port},
-                                 'status': port_status}
-                    response = axapi_call(session_url +
-                                          '&method=slb.virtual_server.\
-                                          vport.update', json.dumps(json_post))
-
-        if state == 'absent':
-            if slb_virtual_port is not None and \
-                    slb_virtual_port_type is not None:
-                response = axapi_call(session_url +
-                                      '&method=slb.virtual_server.\
-                                      vport.delete',
-                                      json.dumps({'name': slb_virtual,
-                                                  'vport':
-                                                  {'protocol':
-                                                   vport_types[slb_virtual_port_type],
-                                                   'port': slb_virtual_port}}))
-            elif slb_virtual_port is None and slb_virtual_port_type is None:
-                response = axapi_call(session_url +
-                                      '&method=slb.virtual_server.delete',
-                                      json.dumps({'name': slb_virtual}))
-            else:
-                module.fail_json(msg='virtual_server_port and \
-                    virtual_server_type are required to remove \
-                    the virtual port')
-
-        result = json.loads(response)
-        axapi_call(session_url + '&method=session.close')
-
-    except Exception, e:
-        return module.fail_json(msg='received exception: %s' % e)
-
-    if 'respone' in result and 'err' in result['response']:
-        return module.fail_json(msg=result['response']['err']['msg'])
-
-    module.exit_json(changed=True, content=result)
-
-from ansible.module_utils.basic import *
-main()
diff --git a/net_infrastructure/a10_virtual_server b/net_infrastructure/a10_virtual_server
new file mode 100644
index 00000000000..3d807c098cf
--- /dev/null
+++ b/net_infrastructure/a10_virtual_server
@@ -0,0 +1,299 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Ansible module to manage A10 Networks slb virtual server objects
+(c) 2014, Mischa Peters <mpeters@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/>.
+"""
+
+DOCUMENTATION = '''
+---
+module: a10_virtual_server
+version_added: 1.8
+short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices
+description:
+    - Manage slb virtual server objects on A10 Networks devices via aXAPI
+author: Mischa Peters
+notes:
+    - Requires A10 Networks aXAPI 2.1
+requirements:
+    - urllib2
+    - re
+options:
+  host:
+    description:
+      - hostname or ip of your A10 Networks device
+    required: true
+    default: null
+    aliases: []
+    choices: []
+  username:
+    description:
+      - admin account of your A10 Networks device
+    required: true
+    default: null
+    aliases: ['user', 'admin']
+    choices: []
+  password:
+    description:
+      - admin password of your A10 Networks device
+    required: true
+    default: null
+    aliases: ['pass', 'pwd']
+    choices: []
+  virtual_server:
+    description:
+      - slb virtual server name
+    required: true
+    default: null
+    aliases: ['vip', 'virtual']
+    choices: []
+  virtual_server_ip:
+    description:
+      - slb virtual server ip address
+    required: false
+    default: null
+    aliases: ['ip', 'address']
+    choices: []
+  virtual_server_status:
+    description:
+      - slb virtual server status
+    required: false
+    default: enable
+    aliases: ['status']
+    choices: ['enabled', 'disabled']
+  virtual_server_ports:
+    description:
+      - A list of ports to create for the virtual server. Each list item should be a
+        dictionary which specifies the C(port:) and C(type:), but can also optionally
+        specify the C(service_group:) as well as the C(status:). See the examples
+        below for details. This parameter is required when C(state) is C(present).
+    required: false
+  write_config:
+    description:
+      - If C(yes), any changes will cause a write of the running configuration
+        to non-volatile memory. This will save I(all) configuration changes,
+        including those that may have been made manually or through other modules,
+        so care should be taken when specifying C(yes).
+    required: false
+    default: "no"
+    choices: ["yes", "no"]
+  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']
+
+'''
+
+EXAMPLES = '''
+# Create a new virtual server
+- a10_virtual_server: 
+    host: a10.mydomain.com
+    username: myadmin
+    password: mypassword
+    virtual_server: vserver1
+    virtual_server_ip: 1.1.1.1
+    virtual_server_ports:
+      - port: 80
+        protocol: TCP
+        service_group: sg-80-tcp
+      - port: 443
+        protocol: HTTPS
+        service_group: sg-443-https
+      - port: 8080
+        protocol: http
+        status: disabled
+
+'''
+
+VALID_PORT_FIELDS = ['port', 'protocol', 'service_group', 'status']
+
+def validate_ports(module, ports):
+    for item in ports:
+        for key in item:
+            if key not in VALID_PORT_FIELDS:
+                module.fail_json(msg="invalid port field (%s), must be one of: %s" % (key, ','.join(VALID_PORT_FIELDS)))
+
+        # validate the port number is present and an integer
+        if 'port' in item:
+            try:
+                item['port'] = int(item['port'])
+            except:
+                module.fail_json(msg="port definitions must be integers")
+        else:
+            module.fail_json(msg="port definitions must define the port field")
+
+        # validate the port protocol is present, and convert it to
+        # the internal API integer value (and validate it)
+        if 'protocol' in item:
+            protocol = axapi_get_vport_protocol(item['protocol'])
+            if not protocol:
+                module.fail_json(msg="invalid port protocol, must be one of: %s" % ','.join(AXAPI_VPORT_PROTOCOLS))
+            else:
+                item['protocol'] = protocol
+        else:
+            module.fail_json(msg="port definitions must define the port protocol (%s)" % ','.join(AXAPI_VPORT_PROTOCOLS))
+
+        # convert the status to the internal API integer value
+        if 'status' in item:
+            item['status'] = axapi_enabled_disabled(item['status'])
+        else:
+            item['status'] = 1
+
+        # ensure the service_group field is at least present
+        if 'service_group' not in item:
+            item['service_group'] = ''
+
+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']),
+            virtual_server=dict(type='str', aliases=['vip', 'virtual'], required=True),
+            virtual_server_ip=dict(type='str', aliases=['ip', 'address'], required=True),
+            virtual_server_status=dict(type='str', default='enabled', aliases=['status'], choices=['enabled', 'disabled']),
+            virtual_server_ports=dict(type='list', required=True),
+        )
+    )
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        supports_check_mode=False
+    )
+
+    host = module.params['host']
+    username = module.params['username']
+    password = module.params['password']
+    state = module.params['state']
+    write_config = module.params['write_config']
+    slb_virtual = module.params['virtual_server']
+    slb_virtual_ip = module.params['virtual_server_ip']
+    slb_virtual_status = module.params['virtual_server_status']
+    slb_virtual_ports = module.params['virtual_server_ports']
+
+    if slb_virtual is None:
+        module.fail_json(msg='virtual_server is required')
+
+    validate_ports(module, slb_virtual_ports)
+
+    axapi_base_url = 'https://%s/services/rest/V2.1/?format=json' % host
+    session_url = axapi_authenticate(module, axapi_base_url, username, password)
+
+    slb_virtual_data = axapi_call(module, session_url + '&method=slb.virtual_server.search', json.dumps({'name': slb_virtual}))
+    slb_virtual_exists = not axapi_failure(slb_virtual_data)
+
+    changed = False
+    if state == 'present':
+        json_post = {
+            'virtual_server': {
+                'name': slb_virtual,
+                'address': slb_virtual_ip,
+                'status': axapi_enabled_disabled(slb_virtual_status),
+                'vport_list': slb_virtual_ports,
+            }
+        }
+
+        # before creating/updating we need to validate that any
+        # service groups defined in the ports list exist since
+        # since the API will still create port definitions for
+        # them while indicating a failure occurred
+        checked_service_groups = []
+        for port in slb_virtual_ports:
+            if 'service_group' in port and port['service_group'] not in checked_service_groups:
+                # skip blank service group entries
+                if port['service_group'] == '':
+                    continue
+                result = axapi_call(module, session_url + '&method=slb.service_group.search', json.dumps({'name': port['service_group']}))
+                if axapi_failure(result):
+                    module.fail_json(msg="the service group %s specified in the ports list does not exist" % port['service_group'])
+                checked_service_groups.append(port['service_group'])
+
+        if not slb_virtual_exists:
+            result = axapi_call(module, session_url + '&method=slb.virtual_server.create', json.dumps(json_post))
+            if axapi_failure(result):
+                module.fail_json(msg="failed to create the virtual server: %s" % result['response']['err']['msg'])
+            changed = True
+        else:
+            def needs_update(src_ports, dst_ports):
+                '''
+                Checks to determine if the port definitions of the src_ports
+                array are in or different from those in dst_ports. If there is
+                a difference, this function returns true, otherwise false.
+                '''
+                for src_port in src_ports:
+                    found = False
+                    different = False
+                    for dst_port in dst_ports:
+                        if src_port['port'] == dst_port['port']:
+                            found = True
+                            for valid_field in VALID_PORT_FIELDS:
+                                if src_port[valid_field] != dst_port[valid_field]:
+                                    different = True
+                                    break
+                            if found or different:
+                                break
+                    if not found or different:
+                        return True
+                # every port from the src exists in the dst, and none of them were different
+                return False
+
+            defined_ports = slb_virtual_data.get('virtual_server', {}).get('vport_list', [])
+
+            # we check for a needed update both ways, in case ports
+            # are missing from either the ones specified by the user
+            # or from those on the device
+            if needs_update(defined_ports, slb_virtual_ports) or needs_update(slb_virtual_ports, defined_ports):
+                result = axapi_call(module, session_url + '&method=slb.virtual_server.update', json.dumps(json_post))
+                if axapi_failure(result):
+                    module.fail_json(msg="failed to create the virtual server: %s" % result['response']['err']['msg'])
+                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.virtual_server.search', json.dumps({'name': slb_virtual}))
+        else:
+            result = slb_virtual_data
+    elif state == 'absent':
+        if slb_virtual_exists:
+            result = axapi_call(module, session_url + '&method=slb.virtual_server.delete', json.dumps({'name': slb_virtual}))
+            changed = True
+        else:
+            result = dict(msg="the virtual server 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
+from ansible.module_utils.basic import *
+from ansible.module_utils.urls import *
+from ansible.module_utils.a10 import *
+
+main()
+