From e5c2e1b7beabdd91fe0df768b8c6aa7f54c603e3 Mon Sep 17 00:00:00 2001
From: Trishna Guha <trishnaguha17@gmail.com>
Date: Mon, 17 Jul 2017 17:20:22 +0530
Subject: [PATCH] iosxr_logging implementation module (#26886)

* iosxr_logging implementation module

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* iosxr_logging integration test

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>
---
 .../modules/network/iosxr/iosxr_logging.py    | 358 ++++++++++++++++++
 test/integration/iosxr.yaml                   |   9 +-
 .../targets/iosxr_logging/defaults/main.yaml  |   3 +
 .../targets/iosxr_logging/meta/main.yaml      |   2 +
 .../targets/iosxr_logging/tasks/cli.yaml      |  16 +
 .../targets/iosxr_logging/tasks/main.yaml     |   2 +
 .../iosxr_logging/tests/cli/basic.yaml        |  90 +++++
 .../targets/net_logging/tests/cli/basic.yaml  |   3 +
 .../net_logging/tests/iosxr/basic.yaml        |  90 +++++
 9 files changed, 572 insertions(+), 1 deletion(-)
 create mode 100644 lib/ansible/modules/network/iosxr/iosxr_logging.py
 create mode 100644 test/integration/targets/iosxr_logging/defaults/main.yaml
 create mode 100644 test/integration/targets/iosxr_logging/meta/main.yaml
 create mode 100644 test/integration/targets/iosxr_logging/tasks/cli.yaml
 create mode 100644 test/integration/targets/iosxr_logging/tasks/main.yaml
 create mode 100644 test/integration/targets/iosxr_logging/tests/cli/basic.yaml
 create mode 100644 test/integration/targets/net_logging/tests/iosxr/basic.yaml

diff --git a/lib/ansible/modules/network/iosxr/iosxr_logging.py b/lib/ansible/modules/network/iosxr/iosxr_logging.py
new file mode 100644
index 00000000000..76264c40812
--- /dev/null
+++ b/lib/ansible/modules/network/iosxr/iosxr_logging.py
@@ -0,0 +1,358 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Ansible by Red Hat, inc
+#
+# This file is part of Ansible by Red Hat
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+ANSIBLE_METADATA = {'metadata_version': '1.0',
+                    'status': ['preview'],
+                    'supported_by': 'core'}
+
+DOCUMENTATION = """
+---
+module: iosxr_logging
+version_added: "2.4"
+author: "Trishna Guha (@trishnag)"
+short_description: Manage logging on network devices
+description:
+  - This module provides declarative management of logging
+    on Cisco IOS XR devices.
+options:
+  dest:
+    description:
+      - Destination of the logs.
+    choices: ['on', 'hostnameprefix', console', 'monitor', 'buffered']
+  name:
+    description:
+      - If value of C(dest) is I(file) it indicates file-name,
+        for I(user) it indicates username and for I(host) indicates
+        the host name to be notified.
+  size:
+    description:
+      - Size of buffer. The acceptable value is in range from 307200 to
+        125000000 bytes.
+    default: 307200
+  facility:
+    description:
+      - Set logging facility.
+    default: local7
+  level:
+    description:
+      - Set logging severity levels.
+    default: debugging
+  collection:
+    description: List of logging definitions.
+  purge:
+    description:
+      - Purge logging not defined in the collections parameter.
+    default: no
+  state:
+    description:
+      - State of the logging configuration.
+    default: present
+    choices: ['present', 'absent']
+"""
+
+EXAMPLES = """
+- name: configure hostnameprefix logging
+  iosxr_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: present
+- name: remove hostnameprefix logging configuration
+  iosxr_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: absent
+- name: configure console logging level and facility
+  iosxr_logging:
+    dest: console
+    facility: local7
+    level: debugging
+    state: present
+- name: enable logging to all
+  iosxr_logging:
+    dest : on
+- name: configure buffer size
+  iosxr_logging:
+    dest: buffered
+    size: 5000
+"""
+
+RETURN = """
+commands:
+  description: The list of configuration mode commands to send to the device
+  returned: always
+  type: list
+  sample:
+    - logging facility local7
+    - logging hostnameprefix 172.16.0.1
+"""
+
+import re
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.iosxr import get_config, load_config
+from ansible.module_utils.iosxr import iosxr_argument_spec, check_args
+
+
+def validate_size(value, module):
+    if value:
+        if not int(307200) <= value <= int(125000000):
+            module.fail_json(msg='size must be between 307200 and 125000000')
+        else:
+            return value
+
+
+def map_obj_to_commands(updates, module):
+    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' and w in have:
+            if dest == 'hostnameprefix':
+                commands.append('no logging hostnameprefix {}'.format(name))
+            elif dest:
+                commands.append('no logging {}'.format(dest))
+            else:
+                module.fail_json(msg='dest must be among console, monitor, buffered, hostnameprefix, on')
+
+            if facility:
+                commands.append('no logging facility {}'.format(facility))
+
+        if state == 'present' and w not in have:
+            if facility:
+                commands.append('logging facility {}'.format(facility))
+
+            if dest == 'hostnameprefix':
+                commands.append('logging hostnameprefix {}'.format(name))
+
+            elif dest == 'on':
+                commands.append('logging on')
+
+            elif dest == 'buffered' and size:
+                commands.append('logging buffered {}'.format(size))
+
+            else:
+                dest_cmd = 'logging {}'.format(dest)
+                if level:
+                    dest_cmd += ' {}'.format(level)
+
+                commands.append(dest_cmd)
+
+    return commands
+
+
+def parse_facility(line):
+    match = re.search(r'logging facility (\S+)', line, re.M)
+    if match:
+        facility = match.group(1)
+    else:
+        facility = 'local7'
+
+    return facility
+
+
+def parse_size(line, dest):
+    size = None
+
+    if dest == 'buffered':
+        match = re.search(r'logging buffered (\S+)', line, re.M)
+        if match:
+            try:
+                int_size = int(match.group(1))
+            except ValueError:
+                int_size = None
+
+            if int_size:
+                if isinstance(int_size, int):
+                    size = str(match.group(1))
+                else:
+                    size = str(307200)
+
+    return size
+
+
+def parse_name(line, dest):
+    if dest == 'hostnameprefix':
+        match = re.search(r'logging hostnameprefix (\S+)', line, re.M)
+        if match:
+            name = match.group(1)
+    else:
+        name = None
+
+    return name
+
+
+def parse_level(line, dest):
+    level_group = ('emergencies', 'alerts', 'critical', 'errors', 'warnings',
+                   'notifications', 'informational', 'debugging')
+
+    if dest == 'hostnameprefix':
+        level = 'debugging'
+
+    else:
+        match = re.search(r'logging {} (\S+)'.format(dest), line, re.M)
+        if match:
+            if match.group(1) in level_group:
+                level = match.group(1)
+            else:
+                level = 'debugging'
+        else:
+            level = 'debugging'
+
+    return level
+
+
+def map_config_to_obj(module):
+    obj = []
+    dest_group = ('console', 'hostnameprefix', 'monitor', 'buffered', 'on')
+
+    data = get_config(module, flags=['logging'])
+
+    for line in data.split('\n'):
+
+        match = re.search(r'logging (\S+)', line, re.M)
+
+        if match.group(1) in dest_group:
+            dest = match.group(1)
+        else:
+            pass
+
+        obj.append({'dest': dest,
+                    'name': parse_name(line, dest),
+                    'size': parse_size(line, dest),
+                    'facility': parse_facility(line),
+                    'level': parse_level(line, dest)})
+
+    return obj
+
+
+def map_params_to_obj(module):
+    obj = []
+
+    if 'aggregate' in module.params and module.params['aggregate']:
+        for c in module.params['aggregate']:
+            d = c.copy()
+            if d['dest'] != 'hostnameprefix':
+                d['name'] = None
+
+            if 'state' not in d:
+                d['state'] = module.params['state']
+            if 'facility' not in d:
+                d['facility'] = module.params['facility']
+            if 'level' not in d:
+                d['level'] = module.params['level']
+
+            if d['dest'] == 'buffered':
+                if 'size' in d:
+                    d['size'] = str(validate_size(d['size'], module))
+                elif 'size' not in d:
+                    d['size'] = str(307200)
+                else:
+                    pass
+
+            if d['dest'] != 'buffered':
+                d['size'] = None
+
+            obj.append(d)
+
+    else:
+        if module.params['dest'] != 'hostnameprefix':
+            module.params['name'] = None
+
+        if module.params['dest'] == 'buffered':
+            if not module.params['size']:
+                module.params['size'] = str(307200)
+        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
+    """
+    argument_spec = dict(
+        dest=dict(type='str', choices=['on', 'hostnameprefix', 'console', 'monitor', 'buffered']),
+        name=dict(type='str'),
+        size=dict(type='int'),
+        facility=dict(type='str', default='local7'),
+        level=dict(type='str', default='debugging'),
+        state=dict(default='present', choices=['present', 'absent']),
+        aggregate=dict(type='list'),
+        purge=dict(default=False, type='bool')
+    )
+
+    argument_spec.update(iosxr_argument_spec)
+
+    required_if = [('dest', 'hostnameprefix', ['name'])]
+
+    module = AnsibleModule(argument_spec=argument_spec,
+                           required_if=required_if,
+                           supports_check_mode=True)
+
+    warnings = list()
+    check_args(module, warnings)
+
+    result = {'changed': False}
+
+    want = map_params_to_obj(module)
+    have = map_config_to_obj(module)
+    commands = map_obj_to_commands((want, have), module)
+
+    result['commands'] = commands
+    result['warnings'] = warnings
+
+    if commands:
+        if not module.check_mode:
+            load_config(module, commands, result['warnings'], commit=True)
+        result['changed'] = True
+
+    module.exit_json(**result)
+
+if __name__ == '__main__':
+    main()
diff --git a/test/integration/iosxr.yaml b/test/integration/iosxr.yaml
index 00aa67698a6..23ee38d6c96 100644
--- a/test/integration/iosxr.yaml
+++ b/test/integration/iosxr.yaml
@@ -63,8 +63,15 @@
       rescue:
         - set_fact: test_failed=true
 
+    - block:
+      - include_role:
+          name: iosxr_logging
+        when: "limit_to in ['*', 'iosxr_logging']"
+      rescue:
+        - set_fact: test_failed=true
+
 ###########
     - name: Has any previous test failed?
       fail:
         msg: "One or more tests failed, check log for details"
-      when: test_failed
\ No newline at end of file
+      when: test_failed
diff --git a/test/integration/targets/iosxr_logging/defaults/main.yaml b/test/integration/targets/iosxr_logging/defaults/main.yaml
new file mode 100644
index 00000000000..9ef5ba51651
--- /dev/null
+++ b/test/integration/targets/iosxr_logging/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+testcase: "*"
+test_items: []
diff --git a/test/integration/targets/iosxr_logging/meta/main.yaml b/test/integration/targets/iosxr_logging/meta/main.yaml
new file mode 100644
index 00000000000..d4da833dd50
--- /dev/null
+++ b/test/integration/targets/iosxr_logging/meta/main.yaml
@@ -0,0 +1,2 @@
+dependencies:
+  - prepare_iosxr_tests
diff --git a/test/integration/targets/iosxr_logging/tasks/cli.yaml b/test/integration/targets/iosxr_logging/tasks/cli.yaml
new file mode 100644
index 00000000000..46d86dd6988
--- /dev/null
+++ b/test/integration/targets/iosxr_logging/tasks/cli.yaml
@@ -0,0 +1,16 @@
+---
+- 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 case
+  include: "{{ test_case_to_run }}"
+  with_items: "{{ test_items }}"
+  loop_control:
+    loop_var: test_case_to_run
diff --git a/test/integration/targets/iosxr_logging/tasks/main.yaml b/test/integration/targets/iosxr_logging/tasks/main.yaml
new file mode 100644
index 00000000000..415c99d8b12
--- /dev/null
+++ b/test/integration/targets/iosxr_logging/tasks/main.yaml
@@ -0,0 +1,2 @@
+---
+- { include: cli.yaml, tags: ['cli'] }
diff --git a/test/integration/targets/iosxr_logging/tests/cli/basic.yaml b/test/integration/targets/iosxr_logging/tests/cli/basic.yaml
new file mode 100644
index 00000000000..1527e3c9365
--- /dev/null
+++ b/test/integration/targets/iosxr_logging/tests/cli/basic.yaml
@@ -0,0 +1,90 @@
+---
+- name: Set up host logging
+  iosxr_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: present
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.chaned == true'
+      - '"logging hostnameprefix 172.16.0.1" in result.commands'
+      - '"logging facility local7" in result.commands'
+
+- name: Set up host logging again (idempotent)
+  iosxr_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: present
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == false'
+
+- name: Delete/disable host logging
+  iosxr_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: absent
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"no logging hostnameprefix 172.16.0.1" in result.commands'
+
+- name: Delete/disable host logging (idempotent)
+  iosxr_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: absent
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == false'
+
+- name: Console logging with level warnings
+  iosxr_logging:
+    dest: console
+    level: warnings
+    state: present
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"logging console warnings" in result.commands'
+
+- name: Configure Buffer size
+  iosxr_logging:
+    dest: buffered
+    size: 4800000
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"logging buffered 4800000" in result.commands'
+
+- name: remove logging as collection tearDown
+  iosxr_logging:
+    aggregate:
+      - { dest: console, level: warnings, state: absent }
+      - { dest: buffered, size: 4800000, state: absent }
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"no logging console" in result.commands'
+      - '"no logging buffered" in result.commands'
diff --git a/test/integration/targets/net_logging/tests/cli/basic.yaml b/test/integration/targets/net_logging/tests/cli/basic.yaml
index 54b7da79742..45768e05f6a 100644
--- a/test/integration/targets/net_logging/tests/cli/basic.yaml
+++ b/test/integration/targets/net_logging/tests/cli/basic.yaml
@@ -8,3 +8,6 @@
 
 - include: "{{ role_path }}/tests/ios/basic.yaml"
   when: hostvars[inventory_hostname]['ansible_network_os'] == 'ios'
+
+- include: "{{ role_path }}/tests/iosxr/basic.yaml"
+  when: hostvars[inventory_hostname]['ansible_network_os'] == 'iosxr'
diff --git a/test/integration/targets/net_logging/tests/iosxr/basic.yaml b/test/integration/targets/net_logging/tests/iosxr/basic.yaml
new file mode 100644
index 00000000000..d563487914f
--- /dev/null
+++ b/test/integration/targets/net_logging/tests/iosxr/basic.yaml
@@ -0,0 +1,90 @@
+---
+- name: Set up host logging
+  net_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: present
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.chaned == true'
+      - '"logging hostnameprefix 172.16.0.1" in result.commands'
+      - '"logging facility local7" in result.commands'
+
+- name: Set up host logging again (idempotent)
+  net_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: present
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == false'
+
+- name: Delete/disable host logging
+  net_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: absent
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"no logging hostnameprefix 172.16.0.1" in result.commands'
+
+- name: Delete/disable host logging (idempotent)
+  net_logging:
+    dest: hostnameprefix
+    name: 172.16.0.1
+    state: absent
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == false'
+
+- name: Console logging with level warnings
+  net_logging:
+    dest: console
+    level: warnings
+    state: present
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"logging console warnings" in result.commands'
+
+- name: Configure monitor logging
+  net_logging:
+    dest: monitor
+    level: debugging
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"logging monitor debugging" in result.commands'
+
+- name: remove logging as collection tearDown
+  net_logging:
+    aggregate:
+      - { dest: console, level: warnings, state: absent }
+      - { dest: monitor, level: debuggning, state: absent }
+    provider: "{{ cli }}"
+  register: result
+
+- assert:
+    that:
+      - 'result.changed == true'
+      - '"no logging console" in result.commands'
+      - '"no logging monitor" in result.commands'