Refactored bigip_device_dns (#32483)

Module was using old coding standards. This updates the module
pull/32488/head
Tim Rupp 7 years ago committed by GitHub
parent cbc5c2d556
commit 60281b85fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,11 +4,15 @@
# 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_dns module: bigip_device_dns
short_description: Manage BIG-IP device DNS settings short_description: Manage BIG-IP device DNS settings
@ -22,17 +26,17 @@ options:
operation each time a lookup is needed. Please note that this applies operation each time a lookup is needed. Please note that this applies
only to Access Policy Manager features, such as ACLs, web application only to Access Policy Manager features, such as ACLs, web application
rewrites, and authentication. rewrites, and authentication.
required: false
default: disable default: disable
choices: choices:
- enable - enabled
- disable - disabled
name_servers: name_servers:
description: description:
- A list of name serverz that the system uses to validate DNS lookups - A list of name servers that the system uses to validate DNS lookups
forwarders: forwarders:
description: description:
- A list of BIND servers that the system can use to perform DNS lookups - A list of BIND servers that the system can use to perform DNS lookups
- Deprecated in 2.4. Use the GUI or edit named.conf.
search: search:
description: description:
- A list of domains that the system searches for local domain lookups, - A list of domains that the system searches for local domain lookups,
@ -40,7 +44,6 @@ options:
ip_version: ip_version:
description: description:
- Specifies whether the DNS specifies IP addresses using IPv4 or IPv6. - Specifies whether the DNS specifies IP addresses using IPv4 or IPv6.
required: false
choices: choices:
- 4 - 4
- 6 - 6
@ -48,14 +51,13 @@ options:
description: description:
- The state of the variable on the system. When C(present), guarantees - The state of the variable on the system. When C(present), guarantees
that an existing variable is set to C(value). that an existing variable is set to C(value).
required: false
default: present default: present
choices: choices:
- absent - absent
- present - present
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 requests install f5-sdk.
extends_documentation_fragment: f5 extends_documentation_fragment: f5
requirements: requirements:
- f5-sdk - f5-sdk
@ -63,327 +65,326 @@ author:
- Tim Rupp (@caphrim007) - Tim Rupp (@caphrim007)
''' '''
EXAMPLES = ''' EXAMPLES = r'''
- name: Set the DNS settings on the BIG-IP - name: Set the DNS settings on the BIG-IP
bigip_device_dns: bigip_device_dns:
name_servers: name_servers:
- 208.67.222.222 - 208.67.222.222
- 208.67.220.220 - 208.67.220.220
search: search:
- localdomain - localdomain
- lab.local - lab.local
state: present password: secret
password: "secret" server: lb.mydomain.com
server: "lb.mydomain.com" user: admin
user: "admin" validate_certs: no
validate_certs: "no"
delegate_to: localhost delegate_to: localhost
''' '''
RETURN = ''' RETURN = r'''
cache: cache:
description: The new value of the DNS caching description: The new value of the DNS caching
returned: changed returned: changed
type: string type: string
sample: "enabled" sample: enabled
name_servers: name_servers:
description: List of name servers that were added or removed description: List of name servers that were set
returned: changed returned: changed
type: list type: list
sample: "['192.0.2.10', '172.17.12.10']" sample: ['192.0.2.10', '172.17.12.10']
forwarders:
description: List of forwarders that were added or removed
returned: changed
type: list
sample: "['192.0.2.10', '172.17.12.10']"
search: search:
description: List of search domains that were added or removed description: List of search domains that were set
returned: changed returned: changed
type: list type: list
sample: "['192.0.2.10', '172.17.12.10']" sample: ['192.0.2.10', '172.17.12.10']
ip_version: ip_version:
description: IP version that was set that DNS will specify IP addresses in description: IP version that was set that DNS will specify IP addresses in
returned: changed returned: changed
type: int type: int
sample: 4 sample: 4
warnings:
description: The list of warnings (if any) generated by module based on arguments
returned: always
type: list
sample: ['...', '...']
''' '''
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.contexts import TransactionContextManager from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
from f5.bigip import ManagementRoot
HAS_F5SDK = True
except ImportError: except ImportError:
HAS_F5SDK = False HAS_F5SDK = False
REQUIRED = ['name_servers', 'search', 'forwarders', 'ip_version', 'cache'] class Parameters(AnsibleF5Parameters):
CACHE = ['disable', 'enable'] api_map = {
IP = [4, 6] 'dhclient.mgmt': 'dhcp',
'dns.cache': 'cache',
'nameServers': 'name_servers',
'include': 'ip_version'
}
api_attributes = [
'nameServers', 'search', 'include'
]
class BigIpDeviceDns(object): updatables = [
def __init__(self, *args, **kwargs): 'cache', 'name_servers', 'search', 'ip_version'
if not HAS_F5SDK: ]
raise F5ModuleError("The python f5-sdk module is required")
# The params that change in the module returnables = [
self.cparams = dict() 'cache', 'name_servers', 'search', 'ip_version'
]
# Stores the params that are sent to the module absentables = [
self.params = kwargs 'name_servers', 'search'
self.api = ManagementRoot(kwargs['server'], ]
kwargs['user'],
kwargs['password'],
port=kwargs['server_port'])
def flush(self): def to_return(self):
result = dict() result = {}
changed = False for returnable in self.returnables:
state = self.params['state'] result[returnable] = getattr(self, returnable)
result = self._filter_params(result)
return result
if self.dhcp_enabled(): def api_params(self):
raise F5ModuleError( result = {}
"DHCP on the mgmt interface must be disabled to make use of " + for api_attribute in self.api_attributes:
"this module" if self.api_map is not None and api_attribute in self.api_map:
) result[api_attribute] = getattr(self, self.api_map[api_attribute])
else:
result[api_attribute] = getattr(self, api_attribute)
result = self._filter_params(result)
return result
if state == 'absent': @property
changed = self.absent() def search(self):
else: result = []
changed = self.present() if self._values['search'] is None:
return None
for server in self._values['search']:
result.append(str(server))
return result
result.update(**self.cparams) @property
result.update(dict(changed=changed)) def name_servers(self):
result = []
if self._values['name_servers'] is None:
return None
for server in self._values['name_servers']:
result.append(str(server))
return result return result
def dhcp_enabled(self): @property
r = self.api.tm.sys.dbs.db.load(name='dhclient.mgmt') def cache(self):
if r.value == 'enable': if str(self._values['cache']) in ['enabled', 'enable']:
return True return 'enable'
else: else:
return False return 'disable'
def read(self):
result = dict()
cache = self.api.tm.sys.dbs.db.load(name='dns.cache') @property
proxy = self.api.tm.sys.dbs.db.load(name='dns.proxy.__iter__') def dhcp(self):
dns = self.api.tm.sys.dns.load() valid = ['enable', 'enabled']
return True if self._values['dhcp'] in valid else False
result['cache'] = str(cache.value) @property
result['forwarders'] = str(proxy.value).split(' ') def forwarders(self):
if self._values['forwarders'] is None:
if hasattr(dns, 'nameServers'): return None
result['name_servers'] = dns.nameServers
if hasattr(dns, 'search'):
result['search'] = dns.search
if hasattr(dns, 'include') and 'options inet6' in dns.include:
result['ip_version'] = 6
else: else:
result['ip_version'] = 4 raise F5ModuleError(
return result "The modifying of forwarders is not supported."
)
def present(self):
params = dict()
current = self.read()
# Temporary locations to hold the changed params
update = dict(
dns=None,
forwarders=None,
cache=None
)
nameservers = self.params['name_servers']
search_domains = self.params['search']
ip_version = self.params['ip_version']
forwarders = self.params['forwarders']
cache = self.params['cache']
check_mode = self.params['check_mode']
if nameservers:
if 'name_servers' in current:
if nameservers != current['name_servers']:
params['nameServers'] = nameservers
else:
params['nameServers'] = nameservers
if search_domains:
if 'search' in current:
if search_domains != current['search']:
params['search'] = search_domains
else:
params['search'] = search_domains
if ip_version:
if 'ip_version' in current:
if ip_version != int(current['ip_version']):
if ip_version == 6:
params['include'] = 'options inet6'
elif ip_version == 4:
params['include'] = ''
else:
if ip_version == 6:
params['include'] = 'options inet6'
elif ip_version == 4:
params['include'] = ''
if params:
self.cparams.update(camel_dict_to_snake_dict(params))
if 'include' in params:
del self.cparams['include']
if params['include'] == '':
self.cparams['ip_version'] = 4
else:
self.cparams['ip_version'] = 6
update['dns'] = params.copy()
params = dict()
if forwarders:
if 'forwarders' in current:
if forwarders != current['forwarders']:
params['forwarders'] = forwarders
else:
params['forwarders'] = forwarders
if params:
self.cparams.update(camel_dict_to_snake_dict(params))
update['forwarders'] = ' '.join(params['forwarders'])
params = dict()
if cache:
if 'cache' in current:
if cache != current['cache']:
params['cache'] = cache
if params: @property
self.cparams.update(camel_dict_to_snake_dict(params)) def ip_version(self):
update['cache'] = params['cache'] if self._values['ip_version'] in [6, '6', 'options inet6']:
params = dict() return "options inet6"
elif self._values['ip_version'] in [4, '4', '']:
if self.cparams: return ""
changed = True
if check_mode:
return changed
else: else:
return False return None
tx = self.api.tm.transactions.transaction
with TransactionContextManager(tx) as api: class ModuleManager(object):
cache = api.tm.sys.dbs.db.load(name='dns.cache') def __init__(self, client):
proxy = api.tm.sys.dbs.db.load(name='dns.proxy.__iter__') self.client = client
dns = api.tm.sys.dns.load() self.have = None
self.want = Parameters(self.client.module.params)
# Empty values can be supplied, but you cannot supply the self.changes = Parameters()
# None value, so we check for that specifically
if update['cache'] is not None: def _update_changed_options(self):
cache.update(value=update['cache']) changed = {}
if update['forwarders'] is not None: for key in Parameters.updatables:
proxy.update(value=update['forwarders']) if getattr(self.want, key) is not None:
if update['dns'] is not None: attr1 = getattr(self.want, key)
dns.update(**update['dns']) attr2 = getattr(self.have, key)
return changed if attr1 != attr2:
changed[key] = attr1
def absent(self): if changed:
params = dict() self.changes = Parameters(changed)
current = self.read() return True
return False
# Temporary locations to hold the changed params def exec_module(self):
update = dict( changed = False
dns=None, result = dict()
forwarders=None state = self.want.state
)
try:
if state == "present":
changed = self.update()
elif state == "absent":
changed = self.absent()
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
changes = self.changes.to_return()
result.update(**changes)
result.update(dict(changed=changed))
return result
nameservers = self.params['name_servers'] def read_current_from_device(self):
search_domains = self.params['search'] want_keys = ['dns.cache']
forwarders = self.params['forwarders'] result = dict()
check_mode = self.params['check_mode'] dbs = self.client.api.tm.sys.dbs.get_collection()
for db in dbs:
if db.name in want_keys:
result[db.name] = db.value
dns = self.client.api.tm.sys.dns.load()
attrs = dns.attrs
if 'include' not in attrs:
attrs['include'] = 4
result.update(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
if forwarders and 'forwarders' in current: def should_update(self):
set_current = set(current['forwarders']) result = self._update_changed_options()
set_new = set(forwarders) if result:
return True
return False
forwarders = set_current - set_new def update_on_device(self):
if forwarders != set_current: params = self.want.api_params()
forwarders = list(forwarders) cache = self.client.api.tm.sys.dbs.db.load(name='dns.cache')
params['forwarders'] = ' '.join(forwarders) dns = self.client.api.tm.sys.dns.load()
# Empty values can be supplied, but you cannot supply the
# None value, so we check for that specifically
if self.want.cache is not None:
cache.update(value=self.want.cache)
if params: if params:
changed = True dns.update(**params)
self.cparams.update(camel_dict_to_snake_dict(params))
update['forwarders'] = params['forwarders'] def _absent_changed_options(self):
params = dict() changed = {}
for key in Parameters.absentables:
if nameservers and 'name_servers' in current: if getattr(self.want, key) is not None:
set_current = set(current['name_servers']) set_want = set(getattr(self.want, key))
set_new = set(nameservers) set_have = set(getattr(self.have, key))
set_new = set_have - set_want
nameservers = set_current - set_new if set_new != set_have:
if nameservers != set_current: changed[key] = list(set_new)
params['nameServers'] = list(nameservers) if changed:
self.changes = Parameters(changed)
if search_domains and 'search' in current: return True
set_current = set(current['search']) return False
set_new = set(search_domains)
search_domains = set_current - set_new
if search_domains != set_current:
params['search'] = list(search_domains)
if params: def should_absent(self):
changed = True result = self._absent_changed_options()
self.cparams.update(camel_dict_to_snake_dict(params)) if result:
update['dns'] = params.copy() return True
params = dict() return False
if not self.cparams: def absent(self):
self.have = self.read_current_from_device()
if not self.should_absent():
return False return False
if self.client.check_mode:
return True
self.absent_on_device()
return True
def absent_on_device(self):
params = self.changes.api_params()
resource = self.client.api.tm.sys.dns.load()
resource.update(**params)
class ArgumentSpec(object):
def __init__(self):
self.supports_check_mode = True
self.argument_spec = dict(
cache=dict(
required=False,
choices=['disabled', 'enabled', 'disable', 'enable'],
default=None
),
name_servers=dict(
required=False,
default=None,
type='list'
),
forwarders=dict(
required=False,
default=None,
type='list'
),
search=dict(
required=False,
default=None,
type='list'
),
ip_version=dict(
required=False,
default=None,
choices=[4, 6],
type='int'
),
state=dict(
required=False,
default='present',
choices=['absent', 'present']
)
)
self.required_one_of = [
['name_servers', 'search', 'forwarders', 'ip_version', 'cache']
]
self.f5_product_name = 'bigip'
if check_mode:
return changed
tx = self.api.tm.transactions.transaction
with TransactionContextManager(tx) as api:
proxy = api.tm.sys.dbs.db.load(name='dns.proxy.__iter__')
dns = api.tm.sys.dns.load()
if update['forwarders'] is not None: def main():
proxy.update(value=update['forwarders']) if not HAS_F5SDK:
if update['dns'] is not None: raise F5ModuleError("The python f5-sdk module is required")
dns.update(**update['dns'])
return changed
spec = ArgumentSpec()
def main(): client = AnsibleF5Client(
argument_spec = f5_argument_spec() argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode,
meta_args = dict( f5_product_name=spec.f5_product_name,
cache=dict(required=False, choices=CACHE, default=None), required_one_of=spec.required_one_of
name_servers=dict(required=False, default=None, type='list'),
forwarders=dict(required=False, default=None, type='list'),
search=dict(required=False, default=None, type='list'),
ip_version=dict(required=False, default=None, choices=IP, type='int')
)
argument_spec.update(meta_args)
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=[REQUIRED],
supports_check_mode=True
) )
try: try:
obj = BigIpDeviceDns(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()

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import pytest
import sys
from nose.plugins.skip import SkipTest
if sys.version_info < (2, 7):
raise SkipTest("F5 Ansible modules require Python >= 2.7")
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch, Mock
from ansible.module_utils import basic
from ansible.module_utils._text import to_bytes
from ansible.module_utils.f5_utils import AnsibleF5Client
from ansible.module_utils.f5_utils import F5ModuleError
try:
from library.bigip_device_dns import Parameters
from library.bigip_device_dns import ModuleManager
from library.bigip_device_dns import ArgumentSpec
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
except ImportError:
try:
from ansible.modules.network.f5.bigip_device_dns import Parameters
from ansible.modules.network.f5.bigip_device_dns import ModuleManager
from ansible.modules.network.f5.bigip_device_dns import ArgumentSpec
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
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 set_module_args(args):
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
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(
cache='disable',
forwarders=['12.12.12.12', '13.13.13.13'],
ip_version=4,
name_servers=['10.10.10.10', '11.11.11.11'],
search=['14.14.14.14', '15.15.15.15'],
server='localhost',
user='admin',
password='password'
)
p = Parameters(args)
assert p.cache == 'disable'
assert p.name_servers == ['10.10.10.10', '11.11.11.11']
assert p.search == ['14.14.14.14', '15.15.15.15']
# BIG-IP considers "ipv4" to be an empty value
assert p.ip_version == ''
def test_ipv6_parameter(self):
args = dict(
ip_version=6
)
p = Parameters(args)
assert p.ip_version == 'options inet6'
def test_ensure_forwards_raises_exception(self):
args = dict(
forwarders=['12.12.12.12', '13.13.13.13'],
)
p = Parameters(args)
with pytest.raises(F5ModuleError) as ex:
foo = p.forwarders
assert 'The modifying of forwarders is not supported' in str(ex)
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(
cache='disable',
forwarders=['12.12.12.12', '13.13.13.13'],
ip_version=4,
name_servers=['10.10.10.10', '11.11.11.11'],
search=['14.14.14.14', '15.15.15.15'],
server='localhost',
user='admin',
password='password'
))
# Configure the parameters that would be returned by querying the
# remote device
current = Parameters(
dict(
cache='enable'
)
)
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
Loading…
Cancel
Save