From a78c40322c63fd10c70bd4192645bf763d9eb799 Mon Sep 17 00:00:00 2001 From: Anil Kumar Muraleedharan Date: Thu, 28 Feb 2019 00:45:56 +0530 Subject: [PATCH] Lenovo cnos logging (#52978) * Adding the module cnos_logging.py to the module suite from Lenovo --- .../modules/network/cnos/cnos_logging.py | 426 ++++++++++++++++++ test/integration/targets/cnos_logging/aliases | 2 + .../cnos_logging/cnos_logging_sample_hosts | 14 + .../targets/cnos_logging/defaults/main.yaml | 2 + .../targets/cnos_logging/tasks/cli.yaml | 22 + .../targets/cnos_logging/tasks/main.yaml | 2 + .../targets/cnos_logging/tests/cli/basic.yaml | 136 ++++++ .../cnos/fixtures/cnos_logging_config.cfg | 9 + .../modules/network/cnos/test_cnos_logging.py | 62 +++ 9 files changed, 675 insertions(+) create mode 100644 lib/ansible/modules/network/cnos/cnos_logging.py create mode 100644 test/integration/targets/cnos_logging/aliases create mode 100644 test/integration/targets/cnos_logging/cnos_logging_sample_hosts create mode 100644 test/integration/targets/cnos_logging/defaults/main.yaml create mode 100644 test/integration/targets/cnos_logging/tasks/cli.yaml create mode 100644 test/integration/targets/cnos_logging/tasks/main.yaml create mode 100644 test/integration/targets/cnos_logging/tests/cli/basic.yaml create mode 100644 test/units/modules/network/cnos/fixtures/cnos_logging_config.cfg create mode 100644 test/units/modules/network/cnos/test_cnos_logging.py diff --git a/lib/ansible/modules/network/cnos/cnos_logging.py b/lib/ansible/modules/network/cnos/cnos_logging.py new file mode 100644 index 00000000000..dceb7465958 --- /dev/null +++ b/lib/ansible/modules/network/cnos/cnos_logging.py @@ -0,0 +1,426 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +# +# Copyright (C) 2019 Lenovo, Inc. +# (c) 2017, Ansible by Red Hat, inc +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +# Module to work on Link Aggregation with Lenovo Switches +# Lenovo Networking +# +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: cnos_logging +version_added: "2.8" +author: "Anil Kumar Muraleedharan (@amuraleedhar)" +short_description: Manage logging on network devices +description: + - This module provides declarative management of logging + on Cisco Cnos devices. +notes: + - Tested against CNOS 10.9.1 +options: + dest: + description: + - Destination of the logs. Lenovo uses the term server instead of host in + its CLI. + choices: ['server', 'console', 'monitor', 'logfile'] + name: + description: + - If value of C(dest) is I(file) it indicates file-name + and for I(server) indicates the server name to be notified. + size: + description: + - Size of buffer. The acceptable value is in range from 4096 to + 4294967295 bytes. + default: 10485760 + facility: + description: + - Set logging facility. This is applicable only for server logging + level: + description: + - Set logging severity levels. 0-emerg;1-alert;2-crit;3-err;4-warn; + 5-notif;6-inform;7-debug + default: 5 + aggregate: + description: List of logging definitions. + state: + description: + - State of the logging configuration. + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +- name: configure server logging + cnos_logging: + dest: server + name: 10.241.107.224 + facility: local7 + state: present + +- name: remove server logging configuration + cnos_logging: + dest: server + name: 10.241.107.224 + state: absent + +- name: configure console logging level and facility + cnos_logging: + dest: console + level: 7 + state: present + +- name: configure buffer size + cnos_logging: + dest: logfile + level: 5 + name: testfile + size: 5000 + +- name: Configure logging using aggregate + cnos_logging: + aggregate: + - { dest: console, level: 6 } + - { dest: logfile, size: 9000 } + +- name: remove logging using aggregate + cnos_logging: + aggregate: + - { dest: console, level: 6 } + - { dest: logfile, name: anil, size: 9000 } + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - logging console 7 + - logging server 10.241.107.224 +""" + +import re + +from copy import deepcopy +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import validate_ip_address +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.cnos.cnos import get_config, load_config +from ansible.module_utils.network.cnos.cnos import get_capabilities +from ansible.module_utils.network.cnos.cnos import check_args +from ansible.module_utils.network.cnos.cnos import cnos_argument_spec + + +def validate_size(value, module): + if value: + if not int(4096) <= int(value) <= int(4294967295): + module.fail_json(msg='size must be between 4096 and 4294967295') + else: + return value + + +def map_obj_to_commands(updates, module): + dest_group = ('console', 'monitor', 'logfile', 'server') + commands = list() + want, have = updates + for w in want: + dest = w['dest'] + name = w['name'] + size = w['size'] + facility = w['facility'] + level = w['level'] + state = w['state'] + del w['state'] + + if state == 'absent': + if dest: + + if dest == 'server': + commands.append('no logging server {0}'.format(name)) + + elif dest in dest_group: + commands.append('no logging {0}'.format(dest)) + + else: + module.fail_json(msg='dest must be among console, monitor, logfile, server') + + if state == 'present' and w not in have: + if dest == 'server': + cmd_str = 'logging server {0}'.format(name) + if level is not None and level > 0 and level < 8: + cmd_str = cmd_str + ' ' + level + if facility is not None: + cmd_str = cmd_str + ' facility ' + facility + commands.append(cmd_str) + + elif dest == 'logfile' and size: + present = False + + for entry in have: + if entry['dest'] == 'logfile' and entry['size'] == size and entry['level'] == level: + present = True + + if not present: + cmd_str = 'logging logfile ' + if name is not None: + cmd_str = cmd_str + name + if level and level != '7': + cmd_str = cmd_str + ' ' + level + else: + cmd_str = cmd_str + ' 7' + if size is not None: + cmd_str = cmd_str + ' size ' + size + commands.append(cmd_str) + else: + module.fail_json(msg='Name of the logfile is a mandatory parameter') + + else: + if dest: + dest_cmd = 'logging {0}'.format(dest) + if level: + dest_cmd += ' {0}'.format(level) + commands.append(dest_cmd) + return commands + + +def parse_facility(line, dest): + facility = None + if dest == 'server': + result = line.split() + i = 0 + for x in result: + if x == 'facility': + return result[i + 1] + i = i + 1 + return facility + + +def parse_size(line, dest): + size = None + if dest == 'logfile': + if 'logging logfile' in line: + result = line.split() + i = 0 + for x in result: + if x == 'size': + return result[i + 1] + i = i + 1 + return '10485760' + return size + + +def parse_name(line, dest): + name = None + if dest == 'server': + if 'logging server' in line: + result = line.split() + i = 0 + for x in result: + if x == 'server': + name = result[i + 1] + elif dest == 'logfile': + if 'logging logfile' in line: + result = line.split() + i = 0 + for x in result: + if x == 'logfile': + name = result[i + 1] + else: + name = None + return name + + +def parse_level(line, dest): + level_group = ('0', '1', '2', '3', '4', '5', '6', '7') + level = '7' + if dest == 'server': + if 'logging server' in line: + result = line.split() + if(len(result) > 3): + if result[3].isdigit(): + level = result[3] + else: + if dest == 'logfile': + if 'logging logfile' in line: + result = line.split() + if result[3].isdigit(): + level = result[3] + else: + match = re.search(r'logging {0} (\S+)'.format(dest), line, re.M) + + return level + + +def map_config_to_obj(module): + obj = [] + dest_group = ('console', 'server', 'monitor', 'logfile') + data = get_config(module, flags=['| include logging']) + index = 0 + for line in data.split('\n'): + logs = line.split() + index = len(logs) + if index == 0 or index == 1: + continue + if logs[0] != 'logging': + continue + if logs[1] == 'monitor' or logs[1] == 'console': + obj.append({'dest': logs[1], 'level': logs[2]}) + elif logs[1] == 'logfile': + level = '5' + if logs[3] is not None: + level = logs[3] + size = '10485760' + if len(logs) > 4: + size = logs[5] + obj.append({'dest': logs[1], 'name': logs[2], 'size': size, 'level': level}) + elif logs[1] == 'server': + level = '5' + facility = None + + if logs[3].isdigit(): + level = logs[3] + if index > 3 and logs[3] == 'facility': + facility = logs[4] + if index > 4 and logs[4] == 'facility': + facility = logs[5] + obj.append({'dest': logs[1], 'name': logs[2], 'facility': facility, 'level': level}) + else: + continue + return obj + + +def map_params_to_obj(module, required_if=None): + obj = [] + aggregate = module.params.get('aggregate') + + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + module._check_required_if(required_if, item) + + d = item.copy() + if d['dest'] != 'server' and d['dest'] != 'logfile': + d['name'] = None + + if d['dest'] == 'logfile': + if 'size' in d: + d['size'] = str(validate_size(d['size'], module)) + elif 'size' not in d: + d['size'] = str(10485760) + else: + pass + + if d['dest'] != 'logfile': + d['size'] = None + + obj.append(d) + + else: + if module.params['dest'] != 'server' and module.params['dest'] != 'logfile': + module.params['name'] = None + + if module.params['dest'] == 'logfile': + if not module.params['size']: + module.params['size'] = str(10485760) + else: + module.params['size'] = None + + if module.params['size'] is None: + obj.append({ + 'dest': module.params['dest'], + 'name': module.params['name'], + 'size': module.params['size'], + 'facility': module.params['facility'], + 'level': module.params['level'], + 'state': module.params['state'] + }) + + else: + obj.append({ + 'dest': module.params['dest'], + 'name': module.params['name'], + 'size': str(validate_size(module.params['size'], module)), + 'facility': module.params['facility'], + 'level': module.params['level'], + 'state': module.params['state'] + }) + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + dest=dict(type='str', + choices=['server', 'console', 'monitor', 'logfile']), + name=dict(type='str'), + size=dict(type='int', default=10485760), + facility=dict(type='str'), + level=dict(type='str', default='5'), + state=dict(default='present', choices=['present', 'absent']), + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + + required_if = [('dest', 'server', ['name'])] + + module = AnsibleModule(argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True) + warnings = list() + check_args(module, warnings) + + result = {'changed': False} + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module, required_if=required_if) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/cnos_logging/aliases b/test/integration/targets/cnos_logging/aliases new file mode 100644 index 00000000000..be010d923f4 --- /dev/null +++ b/test/integration/targets/cnos_logging/aliases @@ -0,0 +1,2 @@ +# No Lenovo Switch simulator yet, so not enabled +unsupported diff --git a/test/integration/targets/cnos_logging/cnos_logging_sample_hosts b/test/integration/targets/cnos_logging/cnos_logging_sample_hosts new file mode 100644 index 00000000000..6cf095132ac --- /dev/null +++ b/test/integration/targets/cnos_logging/cnos_logging_sample_hosts @@ -0,0 +1,14 @@ +# You have to paste this dummy information in /etc/ansible/hosts +# Notes: +# - Comments begin with the '#' character +# - Blank lines are ignored +# - Groups of hosts are delimited by [header] elements +# - You can enter hostnames or ip Addresses +# - A hostname/ip can be a member of multiple groups +# +# In the /etc/ansible/hosts file u have to enter [cnos_portchannel_sample] tag +# Following you should specify IP Addresses details +# Please change and with appropriate value for your switch. + +[cnos_logging_sample] +10.241.107.39 ansible_network_os=cnos ansible_ssh_user= ansible_ssh_pass= diff --git a/test/integration/targets/cnos_logging/defaults/main.yaml b/test/integration/targets/cnos_logging/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/cnos_logging/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/cnos_logging/tasks/cli.yaml b/test/integration/targets/cnos_logging/tasks/cli.yaml new file mode 100644 index 00000000000..1216a3d0bee --- /dev/null +++ b/test/integration/targets/cnos_logging/tasks/cli.yaml @@ -0,0 +1,22 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + +#- name: run test case (connection=local) +# include: "{{ test_case_to_run }} ansible_connection=local" +# with_first_found: "{{ test_items }}" +# loop_control: +# loop_var: test_case_to_run diff --git a/test/integration/targets/cnos_logging/tasks/main.yaml b/test/integration/targets/cnos_logging/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/cnos_logging/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/cnos_logging/tests/cli/basic.yaml b/test/integration/targets/cnos_logging/tests/cli/basic.yaml new file mode 100644 index 00000000000..b82b2ac368f --- /dev/null +++ b/test/integration/targets/cnos_logging/tests/cli/basic.yaml @@ -0,0 +1,136 @@ +--- +# ensure logging configs are empty +- name: Remove server logging + cnos_logging: &remove_server + dest: server + name: 10.241.107.224 + state: absent + +- name: Remove console + cnos_logging: + dest: console + level: 4 + state: absent + +- name: Remove buffer + cnos_logging: + dest: logfile + size: 8000 + state: absent + +# start tests +- name: Set up server logging + cnos_logging: + dest: server + name: 10.241.107.224 + facility: local7 + state: present + register: result + +- assert: + that: + - 'result.changed == true' + - '"logging server 10.241.107.224" in result.commands' + +- name: Set up server logging again (idempotent) + cnos_logging: + dest: server + name: 10.241.107.224 + state: present + register: result + +- assert: &unchanged + that: + - 'result.changed == true' + +- name: Delete/disable server logging + cnos_logging: *remove_server + register: result + +- assert: + that: + - 'result.changed == true' + - '"no logging server 10.241.107.224" in result.commands' + +- name: Delete/disable server logging (idempotent) + cnos_logging: *remove_server + register: result + +- assert: + that: + - 'result.changed == true' + +- name: Console logging with level errors + cnos_logging: + dest: console + level: 3 + state: present + register: result + +- assert: + that: + - 'result.changed == true' + - '"logging console 3" in result.commands' + +- name: Configure Buffer size + cnos_logging: + dest: logfile + name: testfile + level: 6 + size: 8000 + register: result + +- assert: + that: + - 'result.changed == true' + - '"logging logfile testfile 6 size 8000" in result.commands' + + +- name: Change logging parameters using aggregate + cnos_logging: + aggregate: + - { dest: console, level: 5 } + - { dest: logfile, name: anil, level: 3, size: 9000 } + register: result + +- assert: + that: + - 'result.changed == true' + - '"logging logfile anil 3 size 9000" in result.commands' + - '"logging console 5" in result.commands' + +- name: Set both logging destination and facility + cnos_logging: &set_both + dest: logfile + name: mylog + level: 1 + size: 4096 + state: present + register: result + +- assert: + that: + - 'result.changed == true' + - '"logging logfile mylog 1 size 4096" in result.commands' + +- name: Set both logging destination and facility (idempotent) + cnos_logging: *set_both + register: result + +- assert: + that: + - 'result.changed == false' + +- name: remove logging as collection tearDown + cnos_logging: + aggregate: + - { dest: console, level: 6 } + - { dest: logfile, name: mylog, size: 4096, level: 1 } + state: absent + register: result + +- assert: + that: + - 'result.changed == true' + - '"no logging console" in result.commands' + - '"no logging logfile" in result.commands' diff --git a/test/units/modules/network/cnos/fixtures/cnos_logging_config.cfg b/test/units/modules/network/cnos/fixtures/cnos_logging_config.cfg new file mode 100644 index 00000000000..8f3d7d69879 --- /dev/null +++ b/test/units/modules/network/cnos/fixtures/cnos_logging_config.cfg @@ -0,0 +1,9 @@ +! +logging logfile anil 4 size 10485760 +logging level vlan 4 +logging server 1.2.3.4 facility local0 +logging server 1.2.34.5 port 34 +logging server 1.2.3.5 4 facility local2 port 23 +logging server anil 5 +logging server tapas 4 facility local2 port 23 +! diff --git a/test/units/modules/network/cnos/test_cnos_logging.py b/test/units/modules/network/cnos/test_cnos_logging.py new file mode 100644 index 00000000000..1a1204d2659 --- /dev/null +++ b/test/units/modules/network/cnos/test_cnos_logging.py @@ -0,0 +1,62 @@ +# +# (c) 2018 Red Hat Inc. +# Copyright (C) 2017 Lenovo. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from units.compat.mock import patch +from ansible.modules.network.cnos import cnos_logging +from units.modules.utils import set_module_args +from .cnos_module import TestCnosModule, load_fixture + + +class TestCnosLoggingModule(TestCnosModule): + + module = cnos_logging + + def setUp(self): + super(TestCnosLoggingModule, self).setUp() + + self.mock_get_config = patch('ansible.modules.network.cnos.cnos_logging.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.cnos.cnos_logging.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + super(TestCnosLoggingModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None): + self.get_config.return_value = load_fixture('cnos_logging_config.cfg') + self.load_config.return_value = None + + def test_cnos_logging_buffer_size_changed_implicit(self): + set_module_args(dict(dest='logfile', name='anil')) + commands = ['logging logfile anil 5 size 10485760'] + self.execute_module(changed=True, commands=commands) + + def test_cnos_logging_logfile_size_changed_explicit(self): + set_module_args(dict(dest='logfile', name='anil', level='4', size=6000)) + commands = ['logging logfile anil 4 size 6000'] + self.execute_module(changed=True, commands=commands)