#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2018, Christian Kotte # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' --- module: vmware_host_snmp short_description: Configures SNMP on an ESXi host system description: - 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 O(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 O(cluster_name) is not set. type: list elements: str version_added: '3.11.0' state: description: - Enable, disable, or reset the SNMP agent. type: str choices: [ disabled, enabled, reset ] default: disabled community: description: - List of SNMP community strings. type: list default: [] elements: str snmp_port: description: - Port used by the SNMP agent. type: int default: 161 trap_targets: description: - A list of trap targets. - You need to use C(hostname), C(port), and C(community) for each trap target. default: [] type: list elements: dict trap_filter: description: - A list of trap oids for traps not to be sent by agent, e.g. [ 1.3.6.1.4.1.6876.4.1.1.0, 1.3.6.1.4.1.6876.4.1.1.1 ] - Use value V(reset) to clear settings. type: list elements: str send_trap: description: - Send a test trap to validate the configuration. type: bool default: false hw_source: description: - Source hardware events from IPMI sensors or CIM Indications. - The embedded SNMP agent receives hardware events either from IPMI sensors V(sensors) or CIM indications V(indications). type: str choices: [ indications, sensors ] default: indications log_level: description: - Syslog logging level. type: str choices: [ debug, info, warning, error ] default: info sys_contact: description: - System contact who manages the system. type: str sys_location: description: - System location. type: str extends_documentation_fragment: - community.vmware.vmware.documentation ''' EXAMPLES = r''' - name: Enable and configure SNMP community on standalone ESXi host community.vmware.vmware_host_snmp: hostname: '{{ esxi_hostname }}' username: '{{ esxi_username }}' password: '{{ esxi_password }}' community: [ test ] state: enabled delegate_to: localhost - name: Configure SNMP traps and filters on cluster community.vmware.vmware_host_snmp: hostname: '{{ vcenter_hostname }}' username: '{{ vcenter_username }}' password: '{{ vcenter_password }}' cluster_name: '{{ cluster_name }}' community: [ test ] trap_targets: - hostname: 192.168.1.100 port: 162 community: test123 - hostname: 192.168.1.101 port: 162 community: test1234 trap_filter: - 1.3.6.1.4.1.6876.4.1.1.0 - 1.3.6.1.4.1.6876.4.1.1.1 state: enabled delegate_to: localhost - name: Enable and configure SNMP system contact and location on simple ESXi host in vCenter 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 - name: Disable SNMP on standalone ESXi host community.vmware.vmware_host_snmp: hostname: '{{ esxi_hostname }}' username: '{{ esxi_username }}' password: '{{ esxi_password }}' state: disabled delegate_to: localhost ''' RETURN = r''' results: description: metadata about host system's SNMP configuration returned: always type: dict sample: { "changed": true, "esx01.example.local": { "changed": true, "community": [ "test" ], "community_previous": [], "hw_source": "indications", "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: 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 from ansible.module_utils._text import to_native class VmwareHostSnmp(PyVmomi): """Manage SNMP configuration for an ESXi host system""" def __init__(self, module): super(VmwareHostSnmp, self).__init__(module) self.results = {"changed": False} self.changed = False self.hosts = None if self.is_vcenter(): 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: 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()) snmp_state = self.params.get('state') snmp_port = self.params.get('snmp_port') community = self.params.get('community') desired_trap_targets = self.params.get("trap_targets") hw_source = self.params.get("hw_source") log_level = self.params.get("log_level") send_trap = self.params.get("send_trap") trap_filter = self.params.get("trap_filter") sys_contact = self.params.get("sys_contact") sys_location = self.params.get("sys_location") event_filter = None if trap_filter: event_filter = ';'.join(trap_filter) reset_hint = None 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: self.module.fail_json(msg="SNMP system isn't available on the ESXi host") # Check state self.results[host.name]['state'] = snmp_state if snmp_state == 'reset': changed = True # Get previous config if snmp_config_spec.enabled: self.results[host.name]['state_previous'] = 'enabled' else: 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 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: 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: 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("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 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.option = options reset_hint = True if sys_contact and not sys_contact_found: changed = True 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: 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: 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) ) 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): """Create option""" option = vim.KeyValue() option.key = key option.value = value return option @staticmethod def get_previous_targets(trap_targets): """Get target entries from trap targets object""" previous_targets = [] for target in trap_targets: temp = dict() temp['hostname'] = target.hostName temp['port'] = target.port temp['community'] = target.community previous_targets.append(temp) return previous_targets @staticmethod def build_destination(dest_hostname, dest_port, dest_community): """Build destination spec""" destination = vim.host.SnmpSystem.SnmpConfigSpec.Destination() destination.hostName = dest_hostname destination.port = dest_port destination.community = dest_community return destination def check_if_options_are_valid(self, target): """Check if options are valid""" dest_hostname = target.get('hostname', None) if dest_hostname is None: self.module.fail_json( msg="Please specify hostname for the trap target as it's a required parameter" ) dest_port = target.get('port', None) if dest_port is None: self.module.fail_json( msg="Please specify port for the trap target as it's a required parameter" ) dest_community = target.get('community', None) if dest_community is None: self.module.fail_json( msg="Please specify community for the trap target as it's a required parameter" ) return dest_hostname, dest_port, dest_community 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'), trap_targets=dict(type='list', default=list(), elements='dict'), trap_filter=dict(type='list', elements='str'), hw_source=dict(type='str', default='indications', choices=['indications', 'sensors']), log_level=dict(type='str', default='info', choices=['debug', 'info', 'warning', 'error']), send_trap=dict(type='bool', default=False), sys_contact=dict(type='str'), sys_location=dict(type='str') ) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, ) if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi required for this module') host_snmp = VmwareHostSnmp(module) host_snmp.ensure() if __name__ == '__main__': main()