From b090b57eacc677468c8792cc3b55899b0b82a6cd Mon Sep 17 00:00:00 2001
From: Mike Wiebe <mwiebe@cisco.com>
Date: Wed, 6 Mar 2019 13:55:03 -0500
Subject: [PATCH] Module nxos logging fixes (#52883)

* nxos_logging fixes
---
 lib/ansible/module_utils/network/nxos/nxos.py | 32 +++++++++------
 .../modules/network/nxos/nxos_logging.py      | 41 ++++++++++++++++---
 lib/ansible/plugins/httpapi/nxos.py           | 32 +++++++++++----
 3 files changed, 78 insertions(+), 27 deletions(-)

diff --git a/lib/ansible/module_utils/network/nxos/nxos.py b/lib/ansible/module_utils/network/nxos/nxos.py
index b44156a0b01..ceedfea2ed1 100644
--- a/lib/ansible/module_utils/network/nxos/nxos.py
+++ b/lib/ansible/module_utils/network/nxos/nxos.py
@@ -301,14 +301,16 @@ class LocalNxapi:
         if isinstance(commands, (list, set, tuple)):
             commands = ' ;'.join(commands)
 
-        msg = {
-            'version': version,
-            'type': command_type,
-            'chunk': chunk,
-            'sid': sid,
-            'input': commands,
-            'output_format': 'json'
-        }
+        # Order should not matter but some versions of NX-OS software fail
+        # to process the payload properly if 'input' gets serialized before
+        # 'type' and the payload of 'input' contains the word 'type'.
+        msg = collections.OrderedDict()
+        msg['version'] = version
+        msg['type'] = command_type
+        msg['chunk'] = chunk
+        msg['sid'] = sid
+        msg['input'] = commands
+        msg['output_format'] = 'json'
 
         return dict(ins_api=msg)
 
@@ -448,7 +450,6 @@ class LocalNxapi:
             commands = 'config replace {0}'.format(replace)
 
         commands = to_list(commands)
-
         msg, msg_timestamps = self.send_request(commands, output='config', check_status=True,
                                                 return_error=return_error, opts=opts)
         if return_error:
@@ -664,13 +665,18 @@ class HttpApi:
             raise ValueError("commit comment is not supported")
 
     def read_module_context(self, module_key):
-        if self._module_context.get(module_key):
-            return self._module_context[module_key]
+        try:
+            module_context = self._connection.read_module_context(module_key)
+        except ConnectionError as exc:
+            self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
 
-        return None
+        return module_context
 
     def save_module_context(self, module_key, module_context):
-        self._module_context[module_key] = module_context
+        try:
+            self._connection.save_module_context(module_key, module_context)
+        except ConnectionError as exc:
+            self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
 
         return None
 
diff --git a/lib/ansible/modules/network/nxos/nxos_logging.py b/lib/ansible/modules/network/nxos/nxos_logging.py
index be3d451d3c5..e37a02f3e88 100644
--- a/lib/ansible/modules/network/nxos/nxos_logging.py
+++ b/lib/ansible/modules/network/nxos/nxos_logging.py
@@ -84,6 +84,9 @@ options:
   purge:
     description:
       - Remove any switch logging configuration that does not match what has been configured
+        Not supported for ansible_connection local.
+        All nxos_logging tasks must use the same ansible_connection type.
+
     type: bool
     default: no
     version_added: '2.8'
@@ -182,6 +185,7 @@ from ansible.module_utils.network.nxos.nxos import get_config, load_config, run_
 from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args, normalize_interface
 from ansible.module_utils.basic import AnsibleModule
 
+
 STATIC_CLI = {'link-enable': 'logging event link-status enable',
               'link-default': 'logging event link-status default',
               'trunk-enable': 'logging event trunk-status enable',
@@ -207,7 +211,7 @@ DEFAULT_LOGGING_LEVEL = {0: [],
 DEST_GROUP = ['console', 'logfile', 'module', 'monitor', 'server']
 
 
-def map_obj_to_commands(updates):
+def map_obj_to_commands(module, updates):
     commands = list()
     want, have = updates
 
@@ -295,8 +299,9 @@ def map_obj_to_commands(updates):
                         commands.append('logging level {0} {1}'.format(
                             w['facility'], STATIC_CLI[w['facility_link_status']]))
                     else:
-                        commands.append('logging level {0} {1}'.format(w['facility'],
-                                                                       w['facility_level']))
+                        if not match_facility_default(module, w['facility'], w['facility_level']):
+                            commands.append('logging level {0} {1}'.format(w['facility'],
+                                                                           w['facility_level']))
 
             if w['interface']:
                 commands.append('logging source-interface {0} {1}'.format(*split_interface(w['interface'])))
@@ -313,6 +318,30 @@ def map_obj_to_commands(updates):
     return commands
 
 
+def match_facility_default(module, facility, want_level):
+    ''' Check wanted facility to see if it matches current device default '''
+
+    matches_default = False
+    # Sample output from show logging level command
+    # Facility        Default Severity        Current Session Severity
+    # --------        ----------------        ------------------------
+    # bfd                     5                       5
+    #
+    # 0(emergencies)          1(alerts)       2(critical)
+    # 3(errors)               4(warnings)     5(notifications)
+    # 6(information)          7(debugging)
+
+    regexl = r'\S+\s+(\d+)\s+(\d+)'
+    cmd = {'command': 'show logging level {0}'.format(facility), 'output': 'text'}
+    facility_data = run_commands(module, cmd)
+    for line in facility_data[0].split('\n'):
+        mo = re.search(regexl, line)
+        if mo and int(mo.group(1)) == int(want_level) and int(mo.group(2)) == int(want_level):
+            matches_default = True
+
+    return matches_default
+
+
 def split_interface(interface):
     match = re.search(r'(\D+)(\S*)', interface, re.M)
     if match:
@@ -719,7 +748,7 @@ def main():
         timestamp=dict(choices=['microseconds', 'milliseconds', 'seconds']),
         state=dict(default='present', choices=['present', 'absent']),
         aggregate=dict(type='list'),
-        purge=dict(default=False, type='bool')
+        purge=dict(default=False, type='bool'),
     )
 
     argument_spec.update(nxos_argument_spec)
@@ -742,7 +771,7 @@ def main():
     merged_wants = merge_wants(read_module_context(module), want)
     have = map_config_to_obj(module)
 
-    commands = map_obj_to_commands((want, have))
+    commands = map_obj_to_commands(module, (want, have))
     result['commands'] = commands
 
     if commands:
@@ -753,7 +782,7 @@ def main():
     save_module_context(module, merged_wants)
 
     if module.params.get('purge'):
-        pcommands = map_obj_to_commands((outliers(have, merged_wants), have))
+        pcommands = map_obj_to_commands(module, (outliers(have, merged_wants), have))
         if pcommands:
             if not module.check_mode:
                 load_config(module, pcommands)
diff --git a/lib/ansible/plugins/httpapi/nxos.py b/lib/ansible/plugins/httpapi/nxos.py
index 44a7345d834..2f8c40ba27b 100644
--- a/lib/ansible/plugins/httpapi/nxos.py
+++ b/lib/ansible/plugins/httpapi/nxos.py
@@ -17,6 +17,7 @@ version_added: "2.6"
 
 import json
 import re
+import collections
 
 from ansible.module_utils._text import to_text
 from ansible.module_utils.connection import ConnectionError
@@ -36,6 +37,18 @@ class HttpApi(HttpApiBase):
     def __init__(self, *args, **kwargs):
         super(HttpApi, self).__init__(*args, **kwargs)
         self._device_info = None
+        self._module_context = {}
+
+    def read_module_context(self, module_key):
+        if self._module_context.get(module_key):
+            return self._module_context[module_key]
+
+        return None
+
+    def save_module_context(self, module_key, module_context):
+        self._module_context[module_key] = module_context
+
+        return None
 
     def send_request(self, data, **message_kwargs):
         output = None
@@ -201,12 +214,15 @@ def request_builder(commands, output, version='1.0', chunk='0', sid=None):
     if isinstance(commands, (list, set, tuple)):
         commands = ' ;'.join(commands)
 
-    msg = {
-        'version': version,
-        'type': command_type,
-        'chunk': chunk,
-        'sid': sid,
-        'input': commands,
-        'output_format': 'json'
-    }
+    # Order should not matter but some versions of NX-OS software fail
+    # to process the payload properly if 'input' gets serialized before
+    # 'type' and the payload of 'input' contains the word 'type'.
+    msg = collections.OrderedDict()
+    msg['version'] = version
+    msg['type'] = command_type
+    msg['chunk'] = chunk
+    msg['sid'] = sid
+    msg['input'] = commands
+    msg['output_format'] = 'json'
+
     return json.dumps(dict(ins_api=msg))