Repository: ansible-collections/community.vmware

Issue:
Configure SNMP for given ESXi hosts via vCenter
##### SUMMARY
Configure SNMP for given ESXi hosts via vCenter

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
vmware_host_snmp

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->
Desire to connect to any/all ESXi hosts ONLY via vCenter connection.  Lockdown enabled/SSH disabled.
The SNMP configuration is the first module I've encountered that doesn't support the `esxi_hostname` parameter.

Would like to be able to configure SNMP for ESXi hosts via this Ansible module without needing to connect to each ESXi host.
Would also be nice to configure/support SNMPv3 as well.  

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Enable and configure SNMP system contact and location
  community.vmware.vmware_host_snmp:
    hostname: '{{ vcenter_hostname }}'
    username: '{{ vcenter_username }}'
    password: '{{ vcenter_password }}'
    esxi_hostname: '{{ esxi_hostname }}'
    sys_contact: "admin@testemail.com"
    sys_location: "Austin, USA"
    state: enabled
  delegate_to: localhost
```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:

Resolving patch:
diff --git a/plugins/modules/vmware_host_snmp.py b/plugins/modules/vmware_host_snmp.py
--- a/plugins/modules/vmware_host_snmp.py
+++ b/plugins/modules/vmware_host_snmp.py
@@ -17,10 +17,25 @@
 - This module can be used to configure the embedded SNMP agent on an ESXi host.
 author:
 - Christian Kotte (@ckotte)
+- Alexander Nikitin (@ihumster)
 notes:
 - You need to reset the agent (to factory defaults) if you want to clear all community strings, trap targets, or filters
 - SNMP v3 configuration isn't implemented yet
 options:
+  cluster_name:
+    description:
+    - Name of cluster.
+    - All host system from given cluster used to manage SNMP agent.
+    - Required parameter, if C(esxi_hostname) is not set.
+    type: str
+    version_added: '3.11.0'
+  esxi_hostname:
+    description:
+    - List of ESXi hostname to manage SNMP agent.
+    - Required parameter, if C(cluster_name) is not set.
+    type: list
+    elements: str
+    version_added: '3.11.0'
   state:
     description:
         - Enable, disable, or reset the SNMP agent.
@@ -84,7 +99,7 @@
 '''
 
 EXAMPLES = r'''
-- name: Enable and configure SNMP community
+- name: Enable and configure SNMP community on standalone ESXi host
   community.vmware.vmware_host_snmp:
     hostname: '{{ esxi_hostname }}'
     username: '{{ esxi_username }}'
@@ -93,11 +108,12 @@
     state: enabled
   delegate_to: localhost
 
-- name: Configure SNMP traps and filters
+- name: Configure SNMP traps and filters on cluster
   community.vmware.vmware_host_snmp:
-    hostname: '{{ esxi_hostname }}'
-    username: '{{ esxi_username }}'
-    password: '{{ esxi_password }}'
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    cluster_name: '{{ cluster_name }}'
     community: [ test ]
     trap_targets:
       - hostname: 192.168.1.100
@@ -112,17 +128,18 @@
     state: enabled
   delegate_to: localhost
 
-- name: Enable and configure SNMP system contact and location
+- name: Enable and configure SNMP system contact and location on simple ESXi host in vCenter
   community.vmware.vmware_host_snmp:
-    hostname: '{{ esxi_hostname }}'
-    username: '{{ esxi_username }}'
-    password: '{{ esxi_password }}'
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi_hostname }}'
     sys_contact: "admin@testemail.com"
     sys_location: "Austin, USA"
     state: enabled
   delegate_to: localhost
 
-- name: Disable SNMP
+- name: Disable SNMP on standalone ESXi host
   community.vmware.vmware_host_snmp:
     hostname: '{{ esxi_hostname }}'
     username: '{{ esxi_username }}'
@@ -137,22 +154,34 @@
     returned: always
     type: dict
     sample: {
-        "esxi01": {
-            "changed": false,
-            "community": ["test"],
+        "changed": true,
+        "esx01.example.local": {
+            "changed": true,
+            "community": [
+                "test"
+            ],
+            "community_previous": [],
             "hw_source": "indications",
-            "msg": "SNMP already configured properly",
+            "log_level": "info",
+            "log_level_previous": "warning",
+            "msg": "SNMP state, community list, log level, sys contact, and sys location changed",
             "port": 161,
             "state": "enabled",
+            "state_previous": "disabled",
+            "sys_contact_previous": "",
+            "sys_location_previous": "",
+            "trap_filter": null,
             "trap_targets": []
         },
+        "failed": false
     }
 '''
 
 try:
     from pyVmomi import vim
+    HAS_PYVMOMI = True
 except ImportError:
-    pass
+    HAS_PYVMOMI = False
 
 from ansible.module_utils.basic import AnsibleModule
 from ansible_collections.community.vmware.plugins.module_utils.vmware import PyVmomi, vmware_argument_spec, find_obj
@@ -164,20 +193,34 @@ class VmwareHostSnmp(PyVmomi):
 
     def __init__(self, module):
         super(VmwareHostSnmp, self).__init__(module)
+        self.results = {"changed": False}
+        self.changed = False
+        self.hosts = None
 
         if self.is_vcenter():
-            self.module.fail_json(
-                msg="You have to connect directly to the ESXi host. "
-                "It's not possible to configure SNMP through a vCenter connection."
-            )
+            esxi_hostname = self.params['esxi_hostname']
+            cluster_name = self.params['cluster_name']
+            if all([esxi_hostname, cluster_name]):
+                self.module.fail_json(
+                    msg="Only one of parameters [cluster_name, esxi_hostname] is required"
+                )
+            else:
+                if cluster_name:
+                    self.hosts = self.get_all_host_objs(cluster_name=cluster_name)
+                    if self.hosts is None:
+                        self.module.fail_json(msg="Failed to find host system in cluster %s." % cluster_name)
+                if esxi_hostname:
+                    self.hosts = self.get_all_host_objs(esxi_host_name=esxi_hostname)
+                    if self.hosts is None:
+                        self.module.fail_json(msg="Failed to find host system.")
         else:
-            self.host = find_obj(self.content, [vim.HostSystem], None)
-            if self.host is None:
-                self.module.fail_json(msg="Failed to find host system.")
+            host = find_obj(self.content, [vim.HostSystem], None)
+            self.hosts = list()
+            self.hosts.append(host)
 
     def ensure(self):
         """Manage SNMP configuration for an ESXi host system"""
-        results = dict(changed=False, result=dict())
+        # results = dict(changed=False, result=dict())
         snmp_state = self.params.get('state')
         snmp_port = self.params.get('snmp_port')
         community = self.params.get('community')
@@ -191,271 +234,273 @@ def ensure(self):
         event_filter = None
         if trap_filter:
             event_filter = ';'.join(trap_filter)
-        changed = False
         reset_hint = None
-        changed_list = []
-        results = dict(msg='')
 
-        snmp_system = self.host.configManager.snmpSystem
-        if snmp_system:
-            if snmp_system.configuration:
-                snmp_config_spec = snmp_system.configuration
-            else:
-                self.module.fail_json(msg="SNMP agent configuration isn't supported on the ESXi host")
-        else:
-            self.module.fail_json(msg="SNMP system isn't available on the ESXi host")
-
-        # Check state
-        results['state'] = snmp_state
-        if snmp_state == 'reset':
-            changed = True
-            # Get previous config
-            if snmp_config_spec.enabled:
-                results['state_previous'] = 'enabled'
+        for host in self.hosts:
+            changed = False
+            changed_list = []
+            self.results[host.name] = dict(changed=False, msg='')
+            snmp_system = host.configManager.snmpSystem
+            if snmp_system:
+                if snmp_system.configuration:
+                    snmp_config_spec = snmp_system.configuration
+                else:
+                    self.module.fail_json(msg="SNMP agent configuration isn't supported on the ESXi host")
             else:
-                results['state_previous'] = 'disabled'
-            results['port_previous'] = snmp_config_spec.port
-            results['community_previous'] = snmp_config_spec.readOnlyCommunities
-            results['trap_targets_previous'] = self.get_previous_targets(snmp_config_spec.trapTargets)
-            for option in snmp_config_spec.option:
-                if option.key == 'EnvEventSource' and option.value != hw_source:
-                    results['hw_source_previous'] = option.value
-                if option.key == 'loglevel' and option.value != hw_source:
-                    results['log_level_previous'] = option.value
-                if option.key == 'EventFilter' and option.value != hw_source:
-                    results['trap_filter_previous'] = option.value.split(';')
-                if option.key == 'syscontact' and option.value != hw_source:
-                    results['syscontact_previous'] = option.value
-                if option.key == 'syslocation' and option.value != hw_source:
-                    results['syslocation_previous'] = option.value
-            # Build factory default config
-            destination = vim.host.SnmpSystem.SnmpConfigSpec.Destination()
-            destination.hostName = ""
-            destination.port = 0
-            destination.community = ""
-            options = []
-            options.append(self.create_option('EnvEventSource', 'indications'))
-            options.append(self.create_option('EventFilter', 'reset'))
-            snmp_config_spec = vim.host.SnmpSystem.SnmpConfigSpec()
-            # Looks like this value is causing the reset
-            snmp_config_spec.readOnlyCommunities = [""]
-            snmp_config_spec.trapTargets = [destination]
-            snmp_config_spec.port = 161
-            snmp_config_spec.enabled = False
-            snmp_config_spec.option = options
-        else:
-            if snmp_state == 'enabled' and not snmp_config_spec.enabled:
-                changed = True
-                changed_list.append("state")
-                results['state_previous'] = 'disabled'
-                snmp_config_spec.enabled = True
-            elif snmp_state == 'disabled' and snmp_config_spec.enabled:
-                changed = True
-                changed_list.append("state")
-                results['state_previous'] = 'enabled'
-                snmp_config_spec.enabled = False
+                self.module.fail_json(msg="SNMP system isn't available on the ESXi host")
 
-            # Check port
-            results['port'] = snmp_port
-            if snmp_config_spec.port != snmp_port:
-                changed = True
-                changed_list.append("port")
-                results['port_previous'] = snmp_config_spec.port
-                snmp_config_spec.port = snmp_port
-
-            # Check read-only community strings
-            results['community'] = community
-            if snmp_config_spec.readOnlyCommunities != community:
+            # Check state
+            self.results[host.name]['state'] = snmp_state
+            if snmp_state == 'reset':
                 changed = True
-                changed_list.append("community list")
-                results['community_previous'] = snmp_config_spec.readOnlyCommunities
-                if community:
-                    snmp_config_spec.readOnlyCommunities = community
+                # Get previous config
+                if snmp_config_spec.enabled:
+                    self.results[host.name]['state_previous'] = 'enabled'
                 else:
-                    # Doesn't work. Need to reset config instead
-                    # snmp_config_spec.readOnlyCommunities = []
-                    reset_hint = True
+                    self.results[host.name]['state_previous'] = 'disabled'
+                self.results[host.name]['port_previous'] = snmp_config_spec.port
+                self.results[host.name]['community_previous'] = snmp_config_spec.readOnlyCommunities
+                self.results[host.name]['trap_targets_previous'] = self.get_previous_targets(snmp_config_spec.trapTargets)
+                for option in snmp_config_spec.option:
+                    if option.key == 'EnvEventSource' and option.value != hw_source:
+                        self.results[host.name]['hw_source_previous'] = option.value
+                    if option.key == 'loglevel' and option.value != hw_source:
+                        self.results[host.name]['log_level_previous'] = option.value
+                    if option.key == 'EventFilter' and option.value != hw_source:
+                        self.results[host.name]['trap_filter_previous'] = option.value.split(';')
+                    if option.key == 'syscontact' and option.value != hw_source:
+                        self.results[host.name]['syscontact_previous'] = option.value
+                    if option.key == 'syslocation' and option.value != hw_source:
+                        self.results[host.name]['syslocation_previous'] = option.value
+                # Build factory default config
+                destination = vim.host.SnmpSystem.SnmpConfigSpec.Destination()
+                destination.hostName = ""
+                destination.port = 0
+                destination.community = ""
+                options = []
+                options.append(self.create_option('EnvEventSource', 'indications'))
+                options.append(self.create_option('EventFilter', 'reset'))
+                snmp_config_spec = vim.host.SnmpSystem.SnmpConfigSpec()
+                # Looks like this value is causing the reset
+                snmp_config_spec.readOnlyCommunities = [""]
+                snmp_config_spec.trapTargets = [destination]
+                snmp_config_spec.port = 161
+                snmp_config_spec.enabled = False
+                snmp_config_spec.option = options
+            else:
+                if snmp_state == 'enabled' and not snmp_config_spec.enabled:
+                    changed = True
+                    changed_list.append("state")
+                    self.results[host.name]['state_previous'] = 'disabled'
+                    snmp_config_spec.enabled = True
+                elif snmp_state == 'disabled' and snmp_config_spec.enabled:
+                    changed = True
+                    changed_list.append("state")
+                    self.results[host.name]['state_previous'] = 'enabled'
+                    snmp_config_spec.enabled = False
+
+                # Check port
+                self.results[host.name]['port'] = snmp_port
+                if snmp_config_spec.port != snmp_port:
+                    changed = True
+                    changed_list.append("port")
+                    self.results[host.name]['port_previous'] = snmp_config_spec.port
+                    snmp_config_spec.port = snmp_port
 
-            # Check trap targets
-            results['trap_targets'] = desired_trap_targets
-            if snmp_config_spec.trapTargets:
-                if desired_trap_targets:
-                    temp_desired_targets = []
-                    # Loop through desired targets
-                    for target in desired_trap_targets:
-                        dest_hostname, dest_port, dest_community = self.check_if_options_are_valid(target)
-                        trap_target_found = False
+                # Check read-only community strings
+                self.results[host.name]['community'] = community
+                if snmp_config_spec.readOnlyCommunities != community:
+                    changed = True
+                    changed_list.append("community list")
+                    self.results[host.name]['community_previous'] = snmp_config_spec.readOnlyCommunities
+                    if community:
+                        snmp_config_spec.readOnlyCommunities = community
+                    else:
+                        # Doesn't work. Need to reset config instead
+                        # snmp_config_spec.readOnlyCommunities = []
+                        reset_hint = True
+
+                # Check trap targets
+                self.results[host.name]['trap_targets'] = desired_trap_targets
+                if snmp_config_spec.trapTargets:
+                    if desired_trap_targets:
+                        temp_desired_targets = []
+                        # Loop through desired targets
+                        for target in desired_trap_targets:
+                            dest_hostname, dest_port, dest_community = self.check_if_options_are_valid(target)
+                            trap_target_found = False
+                            for trap_target in snmp_config_spec.trapTargets:
+                                if trap_target.hostName == dest_hostname:
+                                    if trap_target.port != dest_port or trap_target.community != dest_community:
+                                        changed = True
+                                        changed_list.append("trap target '%s'" % dest_hostname)
+                                    trap_target_found = True
+                                    break
+                            if not trap_target_found:
+                                changed = True
+                                changed_list.append("trap target '%s'" % dest_hostname)
+                            # Build destination and add to temp target list
+                            destination = self.build_destination(dest_hostname, dest_port, dest_community)
+                            temp_desired_targets.append(destination)
+                        # Loop through existing targets to find targets that need to be deleted
                         for trap_target in snmp_config_spec.trapTargets:
-                            if trap_target.hostName == dest_hostname:
-                                if trap_target.port != dest_port or trap_target.community != dest_community:
-                                    changed = True
-                                    changed_list.append("trap target '%s'" % dest_hostname)
-                                trap_target_found = True
-                                break
-                        if not trap_target_found:
-                            changed = True
-                            changed_list.append("trap target '%s'" % dest_hostname)
-                        # Build destination and add to temp target list
-                        destination = self.build_destination(dest_hostname, dest_port, dest_community)
-                        temp_desired_targets.append(destination)
-                    # Loop through existing targets to find targets that need to be deleted
-                    for trap_target in snmp_config_spec.trapTargets:
-                        target_found = False
+                            target_found = False
+                            for target in desired_trap_targets:
+                                if trap_target.hostName == target.get('hostname'):
+                                    target_found = True
+                                    break
+                            if not target_found:
+                                changed = True
+                                changed_list.append("trap target '%s'" % trap_target.hostName)
+                        # Configure trap targets if something has changed
+                        if changed:
+                            self.results[host.name]['trap_targets_previous'] = self.get_previous_targets(snmp_config_spec.trapTargets)
+                            snmp_config_spec.trapTargets = temp_desired_targets
+                    else:
+                        changed = True
+                        changed_list.append("trap targets")
+                        self.results[host.name]['trap_targets_previous'] = self.get_previous_targets(snmp_config_spec.trapTargets)
+                        # Doesn't work. Need to reset config instead
+                        # snmp_config_spec.trapTargets = []
+                        reset_hint = True
+                else:
+                    if desired_trap_targets:
+                        changed = True
+                        changed_list.append("trap targets")
+                        self.results[host.name]['trap_targets_previous'] = None
+                        desired_targets = []
                         for target in desired_trap_targets:
-                            if trap_target.hostName == target.get('hostname'):
-                                target_found = True
-                                break
-                        if not target_found:
+                            dest_hostname, dest_port, dest_community = self.check_if_options_are_valid(target)
+                            destination = self.build_destination(dest_hostname, dest_port, dest_community)
+                            desired_targets.append(destination)
+                        snmp_config_spec.trapTargets = desired_targets
+
+                # Check options
+                self.results[host.name]['hw_source'] = hw_source
+                self.results[host.name]['log_level'] = log_level
+                self.results[host.name]['trap_filter'] = trap_filter
+                event_filter_found = False
+                sys_contact_found = False
+                sys_location_found = False
+                if snmp_config_spec.option:
+                    for option in snmp_config_spec.option:
+                        if option.key == 'EnvEventSource' and option.value != hw_source:
                             changed = True
-                            changed_list.append("trap target '%s'" % trap_target.hostName)
-                    # Configure trap targets if something has changed
-                    if changed:
-                        results['trap_targets_previous'] = self.get_previous_targets(snmp_config_spec.trapTargets)
-                        snmp_config_spec.trapTargets = temp_desired_targets
-                else:
+                            changed_list.append("HW source")
+                            self.results[host.name]['hw_source_previous'] = option.value
+                            option.value = hw_source
+                        if option.key == 'loglevel' and option.value != log_level:
+                            changed = True
+                            changed_list.append("log level")
+                            self.results[host.name]['log_level_previous'] = option.value
+                            option.value = log_level
+                        if option.key == 'EventFilter':
+                            event_filter_found = True
+                            if event_filter and option.value != event_filter:
+                                changed = True
+                                changed_list.append("trap filter")
+                                self.results[host.name]['trap_filter_previous'] = option.value.split(';')
+                                option.value = event_filter
+                        if option.key == 'syscontact':
+                            sys_contact_found = True
+                            if sys_contact is not None and option.value != sys_contact:
+                                changed = True
+                                changed_list.append("sys contact")
+                                self.results[host.name]['sys_contact_previous'] = option.value
+                                option.value = sys_contact
+                        if option.key == 'syslocation':
+                            sys_location_found = True
+                            if sys_location is not None and option.value != sys_location:
+                                changed = True
+                                changed_list.append("sys location")
+                                self.results[host.name]['sys_location_previous'] = option.value
+                                option.value = sys_location
+                if trap_filter and not event_filter_found:
+                    changed = True
+                    changed_list.append("trap filter")
+                    self.results[host.name]['trap_filter_previous'] = []
+                    snmp_config_spec.option.append(self.create_option('EventFilter', event_filter))
+                elif not trap_filter and event_filter_found:
                     changed = True
-                    changed_list.append("trap targets")
-                    results['trap_targets_previous'] = self.get_previous_targets(snmp_config_spec.trapTargets)
+                    changed_list.append("trap filter")
+                    # options = []
+                    for option in snmp_config_spec.option:
+                        if option.key == 'EventFilter':
+                            self.results[host.name]['trap_filter_previous'] = option.value.split(';')
+                        # else:
+                        #     options.append(option)
                     # Doesn't work. Need to reset config instead
-                    # snmp_config_spec.trapTargets = []
+                    # snmp_config_spec.option = options
                     reset_hint = True
-            else:
-                if desired_trap_targets:
+                if sys_contact and not sys_contact_found:
                     changed = True
-                    changed_list.append("trap targets")
-                    results['trap_targets_previous'] = None
-                    desired_targets = []
-                    for target in desired_trap_targets:
-                        dest_hostname, dest_port, dest_community = self.check_if_options_are_valid(target)
-                        destination = self.build_destination(dest_hostname, dest_port, dest_community)
-                        desired_targets.append(destination)
-                    snmp_config_spec.trapTargets = desired_targets
-
-            # Check options
-            results['hw_source'] = hw_source
-            results['log_level'] = log_level
-            results['trap_filter'] = trap_filter
-            event_filter_found = False
-            sys_contact_found = False
-            sys_location_found = False
-            if snmp_config_spec.option:
-                for option in snmp_config_spec.option:
-                    if option.key == 'EnvEventSource' and option.value != hw_source:
-                        changed = True
-                        changed_list.append("HW source")
-                        results['hw_source_previous'] = option.value
-                        option.value = hw_source
-                    if option.key == 'loglevel' and option.value != log_level:
-                        changed = True
-                        changed_list.append("log level")
-                        results['log_level_previous'] = option.value
-                        option.value = log_level
-                    if option.key == 'EventFilter':
-                        event_filter_found = True
-                        if event_filter and option.value != event_filter:
-                            changed = True
-                            changed_list.append("trap filter")
-                            results['trap_filter_previous'] = option.value.split(';')
-                            option.value = event_filter
-                    if option.key == 'syscontact':
-                        sys_contact_found = True
-                        if sys_contact is not None and option.value != sys_contact:
-                            changed = True
-                            changed_list.append("sys contact")
-                            results['sys_contact_previous'] = option.value
-                            option.value = sys_contact
-                    if option.key == 'syslocation':
-                        sys_location_found = True
-                        if sys_location is not None and option.value != sys_location:
-                            changed = True
-                            changed_list.append("sys location")
-                            results['sys_location_previous'] = option.value
-                            option.value = sys_location
-            if trap_filter and not event_filter_found:
-                changed = True
-                changed_list.append("trap filter")
-                results['trap_filter_previous'] = []
-                snmp_config_spec.option.append(self.create_option('EventFilter', event_filter))
-            elif not trap_filter and event_filter_found:
-                changed = True
-                changed_list.append("trap filter")
-                # options = []
-                for option in snmp_config_spec.option:
-                    if option.key == 'EventFilter':
-                        results['trap_filter_previous'] = option.value.split(';')
-                    # else:
-                    #     options.append(option)
-                # Doesn't work. Need to reset config instead
-                # snmp_config_spec.option = options
-                reset_hint = True
-            if sys_contact and not sys_contact_found:
-                changed = True
-                changed_list.append("sys contact")
-                results['sys_contact_previous'] = ''
-                snmp_config_spec.option.append(self.create_option('syscontact', sys_contact))
-            if sys_location and not sys_location_found:
-                changed = True
-                changed_list.append("sys location")
-                results['sys_location_previous'] = ''
-                snmp_config_spec.option.append(self.create_option('syslocation', sys_location))
-        if changed:
-            if snmp_state == 'reset':
-                if self.module.check_mode:
-                    message = "SNMP agent would be reset to factory defaults"
+                    changed_list.append("sys contact")
+                    self.results[host.name]['sys_contact_previous'] = ''
+                    snmp_config_spec.option.append(self.create_option('syscontact', sys_contact))
+                if sys_location and not sys_location_found:
+                    changed = True
+                    changed_list.append("sys location")
+                    self.results[host.name]['sys_location_previous'] = ''
+                    snmp_config_spec.option.append(self.create_option('syslocation', sys_location))
+            if changed:
+                if snmp_state == 'reset':
+                    if self.module.check_mode:
+                        message = "SNMP agent would be reset to factory defaults"
+                    else:
+                        message = "SNMP agent config reset to factory defaults"
                 else:
-                    message = "SNMP agent config reset to factory defaults"
+                    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]
+                    message = "SNMP " + message + changed_suffix
+                if reset_hint:
+                    message += ". Agent reset required!"
+                if not self.module.check_mode:
+                    try:
+                        snmp_system.ReconfigureSnmpAgent(snmp_config_spec)
+                    except vim.fault.NotFound as not_found:
+                        self.module.fail_json(
+                            msg="Not found : %s" % to_native(not_found)
+                        )
+                    except vim.fault.InsufficientResourcesFault as insufficient_resources:
+                        self.module.fail_json(
+                            msg="Insufficient resources : %s" % to_native(insufficient_resources)
+                        )
             else:
+                message = "SNMP already configured properly"
+            if not snmp_state == 'reset' and send_trap and desired_trap_targets:
+                # Check if there was a change before
+                if changed:
+                    message += " and "
+                else:
+                    message += ", but "
+                changed = True
                 if self.module.check_mode:
-                    changed_suffix = ' would be changed'
+                    message = message + "a test trap would be sent"
                 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]
-                message = "SNMP " + message + changed_suffix
-            if reset_hint:
-                message += ". Agent reset required!"
-            if not self.module.check_mode:
-                try:
-                    snmp_system.ReconfigureSnmpAgent(snmp_config_spec)
-                except vim.fault.NotFound as not_found:
-                    self.module.fail_json(
-                        msg="Not found : %s" % to_native(not_found)
-                    )
-                except vim.fault.InsufficientResourcesFault as insufficient_resources:
-                    self.module.fail_json(
-                        msg="Insufficient resources : %s" % to_native(insufficient_resources)
-                    )
-        else:
-            message = "SNMP already configured properly"
-        if not snmp_state == 'reset' and send_trap and desired_trap_targets:
-            # Check if there was a change before
-            if changed:
-                message += " and "
-            else:
-                message += ", but "
-            changed = True
-            if self.module.check_mode:
-                message = message + "a test trap would be sent"
-            else:
-                try:
-                    snmp_system.SendTestNotification()
-                    message = message + "a test trap was sent"
-                except vim.fault.NotFound as not_found:
-                    self.module.fail_json(
-                        msg="Error during trap test : Not found : %s" % to_native(not_found)
-                    )
-                except vim.fault.InsufficientResourcesFault as insufficient_resources:
-                    self.module.fail_json(
-                        msg="Error during trap test : Insufficient resources : %s" % to_native(insufficient_resources)
-                    )
-        results['changed'] = changed
-        results['msg'] = message
-
-        self.module.exit_json(**results)
+                    try:
+                        snmp_system.SendTestNotification()
+                        message = message + "a test trap was sent"
+                    except vim.fault.NotFound as not_found:
+                        self.module.fail_json(
+                            msg="Error during trap test : Not found : %s" % to_native(not_found)
+                        )
+                    except vim.fault.InsufficientResourcesFault as insufficient_resources:
+                        self.module.fail_json(
+                            msg="Error during trap test : Insufficient resources : %s" % to_native(insufficient_resources)
+                        )
+            self.changed = any([self.changed, changed])
+            self.results[host.name]['changed'] = changed
+            self.results[host.name]['msg'] = message
+        self.results['changed'] = self.changed
+        self.module.exit_json(**self.results)
 
     @staticmethod
     def create_option(key, value):
@@ -510,6 +555,8 @@ def main():
     """Main"""
     argument_spec = vmware_argument_spec()
     argument_spec.update(
+        cluster_name=dict(type='str', required=False),
+        esxi_hostname=dict(type='list', elements='str', required=False),
         state=dict(type='str', default='disabled', choices=['enabled', 'disabled', 'reset']),
         snmp_port=dict(type='int', default=161),
         community=dict(type='list', default=[], elements='str'),
@@ -524,9 +571,12 @@ def main():
 
     module = AnsibleModule(
         argument_spec=argument_spec,
-        supports_check_mode=True
+        supports_check_mode=True,
     )
 
+    if not HAS_PYVMOMI:
+        module.fail_json(msg='pyvmomi required for this module')
+
     host_snmp = VmwareHostSnmp(module)
     host_snmp.ensure()
 


Test patch:
diff --git a/tests/integration/targets/vmware_host_snmp/aliases b/tests/integration/targets/vmware_host_snmp/aliases
--- a/tests/integration/targets/vmware_host_snmp/aliases
+++ b/tests/integration/targets/vmware_host_snmp/aliases
@@ -1,3 +1,3 @@
 cloud/vcenter
 needs/target/prepare_vmware_tests
-zuul/vmware/vcenter_1esxi
+zuul/vmware/vcenter_2esxi
diff --git a/tests/integration/targets/vmware_host_snmp/tasks/main.yml b/tests/integration/targets/vmware_host_snmp/tasks/main.yml
--- a/tests/integration/targets/vmware_host_snmp/tasks/main.yml
+++ b/tests/integration/targets/vmware_host_snmp/tasks/main.yml
@@ -7,7 +7,6 @@
   vars:
     setup_attach_host: true
 
-# SNMP works only with standalone ESXi server
 - name: Enable and configure SNMP community in check mode
   vmware_host_snmp:
     hostname: '{{ esxi1 }}'
@@ -68,3 +67,133 @@
     that:
       - snmp_disabled is defined
       - snmp_disabled.changed
+
+- name: Enable and configure SNMP community via vCenter in check mode
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi2 }}'
+    community: [ test ]
+    state: enabled
+    validate_certs: false
+  register: snmp_enabled_check_mode
+  check_mode: true
+- debug: var=snmp_enabled_check_mode
+- assert:
+    that:
+      - snmp_enabled_check_mode is defined
+      - snmp_enabled_check_mode.changed
+
+- name: Enable and configure SNMP community via vCenter
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi2 }}'
+    community: [ test ]
+    state: enabled
+    validate_certs: false
+  register: snmp_enabled
+- debug: var=snmp_enabled
+- assert:
+    that:
+      - snmp_enabled is defined
+      - snmp_enabled.changed
+
+- name: Enable and configure SNMP system contact and location via vCenter
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi2 }}'
+    sys_contact: "admin@testemail.com"
+    sys_location: "Austin, USA"
+    state: enabled
+    validate_certs: false
+  register: snmp_enabled_sys_options
+- debug: var=snmp_enabled_sys_options
+- assert:
+    that:
+      - snmp_enabled_sys_options is defined
+      - snmp_enabled_sys_options.changed
+
+- name: Disable SNMP via vCenter
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    esxi_hostname: '{{ esxi2 }}'
+    state: disabled
+    validate_certs: false
+  register: snmp_disabled
+- debug: var=snmp_disabled
+- assert:
+    that:
+      - snmp_disabled is defined
+      - snmp_disabled.changed
+
+- name: Enable and configure SNMP community in a cluster via vCenter in check mode
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    cluster_name: "{{ ccr1 }}"
+    community: [ test2 ]
+    state: enabled
+    validate_certs: false
+  register: snmp_enabled_check_mode
+  check_mode: true
+- debug: var=snmp_enabled_check_mode
+- assert:
+    that:
+      - snmp_enabled_check_mode is defined
+      - snmp_enabled_check_mode.changed
+
+- name: Enable and configure SNMP community in a cluster via vCenter
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    cluster_name: "{{ ccr1 }}"
+    community: [ test2 ]
+    state: enabled
+    validate_certs: false
+  register: snmp_enabled
+- debug: var=snmp_enabled
+- assert:
+    that:
+      - snmp_enabled is defined
+      - snmp_enabled.changed
+
+- name: Enable and configure SNMP system contact and location in a cluster via vCenter
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    cluster_name: "{{ ccr1 }}"
+    sys_contact: "admin2@testemail.com"
+    sys_location: "Austin, USA"
+    state: enabled
+    validate_certs: false
+  register: snmp_enabled_sys_options
+- debug: var=snmp_enabled_sys_options
+- assert:
+    that:
+      - snmp_enabled_sys_options is defined
+      - snmp_enabled_sys_options.changed
+
+- name: Disable SNMP in a cluster via vCenter
+  vmware_host_snmp:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    cluster_name: "{{ ccr1 }}"
+    state: disabled
+    validate_certs: false
+  register: snmp_disabled
+- debug: var=snmp_disabled
+- assert:
+    that:
+      - snmp_disabled is defined
+      - snmp_disabled.changed