1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Refactors bigip device sshd (#33473)

* Refactors bigip device sshd

Fixes coding conventions for currect f5 conventions

* Fixes upstream errors
This commit is contained in:
Tim Rupp 2017-12-01 20:27:41 -08:00 committed by GitHub
parent a3b3dbe220
commit 76135a500e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 346 additions and 202 deletions

View file

@ -3,17 +3,20 @@
# #
# Copyright (c) 2017 F5 Networks Inc. # Copyright (c) 2017 F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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', ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'], 'status': ['preview'],
'supported_by': 'community'} 'supported_by': 'community'}
DOCUMENTATION = ''' DOCUMENTATION = r'''
--- ---
module: bigip_device_sshd module: bigip_device_sshd
short_description: Manage the SSHD settings of a BIG-IP short_description: Manage the SSHD settings of a BIG-IP
description: description:
- Manage the SSHD settings of a BIG-IP - Manage the SSHD settings of a BIG-IP.
version_added: "2.2" version_added: "2.2"
options: options:
allow: allow:
@ -28,7 +31,6 @@ options:
banner: banner:
description: description:
- Whether to enable the banner or not. - Whether to enable the banner or not.
required: false
choices: choices:
- enabled - enabled
- disabled - disabled
@ -36,12 +38,10 @@ options:
description: description:
- Specifies the text to include on the pre-login banner that displays - Specifies the text to include on the pre-login banner that displays
when a user attempts to login to the system using SSH. when a user attempts to login to the system using SSH.
required: false
inactivity_timeout: inactivity_timeout:
description: description:
- Specifies the number of seconds before inactivity causes an SSH - Specifies the number of seconds before inactivity causes an SSH
session to log out. session to log out.
required: false
log_level: log_level:
description: description:
- Specifies the minimum SSHD message level to include in the system log. - Specifies the minimum SSHD message level to include in the system log.
@ -62,11 +62,9 @@ options:
choices: choices:
- enabled - enabled
- disabled - disabled
required: false
port: port:
description: description:
- Port that you want the SSH daemon to run on. - Port that you want the SSH daemon to run on.
required: false
notes: notes:
- Requires the f5-sdk Python package on the host This is as easy as pip - Requires the f5-sdk Python package on the host This is as easy as pip
install f5-sdk. install f5-sdk.
@ -78,259 +76,278 @@ author:
- Tim Rupp (@caphrim007) - Tim Rupp (@caphrim007)
''' '''
EXAMPLES = ''' EXAMPLES = r'''
- name: Set the banner for the SSHD service from a string - name: Set the banner for the SSHD service from a string
bigip_device_sshd: bigip_device_sshd:
banner: "enabled" banner: enabled
banner_text: "banner text goes here" banner_text: banner text goes here
password: "secret" password: secret
server: "lb.mydomain.com" server: lb.mydomain.com
user: "admin" user: admin
delegate_to: localhost delegate_to: localhost
- name: Set the banner for the SSHD service from a file - name: Set the banner for the SSHD service from a file
bigip_device_sshd: bigip_device_sshd:
banner: "enabled" banner: enabled
banner_text: "{{ lookup('file', '/path/to/file') }}" banner_text: "{{ lookup('file', '/path/to/file') }}"
password: "secret" password: secret
server: "lb.mydomain.com" server: lb.mydomain.com
user: "admin" user: admin
delegate_to: localhost delegate_to: localhost
- name: Set the SSHD service to run on port 2222 - name: Set the SSHD service to run on port 2222
bigip_device_sshd: bigip_device_sshd:
password: "secret" password: secret
port: 2222 port: 2222
server: "lb.mydomain.com" server: lb.mydomain.com
user: "admin" user: admin
delegate_to: localhost delegate_to: localhost
''' '''
RETURN = ''' RETURN = r'''
allow: allow:
description: > description: >
Specifies, if you have enabled SSH access, the IP address or address Specifies, if you have enabled SSH access, the IP address or address
range for other systems that can use SSH to communicate with this range for other systems that can use SSH to communicate with this
system. system.
returned: changed returned: changed
type: string type: string
sample: "192.0.2.*" sample: 192.0.2.*
banner: banner:
description: Whether the banner is enabled or not. description: Whether the banner is enabled or not.
returned: changed returned: changed
type: string type: string
sample: "true" sample: true
banner_text: banner_text:
description: > description: >
Specifies the text included on the pre-login banner that Specifies the text included on the pre-login banner that
displays when a user attempts to login to the system using SSH. displays when a user attempts to login to the system using SSH.
returned: changed and success returned: changed and success
type: string type: string
sample: "This is a corporate device. Connecting to it without..." sample: This is a corporate device. Connecting to it without...
inactivity_timeout: inactivity_timeout:
description: > description: >
The number of seconds before inactivity causes an SSH. The number of seconds before inactivity causes an SSH
session to log out. session to log out.
returned: changed returned: changed
type: int type: int
sample: "10" sample: 10
log_level: log_level:
description: The minimum SSHD message level to include in the system log. description: The minimum SSHD message level to include in the system log.
returned: changed returned: changed
type: string type: string
sample: "debug" sample: debug
login: login:
description: Specifies that the system accepts SSH communications or not. description: Specifies that the system accepts SSH communications or not.
returned: changed returned: changed
type: bool type: bool
sample: true sample: true
port: port:
description: Port that you want the SSH daemon to run on. description: Port that you want the SSH daemon to run on.
returned: changed returned: changed
type: int type: int
sample: 22 sample: 22
''' '''
from ansible.module_utils.f5_utils import AnsibleF5Client
from ansible.module_utils.f5_utils import AnsibleF5Parameters
from ansible.module_utils.f5_utils import HAS_F5SDK
from ansible.module_utils.f5_utils import F5ModuleError
try: try:
from f5.bigip import ManagementRoot from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
from icontrol.session import iControlUnexpectedHTTPError
HAS_F5SDK = True
except ImportError: except ImportError:
HAS_F5SDK = False HAS_F5SDK = False
CHOICES = ['enabled', 'disabled']
LEVELS = ['debug', 'debug1', 'debug2', 'debug3', 'error', 'fatal', 'info',
'quiet', 'verbose']
class Parameters(AnsibleF5Parameters):
api_map = {
'bannerText': 'banner_text',
'inactivityTimeout': 'inactivity_timeout',
'logLevel': 'log_level'
}
class BigIpDeviceSshd(object): api_attributes = [
def __init__(self, *args, **kwargs): 'allow', 'banner', 'bannerText', 'inactivityTimeout', 'logLevel',
if not HAS_F5SDK: 'login', 'port'
raise F5ModuleError("The python f5-sdk module is required") ]
# The params that change in the module updatables = [
self.cparams = dict() 'allow', 'banner', 'banner_text', 'inactivity_timeout', 'log_level',
'login', 'port'
]
# Stores the params that are sent to the module returnables = [
self.params = kwargs 'allow', 'banner', 'banner_text', 'inactivity_timeout', 'log_level',
self.api = ManagementRoot(kwargs['server'], 'login', 'port'
kwargs['user'], ]
kwargs['password'],
port=kwargs['server_port'])
def update(self): def to_return(self):
changed = False result = {}
current = self.read() for returnable in self.returnables:
params = dict() result[returnable] = getattr(self, returnable)
result = self._filter_params(result)
return result
allow = self.params['allow'] def api_params(self):
banner = self.params['banner'] result = {}
banner_text = self.params['banner_text'] for api_attribute in self.api_attributes:
timeout = self.params['inactivity_timeout'] if self.api_map is not None and api_attribute in self.api_map:
log_level = self.params['log_level'] result[api_attribute] = getattr(self, self.api_map[api_attribute])
login = self.params['login']
port = self.params['port']
check_mode = self.params['check_mode']
if allow:
if 'allow' in current:
items = set(allow)
if items != current['allow']:
params['allow'] = list(items)
else: else:
params['allow'] = allow result[api_attribute] = getattr(self, api_attribute)
result = self._filter_params(result)
return result
if banner: @property
if 'banner' in current: def inactivity_timeout(self):
if banner != current['banner']: if self._values['inactivity_timeout'] is None:
params['banner'] = banner return None
else: return int(self._values['inactivity_timeout'])
params['banner'] = banner
if banner_text: @property
if 'banner_text' in current: def port(self):
if banner_text != current['banner_text']: if self._values['port'] is None:
params['bannerText'] = banner_text return None
else: return int(self._values['port'])
params['bannerText'] = banner_text
if timeout: @property
if 'inactivity_timeout' in current: def allow(self):
if timeout != current['inactivity_timeout']: if self._values['allow'] is None:
params['inactivityTimeout'] = timeout return None
else: allow = self._values['allow']
params['inactivityTimeout'] = timeout return list(set([str(x) for x in allow]))
if log_level:
if 'log_level' in current:
if log_level != current['log_level']:
params['logLevel'] = log_level
else:
params['logLevel'] = log_level
if login: class ModuleManager(object):
if 'login' in current: def __init__(self, client):
if login != current['login']: self.client = client
params['login'] = login self.have = None
else: self.want = Parameters(self.client.module.params)
params['login'] = login self.changes = Parameters()
if port: def _update_changed_options(self):
if 'port' in current: changed = {}
if port != current['port']: for key in Parameters.updatables:
params['port'] = port if getattr(self.want, key) is not None:
else: attr1 = getattr(self.want, key)
params['port'] = port attr2 = getattr(self.have, key)
if attr1 != attr2:
changed[key] = attr1
if changed:
self.changes = Parameters(changed)
return True
return False
if params: def exec_module(self):
changed = True
if check_mode:
return changed
self.cparams = camel_dict_to_snake_dict(params)
else:
return changed
r = self.api.tm.sys.sshd.load()
r.update(**params)
r.refresh()
return changed
def read(self):
"""Read information and transform it
The values that are returned by BIG-IP in the f5-sdk can have encoding
attached to them as well as be completely missing in some cases.
Therefore, this method will transform the data from the BIG-IP into a
format that is more easily consumable by the rest of the class and the
parameters that are supported by the module.
"""
p = dict()
r = self.api.tm.sys.sshd.load()
if hasattr(r, 'allow'):
# Deliberately using sets to suppress duplicates
p['allow'] = set([str(x) for x in r.allow])
if hasattr(r, 'banner'):
p['banner'] = str(r.banner)
if hasattr(r, 'bannerText'):
p['banner_text'] = str(r.bannerText)
if hasattr(r, 'inactivityTimeout'):
p['inactivity_timeout'] = str(r.inactivityTimeout)
if hasattr(r, 'logLevel'):
p['log_level'] = str(r.logLevel)
if hasattr(r, 'login'):
p['login'] = str(r.login)
if hasattr(r, 'port'):
p['port'] = int(r.port)
return p
def flush(self):
result = dict() result = dict()
changed = False
try: try:
changed = self.update() changed = self.update()
except iControlUnexpectedHTTPError as e: except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e)) raise F5ModuleError(str(e))
result.update(**self.cparams) changes = self.changes.to_return()
result.update(**changes)
result.update(dict(changed=changed)) result.update(dict(changed=changed))
return result return result
def read_current_from_device(self):
resource = self.client.api.tm.sys.sshd.load()
result = resource.attrs
return Parameters(result)
def update(self):
self.have = self.read_current_from_device()
if not self.should_update():
return False
if self.client.check_mode:
return True
self.update_on_device()
return True
def should_update(self):
result = self._update_changed_options()
if result:
return True
return False
def update_on_device(self):
params = self.want.api_params()
resource = self.client.api.tm.sys.sshd.load()
resource.update(**params)
class ArgumentSpec(object):
def __init__(self):
self.choices = ['enabled', 'disabled']
self.levels = [
'debug', 'debug1', 'debug2', 'debug3', 'error', 'fatal', 'info',
'quiet', 'verbose'
]
self.supports_check_mode = True
self.argument_spec = dict(
allow=dict(
required=False,
default=None,
type='list'
),
banner=dict(
required=False,
default=None,
choices=self.choices
),
banner_text=dict(
required=False,
default=None
),
inactivity_timeout=dict(
required=False,
default=None,
type='int'
),
log_level=dict(
required=False,
default=None,
choices=self.levels
),
login=dict(
required=False,
default=None,
choices=self.choices
),
port=dict(
required=False,
default=None,
type='int'
),
state=dict(
default='present',
choices=['present']
)
)
self.f5_product_name = 'bigip'
def main(): def main():
argument_spec = f5_argument_spec() if not HAS_F5SDK:
raise F5ModuleError("The python f5-sdk module is required")
meta_args = dict( spec = ArgumentSpec()
allow=dict(required=False, default=None, type='list'),
banner=dict(required=False, default=None, choices=CHOICES),
banner_text=dict(required=False, default=None),
inactivity_timeout=dict(required=False, default=None, type='int'),
log_level=dict(required=False, default=None, choices=LEVELS),
login=dict(required=False, default=None, choices=CHOICES),
port=dict(required=False, default=None, type='int'),
state=dict(default='present', choices=['present'])
)
argument_spec.update(meta_args)
module = AnsibleModule( client = AnsibleF5Client(
argument_spec=argument_spec, argument_spec=spec.argument_spec,
supports_check_mode=True supports_check_mode=spec.supports_check_mode,
f5_product_name=spec.f5_product_name
) )
try: try:
obj = BigIpDeviceSshd(check_mode=module.check_mode, **module.params) mm = ModuleManager(client)
result = obj.flush() results = mm.exec_module()
client.module.exit_json(**results)
module.exit_json(**result)
except F5ModuleError as e: except F5ModuleError as e:
module.fail_json(msg=str(e)) client.module.fail_json(msg=str(e))
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
from ansible.module_utils.f5_utils import *
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import 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.f5_utils import AnsibleF5Client
try:
from library.bigip_device_sshd import Parameters
from library.bigip_device_sshd import ModuleManager
from library.bigip_device_sshd import ArgumentSpec
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
from test.unit.modules.utils import set_module_args
except ImportError:
try:
from ansible.modules.network.f5.bigip_device_sshd import Parameters
from ansible.modules.network.f5.bigip_device_sshd import ModuleManager
from ansible.modules.network.f5.bigip_device_sshd import ArgumentSpec
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
from units.modules.utils import set_module_args
except ImportError:
raise SkipTest("F5 Ansible modules require the f5-sdk Python library")
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except Exception:
pass
fixture_data[path] = data
return data
class TestParameters(unittest.TestCase):
def test_module_parameters(self):
args = dict(
allow=['all'],
banner='enabled',
banner_text='asdf',
inactivity_timeout='100',
log_level='debug',
login='enabled',
port=1010,
server='localhost',
user='admin',
password='password'
)
p = Parameters(args)
assert p.allow == ['all']
assert p.banner == 'enabled'
assert p.banner_text == 'asdf'
assert p.inactivity_timeout == 100
assert p.log_level == 'debug'
assert p.login == 'enabled'
assert p.port == 1010
class TestManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
def test_update_settings(self, *args):
set_module_args(dict(
allow=['all'],
banner='enabled',
banner_text='asdf',
inactivity_timeout='100',
log_level='debug',
login='enabled',
port=1010,
server='localhost',
user='admin',
password='password'
))
# Configure the parameters that would be returned by querying the
# remote device
current = Parameters(
dict(
allow=['172.27.1.1']
)
)
client = AnsibleF5Client(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
f5_product_name=self.spec.f5_product_name
)
mm = ModuleManager(client)
# Override methods to force specific logic in the module to happen
mm.update_on_device = Mock(return_value=True)
mm.read_current_from_device = Mock(return_value=current)
results = mm.exec_module()
assert results['changed'] is True
assert results['allow'] == ['all']