mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-08 06:12:51 +00:00
Relocating extras into lib/ansible/modules/ after merge
This commit is contained in:
committed by
Matt Clay
parent
c65ba07d2c
commit
011ea55a8f
0
lib/ansible/modules/network/a10/__init__.py
Normal file
0
lib/ansible/modules/network/a10/__init__.py
Normal file
303
lib/ansible/modules/network/a10/a10_server.py
Normal file
303
lib/ansible/modules/network/a10/a10_server.py
Normal file
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to manage A10 Networks slb server objects
|
||||
(c) 2014, Mischa Peters <mpeters@a10networks.com>,
|
||||
2016, 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_server
|
||||
version_added: 1.8
|
||||
short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' server object.
|
||||
description:
|
||||
- Manage SLB (Server Load Balancer) server objects on A10 Networks devices via aXAPIv2.
|
||||
author: "Eric Chou (@ericchou) 2016, Mischa Peters (@mischapeters) 2014"
|
||||
notes:
|
||||
- Requires A10 Networks aXAPI 2.1.
|
||||
extends_documentation_fragment: a10
|
||||
options:
|
||||
partition:
|
||||
version_added: "2.3"
|
||||
description:
|
||||
- set active-partition
|
||||
required: false
|
||||
default: null
|
||||
server_name:
|
||||
description:
|
||||
- The SLB (Server Load Balancer) server name.
|
||||
required: true
|
||||
aliases: ['server']
|
||||
server_ip:
|
||||
description:
|
||||
- The SLB server IPv4 address.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['ip', 'address']
|
||||
server_status:
|
||||
description:
|
||||
- The SLB virtual server status.
|
||||
required: false
|
||||
default: enabled
|
||||
aliases: ['status']
|
||||
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
|
||||
state:
|
||||
description:
|
||||
- This is to specify the operation to create, update or remove SLB server.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
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
|
||||
version_added: 2.3
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
#
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a new server
|
||||
- a10_server:
|
||||
host: a10.mydomain.com
|
||||
username: myadmin
|
||||
password: mypassword
|
||||
partition: mypartition
|
||||
server: test
|
||||
server_ip: 1.1.1.100
|
||||
server_ports:
|
||||
- port_num: 8080
|
||||
protocol: tcp
|
||||
- port_num: 8443
|
||||
protocol: TCP
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
content:
|
||||
description: the full info regarding the slb_server
|
||||
returned: success
|
||||
type: string
|
||||
sample: "mynewserver"
|
||||
'''
|
||||
|
||||
|
||||
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)))
|
||||
|
||||
# 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))
|
||||
|
||||
# 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']),
|
||||
server_name=dict(type='str', aliases=['server'], required=True),
|
||||
server_ip=dict(type='str', aliases=['ip', 'address']),
|
||||
server_status=dict(type='str', default='enabled', aliases=['status'], choices=['enabled', 'disabled']),
|
||||
server_ports=dict(type='list', aliases=['port'], default=[]),
|
||||
partition=dict(type='str', default=[]),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
host = module.params['host']
|
||||
partition = module.params['partition']
|
||||
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_status = module.params['server_status']
|
||||
slb_server_ports = module.params['server_ports']
|
||||
|
||||
if slb_server is None:
|
||||
module.fail_json(msg='server_name is required')
|
||||
|
||||
axapi_base_url = 'https://%s/services/rest/V2.1/?format=json' % host
|
||||
session_url = axapi_authenticate(module, axapi_base_url, username, password)
|
||||
|
||||
# validate the ports data structure
|
||||
validate_ports(module, slb_server_ports)
|
||||
|
||||
json_post = {
|
||||
'server': {
|
||||
'name': slb_server,
|
||||
}
|
||||
}
|
||||
|
||||
# add optional module parameters
|
||||
if slb_server_ip:
|
||||
json_post['server']['host'] = slb_server_ip
|
||||
|
||||
if slb_server_ports:
|
||||
json_post['server']['port_list'] = slb_server_ports
|
||||
|
||||
if slb_server_status:
|
||||
json_post['server']['status'] = axapi_enabled_disabled(slb_server_status)
|
||||
|
||||
slb_server_partition = axapi_call(module, session_url + '&method=system.partition.active', json.dumps({'name': partition}))
|
||||
|
||||
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)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not slb_server_exists:
|
||||
if not slb_server_ip:
|
||||
module.fail_json(msg='you must specify an IP address when creating a server')
|
||||
|
||||
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 port_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
|
||||
|
||||
def status_needs_update(current_status, new_status):
|
||||
'''
|
||||
Check to determine if we want to change the status of a server.
|
||||
If there is a difference between the current status of the server and
|
||||
the desired status, return true, otherwise false.
|
||||
'''
|
||||
if current_status != new_status:
|
||||
return True
|
||||
return False
|
||||
|
||||
defined_ports = slb_server_data.get('server', {}).get('port_list', [])
|
||||
current_status = slb_server_data.get('server', {}).get('status')
|
||||
|
||||
# we check for a needed update several ways
|
||||
# - in case ports are missing from the ones specified by the user
|
||||
# - in case ports are missing from those on the device
|
||||
# - in case we are change the status of a server
|
||||
if port_needs_update(defined_ports, slb_server_ports) or port_needs_update(slb_server_ports, defined_ports) or status_needs_update(current_status, axapi_enabled_disabled(slb_server_status)):
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
# 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_get_port_protocol, axapi_enabled_disabled
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
255
lib/ansible/modules/network/a10/a10_server_axapi3.py
Normal file
255
lib/ansible/modules/network/a10/a10_server_axapi3.py
Normal file
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to manage A10 Networks slb server objects
|
||||
(c) 2014, Mischa Peters <mpeters@a10networks.com>, 2016, 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_server_axapi3
|
||||
version_added: 2.3
|
||||
short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices
|
||||
description:
|
||||
- Manage SLB (Server Load Balancer) server objects on A10 Networks devices via aXAPIv3.
|
||||
author: "Eric Chou (@ericchou) based on previous work by Mischa Peters (@mischapeters)"
|
||||
extends_documentation_fragment: a10
|
||||
options:
|
||||
server_name:
|
||||
description:
|
||||
- The SLB (Server Load Balancer) server name.
|
||||
required: true
|
||||
aliases: ['server']
|
||||
server_ip:
|
||||
description:
|
||||
- The SLB (Server Load Balancer) server IPv4 address.
|
||||
required: true
|
||||
aliases: ['ip', 'address']
|
||||
server_status:
|
||||
description:
|
||||
- The SLB (Server Load Balancer) virtual server status.
|
||||
required: false
|
||||
default: enable
|
||||
aliases: ['action']
|
||||
choices: ['enable', 'disable']
|
||||
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:).
|
||||
required: false
|
||||
default: null
|
||||
operation:
|
||||
description:
|
||||
- Create, Update or Remove SLB server. For create and update operation, we use the IP address and server
|
||||
name specified in the POST message. For delete operation, we use the server name in the request URI.
|
||||
required: false
|
||||
default: create
|
||||
choices: ['create', 'update', 'remove']
|
||||
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 server
|
||||
- a10_server:
|
||||
host: a10.mydomain.com
|
||||
username: myadmin
|
||||
password: mypassword
|
||||
server: test
|
||||
server_ip: 1.1.1.100
|
||||
validate_certs: false
|
||||
server_status: enable
|
||||
write_config: yes
|
||||
operation: create
|
||||
server_ports:
|
||||
- port-number: 8080
|
||||
protocol: tcp
|
||||
action: enable
|
||||
- port-number: 8443
|
||||
protocol: TCP
|
||||
|
||||
'''
|
||||
|
||||
VALID_PORT_FIELDS = ['port-number', 'protocol', 'action']
|
||||
|
||||
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-number' in item:
|
||||
try:
|
||||
item['port-number'] = int(item['port-number'])
|
||||
except:
|
||||
module.fail_json(msg="port-number entries in the port definitions must be integers")
|
||||
else:
|
||||
module.fail_json(msg="port definitions must define the port-number field")
|
||||
|
||||
# validate the port protocol is present, no need to convert to the internal API integer value in v3
|
||||
if 'protocol' in item:
|
||||
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))
|
||||
|
||||
# 'status' is 'action' in AXAPIv3
|
||||
# no need to convert the status, a.k.a action, to the internal API integer value in v3
|
||||
# action is either enabled or disabled
|
||||
if 'action' in item:
|
||||
action = item['action']
|
||||
if action not in ['enable', 'disable']:
|
||||
module.fail_json(msg="server action must be enable or disable")
|
||||
else:
|
||||
item['action'] = 'enable'
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = a10_argument_spec()
|
||||
argument_spec.update(url_argument_spec())
|
||||
argument_spec.update(
|
||||
dict(
|
||||
operation=dict(type='str', default='create', choices=['create', 'update', 'delete']),
|
||||
server_name=dict(type='str', aliases=['server'], required=True),
|
||||
server_ip=dict(type='str', aliases=['ip', 'address'], required=True),
|
||||
server_status=dict(type='str', default='enable', aliases=['action'], choices=['enable', 'disable']),
|
||||
server_ports=dict(type='list', aliases=['port'], default=[]),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
host = module.params['host']
|
||||
username = module.params['username']
|
||||
password = module.params['password']
|
||||
operation = module.params['operation']
|
||||
write_config = module.params['write_config']
|
||||
slb_server = module.params['server_name']
|
||||
slb_server_ip = module.params['server_ip']
|
||||
slb_server_status = module.params['server_status']
|
||||
slb_server_ports = module.params['server_ports']
|
||||
|
||||
axapi_base_url = 'https://{}/axapi/v3/'.format(host)
|
||||
axapi_auth_url = axapi_base_url + 'auth/'
|
||||
signature = axapi_authenticate_v3(module, axapi_auth_url, username, password)
|
||||
|
||||
# validate the ports data structure
|
||||
validate_ports(module, slb_server_ports)
|
||||
|
||||
|
||||
json_post = {
|
||||
"server-list": [
|
||||
{
|
||||
"name": slb_server,
|
||||
"host": slb_server_ip
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# add optional module parameters
|
||||
if slb_server_ports:
|
||||
json_post['server-list'][0]['port-list'] = slb_server_ports
|
||||
|
||||
if slb_server_status:
|
||||
json_post['server-list'][0]['action'] = slb_server_status
|
||||
|
||||
slb_server_data = axapi_call_v3(module, axapi_base_url+'slb/server/', method='GET', body='', signature=signature)
|
||||
|
||||
# for empty slb server list
|
||||
if axapi_failure(slb_server_data):
|
||||
slb_server_exists = False
|
||||
else:
|
||||
slb_server_list = [server['name'] for server in slb_server_data['server-list']]
|
||||
if slb_server in slb_server_list:
|
||||
slb_server_exists = True
|
||||
else:
|
||||
slb_server_exists = False
|
||||
|
||||
changed = False
|
||||
if operation == 'create':
|
||||
if slb_server_exists == False:
|
||||
result = axapi_call_v3(module, axapi_base_url+'slb/server/', method='POST', body=json.dumps(json_post), signature=signature)
|
||||
if axapi_failure(result):
|
||||
module.fail_json(msg="failed to create the server: %s" % result['response']['err']['msg'])
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="server already exists, use state='update' instead")
|
||||
changed = False
|
||||
# if we changed things, get the full info regarding result
|
||||
if changed:
|
||||
result = axapi_call_v3(module, axapi_base_url + 'slb/server/' + slb_server, method='GET', body='', signature=signature)
|
||||
else:
|
||||
result = slb_server_data
|
||||
elif operation == 'delete':
|
||||
if slb_server_exists:
|
||||
result = axapi_call_v3(module, axapi_base_url + 'slb/server/' + slb_server, method='DELETE', body='', signature=signature)
|
||||
if axapi_failure(result):
|
||||
module.fail_json(msg="failed to delete server: %s" % result['response']['err']['msg'])
|
||||
changed = True
|
||||
else:
|
||||
result = dict(msg="the server was not present")
|
||||
elif operation == 'update':
|
||||
if slb_server_exists:
|
||||
result = axapi_call_v3(module, axapi_base_url + 'slb/server/', method='PUT', body=json.dumps(json_post), signature=signature)
|
||||
if axapi_failure(result):
|
||||
module.fail_json(msg="failed to update server: %s" % result['response']['err']['msg'])
|
||||
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_v3(module, axapi_base_url+'write/memory/', method='POST', body='', signature=signature)
|
||||
if axapi_failure(write_result):
|
||||
module.fail_json(msg="failed to save the configuration: %s" % write_result['response']['err']['msg'])
|
||||
|
||||
# log out gracefully and exit
|
||||
axapi_call_v3(module, axapi_base_url + 'logoff/', method='POST', body='', signature=signature)
|
||||
module.exit_json(changed=changed, content=result)
|
||||
|
||||
|
||||
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_v3, a10_argument_spec, axapi_authenticate_v3, axapi_failure
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
339
lib/ansible/modules/network/a10/a10_service_group.py
Normal file
339
lib/ansible/modules/network/a10/a10_service_group.py
Normal file
@@ -0,0 +1,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()
|
||||
294
lib/ansible/modules/network/a10/a10_virtual_server.py
Normal file
294
lib/ansible/modules/network/a10/a10_virtual_server.py
Normal file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to manage A10 Networks slb virtual server 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_virtual_server
|
||||
version_added: 1.8
|
||||
short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' virtual servers.
|
||||
description:
|
||||
- Manage SLB (Server Load Balancing) virtual server objects on A10 Networks devices via aXAPIv2.
|
||||
author: "Eric Chou (@ericchou) 2016, Mischa Peters (@mischapeters) 2014"
|
||||
notes:
|
||||
- Requires A10 Networks aXAPI 2.1.
|
||||
extends_documentation_fragment: a10
|
||||
options:
|
||||
partition:
|
||||
version_added: "2.3"
|
||||
description:
|
||||
- set active-partition
|
||||
required: false
|
||||
default: null
|
||||
virtual_server:
|
||||
description:
|
||||
- The SLB (Server Load Balancing) virtual server name.
|
||||
required: true
|
||||
default: null
|
||||
aliases: ['vip', 'virtual']
|
||||
virtual_server_ip:
|
||||
description:
|
||||
- The SLB virtual server IPv4 address.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['ip', 'address']
|
||||
virtual_server_status:
|
||||
description:
|
||||
- The SLB virtual server status, such as enabled or disabled.
|
||||
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
|
||||
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 virtual server
|
||||
- a10_virtual_server:
|
||||
host: a10.mydomain.com
|
||||
username: myadmin
|
||||
password: mypassword
|
||||
partition: mypartition
|
||||
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
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
content:
|
||||
description: the full info regarding the slb_virtual
|
||||
returned: success
|
||||
type: string
|
||||
sample: "mynewvirtualserver"
|
||||
'''
|
||||
|
||||
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),
|
||||
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_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_server_partition = axapi_call(module, session_url + '&method=system.partition.active', json.dumps({'name': partition}))
|
||||
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
|
||||
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, axapi_get_vport_protocol
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
lib/ansible/modules/network/asa/__init__.py
Normal file
0
lib/ansible/modules/network/asa/__init__.py
Normal file
234
lib/ansible/modules/network/asa/asa_acl.py
Normal file
234
lib/ansible/modules/network/asa/asa_acl.py
Normal file
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# 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: asa_acl
|
||||
version_added: "2.2"
|
||||
author: "Patrick Ogenstad (@ogenstad)"
|
||||
short_description: Manage access-lists on a Cisco ASA
|
||||
description:
|
||||
- This module allows you to work with access-lists on a Cisco ASA device.
|
||||
extends_documentation_fragment: asa
|
||||
options:
|
||||
lines:
|
||||
description:
|
||||
- The ordered set of commands that should be configured in the
|
||||
section. The commands must be the exact same commands as found
|
||||
in the device running-config. Be sure to note the configuration
|
||||
command syntax as some commands are automatically modified by the
|
||||
device config parser.
|
||||
required: true
|
||||
before:
|
||||
description:
|
||||
- The ordered set of commands to push on to the command stack if
|
||||
a change needs to be made. This allows the playbook designer
|
||||
the opportunity to perform configuration commands prior to pushing
|
||||
any changes without affecting how the set of commands are matched
|
||||
against the system.
|
||||
required: false
|
||||
default: null
|
||||
after:
|
||||
description:
|
||||
- The ordered set of commands to append to the end of the command
|
||||
stack if a changed needs to be made. Just like with I(before) this
|
||||
allows the playbook designer to append a set of commands to be
|
||||
executed after the command set.
|
||||
required: false
|
||||
default: null
|
||||
match:
|
||||
description:
|
||||
- Instructs the module on the way to perform the matching of
|
||||
the set of commands against the current device config. If
|
||||
match is set to I(line), commands are matched line by line. If
|
||||
match is set to I(strict), command lines are matched with respect
|
||||
to position. Finally if match is set to I(exact), command lines
|
||||
must be an equal match.
|
||||
required: false
|
||||
default: line
|
||||
choices: ['line', 'strict', 'exact']
|
||||
replace:
|
||||
description:
|
||||
- Instructs the module on the way to perform the configuration
|
||||
on the device. If the replace argument is set to I(line) then
|
||||
the modified lines are pushed to the device in configuration
|
||||
mode. If the replace argument is set to I(block) then the entire
|
||||
command block is pushed to the device in configuration mode if any
|
||||
line is not correct.
|
||||
required: false
|
||||
default: line
|
||||
choices: ['line', 'block']
|
||||
force:
|
||||
description:
|
||||
- The force argument instructs the module to not consider the
|
||||
current devices running-config. When set to true, this will
|
||||
cause the module to push the contents of I(src) into the device
|
||||
without first checking if already configured.
|
||||
required: false
|
||||
default: false
|
||||
choices: ['yes', 'no']
|
||||
config:
|
||||
description:
|
||||
- The module, by default, will connect to the remote device and
|
||||
retrieve the current running-config to use as a base for comparing
|
||||
against the contents of source. There are times when it is not
|
||||
desirable to have the task get the current running-config for
|
||||
every task in a playbook. The I(config) argument allows the
|
||||
implementer to pass in the configuruation to use as the base
|
||||
config for comparision.
|
||||
required: false
|
||||
default: null
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Note: examples below use the following provider dict to handle
|
||||
# transport and authentication to the node.
|
||||
vars:
|
||||
cli:
|
||||
host: "{{ inventory_hostname }}"
|
||||
username: cisco
|
||||
password: cisco
|
||||
transport: cli
|
||||
authorize: yes
|
||||
auth_pass: cisco
|
||||
|
||||
- asa_acl:
|
||||
lines:
|
||||
- access-list ACL-ANSIBLE extended permit tcp any any eq 82
|
||||
- access-list ACL-ANSIBLE extended permit tcp any any eq www
|
||||
- access-list ACL-ANSIBLE extended permit tcp any any eq 97
|
||||
- access-list ACL-ANSIBLE extended permit tcp any any eq 98
|
||||
- access-list ACL-ANSIBLE extended permit tcp any any eq 99
|
||||
before: clear configure access-list ACL-ANSIBLE
|
||||
match: strict
|
||||
replace: block
|
||||
provider: "{{ cli }}"
|
||||
|
||||
- asa_acl:
|
||||
lines:
|
||||
- access-list ACL-OUTSIDE extended permit tcp any any eq www
|
||||
- access-list ACL-OUTSIDE extended permit tcp any any eq https
|
||||
context: customer_a
|
||||
provider: "{{ cli }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
updates:
|
||||
description: The set of commands that will be pushed to the remote device
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
|
||||
responses:
|
||||
description: The set of responses from issuing the commands on the device
|
||||
retured: when not check_mode
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
"""
|
||||
import ansible.module_utils.asa
|
||||
|
||||
from ansible.module_utils.network import NetworkModule
|
||||
from ansible.module_utils.netcfg import NetworkConfig, dumps
|
||||
|
||||
|
||||
def get_config(module, acl_name):
|
||||
contents = module.params['config']
|
||||
if not contents:
|
||||
contents = module.config.get_config()
|
||||
|
||||
filtered_config = list()
|
||||
for item in contents.split('\n'):
|
||||
if item.startswith('access-list %s ' % acl_name):
|
||||
filtered_config.append(item)
|
||||
|
||||
return NetworkConfig(indent=1, contents='\n'.join(filtered_config))
|
||||
|
||||
def parse_acl_name(module):
|
||||
first_line = True
|
||||
for line in module.params['lines']:
|
||||
ace = line.split()
|
||||
if ace[0] != 'access-list':
|
||||
module.fail_json(msg='All lines/commands must begin with "access-list" %s is not permitted' % ace[0])
|
||||
if len(ace) <= 1:
|
||||
module.fail_json(msg='All lines/commands must contain the name of the access-list')
|
||||
if first_line:
|
||||
acl_name = ace[1]
|
||||
else:
|
||||
if acl_name != ace[1]:
|
||||
module.fail_json(msg='All lines/commands must use the same access-list %s is not %s' % (ace[1], acl_name))
|
||||
first_line = False
|
||||
|
||||
return acl_name
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = dict(
|
||||
lines=dict(aliases=['commands'], required=True, type='list'),
|
||||
before=dict(type='list'),
|
||||
after=dict(type='list'),
|
||||
match=dict(default='line', choices=['line', 'strict', 'exact']),
|
||||
replace=dict(default='line', choices=['line', 'block']),
|
||||
force=dict(default=False, type='bool'),
|
||||
config=dict()
|
||||
)
|
||||
|
||||
module = NetworkModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
lines = module.params['lines']
|
||||
|
||||
before = module.params['before']
|
||||
after = module.params['after']
|
||||
|
||||
match = module.params['match']
|
||||
replace = module.params['replace']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
candidate = NetworkConfig(indent=1)
|
||||
candidate.add(lines)
|
||||
|
||||
acl_name = parse_acl_name(module)
|
||||
|
||||
if not module.params['force']:
|
||||
contents = get_config(module, acl_name)
|
||||
config = NetworkConfig(indent=1, contents=contents)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = dumps(commands, 'commands').split('\n')
|
||||
commands = [str(c) for c in commands if c]
|
||||
else:
|
||||
commands = str(candidate).split('\n')
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
response = module.config(commands)
|
||||
result['responses'] = response
|
||||
result['changed'] = True
|
||||
|
||||
result['updates'] = commands
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
228
lib/ansible/modules/network/asa/asa_command.py
Normal file
228
lib/ansible/modules/network/asa/asa_command.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# 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: asa_command
|
||||
version_added: "2.2"
|
||||
author: "Peter Sprygada (@privateip), Patrick Ogenstad (@ogenstad)"
|
||||
short_description: Run arbitrary commands on Cisco ASA devices.
|
||||
description:
|
||||
- Sends arbitrary commands to an ASA node and returns the results
|
||||
read from the device. The M(asa_command) module includes an
|
||||
argument that will cause the module to wait for a specific condition
|
||||
before returning or timing out if the condition is not met.
|
||||
extends_documentation_fragment: asa
|
||||
options:
|
||||
commands:
|
||||
description:
|
||||
- List of commands to send to the remote device over the
|
||||
configured provider. The resulting output from the command
|
||||
is returned. If the I(wait_for) argument is provided, the
|
||||
module is not returned until the condition is satisfied or
|
||||
the number of retires as expired.
|
||||
required: true
|
||||
wait_for:
|
||||
description:
|
||||
- List of conditions to evaluate against the output of the
|
||||
command. The task will wait for each condition to be true
|
||||
before moving forward. If the conditional is not true
|
||||
within the configured number of retries, the task fails.
|
||||
See examples.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['waitfor']
|
||||
match:
|
||||
description:
|
||||
- The I(match) argument is used in conjunction with the
|
||||
I(wait_for) argument to specify the match policy. Valid
|
||||
values are C(all) or C(any). If the value is set to C(all)
|
||||
then all conditionals in the wait_for must be satisfied. If
|
||||
the value is set to C(any) then only one of the values must be
|
||||
satisfied.
|
||||
required: false
|
||||
default: all
|
||||
choices: ['any', 'all']
|
||||
retries:
|
||||
description:
|
||||
- Specifies the number of retries a command should by tried
|
||||
before it is considered failed. The command is run on the
|
||||
target device every retry and evaluated against the
|
||||
I(wait_for) conditions.
|
||||
required: false
|
||||
default: 10
|
||||
interval:
|
||||
description:
|
||||
- Configures the interval in seconds to wait between retries
|
||||
of the command. If the command does not pass the specified
|
||||
conditions, the interval indicates how long to wait before
|
||||
trying the command again.
|
||||
required: false
|
||||
default: 1
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Note: examples below use the following provider dict to handle
|
||||
# transport and authentication to the node.
|
||||
vars:
|
||||
cli:
|
||||
host: "{{ inventory_hostname }}"
|
||||
username: cisco
|
||||
password: cisco
|
||||
authorize: yes
|
||||
auth_pass: cisco
|
||||
transport: cli
|
||||
|
||||
|
||||
- asa_command:
|
||||
commands:
|
||||
- show version
|
||||
provider: "{{ cli }}"
|
||||
|
||||
- asa_command:
|
||||
commands:
|
||||
- show asp drop
|
||||
- show memory
|
||||
provider: "{{ cli }}"
|
||||
|
||||
- asa_command:
|
||||
commands:
|
||||
- show version
|
||||
provider: "{{ cli }}"
|
||||
context: system
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
stdout:
|
||||
description: the set of responses from the commands
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
|
||||
stdout_lines:
|
||||
description: The value of stdout split into a list
|
||||
returned: always
|
||||
type: list
|
||||
sample: [['...', '...'], ['...'], ['...']]
|
||||
|
||||
failed_conditions:
|
||||
description: the conditionals that failed
|
||||
retured: failed
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
"""
|
||||
from ansible.module_utils.basic import get_exception
|
||||
from ansible.module_utils.netcli import CommandRunner
|
||||
from ansible.module_utils.netcli import AddCommandError, FailedConditionsError
|
||||
from ansible.module_utils.asa import NetworkModule, NetworkError
|
||||
|
||||
VALID_KEYS = ['command', 'prompt', 'response']
|
||||
|
||||
def to_lines(stdout):
|
||||
for item in stdout:
|
||||
if isinstance(item, basestring):
|
||||
item = str(item).split('\n')
|
||||
yield item
|
||||
|
||||
def parse_commands(module):
|
||||
for cmd in module.params['commands']:
|
||||
if isinstance(cmd, basestring):
|
||||
cmd = dict(command=cmd, output=None)
|
||||
elif 'command' not in cmd:
|
||||
module.fail_json(msg='command keyword argument is required')
|
||||
elif not set(cmd.keys()).issubset(VALID_KEYS):
|
||||
module.fail_json(msg='unknown keyword specified')
|
||||
yield cmd
|
||||
|
||||
def main():
|
||||
spec = dict(
|
||||
# { command: <str>, prompt: <str>, response: <str> }
|
||||
commands=dict(type='list', required=True),
|
||||
|
||||
wait_for=dict(type='list', aliases=['waitfor']),
|
||||
match=dict(default='all', choices=['all', 'any']),
|
||||
|
||||
retries=dict(default=10, type='int'),
|
||||
interval=dict(default=1, type='int')
|
||||
)
|
||||
|
||||
module = NetworkModule(argument_spec=spec,
|
||||
connect_on_load=False,
|
||||
supports_check_mode=True)
|
||||
|
||||
commands = list(parse_commands(module))
|
||||
conditionals = module.params['wait_for'] or list()
|
||||
|
||||
warnings = list()
|
||||
|
||||
runner = CommandRunner(module)
|
||||
|
||||
for cmd in commands:
|
||||
if module.check_mode and not cmd['command'].startswith('show'):
|
||||
warnings.append('only show commands are supported when using '
|
||||
'check mode, not executing `%s`' % cmd['command'])
|
||||
else:
|
||||
if cmd['command'].startswith('conf'):
|
||||
module.fail_json(msg='asa_command does not support running '
|
||||
'config mode commands. Please use '
|
||||
'asa_config instead')
|
||||
try:
|
||||
runner.add_command(**cmd)
|
||||
except AddCommandError:
|
||||
exc = get_exception()
|
||||
warnings.append('duplicate command detected: %s' % cmd)
|
||||
|
||||
for item in conditionals:
|
||||
runner.add_conditional(item)
|
||||
|
||||
runner.retries = module.params['retries']
|
||||
runner.interval = module.params['interval']
|
||||
runner.match = module.params['match']
|
||||
|
||||
try:
|
||||
runner.run()
|
||||
except FailedConditionsError:
|
||||
exc = get_exception()
|
||||
module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions)
|
||||
except NetworkError:
|
||||
exc = get_exception()
|
||||
module.fail_json(msg=str(exc))
|
||||
|
||||
result = dict(changed=False, stdout=list())
|
||||
|
||||
for cmd in commands:
|
||||
try:
|
||||
output = runner.get_command(cmd['command'])
|
||||
except ValueError:
|
||||
output = 'command not executed due to check_mode, see warnings'
|
||||
result['stdout'].append(output)
|
||||
|
||||
result['warnings'] = warnings
|
||||
result['stdout_lines'] = list(to_lines(result['stdout']))
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
346
lib/ansible/modules/network/asa/asa_config.py
Normal file
346
lib/ansible/modules/network/asa/asa_config.py
Normal file
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# 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: asa_config
|
||||
version_added: "2.2"
|
||||
author: "Peter Sprygada (@privateip), Patrick Ogenstad (@ogenstad)"
|
||||
short_description: Manage Cisco ASA configuration sections
|
||||
description:
|
||||
- Cisco ASA configurations use a simple block indent file syntax
|
||||
for segmenting configuration into sections. This module provides
|
||||
an implementation for working with ASA configuration sections in
|
||||
a deterministic way.
|
||||
extends_documentation_fragment: asa
|
||||
options:
|
||||
lines:
|
||||
description:
|
||||
- The ordered set of commands that should be configured in the
|
||||
section. The commands must be the exact same commands as found
|
||||
in the device running-config. Be sure to note the configuration
|
||||
command syntax as some commands are automatically modified by the
|
||||
device config parser.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['commands']
|
||||
parents:
|
||||
description:
|
||||
- The ordered set of parents that uniquely identify the section
|
||||
the commands should be checked against. If the parents argument
|
||||
is omitted, the commands are checked against the set of top
|
||||
level or global commands.
|
||||
required: false
|
||||
default: null
|
||||
src:
|
||||
description:
|
||||
- Specifies the source path to the file that contains the configuration
|
||||
or configuration template to load. The path to the source file can
|
||||
either be the full path on the Ansible control host or a relative
|
||||
path from the playbook or role root directory. This argument is mutually
|
||||
exclusive with I(lines).
|
||||
required: false
|
||||
default: null
|
||||
before:
|
||||
description:
|
||||
- The ordered set of commands to push on to the command stack if
|
||||
a change needs to be made. This allows the playbook designer
|
||||
the opportunity to perform configuration commands prior to pushing
|
||||
any changes without affecting how the set of commands are matched
|
||||
against the system
|
||||
required: false
|
||||
default: null
|
||||
after:
|
||||
description:
|
||||
- The ordered set of commands to append to the end of the command
|
||||
stack if a change needs to be made. Just like with I(before) this
|
||||
allows the playbook designer to append a set of commands to be
|
||||
executed after the command set.
|
||||
required: false
|
||||
default: null
|
||||
match:
|
||||
description:
|
||||
- Instructs the module on the way to perform the matching of
|
||||
the set of commands against the current device config. If
|
||||
match is set to I(line), commands are matched line by line. If
|
||||
match is set to I(strict), command lines are matched with respect
|
||||
to position. If match is set to I(exact), command lines
|
||||
must be an equal match. Finally, if match is set to I(none), the
|
||||
module will not attempt to compare the source configuration with
|
||||
the running configuration on the remote device.
|
||||
required: false
|
||||
default: line
|
||||
choices: ['line', 'strict', 'exact', 'none']
|
||||
replace:
|
||||
description:
|
||||
- Instructs the module on the way to perform the configuration
|
||||
on the device. If the replace argument is set to I(line) then
|
||||
the modified lines are pushed to the device in configuration
|
||||
mode. If the replace argument is set to I(block) then the entire
|
||||
command block is pushed to the device in configuration mode if any
|
||||
line is not correct
|
||||
required: false
|
||||
default: line
|
||||
choices: ['line', 'block']
|
||||
update:
|
||||
description:
|
||||
- The I(update) argument controls how the configuration statements
|
||||
are processed on the remote device. Valid choices for the I(update)
|
||||
argument are I(merge) and I(check). When the argument is set to
|
||||
I(merge), the configuration changes are merged with the current
|
||||
device running configuration. When the argument is set to I(check)
|
||||
the configuration updates are determined but not actually configured
|
||||
on the remote device.
|
||||
required: false
|
||||
default: merge
|
||||
choices: ['merge', 'check']
|
||||
commit:
|
||||
description:
|
||||
- This argument specifies the update method to use when applying the
|
||||
configuration changes to the remote node. If the value is set to
|
||||
I(merge) the configuration updates are merged with the running-
|
||||
config. If the value is set to I(check), no changes are made to
|
||||
the remote host.
|
||||
required: false
|
||||
default: merge
|
||||
choices: ['merge', 'check']
|
||||
backup:
|
||||
description:
|
||||
- This argument will cause the module to create a full backup of
|
||||
the current C(running-config) from the remote device before any
|
||||
changes are made. The backup file is written to the C(backup)
|
||||
folder in the playbook root directory. If the directory does not
|
||||
exist, it is created.
|
||||
required: false
|
||||
default: no
|
||||
choices: ['yes', 'no']
|
||||
config:
|
||||
description:
|
||||
- The C(config) argument allows the playbook designer to supply
|
||||
the base configuration to be used to validate configuration
|
||||
changes necessary. If this argument is provided, the module
|
||||
will not download the running-config from the remote node.
|
||||
required: false
|
||||
default: null
|
||||
defaults:
|
||||
description:
|
||||
- This argument specifies whether or not to collect all defaults
|
||||
when getting the remote device running config. When enabled,
|
||||
the module will get the current config by issuing the command
|
||||
C(show running-config all).
|
||||
required: false
|
||||
default: no
|
||||
choices: ['yes', 'no']
|
||||
passwords:
|
||||
description:
|
||||
- This argument specifies to include passwords in the config
|
||||
when retrieving the running-config from the remote device. This
|
||||
includes passwords related to VPN endpoints. This argument is
|
||||
mutually exclusive with I(defaults).
|
||||
required: false
|
||||
default: no
|
||||
choices: ['yes', 'no']
|
||||
save:
|
||||
description:
|
||||
- The C(save) argument instructs the module to save the running-
|
||||
config to the startup-config at the conclusion of the module
|
||||
running. If check mode is specified, this argument is ignored.
|
||||
required: false
|
||||
default: no
|
||||
choices: ['yes', 'no']
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Note: examples below use the following provider dict to handle
|
||||
# transport and authentication to the node.
|
||||
vars:
|
||||
cli:
|
||||
host: "{{ inventory_hostname }}"
|
||||
username: cisco
|
||||
password: cisco
|
||||
authorize: yes
|
||||
auth_pass: cisco
|
||||
transport: cli
|
||||
|
||||
- asa_config:
|
||||
lines:
|
||||
- network-object host 10.80.30.18
|
||||
- network-object host 10.80.30.19
|
||||
- network-object host 10.80.30.20
|
||||
parents: ['object-group network OG-MONITORED-SERVERS']
|
||||
provider: "{{ cli }}"
|
||||
|
||||
- asa_config:
|
||||
host: "{{ inventory_hostname }}"
|
||||
lines:
|
||||
- message-length maximum client auto
|
||||
- message-length maximum 512
|
||||
match: line
|
||||
parents: ['policy-map type inspect dns PM-DNS', 'parameters']
|
||||
authorize: yes
|
||||
auth_pass: cisco
|
||||
username: admin
|
||||
password: cisco
|
||||
context: ansible
|
||||
|
||||
- asa_config:
|
||||
lines:
|
||||
- ikev1 pre-shared-key MyS3cretVPNK3y
|
||||
parents: tunnel-group 1.1.1.1 ipsec-attributes
|
||||
passwords: yes
|
||||
provider: "{{ cli }}"
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
updates:
|
||||
description: The set of commands that will be pushed to the remote device
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
backup_path:
|
||||
description: The full path to the backup file
|
||||
returned: when backup is yes
|
||||
type: path
|
||||
sample: /playbooks/ansible/backup/asa_config.2016-07-16@22:28:34
|
||||
responses:
|
||||
description: The set of responses from issuing the commands on the device
|
||||
returned: when not check_mode
|
||||
type: list
|
||||
sample: ['...', '...']
|
||||
"""
|
||||
import re
|
||||
|
||||
import ansible.module_utils.asa
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
from ansible.module_utils.network import NetworkModule, NetworkError
|
||||
from ansible.module_utils.netcfg import NetworkConfig, dumps
|
||||
|
||||
def get_config(module):
|
||||
contents = module.params['config']
|
||||
if not contents:
|
||||
if module.params['defaults']:
|
||||
include = 'defaults'
|
||||
elif module.params['passwords']:
|
||||
include = 'passwords'
|
||||
else:
|
||||
include = None
|
||||
contents = module.config.get_config(include=include)
|
||||
return NetworkConfig(indent=1, contents=contents)
|
||||
|
||||
def get_candidate(module):
|
||||
candidate = NetworkConfig(indent=1)
|
||||
if module.params['src']:
|
||||
candidate.load(module.params['src'])
|
||||
elif module.params['lines']:
|
||||
parents = module.params['parents'] or list()
|
||||
candidate.add(module.params['lines'], parents=parents)
|
||||
return candidate
|
||||
|
||||
def run(module, result):
|
||||
match = module.params['match']
|
||||
replace = module.params['replace']
|
||||
path = module.params['parents']
|
||||
|
||||
candidate = get_candidate(module)
|
||||
|
||||
if match != 'none':
|
||||
config = get_config(module)
|
||||
configobjs = candidate.difference(config, path=path, match=match,
|
||||
replace=replace)
|
||||
else:
|
||||
configobjs = candidate.items
|
||||
|
||||
if configobjs:
|
||||
commands = dumps(configobjs, 'commands').split('\n')
|
||||
|
||||
if module.params['lines']:
|
||||
if module.params['before']:
|
||||
commands[:0] = module.params['before']
|
||||
|
||||
if module.params['after']:
|
||||
commands.extend(module.params['after'])
|
||||
|
||||
result['updates'] = commands
|
||||
|
||||
# send the configuration commands to the device and merge
|
||||
# them with the current running config
|
||||
if not module.check_mode:
|
||||
module.config.load_config(commands)
|
||||
result['changed'] = True
|
||||
|
||||
if module.params['save']:
|
||||
if not module.check_mode:
|
||||
module.config.save_config()
|
||||
result['changed'] = True
|
||||
|
||||
def main():
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
src=dict(type='path'),
|
||||
|
||||
lines=dict(aliases=['commands'], type='list'),
|
||||
parents=dict(type='list'),
|
||||
|
||||
before=dict(type='list'),
|
||||
after=dict(type='list'),
|
||||
|
||||
match=dict(default='line', choices=['line', 'strict', 'exact', 'none']),
|
||||
replace=dict(default='line', choices=['line', 'block']),
|
||||
|
||||
config=dict(),
|
||||
defaults=dict(type='bool', default=False),
|
||||
passwords=dict(type='bool', default=False),
|
||||
|
||||
backup=dict(type='bool', default=False),
|
||||
save=dict(type='bool', default=False),
|
||||
)
|
||||
|
||||
mutually_exclusive = [('lines', 'src'), ('defaults', 'passwords')]
|
||||
|
||||
required_if = [('match', 'strict', ['lines']),
|
||||
('match', 'exact', ['lines']),
|
||||
('replace', 'block', ['lines'])]
|
||||
|
||||
module = NetworkModule(argument_spec=argument_spec,
|
||||
connect_on_load=False,
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
required_if=required_if,
|
||||
supports_check_mode=True)
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if module.params['backup']:
|
||||
result['__backup__'] = module.config.get_config()
|
||||
|
||||
try:
|
||||
run(module, result)
|
||||
except NetworkError:
|
||||
exc = get_exception()
|
||||
module.fail_json(msg=str(exc), **exc.kwargs)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
lib/ansible/modules/network/citrix/__init__.py
Normal file
0
lib/ansible/modules/network/citrix/__init__.py
Normal file
210
lib/ansible/modules/network/citrix/netscaler.py
Normal file
210
lib/ansible/modules/network/citrix/netscaler.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to manage Citrix NetScaler entities
|
||||
(c) 2013, Nandor Sivok <nandor@gawker.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: netscaler
|
||||
version_added: "1.1"
|
||||
short_description: Manages Citrix NetScaler entities
|
||||
description:
|
||||
- Manages Citrix NetScaler server and service entities.
|
||||
options:
|
||||
nsc_host:
|
||||
description:
|
||||
- hostname or ip of your netscaler
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
nsc_protocol:
|
||||
description:
|
||||
- protocol used to access netscaler
|
||||
required: false
|
||||
default: https
|
||||
aliases: []
|
||||
user:
|
||||
description:
|
||||
- username
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
password:
|
||||
description:
|
||||
- password
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
action:
|
||||
description:
|
||||
- the action you want to perform on the entity
|
||||
required: false
|
||||
default: disable
|
||||
choices: ["enable", "disable"]
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- name of the entity
|
||||
required: true
|
||||
default: hostname
|
||||
aliases: []
|
||||
type:
|
||||
description:
|
||||
- type of the entity
|
||||
required: false
|
||||
default: server
|
||||
choices: ["server", "service"]
|
||||
aliases: []
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates for the target url will not be validated. This should only be used
|
||||
on personally controlled sites using self-signed certificates.
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
|
||||
requirements: []
|
||||
author: "Nandor Sivok (@dominis)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Disable the server
|
||||
- netscaler:
|
||||
nsc_host: nsc.example.com
|
||||
user: apiuser
|
||||
password: apipass
|
||||
|
||||
# Enable the server
|
||||
- netscaler:
|
||||
nsc_host: nsc.example.com
|
||||
user: apiuser
|
||||
password: apipass
|
||||
action: enable
|
||||
|
||||
# Disable the service local:8080
|
||||
- netscaler:
|
||||
nsc_host: nsc.example.com
|
||||
user: apiuser
|
||||
password: apipass
|
||||
name: 'local:8080'
|
||||
type: service
|
||||
action: disable
|
||||
'''
|
||||
|
||||
|
||||
import base64
|
||||
import socket
|
||||
import urllib
|
||||
|
||||
class netscaler(object):
|
||||
|
||||
_nitro_base_url = '/nitro/v1/'
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
def http_request(self, api_endpoint, data_json={}):
|
||||
request_url = self._nsc_protocol + '://' + self._nsc_host + self._nitro_base_url + api_endpoint
|
||||
|
||||
data_json = urllib.urlencode(data_json)
|
||||
if not len(data_json):
|
||||
data_json = None
|
||||
|
||||
auth = base64.encodestring('%s:%s' % (self._nsc_user, self._nsc_pass)).replace('\n', '').strip()
|
||||
headers = {
|
||||
'Authorization': 'Basic %s' % auth,
|
||||
'Content-Type' : 'application/x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
response, info = fetch_url(self.module, request_url, data=data_json, headers=headers)
|
||||
|
||||
return json.load(response)
|
||||
|
||||
def prepare_request(self, action):
|
||||
resp = self.http_request(
|
||||
'config',
|
||||
{
|
||||
"object":
|
||||
{
|
||||
"params": {"action": action},
|
||||
self._type: {"name": self._name}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def core(module):
|
||||
n = netscaler(module)
|
||||
n._nsc_host = module.params.get('nsc_host')
|
||||
n._nsc_user = module.params.get('user')
|
||||
n._nsc_pass = module.params.get('password')
|
||||
n._nsc_protocol = module.params.get('nsc_protocol')
|
||||
n._name = module.params.get('name')
|
||||
n._type = module.params.get('type')
|
||||
action = module.params.get('action')
|
||||
|
||||
r = n.prepare_request(action)
|
||||
|
||||
return r['errorcode'], r
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
nsc_host = dict(required=True),
|
||||
nsc_protocol = dict(default='https'),
|
||||
user = dict(required=True),
|
||||
password = dict(required=True),
|
||||
action = dict(default='enable', choices=['enable','disable']),
|
||||
name = dict(default=socket.gethostname()),
|
||||
type = dict(default='server', choices=['service', 'server']),
|
||||
validate_certs=dict(default='yes', type='bool'),
|
||||
)
|
||||
)
|
||||
|
||||
rc = 0
|
||||
try:
|
||||
rc, result = core(module)
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(rc=rc, msg=result)
|
||||
else:
|
||||
result['changed'] = True
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.urls import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
672
lib/ansible/modules/network/cloudflare_dns.py
Normal file
672
lib/ansible/modules/network/cloudflare_dns.py
Normal file
@@ -0,0 +1,672 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
#
|
||||
# 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: cloudflare_dns
|
||||
author: "Michael Gruener (@mgruener)"
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
version_added: "2.1"
|
||||
short_description: manage Cloudflare DNS records
|
||||
description:
|
||||
- "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)"
|
||||
options:
|
||||
account_api_token:
|
||||
description:
|
||||
- "Account API token. You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://www.cloudflare.com/a/account)"
|
||||
required: true
|
||||
account_email:
|
||||
description:
|
||||
- "Account email."
|
||||
required: true
|
||||
port:
|
||||
description: Service port. Required for C(type=SRV)
|
||||
required: false
|
||||
default: null
|
||||
priority:
|
||||
description: Record priority. Required for C(type=MX) and C(type=SRV)
|
||||
required: false
|
||||
default: "1"
|
||||
proto:
|
||||
description: Service protocol. Required for C(type=SRV)
|
||||
required: false
|
||||
choices: [ 'tcp', 'udp' ]
|
||||
default: null
|
||||
proxied:
|
||||
description: Proxy through cloudflare network or just use DNS
|
||||
required: false
|
||||
default: no
|
||||
version_added: "2.3"
|
||||
record:
|
||||
description:
|
||||
- Record to add. Required if C(state=present). Default is C(@) (e.g. the zone name)
|
||||
required: false
|
||||
default: "@"
|
||||
aliases: [ "name" ]
|
||||
service:
|
||||
description: Record service. Required for C(type=SRV)
|
||||
required: false
|
||||
default: null
|
||||
solo:
|
||||
description:
|
||||
- Whether the record should be the only one for that record type and record name. Only use with C(state=present)
|
||||
- This will delete all other records with the same record name and type.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Whether the record(s) should exist or not
|
||||
required: false
|
||||
choices: [ 'present', 'absent' ]
|
||||
default: present
|
||||
timeout:
|
||||
description:
|
||||
- Timeout for Cloudflare API calls
|
||||
required: false
|
||||
default: 30
|
||||
ttl:
|
||||
description:
|
||||
- The TTL to give the new record. Must be between 120 and 2,147,483,647 seconds, or 1 for automatic.
|
||||
required: false
|
||||
default: 1 (automatic)
|
||||
type:
|
||||
description:
|
||||
- The type of DNS record to create. Required if C(state=present)
|
||||
required: false
|
||||
choices: [ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'SPF' ]
|
||||
default: null
|
||||
value:
|
||||
description:
|
||||
- The record value. Required for C(state=present)
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ "content" ]
|
||||
weight:
|
||||
description: Service weight. Required for C(type=SRV)
|
||||
required: false
|
||||
default: "1"
|
||||
zone:
|
||||
description:
|
||||
- The name of the Zone to work with (e.g. "example.com"). The Zone must already exist.
|
||||
required: true
|
||||
aliases: ["domain"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# create a test.my.com A record to point to 127.0.0.1
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
record: test
|
||||
type: A
|
||||
value: 127.0.0.1
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
register: record
|
||||
|
||||
# create a my.com CNAME record to example.com
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: present
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# change it's ttl
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
ttl: 600
|
||||
state: present
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# and delete the record
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: absent
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# create a my.com CNAME record to example.com and proxy through cloudflare's network
|
||||
- cloudflare_dns:
|
||||
zone: my.com
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: present
|
||||
proxied: yes
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# create TXT record "test.my.com" with value "unique value"
|
||||
# delete all other TXT records named "test.my.com"
|
||||
- cloudflare_dns:
|
||||
domain: my.com
|
||||
record: test
|
||||
type: TXT
|
||||
value: unique value
|
||||
state: present
|
||||
solo: true
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
|
||||
# create a SRV record _foo._tcp.my.com
|
||||
- cloudflare_dns:
|
||||
domain: my.com
|
||||
service: foo
|
||||
proto: tcp
|
||||
port: 3500
|
||||
priority: 10
|
||||
weight: 20
|
||||
type: SRV
|
||||
value: fooserver.my.com
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
record:
|
||||
description: dictionary containing the record data
|
||||
returned: success, except on record deletion
|
||||
type: dictionary
|
||||
contains:
|
||||
content:
|
||||
description: the record content (details depend on record type)
|
||||
returned: success
|
||||
type: string
|
||||
sample: 192.0.2.91
|
||||
created_on:
|
||||
description: the record creation date
|
||||
returned: success
|
||||
type: string
|
||||
sample: 2016-03-25T19:09:42.516553Z
|
||||
data:
|
||||
description: additional record data
|
||||
returned: success, if type is SRV
|
||||
type: dictionary
|
||||
sample: {
|
||||
name: "jabber",
|
||||
port: 8080,
|
||||
priority: 10,
|
||||
proto: "_tcp",
|
||||
service: "_xmpp",
|
||||
target: "jabberhost.sample.com",
|
||||
weight: 5,
|
||||
}
|
||||
id:
|
||||
description: the record id
|
||||
returned: success
|
||||
type: string
|
||||
sample: f9efb0549e96abcb750de63b38c9576e
|
||||
locked:
|
||||
description: No documentation available
|
||||
returned: success
|
||||
type: boolean
|
||||
sample: False
|
||||
meta:
|
||||
description: No documentation available
|
||||
returned: success
|
||||
type: dictionary
|
||||
sample: { auto_added: false }
|
||||
modified_on:
|
||||
description: record modification date
|
||||
returned: success
|
||||
type: string
|
||||
sample: 2016-03-25T19:09:42.516553Z
|
||||
name:
|
||||
description: the record name as FQDN (including _service and _proto for SRV)
|
||||
returned: success
|
||||
type: string
|
||||
sample: www.sample.com
|
||||
priority:
|
||||
description: priority of the MX record
|
||||
returned: success, if type is MX
|
||||
type: int
|
||||
sample: 10
|
||||
proxiable:
|
||||
description: whether this record can be proxied through cloudflare
|
||||
returned: success
|
||||
type: boolean
|
||||
sample: False
|
||||
proxied:
|
||||
description: whether the record is proxied through cloudflare
|
||||
returned: success
|
||||
type: boolean
|
||||
sample: False
|
||||
ttl:
|
||||
description: the time-to-live for the record
|
||||
returned: success
|
||||
type: int
|
||||
sample: 300
|
||||
type:
|
||||
description: the record type
|
||||
returned: success
|
||||
type: string
|
||||
sample: A
|
||||
zone_id:
|
||||
description: the id of the zone containing the record
|
||||
returned: success
|
||||
type: string
|
||||
sample: abcede0bf9f0066f94029d2e6b73856a
|
||||
zone_name:
|
||||
description: the name of the zone containing the record
|
||||
returned: success
|
||||
type: string
|
||||
sample: sample.com
|
||||
'''
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
# Let snippet from module_utils/basic.py return a proper error in this case
|
||||
pass
|
||||
|
||||
import urllib
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
|
||||
class CloudflareAPI(object):
|
||||
|
||||
cf_api_endpoint = 'https://api.cloudflare.com/client/v4'
|
||||
changed = False
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.account_api_token = module.params['account_api_token']
|
||||
self.account_email = module.params['account_email']
|
||||
self.port = module.params['port']
|
||||
self.priority = module.params['priority']
|
||||
self.proto = module.params['proto']
|
||||
self.proxied = module.params['proxied']
|
||||
self.record = module.params['record']
|
||||
self.service = module.params['service']
|
||||
self.is_solo = module.params['solo']
|
||||
self.state = module.params['state']
|
||||
self.timeout = module.params['timeout']
|
||||
self.ttl = module.params['ttl']
|
||||
self.type = module.params['type']
|
||||
self.value = module.params['value']
|
||||
self.weight = module.params['weight']
|
||||
self.zone = module.params['zone']
|
||||
|
||||
if self.record == '@':
|
||||
self.record = self.zone
|
||||
|
||||
if (self.type in ['CNAME','NS','MX','SRV']) and (self.value is not None):
|
||||
self.value = self.value.rstrip('.')
|
||||
|
||||
if (self.type == 'SRV'):
|
||||
if (self.proto is not None) and (not self.proto.startswith('_')):
|
||||
self.proto = '_' + self.proto
|
||||
if (self.service is not None) and (not self.service.startswith('_')):
|
||||
self.service = '_' + self.service
|
||||
|
||||
if not self.record.endswith(self.zone):
|
||||
self.record = self.record + '.' + self.zone
|
||||
|
||||
def _cf_simple_api_call(self,api_call,method='GET',payload=None):
|
||||
headers = { 'X-Auth-Email': self.account_email,
|
||||
'X-Auth-Key': self.account_api_token,
|
||||
'Content-Type': 'application/json' }
|
||||
data = None
|
||||
if payload:
|
||||
try:
|
||||
data = json.dumps(payload)
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="Failed to encode payload as JSON: %s " % str(e))
|
||||
|
||||
resp, info = fetch_url(self.module,
|
||||
self.cf_api_endpoint + api_call,
|
||||
headers=headers,
|
||||
data=data,
|
||||
method=method,
|
||||
timeout=self.timeout)
|
||||
|
||||
if info['status'] not in [200,304,400,401,403,429,405,415]:
|
||||
self.module.fail_json(msg="Failed API call {0}; got unexpected HTTP code {1}".format(api_call,info['status']))
|
||||
|
||||
error_msg = ''
|
||||
if info['status'] == 401:
|
||||
# Unauthorized
|
||||
error_msg = "API user does not have permission; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call)
|
||||
elif info['status'] == 403:
|
||||
# Forbidden
|
||||
error_msg = "API request not authenticated; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call)
|
||||
elif info['status'] == 429:
|
||||
# Too many requests
|
||||
error_msg = "API client is rate limited; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call)
|
||||
elif info['status'] == 405:
|
||||
# Method not allowed
|
||||
error_msg = "API incorrect HTTP method provided; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call)
|
||||
elif info['status'] == 415:
|
||||
# Unsupported Media Type
|
||||
error_msg = "API request is not valid JSON; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call)
|
||||
elif info ['status'] == 400:
|
||||
# Bad Request
|
||||
error_msg = "API bad request; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call)
|
||||
|
||||
result = None
|
||||
try:
|
||||
content = resp.read()
|
||||
except AttributeError:
|
||||
if info['body']:
|
||||
content = info['body']
|
||||
else:
|
||||
error_msg += "; The API response was empty"
|
||||
|
||||
if content:
|
||||
try:
|
||||
result = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
error_msg += "; Failed to parse API response: {0}".format(content)
|
||||
|
||||
# received an error status but no data with details on what failed
|
||||
if (info['status'] not in [200,304]) and (result is None):
|
||||
self.module.fail_json(msg=error_msg)
|
||||
|
||||
if not result['success']:
|
||||
error_msg += "; Error details: "
|
||||
for error in result['errors']:
|
||||
error_msg += "code: {0}, error: {1}; ".format(error['code'],error['message'])
|
||||
if 'error_chain' in error:
|
||||
for chain_error in error['error_chain']:
|
||||
error_msg += "code: {0}, error: {1}; ".format(chain_error['code'],chain_error['message'])
|
||||
self.module.fail_json(msg=error_msg)
|
||||
|
||||
return result, info['status']
|
||||
|
||||
def _cf_api_call(self,api_call,method='GET',payload=None):
|
||||
result, status = self._cf_simple_api_call(api_call,method,payload)
|
||||
|
||||
data = result['result']
|
||||
|
||||
if 'result_info' in result:
|
||||
pagination = result['result_info']
|
||||
if pagination['total_pages'] > 1:
|
||||
next_page = int(pagination['page']) + 1
|
||||
parameters = ['page={0}'.format(next_page)]
|
||||
# strip "page" parameter from call parameters (if there are any)
|
||||
if '?' in api_call:
|
||||
raw_api_call,query = api_call.split('?',1)
|
||||
parameters += [param for param in query.split('&') if not param.startswith('page')]
|
||||
else:
|
||||
raw_api_call = api_call
|
||||
while next_page <= pagination['total_pages']:
|
||||
raw_api_call += '?' + '&'.join(parameters)
|
||||
result, status = self._cf_simple_api_call(raw_api_call,method,payload)
|
||||
data += result['result']
|
||||
next_page += 1
|
||||
|
||||
return data, status
|
||||
|
||||
def _get_zone_id(self,zone=None):
|
||||
if not zone:
|
||||
zone = self.zone
|
||||
|
||||
zones = self.get_zones(zone)
|
||||
if len(zones) > 1:
|
||||
self.module.fail_json(msg="More than one zone matches {0}".format(zone))
|
||||
|
||||
if len(zones) < 1:
|
||||
self.module.fail_json(msg="No zone found with name {0}".format(zone))
|
||||
|
||||
return zones[0]['id']
|
||||
|
||||
def get_zones(self,name=None):
|
||||
if not name:
|
||||
name = self.zone
|
||||
param = ''
|
||||
if name:
|
||||
param = '?' + urllib.urlencode({'name' : name})
|
||||
zones,status = self._cf_api_call('/zones' + param)
|
||||
return zones
|
||||
|
||||
def get_dns_records(self,zone_name=None,type=None,record=None,value=''):
|
||||
if not zone_name:
|
||||
zone_name = self.zone
|
||||
if not type:
|
||||
type = self.type
|
||||
if not record:
|
||||
record = self.record
|
||||
# necessary because None as value means to override user
|
||||
# set module value
|
||||
if (not value) and (value is not None):
|
||||
value = self.value
|
||||
|
||||
zone_id = self._get_zone_id()
|
||||
api_call = '/zones/{0}/dns_records'.format(zone_id)
|
||||
query = {}
|
||||
if type:
|
||||
query['type'] = type
|
||||
if record:
|
||||
query['name'] = record
|
||||
if value:
|
||||
query['content'] = value
|
||||
if query:
|
||||
api_call += '?' + urllib.urlencode(query)
|
||||
|
||||
records,status = self._cf_api_call(api_call)
|
||||
return records
|
||||
|
||||
def delete_dns_records(self,**kwargs):
|
||||
params = {}
|
||||
for param in ['port','proto','service','solo','type','record','value','weight','zone']:
|
||||
if param in kwargs:
|
||||
params[param] = kwargs[param]
|
||||
else:
|
||||
params[param] = getattr(self,param)
|
||||
|
||||
records = []
|
||||
content = params['value']
|
||||
search_record = params['record']
|
||||
if params['type'] == 'SRV':
|
||||
content = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
|
||||
search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
|
||||
if params['solo']:
|
||||
search_value = None
|
||||
else:
|
||||
search_value = content
|
||||
|
||||
records = self.get_dns_records(params['zone'],params['type'],search_record,search_value)
|
||||
|
||||
for rr in records:
|
||||
if params['solo']:
|
||||
if not ((rr['type'] == params['type']) and (rr['name'] == search_record) and (rr['content'] == content)):
|
||||
self.changed = True
|
||||
if not self.module.check_mode:
|
||||
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'],rr['id']),'DELETE')
|
||||
else:
|
||||
self.changed = True
|
||||
if not self.module.check_mode:
|
||||
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'],rr['id']),'DELETE')
|
||||
return self.changed
|
||||
|
||||
def ensure_dns_record(self,**kwargs):
|
||||
params = {}
|
||||
for param in ['port','priority','proto','proxied','service','ttl','type','record','value','weight','zone']:
|
||||
if param in kwargs:
|
||||
params[param] = kwargs[param]
|
||||
else:
|
||||
params[param] = getattr(self,param)
|
||||
|
||||
search_value = params['value']
|
||||
search_record = params['record']
|
||||
new_record = None
|
||||
if (params['type'] is None) or (params['record'] is None):
|
||||
self.module.fail_json(msg="You must provide a type and a record to create a new record")
|
||||
|
||||
if (params['type'] in [ 'A','AAAA','CNAME','TXT','MX','NS','SPF']):
|
||||
if not params['value']:
|
||||
self.module.fail_json(msg="You must provide a non-empty value to create this record type")
|
||||
|
||||
# there can only be one CNAME per record
|
||||
# ignoring the value when searching for existing
|
||||
# CNAME records allows us to update the value if it
|
||||
# changes
|
||||
if params['type'] == 'CNAME':
|
||||
search_value = None
|
||||
|
||||
new_record = {
|
||||
"type": params['type'],
|
||||
"name": params['record'],
|
||||
"content": params['value'],
|
||||
"ttl": params['ttl']
|
||||
}
|
||||
|
||||
if (params['type'] in [ 'A', 'AAAA', 'CNAME' ]):
|
||||
new_record["proxied"] = params["proxied"]
|
||||
|
||||
if params['type'] == 'MX':
|
||||
for attr in [params['priority'],params['value']]:
|
||||
if (attr is None) or (attr == ''):
|
||||
self.module.fail_json(msg="You must provide priority and a value to create this record type")
|
||||
new_record = {
|
||||
"type": params['type'],
|
||||
"name": params['record'],
|
||||
"content": params['value'],
|
||||
"priority": params['priority'],
|
||||
"ttl": params['ttl']
|
||||
}
|
||||
|
||||
if params['type'] == 'SRV':
|
||||
for attr in [params['port'],params['priority'],params['proto'],params['service'],params['weight'],params['value']]:
|
||||
if (attr is None) or (attr == ''):
|
||||
self.module.fail_json(msg="You must provide port, priority, proto, service, weight and a value to create this record type")
|
||||
srv_data = {
|
||||
"target": params['value'],
|
||||
"port": params['port'],
|
||||
"weight": params['weight'],
|
||||
"priority": params['priority'],
|
||||
"name": params['record'][:-len('.' + params['zone'])],
|
||||
"proto": params['proto'],
|
||||
"service": params['service']
|
||||
}
|
||||
new_record = { "type": params['type'], "ttl": params['ttl'], 'data': srv_data }
|
||||
search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
|
||||
search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
|
||||
|
||||
zone_id = self._get_zone_id(params['zone'])
|
||||
records = self.get_dns_records(params['zone'],params['type'],search_record,search_value)
|
||||
# in theory this should be impossible as cloudflare does not allow
|
||||
# the creation of duplicate records but lets cover it anyways
|
||||
if len(records) > 1:
|
||||
self.module.fail_json(msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!")
|
||||
# record already exists, check if it must be updated
|
||||
if len(records) == 1:
|
||||
cur_record = records[0]
|
||||
do_update = False
|
||||
if (params['ttl'] is not None) and (cur_record['ttl'] != params['ttl'] ):
|
||||
do_update = True
|
||||
if (params['priority'] is not None) and ('priority' in cur_record) and (cur_record['priority'] != params['priority']):
|
||||
do_update = True
|
||||
if ('data' in new_record) and ('data' in cur_record):
|
||||
if (cur_record['data'] > new_record['data']) - (cur_record['data'] < new_record['data']):
|
||||
do_update = True
|
||||
if (type == 'CNAME') and (cur_record['content'] != new_record['content']):
|
||||
do_update = True
|
||||
if do_update:
|
||||
if not self.module.check_mode:
|
||||
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(zone_id,records[0]['id']),'PUT',new_record)
|
||||
self.changed = True
|
||||
return result,self.changed
|
||||
else:
|
||||
return records,self.changed
|
||||
if not self.module.check_mode:
|
||||
result, info = self._cf_api_call('/zones/{0}/dns_records'.format(zone_id),'POST',new_record)
|
||||
self.changed = True
|
||||
return result,self.changed
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
account_api_token = dict(required=True, no_log=True, type='str'),
|
||||
account_email = dict(required=True, type='str'),
|
||||
port = dict(required=False, default=None, type='int'),
|
||||
priority = dict(required=False, default=1, type='int'),
|
||||
proto = dict(required=False, default=None, choices=[ 'tcp', 'udp' ], type='str'),
|
||||
proxied = dict(required=False, default=False, type='bool'),
|
||||
record = dict(required=False, default='@', aliases=['name'], type='str'),
|
||||
service = dict(required=False, default=None, type='str'),
|
||||
solo = dict(required=False, default=None, type='bool'),
|
||||
state = dict(required=False, default='present', choices=['present', 'absent'], type='str'),
|
||||
timeout = dict(required=False, default=30, type='int'),
|
||||
ttl = dict(required=False, default=1, type='int'),
|
||||
type = dict(required=False, default=None, choices=[ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'SPF' ], type='str'),
|
||||
value = dict(required=False, default=None, aliases=['content'], type='str'),
|
||||
weight = dict(required=False, default=1, type='int'),
|
||||
zone = dict(required=True, default=None, aliases=['domain'], type='str'),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
required_if = ([
|
||||
('state','present',['record','type']),
|
||||
('type','MX',['priority','value']),
|
||||
('type','SRV',['port','priority','proto','service','value','weight']),
|
||||
('type','A',['value']),
|
||||
('type','AAAA',['value']),
|
||||
('type','CNAME',['value']),
|
||||
('type','TXT',['value']),
|
||||
('type','NS',['value']),
|
||||
('type','SPF',['value'])
|
||||
]
|
||||
),
|
||||
required_one_of = (
|
||||
[['record','value','type']]
|
||||
)
|
||||
)
|
||||
|
||||
changed = False
|
||||
cf_api = CloudflareAPI(module)
|
||||
|
||||
# sanity checks
|
||||
if cf_api.is_solo and cf_api.state == 'absent':
|
||||
module.fail_json(msg="solo=true can only be used with state=present")
|
||||
|
||||
# perform add, delete or update (only the TTL can be updated) of one or
|
||||
# more records
|
||||
if cf_api.state == 'present':
|
||||
# delete all records matching record name + type
|
||||
if cf_api.is_solo:
|
||||
changed = cf_api.delete_dns_records(solo=cf_api.is_solo)
|
||||
result,changed = cf_api.ensure_dns_record()
|
||||
if isinstance(result,list):
|
||||
module.exit_json(changed=changed,result={'record': result[0]})
|
||||
else:
|
||||
module.exit_json(changed=changed,result={'record': result})
|
||||
else:
|
||||
# force solo to False, just to be sure
|
||||
changed = cf_api.delete_dns_records(solo=False)
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
343
lib/ansible/modules/network/dnsimple.py
Normal file
343
lib/ansible/modules/network/dnsimple.py
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/python
|
||||
# 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: dnsimple
|
||||
version_added: "1.6"
|
||||
short_description: Interface with dnsimple.com (a DNS hosting service).
|
||||
description:
|
||||
- "Manages domains and records via the DNSimple API, see the docs: U(http://developer.dnsimple.com/)"
|
||||
options:
|
||||
account_email:
|
||||
description:
|
||||
- "Account email. If omitted, the env variables DNSIMPLE_EMAIL and DNSIMPLE_API_TOKEN will be looked for. If those aren't found, a C(.dnsimple) file will be looked for, see: U(https://github.com/mikemaccana/dnsimple-python#getting-started)"
|
||||
required: false
|
||||
default: null
|
||||
|
||||
account_api_token:
|
||||
description:
|
||||
- Account API token. See I(account_email) for info.
|
||||
required: false
|
||||
default: null
|
||||
|
||||
domain:
|
||||
description:
|
||||
- Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNSimple. If omitted, a list of domains will be returned.
|
||||
- If domain is present but the domain doesn't exist, it will be created.
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record:
|
||||
description:
|
||||
- Record to add, if blank a record for the domain will be created, supports the wildcard (*)
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record_ids:
|
||||
description:
|
||||
- List of records to ensure they either exist or don't exist
|
||||
required: false
|
||||
default: null
|
||||
|
||||
type:
|
||||
description:
|
||||
- The type of DNS record to create
|
||||
required: false
|
||||
choices: [ 'A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL' ]
|
||||
default: null
|
||||
|
||||
ttl:
|
||||
description:
|
||||
- The TTL to give the new record
|
||||
required: false
|
||||
default: 3600 (one hour)
|
||||
|
||||
value:
|
||||
description:
|
||||
- Record value
|
||||
- "Must be specified when trying to ensure a record exists"
|
||||
required: false
|
||||
default: null
|
||||
|
||||
priority:
|
||||
description:
|
||||
- Record priority
|
||||
required: false
|
||||
default: null
|
||||
|
||||
state:
|
||||
description:
|
||||
- whether the record should exist or not
|
||||
required: false
|
||||
choices: [ 'present', 'absent' ]
|
||||
default: null
|
||||
|
||||
solo:
|
||||
description:
|
||||
- Whether the record should be the only one for that record type and record name. Only use with state=present on a record
|
||||
required: false
|
||||
default: null
|
||||
|
||||
requirements: [ dnsimple ]
|
||||
author: "Alex Coomans (@drcapulet)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# authenticate using email and API token and fetch all domains
|
||||
- dnsimple:
|
||||
account_email: test@example.com
|
||||
account_api_token: dummyapitoken
|
||||
delegate_to: localhost
|
||||
|
||||
# fetch my.com domain records
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
register: records
|
||||
|
||||
# delete a domain
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
|
||||
# create a test.my.com A record to point to 127.0.0.01
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record: test
|
||||
type: A
|
||||
value: 127.0.0.1
|
||||
delegate_to: localhost
|
||||
register: record
|
||||
|
||||
# and then delete it
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record_ids: '{{ record["id"] }}'
|
||||
delegate_to: localhost
|
||||
|
||||
# create a my.com CNAME record to example.com
|
||||
- dnsimple
|
||||
domain: my.com
|
||||
record: ''
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
# change it's ttl
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record: ''
|
||||
type: CNAME
|
||||
value: example.com
|
||||
ttl: 600
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
# and delete the record
|
||||
- dnsimple:
|
||||
domain: my.com
|
||||
record: ''
|
||||
type: CNAME
|
||||
value: example.com
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
import os
|
||||
try:
|
||||
from dnsimple import DNSimple
|
||||
from dnsimple.dnsimple import DNSimpleException
|
||||
HAS_DNSIMPLE = True
|
||||
except ImportError:
|
||||
HAS_DNSIMPLE = False
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
account_email = dict(required=False),
|
||||
account_api_token = dict(required=False, no_log=True),
|
||||
domain = dict(required=False),
|
||||
record = dict(required=False),
|
||||
record_ids = dict(required=False, type='list'),
|
||||
type = dict(required=False, choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL']),
|
||||
ttl = dict(required=False, default=3600, type='int'),
|
||||
value = dict(required=False),
|
||||
priority = dict(required=False, type='int'),
|
||||
state = dict(required=False, choices=['present', 'absent']),
|
||||
solo = dict(required=False, type='bool'),
|
||||
),
|
||||
required_together = (
|
||||
['record', 'value']
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
if not HAS_DNSIMPLE:
|
||||
module.fail_json(msg="dnsimple required for this module")
|
||||
|
||||
account_email = module.params.get('account_email')
|
||||
account_api_token = module.params.get('account_api_token')
|
||||
domain = module.params.get('domain')
|
||||
record = module.params.get('record')
|
||||
record_ids = module.params.get('record_ids')
|
||||
record_type = module.params.get('type')
|
||||
ttl = module.params.get('ttl')
|
||||
value = module.params.get('value')
|
||||
priority = module.params.get('priority')
|
||||
state = module.params.get('state')
|
||||
is_solo = module.params.get('solo')
|
||||
|
||||
if account_email and account_api_token:
|
||||
client = DNSimple(email=account_email, api_token=account_api_token)
|
||||
elif os.environ.get('DNSIMPLE_EMAIL') and os.environ.get('DNSIMPLE_API_TOKEN'):
|
||||
client = DNSimple(email=os.environ.get('DNSIMPLE_EMAIL'), api_token=os.environ.get('DNSIMPLE_API_TOKEN'))
|
||||
else:
|
||||
client = DNSimple()
|
||||
|
||||
try:
|
||||
# Let's figure out what operation we want to do
|
||||
|
||||
# No domain, return a list
|
||||
if not domain:
|
||||
domains = client.domains()
|
||||
module.exit_json(changed=False, result=[d['domain'] for d in domains])
|
||||
|
||||
# Domain & No record
|
||||
if domain and record is None and not record_ids:
|
||||
domains = [d['domain'] for d in client.domains()]
|
||||
if domain.isdigit():
|
||||
dr = next((d for d in domains if d['id'] == int(domain)), None)
|
||||
else:
|
||||
dr = next((d for d in domains if d['name'] == domain), None)
|
||||
if state == 'present':
|
||||
if dr:
|
||||
module.exit_json(changed=False, result=dr)
|
||||
else:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=True, result=client.add_domain(domain)['domain'])
|
||||
elif state == 'absent':
|
||||
if dr:
|
||||
if not module.check_mode:
|
||||
client.delete(domain)
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
module.fail_json(msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
# need the not none check since record could be an empty string
|
||||
if domain and record is not None:
|
||||
records = [r['record'] for r in client.records(str(domain))]
|
||||
|
||||
if not record_type:
|
||||
module.fail_json(msg="Missing the record type")
|
||||
|
||||
if not value:
|
||||
module.fail_json(msg="Missing the record value")
|
||||
|
||||
rr = next((r for r in records if r['name'] == record and r['record_type'] == record_type and r['content'] == value), None)
|
||||
|
||||
if state == 'present':
|
||||
changed = False
|
||||
if is_solo:
|
||||
# delete any records that have the same name and record type
|
||||
same_type = [r['id'] for r in records if r['name'] == record and r['record_type'] == record_type]
|
||||
if rr:
|
||||
same_type = [rid for rid in same_type if rid != rr['id']]
|
||||
if same_type:
|
||||
if not module.check_mode:
|
||||
for rid in same_type:
|
||||
client.delete_record(str(domain), rid)
|
||||
changed = True
|
||||
if rr:
|
||||
# check if we need to update
|
||||
if rr['ttl'] != ttl or rr['prio'] != priority:
|
||||
data = {}
|
||||
if ttl: data['ttl'] = ttl
|
||||
if priority: data['prio'] = priority
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=True, result=client.update_record(str(domain), str(rr['id']), data)['record'])
|
||||
else:
|
||||
module.exit_json(changed=changed, result=rr)
|
||||
else:
|
||||
# create it
|
||||
data = {
|
||||
'name': record,
|
||||
'record_type': record_type,
|
||||
'content': value,
|
||||
}
|
||||
if ttl: data['ttl'] = ttl
|
||||
if priority: data['prio'] = priority
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=True, result=client.add_record(str(domain), data)['record'])
|
||||
elif state == 'absent':
|
||||
if rr:
|
||||
if not module.check_mode:
|
||||
client.delete_record(str(domain), rr['id'])
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
module.fail_json(msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
# Make sure these record_ids either all exist or none
|
||||
if domain and record_ids:
|
||||
current_records = [str(r['record']['id']) for r in client.records(str(domain))]
|
||||
wanted_records = [str(r) for r in record_ids]
|
||||
if state == 'present':
|
||||
difference = list(set(wanted_records) - set(current_records))
|
||||
if difference:
|
||||
module.fail_json(msg="Missing the following records: %s" % difference)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
elif state == 'absent':
|
||||
difference = list(set(wanted_records) & set(current_records))
|
||||
if difference:
|
||||
if not module.check_mode:
|
||||
for rid in difference:
|
||||
client.delete_record(str(domain), rid)
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
module.fail_json(msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
except DNSimpleException:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="Unable to contact DNSimple: %s" % e.message)
|
||||
|
||||
module.fail_json(msg="Unknown what you wanted me to do")
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
401
lib/ansible/modules/network/dnsmadeeasy.py
Normal file
401
lib/ansible/modules/network/dnsmadeeasy.py
Normal file
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/python
|
||||
# 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: dnsmadeeasy
|
||||
version_added: "1.3"
|
||||
short_description: Interface with dnsmadeeasy.com (a DNS hosting service).
|
||||
description:
|
||||
- "Manages DNS records via the v2 REST API of the DNS Made Easy service. It handles records only; there is no manipulation of domains or monitor/account support yet. See: U(https://www.dnsmadeeasy.com/integration/restapi/)"
|
||||
options:
|
||||
account_key:
|
||||
description:
|
||||
- Account API Key.
|
||||
required: true
|
||||
default: null
|
||||
|
||||
account_secret:
|
||||
description:
|
||||
- Account Secret Key.
|
||||
required: true
|
||||
default: null
|
||||
|
||||
domain:
|
||||
description:
|
||||
- Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNS Made Easy (e.g. "839989") for faster resolution.
|
||||
required: true
|
||||
default: null
|
||||
|
||||
record_name:
|
||||
description:
|
||||
- Record name to get/create/delete/update. If record_name is not specified; all records for the domain will be returned in "result" regardless of the state argument.
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record_type:
|
||||
description:
|
||||
- Record type.
|
||||
required: false
|
||||
choices: [ 'A', 'AAAA', 'CNAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT' ]
|
||||
default: null
|
||||
|
||||
record_value:
|
||||
description:
|
||||
- "Record value. HTTPRED: <redirection URL>, MX: <priority> <target name>, NS: <name server>, PTR: <target name>, SRV: <priority> <weight> <port> <target name>, TXT: <text value>"
|
||||
- "If record_value is not specified; no changes will be made and the record will be returned in 'result' (in other words, this module can be used to fetch a record's current id, type, and ttl)"
|
||||
required: false
|
||||
default: null
|
||||
|
||||
record_ttl:
|
||||
description:
|
||||
- record's "Time to live". Number of seconds the record remains cached in DNS servers.
|
||||
required: false
|
||||
default: 1800
|
||||
|
||||
state:
|
||||
description:
|
||||
- whether the record should exist or not
|
||||
required: true
|
||||
choices: [ 'present', 'absent' ]
|
||||
default: null
|
||||
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be used
|
||||
on personally controlled sites using self-signed certificates.
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
version_added: 1.5.1
|
||||
|
||||
notes:
|
||||
- The DNS Made Easy service requires that machines interacting with the API have the proper time and timezone set. Be sure you are within a few seconds of actual time by using NTP.
|
||||
- This module returns record(s) in the "result" element when 'state' is set to 'present'. This value can be be registered and used in your playbooks.
|
||||
|
||||
requirements: [ hashlib, hmac ]
|
||||
author: "Brice Burgess (@briceburg)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# fetch my.com domain records
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
register: response
|
||||
|
||||
# create / ensure the presence of a record
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
|
||||
# update the previously created record
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_value: 192.0.2.23
|
||||
|
||||
# fetch a specific record
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
register: response
|
||||
|
||||
# delete a record / ensure it is absent
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: absent
|
||||
record_name: test
|
||||
'''
|
||||
|
||||
# ============================================
|
||||
# DNSMadeEasy module specific support methods.
|
||||
#
|
||||
|
||||
import urllib
|
||||
|
||||
IMPORT_ERROR = None
|
||||
try:
|
||||
import json
|
||||
from time import strftime, gmtime
|
||||
import hashlib
|
||||
import hmac
|
||||
except ImportError:
|
||||
e = get_exception()
|
||||
IMPORT_ERROR = str(e)
|
||||
|
||||
class DME2:
|
||||
|
||||
def __init__(self, apikey, secret, domain, module):
|
||||
self.module = module
|
||||
|
||||
self.api = apikey
|
||||
self.secret = secret
|
||||
self.baseurl = 'https://api.dnsmadeeasy.com/V2.0/'
|
||||
self.domain = str(domain)
|
||||
self.domain_map = None # ["domain_name"] => ID
|
||||
self.record_map = None # ["record_name"] => ID
|
||||
self.records = None # ["record_ID"] => <record>
|
||||
self.all_records = None
|
||||
|
||||
# Lookup the domain ID if passed as a domain name vs. ID
|
||||
if not self.domain.isdigit():
|
||||
self.domain = self.getDomainByName(self.domain)['id']
|
||||
|
||||
self.record_url = 'dns/managed/' + str(self.domain) + '/records'
|
||||
|
||||
def _headers(self):
|
||||
currTime = self._get_date()
|
||||
hashstring = self._create_hash(currTime)
|
||||
headers = {'x-dnsme-apiKey': self.api,
|
||||
'x-dnsme-hmac': hashstring,
|
||||
'x-dnsme-requestDate': currTime,
|
||||
'content-type': 'application/json'}
|
||||
return headers
|
||||
|
||||
def _get_date(self):
|
||||
return strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime())
|
||||
|
||||
def _create_hash(self, rightnow):
|
||||
return hmac.new(self.secret.encode(), rightnow.encode(), hashlib.sha1).hexdigest()
|
||||
|
||||
def query(self, resource, method, data=None):
|
||||
url = self.baseurl + resource
|
||||
if data and not isinstance(data, basestring):
|
||||
data = urllib.urlencode(data)
|
||||
|
||||
response, info = fetch_url(self.module, url, data=data, method=method, headers=self._headers())
|
||||
if info['status'] not in (200, 201, 204):
|
||||
self.module.fail_json(msg="%s returned %s, with body: %s" % (url, info['status'], info['msg']))
|
||||
|
||||
try:
|
||||
return json.load(response)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def getDomain(self, domain_id):
|
||||
if not self.domain_map:
|
||||
self._instMap('domain')
|
||||
|
||||
return self.domains.get(domain_id, False)
|
||||
|
||||
def getDomainByName(self, domain_name):
|
||||
if not self.domain_map:
|
||||
self._instMap('domain')
|
||||
|
||||
return self.getDomain(self.domain_map.get(domain_name, 0))
|
||||
|
||||
def getDomains(self):
|
||||
return self.query('dns/managed', 'GET')['data']
|
||||
|
||||
def getRecord(self, record_id):
|
||||
if not self.record_map:
|
||||
self._instMap('record')
|
||||
|
||||
return self.records.get(record_id, False)
|
||||
|
||||
# Try to find a single record matching this one.
|
||||
# How we do this depends on the type of record. For instance, there
|
||||
# can be several MX records for a single record_name while there can
|
||||
# only be a single CNAME for a particular record_name. Note also that
|
||||
# there can be several records with different types for a single name.
|
||||
def getMatchingRecord(self, record_name, record_type, record_value):
|
||||
# Get all the records if not already cached
|
||||
if not self.all_records:
|
||||
self.all_records = self.getRecords()
|
||||
|
||||
if record_type in ["A", "AAAA", "CNAME", "HTTPRED", "PTR"]:
|
||||
for result in self.all_records:
|
||||
if result['name'] == record_name and result['type'] == record_type:
|
||||
return result
|
||||
return False
|
||||
elif record_type in ["MX", "NS", "TXT", "SRV"]:
|
||||
for result in self.all_records:
|
||||
if record_type == "MX":
|
||||
value = record_value.split(" ")[1]
|
||||
elif record_type == "SRV":
|
||||
value = record_value.split(" ")[3]
|
||||
else:
|
||||
value = record_value
|
||||
if result['name'] == record_name and result['type'] == record_type and result['value'] == value:
|
||||
return result
|
||||
return False
|
||||
else:
|
||||
raise Exception('record_type not yet supported')
|
||||
|
||||
def getRecords(self):
|
||||
return self.query(self.record_url, 'GET')['data']
|
||||
|
||||
def _instMap(self, type):
|
||||
#@TODO cache this call so it's executed only once per ansible execution
|
||||
map = {}
|
||||
results = {}
|
||||
|
||||
# iterate over e.g. self.getDomains() || self.getRecords()
|
||||
for result in getattr(self, 'get' + type.title() + 's')():
|
||||
|
||||
map[result['name']] = result['id']
|
||||
results[result['id']] = result
|
||||
|
||||
# e.g. self.domain_map || self.record_map
|
||||
setattr(self, type + '_map', map)
|
||||
setattr(self, type + 's', results) # e.g. self.domains || self.records
|
||||
|
||||
def prepareRecord(self, data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
def createRecord(self, data):
|
||||
#@TODO update the cache w/ resultant record + id when impleneted
|
||||
return self.query(self.record_url, 'POST', data)
|
||||
|
||||
def updateRecord(self, record_id, data):
|
||||
#@TODO update the cache w/ resultant record + id when impleneted
|
||||
return self.query(self.record_url + '/' + str(record_id), 'PUT', data)
|
||||
|
||||
def deleteRecord(self, record_id):
|
||||
#@TODO remove record from the cache when impleneted
|
||||
return self.query(self.record_url + '/' + str(record_id), 'DELETE')
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
account_key=dict(required=True),
|
||||
account_secret=dict(required=True, no_log=True),
|
||||
domain=dict(required=True),
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
record_name=dict(required=False),
|
||||
record_type=dict(required=False, choices=[
|
||||
'A', 'AAAA', 'CNAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT']),
|
||||
record_value=dict(required=False),
|
||||
record_ttl=dict(required=False, default=1800, type='int'),
|
||||
validate_certs = dict(default='yes', type='bool'),
|
||||
),
|
||||
required_together=(
|
||||
['record_value', 'record_ttl', 'record_type']
|
||||
)
|
||||
)
|
||||
|
||||
if IMPORT_ERROR:
|
||||
module.fail_json(msg="Import Error: " + IMPORT_ERROR)
|
||||
|
||||
DME = DME2(module.params["account_key"], module.params[
|
||||
"account_secret"], module.params["domain"], module)
|
||||
state = module.params["state"]
|
||||
record_name = module.params["record_name"]
|
||||
record_type = module.params["record_type"]
|
||||
record_value = module.params["record_value"]
|
||||
|
||||
# Follow Keyword Controlled Behavior
|
||||
if record_name is None:
|
||||
domain_records = DME.getRecords()
|
||||
if not domain_records:
|
||||
module.fail_json(
|
||||
msg="The requested domain name is not accessible with this api_key; try using its ID if known.")
|
||||
module.exit_json(changed=False, result=domain_records)
|
||||
|
||||
# Fetch existing record + Build new one
|
||||
current_record = DME.getMatchingRecord(record_name, record_type, record_value)
|
||||
new_record = {'name': record_name}
|
||||
for i in ["record_value", "record_type", "record_ttl"]:
|
||||
if not module.params[i] is None:
|
||||
new_record[i[len("record_"):]] = module.params[i]
|
||||
# Special handling for mx record
|
||||
if new_record["type"] == "MX":
|
||||
new_record["mxLevel"] = new_record["value"].split(" ")[0]
|
||||
new_record["value"] = new_record["value"].split(" ")[1]
|
||||
|
||||
# Special handling for SRV records
|
||||
if new_record["type"] == "SRV":
|
||||
new_record["priority"] = new_record["value"].split(" ")[0]
|
||||
new_record["weight"] = new_record["value"].split(" ")[1]
|
||||
new_record["port"] = new_record["value"].split(" ")[2]
|
||||
new_record["value"] = new_record["value"].split(" ")[3]
|
||||
|
||||
# Compare new record against existing one
|
||||
changed = False
|
||||
if current_record:
|
||||
for i in new_record:
|
||||
if str(current_record[i]) != str(new_record[i]):
|
||||
changed = True
|
||||
new_record['id'] = str(current_record['id'])
|
||||
|
||||
# Follow Keyword Controlled Behavior
|
||||
if state == 'present':
|
||||
# return the record if no value is specified
|
||||
if not "value" in new_record:
|
||||
if not current_record:
|
||||
module.fail_json(
|
||||
msg="A record with name '%s' does not exist for domain '%s.'" % (record_name, module.params['domain']))
|
||||
module.exit_json(changed=False, result=current_record)
|
||||
|
||||
# create record as it does not exist
|
||||
if not current_record:
|
||||
record = DME.createRecord(DME.prepareRecord(new_record))
|
||||
module.exit_json(changed=True, result=record)
|
||||
|
||||
# update the record
|
||||
if changed:
|
||||
DME.updateRecord(
|
||||
current_record['id'], DME.prepareRecord(new_record))
|
||||
module.exit_json(changed=True, result=new_record)
|
||||
|
||||
# return the record (no changes)
|
||||
module.exit_json(changed=False, result=current_record)
|
||||
|
||||
elif state == 'absent':
|
||||
# delete the record if it exists
|
||||
if current_record:
|
||||
DME.deleteRecord(current_record['id'])
|
||||
module.exit_json(changed=True)
|
||||
|
||||
# record does not exist, return w/o change.
|
||||
module.exit_json(changed=False)
|
||||
|
||||
else:
|
||||
module.fail_json(
|
||||
msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.urls import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
lib/ansible/modules/network/exoscale/__init__.py
Normal file
0
lib/ansible/modules/network/exoscale/__init__.py
Normal file
259
lib/ansible/modules/network/exoscale/exo_dns_domain.py
Normal file
259
lib/ansible/modules/network/exoscale/exo_dns_domain.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, René Moser <mail@renemoser.net>
|
||||
#
|
||||
# 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: exo_dns_domain
|
||||
short_description: Manages domain records on Exoscale DNS API.
|
||||
description:
|
||||
- Create and remove domain records.
|
||||
version_added: "2.2"
|
||||
author: "René Moser (@resmo)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the record.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- State of the resource.
|
||||
required: false
|
||||
default: 'present'
|
||||
choices: [ 'present', 'absent' ]
|
||||
api_key:
|
||||
description:
|
||||
- API key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_secret:
|
||||
description:
|
||||
- Secret key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_timeout:
|
||||
description:
|
||||
- HTTP timeout to Exoscale DNS API.
|
||||
required: false
|
||||
default: 10
|
||||
api_region:
|
||||
description:
|
||||
- Name of the ini section in the C(cloustack.ini) file.
|
||||
required: false
|
||||
default: cloudstack
|
||||
validate_certs:
|
||||
description:
|
||||
- Validate SSL certs of the Exoscale DNS API.
|
||||
required: false
|
||||
default: true
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
notes:
|
||||
- As Exoscale DNS uses the same API key and secret for all services, we reuse the config used for Exscale Compute based on CloudStack.
|
||||
The config is read from several locations, in the following order.
|
||||
The C(CLOUDSTACK_KEY), C(CLOUDSTACK_SECRET) environment variables.
|
||||
A C(CLOUDSTACK_CONFIG) environment variable pointing to an C(.ini) file,
|
||||
A C(cloudstack.ini) file in the current working directory.
|
||||
A C(.cloudstack.ini) file in the users home directory.
|
||||
Optionally multiple credentials and endpoints can be specified using ini sections in C(cloudstack.ini).
|
||||
Use the argument C(api_region) to select the section name, default section is C(cloudstack).
|
||||
- This module does not support multiple A records and will complain properly if you try.
|
||||
- More information Exoscale DNS can be found on https://community.exoscale.ch/documentation/dns/.
|
||||
- This module supports check mode and diff.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a domain.
|
||||
- local_action:
|
||||
module: exo_dns_domain
|
||||
name: example.com
|
||||
|
||||
# Remove a domain.
|
||||
- local_action:
|
||||
module: exo_dns_domain
|
||||
name: example.com
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
---
|
||||
exo_dns_domain:
|
||||
description: API domain results
|
||||
returned: success
|
||||
type: dictionary
|
||||
contains:
|
||||
account_id:
|
||||
description: Your account ID
|
||||
returned: success
|
||||
type: int
|
||||
sample: 34569
|
||||
auto_renew:
|
||||
description: Whether domain is auto renewed or not
|
||||
returned: success
|
||||
type: bool
|
||||
sample: false
|
||||
created_at:
|
||||
description: When the domain was created
|
||||
returned: success
|
||||
type: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
expires_on:
|
||||
description: When the domain expires
|
||||
returned: success
|
||||
type: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
id:
|
||||
description: ID of the domain
|
||||
returned: success
|
||||
type: int
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
lockable:
|
||||
description: Whether the domain is lockable or not
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
name:
|
||||
description: Domain name
|
||||
returned: success
|
||||
type: string
|
||||
sample: example.com
|
||||
record_count:
|
||||
description: Number of records related to this domain
|
||||
returned: success
|
||||
type: int
|
||||
sample: 5
|
||||
registrant_id:
|
||||
description: ID of the registrant
|
||||
returned: success
|
||||
type: int
|
||||
sample: null
|
||||
service_count:
|
||||
description: Number of services
|
||||
returned: success
|
||||
type: int
|
||||
sample: 0
|
||||
state:
|
||||
description: State of the domain
|
||||
returned: success
|
||||
type: string
|
||||
sample: "hosted"
|
||||
token:
|
||||
description: Token
|
||||
returned: success
|
||||
type: string
|
||||
sample: "r4NzTRp6opIeFKfaFYvOd6MlhGyD07jl"
|
||||
unicode_name:
|
||||
description: Domain name as unicode
|
||||
returned: success
|
||||
type: string
|
||||
sample: "example.com"
|
||||
updated_at:
|
||||
description: When the domain was updated last.
|
||||
returned: success
|
||||
type: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
user_id:
|
||||
description: ID of the user
|
||||
returned: success
|
||||
type: int
|
||||
sample: null
|
||||
whois_protected:
|
||||
description: Wheter the whois is protected or not
|
||||
returned: success
|
||||
type: bool
|
||||
sample: false
|
||||
'''
|
||||
|
||||
# import exoscale common
|
||||
from ansible.module_utils.exoscale import *
|
||||
|
||||
|
||||
class ExoDnsDomain(ExoDns):
|
||||
|
||||
def __init__(self, module):
|
||||
super(ExoDnsDomain, self).__init__(module)
|
||||
self.name = self.module.params.get('name').lower()
|
||||
|
||||
def get_domain(self):
|
||||
domains = self.api_query("/domains", "GET")
|
||||
for z in domains:
|
||||
if z['domain']['name'].lower() == self.name:
|
||||
return z
|
||||
return None
|
||||
|
||||
def present_domain(self):
|
||||
domain = self.get_domain()
|
||||
data = {
|
||||
'domain': {
|
||||
'name': self.name,
|
||||
}
|
||||
}
|
||||
if not domain:
|
||||
self.result['diff']['after'] = data['domain']
|
||||
self.result['changed'] = True
|
||||
if not self.module.check_mode:
|
||||
domain = self.api_query("/domains", "POST", data)
|
||||
return domain
|
||||
|
||||
def absent_domain(self):
|
||||
domain = self.get_domain()
|
||||
if domain:
|
||||
self.result['diff']['before'] = domain
|
||||
self.result['changed'] = True
|
||||
if not self.module.check_mode:
|
||||
self.api_query("/domains/%s" % domain['domain']['name'], "DELETE")
|
||||
return domain
|
||||
|
||||
def get_result(self, resource):
|
||||
if resource:
|
||||
self.result['exo_dns_domain'] = resource['domain']
|
||||
return self.result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = exo_dns_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name=dict(required=True),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=exo_dns_required_together(),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
exo_dns_domain = ExoDnsDomain(module)
|
||||
if module.params.get('state') == "present":
|
||||
resource = exo_dns_domain.present_domain()
|
||||
else:
|
||||
resource = exo_dns_domain.absent_domain()
|
||||
result = exo_dns_domain.get_result(resource)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
395
lib/ansible/modules/network/exoscale/exo_dns_record.py
Normal file
395
lib/ansible/modules/network/exoscale/exo_dns_record.py
Normal file
@@ -0,0 +1,395 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, René Moser <mail@renemoser.net>
|
||||
#
|
||||
# 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: exo_dns_record
|
||||
short_description: Manages DNS records on Exoscale DNS.
|
||||
description:
|
||||
- Create, update and delete records.
|
||||
version_added: "2.2"
|
||||
author: "René Moser (@resmo)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the record.
|
||||
required: false
|
||||
default: ""
|
||||
domain:
|
||||
description:
|
||||
- Domain the record is related to.
|
||||
required: true
|
||||
record_type:
|
||||
description:
|
||||
- Type of the record.
|
||||
required: false
|
||||
default: A
|
||||
choices: ['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL']
|
||||
aliases: ['rtype', 'type']
|
||||
content:
|
||||
description:
|
||||
- Content of the record.
|
||||
- Required if C(state=present) or C(name="")
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['value', 'address']
|
||||
ttl:
|
||||
description:
|
||||
- TTL of the record in seconds.
|
||||
required: false
|
||||
default: 3600
|
||||
prio:
|
||||
description:
|
||||
- Priority of the record.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['priority']
|
||||
multiple:
|
||||
description:
|
||||
- Whether there are more than one records with similar C(name).
|
||||
- Only allowed with C(record_type=A).
|
||||
- C(content) will not be updated as it is used as key to find the record.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['priority']
|
||||
state:
|
||||
description:
|
||||
- State of the record.
|
||||
required: false
|
||||
default: 'present'
|
||||
choices: [ 'present', 'absent' ]
|
||||
api_key:
|
||||
description:
|
||||
- API key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_secret:
|
||||
description:
|
||||
- Secret key of the Exoscale DNS API.
|
||||
required: false
|
||||
default: null
|
||||
api_timeout:
|
||||
description:
|
||||
- HTTP timeout to Exoscale DNS API.
|
||||
required: false
|
||||
default: 10
|
||||
api_region:
|
||||
description:
|
||||
- Name of the ini section in the C(cloustack.ini) file.
|
||||
required: false
|
||||
default: cloudstack
|
||||
validate_certs:
|
||||
description:
|
||||
- Validate SSL certs of the Exoscale DNS API.
|
||||
required: false
|
||||
default: true
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
notes:
|
||||
- As Exoscale DNS uses the same API key and secret for all services, we reuse the config used for Exscale Compute based on CloudStack.
|
||||
The config is read from several locations, in the following order.
|
||||
The C(CLOUDSTACK_KEY), C(CLOUDSTACK_SECRET) environment variables.
|
||||
A C(CLOUDSTACK_CONFIG) environment variable pointing to an C(.ini) file,
|
||||
A C(cloudstack.ini) file in the current working directory.
|
||||
A C(.cloudstack.ini) file in the users home directory.
|
||||
Optionally multiple credentials and endpoints can be specified using ini sections in C(cloudstack.ini).
|
||||
Use the argument C(api_region) to select the section name, default section is C(cloudstack).
|
||||
- This module does not support multiple A records and will complain properly if you try.
|
||||
- More information Exoscale DNS can be found on https://community.exoscale.ch/documentation/dns/.
|
||||
- This module supports check mode and diff.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create or update an A record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: web-vm-1
|
||||
domain: example.com
|
||||
content: 1.2.3.4
|
||||
|
||||
# Update an existing A record with a new IP.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: web-vm-1
|
||||
domain: example.com
|
||||
content: 1.2.3.5
|
||||
|
||||
# Create another A record with same name.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: web-vm-1
|
||||
domain: example.com
|
||||
content: 1.2.3.6
|
||||
multiple: yes
|
||||
|
||||
# Create or update a CNAME record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: www
|
||||
domain: example.com
|
||||
record_type: CNAME
|
||||
content: web-vm-1
|
||||
|
||||
# Create or update a MX record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
domain: example.com
|
||||
record_type: MX
|
||||
content: mx1.example.com
|
||||
prio: 10
|
||||
|
||||
# delete a MX record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
domain: example.com
|
||||
record_type: MX
|
||||
content: mx1.example.com
|
||||
state: absent
|
||||
|
||||
# Remove a record.
|
||||
- local_action:
|
||||
module: exo_dns_record
|
||||
name: www
|
||||
domain: example.com
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
---
|
||||
exo_dns_record:
|
||||
description: API record results
|
||||
returned: success
|
||||
type: dictionary
|
||||
contains:
|
||||
content:
|
||||
description: value of the record
|
||||
returned: success
|
||||
type: string
|
||||
sample: 1.2.3.4
|
||||
created_at:
|
||||
description: When the record was created
|
||||
returned: success
|
||||
type: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
domain:
|
||||
description: Name of the domain
|
||||
returned: success
|
||||
type: string
|
||||
sample: example.com
|
||||
domain_id:
|
||||
description: ID of the domain
|
||||
returned: success
|
||||
type: int
|
||||
sample: 254324
|
||||
id:
|
||||
description: ID of the record
|
||||
returned: success
|
||||
type: int
|
||||
sample: 254324
|
||||
name:
|
||||
description: name of the record
|
||||
returned: success
|
||||
type: string
|
||||
sample: www
|
||||
parent_id:
|
||||
description: ID of the parent
|
||||
returned: success
|
||||
type: int
|
||||
sample: null
|
||||
prio:
|
||||
description: Priority of the record
|
||||
returned: success
|
||||
type: int
|
||||
sample: 10
|
||||
record_type:
|
||||
description: Priority of the record
|
||||
returned: success
|
||||
type: string
|
||||
sample: A
|
||||
system_record:
|
||||
description: Whether the record is a system record or not
|
||||
returned: success
|
||||
type: bool
|
||||
sample: false
|
||||
ttl:
|
||||
description: Time to live of the record
|
||||
returned: success
|
||||
type: int
|
||||
sample: 3600
|
||||
updated_at:
|
||||
description: When the record was updated
|
||||
returned: success
|
||||
type: string
|
||||
sample: "2016-08-12T15:24:23.989Z"
|
||||
'''
|
||||
|
||||
# import exoscale common
|
||||
from ansible.module_utils.exoscale import *
|
||||
|
||||
|
||||
class ExoDnsRecord(ExoDns):
|
||||
|
||||
def __init__(self, module):
|
||||
super(ExoDnsRecord, self).__init__(module)
|
||||
|
||||
self.content = self.module.params.get('content')
|
||||
if self.content:
|
||||
self.content = self.content.lower()
|
||||
|
||||
self.domain = self.module.params.get('domain').lower()
|
||||
self.name = self.module.params.get('name').lower()
|
||||
if self.name == self.domain:
|
||||
self.name = ""
|
||||
|
||||
self.multiple = self.module.params.get('multiple')
|
||||
self.record_type = self.module.params.get('record_type')
|
||||
if self.multiple and self.record_type != 'A':
|
||||
self.module.fail_json("Multiple is only usable with record_type A")
|
||||
|
||||
|
||||
def _create_record(self, record):
|
||||
self.result['changed'] = True
|
||||
data = {
|
||||
'record': {
|
||||
'name': self.name,
|
||||
'record_type': self.record_type,
|
||||
'content': self.content,
|
||||
'ttl': self.module.params.get('ttl'),
|
||||
'prio': self.module.params.get('prio'),
|
||||
}
|
||||
}
|
||||
self.result['diff']['after'] = data['record']
|
||||
if not self.module.check_mode:
|
||||
record = self.api_query("/domains/%s/records" % self.domain, "POST", data)
|
||||
return record
|
||||
|
||||
def _update_record(self, record):
|
||||
data = {
|
||||
'record': {
|
||||
'name': self.name,
|
||||
'content': self.content,
|
||||
'ttl': self.module.params.get('ttl'),
|
||||
'prio': self.module.params.get('prio'),
|
||||
}
|
||||
}
|
||||
if self.has_changed(data['record'], record['record']):
|
||||
self.result['changed'] = True
|
||||
if not self.module.check_mode:
|
||||
record = self.api_query("/domains/%s/records/%s" % (self.domain, record['record']['id']), "PUT", data)
|
||||
return record
|
||||
|
||||
def get_record(self):
|
||||
domain = self.module.params.get('domain')
|
||||
records = self.api_query("/domains/%s/records" % domain, "GET")
|
||||
|
||||
record = None
|
||||
for r in records:
|
||||
found_record = None
|
||||
if r['record']['record_type'] == self.record_type:
|
||||
r_name = r['record']['name'].lower()
|
||||
r_content = r['record']['content'].lower()
|
||||
|
||||
# there are multiple A records but we found an exact match
|
||||
if self.multiple and self.name == r_name and self.content == r_content:
|
||||
record = r
|
||||
break
|
||||
|
||||
# We do not expect to found more then one record with that content
|
||||
if not self.multiple and not self.name and self.content == r_content:
|
||||
found_record = r
|
||||
|
||||
# We do not expect to found more then one record with that name
|
||||
elif not self.multiple and self.name and self.name == r_name:
|
||||
found_record = r
|
||||
|
||||
if record and found_record:
|
||||
self.module.fail_json(msg="More than one record with your params. Use multiple=yes for more than one A record.")
|
||||
if found_record:
|
||||
record = found_record
|
||||
return record
|
||||
|
||||
def present_record(self):
|
||||
record = self.get_record()
|
||||
if not record:
|
||||
record = self._create_record(record);
|
||||
else:
|
||||
record = self._update_record(record);
|
||||
return record
|
||||
|
||||
def absent_record(self):
|
||||
record = self.get_record()
|
||||
if record:
|
||||
self.result['diff']['before'] = record
|
||||
self.result['changed'] = True
|
||||
if not self.module.check_mode:
|
||||
self.api_query("/domains/%s/records/%s" % (self.domain, record['record']['id']), "DELETE")
|
||||
return record
|
||||
|
||||
def get_result(self, resource):
|
||||
if resource:
|
||||
self.result['exo_dns_record'] = resource['record']
|
||||
self.result['exo_dns_record']['domain'] = self.domain
|
||||
return self.result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = exo_dns_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name=dict(default=""),
|
||||
record_type=dict(choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL'], aliases=['rtype', 'type'], default='A'),
|
||||
content=dict(aliases=['value', 'address']),
|
||||
multiple=(dict(type='bool', default=False)),
|
||||
ttl=dict(type='int', default=3600),
|
||||
prio=dict(type='int', aliases=['priority']),
|
||||
domain=dict(required=True),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=exo_dns_required_together(),
|
||||
required_if=[
|
||||
['state', 'present', ['content']],
|
||||
['name', '', ['content']],
|
||||
],
|
||||
required_one_of=[
|
||||
['content', 'name'],
|
||||
],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
exo_dns_record = ExoDnsRecord(module)
|
||||
if module.params.get('state') == "present":
|
||||
resource = exo_dns_record.present_record()
|
||||
else:
|
||||
resource = exo_dns_record.absent_record()
|
||||
|
||||
result = exo_dns_record.get_result(resource)
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
lib/ansible/modules/network/f5/__init__.py
Normal file
0
lib/ansible/modules/network/f5/__init__.py
Normal file
403
lib/ansible/modules/network/f5/bigip_device_dns.py
Normal file
403
lib/ansible/modules/network/f5/bigip_device_dns.py
Normal file
@@ -0,0 +1,403 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_device_dns
|
||||
short_description: Manage BIG-IP device DNS settings
|
||||
description:
|
||||
- Manage BIG-IP device DNS settings
|
||||
version_added: "2.2"
|
||||
options:
|
||||
cache:
|
||||
description:
|
||||
- Specifies whether the system caches DNS lookups or performs the
|
||||
operation each time a lookup is needed. Please note that this applies
|
||||
only to Access Policy Manager features, such as ACLs, web application
|
||||
rewrites, and authentication.
|
||||
required: false
|
||||
default: disable
|
||||
choices:
|
||||
- enable
|
||||
- disable
|
||||
name_servers:
|
||||
description:
|
||||
- A list of name serverz that the system uses to validate DNS lookups
|
||||
forwarders:
|
||||
description:
|
||||
- A list of BIND servers that the system can use to perform DNS lookups
|
||||
search:
|
||||
description:
|
||||
- A list of domains that the system searches for local domain lookups,
|
||||
to resolve local host names.
|
||||
ip_version:
|
||||
description:
|
||||
- Specifies whether the DNS specifies IP addresses using IPv4 or IPv6.
|
||||
required: false
|
||||
choices:
|
||||
- 4
|
||||
- 6
|
||||
state:
|
||||
description:
|
||||
- The state of the variable on the system. When C(present), guarantees
|
||||
that an existing variable is set to C(value).
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- absent
|
||||
- present
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install requests
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set the DNS settings on the BIG-IP
|
||||
bigip_device_dns:
|
||||
name_servers:
|
||||
- 208.67.222.222
|
||||
- 208.67.220.220
|
||||
search:
|
||||
- localdomain
|
||||
- lab.local
|
||||
state: present
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
cache:
|
||||
description: The new value of the DNS caching
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "enabled"
|
||||
name_servers:
|
||||
description: List of name servers that were added or removed
|
||||
returned: changed
|
||||
type: list
|
||||
sample: "['192.0.2.10', '172.17.12.10']"
|
||||
forwarders:
|
||||
description: List of forwarders that were added or removed
|
||||
returned: changed
|
||||
type: list
|
||||
sample: "['192.0.2.10', '172.17.12.10']"
|
||||
search:
|
||||
description: List of search domains that were added or removed
|
||||
returned: changed
|
||||
type: list
|
||||
sample: "['192.0.2.10', '172.17.12.10']"
|
||||
ip_version:
|
||||
description: IP version that was set that DNS will specify IP addresses in
|
||||
returned: changed
|
||||
type: int
|
||||
sample: 4
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip.contexts import TransactionContextManager
|
||||
from f5.bigip import ManagementRoot
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
REQUIRED = ['name_servers', 'search', 'forwarders', 'ip_version', 'cache']
|
||||
CACHE = ['disable', 'enable']
|
||||
IP = [4, 6]
|
||||
|
||||
|
||||
class BigIpDeviceDns(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
changed = False
|
||||
state = self.params['state']
|
||||
|
||||
if self.dhcp_enabled():
|
||||
raise F5ModuleError(
|
||||
"DHCP on the mgmt interface must be disabled to make use of " +
|
||||
"this module"
|
||||
)
|
||||
|
||||
if state == 'absent':
|
||||
changed = self.absent()
|
||||
else:
|
||||
changed = self.present()
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def dhcp_enabled(self):
|
||||
r = self.api.tm.sys.dbs.db.load(name='dhclient.mgmt')
|
||||
if r.value == 'enable':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
result = dict()
|
||||
|
||||
cache = self.api.tm.sys.dbs.db.load(name='dns.cache')
|
||||
proxy = self.api.tm.sys.dbs.db.load(name='dns.proxy.__iter__')
|
||||
dns = self.api.tm.sys.dns.load()
|
||||
|
||||
result['cache'] = str(cache.value)
|
||||
result['forwarders'] = str(proxy.value).split(' ')
|
||||
|
||||
if hasattr(dns, 'nameServers'):
|
||||
result['name_servers'] = dns.nameServers
|
||||
if hasattr(dns, 'search'):
|
||||
result['search'] = dns.search
|
||||
if hasattr(dns, 'include') and 'options inet6' in dns.include:
|
||||
result['ip_version'] = 6
|
||||
else:
|
||||
result['ip_version'] = 4
|
||||
return result
|
||||
|
||||
def present(self):
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
# Temporary locations to hold the changed params
|
||||
update = dict(
|
||||
dns=None,
|
||||
forwarders=None,
|
||||
cache=None
|
||||
)
|
||||
|
||||
nameservers = self.params['name_servers']
|
||||
search_domains = self.params['search']
|
||||
ip_version = self.params['ip_version']
|
||||
forwarders = self.params['forwarders']
|
||||
cache = self.params['cache']
|
||||
check_mode = self.params['check_mode']
|
||||
|
||||
if nameservers:
|
||||
if 'name_servers' in current:
|
||||
if nameservers != current['name_servers']:
|
||||
params['nameServers'] = nameservers
|
||||
else:
|
||||
params['nameServers'] = nameservers
|
||||
|
||||
if search_domains:
|
||||
if 'search' in current:
|
||||
if search_domains != current['search']:
|
||||
params['search'] = search_domains
|
||||
else:
|
||||
params['search'] = search_domains
|
||||
|
||||
if ip_version:
|
||||
if 'ip_version' in current:
|
||||
if ip_version != int(current['ip_version']):
|
||||
if ip_version == 6:
|
||||
params['include'] = 'options inet6'
|
||||
elif ip_version == 4:
|
||||
params['include'] = ''
|
||||
else:
|
||||
if ip_version == 6:
|
||||
params['include'] = 'options inet6'
|
||||
elif ip_version == 4:
|
||||
params['include'] = ''
|
||||
|
||||
if params:
|
||||
self.cparams.update(camel_dict_to_snake_dict(params))
|
||||
|
||||
if 'include' in params:
|
||||
del self.cparams['include']
|
||||
if params['include'] == '':
|
||||
self.cparams['ip_version'] = 4
|
||||
else:
|
||||
self.cparams['ip_version'] = 6
|
||||
|
||||
update['dns'] = params.copy()
|
||||
params = dict()
|
||||
|
||||
if forwarders:
|
||||
if 'forwarders' in current:
|
||||
if forwarders != current['forwarders']:
|
||||
params['forwarders'] = forwarders
|
||||
else:
|
||||
params['forwarders'] = forwarders
|
||||
|
||||
if params:
|
||||
self.cparams.update(camel_dict_to_snake_dict(params))
|
||||
update['forwarders'] = ' '.join(params['forwarders'])
|
||||
params = dict()
|
||||
|
||||
if cache:
|
||||
if 'cache' in current:
|
||||
if cache != current['cache']:
|
||||
params['cache'] = cache
|
||||
|
||||
if params:
|
||||
self.cparams.update(camel_dict_to_snake_dict(params))
|
||||
update['cache'] = params['cache']
|
||||
params = dict()
|
||||
|
||||
if self.cparams:
|
||||
changed = True
|
||||
if check_mode:
|
||||
return changed
|
||||
else:
|
||||
return False
|
||||
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
cache = api.tm.sys.dbs.db.load(name='dns.cache')
|
||||
proxy = api.tm.sys.dbs.db.load(name='dns.proxy.__iter__')
|
||||
dns = api.tm.sys.dns.load()
|
||||
|
||||
# Empty values can be supplied, but you cannot supply the
|
||||
# None value, so we check for that specifically
|
||||
if update['cache'] is not None:
|
||||
cache.update(value=update['cache'])
|
||||
if update['forwarders'] is not None:
|
||||
proxy.update(value=update['forwarders'])
|
||||
if update['dns'] is not None:
|
||||
dns.update(**update['dns'])
|
||||
return changed
|
||||
|
||||
def absent(self):
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
# Temporary locations to hold the changed params
|
||||
update = dict(
|
||||
dns=None,
|
||||
forwarders=None
|
||||
)
|
||||
|
||||
nameservers = self.params['name_servers']
|
||||
search_domains = self.params['search']
|
||||
forwarders = self.params['forwarders']
|
||||
check_mode = self.params['check_mode']
|
||||
|
||||
if forwarders and 'forwarders' in current:
|
||||
set_current = set(current['forwarders'])
|
||||
set_new = set(forwarders)
|
||||
|
||||
forwarders = set_current - set_new
|
||||
if forwarders != set_current:
|
||||
forwarders = list(forwarders)
|
||||
params['forwarders'] = ' '.join(forwarders)
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
self.cparams.update(camel_dict_to_snake_dict(params))
|
||||
update['forwarders'] = params['forwarders']
|
||||
params = dict()
|
||||
|
||||
if nameservers and 'name_servers' in current:
|
||||
set_current = set(current['name_servers'])
|
||||
set_new = set(nameservers)
|
||||
|
||||
nameservers = set_current - set_new
|
||||
if nameservers != set_current:
|
||||
params['nameServers'] = list(nameservers)
|
||||
|
||||
if search_domains and 'search' in current:
|
||||
set_current = set(current['search'])
|
||||
set_new = set(search_domains)
|
||||
|
||||
search_domains = set_current - set_new
|
||||
if search_domains != set_current:
|
||||
params['search'] = list(search_domains)
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
self.cparams.update(camel_dict_to_snake_dict(params))
|
||||
update['dns'] = params.copy()
|
||||
params = dict()
|
||||
|
||||
if not self.cparams:
|
||||
return False
|
||||
|
||||
if check_mode:
|
||||
return changed
|
||||
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
proxy = api.tm.sys.dbs.db.load(name='dns.proxy.__iter__')
|
||||
dns = api.tm.sys.dns.load()
|
||||
|
||||
if update['forwarders'] is not None:
|
||||
proxy.update(value=update['forwarders'])
|
||||
if update['dns'] is not None:
|
||||
dns.update(**update['dns'])
|
||||
return changed
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
cache=dict(required=False, choices=CACHE, default=None),
|
||||
name_servers=dict(required=False, default=None, type='list'),
|
||||
forwarders=dict(required=False, default=None, type='list'),
|
||||
search=dict(required=False, default=None, type='list'),
|
||||
ip_version=dict(required=False, default=None, choices=IP, type='int')
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_one_of=[REQUIRED],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpDeviceDns(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
263
lib/ansible/modules/network/f5/bigip_device_ntp.py
Normal file
263
lib/ansible/modules/network/f5/bigip_device_ntp.py
Normal file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_device_ntp
|
||||
short_description: Manage NTP servers on a BIG-IP
|
||||
description:
|
||||
- Manage NTP servers on a BIG-IP
|
||||
version_added: "2.2"
|
||||
options:
|
||||
ntp_servers:
|
||||
description:
|
||||
- A list of NTP servers to set on the device. At least one of C(ntp_servers)
|
||||
or C(timezone) is required.
|
||||
required: false
|
||||
default: []
|
||||
state:
|
||||
description:
|
||||
- The state of the NTP servers on the system. When C(present), guarantees
|
||||
that the NTP servers are set on the system. When C(absent), removes the
|
||||
specified NTP servers from the device configuration.
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- absent
|
||||
- present
|
||||
timezone:
|
||||
description:
|
||||
- The timezone to set for NTP lookups. At least one of C(ntp_servers) or
|
||||
C(timezone) is required.
|
||||
default: UTC
|
||||
required: false
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install f5-sdk.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set NTP server
|
||||
bigip_device_ntp:
|
||||
ntp_servers:
|
||||
- "192.0.2.23"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Set timezone
|
||||
bigip_device_ntp:
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
timezone: "America/Los_Angeles"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
ntp_servers:
|
||||
description: The NTP servers that were set on the device
|
||||
returned: changed
|
||||
type: list
|
||||
sample: ["192.0.2.23", "192.0.2.42"]
|
||||
timezone:
|
||||
description: The timezone that was set on the device
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "true"
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
class BigIpDeviceNtp(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
changed = False
|
||||
state = self.params['state']
|
||||
|
||||
try:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
elif state == "absent":
|
||||
changed = self.absent()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
if 'servers' in self.cparams:
|
||||
self.cparams['ntp_servers'] = self.cparams.pop('servers')
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def read(self):
|
||||
"""Read information and transform it
|
||||
|
||||
The values that are returned by BIG-IP in the f5-sdk can have encoding
|
||||
attached to them as well as be completely missing in some cases.
|
||||
|
||||
Therefore, this method will transform the data from the BIG-IP into a
|
||||
format that is more easily consumable by the rest of the class and the
|
||||
parameters that are supported by the module.
|
||||
"""
|
||||
p = dict()
|
||||
r = self.api.tm.sys.ntp.load()
|
||||
|
||||
if hasattr(r, 'servers'):
|
||||
# Deliberately using sets to supress duplicates
|
||||
p['servers'] = set([str(x) for x in r.servers])
|
||||
if hasattr(r, 'timezone'):
|
||||
p['timezone'] = str(r.timezone)
|
||||
return p
|
||||
|
||||
def present(self):
|
||||
changed = False
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
ntp_servers = self.params['ntp_servers']
|
||||
timezone = self.params['timezone']
|
||||
|
||||
# NTP servers can be set independently
|
||||
if ntp_servers is not None:
|
||||
if 'servers' in current:
|
||||
items = set(ntp_servers)
|
||||
if items != current['servers']:
|
||||
params['servers'] = list(ntp_servers)
|
||||
else:
|
||||
params['servers'] = ntp_servers
|
||||
|
||||
# Timezone can be set independently
|
||||
if timezone is not None:
|
||||
if 'timezone' in current and current['timezone'] != timezone:
|
||||
params['timezone'] = timezone
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return changed
|
||||
else:
|
||||
return changed
|
||||
|
||||
r = self.api.tm.sys.ntp.load()
|
||||
r.update(**params)
|
||||
r.refresh()
|
||||
|
||||
return changed
|
||||
|
||||
def absent(self):
|
||||
changed = False
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
ntp_servers = self.params['ntp_servers']
|
||||
|
||||
if not ntp_servers:
|
||||
raise F5ModuleError(
|
||||
"Absent can only be used when removing NTP servers"
|
||||
)
|
||||
|
||||
if ntp_servers and 'servers' in current:
|
||||
servers = current['servers']
|
||||
new_servers = [x for x in servers if x not in ntp_servers]
|
||||
|
||||
if servers != new_servers:
|
||||
params['servers'] = new_servers
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return changed
|
||||
else:
|
||||
return changed
|
||||
|
||||
r = self.api.tm.sys.ntp.load()
|
||||
r.update(**params)
|
||||
r.refresh()
|
||||
return changed
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
ntp_servers=dict(required=False, type='list', default=None),
|
||||
timezone=dict(default=None, required=False)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_one_of=[
|
||||
['ntp_servers', 'timezone']
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpDeviceNtp(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
350
lib/ansible/modules/network/f5/bigip_device_sshd.py
Normal file
350
lib/ansible/modules/network/f5/bigip_device_sshd.py
Normal file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_device_sshd
|
||||
short_description: Manage the SSHD settings of a BIG-IP
|
||||
description:
|
||||
- Manage the SSHD settings of a BIG-IP
|
||||
version_added: "2.2"
|
||||
options:
|
||||
allow:
|
||||
description:
|
||||
- Specifies, if you have enabled SSH access, the IP address or address
|
||||
range for other systems that can use SSH to communicate with this
|
||||
system.
|
||||
choices:
|
||||
- all
|
||||
- IP address, such as 172.27.1.10
|
||||
- IP range, such as 172.27.*.* or 172.27.0.0/255.255.0.0
|
||||
banner:
|
||||
description:
|
||||
- Whether to enable the banner or not.
|
||||
required: false
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
banner_text:
|
||||
description:
|
||||
- Specifies the text to include on the pre-login banner that displays
|
||||
when a user attempts to login to the system using SSH.
|
||||
required: false
|
||||
inactivity_timeout:
|
||||
description:
|
||||
- Specifies the number of seconds before inactivity causes an SSH
|
||||
session to log out.
|
||||
required: false
|
||||
log_level:
|
||||
description:
|
||||
- Specifies the minimum SSHD message level to include in the system log.
|
||||
choices:
|
||||
- debug
|
||||
- debug1
|
||||
- debug2
|
||||
- debug3
|
||||
- error
|
||||
- fatal
|
||||
- info
|
||||
- quiet
|
||||
- verbose
|
||||
login:
|
||||
description:
|
||||
- Specifies, when checked C(enabled), that the system accepts SSH
|
||||
communications.
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
required: false
|
||||
port:
|
||||
description:
|
||||
- Port that you want the SSH daemon to run on.
|
||||
required: false
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host This is as easy as pip
|
||||
install f5-sdk.
|
||||
- Requires BIG-IP version 12.0.0 or greater
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set the banner for the SSHD service from a string
|
||||
bigip_device_sshd:
|
||||
banner: "enabled"
|
||||
banner_text: "banner text goes here"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Set the banner for the SSHD service from a file
|
||||
bigip_device_sshd:
|
||||
banner: "enabled"
|
||||
banner_text: "{{ lookup('file', '/path/to/file') }}"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Set the SSHD service to run on port 2222
|
||||
bigip_device_sshd:
|
||||
password: "secret"
|
||||
port: 2222
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
allow:
|
||||
description: >
|
||||
Specifies, if you have enabled SSH access, the IP address or address
|
||||
range for other systems that can use SSH to communicate with this
|
||||
system.
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "192.0.2.*"
|
||||
banner:
|
||||
description: Whether the banner is enabled or not.
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "true"
|
||||
banner_text:
|
||||
description: >
|
||||
Specifies the text included on the pre-login banner that
|
||||
displays when a user attempts to login to the system using SSH.
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "This is a corporate device. Connecting to it without..."
|
||||
inactivity_timeout:
|
||||
description: >
|
||||
The number of seconds before inactivity causes an SSH.
|
||||
session to log out
|
||||
returned: changed
|
||||
type: int
|
||||
sample: "10"
|
||||
log_level:
|
||||
description: The minimum SSHD message level to include in the system log.
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "debug"
|
||||
login:
|
||||
description: Specifies that the system accepts SSH communications or not.
|
||||
return: changed
|
||||
type: bool
|
||||
sample: true
|
||||
port:
|
||||
description: Port that you want the SSH daemon to run on.
|
||||
return: changed
|
||||
type: int
|
||||
sample: 22
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
CHOICES = ['enabled', 'disabled']
|
||||
LEVELS = ['debug', 'debug1', 'debug2', 'debug3', 'error', 'fatal', 'info',
|
||||
'quiet', 'verbose']
|
||||
|
||||
|
||||
class BigIpDeviceSshd(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def update(self):
|
||||
changed = False
|
||||
current = self.read()
|
||||
params = dict()
|
||||
|
||||
allow = self.params['allow']
|
||||
banner = self.params['banner']
|
||||
banner_text = self.params['banner_text']
|
||||
timeout = self.params['inactivity_timeout']
|
||||
log_level = self.params['log_level']
|
||||
login = self.params['login']
|
||||
port = self.params['port']
|
||||
check_mode = self.params['check_mode']
|
||||
|
||||
if allow:
|
||||
if 'allow' in current:
|
||||
items = set(allow)
|
||||
if items != current['allow']:
|
||||
params['allow'] = list(items)
|
||||
else:
|
||||
params['allow'] = allow
|
||||
|
||||
if banner:
|
||||
if 'banner' in current:
|
||||
if banner != current['banner']:
|
||||
params['banner'] = banner
|
||||
else:
|
||||
params['banner'] = banner
|
||||
|
||||
if banner_text:
|
||||
if 'banner_text' in current:
|
||||
if banner_text != current['banner_text']:
|
||||
params['bannerText'] = banner_text
|
||||
else:
|
||||
params['bannerText'] = banner_text
|
||||
|
||||
if timeout:
|
||||
if 'inactivity_timeout' in current:
|
||||
if timeout != current['inactivity_timeout']:
|
||||
params['inactivityTimeout'] = timeout
|
||||
else:
|
||||
params['inactivityTimeout'] = timeout
|
||||
|
||||
if log_level:
|
||||
if 'log_level' in current:
|
||||
if log_level != current['log_level']:
|
||||
params['logLevel'] = log_level
|
||||
else:
|
||||
params['logLevel'] = log_level
|
||||
|
||||
if login:
|
||||
if 'login' in current:
|
||||
if login != current['login']:
|
||||
params['login'] = login
|
||||
else:
|
||||
params['login'] = login
|
||||
|
||||
if port:
|
||||
if 'port' in current:
|
||||
if port != current['port']:
|
||||
params['port'] = port
|
||||
else:
|
||||
params['port'] = port
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
if check_mode:
|
||||
return changed
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
else:
|
||||
return changed
|
||||
|
||||
r = self.api.tm.sys.sshd.load()
|
||||
r.update(**params)
|
||||
r.refresh()
|
||||
|
||||
return changed
|
||||
|
||||
def read(self):
|
||||
"""Read information and transform it
|
||||
|
||||
The values that are returned by BIG-IP in the f5-sdk can have encoding
|
||||
attached to them as well as be completely missing in some cases.
|
||||
|
||||
Therefore, this method will transform the data from the BIG-IP into a
|
||||
format that is more easily consumable by the rest of the class and the
|
||||
parameters that are supported by the module.
|
||||
"""
|
||||
p = dict()
|
||||
r = self.api.tm.sys.sshd.load()
|
||||
|
||||
if hasattr(r, 'allow'):
|
||||
# Deliberately using sets to supress duplicates
|
||||
p['allow'] = set([str(x) for x in r.allow])
|
||||
if hasattr(r, 'banner'):
|
||||
p['banner'] = str(r.banner)
|
||||
if hasattr(r, 'bannerText'):
|
||||
p['banner_text'] = str(r.bannerText)
|
||||
if hasattr(r, 'inactivityTimeout'):
|
||||
p['inactivity_timeout'] = str(r.inactivityTimeout)
|
||||
if hasattr(r, 'logLevel'):
|
||||
p['log_level'] = str(r.logLevel)
|
||||
if hasattr(r, 'login'):
|
||||
p['login'] = str(r.login)
|
||||
if hasattr(r, 'port'):
|
||||
p['port'] = int(r.port)
|
||||
return p
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
changed = False
|
||||
|
||||
try:
|
||||
changed = self.update()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
allow=dict(required=False, default=None, type='list'),
|
||||
banner=dict(required=False, default=None, choices=CHOICES),
|
||||
banner_text=dict(required=False, default=None),
|
||||
inactivity_timeout=dict(required=False, default=None, type='int'),
|
||||
log_level=dict(required=False, default=None, choices=LEVELS),
|
||||
login=dict(required=False, default=None, choices=CHOICES),
|
||||
port=dict(required=False, default=None, type='int'),
|
||||
state=dict(default='present', choices=['present'])
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpDeviceSshd(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1728
lib/ansible/modules/network/f5/bigip_facts.py
Normal file
1728
lib/ansible/modules/network/f5/bigip_facts.py
Normal file
File diff suppressed because it is too large
Load Diff
372
lib/ansible/modules/network/f5/bigip_gtm_datacenter.py
Normal file
372
lib/ansible/modules/network/f5/bigip_gtm_datacenter.py
Normal file
@@ -0,0 +1,372 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_gtm_datacenter
|
||||
short_description: Manage Datacenter configuration in BIG-IP
|
||||
description:
|
||||
- Manage BIG-IP data center configuration. A data center defines the location
|
||||
where the physical network components reside, such as the server and link
|
||||
objects that share the same subnet on the network. This module is able to
|
||||
manipulate the data center definitions in a BIG-IP
|
||||
version_added: "2.2"
|
||||
options:
|
||||
contact:
|
||||
description:
|
||||
- The name of the contact for the data center.
|
||||
description:
|
||||
description:
|
||||
- The description of the data center.
|
||||
enabled:
|
||||
description:
|
||||
- Whether the data center should be enabled. At least one of C(state) and
|
||||
C(enabled) are required.
|
||||
choices:
|
||||
- yes
|
||||
- no
|
||||
location:
|
||||
description:
|
||||
- The location of the data center.
|
||||
name:
|
||||
description:
|
||||
- The name of the data center.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- The state of the datacenter on the BIG-IP. When C(present), guarantees
|
||||
that the data center exists. When C(absent) removes the data center
|
||||
from the BIG-IP. C(enabled) will enable the data center and C(disabled)
|
||||
will ensure the data center is disabled. At least one of state and
|
||||
enabled are required.
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as
|
||||
pip install f5-sdk.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create data center "New York"
|
||||
bigip_gtm_datacenter:
|
||||
server: "big-ip"
|
||||
name: "New York"
|
||||
location: "222 West 23rd"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
contact:
|
||||
description: The contact that was set on the datacenter
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "admin@root.local"
|
||||
description:
|
||||
description: The description that was set for the datacenter
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "Datacenter in NYC"
|
||||
enabled:
|
||||
description: Whether the datacenter is enabled or not
|
||||
returned: changed
|
||||
type: bool
|
||||
sample: true
|
||||
location:
|
||||
description: The location that is set for the datacenter
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "222 West 23rd"
|
||||
name:
|
||||
description: Name of the datacenter being manipulated
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "foo"
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
class BigIpGtmDatacenter(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def create(self):
|
||||
params = dict()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
contact = self.params['contact']
|
||||
description = self.params['description']
|
||||
location = self.params['location']
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
enabled = self.params['enabled']
|
||||
|
||||
# Specifically check for None because a person could supply empty
|
||||
# values which would technically still be valid
|
||||
if contact is not None:
|
||||
params['contact'] = contact
|
||||
|
||||
if description is not None:
|
||||
params['description'] = description
|
||||
|
||||
if location is not None:
|
||||
params['location'] = location
|
||||
|
||||
if enabled is not None:
|
||||
params['enabled'] = True
|
||||
else:
|
||||
params['disabled'] = False
|
||||
|
||||
params['name'] = name
|
||||
params['partition'] = partition
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
d = self.api.tm.gtm.datacenters.datacenter
|
||||
d.create(**params)
|
||||
|
||||
if not self.exists():
|
||||
raise F5ModuleError("Failed to create the datacenter")
|
||||
return True
|
||||
|
||||
def read(self):
|
||||
"""Read information and transform it
|
||||
|
||||
The values that are returned by BIG-IP in the f5-sdk can have encoding
|
||||
attached to them as well as be completely missing in some cases.
|
||||
|
||||
Therefore, this method will transform the data from the BIG-IP into a
|
||||
format that is more easily consumable by the rest of the class and the
|
||||
parameters that are supported by the module.
|
||||
"""
|
||||
p = dict()
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
r = self.api.tm.gtm.datacenters.datacenter.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
|
||||
if hasattr(r, 'servers'):
|
||||
# Deliberately using sets to supress duplicates
|
||||
p['servers'] = set([str(x) for x in r.servers])
|
||||
if hasattr(r, 'contact'):
|
||||
p['contact'] = str(r.contact)
|
||||
if hasattr(r, 'location'):
|
||||
p['location'] = str(r.location)
|
||||
if hasattr(r, 'description'):
|
||||
p['description'] = str(r.description)
|
||||
if r.enabled:
|
||||
p['enabled'] = True
|
||||
else:
|
||||
p['enabled'] = False
|
||||
p['name'] = name
|
||||
return p
|
||||
|
||||
def update(self):
|
||||
changed = False
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
contact = self.params['contact']
|
||||
description = self.params['description']
|
||||
location = self.params['location']
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
enabled = self.params['enabled']
|
||||
|
||||
if contact is not None:
|
||||
if 'contact' in current:
|
||||
if contact != current['contact']:
|
||||
params['contact'] = contact
|
||||
else:
|
||||
params['contact'] = contact
|
||||
|
||||
if description is not None:
|
||||
if 'description' in current:
|
||||
if description != current['description']:
|
||||
params['description'] = description
|
||||
else:
|
||||
params['description'] = description
|
||||
|
||||
if location is not None:
|
||||
if 'location' in current:
|
||||
if location != current['location']:
|
||||
params['location'] = location
|
||||
else:
|
||||
params['location'] = location
|
||||
|
||||
if enabled is not None:
|
||||
if current['enabled'] != enabled:
|
||||
if enabled is True:
|
||||
params['enabled'] = True
|
||||
params['disabled'] = False
|
||||
else:
|
||||
params['disabled'] = True
|
||||
params['enabled'] = False
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
if check_mode:
|
||||
return changed
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
else:
|
||||
return changed
|
||||
|
||||
r = self.api.tm.gtm.datacenters.datacenter.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
r.update(**params)
|
||||
r.refresh()
|
||||
|
||||
return True
|
||||
|
||||
def delete(self):
|
||||
params = dict()
|
||||
check_mode = self.params['check_mode']
|
||||
|
||||
params['name'] = self.params['name']
|
||||
params['partition'] = self.params['partition']
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
dc = self.api.tm.gtm.datacenters.datacenter.load(**params)
|
||||
dc.delete()
|
||||
|
||||
if self.exists():
|
||||
raise F5ModuleError("Failed to delete the datacenter")
|
||||
return True
|
||||
|
||||
def present(self):
|
||||
changed = False
|
||||
|
||||
if self.exists():
|
||||
changed = self.update()
|
||||
else:
|
||||
changed = self.create()
|
||||
|
||||
return changed
|
||||
|
||||
def absent(self):
|
||||
changed = False
|
||||
|
||||
if self.exists():
|
||||
changed = self.delete()
|
||||
|
||||
return changed
|
||||
|
||||
def exists(self):
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
|
||||
return self.api.tm.gtm.datacenters.datacenter.exists(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
state = self.params['state']
|
||||
enabled = self.params['enabled']
|
||||
|
||||
if state is None and enabled is None:
|
||||
module.fail_json(msg="Neither 'state' nor 'enabled' set")
|
||||
|
||||
try:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
|
||||
# Ensure that this field is not returned to the user since it
|
||||
# is not a valid parameter to the module.
|
||||
if 'disabled' in self.cparams:
|
||||
del self.cparams['disabled']
|
||||
elif state == "absent":
|
||||
changed = self.absent()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
contact=dict(required=False, default=None),
|
||||
description=dict(required=False, default=None),
|
||||
enabled=dict(required=False, type='bool', default=None, choices=BOOLEANS),
|
||||
location=dict(required=False, default=None),
|
||||
name=dict(required=True)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpGtmDatacenter(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
495
lib/ansible/modules/network/f5/bigip_gtm_facts.py
Normal file
495
lib/ansible/modules/network/f5/bigip_gtm_facts.py
Normal file
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_gtm_facts
|
||||
short_description: Collect facts from F5 BIG-IP GTM devices.
|
||||
description:
|
||||
- Collect facts from F5 BIG-IP GTM devices.
|
||||
version_added: "2.3"
|
||||
options:
|
||||
include:
|
||||
description:
|
||||
- Fact category to collect
|
||||
required: true
|
||||
choices:
|
||||
- pool
|
||||
- wide_ip
|
||||
- virtual_server
|
||||
filter:
|
||||
description:
|
||||
- Perform regex filter of response. Filtering is done on the name of
|
||||
the resource. Valid filters are anything that can be provided to
|
||||
Python's C(re) module.
|
||||
required: false
|
||||
default: None
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as
|
||||
pip install f5-sdk
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Get pool facts
|
||||
bigip_gtm_facts:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
include: "pool"
|
||||
filter: "my_pool"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
wide_ip:
|
||||
description:
|
||||
Contains the lb method for the wide ip and the pools
|
||||
that are within the wide ip.
|
||||
returned: changed
|
||||
type: dict
|
||||
sample:
|
||||
wide_ip:
|
||||
- enabled: "True"
|
||||
failure_rcode: "noerror"
|
||||
failure_rcode_response: "disabled"
|
||||
failure_rcode_ttl: "0"
|
||||
full_path: "/Common/foo.ok.com"
|
||||
last_resort_pool: ""
|
||||
minimal_response: "enabled"
|
||||
name: "foo.ok.com"
|
||||
partition: "Common"
|
||||
persist_cidr_ipv4: "32"
|
||||
persist_cidr_ipv6: "128"
|
||||
persistence: "disabled"
|
||||
pool_lb_mode: "round-robin"
|
||||
pools:
|
||||
- name: "d3qw"
|
||||
order: "0"
|
||||
partition: "Common"
|
||||
ratio: "1"
|
||||
ttl_persistence: "3600"
|
||||
type: "naptr"
|
||||
pool:
|
||||
description: Contains the pool object status and enabled status.
|
||||
returned: changed
|
||||
type: dict
|
||||
sample:
|
||||
pool:
|
||||
- alternate_mode: "round-robin"
|
||||
dynamic_ratio: "disabled"
|
||||
enabled: "True"
|
||||
fallback_mode: "return-to-dns"
|
||||
full_path: "/Common/d3qw"
|
||||
load_balancing_mode: "round-robin"
|
||||
manual_resume: "disabled"
|
||||
max_answers_returned: "1"
|
||||
members:
|
||||
- disabled: "True"
|
||||
flags: "a"
|
||||
full_path: "ok3.com"
|
||||
member_order: "0"
|
||||
name: "ok3.com"
|
||||
order: "10"
|
||||
preference: "10"
|
||||
ratio: "1"
|
||||
service: "80"
|
||||
name: "d3qw"
|
||||
partition: "Common"
|
||||
qos_hit_ratio: "5"
|
||||
qos_hops: "0"
|
||||
qos_kilobytes_second: "3"
|
||||
qos_lcs: "30"
|
||||
qos_packet_rate: "1"
|
||||
qos_rtt: "50"
|
||||
qos_topology: "0"
|
||||
qos_vs_capacity: "0"
|
||||
qos_vs_score: "0"
|
||||
ttl: "30"
|
||||
type: "naptr"
|
||||
verify_member_availability: "disabled"
|
||||
virtual_server:
|
||||
description:
|
||||
Contains the virtual server enabled and availability
|
||||
status, and address
|
||||
returned: changed
|
||||
type: dict
|
||||
sample:
|
||||
virtual_server:
|
||||
- addresses:
|
||||
- device_name: "/Common/qweqwe"
|
||||
name: "10.10.10.10"
|
||||
translation: "none"
|
||||
datacenter: "/Common/xfxgh"
|
||||
enabled: "True"
|
||||
expose_route_domains: "no"
|
||||
full_path: "/Common/qweqwe"
|
||||
iq_allow_path: "yes"
|
||||
iq_allow_service_check: "yes"
|
||||
iq_allow_snmp: "yes"
|
||||
limit_cpu_usage: "0"
|
||||
limit_cpu_usage_status: "disabled"
|
||||
limit_max_bps: "0"
|
||||
limit_max_bps_status: "disabled"
|
||||
limit_max_connections: "0"
|
||||
limit_max_connections_status: "disabled"
|
||||
limit_max_pps: "0"
|
||||
limit_max_pps_status: "disabled"
|
||||
limit_mem_avail: "0"
|
||||
limit_mem_avail_status: "disabled"
|
||||
link_discovery: "disabled"
|
||||
monitor: "/Common/bigip "
|
||||
name: "qweqwe"
|
||||
partition: "Common"
|
||||
product: "single-bigip"
|
||||
virtual_server_discovery: "disabled"
|
||||
virtual_servers:
|
||||
- destination: "10.10.10.10:0"
|
||||
enabled: "True"
|
||||
full_path: "jsdfhsd"
|
||||
limit_max_bps: "0"
|
||||
limit_max_bps_status: "disabled"
|
||||
limit_max_connections: "0"
|
||||
limit_max_connections_status: "disabled"
|
||||
limit_max_pps: "0"
|
||||
limit_max_pps_status: "disabled"
|
||||
name: "jsdfhsd"
|
||||
translation_address: "none"
|
||||
translation_port: "0"
|
||||
'''
|
||||
|
||||
try:
|
||||
from distutils.version import LooseVersion
|
||||
from f5.bigip.contexts import TransactionContextManager
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class BigIpGtmFactsCommon(object):
|
||||
def __init__(self):
|
||||
self.api = None
|
||||
self.attributes_to_remove = [
|
||||
'kind', 'generation', 'selfLink', '_meta_data',
|
||||
'membersReference', 'datacenterReference',
|
||||
'virtualServersReference', 'nameReference'
|
||||
]
|
||||
self.gtm_types = dict(
|
||||
a_s='a',
|
||||
aaaas='aaaa',
|
||||
cnames='cname',
|
||||
mxs='mx',
|
||||
naptrs='naptr',
|
||||
srvs='srv'
|
||||
)
|
||||
self.request_params = dict(
|
||||
params='expandSubcollections=true'
|
||||
)
|
||||
|
||||
def is_version_less_than_12(self):
|
||||
version = self.api.tmos_version
|
||||
if LooseVersion(version) < LooseVersion('12.0.0'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def format_string_facts(self, parameters):
|
||||
result = dict()
|
||||
for attribute in self.attributes_to_remove:
|
||||
parameters.pop(attribute, None)
|
||||
for key, val in parameters.iteritems():
|
||||
result[key] = str(val)
|
||||
return result
|
||||
|
||||
def filter_matches_name(self, name):
|
||||
if not self.params['filter']:
|
||||
return True
|
||||
matches = re.match(self.params['filter'], str(name))
|
||||
if matches:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_facts_from_collection(self, collection, collection_type=None):
|
||||
results = []
|
||||
for item in collection:
|
||||
if not self.filter_matches_name(item.name):
|
||||
continue
|
||||
facts = self.format_facts(item, collection_type)
|
||||
results.append(facts)
|
||||
return results
|
||||
|
||||
def connect_to_bigip(self, **kwargs):
|
||||
return ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
|
||||
class BigIpGtmFactsPools(BigIpGtmFactsCommon):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BigIpGtmFactsPools, self).__init__()
|
||||
self.params = kwargs
|
||||
|
||||
def get_facts(self):
|
||||
self.api = self.connect_to_bigip(**self.params)
|
||||
return self.get_facts_from_device()
|
||||
|
||||
def get_facts_from_device(self):
|
||||
try:
|
||||
if self.is_version_less_than_12():
|
||||
return self.get_facts_without_types()
|
||||
else:
|
||||
return self.get_facts_with_types()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
def get_facts_with_types(self):
|
||||
result = []
|
||||
for key, type in self.gtm_types.iteritems():
|
||||
facts = self.get_all_facts_by_type(key, type)
|
||||
if facts:
|
||||
result.append(facts)
|
||||
return result
|
||||
|
||||
def get_facts_without_types(self):
|
||||
pools = self.api.tm.gtm.pools.get_collection(**self.request_params)
|
||||
return self.get_facts_from_collection(pools)
|
||||
|
||||
def get_all_facts_by_type(self, key, type):
|
||||
collection = getattr(self.api.tm.gtm.pools, key)
|
||||
pools = collection.get_collection(**self.request_params)
|
||||
return self.get_facts_from_collection(pools, type)
|
||||
|
||||
def format_facts(self, pool, collection_type):
|
||||
result = dict()
|
||||
pool_dict = pool.to_dict()
|
||||
result.update(self.format_string_facts(pool_dict))
|
||||
result.update(self.format_member_facts(pool))
|
||||
if collection_type:
|
||||
result['type'] = collection_type
|
||||
return camel_dict_to_snake_dict(result)
|
||||
|
||||
def format_member_facts(self, pool):
|
||||
result = []
|
||||
if not 'items' in pool.membersReference:
|
||||
return dict(members=[])
|
||||
for member in pool.membersReference['items']:
|
||||
member_facts = self.format_string_facts(member)
|
||||
result.append(member_facts)
|
||||
return dict(members=result)
|
||||
|
||||
|
||||
class BigIpGtmFactsWideIps(BigIpGtmFactsCommon):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BigIpGtmFactsWideIps, self).__init__()
|
||||
self.params = kwargs
|
||||
|
||||
def get_facts(self):
|
||||
self.api = self.connect_to_bigip(**self.params)
|
||||
return self.get_facts_from_device()
|
||||
|
||||
def get_facts_from_device(self):
|
||||
try:
|
||||
if self.is_version_less_than_12():
|
||||
return self.get_facts_without_types()
|
||||
else:
|
||||
return self.get_facts_with_types()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
def get_facts_with_types(self):
|
||||
result = []
|
||||
for key, type in self.gtm_types.iteritems():
|
||||
facts = self.get_all_facts_by_type(key, type)
|
||||
if facts:
|
||||
result.append(facts)
|
||||
return result
|
||||
|
||||
def get_facts_without_types(self):
|
||||
wideips = self.api.tm.gtm.wideips.get_collection(
|
||||
**self.request_params
|
||||
)
|
||||
return self.get_facts_from_collection(wideips)
|
||||
|
||||
def get_all_facts_by_type(self, key, type):
|
||||
collection = getattr(self.api.tm.gtm.wideips, key)
|
||||
wideips = collection.get_collection(**self.request_params)
|
||||
return self.get_facts_from_collection(wideips, type)
|
||||
|
||||
def format_facts(self, wideip, collection_type):
|
||||
result = dict()
|
||||
wideip_dict = wideip.to_dict()
|
||||
result.update(self.format_string_facts(wideip_dict))
|
||||
result.update(self.format_pool_facts(wideip))
|
||||
if collection_type:
|
||||
result['type'] = collection_type
|
||||
return camel_dict_to_snake_dict(result)
|
||||
|
||||
def format_pool_facts(self, wideip):
|
||||
result = []
|
||||
if not hasattr(wideip, 'pools'):
|
||||
return dict(pools=[])
|
||||
for pool in wideip.pools:
|
||||
pool_facts = self.format_string_facts(pool)
|
||||
result.append(pool_facts)
|
||||
return dict(pools=result)
|
||||
|
||||
|
||||
class BigIpGtmFactsVirtualServers(BigIpGtmFactsCommon):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BigIpGtmFactsVirtualServers, self).__init__()
|
||||
self.params = kwargs
|
||||
|
||||
def get_facts(self):
|
||||
try:
|
||||
self.api = self.connect_to_bigip(**self.params)
|
||||
return self.get_facts_from_device()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
def get_facts_from_device(self):
|
||||
servers = self.api.tm.gtm.servers.get_collection(
|
||||
**self.request_params
|
||||
)
|
||||
return self.get_facts_from_collection(servers)
|
||||
|
||||
def format_facts(self, server, collection_type=None):
|
||||
result = dict()
|
||||
server_dict = server.to_dict()
|
||||
result.update(self.format_string_facts(server_dict))
|
||||
result.update(self.format_address_facts(server))
|
||||
result.update(self.format_virtual_server_facts(server))
|
||||
return camel_dict_to_snake_dict(result)
|
||||
|
||||
def format_address_facts(self, server):
|
||||
result = []
|
||||
if not hasattr(server, 'addresses'):
|
||||
return dict(addresses=[])
|
||||
for address in server.addresses:
|
||||
address_facts = self.format_string_facts(address)
|
||||
result.append(address_facts)
|
||||
return dict(addresses=result)
|
||||
|
||||
def format_virtual_server_facts(self, server):
|
||||
result = []
|
||||
if not 'items' in server.virtualServersReference:
|
||||
return dict(virtual_servers=[])
|
||||
for server in server.virtualServersReference['items']:
|
||||
server_facts = self.format_string_facts(server)
|
||||
result.append(server_facts)
|
||||
return dict(virtual_servers=result)
|
||||
|
||||
class BigIpGtmFactsManager(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.params = kwargs
|
||||
self.api = None
|
||||
|
||||
def get_facts(self):
|
||||
result = dict()
|
||||
facts = dict()
|
||||
|
||||
if 'pool' in self.params['include']:
|
||||
facts['pool'] = self.get_pool_facts()
|
||||
if 'wide_ip' in self.params['include']:
|
||||
facts['wide_ip'] = self.get_wide_ip_facts()
|
||||
if 'virtual_server' in self.params['include']:
|
||||
facts['virtual_server'] = self.get_virtual_server_facts()
|
||||
|
||||
result.update(**facts)
|
||||
result.update(dict(changed=True))
|
||||
return result
|
||||
|
||||
def get_pool_facts(self):
|
||||
pools = BigIpGtmFactsPools(**self.params)
|
||||
return pools.get_facts()
|
||||
|
||||
def get_wide_ip_facts(self):
|
||||
wide_ips = BigIpGtmFactsWideIps(**self.params)
|
||||
return wide_ips.get_facts()
|
||||
|
||||
def get_virtual_server_facts(self):
|
||||
wide_ips = BigIpGtmFactsVirtualServers(**self.params)
|
||||
return wide_ips.get_facts()
|
||||
|
||||
|
||||
class BigIpGtmFactsModuleConfig(object):
|
||||
def __init__(self):
|
||||
self.argument_spec = dict()
|
||||
self.meta_args = dict()
|
||||
self.supports_check_mode = False
|
||||
self.valid_includes = ['pool', 'wide_ip', 'virtual_server']
|
||||
self.initialize_meta_args()
|
||||
self.initialize_argument_spec()
|
||||
|
||||
def initialize_meta_args(self):
|
||||
args = dict(
|
||||
include=dict(type='list', required=True),
|
||||
filter=dict(type='str', required=False)
|
||||
)
|
||||
self.meta_args = args
|
||||
|
||||
def initialize_argument_spec(self):
|
||||
self.argument_spec = f5_argument_spec()
|
||||
self.argument_spec.update(self.meta_args)
|
||||
|
||||
def create(self):
|
||||
return AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=self.supports_check_mode
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
config = BigIpGtmFactsModuleConfig()
|
||||
module = config.create()
|
||||
|
||||
try:
|
||||
obj = BigIpGtmFactsManager(
|
||||
check_mode=module.check_mode, **module.params
|
||||
)
|
||||
result = obj.get_facts()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
243
lib/ansible/modules/network/f5/bigip_gtm_virtual_server.py
Normal file
243
lib/ansible/modules/network/f5/bigip_gtm_virtual_server.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2015, Michael Perzel
|
||||
#
|
||||
# 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: bigip_gtm_virtual_server
|
||||
short_description: "Manages F5 BIG-IP GTM virtual servers"
|
||||
description:
|
||||
- "Manages F5 BIG-IP GTM virtual servers"
|
||||
version_added: "2.2"
|
||||
author:
|
||||
- Michael Perzel (@perzizzle)
|
||||
- Tim Rupp (@caphrim007)
|
||||
notes:
|
||||
- "Requires BIG-IP software version >= 11.4"
|
||||
- "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)"
|
||||
- "Best run as a local_action in your playbook"
|
||||
- "Tested with manager and above account privilege level"
|
||||
|
||||
requirements:
|
||||
- bigsuds
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Virtual server state
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present', 'absent','enabled','disabled']
|
||||
virtual_server_name:
|
||||
description:
|
||||
- Virtual server name
|
||||
required: True
|
||||
virtual_server_server:
|
||||
description:
|
||||
- Virtual server server
|
||||
required: true
|
||||
host:
|
||||
description:
|
||||
- Virtual server host
|
||||
required: false
|
||||
default: None
|
||||
aliases: ['address']
|
||||
port:
|
||||
description:
|
||||
- Virtual server port
|
||||
required: false
|
||||
default: None
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Enable virtual server
|
||||
local_action: >
|
||||
bigip_gtm_virtual_server
|
||||
server=192.0.2.1
|
||||
user=admin
|
||||
password=mysecret
|
||||
virtual_server_name=myname
|
||||
virtual_server_server=myserver
|
||||
state=enabled
|
||||
'''
|
||||
|
||||
RETURN = '''# '''
|
||||
|
||||
try:
|
||||
import bigsuds
|
||||
except ImportError:
|
||||
bigsuds_found = False
|
||||
else:
|
||||
bigsuds_found = True
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils.f5 import bigip_api, f5_argument_spec
|
||||
|
||||
|
||||
def server_exists(api, server):
|
||||
# hack to determine if virtual server exists
|
||||
result = False
|
||||
try:
|
||||
api.GlobalLB.Server.get_object_status([server])
|
||||
result = True
|
||||
except bigsuds.OperationFailed:
|
||||
e = get_exception()
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def virtual_server_exists(api, name, server):
|
||||
# hack to determine if virtual server exists
|
||||
result = False
|
||||
try:
|
||||
virtual_server_id = {'name': name, 'server': server}
|
||||
api.GlobalLB.VirtualServerV2.get_object_status([virtual_server_id])
|
||||
result = True
|
||||
except bigsuds.OperationFailed:
|
||||
e = get_exception()
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def add_virtual_server(api, virtual_server_name, virtual_server_server, address, port):
|
||||
addresses = {'address': address, 'port': port}
|
||||
virtual_server_id = {'name': virtual_server_name, 'server': virtual_server_server}
|
||||
api.GlobalLB.VirtualServerV2.create([virtual_server_id], [addresses])
|
||||
|
||||
|
||||
def remove_virtual_server(api, virtual_server_name, virtual_server_server):
|
||||
virtual_server_id = {'name': virtual_server_name, 'server': virtual_server_server}
|
||||
api.GlobalLB.VirtualServerV2.delete_virtual_server([virtual_server_id])
|
||||
|
||||
|
||||
def get_virtual_server_state(api, name, server):
|
||||
virtual_server_id = {'name': name, 'server': server}
|
||||
state = api.GlobalLB.VirtualServerV2.get_enabled_state([virtual_server_id])
|
||||
state = state[0].split('STATE_')[1].lower()
|
||||
return state
|
||||
|
||||
|
||||
def set_virtual_server_state(api, name, server, state):
|
||||
virtual_server_id = {'name': name, 'server': server}
|
||||
state = "STATE_%s" % state.strip().upper()
|
||||
api.GlobalLB.VirtualServerV2.set_enabled_state([virtual_server_id], [state])
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
|
||||
host=dict(type='str', default=None, aliases=['address']),
|
||||
port=dict(type='int', default=None),
|
||||
virtual_server_name=dict(type='str', required=True),
|
||||
virtual_server_server=dict(type='str', required=True)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if not bigsuds_found:
|
||||
module.fail_json(msg="the python bigsuds module is required")
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
validate_certs = module.params['validate_certs']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
virtual_server_name = module.params['virtual_server_name']
|
||||
virtual_server_server = module.params['virtual_server_server']
|
||||
state = module.params['state']
|
||||
address = module.params['host']
|
||||
port = module.params['port']
|
||||
|
||||
result = {'changed': False} # default
|
||||
|
||||
try:
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
|
||||
if state == 'absent':
|
||||
if virtual_server_exists(api, virtual_server_name, virtual_server_server):
|
||||
if not module.check_mode:
|
||||
remove_virtual_server(api, virtual_server_name, virtual_server_server)
|
||||
result = {'changed': True}
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
elif state == 'present':
|
||||
if virtual_server_name and virtual_server_server and address and port:
|
||||
if not virtual_server_exists(api, virtual_server_name, virtual_server_server):
|
||||
if not module.check_mode:
|
||||
if server_exists(api, virtual_server_server):
|
||||
add_virtual_server(api, virtual_server_name, virtual_server_server, address, port)
|
||||
result = {'changed': True}
|
||||
else:
|
||||
module.fail_json(msg="server does not exist")
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
else:
|
||||
# virtual server exists -- potentially modify attributes --future feature
|
||||
result = {'changed': False}
|
||||
else:
|
||||
module.fail_json(msg="Address and port are required to create virtual server")
|
||||
elif state == 'enabled':
|
||||
if not virtual_server_exists(api, virtual_server_name, virtual_server_server):
|
||||
module.fail_json(msg="virtual server does not exist")
|
||||
if state != get_virtual_server_state(api, virtual_server_name, virtual_server_server):
|
||||
if not module.check_mode:
|
||||
set_virtual_server_state(api, virtual_server_name, virtual_server_server, state)
|
||||
result = {'changed': True}
|
||||
else:
|
||||
result = {'changed': True}
|
||||
elif state == 'disabled':
|
||||
if not virtual_server_exists(api, virtual_server_name, virtual_server_server):
|
||||
module.fail_json(msg="virtual server does not exist")
|
||||
if state != get_virtual_server_state(api, virtual_server_name, virtual_server_server):
|
||||
if not module.check_mode:
|
||||
set_virtual_server_state(api, virtual_server_name, virtual_server_server, state)
|
||||
result = {'changed': True}
|
||||
else:
|
||||
result = {'changed': True}
|
||||
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
167
lib/ansible/modules/network/f5/bigip_gtm_wide_ip.py
Normal file
167
lib/ansible/modules/network/f5/bigip_gtm_wide_ip.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2015, Michael Perzel
|
||||
#
|
||||
# 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: bigip_gtm_wide_ip
|
||||
short_description: "Manages F5 BIG-IP GTM wide ip"
|
||||
description:
|
||||
- "Manages F5 BIG-IP GTM wide ip"
|
||||
version_added: "2.0"
|
||||
author:
|
||||
- Michael Perzel (@perzizzle)
|
||||
- Tim Rupp (@caphrim007)
|
||||
notes:
|
||||
- "Requires BIG-IP software version >= 11.4"
|
||||
- "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)"
|
||||
- "Best run as a local_action in your playbook"
|
||||
- "Tested with manager and above account privilege level"
|
||||
|
||||
requirements:
|
||||
- bigsuds
|
||||
options:
|
||||
lb_method:
|
||||
description:
|
||||
- LB method of wide ip
|
||||
required: true
|
||||
choices: ['return_to_dns', 'null', 'round_robin',
|
||||
'ratio', 'topology', 'static_persist', 'global_availability',
|
||||
'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops',
|
||||
'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps',
|
||||
'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score']
|
||||
wide_ip:
|
||||
description:
|
||||
- Wide IP name
|
||||
required: true
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set lb method
|
||||
local_action: >
|
||||
bigip_gtm_wide_ip
|
||||
server=192.0.2.1
|
||||
user=admin
|
||||
password=mysecret
|
||||
lb_method=round_robin
|
||||
wide_ip=my-wide-ip.example.com
|
||||
'''
|
||||
|
||||
try:
|
||||
import bigsuds
|
||||
except ImportError:
|
||||
bigsuds_found = False
|
||||
else:
|
||||
bigsuds_found = True
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils.f5 import bigip_api, f5_argument_spec
|
||||
|
||||
|
||||
def get_wide_ip_lb_method(api, wide_ip):
|
||||
lb_method = api.GlobalLB.WideIP.get_lb_method(wide_ips=[wide_ip])[0]
|
||||
lb_method = lb_method.strip().replace('LB_METHOD_', '').lower()
|
||||
return lb_method
|
||||
|
||||
def get_wide_ip_pools(api, wide_ip):
|
||||
try:
|
||||
return api.GlobalLB.WideIP.get_wideip_pool([wide_ip])
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
print(e)
|
||||
|
||||
def wide_ip_exists(api, wide_ip):
|
||||
# hack to determine if wide_ip exists
|
||||
result = False
|
||||
try:
|
||||
api.GlobalLB.WideIP.get_object_status(wide_ips=[wide_ip])
|
||||
result = True
|
||||
except bigsuds.OperationFailed:
|
||||
e = get_exception()
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
def set_wide_ip_lb_method(api, wide_ip, lb_method):
|
||||
lb_method = "LB_METHOD_%s" % lb_method.strip().upper()
|
||||
api.GlobalLB.WideIP.set_lb_method(wide_ips=[wide_ip], lb_methods=[lb_method])
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
lb_method_choices = ['return_to_dns', 'null', 'round_robin',
|
||||
'ratio', 'topology', 'static_persist', 'global_availability',
|
||||
'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops',
|
||||
'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps',
|
||||
'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score']
|
||||
meta_args = dict(
|
||||
lb_method = dict(type='str', required=True, choices=lb_method_choices),
|
||||
wide_ip = dict(type='str', required=True)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if not bigsuds_found:
|
||||
module.fail_json(msg="the python bigsuds module is required")
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
wide_ip = module.params['wide_ip']
|
||||
lb_method = module.params['lb_method']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
result = {'changed': False} # default
|
||||
|
||||
try:
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
|
||||
if not wide_ip_exists(api, wide_ip):
|
||||
module.fail_json(msg="wide ip %s does not exist" % wide_ip)
|
||||
|
||||
if lb_method is not None and lb_method != get_wide_ip_lb_method(api, wide_ip):
|
||||
if not module.check_mode:
|
||||
set_wide_ip_lb_method(api, wide_ip, lb_method)
|
||||
result = {'changed': True}
|
||||
else:
|
||||
result = {'changed': True}
|
||||
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
188
lib/ansible/modules/network/f5/bigip_hostname.py
Normal file
188
lib/ansible/modules/network/f5/bigip_hostname.py
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_hostname
|
||||
short_description: Manage the hostname of a BIG-IP.
|
||||
description:
|
||||
- Manage the hostname of a BIG-IP.
|
||||
version_added: "2.3"
|
||||
options:
|
||||
hostname:
|
||||
description:
|
||||
- Hostname of the BIG-IP host.
|
||||
required: true
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install f5-sdk.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set the hostname of the BIG-IP
|
||||
bigip_hostname:
|
||||
hostname: "bigip.localhost.localdomain"
|
||||
password: "admin"
|
||||
server: "bigip.localhost.localdomain"
|
||||
user: "admin"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
hostname:
|
||||
description: The new hostname of the device
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "big-ip01.internal"
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip.contexts import TransactionContextManager
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
class BigIpHostnameManager(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.changed_params = dict()
|
||||
self.params = kwargs
|
||||
self.api = None
|
||||
|
||||
def connect_to_bigip(self, **kwargs):
|
||||
return ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def ensure_hostname_is_present(self):
|
||||
self.changed_params['hostname'] = self.params['hostname']
|
||||
|
||||
if self.params['check_mode']:
|
||||
return True
|
||||
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
r = api.tm.sys.global_settings.load()
|
||||
r.update(hostname=self.params['hostname'])
|
||||
|
||||
if self.hostname_exists():
|
||||
return True
|
||||
else:
|
||||
raise F5ModuleError("Failed to set the hostname")
|
||||
|
||||
def hostname_exists(self):
|
||||
if self.params['hostname'] == self.current_hostname():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def present(self):
|
||||
if self.hostname_exists():
|
||||
return False
|
||||
else:
|
||||
|
||||
return self.ensure_hostname_is_present()
|
||||
|
||||
def current_hostname(self):
|
||||
r = self.api.tm.sys.global_settings.load()
|
||||
return r.hostname
|
||||
|
||||
def apply_changes(self):
|
||||
result = dict()
|
||||
|
||||
changed = self.apply_to_running_config()
|
||||
if changed:
|
||||
self.save_running_config()
|
||||
|
||||
result.update(**self.changed_params)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def apply_to_running_config(self):
|
||||
try:
|
||||
self.api = self.connect_to_bigip(**self.params)
|
||||
return self.present()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
def save_running_config(self):
|
||||
self.api.tm.sys.config.exec_cmd('save')
|
||||
|
||||
|
||||
class BigIpHostnameModuleConfig(object):
|
||||
def __init__(self):
|
||||
self.argument_spec = dict()
|
||||
self.meta_args = dict()
|
||||
self.supports_check_mode = True
|
||||
|
||||
self.initialize_meta_args()
|
||||
self.initialize_argument_spec()
|
||||
|
||||
def initialize_meta_args(self):
|
||||
args = dict(
|
||||
hostname=dict(required=True)
|
||||
)
|
||||
self.meta_args = args
|
||||
|
||||
def initialize_argument_spec(self):
|
||||
self.argument_spec = f5_argument_spec()
|
||||
self.argument_spec.update(self.meta_args)
|
||||
|
||||
def create(self):
|
||||
return AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=self.supports_check_mode
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
config = BigIpHostnameModuleConfig()
|
||||
module = config.create()
|
||||
|
||||
try:
|
||||
obj = BigIpHostnameManager(
|
||||
check_mode=module.check_mode, **module.params
|
||||
)
|
||||
result = obj.apply_changes()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
388
lib/ansible/modules/network/f5/bigip_irule.py
Normal file
388
lib/ansible/modules/network/f5/bigip_irule.py
Normal file
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_irule
|
||||
short_description: Manage iRules across different modules on a BIG-IP.
|
||||
description:
|
||||
- Manage iRules across different modules on a BIG-IP.
|
||||
version_added: "2.2"
|
||||
options:
|
||||
content:
|
||||
description:
|
||||
- When used instead of 'src', sets the contents of an iRule directly to
|
||||
the specified value. This is for simple values, but can be used with
|
||||
lookup plugins for anything complex or with formatting. Either one
|
||||
of C(src) or C(content) must be provided.
|
||||
module:
|
||||
description:
|
||||
- The BIG-IP module to add the iRule to.
|
||||
required: true
|
||||
choices:
|
||||
- ltm
|
||||
- gtm
|
||||
partition:
|
||||
description:
|
||||
- The partition to create the iRule on.
|
||||
required: false
|
||||
default: Common
|
||||
name:
|
||||
description:
|
||||
- The name of the iRule.
|
||||
required: true
|
||||
src:
|
||||
description:
|
||||
- The iRule file to interpret and upload to the BIG-IP. Either one
|
||||
of C(src) or C(content) must be provided.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Whether the iRule should exist or not.
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as
|
||||
pip install f5-sdk.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add the iRule contained in templated irule.tcl to the LTM module
|
||||
bigip_irule:
|
||||
content: "{{ lookup('template', 'irule-template.tcl') }}"
|
||||
module: "ltm"
|
||||
name: "MyiRule"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "present"
|
||||
user: "admin"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Add the iRule contained in static file irule.tcl to the LTM module
|
||||
bigip_irule:
|
||||
module: "ltm"
|
||||
name: "MyiRule"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
src: "irule-static.tcl"
|
||||
state: "present"
|
||||
user: "admin"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
module:
|
||||
description: The module that the iRule was added to
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "gtm"
|
||||
src:
|
||||
description: The filename that included the iRule source
|
||||
returned: changed and success, when provided
|
||||
type: string
|
||||
sample: "/opt/src/irules/example1.tcl"
|
||||
name:
|
||||
description: The name of the iRule that was managed
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "my-irule"
|
||||
content:
|
||||
description: The content of the iRule that was managed
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "when LB_FAILED { set wipHost [LB::server addr] }"
|
||||
partition:
|
||||
description: The partition in which the iRule was managed
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "Common"
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
MODULES = ['gtm', 'ltm']
|
||||
|
||||
|
||||
class BigIpiRule(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
if kwargs['state'] != 'absent':
|
||||
if not kwargs['content'] and not kwargs['src']:
|
||||
raise F5ModuleError(
|
||||
"Either 'content' or 'src' must be provided"
|
||||
)
|
||||
|
||||
source = kwargs['src']
|
||||
if source:
|
||||
with open(source) as f:
|
||||
kwargs['content'] = f.read()
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
state = self.params['state']
|
||||
|
||||
try:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
elif state == "absent":
|
||||
changed = self.absent()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def read(self):
|
||||
"""Read information and transform it
|
||||
|
||||
The values that are returned by BIG-IP in the f5-sdk can have encoding
|
||||
attached to them as well as be completely missing in some cases.
|
||||
|
||||
Therefore, this method will transform the data from the BIG-IP into a
|
||||
format that is more easily consumable by the rest of the class and the
|
||||
parameters that are supported by the module.
|
||||
"""
|
||||
p = dict()
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
module = self.params['module']
|
||||
|
||||
if module == 'ltm':
|
||||
r = self.api.tm.ltm.rules.rule.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
elif module == 'gtm':
|
||||
r = self.api.tm.gtm.rules.rule.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
|
||||
if hasattr(r, 'apiAnonymous'):
|
||||
p['content'] = str(r.apiAnonymous.strip())
|
||||
p['name'] = name
|
||||
return p
|
||||
|
||||
def delete(self):
|
||||
params = dict()
|
||||
check_mode = self.params['check_mode']
|
||||
module = self.params['module']
|
||||
|
||||
params['name'] = self.params['name']
|
||||
params['partition'] = self.params['partition']
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
if module == 'ltm':
|
||||
r = self.api.tm.ltm.rules.rule.load(**params)
|
||||
r.delete()
|
||||
elif module == 'gtm':
|
||||
r = self.api.tm.gtm.rules.rule.load(**params)
|
||||
r.delete()
|
||||
|
||||
if self.exists():
|
||||
raise F5ModuleError("Failed to delete the iRule")
|
||||
return True
|
||||
|
||||
def exists(self):
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
module = self.params['module']
|
||||
|
||||
if module == 'ltm':
|
||||
return self.api.tm.ltm.rules.rule.exists(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
elif module == 'gtm':
|
||||
return self.api.tm.gtm.rules.rule.exists(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
|
||||
def present(self):
|
||||
if self.exists():
|
||||
return self.update()
|
||||
else:
|
||||
return self.create()
|
||||
|
||||
def update(self):
|
||||
params = dict()
|
||||
current = self.read()
|
||||
changed = False
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
content = self.params['content']
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
module = self.params['module']
|
||||
|
||||
if content is not None:
|
||||
content = content.strip()
|
||||
if 'content' in current:
|
||||
if content != current['content']:
|
||||
params['apiAnonymous'] = content
|
||||
else:
|
||||
params['apiAnonymous'] = content
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
params['name'] = name
|
||||
params['partition'] = partition
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if 'api_anonymous' in self.cparams:
|
||||
self.cparams['content'] = self.cparams.pop('api_anonymous')
|
||||
if self.params['src']:
|
||||
self.cparams['src'] = self.params['src']
|
||||
|
||||
if check_mode:
|
||||
return changed
|
||||
else:
|
||||
return changed
|
||||
|
||||
if module == 'ltm':
|
||||
d = self.api.tm.ltm.rules.rule.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
d.update(**params)
|
||||
d.refresh()
|
||||
elif module == 'gtm':
|
||||
d = self.api.tm.gtm.rules.rule.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
d.update(**params)
|
||||
d.refresh()
|
||||
|
||||
return True
|
||||
|
||||
def create(self):
|
||||
params = dict()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
content = self.params['content']
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
module = self.params['module']
|
||||
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
if content is not None:
|
||||
params['apiAnonymous'] = content.strip()
|
||||
|
||||
params['name'] = name
|
||||
params['partition'] = partition
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if 'api_anonymous' in self.cparams:
|
||||
self.cparams['content'] = self.cparams.pop('api_anonymous')
|
||||
if self.params['src']:
|
||||
self.cparams['src'] = self.params['src']
|
||||
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
if module == 'ltm':
|
||||
d = self.api.tm.ltm.rules.rule
|
||||
d.create(**params)
|
||||
elif module == 'gtm':
|
||||
d = self.api.tm.gtm.rules.rule
|
||||
d.create(**params)
|
||||
|
||||
if not self.exists():
|
||||
raise F5ModuleError("Failed to create the iRule")
|
||||
return True
|
||||
|
||||
def absent(self):
|
||||
changed = False
|
||||
|
||||
if self.exists():
|
||||
changed = self.delete()
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
content=dict(required=False, default=None),
|
||||
src=dict(required=False, default=None),
|
||||
name=dict(required=True),
|
||||
module=dict(required=True, choices=MODULES)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[
|
||||
['content', 'src']
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpiRule(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
447
lib/ansible/modules/network/f5/bigip_monitor_http.py
Normal file
447
lib/ansible/modules/network/f5/bigip_monitor_http.py
Normal file
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2013, serge van Ginderachter <serge@vanginderachter.be>
|
||||
# based on Matt Hite's bigip_pool module
|
||||
# (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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: bigip_monitor_http
|
||||
short_description: "Manages F5 BIG-IP LTM http monitors"
|
||||
description:
|
||||
- Manages F5 BIG-IP LTM monitors via iControl SOAP API
|
||||
version_added: "1.4"
|
||||
author:
|
||||
- Serge van Ginderachter (@srvg)
|
||||
- Tim Rupp (@caphrim007)
|
||||
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"
|
||||
- "Monitor API documentation: https://devcentral.f5.com/wiki/iControl.LocalLB__Monitor.ashx"
|
||||
requirements:
|
||||
- bigsuds
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Monitor state
|
||||
required: false
|
||||
default: 'present'
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
name:
|
||||
description:
|
||||
- Monitor name
|
||||
required: true
|
||||
default: null
|
||||
aliases:
|
||||
- monitor
|
||||
partition:
|
||||
description:
|
||||
- Partition for the monitor
|
||||
required: false
|
||||
default: 'Common'
|
||||
parent:
|
||||
description:
|
||||
- The parent template of this monitor template
|
||||
required: false
|
||||
default: 'http'
|
||||
parent_partition:
|
||||
description:
|
||||
- Partition for the parent monitor
|
||||
required: false
|
||||
default: 'Common'
|
||||
send:
|
||||
description:
|
||||
- The send string for the monitor call
|
||||
required: true
|
||||
default: none
|
||||
receive:
|
||||
description:
|
||||
- The receive string for the monitor call
|
||||
required: true
|
||||
default: none
|
||||
receive_disable:
|
||||
description:
|
||||
- The receive disable string for the monitor call
|
||||
required: true
|
||||
default: none
|
||||
ip:
|
||||
description:
|
||||
- IP address part of the ipport definition. The default API setting
|
||||
is "0.0.0.0".
|
||||
required: false
|
||||
default: none
|
||||
port:
|
||||
description:
|
||||
- Port address part of the ip/port definition. The default API
|
||||
setting is 0.
|
||||
required: false
|
||||
default: none
|
||||
interval:
|
||||
description:
|
||||
- The interval specifying how frequently the monitor instance
|
||||
of this template will run. By default, this interval is used for up and
|
||||
down states. The default API setting is 5.
|
||||
required: false
|
||||
default: none
|
||||
timeout:
|
||||
description:
|
||||
- The number of seconds in which the node or service must respond to
|
||||
the monitor request. If the target responds within the set time
|
||||
period, it is considered up. If the target does not respond within
|
||||
the set time period, it is considered down. You can change this
|
||||
number to any number you want, however, it should be 3 times the
|
||||
interval number of seconds plus 1 second. The default API setting
|
||||
is 16.
|
||||
required: false
|
||||
default: none
|
||||
time_until_up:
|
||||
description:
|
||||
- Specifies the amount of time in seconds after the first successful
|
||||
response before a node will be marked up. A value of 0 will cause a
|
||||
node to be marked up immediately after a valid response is received
|
||||
from the node. The default API setting is 0.
|
||||
required: false
|
||||
default: none
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: BIGIP F5 | Create HTTP Monitor
|
||||
bigip_monitor_http:
|
||||
state: "present"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my_http_monitor"
|
||||
send: "http string to send"
|
||||
receive: "http string to receive"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: BIGIP F5 | Remove HTTP Monitor
|
||||
bigip_monitor_http:
|
||||
state: "absent"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my_http_monitor"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
TEMPLATE_TYPE = 'TTYPE_HTTP'
|
||||
DEFAULT_PARENT_TYPE = 'http'
|
||||
|
||||
|
||||
def check_monitor_exists(module, api, monitor, parent):
|
||||
# hack to determine if monitor exists
|
||||
result = False
|
||||
try:
|
||||
ttype = api.LocalLB.Monitor.get_template_type(template_names=[monitor])[0]
|
||||
parent2 = api.LocalLB.Monitor.get_parent_template(template_names=[monitor])[0]
|
||||
if ttype == TEMPLATE_TYPE and parent == parent2:
|
||||
result = True
|
||||
else:
|
||||
module.fail_json(msg='Monitor already exists, but has a different type (%s) or parent(%s)' % (ttype, parent))
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def create_monitor(api, monitor, template_attributes):
|
||||
try:
|
||||
api.LocalLB.Monitor.create_template(
|
||||
templates=[{
|
||||
'template_name': monitor,
|
||||
'template_type': TEMPLATE_TYPE
|
||||
}],
|
||||
template_attributes=[template_attributes]
|
||||
)
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "already exists" in str(e):
|
||||
return False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def delete_monitor(api, monitor):
|
||||
try:
|
||||
api.LocalLB.Monitor.delete_template(template_names=[monitor])
|
||||
except bigsuds.OperationFailed as e:
|
||||
# maybe it was deleted since we checked
|
||||
if "was not found" in str(e):
|
||||
return False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def check_string_property(api, monitor, str_property):
|
||||
try:
|
||||
template_prop = api.LocalLB.Monitor.get_template_string_property(
|
||||
[monitor], [str_property['type']]
|
||||
)[0]
|
||||
return str_property == template_prop
|
||||
except bigsuds.OperationFailed as e:
|
||||
# happens in check mode if not created yet
|
||||
if "was not found" in str(e):
|
||||
return True
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
|
||||
|
||||
def set_string_property(api, monitor, str_property):
|
||||
api.LocalLB.Monitor.set_template_string_property(
|
||||
template_names=[monitor],
|
||||
values=[str_property]
|
||||
)
|
||||
|
||||
|
||||
def check_integer_property(api, monitor, int_property):
|
||||
try:
|
||||
template_prop = api.LocalLB.Monitor.get_template_integer_property(
|
||||
[monitor], [int_property['type']]
|
||||
)[0]
|
||||
return int_property == template_prop
|
||||
except bigsuds.OperationFailed as e:
|
||||
# happens in check mode if not created yet
|
||||
if "was not found" in str(e):
|
||||
return True
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
|
||||
|
||||
def set_integer_property(api, monitor, int_property):
|
||||
api.LocalLB.Monitor.set_template_integer_property(
|
||||
template_names=[monitor],
|
||||
values=[int_property]
|
||||
)
|
||||
|
||||
|
||||
def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties):
|
||||
changed = False
|
||||
for str_property in template_string_properties:
|
||||
if str_property['value'] is not None and not check_string_property(api, monitor, str_property):
|
||||
if not module.check_mode:
|
||||
set_string_property(api, monitor, str_property)
|
||||
changed = True
|
||||
for int_property in template_integer_properties:
|
||||
if int_property['value'] is not None and not check_integer_property(api, monitor, int_property):
|
||||
if not module.check_mode:
|
||||
set_integer_property(api, monitor, int_property)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def get_ipport(api, monitor):
|
||||
return api.LocalLB.Monitor.get_template_destination(template_names=[monitor])[0]
|
||||
|
||||
|
||||
def set_ipport(api, monitor, ipport):
|
||||
try:
|
||||
api.LocalLB.Monitor.set_template_destination(
|
||||
template_names=[monitor], destinations=[ipport]
|
||||
)
|
||||
return True, ""
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "Cannot modify the address type of monitor" in str(e):
|
||||
return False, "Cannot modify the address type of monitor if already assigned to a pool."
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
name=dict(required=True),
|
||||
parent=dict(default=DEFAULT_PARENT_TYPE),
|
||||
parent_partition=dict(default='Common'),
|
||||
send=dict(required=False),
|
||||
receive=dict(required=False),
|
||||
receive_disable=dict(required=False),
|
||||
ip=dict(required=False),
|
||||
port=dict(required=False, type='int'),
|
||||
interval=dict(required=False, type='int'),
|
||||
timeout=dict(required=False, type='int'),
|
||||
time_until_up=dict(required=False, type='int', default=0)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
state = module.params['state']
|
||||
partition = module.params['partition']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
parent_partition = module.params['parent_partition']
|
||||
name = module.params['name']
|
||||
parent = fq_name(parent_partition, module.params['parent'])
|
||||
monitor = fq_name(partition, name)
|
||||
send = module.params['send']
|
||||
receive = module.params['receive']
|
||||
receive_disable = module.params['receive_disable']
|
||||
ip = module.params['ip']
|
||||
port = module.params['port']
|
||||
interval = module.params['interval']
|
||||
timeout = module.params['timeout']
|
||||
time_until_up = module.params['time_until_up']
|
||||
|
||||
# end monitor specific stuff
|
||||
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
monitor_exists = check_monitor_exists(module, api, monitor, parent)
|
||||
|
||||
# ipport is a special setting
|
||||
if monitor_exists:
|
||||
cur_ipport = get_ipport(api, monitor)
|
||||
if ip is None:
|
||||
ip = cur_ipport['ipport']['address']
|
||||
if port is None:
|
||||
port = cur_ipport['ipport']['port']
|
||||
else:
|
||||
if interval is None:
|
||||
interval = 5
|
||||
if timeout is None:
|
||||
timeout = 16
|
||||
if ip is None:
|
||||
ip = '0.0.0.0'
|
||||
if port is None:
|
||||
port = 0
|
||||
if send is None:
|
||||
send = ''
|
||||
if receive is None:
|
||||
receive = ''
|
||||
if receive_disable is None:
|
||||
receive_disable = ''
|
||||
|
||||
# define and set address type
|
||||
if ip == '0.0.0.0' and port == 0:
|
||||
address_type = 'ATYPE_STAR_ADDRESS_STAR_PORT'
|
||||
elif ip == '0.0.0.0' and port != 0:
|
||||
address_type = 'ATYPE_STAR_ADDRESS_EXPLICIT_PORT'
|
||||
elif ip != '0.0.0.0' and port != 0:
|
||||
address_type = 'ATYPE_EXPLICIT_ADDRESS_EXPLICIT_PORT'
|
||||
else:
|
||||
address_type = 'ATYPE_UNSET'
|
||||
|
||||
ipport = {'address_type': address_type,
|
||||
'ipport': {'address': ip,
|
||||
'port': port}}
|
||||
|
||||
template_attributes = {'parent_template': parent,
|
||||
'interval': interval,
|
||||
'timeout': timeout,
|
||||
'dest_ipport': ipport,
|
||||
'is_read_only': False,
|
||||
'is_directly_usable': True}
|
||||
|
||||
# monitor specific stuff
|
||||
template_string_properties = [{'type': 'STYPE_SEND',
|
||||
'value': send},
|
||||
{'type': 'STYPE_RECEIVE',
|
||||
'value': receive},
|
||||
{'type': 'STYPE_RECEIVE_DRAIN',
|
||||
'value': receive_disable}]
|
||||
|
||||
template_integer_properties = [
|
||||
{
|
||||
'type': 'ITYPE_INTERVAL',
|
||||
'value': interval
|
||||
},
|
||||
{
|
||||
'type': 'ITYPE_TIMEOUT',
|
||||
'value': timeout
|
||||
},
|
||||
{
|
||||
'type': 'ITYPE_TIME_UNTIL_UP',
|
||||
'value': time_until_up
|
||||
}
|
||||
]
|
||||
|
||||
# main logic, monitor generic
|
||||
|
||||
try:
|
||||
result = {'changed': False} # default
|
||||
|
||||
if state == 'absent':
|
||||
if monitor_exists:
|
||||
if not module.check_mode:
|
||||
# possible race condition if same task
|
||||
# on other node deleted it first
|
||||
result['changed'] |= delete_monitor(api, monitor)
|
||||
else:
|
||||
result['changed'] |= True
|
||||
else:
|
||||
# check for monitor itself
|
||||
if not monitor_exists:
|
||||
if not module.check_mode:
|
||||
# again, check changed status here b/c race conditions
|
||||
# if other task already created it
|
||||
result['changed'] |= create_monitor(api, monitor, template_attributes)
|
||||
else:
|
||||
result['changed'] |= True
|
||||
|
||||
# check for monitor parameters
|
||||
# whether it already existed, or was just created, now update
|
||||
# the update functions need to check for check mode but
|
||||
# cannot update settings if it doesn't exist which happens in check mode
|
||||
result['changed'] |= update_monitor_properties(api, module, monitor,
|
||||
template_string_properties,
|
||||
template_integer_properties)
|
||||
|
||||
# we just have to update the ipport if monitor already exists and it's different
|
||||
if monitor_exists and cur_ipport != ipport:
|
||||
set_ipport(api, monitor, ipport)
|
||||
result['changed'] |= True
|
||||
# else: monitor doesn't exist (check mode) or ipport is already ok
|
||||
except Exception as e:
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
489
lib/ansible/modules/network/f5/bigip_monitor_tcp.py
Normal file
489
lib/ansible/modules/network/f5/bigip_monitor_tcp.py
Normal file
@@ -0,0 +1,489 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, serge van Ginderachter <serge@vanginderachter.be>
|
||||
#
|
||||
# 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: bigip_monitor_tcp
|
||||
short_description: "Manages F5 BIG-IP LTM tcp monitors"
|
||||
description:
|
||||
- "Manages F5 BIG-IP LTM tcp monitors via iControl SOAP API"
|
||||
version_added: "1.4"
|
||||
author:
|
||||
- Serge van Ginderachter (@srvg)
|
||||
- Tim Rupp (@caphrim007)
|
||||
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"
|
||||
- "Monitor API documentation: https://devcentral.f5.com/wiki/iControl.LocalLB__Monitor.ashx"
|
||||
requirements:
|
||||
- bigsuds
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Monitor state
|
||||
required: false
|
||||
default: 'present'
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
name:
|
||||
description:
|
||||
- Monitor name
|
||||
required: true
|
||||
default: null
|
||||
aliases:
|
||||
- monitor
|
||||
partition:
|
||||
description:
|
||||
- Partition for the monitor
|
||||
required: false
|
||||
default: 'Common'
|
||||
type:
|
||||
description:
|
||||
- The template type of this monitor template
|
||||
required: false
|
||||
default: 'tcp'
|
||||
choices:
|
||||
- TTYPE_TCP
|
||||
- TTYPE_TCP_ECHO
|
||||
- TTYPE_TCP_HALF_OPEN
|
||||
parent:
|
||||
description:
|
||||
- The parent template of this monitor template
|
||||
required: false
|
||||
default: 'tcp'
|
||||
choices:
|
||||
- tcp
|
||||
- tcp_echo
|
||||
- tcp_half_open
|
||||
parent_partition:
|
||||
description:
|
||||
- Partition for the parent monitor
|
||||
required: false
|
||||
default: 'Common'
|
||||
send:
|
||||
description:
|
||||
- The send string for the monitor call
|
||||
required: true
|
||||
default: none
|
||||
receive:
|
||||
description:
|
||||
- The receive string for the monitor call
|
||||
required: true
|
||||
default: none
|
||||
ip:
|
||||
description:
|
||||
- IP address part of the ipport definition. The default API setting
|
||||
is "0.0.0.0".
|
||||
required: false
|
||||
default: none
|
||||
port:
|
||||
description:
|
||||
- Port address part op the ipport definition. The default API
|
||||
setting is 0.
|
||||
required: false
|
||||
default: none
|
||||
interval:
|
||||
description:
|
||||
- The interval specifying how frequently the monitor instance
|
||||
of this template will run. By default, this interval is used for up and
|
||||
down states. The default API setting is 5.
|
||||
required: false
|
||||
default: none
|
||||
timeout:
|
||||
description:
|
||||
- The number of seconds in which the node or service must respond to
|
||||
the monitor request. If the target responds within the set time
|
||||
period, it is considered up. If the target does not respond within
|
||||
the set time period, it is considered down. You can change this
|
||||
number to any number you want, however, it should be 3 times the
|
||||
interval number of seconds plus 1 second. The default API setting
|
||||
is 16.
|
||||
required: false
|
||||
default: none
|
||||
time_until_up:
|
||||
description:
|
||||
- Specifies the amount of time in seconds after the first successful
|
||||
response before a node will be marked up. A value of 0 will cause a
|
||||
node to be marked up immediately after a valid response is received
|
||||
from the node. The default API setting is 0.
|
||||
required: false
|
||||
default: none
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create TCP Monitor
|
||||
bigip_monitor_tcp:
|
||||
state: "present"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my_tcp_monitor"
|
||||
type: "tcp"
|
||||
send: "tcp string to send"
|
||||
receive: "tcp string to receive"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create TCP half open Monitor
|
||||
bigip_monitor_tcp:
|
||||
state: "present"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my_tcp_monitor"
|
||||
type: "tcp"
|
||||
send: "tcp string to send"
|
||||
receive: "http string to receive"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Remove TCP Monitor
|
||||
bigip_monitor_tcp:
|
||||
state: "absent"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my_tcp_monitor"
|
||||
'''
|
||||
|
||||
TEMPLATE_TYPE = DEFAULT_TEMPLATE_TYPE = 'TTYPE_TCP'
|
||||
TEMPLATE_TYPE_CHOICES = ['tcp', 'tcp_echo', 'tcp_half_open']
|
||||
DEFAULT_PARENT = DEFAULT_TEMPLATE_TYPE_CHOICE = DEFAULT_TEMPLATE_TYPE.replace('TTYPE_', '').lower()
|
||||
|
||||
|
||||
def check_monitor_exists(module, api, monitor, parent):
|
||||
# hack to determine if monitor exists
|
||||
result = False
|
||||
try:
|
||||
ttype = api.LocalLB.Monitor.get_template_type(template_names=[monitor])[0]
|
||||
parent2 = api.LocalLB.Monitor.get_parent_template(template_names=[monitor])[0]
|
||||
if ttype == TEMPLATE_TYPE and parent == parent2:
|
||||
result = True
|
||||
else:
|
||||
module.fail_json(msg='Monitor already exists, but has a different type (%s) or parent(%s)' % (ttype, parent))
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def create_monitor(api, monitor, template_attributes):
|
||||
try:
|
||||
api.LocalLB.Monitor.create_template(
|
||||
templates=[{
|
||||
'template_name': monitor,
|
||||
'template_type': TEMPLATE_TYPE
|
||||
}],
|
||||
template_attributes=[template_attributes]
|
||||
)
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "already exists" in str(e):
|
||||
return False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def delete_monitor(api, monitor):
|
||||
try:
|
||||
api.LocalLB.Monitor.delete_template(template_names=[monitor])
|
||||
except bigsuds.OperationFailed as e:
|
||||
# maybe it was deleted since we checked
|
||||
if "was not found" in str(e):
|
||||
return False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def check_string_property(api, monitor, str_property):
|
||||
try:
|
||||
template_prop = api.LocalLB.Monitor.get_template_string_property(
|
||||
[monitor], [str_property['type']]
|
||||
)[0]
|
||||
return str_property == template_prop
|
||||
except bigsuds.OperationFailed as e:
|
||||
# happens in check mode if not created yet
|
||||
if "was not found" in str(e):
|
||||
return True
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
|
||||
|
||||
def set_string_property(api, monitor, str_property):
|
||||
api.LocalLB.Monitor.set_template_string_property(
|
||||
template_names=[monitor],
|
||||
values=[str_property]
|
||||
)
|
||||
|
||||
|
||||
def check_integer_property(api, monitor, int_property):
|
||||
try:
|
||||
return int_property == api.LocalLB.Monitor.get_template_integer_property(
|
||||
[monitor], [int_property['type']]
|
||||
)[0]
|
||||
except bigsuds.OperationFailed as e:
|
||||
# happens in check mode if not created yet
|
||||
if "was not found" in str(e):
|
||||
return True
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
|
||||
|
||||
def set_integer_property(api, monitor, int_property):
|
||||
api.LocalLB.Monitor.set_template_integer_property(
|
||||
template_names=[monitor],
|
||||
values=[int_property]
|
||||
)
|
||||
|
||||
|
||||
def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties):
|
||||
changed = False
|
||||
for str_property in template_string_properties:
|
||||
if str_property['value'] is not None and not check_string_property(api, monitor, str_property):
|
||||
if not module.check_mode:
|
||||
set_string_property(api, monitor, str_property)
|
||||
changed = True
|
||||
|
||||
for int_property in template_integer_properties:
|
||||
if int_property['value'] is not None and not check_integer_property(api, monitor, int_property):
|
||||
if not module.check_mode:
|
||||
set_integer_property(api, monitor, int_property)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def get_ipport(api, monitor):
|
||||
return api.LocalLB.Monitor.get_template_destination(template_names=[monitor])[0]
|
||||
|
||||
|
||||
def set_ipport(api, monitor, ipport):
|
||||
try:
|
||||
api.LocalLB.Monitor.set_template_destination(
|
||||
template_names=[monitor], destinations=[ipport]
|
||||
)
|
||||
return True, ""
|
||||
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "Cannot modify the address type of monitor" in str(e):
|
||||
return False, "Cannot modify the address type of monitor if already assigned to a pool."
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
name=dict(required=True),
|
||||
type=dict(default=DEFAULT_TEMPLATE_TYPE_CHOICE, choices=TEMPLATE_TYPE_CHOICES),
|
||||
parent=dict(default=DEFAULT_PARENT),
|
||||
parent_partition=dict(default='Common'),
|
||||
send=dict(required=False),
|
||||
receive=dict(required=False),
|
||||
ip=dict(required=False),
|
||||
port=dict(required=False, type='int'),
|
||||
interval=dict(required=False, type='int'),
|
||||
timeout=dict(required=False, type='int'),
|
||||
time_until_up=dict(required=False, type='int', default=0)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if module.params['validate_certs']:
|
||||
import ssl
|
||||
if not hasattr(ssl, 'SSLContext'):
|
||||
module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task')
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
state = module.params['state']
|
||||
partition = module.params['partition']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
parent_partition = module.params['parent_partition']
|
||||
name = module.params['name']
|
||||
type = 'TTYPE_' + module.params['type'].upper()
|
||||
parent = fq_name(parent_partition, module.params['parent'])
|
||||
monitor = fq_name(partition, name)
|
||||
send = module.params['send']
|
||||
receive = module.params['receive']
|
||||
ip = module.params['ip']
|
||||
port = module.params['port']
|
||||
interval = module.params['interval']
|
||||
timeout = module.params['timeout']
|
||||
time_until_up = module.params['time_until_up']
|
||||
|
||||
# tcp monitor has multiple types, so overrule
|
||||
global TEMPLATE_TYPE
|
||||
TEMPLATE_TYPE = type
|
||||
|
||||
# end monitor specific stuff
|
||||
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
monitor_exists = check_monitor_exists(module, api, monitor, parent)
|
||||
|
||||
# ipport is a special setting
|
||||
if monitor_exists:
|
||||
# make sure to not update current settings if not asked
|
||||
cur_ipport = get_ipport(api, monitor)
|
||||
if ip is None:
|
||||
ip = cur_ipport['ipport']['address']
|
||||
if port is None:
|
||||
port = cur_ipport['ipport']['port']
|
||||
else:
|
||||
# use API defaults if not defined to create it
|
||||
if interval is None:
|
||||
interval = 5
|
||||
if timeout is None:
|
||||
timeout = 16
|
||||
if ip is None:
|
||||
ip = '0.0.0.0'
|
||||
if port is None:
|
||||
port = 0
|
||||
if send is None:
|
||||
send = ''
|
||||
if receive is None:
|
||||
receive = ''
|
||||
|
||||
# define and set address type
|
||||
if ip == '0.0.0.0' and port == 0:
|
||||
address_type = 'ATYPE_STAR_ADDRESS_STAR_PORT'
|
||||
elif ip == '0.0.0.0' and port != 0:
|
||||
address_type = 'ATYPE_STAR_ADDRESS_EXPLICIT_PORT'
|
||||
elif ip != '0.0.0.0' and port != 0:
|
||||
address_type = 'ATYPE_EXPLICIT_ADDRESS_EXPLICIT_PORT'
|
||||
else:
|
||||
address_type = 'ATYPE_UNSET'
|
||||
|
||||
ipport = {
|
||||
'address_type': address_type,
|
||||
'ipport': {
|
||||
'address': ip,
|
||||
'port': port
|
||||
}
|
||||
}
|
||||
|
||||
template_attributes = {
|
||||
'parent_template': parent,
|
||||
'interval': interval,
|
||||
'timeout': timeout,
|
||||
'dest_ipport': ipport,
|
||||
'is_read_only': False,
|
||||
'is_directly_usable': True
|
||||
}
|
||||
|
||||
# monitor specific stuff
|
||||
if type == 'TTYPE_TCP':
|
||||
template_string_properties = [
|
||||
{
|
||||
'type': 'STYPE_SEND',
|
||||
'value': send
|
||||
},
|
||||
{
|
||||
'type': 'STYPE_RECEIVE',
|
||||
'value': receive
|
||||
}
|
||||
]
|
||||
else:
|
||||
template_string_properties = []
|
||||
|
||||
template_integer_properties = [
|
||||
{
|
||||
'type': 'ITYPE_INTERVAL',
|
||||
'value': interval
|
||||
},
|
||||
{
|
||||
'type': 'ITYPE_TIMEOUT',
|
||||
'value': timeout
|
||||
},
|
||||
{
|
||||
'type': 'ITYPE_TIME_UNTIL_UP',
|
||||
'value': time_until_up
|
||||
}
|
||||
]
|
||||
|
||||
# main logic, monitor generic
|
||||
|
||||
try:
|
||||
result = {'changed': False} # default
|
||||
|
||||
if state == 'absent':
|
||||
if monitor_exists:
|
||||
if not module.check_mode:
|
||||
# possible race condition if same task
|
||||
# on other node deleted it first
|
||||
result['changed'] |= delete_monitor(api, monitor)
|
||||
else:
|
||||
result['changed'] |= True
|
||||
else:
|
||||
# check for monitor itself
|
||||
if not monitor_exists:
|
||||
if not module.check_mode:
|
||||
# again, check changed status here b/c race conditions
|
||||
# if other task already created it
|
||||
result['changed'] |= create_monitor(api, monitor, template_attributes)
|
||||
else:
|
||||
result['changed'] |= True
|
||||
|
||||
# check for monitor parameters
|
||||
# whether it already existed, or was just created, now update
|
||||
# the update functions need to check for check mode but
|
||||
# cannot update settings if it doesn't exist which happens in check mode
|
||||
result['changed'] |= update_monitor_properties(api, module, monitor,
|
||||
template_string_properties,
|
||||
template_integer_properties)
|
||||
|
||||
# we just have to update the ipport if monitor already exists and it's different
|
||||
if monitor_exists and cur_ipport != ipport:
|
||||
set_ipport(api, monitor, ipport)
|
||||
result['changed'] |= True
|
||||
# else: monitor doesn't exist (check mode) or ipport is already ok
|
||||
except Exception as e:
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
467
lib/ansible/modules/network/f5/bigip_node.py
Normal file
467
lib/ansible/modules/network/f5/bigip_node.py
Normal file
@@ -0,0 +1,467 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: bigip_node
|
||||
short_description: "Manages F5 BIG-IP LTM nodes"
|
||||
description:
|
||||
- "Manages F5 BIG-IP LTM nodes via iControl SOAP API"
|
||||
version_added: "1.4"
|
||||
author:
|
||||
- Matt Hite (@mhite)
|
||||
- Tim Rupp (@caphrim007)
|
||||
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:
|
||||
state:
|
||||
description:
|
||||
- Pool member state
|
||||
required: true
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
aliases: []
|
||||
session_state:
|
||||
description:
|
||||
- Set new session availability status for node
|
||||
version_added: "1.9"
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enabled', 'disabled']
|
||||
aliases: []
|
||||
monitor_state:
|
||||
description:
|
||||
- Set monitor availability status for node
|
||||
version_added: "1.9"
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enabled', 'disabled']
|
||||
aliases: []
|
||||
partition:
|
||||
description:
|
||||
- Partition
|
||||
required: false
|
||||
default: 'Common'
|
||||
choices: []
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- "Node name"
|
||||
required: false
|
||||
default: null
|
||||
choices: []
|
||||
monitor_type:
|
||||
description:
|
||||
- Monitor rule type when monitors > 1
|
||||
version_added: "2.2"
|
||||
required: False
|
||||
default: null
|
||||
choices: ['and_list', 'm_of_n']
|
||||
aliases: []
|
||||
quorum:
|
||||
description:
|
||||
- Monitor quorum value when monitor_type is m_of_n
|
||||
version_added: "2.2"
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases: []
|
||||
monitors:
|
||||
description:
|
||||
- Monitor template name list. Always use the full path to the monitor.
|
||||
version_added: "2.2"
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases: []
|
||||
host:
|
||||
description:
|
||||
- "Node IP. Required when state=present and node does not exist. Error when state=absent."
|
||||
required: true
|
||||
default: null
|
||||
choices: []
|
||||
aliases: ['address', 'ip']
|
||||
description:
|
||||
description:
|
||||
- "Node description."
|
||||
required: false
|
||||
default: null
|
||||
choices: []
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add node
|
||||
bigip_node:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
partition: "Common"
|
||||
host: "10.20.30.40"
|
||||
name: "10.20.30.40"
|
||||
|
||||
# Note that the BIG-IP automatically names the node using the
|
||||
# IP address specified in previous play's host parameter.
|
||||
# Future plays referencing this node no longer use the host
|
||||
# parameter but instead use the name parameter.
|
||||
# Alternatively, you could have specified a name with the
|
||||
# name parameter when state=present.
|
||||
|
||||
- name: Add node with a single 'ping' monitor
|
||||
bigip_node:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
partition: "Common"
|
||||
host: "10.20.30.40"
|
||||
name: "mytestserver"
|
||||
monitors:
|
||||
- /Common/icmp
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Modify node description
|
||||
bigip_node:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
partition: "Common"
|
||||
name: "10.20.30.40"
|
||||
description: "Our best server yet"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete node
|
||||
bigip_node:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "absent"
|
||||
partition: "Common"
|
||||
name: "10.20.30.40"
|
||||
|
||||
# The BIG-IP GUI doesn't map directly to the API calls for "Node ->
|
||||
# General Properties -> State". The following states map to API monitor
|
||||
# and session states.
|
||||
#
|
||||
# Enabled (all traffic allowed):
|
||||
# monitor_state=enabled, session_state=enabled
|
||||
# Disabled (only persistent or active connections allowed):
|
||||
# monitor_state=enabled, session_state=disabled
|
||||
# Forced offline (only active connections allowed):
|
||||
# monitor_state=disabled, session_state=disabled
|
||||
#
|
||||
# See https://devcentral.f5.com/questions/icontrol-equivalent-call-for-b-node-down
|
||||
|
||||
- name: Force node offline
|
||||
bigip_node:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "mysecret"
|
||||
state: "present"
|
||||
session_state: "disabled"
|
||||
monitor_state: "disabled"
|
||||
partition: "Common"
|
||||
name: "10.20.30.40"
|
||||
'''
|
||||
|
||||
|
||||
def node_exists(api, address):
|
||||
# hack to determine if node exists
|
||||
result = False
|
||||
try:
|
||||
api.LocalLB.NodeAddressV2.get_object_status(nodes=[address])
|
||||
result = True
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def create_node_address(api, address, name):
|
||||
try:
|
||||
api.LocalLB.NodeAddressV2.create(
|
||||
nodes=[name],
|
||||
addresses=[address],
|
||||
limits=[0]
|
||||
)
|
||||
result = True
|
||||
desc = ""
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "already exists" in str(e):
|
||||
result = False
|
||||
desc = "referenced name or IP already in use"
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return (result, desc)
|
||||
|
||||
|
||||
def get_node_address(api, name):
|
||||
return api.LocalLB.NodeAddressV2.get_address(nodes=[name])[0]
|
||||
|
||||
|
||||
def delete_node_address(api, address):
|
||||
try:
|
||||
api.LocalLB.NodeAddressV2.delete_node_address(nodes=[address])
|
||||
result = True
|
||||
desc = ""
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "is referenced by a member of pool" in str(e):
|
||||
result = False
|
||||
desc = "node referenced by pool"
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return (result, desc)
|
||||
|
||||
|
||||
def set_node_description(api, name, description):
|
||||
api.LocalLB.NodeAddressV2.set_description(nodes=[name],
|
||||
descriptions=[description])
|
||||
|
||||
|
||||
def get_node_description(api, name):
|
||||
return api.LocalLB.NodeAddressV2.get_description(nodes=[name])[0]
|
||||
|
||||
|
||||
def set_node_session_enabled_state(api, name, session_state):
|
||||
session_state = "STATE_%s" % session_state.strip().upper()
|
||||
api.LocalLB.NodeAddressV2.set_session_enabled_state(nodes=[name],
|
||||
states=[session_state])
|
||||
|
||||
|
||||
def get_node_session_status(api, name):
|
||||
result = api.LocalLB.NodeAddressV2.get_session_status(nodes=[name])[0]
|
||||
result = result.split("SESSION_STATUS_")[-1].lower()
|
||||
return result
|
||||
|
||||
|
||||
def set_node_monitor_state(api, name, monitor_state):
|
||||
monitor_state = "STATE_%s" % monitor_state.strip().upper()
|
||||
api.LocalLB.NodeAddressV2.set_monitor_state(nodes=[name],
|
||||
states=[monitor_state])
|
||||
|
||||
|
||||
def get_node_monitor_status(api, name):
|
||||
result = api.LocalLB.NodeAddressV2.get_monitor_status(nodes=[name])[0]
|
||||
result = result.split("MONITOR_STATUS_")[-1].lower()
|
||||
return result
|
||||
|
||||
|
||||
def get_monitors(api, name):
|
||||
result = api.LocalLB.NodeAddressV2.get_monitor_rule(nodes=[name])[0]
|
||||
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, name, 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}
|
||||
api.LocalLB.NodeAddressV2.set_monitor_rule(nodes=[name],
|
||||
monitor_rules=[monitor_rule])
|
||||
|
||||
|
||||
def main():
|
||||
monitor_type_choices = ['and_list', 'm_of_n']
|
||||
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
session_state=dict(type='str', choices=['enabled', 'disabled']),
|
||||
monitor_state=dict(type='str', choices=['enabled', 'disabled']),
|
||||
name=dict(type='str', required=True),
|
||||
host=dict(type='str', aliases=['address', 'ip']),
|
||||
description=dict(type='str'),
|
||||
monitor_type=dict(type='str', choices=monitor_type_choices),
|
||||
quorum=dict(type='int'),
|
||||
monitors=dict(type='list')
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if module.params['validate_certs']:
|
||||
import ssl
|
||||
if not hasattr(ssl, 'SSLContext'):
|
||||
module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task')
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
state = module.params['state']
|
||||
partition = module.params['partition']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
session_state = module.params['session_state']
|
||||
monitor_state = module.params['monitor_state']
|
||||
host = module.params['host']
|
||||
name = module.params['name']
|
||||
address = fq_name(partition, name)
|
||||
description = module.params['description']
|
||||
monitor_type = module.params['monitor_type']
|
||||
if monitor_type:
|
||||
monitor_type = monitor_type.lower()
|
||||
quorum = module.params['quorum']
|
||||
monitors = module.params['monitors']
|
||||
if monitors:
|
||||
monitors = []
|
||||
for monitor in module.params['monitors']:
|
||||
monitors.append(fq_name(partition, monitor))
|
||||
|
||||
# sanity check user supplied values
|
||||
if state == 'absent' and host is not None:
|
||||
module.fail_json(msg="host parameter invalid when state=absent")
|
||||
|
||||
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")
|
||||
|
||||
try:
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
result = {'changed': False} # default
|
||||
|
||||
if state == 'absent':
|
||||
if node_exists(api, address):
|
||||
if not module.check_mode:
|
||||
deleted, desc = delete_node_address(api, address)
|
||||
if not deleted:
|
||||
module.fail_json(msg="unable to delete: %s" % desc)
|
||||
else:
|
||||
result = {'changed': True}
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
|
||||
elif state == 'present':
|
||||
if not node_exists(api, address):
|
||||
if host is None:
|
||||
module.fail_json(msg="host parameter required when "
|
||||
"state=present and node does not exist")
|
||||
if not module.check_mode:
|
||||
created, desc = create_node_address(api, address=host, name=address)
|
||||
if not created:
|
||||
module.fail_json(msg="unable to create: %s" % desc)
|
||||
else:
|
||||
result = {'changed': True}
|
||||
if session_state is not None:
|
||||
set_node_session_enabled_state(api, address,
|
||||
session_state)
|
||||
result = {'changed': True}
|
||||
if monitor_state is not None:
|
||||
set_node_monitor_state(api, address, monitor_state)
|
||||
result = {'changed': True}
|
||||
if description is not None:
|
||||
set_node_description(api, address, description)
|
||||
result = {'changed': True}
|
||||
if monitors:
|
||||
set_monitors(api, address, monitor_type, quorum, monitors)
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
else:
|
||||
# node exists -- potentially modify attributes
|
||||
if host is not None:
|
||||
if get_node_address(api, address) != host:
|
||||
module.fail_json(msg="Changing the node address is "
|
||||
"not supported by the API; "
|
||||
"delete and recreate the node.")
|
||||
if session_state is not None:
|
||||
session_status = get_node_session_status(api, address)
|
||||
if session_state == 'enabled' and \
|
||||
session_status == 'forced_disabled':
|
||||
if not module.check_mode:
|
||||
set_node_session_enabled_state(api, address,
|
||||
session_state)
|
||||
result = {'changed': True}
|
||||
elif session_state == 'disabled' and \
|
||||
session_status != 'force_disabled':
|
||||
if not module.check_mode:
|
||||
set_node_session_enabled_state(api, address,
|
||||
session_state)
|
||||
result = {'changed': True}
|
||||
if monitor_state is not None:
|
||||
monitor_status = get_node_monitor_status(api, address)
|
||||
if monitor_state == 'enabled' and \
|
||||
monitor_status == 'forced_down':
|
||||
if not module.check_mode:
|
||||
set_node_monitor_state(api, address,
|
||||
monitor_state)
|
||||
result = {'changed': True}
|
||||
elif monitor_state == 'disabled' and \
|
||||
monitor_status != 'forced_down':
|
||||
if not module.check_mode:
|
||||
set_node_monitor_state(api, address,
|
||||
monitor_state)
|
||||
result = {'changed': True}
|
||||
if description is not None:
|
||||
if get_node_description(api, address) != description:
|
||||
if not module.check_mode:
|
||||
set_node_description(api, address, description)
|
||||
result = {'changed': True}
|
||||
if monitors:
|
||||
t_monitor_type, t_quorum, t_monitor_templates = get_monitors(api, address)
|
||||
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, address, monitor_type, quorum, monitors)
|
||||
result = {'changed': True}
|
||||
except Exception as e:
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
565
lib/ansible/modules/network/f5/bigip_pool.py
Normal file
565
lib/ansible/modules/network/f5/bigip_pool.py
Normal file
@@ -0,0 +1,565 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: bigip_pool
|
||||
short_description: "Manages F5 BIG-IP LTM pools"
|
||||
description:
|
||||
- Manages F5 BIG-IP LTM pools via iControl SOAP API
|
||||
version_added: 1.2
|
||||
author:
|
||||
- Matt Hite (@mhite)
|
||||
- Tim Rupp (@caphrim007)
|
||||
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:
|
||||
state:
|
||||
description:
|
||||
- Pool/pool member state
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- Pool name
|
||||
required: true
|
||||
default: null
|
||||
choices: []
|
||||
aliases:
|
||||
- pool
|
||||
partition:
|
||||
description:
|
||||
- Partition of pool/pool member
|
||||
required: false
|
||||
default: 'Common'
|
||||
choices: []
|
||||
aliases: []
|
||||
lb_method:
|
||||
description:
|
||||
- Load balancing method
|
||||
version_added: "1.3"
|
||||
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
|
||||
- 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
|
||||
version_added: "1.3"
|
||||
required: False
|
||||
default: null
|
||||
choices: ['and_list', 'm_of_n']
|
||||
aliases: []
|
||||
quorum:
|
||||
description:
|
||||
- Monitor quorum value when monitor_type is m_of_n
|
||||
version_added: "1.3"
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases: []
|
||||
monitors:
|
||||
description:
|
||||
- Monitor template name list. Always use the full path to the monitor.
|
||||
version_added: "1.3"
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases: []
|
||||
slow_ramp_time:
|
||||
description:
|
||||
- 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"
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases: []
|
||||
reselect_tries:
|
||||
description:
|
||||
- Sets the number of times the system tries to contact a pool member
|
||||
after a passive failure
|
||||
version_added: "2.2"
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases: []
|
||||
service_down_action:
|
||||
description:
|
||||
- Sets the action to take when node goes down in pool
|
||||
version_added: "1.3"
|
||||
required: False
|
||||
default: null
|
||||
choices:
|
||||
- none
|
||||
- reset
|
||||
- drop
|
||||
- reselect
|
||||
aliases: []
|
||||
host:
|
||||
description:
|
||||
- "Pool member IP"
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases:
|
||||
- address
|
||||
port:
|
||||
description:
|
||||
- Pool member port
|
||||
required: False
|
||||
default: null
|
||||
choices: []
|
||||
aliases: []
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create pool
|
||||
bigip_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
name: "my-pool"
|
||||
partition: "Common"
|
||||
lb_method: "least_connection_member"
|
||||
slow_ramp_time: 120
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Modify load balancer method
|
||||
bigip_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
name: "my-pool"
|
||||
partition: "Common"
|
||||
lb_method: "round_robin"
|
||||
|
||||
- name: Add pool member
|
||||
bigip_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
name: "my-pool"
|
||||
partition: "Common"
|
||||
host: "{{ ansible_default_ipv4["address"] }}"
|
||||
port: 80
|
||||
|
||||
- name: Remove pool member from pool
|
||||
bigip_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "absent"
|
||||
name: "my-pool"
|
||||
partition: "Common"
|
||||
host: "{{ ansible_default_ipv4["address"] }}"
|
||||
port: 80
|
||||
|
||||
- name: Delete pool
|
||||
bigip_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "absent"
|
||||
name: "my-pool"
|
||||
partition: "Common"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
|
||||
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 as e:
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def create_pool(api, pool, lb_method):
|
||||
# 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'
|
||||
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_reselect_tries(api, pool):
|
||||
result = api.LocalLB.Pool.get_reselect_tries(pool_names=[pool])[0]
|
||||
return result
|
||||
|
||||
|
||||
def set_reselect_tries(api, pool, tries):
|
||||
api.LocalLB.Pool.set_reselect_tries(pool_names=[pool], values=[tries])
|
||||
|
||||
|
||||
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])
|
||||
|
||||
|
||||
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 as 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 as 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():
|
||||
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',
|
||||
'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']
|
||||
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
name=dict(type='str', required=True, aliases=['pool']),
|
||||
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'),
|
||||
reselect_tries=dict(type='int'),
|
||||
service_down_action=dict(type='str', choices=service_down_choices),
|
||||
host=dict(type='str', aliases=['address']),
|
||||
port=dict(type='int')
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if not bigsuds_found:
|
||||
module.fail_json(msg="the python bigsuds module is required")
|
||||
|
||||
if module.params['validate_certs']:
|
||||
import ssl
|
||||
if not hasattr(ssl, 'SSLContext'):
|
||||
module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task')
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
state = module.params['state']
|
||||
partition = module.params['partition']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
name = module.params['name']
|
||||
pool = fq_name(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']
|
||||
if monitors:
|
||||
monitors = []
|
||||
for monitor in module.params['monitors']:
|
||||
monitors.append(fq_name(partition, monitor))
|
||||
slow_ramp_time = module.params['slow_ramp_time']
|
||||
reselect_tries = module.params['reselect_tries']
|
||||
service_down_action = module.params['service_down_action']
|
||||
if service_down_action:
|
||||
service_down_action = service_down_action.lower()
|
||||
host = module.params['host']
|
||||
address = fq_name(partition, host)
|
||||
port = module.params['port']
|
||||
|
||||
# sanity check user supplied values
|
||||
|
||||
if (host and port is None) or (port is not None and not host):
|
||||
module.fail_json(msg="both host and port must be supplied")
|
||||
|
||||
if port is not None and (0 > port or port > 65535):
|
||||
module.fail_json(msg="valid ports must be in range 0 - 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")
|
||||
|
||||
try:
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
result = {'changed': False} # default
|
||||
|
||||
if state == 'absent':
|
||||
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:
|
||||
# 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 as e:
|
||||
if "was not found" in str(e):
|
||||
result = {'changed': False}
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
|
||||
elif state == 'present':
|
||||
update = False
|
||||
if not pool_exists(api, pool):
|
||||
# pool does not exist -- need to create it
|
||||
if not module.check_mode:
|
||||
# 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 as 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 reselect_tries:
|
||||
set_reselect_tries(api, pool, reselect_tries)
|
||||
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}
|
||||
else:
|
||||
# pool exists -- potentially modify attributes
|
||||
update = True
|
||||
|
||||
if update:
|
||||
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 reselect_tries and reselect_tries != get_reselect_tries(api, pool):
|
||||
if not module.check_mode:
|
||||
set_reselect_tries(api, pool, reselect_tries)
|
||||
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}
|
||||
if (host and port == 0) and not member_exists(api, pool, address, port):
|
||||
if not module.check_mode:
|
||||
add_pool_member(api, pool, address, port)
|
||||
result = {'changed': True}
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
509
lib/ansible/modules/network/f5/bigip_pool_member.py
Normal file
509
lib/ansible/modules/network/f5/bigip_pool_member.py
Normal file
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/python
|
||||
# -*- 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: bigip_pool_member
|
||||
short_description: Manages F5 BIG-IP LTM pool members
|
||||
description:
|
||||
- Manages F5 BIG-IP LTM pool members via iControl SOAP API
|
||||
version_added: 1.4
|
||||
author:
|
||||
- Matt Hite (@mhite)
|
||||
- Tim Rupp (@caphrim007)
|
||||
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
|
||||
- Supersedes bigip_pool for managing pool members
|
||||
requirements:
|
||||
- bigsuds
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Pool member state
|
||||
required: true
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
session_state:
|
||||
description:
|
||||
- Set new session availability status for pool member
|
||||
version_added: 2.0
|
||||
required: false
|
||||
default: null
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
monitor_state:
|
||||
description:
|
||||
- Set monitor availability status for pool member
|
||||
version_added: 2.0
|
||||
required: false
|
||||
default: null
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
pool:
|
||||
description:
|
||||
- Pool name. This pool must exist.
|
||||
required: true
|
||||
partition:
|
||||
description:
|
||||
- Partition
|
||||
required: false
|
||||
default: 'Common'
|
||||
host:
|
||||
description:
|
||||
- Pool member IP
|
||||
required: true
|
||||
aliases:
|
||||
- address
|
||||
- name
|
||||
port:
|
||||
description:
|
||||
- Pool member port
|
||||
required: true
|
||||
connection_limit:
|
||||
description:
|
||||
- Pool member connection limit. Setting this to 0 disables the limit.
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
description:
|
||||
- Pool member description
|
||||
required: false
|
||||
default: null
|
||||
rate_limit:
|
||||
description:
|
||||
- Pool member rate limit (connections-per-second). Setting this to 0
|
||||
disables the limit.
|
||||
required: false
|
||||
default: null
|
||||
ratio:
|
||||
description:
|
||||
- Pool member ratio weight. Valid values range from 1 through 100.
|
||||
New pool members -- unless overriden with this value -- default
|
||||
to 1.
|
||||
required: false
|
||||
default: null
|
||||
preserve_node:
|
||||
description:
|
||||
- When state is absent and the pool member is no longer referenced
|
||||
in other pools, the default behavior removes the unused node
|
||||
o bject. Setting this to 'yes' disables this behavior.
|
||||
required: false
|
||||
default: 'no'
|
||||
choices:
|
||||
- yes
|
||||
- no
|
||||
version_added: 2.1
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add pool member
|
||||
bigip_pool_member:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
pool: "my-pool"
|
||||
partition: "Common"
|
||||
host: "{{ ansible_default_ipv4["address"] }}"
|
||||
port: 80
|
||||
description: "web server"
|
||||
connection_limit: 100
|
||||
rate_limit: 50
|
||||
ratio: 2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Modify pool member ratio and description
|
||||
bigip_pool_member:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
pool: "my-pool"
|
||||
partition: "Common"
|
||||
host: "{{ ansible_default_ipv4["address"] }}"
|
||||
port: 80
|
||||
ratio: 1
|
||||
description: "nginx server"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Remove pool member from pool
|
||||
bigip_pool_member:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "absent"
|
||||
pool: "my-pool"
|
||||
partition: "Common"
|
||||
host: "{{ ansible_default_ipv4["address"] }}"
|
||||
port: 80
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
# The BIG-IP GUI doesn't map directly to the API calls for "Pool ->
|
||||
# Members -> State". The following states map to API monitor
|
||||
# and session states.
|
||||
#
|
||||
# Enabled (all traffic allowed):
|
||||
# monitor_state=enabled, session_state=enabled
|
||||
# Disabled (only persistent or active connections allowed):
|
||||
# monitor_state=enabled, session_state=disabled
|
||||
# Forced offline (only active connections allowed):
|
||||
# monitor_state=disabled, session_state=disabled
|
||||
#
|
||||
# See https://devcentral.f5.com/questions/icontrol-equivalent-call-for-b-node-down
|
||||
|
||||
- name: Force pool member offline
|
||||
bigip_pool_member:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
session_state: "disabled"
|
||||
monitor_state: "disabled"
|
||||
pool: "my-pool"
|
||||
partition: "Common"
|
||||
host: "{{ ansible_default_ipv4["address"] }}"
|
||||
port: 80
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
|
||||
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 as e:
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
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 as 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 as 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 get_connection_limit(api, pool, address, port):
|
||||
members = [{'address': address, 'port': port}]
|
||||
result = api.LocalLB.Pool.get_member_connection_limit(
|
||||
pool_names=[pool],
|
||||
members=[members]
|
||||
)[0][0]
|
||||
return result
|
||||
|
||||
|
||||
def set_connection_limit(api, pool, address, port, limit):
|
||||
members = [{'address': address, 'port': port}]
|
||||
api.LocalLB.Pool.set_member_connection_limit(
|
||||
pool_names=[pool],
|
||||
members=[members],
|
||||
limits=[[limit]]
|
||||
)
|
||||
|
||||
|
||||
def get_description(api, pool, address, port):
|
||||
members = [{'address': address, 'port': port}]
|
||||
result = api.LocalLB.Pool.get_member_description(
|
||||
pool_names=[pool],
|
||||
members=[members]
|
||||
)[0][0]
|
||||
return result
|
||||
|
||||
|
||||
def set_description(api, pool, address, port, description):
|
||||
members = [{'address': address, 'port': port}]
|
||||
api.LocalLB.Pool.set_member_description(
|
||||
pool_names=[pool],
|
||||
members=[members],
|
||||
descriptions=[[description]]
|
||||
)
|
||||
|
||||
|
||||
def get_rate_limit(api, pool, address, port):
|
||||
members = [{'address': address, 'port': port}]
|
||||
result = api.LocalLB.Pool.get_member_rate_limit(
|
||||
pool_names=[pool],
|
||||
members=[members]
|
||||
)[0][0]
|
||||
return result
|
||||
|
||||
|
||||
def set_rate_limit(api, pool, address, port, limit):
|
||||
members = [{'address': address, 'port': port}]
|
||||
api.LocalLB.Pool.set_member_rate_limit(
|
||||
pool_names=[pool],
|
||||
members=[members],
|
||||
limits=[[limit]]
|
||||
)
|
||||
|
||||
|
||||
def get_ratio(api, pool, address, port):
|
||||
members = [{'address': address, 'port': port}]
|
||||
result = api.LocalLB.Pool.get_member_ratio(
|
||||
pool_names=[pool],
|
||||
members=[members]
|
||||
)[0][0]
|
||||
return result
|
||||
|
||||
|
||||
def set_ratio(api, pool, address, port, ratio):
|
||||
members = [{'address': address, 'port': port}]
|
||||
api.LocalLB.Pool.set_member_ratio(
|
||||
pool_names=[pool],
|
||||
members=[members],
|
||||
ratios=[[ratio]]
|
||||
)
|
||||
|
||||
|
||||
def set_member_session_enabled_state(api, pool, address, port, session_state):
|
||||
members = [{'address': address, 'port': port}]
|
||||
session_state = ["STATE_%s" % session_state.strip().upper()]
|
||||
api.LocalLB.Pool.set_member_session_enabled_state(
|
||||
pool_names=[pool],
|
||||
members=[members],
|
||||
session_states=[session_state]
|
||||
)
|
||||
|
||||
|
||||
def get_member_session_status(api, pool, address, port):
|
||||
members = [{'address': address, 'port': port}]
|
||||
result = api.LocalLB.Pool.get_member_session_status(
|
||||
pool_names=[pool],
|
||||
members=[members]
|
||||
)[0][0]
|
||||
result = result.split("SESSION_STATUS_")[-1].lower()
|
||||
return result
|
||||
|
||||
|
||||
def set_member_monitor_state(api, pool, address, port, monitor_state):
|
||||
members = [{'address': address, 'port': port}]
|
||||
monitor_state = ["STATE_%s" % monitor_state.strip().upper()]
|
||||
api.LocalLB.Pool.set_member_monitor_state(
|
||||
pool_names=[pool],
|
||||
members=[members],
|
||||
monitor_states=[monitor_state]
|
||||
)
|
||||
|
||||
|
||||
def get_member_monitor_status(api, pool, address, port):
|
||||
members = [{'address': address, 'port': port}]
|
||||
result = api.LocalLB.Pool.get_member_monitor_status(
|
||||
pool_names=[pool],
|
||||
members=[members]
|
||||
)[0][0]
|
||||
result = result.split("MONITOR_STATUS_")[-1].lower()
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
session_state=dict(type='str', choices=['enabled', 'disabled']),
|
||||
monitor_state=dict(type='str', choices=['enabled', 'disabled']),
|
||||
pool=dict(type='str', required=True),
|
||||
host=dict(type='str', required=True, aliases=['address', 'name']),
|
||||
port=dict(type='int', required=True),
|
||||
connection_limit=dict(type='int'),
|
||||
description=dict(type='str'),
|
||||
rate_limit=dict(type='int'),
|
||||
ratio=dict(type='int'),
|
||||
preserve_node=dict(type='bool', default=False)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if module.params['validate_certs']:
|
||||
import ssl
|
||||
if not hasattr(ssl, 'SSLContext'):
|
||||
module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task')
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
state = module.params['state']
|
||||
partition = module.params['partition']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
session_state = module.params['session_state']
|
||||
monitor_state = module.params['monitor_state']
|
||||
pool = fq_name(partition, module.params['pool'])
|
||||
connection_limit = module.params['connection_limit']
|
||||
description = module.params['description']
|
||||
rate_limit = module.params['rate_limit']
|
||||
ratio = module.params['ratio']
|
||||
host = module.params['host']
|
||||
address = fq_name(partition, host)
|
||||
port = module.params['port']
|
||||
preserve_node = module.params['preserve_node']
|
||||
|
||||
if (host and port is None) or (port is not None and not host):
|
||||
module.fail_json(msg="both host and port must be supplied")
|
||||
|
||||
if 0 > port or port > 65535:
|
||||
module.fail_json(msg="valid ports must be in range 0 - 65535")
|
||||
|
||||
try:
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
if not pool_exists(api, pool):
|
||||
module.fail_json(msg="pool %s does not exist" % pool)
|
||||
result = {'changed': False} # default
|
||||
|
||||
if state == 'absent':
|
||||
if member_exists(api, pool, address, port):
|
||||
if not module.check_mode:
|
||||
remove_pool_member(api, pool, address, port)
|
||||
if preserve_node:
|
||||
result = {'changed': True}
|
||||
else:
|
||||
deleted = delete_node_address(api, address)
|
||||
result = {'changed': True, 'deleted': deleted}
|
||||
else:
|
||||
result = {'changed': True}
|
||||
|
||||
elif state == 'present':
|
||||
if not member_exists(api, pool, address, port):
|
||||
if not module.check_mode:
|
||||
add_pool_member(api, pool, address, port)
|
||||
if connection_limit is not None:
|
||||
set_connection_limit(api, pool, address, port, connection_limit)
|
||||
if description is not None:
|
||||
set_description(api, pool, address, port, description)
|
||||
if rate_limit is not None:
|
||||
set_rate_limit(api, pool, address, port, rate_limit)
|
||||
if ratio is not None:
|
||||
set_ratio(api, pool, address, port, ratio)
|
||||
if session_state is not None:
|
||||
set_member_session_enabled_state(api, pool, address, port, session_state)
|
||||
if monitor_state is not None:
|
||||
set_member_monitor_state(api, pool, address, port, monitor_state)
|
||||
result = {'changed': True}
|
||||
else:
|
||||
# pool member exists -- potentially modify attributes
|
||||
if connection_limit is not None and connection_limit != get_connection_limit(api, pool, address, port):
|
||||
if not module.check_mode:
|
||||
set_connection_limit(api, pool, address, port, connection_limit)
|
||||
result = {'changed': True}
|
||||
if description is not None and description != get_description(api, pool, address, port):
|
||||
if not module.check_mode:
|
||||
set_description(api, pool, address, port, description)
|
||||
result = {'changed': True}
|
||||
if rate_limit is not None and rate_limit != get_rate_limit(api, pool, address, port):
|
||||
if not module.check_mode:
|
||||
set_rate_limit(api, pool, address, port, rate_limit)
|
||||
result = {'changed': True}
|
||||
if ratio is not None and ratio != get_ratio(api, pool, address, port):
|
||||
if not module.check_mode:
|
||||
set_ratio(api, pool, address, port, ratio)
|
||||
result = {'changed': True}
|
||||
if session_state is not None:
|
||||
session_status = get_member_session_status(api, pool, address, port)
|
||||
if session_state == 'enabled' and session_status == 'forced_disabled':
|
||||
if not module.check_mode:
|
||||
set_member_session_enabled_state(api, pool, address, port, session_state)
|
||||
result = {'changed': True}
|
||||
elif session_state == 'disabled' and session_status != 'forced_disabled':
|
||||
if not module.check_mode:
|
||||
set_member_session_enabled_state(api, pool, address, port, session_state)
|
||||
result = {'changed': True}
|
||||
if monitor_state is not None:
|
||||
monitor_status = get_member_monitor_status(api, pool, address, port)
|
||||
if monitor_state == 'enabled' and monitor_status == 'forced_down':
|
||||
if not module.check_mode:
|
||||
set_member_monitor_state(api, pool, address, port, monitor_state)
|
||||
result = {'changed': True}
|
||||
elif monitor_state == 'disabled' and monitor_status != 'forced_down':
|
||||
if not module.check_mode:
|
||||
set_member_monitor_state(api, pool, address, port, monitor_state)
|
||||
result = {'changed': True}
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
530
lib/ansible/modules/network/f5/bigip_routedomain.py
Normal file
530
lib/ansible/modules/network/f5/bigip_routedomain.py
Normal file
@@ -0,0 +1,530 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_routedomain
|
||||
short_description: Manage route domains on a BIG-IP
|
||||
description:
|
||||
- Manage route domains on a BIG-IP
|
||||
version_added: "2.2"
|
||||
options:
|
||||
bwc_policy:
|
||||
description:
|
||||
- The bandwidth controller for the route domain.
|
||||
connection_limit:
|
||||
description:
|
||||
- The maximum number of concurrent connections allowed for the
|
||||
route domain. Setting this to C(0) turns off connection limits.
|
||||
description:
|
||||
description:
|
||||
- Specifies descriptive text that identifies the route domain.
|
||||
flow_eviction_policy:
|
||||
description:
|
||||
- The eviction policy to use with this route domain. Apply an eviction
|
||||
policy to provide customized responses to flow overflows and slow
|
||||
flows on the route domain.
|
||||
id:
|
||||
description:
|
||||
- The unique identifying integer representing the route domain.
|
||||
required: true
|
||||
parent:
|
||||
description:
|
||||
Specifies the route domain the system searches when it cannot
|
||||
find a route in the configured domain.
|
||||
required: false
|
||||
routing_protocol:
|
||||
description:
|
||||
- Dynamic routing protocols for the system to use in the route domain.
|
||||
choices:
|
||||
- BFD
|
||||
- BGP
|
||||
- IS-IS
|
||||
- OSPFv2
|
||||
- OSPFv3
|
||||
- PIM
|
||||
- RIP
|
||||
- RIPng
|
||||
service_policy:
|
||||
description:
|
||||
- Service policy to associate with the route domain.
|
||||
state:
|
||||
description:
|
||||
- Whether the route domain should exist or not.
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
strict:
|
||||
description:
|
||||
- Specifies whether the system enforces cross-routing restrictions
|
||||
or not.
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
vlans:
|
||||
description:
|
||||
- VLANs for the system to use in the route domain
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as
|
||||
pip install f5-sdk.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create a route domain
|
||||
bigip_routedomain:
|
||||
id: "1234"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "present"
|
||||
user: "admin"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Set VLANs on the route domain
|
||||
bigip_routedomain:
|
||||
id: "1234"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "present"
|
||||
user: "admin"
|
||||
vlans:
|
||||
- net1
|
||||
- foo
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
id:
|
||||
description: The ID of the route domain that was changed
|
||||
returned: changed
|
||||
type: int
|
||||
sample: 2
|
||||
description:
|
||||
description: The description of the route domain
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "route domain foo"
|
||||
strict:
|
||||
description: The new strict isolation setting
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "enabled"
|
||||
parent:
|
||||
description: The new parent route domain
|
||||
returned: changed
|
||||
type: int
|
||||
sample: 0
|
||||
vlans:
|
||||
description: List of new VLANs the route domain is applied to
|
||||
returned: changed
|
||||
type: list
|
||||
sample: ['/Common/http-tunnel', '/Common/socks-tunnel']
|
||||
routing_protocol:
|
||||
description: List of routing protocols applied to the route domain
|
||||
returned: changed
|
||||
type: list
|
||||
sample: ['bfd', 'bgp']
|
||||
bwc_policy:
|
||||
description: The new bandwidth controller
|
||||
returned: changed
|
||||
type: string
|
||||
sample: /Common/foo
|
||||
connection_limit:
|
||||
description: The new connection limit for the route domain
|
||||
returned: changed
|
||||
type: integer
|
||||
sample: 100
|
||||
flow_eviction_policy:
|
||||
description: The new eviction policy to use with this route domain
|
||||
returned: changed
|
||||
type: string
|
||||
sample: /Common/default-eviction-policy
|
||||
service_policy:
|
||||
description: The new service policy to use with this route domain
|
||||
returned: changed
|
||||
type: string
|
||||
sample: /Common-my-service-policy
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
PROTOCOLS = [
|
||||
'BFD', 'BGP', 'IS-IS', 'OSPFv2', 'OSPFv3', 'PIM', 'RIP', 'RIPng'
|
||||
]
|
||||
|
||||
STRICTS = ['enabled', 'disabled']
|
||||
|
||||
|
||||
class BigIpRouteDomain(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
kwargs['name'] = str(kwargs['id'])
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def absent(self):
|
||||
if not self.exists():
|
||||
return False
|
||||
|
||||
if self.params['check_mode']:
|
||||
return True
|
||||
|
||||
rd = self.api.tm.net.route_domains.route_domain.load(
|
||||
name=self.params['name']
|
||||
)
|
||||
rd.delete()
|
||||
|
||||
if self.exists():
|
||||
raise F5ModuleError("Failed to delete the route domain")
|
||||
else:
|
||||
return True
|
||||
|
||||
def present(self):
|
||||
if self.exists():
|
||||
return self.update()
|
||||
else:
|
||||
if self.params['check_mode']:
|
||||
return True
|
||||
return self.create()
|
||||
|
||||
def read(self):
|
||||
"""Read information and transform it
|
||||
|
||||
The values that are returned by BIG-IP in the f5-sdk can have encoding
|
||||
attached to them as well as be completely missing in some cases.
|
||||
|
||||
Therefore, this method will transform the data from the BIG-IP into a
|
||||
format that is more easily consumable by the rest of the class and the
|
||||
parameters that are supported by the module.
|
||||
"""
|
||||
p = dict()
|
||||
r = self.api.tm.net.route_domains.route_domain.load(
|
||||
name=self.params['name']
|
||||
)
|
||||
|
||||
p['id'] = int(r.id)
|
||||
p['name'] = str(r.name)
|
||||
|
||||
if hasattr(r, 'connectionLimit'):
|
||||
p['connection_limit'] = int(r.connectionLimit)
|
||||
if hasattr(r, 'description'):
|
||||
p['description'] = str(r.description)
|
||||
if hasattr(r, 'strict'):
|
||||
p['strict'] = str(r.strict)
|
||||
if hasattr(r, 'parent'):
|
||||
p['parent'] = r.parent
|
||||
if hasattr(r, 'vlans'):
|
||||
p['vlans'] = list(set([str(x) for x in r.vlans]))
|
||||
if hasattr(r, 'routingProtocol'):
|
||||
p['routing_protocol'] = list(set([str(x) for x in r.routingProtocol]))
|
||||
if hasattr(r, 'flowEvictionPolicy'):
|
||||
p['flow_eviction_policy'] = str(r.flowEvictionPolicy)
|
||||
if hasattr(r, 'bwcPolicy'):
|
||||
p['bwc_policy'] = str(r.bwcPolicy)
|
||||
if hasattr(r, 'servicePolicy'):
|
||||
p['service_policy'] = str(r.servicePolicy)
|
||||
return p
|
||||
|
||||
def domains(self):
|
||||
result = []
|
||||
|
||||
domains = self.api.tm.net.route_domains.get_collection()
|
||||
for domain in domains:
|
||||
# Just checking for the addition of the partition here for
|
||||
# different versions of BIG-IP
|
||||
if '/' + self.params['partition'] + '/' in domain.name:
|
||||
result.append(domain.name)
|
||||
else:
|
||||
full_name = '/%s/%s' % (self.params['partition'], domain.name)
|
||||
result.append(full_name)
|
||||
return result
|
||||
|
||||
def create(self):
|
||||
params = dict()
|
||||
params['id'] = self.params['id']
|
||||
params['name'] = self.params['name']
|
||||
|
||||
partition = self.params['partition']
|
||||
description = self.params['description']
|
||||
strict = self.params['strict']
|
||||
parent = self.params['parent']
|
||||
bwc_policy = self.params['bwc_policy']
|
||||
vlans = self.params['vlans']
|
||||
routing_protocol = self.params['routing_protocol']
|
||||
connection_limit = self.params['connection_limit']
|
||||
flow_eviction_policy = self.params['flow_eviction_policy']
|
||||
service_policy = self.params['service_policy']
|
||||
|
||||
if description is not None:
|
||||
params['description'] = description
|
||||
|
||||
if strict is not None:
|
||||
params['strict'] = strict
|
||||
|
||||
if parent is not None:
|
||||
parent = '/%s/%s' % (partition, parent)
|
||||
if parent in self.domains():
|
||||
params['parent'] = parent
|
||||
else:
|
||||
raise F5ModuleError(
|
||||
"The parent route domain was not found"
|
||||
)
|
||||
|
||||
if bwc_policy is not None:
|
||||
policy = '/%s/%s' % (partition, bwc_policy)
|
||||
params['bwcPolicy'] = policy
|
||||
|
||||
if vlans is not None:
|
||||
params['vlans'] = []
|
||||
for vlan in vlans:
|
||||
vname = '/%s/%s' % (partition, vlan)
|
||||
params['vlans'].append(vname)
|
||||
|
||||
if routing_protocol is not None:
|
||||
params['routingProtocol'] = []
|
||||
for protocol in routing_protocol:
|
||||
if protocol in PROTOCOLS:
|
||||
params['routingProtocol'].append(protocol)
|
||||
else:
|
||||
raise F5ModuleError(
|
||||
"routing_protocol must be one of: %s" % (PROTOCOLS)
|
||||
)
|
||||
|
||||
if connection_limit is not None:
|
||||
params['connectionLimit'] = connection_limit
|
||||
|
||||
if flow_eviction_policy is not None:
|
||||
policy = '/%s/%s' % (partition, flow_eviction_policy)
|
||||
params['flowEvictionPolicy'] = policy
|
||||
|
||||
if service_policy is not None:
|
||||
policy = '/%s/%s' % (partition, service_policy)
|
||||
params['servicePolicy'] = policy
|
||||
|
||||
self.api.tm.net.route_domains.route_domain.create(**params)
|
||||
exists = self.api.tm.net.route_domains.route_domain.exists(
|
||||
name=self.params['name']
|
||||
)
|
||||
|
||||
if exists:
|
||||
return True
|
||||
else:
|
||||
raise F5ModuleError(
|
||||
"An error occurred while creating the route domain"
|
||||
)
|
||||
|
||||
def update(self):
|
||||
changed = False
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
partition = self.params['partition']
|
||||
description = self.params['description']
|
||||
strict = self.params['strict']
|
||||
parent = self.params['parent']
|
||||
bwc_policy = self.params['bwc_policy']
|
||||
vlans = self.params['vlans']
|
||||
routing_protocol = self.params['routing_protocol']
|
||||
connection_limit = self.params['connection_limit']
|
||||
flow_eviction_policy = self.params['flow_eviction_policy']
|
||||
service_policy = self.params['service_policy']
|
||||
|
||||
if description is not None:
|
||||
if 'description' in current:
|
||||
if description != current['description']:
|
||||
params['description'] = description
|
||||
else:
|
||||
params['description'] = description
|
||||
|
||||
if strict is not None:
|
||||
if strict != current['strict']:
|
||||
params['strict'] = strict
|
||||
|
||||
if parent is not None:
|
||||
parent = '/%s/%s' % (partition, parent)
|
||||
if 'parent' in current:
|
||||
if parent != current['parent']:
|
||||
params['parent'] = parent
|
||||
else:
|
||||
params['parent'] = parent
|
||||
|
||||
if bwc_policy is not None:
|
||||
policy = '/%s/%s' % (partition, bwc_policy)
|
||||
if 'bwc_policy' in current:
|
||||
if policy != current['bwc_policy']:
|
||||
params['bwcPolicy'] = policy
|
||||
else:
|
||||
params['bwcPolicy'] = policy
|
||||
|
||||
if vlans is not None:
|
||||
tmp = set()
|
||||
for vlan in vlans:
|
||||
vname = '/%s/%s' % (partition, vlan)
|
||||
tmp.add(vname)
|
||||
tmp = list(tmp)
|
||||
if 'vlans' in current:
|
||||
if tmp != current['vlans']:
|
||||
params['vlans'] = tmp
|
||||
else:
|
||||
params['vlans'] = tmp
|
||||
|
||||
if routing_protocol is not None:
|
||||
tmp = set()
|
||||
for protocol in routing_protocol:
|
||||
if protocol in PROTOCOLS:
|
||||
tmp.add(protocol)
|
||||
else:
|
||||
raise F5ModuleError(
|
||||
"routing_protocol must be one of: %s" % (PROTOCOLS)
|
||||
)
|
||||
tmp = list(tmp)
|
||||
if 'routing_protocol' in current:
|
||||
if tmp != current['routing_protocol']:
|
||||
params['routingProtocol'] = tmp
|
||||
else:
|
||||
params['routingProtocol'] = tmp
|
||||
|
||||
if connection_limit is not None:
|
||||
if connection_limit != current['connection_limit']:
|
||||
params['connectionLimit'] = connection_limit
|
||||
|
||||
if flow_eviction_policy is not None:
|
||||
policy = '/%s/%s' % (partition, flow_eviction_policy)
|
||||
if 'flow_eviction_policy' in current:
|
||||
if policy != current['flow_eviction_policy']:
|
||||
params['flowEvictionPolicy'] = policy
|
||||
else:
|
||||
params['flowEvictionPolicy'] = policy
|
||||
|
||||
if service_policy is not None:
|
||||
policy = '/%s/%s' % (partition, service_policy)
|
||||
if 'service_policy' in current:
|
||||
if policy != current['service_policy']:
|
||||
params['servicePolicy'] = policy
|
||||
else:
|
||||
params['servicePolicy'] = policy
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return changed
|
||||
else:
|
||||
return changed
|
||||
|
||||
try:
|
||||
rd = self.api.tm.net.route_domains.route_domain.load(
|
||||
name=self.params['name']
|
||||
)
|
||||
rd.update(**params)
|
||||
rd.refresh()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(e)
|
||||
|
||||
return True
|
||||
|
||||
def exists(self):
|
||||
return self.api.tm.net.route_domains.route_domain.exists(
|
||||
name=self.params['name']
|
||||
)
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
state = self.params['state']
|
||||
|
||||
if self.params['check_mode']:
|
||||
if value == current:
|
||||
changed = False
|
||||
else:
|
||||
changed = True
|
||||
else:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
current = self.read()
|
||||
result.update(current)
|
||||
elif state == "absent":
|
||||
changed = self.absent()
|
||||
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
id=dict(required=True, type='int'),
|
||||
description=dict(required=False, default=None),
|
||||
strict=dict(required=False, default=None, choices=STRICTS),
|
||||
parent=dict(required=False, type='int', default=None),
|
||||
vlans=dict(required=False, default=None, type='list'),
|
||||
routing_protocol=dict(required=False, default=None, type='list'),
|
||||
bwc_policy=dict(required=False, type='str', default=None),
|
||||
connection_limit=dict(required=False, type='int', default=None),
|
||||
flow_eviction_policy=dict(required=False, type='str', default=None),
|
||||
service_policy=dict(required=False, type='str', default=None)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpRouteDomain(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
704
lib/ansible/modules/network/f5/bigip_selfip.py
Normal file
704
lib/ansible/modules/network/f5/bigip_selfip.py
Normal file
@@ -0,0 +1,704 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_selfip
|
||||
short_description: Manage Self-IPs on a BIG-IP system
|
||||
description:
|
||||
- Manage Self-IPs on a BIG-IP system
|
||||
version_added: "2.2"
|
||||
options:
|
||||
address:
|
||||
description:
|
||||
- The IP addresses for the new self IP. This value is ignored upon update
|
||||
as addresses themselves cannot be changed after they are created.
|
||||
allow_service:
|
||||
description:
|
||||
- Configure port lockdown for the Self IP. By default, the Self IP has a
|
||||
"default deny" policy. This can be changed to allow TCP and UDP ports
|
||||
as well as specific protocols. This list should contain C(protocol):C(port)
|
||||
values.
|
||||
name:
|
||||
description:
|
||||
- The self IP to create.
|
||||
required: true
|
||||
default: Value of C(address)
|
||||
netmask:
|
||||
description:
|
||||
- The netmasks for the self IP.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- The state of the variable on the system. When C(present), guarantees
|
||||
that the Self-IP exists with the provided attributes. When C(absent),
|
||||
removes the Self-IP from the system.
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- absent
|
||||
- present
|
||||
traffic_group:
|
||||
description:
|
||||
- The traffic group for the self IP addresses in an active-active,
|
||||
redundant load balancer configuration.
|
||||
required: false
|
||||
vlan:
|
||||
description:
|
||||
- The VLAN that the new self IPs will be on.
|
||||
required: true
|
||||
route_domain:
|
||||
description:
|
||||
- The route domain id of the system.
|
||||
If none, id of the route domain will be "0" (default route domain)
|
||||
required: false
|
||||
default: none
|
||||
version_added: 2.3
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install f5-sdk.
|
||||
- Requires the netaddr Python package on the host.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- netaddr
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create Self IP
|
||||
bigip_selfip:
|
||||
address: "10.10.10.10"
|
||||
name: "self1"
|
||||
netmask: "255.255.255.0"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
vlan: "vlan1"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create Self IP with a Route Domain
|
||||
bigip_selfip:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
validate_certs: "no"
|
||||
name: "self1"
|
||||
address: "10.10.10.10"
|
||||
netmask: "255.255.255.0"
|
||||
vlan: "vlan1"
|
||||
route_domain: "10"
|
||||
allow_service: "default"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete Self IP
|
||||
bigip_selfip:
|
||||
name: "self1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "absent"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Allow management web UI to be accessed on this Self IP
|
||||
bigip_selfip:
|
||||
name: "self1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "absent"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
allow_service:
|
||||
- "tcp:443"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Allow HTTPS and SSH access to this Self IP
|
||||
bigip_selfip:
|
||||
name: "self1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "absent"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
allow_service:
|
||||
- "tcp:443"
|
||||
- "tpc:22"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Allow all services access to this Self IP
|
||||
bigip_selfip:
|
||||
name: "self1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "absent"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
allow_service:
|
||||
- all
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Allow only GRE and IGMP protocols access to this Self IP
|
||||
bigip_selfip:
|
||||
name: "self1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "absent"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
allow_service:
|
||||
- gre:0
|
||||
- igmp:0
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Allow all TCP, but no other protocols access to this Self IP
|
||||
bigip_selfip:
|
||||
name: "self1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
state: "absent"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
allow_service:
|
||||
- tcp:0
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
allow_service:
|
||||
description: Services that allowed via this Self IP
|
||||
returned: changed
|
||||
type: list
|
||||
sample: ['igmp:0','tcp:22','udp:53']
|
||||
address:
|
||||
description: The address for the Self IP
|
||||
returned: created
|
||||
type: string
|
||||
sample: "192.0.2.10"
|
||||
name:
|
||||
description: The name of the Self IP
|
||||
returned:
|
||||
- created
|
||||
- changed
|
||||
- deleted
|
||||
type: string
|
||||
sample: "self1"
|
||||
netmask:
|
||||
description: The netmask of the Self IP
|
||||
returned:
|
||||
- changed
|
||||
- created
|
||||
type: string
|
||||
sample: "255.255.255.0"
|
||||
traffic_group:
|
||||
description: The traffic group that the Self IP is a member of
|
||||
return:
|
||||
- changed
|
||||
- created
|
||||
type: string
|
||||
sample: "traffic-group-local-only"
|
||||
vlan:
|
||||
description: The VLAN set on the Self IP
|
||||
return:
|
||||
- changed
|
||||
- created
|
||||
type: string
|
||||
sample: "vlan1"
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
try:
|
||||
from netaddr import IPNetwork, AddrFormatError
|
||||
HAS_NETADDR = True
|
||||
except ImportError:
|
||||
HAS_NETADDR = False
|
||||
|
||||
FLOAT = ['enabled', 'disabled']
|
||||
DEFAULT_TG = 'traffic-group-local-only'
|
||||
ALLOWED_PROTOCOLS = ['eigrp', 'egp', 'gre', 'icmp', 'igmp', 'igp', 'ipip',
|
||||
'l2tp', 'ospf', 'pim', 'tcp', 'udp']
|
||||
|
||||
|
||||
class BigIpSelfIp(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def present(self):
|
||||
changed = False
|
||||
|
||||
if self.exists():
|
||||
changed = self.update()
|
||||
else:
|
||||
changed = self.create()
|
||||
|
||||
return changed
|
||||
|
||||
def absent(self):
|
||||
changed = False
|
||||
|
||||
if self.exists():
|
||||
changed = self.delete()
|
||||
|
||||
return changed
|
||||
|
||||
def read(self):
|
||||
"""Read information and transform it
|
||||
|
||||
The values that are returned by BIG-IP in the f5-sdk can have encoding
|
||||
attached to them as well as be completely missing in some cases.
|
||||
|
||||
Therefore, this method will transform the data from the BIG-IP into a
|
||||
format that is more easily consumable by the rest of the class and the
|
||||
parameters that are supported by the module.
|
||||
|
||||
:return: List of values currently stored in BIG-IP, formatted for use
|
||||
in this class.
|
||||
"""
|
||||
p = dict()
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
r = self.api.tm.net.selfips.selfip.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
|
||||
if hasattr(r, 'address'):
|
||||
p['route_domain'] = str(None)
|
||||
if '%' in r.address:
|
||||
ipaddr = []
|
||||
ipaddr = r.address.split('%', 1)
|
||||
rdmask = ipaddr[1].split('/', 1)
|
||||
r.address = "%s/%s" % (ipaddr[0], rdmask[1])
|
||||
p['route_domain'] = str(rdmask[0])
|
||||
ipnet = IPNetwork(r.address)
|
||||
p['address'] = str(ipnet.ip)
|
||||
p['netmask'] = str(ipnet.netmask)
|
||||
if hasattr(r, 'trafficGroup'):
|
||||
p['traffic_group'] = str(r.trafficGroup)
|
||||
if hasattr(r, 'vlan'):
|
||||
p['vlan'] = str(r.vlan)
|
||||
if hasattr(r, 'allowService'):
|
||||
if r.allowService == 'all':
|
||||
p['allow_service'] = set(['all'])
|
||||
else:
|
||||
p['allow_service'] = set([str(x) for x in r.allowService])
|
||||
else:
|
||||
p['allow_service'] = set(['none'])
|
||||
p['name'] = name
|
||||
return p
|
||||
|
||||
def verify_services(self):
|
||||
"""Verifies that a supplied service string has correct format
|
||||
|
||||
The string format for port lockdown is PROTOCOL:PORT. This method
|
||||
will verify that the provided input matches the allowed protocols
|
||||
and the port ranges before submitting to BIG-IP.
|
||||
|
||||
The only allowed exceptions to this rule are the following values
|
||||
|
||||
* all
|
||||
* default
|
||||
* none
|
||||
|
||||
These are special cases that are handled differently in the API.
|
||||
"all" is set as a string, "default" is set as a one item list, and
|
||||
"none" removes the key entirely from the REST API.
|
||||
|
||||
:raises F5ModuleError:
|
||||
"""
|
||||
result = []
|
||||
for svc in self.params['allow_service']:
|
||||
if svc in ['all', 'none', 'default']:
|
||||
result = [svc]
|
||||
break
|
||||
|
||||
tmp = svc.split(':')
|
||||
if tmp[0] not in ALLOWED_PROTOCOLS:
|
||||
raise F5ModuleError(
|
||||
"The provided protocol '%s' is invalid" % (tmp[0])
|
||||
)
|
||||
try:
|
||||
port = int(tmp[1])
|
||||
except Exception:
|
||||
raise F5ModuleError(
|
||||
"The provided port '%s' is not a number" % (tmp[1])
|
||||
)
|
||||
|
||||
if port < 0 or port > 65535:
|
||||
raise F5ModuleError(
|
||||
"The provided port '%s' must be between 0 and 65535"
|
||||
% (port)
|
||||
)
|
||||
else:
|
||||
result.append(svc)
|
||||
return set(result)
|
||||
|
||||
def fmt_services(self, services):
|
||||
"""Returns services formatted for consumption by f5-sdk update
|
||||
|
||||
The BIG-IP endpoint for services takes different values depending on
|
||||
what you want the "allowed services" to be. It can be any of the
|
||||
following
|
||||
|
||||
- a list containing "protocol:port" values
|
||||
- the string "all"
|
||||
- a null value, or None
|
||||
|
||||
This is a convenience function to massage the values the user has
|
||||
supplied so that they are formatted in such a way that BIG-IP will
|
||||
accept them and apply the specified policy.
|
||||
|
||||
:param services: The services to format. This is always a Python set
|
||||
:return:
|
||||
"""
|
||||
result = list(services)
|
||||
if result[0] == 'all':
|
||||
return 'all'
|
||||
elif result[0] == 'none':
|
||||
return None
|
||||
else:
|
||||
return list(services)
|
||||
|
||||
def traffic_groups(self):
|
||||
result = []
|
||||
|
||||
groups = self.api.tm.cm.traffic_groups.get_collection()
|
||||
for group in groups:
|
||||
# Just checking for the addition of the partition here for
|
||||
# different versions of BIG-IP
|
||||
if '/' + self.params['partition'] + '/' in group.name:
|
||||
result.append(group.name)
|
||||
else:
|
||||
full_name = '/%s/%s' % (self.params['partition'], group.name)
|
||||
result.append(str(full_name))
|
||||
return result
|
||||
|
||||
def update(self):
|
||||
changed = False
|
||||
svcs = []
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
address = self.params['address']
|
||||
allow_service = self.params['allow_service']
|
||||
name = self.params['name']
|
||||
netmask = self.params['netmask']
|
||||
partition = self.params['partition']
|
||||
traffic_group = self.params['traffic_group']
|
||||
vlan = self.params['vlan']
|
||||
route_domain = self.params['route_domain']
|
||||
|
||||
if address is not None and address != current['address']:
|
||||
raise F5ModuleError(
|
||||
'Self IP addresses cannot be updated'
|
||||
)
|
||||
|
||||
if netmask is not None:
|
||||
# I ignore the address value here even if they provide it because
|
||||
# you are not allowed to change it.
|
||||
try:
|
||||
address = IPNetwork(current['address'])
|
||||
|
||||
new_addr = "%s/%s" % (address.ip, netmask)
|
||||
nipnet = IPNetwork(new_addr)
|
||||
if route_domain is not None:
|
||||
nipnet = "%s%s%s" % (address.ip, route_domain, netmask)
|
||||
|
||||
cur_addr = "%s/%s" % (current['address'], current['netmask'])
|
||||
cipnet = IPNetwork(cur_addr)
|
||||
if route_domain is not None:
|
||||
cipnet = "%s%s%s" % (current['address'], current['route_domain'], current['netmask'])
|
||||
|
||||
if nipnet != cipnet:
|
||||
if route_domain is not None:
|
||||
address = "%s%s%s/%s" % (address.ip, '%', route_domain, netmask)
|
||||
else:
|
||||
address = "%s/%s" % (nipnet.ip, nipnet.prefixlen)
|
||||
params['address'] = address
|
||||
except AddrFormatError:
|
||||
raise F5ModuleError(
|
||||
'The provided address/netmask value was invalid'
|
||||
)
|
||||
|
||||
if traffic_group is not None:
|
||||
traffic_group = "/%s/%s" % (partition, traffic_group)
|
||||
if traffic_group not in self.traffic_groups():
|
||||
raise F5ModuleError(
|
||||
'The specified traffic group was not found'
|
||||
)
|
||||
|
||||
if 'traffic_group' in current:
|
||||
if traffic_group != current['traffic_group']:
|
||||
params['trafficGroup'] = traffic_group
|
||||
else:
|
||||
params['trafficGroup'] = traffic_group
|
||||
|
||||
if vlan is not None:
|
||||
vlans = self.get_vlans()
|
||||
vlan = "/%s/%s" % (partition, vlan)
|
||||
|
||||
if 'vlan' in current:
|
||||
if vlan != current['vlan']:
|
||||
params['vlan'] = vlan
|
||||
else:
|
||||
params['vlan'] = vlan
|
||||
|
||||
if vlan not in vlans:
|
||||
raise F5ModuleError(
|
||||
'The specified VLAN was not found'
|
||||
)
|
||||
|
||||
if allow_service is not None:
|
||||
svcs = self.verify_services()
|
||||
if 'allow_service' in current:
|
||||
if svcs != current['allow_service']:
|
||||
params['allowService'] = self.fmt_services(svcs)
|
||||
else:
|
||||
params['allowService'] = self.fmt_services(svcs)
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
params['name'] = name
|
||||
params['partition'] = partition
|
||||
if check_mode:
|
||||
return changed
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if svcs:
|
||||
self.cparams['allow_service'] = list(svcs)
|
||||
else:
|
||||
return changed
|
||||
|
||||
r = self.api.tm.net.selfips.selfip.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
r.update(**params)
|
||||
r.refresh()
|
||||
|
||||
return True
|
||||
|
||||
def get_vlans(self):
|
||||
"""Returns formatted list of VLANs
|
||||
|
||||
The VLAN values stored in BIG-IP are done so using their fully
|
||||
qualified name which includes the partition. Therefore, "correct"
|
||||
values according to BIG-IP look like this
|
||||
|
||||
/Common/vlan1
|
||||
|
||||
This is in contrast to the formats that most users think of VLANs
|
||||
as being stored as
|
||||
|
||||
vlan1
|
||||
|
||||
To provide for the consistent user experience while not turfing
|
||||
BIG-IP, we need to massage the values that are provided by the
|
||||
user so that they include the partition.
|
||||
|
||||
:return: List of vlans formatted with preceeding partition
|
||||
"""
|
||||
partition = self.params['partition']
|
||||
vlans = self.api.tm.net.vlans.get_collection()
|
||||
return [str("/" + partition + "/" + x.name) for x in vlans]
|
||||
|
||||
def create(self):
|
||||
params = dict()
|
||||
|
||||
svcs = []
|
||||
check_mode = self.params['check_mode']
|
||||
address = self.params['address']
|
||||
allow_service = self.params['allow_service']
|
||||
name = self.params['name']
|
||||
netmask = self.params['netmask']
|
||||
partition = self.params['partition']
|
||||
traffic_group = self.params['traffic_group']
|
||||
vlan = self.params['vlan']
|
||||
route_domain = self.params['route_domain']
|
||||
|
||||
if address is None or netmask is None:
|
||||
raise F5ModuleError(
|
||||
'An address and a netmask must be specififed'
|
||||
)
|
||||
|
||||
if vlan is None:
|
||||
raise F5ModuleError(
|
||||
'A VLAN name must be specified'
|
||||
)
|
||||
else:
|
||||
vlan = "/%s/%s" % (partition, vlan)
|
||||
|
||||
try:
|
||||
ipin = "%s/%s" % (address, netmask)
|
||||
ipnet = IPNetwork(ipin)
|
||||
if route_domain is not None:
|
||||
params['address'] = "%s%s%s/%s" % (ipnet.ip, '%', route_domain, ipnet.prefixlen)
|
||||
else:
|
||||
params['address'] = "%s/%s" % (ipnet.ip, ipnet.prefixlen)
|
||||
except AddrFormatError:
|
||||
raise F5ModuleError(
|
||||
'The provided address/netmask value was invalid'
|
||||
)
|
||||
|
||||
if traffic_group is None:
|
||||
params['trafficGroup'] = "/%s/%s" % (partition, DEFAULT_TG)
|
||||
else:
|
||||
traffic_group = "/%s/%s" % (partition, traffic_group)
|
||||
if traffic_group in self.traffic_groups():
|
||||
params['trafficGroup'] = traffic_group
|
||||
else:
|
||||
raise F5ModuleError(
|
||||
'The specified traffic group was not found'
|
||||
)
|
||||
|
||||
vlans = self.get_vlans()
|
||||
if vlan in vlans:
|
||||
params['vlan'] = vlan
|
||||
else:
|
||||
raise F5ModuleError(
|
||||
'The specified VLAN was not found'
|
||||
)
|
||||
|
||||
if allow_service is not None:
|
||||
svcs = self.verify_services()
|
||||
params['allowService'] = self.fmt_services(svcs)
|
||||
|
||||
params['name'] = name
|
||||
params['partition'] = partition
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if svcs:
|
||||
self.cparams['allow_service'] = list(svcs)
|
||||
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
d = self.api.tm.net.selfips.selfip
|
||||
d.create(**params)
|
||||
|
||||
if self.exists():
|
||||
return True
|
||||
else:
|
||||
raise F5ModuleError("Failed to create the self IP")
|
||||
|
||||
def delete(self):
|
||||
params = dict()
|
||||
check_mode = self.params['check_mode']
|
||||
|
||||
params['name'] = self.params['name']
|
||||
params['partition'] = self.params['partition']
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
dc = self.api.tm.net.selfips.selfip.load(**params)
|
||||
dc.delete()
|
||||
|
||||
if self.exists():
|
||||
raise F5ModuleError("Failed to delete the self IP")
|
||||
return True
|
||||
|
||||
def exists(self):
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
return self.api.tm.net.selfips.selfip.exists(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
state = self.params['state']
|
||||
|
||||
try:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
elif state == "absent":
|
||||
changed = self.absent()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
address=dict(required=False, default=None),
|
||||
allow_service=dict(type='list', default=None),
|
||||
name=dict(required=True),
|
||||
netmask=dict(required=False, default=None),
|
||||
traffic_group=dict(required=False, default=None),
|
||||
vlan=dict(required=False, default=None),
|
||||
route_domain=dict(required=False, default=None)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
try:
|
||||
if not HAS_NETADDR:
|
||||
raise F5ModuleError(
|
||||
"The netaddr python module is required."
|
||||
)
|
||||
|
||||
obj = BigIpSelfIp(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
417
lib/ansible/modules/network/f5/bigip_snat_pool.py
Normal file
417
lib/ansible/modules/network/f5/bigip_snat_pool.py
Normal file
@@ -0,0 +1,417 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_snat_pool
|
||||
short_description: Manage SNAT pools on a BIG-IP.
|
||||
description:
|
||||
- Manage SNAT pools on a BIG-IP.
|
||||
version_added: "2.3"
|
||||
options:
|
||||
append:
|
||||
description:
|
||||
- When C(yes), will only add members to the SNAT pool. When C(no), will
|
||||
replace the existing member list with the provided member list.
|
||||
choices:
|
||||
- yes
|
||||
- no
|
||||
default: no
|
||||
members:
|
||||
description:
|
||||
- List of members to put in the SNAT pool. When a C(state) of present is
|
||||
provided, this parameter is required. Otherwise, it is optional.
|
||||
required: false
|
||||
default: None
|
||||
aliases: ['member']
|
||||
name:
|
||||
description: The name of the SNAT pool.
|
||||
required: True
|
||||
state:
|
||||
description:
|
||||
- Whether the SNAT pool should exist or not.
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as
|
||||
pip install f5-sdk
|
||||
- Requires the netaddr Python package on the host. This is as easy as
|
||||
pip install netaddr
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add the SNAT pool 'my-snat-pool'
|
||||
bigip_snat_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my-snat-pool"
|
||||
state: "present"
|
||||
members:
|
||||
- 10.10.10.10
|
||||
- 20.20.20.20
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Change the SNAT pool's members to a single member
|
||||
bigip_snat_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my-snat-pool"
|
||||
state: "present"
|
||||
member: "30.30.30.30"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Append a new list of members to the existing pool
|
||||
bigip_snat_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "my-snat-pool"
|
||||
state: "present"
|
||||
members:
|
||||
- 10.10.10.10
|
||||
- 20.20.20.20
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Remove the SNAT pool 'my-snat-pool'
|
||||
bigip_snat_pool:
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
name: "johnd"
|
||||
state: "absent"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
members:
|
||||
description:
|
||||
- List of members that are part of the SNAT pool.
|
||||
returned: changed and success
|
||||
type: list
|
||||
sample: "['10.10.10.10']"
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip.contexts import TransactionContextManager
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
try:
|
||||
from netaddr import IPAddress, AddrFormatError
|
||||
HAS_NETADDR = True
|
||||
except ImportError:
|
||||
HAS_NETADDR = False
|
||||
|
||||
|
||||
class BigIpSnatPoolManager(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.changed_params = dict()
|
||||
self.params = kwargs
|
||||
self.api = None
|
||||
|
||||
def apply_changes(self):
|
||||
result = dict()
|
||||
|
||||
changed = self.apply_to_running_config()
|
||||
if changed:
|
||||
self.save_running_config()
|
||||
|
||||
result.update(**self.changed_params)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def apply_to_running_config(self):
|
||||
try:
|
||||
self.api = self.connect_to_bigip(**self.params)
|
||||
if self.params['state'] == "present":
|
||||
return self.present()
|
||||
elif self.params['state'] == "absent":
|
||||
return self.absent()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
def save_running_config(self):
|
||||
self.api.tm.sys.config.exec_cmd('save')
|
||||
|
||||
def present(self):
|
||||
if self.params['members'] is None:
|
||||
raise F5ModuleError(
|
||||
"The members parameter must be specified"
|
||||
)
|
||||
|
||||
if self.snat_pool_exists():
|
||||
return self.update_snat_pool()
|
||||
else:
|
||||
return self.ensure_snat_pool_is_present()
|
||||
|
||||
def absent(self):
|
||||
changed = False
|
||||
if self.snat_pool_exists():
|
||||
changed = self.ensure_snat_pool_is_absent()
|
||||
return changed
|
||||
|
||||
def connect_to_bigip(self, **kwargs):
|
||||
return ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def read_snat_pool_information(self):
|
||||
pool = self.load_snat_pool()
|
||||
return self.format_snat_pool_information(pool)
|
||||
|
||||
def format_snat_pool_information(self, pool):
|
||||
"""Ensure that the pool information is in a standard format
|
||||
|
||||
The SDK provides information back in a format that may change with
|
||||
the version of BIG-IP being worked with. Therefore, we need to make
|
||||
sure that the data is formatted in a way that our module expects it.
|
||||
|
||||
Additionally, this takes care of minor variations between Python 2
|
||||
and Python 3.
|
||||
|
||||
:param pool:
|
||||
:return:
|
||||
"""
|
||||
result = dict()
|
||||
result['name'] = str(pool.name)
|
||||
if hasattr(pool, 'members'):
|
||||
result['members'] = self.format_current_members(pool)
|
||||
return result
|
||||
|
||||
def format_current_members(self, pool):
|
||||
result = set()
|
||||
partition_prefix = "/{0}/".format(self.params['partition'])
|
||||
|
||||
for member in pool.members:
|
||||
member = str(member.replace(partition_prefix, ''))
|
||||
result.update([member])
|
||||
return list(result)
|
||||
|
||||
def load_snat_pool(self):
|
||||
return self.api.tm.ltm.snatpools.snatpool.load(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
|
||||
def snat_pool_exists(self):
|
||||
return self.api.tm.ltm.snatpools.snatpool.exists(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
|
||||
def update_snat_pool(self):
|
||||
params = self.get_changed_parameters()
|
||||
if params:
|
||||
self.changed_params = camel_dict_to_snake_dict(params)
|
||||
if self.params['check_mode']:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
params['name'] = self.params['name']
|
||||
params['partition'] = self.params['partition']
|
||||
self.update_snat_pool_on_device(params)
|
||||
return True
|
||||
|
||||
def update_snat_pool_on_device(self, params):
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
r = api.tm.ltm.snatpools.snatpool.load(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
r.modify(**params)
|
||||
|
||||
def get_changed_parameters(self):
|
||||
result = dict()
|
||||
current = self.read_snat_pool_information()
|
||||
if self.are_members_changed(current):
|
||||
result['members'] = self.get_new_member_list(current['members'])
|
||||
return result
|
||||
|
||||
def are_members_changed(self, current):
|
||||
if self.params['members'] is None:
|
||||
return False
|
||||
if 'members' not in current:
|
||||
return True
|
||||
if set(self.params['members']) == set(current['members']):
|
||||
return False
|
||||
if not self.params['append']:
|
||||
return True
|
||||
|
||||
# Checking to see if the supplied list is a subset of the current
|
||||
# list is only relevant if the `append` parameter is provided.
|
||||
new_members = set(self.params['members'])
|
||||
current_members = set(current['members'])
|
||||
if new_members.issubset(current_members):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_new_member_list(self, current_members):
|
||||
result = set()
|
||||
|
||||
if self.params['append']:
|
||||
result.update(set(current_members))
|
||||
result.update(set(self.params['members']))
|
||||
else:
|
||||
result.update(set(self.params['members']))
|
||||
return list(result)
|
||||
|
||||
def ensure_snat_pool_is_present(self):
|
||||
params = self.get_snat_pool_creation_parameters()
|
||||
self.changed_params = camel_dict_to_snake_dict(params)
|
||||
if self.params['check_mode']:
|
||||
return True
|
||||
self.create_snat_pool_on_device(params)
|
||||
if self.snat_pool_exists():
|
||||
return True
|
||||
else:
|
||||
raise F5ModuleError("Failed to create the SNAT pool")
|
||||
|
||||
def get_snat_pool_creation_parameters(self):
|
||||
members = self.get_formatted_members_list()
|
||||
return dict(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition'],
|
||||
members=members
|
||||
)
|
||||
|
||||
def get_formatted_members_list(self):
|
||||
result = set()
|
||||
try:
|
||||
for ip in self.params['members']:
|
||||
address = str(IPAddress(ip))
|
||||
result.update([address])
|
||||
return list(result)
|
||||
except AddrFormatError:
|
||||
raise F5ModuleError(
|
||||
'The provided member address is not a valid IP address'
|
||||
)
|
||||
|
||||
def create_snat_pool_on_device(self, params):
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
api.tm.ltm.snatpools.snatpool.create(**params)
|
||||
|
||||
def ensure_snat_pool_is_absent(self):
|
||||
if self.params['check_mode']:
|
||||
return True
|
||||
self.delete_snat_pool_from_device()
|
||||
if self.snat_pool_exists():
|
||||
raise F5ModuleError("Failed to delete the SNAT pool")
|
||||
return True
|
||||
|
||||
def delete_snat_pool_from_device(self):
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
pool = api.tm.ltm.snatpools.snatpool.load(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
pool.delete()
|
||||
|
||||
|
||||
class BigIpSnatPoolModuleConfig(object):
|
||||
def __init__(self):
|
||||
self.argument_spec = dict()
|
||||
self.meta_args = dict()
|
||||
self.supports_check_mode = True
|
||||
self.states = ['absent', 'present']
|
||||
|
||||
self.initialize_meta_args()
|
||||
self.initialize_argument_spec()
|
||||
|
||||
def initialize_meta_args(self):
|
||||
args = dict(
|
||||
append=dict(
|
||||
default=False,
|
||||
type='bool',
|
||||
choices=BOOLEANS
|
||||
),
|
||||
name=dict(required=True),
|
||||
members=dict(
|
||||
required=False,
|
||||
default=None,
|
||||
type='list',
|
||||
aliases=['member']
|
||||
),
|
||||
state=dict(
|
||||
default='present',
|
||||
choices=self.states
|
||||
)
|
||||
)
|
||||
self.meta_args = args
|
||||
|
||||
def initialize_argument_spec(self):
|
||||
self.argument_spec = f5_argument_spec()
|
||||
self.argument_spec.update(self.meta_args)
|
||||
|
||||
def create(self):
|
||||
return AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=self.supports_check_mode
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
if not HAS_NETADDR:
|
||||
raise F5ModuleError("The python netaddr module is required")
|
||||
|
||||
config = BigIpSnatPoolModuleConfig()
|
||||
module = config.create()
|
||||
|
||||
try:
|
||||
obj = BigIpSnatPoolManager(
|
||||
check_mode=module.check_mode, **module.params
|
||||
)
|
||||
result = obj.apply_changes()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
520
lib/ansible/modules/network/f5/bigip_ssl_certificate.py
Normal file
520
lib/ansible/modules/network/f5/bigip_ssl_certificate.py
Normal file
@@ -0,0 +1,520 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2016, Kevin Coming (@waffie1)
|
||||
#
|
||||
# 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: bigip_ssl_certificate
|
||||
short_description: Import/Delete certificates from BIG-IP
|
||||
description:
|
||||
- This module will import/delete SSL certificates on BIG-IP LTM.
|
||||
Certificates can be imported from certificate and key files on the local
|
||||
disk, in PEM format.
|
||||
version_added: 2.2
|
||||
options:
|
||||
cert_content:
|
||||
description:
|
||||
- When used instead of 'cert_src', sets the contents of a certificate directly
|
||||
to the specified value. This is used with lookup plugins or for anything
|
||||
with formatting or templating. Either one of C(key_src),
|
||||
C(key_content), C(cert_src) or C(cert_content) must be provided when
|
||||
C(state) is C(present).
|
||||
required: false
|
||||
key_content:
|
||||
description:
|
||||
- When used instead of 'key_src', sets the contents of a certificate key
|
||||
directly to the specified value. This is used with lookup plugins or for
|
||||
anything with formatting or templating. Either one of C(key_src),
|
||||
C(key_content), C(cert_src) or C(cert_content) must be provided when
|
||||
C(state) is C(present).
|
||||
required: false
|
||||
state:
|
||||
description:
|
||||
- Certificate and key state. This determines if the provided certificate
|
||||
and key is to be made C(present) on the device or C(absent).
|
||||
required: true
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
partition:
|
||||
description:
|
||||
- BIG-IP partition to use when adding/deleting certificate.
|
||||
required: false
|
||||
default: Common
|
||||
name:
|
||||
description:
|
||||
- SSL Certificate Name. This is the cert/key pair name used
|
||||
when importing a certificate/key into the F5. It also
|
||||
determines the filenames of the objects on the LTM
|
||||
(:Partition:name.cer_11111_1 and :Partition_name.key_11111_1).
|
||||
required: true
|
||||
cert_src:
|
||||
description:
|
||||
- This is the local filename of the certificate. Either one of C(key_src),
|
||||
C(key_content), C(cert_src) or C(cert_content) must be provided when
|
||||
C(state) is C(present).
|
||||
required: false
|
||||
key_src:
|
||||
description:
|
||||
- This is the local filename of the private key. Either one of C(key_src),
|
||||
C(key_content), C(cert_src) or C(cert_content) must be provided when
|
||||
C(state) is C(present).
|
||||
required: false
|
||||
passphrase:
|
||||
description:
|
||||
- Passphrase on certificate private key
|
||||
required: false
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install f5-sdk.
|
||||
- Requires the netaddr Python package on the host.
|
||||
- If you use this module, you will not be able to remove the certificates
|
||||
and keys that are managed, via the web UI. You can only remove them via
|
||||
tmsh or these modules.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk >= 1.5.0
|
||||
- BigIP >= v12
|
||||
author:
|
||||
- Kevin Coming (@waffie1)
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Import PEM Certificate from local disk
|
||||
bigip_ssl_certificate:
|
||||
name: "certificate-name"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
cert_src: "/path/to/cert.crt"
|
||||
key_src: "/path/to/key.key"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Use a file lookup to import PEM Certificate
|
||||
bigip_ssl_certificate:
|
||||
name: "certificate-name"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "present"
|
||||
cert_content: "{{ lookup('file', '/path/to/cert.crt') }}"
|
||||
key_content: "{{ lookup('file', '/path/to/key.key') }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Delete Certificate"
|
||||
bigip_ssl_certificate:
|
||||
name: "certificate-name"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
state: "absent"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
cert_name:
|
||||
description: >
|
||||
The name of the SSL certificate. The C(cert_name) and
|
||||
C(key_name) will be equal to each other.
|
||||
returned:
|
||||
- created
|
||||
- changed
|
||||
- deleted
|
||||
type: string
|
||||
sample: "cert1"
|
||||
key_name:
|
||||
description: >
|
||||
The name of the SSL certificate key. The C(key_name) and
|
||||
C(cert_name) will be equal to each other.
|
||||
returned:
|
||||
- created
|
||||
- changed
|
||||
- deleted
|
||||
type: string
|
||||
sample: "key1"
|
||||
partition:
|
||||
description: Partition in which the cert/key was created
|
||||
returned:
|
||||
- changed
|
||||
- created
|
||||
- deleted
|
||||
type: string
|
||||
sample: "Common"
|
||||
key_checksum:
|
||||
description: SHA1 checksum of the key that was provided
|
||||
return:
|
||||
- changed
|
||||
- created
|
||||
type: string
|
||||
sample: "cf23df2207d99a74fbe169e3eba035e633b65d94"
|
||||
cert_checksum:
|
||||
description: SHA1 checksum of the cert that was provided
|
||||
return:
|
||||
- changed
|
||||
- created
|
||||
type: string
|
||||
sample: "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
|
||||
'''
|
||||
|
||||
|
||||
try:
|
||||
from f5.bigip.contexts import TransactionContextManager
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
import hashlib
|
||||
import StringIO
|
||||
|
||||
|
||||
class BigIpSslCertificate(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
required_args = ['key_content', 'key_src', 'cert_content', 'cert_src']
|
||||
|
||||
ksource = kwargs['key_src']
|
||||
if ksource:
|
||||
with open(ksource) as f:
|
||||
kwargs['key_content'] = f.read()
|
||||
|
||||
csource = kwargs['cert_src']
|
||||
if csource:
|
||||
with open(csource) as f:
|
||||
kwargs['cert_content'] = f.read()
|
||||
|
||||
if kwargs['state'] == 'present':
|
||||
if not any(kwargs[k] is not None for k in required_args):
|
||||
raise F5ModuleError(
|
||||
"Either 'key_content', 'key_src', 'cert_content' or "
|
||||
"'cert_src' must be provided"
|
||||
)
|
||||
|
||||
# This is the remote BIG-IP path from where it will look for certs
|
||||
# to install.
|
||||
self.dlpath = '/var/config/rest/downloads'
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def exists(self):
|
||||
cert = self.cert_exists()
|
||||
key = self.key_exists()
|
||||
|
||||
if cert and key:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_hash(self, content):
|
||||
k = hashlib.sha1()
|
||||
s = StringIO.StringIO(content)
|
||||
while True:
|
||||
data = s.read(1024)
|
||||
if not data:
|
||||
break
|
||||
k.update(data)
|
||||
return k.hexdigest()
|
||||
|
||||
def present(self):
|
||||
current = self.read()
|
||||
changed = False
|
||||
do_key = False
|
||||
do_cert = False
|
||||
chash = None
|
||||
khash = None
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
cert_content = self.params['cert_content']
|
||||
key_content = self.params['key_content']
|
||||
passphrase = self.params['passphrase']
|
||||
|
||||
# Technically you dont need to provide us with anything in the form
|
||||
# of content for your cert, but that's kind of illogical, so we just
|
||||
# return saying you didn't "do" anything if you left the cert and keys
|
||||
# empty.
|
||||
if not cert_content and not key_content:
|
||||
return False
|
||||
|
||||
if key_content is not None:
|
||||
if 'key_checksum' in current:
|
||||
khash = self.get_hash(key_content)
|
||||
if khash not in current['key_checksum']:
|
||||
do_key = "update"
|
||||
else:
|
||||
do_key = "create"
|
||||
|
||||
if cert_content is not None:
|
||||
if 'cert_checksum' in current:
|
||||
chash = self.get_hash(cert_content)
|
||||
if chash not in current['cert_checksum']:
|
||||
do_cert = "update"
|
||||
else:
|
||||
do_cert = "create"
|
||||
|
||||
if do_cert or do_key:
|
||||
changed = True
|
||||
params = dict()
|
||||
params['cert_name'] = name
|
||||
params['key_name'] = name
|
||||
params['partition'] = partition
|
||||
if khash:
|
||||
params['key_checksum'] = khash
|
||||
if chash:
|
||||
params['cert_checksum'] = chash
|
||||
self.cparams = params
|
||||
|
||||
if check_mode:
|
||||
return changed
|
||||
|
||||
if not do_cert and not do_key:
|
||||
return False
|
||||
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
if do_cert:
|
||||
# Upload the content of a certificate as a StringIO object
|
||||
cstring = StringIO.StringIO(cert_content)
|
||||
filename = "%s.crt" % (name)
|
||||
filepath = os.path.join(self.dlpath, filename)
|
||||
api.shared.file_transfer.uploads.upload_stringio(
|
||||
cstring,
|
||||
filename
|
||||
)
|
||||
|
||||
if do_cert == "update":
|
||||
# Install the certificate
|
||||
params = {
|
||||
'name': name,
|
||||
'partition': partition
|
||||
}
|
||||
cert = api.tm.sys.file.ssl_certs.ssl_cert.load(**params)
|
||||
|
||||
# This works because, while the source path is the same,
|
||||
# calling update causes the file to be re-read
|
||||
cert.update()
|
||||
changed = True
|
||||
elif do_cert == "create":
|
||||
# Install the certificate
|
||||
params = {
|
||||
'sourcePath': "file://" + filepath,
|
||||
'name': name,
|
||||
'partition': partition
|
||||
}
|
||||
api.tm.sys.file.ssl_certs.ssl_cert.create(**params)
|
||||
changed = True
|
||||
|
||||
if do_key:
|
||||
# Upload the content of a certificate key as a StringIO object
|
||||
kstring = StringIO.StringIO(key_content)
|
||||
filename = "%s.key" % (name)
|
||||
filepath = os.path.join(self.dlpath, filename)
|
||||
api.shared.file_transfer.uploads.upload_stringio(
|
||||
kstring,
|
||||
filename
|
||||
)
|
||||
|
||||
if do_key == "update":
|
||||
# Install the key
|
||||
params = {
|
||||
'name': name,
|
||||
'partition': partition
|
||||
}
|
||||
key = api.tm.sys.file.ssl_keys.ssl_key.load(**params)
|
||||
|
||||
params = dict()
|
||||
|
||||
if passphrase:
|
||||
params['passphrase'] = passphrase
|
||||
else:
|
||||
params['passphrase'] = None
|
||||
|
||||
key.update(**params)
|
||||
changed = True
|
||||
elif do_key == "create":
|
||||
# Install the key
|
||||
params = {
|
||||
'sourcePath': "file://" + filepath,
|
||||
'name': name,
|
||||
'partition': partition
|
||||
}
|
||||
if passphrase:
|
||||
params['passphrase'] = self.params['passphrase']
|
||||
else:
|
||||
params['passphrase'] = None
|
||||
|
||||
api.tm.sys.file.ssl_keys.ssl_key.create(**params)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
def key_exists(self):
|
||||
return self.api.tm.sys.file.ssl_keys.ssl_key.exists(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
|
||||
def cert_exists(self):
|
||||
return self.api.tm.sys.file.ssl_certs.ssl_cert.exists(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
|
||||
def read(self):
|
||||
p = dict()
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
|
||||
if self.key_exists():
|
||||
key = self.api.tm.sys.file.ssl_keys.ssl_key.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
if hasattr(key, 'checksum'):
|
||||
p['key_checksum'] = str(key.checksum)
|
||||
|
||||
if self.cert_exists():
|
||||
cert = self.api.tm.sys.file.ssl_certs.ssl_cert.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
if hasattr(cert, 'checksum'):
|
||||
p['cert_checksum'] = str(cert.checksum)
|
||||
|
||||
p['name'] = name
|
||||
return p
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
state = self.params['state']
|
||||
|
||||
try:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
elif state == "absent":
|
||||
changed = self.absent()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def absent(self):
|
||||
changed = False
|
||||
|
||||
if self.exists():
|
||||
changed = self.delete()
|
||||
|
||||
return changed
|
||||
|
||||
def delete(self):
|
||||
changed = False
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
|
||||
delete_cert = self.cert_exists()
|
||||
delete_key = self.key_exists()
|
||||
|
||||
if not delete_cert and not delete_key:
|
||||
return changed
|
||||
|
||||
if check_mode:
|
||||
params = dict()
|
||||
params['cert_name'] = name
|
||||
params['key_name'] = name
|
||||
params['partition'] = partition
|
||||
self.cparams = params
|
||||
return True
|
||||
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
if delete_cert:
|
||||
# Delete the certificate
|
||||
c = api.tm.sys.file.ssl_certs.ssl_cert.load(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
c.delete()
|
||||
changed = True
|
||||
|
||||
if delete_key:
|
||||
# Delete the certificate key
|
||||
k = self.api.tm.sys.file.ssl_keys.ssl_key.load(
|
||||
name=self.params['name'],
|
||||
partition=self.params['partition']
|
||||
)
|
||||
k.delete()
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
name=dict(type='str', required=True),
|
||||
cert_content=dict(type='str', default=None),
|
||||
cert_src=dict(type='path', default=None),
|
||||
key_content=dict(type='str', default=None),
|
||||
key_src=dict(type='path', default=None),
|
||||
passphrase=dict(type='str', default=None, no_log=True)
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[
|
||||
['key_content', 'key_src'],
|
||||
['cert_content', 'cert_src']
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpSslCertificate(check_mode=module.check_mode,
|
||||
**module.params)
|
||||
result = obj.flush()
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
227
lib/ansible/modules/network/f5/bigip_sys_db.py
Normal file
227
lib/ansible/modules/network/f5/bigip_sys_db.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_sys_db
|
||||
short_description: Manage BIG-IP system database variables
|
||||
description:
|
||||
- Manage BIG-IP system database variables
|
||||
version_added: "2.2"
|
||||
options:
|
||||
key:
|
||||
description:
|
||||
- The database variable to manipulate.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- The state of the variable on the system. When C(present), guarantees
|
||||
that an existing variable is set to C(value). When C(reset) sets the
|
||||
variable back to the default value. At least one of value and state
|
||||
C(reset) are required.
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- reset
|
||||
value:
|
||||
description:
|
||||
- The value to set the key to. At least one of value and state C(reset)
|
||||
are required.
|
||||
required: false
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install f5-sdk.
|
||||
- Requires BIG-IP version 12.0.0 or greater
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set the boot.quiet DB variable on the BIG-IP
|
||||
bigip_sys_db:
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
key: "boot.quiet"
|
||||
value: "disable"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Disable the initial setup screen
|
||||
bigip_sys_db:
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
key: "setup.run"
|
||||
value: "false"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Reset the initial setup screen
|
||||
bigip_sys_db:
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
key: "setup.run"
|
||||
state: "reset"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
name:
|
||||
description: The key in the system database that was specified
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "setup.run"
|
||||
default_value:
|
||||
description: The default value of the key
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "true"
|
||||
value:
|
||||
description: The value that you set the key to
|
||||
returned: changed and success
|
||||
type: string
|
||||
sample: "false"
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
class BigIpSysDb(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
state = self.params['state']
|
||||
value = self.params['value']
|
||||
|
||||
if not state == 'reset' and not value:
|
||||
raise F5ModuleError(
|
||||
"When setting a key, a value must be supplied"
|
||||
)
|
||||
|
||||
current = self.read()
|
||||
|
||||
if self.params['check_mode']:
|
||||
if value == current:
|
||||
changed = False
|
||||
else:
|
||||
changed = True
|
||||
else:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
elif state == "reset":
|
||||
changed = self.reset()
|
||||
current = self.read()
|
||||
result.update(
|
||||
name=current.name,
|
||||
default_value=current.defaultValue,
|
||||
value=current.value
|
||||
)
|
||||
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def read(self):
|
||||
dbs = self.api.tm.sys.dbs.db.load(
|
||||
name=self.params['key']
|
||||
)
|
||||
return dbs
|
||||
|
||||
def present(self):
|
||||
current = self.read()
|
||||
|
||||
if current.value == self.params['value']:
|
||||
return False
|
||||
|
||||
current.update(value=self.params['value'])
|
||||
current.refresh()
|
||||
|
||||
if current.value != self.params['value']:
|
||||
raise F5ModuleError(
|
||||
"Failed to set the DB variable"
|
||||
)
|
||||
return True
|
||||
|
||||
def reset(self):
|
||||
current = self.read()
|
||||
|
||||
default = current.defaultValue
|
||||
if current.value == default:
|
||||
return False
|
||||
|
||||
current.update(value=default)
|
||||
current.refresh()
|
||||
|
||||
if current.value != current.defaultValue:
|
||||
raise F5ModuleError(
|
||||
"Failed to reset the DB variable"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
key=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'reset']),
|
||||
value=dict(required=False, default=None)
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpSysDb(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
430
lib/ansible/modules/network/f5/bigip_sys_global.py
Normal file
430
lib/ansible/modules/network/f5/bigip_sys_global.py
Normal file
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_sys_global
|
||||
short_description: Manage BIG-IP global settings.
|
||||
description:
|
||||
- Manage BIG-IP global settings.
|
||||
version_added: "2.3"
|
||||
options:
|
||||
banner_text:
|
||||
description:
|
||||
- Specifies the text to present in the advisory banner.
|
||||
console_timeout:
|
||||
description:
|
||||
- Specifies the number of seconds of inactivity before the system logs
|
||||
off a user that is logged on.
|
||||
gui_setup:
|
||||
description:
|
||||
- C(enable) or C(disabled) the Setup utility in the browser-based
|
||||
Configuration utility
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
lcd_display:
|
||||
description:
|
||||
- Specifies, when C(enabled), that the system menu displays on the
|
||||
LCD screen on the front of the unit. This setting has no effect
|
||||
when used on the VE platform.
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
mgmt_dhcp:
|
||||
description:
|
||||
- Specifies whether or not to enable DHCP client on the management
|
||||
interface
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
net_reboot:
|
||||
description:
|
||||
- Specifies, when C(enabled), that the next time you reboot the system,
|
||||
the system boots to an ISO image on the network, rather than an
|
||||
internal media drive.
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
quiet_boot:
|
||||
description:
|
||||
- Specifies, when C(enabled), that the system suppresses informational
|
||||
text on the console during the boot cycle. When C(disabled), the
|
||||
system presents messages and informational text on the console during
|
||||
the boot cycle.
|
||||
security_banner:
|
||||
description:
|
||||
- Specifies whether the system displays an advisory message on the
|
||||
login screen.
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
state:
|
||||
description:
|
||||
- The state of the variable on the system. When C(present), guarantees
|
||||
that an existing variable is set to C(value).
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install f5-sdk.
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Disable the setup utility
|
||||
bigip_sys_global:
|
||||
gui_setup: "disabled"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
state: "present"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
banner_text:
|
||||
description: The new text to present in the advisory banner.
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "This is a corporate device. Do not touch."
|
||||
console_timeout:
|
||||
description: >
|
||||
The new number of seconds of inactivity before the system
|
||||
logs off a user that is logged on.
|
||||
returned: changed
|
||||
type: integer
|
||||
sample: 600
|
||||
gui_setup:
|
||||
description: The new setting for the Setup utility.
|
||||
returned: changed
|
||||
type: string
|
||||
sample: enabled
|
||||
lcd_display:
|
||||
description: The new setting for displaying the system menu on the LCD.
|
||||
returned: changed
|
||||
type: string
|
||||
sample: enabled
|
||||
mgmt_dhcp:
|
||||
description: >
|
||||
The new setting for whether the mgmt interface should DHCP
|
||||
or not
|
||||
returned: changed
|
||||
type: string
|
||||
sample: enabled
|
||||
net_reboot:
|
||||
description: >
|
||||
The new setting for whether the system should boot to an ISO on the
|
||||
network or not
|
||||
returned: changed
|
||||
type: string
|
||||
sample: enabled
|
||||
quiet_boot:
|
||||
description: >
|
||||
The new setting for whether the system should suppress information to
|
||||
the console during boot or not.
|
||||
returned: changed
|
||||
type: string
|
||||
sample: enabled
|
||||
security_banner:
|
||||
description: >
|
||||
The new setting for whether the system should display an advisory message
|
||||
on the login screen or not
|
||||
returned: changed
|
||||
type: string
|
||||
sample: enabled
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip.contexts import TransactionContextManager
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
class BigIpSysGlobalManager(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.changed_params = dict()
|
||||
self.params = kwargs
|
||||
self.api = None
|
||||
|
||||
def apply_changes(self):
|
||||
result = dict()
|
||||
|
||||
changed = self.apply_to_running_config()
|
||||
|
||||
result.update(**self.changed_params)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
def apply_to_running_config(self):
|
||||
try:
|
||||
self.api = self.connect_to_bigip(**self.params)
|
||||
return self.update_sys_global_settings()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
def connect_to_bigip(self, **kwargs):
|
||||
return ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def read_sys_global_information(self):
|
||||
settings = self.load_sys_global()
|
||||
return self.format_sys_global_information(settings)
|
||||
|
||||
def load_sys_global(self):
|
||||
return self.api.tm.sys.global_settings.load()
|
||||
|
||||
def get_changed_parameters(self):
|
||||
result = dict()
|
||||
current = self.read_sys_global_information()
|
||||
if self.security_banner_is_changed(current):
|
||||
result['guiSecurityBanner'] = self.params['security_banner']
|
||||
if self.banner_text_is_changed(current):
|
||||
result['guiSecurityBannerText'] = self.params['banner_text']
|
||||
if self.gui_setup_is_changed(current):
|
||||
result['guiSetup'] = self.params['gui_setup']
|
||||
if self.lcd_display_is_changed(current):
|
||||
result['lcdDisplay'] = self.params['lcd_display']
|
||||
if self.mgmt_dhcp_is_changed(current):
|
||||
result['mgmtDhcp'] = self.params['mgmt_dhcp']
|
||||
if self.net_reboot_is_changed(current):
|
||||
result['netReboot'] = self.params['net_reboot']
|
||||
if self.quiet_boot_is_changed(current):
|
||||
result['quietBoot'] = self.params['quiet_boot']
|
||||
if self.console_timeout_is_changed(current):
|
||||
result['consoleInactivityTimeout'] = self.params['console_timeout']
|
||||
return result
|
||||
|
||||
def security_banner_is_changed(self, current):
|
||||
if self.params['security_banner'] is None:
|
||||
return False
|
||||
if 'security_banner' not in current:
|
||||
return True
|
||||
if self.params['security_banner'] == current['security_banner']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def banner_text_is_changed(self, current):
|
||||
if self.params['banner_text'] is None:
|
||||
return False
|
||||
if 'banner_text' not in current:
|
||||
return True
|
||||
if self.params['banner_text'] == current['banner_text']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def gui_setup_is_changed(self, current):
|
||||
if self.params['gui_setup'] is None:
|
||||
return False
|
||||
if 'gui_setup' not in current:
|
||||
return True
|
||||
if self.params['gui_setup'] == current['gui_setup']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def lcd_display_is_changed(self, current):
|
||||
if self.params['lcd_display'] is None:
|
||||
return False
|
||||
if 'lcd_display' not in current:
|
||||
return True
|
||||
if self.params['lcd_display'] == current['lcd_display']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def mgmt_dhcp_is_changed(self, current):
|
||||
if self.params['mgmt_dhcp'] is None:
|
||||
return False
|
||||
if 'mgmt_dhcp' not in current:
|
||||
return True
|
||||
if self.params['mgmt_dhcp'] == current['mgmt_dhcp']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def net_reboot_is_changed(self, current):
|
||||
if self.params['net_reboot'] is None:
|
||||
return False
|
||||
if 'net_reboot' not in current:
|
||||
return True
|
||||
if self.params['net_reboot'] == current['net_reboot']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def quiet_boot_is_changed(self, current):
|
||||
if self.params['quiet_boot'] is None:
|
||||
return False
|
||||
if 'quiet_boot' not in current:
|
||||
return True
|
||||
if self.params['quiet_boot'] == current['quiet_boot']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def console_timeout_is_changed(self, current):
|
||||
if self.params['console_timeout'] is None:
|
||||
return False
|
||||
if 'console_timeout' not in current:
|
||||
return True
|
||||
if self.params['console_timeout'] == current['console_timeout']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def format_sys_global_information(self, settings):
|
||||
result = dict()
|
||||
if hasattr(settings, 'guiSecurityBanner'):
|
||||
result['security_banner'] = str(settings.guiSecurityBanner)
|
||||
if hasattr(settings, 'guiSecurityBannerText'):
|
||||
result['banner_text'] = str(settings.guiSecurityBannerText)
|
||||
if hasattr(settings, 'guiSetup'):
|
||||
result['gui_setup'] = str(settings.guiSetup)
|
||||
if hasattr(settings, 'lcdDisplay'):
|
||||
result['lcd_display'] = str(settings.lcdDisplay)
|
||||
if hasattr(settings, 'mgmtDhcp'):
|
||||
result['mgmt_dhcp'] = str(settings.mgmtDhcp)
|
||||
if hasattr(settings, 'netReboot'):
|
||||
result['net_reboot'] = str(settings.netReboot)
|
||||
if hasattr(settings, 'quietBoot'):
|
||||
result['quiet_boot'] = str(settings.quietBoot)
|
||||
if hasattr(settings, 'consoleInactivityTimeout'):
|
||||
result['console_timeout'] = int(settings.consoleInactivityTimeout)
|
||||
return result
|
||||
|
||||
def update_sys_global_settings(self):
|
||||
params = self.get_changed_parameters()
|
||||
if params:
|
||||
self.changed_params = camel_dict_to_snake_dict(params)
|
||||
if self.params['check_mode']:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
self.update_sys_global_settings_on_device(params)
|
||||
return True
|
||||
|
||||
def update_sys_global_settings_on_device(self, params):
|
||||
tx = self.api.tm.transactions.transaction
|
||||
with TransactionContextManager(tx) as api:
|
||||
r = api.tm.sys.global_settings.load()
|
||||
r.update(**params)
|
||||
|
||||
|
||||
class BigIpSysGlobalModuleConfig(object):
|
||||
def __init__(self):
|
||||
self.argument_spec = dict()
|
||||
self.meta_args = dict()
|
||||
self.supports_check_mode = True
|
||||
self.states = ['present']
|
||||
self.on_off_choices = ['enabled', 'disabled']
|
||||
|
||||
self.initialize_meta_args()
|
||||
self.initialize_argument_spec()
|
||||
|
||||
def initialize_meta_args(self):
|
||||
args = dict(
|
||||
security_banner=dict(
|
||||
required=False,
|
||||
choices=self.on_off_choices,
|
||||
default=None
|
||||
),
|
||||
banner_text=dict(required=False, default=None),
|
||||
gui_setup=dict(
|
||||
required=False,
|
||||
choices=self.on_off_choices,
|
||||
default=None
|
||||
),
|
||||
lcd_display=dict(
|
||||
required=False,
|
||||
choices=self.on_off_choices,
|
||||
default=None
|
||||
),
|
||||
mgmt_dhcp=dict(
|
||||
required=False,
|
||||
choices=self.on_off_choices,
|
||||
default=None
|
||||
),
|
||||
net_reboot=dict(
|
||||
required=False,
|
||||
choices=self.on_off_choices,
|
||||
default=None
|
||||
),
|
||||
quiet_boot=dict(
|
||||
required=False,
|
||||
choices=self.on_off_choices,
|
||||
default=None
|
||||
),
|
||||
console_timeout=dict(required=False, type='int', default=None),
|
||||
state=dict(default='present', choices=['present'])
|
||||
)
|
||||
self.meta_args = args
|
||||
|
||||
def initialize_argument_spec(self):
|
||||
self.argument_spec = f5_argument_spec()
|
||||
self.argument_spec.update(self.meta_args)
|
||||
|
||||
def create(self):
|
||||
return AnsibleModule(
|
||||
argument_spec=self.argument_spec,
|
||||
supports_check_mode=self.supports_check_mode
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
config = BigIpSysGlobalModuleConfig()
|
||||
module = config.create()
|
||||
|
||||
try:
|
||||
obj = BigIpSysGlobalManager(
|
||||
check_mode=module.check_mode, **module.params
|
||||
)
|
||||
result = obj.apply_changes()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
717
lib/ansible/modules/network/f5/bigip_virtual_server.py
Normal file
717
lib/ansible/modules/network/f5/bigip_virtual_server.py
Normal file
@@ -0,0 +1,717 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Etienne Carriere <etienne.carriere@gmail.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: bigip_virtual_server
|
||||
short_description: "Manages F5 BIG-IP LTM virtual servers"
|
||||
description:
|
||||
- "Manages F5 BIG-IP LTM virtual servers via iControl SOAP API"
|
||||
version_added: "2.1"
|
||||
author:
|
||||
- Etienne Carriere (@Etienne-Carriere)
|
||||
- Tim Rupp (@caphrim007)
|
||||
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:
|
||||
state:
|
||||
description:
|
||||
- Virtual Server state
|
||||
- Absent, delete the VS if present
|
||||
- C(present) (and its synonym enabled), create if needed the VS and set
|
||||
state to enabled
|
||||
- C(disabled), create if needed the VS and set state to disabled
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
- enabled
|
||||
- disabled
|
||||
aliases: []
|
||||
partition:
|
||||
description:
|
||||
- Partition
|
||||
required: false
|
||||
default: 'Common'
|
||||
name:
|
||||
description:
|
||||
- Virtual server name
|
||||
required: true
|
||||
aliases:
|
||||
- vs
|
||||
destination:
|
||||
description:
|
||||
- Destination IP of the virtual server (only host is currently supported).
|
||||
Required when state=present and vs does not exist.
|
||||
required: true
|
||||
aliases:
|
||||
- address
|
||||
- ip
|
||||
port:
|
||||
description:
|
||||
- Port of the virtual server . Required when state=present and vs does not exist
|
||||
required: false
|
||||
default: None
|
||||
all_profiles:
|
||||
description:
|
||||
- List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used
|
||||
by the virtual server
|
||||
required: false
|
||||
default: None
|
||||
all_rules:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- List of rules to be applied in priority order
|
||||
required: false
|
||||
default: None
|
||||
enabled_vlans:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- List of vlans to be enabled. When a VLAN named C(ALL) is used, all
|
||||
VLANs will be allowed.
|
||||
required: false
|
||||
default: None
|
||||
pool:
|
||||
description:
|
||||
- Default pool for the virtual server
|
||||
required: false
|
||||
default: None
|
||||
snat:
|
||||
description:
|
||||
- Source network address policy
|
||||
required: false
|
||||
choices:
|
||||
- None
|
||||
- Automap
|
||||
- Name of a SNAT pool (eg "/Common/snat_pool_name") to enable SNAT with the specific pool
|
||||
default: None
|
||||
default_persistence_profile:
|
||||
description:
|
||||
- Default Profile which manages the session persistence
|
||||
required: false
|
||||
default: None
|
||||
route_advertisement_state:
|
||||
description:
|
||||
- Enable route advertisement for destination
|
||||
required: false
|
||||
default: disabled
|
||||
version_added: "2.3"
|
||||
description:
|
||||
description:
|
||||
- Virtual server description
|
||||
required: false
|
||||
default: None
|
||||
extends_documentation_fragment: f5
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add virtual server
|
||||
bigip_virtual_server:
|
||||
server: lb.mydomain.net
|
||||
user: admin
|
||||
password: secret
|
||||
state: present
|
||||
partition: MyPartition
|
||||
name: myvirtualserver
|
||||
destination: "{{ ansible_default_ipv4['address'] }}"
|
||||
port: 443
|
||||
pool: "{{ mypool }}"
|
||||
snat: Automap
|
||||
description: Test Virtual Server
|
||||
all_profiles:
|
||||
- http
|
||||
- clientssl
|
||||
enabled_vlans:
|
||||
- /Common/vlan2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Modify Port of the Virtual Server
|
||||
bigip_virtual_server:
|
||||
server: lb.mydomain.net
|
||||
user: admin
|
||||
password: secret
|
||||
state: present
|
||||
partition: MyPartition
|
||||
name: myvirtualserver
|
||||
port: 8080
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete virtual server
|
||||
bigip_virtual_server:
|
||||
server: lb.mydomain.net
|
||||
user: admin
|
||||
password: secret
|
||||
state: absent
|
||||
partition: MyPartition
|
||||
name: myvirtualserver
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
---
|
||||
deleted:
|
||||
description: Name of a virtual server that was deleted
|
||||
returned: changed
|
||||
type: string
|
||||
sample: "my-virtual-server"
|
||||
'''
|
||||
|
||||
|
||||
# map of state values
|
||||
STATES = {
|
||||
'enabled': 'STATE_ENABLED',
|
||||
'disabled': 'STATE_DISABLED'
|
||||
}
|
||||
|
||||
STATUSES = {
|
||||
'enabled': 'SESSION_STATUS_ENABLED',
|
||||
'disabled': 'SESSION_STATUS_DISABLED',
|
||||
'offline': 'SESSION_STATUS_FORCED_DISABLED'
|
||||
}
|
||||
|
||||
|
||||
def vs_exists(api, vs):
|
||||
# hack to determine if pool exists
|
||||
result = False
|
||||
try:
|
||||
api.LocalLB.VirtualServer.get_object_status(virtual_servers=[vs])
|
||||
result = True
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "was not found" in str(e):
|
||||
result = False
|
||||
else:
|
||||
# genuine exception
|
||||
raise
|
||||
return result
|
||||
|
||||
|
||||
def vs_create(api, name, destination, port, pool):
|
||||
_profiles = [[{'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': 'tcp'}]]
|
||||
created = False
|
||||
# a bit of a hack to handle concurrent runs of this module.
|
||||
# even though we've checked the vs doesn't exist,
|
||||
# it may exist by the time we run create_vs().
|
||||
# this catches the exception and does something smart
|
||||
# about it!
|
||||
try:
|
||||
api.LocalLB.VirtualServer.create(
|
||||
definitions=[{'name': [name], 'address': [destination], 'port': port, 'protocol': 'PROTOCOL_TCP'}],
|
||||
wildmasks=['255.255.255.255'],
|
||||
resources=[{'type': 'RESOURCE_TYPE_POOL', 'default_pool_name': pool}],
|
||||
profiles=_profiles)
|
||||
created = True
|
||||
return created
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "already exists" not in str(e):
|
||||
raise Exception('Error on creating Virtual Server : %s' % e)
|
||||
|
||||
|
||||
def vs_remove(api, name):
|
||||
api.LocalLB.VirtualServer.delete_virtual_server(
|
||||
virtual_servers=[name]
|
||||
)
|
||||
|
||||
|
||||
def get_rules(api, name):
|
||||
return api.LocalLB.VirtualServer.get_rule(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_rules(api, name, rules_list):
|
||||
updated = False
|
||||
if rules_list is None:
|
||||
return False
|
||||
rules_list = list(enumerate(rules_list))
|
||||
try:
|
||||
current_rules = map(lambda x: (x['priority'], x['rule_name']), get_rules(api, name))
|
||||
to_add_rules = []
|
||||
for i, x in rules_list:
|
||||
if (i, x) not in current_rules:
|
||||
to_add_rules.append({'priority': i, 'rule_name': x})
|
||||
to_del_rules = []
|
||||
for i, x in current_rules:
|
||||
if (i, x) not in rules_list:
|
||||
to_del_rules.append({'priority': i, 'rule_name': x})
|
||||
if len(to_del_rules) > 0:
|
||||
api.LocalLB.VirtualServer.remove_rule(
|
||||
virtual_servers=[name],
|
||||
rules=[to_del_rules]
|
||||
)
|
||||
updated = True
|
||||
if len(to_add_rules) > 0:
|
||||
api.LocalLB.VirtualServer.add_rule(
|
||||
virtual_servers=[name],
|
||||
rules=[to_add_rules]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting rules : %s' % e)
|
||||
|
||||
|
||||
def get_profiles(api, name):
|
||||
return api.LocalLB.VirtualServer.get_profile(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_profiles(api, name, profiles_list):
|
||||
updated = False
|
||||
try:
|
||||
if profiles_list is None:
|
||||
return False
|
||||
current_profiles = list(map(lambda x: x['profile_name'], get_profiles(api, name)))
|
||||
to_add_profiles = []
|
||||
for x in profiles_list:
|
||||
if x not in current_profiles:
|
||||
to_add_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x})
|
||||
to_del_profiles = []
|
||||
for x in current_profiles:
|
||||
if (x not in profiles_list) and (x != "/Common/tcp"):
|
||||
to_del_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x})
|
||||
if len(to_del_profiles) > 0:
|
||||
api.LocalLB.VirtualServer.remove_profile(
|
||||
virtual_servers=[name],
|
||||
profiles=[to_del_profiles]
|
||||
)
|
||||
updated = True
|
||||
if len(to_add_profiles) > 0:
|
||||
api.LocalLB.VirtualServer.add_profile(
|
||||
virtual_servers=[name],
|
||||
profiles=[to_add_profiles]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting profiles : %s' % e)
|
||||
|
||||
|
||||
def get_vlan(api, name):
|
||||
return api.LocalLB.VirtualServer.get_vlan(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_enabled_vlans(api, name, vlans_enabled_list):
|
||||
updated = False
|
||||
to_add_vlans = []
|
||||
try:
|
||||
if vlans_enabled_list is None:
|
||||
return updated
|
||||
current_vlans = get_vlan(api, name)
|
||||
|
||||
# Set allowed list back to default ("all")
|
||||
#
|
||||
# This case allows you to undo what you may have previously done.
|
||||
# The default case is "All VLANs and Tunnels". This case will handle
|
||||
# that situation.
|
||||
if 'ALL' in vlans_enabled_list:
|
||||
# The user is coming from a situation where they previously
|
||||
# were specifying a list of allowed VLANs
|
||||
if len(current_vlans['vlans']) > 0 or \
|
||||
current_vlans['state'] is "STATE_ENABLED":
|
||||
api.LocalLB.VirtualServer.set_vlan(
|
||||
virtual_servers=[name],
|
||||
vlans=[{'state': 'STATE_DISABLED', 'vlans': []}]
|
||||
)
|
||||
updated = True
|
||||
else:
|
||||
if current_vlans['state'] is "STATE_DISABLED":
|
||||
to_add_vlans = vlans_enabled_list
|
||||
else:
|
||||
for vlan in vlans_enabled_list:
|
||||
if vlan not in current_vlans['vlans']:
|
||||
updated = True
|
||||
to_add_vlans = vlans_enabled_list
|
||||
break
|
||||
if updated:
|
||||
api.LocalLB.VirtualServer.set_vlan(
|
||||
virtual_servers=[name],
|
||||
vlans=[{
|
||||
'state': 'STATE_ENABLED',
|
||||
'vlans': [to_add_vlans]
|
||||
}]
|
||||
)
|
||||
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting enabled vlans : %s' % e)
|
||||
|
||||
|
||||
def set_snat(api, name, snat):
|
||||
updated = False
|
||||
try:
|
||||
current_state = get_snat_type(api, name)
|
||||
current_snat_pool = get_snat_pool(api, name)
|
||||
if snat is None:
|
||||
return updated
|
||||
elif snat == 'None' and current_state != 'SRC_TRANS_NONE':
|
||||
api.LocalLB.VirtualServer.set_source_address_translation_none(
|
||||
virtual_servers=[name]
|
||||
)
|
||||
updated = True
|
||||
elif snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP':
|
||||
api.LocalLB.VirtualServer.set_source_address_translation_automap(
|
||||
virtual_servers=[name]
|
||||
)
|
||||
updated = True
|
||||
elif snat_settings_need_updating(snat, current_state, current_snat_pool):
|
||||
api.LocalLB.VirtualServer.set_source_address_translation_snat_pool(
|
||||
virtual_servers=[name],
|
||||
pools=[snat]
|
||||
)
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting snat : %s' % e)
|
||||
|
||||
|
||||
def get_snat_type(api, name):
|
||||
return api.LocalLB.VirtualServer.get_source_address_translation_type(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def get_snat_pool(api, name):
|
||||
return api.LocalLB.VirtualServer.get_source_address_translation_snat_pool(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def snat_settings_need_updating(snat, current_state, current_snat_pool):
|
||||
if snat == 'None' or snat == 'Automap':
|
||||
return False
|
||||
elif snat and current_state != 'SRC_TRANS_SNATPOOL':
|
||||
return True
|
||||
elif snat and current_state == 'SRC_TRANS_SNATPOOL' and current_snat_pool != snat:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_pool(api, name):
|
||||
return api.LocalLB.VirtualServer.get_default_pool_name(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_pool(api, name, pool):
|
||||
updated = False
|
||||
try:
|
||||
current_pool = get_pool(api, name)
|
||||
if pool is not None and (pool != current_pool):
|
||||
api.LocalLB.VirtualServer.set_default_pool_name(
|
||||
virtual_servers=[name],
|
||||
default_pools=[pool]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting pool : %s' % e)
|
||||
|
||||
|
||||
def get_destination(api, name):
|
||||
return api.LocalLB.VirtualServer.get_destination_v2(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_destination(api, name, destination):
|
||||
updated = False
|
||||
try:
|
||||
current_destination = get_destination(api, name)
|
||||
if destination is not None and destination != current_destination['address']:
|
||||
api.LocalLB.VirtualServer.set_destination_v2(
|
||||
virtual_servers=[name],
|
||||
destinations=[{'address': destination, 'port': current_destination['port']}]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting destination : %s' % e)
|
||||
|
||||
|
||||
def set_port(api, name, port):
|
||||
updated = False
|
||||
try:
|
||||
current_destination = get_destination(api, name)
|
||||
if port is not None and port != current_destination['port']:
|
||||
api.LocalLB.VirtualServer.set_destination_v2(
|
||||
virtual_servers=[name],
|
||||
destinations=[{'address': current_destination['address'], 'port': port}]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting port : %s' % e)
|
||||
|
||||
|
||||
def get_state(api, name):
|
||||
return api.LocalLB.VirtualServer.get_enabled_state(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_state(api, name, state):
|
||||
updated = False
|
||||
try:
|
||||
current_state = get_state(api, name)
|
||||
# We consider that being present is equivalent to enabled
|
||||
if state == 'present':
|
||||
state = 'enabled'
|
||||
if STATES[state] != current_state:
|
||||
api.LocalLB.VirtualServer.set_enabled_state(
|
||||
virtual_servers=[name],
|
||||
states=[STATES[state]]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting state : %s' % e)
|
||||
|
||||
|
||||
def get_description(api, name):
|
||||
return api.LocalLB.VirtualServer.get_description(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_description(api, name, description):
|
||||
updated = False
|
||||
try:
|
||||
current_description = get_description(api, name)
|
||||
if description is not None and current_description != description:
|
||||
api.LocalLB.VirtualServer.set_description(
|
||||
virtual_servers=[name],
|
||||
descriptions=[description]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting description : %s ' % e)
|
||||
|
||||
|
||||
def get_persistence_profiles(api, name):
|
||||
return api.LocalLB.VirtualServer.get_persistence_profile(
|
||||
virtual_servers=[name]
|
||||
)[0]
|
||||
|
||||
|
||||
def set_default_persistence_profiles(api, name, persistence_profile):
|
||||
updated = False
|
||||
if persistence_profile is None:
|
||||
return updated
|
||||
try:
|
||||
current_persistence_profiles = get_persistence_profiles(api, name)
|
||||
default = None
|
||||
for profile in current_persistence_profiles:
|
||||
if profile['default_profile']:
|
||||
default = profile['profile_name']
|
||||
break
|
||||
if default is not None and default != persistence_profile:
|
||||
api.LocalLB.VirtualServer.remove_persistence_profile(
|
||||
virtual_servers=[name],
|
||||
profiles=[[{'profile_name': default, 'default_profile': True}]]
|
||||
)
|
||||
if default != persistence_profile:
|
||||
api.LocalLB.VirtualServer.add_persistence_profile(
|
||||
virtual_servers=[name],
|
||||
profiles=[[{'profile_name': persistence_profile, 'default_profile': True}]]
|
||||
)
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting default persistence profile : %s' % e)
|
||||
|
||||
|
||||
def get_route_advertisement_status(api, address):
|
||||
result = api.LocalLB.VirtualAddressV2.get_route_advertisement_state(virtual_addresses=[address]).pop(0)
|
||||
result = result.split("STATE_")[-1].lower()
|
||||
return result
|
||||
|
||||
|
||||
def set_route_advertisement_state(api, destination, partition, route_advertisement_state):
|
||||
updated = False
|
||||
|
||||
try:
|
||||
state = "STATE_%s" % route_advertisement_state.strip().upper()
|
||||
address = fq_name(partition, destination,)
|
||||
current_route_advertisement_state=get_route_advertisement_status(api,address)
|
||||
if current_route_advertisement_state != route_advertisement_state:
|
||||
api.LocalLB.VirtualAddressV2.set_route_advertisement_state(virtual_addresses=[address], states=[state])
|
||||
updated = True
|
||||
return updated
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on setting profiles : %s' % e)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
state=dict(type='str', default='present',
|
||||
choices=['present', 'absent', 'disabled', 'enabled']),
|
||||
name=dict(type='str', required=True, aliases=['vs']),
|
||||
destination=dict(type='str', aliases=['address', 'ip']),
|
||||
port=dict(type='int'),
|
||||
all_profiles=dict(type='list'),
|
||||
all_rules=dict(type='list'),
|
||||
enabled_vlans=dict(type='list'),
|
||||
pool=dict(type='str'),
|
||||
description=dict(type='str'),
|
||||
snat=dict(type='str'),
|
||||
route_advertisement_state=dict(type='str', default='disabled', choices=['enabled', 'disabled']),
|
||||
default_persistence_profile=dict(type='str')
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if not bigsuds_found:
|
||||
module.fail_json(msg="the python bigsuds module is required")
|
||||
|
||||
if module.params['validate_certs']:
|
||||
import ssl
|
||||
if not hasattr(ssl, 'SSLContext'):
|
||||
module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task')
|
||||
|
||||
server = module.params['server']
|
||||
server_port = module.params['server_port']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
state = module.params['state']
|
||||
partition = module.params['partition']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
name = fq_name(partition, module.params['name'])
|
||||
destination = module.params['destination']
|
||||
port = module.params['port']
|
||||
all_profiles = fq_list_names(partition, module.params['all_profiles'])
|
||||
all_rules = fq_list_names(partition, module.params['all_rules'])
|
||||
|
||||
enabled_vlans = module.params['enabled_vlans']
|
||||
if enabled_vlans is None or 'ALL' in enabled_vlans:
|
||||
all_enabled_vlans = enabled_vlans
|
||||
else:
|
||||
all_enabled_vlans = fq_list_names(partition, enabled_vlans)
|
||||
|
||||
pool = fq_name(partition, module.params['pool'])
|
||||
description = module.params['description']
|
||||
snat = module.params['snat']
|
||||
route_advertisement_state = module.params['route_advertisement_state']
|
||||
default_persistence_profile = fq_name(partition, module.params['default_persistence_profile'])
|
||||
|
||||
if 1 > port > 65535:
|
||||
module.fail_json(msg="valid ports must be in range 1 - 65535")
|
||||
|
||||
try:
|
||||
api = bigip_api(server, user, password, validate_certs, port=server_port)
|
||||
result = {'changed': False} # default
|
||||
|
||||
if state == 'absent':
|
||||
if not module.check_mode:
|
||||
if vs_exists(api, name):
|
||||
# hack to handle concurrent runs of module
|
||||
# pool might be gone before we actually remove
|
||||
try:
|
||||
vs_remove(api, name)
|
||||
result = {'changed': True, 'deleted': name}
|
||||
except bigsuds.OperationFailed as e:
|
||||
if "was not found" in str(e):
|
||||
result['changed'] = False
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
|
||||
else:
|
||||
update = False
|
||||
if not vs_exists(api, name):
|
||||
if (not destination) or (not port):
|
||||
module.fail_json(msg="both destination and port must be supplied to create a VS")
|
||||
if not module.check_mode:
|
||||
# a bit of a hack to handle concurrent runs of this module.
|
||||
# even though we've checked the virtual_server doesn't exist,
|
||||
# it may exist by the time we run virtual_server().
|
||||
# this catches the exception and does something smart
|
||||
# about it!
|
||||
try:
|
||||
vs_create(api, name, destination, port, pool)
|
||||
set_profiles(api, name, all_profiles)
|
||||
set_enabled_vlans(api, name, all_enabled_vlans)
|
||||
set_rules(api, name, all_rules)
|
||||
set_snat(api, name, snat)
|
||||
set_description(api, name, description)
|
||||
set_default_persistence_profiles(api, name, default_persistence_profile)
|
||||
set_state(api, name, state)
|
||||
set_route_advertisement_state(api, destination, partition, route_advertisement_state)
|
||||
result = {'changed': True}
|
||||
except bigsuds.OperationFailed as e:
|
||||
raise Exception('Error on creating Virtual Server : %s' % e)
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
else:
|
||||
update = True
|
||||
if update:
|
||||
# VS exists
|
||||
if not module.check_mode:
|
||||
# Have a transaction for all the changes
|
||||
try:
|
||||
api.System.Session.start_transaction()
|
||||
result['changed'] |= set_destination(api, name, fq_name(partition, destination))
|
||||
result['changed'] |= set_port(api, name, port)
|
||||
result['changed'] |= set_pool(api, name, pool)
|
||||
result['changed'] |= set_description(api, name, description)
|
||||
result['changed'] |= set_snat(api, name, snat)
|
||||
result['changed'] |= set_profiles(api, name, all_profiles)
|
||||
result['changed'] |= set_enabled_vlans(api, name, all_enabled_vlans)
|
||||
result['changed'] |= set_rules(api, name, all_rules)
|
||||
result['changed'] |= set_default_persistence_profiles(api, name, default_persistence_profile)
|
||||
result['changed'] |= set_state(api, name, state)
|
||||
result['changed'] |= set_route_advertisement_state(api, destination, partition, route_advertisement_state)
|
||||
api.System.Session.submit_transaction()
|
||||
except Exception as e:
|
||||
raise Exception("Error on updating Virtual Server : %s" % e)
|
||||
else:
|
||||
# check-mode return value
|
||||
result = {'changed': True}
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="received exception: %s" % e)
|
||||
|
||||
module.exit_json(**result)
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
451
lib/ansible/modules/network/f5/bigip_vlan.py
Normal file
451
lib/ansible/modules/network/f5/bigip_vlan.py
Normal file
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 F5 Networks Inc.
|
||||
#
|
||||
# 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: bigip_vlan
|
||||
short_description: Manage VLANs on a BIG-IP system
|
||||
description:
|
||||
- Manage VLANs on a BIG-IP system
|
||||
version_added: "2.2"
|
||||
options:
|
||||
description:
|
||||
description:
|
||||
- The description to give to the VLAN.
|
||||
tagged_interfaces:
|
||||
description:
|
||||
- Specifies a list of tagged interfaces and trunks that you want to
|
||||
configure for the VLAN. Use tagged interfaces or trunks when
|
||||
you want to assign a single interface or trunk to multiple VLANs.
|
||||
required: false
|
||||
aliases:
|
||||
- tagged_interface
|
||||
untagged_interfaces:
|
||||
description:
|
||||
- Specifies a list of untagged interfaces and trunks that you want to
|
||||
configure for the VLAN.
|
||||
required: false
|
||||
aliases:
|
||||
- untagged_interface
|
||||
name:
|
||||
description:
|
||||
- The VLAN to manage. If the special VLAN C(ALL) is specified with
|
||||
the C(state) value of C(absent) then all VLANs will be removed.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- The state of the VLAN on the system. When C(present), guarantees
|
||||
that the VLAN exists with the provided attributes. When C(absent),
|
||||
removes the VLAN from the system.
|
||||
required: false
|
||||
default: present
|
||||
choices:
|
||||
- absent
|
||||
- present
|
||||
tag:
|
||||
description:
|
||||
- Tag number for the VLAN. The tag number can be any integer between 1
|
||||
and 4094. The system automatically assigns a tag number if you do not
|
||||
specify a value.
|
||||
notes:
|
||||
- Requires the f5-sdk Python package on the host. This is as easy as pip
|
||||
install f5-sdk.
|
||||
- Requires BIG-IP versions >= 12.0.0
|
||||
extends_documentation_fragment: f5
|
||||
requirements:
|
||||
- f5-sdk
|
||||
author:
|
||||
- Tim Rupp (@caphrim007)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create VLAN
|
||||
bigip_vlan:
|
||||
name: "net1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Set VLAN tag
|
||||
bigip_vlan:
|
||||
name: "net1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
tag: "2345"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Add VLAN 2345 as tagged to interface 1.1
|
||||
bigip_vlan:
|
||||
tagged_interface: 1.1
|
||||
name: "net1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
tag: "2345"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Add VLAN 1234 as tagged to interfaces 1.1 and 1.2
|
||||
bigip_vlan:
|
||||
tagged_interfaces:
|
||||
- 1.1
|
||||
- 1.2
|
||||
name: "net1"
|
||||
password: "secret"
|
||||
server: "lb.mydomain.com"
|
||||
tag: "1234"
|
||||
user: "admin"
|
||||
validate_certs: "no"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
description:
|
||||
description: The description set on the VLAN
|
||||
returned: changed
|
||||
type: string
|
||||
sample: foo VLAN
|
||||
interfaces:
|
||||
description: Interfaces that the VLAN is assigned to
|
||||
returned: changed
|
||||
type: list
|
||||
sample: ['1.1','1.2']
|
||||
name:
|
||||
description: The name of the VLAN
|
||||
returned: changed
|
||||
type: string
|
||||
sample: net1
|
||||
partition:
|
||||
description: The partition that the VLAN was created on
|
||||
returned: changed
|
||||
type: string
|
||||
sample: Common
|
||||
tag:
|
||||
description: The ID of the VLAN
|
||||
returned: changed
|
||||
type: int
|
||||
sample: 2345
|
||||
'''
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot
|
||||
from icontrol.session import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
class BigIpVlan(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not HAS_F5SDK:
|
||||
raise F5ModuleError("The python f5-sdk module is required")
|
||||
|
||||
# The params that change in the module
|
||||
self.cparams = dict()
|
||||
|
||||
# Stores the params that are sent to the module
|
||||
self.params = kwargs
|
||||
self.api = ManagementRoot(kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'])
|
||||
|
||||
def present(self):
|
||||
if self.exists():
|
||||
return self.update()
|
||||
else:
|
||||
return self.create()
|
||||
|
||||
def absent(self):
|
||||
changed = False
|
||||
|
||||
if self.exists():
|
||||
changed = self.delete()
|
||||
|
||||
return changed
|
||||
|
||||
def read(self):
|
||||
"""Read information and transform it
|
||||
|
||||
The values that are returned by BIG-IP in the f5-sdk can have encoding
|
||||
attached to them as well as be completely missing in some cases.
|
||||
|
||||
Therefore, this method will transform the data from the BIG-IP into a
|
||||
format that is more easily consumable by the rest of the class and the
|
||||
parameters that are supported by the module.
|
||||
"""
|
||||
p = dict()
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
r = self.api.tm.net.vlans.vlan.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
ifcs = r.interfaces_s.get_collection()
|
||||
if hasattr(r, 'tag'):
|
||||
p['tag'] = int(r.tag)
|
||||
if hasattr(r, 'description'):
|
||||
p['description'] = str(r.description)
|
||||
if len(ifcs) is not 0:
|
||||
untagged = []
|
||||
tagged = []
|
||||
for x in ifcs:
|
||||
if hasattr(x, 'tagged'):
|
||||
tagged.append(str(x.name))
|
||||
elif hasattr(x, 'untagged'):
|
||||
untagged.append(str(x.name))
|
||||
if untagged:
|
||||
p['untagged_interfaces'] = list(set(untagged))
|
||||
if tagged:
|
||||
p['tagged_interfaces'] = list(set(tagged))
|
||||
p['name'] = name
|
||||
return p
|
||||
|
||||
def create(self):
|
||||
params = dict()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
description = self.params['description']
|
||||
name = self.params['name']
|
||||
untagged_interfaces = self.params['untagged_interfaces']
|
||||
tagged_interfaces = self.params['tagged_interfaces']
|
||||
partition = self.params['partition']
|
||||
tag = self.params['tag']
|
||||
|
||||
if tag is not None:
|
||||
params['tag'] = tag
|
||||
|
||||
if untagged_interfaces is not None or tagged_interfaces is not None:
|
||||
tmp = []
|
||||
ifcs = self.api.tm.net.interfaces.get_collection()
|
||||
ifcs = [str(x.name) for x in ifcs]
|
||||
|
||||
if len(ifcs) is 0:
|
||||
raise F5ModuleError(
|
||||
'No interfaces were found'
|
||||
)
|
||||
|
||||
pinterfaces = []
|
||||
if untagged_interfaces:
|
||||
interfaces = untagged_interfaces
|
||||
elif tagged_interfaces:
|
||||
interfaces = tagged_interfaces
|
||||
|
||||
for ifc in interfaces:
|
||||
ifc = str(ifc)
|
||||
if ifc in ifcs:
|
||||
pinterfaces.append(ifc)
|
||||
|
||||
if tagged_interfaces:
|
||||
tmp = [dict(name=x, tagged=True) for x in pinterfaces]
|
||||
elif untagged_interfaces:
|
||||
tmp = [dict(name=x, untagged=True) for x in pinterfaces]
|
||||
|
||||
if tmp:
|
||||
params['interfaces'] = tmp
|
||||
|
||||
if description is not None:
|
||||
params['description'] = self.params['description']
|
||||
|
||||
params['name'] = name
|
||||
params['partition'] = partition
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
d = self.api.tm.net.vlans.vlan
|
||||
d.create(**params)
|
||||
|
||||
if self.exists():
|
||||
return True
|
||||
else:
|
||||
raise F5ModuleError("Failed to create the VLAN")
|
||||
|
||||
def update(self):
|
||||
changed = False
|
||||
params = dict()
|
||||
current = self.read()
|
||||
|
||||
check_mode = self.params['check_mode']
|
||||
description = self.params['description']
|
||||
name = self.params['name']
|
||||
tag = self.params['tag']
|
||||
partition = self.params['partition']
|
||||
tagged_interfaces = self.params['tagged_interfaces']
|
||||
untagged_interfaces = self.params['untagged_interfaces']
|
||||
|
||||
if untagged_interfaces is not None or tagged_interfaces is not None:
|
||||
ifcs = self.api.tm.net.interfaces.get_collection()
|
||||
ifcs = [str(x.name) for x in ifcs]
|
||||
|
||||
if len(ifcs) is 0:
|
||||
raise F5ModuleError(
|
||||
'No interfaces were found'
|
||||
)
|
||||
|
||||
pinterfaces = []
|
||||
if untagged_interfaces:
|
||||
interfaces = untagged_interfaces
|
||||
elif tagged_interfaces:
|
||||
interfaces = tagged_interfaces
|
||||
|
||||
for ifc in interfaces:
|
||||
ifc = str(ifc)
|
||||
if ifc in ifcs:
|
||||
pinterfaces.append(ifc)
|
||||
else:
|
||||
raise F5ModuleError(
|
||||
'The specified interface "%s" was not found' % (ifc)
|
||||
)
|
||||
|
||||
if tagged_interfaces:
|
||||
tmp = [dict(name=x, tagged=True) for x in pinterfaces]
|
||||
if 'tagged_interfaces' in current:
|
||||
if pinterfaces != current['tagged_interfaces']:
|
||||
params['interfaces'] = tmp
|
||||
else:
|
||||
params['interfaces'] = tmp
|
||||
elif untagged_interfaces:
|
||||
tmp = [dict(name=x, untagged=True) for x in pinterfaces]
|
||||
if 'untagged_interfaces' in current:
|
||||
if pinterfaces != current['untagged_interfaces']:
|
||||
params['interfaces'] = tmp
|
||||
else:
|
||||
params['interfaces'] = tmp
|
||||
|
||||
if description is not None:
|
||||
if 'description' in current:
|
||||
if description != current['description']:
|
||||
params['description'] = description
|
||||
else:
|
||||
params['description'] = description
|
||||
|
||||
if tag is not None:
|
||||
if 'tag' in current:
|
||||
if tag != current['tag']:
|
||||
params['tag'] = tag
|
||||
else:
|
||||
params['tag'] = tag
|
||||
|
||||
if params:
|
||||
changed = True
|
||||
params['name'] = name
|
||||
params['partition'] = partition
|
||||
if check_mode:
|
||||
return changed
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
else:
|
||||
return changed
|
||||
|
||||
r = self.api.tm.net.vlans.vlan.load(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
r.update(**params)
|
||||
r.refresh()
|
||||
|
||||
return True
|
||||
|
||||
def delete(self):
|
||||
params = dict()
|
||||
check_mode = self.params['check_mode']
|
||||
|
||||
params['name'] = self.params['name']
|
||||
params['partition'] = self.params['partition']
|
||||
|
||||
self.cparams = camel_dict_to_snake_dict(params)
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
dc = self.api.tm.net.vlans.vlan.load(**params)
|
||||
dc.delete()
|
||||
|
||||
if self.exists():
|
||||
raise F5ModuleError("Failed to delete the VLAN")
|
||||
return True
|
||||
|
||||
def exists(self):
|
||||
name = self.params['name']
|
||||
partition = self.params['partition']
|
||||
return self.api.tm.net.vlans.vlan.exists(
|
||||
name=name,
|
||||
partition=partition
|
||||
)
|
||||
|
||||
def flush(self):
|
||||
result = dict()
|
||||
state = self.params['state']
|
||||
|
||||
try:
|
||||
if state == "present":
|
||||
changed = self.present()
|
||||
elif state == "absent":
|
||||
changed = self.absent()
|
||||
except iControlUnexpectedHTTPError as e:
|
||||
raise F5ModuleError(str(e))
|
||||
|
||||
result.update(**self.cparams)
|
||||
result.update(dict(changed=changed))
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = f5_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
description=dict(required=False, default=None),
|
||||
tagged_interfaces=dict(required=False, default=None, type='list', aliases=['tagged_interface']),
|
||||
untagged_interfaces=dict(required=False, default=None, type='list', aliases=['untagged_interface']),
|
||||
name=dict(required=True),
|
||||
tag=dict(required=False, default=None, type='int')
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[
|
||||
['tagged_interfaces', 'untagged_interfaces']
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
obj = BigIpVlan(check_mode=module.check_mode, **module.params)
|
||||
result = obj.flush()
|
||||
|
||||
module.exit_json(**result)
|
||||
except F5ModuleError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
|
||||
from ansible.module_utils.f5 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
394
lib/ansible/modules/network/haproxy.py
Normal file
394
lib/ansible/modules/network/haproxy.py
Normal file
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Ravi Bhure <ravibhure@gmail.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: haproxy
|
||||
version_added: "1.9"
|
||||
short_description: Enable, disable, and set weights for HAProxy backend servers using socket commands.
|
||||
author: "Ravi Bhure (@ravibhure)"
|
||||
description:
|
||||
- Enable, disable, and set weights for HAProxy backend servers using socket
|
||||
commands.
|
||||
notes:
|
||||
- Enable and disable commands are restricted and can only be issued on
|
||||
sockets configured for level 'admin'. For example, you can add the line
|
||||
'stats socket /var/run/haproxy.sock level admin' to the general section of
|
||||
haproxy.cfg. See http://haproxy.1wt.eu/download/1.5/doc/configuration.txt.
|
||||
options:
|
||||
backend:
|
||||
description:
|
||||
- Name of the HAProxy backend pool.
|
||||
required: false
|
||||
default: auto-detected
|
||||
host:
|
||||
description:
|
||||
- Name of the backend host to change.
|
||||
required: true
|
||||
default: null
|
||||
shutdown_sessions:
|
||||
description:
|
||||
- When disabling a server, immediately terminate all the sessions attached
|
||||
to the specified server. This can be used to terminate long-running
|
||||
sessions after a server is put into maintenance mode.
|
||||
required: false
|
||||
default: false
|
||||
socket:
|
||||
description:
|
||||
- Path to the HAProxy socket file.
|
||||
required: false
|
||||
default: /var/run/haproxy.sock
|
||||
state:
|
||||
description:
|
||||
- Desired state of the provided backend host.
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "enabled", "disabled" ]
|
||||
fail_on_not_found:
|
||||
description:
|
||||
- Fail whenever trying to enable/disable a backend host that does not exist
|
||||
required: false
|
||||
default: false
|
||||
version_added: "2.2"
|
||||
wait:
|
||||
description:
|
||||
- Wait until the server reports a status of 'UP' when `state=enabled`, or
|
||||
status of 'MAINT' when `state=disabled`.
|
||||
required: false
|
||||
default: false
|
||||
version_added: "2.0"
|
||||
wait_interval:
|
||||
description:
|
||||
- Number of seconds to wait between retries.
|
||||
required: false
|
||||
default: 5
|
||||
version_added: "2.0"
|
||||
wait_retries:
|
||||
description:
|
||||
- Number of times to check for status after changing the state.
|
||||
required: false
|
||||
default: 25
|
||||
version_added: "2.0"
|
||||
weight:
|
||||
description:
|
||||
- The value passed in argument. If the value ends with the `%` sign, then
|
||||
the new weight will be relative to the initially configured weight.
|
||||
Relative weights are only permitted between 0 and 100% and absolute
|
||||
weights are permitted between 0 and 256.
|
||||
required: false
|
||||
default: null
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# disable server in 'www' backend pool
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
|
||||
# disable server without backend pool name (apply to all available backend pool)
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
|
||||
# disable server, provide socket file
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
socket: /var/run/haproxy.sock
|
||||
backend: www
|
||||
|
||||
# disable server, provide socket file, wait until status reports in maintenance
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
socket: /var/run/haproxy.sock
|
||||
backend: www
|
||||
wait: yes
|
||||
|
||||
# disable backend server in 'www' backend pool and drop open sessions to it
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
socket: /var/run/haproxy.sock
|
||||
shutdown_sessions: true
|
||||
|
||||
# disable server without backend pool name (apply to all available backend pool) but fail when the backend host is not found
|
||||
- haproxy:
|
||||
state: disabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
fail_on_not_found: yes
|
||||
|
||||
# enable server in 'www' backend pool
|
||||
- haproxy:
|
||||
state: enabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
|
||||
# enable server in 'www' backend pool wait until healthy
|
||||
- haproxy:
|
||||
state: enabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
wait: yes
|
||||
|
||||
# enable server in 'www' backend pool wait until healthy. Retry 10 times with intervals of 5 seconds to retrieve the health
|
||||
- haproxy:
|
||||
state: enabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
backend: www
|
||||
wait: yes
|
||||
wait_retries: 10
|
||||
wait_interval: 5
|
||||
|
||||
# enable server in 'www' backend pool with change server(s) weight
|
||||
- haproxy:
|
||||
state: enabled
|
||||
host: '{{ inventory_hostname }}'
|
||||
socket: /var/run/haproxy.sock
|
||||
weight: 10
|
||||
backend: www
|
||||
'''
|
||||
|
||||
import socket
|
||||
import csv
|
||||
import time
|
||||
from string import Template
|
||||
|
||||
|
||||
DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock"
|
||||
RECV_SIZE = 1024
|
||||
ACTION_CHOICES = ['enabled', 'disabled']
|
||||
WAIT_RETRIES=25
|
||||
WAIT_INTERVAL=5
|
||||
|
||||
######################################################################
|
||||
class TimeoutException(Exception):
|
||||
pass
|
||||
|
||||
class HAProxy(object):
|
||||
"""
|
||||
Used for communicating with HAProxy through its local UNIX socket interface.
|
||||
Perform common tasks in Haproxy related to enable server and
|
||||
disable server.
|
||||
|
||||
The complete set of external commands Haproxy handles is documented
|
||||
on their website:
|
||||
|
||||
http://haproxy.1wt.eu/download/1.5/doc/configuration.txt#Unix Socket commands
|
||||
"""
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.state = self.module.params['state']
|
||||
self.host = self.module.params['host']
|
||||
self.backend = self.module.params['backend']
|
||||
self.weight = self.module.params['weight']
|
||||
self.socket = self.module.params['socket']
|
||||
self.shutdown_sessions = self.module.params['shutdown_sessions']
|
||||
self.fail_on_not_found = self.module.params['fail_on_not_found']
|
||||
self.wait = self.module.params['wait']
|
||||
self.wait_retries = self.module.params['wait_retries']
|
||||
self.wait_interval = self.module.params['wait_interval']
|
||||
self.command_results = {}
|
||||
|
||||
def execute(self, cmd, timeout=200, capture_output=True):
|
||||
"""
|
||||
Executes a HAProxy command by sending a message to a HAProxy's local
|
||||
UNIX socket and waiting up to 'timeout' milliseconds for the response.
|
||||
"""
|
||||
self.client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.client.connect(self.socket)
|
||||
self.client.sendall('%s\n' % cmd)
|
||||
result = ''
|
||||
buf = ''
|
||||
buf = self.client.recv(RECV_SIZE)
|
||||
while buf:
|
||||
result += buf
|
||||
buf = self.client.recv(RECV_SIZE)
|
||||
if capture_output:
|
||||
self.capture_command_output(cmd, result.strip())
|
||||
self.client.close()
|
||||
return result
|
||||
|
||||
|
||||
def capture_command_output(self, cmd, output):
|
||||
"""
|
||||
Capture the output for a command
|
||||
"""
|
||||
if 'command' not in self.command_results:
|
||||
self.command_results['command'] = []
|
||||
self.command_results['command'].append(cmd)
|
||||
if 'output' not in self.command_results:
|
||||
self.command_results['output'] = []
|
||||
self.command_results['output'].append(output)
|
||||
|
||||
|
||||
def discover_all_backends(self):
|
||||
"""
|
||||
Discover all entries with svname = 'BACKEND' and return a list of their corresponding
|
||||
pxnames
|
||||
"""
|
||||
data = self.execute('show stat', 200, False).lstrip('# ')
|
||||
r = csv.DictReader(data.splitlines())
|
||||
return tuple(map(lambda d: d['pxname'], filter(lambda d: d['svname'] == 'BACKEND', r)))
|
||||
|
||||
|
||||
def execute_for_backends(self, cmd, pxname, svname, wait_for_status = None):
|
||||
"""
|
||||
Run some command on the specified backends. If no backends are provided they will
|
||||
be discovered automatically (all backends)
|
||||
"""
|
||||
# Discover backends if none are given
|
||||
if pxname is None:
|
||||
backends = self.discover_all_backends()
|
||||
else:
|
||||
backends = [pxname]
|
||||
|
||||
# Run the command for each requested backend
|
||||
for backend in backends:
|
||||
# Fail when backends were not found
|
||||
state = self.get_state_for(backend, svname)
|
||||
if (self.fail_on_not_found or self.wait) and state is None:
|
||||
self.module.fail_json(msg="The specified backend '%s/%s' was not found!" % (backend, svname))
|
||||
|
||||
self.execute(Template(cmd).substitute(pxname = backend, svname = svname))
|
||||
if self.wait:
|
||||
self.wait_until_status(backend, svname, wait_for_status)
|
||||
|
||||
|
||||
def get_state_for(self, pxname, svname):
|
||||
"""
|
||||
Find the state of specific services. When pxname is not set, get all backends for a specific host.
|
||||
Returns a list of dictionaries containing the status and weight for those services.
|
||||
"""
|
||||
data = self.execute('show stat', 200, False).lstrip('# ')
|
||||
r = csv.DictReader(data.splitlines())
|
||||
state = tuple(map(lambda d: { 'status': d['status'], 'weight': d['weight'] }, filter(lambda d: (pxname is None or d['pxname'] == pxname) and d['svname'] == svname, r)))
|
||||
return state or None
|
||||
|
||||
|
||||
def wait_until_status(self, pxname, svname, status):
|
||||
"""
|
||||
Wait for a service to reach the specified status. Try RETRIES times
|
||||
with INTERVAL seconds of sleep in between. If the service has not reached
|
||||
the expected status in that time, the module will fail. If the service was
|
||||
not found, the module will fail.
|
||||
"""
|
||||
for i in range(1, self.wait_retries):
|
||||
state = self.get_state_for(pxname, svname)
|
||||
|
||||
# We can assume there will only be 1 element in state because both svname and pxname are always set when we get here
|
||||
if state[0]['status'] == status:
|
||||
return True
|
||||
else:
|
||||
time.sleep(self.wait_interval)
|
||||
|
||||
self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." % (pxname, svname, status, self.wait_retries))
|
||||
|
||||
|
||||
def enabled(self, host, backend, weight):
|
||||
"""
|
||||
Enabled action, marks server to UP and checks are re-enabled,
|
||||
also supports to get current weight for server (default) and
|
||||
set the weight for haproxy backend server when provides.
|
||||
"""
|
||||
cmd = "get weight $pxname/$svname; enable server $pxname/$svname"
|
||||
if weight:
|
||||
cmd += "; set weight $pxname/$svname %s" % weight
|
||||
self.execute_for_backends(cmd, backend, host, 'UP')
|
||||
|
||||
|
||||
def disabled(self, host, backend, shutdown_sessions):
|
||||
"""
|
||||
Disabled action, marks server to DOWN for maintenance. In this mode, no more checks will be
|
||||
performed on the server until it leaves maintenance,
|
||||
also it shutdown sessions while disabling backend host server.
|
||||
"""
|
||||
cmd = "get weight $pxname/$svname; disable server $pxname/$svname"
|
||||
if shutdown_sessions:
|
||||
cmd += "; shutdown sessions server $pxname/$svname"
|
||||
self.execute_for_backends(cmd, backend, host, 'MAINT')
|
||||
|
||||
|
||||
def act(self):
|
||||
"""
|
||||
Figure out what you want to do from ansible, and then do it.
|
||||
"""
|
||||
# Get the state before the run
|
||||
state_before = self.get_state_for(self.backend, self.host)
|
||||
self.command_results['state_before'] = state_before
|
||||
|
||||
# toggle enable/disbale server
|
||||
if self.state == 'enabled':
|
||||
self.enabled(self.host, self.backend, self.weight)
|
||||
elif self.state == 'disabled':
|
||||
self.disabled(self.host, self.backend, self.shutdown_sessions)
|
||||
else:
|
||||
self.module.fail_json(msg="unknown state specified: '%s'" % self.state)
|
||||
|
||||
# Get the state after the run
|
||||
state_after = self.get_state_for(self.backend, self.host)
|
||||
self.command_results['state_after'] = state_after
|
||||
|
||||
# Report change status
|
||||
if state_before != state_after:
|
||||
self.command_results['changed'] = True
|
||||
self.module.exit_json(**self.command_results)
|
||||
else:
|
||||
self.command_results['changed'] = False
|
||||
self.module.exit_json(**self.command_results)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# load ansible module object
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(required=True, default=None, choices=ACTION_CHOICES),
|
||||
host=dict(required=True, default=None),
|
||||
backend=dict(required=False, default=None),
|
||||
weight=dict(required=False, default=None),
|
||||
socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION),
|
||||
shutdown_sessions=dict(required=False, default=False, type='bool'),
|
||||
fail_on_not_found=dict(required=False, default=False, type='bool'),
|
||||
wait=dict(required=False, default=False, type='bool'),
|
||||
wait_retries=dict(required=False, default=WAIT_RETRIES, type='int'),
|
||||
wait_interval=dict(required=False, default=WAIT_INTERVAL, type='int'),
|
||||
),
|
||||
)
|
||||
|
||||
if not socket:
|
||||
module.fail_json(msg="unable to locate haproxy socket")
|
||||
|
||||
ansible_haproxy = HAProxy(module)
|
||||
ansible_haproxy.act()
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
lib/ansible/modules/network/illumos/__init__.py
Normal file
0
lib/ansible/modules/network/illumos/__init__.py
Normal file
181
lib/ansible/modules/network/illumos/dladm_etherstub.py
Normal file
181
lib/ansible/modules/network/illumos/dladm_etherstub.py
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Adam Števko <adam.stevko@gmail.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: dladm_etherstub
|
||||
short_description: Manage etherstubs on Solaris/illumos systems.
|
||||
description:
|
||||
- Create or delete etherstubs on Solaris/illumos systems.
|
||||
version_added: "2.2"
|
||||
author: Adam Števko (@xen0l)
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Etherstub name.
|
||||
required: true
|
||||
temporary:
|
||||
description:
|
||||
- Specifies that the etherstub is temporary. Temporary etherstubs
|
||||
do not persist across reboots.
|
||||
required: false
|
||||
default: false
|
||||
choices: [ "true", "false" ]
|
||||
state:
|
||||
description:
|
||||
- Create or delete Solaris/illumos etherstub.
|
||||
required: false
|
||||
default: "present"
|
||||
choices: [ "present", "absent" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create 'stub0' etherstub
|
||||
- dladm_etherstub:
|
||||
name: stub0
|
||||
state: present
|
||||
|
||||
# Remove 'stub0 etherstub
|
||||
- dladm_etherstub:
|
||||
name: stub0
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
name:
|
||||
description: etherstub name
|
||||
returned: always
|
||||
type: string
|
||||
sample: "switch0"
|
||||
state:
|
||||
description: state of the target
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
temporary:
|
||||
description: etherstub's persistence
|
||||
returned: always
|
||||
type: boolean
|
||||
sample: "True"
|
||||
'''
|
||||
|
||||
|
||||
class Etherstub(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.name = module.params['name']
|
||||
self.temporary = module.params['temporary']
|
||||
self.state = module.params['state']
|
||||
|
||||
def etherstub_exists(self):
|
||||
cmd = [self.module.get_bin_path('dladm', True)]
|
||||
|
||||
cmd.append('show-etherstub')
|
||||
cmd.append(self.name)
|
||||
|
||||
(rc, _, _) = self.module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def create_etherstub(self):
|
||||
cmd = [self.module.get_bin_path('dladm', True)]
|
||||
|
||||
cmd.append('create-etherstub')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def delete_etherstub(self):
|
||||
cmd = [self.module.get_bin_path('dladm', True)]
|
||||
|
||||
cmd.append('delete-etherstub')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True),
|
||||
temporary=dict(default=False, type='bool'),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
etherstub = Etherstub(module)
|
||||
|
||||
rc = None
|
||||
out = ''
|
||||
err = ''
|
||||
result = {}
|
||||
result['name'] = etherstub.name
|
||||
result['state'] = etherstub.state
|
||||
result['temporary'] = etherstub.temporary
|
||||
|
||||
if etherstub.state == 'absent':
|
||||
if etherstub.etherstub_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
(rc, out, err) = etherstub.delete_etherstub()
|
||||
if rc != 0:
|
||||
module.fail_json(name=etherstub.name, msg=err, rc=rc)
|
||||
elif etherstub.state == 'present':
|
||||
if not etherstub.etherstub_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
(rc, out, err) = etherstub.create_etherstub()
|
||||
|
||||
if rc is not None and rc != 0:
|
||||
module.fail_json(name=etherstub.name, msg=err, rc=rc)
|
||||
|
||||
if rc is None:
|
||||
result['changed'] = False
|
||||
else:
|
||||
result['changed'] = True
|
||||
|
||||
if out:
|
||||
result['stdout'] = out
|
||||
if err:
|
||||
result['stderr'] = err
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
274
lib/ansible/modules/network/illumos/dladm_vnic.py
Normal file
274
lib/ansible/modules/network/illumos/dladm_vnic.py
Normal file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Adam Števko <adam.stevko@gmail.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: dladm_vnic
|
||||
short_description: Manage VNICs on Solaris/illumos systems.
|
||||
description:
|
||||
- Create or delete VNICs on Solaris/illumos systems.
|
||||
version_added: "2.2"
|
||||
author: Adam Števko (@xen0l)
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- VNIC name.
|
||||
required: true
|
||||
link:
|
||||
description:
|
||||
- VNIC underlying link name.
|
||||
required: true
|
||||
temporary:
|
||||
description:
|
||||
- Specifies that the VNIC is temporary. Temporary VNICs
|
||||
do not persist across reboots.
|
||||
required: false
|
||||
default: false
|
||||
choices: [ "true", "false" ]
|
||||
mac:
|
||||
description:
|
||||
- Sets the VNIC's MAC address. Must be valid unicast MAC address.
|
||||
required: false
|
||||
default: false
|
||||
aliases: [ "macaddr" ]
|
||||
vlan:
|
||||
description:
|
||||
- Enable VLAN tagging for this VNIC. The VLAN tag will have id
|
||||
I(vlan).
|
||||
required: false
|
||||
default: false
|
||||
aliases: [ "vlan_id" ]
|
||||
state:
|
||||
description:
|
||||
- Create or delete Solaris/illumos VNIC.
|
||||
required: false
|
||||
default: "present"
|
||||
choices: [ "present", "absent" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create 'vnic0' VNIC over 'bnx0' link
|
||||
- dladm_vnic:
|
||||
name: vnic0
|
||||
link: bnx0
|
||||
state: present
|
||||
|
||||
# Create VNIC with specified MAC and VLAN tag over 'aggr0'
|
||||
- dladm_vnic:
|
||||
name: vnic1
|
||||
link: aggr0
|
||||
mac: '00:00:5E:00:53:23'
|
||||
vlan: 4
|
||||
|
||||
# Remove 'vnic0' VNIC
|
||||
- dladm_vnic:
|
||||
name: vnic0
|
||||
link: bnx0
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
name:
|
||||
description: VNIC name
|
||||
returned: always
|
||||
type: string
|
||||
sample: "vnic0"
|
||||
link:
|
||||
description: VNIC underlying link name
|
||||
returned: always
|
||||
type: string
|
||||
sample: "igb0"
|
||||
state:
|
||||
description: state of the target
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
temporary:
|
||||
description: VNIC's persistence
|
||||
returned: always
|
||||
type: boolean
|
||||
sample: "True"
|
||||
mac:
|
||||
description: MAC address to use for VNIC
|
||||
returned: if mac is specified
|
||||
type: string
|
||||
sample: "00:00:5E:00:53:42"
|
||||
vlan:
|
||||
description: VLAN to use for VNIC
|
||||
returned: success
|
||||
type: int
|
||||
sample: 42
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class VNIC(object):
|
||||
|
||||
UNICAST_MAC_REGEX = r'^[a-f0-9][2-9a-f0]:([a-f0-9]{2}:){4}[a-f0-9]{2}$'
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.name = module.params['name']
|
||||
self.link = module.params['link']
|
||||
self.mac = module.params['mac']
|
||||
self.vlan = module.params['vlan']
|
||||
self.temporary = module.params['temporary']
|
||||
self.state = module.params['state']
|
||||
|
||||
def vnic_exists(self):
|
||||
cmd = [self.module.get_bin_path('dladm', True)]
|
||||
|
||||
cmd.append('show-vnic')
|
||||
cmd.append(self.name)
|
||||
|
||||
(rc, _, _) = self.module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def create_vnic(self):
|
||||
cmd = [self.module.get_bin_path('dladm', True)]
|
||||
|
||||
cmd.append('create-vnic')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
|
||||
if self.mac:
|
||||
cmd.append('-m')
|
||||
cmd.append(self.mac)
|
||||
|
||||
if self.vlan:
|
||||
cmd.append('-v')
|
||||
cmd.append(self.vlan)
|
||||
|
||||
cmd.append('-l')
|
||||
cmd.append(self.link)
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def delete_vnic(self):
|
||||
cmd = [self.module.get_bin_path('dladm', True)]
|
||||
|
||||
cmd.append('delete-vnic')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def is_valid_unicast_mac(self):
|
||||
|
||||
mac_re = re.match(self.UNICAST_MAC_REGEX, self.mac)
|
||||
|
||||
return mac_re is None
|
||||
|
||||
def is_valid_vlan_id(self):
|
||||
|
||||
return 0 <= self.vlan <= 4095
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True),
|
||||
link=dict(required=True),
|
||||
mac=dict(default=None, aliases=['macaddr']),
|
||||
vlan=dict(default=None, aliases=['vlan_id']),
|
||||
temporary=dict(default=False, type='bool'),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
vnic = VNIC(module)
|
||||
|
||||
rc = None
|
||||
out = ''
|
||||
err = ''
|
||||
result = {}
|
||||
result['name'] = vnic.name
|
||||
result['link'] = vnic.link
|
||||
result['state'] = vnic.state
|
||||
result['temporary'] = vnic.temporary
|
||||
|
||||
if vnic.mac is not None:
|
||||
if vnic.is_valid_unicast_mac():
|
||||
module.fail_json(msg='Invalid unicast MAC address',
|
||||
mac=vnic.mac,
|
||||
name=vnic.name,
|
||||
state=vnic.state,
|
||||
link=vnic.link,
|
||||
vlan=vnic.vlan)
|
||||
result['mac'] = vnic.mac
|
||||
|
||||
if vnic.vlan is not None:
|
||||
if vnic.is_valid_vlan_id():
|
||||
module.fail_json(msg='Invalid VLAN tag',
|
||||
mac=vnic.mac,
|
||||
name=vnic.name,
|
||||
state=vnic.state,
|
||||
link=vnic.link,
|
||||
vlan=vnic.vlan)
|
||||
result['vlan'] = vnic.vlan
|
||||
|
||||
if vnic.state == 'absent':
|
||||
if vnic.vnic_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
(rc, out, err) = vnic.delete_vnic()
|
||||
if rc != 0:
|
||||
module.fail_json(name=vnic.name, msg=err, rc=rc)
|
||||
elif vnic.state == 'present':
|
||||
if not vnic.vnic_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
(rc, out, err) = vnic.create_vnic()
|
||||
|
||||
if rc is not None and rc != 0:
|
||||
module.fail_json(name=vnic.name, msg=err, rc=rc)
|
||||
|
||||
if rc is None:
|
||||
result['changed'] = False
|
||||
else:
|
||||
result['changed'] = True
|
||||
|
||||
if out:
|
||||
result['stdout'] = out
|
||||
if err:
|
||||
result['stderr'] = err
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
523
lib/ansible/modules/network/illumos/flowadm.py
Normal file
523
lib/ansible/modules/network/illumos/flowadm.py
Normal file
@@ -0,0 +1,523 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016, Adam Števko <adam.stevko@gmail.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: flowadm
|
||||
short_description: Manage bandwidth resource control and priority for protocols, services and zones.
|
||||
description:
|
||||
- Create/modify/remove networking bandwidth and associated resources for a type of traffic on a particular link.
|
||||
version_added: "2.2"
|
||||
author: Adam Števko (@xen0l)
|
||||
options:
|
||||
name:
|
||||
description: >
|
||||
- A flow is defined as a set of attributes based on Layer 3 and Layer 4
|
||||
headers, which can be used to identify a protocol, service, or a zone.
|
||||
required: true
|
||||
aliases: [ 'flow' ]
|
||||
link:
|
||||
description:
|
||||
- Specifiies a link to configure flow on.
|
||||
required: false
|
||||
local_ip:
|
||||
description:
|
||||
- Identifies a network flow by the local IP address.
|
||||
required: false
|
||||
remove_ip:
|
||||
description:
|
||||
- Identifies a network flow by the remote IP address.
|
||||
required: false
|
||||
transport:
|
||||
description: >
|
||||
- Specifies a Layer 4 protocol to be used. It is typically used in combination with I(local_port) to
|
||||
identify the service that needs special attention.
|
||||
required: false
|
||||
local_port:
|
||||
description:
|
||||
- Identifies a service specified by the local port.
|
||||
required: false
|
||||
dsfield:
|
||||
description: >
|
||||
- Identifies the 8-bit differentiated services field (as defined in
|
||||
RFC 2474). The optional dsfield_mask is used to state the bits of interest in
|
||||
the differentiated services field when comparing with the dsfield
|
||||
value. Both values must be in hexadecimal.
|
||||
required: false
|
||||
maxbw:
|
||||
description: >
|
||||
- Sets the full duplex bandwidth for the flow. The bandwidth is
|
||||
specified as an integer with one of the scale suffixes(K, M, or G
|
||||
for Kbps, Mbps, and Gbps). If no units are specified, the input
|
||||
value will be read as Mbps.
|
||||
required: false
|
||||
priority:
|
||||
description:
|
||||
- Sets the relative priority for the flow.
|
||||
required: false
|
||||
default: 'medium'
|
||||
choices: [ 'low', 'medium', 'high' ]
|
||||
temporary:
|
||||
description:
|
||||
- Specifies that the configured flow is temporary. Temporary
|
||||
flows do not persist across reboots.
|
||||
required: false
|
||||
default: false
|
||||
choices: [ "true", "false" ]
|
||||
state:
|
||||
description:
|
||||
- Create/delete/enable/disable an IP address on the network interface.
|
||||
required: false
|
||||
default: present
|
||||
choices: [ 'absent', 'present', 'resetted' ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Limit SSH traffic to 100M via vnic0 interface
|
||||
- flowadm:
|
||||
link: vnic0
|
||||
flow: ssh_out
|
||||
transport: tcp
|
||||
local_port: 22
|
||||
maxbw: 100M
|
||||
state: present
|
||||
|
||||
# Reset flow properties
|
||||
- flowadm:
|
||||
name: dns
|
||||
state: resetted
|
||||
|
||||
# Configure policy for EF PHB (DSCP value of 101110 from RFC 2598) with a bandwidth of 500 Mbps and a high priority.
|
||||
- flowadm:
|
||||
link: bge0
|
||||
dsfield: '0x2e:0xfc'
|
||||
maxbw: 500M
|
||||
priority: high
|
||||
flow: efphb-flow
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
name:
|
||||
description: flow name
|
||||
returned: always
|
||||
type: string
|
||||
sample: "http_drop"
|
||||
link:
|
||||
description: flow's link
|
||||
returned: if link is defined
|
||||
type: string
|
||||
sample: "vnic0"
|
||||
state:
|
||||
description: state of the target
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
temporary:
|
||||
description: flow's persistence
|
||||
returned: always
|
||||
type: boolean
|
||||
sample: "True"
|
||||
priority:
|
||||
description: flow's priority
|
||||
returned: if priority is defined
|
||||
type: string
|
||||
sample: "low"
|
||||
transport:
|
||||
description: flow's transport
|
||||
returned: if transport is defined
|
||||
type: string
|
||||
sample: "tcp"
|
||||
maxbw:
|
||||
description: flow's maximum bandwidth
|
||||
returned: if maxbw is defined
|
||||
type: string
|
||||
sample: "100M"
|
||||
local_Ip:
|
||||
description: flow's local IP address
|
||||
returned: if local_ip is defined
|
||||
type: string
|
||||
sample: "10.0.0.42"
|
||||
local_port:
|
||||
description: flow's local port
|
||||
returned: if local_port is defined
|
||||
type: int
|
||||
sample: 1337
|
||||
remote_Ip:
|
||||
description: flow's remote IP address
|
||||
returned: if remote_ip is defined
|
||||
type: string
|
||||
sample: "10.0.0.42"
|
||||
dsfield:
|
||||
description: flow's differentiated services value
|
||||
returned: if dsfield is defined
|
||||
type: string
|
||||
sample: "0x2e:0xfc"
|
||||
'''
|
||||
|
||||
|
||||
import socket
|
||||
|
||||
SUPPORTED_TRANSPORTS = ['tcp', 'udp', 'sctp', 'icmp', 'icmpv6']
|
||||
SUPPORTED_PRIORITIES = ['low', 'medium', 'high']
|
||||
|
||||
SUPPORTED_ATTRIBUTES = ['local_ip', 'remote_ip', 'transport', 'local_port', 'dsfield']
|
||||
SUPPORTPED_PROPERTIES = ['maxbw', 'priority']
|
||||
|
||||
|
||||
class Flow(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.name = module.params['name']
|
||||
self.link = module.params['link']
|
||||
self.local_ip = module.params['local_ip']
|
||||
self.remote_ip = module.params['remote_ip']
|
||||
self.transport = module.params['transport']
|
||||
self.local_port = module.params['local_port']
|
||||
self.dsfield = module.params['dsfield']
|
||||
self.maxbw = module.params['maxbw']
|
||||
self.priority = module.params['priority']
|
||||
self.temporary = module.params['temporary']
|
||||
self.state = module.params['state']
|
||||
|
||||
self._needs_updating = {
|
||||
'maxbw': False,
|
||||
'priority': False,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def is_valid_port(cls, port):
|
||||
return 1 <= int(port) <= 65535
|
||||
|
||||
@classmethod
|
||||
def is_valid_address(cls, ip):
|
||||
|
||||
if ip.count('/') == 1:
|
||||
ip_address, netmask = ip.split('/')
|
||||
else:
|
||||
ip_address = ip
|
||||
|
||||
if len(ip_address.split('.')) == 4:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, ip_address)
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
if not 0 <= netmask <= 32:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, ip_address)
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
if not 0 <= netmask <= 128:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_hex(cls, number):
|
||||
try:
|
||||
int(number, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_valid_dsfield(cls, dsfield):
|
||||
|
||||
dsmask = None
|
||||
|
||||
if dsfield.count(':') == 1:
|
||||
dsval = dsfield.split(':')[0]
|
||||
else:
|
||||
dsval, dsmask = dsfield.split(':')
|
||||
|
||||
if dsmask and not 0x01 <= int(dsmask, 16) <= 0xff and not 0x01 <= int(dsval, 16) <= 0xff:
|
||||
return False
|
||||
elif not 0x01 <= int(dsval, 16) <= 0xff:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def flow_exists(self):
|
||||
cmd = [self.module.get_bin_path('flowadm')]
|
||||
|
||||
cmd.append('show-flow')
|
||||
cmd.append(self.name)
|
||||
|
||||
(rc, _, _) = self.module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def delete_flow(self):
|
||||
cmd = [self.module.get_bin_path('flowadm')]
|
||||
|
||||
cmd.append('remove-flow')
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def create_flow(self):
|
||||
cmd = [self.module.get_bin_path('flowadm')]
|
||||
|
||||
cmd.append('add-flow')
|
||||
cmd.append('-l')
|
||||
cmd.append(self.link)
|
||||
|
||||
if self.local_ip:
|
||||
cmd.append('-a')
|
||||
cmd.append('local_ip=' + self.local_ip)
|
||||
|
||||
if self.remote_ip:
|
||||
cmd.append('-a')
|
||||
cmd.append('remote_ip=' + self.remote_ip)
|
||||
|
||||
if self.transport:
|
||||
cmd.append('-a')
|
||||
cmd.append('transport=' + self.transport)
|
||||
|
||||
if self.local_port:
|
||||
cmd.append('-a')
|
||||
cmd.append('local_port=' + self.local_port)
|
||||
|
||||
if self.dsfield:
|
||||
cmd.append('-a')
|
||||
cmd.append('dsfield=' + self.dsfield)
|
||||
|
||||
if self.maxbw:
|
||||
cmd.append('-p')
|
||||
cmd.append('maxbw=' + self.maxbw)
|
||||
|
||||
if self.priority:
|
||||
cmd.append('-p')
|
||||
cmd.append('priority=' + self.priority)
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def _query_flow_props(self):
|
||||
cmd = [self.module.get_bin_path('flowadm')]
|
||||
|
||||
cmd.append('show-flowprop')
|
||||
cmd.append('-c')
|
||||
cmd.append('-o')
|
||||
cmd.append('property,possible')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def flow_needs_udpating(self):
|
||||
(rc, out, err) = self._query_flow_props()
|
||||
|
||||
NEEDS_UPDATING = False
|
||||
|
||||
if rc == 0:
|
||||
properties = (line.split(':') for line in out.rstrip().split('\n'))
|
||||
for prop, value in properties:
|
||||
if prop == 'maxbw' and self.maxbw != value:
|
||||
self._needs_updating.update({prop: True})
|
||||
NEEDS_UPDATING = True
|
||||
|
||||
elif prop == 'priority' and self.priority != value:
|
||||
self._needs_updating.update({prop: True})
|
||||
NEEDS_UPDATING = True
|
||||
|
||||
return NEEDS_UPDATING
|
||||
else:
|
||||
self.module.fail_json(msg='Error while checking flow properties: %s' % err,
|
||||
stderr=err,
|
||||
rc=rc)
|
||||
|
||||
def update_flow(self):
|
||||
cmd = [self.module.get_bin_path('flowadm')]
|
||||
|
||||
cmd.append('set-flowprop')
|
||||
|
||||
if self.maxbw and self._needs_updating['maxbw']:
|
||||
cmd.append('-p')
|
||||
cmd.append('maxbw=' + self.maxbw)
|
||||
|
||||
if self.priority and self._needs_updating['priority']:
|
||||
cmd.append('-p')
|
||||
cmd.append('priority=' + self.priority)
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True, aliases=['flow']),
|
||||
link=dict(required=False),
|
||||
local_ip=dict(required=False),
|
||||
remote_ip=dict(required=False),
|
||||
transport=dict(required=False, choices=SUPPORTED_TRANSPORTS),
|
||||
local_port=dict(required=False),
|
||||
dsfield=dict(required=False),
|
||||
maxbw=dict(required=False),
|
||||
priority=dict(required=False,
|
||||
default='medium',
|
||||
choices=SUPPORTED_PRIORITIES),
|
||||
temporary=dict(default=False, type='bool'),
|
||||
state=dict(required=False,
|
||||
default='present',
|
||||
choices=['absent', 'present', 'resetted']),
|
||||
),
|
||||
mutually_exclusive=[
|
||||
('local_ip', 'remote_ip'),
|
||||
('local_ip', 'transport'),
|
||||
('local_ip', 'local_port'),
|
||||
('local_ip', 'dsfield'),
|
||||
('remote_ip', 'transport'),
|
||||
('remote_ip', 'local_port'),
|
||||
('remote_ip', 'dsfield'),
|
||||
('transport', 'dsfield'),
|
||||
('local_port', 'dsfield'),
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
flow = Flow(module)
|
||||
|
||||
rc = None
|
||||
out = ''
|
||||
err = ''
|
||||
result = {}
|
||||
result['name'] = flow.name
|
||||
result['state'] = flow.state
|
||||
result['temporary'] = flow.temporary
|
||||
|
||||
if flow.link:
|
||||
result['link'] = flow.link
|
||||
|
||||
if flow.maxbw:
|
||||
result['maxbw'] = flow.maxbw
|
||||
|
||||
if flow.priority:
|
||||
result['priority'] = flow.priority
|
||||
|
||||
if flow.local_ip:
|
||||
if flow.is_valid_address(flow.local_ip):
|
||||
result['local_ip'] = flow.local_ip
|
||||
|
||||
if flow.remote_ip:
|
||||
if flow.is_valid_address(flow.remote_ip):
|
||||
result['remote_ip'] = flow.remote_ip
|
||||
|
||||
if flow.transport:
|
||||
result['transport'] = flow.transport
|
||||
|
||||
if flow.local_port:
|
||||
if flow.is_valid_port(flow.local_port):
|
||||
result['local_port'] = flow.local_port
|
||||
else:
|
||||
module.fail_json(msg='Invalid port: %s' % flow.local_port,
|
||||
rc=1)
|
||||
|
||||
if flow.dsfield:
|
||||
if flow.is_valid_dsfield(flow.dsfield):
|
||||
result['dsfield'] = flow.dsfield
|
||||
else:
|
||||
module.fail_json(msg='Invalid dsfield: %s' % flow.dsfield,
|
||||
rc=1)
|
||||
|
||||
if flow.state == 'absent':
|
||||
if flow.flow_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
(rc, out, err) = flow.delete_flow()
|
||||
if rc != 0:
|
||||
module.fail_json(msg='Error while deleting flow: "%s"' % err,
|
||||
name=flow.name,
|
||||
stderr=err,
|
||||
rc=rc)
|
||||
|
||||
elif flow.state == 'present':
|
||||
if not flow.flow_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
(rc, out, err) = flow.create_flow()
|
||||
if rc != 0:
|
||||
module.fail_json(msg='Error while creating flow: "%s"' % err,
|
||||
name=flow.name,
|
||||
stderr=err,
|
||||
rc=rc)
|
||||
else:
|
||||
if flow.flow_needs_udpating():
|
||||
(rc, out, err) = flow.update_flow()
|
||||
if rc != 0:
|
||||
module.fail_json(msg='Error while updating flow: "%s"' % err,
|
||||
name=flow.name,
|
||||
stderr=err,
|
||||
rc=rc)
|
||||
|
||||
elif flow.state == 'resetted':
|
||||
if flow.flow_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
(rc, out, err) = flow.reset_flow()
|
||||
if rc != 0:
|
||||
module.fail_json(msg='Error while resetting flow: "%s"' % err,
|
||||
name=flow.name,
|
||||
stderr=err,
|
||||
rc=rc)
|
||||
|
||||
if rc is None:
|
||||
result['changed'] = False
|
||||
else:
|
||||
result['changed'] = True
|
||||
|
||||
if out:
|
||||
result['stdout'] = out
|
||||
if err:
|
||||
result['stderr'] = err
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
232
lib/ansible/modules/network/illumos/ipadm_if.py
Normal file
232
lib/ansible/modules/network/illumos/ipadm_if.py
Normal file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Adam Števko <adam.stevko@gmail.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: ipadm_if
|
||||
short_description: Manage IP interfaces on Solaris/illumos systems.
|
||||
description:
|
||||
- Create, delete, enable or disable IP interfaces on Solaris/illumos
|
||||
systems.
|
||||
version_added: "2.2"
|
||||
author: Adam Števko (@xen0l)
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- IP interface name.
|
||||
required: true
|
||||
temporary:
|
||||
description:
|
||||
- Specifies that the IP interface is temporary. Temporary IP
|
||||
interfaces do not persist across reboots.
|
||||
required: false
|
||||
default: false
|
||||
choices: [ "true", "false" ]
|
||||
state:
|
||||
description:
|
||||
- Create or delete Solaris/illumos IP interfaces.
|
||||
required: false
|
||||
default: "present"
|
||||
choices: [ "present", "absent", "enabled", "disabled" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create vnic0 interface
|
||||
- ipadm_if:
|
||||
name: vnic0
|
||||
state: enabled
|
||||
|
||||
# Disable vnic0 interface
|
||||
- ipadm_if:
|
||||
name: vnic0
|
||||
state: disabled
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
name:
|
||||
description: IP interface name
|
||||
returned: always
|
||||
type: string
|
||||
sample: "vnic0"
|
||||
state:
|
||||
description: state of the target
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
temporary:
|
||||
description: persistence of a IP interface
|
||||
returned: always
|
||||
type: boolean
|
||||
sample: "True"
|
||||
'''
|
||||
|
||||
|
||||
class IPInterface(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.name = module.params['name']
|
||||
self.temporary = module.params['temporary']
|
||||
self.state = module.params['state']
|
||||
|
||||
def interface_exists(self):
|
||||
cmd = [self.module.get_bin_path('ipadm', True)]
|
||||
|
||||
cmd.append('show-if')
|
||||
cmd.append(self.name)
|
||||
|
||||
(rc, _, _) = self.module.run_command(cmd)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def interface_is_disabled(self):
|
||||
cmd = [self.module.get_bin_path('ipadm', True)]
|
||||
|
||||
cmd.append('show-if')
|
||||
cmd.append('-o')
|
||||
cmd.append('state')
|
||||
cmd.append(self.name)
|
||||
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(name=self.name, rc=rc, msg=err)
|
||||
|
||||
return 'disabled' in out
|
||||
|
||||
def create_interface(self):
|
||||
cmd = [self.module.get_bin_path('ipadm', True)]
|
||||
|
||||
cmd.append('create-if')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def delete_interface(self):
|
||||
cmd = [self.module.get_bin_path('ipadm', True)]
|
||||
|
||||
cmd.append('delete-if')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def enable_interface(self):
|
||||
cmd = [self.module.get_bin_path('ipadm', True)]
|
||||
|
||||
cmd.append('enable-if')
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def disable_interface(self):
|
||||
cmd = [self.module.get_bin_path('ipadm', True)]
|
||||
|
||||
cmd.append('disable-if')
|
||||
cmd.append('-t')
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True),
|
||||
temporary=dict(default=False, type='bool'),
|
||||
state=dict(default='present', choices=['absent',
|
||||
'present',
|
||||
'enabled',
|
||||
'disabled']),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
interface = IPInterface(module)
|
||||
|
||||
rc = None
|
||||
out = ''
|
||||
err = ''
|
||||
result = {}
|
||||
result['name'] = interface.name
|
||||
result['state'] = interface.state
|
||||
result['temporary'] = interface.temporary
|
||||
|
||||
if interface.state == 'absent':
|
||||
if interface.interface_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
(rc, out, err) = interface.delete_interface()
|
||||
if rc != 0:
|
||||
module.fail_json(name=interface.name, msg=err, rc=rc)
|
||||
elif interface.state == 'present':
|
||||
if not interface.interface_exists():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
(rc, out, err) = interface.create_interface()
|
||||
|
||||
if rc is not None and rc != 0:
|
||||
module.fail_json(name=interface.name, msg=err, rc=rc)
|
||||
|
||||
elif interface.state == 'enabled':
|
||||
if interface.interface_is_disabled():
|
||||
(rc, out, err) = interface.enable_interface()
|
||||
|
||||
if rc is not None and rc != 0:
|
||||
module.fail_json(name=interface.name, msg=err, rc=rc)
|
||||
|
||||
elif interface.state == 'disabled':
|
||||
if not interface.interface_is_disabled():
|
||||
(rc, out, err) = interface.disable_interface()
|
||||
|
||||
if rc is not None and rc != 0:
|
||||
module.fail_json(name=interface.name, msg=err, rc=rc)
|
||||
|
||||
if rc is None:
|
||||
result['changed'] = False
|
||||
else:
|
||||
result['changed'] = True
|
||||
|
||||
if out:
|
||||
result['stdout'] = out
|
||||
if err:
|
||||
result['stderr'] = err
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
270
lib/ansible/modules/network/illumos/ipadm_prop.py
Normal file
270
lib/ansible/modules/network/illumos/ipadm_prop.py
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Adam Števko <adam.stevko@gmail.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: ipadm_prop
|
||||
short_description: Manage protocol properties on Solaris/illumos systems.
|
||||
description:
|
||||
- Modify protocol properties on Solaris/illumos systems.
|
||||
version_added: "2.2"
|
||||
author: Adam Števko (@xen0l)
|
||||
options:
|
||||
protocol:
|
||||
description:
|
||||
- Specifies the procotol for which we want to manage properties.
|
||||
required: true
|
||||
property:
|
||||
description:
|
||||
- Specifies the name of property we want to manage.
|
||||
required: true
|
||||
value:
|
||||
description:
|
||||
- Specifies the value we want to set for the property.
|
||||
required: false
|
||||
temporary:
|
||||
description:
|
||||
- Specifies that the property value is temporary. Temporary
|
||||
property values do not persist across reboots.
|
||||
required: false
|
||||
default: false
|
||||
choices: [ "true", "false" ]
|
||||
state:
|
||||
description:
|
||||
- Set or reset the property value.
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent", "reset" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Set TCP receive buffer size
|
||||
ipadm_prop: protocol=tcp property=recv_buf value=65536
|
||||
|
||||
# Reset UDP send buffer size to the default value
|
||||
ipadm_prop: protocol=udp property=send_buf state=reset
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
protocol:
|
||||
description: property's protocol
|
||||
returned: always
|
||||
type: string
|
||||
sample: "TCP"
|
||||
property:
|
||||
description: name of the property
|
||||
returned: always
|
||||
type: string
|
||||
sample: "recv_maxbuf"
|
||||
state:
|
||||
description: state of the target
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
temporary:
|
||||
description: property's persistence
|
||||
returned: always
|
||||
type: boolean
|
||||
sample: "True"
|
||||
value:
|
||||
description: value of the property
|
||||
returned: always
|
||||
type: int/string (depends on property)
|
||||
sample: 1024/never
|
||||
'''
|
||||
|
||||
SUPPORTED_PROTOCOLS = ['ipv4', 'ipv6', 'icmp', 'tcp', 'udp', 'sctp']
|
||||
|
||||
|
||||
class Prop(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.protocol = module.params['protocol']
|
||||
self.property = module.params['property']
|
||||
self.value = module.params['value']
|
||||
self.temporary = module.params['temporary']
|
||||
self.state = module.params['state']
|
||||
|
||||
def property_exists(self):
|
||||
cmd = [self.module.get_bin_path('ipadm')]
|
||||
|
||||
cmd.append('show-prop')
|
||||
cmd.append('-p')
|
||||
cmd.append(self.property)
|
||||
cmd.append(self.protocol)
|
||||
|
||||
(rc, _, _) = self.module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
self.module.fail_json(msg='Unknown property "%s" for protocol %s' %
|
||||
(self.property, self.protocol),
|
||||
protocol=self.protocol,
|
||||
property=self.property)
|
||||
|
||||
def property_is_modified(self):
|
||||
cmd = [self.module.get_bin_path('ipadm')]
|
||||
|
||||
cmd.append('show-prop')
|
||||
cmd.append('-c')
|
||||
cmd.append('-o')
|
||||
cmd.append('current,default')
|
||||
cmd.append('-p')
|
||||
cmd.append(self.property)
|
||||
cmd.append(self.protocol)
|
||||
|
||||
(rc, out, _) = self.module.run_command(cmd)
|
||||
|
||||
out = out.rstrip()
|
||||
(value, default) = out.split(':')
|
||||
|
||||
if rc == 0 and value == default:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def property_is_set(self):
|
||||
cmd = [self.module.get_bin_path('ipadm')]
|
||||
|
||||
cmd.append('show-prop')
|
||||
cmd.append('-c')
|
||||
cmd.append('-o')
|
||||
cmd.append('current')
|
||||
cmd.append('-p')
|
||||
cmd.append(self.property)
|
||||
cmd.append(self.protocol)
|
||||
|
||||
(rc, out, _) = self.module.run_command(cmd)
|
||||
|
||||
out = out.rstrip()
|
||||
|
||||
if rc == 0 and self.value == out:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_property(self):
|
||||
cmd = [self.module.get_bin_path('ipadm')]
|
||||
|
||||
cmd.append('set-prop')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
|
||||
cmd.append('-p')
|
||||
cmd.append(self.property + "=" + self.value)
|
||||
cmd.append(self.protocol)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def reset_property(self):
|
||||
cmd = [self.module.get_bin_path('ipadm')]
|
||||
|
||||
cmd.append('reset-prop')
|
||||
|
||||
if self.temporary:
|
||||
cmd.append('-t')
|
||||
|
||||
cmd.append('-p')
|
||||
cmd.append(self.property)
|
||||
cmd.append(self.protocol)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
protocol=dict(required=True, choices=SUPPORTED_PROTOCOLS),
|
||||
property=dict(required=True),
|
||||
value=dict(required=False),
|
||||
temporary=dict(default=False, type='bool'),
|
||||
state=dict(
|
||||
default='present', choices=['absent', 'present', 'reset']),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
prop = Prop(module)
|
||||
|
||||
rc = None
|
||||
out = ''
|
||||
err = ''
|
||||
result = {}
|
||||
result['protocol'] = prop.protocol
|
||||
result['property'] = prop.property
|
||||
result['state'] = prop.state
|
||||
result['temporary'] = prop.temporary
|
||||
if prop.value:
|
||||
result['value'] = prop.value
|
||||
|
||||
if prop.state == 'absent' or prop.state == 'reset':
|
||||
if prop.property_exists():
|
||||
if not prop.property_is_modified():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
(rc, out, err) = prop.reset_property()
|
||||
if rc != 0:
|
||||
module.fail_json(protocol=prop.protocol,
|
||||
property=prop.property,
|
||||
msg=err,
|
||||
rc=rc)
|
||||
|
||||
elif prop.state == 'present':
|
||||
if prop.value is None:
|
||||
module.fail_json(msg='Value is mandatory with state "present"')
|
||||
|
||||
if prop.property_exists():
|
||||
if not prop.property_is_set():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
(rc, out, err) = prop.set_property()
|
||||
if rc != 0:
|
||||
module.fail_json(protocol=prop.protocol,
|
||||
property=prop.property,
|
||||
msg=err,
|
||||
rc=rc)
|
||||
|
||||
if rc is None:
|
||||
result['changed'] = False
|
||||
else:
|
||||
result['changed'] = True
|
||||
|
||||
if out:
|
||||
result['stdout'] = out
|
||||
if err:
|
||||
result['stderr'] = err
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
118
lib/ansible/modules/network/ipify_facts.py
Normal file
118
lib/ansible/modules/network/ipify_facts.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2015, René Moser <mail@renemoser.net>
|
||||
#
|
||||
# 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: ipify_facts
|
||||
short_description: Retrieve the public IP of your internet gateway.
|
||||
description:
|
||||
- If behind NAT and need to know the public IP of your internet gateway.
|
||||
version_added: '2.0'
|
||||
author: "René Moser (@resmo)"
|
||||
options:
|
||||
api_url:
|
||||
description:
|
||||
- URL of the ipify.org API service.
|
||||
- C(?format=json) will be appended per default.
|
||||
required: false
|
||||
default: 'https://api.ipify.org'
|
||||
timeout:
|
||||
description:
|
||||
- HTTP connection timeout in seconds.
|
||||
required: false
|
||||
default: 10
|
||||
version_added: "2.3"
|
||||
notes:
|
||||
- "Visit https://www.ipify.org to get more information."
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Gather IP facts from ipify.org
|
||||
- name: get my public IP
|
||||
ipify_facts:
|
||||
|
||||
# Gather IP facts from your own ipify service endpoint with a custom timeout
|
||||
- name: get my public IP
|
||||
ipify_facts:
|
||||
api_url: http://api.example.com/ipify
|
||||
timeout: 20
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
---
|
||||
ipify_public_ip:
|
||||
description: Public IP of the internet gateway.
|
||||
returned: success
|
||||
type: string
|
||||
sample: 1.2.3.4
|
||||
'''
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
# Let snippet from module_utils/basic.py return a proper error in this case
|
||||
pass
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
|
||||
class IpifyFacts(object):
|
||||
|
||||
def __init__(self):
|
||||
self.api_url = module.params.get('api_url')
|
||||
self.timeout = module.params.get('timeout')
|
||||
|
||||
def run(self):
|
||||
result = {
|
||||
'ipify_public_ip': None
|
||||
}
|
||||
(response, info) = fetch_url(module=module, url=self.api_url + "?format=json" , force=True, timeout=self.timeout)
|
||||
|
||||
if not response:
|
||||
module.fail_json(msg="No valid or no response from url %s within %s seconds (timeout)" % (self.api_url, self.timeout))
|
||||
|
||||
data = json.loads(response.read())
|
||||
result['ipify_public_ip'] = data.get('ip')
|
||||
return result
|
||||
|
||||
def main():
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
api_url=dict(default='https://api.ipify.org'),
|
||||
timeout=dict(type='int', default=10),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
ipify_facts = IpifyFacts().run()
|
||||
ipify_facts_result = dict(changed=False, ansible_facts=ipify_facts)
|
||||
module.exit_json(**ipify_facts_result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
141
lib/ansible/modules/network/ipinfoio_facts.py
Normal file
141
lib/ansible/modules/network/ipinfoio_facts.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2016, Aleksei Kostiuk <unitoff@gmail.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: ipinfoio_facts
|
||||
short_description: "Retrieve IP geolocation facts of a host's IP address"
|
||||
description:
|
||||
- "Gather IP geolocation facts of a host's IP address using ipinfo.io API"
|
||||
version_added: "2.3"
|
||||
author: "Aleksei Kostiuk (@akostyuk)"
|
||||
options:
|
||||
timeout:
|
||||
description:
|
||||
- HTTP connection timeout in seconds
|
||||
required: false
|
||||
default: 10
|
||||
http_agent:
|
||||
description:
|
||||
- Set http user agent
|
||||
required: false
|
||||
default: "ansible-ipinfoio-module/0.0.1"
|
||||
notes:
|
||||
- "Check http://ipinfo.io/ for more information"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Retrieve geolocation data of a host's IP address
|
||||
- name: get IP geolocation data
|
||||
ipinfoio_facts:
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
ansible_facts:
|
||||
description: "Dictionary of ip geolocation facts for a host's IP address"
|
||||
returned: changed
|
||||
type: dictionary
|
||||
contains:
|
||||
ip:
|
||||
description: "Public IP address of a host"
|
||||
type: string
|
||||
sample: "8.8.8.8"
|
||||
hostname:
|
||||
description: Domain name
|
||||
type: string
|
||||
sample: "google-public-dns-a.google.com"
|
||||
country:
|
||||
description: ISO 3166-1 alpha-2 country code
|
||||
type: string
|
||||
sample: "US"
|
||||
region:
|
||||
description: State or province name
|
||||
type: string
|
||||
sample: "California"
|
||||
city:
|
||||
description: City name
|
||||
type: string
|
||||
sample: "Mountain View"
|
||||
loc:
|
||||
description: Latitude and Longitude of the location
|
||||
type: string
|
||||
sample: "37.3860,-122.0838"
|
||||
org:
|
||||
description: "organization's name"
|
||||
type: string
|
||||
sample: "AS3356 Level 3 Communications, Inc."
|
||||
postal:
|
||||
description: Postal code
|
||||
type: string
|
||||
sample: "94035"
|
||||
'''
|
||||
|
||||
USER_AGENT = 'ansible-ipinfoio-module/0.0.1'
|
||||
|
||||
|
||||
class IpinfoioFacts(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.url = 'https://ipinfo.io/json'
|
||||
self.timeout = module.params.get('timeout')
|
||||
self.module = module
|
||||
|
||||
def get_geo_data(self):
|
||||
response, info = fetch_url(self.module, self.url, force=True, # NOQA
|
||||
timeout=self.timeout)
|
||||
try:
|
||||
info['status'] == 200
|
||||
except AssertionError:
|
||||
self.module.fail_json(msg='Could not get {} page, '
|
||||
'check for connectivity!'.format(self.url))
|
||||
else:
|
||||
try:
|
||||
content = response.read()
|
||||
result = self.module.from_json(content.decode('utf8'))
|
||||
except ValueError:
|
||||
self.module.fail_json(
|
||||
msg='Failed to parse the ipinfo.io response: '
|
||||
'{0} {1}'.format(self.url, content))
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule( # NOQA
|
||||
argument_spec=dict(
|
||||
http_agent=dict(default=USER_AGENT),
|
||||
timeout=dict(type='int', default=10),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
ipinfoio = IpinfoioFacts(module)
|
||||
ipinfoio_result = dict(
|
||||
changed=False, ansible_facts=ipinfoio.get_geo_data())
|
||||
module.exit_json(**ipinfoio_result)
|
||||
|
||||
from ansible.module_utils.basic import * # NOQA
|
||||
from ansible.module_utils.urls import * # NOQA
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
92
lib/ansible/modules/network/lldp.py
Normal file
92
lib/ansible/modules/network/lldp.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/python -tt
|
||||
# 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/>.
|
||||
|
||||
import subprocess
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: lldp
|
||||
requirements: [ lldpctl ]
|
||||
version_added: 1.6
|
||||
short_description: get details reported by lldp
|
||||
description:
|
||||
- Reads data out of lldpctl
|
||||
options: {}
|
||||
author: "Andy Hill (@andyhky)"
|
||||
notes:
|
||||
- Requires lldpd running and lldp enabled on switches
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Retrieve switch/port information
|
||||
- name: Gather information from lldp
|
||||
lldp:
|
||||
|
||||
- name: Print each switch/port
|
||||
debug:
|
||||
msg: "{{ lldp[item]['chassis']['name'] }} / {{ lldp[item]['port']['ifalias'] }}"
|
||||
with_items: "{{ lldp.keys() }}"
|
||||
|
||||
# TASK: [Print each switch/port] ***********************************************************
|
||||
# ok: [10.13.0.22] => (item=eth2) => {"item": "eth2", "msg": "switch1.example.com / Gi0/24"}
|
||||
# ok: [10.13.0.22] => (item=eth1) => {"item": "eth1", "msg": "switch2.example.com / Gi0/3"}
|
||||
# ok: [10.13.0.22] => (item=eth0) => {"item": "eth0", "msg": "switch3.example.com / Gi0/3"}
|
||||
|
||||
'''
|
||||
|
||||
def gather_lldp():
|
||||
cmd = ['lldpctl', '-f', 'keyvalue']
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
(output, err) = proc.communicate()
|
||||
if output:
|
||||
output_dict = {}
|
||||
lldp_entries = output.split("\n")
|
||||
|
||||
for entry in lldp_entries:
|
||||
if entry.startswith('lldp'):
|
||||
path, value = entry.strip().split("=", 1)
|
||||
path = path.split(".")
|
||||
path_components, final = path[:-1], path[-1]
|
||||
else:
|
||||
value = current_dict[final] + '\n' + entry
|
||||
|
||||
current_dict = output_dict
|
||||
for path_component in path_components:
|
||||
current_dict[path_component] = current_dict.get(path_component, {})
|
||||
current_dict = current_dict[path_component]
|
||||
current_dict[final] = value
|
||||
return output_dict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule({})
|
||||
|
||||
lldp_output = gather_lldp()
|
||||
try:
|
||||
data = {'lldp': lldp_output['lldp']}
|
||||
module.exit_json(ansible_facts=data)
|
||||
except TypeError:
|
||||
module.fail_json(msg="lldpctl command failed. is lldpd running?")
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
lib/ansible/modules/network/netconf/__init__.py
Normal file
0
lib/ansible/modules/network/netconf/__init__.py
Normal file
225
lib/ansible/modules/network/netconf/netconf_config.py
Executable file
225
lib/ansible/modules/network/netconf/netconf_config.py
Executable file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2016, Leandro Lisboa Penz <lpenz at lpenz.org>
|
||||
#
|
||||
# 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: netconf_config
|
||||
author: "Leandro Lisboa Penz (@lpenz)"
|
||||
short_description: netconf device configuration
|
||||
description:
|
||||
- Netconf is a network management protocol developed and standardized by
|
||||
the IETF. It is documented in RFC 6241.
|
||||
|
||||
- This module allows the user to send a configuration XML file to a netconf
|
||||
device, and detects if there was a configuration change.
|
||||
notes:
|
||||
- This module supports devices with and without the the candidate and
|
||||
confirmed-commit capabilities. It always use the safer feature.
|
||||
version_added: "2.2"
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
- the hostname or ip address of the netconf device
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- the netconf port
|
||||
default: 830
|
||||
required: false
|
||||
hostkey_verify:
|
||||
description:
|
||||
- if true, the ssh host key of the device must match a ssh key present on the host
|
||||
- if false, the ssh host key of the device is not checked
|
||||
default: true
|
||||
required: false
|
||||
username:
|
||||
description:
|
||||
- the username to authenticate with
|
||||
required: true
|
||||
password:
|
||||
description:
|
||||
- password of the user to authenticate with
|
||||
required: true
|
||||
xml:
|
||||
description:
|
||||
- the XML content to send to the device
|
||||
required: true
|
||||
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "ncclient"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: set ntp server in the device
|
||||
netconf_config:
|
||||
host: 10.0.0.1
|
||||
username: admin
|
||||
password: admin
|
||||
xml: |
|
||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<system xmlns="urn:ietf:params:xml:ns:yang:ietf-system">
|
||||
<ntp>
|
||||
<enabled>true</enabled>
|
||||
<server>
|
||||
<name>ntp1</name>
|
||||
<udp><address>127.0.0.1</address></udp>
|
||||
</server>
|
||||
</ntp>
|
||||
</system>
|
||||
</config>
|
||||
|
||||
- name: wipe ntp configuration
|
||||
netconf_config:
|
||||
host: 10.0.0.1
|
||||
username: admin
|
||||
password: admin
|
||||
xml: |
|
||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<system xmlns="urn:ietf:params:xml:ns:yang:ietf-system">
|
||||
<ntp>
|
||||
<enabled>false</enabled>
|
||||
<server operation="remove">
|
||||
<name>ntp1</name>
|
||||
</server>
|
||||
</ntp>
|
||||
</system>
|
||||
</config>
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
server_capabilities:
|
||||
description: list of capabilities of the server
|
||||
returned: success
|
||||
type: list of strings
|
||||
sample: ['urn:ietf:params:netconf:base:1.1','urn:ietf:params:netconf:capability:confirmed-commit:1.0','urn:ietf:params:netconf:capability:candidate:1.0']
|
||||
|
||||
'''
|
||||
|
||||
import xml.dom.minidom
|
||||
try:
|
||||
import ncclient.manager
|
||||
HAS_NCCLIENT = True
|
||||
except ImportError:
|
||||
HAS_NCCLIENT = False
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def netconf_edit_config(m, xml, commit, retkwargs):
|
||||
if ":candidate" in m.server_capabilities:
|
||||
datastore = 'candidate'
|
||||
else:
|
||||
datastore = 'running'
|
||||
m.lock(target=datastore)
|
||||
try:
|
||||
m.discard_changes()
|
||||
config_before = m.get_config(source=datastore)
|
||||
m.edit_config(target=datastore, config=xml)
|
||||
config_after = m.get_config(source=datastore)
|
||||
changed = config_before.data_xml != config_after.data_xml
|
||||
if changed and commit:
|
||||
if ":confirmed-commit" in m.server_capabilities:
|
||||
m.commit(confirmed=True)
|
||||
m.commit()
|
||||
else:
|
||||
m.commit()
|
||||
return changed
|
||||
finally:
|
||||
m.unlock(target=datastore)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- #
|
||||
# Main
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
host=dict(type='str', required=True),
|
||||
port=dict(type='int', default=830),
|
||||
hostkey_verify=dict(type='bool', default=True),
|
||||
username=dict(type='str', required=True, no_log=True),
|
||||
password=dict(type='str', required=True, no_log=True),
|
||||
xml=dict(type='str', required=True),
|
||||
)
|
||||
)
|
||||
|
||||
if not HAS_NCCLIENT:
|
||||
module.fail_json(msg='could not import the python library '
|
||||
'ncclient required by this module')
|
||||
|
||||
try:
|
||||
xml.dom.minidom.parseString(module.params['xml'])
|
||||
except:
|
||||
e = get_exception()
|
||||
module.fail_json(
|
||||
msg='error parsing XML: ' +
|
||||
str(e)
|
||||
)
|
||||
return
|
||||
|
||||
nckwargs = dict(
|
||||
host=module.params['host'],
|
||||
port=module.params['port'],
|
||||
hostkey_verify=module.params['hostkey_verify'],
|
||||
username=module.params['username'],
|
||||
password=module.params['password'],
|
||||
)
|
||||
retkwargs = dict()
|
||||
|
||||
try:
|
||||
m = ncclient.manager.connect(**nckwargs)
|
||||
except ncclient.transport.errors.AuthenticationError:
|
||||
module.fail_json(
|
||||
msg='authentication failed while connecting to device'
|
||||
)
|
||||
except:
|
||||
e = get_exception()
|
||||
module.fail_json(
|
||||
msg='error connecting to the device: ' +
|
||||
str(e)
|
||||
)
|
||||
return
|
||||
retkwargs['server_capabilities'] = list(m.server_capabilities)
|
||||
try:
|
||||
changed = netconf_edit_config(
|
||||
m=m,
|
||||
xml=module.params['xml'],
|
||||
commit=True,
|
||||
retkwargs=retkwargs,
|
||||
)
|
||||
finally:
|
||||
m.close_session()
|
||||
module.exit_json(changed=changed, **retkwargs)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1198
lib/ansible/modules/network/nmcli.py
Normal file
1198
lib/ansible/modules/network/nmcli.py
Normal file
File diff suppressed because it is too large
Load Diff
317
lib/ansible/modules/network/openvswitch_bridge.py
Normal file
317
lib/ansible/modules/network/openvswitch_bridge.py
Normal file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/python
|
||||
#coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, David Stygstra <david.stygstra@gmail.com>
|
||||
#
|
||||
# Portions copyright @ 2015 VMware, Inc.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# This module 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.
|
||||
#
|
||||
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pylint: disable=C0111
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: openvswitch_bridge
|
||||
version_added: 1.4
|
||||
author: "David Stygstra (@stygstra)"
|
||||
short_description: Manage Open vSwitch bridges
|
||||
requirements: [ ovs-vsctl ]
|
||||
description:
|
||||
- Manage Open vSwitch bridges
|
||||
options:
|
||||
bridge:
|
||||
required: true
|
||||
description:
|
||||
- Name of bridge or fake bridge to manage
|
||||
parent:
|
||||
version_added: "2.3"
|
||||
required: false
|
||||
default: None
|
||||
description:
|
||||
- Bridge parent of the fake bridge to manage
|
||||
vlan:
|
||||
version_added: "2.3"
|
||||
required: false
|
||||
default: None
|
||||
description:
|
||||
- The VLAN id of the fake bridge to manage (must be between 0 and 4095)
|
||||
state:
|
||||
required: false
|
||||
default: "present"
|
||||
choices: [ present, absent ]
|
||||
description:
|
||||
- Whether the bridge should exist
|
||||
timeout:
|
||||
required: false
|
||||
default: 5
|
||||
description:
|
||||
- How long to wait for ovs-vswitchd to respond
|
||||
external_ids:
|
||||
version_added: 2.0
|
||||
required: false
|
||||
default: None
|
||||
description:
|
||||
- A dictionary of external-ids. Omitting this parameter is a No-op.
|
||||
To clear all external-ids pass an empty value.
|
||||
fail_mode:
|
||||
version_added: 2.0
|
||||
default: None
|
||||
required: false
|
||||
choices : [secure, standalone]
|
||||
description:
|
||||
- Set bridge fail-mode. The default value (None) is a No-op.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a bridge named br-int
|
||||
- openvswitch_bridge:
|
||||
bridge: br-int
|
||||
state: present
|
||||
|
||||
# Create a fake bridge named br-int within br-parent on the VLAN 405
|
||||
- openvswitch_bridge:
|
||||
bridge: br-int
|
||||
parent: br-parent
|
||||
vlan: 405
|
||||
state: present
|
||||
|
||||
# Create an integration bridge
|
||||
- openvswitch_bridge:
|
||||
bridge: br-int
|
||||
state: present
|
||||
fail_mode: secure
|
||||
args:
|
||||
external_ids:
|
||||
bridge-id: br-int
|
||||
'''
|
||||
|
||||
|
||||
class OVSBridge(object):
|
||||
""" Interface to ovs-vsctl. """
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.bridge = module.params['bridge']
|
||||
self.parent = module.params['parent']
|
||||
self.vlan = module.params['vlan']
|
||||
self.state = module.params['state']
|
||||
self.timeout = module.params['timeout']
|
||||
self.fail_mode = module.params['fail_mode']
|
||||
|
||||
if self.parent:
|
||||
if self.vlan is None:
|
||||
self.module.fail_json(msg='VLAN id must be set when parent is defined')
|
||||
elif self.vlan < 0 or self.vlan > 4095:
|
||||
self.module.fail_json(msg='Invalid VLAN ID (must be between 0 and 4095)')
|
||||
|
||||
def _vsctl(self, command):
|
||||
'''Run ovs-vsctl command'''
|
||||
return self.module.run_command(['ovs-vsctl', '-t',
|
||||
str(self.timeout)] + command)
|
||||
|
||||
def exists(self):
|
||||
'''Check if the bridge already exists'''
|
||||
rtc, _, err = self._vsctl(['br-exists', self.bridge])
|
||||
if rtc == 0: # See ovs-vsctl(8) for status codes
|
||||
return True
|
||||
if rtc == 2:
|
||||
return False
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
def add(self):
|
||||
'''Create the bridge'''
|
||||
if self.parent and self.vlan: # Add fake bridge
|
||||
rtc, _, err = self._vsctl(['add-br', self.bridge, self.parent, self.vlan])
|
||||
else:
|
||||
rtc, _, err = self._vsctl(['add-br', self.bridge])
|
||||
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
if self.fail_mode:
|
||||
self.set_fail_mode()
|
||||
|
||||
def delete(self):
|
||||
'''Delete the bridge'''
|
||||
rtc, _, err = self._vsctl(['del-br', self.bridge])
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
def check(self):
|
||||
'''Run check mode'''
|
||||
changed = False
|
||||
|
||||
# pylint: disable=W0703
|
||||
try:
|
||||
if self.state == 'present' and self.exists():
|
||||
if (self.fail_mode and
|
||||
(self.fail_mode != self.get_fail_mode())):
|
||||
changed = True
|
||||
|
||||
##
|
||||
# Check if external ids would change.
|
||||
current_external_ids = self.get_external_ids()
|
||||
exp_external_ids = self.module.params['external_ids']
|
||||
if exp_external_ids is not None:
|
||||
for (key, value) in exp_external_ids:
|
||||
if ((key in current_external_ids) and
|
||||
(value != current_external_ids[key])):
|
||||
changed = True
|
||||
|
||||
##
|
||||
# Check if external ids would be removed.
|
||||
for (key, value) in current_external_ids.items():
|
||||
if key not in exp_external_ids:
|
||||
changed = True
|
||||
|
||||
elif self.state == 'absent' and self.exists():
|
||||
changed = True
|
||||
elif self.state == 'present' and not self.exists():
|
||||
changed = True
|
||||
except Exception:
|
||||
earg = get_exception()
|
||||
self.module.fail_json(msg=str(earg))
|
||||
|
||||
# pylint: enable=W0703
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
def run(self):
|
||||
'''Make the necessary changes'''
|
||||
changed = False
|
||||
# pylint: disable=W0703
|
||||
|
||||
try:
|
||||
if self.state == 'absent':
|
||||
if self.exists():
|
||||
self.delete()
|
||||
changed = True
|
||||
elif self.state == 'present':
|
||||
|
||||
if not self.exists():
|
||||
self.add()
|
||||
changed = True
|
||||
|
||||
current_fail_mode = self.get_fail_mode()
|
||||
if self.fail_mode and (self.fail_mode != current_fail_mode):
|
||||
self.module.log( "changing fail mode %s to %s" % (current_fail_mode, self.fail_mode))
|
||||
self.set_fail_mode()
|
||||
changed = True
|
||||
|
||||
current_external_ids = self.get_external_ids()
|
||||
|
||||
##
|
||||
# Change and add existing external ids.
|
||||
exp_external_ids = self.module.params['external_ids']
|
||||
if exp_external_ids is not None:
|
||||
for (key, value) in exp_external_ids.items():
|
||||
if ((value != current_external_ids.get(key, None)) and
|
||||
self.set_external_id(key, value)):
|
||||
changed = True
|
||||
|
||||
##
|
||||
# Remove current external ids that are not passed in.
|
||||
for (key, value) in current_external_ids.items():
|
||||
if ((key not in exp_external_ids) and
|
||||
self.set_external_id(key, None)):
|
||||
changed = True
|
||||
|
||||
except Exception:
|
||||
earg = get_exception()
|
||||
self.module.fail_json(msg=str(earg))
|
||||
# pylint: enable=W0703
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
def get_external_ids(self):
|
||||
""" Return the bridge's external ids as a dict. """
|
||||
results = {}
|
||||
if self.exists():
|
||||
rtc, out, err = self._vsctl(['br-get-external-id', self.bridge])
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
lines = out.split("\n")
|
||||
lines = [item.split("=") for item in lines if len(item) > 0]
|
||||
for item in lines:
|
||||
results[item[0]] = item[1]
|
||||
|
||||
return results
|
||||
|
||||
def set_external_id(self, key, value):
|
||||
""" Set external id. """
|
||||
if self.exists():
|
||||
cmd = ['br-set-external-id', self.bridge, key]
|
||||
if value:
|
||||
cmd += [value]
|
||||
|
||||
(rtc, _, err) = self._vsctl(cmd)
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_fail_mode(self):
|
||||
""" Get failure mode. """
|
||||
value = ''
|
||||
if self.exists():
|
||||
rtc, out, err = self._vsctl(['get-fail-mode', self.bridge])
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
value = out.strip("\n")
|
||||
return value
|
||||
|
||||
def set_fail_mode(self):
|
||||
""" Set failure mode. """
|
||||
|
||||
if self.exists():
|
||||
(rtc, _, err) = self._vsctl(['set-fail-mode', self.bridge,
|
||||
self.fail_mode])
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
|
||||
# pylint: disable=E0602
|
||||
def main():
|
||||
""" Entry point. """
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'bridge': {'required': True},
|
||||
'parent': {'default': None},
|
||||
'vlan': {'default': None, 'type': 'int'},
|
||||
'state': {'default': 'present', 'choices': ['present', 'absent']},
|
||||
'timeout': {'default': 5, 'type': 'int'},
|
||||
'external_ids': {'default': None, 'type': 'dict'},
|
||||
'fail_mode': {'default': None},
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
bridge = OVSBridge(module)
|
||||
if module.check_mode:
|
||||
bridge.check()
|
||||
else:
|
||||
bridge.run()
|
||||
|
||||
# pylint: disable=W0614
|
||||
# pylint: disable=W0401
|
||||
# pylint: disable=W0622
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
146
lib/ansible/modules/network/openvswitch_db.py
Normal file
146
lib/ansible/modules/network/openvswitch_db.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# pylint: disable=C0111
|
||||
|
||||
#
|
||||
# (c) 2015, Mark Hamilton <mhamilton@vmware.com>
|
||||
#
|
||||
# Portions copyright @ 2015 VMware, Inc.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# This module 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.
|
||||
#
|
||||
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: openvswitch_db
|
||||
author: "Mark Hamilton (mhamilton@vmware.com)"
|
||||
version_added: 2.0
|
||||
short_description: Configure open vswitch database.
|
||||
requirements: [ "ovs-vsctl >= 2.3.3" ]
|
||||
description:
|
||||
- Set column values in record in database table.
|
||||
options:
|
||||
table:
|
||||
required: true
|
||||
description:
|
||||
- Identifies the table in the database.
|
||||
record:
|
||||
required: true
|
||||
description:
|
||||
- Identifies the recoard in the table.
|
||||
column:
|
||||
required: true
|
||||
description:
|
||||
- Identifies the column in the record.
|
||||
key:
|
||||
required: true
|
||||
description:
|
||||
- Identifies the key in the record column
|
||||
value:
|
||||
required: true
|
||||
description:
|
||||
- Expected value for the table, record, column and key.
|
||||
timeout:
|
||||
required: false
|
||||
default: 5
|
||||
description:
|
||||
- How long to wait for ovs-vswitchd to respond
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
# Increase the maximum idle time to 50 seconds before pruning unused kernel
|
||||
# rules.
|
||||
- openvswitch_db:
|
||||
table: open_vswitch
|
||||
record: .
|
||||
col: other_config
|
||||
key: max-idle
|
||||
value: 50000
|
||||
|
||||
# Disable in band copy
|
||||
- openvswitch_db:
|
||||
table: Bridge
|
||||
record: br-int
|
||||
col: other_config
|
||||
key: disable-in-band
|
||||
value: true
|
||||
'''
|
||||
|
||||
|
||||
def cmd_run(module, cmd, check_rc=True):
|
||||
""" Log and run ovs-vsctl command. """
|
||||
return module.run_command(cmd.split(" "), check_rc=check_rc)
|
||||
|
||||
|
||||
def params_set(module):
|
||||
""" Implement the ovs-vsctl set commands. """
|
||||
|
||||
changed = False
|
||||
|
||||
##
|
||||
# Place in params dictionary in order to support the string format below.
|
||||
module.params["ovs-vsctl"] = module.get_bin_path("ovs-vsctl", True)
|
||||
|
||||
fmt = "%(ovs-vsctl)s -t %(timeout)s get %(table)s %(record)s " \
|
||||
"%(col)s:%(key)s"
|
||||
|
||||
cmd = fmt % module.params
|
||||
|
||||
(_, output, _) = cmd_run(module, cmd, False)
|
||||
if module.params['value'] not in output:
|
||||
fmt = "%(ovs-vsctl)s -t %(timeout)s set %(table)s %(record)s " \
|
||||
"%(col)s:%(key)s=%(value)s"
|
||||
cmd = fmt % module.params
|
||||
##
|
||||
# Check if flow exists and is the same.
|
||||
(rtc, _, err) = cmd_run(module, cmd)
|
||||
if rtc != 0:
|
||||
module.fail_json(msg=err)
|
||||
changed = True
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
# pylint: disable=E0602
|
||||
def main():
|
||||
""" Entry point for ansible module. """
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'table': {'required': True},
|
||||
'record': {'required': True},
|
||||
'col': {'required': True},
|
||||
'key': {'required': True},
|
||||
'value': {'required': True},
|
||||
'timeout': {'default': 5, 'type': 'int'},
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
params_set(module)
|
||||
|
||||
|
||||
# pylint: disable=W0614
|
||||
# pylint: disable=W0401
|
||||
# pylint: disable=W0622
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
294
lib/ansible/modules/network/openvswitch_port.py
Normal file
294
lib/ansible/modules/network/openvswitch_port.py
Normal file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/python
|
||||
#coding: utf-8 -*-
|
||||
|
||||
# pylint: disable=C0111
|
||||
|
||||
# (c) 2013, David Stygstra <david.stygstra@gmail.com>
|
||||
#
|
||||
# Portions copyright @ 2015 VMware, Inc.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# This module 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.
|
||||
#
|
||||
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: openvswitch_port
|
||||
version_added: 1.4
|
||||
author: "David Stygstra (@stygstra)"
|
||||
short_description: Manage Open vSwitch ports
|
||||
requirements: [ ovs-vsctl ]
|
||||
description:
|
||||
- Manage Open vSwitch ports
|
||||
options:
|
||||
bridge:
|
||||
required: true
|
||||
description:
|
||||
- Name of bridge to manage
|
||||
port:
|
||||
required: true
|
||||
description:
|
||||
- Name of port to manage on the bridge
|
||||
tag:
|
||||
version_added: 2.2
|
||||
required: false
|
||||
description:
|
||||
- VLAN tag for this port
|
||||
state:
|
||||
required: false
|
||||
default: "present"
|
||||
choices: [ present, absent ]
|
||||
description:
|
||||
- Whether the port should exist
|
||||
timeout:
|
||||
required: false
|
||||
default: 5
|
||||
description:
|
||||
- How long to wait for ovs-vswitchd to respond
|
||||
external_ids:
|
||||
version_added: 2.0
|
||||
required: false
|
||||
default: {}
|
||||
description:
|
||||
- Dictionary of external_ids applied to a port.
|
||||
set:
|
||||
version_added: 2.0
|
||||
required: false
|
||||
default: None
|
||||
description:
|
||||
- Set a single property on a port.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Creates port eth2 on bridge br-ex
|
||||
- openvswitch_port:
|
||||
bridge: br-ex
|
||||
port: eth2
|
||||
state: present
|
||||
|
||||
# Creates port eth6
|
||||
- openvswitch_port:
|
||||
bridge: bridge-loop
|
||||
port: eth6
|
||||
state: present
|
||||
set: Interface eth6
|
||||
|
||||
# Creates port vlan10 with tag 10 on bridge br-ex
|
||||
- openvswitch_port:
|
||||
bridge: br-ex
|
||||
port: vlan10
|
||||
tag: 10
|
||||
state: present
|
||||
set: Interface vlan10
|
||||
|
||||
# Assign interface id server1-vifeth6 and mac address 00:00:5E:00:53:23
|
||||
# to port vifeth6 and setup port to be managed by a controller.
|
||||
- openvswitch_port:
|
||||
bridge: br-int
|
||||
port: vifeth6
|
||||
state: present
|
||||
args:
|
||||
external_ids:
|
||||
iface-id: '{{ inventory_hostname }}-vifeth6'
|
||||
attached-mac: '00:00:5E:00:53:23'
|
||||
vm-id: '{{ inventory_hostname }}'
|
||||
iface-status: active
|
||||
'''
|
||||
|
||||
# pylint: disable=W0703
|
||||
|
||||
|
||||
def truncate_before(value, srch):
|
||||
""" Return content of str before the srch parameters. """
|
||||
|
||||
before_index = value.find(srch)
|
||||
if (before_index >= 0):
|
||||
return value[:before_index]
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def _set_to_get(set_cmd, module):
|
||||
""" Convert set command to get command and set value.
|
||||
return tuple (get command, set value)
|
||||
"""
|
||||
|
||||
##
|
||||
# If set has option: then we want to truncate just before that.
|
||||
set_cmd = truncate_before(set_cmd, " option:")
|
||||
get_cmd = set_cmd.split(" ")
|
||||
(key, value) = get_cmd[-1].split("=")
|
||||
module.log("get commands %s " % key)
|
||||
return (["--", "get"] + get_cmd[:-1] + [key], value)
|
||||
|
||||
|
||||
# pylint: disable=R0902
|
||||
class OVSPort(object):
|
||||
""" Interface to OVS port. """
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.bridge = module.params['bridge']
|
||||
self.port = module.params['port']
|
||||
self.tag = module.params['tag']
|
||||
self.state = module.params['state']
|
||||
self.timeout = module.params['timeout']
|
||||
self.set_opt = module.params.get('set', None)
|
||||
|
||||
def _vsctl(self, command, check_rc=True):
|
||||
'''Run ovs-vsctl command'''
|
||||
|
||||
cmd = ['ovs-vsctl', '-t', str(self.timeout)] + command
|
||||
return self.module.run_command(cmd, check_rc=check_rc)
|
||||
|
||||
def exists(self):
|
||||
'''Check if the port already exists'''
|
||||
|
||||
(rtc, out, err) = self._vsctl(['list-ports', self.bridge])
|
||||
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
return any(port.rstrip() == self.port for port in out.split('\n')) or self.port == self.bridge
|
||||
|
||||
def set(self, set_opt):
|
||||
""" Set attributes on a port. """
|
||||
self.module.log("set called %s" % set_opt)
|
||||
if (not set_opt):
|
||||
return False
|
||||
|
||||
(get_cmd, set_value) = _set_to_get(set_opt, self.module)
|
||||
(rtc, out, err) = self._vsctl(get_cmd, False)
|
||||
if rtc != 0:
|
||||
##
|
||||
# ovs-vsctl -t 5 -- get Interface port external_ids:key
|
||||
# returns failure if key does not exist.
|
||||
out = None
|
||||
else:
|
||||
out = out.strip("\n")
|
||||
out = out.strip('"')
|
||||
|
||||
if (out == set_value):
|
||||
return False
|
||||
|
||||
(rtc, out, err) = self._vsctl(["--", "set"] + set_opt.split(" "))
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
return True
|
||||
|
||||
def add(self):
|
||||
'''Add the port'''
|
||||
cmd = ['add-port', self.bridge, self.port]
|
||||
if self.tag:
|
||||
cmd += ["tag=" + self.tag]
|
||||
if self.set and self.set_opt:
|
||||
cmd += ["--", "set"]
|
||||
cmd += self.set_opt.split(" ")
|
||||
|
||||
(rtc, _, err) = self._vsctl(cmd)
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
return True
|
||||
|
||||
def delete(self):
|
||||
'''Remove the port'''
|
||||
(rtc, _, err) = self._vsctl(['del-port', self.bridge, self.port])
|
||||
if rtc != 0:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
def check(self):
|
||||
'''Run check mode'''
|
||||
try:
|
||||
if self.state == 'absent' and self.exists():
|
||||
changed = True
|
||||
elif self.state == 'present' and not self.exists():
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
except Exception:
|
||||
earg = get_exception()
|
||||
self.module.fail_json(msg=str(earg))
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
def run(self):
|
||||
'''Make the necessary changes'''
|
||||
changed = False
|
||||
try:
|
||||
if self.state == 'absent':
|
||||
if self.exists():
|
||||
self.delete()
|
||||
changed = True
|
||||
elif self.state == 'present':
|
||||
##
|
||||
# Add any missing ports.
|
||||
if (not self.exists()):
|
||||
self.add()
|
||||
changed = True
|
||||
|
||||
##
|
||||
# If the -- set changed check here and make changes
|
||||
# but this only makes sense when state=present.
|
||||
if (not changed):
|
||||
changed = self.set(self.set_opt) or changed
|
||||
items = self.module.params['external_ids'].items()
|
||||
for (key, value) in items:
|
||||
value = value.replace('"', '')
|
||||
fmt_opt = "Interface %s external_ids:%s=%s"
|
||||
external_id = fmt_opt % (self.port, key, value)
|
||||
changed = self.set(external_id) or changed
|
||||
##
|
||||
except Exception:
|
||||
earg = get_exception()
|
||||
self.module.fail_json(msg=str(earg))
|
||||
self.module.exit_json(changed=changed)
|
||||
|
||||
|
||||
# pylint: disable=E0602
|
||||
def main():
|
||||
""" Entry point. """
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'bridge': {'required': True},
|
||||
'port': {'required': True},
|
||||
'tag': {'required': False},
|
||||
'state': {'default': 'present', 'choices': ['present', 'absent']},
|
||||
'timeout': {'default': 5, 'type': 'int'},
|
||||
'set': {'required': False, 'default': None},
|
||||
'external_ids': {'default': {}, 'required': False, 'type': 'dict'},
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
port = OVSPort(module)
|
||||
if module.check_mode:
|
||||
port.check()
|
||||
else:
|
||||
port.run()
|
||||
|
||||
|
||||
# pylint: disable=W0614
|
||||
# pylint: disable=W0401
|
||||
# pylint: disable=W0622
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
lib/ansible/modules/network/panos/__init__.py
Normal file
0
lib/ansible/modules/network/panos/__init__.py
Normal file
204
lib/ansible/modules/network/panos/panos_admin.py
Executable file
204
lib/ansible/modules/network/panos/panos_admin.py
Executable file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Ansible module to manage PaloAltoNetworks Firewall
|
||||
# (c) 2016, techbizdev <techbizdev@paloaltonetworks.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: panos_admin
|
||||
short_description: Add or modify PAN-OS user accounts password.
|
||||
description:
|
||||
- PanOS module that allows changes to the user account passwords by doing
|
||||
API calls to the Firewall using pan-api as the protocol.
|
||||
author: "Luigi Mori (@jtschichold), Ivan Bojer (@ivanbojer)"
|
||||
version_added: "2.3"
|
||||
requirements:
|
||||
- pan-python
|
||||
options:
|
||||
ip_address:
|
||||
description:
|
||||
- IP address (or hostname) of PAN-OS device
|
||||
required: true
|
||||
password:
|
||||
description:
|
||||
- password for authentication
|
||||
required: true
|
||||
username:
|
||||
description:
|
||||
- username for authentication
|
||||
required: false
|
||||
default: "admin"
|
||||
admin_username:
|
||||
description:
|
||||
- username for admin user
|
||||
required: false
|
||||
default: "admin"
|
||||
admin_password:
|
||||
description:
|
||||
- password for admin user
|
||||
required: true
|
||||
role:
|
||||
description:
|
||||
- role for admin user
|
||||
required: false
|
||||
default: null
|
||||
commit:
|
||||
description:
|
||||
- commit if changed
|
||||
required: false
|
||||
default: true
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Set the password of user admin to "badpassword"
|
||||
# Doesn't commit the candidate config
|
||||
- name: set admin password
|
||||
panos_admin:
|
||||
ip_address: "192.168.1.1"
|
||||
password: "admin"
|
||||
admin_username: admin
|
||||
admin_password: "badpassword"
|
||||
commit: False
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
status:
|
||||
description: success status
|
||||
returned: success
|
||||
type: string
|
||||
sample: "okey dokey"
|
||||
'''
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
try:
|
||||
import pan.xapi
|
||||
HAS_LIB = True
|
||||
except ImportError:
|
||||
HAS_LIB = False
|
||||
|
||||
_ADMIN_XPATH = "/config/mgt-config/users/entry[@name='%s']"
|
||||
|
||||
|
||||
def admin_exists(xapi, admin_username):
|
||||
xapi.get(_ADMIN_XPATH % admin_username)
|
||||
e = xapi.element_root.find('.//entry')
|
||||
return e
|
||||
|
||||
|
||||
def admin_set(xapi, module, admin_username, admin_password, role):
|
||||
if admin_password is not None:
|
||||
xapi.op(cmd='request password-hash password "%s"' % admin_password,
|
||||
cmd_xml=True)
|
||||
r = xapi.element_root
|
||||
phash = r.find('.//phash').text
|
||||
if role is not None:
|
||||
rbval = "yes"
|
||||
if role != "superuser" and role != 'superreader':
|
||||
rbval = ""
|
||||
|
||||
ea = admin_exists(xapi, admin_username)
|
||||
if ea is not None:
|
||||
# user exists
|
||||
changed = False
|
||||
|
||||
if role is not None:
|
||||
rb = ea.find('.//role-based')
|
||||
if rb is not None:
|
||||
if rb[0].tag != role:
|
||||
changed = True
|
||||
xpath = _ADMIN_XPATH % admin_username
|
||||
xpath += '/permissions/role-based/%s' % rb[0].tag
|
||||
xapi.delete(xpath=xpath)
|
||||
|
||||
xpath = _ADMIN_XPATH % admin_username
|
||||
xpath += '/permissions/role-based'
|
||||
xapi.set(xpath=xpath,
|
||||
element='<%s>%s</%s>' % (role, rbval, role))
|
||||
|
||||
if admin_password is not None:
|
||||
xapi.edit(xpath=_ADMIN_XPATH % admin_username+'/phash',
|
||||
element='<phash>%s</phash>' % phash)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
# setup the non encrypted part of the monitor
|
||||
exml = []
|
||||
|
||||
exml.append('<phash>%s</phash>' % phash)
|
||||
exml.append('<permissions><role-based><%s>%s</%s>'
|
||||
'</role-based></permissions>' % (role, rbval, role))
|
||||
|
||||
exml = ''.join(exml)
|
||||
# module.fail_json(msg=exml)
|
||||
|
||||
xapi.set(xpath=_ADMIN_XPATH % admin_username, element=exml)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
ip_address=dict(),
|
||||
password=dict(no_log=True),
|
||||
username=dict(default='admin'),
|
||||
admin_username=dict(default='admin'),
|
||||
admin_password=dict(no_log=True),
|
||||
role=dict(),
|
||||
commit=dict(type='bool', default=True)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
|
||||
if not HAS_LIB:
|
||||
module.fail_json(msg='pan-python required for this module')
|
||||
|
||||
ip_address = module.params["ip_address"]
|
||||
if not ip_address:
|
||||
module.fail_json(msg="ip_address should be specified")
|
||||
password = module.params["password"]
|
||||
if not password:
|
||||
module.fail_json(msg="password is required")
|
||||
username = module.params['username']
|
||||
|
||||
xapi = pan.xapi.PanXapi(
|
||||
hostname=ip_address,
|
||||
api_username=username,
|
||||
api_password=password
|
||||
)
|
||||
|
||||
admin_username = module.params['admin_username']
|
||||
if admin_username is None:
|
||||
module.fail_json(msg="admin_username is required")
|
||||
admin_password = module.params['admin_password']
|
||||
role = module.params['role']
|
||||
commit = module.params['commit']
|
||||
|
||||
changed = admin_set(xapi, module, admin_username, admin_password, role)
|
||||
|
||||
if changed and commit:
|
||||
xapi.commit(cmd="<commit></commit>", sync=True, interval=1)
|
||||
|
||||
module.exit_json(changed=changed, msg="okey dokey")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
375
lib/ansible/modules/network/snmp_facts.py
Normal file
375
lib/ansible/modules/network/snmp_facts.py
Normal file
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# This file is part of Networklore's snmp library for Ansible
|
||||
#
|
||||
# The module 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.
|
||||
#
|
||||
# The module 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: snmp_facts
|
||||
version_added: "1.9"
|
||||
author: "Patrick Ogenstad (@ogenstad)"
|
||||
short_description: Retrieve facts for a device using SNMP.
|
||||
description:
|
||||
- Retrieve facts for a device using SNMP, the facts will be
|
||||
inserted to the ansible_facts key.
|
||||
requirements:
|
||||
- pysnmp
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
- Set to target snmp server (normally {{inventory_hostname}})
|
||||
required: true
|
||||
version:
|
||||
description:
|
||||
- SNMP Version to use, v2/v2c or v3
|
||||
choices: [ 'v2', 'v2c', 'v3' ]
|
||||
required: true
|
||||
community:
|
||||
description:
|
||||
- The SNMP community string, required if version is v2/v2c
|
||||
required: false
|
||||
level:
|
||||
description:
|
||||
- Authentication level, required if version is v3
|
||||
choices: [ 'authPriv', 'authNoPriv' ]
|
||||
required: false
|
||||
username:
|
||||
description:
|
||||
- Username for SNMPv3, required if version is v3
|
||||
required: false
|
||||
integrity:
|
||||
description:
|
||||
- Hashing algoritm, required if version is v3
|
||||
choices: [ 'md5', 'sha' ]
|
||||
required: false
|
||||
authkey:
|
||||
description:
|
||||
- Authentication key, required if version is v3
|
||||
required: false
|
||||
privacy:
|
||||
description:
|
||||
- Encryption algoritm, required if level is authPriv
|
||||
choices: [ 'des', 'aes' ]
|
||||
required: false
|
||||
privkey:
|
||||
description:
|
||||
- Encryption key, required if version is authPriv
|
||||
required: false
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Gather facts with SNMP version 2
|
||||
- snmp_facts:
|
||||
host: '{{ inventory_hostname }}'
|
||||
version: 2c
|
||||
community: public
|
||||
delegate_to: local
|
||||
|
||||
# Gather facts using SNMP version 3
|
||||
- snmp_facts:
|
||||
host: '{{ inventory_hostname }}'
|
||||
version: v3
|
||||
level: authPriv
|
||||
integrity: sha
|
||||
privacy: aes
|
||||
username: snmp-user
|
||||
authkey: abc12345
|
||||
privkey: def6789
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
has_pysnmp = True
|
||||
except:
|
||||
has_pysnmp = False
|
||||
|
||||
class DefineOid(object):
|
||||
|
||||
def __init__(self,dotprefix=False):
|
||||
if dotprefix:
|
||||
dp = "."
|
||||
else:
|
||||
dp = ""
|
||||
|
||||
# From SNMPv2-MIB
|
||||
self.sysDescr = dp + "1.3.6.1.2.1.1.1.0"
|
||||
self.sysObjectId = dp + "1.3.6.1.2.1.1.2.0"
|
||||
self.sysUpTime = dp + "1.3.6.1.2.1.1.3.0"
|
||||
self.sysContact = dp + "1.3.6.1.2.1.1.4.0"
|
||||
self.sysName = dp + "1.3.6.1.2.1.1.5.0"
|
||||
self.sysLocation = dp + "1.3.6.1.2.1.1.6.0"
|
||||
|
||||
# From IF-MIB
|
||||
self.ifIndex = dp + "1.3.6.1.2.1.2.2.1.1"
|
||||
self.ifDescr = dp + "1.3.6.1.2.1.2.2.1.2"
|
||||
self.ifMtu = dp + "1.3.6.1.2.1.2.2.1.4"
|
||||
self.ifSpeed = dp + "1.3.6.1.2.1.2.2.1.5"
|
||||
self.ifPhysAddress = dp + "1.3.6.1.2.1.2.2.1.6"
|
||||
self.ifAdminStatus = dp + "1.3.6.1.2.1.2.2.1.7"
|
||||
self.ifOperStatus = dp + "1.3.6.1.2.1.2.2.1.8"
|
||||
self.ifAlias = dp + "1.3.6.1.2.1.31.1.1.1.18"
|
||||
|
||||
# From IP-MIB
|
||||
self.ipAdEntAddr = dp + "1.3.6.1.2.1.4.20.1.1"
|
||||
self.ipAdEntIfIndex = dp + "1.3.6.1.2.1.4.20.1.2"
|
||||
self.ipAdEntNetMask = dp + "1.3.6.1.2.1.4.20.1.3"
|
||||
|
||||
|
||||
def decode_hex(hexstring):
|
||||
|
||||
if len(hexstring) < 3:
|
||||
return hexstring
|
||||
if hexstring[:2] == "0x":
|
||||
return hexstring[2:].decode("hex")
|
||||
else:
|
||||
return hexstring
|
||||
|
||||
def decode_mac(hexstring):
|
||||
|
||||
if len(hexstring) != 14:
|
||||
return hexstring
|
||||
if hexstring[:2] == "0x":
|
||||
return hexstring[2:]
|
||||
else:
|
||||
return hexstring
|
||||
|
||||
def lookup_adminstatus(int_adminstatus):
|
||||
adminstatus_options = {
|
||||
1: 'up',
|
||||
2: 'down',
|
||||
3: 'testing'
|
||||
}
|
||||
if int_adminstatus in adminstatus_options:
|
||||
return adminstatus_options[int_adminstatus]
|
||||
else:
|
||||
return ""
|
||||
|
||||
def lookup_operstatus(int_operstatus):
|
||||
operstatus_options = {
|
||||
1: 'up',
|
||||
2: 'down',
|
||||
3: 'testing',
|
||||
4: 'unknown',
|
||||
5: 'dormant',
|
||||
6: 'notPresent',
|
||||
7: 'lowerLayerDown'
|
||||
}
|
||||
if int_operstatus in operstatus_options:
|
||||
return operstatus_options[int_operstatus]
|
||||
else:
|
||||
return ""
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
host=dict(required=True),
|
||||
version=dict(required=True, choices=['v2', 'v2c', 'v3']),
|
||||
community=dict(required=False, default=False),
|
||||
username=dict(required=False),
|
||||
level=dict(required=False, choices=['authNoPriv', 'authPriv']),
|
||||
integrity=dict(required=False, choices=['md5', 'sha']),
|
||||
privacy=dict(required=False, choices=['des', 'aes']),
|
||||
authkey=dict(required=False),
|
||||
privkey=dict(required=False),
|
||||
removeplaceholder=dict(required=False)),
|
||||
required_together = ( ['username','level','integrity','authkey'],['privacy','privkey'],),
|
||||
supports_check_mode=False)
|
||||
|
||||
m_args = module.params
|
||||
|
||||
if not has_pysnmp:
|
||||
module.fail_json(msg='Missing required pysnmp module (check docs)')
|
||||
|
||||
cmdGen = cmdgen.CommandGenerator()
|
||||
|
||||
# Verify that we receive a community when using snmp v2
|
||||
if m_args['version'] == "v2" or m_args['version'] == "v2c":
|
||||
if m_args['community'] == False:
|
||||
module.fail_json(msg='Community not set when using snmp version 2')
|
||||
|
||||
if m_args['version'] == "v3":
|
||||
if m_args['username'] == None:
|
||||
module.fail_json(msg='Username not set when using snmp version 3')
|
||||
|
||||
if m_args['level'] == "authPriv" and m_args['privacy'] == None:
|
||||
module.fail_json(msg='Privacy algorithm not set when using authPriv')
|
||||
|
||||
|
||||
if m_args['integrity'] == "sha":
|
||||
integrity_proto = cmdgen.usmHMACSHAAuthProtocol
|
||||
elif m_args['integrity'] == "md5":
|
||||
integrity_proto = cmdgen.usmHMACMD5AuthProtocol
|
||||
|
||||
if m_args['privacy'] == "aes":
|
||||
privacy_proto = cmdgen.usmAesCfb128Protocol
|
||||
elif m_args['privacy'] == "des":
|
||||
privacy_proto = cmdgen.usmDESPrivProtocol
|
||||
|
||||
# Use SNMP Version 2
|
||||
if m_args['version'] == "v2" or m_args['version'] == "v2c":
|
||||
snmp_auth = cmdgen.CommunityData(m_args['community'])
|
||||
|
||||
# Use SNMP Version 3 with authNoPriv
|
||||
elif m_args['level'] == "authNoPriv":
|
||||
snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], authProtocol=integrity_proto)
|
||||
|
||||
# Use SNMP Version 3 with authPriv
|
||||
else:
|
||||
snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], privKey=m_args['privkey'], authProtocol=integrity_proto, privProtocol=privacy_proto)
|
||||
|
||||
# Use p to prefix OIDs with a dot for polling
|
||||
p = DefineOid(dotprefix=True)
|
||||
# Use v without a prefix to use with return values
|
||||
v = DefineOid(dotprefix=False)
|
||||
|
||||
Tree = lambda: defaultdict(Tree)
|
||||
|
||||
results = Tree()
|
||||
|
||||
errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
|
||||
snmp_auth,
|
||||
cmdgen.UdpTransportTarget((m_args['host'], 161)),
|
||||
cmdgen.MibVariable(p.sysDescr,),
|
||||
cmdgen.MibVariable(p.sysObjectId,),
|
||||
cmdgen.MibVariable(p.sysUpTime,),
|
||||
cmdgen.MibVariable(p.sysContact,),
|
||||
cmdgen.MibVariable(p.sysName,),
|
||||
cmdgen.MibVariable(p.sysLocation,),
|
||||
lookupMib=False
|
||||
)
|
||||
|
||||
|
||||
if errorIndication:
|
||||
module.fail_json(msg=str(errorIndication))
|
||||
|
||||
for oid, val in varBinds:
|
||||
current_oid = oid.prettyPrint()
|
||||
current_val = val.prettyPrint()
|
||||
if current_oid == v.sysDescr:
|
||||
results['ansible_sysdescr'] = decode_hex(current_val)
|
||||
elif current_oid == v.sysObjectId:
|
||||
results['ansible_sysobjectid'] = current_val
|
||||
elif current_oid == v.sysUpTime:
|
||||
results['ansible_sysuptime'] = current_val
|
||||
elif current_oid == v.sysContact:
|
||||
results['ansible_syscontact'] = current_val
|
||||
elif current_oid == v.sysName:
|
||||
results['ansible_sysname'] = current_val
|
||||
elif current_oid == v.sysLocation:
|
||||
results['ansible_syslocation'] = current_val
|
||||
|
||||
errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd(
|
||||
snmp_auth,
|
||||
cmdgen.UdpTransportTarget((m_args['host'], 161)),
|
||||
cmdgen.MibVariable(p.ifIndex,),
|
||||
cmdgen.MibVariable(p.ifDescr,),
|
||||
cmdgen.MibVariable(p.ifMtu,),
|
||||
cmdgen.MibVariable(p.ifSpeed,),
|
||||
cmdgen.MibVariable(p.ifPhysAddress,),
|
||||
cmdgen.MibVariable(p.ifAdminStatus,),
|
||||
cmdgen.MibVariable(p.ifOperStatus,),
|
||||
cmdgen.MibVariable(p.ipAdEntAddr,),
|
||||
cmdgen.MibVariable(p.ipAdEntIfIndex,),
|
||||
cmdgen.MibVariable(p.ipAdEntNetMask,),
|
||||
|
||||
cmdgen.MibVariable(p.ifAlias,),
|
||||
lookupMib=False
|
||||
)
|
||||
|
||||
|
||||
if errorIndication:
|
||||
module.fail_json(msg=str(errorIndication))
|
||||
|
||||
interface_indexes = []
|
||||
|
||||
all_ipv4_addresses = []
|
||||
ipv4_networks = Tree()
|
||||
|
||||
for varBinds in varTable:
|
||||
for oid, val in varBinds:
|
||||
current_oid = oid.prettyPrint()
|
||||
current_val = val.prettyPrint()
|
||||
if v.ifIndex in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['ifindex'] = current_val
|
||||
interface_indexes.append(ifIndex)
|
||||
if v.ifDescr in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['name'] = current_val
|
||||
if v.ifMtu in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['mtu'] = current_val
|
||||
if v.ifMtu in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['speed'] = current_val
|
||||
if v.ifPhysAddress in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['mac'] = decode_mac(current_val)
|
||||
if v.ifAdminStatus in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['adminstatus'] = lookup_adminstatus(int(current_val))
|
||||
if v.ifOperStatus in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['operstatus'] = lookup_operstatus(int(current_val))
|
||||
if v.ipAdEntAddr in current_oid:
|
||||
curIPList = current_oid.rsplit('.', 4)[-4:]
|
||||
curIP = ".".join(curIPList)
|
||||
ipv4_networks[curIP]['address'] = current_val
|
||||
all_ipv4_addresses.append(current_val)
|
||||
if v.ipAdEntIfIndex in current_oid:
|
||||
curIPList = current_oid.rsplit('.', 4)[-4:]
|
||||
curIP = ".".join(curIPList)
|
||||
ipv4_networks[curIP]['interface'] = current_val
|
||||
if v.ipAdEntNetMask in current_oid:
|
||||
curIPList = current_oid.rsplit('.', 4)[-4:]
|
||||
curIP = ".".join(curIPList)
|
||||
ipv4_networks[curIP]['netmask'] = current_val
|
||||
|
||||
if v.ifAlias in current_oid:
|
||||
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||
results['ansible_interfaces'][ifIndex]['description'] = current_val
|
||||
|
||||
interface_to_ipv4 = {}
|
||||
for ipv4_network in ipv4_networks:
|
||||
current_interface = ipv4_networks[ipv4_network]['interface']
|
||||
current_network = {
|
||||
'address': ipv4_networks[ipv4_network]['address'],
|
||||
'netmask': ipv4_networks[ipv4_network]['netmask']
|
||||
}
|
||||
if not current_interface in interface_to_ipv4:
|
||||
interface_to_ipv4[current_interface] = []
|
||||
interface_to_ipv4[current_interface].append(current_network)
|
||||
else:
|
||||
interface_to_ipv4[current_interface].append(current_network)
|
||||
|
||||
for interface in interface_to_ipv4:
|
||||
results['ansible_interfaces'][int(interface)]['ipv4'] = interface_to_ipv4[interface]
|
||||
|
||||
results['ansible_all_ipv4_addresses'] = all_ipv4_addresses
|
||||
|
||||
module.exit_json(ansible_facts=results)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
135
lib/ansible/modules/network/wakeonlan.py
Normal file
135
lib/ansible/modules/network/wakeonlan.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016, Dag Wieers <dag@wieers.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: wakeonlan
|
||||
version_added: 2.2
|
||||
short_description: Send a magic Wake-on-LAN (WoL) broadcast packet
|
||||
description:
|
||||
- The M(wakeonlan) module sends magic Wake-on-LAN (WoL) broadcast packets.
|
||||
options:
|
||||
mac:
|
||||
description:
|
||||
- MAC address to send Wake-on-LAN broadcast packet for
|
||||
required: true
|
||||
default: null
|
||||
broadcast:
|
||||
description:
|
||||
- Network broadcast address to use for broadcasting magic Wake-on-LAN packet
|
||||
required: false
|
||||
default: 255.255.255.255
|
||||
port:
|
||||
description:
|
||||
- UDP port to use for magic Wake-on-LAN packet
|
||||
required: false
|
||||
default: 7
|
||||
author: "Dag Wieers (@dagwieers)"
|
||||
todo:
|
||||
- Add arping support to check whether the system is up (before and after)
|
||||
- Enable check-mode support (when we have arping support)
|
||||
- Does not have SecureOn password support
|
||||
notes:
|
||||
- This module sends a magic packet, without knowing whether it worked
|
||||
- Only works if the target system was properly configured for Wake-on-LAN (in the BIOS and/or the OS)
|
||||
- Some BIOSes have a different (configurable) Wake-on-LAN boot order (i.e. PXE first) when turned off
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Send a magic Wake-on-LAN packet to 00:00:5E:00:53:66
|
||||
- wakeonlan:
|
||||
mac: '00:00:5E:00:53:66'
|
||||
broadcast: 192.0.2.23
|
||||
delegate_to: loclahost
|
||||
|
||||
- wakeonlan:
|
||||
mac: 00:00:5E:00:53:66
|
||||
port: 9
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN='''
|
||||
# Default return values
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
import socket
|
||||
import struct
|
||||
|
||||
|
||||
def wakeonlan(module, mac, broadcast, port):
|
||||
""" Send a magic Wake-on-LAN packet. """
|
||||
|
||||
mac_orig = mac
|
||||
|
||||
# Remove possible seperator from MAC address
|
||||
if len(mac) == 12 + 5:
|
||||
mac = mac.replace(mac[2], '')
|
||||
|
||||
# If we don't end up with 12 hexadecimal characters, fail
|
||||
if len(mac) != 12:
|
||||
module.fail_json(msg="Incorrect MAC address length: %s" % mac_orig)
|
||||
|
||||
# Test if it converts to an integer, otherwise fail
|
||||
try:
|
||||
int(mac, 16)
|
||||
except ValueError:
|
||||
module.fail_json(msg="Incorrect MAC address format: %s" % mac_orig)
|
||||
|
||||
# Create payload for magic packet
|
||||
data = ''
|
||||
padding = ''.join(['FFFFFFFFFFFF', mac * 20])
|
||||
for i in range(0, len(padding), 2):
|
||||
data = ''.join([data, struct.pack('B', int(padding[i: i + 2], 16))])
|
||||
|
||||
# Broadcast payload to network
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
try:
|
||||
sock.sendto(data, (broadcast, port))
|
||||
except socket.error:
|
||||
e = get_exception()
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
mac = dict(required=True, type='str'),
|
||||
broadcast = dict(required=False, default='255.255.255.255'),
|
||||
port = dict(required=False, type='int', default=7),
|
||||
),
|
||||
)
|
||||
|
||||
mac = module.params.get('mac')
|
||||
broadcast = module.params.get('broadcast')
|
||||
port = module.params.get('port')
|
||||
|
||||
wakeonlan(module, mac, broadcast, port)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user