#!/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': ['stableinterface'], 'supported_by': 'certified'} DOCUMENTATION = r''' --- module: bigip_device_dns short_description: Manage BIG-IP device DNS settings description: - Manage BIG-IP device DNS settings. version_added: 2.2 options: cache: description: - Specifies whether the system caches DNS lookups or performs the operation each time a lookup is needed. Please note that this applies only to Access Policy Manager features, such as ACLs, web application rewrites, and authentication. choices: - enabled - disabled - enable - disable name_servers: description: - A list of name servers that the system uses to validate DNS lookups search: description: - A list of domains that the system searches for local domain lookups, to resolve local host names. ip_version: description: - Specifies whether the DNS specifies IP addresses using IPv4 or IPv6. choices: - 4 - 6 state: description: - The state of the variable on the system. When C(present), guarantees that an existing variable is set to C(value). default: present choices: - absent - present extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) - Wojciech Wypior (@wojtek0806) ''' EXAMPLES = r''' - name: Set the DNS settings on the BIG-IP bigip_device_dns: name_servers: - 208.67.222.222 - 208.67.220.220 search: - localdomain - lab.local provider: password: secret server: lb.mydomain.com user: admin delegate_to: localhost ''' RETURN = r''' cache: description: The new value of the DNS caching returned: changed type: string sample: enabled name_servers: description: List of name servers that were set returned: changed type: list sample: ['192.0.2.10', '172.17.12.10'] search: description: List of search domains that were set returned: changed type: list sample: ['192.0.2.10', '172.17.12.10'] ip_version: description: IP version that was set that DNS will specify IP addresses in returned: changed type: int 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.basic import AnsibleModule 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 from library.module_utils.network.f5.common import is_empty_list 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 from ansible.module_utils.network.f5.common import is_empty_list class Parameters(AnsibleF5Parameters): api_map = { 'dns.cache': 'cache', 'nameServers': 'name_servers', 'include': 'ip_version', } api_attributes = [ 'nameServers', 'search', 'include', ] updatables = [ 'cache', 'name_servers', 'search', 'ip_version', ] returnables = [ 'cache', 'name_servers', 'search', 'ip_version', ] absentables = [ 'name_servers', 'search', ] class ApiParameters(Parameters): pass class ModuleParameters(Parameters): @property def search(self): search = self._values['search'] if search is None: return None if isinstance(search, str) and search != "": result = list() result.append(str(search)) return result if is_empty_list(search): return [] return search @property def name_servers(self): name_servers = self._values['name_servers'] if name_servers is None: return None if isinstance(name_servers, str) and name_servers != "": result = list() result.append(str(name_servers)) return result if is_empty_list(name_servers): return [] return name_servers @property def cache(self): if self._values['cache'] is None: return None if str(self._values['cache']) in ['enabled', 'enable']: return 'enable' else: return 'disable' @property def ip_version(self): if self._values['ip_version'] == 6: return "options inet6" elif self._values['ip_version'] == 4: return "" else: return None class Changes(Parameters): def to_return(self): result = {} try: for returnable in self.returnables: change = getattr(self, returnable) if isinstance(change, dict): result.update(change) else: result[returnable] = change result = self._filter_params(result) except Exception: pass return result class UsableChanges(Changes): pass class ReportableChanges(Changes): @property def ip_version(self): if self._values['ip_version'] == 'options inet6': return 6 elif self._values['ip_version'] == "": return 4 else: 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): attr1 = getattr(self.want, param) try: attr2 = getattr(self.have, param) if attr1 != attr2: return attr1 except AttributeError: return attr1 @property def ip_version(self): if self.want.ip_version is None: return None if self.want.ip_version == "" and self.have.ip_version is None: return None if self.want.ip_version == self.have.ip_version: return None if self.want.ip_version != self.have.ip_version: return self.want.ip_version @property def name_servers(self): state = self.want.state if self.want.name_servers is None: return None if state == 'absent': if self.have.name_servers is None and self.want.name_servers: return None if set(self.want.name_servers) == set(self.have.name_servers): return [] if set(self.want.name_servers) != set(self.have.name_servers): return list(set(self.want.name_servers).difference(self.have.name_servers)) if not self.want.name_servers: if self.have.name_servers is None: return None if self.have.name_servers is not None: return self.want.name_servers if self.have.name_servers is None: return self.want.name_servers if set(self.want.name_servers) != set(self.have.name_servers): return self.want.name_servers @property def search(self): state = self.want.state if self.want.search is None: return None if not self.want.search: if self.have.search is None: return None if self.have.search is not None: return self.want.search if state == 'absent': if self.have.search is None and self.want.search: return None if set(self.want.search) == set(self.have.search): return [] if set(self.want.search) != set(self.have.search): return list(set(self.want.search).difference(self.have.search)) if self.have.search is None: return self.want.search if set(self.want.search) != set(self.have.search): return self.want.search class ModuleManager(object): def __init__(self, *args, **kwargs): self.module = kwargs.pop('module', None) self.client = kwargs.pop('client', None) self.want = ModuleParameters(params=self.module.params) self.have = ApiParameters() self.changes = UsableChanges() def _announce_deprecations(self, result): warnings = result.pop('__warnings', []) for warning in warnings: self.module.deprecate( msg=warning['msg'], version=warning['version'] ) def _update_changed_options(self): diff = Difference(self.want, self.have) updatables = Parameters.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 _absent_changed_options(self): diff = Difference(self.want, self.have) absentables = Parameters.absentables changed = dict() for k in absentables: 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 exec_module(self): changed = False result = dict() state = self.want.state if state == "present": changed = self.update() elif state == "absent": changed = self.absent() 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 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 should_update(self): result = self._update_changed_options() if result: return True return False def should_absent(self): result = self._absent_changed_options() if result: return True return False def absent(self): self.have = self.read_current_from_device() if not self.should_absent(): return False if self.module.check_mode: return True self.absent_on_device() return True def read_dns_cache_setting(self): uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( self.client.provider['server'], self.client.provider['server_port'], 'dns.cache' ) 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 def read_current_from_device(self): cache = self.read_dns_cache_setting() uri = "https://{0}:{1}/mgmt/tm/sys/dns/".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 cache: response['cache'] = cache['value'] return ApiParameters(params=response) def update_on_device(self): params = self.changes.api_params() if params: uri = "https://{0}:{1}/mgmt/tm/sys/dns/".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) if self.want.cache: uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( self.client.provider['server'], self.client.provider['server_port'], 'dns.cache' ) payload = {"value": self.want.cache} resp = self.client.api.patch(uri, json=payload) 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 absent_on_device(self): params = self.changes.api_params() uri = "https://{0}:{1}/mgmt/tm/sys/dns/".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) class ArgumentSpec(object): def __init__(self): self.supports_check_mode = True argument_spec = dict( cache=dict( choices=['disabled', 'enabled', 'disable', 'enable'] ), name_servers=dict( type='list' ), search=dict( type='list' ), ip_version=dict( choices=[4, 6], type='int' ), state=dict( default='present', choices=['absent', 'present'] ) ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) self.argument_spec.update(argument_spec) self.required_one_of = [ ['name_servers', 'search', 'ip_version', 'cache'] ] def main(): spec = ArgumentSpec() module = AnsibleModule( argument_spec=spec.argument_spec, supports_check_mode=spec.supports_check_mode, required_one_of=spec.required_one_of ) client = F5RestClient(**module.params) try: mm = ModuleManager(module=module, client=client) results = mm.exec_module() cleanup_tokens(client) exit_json(module, results, client) except F5ModuleError as ex: cleanup_tokens(client) fail_json(module, ex, client) if __name__ == '__main__': main()