VMware: New module vmware_host_snmp (#47937)

This commit is contained in:
Christian Kotte 2018-12-10 06:31:41 +01:00 committed by Abhijeet Kasurde
parent 4903e42e2f
commit 36e3edff50
3 changed files with 563 additions and 0 deletions

View file

@ -0,0 +1,491 @@
#!/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 = '''
---
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.
version_added: 2.8
author:
- Christian Kotte (@ckotte)
notes:
- Tested on vSphere 6.5
- 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
requirements:
- python >= 2.6
- PyVmomi
options:
state:
description:
- Enable, disable, or reset the SNMP agent.
type: str
choices: [ enabled, disabled, reset ]
default: 'disabled'
community:
description:
- List of SNMP community strings.
type: list
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: []
trap_filter:
description:
- Comma separated 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 C(reset) to clear settings.
type: 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 C(sensors) or CIM indications C(indications).
type: str
choices: [ indications, sensors ]
default: 'indications'
log_level:
description:
- Syslog logging level.
type: str
choices: [ debug, info, warning, error ]
default: 'info'
extends_documentation_fragment: vmware.documentation
'''
EXAMPLES = r'''
- name: Enable and configure SNMP community
vmware_host_snmp:
hostname: '{{ esxi_hostname }}'
username: '{{ esxi_username }}'
password: '{{ esxi_password }}'
community: [ test ]
state: enabled
validate_certs: no
delegate_to: localhost
- name: Configure SNMP traps and filters
vmware_host_snmp:
hostname: '{{ esxi_hostname }}'
username: '{{ esxi_username }}'
password: '{{ esxi_password }}'
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
validate_certs: no
delegate_to: localhost
- name: Disable SNMP
vmware_host_snmp:
hostname: '{{ esxi_hostname }}'
username: '{{ esxi_username }}'
password: '{{ esxi_password }}'
state: disabled
validate_certs: no
delegate_to: localhost
'''
RETURN = r'''
results:
description: metadata about host system's SNMP configuration
returned: always
type: dict
sample: {
"esxi01": {
"changed": false,
"community": ["test"],
"hw_source": "indications",
"msg": "SNMP already configured properly",
"port": 161,
"state": "enabled",
"trap_targets": []
},
}
'''
try:
from pyVmomi import vim
except ImportError:
pass
from ansible.module_utils.basic import AnsibleModule
from ansible.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)
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."
)
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.")
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")
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'
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(';')
# 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
# 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:
changed = True
changed_list.append("community list")
results['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
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
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:
results['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")
results['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")
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
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 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 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)
)
results['changed'] = changed
results['msg'] = message
self.module.exit_json(**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(
state=dict(default='disabled', choices=['enabled', 'disabled', 'reset']),
snmp_port=dict(type='int', default=161),
community=dict(type='list', default=[]),
trap_targets=dict(type='list', default=list(), required=False),
trap_filter=dict(type='list', required=False),
hw_source=dict(default='indications', choices=['indications', 'sensors']),
log_level=dict(default='info', choices=['debug', 'info', 'warning', 'error']),
send_trap=dict(type='bool', default=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True
)
host_snmp = VmwareHostSnmp(module)
host_snmp.ensure()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,2 @@
cloud/vcenter
unsupported

View file

@ -0,0 +1,70 @@
# Test code for the vmware_host_snmp.
# 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)
- name: store the vcenter container ip
set_fact:
vcsim: "{{ lookup('env', 'vcenter_host') }}"
- debug: var=vcsim
- name: Wait for Flask controller to come up online
wait_for:
host: "{{ vcsim }}"
port: 5000
state: started
- name: kill vcsim
uri:
url: http://{{ vcsim }}:5000/killall
# SNMP works only with standalone ESXi server
- name: start vcsim
uri:
url: http://{{ vcsim }}:5000/spawn?esx=1
register: vcsim_instance
- debug:
var: vcsim_instance
- name: Wait for vcsim server to come up online
wait_for:
host: "{{ vcsim }}"
port: 443
state: started
- name: Enable and configure SNMP community in check mode
vmware_host_snmp:
hostname: "{{ vcsim }}"
username: "{{ vcsim_instance.json.username }}"
password: "{{ vcsim_instance.json.password }}"
community: [ test ]
state: enabled
validate_certs: no
register: snmp_enabled_check_mode
check_mode: yes
- 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
vmware_host_snmp:
hostname: "{{ vcsim }}"
username: "{{ vcsim_instance.json.username }}"
password: "{{ vcsim_instance.json.password }}"
community: [ test ]
state: enabled
validate_certs: no
register: snmp_enabled
check_mode: yes
- debug: var=snmp_enabled
- assert:
that:
- snmp_enabled is defined
- snmp_enabled.changed