Relocating extras into lib/ansible/modules/ after merge

This commit is contained in:
James Cammarata 2016-12-08 00:36:57 -05:00 committed by Matt Clay
commit 011ea55a8f
596 changed files with 0 additions and 266 deletions

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

File diff suppressed because it is too large Load diff

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

File diff suppressed because it is too large Load diff

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()