Adds the bigip_data_group module (#39180)

This module can be used to manipulate bigip data groups.
pull/39161/head
Tim Rupp 7 years ago committed by GitHub
parent c262dbfd30
commit d38ae9b6c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,31 +19,87 @@ except ImportError:
try: try:
from library.module_utils.network.f5.common import F5BaseClient from library.module_utils.network.f5.common import F5BaseClient
from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.icontrol import iControlRestSession
except ImportError: except ImportError:
from ansible.module_utils.network.f5.common import F5BaseClient from ansible.module_utils.network.f5.common import F5BaseClient
from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import F5ModuleError
from ansible.module_utils.network.f5.icontrol import iControlRestSession
class F5Client(F5BaseClient): class F5Client(F5BaseClient):
def __init__(self, *args, **kwargs):
super(F5Client, self).__init__(*args, **kwargs)
self.provider = self.merge_provider_params()
@property @property
def api(self): def api(self):
exc = None
if self._client: if self._client:
return self._client return self._client
for x in range(0, 10):
for x in range(0, 60):
try: try:
result = ManagementRoot( result = ManagementRoot(
self.params['server'], self.provider['server'],
self.params['user'], self.provider['user'],
self.params['password'], self.provider['password'],
port=self.params['server_port'], port=self.provider['server_port'],
verify=self.params['validate_certs'], verify=self.provider['validate_certs'],
token='tmos' token='tmos'
) )
self._client = result self._client = result
return self._client return self._client
except Exception: except Exception as ex:
time.sleep(3) exc = ex
raise F5ModuleError( time.sleep(1)
'Unable to connect to {0} on port {1}. ' error = 'Unable to connect to {0} on port {1}.'.format(
'Is "validate_certs" preventing this?'.format(self.params['server'], self.params['server_port']) self.params['server'], self.params['server_port']
)
if exc is not None:
error += ' The reported error was "{0}".'.format(str(exc))
raise F5ModuleError(error)
class F5RestClient(F5BaseClient):
def __init__(self, *args, **kwargs):
super(F5RestClient, self).__init__(*args, **kwargs)
self.provider = self.merge_provider_params()
@property
def api(self):
exc = None
if self._client:
return self._client
for x in range(0, 10):
try:
url = "https://{0}:{1}/mgmt/shared/authn/login".format(
self.provider['server'], self.provider['server_port']
)
payload = {
'username': self.provider['user'],
'password': self.provider['password'],
'loginProviderName': self.provider['auth_provider']
}
session = iControlRestSession()
session.verify = self.provider['validate_certs']
response = session.post(url, json=payload)
if response.status_code not in [200]:
raise F5ModuleError('{0} Unexpected Error: {1} for uri: {2}\nText: {3}'.format(
response.status_code, response.reason, response.url, response._content
))
session.headers['X-F5-Auth-Token'] = response.json()['token']['token']
self._client = session
return self._client
except Exception as ex:
exc = ex
time.sleep(1)
error = 'Unable to connect to {0} on port {1}.'.format(
self.params['server'], self.params['server_port']
) )
if exc is not None:
error += ' The reported error was "{0}".'.format(str(exc))
raise F5ModuleError(error)

@ -19,31 +19,77 @@ except ImportError:
try: try:
from library.module_utils.network.f5.common import F5BaseClient from library.module_utils.network.f5.common import F5BaseClient
from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.common import is_ansible_debug
from library.module_utils.network.f5.icontrol import iControlRestSession
except ImportError: except ImportError:
from ansible.module_utils.network.f5.common import F5BaseClient from ansible.module_utils.network.f5.common import F5BaseClient
from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import F5ModuleError
from ansible.module_utils.network.f5.common import is_ansible_debug
from ansible.module_utils.network.f5.icontrol import iControlRestSession
class F5Client(F5BaseClient): class F5Client(F5BaseClient):
@property @property
def api(self): def api(self):
exc = None
if self._client: if self._client:
return self._client return self._client
for x in range(0, 10): for x in range(0, 3):
try: try:
server = self.params['provider']['server'] or self.params['server']
user = self.params['provider']['user'] or self.params['user']
password = self.params['provider']['password'] or self.params['password']
server_port = self.params['provider']['server_port'] or self.params['server_port'] or 443
validate_certs = self.params['provider']['validate_certs'] or self.params['validate_certs']
result = ManagementRoot( result = ManagementRoot(
self.params['server'], server,
self.params['user'], user,
self.params['password'], password,
port=self.params['server_port'], port=server_port,
verify=self.params['validate_certs'], verify=validate_certs
token='local' )
self._client = result
return self._client
except Exception as ex:
exc = ex
time.sleep(1)
error = 'Unable to connect to {0} on port {1}.'.format(self.params['server'], self.params['server_port'])
if exc is not None:
error += ' The reported error was "{0}".'.format(str(exc))
raise F5ModuleError(error)
class F5RestClient(F5BaseClient):
@property
def api(self):
ex = None
if self._client:
return self._client
for x in range(0, 10):
try:
server = self.params['provider']['server'] or self.params['server']
user = self.params['provider']['user'] or self.params['user']
password = self.params['provider']['password'] or self.params['password']
server_port = self.params['provider']['server_port'] or self.params['server_port'] or 443
validate_certs = self.params['provider']['validate_certs'] or self.params['validate_certs']
# Should we import from module??
# self.module.params['server'],
result = iControlRestSession(
server,
user,
password,
port=server_port,
verify=validate_certs,
auth_provider='local',
debug=is_ansible_debug(self.module)
) )
self._client = result self._client = result
return self._client return self._client
except Exception: except Exception as ex:
time.sleep(3) time.sleep(1)
raise F5ModuleError( error = 'Unable to connect to {0} on port {1}.'.format(self.params['server'], self.params['server_port'])
'Unable to connect to {0} on port {1}. ' if ex is not None:
'Is "validate_certs" preventing this?'.format(self.params['server'], self.params['server_port']) error += ' The reported error was "{0}".'.format(str(ex))
) raise F5ModuleError(error)

@ -6,6 +6,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import os
import re import re
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
@ -13,6 +14,7 @@ from ansible.module_utils.basic import env_fallback
from ansible.module_utils.connection import exec_command from ansible.module_utils.connection import exec_command
from ansible.module_utils.network.common.utils import to_list, ComplexList from ansible.module_utils.network.common.utils import to_list, ComplexList
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
from collections import defaultdict from collections import defaultdict
try: try:
@ -28,7 +30,6 @@ f5_provider_spec = {
), ),
'server_port': dict( 'server_port': dict(
type='int', type='int',
default=443,
fallback=(env_fallback, ['F5_SERVER_PORT']) fallback=(env_fallback, ['F5_SERVER_PORT'])
), ),
'user': dict( 'user': dict(
@ -40,7 +41,6 @@ f5_provider_spec = {
fallback=(env_fallback, ['F5_PASSWORD', 'ANSIBLE_NET_PASSWORD']) fallback=(env_fallback, ['F5_PASSWORD', 'ANSIBLE_NET_PASSWORD'])
), ),
'ssh_keyfile': dict( 'ssh_keyfile': dict(
fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']),
type='path' type='path'
), ),
'validate_certs': dict( 'validate_certs': dict(
@ -48,8 +48,8 @@ f5_provider_spec = {
fallback=(env_fallback, ['F5_VALIDATE_CERTS']) fallback=(env_fallback, ['F5_VALIDATE_CERTS'])
), ),
'transport': dict( 'transport': dict(
default='rest', choices=['cli', 'rest'],
choices=['cli', 'rest'] default='rest'
), ),
'timeout': dict(type='int'), 'timeout': dict(type='int'),
} }
@ -81,12 +81,10 @@ f5_top_spec = {
'server_port': dict( 'server_port': dict(
removed_in_version=2.9, removed_in_version=2.9,
type='int', type='int',
default=443,
fallback=(env_fallback, ['F5_SERVER_PORT']) fallback=(env_fallback, ['F5_SERVER_PORT'])
), ),
'transport': dict( 'transport': dict(
removed_in_version=2.9, removed_in_version=2.9,
default='rest',
choices=['cli', 'rest'] choices=['cli', 'rest']
) )
} }
@ -107,8 +105,59 @@ def load_params(params):
# Fully Qualified name (with the partition) # Fully Qualified name (with the partition)
def fqdn_name(partition, value): def fqdn_name(partition, value):
if value is not None and not value.startswith('/'): """This method is not used
return '/{0}/{1}'.format(partition, value)
This was the original name of a method that was used throughout all
the F5 Ansible modules. This is now deprecated, and should be removed
in 2.9. All modules should be changed to use ``fq_name``.
TODO(Remove in Ansible 2.9)
"""
return fq_name(partition, value)
def fq_name(partition, value):
"""Returns a 'Fully Qualified' name
A BIG-IP expects most names of resources to be in a fully-qualified
form. This means that both the simple name, and the partition need
to be combined.
The Ansible modules, however, can accept (as names for several
resources) their name in the FQ format. This becomes an issue when
the FQ name and the partition are both specified as separate values.
Consider the following examples.
# Name not FQ
name: foo
partition: Common
# Name FQ
name: /Common/foo
partition: Common
This method will rectify the above situation and will, in both cases,
return the following for name.
/Common/foo
Args:
partition (string): The partition that you would want attached to
the name if the name has no partition.
value (string): The name that you want to attach a partition to.
This value will be returned unchanged if it has a partition
attached to it already.
Returns:
string: The fully qualified name, given the input parameters.
"""
if value is not None:
try:
int(value)
return '/{0}/{1}'.format(partition, value)
except (ValueError, TypeError):
if not value.startswith('/'):
return '/{0}/{1}'.format(partition, value)
return value return value
@ -137,7 +186,8 @@ def run_commands(module, commands, check_rc=True):
rc, out, err = exec_command(module, cmd) rc, out, err = exec_command(module, cmd)
if check_rc and rc != 0: if check_rc and rc != 0:
raise F5ModuleError(to_text(err, errors='surrogate_then_replace')) raise F5ModuleError(to_text(err, errors='surrogate_then_replace'))
responses.append(to_text(out, errors='surrogate_then_replace')) result = to_text(out, errors='surrogate_then_replace')
responses.append(result)
return responses return responses
@ -183,6 +233,101 @@ def is_valid_hostname(host):
return result return result
def is_valid_fqdn(host):
"""Reasonable attempt at validating a hostname
Compiled from various paragraphs outlined here
https://tools.ietf.org/html/rfc3696#section-2
https://tools.ietf.org/html/rfc1123
Notably,
* Host software MUST handle host names of up to 63 characters and
SHOULD handle host names of up to 255 characters.
* The "LDH rule", after the characters that it permits. (letters, digits, hyphen)
* If the hyphen is used, it is not permitted to appear at
either the beginning or end of a label
:param host:
:return:
"""
if len(host) > 255:
return False
host = host.rstrip(".")
allowed = re.compile(r'(?!-)[A-Z0-9-]{1,63}(?<!-)$', re.IGNORECASE)
result = all(allowed.match(x) for x in host.split("."))
if result:
parts = host.split('.')
if len(parts) > 1:
return True
return False
def dict2tuple(items):
"""Convert a dictionary to a list of tuples
This method is used in cases where dictionaries need to be compared. Due
to dictionaries inherently having no order, it is easier to compare list
of tuples because these lists can be converted to sets.
This conversion only supports dicts of simple values. Do not give it dicts
that contain sub-dicts. This will not give you the result you want when using
the returned tuple for comparison.
Args:
items (dict): The dictionary of items that should be converted
Returns:
list: Returns a list of tuples upon success. Otherwise, an empty list.
"""
result = []
for x in items:
tmp = [(str(k), str(v)) for k, v in iteritems(x)]
result += tmp
return result
def compare_dictionary(want, have):
"""Performs a dictionary comparison
Args:
want (dict): Dictionary to compare with second parameter.
have (dict): Dictionary to compare with first parameter.
Returns:
bool:
:param have:
:return:
"""
if want == [] and have is None:
return None
if want is None:
return None
w = dict2tuple(want)
h = dict2tuple(have)
if set(w) == set(h):
return None
else:
return want
def is_ansible_debug(module):
if module._debug and module._verbosity >= 4:
return True
return False
def fail_json(module, ex, client=None):
if is_ansible_debug(module) and client:
module.fail_json(msg=str(ex), __f5debug__=client.api.debug_output)
module.fail_json(msg=str(ex))
def exit_json(module, results, client=None):
if is_ansible_debug(module) and client:
results['__f5debug__'] = client.api.debug_output
module.exit_json(**results)
class Noop(object): class Noop(object):
"""Represent no-operation required """Represent no-operation required
@ -200,6 +345,7 @@ class Noop(object):
class F5BaseClient(object): class F5BaseClient(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.params = kwargs self.params = kwargs
self.module = kwargs.get('module', None)
load_params(self.params) load_params(self.params)
self._client = None self._client = None
@ -222,7 +368,73 @@ class F5BaseClient(object):
:return: :return:
:raises iControlUnexpectedHTTPError :raises iControlUnexpectedHTTPError
""" """
self._client = self.mgmt self._client = None
def merge_provider_params(self):
result = dict()
provider = self.params.get('provider', {})
if provider.get('server', None):
result['server'] = provider.get('server', None)
elif self.params.get('server', None):
result['server'] = self.params.get('server', None)
elif os.environ.get('F5_SERVER', None):
result['server'] = os.environ.get('F5_SERVER', None)
if provider.get('server_port', None):
result['server_port'] = provider.get('server_port', None)
elif self.params.get('server_port', None):
result['server_port'] = self.params.get('server_port', None)
elif os.environ.get('F5_SERVER_PORT', None):
result['server_port'] = os.environ.get('F5_SERVER_PORT', None)
else:
result['server_port'] = 443
if provider.get('validate_certs', None) is not None:
result['validate_certs'] = provider.get('validate_certs', None)
elif self.params.get('validate_certs', None) is not None:
result['validate_certs'] = self.params.get('validate_certs', None)
elif os.environ.get('F5_VALIDATE_CERTS', None) is not None:
result['validate_certs'] = os.environ.get('F5_VALIDATE_CERTS', None)
else:
result['validate_certs'] = True
if provider.get('auth_provider', None):
result['auth_provider'] = provider.get('auth_provider', None)
elif self.params.get('auth_provider', None):
result['auth_provider'] = self.params.get('auth_provider', None)
else:
result['auth_provider'] = 'tmos'
if provider.get('user', None):
result['user'] = provider.get('user', None)
elif self.params.get('user', None):
result['user'] = self.params.get('user', None)
elif os.environ.get('F5_USER', None):
result['user'] = os.environ.get('F5_USER', None)
elif os.environ.get('ANSIBLE_NET_USERNAME', None):
result['user'] = os.environ.get('ANSIBLE_NET_USERNAME', None)
else:
result['user'] = True
if provider.get('password', None):
result['password'] = provider.get('password', None)
elif self.params.get('user', None):
result['password'] = self.params.get('password', None)
elif os.environ.get('F5_PASSWORD', None):
result['password'] = os.environ.get('F5_PASSWORD', None)
elif os.environ.get('ANSIBLE_NET_PASSWORD', None):
result['password'] = os.environ.get('ANSIBLE_NET_PASSWORD', None)
else:
result['password'] = True
if result['validate_certs'] in BOOLEANS_TRUE:
result['validate_certs'] = True
else:
result['validate_certs'] = False
return result
class AnsibleF5Parameters(object): class AnsibleF5Parameters(object):

@ -0,0 +1,367 @@
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2017, F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
from ansible.module_utils.urls import open_url, fetch_url
from ansible.module_utils.parsing.convert_bool import BOOLEANS
from ansible.module_utils.six import string_types
from ansible.module_utils.six import iteritems
from ansible.module_utils.urls import urllib_error
from ansible.module_utils._text import to_native
from ansible.module_utils.six import PY3
try:
import json as _json
except ImportError:
import simplejson as _json
try:
from library.module_utils.network.f5.common import F5ModuleError
except ImportError:
from ansible.module_utils.network.f5.common import F5ModuleError
"""An F5 REST API URI handler.
Use this module to make calls to an F5 REST server. It is influenced by the same
API that the Python ``requests`` tool uses, but the two are not the same, as the
library here is **much** more simple and targeted specifically to F5's needs.
The ``requests`` design was chosen due to familiarity with the tool. Internals though
use Ansible native libraries.
The means by which you should use it are similar to ``requests`` basic usage.
Authentication is not handled for you automatically by this library, however it *is*
handled automatically for you in the supporting F5 module_utils code; specifically the
different product module_util files (bigip.py, bigiq.py, etc).
Internal (non-module) usage of this library looks like this.
```
# Create a session instance
mgmt = iControlRestSession()
mgmt.verify = False
server = '1.1.1.1'
port = 443
# Payload used for getting an initial authentication token
payload = {
'username': 'admin',
'password': 'secret',
'loginProviderName': 'tmos'
}
# Create URL to call, injecting server and port
url = f"https://{server}:{port}/mgmt/shared/authn/login"
# Call the API
resp = session.post(url, json=payload)
# View the response
print(resp.json())
# Update the session with the authentication token
session.headers['X-F5-Auth-Token'] = resp.json()['token']['token']
# Create another URL to call, injecting server and port
url = f"https://{server}:{port}/mgmt/tm/ltm/virtual/~Common~virtual1"
# Call the API
resp = session.get(url)
# View the details of a virtual payload
print(resp.json())
```
"""
class Request(object):
def __init__(self, method=None, url=None, headers=None, data=None, params=None,
auth=None, json=None):
self.method = method
self.url = url
self.headers = headers or {}
self.data = data or []
self.json = json
self.params = params or {}
self.auth = auth
def prepare(self):
p = PreparedRequest()
p.prepare(
method=self.method,
url=self.url,
headers=self.headers,
data=self.data,
json=self.json,
params=self.params,
)
return p
class PreparedRequest(object):
def __init__(self):
self.method = None
self.url = None
self.headers = None
self.body = None
def prepare(self, method=None, url=None, headers=None, data=None, params=None, json=None):
self.prepare_method(method)
self.prepare_url(url, params)
self.prepare_headers(headers)
self.prepare_body(data, json)
def prepare_url(self, url, params):
self.url = url
def prepare_method(self, method):
self.method = method
if self.method:
self.method = self.method.upper()
def prepare_headers(self, headers):
self.headers = {}
if headers:
for k, v in iteritems(headers):
self.headers[k] = v
def prepare_body(self, data, json=None):
body = None
content_type = None
if not data and json is not None:
self.headers['Content-Type'] = 'application/json'
body = _json.dumps(json)
if not isinstance(body, bytes):
body = body.encode('utf-8')
if data:
body = data
content_type = None
if content_type and 'content-type' not in self.headers:
self.headers['Content-Type'] = content_type
self.body = body
class Response(object):
def __init__(self):
self._content = None
self.status_code = None
self.headers = dict()
self.url = None
self.reason = None
self.request = None
def json(self):
return _json.loads(self._content)
class iControlRestSession(object):
"""Represents a session that communicates with a BigIP.
Instantiate one of these when you want to communicate with an F5 REST
Server, it will handle F5-specific authentication.
Pass an existing authentication token to the ``token`` argument to re-use
that token for authentication. Otherwise, token authentication is handled
automatically for you.
On BIG-IQ, it may be necessary to pass the ``auth_provider`` argument if the
user has a different authentication handler configured. Otherwise, the system
defaults for the different products will be used.
"""
def __init__(self):
self.headers = self.default_headers()
self.verify = True
self.params = {}
self.auth = None
self.timeout = 30
def _normalize_headers(self, headers):
result = {}
result.update(dict((k.lower(), v) for k, v in headers))
# Don't be lossy, append header values for duplicate headers
# In Py2 there is nothing that needs done, py2 does this for us
if PY3:
temp_headers = {}
for name, value in headers:
# The same as above, lower case keys to match py2 behavior, and create more consistent results
name = name.lower()
if name in temp_headers:
temp_headers[name] = ', '.join((temp_headers[name], value))
else:
temp_headers[name] = value
result.update(temp_headers)
return result
def default_headers(self):
return {
'connection': 'keep-alive',
'accept': '*/*',
}
def prepare_request(self, request):
headers = self.headers.copy()
params = self.params.copy()
if request.headers is not None:
headers.update(request.headers)
if request.params is not None:
params.update(request.params)
prepared = PreparedRequest()
prepared.prepare(
method=request.method,
url=request.url,
data=request.data,
json=request.json,
headers=headers,
params=params,
)
return prepared
def request(self, method, url, params=None, data=None, headers=None, auth=None,
timeout=None, verify=None, json=None):
request = Request(
method=method.upper(),
url=url,
headers=headers,
json=json,
data=data or {},
params=params or {},
auth=auth
)
kwargs = dict(
timeout=timeout,
verify=verify
)
prepared = self.prepare_request(request)
return self.send(prepared, **kwargs)
def send(self, request, **kwargs):
response = Response()
params = dict(
method=request.method,
data=request.body,
timeout=kwargs.get('timeout', None) or self.timeout,
headers=request.headers
)
try:
result = open_url(request.url, **params)
response._content = result.read()
response.status = result.getcode()
response.url = result.geturl()
response.msg = "OK (%s bytes)" % result.headers.get('Content-Length', 'unknown')
response.headers = self._normalize_headers(result.headers.items())
response.request = request
except urllib_error.HTTPError as e:
try:
response._content = e.read()
except AttributeError:
response._content = ''
response.reason = to_native(e)
response.status_code = e.code
return response
def delete(self, url, **kwargs):
"""Sends a HTTP DELETE command to an F5 REST Server.
Use this method to send a DELETE command to an F5 product.
Args:
url (string): URL to call.
data (bytes): An object specifying additional data to send to the server,
or ``None`` if no such data is needed. Currently HTTP requests are the
only ones that use data. The supported object types include bytes,
file-like objects, and iterables.
See https://docs.python.org/3/library/urllib.request.html#urllib.request.Request
\\*\\*kwargs (dict): Optional arguments to send to the request.
"""
return self.request('DELETE', url, **kwargs)
def get(self, url, **kwargs):
"""Sends a HTTP GET command to an F5 REST Server.
Use this method to send a GET command to an F5 product.
Args:
url (string): URL to call.
\\*\\*kwargs (dict): Optional arguments to send to the request.
"""
return self.request('GET', url, **kwargs)
def patch(self, url, data=None, **kwargs):
"""Sends a HTTP PATCH command to an F5 REST Server.
Use this method to send a PATCH command to an F5 product.
Args:
url (string): URL to call.
data (bytes): An object specifying additional data to send to the server,
or ``None`` if no such data is needed. Currently HTTP requests are the
only ones that use data. The supported object types include bytes,
file-like objects, and iterables.
See https://docs.python.org/3/library/urllib.request.html#urllib.request.Request
\\*\\*kwargs (dict): Optional arguments to send to the request.
"""
return self.request('PATCH', url, data=data, **kwargs)
def post(self, url, data=None, json=None, **kwargs):
"""Sends a HTTP POST command to an F5 REST Server.
Use this method to send a POST command to an F5 product.
Args:
url (string): URL to call.
data (dict): An object specifying additional data to send to the server,
or ``None`` if no such data is needed. Currently HTTP requests are the
only ones that use data. The supported object types include bytes,
file-like objects, and iterables.
See https://docs.python.org/3/library/urllib.request.html#urllib.request.Request
\\*\\*kwargs (dict): Optional arguments to the request.
"""
return self.request('POST', url, data=data, json=json, **kwargs)
def put(self, url, data=None, **kwargs):
"""Sends a HTTP PUT command to an F5 REST Server.
Use this method to send a PUT command to an F5 product.
Args:
url (string): URL to call.
data (bytes): An object specifying additional data to send to the server,
or ``None`` if no such data is needed. Currently HTTP requests are the
only ones that use data. The supported object types include bytes,
file-like objects, and iterables.
See https://docs.python.org/3/library/urllib.request.html#urllib.request.Request
\\*\\*kwargs (dict): Optional arguments to the request.
"""
return self.request('PUT', url, data=data, **kwargs)
def debug_prepared_request(url, method, headers, data=None):
result = "curl -k -X {0} {1}".format(method.upper(), url)
for k, v in iteritems(headers):
result = result + " -H '{0}: {1}'".format(k, v)
if any(v == 'application/json' for k, v in iteritems(headers)):
if data:
kwargs = _json.loads(data.decode('utf-8'))
result = result + " -d '" + _json.dumps(kwargs, sort_keys=True) + "'"
return result

@ -27,23 +27,31 @@ except ImportError:
class F5Client(F5BaseClient): class F5Client(F5BaseClient):
@property @property
def api(self): def api(self):
exc = None
if self._client: if self._client:
return self._client return self._client
for x in range(0, 10): for x in range(0, 3):
try: try:
server = self.params['provider']['server'] or self.params['server']
user = self.params['provider']['user'] or self.params['user']
password = self.params['provider']['password'] or self.params['password']
server_port = self.params['provider']['server_port'] or self.params['server_port'] or 443
validate_certs = self.params['provider']['validate_certs'] or self.params['validate_certs']
result = ManagementRoot( result = ManagementRoot(
self.params['server'], server,
self.params['user'], user,
self.params['password'], password,
port=self.params['server_port'], port=server_port,
verify=self.params['validate_certs'], verify=validate_certs,
token='local' token='local'
) )
self._client = result self._client = result
return self._client return self._client
except Exception: except Exception as ex:
exc = ex
time.sleep(3) time.sleep(3)
raise F5ModuleError( error = 'Unable to connect to {0} on port {1}.'.format(self.params['server'], self.params['server_port'])
'Unable to connect to {0} on port {1}. ' if exc is not None:
'Is "validate_certs" preventing this?'.format(self.params['server'], self.params['server_port']) error += ' The reported error was "{0}".'.format(str(exc))
) raise F5ModuleError(error)

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
try:
import bigsuds
bigsuds_found = True
except ImportError:
bigsuds_found = False
from ansible.module_utils.basic import env_fallback
def f5_argument_spec():
return dict(
server=dict(
type='str',
required=True,
fallback=(env_fallback, ['F5_SERVER'])
),
user=dict(
type='str',
required=True,
fallback=(env_fallback, ['F5_USER'])
),
password=dict(
type='str',
aliases=['pass', 'pwd'],
required=True,
no_log=True,
fallback=(env_fallback, ['F5_PASSWORD'])
),
validate_certs=dict(
default='yes',
type='bool',
fallback=(env_fallback, ['F5_VALIDATE_CERTS'])
),
server_port=dict(
type='int',
default=443,
fallback=(env_fallback, ['F5_SERVER_PORT'])
),
state=dict(
type='str',
default='present',
choices=['present', 'absent']
),
partition=dict(
type='str',
default='Common',
fallback=(env_fallback, ['F5_PARTITION'])
)
)
def f5_parse_arguments(module):
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'")
return (
module.params['server'],
module.params['user'],
module.params['password'],
module.params['state'],
module.params['partition'],
module.params['validate_certs'],
module.params['server_port']
)
def bigip_api(bigip, user, password, validate_certs, port=443):
try:
if bigsuds.__version__ >= '1.0.4':
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password, verify=validate_certs, port=port)
elif bigsuds.__version__ == '1.0.3':
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password, verify=validate_certs)
else:
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
except TypeError:
# bigsuds < 1.0.3, no verify param
if validate_certs:
# Note: verified we have SSLContext when we parsed params
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
else:
import ssl
if hasattr(ssl, 'SSLContext'):
# Really, you should never do this. It disables certificate
# verification *globally*. But since older bigip libraries
# don't give us a way to toggle verification we need to
# disable it at the global level.
# From https://www.python.org/dev/peps/pep-0476/#id29
ssl._create_default_https_context = ssl._create_unverified_context
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
return api
# Fully Qualified name (with the partition)
def fq_name(partition, name):
if name is not None and not name.startswith('/'):
return '/%s/%s' % (partition, name)
return name
# Fully Qualified name (with partition) for a list
def fq_list_names(partition, list_names):
if list_names is None:
return None
return map(lambda x: fq_name(partition, x), list_names)

File diff suppressed because it is too large Load Diff

@ -20,7 +20,7 @@ description:
to accept configuration. to accept configuration.
- This module can take into account situations where the device is in the middle - This module can take into account situations where the device is in the middle
of rebooting due to a configuration change. of rebooting due to a configuration change.
version_added: "2.5" version_added: 2.5
options: options:
timeout: timeout:
description: description:
@ -80,30 +80,21 @@ import time
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
HAS_DEVEL_IMPORTS = False
try: try:
# Sideband repository used for dev
from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import HAS_F5SDK
from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.bigip import F5Client
from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import AnsibleF5Parameters
from library.module_utils.network.f5.common import cleanup_tokens
from library.module_utils.network.f5.common import fqdn_name
from library.module_utils.network.f5.common import f5_argument_spec from library.module_utils.network.f5.common import f5_argument_spec
try: try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
except ImportError: except ImportError:
HAS_F5SDK = False HAS_F5SDK = False
HAS_DEVEL_IMPORTS = True
except ImportError: except ImportError:
# Upstream Ansible
from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import HAS_F5SDK
from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.bigip import F5Client
from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import F5ModuleError
from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import AnsibleF5Parameters
from ansible.module_utils.network.f5.common import cleanup_tokens
from ansible.module_utils.network.f5.common import fqdn_name
from ansible.module_utils.network.f5.common import f5_argument_spec from ansible.module_utils.network.f5.common import f5_argument_spec
try: try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
@ -186,6 +177,9 @@ class ModuleManager(object):
version=warning['version'] version=warning['version']
) )
def _get_client_connection(self):
return F5Client(**self.module.params)
def execute(self): def execute(self):
signal.signal( signal.signal(
signal.SIGALRM, signal.SIGALRM,
@ -204,7 +198,7 @@ class ModuleManager(object):
try: try:
# The first test verifies that the REST API is available; this is done # The first test verifies that the REST API is available; this is done
# by repeatedly trying to login to it. # by repeatedly trying to login to it.
self.client = F5Client(**self.module.params) self.client = self._get_client_connection()
if not self.client: if not self.client:
continue continue

@ -0,0 +1,5 @@
network 10.0.0.0 prefixlen 8 := "Network1",
network 172.16.0.0 prefixlen 12 := "Network2",
network 192.168.0.0 prefixlen 16 := "Network3",
host 192.168.20.1 := "Host1",
host 172.16.1.1 := "Host2",

@ -0,0 +1,6 @@
1 := alpha
2 := bravo
3 := charlie
4 := x-ray
5 := yankee
6 := zulu

@ -0,0 +1,6 @@
a := alpha
b := bravo
c := charlie
x := x-ray
y := yankee
z := zulu

@ -0,0 +1,463 @@
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2017, F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import pytest
import sys
from nose.plugins.skip import SkipTest
if sys.version_info < (2, 7):
raise SkipTest("F5 Ansible modules require Python >= 2.7")
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import Mock
from ansible.compat.tests.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
from library.modules.bigip_data_group import ModuleParameters
from library.modules.bigip_data_group import ModuleManager
from library.modules.bigip_data_group import ExternalManager
from library.modules.bigip_data_group import InternalManager
from library.modules.bigip_data_group import ArgumentSpec
from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
from test.unit.modules.utils import set_module_args
except ImportError:
try:
from ansible.modules.network.f5.bigip_data_group import ModuleParameters
from ansible.modules.network.f5.bigip_data_group import ModuleManager
from ansible.modules.network.f5.bigip_data_group import ExternalManager
from ansible.modules.network.f5.bigip_data_group import InternalManager
from ansible.modules.network.f5.bigip_data_group import ArgumentSpec
from ansible.module_utils.network.f5.common import F5ModuleError
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
from units.modules.utils import set_module_args
except ImportError:
raise SkipTest("F5 Ansible modules require the f5-sdk Python library")
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except Exception:
pass
fixture_data[path] = data
return data
class TestParameters(unittest.TestCase):
def test_module_parameters(self):
args = dict(
name='foo',
type='address',
delete_data_group_file=False,
internal=False,
records=[
dict(
key='10.10.10.10/32',
value='bar'
)
],
separator=':=',
state='present',
partition='Common'
)
p = ModuleParameters(params=args)
assert p.name == 'foo'
assert p.type == 'ip'
assert p.delete_data_group_file is False
assert len(p.records) == 1
assert 'data' in p.records[0]
assert 'name' in p.records[0]
assert p.records[0]['data'] == 'bar'
assert p.records[0]['name'] == '10.10.10.10/32'
assert p.separator == ':='
assert p.state == 'present'
assert p.partition == 'Common'
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
class TestManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
def test_create_external_datagroup_type_string(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=False,
records_src="{0}/data-group-string.txt".format(fixture_path),
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = ExternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
results = mm0.exec_module()
assert results['changed'] is True
def test_create_external_incorrect_address_data(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=False,
type='address',
records_src="{0}/data-group-string.txt".format(fixture_path),
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = ExternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
with pytest.raises(F5ModuleError) as ex:
mm0.exec_module()
assert "When specifying an 'address' type, the value to the left of the separator must be an IP." == str(ex.value)
def test_create_external_incorrect_integer_data(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=False,
type='integer',
records_src="{0}/data-group-string.txt".format(fixture_path),
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = ExternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
with pytest.raises(F5ModuleError) as ex:
mm0.exec_module()
assert "When specifying an 'integer' type, the value to the left of the separator must be a number." == str(ex.value)
def test_remove_data_group_keep_file(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=False,
state='absent',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = ExternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[True, False])
mm1.remove_from_device = Mock(return_value=True)
mm1.external_file_exists = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
results = mm0.exec_module()
assert results['changed'] is True
def test_remove_data_group_remove_file(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=True,
internal=False,
state='absent',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = ExternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[True, False])
mm1.remove_from_device = Mock(return_value=True)
mm1.external_file_exists = Mock(return_value=True)
mm1.remove_data_group_file_from_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
results = mm0.exec_module()
assert results['changed'] is True
def test_create_internal_datagroup_type_string(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=True,
records_src="{0}/data-group-string.txt".format(fixture_path),
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = InternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
results = mm0.exec_module()
assert results['changed'] is True
def test_create_internal_incorrect_integer_data(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=True,
type='integer',
records_src="{0}/data-group-string.txt".format(fixture_path),
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = InternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
with pytest.raises(F5ModuleError) as ex:
mm0.exec_module()
assert "When specifying an 'integer' type, the value to the left of the separator must be a number." == str(ex.value)
def test_create_internal_datagroup_type_integer(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=True,
type='integer',
records_src="{0}/data-group-integer.txt".format(fixture_path),
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = InternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
results = mm0.exec_module()
assert results['changed'] is True
def test_create_internal_datagroup_type_address(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=True,
type='address',
records_src="{0}/data-group-address.txt".format(fixture_path),
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = InternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
results = mm0.exec_module()
assert results['changed'] is True
def test_create_internal_datagroup_type_address_list(self, *args):
set_module_args(dict(
name='foo',
delete_data_group_file=False,
internal=True,
type='address',
records=[
dict(
key='10.0.0.0/8',
value='Network1'
),
dict(
key='172.16.0.0/12',
value='Network2'
),
dict(
key='192.168.20.1/16',
value='Network3'
),
dict(
key='192.168.20.1',
value='Host1'
),
dict(
key='172.16.1.1',
value='Host2'
)
],
separator=':=',
state='present',
partition='Common',
server='localhost',
password='password',
user='admin'
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
mutually_exclusive=self.spec.mutually_exclusive,
)
# Override methods in the specific type of manager
mm1 = InternalManager(module=module, params=module.params)
mm1.exists = Mock(side_effect=[False, True])
mm1.create_on_device = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm0 = ModuleManager(module=module)
mm0.get_manager = Mock(return_value=mm1)
results = mm0.exec_module()
assert results['changed'] is True

@ -21,9 +21,9 @@ from ansible.compat.tests.mock import patch
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
try: try:
from library.bigip_wait import Parameters from library.modules.bigip_wait import Parameters
from library.bigip_wait import ModuleManager from library.modules.bigip_wait import ModuleManager
from library.bigip_wait import ArgumentSpec from library.modules.bigip_wait import ArgumentSpec
from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
from test.unit.modules.utils import set_module_args from test.unit.modules.utils import set_module_args
@ -116,6 +116,7 @@ class TestManager(unittest.TestCase):
mm._connect_to_device = Mock(return_value=True) mm._connect_to_device = Mock(return_value=True)
mm._device_is_rebooting = Mock(return_value=False) mm._device_is_rebooting = Mock(return_value=False)
mm._is_mprov_running_on_device = Mock(return_value=False) mm._is_mprov_running_on_device = Mock(return_value=False)
mm._get_client_connection = Mock(return_value=True)
results = mm.exec_module() results = mm.exec_module()

Loading…
Cancel
Save