VMware: New module vmware_host_snmp (#47937)
This commit is contained in:
parent
4903e42e2f
commit
36e3edff50
3 changed files with 563 additions and 0 deletions
491
lib/ansible/modules/cloud/vmware/vmware_host_snmp.py
Normal file
491
lib/ansible/modules/cloud/vmware/vmware_host_snmp.py
Normal 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()
|
2
test/integration/targets/vmware_host_snmp/aliases
Normal file
2
test/integration/targets/vmware_host_snmp/aliases
Normal file
|
@ -0,0 +1,2 @@
|
|||
cloud/vcenter
|
||||
unsupported
|
70
test/integration/targets/vmware_host_snmp/tasks/main.yml
Normal file
70
test/integration/targets/vmware_host_snmp/tasks/main.yml
Normal 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
|
Loading…
Reference in a new issue