Adds bigip_device_auth module (#44373)

This module can be used to configure auth settings to the mgmt
interface on a BIG-IP.
pull/44375/head
Tim Rupp 6 years ago committed by GitHub
parent b54f6cd132
commit 359d97f01b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,805 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2018, 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_device_auth
short_description: Manage system authentication on a BIG-IP
description:
- Manage the system authentication configuration. This module can assist in configuring
a number of different system authentication types. Note that this module can not be used
to configure APM authentication types.
version_added: 2.7
options:
type:
description:
- The authentication type to manage with this module.
- Take special note that the parameters supported by this module will vary depending
on the C(type) that you are configuring.
- This module only supports a subset, at this time, of the total available auth types.
choices:
- tacacs
- local
servers:
description:
- Specifies a list of the IPv4 addresses for servers using the Terminal
Access Controller Access System (TACACS)+ protocol with which the system
communicates to obtain authorization data.
- For each address, an alternate TCP port number may be optionally specified
by specifying the C(port) key.
- If no port number is specified, the default port C(49163) is used.
- This parameter is supported by the C(tacacs) type.
suboptions:
address:
description:
- The IP address of the server.
- This field is required, unless you are specifying a simple list of servers.
In that case, the simple list can specify server IPs. See examples for
more clarification.
port:
description:
- The port of the server.
default: 49163
secret:
description:
- Secret key used to encrypt and decrypt packets sent or received from the
server.
- B(Do not) use the pound/hash sign in the secret for TACACS+ servers.
- When configuring TACACS+ auth for the first time, this value is required.
service_name:
description:
- Specifies the name of the service that the user is requesting to be
authorized to use.
- Identifying what the user is asking to be authorized for, enables the
TACACS+ server to behave differently for different types of authorization
requests.
- When configuring this form of system authentication, this setting is required.
- Note that the majority of TACACS+ implementations are of service type C(ppp),
so try that first.
choices:
- slip
- ppp
- arap
- shell
- tty-daemon
- connection
- system
- firewall
protocol_name:
description:
- Specifies the protocol associated with the value specified in C(service_name),
which is a subset of the associated service being used for client authorization
or system accounting.
- Note that the majority of TACACS+ implementations are of protocol type C(ip),
so try that first.
choices:
- lcp
- ip
- ipx
- atalk
- vines
- lat
- xremote
- tn3270
- telnet
- rlogin
- pad
- vpdn
- ftp
- http
- deccp
- osicp
- unknown
authentication:
description:
- Specifies the process the system employs when sending authentication requests.
- When C(use-first-server), specifies that the system sends authentication
attempts to only the first server in the list.
- When C(use-all-servers), specifies that the system sends an authentication
request to each server until authentication succeeds, or until the system has
sent a request to all servers in the list.
- This parameter is supported by the C(tacacs) type.
choices:
- use-first-server
- use-all-servers
use_for_auth:
description:
- Specifies whether or not this auth source is put in use on the system.
type: bool
state:
description:
- The state of the authentication configuration on the system.
- When C(present), guarantees that the system is configured for the specified C(type).
- When C(absent), sets the system auth source back to C(local).
default: present
choices:
- absent
- present
update_secret:
description:
- C(always) will allow to update secrets if the user chooses to do so.
- C(on_create) will only set the secret when a C(use_auth_source) is C(yes)
and TACACS+ is not currently the auth source.
default: always
choices:
- always
- on_create
extends_documentation_fragment: f5
author:
- Tim Rupp (@caphrim007)
'''
EXAMPLES = r'''
- name: Set the system auth to TACACS+, default server port
bigip_device_auth:
type: tacacs
authentication: use-all-servers
protocol_name: ip
secret: secret
servers:
- 10.10.10.10
- 10.10.10.11
service_name: ppp
state: present
use_for_auth: yes
provider:
password: secret
server: lb.mydomain.com
user: admin
delegate_to: localhost
- name: Set the system auth to TACACS+, override server port
bigip_device_auth:
type: tacacs
authentication: use-all-servers
protocol_name: ip
secret: secret
servers:
- address: 10.10.10.10
port: 1234
- 10.10.10.11
service_name: ppp
use_for_auth: yes
state: present
provider:
password: secret
server: lb.mydomain.com
user: admin
delegate_to: localhost
'''
RETURN = r'''
servers:
description: List of servers used in TACACS authentication.
returned: changed
type: list
sample: ['1.2.2.1', '4.5.5.4']
authentication:
description: Process the system uses to serve authentication requests when using TACACS.
returned: changed
type: string
sample: use-all-servers
service_name:
description: Name of the service the user is requesting to be authorized to use.
returned: changed
type: string
sample: ppp
protocol_name:
description: Name of the protocol associated with C(service_name) used for client authentication.
returned: changed
type: string
sample: ip
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import string_types
try:
from library.module_utils.network.f5.bigip import F5RestClient
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 fq_name
from library.module_utils.network.f5.common import f5_argument_spec
from library.module_utils.network.f5.common import exit_json
from library.module_utils.network.f5.common import fail_json
except ImportError:
from ansible.module_utils.network.f5.bigip import F5RestClient
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 fq_name
from ansible.module_utils.network.f5.common import f5_argument_spec
from ansible.module_utils.network.f5.common import exit_json
from ansible.module_utils.network.f5.common import fail_json
class BaseParameters(AnsibleF5Parameters):
@property
def api_map(self):
return {}
@property
def api_attributes(self):
return []
@property
def returnables(self):
return []
@property
def updatables(self):
return []
class BaseApiParameters(BaseParameters):
pass
class BaseModuleParameters(BaseParameters):
pass
class BaseChanges(BaseParameters):
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 BaseUsableChanges(BaseChanges):
pass
class BaseReportableChanges(BaseChanges):
pass
class TacacsParameters(BaseParameters):
api_map = {
'protocol': 'protocol_name',
'service': 'service_name'
}
api_attributes = [
'authentication',
'protocol',
'service',
'secret',
'servers'
]
returnables = [
'servers',
'secret',
'authentication',
'service_name',
'protocol_name'
]
updatables = [
'servers',
'secret',
'authentication',
'service_name',
'protocol_name',
'auth_source',
]
class TacacsApiParameters(TacacsParameters):
pass
class TacacsModuleParameters(TacacsParameters):
@property
def servers(self):
if self._values['servers'] is None:
return None
result = []
for server in self._values['servers']:
if isinstance(server, dict):
if 'address' not in server:
raise F5ModuleError(
"An 'address' field must be provided when specifying separate fields to the 'servers' parameter."
)
address = server.get('address')
port = server.get('port', 49163)
elif isinstance(server, string_types):
address = server
port = 49163
result.append('{0}:{1}'.format(address, port))
return result
@property
def auth_source(self):
return 'tacacs'
class TacacsChanges(BaseChanges, TacacsParameters):
pass
class TacacsUsableChanges(TacacsChanges):
pass
class TacacsReportableChanges(TacacsChanges):
@property
def secret(self):
return None
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):
want = getattr(self.want, param)
try:
have = getattr(self.have, param)
if want != have:
return want
except AttributeError:
return want
@property
def secret(self):
if self.want.secret != self.have.secret and self.want.update_secret == 'always':
return self.want.secret
class BaseManager(object):
def _set_changed_options(self):
changed = {}
for key in self.returnables:
if getattr(self.want, key) is not None:
changed[key] = getattr(self.want, key)
if changed:
self.changes = self.get_usable_changes(params=changed)
def _update_changed_options(self):
diff = Difference(self.want, self.have)
updatables = self.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 = self.get_usable_changes(params=changed)
return True
return False
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
if state == "present":
changed = self.present()
elif state == "absent":
changed = self.absent()
reportable = self.get_reportable_changes(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 present(self):
if self.exists():
return self.update()
return self.create()
def absent(self):
if self.exists():
return self.remove()
return False
def update_auth_source_on_device(self, source):
"""Set the system auth source.
Configuring the authentication source is only one step in the process of setting
up an auth source. The other step is to inform the system of the auth source
you want to use.
This method is used for situations where
* The ``use_for_auth`` parameter is set to ``yes``
* The ``use_for_auth`` parameter is set to ``no``
* The ``state`` parameter is set to ``absent``
When ``state`` equal to ``absent``, before you can delete the TACACS+ configuration,
you must set the system auth to "something else". The system ships with a system
auth called "local", so this is the logical "something else" to use.
When ``use_for_auth`` is no, the same situation applies as when ``state`` equal
to ``absent`` is done above.
When ``use_for_auth`` is ``yes``, this method will set the current system auth
state to TACACS+.
Arguments:
source (string): The source that you want to set on the device.
"""
params = dict(
type=source
)
uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.patch(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
def read_current_auth_source_from_device(self):
uri = "https://{0}:{1}/mgmt/tm/auth/source".format(
self.client.provider['server'],
self.client.provider['server_port'],
)
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return response['type']
class LocalManager(BaseManager):
def __init__(self, *args, **kwargs):
self.module = kwargs.get('module', None)
self.client = kwargs.get('client', None)
self.want = self.get_module_parameters(params=self.module.params)
self.have = self.get_api_parameters()
self.changes = self.get_usable_changes()
@property
def returnables(self):
return []
@property
def updatables(self):
return []
def get_parameters(self, params=None):
return BaseParameters(params=params)
def get_usable_changes(self, params=None):
return BaseUsableChanges(params=params)
def get_reportable_changes(self, params=None):
return BaseReportableChanges(params=params)
def get_module_parameters(self, params=None):
return BaseModuleParameters(params=params)
def get_api_parameters(self, params=None):
return BaseApiParameters(params=params)
def exists(self):
uri = "https://{0}:{1}/mgmt/tm/auth/source".format(
self.client.provider['server'],
self.client.provider['server_port'],
)
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
if response['type'] == 'local':
return True
return False
def create(self):
self._set_changed_options()
if self.module.check_mode:
return True
self.update_auth_source_on_device('local')
return True
def present(self):
if not self.exists():
return self.create()
def absent(self):
raise F5ModuleError(
"The 'local' type cannot be removed. "
"Instead, specify a 'state' of 'present' on other types."
)
class TacacsManager(BaseManager):
def __init__(self, *args, **kwargs):
self.module = kwargs.get('module', None)
self.client = kwargs.get('client', None)
self.want = self.get_module_parameters(params=self.module.params)
self.have = self.get_api_parameters()
self.changes = self.get_usable_changes()
@property
def returnables(self):
return TacacsParameters.returnables
@property
def updatables(self):
return TacacsParameters.updatables
def get_usable_changes(self, params=None):
return TacacsUsableChanges(params=params)
def get_reportable_changes(self, params=None):
return TacacsReportableChanges(params=params)
def get_module_parameters(self, params=None):
return TacacsModuleParameters(params=params)
def get_api_parameters(self, params=None):
return TacacsApiParameters(params=params)
def exists(self):
uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format(
self.client.provider['server'],
self.client.provider['server_port'],
)
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError:
return False
if resp.status == 404 or 'code' in response and response['code'] == 404:
return False
return True
def create(self):
self._set_changed_options()
if self.module.check_mode:
return True
self.create_on_device()
if self.want.use_for_auth:
self.update_auth_source_on_device('tacacs')
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
result = False
if self.update_on_device():
result = True
if self.want.use_for_auth and self.changes.auth_source == 'tacacs':
self.update_auth_source_on_device('tacacs')
result = True
return result
def remove(self):
if self.module.check_mode:
return True
self.update_auth_source_on_device('local')
self.remove_from_device()
if self.exists():
raise F5ModuleError("Failed to delete the resource.")
return True
def create_on_device(self):
params = self.changes.api_params()
params['name'] = 'system-auth'
uri = "https://{0}:{1}/mgmt/tm/auth/tacacs".format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.post(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] in [400, 403]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
def update_on_device(self):
params = self.changes.api_params()
if not params:
return False
uri = 'https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth'.format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.patch(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return True
def remove_from_device(self):
uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.delete(uri)
if resp.status == 200:
return True
raise F5ModuleError(resp.content)
def read_current_from_device(self):
uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format(
self.client.provider['server'],
self.client.provider['server_port'],
)
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
response['auth_source'] = self.read_current_auth_source_from_device()
return self.get_api_parameters(params=response)
class ModuleManager(object):
def __init__(self, *args, **kwargs):
self.module = kwargs.get('module', None)
self.client = kwargs.get('client', None)
self.kwargs = kwargs
def exec_module(self):
manager = self.get_manager(self.module.params['type'])
return manager.exec_module()
def get_manager(self, type):
if type == 'tacacs':
return TacacsManager(**self.kwargs)
elif type == 'local':
return LocalManager(**self.kwargs)
else:
raise F5ModuleError(
"The provided 'type' is unknown."
)
class ArgumentSpec(object):
def __init__(self):
self.supports_check_mode = True
argument_spec = dict(
type=dict(
required=True,
choices=['local', 'tacacs']
),
servers=dict(type='raw'),
secret=dict(no_log=True),
service_name=dict(
choices=[
'slip', 'ppp', 'arap', 'shell', 'tty-daemon',
'connection', 'system', 'firewall'
]
),
protocol_name=dict(
choices=[
'lcp', 'ip', 'ipx', 'atalk', 'vines', 'lat',
'xremote', 'tn3270', 'telnet', 'rlogin', 'pad',
'vpdn', 'ftp', 'http', 'deccp', 'osicp', 'unknown'
]
),
authentication=dict(
choices=[
'use-first-server',
'use-all-servers'
]
),
use_for_auth=dict(type='bool'),
update_secret=dict(
choices=['always', 'on_create'],
default='always'
),
state=dict(
default='present',
choices=['present', 'absent']
),
)
self.argument_spec = {}
self.argument_spec.update(f5_argument_spec)
self.argument_spec.update(argument_spec)
def main():
spec = ArgumentSpec()
module = AnsibleModule(
argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode,
)
try:
client = F5RestClient(**module.params)
mm = ModuleManager(module=module, client=client)
results = mm.exec_module()
exit_json(module, results, client)
except F5ModuleError as ex:
fail_json(module, ex, client)
if __name__ == '__main__':
main()

@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2018, 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_device_auth import TacacsApiParameters
from library.modules.bigip_device_auth import TacacsModuleParameters
from library.modules.bigip_device_auth import TacacsManager
from library.modules.bigip_device_auth import ModuleManager
from library.modules.bigip_device_auth import ArgumentSpec
from library.module_utils.network.f5.common import F5ModuleError
from test.unit.modules.utils import set_module_args
except ImportError:
try:
from ansible.modules.network.f5.bigip_sys_auth import TacacsApiParameters
from ansible.modules.network.f5.bigip_sys_auth import TacacsModuleParameters
from ansible.modules.network.f5.bigip_sys_auth import TacacsManager
from ansible.modules.network.f5.bigip_sys_auth import ModuleManager
from ansible.modules.network.f5.bigip_sys_auth import ArgumentSpec
from ansible.module_utils.network.f5.common import F5ModuleError
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(
type="tacacs",
authentication="use-all-servers",
protocol_name="ip",
secret="$XXXXXXXXXXXXXXXXXXXX==",
servers=['10.10.10.10', '10.10.10.11'],
service_name="ppp",
use_for_auth=True,
update_secret="on_create",
)
p = TacacsModuleParameters(params=args)
assert p.type == 'tacacs'
assert p.authentication == 'use-all-servers'
def test_api_parameters(self):
args = load_fixture('load_tm_auth_tacacs_1.json')
p = TacacsApiParameters(params=args)
assert p.authentication == 'use-first-server'
assert p.protocol_name == 'ftp'
assert p.secret == 'secret'
assert p.servers == ['11.11.11.11']
assert p.service_name == 'ppp'
@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(self, *args):
set_module_args(dict(
type="tacacs",
authentication="use-all-servers",
protocol_name="ip",
secret="secret",
servers=['10.10.10.10', '10.10.10.11'],
service_name="ppp",
use_for_auth=True,
update_secret="on_create",
state='present',
provider=dict(
password='admin',
server='localhost',
user='admin'
)
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode
)
# Override methods to force specific logic in the module to happen
m1 = TacacsManager(module=module)
m1.exists = Mock(return_value=False)
m1.create_on_device = Mock(return_value=True)
m1.update_auth_source_on_device = Mock(return_value=True)
mm = ModuleManager(module=module)
mm.get_manager = Mock(return_value=m1)
results = mm.exec_module()
assert results['changed'] is True
Loading…
Cancel
Save