diff --git a/lib/ansible/modules/network/onyx/onyx_syslog_remote.py b/lib/ansible/modules/network/onyx/onyx_syslog_remote.py new file mode 100644 index 00000000000..664ca3f81dd --- /dev/null +++ b/lib/ansible/modules/network/onyx/onyx_syslog_remote.py @@ -0,0 +1,350 @@ +#!/usr/bin/python +# +# Copyright: Ansible Project +# 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: onyx_syslog_remote +version_added: "2.10" +author: "Anas Shami (@anass)" +short_description: Configure remote syslog module +description: + - This module provides declarative management of syslog + on Mellanox ONYX network devices. +notes: +options: + enabled: + description: + - Disable/Enable logging to given remote host + default: true + type: bool + host: + description: + - Send event logs to this server using the syslog protocol + required: true + type: str + port: + description: + - Set remote server destination port for log messages + type: int + trap: + description: + - Minimum severity level for messages to this syslog server + choices: ['none', 'debug', 'info', 'notice', 'alert', 'warning', 'err', 'emerg', 'crit'] + type: str + trap_override: + description: + - Override log levels for this sink on a per-class basis + type: list + suboptions: + override_class: + description: + - Specify a class whose log level to override + choices: ['mgmt-front', 'mgmt-back', 'mgmt-core', 'events', 'debug-module', 'sx-sdk', 'mlx-daemons', 'protocol-stack'] + required: True + type: str + override_priority: + description: + -Specify a priority whose log level to override + choices: ['none', 'debug', 'info', 'notice', 'alert', 'warning', 'err', 'emerg', 'crit'] + type: str + override_enabled: + description: + - disable override priorities for specific class. + default: True + type: bool + + filter: + description: + - Specify a filter type + choices: ['include', 'exclude'] + type: str + filter_str: + description: + - Specify a regex filter string + type: str +""" + +EXAMPLES = """ +- name: remote logging port 8080 +- onyx_syslog_remote: + host: 10.10.10.10 + port: 8080 + +- name: remote logging trap override +- onyx_syslog_remote: + host: 10.10.10.10 + trap_override: + - override_class: events + override_priority: emerg + +- name: remote logging trap emerg +- onyx_syslog_remote: + host: 10.10.10.10 + trap: emerg + +- name: remote logging filter include 'ERR' +- onyx_syslog_remote: + host: 10.10.10.10 + filter: include + filter_str: /ERR/ + +- name: disable remote logging with class events +- onyx_syslog_remote: + enabled: False + host: 10.10.10.10 + class: events +- name : disable remote logging +- onyx_syslog_remote: + enabled: False + host: 10.10.10.10 + +- name : enable/disable override class +- onyx_syslog_remote: + host: 10.7.144.71 + trap_override: + - override_class: events + override_priority: emerg + override_enabled: False + - override_class: mgmt-front + override_priority: alert +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device. + returned: always + type: list + sample: + - logging x port 8080 + - logging 10.10.10.10 trap override class events priority emerg + - no logging 10.10.10.10 trap override class events + - logging 10.10.10.10 trap emerg + - logging 10.10.10.10 filter [include | exclude] ERR +""" + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.onyx.onyx import show_cmd +from ansible.module_utils.network.onyx.onyx import BaseOnyxModule + + +class OnyxSyslogRemoteModule(BaseOnyxModule): + MAX_PORT = 65535 + LEVELS = ['none', 'debug', 'info', 'notice', 'alert', 'warning', 'err', 'emerg', 'crit'] + CLASSES = ['mgmt-front', 'mgmt-back', 'mgmt-core', 'events', 'debug-module', 'sx-sdk', 'mlx-daemons', 'protocol-stack'] + FILTER = ['include', 'exclude'] + + LOGGING_HOST = re.compile(r'^logging ([a-z0-9\.]+)$') + LOGGING_PORT = re.compile(r'^logging ([a-z0-9\.]+) port ([0-9]+)$') + LOGGING_TRAP = re.compile(r'^logging ([a-z0-9\.]+) trap ([a-z]+)$') + LOGGING_TRAP_OVERRIDE = re.compile(r'^logging ([a-z0-9\.]+) trap override class ([a-z\-]+) priority ([a-z]+)$') + LOGGING_FILTER = re.compile(r'^logging ([a-z0-9\.]+) filter (include|exclude) "([\D\d]+)"$') + + def init_module(self): + """" Ansible module initialization + """ + override_spec = dict(override_priority=dict(choices=self.LEVELS), + override_class=dict(choices=self.CLASSES, required=True), + override_enabled=dict(default=True, type="bool")) + + element_spec = dict(enabled=dict(type="bool", default=True), + host=dict(type="str", required=True), + port=dict(type="int"), + trap=dict(choices=self.LEVELS), + trap_override=dict(type="list", elements='dict', options=override_spec), + filter=dict(choices=self.FILTER), + filter_str=dict(type="str")) + + argument_spec = dict() + argument_spec.update(element_spec) + self._module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_together=[ + ['filter', 'filter_str'] + ]) + + def validate_port(self, port): + if port and (port < 1 or port > self.MAX_PORT): + self._module.fail_json(msg='logging port must be between 1 and {0}'.format(self.MAX_PORT)) + + def show_logging(self): + # we can't use show logging it has lack of information + return show_cmd(self._module, "show running-config | include .*logging.*", json_fmt=False, fail_on_error=False) + + def load_current_config(self): + self._current_config = dict() + current_config = self.show_logging().split('\n') + for line in current_config: + line = line.strip() + match = self.LOGGING_HOST.match(line) + if match: + host = match.group(1) + self._current_config[host] = dict() + continue + + match = self.LOGGING_PORT.match(line) + if match: + host = match.group(1) + port = int(match.group(2)) + if host in self._current_config: + self._current_config[host]['port'] = port + else: + self._current_config[host] = dict(port=port) + continue + + match = self.LOGGING_TRAP.match(line) + if match: + host = match.group(1) + trap = match.group(2) + host_config = self._current_config.get(host) + if host_config: + self._current_config[host]['trap'] = trap + else: + self._current_config[host] = dict(trap=trap) + continue + + match = self.LOGGING_TRAP_OVERRIDE.match(line) + if match: + host = match.group(1) + override_class = match.group(2) + override_priority = match.group(3) + host_config = self._current_config.get(host) + + if host_config: + if 'trap_override' in host_config: + self._current_config[host]['trap_override'].append(dict(override_class=override_class, override_priority=override_priority)) + else: + self._current_config[host]['trap_override'] = [dict(override_class=override_class, override_priority=override_priority)] + else: + self._current_config[host] = {'trap_override': [dict(override_class=override_class, override_priority=override_priority)]} + continue + + match = self.LOGGING_FILTER.match(line) + if match: + host = match.group(1) + filter_type = match.group(2) + filter_str = match.group(3) + if host in self._current_config: + self._current_config[host].update({'filter': filter_type, 'filter_str': filter_str}) + else: + self._current_config[host] = dict(filter=filter_type, filter_str=filter_str) + + def get_required_config(self): + self._required_config = dict() + required_config = dict() + module_params = self._module.params + port = module_params.get('port') + trap = module_params.get('trap') + trap_override = module_params.get('trap_override') + filtered = module_params.get('filter') + + required_config['host'] = module_params.get('host') + required_config['enabled'] = module_params.get('enabled') + + if port: + required_config['port'] = port + if trap: + required_config['trap'] = trap + if trap_override: + required_config['trap_override'] = trap_override + if filtered: + required_config['filter'] = filtered + required_config['filter_str'] = module_params.get('filter_str', '') + + self.validate_param_values(required_config) + self._required_config = required_config + + def generate_commands(self): + required_config = self._required_config + current_config = self._current_config + host = required_config.get('host') + enabled = required_config['enabled'] + ''' + cases: + if host in current config and current config != required config and its enable + if host in current config and its disable + if host in current and it has override_class with disable flag + ''' + host_config = current_config.get(host, dict()) + + if host in current_config and not enabled: + self._commands.append('no logging {0}'.format(host)) + else: + if host not in current_config: + self._commands.append('logging {0}'.format(host)) + if 'port' in required_config: + if required_config['port'] != host_config.get('port', None) or not host_config: + '''Edit/Create new one''' + self._commands.append('logging {0} port {1}'.format(host, required_config['port'])) + + if 'trap' in required_config or 'trap_override' in required_config: + trap_commands = self._get_trap_commands(host) + self._commands += trap_commands + + if 'filter' in required_config: + is_change = host_config.get('filter', None) != required_config['filter'] or \ + host_config.get('filter_str', None) != required_config['filter_str'] + if is_change or not host_config: + self._commands.append('logging {0} filter {1} {2}'.format(host, required_config['filter'], required_config['filter_str'])) + + ''' ********** private methods ********** ''' + def _get_trap_commands(self, host): + current_config = self._current_config + required_config = self._required_config + trap_commands = [] + host_config = current_config.get(host, dict()) + + override_list = required_config.get('trap_override') + if override_list: + current_override_list = host_config.get('trap_override', []) + + for override_trap in override_list: + override_class = override_trap.get('override_class') + override_priority = override_trap.get('override_priority') + override_enabled = override_trap.get('override_enabled') + found, found_class = False, False + for current_override in current_override_list: + if current_override.get('override_class') == override_class: + found_class = True + if not override_enabled: + break + if override_priority and current_override.get('override_priority') == override_priority: + found = True + break + + if override_enabled: + if not found and override_priority: + trap_commands.append('logging {0} trap override class {1} priority {2}'.format( + host, override_class, override_priority)) + elif found_class: # disabled option will use only class + trap_commands.append('no logging {0} trap override class {1}'.format( + host, override_class)) + + else: + if required_config['enabled']: # no disabled option for this, just override trap level can be disabled + trap = required_config.get('trap') + if trap and (trap != host_config.get('trap', None) or not host_config): + trap_commands.append('logging {0} trap {1}'.format( + host, trap)) + '''no disable for trap''' + + return trap_commands + + +def main(): + """ main entry point for module execution + """ + OnyxSyslogRemoteModule.main() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/onyx/fixtures/onyx_logging_config_show.cfg b/test/units/modules/network/onyx/fixtures/onyx_logging_config_show.cfg new file mode 100644 index 00000000000..235205c2d05 --- /dev/null +++ b/test/units/modules/network/onyx/fixtures/onyx_logging_config_show.cfg @@ -0,0 +1,11 @@ + +logging trap alert +logging 10.10.10.10 +logging 10.10.10.10 filter exclude ".*ERR.*" +logging 10.10.10.10 trap info +logging 10.10.10.12 +logging 10.10.10.12 port 80 +logging 10.10.10.12 trap override class sx-sdk priority emerg +logging files rotation criteria size-pct 10.000 +logging local info +logging receive diff --git a/test/units/modules/network/onyx/test_onyx_syslog_remote.py b/test/units/modules/network/onyx/test_onyx_syslog_remote.py new file mode 100644 index 00000000000..f5b9d5d72d8 --- /dev/null +++ b/test/units/modules/network/onyx/test_onyx_syslog_remote.py @@ -0,0 +1,91 @@ +# +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import patch +from ansible.modules.network.onyx import onyx_syslog_remote +from units.modules.utils import set_module_args +from .onyx_module import TestOnyxModule, load_fixture + + +class TestOnyxSysLogRemoteModule(TestOnyxModule): + + module = onyx_syslog_remote + + def setUp(self): + self.enabled = False + super(TestOnyxSysLogRemoteModule, self).setUp() + self.mock_get_config = patch.object( + onyx_syslog_remote.OnyxSyslogRemoteModule, "show_logging") + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + 'ansible.module_utils.network.onyx.onyx.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + super(TestOnyxSysLogRemoteModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None, transport='cli'): + config_file = 'onyx_logging_config_show.cfg' + self.get_config.return_value = load_fixture(config_file) + self.load_config.return_value = None + + def test_syslog_new_host(self): + set_module_args(dict(host="10.10.20.20")) + commands = ["logging 10.10.20.20"] + self.execute_module(changed=True, commands=commands) + + def test_syslog_new_host_port(self): + set_module_args(dict(host="10.10.20.20", port=8080)) + commands = ['logging 10.10.20.20', 'logging 10.10.20.20 port 8080'] + self.execute_module(changed=True, commands=commands) + + def test_syslog_override(self): + set_module_args(dict(host="10.10.10.12", trap_override=[dict(override_class="sx-sdk", override_priority='emerg'), + dict(override_class="mgmt-back", override_priority='emerg')])) + commands = ["logging 10.10.10.12 trap override class mgmt-back priority emerg"] # no sx-sdk its already configured + self.execute_module(changed=True, commands=commands) + + def test_syslog_trap(self): + set_module_args(dict(host="10.10.10.10", trap="notice")) + commands = ["logging 10.10.10.10 trap notice"] + self.execute_module(changed=True, commands=commands) + + def test_syslog_include_filter(self): + set_module_args(dict(host="10.10.10.10", filter="include", filter_str=".*ERR.*")) + commands = ['logging 10.10.10.10 filter include .*ERR.*'] + self.execute_module(changed=True, commands=commands) + + def test_syslog_no_override(self): + set_module_args(dict(host="10.10.10.12", trap_override=[dict(override_class="sx-sdk", override_enabled=False), + dict(override_class="mgmt-front", override_enabled=False)])) + commands = ['no logging 10.10.10.12 trap override class sx-sdk'] # no mgmt-front because doesn't configured + self.execute_module(changed=True, commands=commands) + + def test_syslog_no_port(self): + set_module_args(dict(host="10.10.10.12", enabled=False)) + commands = ['no logging 10.10.10.12'] + self.execute_module(changed=True, commands=commands) + + def test_syslog_filter_no_change(self): + set_module_args(dict(host="10.10.10.10", filter="exclude", filter_str=".*ERR.*")) + self.execute_module(changed=False) + + def test_syslog_trap_no_change(self): + set_module_args(dict(host="10.10.10.10", trap="info")) + self.execute_module(changed=False) + + def test_syslog_add_port_no_change(self): + set_module_args(dict(host="10.10.10.12", port=80)) + self.execute_module(changed=False) + + def test_syslog_override_no_change(self): + set_module_args(dict(host="10.10.10.12", trap_override=[dict(override_priority="emerg", override_class="sx-sdk")])) + self.execute_module(changed=False)