Fixes and additions for f5 modules (#39986)

Small fixes in the f5 module utils. I believe the action plugins now
work consistently across types of connections
This commit is contained in:
Tim Rupp 2018-05-11 11:45:42 -07:00 committed by GitHub
parent 8654508cbd
commit 00a6b19e58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 172 additions and 162 deletions

View file

@ -80,15 +80,15 @@ class F5RestClient(F5BaseClient):
payload = {
'username': self.provider['user'],
'password': self.provider['password'],
'loginProviderName': self.provider['auth_provider']
'loginProviderName': self.provider['auth_provider'] or 'tmos'
}
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
if response.status not in [200]:
raise F5ModuleError('Status code: {0}. Unexpected Error: {1} for uri: {2}\nText: {3}'.format(
response.status, response.reason, response.url, response._content
))
session.headers['X-F5-Auth-Token'] = response.json()['token']['token']
@ -98,7 +98,7 @@ class F5RestClient(F5BaseClient):
exc = ex
time.sleep(1)
error = 'Unable to connect to {0} on port {1}.'.format(
self.params['server'], self.params['server_port']
self.provider['server'], self.provider['server_port']
)
if exc is not None:
error += ' The reported error was "{0}".'.format(str(exc))

View file

@ -61,35 +61,43 @@ class F5Client(F5BaseClient):
class F5RestClient(F5BaseClient):
def __init__(self, *args, **kwargs):
super(F5RestClient, self).__init__(*args, **kwargs)
self.provider = self.merge_provider_params()
@property
def api(self):
ex = None
exc = 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)
url = "https://{0}:{1}/mgmt/shared/authn/login".format(
self.provider['server'], self.provider['server_port']
)
self._client = result
payload = {
'username': self.provider['user'],
'password': self.provider['password'],
'loginProviderName': self.provider['auth_provider'] or 'local'
}
session = iControlRestSession()
session.verify = self.provider['validate_certs']
response = session.post(url, json=payload)
if response.status not in [200]:
raise F5ModuleError('Status code: {0}. Unexpected Error: {1} for uri: {2}\nText: {3}'.format(
response.status, 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 ex is not None:
error += ' The reported error was "{0}".'.format(str(ex))
error = 'Unable to connect to {0} on port {1}.'.format(
self.provider['server'], self.provider['server_port']
)
if exc is not None:
error += ' The reported error was "{0}".'.format(str(exc))
raise F5ModuleError(error)

View file

@ -295,8 +295,6 @@ def compare_dictionary(want, have):
Returns:
bool:
:param have:
:return:
"""
if want == [] and have is None:
return None
@ -328,6 +326,26 @@ def exit_json(module, results, client=None):
module.exit_json(**results)
def is_uuid(uuid=None):
"""Check to see if value is an F5 UUID
UUIDs are used in BIG-IQ and in select areas of BIG-IP (notably ASM). This method
will check to see if the provided value matches a UUID as known by these products.
Args:
uuid (string): The value to check for UUID-ness
Returns:
bool:
"""
if uuid is None:
return False
pattern = r'[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}'
if re.match(pattern, uuid):
return True
return False
class Noop(object):
"""Represent no-operation required
@ -405,7 +423,7 @@ class F5BaseClient(object):
elif self.params.get('auth_provider', None):
result['auth_provider'] = self.params.get('auth_provider', None)
else:
result['auth_provider'] = 'tmos'
result['auth_provider'] = None
if provider.get('user', None):
result['user'] = provider.get('user', None)

View file

@ -35,8 +35,8 @@ 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 ``requests`` design was chosen due to familiarity with the tool. Internally,
the classes contained herein use Ansible native libraries.
The means by which you should use it are similar to ``requests`` basic usage.
@ -159,7 +159,7 @@ class PreparedRequest(object):
class Response(object):
def __init__(self):
self._content = None
self.status_code = None
self.status = None
self.headers = dict()
self.url = None
self.reason = None
@ -187,9 +187,14 @@ class iControlRestSession(object):
self.headers = self.default_headers()
self.verify = True
self.params = {}
self.auth = None
self.timeout = 30
self.server = None
self.user = None
self.password = None
self.server_port = None
self.auth_provider = None
def _normalize_headers(self, headers):
result = {}
result.update(dict((k.lower(), v) for k, v in headers))
@ -259,12 +264,13 @@ class iControlRestSession(object):
method=request.method,
data=request.body,
timeout=kwargs.get('timeout', None) or self.timeout,
validate_certs=kwargs.get('verify', None) or self.verify,
headers=request.headers
)
try:
result = open_url(request.url, **params)
response._content = result.read()
response._content = result.read().decode('utf-8')
response.status = result.getcode()
response.url = result.geturl()
response.msg = "OK (%s bytes)" % result.headers.get('Content-Length', 'unknown')
@ -280,79 +286,19 @@ class iControlRestSession(object):
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 delete(self, url, json=None, **kwargs):
return self.request('DELETE', url, json=json, **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)

View file

@ -19,6 +19,7 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
import copy
@ -43,17 +44,16 @@ except ImportError:
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None):
del tmp # tmp no longer has any effect
socket_path = None
transport = 'rest'
if self._play_context.connection == 'network_cli':
provider = self._task.args.get('provider', {})
if any(provider.values()):
display.warning('provider is unnecessary when using network_cli and will be ignored')
del self._task.args['provider']
display.warning("'provider' is unnecessary when using 'network_cli' and will be ignored")
elif self._play_context.connection == 'local':
provider = load_provider(f5_provider_spec, self._task.args)
transport = provider['transport'] or 'rest'
transport = provider['transport'] or transport
display.vvvv('connection transport is %s' % transport, self._play_context.remote_addr)
@ -70,17 +70,14 @@ class ActionModule(_ActionModule):
display.vvv('using connection plugin %s' % pc.connection, pc.remote_addr)
connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin)
socket_path = connection.run()
display.vvvv('socket_path: %s' % socket_path, pc.remote_addr)
if not socket_path:
return {'failed': True,
'msg': 'unable to open shell. Please see: ' +
'msg': 'Unable to open shell. Please see: ' +
'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'}
task_vars['ansible_socket'] = socket_path
else:
self._task.args['provider'] = ActionModule.rest_implementation(provider, self._play_context)
else:
return {'failed': True, 'msg': 'Connection type %s is not valid for this module' % self._play_context.connection}
@ -96,61 +93,5 @@ class ActionModule(_ActionModule):
conn.send_command('exit')
out = conn.get_prompt()
result = super(ActionModule, self).run(task_vars=task_vars)
result = super(ActionModule, self).run(tmp, task_vars)
return result
@staticmethod
def rest_implementation(provider, play_context):
"""Provides a generic argument spec using Play context vars
This method will return a set of default values to use for connecting
to a remote BIG-IP in the event that you do not use either
* The environment fallback variables F5_USER, F5_PASSWORD, etc
* The "provider" spec
With this "spec" (for lack of a better name) Ansible will attempt
to fill in the provider arguments itself using the play context variables.
These variables are contained in the list of MAGIC_VARIABLE_MAPPING
found in the constants file
* https://github.com/ansible/ansible/blob/devel/lib/ansible/constants.py
Therefore, if you do not use the provider nor that environment args, this
method here will be populate the "provider" dict with with the necessary
F5 connection params, from the following host vars,
* remote_addr=('ansible_ssh_host', 'ansible_host'),
* remote_user=('ansible_ssh_user', 'ansible_user'),
* password=('ansible_ssh_pass', 'ansible_password'),
* port=('ansible_ssh_port', 'ansible_port'),
* timeout=('ansible_ssh_timeout', 'ansible_timeout'),
* private_key_file=('ansible_ssh_private_key_file', 'ansible_private_key_file'),
For example, this may leave your inventory looking like this
bigip2 ansible_host=1.2.3.4 ansible_port=10443 ansible_user=admin ansible_password=admin
:param provider:
:param play_context:
:return:
"""
provider['transport'] = 'rest'
if provider.get('server') is None:
provider['server'] = play_context.remote_addr
if provider.get('server_port') is None:
default_port = provider['server_port'] if provider['server_port'] else 443
provider['server_port'] = int(play_context.port or default_port)
if provider.get('timeout') is None:
provider['timeout'] = C.PERSISTENT_COMMAND_TIMEOUT
if provider.get('user') is None:
provider['user'] = play_context.connection_user
if provider.get('password') is None:
provider['password'] = play_context.password
return provider

View file

@ -0,0 +1,97 @@
#
# (c) 2016 Red Hat Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
import copy
from ansible import constants as C
from ansible.module_utils._text import to_text
from ansible.module_utils.connection import Connection
from ansible.module_utils.network.common.utils import load_provider
from ansible.plugins.action.normal import ActionModule as _ActionModule
try:
from library.module_utils.network.f5.common import f5_provider_spec
except:
from ansible.module_utils.network.f5.common import f5_provider_spec
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None):
socket_path = None
transport = 'rest'
if self._play_context.connection == 'network_cli':
provider = self._task.args.get('provider', {})
if any(provider.values()):
display.warning("'provider' is unnecessary when using 'network_cli' and will be ignored")
elif self._play_context.connection == 'local':
provider = load_provider(f5_provider_spec, self._task.args)
transport = provider['transport'] or transport
display.vvvv('connection transport is %s' % transport, self._play_context.remote_addr)
if transport == 'cli':
pc = copy.deepcopy(self._play_context)
pc.connection = 'network_cli'
pc.network_os = 'bigiq'
pc.remote_addr = provider.get('server', self._play_context.remote_addr)
pc.port = int(provider['server_port'] or self._play_context.port or 22)
pc.remote_user = provider.get('user', self._play_context.connection_user)
pc.password = provider.get('password', self._play_context.password)
pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file
pc.timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT)
display.vvv('using connection plugin %s' % pc.connection, pc.remote_addr)
connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin)
socket_path = connection.run()
display.vvvv('socket_path: %s' % socket_path, pc.remote_addr)
if not socket_path:
return {'failed': True,
'msg': 'unable to open shell. Please see: ' +
'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'}
task_vars['ansible_socket'] = socket_path
else:
return {'failed': True, 'msg': 'Connection type %s is not valid for this module' % self._play_context.connection}
if (self._play_context.connection == 'local' and transport == 'cli') or self._play_context.connection == 'network_cli':
# make sure we are in the right cli context which should be
# enable mode and not config module
if socket_path is None:
socket_path = self._connection.socket_path
conn = Connection(socket_path)
out = conn.get_prompt()
while '(config' in to_text(out, errors='surrogate_then_replace').strip():
display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr)
conn.send_command('exit')
out = conn.get_prompt()
result = super(ActionModule, self).run(tmp, task_vars)
return result