Repository: ansible-collections/community.vmware

Issue:
vSphere API breaks vmware_host_ntp module when a host is disconnected
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Explain the problem briefly below -->
Running the vmware_host_ntp module against a vSphere 7.0.1+ server results in a failure and traceback.

This module works against vSphere 7.0.0 or if I point directly at the ESXi host instead of going through vSphere.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
vmware_host_ntp

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
ansible 2.9.15
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/e393260/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.6.8 (default, Aug 18 2020, 08:33:21) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below
empty
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
Red Hat Enterprise Linux release 8.3 (Ootpa)

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
I have tested this with the stock Ansible 2.9.15 module and the Galaxy Community module. Both fail the same way. I'll list the Community method I used.

1. ansible-galaxy collection install community.vmware
2. ansible-playbook -v ./test.yml

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- hosts: localhost
  gather_facts: false
  tasks:
  - name: Configure NTP
    community.vmware.vmware_host_ntp:
      hostname: "vcenter.host.com"
      username: "administrator@vcenter.host.com"
      password: "<password>"
      cluster_name: "Cluster"
      state: present
      ntp_servers:
        - 1.2.3.4
        - 5.6.7.8
        - 9.10.11.12
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
Configured NTP servers.

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes -->
```paste below
The full traceback is:
Traceback (most recent call last):
  File "/home/foouser/.ansible/tmp/ansible-tmp-1606854652.469246-309393-280139637752886/AnsiballZ_vmware_host_ntp.py", line 102, in <module>
    _ansiballz_main()
  File "/home/foouser/.ansible/tmp/ansible-tmp-1606854652.469246-309393-280139637752886/AnsiballZ_vmware_host_ntp.py", line 94, in _ansiballz_main
    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)
  File "/home/foouser/.ansible/tmp/ansible-tmp-1606854652.469246-309393-280139637752886/AnsiballZ_vmware_host_ntp.py", line 40, in invoke_module
    runpy.run_module(mod_name='ansible_collections.community.vmware.plugins.modules.vmware_host_ntp', init_globals=None, run_name='__main__', alter_sys=True)
  File "/usr/lib64/python3.6/runpy.py", line 205, in run_module
    return _run_module_code(code, init_globals, run_name, mod_spec)
  File "/usr/lib64/python3.6/runpy.py", line 96, in _run_module_code
    mod_name, mod_spec, pkg_name, script_name)
  File "/usr/lib64/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py", line 387, in <module>
  File "/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py", line 383, in main
  File "/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py", line 206, in check_host_state
  File "/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py", line 262, in check_ntp_servers
AttributeError: 'NoneType' object has no attribute 'ntpConfig'
fatal: [localhost]: FAILED! => {
    "changed": false,
    "module_stderr": "Traceback (most recent call last):\n  File \"/home/foouser/.ansible/tmp/ansible-tmp-1606854652.469246-309393-280139637752886/AnsiballZ_vmware_host_ntp.py\", line 102, in <module>\n    _ansiballz_main()\n  File \"/home/foouser/.ansible/tmp/ansible-tmp-1606854652.469246-309393-280139637752886/AnsiballZ_vmware_host_ntp.py\", line 94, in _ansiballz_main\n    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\n  File \"/home/foouser/.ansible/tmp/ansible-tmp-1606854652.469246-309393-280139637752886/AnsiballZ_vmware_host_ntp.py\", line 40, in invoke_module\n    runpy.run_module(mod_name='ansible_collections.community.vmware.plugins.modules.vmware_host_ntp', init_globals=None, run_name='__main__', alter_sys=True)\n  File \"/usr/lib64/python3.6/runpy.py\", line 205, in run_module\n    return _run_module_code(code, init_globals, run_name, mod_spec)\n  File \"/usr/lib64/python3.6/runpy.py\", line 96, in _run_module_code\n    mod_name, mod_spec, pkg_name, script_name)\n  File \"/usr/lib64/python3.6/runpy.py\", line 85, in _run_code\n    exec(code, run_globals)\n  File \"/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py\", line 387, in <module>\n  File \"/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py\", line 383, in main\n  File \"/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py\", line 206, in check_host_state\n  File \"/tmp/ansible_community.vmware.vmware_host_ntp_payload_7v10tmq0/ansible_community.vmware.vmware_host_ntp_payload.zip/ansible_collections/community/vmware/plugins/modules/vmware_host_ntp.py\", line 262, in check_ntp_servers\nAttributeError: 'NoneType' object has no attribute 'ntpConfig'\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 1
}
```



Discussion:
Sorry, I can't confirm this. Works for me, both with Ansible 2.9.14 and Ansible 2.10.1 (I think that includes version 1.2.0 of this collection).

Is this a newly installed ESXi vSphere 7.0.1+? Or did `vmware_host_ntp` work with ESXi vSphere 7.0.0, you updated the host and it stoped working?
Ansible 2.10.4 works for me, too. Both with pyvmomi 7.0 and 7.0.1.
The vSphere host was originally 7.0.0 and was working and then upgraded to 7.0.1. The bare metal ESXi host is still running 7.0.0.

I'm using pyvmomi 7.0.1 from EPEL.
> The vSphere host was originally 7.0.0 and was working and then upgraded to 7.0.1. The bare metal ESXi host is still running 7.0.0.

All our vCenters are now on 7.0U1; most of our ESXi hosts are still 6.7U3 although two clusters (managed by two different vCenters) are already running ESXi 7.0U1. I still can't reproduce your issue. Unfortunatelly, I can't test the combination vCenter 7.0U1 / ESXi 7.0GA at the moment.
 
> I'm using pyvmomi 7.0.1 from EPEL.

What a pity, I was hoping for a problem with an old pyvmomi version.

@Tomorrow9 @sky-joker Can you please have a look at this? Can you reproduce the issue?

It looks like the `'NoneType' object has no attribute 'ntpConfig'` error is here:

https://github.com/ansible-collections/community.vmware/blob/82eff8a728a82192417b43c6d084b61eaf251543/plugins/modules/vmware_host_ntp.py#L262

Interesting. Although `ntpConfig` is documented as optional ([Need not be set ](https://vdc-download.vmware.com/vmwb-repository/dcr-public/a5f4000f-1ea8-48a9-9221-586adff3c557/7ff50256-2cf2-45ea-aacd-87d231ab1ac7/vim.host.DateTimeInfo.html)) I've never seen this unset.
I do need to mention that this vCenter has 2 ESXi hosts and one of the two is offline at the moment (intentionally). The offline host was only made offline after the 7.0.1 upgrade so it may have nothing to do with versions. Could vCenter be sending back empty NTP info for the offline host? I haven't taken a capture of the returned data to confirm.
> I do need to mention that this vCenter has 2 ESXi hosts and one of the two is offline at the moment (intentionally). The offline host was only made offline after the 7.0.1 upgrade so it may have nothing to do with versions. Could vCenter be sending back empty NTP info for the offline host?

That might well be the case. But even if is this isn't a problem with 7.0.1, I consider this an issue: The module should skip hosts that are offline or fail, but it shouldn't crash.


> That might well be the case. But even if is this isn't a problem with 7.0.1, I consider this an issue: The module should skip hosts that are offline or fail, but it shouldn't crash.

I removed the offline/disconnected host and the module now works.

Resolving patch:
diff --git a/plugins/modules/vmware_host_ntp.py b/plugins/modules/vmware_host_ntp.py
--- a/plugins/modules/vmware_host_ntp.py
+++ b/plugins/modules/vmware_host_ntp.py
@@ -203,52 +203,56 @@ def check_host_state(self):
         changed = False
         for host in self.hosts:
             self.results[host.name] = dict()
-            ntp_servers_configured, ntp_servers_to_change = self.check_ntp_servers(host=host)
-            # add/remove NTP servers
-            if self.desired_state:
-                self.results[host.name]['state'] = self.desired_state
-                if ntp_servers_to_change:
-                    self.results[host.name]['ntp_servers_changed'] = ntp_servers_to_change
-                    operation = 'add' if self.desired_state == 'present' else 'delete'
-                    new_ntp_servers = self.update_ntp_servers(
-                        host=host,
-                        ntp_servers_configured=ntp_servers_configured,
-                        ntp_servers_to_change=ntp_servers_to_change,
-                        operation=operation
-                    )
-                    self.results[host.name]['ntp_servers_current'] = new_ntp_servers
-                    self.results[host.name]['changed'] = True
-                    change_list.append(True)
+            if host.runtime.connectionState == "connected":
+                ntp_servers_configured, ntp_servers_to_change = self.check_ntp_servers(host=host)
+                # add/remove NTP servers
+                if self.desired_state:
+                    self.results[host.name]['state'] = self.desired_state
+                    if ntp_servers_to_change:
+                        self.results[host.name]['ntp_servers_changed'] = ntp_servers_to_change
+                        operation = 'add' if self.desired_state == 'present' else 'delete'
+                        new_ntp_servers = self.update_ntp_servers(
+                            host=host,
+                            ntp_servers_configured=ntp_servers_configured,
+                            ntp_servers_to_change=ntp_servers_to_change,
+                            operation=operation
+                        )
+                        self.results[host.name]['ntp_servers_current'] = new_ntp_servers
+                        self.results[host.name]['changed'] = True
+                        change_list.append(True)
+                    else:
+                        self.results[host.name]['ntp_servers_current'] = ntp_servers_configured
+                        if self.verbose:
+                            self.results[host.name]['msg'] = (
+                                "NTP servers already added" if self.desired_state == 'present'
+                                else "NTP servers already removed"
+                            )
+                        self.results[host.name]['changed'] = False
+                        change_list.append(False)
+                # overwrite NTP servers
                 else:
-                    self.results[host.name]['ntp_servers_current'] = ntp_servers_configured
-                    if self.verbose:
-                        self.results[host.name]['msg'] = (
-                            "NTP servers already added" if self.desired_state == 'present'
-                            else "NTP servers already removed"
+                    self.results[host.name]['ntp_servers'] = self.ntp_servers
+                    if ntp_servers_to_change:
+                        self.results[host.name]['ntp_servers_changed'] = self.get_differt_entries(
+                            ntp_servers_configured,
+                            ntp_servers_to_change
                         )
-                    self.results[host.name]['changed'] = False
-                    change_list.append(False)
-            # overwrite NTP servers
+                        self.update_ntp_servers(
+                            host=host,
+                            ntp_servers_configured=ntp_servers_configured,
+                            ntp_servers_to_change=ntp_servers_to_change,
+                            operation='overwrite'
+                        )
+                        self.results[host.name]['changed'] = True
+                        change_list.append(True)
+                    else:
+                        if self.verbose:
+                            self.results[host.name]['msg'] = "NTP servers already configured"
+                        self.results[host.name]['changed'] = False
+                        change_list.append(False)
             else:
-                self.results[host.name]['ntp_servers'] = self.ntp_servers
-                if ntp_servers_to_change:
-                    self.results[host.name]['ntp_servers_changed'] = self.get_differt_entries(
-                        ntp_servers_configured,
-                        ntp_servers_to_change
-                    )
-                    self.update_ntp_servers(
-                        host=host,
-                        ntp_servers_configured=ntp_servers_configured,
-                        ntp_servers_to_change=ntp_servers_to_change,
-                        operation='overwrite'
-                    )
-                    self.results[host.name]['changed'] = True
-                    change_list.append(True)
-                else:
-                    if self.verbose:
-                        self.results[host.name]['msg'] = "NTP servers already configured"
-                    self.results[host.name]['changed'] = False
-                    change_list.append(False)
+                self.results[host.name]['changed'] = False
+                self.results[host.name]['msg'] = "Host %s is disconnected and cannot be changed." % host.name
 
         if any(change_list):
             changed = True


Test patch: