summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorTim Rupp <caphrim007@gmail.com>2018-04-23 11:52:57 -0700
committerGitHub <noreply@github.com>2018-04-23 11:52:57 -0700
commitd38ae9b6c9b03dd14ede044e5e4a36fd3a4821d3 (patch)
treed2c69487e5e5c7ac18e60f162fc58a7b1c2572ff /lib
parentc262dbfd308d2d98de078d82465982a0fbd60dc9 (diff)
downloadansible-d38ae9b6c9b03dd14ede044e5e4a36fd3a4821d3.tar.gz
Adds the bigip_data_group module (#39180)
This module can be used to manipulate bigip data groups.
Diffstat (limited to 'lib')
-rw-r--r--lib/ansible/module_utils/network/f5/bigip.py78
-rw-r--r--lib/ansible/module_utils/network/f5/bigiq.py72
-rw-r--r--lib/ansible/module_utils/network/f5/common.py232
-rw-r--r--lib/ansible/module_utils/network/f5/icontrol.py367
-rw-r--r--lib/ansible/module_utils/network/f5/iworkflow.py30
-rw-r--r--lib/ansible/module_utils/network/f5/legacy.py121
-rw-r--r--lib/ansible/modules/network/f5/bigip_data_group.py1069
-rw-r--r--lib/ansible/modules/network/f5/bigip_wait.py16
8 files changed, 1929 insertions, 56 deletions
diff --git a/lib/ansible/module_utils/network/f5/bigip.py b/lib/ansible/module_utils/network/f5/bigip.py
index 63887fac5d..8fde9fc7a7 100644
--- a/lib/ansible/module_utils/network/f5/bigip.py
+++ b/lib/ansible/module_utils/network/f5/bigip.py
@@ -19,31 +19,87 @@ except ImportError:
try:
from library.module_utils.network.f5.common import F5BaseClient
from library.module_utils.network.f5.common import F5ModuleError
+ from library.module_utils.network.f5.icontrol import iControlRestSession
except ImportError:
from ansible.module_utils.network.f5.common import F5BaseClient
from ansible.module_utils.network.f5.common import F5ModuleError
+ from ansible.module_utils.network.f5.icontrol import iControlRestSession
class F5Client(F5BaseClient):
+ def __init__(self, *args, **kwargs):
+ super(F5Client, 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):
+
+ for x in range(0, 60):
try:
result = ManagementRoot(
- self.params['server'],
- self.params['user'],
- self.params['password'],
- port=self.params['server_port'],
- verify=self.params['validate_certs'],
+ self.provider['server'],
+ self.provider['user'],
+ self.provider['password'],
+ port=self.provider['server_port'],
+ verify=self.provider['validate_certs'],
token='tmos'
)
self._client = result
return self._client
- except Exception:
- time.sleep(3)
- raise F5ModuleError(
- 'Unable to connect to {0} on port {1}. '
- 'Is "validate_certs" preventing this?'.format(self.params['server'], self.params['server_port'])
+ 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):
+ 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)
diff --git a/lib/ansible/module_utils/network/f5/bigiq.py b/lib/ansible/module_utils/network/f5/bigiq.py
index 458f2696fa..17f4ceeb8d 100644
--- a/lib/ansible/module_utils/network/f5/bigiq.py
+++ b/lib/ansible/module_utils/network/f5/bigiq.py
@@ -19,31 +19,77 @@ except ImportError:
try:
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 is_ansible_debug
+ from library.module_utils.network.f5.icontrol import iControlRestSession
except ImportError:
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 is_ansible_debug
+ from ansible.module_utils.network.f5.icontrol import iControlRestSession
class F5Client(F5BaseClient):
@property
def api(self):
+ exc = None
if self._client:
return self._client
- for x in range(0, 10):
+ for x in range(0, 3):
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(
- self.params['server'],
- self.params['user'],
- self.params['password'],
- port=self.params['server_port'],
- verify=self.params['validate_certs'],
- token='local'
+ server,
+ user,
+ password,
+ port=server_port,
+ verify=validate_certs
+ )
+ 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
return self._client
- except Exception:
- time.sleep(3)
- raise F5ModuleError(
- 'Unable to connect to {0} on port {1}. '
- 'Is "validate_certs" preventing this?'.format(self.params['server'], self.params['server_port'])
- )
+ except Exception as ex:
+ time.sleep(1)
+ error = 'Unable to connect to {0} on port {1}.'.format(self.params['server'], self.params['server_port'])
+ if ex is not None:
+ error += ' The reported error was "{0}".'.format(str(ex))
+ raise F5ModuleError(error)
diff --git a/lib/ansible/module_utils/network/f5/common.py b/lib/ansible/module_utils/network/f5/common.py
index 0f6c2c5b62..60c851d090 100644
--- a/lib/ansible/module_utils/network/f5/common.py
+++ b/lib/ansible/module_utils/network/f5/common.py
@@ -6,6 +6,7 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
+import os
import re
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.network.common.utils import to_list, ComplexList
from ansible.module_utils.six import iteritems
+from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
from collections import defaultdict
try:
@@ -28,7 +30,6 @@ f5_provider_spec = {
),
'server_port': dict(
type='int',
- default=443,
fallback=(env_fallback, ['F5_SERVER_PORT'])
),
'user': dict(
@@ -40,7 +41,6 @@ f5_provider_spec = {
fallback=(env_fallback, ['F5_PASSWORD', 'ANSIBLE_NET_PASSWORD'])
),
'ssh_keyfile': dict(
- fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']),
type='path'
),
'validate_certs': dict(
@@ -48,8 +48,8 @@ f5_provider_spec = {
fallback=(env_fallback, ['F5_VALIDATE_CERTS'])
),
'transport': dict(
- default='rest',
- choices=['cli', 'rest']
+ choices=['cli', 'rest'],
+ default='rest'
),
'timeout': dict(type='int'),
}
@@ -81,12 +81,10 @@ f5_top_spec = {
'server_port': dict(
removed_in_version=2.9,
type='int',
- default=443,
fallback=(env_fallback, ['F5_SERVER_PORT'])
),
'transport': dict(
removed_in_version=2.9,
- default='rest',
choices=['cli', 'rest']
)
}
@@ -107,8 +105,59 @@ def load_params(params):
# Fully Qualified name (with the partition)
def fqdn_name(partition, value):
- if value is not None and not value.startswith('/'):
- return '/{0}/{1}'.format(partition, value)
+ """This method is not used
+
+ 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
@@ -137,7 +186,8 @@ def run_commands(module, commands, check_rc=True):
rc, out, err = exec_command(module, cmd)
if check_rc and rc != 0:
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
@@ -183,6 +233,101 @@ def is_valid_hostname(host):
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):
"""Represent no-operation required
@@ -200,6 +345,7 @@ class Noop(object):
class F5BaseClient(object):
def __init__(self, *args, **kwargs):
self.params = kwargs
+ self.module = kwargs.get('module', None)
load_params(self.params)
self._client = None
@@ -222,7 +368,73 @@ class F5BaseClient(object):
:return:
: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):
diff --git a/lib/ansible/module_utils/network/f5/icontrol.py b/lib/ansible/module_utils/network/f5/icontrol.py
new file mode 100644
index 0000000000..daf96c1c64
--- /dev/null
+++ b/lib/ansible/module_utils/network/f5/icontrol.py
@@ -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
diff --git a/lib/ansible/module_utils/network/f5/iworkflow.py b/lib/ansible/module_utils/network/f5/iworkflow.py
index 2b2e995795..a7de5c1c41 100644
--- a/lib/ansible/module_utils/network/f5/iworkflow.py
+++ b/lib/ansible/module_utils/network/f5/iworkflow.py
@@ -27,23 +27,31 @@ except ImportError:
class F5Client(F5BaseClient):
@property
def api(self):
+ exc = None
if self._client:
return self._client
- for x in range(0, 10):
+ for x in range(0, 3):
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(
- self.params['server'],
- self.params['user'],
- self.params['password'],
- port=self.params['server_port'],
- verify=self.params['validate_certs'],
+ server,
+ user,
+ password,
+ port=server_port,
+ verify=validate_certs,
token='local'
)
self._client = result
return self._client
- except Exception:
+ except Exception as ex:
+ exc = ex
time.sleep(3)
- raise F5ModuleError(
- 'Unable to connect to {0} on port {1}. '
- 'Is "validate_certs" preventing this?'.format(self.params['server'], self.params['server_port'])
- )
+ 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)
diff --git a/lib/ansible/module_utils/network/f5/legacy.py b/lib/ansible/module_utils/network/f5/legacy.py
new file mode 100644
index 0000000000..bb2189c2bb
--- /dev/null
+++ b/lib/ansible/module_utils/network/f5/legacy.py
@@ -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)
diff --git a/lib/ansible/modules/network/f5/bigip_data_group.py b/lib/ansible/modules/network/f5/bigip_data_group.py
new file mode 100644
index 0000000000..7263ac9fd4
--- /dev/null
+++ b/lib/ansible/modules/network/f5/bigip_data_group.py
@@ -0,0 +1,1069 @@
+#!/usr/bin/python
+# -*- 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: bigip_data_group
+short_description: Manage data groups on a BIG-IP
+description:
+ - Allows for managing data groups on a BIG-IP. Data groups provide a way to store collections
+ of values on a BIG-IP for later use in things such as LTM rules, iRules, and ASM policies.
+version_added: 2.6
+options:
+ name:
+ description:
+ - Specifies the name of the data group.
+ required: True
+ type:
+ description:
+ - The type of records in this data group.
+ - This parameter is especially important because it causes BIG-IP to store your data
+ in different ways so-as to optimize access to it. For example, it would be wrong
+ to specify a list of records containing IP addresses, but label them as a C(string)
+ type.
+ - This value cannot be changed once the data group is created.
+ choices:
+ - address
+ - addr
+ - ip
+ - string
+ - str
+ - integer
+ - int
+ default: string
+ internal:
+ description:
+ - The type of this data group.
+ - You should only consider setting this value in cases where you know exactly what
+ you're doing, B(or), you are working with a pre-existing internal data group.
+ - Be aware that if you deliberately force this parameter to C(yes), and you have a
+ either a large number of records or a large total records size, this large amount
+ of data will be reflected in your BIG-IP configuration. This can lead to B(long)
+ system configuration load times due to needing to parse and verify the large
+ configuration.
+ - There is a limit of either 4 megabytes or 65,000 records (whichever is more restrictive)
+ for uploads when this parameter is C(yes).
+ - This value cannot be changed once the data group is created.
+ type: bool
+ default: no
+ external_file_name:
+ description:
+ - When creating a new data group, this specifies the file name that you want to give an
+ external data group file on the BIG-IP.
+ - This parameter is ignored when C(internal) is C(yes).
+ - This parameter can be used to select an existing data group file to use with an
+ existing external data group.
+ - If this value is not provided, it will be given the value specified in C(name) and,
+ therefore, match the name of the data group.
+ - This value may only contain letters, numbers, underscores, dashes, or a period.
+ records:
+ description:
+ - Specifies the records that you want to add to a data group.
+ - If you have a large number of records, it is recommended that you use C(records_content)
+ instead of typing all those records here.
+ - The technical limit of either 1. the number of records, or 2. the total size of all
+ records, varies with the size of the total resources on your system; in particular,
+ RAM.
+ - When C(internal) is C(no), at least one record must be specified in either C(records)
+ or C(records_content).
+ suboptions:
+ key:
+ description:
+ - The key describing the record in the data group.
+ - Your key will be used for validation of the C(type) parameter to this module.
+ required: True
+ value:
+ description:
+ - The value of the key describing the record in the data group.
+ records_src:
+ description:
+ - Path to a file with records in it.
+ - The file should be well-formed. This means that it includes records, one per line,
+ that resemble the following format "key separator value". For example, C(foo := bar).
+ - BIG-IP is strict about this format, but this module is a bit more lax. It will allow
+ you to include arbitrary amounts (including none) of empty space on either side of
+ the separator. For an illustration of this, see the Examples section.
+ - Record keys are limited in length to no more than 65520 characters.
+ - Values of record keys are limited in length to no more than 65520 characters.
+ - The total number of records you can have in your BIG-IP is limited by the memory
+ of the BIG-IP.
+ - The format of this content is slightly different depending on whether you specify
+ a C(type) of C(address), C(integer), or C(string). See the examples section for
+ examples of the different types of payload formats that are expected in your data
+ group file.
+ - When C(internal) is C(no), at least one record must be specified in either C(records)
+ or C(records_content).
+ separator:
+ description:
+ - When specifying C(records_content), this is the string of characters that will
+ be used to break apart entries in the C(records_content) into key/value pairs.
+ - By default, this parameter's value is C(:=).
+ - This value cannot be changed once it is set.
+ - This parameter is only relevant when C(internal) is C(no). It will be ignored
+ otherwise.
+ default: ":="
+ delete_data_group_file:
+ description:
+ - When C(yes), will ensure that the remote data group file is deleted.
+ - This parameter is only relevant when C(state) is C(absent) and C(internal) is C(no).
+ default: no
+ type: bool
+ partition:
+ description:
+ - Device partition to manage resources on.
+ default: Common
+ state:
+ description:
+ - When C(state) is C(present), ensures the data group exists.
+ - When C(state) is C(absent), ensures that the data group is removed.
+ choices:
+ - present
+ - absent
+ default: present
+extends_documentation_fragment: f5
+author:
+ - Tim Rupp (@caphrim007)
+'''
+
+EXAMPLES = r'''
+- name: Create a data group of addresses
+ bigip_data_group:
+ name: foo
+ password: secret
+ server: lb.mydomain.com
+ state: present
+ user: admin
+ records:
+ - key: 0.0.0.0/32
+ value: External_NAT
+ - key: 10.10.10.10
+ value: No_NAT
+ type: address
+ delegate_to: localhost
+
+- name: Create a data group of strings
+ bigip_data_group:
+ name: foo
+ password: secret
+ server: lb.mydomain.com
+ state: present
+ user: admin
+ records:
+ - key: caddy
+ value: ""
+ - key: cafeteria
+ value: ""
+ - key: cactus
+ value: ""
+ type: string
+ delegate_to: localhost
+
+- name: Create a data group of IP addresses from a file
+ bigip_data_group:
+ name: foo
+ password: secret
+ server: lb.mydomain.com
+ state: present
+ user: admin
+ records_src: /path/to/dg-file
+ type: address
+ delegate_to: localhost
+
+- name: Update an existing internal data group of strings
+ bigip_data_group:
+ name: foo
+ password: secret
+ server: lb.mydomain.com
+ state: present
+ internal: yes
+ user: admin
+ records:
+ - key: caddy
+ value: ""
+ - key: cafeteria
+ value: ""
+ - key: cactus
+ value: ""
+ delegate_to: localhost
+
+- name: Show the data format expected for records_content - address 1
+ copy:
+ dest: /path/to/addresses.txt
+ content: |
+ network 10.0.0.0 prefixlen 8 := "Network1",
+ network 172.16.0.0 prefixlen 12 := "Network2",
+ network 192.168.0.0 prefixlen 16 := "Network3",
+ network 2402:9400:1000:0:: prefixlen 64 := "Network4",
+ host 192.168.20.1 := "Host1",
+ host 172.16.1.1 := "Host2",
+ host 172.16.1.1/32 := "Host3",
+ host 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4",
+ host 2001:0db8:85a3:0000:0000:8a2e:0370:7334/128 := "Host5"
+
+- name: Show the data format expected for records_content - address 2
+ copy:
+ dest: /path/to/addresses.txt
+ content: |
+ 10.0.0.0/8 := "Network1",
+ 172.16.0.0/12 := "Network2",
+ 192.168.0.0/16 := "Network3",
+ 2402:9400:1000:0::/64 := "Network4",
+ 192.168.20.1 := "Host1",
+ 172.16.1.1 := "Host2",
+ 172.16.1.1/32 := "Host3",
+ 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4",
+ 2001:0db8:85a3:0000:0000:8a2e:0370:7334/128 := "Host5"
+
+- name: Show the data format expected for records_content - string
+ copy:
+ dest: /path/to/strings.txt
+ content: |
+ a := alpha,
+ b := bravo,
+ c := charlie,
+ x := x-ray,
+ y := yankee,
+ z := zulu,
+
+- name: Show the data format expected for records_content - integer
+ copy:
+ dest: /path/to/integers.txt
+ content: |
+ 1 := bar,
+ 2 := baz,
+ 3,
+ 4,
+'''
+
+RETURN = r'''
+# only common fields returned
+'''
+
+import hashlib
+import os
+import re
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.basic import env_fallback
+from io import StringIO
+
+try:
+ 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.common import F5ModuleError
+ 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 compare_dictionary
+ from library.module_utils.network.f5.common import f5_argument_spec
+ try:
+ from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
+ except ImportError:
+ HAS_F5SDK = False
+except ImportError:
+ 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.common import F5ModuleError
+ 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 compare_dictionary
+ from ansible.module_utils.network.f5.common import f5_argument_spec
+ try:
+ from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
+ except ImportError:
+ HAS_F5SDK = False
+
+try:
+ import netaddr
+ HAS_NETADDR = True
+except ImportError:
+ HAS_NETADDR = False
+
+
+LINE_LIMIT = 65000
+SIZE_LIMIT_BYTES = 4000000
+
+
+def zero_length(content):
+ content.seek(0, os.SEEK_END)
+ length = content.tell()
+ content.seek(0)
+ if length == 0:
+ return True
+ return False
+
+
+def size_exceeded(content):
+ records = content
+ records.seek(0, os.SEEK_END)
+ size = records.tell()
+ records.seek(0)
+ if size > SIZE_LIMIT_BYTES:
+ return True
+ return False
+
+
+def lines_exceeded(content):
+ result = False
+ for i, line in enumerate(content):
+ if i > LINE_LIMIT:
+ result = True
+ content.seek(0)
+ return result
+
+
+class RecordsEncoder(object):
+ def __init__(self, record_type=None, separator=None):
+ self._record_type = record_type
+ self._separator = separator
+ self._network_pattern = re.compile(r'^network\s+(?P<addr>[^ ]+)\s+prefixlen\s+(?P<prefix>\d+)\s+.*')
+ self._host_pattern = re.compile(r'^host\s+(?P<addr>[^ ]+)\s+.*')
+
+ def encode(self, record):
+ if isinstance(record, dict):
+ return self.encode_dict(record)
+ else:
+ return self.encode_string(record)
+
+ def encode_dict(self, record):
+ if self._record_type == 'ip':
+ return self.encode_address_from_dict(record)
+ elif self._record_type == 'integer':
+ return self.encode_integer_from_dict(record)
+ else:
+ return self.encode_string_from_dict(record)
+
+ def encode_address_from_dict(self, record):
+ try:
+ key = netaddr.IPNetwork(record['key'])
+ except netaddr.core.AddrFormatError:
+ raise F5ModuleError(
+ "When specifying an 'address' type, the value to the left of the separator must be an IP."
+ )
+ if key and 'value' in record:
+ if key.prefixlen in [32, 128]:
+ return self.encode_host(key.ip, record['value'])
+ else:
+ return self.encode_network(key.network, key.prefixlen, record['value'])
+ elif key:
+ if key.prefixlen in [32, 128]:
+ return self.encode_host(key.ip, key.ip)
+ else:
+ return self.encode_network(key.network, key.prefixlen, key.network)
+
+ def encode_integer_from_dict(self, record):
+ try:
+ int(record['key'])
+ except ValueError:
+ raise F5ModuleError(
+ "When specifying an 'integer' type, the value to the left of the separator must be a number."
+ )
+ if 'key' in record and 'value' in record:
+ return '{0} {1} {2}'.format(record['key'], self._separator, record['value'])
+ elif 'key' in record:
+ return str(record['key'])
+
+ def encode_string_from_dict(self, record):
+ if 'key' in record and 'value' in record:
+ return '{0} {1} {2}'.format(record['key'], self._separator, record['value'])
+ elif 'key' in record:
+ return '{0} {1} ""'.format(record['key'], self._separator)
+
+ def encode_string(self, record):
+ record = record.strip().strip(',')
+ if self._record_type == 'ip':
+ return self.encode_address_from_string(record)
+ elif self._record_type == 'integer':
+ return self.encode_integer_from_string(record)
+ else:
+ return self.encode_string_from_string(record)
+
+ def encode_address_from_string(self, record):
+ if self._network_pattern.match(record):
+ # network 192.168.0.0 prefixlen 16 := "Network3",
+ # network 2402:9400:1000:0:: prefixlen 64 := "Network4",
+ return record
+ elif self._host_pattern.match(record):
+ # host 172.16.1.1/32 := "Host3"
+ # host 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4"
+ return record
+ else:
+ # 192.168.0.0/16 := "Network3",
+ # 2402:9400:1000:0::/64 := "Network4",
+ try:
+ parts = record.split(self._separator)
+ if len(parts) == 2:
+ key = netaddr.IPNetwork(parts[0])
+ if key.prefixlen in [32, 128]:
+ return self.encode_host(key.ip, parts[1])
+ else:
+ return self.encode_network(key.network, key.prefixlen, parts[1])
+ elif len(parts) == 1 and parts[0] != '':
+ key = netaddr.IPNetwork(parts[0])
+ if key.prefixlen in [32, 128]:
+ return self.encode_host(key.ip, key.ip)
+ else:
+ return self.encode_network(key.network, key.prefixlen, key.network)
+ except netaddr.core.AddrFormatError:
+ raise F5ModuleError(
+ "When specifying an 'address' type, the value to the left of the separator must be an IP."
+ )
+
+ def encode_host(self, key, value):
+ return 'host {0} {1} {2}'.format(str(key), self._separator, str(value))
+
+ def encode_network(self, key, prefixlen, value):
+ return 'network {0} prefixlen {1} {2} {3}'.format(
+ str(key), str(prefixlen), self._separator, str(value)
+ )
+
+ def encode_integer_from_string(self, record):
+ parts = record.split(self._separator)
+ if len(parts) == 1 and parts[0] == '':
+ return None
+ try:
+ int(parts[0])
+ except ValueError:
+ raise F5ModuleError(
+ "When specifying an 'integer' type, the value to the left of the separator must be a number."
+ )
+ if len(parts) == 2:
+ return '{0} {1} {2}'.format(parts[0], self._separator, parts[1])
+ elif len(parts) == 1:
+ return str(parts[0])
+
+ def encode_string_from_string(self, record):
+ parts = record.split(self._separator)
+ if len(parts) == 2:
+ return '{0} {1} {2}'.format(parts[0], self._separator, parts[1])
+ elif len(parts) == 1 and parts[0] != '':
+ return '{0} {1} ""'.format(parts[0], self._separator)
+
+
+class RecordsDecoder(object):
+ def __init__(self, record_type=None, separator=None):
+ self._record_type = record_type
+ self._separator = separator
+ self._network_pattern = re.compile(r'^network\s+(?P<addr>[^ ]+)\s+prefixlen\s+(?P<prefix>\d+)\s+.*')
+ self._host_pattern = re.compile(r'^host\s+(?P<addr>[^ ]+)\s+.*')
+
+ def decode(self, record):
+ record = record.strip().strip(',')
+ if self._record_type == 'ip':
+ return self.decode_address_from_string(record)
+ else:
+ return self.decode_from_string(record)
+
+ def decode_address_from_string(self, record):
+ try:
+ matches = self._network_pattern.match(record)
+ if matches:
+ # network 192.168.0.0 prefixlen 16 := "Network3",
+ # network 2402:9400:1000:0:: prefixlen 64 := "Network4",
+ key = "{0}/{1}".format(matches.group('addr'), matches.group('prefix'))
+ addr = netaddr.IPNetwork(key)
+ value = record.split(self._separator)[1].strip().strip('"')
+ result = dict(name=str(addr), data=value)
+ return result
+ matches = self._host_pattern.match(record)
+ if matches:
+ # host 172.16.1.1/32 := "Host3"
+ # host 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4"
+ key = matches.group('addr')
+ addr = netaddr.IPNetwork(key)
+ value = record.split(self._separator)[1].strip().strip('"')
+ result = dict(name=str(addr), data=value)
+ return result
+ except netaddr.core.AddrFormatError:
+ raise F5ModuleError(
+ 'The value "{0}" is not an address'.format(record)
+ )
+
+ def decode_from_string(self, record):
+ parts = record.split(self._separator)
+ if len(parts) == 2:
+ return dict(name=parts[0].strip(), data=parts[1].strip('"').strip())
+ else:
+ return dict(name=parts[0].strip(), data="")
+
+
+class Parameters(AnsibleF5Parameters):
+ api_map = {
+ 'externalFileName': 'external_file_name'
+ }
+
+ api_attributes = [
+ 'records', 'type'
+ ]
+
+ returnables = []
+
+ updatables = [
+ 'records', 'checksum'
+ ]
+
+ @property
+ def type(self):
+ if self._values['type'] in ['address', 'addr', 'ip']:
+ return 'ip'
+ elif self._values['type'] in ['integer', 'int']:
+ return 'integer'
+ elif self._values['type'] in ['string']:
+ return 'string'
+
+ @property
+ def records_src(self):
+ try:
+ self._values['records_src'].seek(0)
+ return self._values['records_src']
+ except AttributeError:
+ pass
+ if self._values['records_src']:
+ records = open(self._values['records_src'])
+ else:
+ records = self._values['records']
+
+ # There is a 98% chance that the user will supply a data group that is < 1MB.
+ # 99.917% chance it is less than 10 MB. This is well within the range of typical
+ # memory available on a system.
+ #
+ # If this changes, this may need to be changed to use temporary files instead.
+ self._values['records_src'] = StringIO()
+
+ self._write_records_to_file(records)
+ return self._values['records_src']
+
+ def _write_records_to_file(self, records):
+ bucket_size = 1000000
+ bucket = []
+ encoder = RecordsEncoder(record_type=self.type, separator=self.separator)
+ for record in records:
+ result = encoder.encode(record)
+ if result:
+ bucket.append(to_text(result + ",\n"))
+ if len(bucket) == bucket_size:
+ self._values['records_src'].writelines(bucket)
+ bucket = []
+ self._values['records_src'].writelines(bucket)
+ self._values['records_src'].seek(0)
+
+
+class ApiParameters(Parameters):
+ @property
+ def checksum(self):
+ if self._values['checksum'] is None:
+ return None
+ result = self._values['checksum'].split(':')[2]
+ return result
+
+ @property
+ def records(self):
+ if self._values['records'] is None:
+ return None
+ return self._values['records']
+
+ @property
+ def records_list(self):
+ return self.records
+
+
+class ModuleParameters(Parameters):
+ @property
+ def checksum(self):
+ if self._values['checksum']:
+ return self._values['checksum']
+ result = hashlib.sha1()
+ records = self.records_src
+ while True:
+ data = records.read(4096)
+ if not data:
+ break
+ result.update(data)
+ result = result.hexdigest()
+ self._values['checksum'] = result
+ return result
+
+ @property
+ def external_file_name(self):
+ if self._values['external_file_name'] is None:
+ name = self.name
+ else:
+ name = self._values['external_file_name']
+ if re.search(r'[^a-z0-9-_.]', name):
+ raise F5ModuleError(
+ "'external_file_name' may only contain letters, numbers, underscores, dashes, or a period."
+ )
+ return name
+
+ @property
+ def records(self):
+ results = []
+ decoder = RecordsDecoder(record_type=self.type, separator=self.separator)
+ for record in self.records_src:
+ result = decoder.decode(record)
+ if result:
+ results.append(result)
+ return results
+
+ @property
+ def records_list(self):
+ if self._values['records'] is None:
+ return None
+ return self.records
+
+
+class Changes(Parameters):
+ def to_return(self):
+ result = {}
+ try:
+ for returnable in self.returnables:
+ result[returnable] = getattr(self, returnable)
+ result = self._filter_params(result)
+ except Exception:
+ pass
+ return result
+
+
+class UsableChanges(Changes):
+ pass
+
+
+class ReportableChanges(Changes):
+ pass
+
+
+class Difference(object):
+ def __init__(self, want, have=None):
+ self.want = want
+ self.have = have
+
+ def compare(self, param):
+ try:
+ result = getattr(self, param)
+ return result
+ except AttributeError:
+ return self.__default(param)
+
+ def __default(self, param):
+ attr1 = getattr(self.want, param)
+ try:
+ attr2 = getattr(self.have, param)
+ if attr1 != attr2:
+ return attr1
+ except AttributeError:
+ return attr1
+
+ @property
+ def records(self):
+ # External data groups are compared by their checksum, not their records. This
+ # is because the BIG-IP does not store the actual records in the API. It instead
+ # stores the checksum of the file. External DGs have the possibility of being huge
+ # and we would never want to do a comparison of such huge files.
+ #
+ # Therefore, comparison is no-op if the DG being worked with is an external DG.
+ if self.want.internal is False:
+ return None
+ if self.have.records is None and self.want.records == []:
+ return None
+ if self.have.records is None:
+ return self.want.records
+ result = compare_dictionary(self.want.records, self.have.records)
+ return result
+
+ @property
+ def type(self):
+ return None
+
+ @property
+ def checksum(self):
+ if self.want.internal:
+ return None
+ if self.want.checksum != self.have.checksum:
+ return True
+
+
+class BaseManager(object):
+ def __init__(self, *args, **kwargs):
+ self.module = kwargs.get('module', None)
+ self.client = kwargs.get('client', None)
+ self.want = ModuleParameters(params=self.module.params)
+ self.have = ApiParameters()
+ self.changes = UsableChanges()
+
+ def should_update(self):
+ result = self._update_changed_options()
+ if result:
+ return True
+ return False
+
+ def exec_module(self):
+ changed = False
+ result = dict()
+ state = self.want.state
+
+ try:
+ if state == "present":
+ changed = self.present()
+ elif state == "absent":
+ changed = self.absent()
+ except iControlUnexpectedHTTPError as e:
+ raise F5ModuleError(str(e))
+
+ reportable = ReportableChanges(params=self.changes.to_return())
+ changes = reportable.to_return()
+ result.update(**changes)
+ result.update(dict(changed=changed))
+ self._announce_deprecations(result)
+ return result
+
+ def _announce_deprecations(self, result):
+ warnings = result.pop('__warnings', [])
+ for warning in warnings:
+ self.client.module.deprecate(
+ msg=warning['msg'],
+ version=warning['version']
+ )
+
+ def _set_changed_options(self):
+ changed = {}
+ for key in ApiParameters.returnables:
+ if getattr(self.want, key) is not None:
+ changed[key] = getattr(self.want, key)
+ if changed:
+ self.changes = UsableChanges(params=changed)
+
+ def _update_changed_options(self):
+ diff = Difference(self.want, self.have)
+ updatables = ApiParameters.updatables
+ changed = dict()
+ for k in updatables:
+ change = diff.compare(k)
+ if change is None:
+ continue
+ else:
+ if isinstance(change, dict):
+ changed.update(change)
+ else:
+ changed[k] = change
+ if changed:
+ self.changes = UsableChanges(params=changed)
+ return True
+ return False
+
+ def present(self):
+ if self.exists():
+ return self.update()
+ else:
+ return self.create()
+
+ def absent(self):
+ if self.exists():
+ return self.remove()
+ return False
+
+ def remove(self):
+ if self.module.check_mode:
+ return True
+ self.remove_from_device()
+ if self.exists():
+ raise F5ModuleError("Failed to delete the resource.")
+ return True
+
+
+class InternalManager(BaseManager):
+ def create(self):
+ self._set_changed_options()
+ if size_exceeded(self.want.records_src) or lines_exceeded(self.want.records_src):
+ raise F5ModuleError(
+ "The size of the provided data (or file) is too large for an internal data group."
+ )
+ if self.module.check_mode:
+ return True
+ self.create_on_device()
+ return True
+
+ def update(self):
+ self.have = self.read_current_from_device()
+ if not self.should_update():
+ return False
+ if self.module.check_mode:
+ return True
+ self.update_on_device()
+ return True
+
+ def exists(self):
+ result = self.client.api.tm.ltm.data_group.internals.internal.exists(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ return result
+
+ def create_on_device(self):
+ params = self.want.api_params()
+ self.client.api.tm.ltm.data_group.internals.internal.create(
+ name=self.want.name,
+ partition=self.want.partition,
+ **params
+ )
+
+ def update_on_device(self):
+ params = self.changes.api_params()
+ resource = self.client.api.tm.ltm.data_group.internals.internal.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ resource.modify(**params)
+
+ def remove_from_device(self):
+ resource = self.client.api.tm.ltm.data_group.internals.internal.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ if resource:
+ resource.delete()
+
+ def read_current_from_device(self):
+ resource = self.client.api.tm.ltm.data_group.internals.internal.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ result = resource.attrs
+ return ApiParameters(params=result)
+
+
+class ExternalManager(BaseManager):
+ def absent(self):
+ result = False
+ if self.exists():
+ result = self.remove()
+ if self.external_file_exists() and self.want.delete_data_group_file:
+ result = self.remove_data_group_file_from_device()
+ return result
+
+ def create(self):
+ if zero_length(self.want.records_src):
+ raise F5ModuleError(
+ "An external data group cannot be empty."
+ )
+ self._set_changed_options()
+ if self.module.check_mode:
+ return True
+ self.create_on_device()
+ return True
+
+ def update(self):
+ self.have = self.read_current_from_device()
+ if not self.should_update():
+ return False
+ if zero_length(self.want.records_src):
+ raise F5ModuleError(
+ "An external data group cannot be empty."
+ )
+ if self.module.check_mode:
+ return True
+ self.update_on_device()
+ return True
+
+ def exists(self):
+ result = self.client.api.tm.ltm.data_group.externals.external.exists(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ return result
+
+ def external_file_exists(self):
+ result = self.client.api.tm.sys.file.data_groups.data_group.exists(
+ name=self.want.external_file_name,
+ partition=self.want.partition
+ )
+ return result
+
+ def _upload_to_file(self, name, type, remote_path, update=False):
+ self.client.api.shared.file_transfer.uploads.upload_stringio(self.want.records_src, name)
+ resource = self.client.api.tm.sys.file.data_groups
+ if update:
+ resource = resource.data_group.load(
+ name=name,
+ partition=self.want.partition
+ )
+ resource.modify(
+ sourcePath='file:{0}'.format(remote_path)
+ )
+ resource.refresh()
+ result = resource
+ else:
+ result = resource.data_group.create(
+ name=name,
+ type=type,
+ sourcePath='file:{0}'.format(remote_path)
+ )
+ return result.name
+
+ def create_on_device(self):
+ name = self.want.external_file_name
+ remote_path = '/var/config/rest/downloads/{0}'.format(name)
+ external_file = self._upload_to_file(name, self.want.type, remote_path, update=False)
+ self.client.api.tm.ltm.data_group.externals.external.create(
+ name=self.want.name,
+ partition=self.want.partition,
+ externalFileName=external_file
+ )
+ self.client.api.tm.util.unix_rm.exec_cmd('run', utilCmdArgs=remote_path)
+
+ def update_on_device(self):
+ name = self.want.external_file_name
+ remote_path = '/var/config/rest/downloads/{0}'.format(name)
+ external_file = self._upload_to_file(name, self.have.type, remote_path, update=True)
+ resource = self.client.api.tm.ltm.data_group.externals.external.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ resource.modify(
+ externalFileName=external_file
+ )
+
+ def remove_from_device(self):
+ resource = self.client.api.tm.ltm.data_group.externals.external.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ if resource:
+ resource.delete()
+
+ # Remove the remote data group file if asked to
+ if self.want.delete_data_group_file:
+ self.remove_data_group_file_from_device()
+
+ def remove_data_group_file_from_device(self):
+ resource = self.client.api.tm.sys.file.data_groups.data_group.load(
+ name=self.want.external_file_name,
+ partition=self.want.partition
+ )
+ if resource:
+ resource.delete()
+ return True
+ return False
+
+ def read_current_from_device(self):
+ """Reads the current configuration from the device
+
+ For an external data group, we are interested in two things from the
+ current configuration
+
+ * ``checksum``
+ * ``type``
+
+ The ``checksum`` will allow us to compare the data group value we have
+ with the data group value being provided.
+
+ The ``type`` will allow us to do validation on the data group value being
+ provided (if any).
+
+ Returns:
+ ExternalApiParameters: Attributes of the remote resource.
+ """
+ resource = self.client.api.tm.ltm.data_group.externals.external.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ external_file = os.path.basename(resource.externalFileName)
+ external_file_partition = os.path.dirname(resource.externalFileName).strip('/')
+ resource = self.client.api.tm.sys.file.data_groups.data_group.load(
+ name=external_file,
+ partition=external_file_partition
+ )
+ result = resource.attrs
+ return ApiParameters(params=result)
+
+
+class ModuleManager(object):
+ def __init__(self, *args, **kwargs):
+ self.kwargs = kwargs
+ self.module = kwargs.get('module')
+ self.client = kwargs.get('client', None)
+
+ def exec_module(self):
+ if self.module.params['internal']:
+ manager = self.get_manager('internal')
+ else:
+ manager = self.get_manager('external')
+ return manager.exec_module()
+
+ def get_manager(self, type):
+ if type == 'internal':
+ return InternalManager(**self.kwargs)
+ elif type == 'external':
+ return ExternalManager(**self.kwargs)
+
+
+class ArgumentSpec(object):
+ def __init__(self):
+ self.supports_check_mode = True
+ argument_spec = dict(
+ name=dict(required=True),
+ type=dict(
+ choices=['address', 'addr', 'ip', 'string', 'str', 'integer', 'int'],
+ default='string'
+ ),
+ delete_data_group_file=dict(type='bool'),
+ internal=dict(type='bool', default='no'),
+ records=dict(
+ type='list',
+ suboptions=dict(
+ key=dict(required=True),
+ value=dict(type='raw')
+ )
+ ),
+ records_src=dict(type='path'),
+ external_file_name=dict(),
+ separator=dict(default=':='),
+ state=dict(choices=['absent', 'present'], default='present'),
+ partition=dict(
+ default='Common',
+ fallback=(env_fallback, ['F5_PARTITION'])
+ )
+ )
+ self.argument_spec = {}
+ self.argument_spec.update(f5_argument_spec)
+ self.argument_spec.update(argument_spec)
+ self.mutually_exclusive = [
+ ['records', 'records_content', 'external_file_name']
+ ]
+
+
+def main():
+ spec = ArgumentSpec()
+
+ module = AnsibleModule(
+ argument_spec=spec.argument_spec,
+ supports_check_mode=spec.supports_check_mode
+ )
+ if not HAS_F5SDK:
+ module.fail_json(msg="The python f5-sdk module is required")
+ if not HAS_NETADDR:
+ module.fail_json(msg="The python netaddr module is required")
+
+ try:
+ client = F5Client(**module.params)
+ mm = ModuleManager(module=module, client=client)
+ results = mm.exec_module()
+ cleanup_tokens(client)
+ module.exit_json(**results)
+ except F5ModuleError as ex:
+ cleanup_tokens(client)
+ module.fail_json(msg=str(ex))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/lib/ansible/modules/network/f5/bigip_wait.py b/lib/ansible/modules/network/f5/bigip_wait.py
index fb1e731346..78ca05a504 100644
--- a/lib/ansible/modules/network/f5/bigip_wait.py
+++ b/lib/ansible/modules/network/f5/bigip_wait.py
@@ -20,7 +20,7 @@ description:
to accept configuration.
- This module can take into account situations where the device is in the middle
of rebooting due to a configuration change.
-version_added: "2.5"
+version_added: 2.5
options:
timeout:
description:
@@ -80,30 +80,21 @@ import time
from ansible.module_utils.basic import AnsibleModule
-HAS_DEVEL_IMPORTS = False
-
try:
- # Sideband repository used for dev
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.common import F5ModuleError
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
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
- HAS_DEVEL_IMPORTS = True
except ImportError:
- # Upstream Ansible
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.common import F5ModuleError
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
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
@@ -186,6 +177,9 @@ class ModuleManager(object):
version=warning['version']
)
+ def _get_client_connection(self):
+ return F5Client(**self.module.params)
+
def execute(self):
signal.signal(
signal.SIGALRM,
@@ -204,7 +198,7 @@ class ModuleManager(object):
try:
# The first test verifies that the REST API is available; this is done
# by repeatedly trying to login to it.
- self.client = F5Client(**self.module.params)
+ self.client = self._get_client_connection()
if not self.client:
continue