From 034df49c56f03601aaf0049c71cc927c1efa6ac1 Mon Sep 17 00:00:00 2001
From: Mario Lenz <m@riolenz.de>
Date: Sat, 16 Nov 2019 06:55:06 +0100
Subject: [PATCH] VMware: New module vmware_host_dns (#64458)

* Restore module vmware_dns_config
* Remove domainname and change_hostname_to
* Changed version_added from 2.10 to '2.10'
* Add setup_attach_host: true to test case
* Add 'vcsim is not defined' block to integration tests
* Change 'result' to 'dns_config_result'
* Bugfix: Changing some static configurations while keeping others can crash the module
* Implement changing DNS config from DHCP to static on a cluster
* Update documentation for vmware_host_dns
* vmware_host_dns integration tests: Always revert to original DNS configuration, even if a test fails
* Deprecate vmware_dns_config
---
 .../fragments/64458-vmware_host_dns.yaml      |   4 +
 .../rst/porting_guides/porting_guide_2.10.rst |   4 +
 ...re_dns_config.py => _vmware_dns_config.py} |   6 +-
 .../modules/cloud/vmware/vmware_host_dns.py   | 470 ++++++++++++++++++
 .../targets/vmware_host_dns/aliases           |   3 +
 .../targets/vmware_host_dns/tasks/dhcp.yml    | 303 +++++++++++
 .../targets/vmware_host_dns/tasks/main.yml    |  28 ++
 .../targets/vmware_host_dns/tasks/static.yml  | 215 ++++++++
 8 files changed, 1032 insertions(+), 1 deletion(-)
 create mode 100644 changelogs/fragments/64458-vmware_host_dns.yaml
 rename lib/ansible/modules/cloud/vmware/{vmware_dns_config.py => _vmware_dns_config.py} (95%)
 create mode 100644 lib/ansible/modules/cloud/vmware/vmware_host_dns.py
 create mode 100644 test/integration/targets/vmware_host_dns/aliases
 create mode 100644 test/integration/targets/vmware_host_dns/tasks/dhcp.yml
 create mode 100644 test/integration/targets/vmware_host_dns/tasks/main.yml
 create mode 100644 test/integration/targets/vmware_host_dns/tasks/static.yml

diff --git a/changelogs/fragments/64458-vmware_host_dns.yaml b/changelogs/fragments/64458-vmware_host_dns.yaml
new file mode 100644
index 00000000000..45303c2ea54
--- /dev/null
+++ b/changelogs/fragments/64458-vmware_host_dns.yaml
@@ -0,0 +1,4 @@
+minor_changes:
+- vmware_host_dns - New module replacing vmware_dns_config with increased functionality.
+deprecated_features:
+- vmware_dns_config - Deprecate in favour of new module vmware_host_dns.
diff --git a/docs/docsite/rst/porting_guides/porting_guide_2.10.rst b/docs/docsite/rst/porting_guides/porting_guide_2.10.rst
index 7f8362e3646..8beb665f383 100644
--- a/docs/docsite/rst/porting_guides/porting_guide_2.10.rst
+++ b/docs/docsite/rst/porting_guides/porting_guide_2.10.rst
@@ -68,6 +68,10 @@ The following functionality will change in Ansible 2.14. Please update update yo
 
 * The :ref:`docker_container <docker_container_module>` module has a new option, ``container_default_behavior``, whose default value will change from ``compatibility`` to ``no_defaults``. Set to an explicit value to avoid deprecation warnings.
 
+The following modules will be removed in Ansible 2.14. Please update your playbooks accordingly.
+
+* ``vmware_dns_config`` use :ref:`vmware_host_dns <vmware_host_dns_module>` instead.
+
 
 Noteworthy module changes
 -------------------------
diff --git a/lib/ansible/modules/cloud/vmware/vmware_dns_config.py b/lib/ansible/modules/cloud/vmware/_vmware_dns_config.py
similarity index 95%
rename from lib/ansible/modules/cloud/vmware/vmware_dns_config.py
rename to lib/ansible/modules/cloud/vmware/_vmware_dns_config.py
index d86d5e527d7..45fe1e9b297 100644
--- a/lib/ansible/modules/cloud/vmware/vmware_dns_config.py
+++ b/lib/ansible/modules/cloud/vmware/_vmware_dns_config.py
@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function
 __metaclass__ = type
 
 ANSIBLE_METADATA = {'metadata_version': '1.1',
-                    'status': ['preview'],
+                    'status': ['deprecated'],
                     'supported_by': 'community'}
 
 DOCUMENTATION = '''
@@ -25,6 +25,10 @@ notes:
 requirements:
     - "python >= 2.6"
     - PyVmomi
+deprecated:
+    removed_in: '2.14'
+    why: Will be replaced with new module vmware_host_dns.
+    alternative: Use M(vmware_host_dns) instead.
 options:
     change_hostname_to:
         description:
diff --git a/lib/ansible/modules/cloud/vmware/vmware_host_dns.py b/lib/ansible/modules/cloud/vmware/vmware_host_dns.py
new file mode 100644
index 00000000000..ffbb84f09dd
--- /dev/null
+++ b/lib/ansible/modules/cloud/vmware/vmware_host_dns.py
@@ -0,0 +1,470 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Christian Kotte <christian.kotte@gmx.de>
+#
+# 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: vmware_host_dns
+short_description: Manage DNS configuration of an ESXi host system
+description:
+- This module can be used to configure DNS for the default TCP/IP stack on an ESXi host system.
+version_added: '2.10'
+author:
+- Christian Kotte (@ckotte)
+- Mario Lenz (@mariolenz)
+notes:
+- This module is a replacement for the module C(vmware_dns_config)
+- Tested on vSphere 6.7
+requirements:
+- python >= 2.6
+- PyVmomi
+options:
+  type:
+    description:
+    - Type of IP assignment. Either C(dhcp) or C(static).
+    - A VMkernel adapter needs to be set to DHCP if C(type) is set to C(dhcp).
+    type: str
+    choices: [ 'dhcp', 'static' ]
+    required: true
+  device:
+    description:
+    - The VMkernel network adapter to obtain DNS settings from.
+    - Needs to get its IP through DHCP, a static network configuration combined with a dynamic DNS configuration doesn't work.
+    - The parameter is only required in case of C(type) is set to C(dhcp).
+    type: str
+  host_name:
+    description:
+    - The hostname to be used for the ESXi host.
+    - Cannot be used when configuring a complete cluster.
+    type: str
+  domain:
+    description:
+    - The domain name to be used for the the ESXi host.
+    type: str
+  dns_servers:
+    description:
+    - A list of DNS servers to be used.
+    - The order of the DNS servers is important as they are used consecutively in order.
+    type: list
+  search_domains:
+    description:
+    - A list of domains to be searched through by the resolver.
+    type: list
+  verbose:
+    description:
+    - Verbose output of the DNS server configuration change.
+    - Explains if an DNS server was added, removed, or if the DNS server sequence was changed.
+    type: bool
+    default: false
+  esxi_hostname:
+    description:
+    - Name of the host system to work with.
+    - This parameter is required if C(cluster_name) is not specified and you connect to a vCenter.
+    - Cannot be used when you connect directly to an ESXi host.
+    type: str
+  cluster_name:
+    description:
+    - Name of the cluster from which all host systems will be used.
+    - This parameter is required if C(esxi_hostname) is not specified and you connect to a vCenter.
+    - Cannot be used when you connect directly to an ESXi host.
+    type: str
+extends_documentation_fragment: vmware.documentation
+'''
+
+EXAMPLES = r'''
+- name: Configure DNS for an ESXi host
+  vmware_host_dns:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi_hostname }}'
+    host_name: esx01
+    domain: example.local
+    dns_servers:
+      - 192.168.1.10
+      - 192.168.1.11
+    search_domains:
+      - subdomain.example.local
+      - example.local
+  delegate_to: localhost
+
+- name: Configure DNS for all ESXi hosts of a cluster
+  vmware_host_dns:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    cluster_name: '{{ cluster_name }}'
+    domain: example.local
+    dns_servers:
+      - 192.168.1.10
+      - 192.168.1.11
+    search_domains:
+      - subdomain.example.local
+      - example.local
+  delegate_to: localhost
+
+- name: Configure DNS via DHCP for an ESXi host
+  vmware_host_dns:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi_hostname }}'
+    type: dhcp
+    device: vmk0
+  delegate_to: localhost
+'''
+
+RETURN = r'''
+dns_config_result:
+  description: metadata about host system's DNS configuration
+  returned: always
+  type: dict
+  sample: {
+    "esx01.example.local": {
+      "changed": true,
+      "dns_servers_changed": ["192.168.1.12", "192.168.1.13"],
+      "dns_servers": ["192.168.1.10", "192.168.1.11"],
+      "dns_servers_previous": ["192.168.1.10", "192.168.1.11", "192.168.1.12", "192.168.1.13"],
+      "domain": "example.local",
+      "host_name": "esx01",
+      "msg": "DNS servers and Search domains changed",
+      "search_domains_changed": ["subdomain.example.local"],
+      "search_domains": ["subdomain.example.local", "example.local"],
+      "search_domains_previous": ["example.local"],
+    },
+  }
+'''
+
+try:
+    from pyVmomi import vim, vmodl
+except ImportError:
+    pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.vmware import PyVmomi, vmware_argument_spec
+from ansible.module_utils._text import to_native
+
+
+class VmwareHostDNS(PyVmomi):
+    """Class to manage DNS configuration of an ESXi host system"""
+
+    def __init__(self, module):
+        super(VmwareHostDNS, self).__init__(module)
+        self.cluster_name = self.params.get('cluster_name')
+        self.esxi_host_name = self.params.get('esxi_hostname')
+        if self.is_vcenter():
+            if not self.cluster_name and not self.esxi_host_name:
+                self.module.fail_json(
+                    msg="You connected to a vCenter but didn't specify the cluster_name or esxi_hostname you want to configure."
+                )
+        else:
+            if self.cluster_name:
+                self.module.warn(
+                    "You connected directly to an ESXi host, cluster_name will be ignored."
+                )
+            if self.esxi_host_name:
+                self.module.warn(
+                    "You connected directly to an ESXi host, esxi_host_name will be ignored."
+                )
+        self.hosts = self.get_all_host_objs(cluster_name=self.cluster_name, esxi_host_name=self.esxi_host_name)
+        if not self.hosts:
+            self.module.fail_json(msg="Failed to find host system(s).")
+        self.network_type = self.params.get('type')
+        self.vmkernel_device = self.params.get('device')
+        self.host_name = self.params.get('host_name')
+        self.domain = self.params.get('domain')
+        self.dns_servers = self.params.get('dns_servers')
+        self.search_domains = self.params.get('search_domains')
+
+    def ensure(self):
+        """Function to manage DNS configuration of an ESXi host system"""
+        results = dict(changed=False, dns_config_result=dict())
+        verbose = self.module.params.get('verbose', False)
+        host_change_list = []
+        for host in self.hosts:
+            changed = False
+            changed_list = []
+            results['dns_config_result'][host.name] = dict(changed='', msg='')
+
+            host_netstack_config = host.config.network.netStackInstance
+            for instance in host_netstack_config:
+                if instance.key == 'defaultTcpipStack':
+                    netstack_spec = vim.host.NetworkConfig.NetStackSpec()
+                    netstack_spec.operation = 'edit'
+                    netstack_spec.netStackInstance = vim.host.NetStackInstance()
+                    netstack_spec.netStackInstance.key = 'defaultTcpipStack'
+                    dns_config = vim.host.DnsConfig()
+                    results['dns_config_result'][host.name]['dns_config'] = self.network_type
+                    if self.network_type == 'static':
+                        if instance.dnsConfig.dhcp:
+                            results['dns_config_result'][host.name]['domain'] = self.domain
+                            results['dns_config_result'][host.name]['dns_servers'] = self.dns_servers
+                            results['dns_config_result'][host.name]['search_domains'] = self.search_domains
+                            results['dns_config_result'][host.name]['dns_config_previous'] = 'DHCP'
+                            changed = True
+                            changed_list.append("DNS configuration")
+                            dns_config.dhcp = False
+                            dns_config.virtualNicDevice = None
+                            if self.host_name:
+                                dns_config.hostName = self.host_name
+                            else:
+                                dns_config.hostName = instance.dnsConfig.hostName
+                            dns_config.domainName = self.domain
+                            dns_config.address = self.dns_servers
+                            dns_config.searchDomain = self.search_domains
+                        else:
+                            results['dns_config_result'][host.name]['host_name'] = self.host_name
+                            # Check host name
+                            if self.host_name:
+                                if instance.dnsConfig.hostName != self.host_name:
+                                    results['dns_config_result'][host.name]['host_name_previous'] = instance.dnsConfig.hostName
+                                    changed = True
+                                    changed_list.append("Host name")
+                                dns_config.hostName = self.host_name
+                            else:
+                                dns_config.hostName = instance.dnsConfig.hostName
+
+                            # Check domain
+                            results['dns_config_result'][host.name]['domain'] = self.domain
+                            if self.domain:
+                                if instance.dnsConfig.domainName != self.domain:
+                                    results['dns_config_result'][host.name]['domain_previous'] = instance.dnsConfig.domainName
+                                    changed = True
+                                    changed_list.append("Domain")
+                                dns_config.domainName = self.domain
+                            else:
+                                dns_config.domainName = instance.dnsConfig.domainName
+
+                            # Check DNS server(s)
+                            results['dns_config_result'][host.name]['dns_servers'] = self.dns_servers
+                            if self.dns_servers:
+                                if instance.dnsConfig.address != self.dns_servers:
+                                    results['dns_config_result'][host.name]['dns_servers_previous'] = instance.dnsConfig.address
+                                    results['dns_config_result'][host.name]['dns_servers_changed'] = (
+                                        self.get_differt_entries(instance.dnsConfig.address, self.dns_servers)
+                                    )
+                                    changed = True
+                                    # build verbose message
+                                    if verbose:
+                                        dns_servers_verbose_message = self.build_changed_message(
+                                            instance.dnsConfig.address,
+                                            self.dns_servers
+                                        )
+                                    else:
+                                        changed_list.append("DNS servers")
+                                dns_config.address = self.dns_servers
+                            else:
+                                dns_config.address = instance.dnsConfig.address
+
+                            # Check search domain config
+                            results['dns_config_result'][host.name]['search_domains'] = self.search_domains
+                            if self.search_domains:
+                                if instance.dnsConfig.searchDomain != self.search_domains:
+                                    results['dns_config_result'][host.name]['search_domains_previous'] = instance.dnsConfig.searchDomain
+                                    results['dns_config_result'][host.name]['search_domains_changed'] = (
+                                        self.get_differt_entries(instance.dnsConfig.searchDomain, self.search_domains)
+                                    )
+                                    changed = True
+                                    changed_list.append("Search domains")
+                                dns_config.searchDomain = self.search_domains
+                            else:
+                                dns_config.searchDomain = instance.dnsConfig.searchDomain
+                    elif self.network_type == 'dhcp' and not instance.dnsConfig.dhcp:
+                        results['dns_config_result'][host.name]['device'] = self.vmkernel_device
+                        results['dns_config_result'][host.name]['dns_config_previous'] = 'static'
+                        changed = True
+                        changed_list.append("DNS configuration")
+                        dns_config.dhcp = True
+                        dns_config.virtualNicDevice = self.vmkernel_device
+                    netstack_spec.netStackInstance.dnsConfig = dns_config
+                    config = vim.host.NetworkConfig()
+                    config.netStackSpec = [netstack_spec]
+
+            if changed:
+                if self.module.check_mode:
+                    changed_suffix = ' would be changed'
+                else:
+                    changed_suffix = ' changed'
+                if len(changed_list) > 2:
+                    message = ', '.join(changed_list[:-1]) + ', and ' + str(changed_list[-1])
+                elif len(changed_list) == 2:
+                    message = ' and '.join(changed_list)
+                elif len(changed_list) == 1:
+                    message = changed_list[0]
+                if verbose and dns_servers_verbose_message:
+                    if changed_list:
+                        message = message + changed_suffix + '. ' + dns_servers_verbose_message + '.'
+                    else:
+                        message = dns_servers_verbose_message
+                else:
+                    message += changed_suffix
+                results['dns_config_result'][host.name]['changed'] = True
+                host_network_system = host.configManager.networkSystem
+                if not self.module.check_mode:
+                    try:
+                        host_network_system.UpdateNetworkConfig(config, 'modify')
+                    except vim.fault.AlreadyExists:
+                        self.module.fail_json(
+                            msg="Network entity specified in the configuration already exist on host '%s'" % host.name
+                        )
+                    except vim.fault.NotFound:
+                        self.module.fail_json(
+                            msg="Network entity specified in the configuration doesn't exist on host '%s'" % host.name
+                        )
+                    except vim.fault.ResourceInUse:
+                        self.module.fail_json(msg="Resource is in use on host '%s'" % host.name)
+                    except vmodl.fault.InvalidArgument:
+                        self.module.fail_json(
+                            msg="An invalid parameter is passed in for one of the networking objects for host '%s'" %
+                            host.name
+                        )
+                    except vmodl.fault.NotSupported as not_supported:
+                        self.module.fail_json(
+                            msg="Operation isn't supported for the instance on '%s' : %s" %
+                            (host.name, to_native(not_supported.msg))
+                        )
+                    except vim.fault.HostConfigFault as config_fault:
+                        self.module.fail_json(
+                            msg="Failed to configure TCP/IP stacks for host '%s' due to : %s" %
+                            (host.name, to_native(config_fault.msg))
+                        )
+            else:
+                results['dns_config_result'][host.name]['changed'] = False
+                message = 'All settings are already configured'
+            results['dns_config_result'][host.name]['msg'] = message
+
+            host_change_list.append(changed)
+
+        if any(host_change_list):
+            results['changed'] = True
+        self.module.exit_json(**results)
+
+    def build_changed_message(self, dns_servers_configured, dns_servers_new):
+        """Build changed message"""
+        check_mode = 'would be ' if self.module.check_mode else ''
+        # get differences
+        add = self.get_not_in_list_one(dns_servers_new, dns_servers_configured)
+        remove = self.get_not_in_list_one(dns_servers_configured, dns_servers_new)
+        diff_servers = list(dns_servers_configured)
+        if add and remove:
+            for server in add:
+                diff_servers.append(server)
+            for server in remove:
+                diff_servers.remove(server)
+            if dns_servers_new != diff_servers:
+                message = (
+                    "DNS server %s %sadded and %s %sremoved and the server sequence %schanged as well" %
+                    (self.array_to_string(add), check_mode, self.array_to_string(remove), check_mode, check_mode)
+                )
+            else:
+                if dns_servers_new != dns_servers_configured:
+                    message = (
+                        "DNS server %s %sreplaced with %s" %
+                        (self.array_to_string(remove), check_mode, self.array_to_string(add))
+                    )
+                else:
+                    message = (
+                        "DNS server %s %sremoved and %s %sadded" %
+                        (self.array_to_string(remove), check_mode, self.array_to_string(add), check_mode)
+                    )
+        elif add:
+            for server in add:
+                diff_servers.append(server)
+            if dns_servers_new != diff_servers:
+                message = (
+                    "DNS server %s %sadded and the server sequence %schanged as well" %
+                    (self.array_to_string(add), check_mode, check_mode)
+                )
+            else:
+                message = "DNS server %s %sadded" % (self.array_to_string(add), check_mode)
+        elif remove:
+            for server in remove:
+                diff_servers.remove(server)
+            if dns_servers_new != diff_servers:
+                message = (
+                    "DNS server %s %sremoved and the server sequence %schanged as well" %
+                    (self.array_to_string(remove), check_mode, check_mode)
+                )
+            else:
+                message = "DNS server %s %sremoved" % (self.array_to_string(remove), check_mode)
+        else:
+            message = "DNS server sequence %schanged" % check_mode
+
+        return message
+
+    @staticmethod
+    def get_not_in_list_one(list1, list2):
+        """Return entries that ore not in list one"""
+        return [x for x in list1 if x not in set(list2)]
+
+    @staticmethod
+    def array_to_string(array):
+        """Return string from array"""
+        if len(array) > 2:
+            string = (
+                ', '.join("'{0}'".format(element) for element in array[:-1]) + ', and '
+                + "'{0}'".format(str(array[-1]))
+            )
+        elif len(array) == 2:
+            string = ' and '.join("'{0}'".format(element) for element in array)
+        elif len(array) == 1:
+            string = "'{0}'".format(array[0])
+        return string
+
+    @staticmethod
+    def get_differt_entries(list1, list2):
+        """Return different entries of two lists"""
+        return [a for a in list1 + list2 if (a not in list1) or (a not in list2)]
+
+
+def main():
+    """Main"""
+    argument_spec = vmware_argument_spec()
+    argument_spec.update(
+        type=dict(required=True, type='str', choices=['dhcp', 'static']),
+        device=dict(type='str'),
+        host_name=dict(required=False, type='str'),
+        domain=dict(required=False, type='str'),
+        dns_servers=dict(required=False, type='list'),
+        search_domains=dict(required=False, type='list'),
+        esxi_hostname=dict(required=False, type='str'),
+        cluster_name=dict(required=False, type='str'),
+        verbose=dict(type='bool', default=False, required=False)
+    )
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        required_if=[
+            ['type', 'dhcp', ['device']],
+        ],
+        mutually_exclusive=[
+            ['cluster_name', 'host_name'],
+            ['cluster_name', 'esxi_host_name'],
+            ['static', 'device'],
+            ['dhcp', 'host_name'],
+            ['dhcp', 'domain'],
+            ['dhcp', 'dns_servers'],
+            ['dhcp', 'search_domains'],
+        ],
+        supports_check_mode=True
+    )
+
+    dns = VmwareHostDNS(module)
+    dns.ensure()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/test/integration/targets/vmware_host_dns/aliases b/test/integration/targets/vmware_host_dns/aliases
new file mode 100644
index 00000000000..3eede2cbf01
--- /dev/null
+++ b/test/integration/targets/vmware_host_dns/aliases
@@ -0,0 +1,3 @@
+cloud/vcenter
+shippable/vcenter/group1
+needs/target/prepare_vmware_tests
diff --git a/test/integration/targets/vmware_host_dns/tasks/dhcp.yml b/test/integration/targets/vmware_host_dns/tasks/dhcp.yml
new file mode 100644
index 00000000000..627580fca9c
--- /dev/null
+++ b/test/integration/targets/vmware_host_dns/tasks/dhcp.yml
@@ -0,0 +1,303 @@
+- name: Tests when the hosts have a DHCP DNS config
+  block:
+    # Testcase 0001: Ensure DNS config directly on the host is idempotent for DHCP
+    - name: Ensure DHCP config is idempotent when done directly on the host
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'dhcp'
+        device: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['virtual_nic_device'] }}"
+      register: vmware_host_dns_result_0001
+
+    - name: Ensure DNS config wasn't changed
+      assert:
+        that:
+          - vmware_host_dns_result_0001 is not changed
+
+    # Testcase 0002: Change DNS config directly on the host from DHCP to static
+    - name: Change DNS config from DHCP to static directly on the host
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['host_name'] }}"
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0002
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0002 is changed
+
+    # Testcase 0003: Ensure DNS config directly on the host is idempotent for static
+    - name: Ensure static DNS config is idempotent when done directly on the host
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['host_name'] }}"
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0003
+
+    - name: Ensure DNS config wasn't changed
+      assert:
+        that:
+          - vmware_host_dns_result_0003 is not changed
+
+    # Testcase 0004: Ensure changing the hostname directly on the host works
+    - name: Ensure changing the hostname directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0004
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0004 is changed
+
+    # Testcase 0005: Ensure changing the domain directly on the host works
+    - name: Ensure changing the domain directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: new.domain
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0005
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0005 is changed
+
+    # Testcase 0006: Ensure changing the DNS servers directly on the host works
+    - name: Ensure changing the domain directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0006
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0006 is changed
+
+    # Testcase 0007: Ensure changing the search domain directly on the host works
+    - name: Ensure changing the domain directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains:
+          - subdomain.example.local
+          - example.local
+      register: vmware_host_dns_result_0007
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0007 is changed
+
+    # Testcase 0008: Change DNS config directly on the host from static to DHCP
+    - name: Change DNS config from static to DHCP directly on the host
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'dhcp'
+        device: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['virtual_nic_device'] }}"
+      register: vmware_host_dns_result_0008
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0008 is changed
+ 
+    # Testcase 0009: Ensure DNS config on the cluster is idempotent for DHCP
+    - name: Ensure DHCP config is idempotent when done on the cluster
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'dhcp'
+        device: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['virtual_nic_device'] }}"
+      register: vmware_host_dns_result_0009
+
+    - name: Ensure DNS config wasn't changed
+      assert:
+        that:
+          - vmware_host_dns_result_0009 is not changed
+
+    # Testcase 0010: Ensure changing DNS config on the cluster from DHCP to static works
+    - name: Ensure changing DNS config on the cluster from DHCP to static works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0010
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0010 is changed
+
+    # Testcase 0011: Ensure DNS config on the cluster is idempotent for static
+    - name: Ensure static DNS config is idempotent when done on the cluster
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0011
+
+    - name: Ensure DNS config wasn't changed
+      assert:
+        that:
+          - vmware_host_dns_result_0011 is not changed
+
+    # Testcase 0012: Ensure changing the domain on the cluster works
+    - name: Ensure changing the domain on the cluster works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: new.domain
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0012
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0012 is changed
+
+    # Testcase 0013: Ensure changing the DNS servers on the cluster works
+    - name: Ensure changing the DNS servers on the cluster works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0013
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0013 is changed
+
+    # Testcase 0014: Ensure changing the search domains on the cluster works
+    - name: Ensure changing the search domains on the cluster works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains:
+          - subdomain.example.local
+          - example.local
+      register: vmware_host_dns_result_0014
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0014 is changed
+
+    # Testcase 0015: Ensure changing DNS config on the cluster from static to DHCP works
+    - name: Ensure changing DNS config on the cluster from static to DHCP works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'dhcp'
+        device: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['virtual_nic_device'] }}"
+      register: vmware_host_dns_result_0015
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0015 is changed
+
+  always:
+    # Revert to original DNS configuration
+    - name: Revert to original DNS configuration
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        esxi_hostname: "{{ item }}"
+        validate_certs: False
+        type: 'dhcp'
+        device: "{{ dns['results'][index]['hosts_dns_info'][esxi1]['virtual_nic_device'] }}"
+      loop: "{{ esxi_hosts }}"
+      loop_control:
+        index_var: index
diff --git a/test/integration/targets/vmware_host_dns/tasks/main.yml b/test/integration/targets/vmware_host_dns/tasks/main.yml
new file mode 100644
index 00000000000..2c247c515d6
--- /dev/null
+++ b/test/integration/targets/vmware_host_dns/tasks/main.yml
@@ -0,0 +1,28 @@
+# Test code for the vmware_host_dns module.
+# Copyright: (c) 2019, Mario Lenz <m@riolenz.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# This tests assume that all ESXi hosts either have a static or a dynamic DNS
+# configuration. They probably will fail if on host is 'static' and the other
+# is 'dhcp' configured.
+- when: vcsim is not defined
+  block:
+    - import_role:
+        name: prepare_vmware_tests
+      vars:
+        setup_attach_host: true
+
+    - name: Gather DNS facts about ESXi Host
+      vmware_host_dns_info:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        esxi_hostname: "{{ item }}"
+        validate_certs: False
+      loop: "{{ esxi_hosts }}"
+      register: dns
+
+    - include_tasks: dhcp.yml
+      when: dns['results'][0]['hosts_dns_info'][esxi1]['dhcp']
+    - include_tasks: static.yml
+      when: not dns['results'][0]['hosts_dns_info'][esxi1]['dhcp']
diff --git a/test/integration/targets/vmware_host_dns/tasks/static.yml b/test/integration/targets/vmware_host_dns/tasks/static.yml
new file mode 100644
index 00000000000..610dfe26f26
--- /dev/null
+++ b/test/integration/targets/vmware_host_dns/tasks/static.yml
@@ -0,0 +1,215 @@
+- name: Tests when the hosts have a static DNS config
+  block:
+    # Testcase 0001: Ensure DNS config directly on the host is idempotent for static
+    - name: Ensure static DNS config is idempotent when done directly on the host
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['host_name'] }}"
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0001
+
+    - name: Ensure DNS config wasn't changed
+      assert:
+        that:
+          - vmware_host_dns_result_0001 is not changed
+
+    # Testcase 0002: Ensure changing the hostname directly on the host works
+    - name: Ensure changing the hostname directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0002
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0002 is changed
+
+    # Testcase 0003: Ensure changing the domain directly on the host works
+    - name: Ensure changing the domain directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: new.domain
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0003
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0003 is changed
+
+    # Testcase 0004: Ensure changing the DNS servers directly on the host works
+    - name: Ensure changing the domain directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0004
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0004 is changed
+
+    # Testcase 0005: Ensure changing the search domain directly on the host works
+    - name: Ensure changing the domain directly on the host works
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: newname
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains:
+          - subdomain.example.local
+          - example.local
+      register: vmware_host_dns_result_0005
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0005 is changed
+
+    # Revert to original DNS configuration
+    - name: Revert to original DNS configuration
+      vmware_host_dns:
+        hostname: '{{ esxi1 }}'
+        username: '{{ esxi_user }}'
+        password: '{{ esxi_password }}'
+        validate_certs: False
+        type: 'static'
+        host_name: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['host_name'] }}"
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+
+    # Testcase 0006: Ensure DNS config on the cluster is idempotent for static
+    - name: Ensure static DNS config is idempotent when done on the cluster
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0006
+
+    - name: Ensure DNS config wasn't changed
+      assert:
+        that:
+          - vmware_host_dns_result_0006 is not changed
+
+    # Testcase 0007: Ensure changing the domain on the cluster works
+    - name: Ensure changing the domain on the cluster works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: new.domain
+        dns_servers: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['ip_address'] }}"
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0007
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0007 is changed
+
+    # Testcase 0008: Ensure changing the DNS servers on the cluster works
+    - name: Ensure changing the DNS servers on the cluster works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains: "{{ dns['results'][0]['hosts_dns_info'][esxi1]['search_domain'] }}"
+      register: vmware_host_dns_result_0008
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0008 is changed
+
+    # Testcase 0009: Ensure changing the search domains on the cluster works
+    - name: Ensure changing the search domains on the cluster works
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        cluster_name: "{{ ccr1 }}"
+        validate_certs: False
+        type: 'static'
+        domain: new.domain
+        dns_servers:
+          - 1.2.3.4
+          - 5.6.7.8
+        search_domains:
+          - subdomain.example.local
+          - example.local
+      register: vmware_host_dns_result_0009
+
+    - name: Ensure DNS config was changed
+      assert:
+        that:
+          - vmware_host_dns_result_0009 is changed
+
+  always:
+    # Revert to original DNS configuration
+    - name: Revert to original DNS configuration
+      vmware_host_dns:
+        hostname: '{{ vcenter_hostname }}'
+        username: '{{ vcenter_username }}'
+        password: '{{ vcenter_password }}'
+        esxi_hostname: "{{ item }}"
+        validate_certs: False
+        type: 'static'
+        host_name: "{{ dns['results'][index]['hosts_dns_info'][item]['host_name'] }}"
+        domain: "{{ dns['results'][index]['hosts_dns_info'][item]['domain_name'] }}"
+        dns_servers: "{{ dns['results'][index]['hosts_dns_info'][item]['ip_address'] }}"
+        search_domains: "{{ dns['results'][index]['hosts_dns_info'][item]['search_domain'] }}"
+      loop: "{{ esxi_hosts }}"
+      loop_control:
+        index_var: index