From 4ab4b6698d0ae055e100538c98a7fb5ecdfac4b9 Mon Sep 17 00:00:00 2001
From: Peter Sprygada <psprygada@ansible.com>
Date: Sun, 21 Aug 2016 08:57:49 -0400
Subject: [PATCH] updates nxos shared module refactor

This commit updates the nxos transport shared plugins for
2.2.  This includes updates to both Cli and Nxapi.  This commit
also includes the nxos_config action plugin
---
 lib/ansible/module_utils/nxos.py          | 194 ++++++++++------------
 lib/ansible/plugins/action/nxos_config.py |  28 ++++
 2 files changed, 120 insertions(+), 102 deletions(-)
 create mode 100644 lib/ansible/plugins/action/nxos_config.py

diff --git a/lib/ansible/module_utils/nxos.py b/lib/ansible/module_utils/nxos.py
index 49c3652ecf..32dfd9be26 100644
--- a/lib/ansible/module_utils/nxos.py
+++ b/lib/ansible/module_utils/nxos.py
@@ -18,54 +18,19 @@
 #
 
 import re
+import collections
 
-from ansible.module_utils.basic import json, json_dict_bytes_to_unicode
-from ansible.module_utils.network import NetCli, NetworkError, ModuleStub
+from ansible.module_utils.basic import json, get_exception
+from ansible.module_utils.network import NetworkModule, NetworkError, ModuleStub
 from ansible.module_utils.network import add_argument, register_transport, to_list
+from ansible.module_utils.shell import CliBase
 from ansible.module_utils.netcfg import NetworkConfig
+from ansible.module_utils.netcli import Command
 from ansible.module_utils.urls import fetch_url, url_argument_spec
 
-# temporary fix until modules are update.  to be removed before 2.2 final
-from ansible.module_utils.network import get_module
-
 add_argument('use_ssl', dict(default=False, type='bool'))
 add_argument('validate_certs', dict(default=True, type='bool'))
 
-def argument_spec():
-    return dict(
-        # config options
-        running_config=dict(aliases=['config']),
-        save_config=dict(type='bool', default=False, aliases=['save']),
-    )
-nxos_argument_spec = argument_spec()
-
-def get_config(module):
-    config = module.params['running_config']
-    if not config:
-        config = module.config.get_config(include_defaults=True)
-    return NetworkConfig(indent=2, contents=config)
-
-def load_config(module, candidate):
-    config = get_config(module)
-
-    commands = candidate.difference(config)
-    commands = [str(c).strip() for c in commands]
-
-    save_config = module.params['save_config']
-
-    result = dict(changed=False)
-
-    if commands:
-        if not module.check_mode:
-            module.config(commands)
-            if save_config:
-                module.config.save_config()
-
-        result['changed'] = True
-        result['updates'] = commands
-
-    return result
-
 
 class Nxapi(object):
 
@@ -85,7 +50,9 @@ class Nxapi(object):
 
     def _error(self, msg, **kwargs):
         self._nxapi_auth = None
-        raise NetworkError(msg, url=self.url)
+        if 'url' not in kwargs:
+            kwargs['url'] = self.url
+        raise NetworkError(msg, **kwargs)
 
     def _get_body(self, commands, output, version='1.0', chunk='0', sid=None):
         """Encodes a NXAPI JSON request message
@@ -106,7 +73,7 @@ class Nxapi(object):
             'chunk': chunk,
             'sid': sid,
             'input': commands,
-            'output_format': output
+            'output_format': 'json'
         }
 
         return dict(ins_api=msg)
@@ -136,43 +103,79 @@ class Nxapi(object):
         self._connected = False
 
     def execute(self, commands, output=None, **kwargs):
-        """Send commands to the device.
-        """
-        commands = to_list(commands)
+        commands = collections.deque(commands)
         output = output or self.default_output
 
-        data = self._get_body(commands, output)
-        data = self._jsonify(data)
+        # only 10 commands can be encoded in each request
+        # messages sent to the remote device
+        stack = list()
+        requests = list()
+
+        while commands:
+            stack.append(commands.popleft())
+            if len(stack) == 10:
+                data = self._get_body(stack, output)
+                data = self._jsonify(data)
+                requests.append(data)
+                stack = list()
+
+        if stack:
+            data = self._get_body(stack, output)
+            data = self._jsonify(data)
+            requests.append(data)
 
         headers = {'Content-Type': 'application/json'}
-        if self._nxapi_auth:
-            headers['Cookie'] = self._nxapi_auth
-
-        response, headers = fetch_url(
-            self.url_args, self.url, data=data, headers=headers, method='POST'
-        )
-        self._nxapi_auth = headers.get('set-cookie')
-
-        if headers['status'] != 200:
-            self._error(**headers)
-
-        try:
-            response = json.loads(response.read())
-        except ValueError:
-            raise NetworkError(msg='unable to load repsonse from device')
-
         result = list()
 
-        output = response['ins_api']['outputs']['output']
-        for item in to_list(output):
-            if item['code'] != '200':
-                self._error(**item)
-            else:
-                result.append(item['body'])
+        for req in requests:
+            if self._nxapi_auth:
+                headers['Cookie'] = self._nxapi_auth
+
+            response, headers = fetch_url(
+                self.url_args, self.url, data=data, headers=headers, method='POST'
+            )
+            self._nxapi_auth = headers.get('set-cookie')
+
+            if headers['status'] != 200:
+                self._error(**headers)
+
+            try:
+                response = json.loads(response.read())
+            except ValueError:
+                raise NetworkError(msg='unable to load response from device')
+
+            output = response['ins_api']['outputs']['output']
+            for item in to_list(output):
+                if item['code'] != '200':
+                    self._error(output=output, **item)
+                else:
+                    result.append(item['body'])
 
         return result
 
-    ### implemented by network.Config ###
+    ### implemention of netcli.Cli ###
+
+    def run_commands(self, commands):
+        output = None
+        cmds = list()
+        responses = list()
+
+        for cmd in commands:
+            if output and output != cmd.output:
+                responses.extend(self.execute(cmds, output=output))
+                cmds = list()
+            output = cmd.output
+            cmds.append(str(cmd))
+
+        if cmds:
+            responses.extend(self.execute(cmds, output=output))
+
+        return responses
+
+
+    ### end of netcli.Cli ###
+
+    ### implemention of netcfg.Config ###
 
     def configure(self, commands):
         commands = to_list(commands)
@@ -184,17 +187,13 @@ class Nxapi(object):
             cmd += ' all'
         return self.execute([cmd], output='text')[0]
 
-    def load_config(self, **kwargs):
-        raise NotImplementedError
+    def load_config(self, config, **kwargs):
+        return self.configure(config)
 
-    def replace_config(self, **kwargs):
-        raise NotImplementedError
+    def save_config(self, **kwargs):
+        self.execute(['copy running-config startup-config'])
 
-    def commit_config(self, **kwargs):
-        raise NotImplementedError
-
-    def abort_config(self, **kwargs):
-        raise NotImplementedError
+    ### end netcfg.Config ###
 
     def _jsonify(self, data):
         for encoding in ("utf-8", "latin-1"):
@@ -210,10 +209,12 @@ class Nxapi(object):
             except UnicodeDecodeError:
                 continue
         self._error(msg='Invalid unicode encoding encountered')
+
 Nxapi = register_transport('nxapi')(Nxapi)
 
 
-class Cli(NetCli):
+class Cli(CliBase):
+
     NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
 
     CLI_PROMPTS_RE = [
@@ -238,7 +239,7 @@ class Cli(NetCli):
         super(Cli, self).connect(params, kickstart=False, **kwargs)
         self.shell.send('terminal length 0')
 
-    ### implementation of network.Cli ###
+    ### implementation of netcli.Cli ###
 
     def run_commands(self, commands):
         cmds = list(prepare_commands(commands))
@@ -254,13 +255,7 @@ class Cli(NetCli):
                     )
         return responses
 
-    ### implemented by network.Config ###
-
-    def get_config(self, **kwargs):
-        cmd = 'show running-config'
-        if kwargs.get('include_defaults'):
-            cmd += ' all'
-        return self.execute([cmd])[0]
+    ### implemented by netcfg.Config ###
 
     def configure(self, commands, **kwargs):
         commands = prepare_config(commands)
@@ -268,20 +263,18 @@ class Cli(NetCli):
         responses.pop(0)
         return responses
 
-    def load_config(self):
-        raise NotImplementedError
+    def get_config(self, include_defaults=False, **kwargs):
+        cmd = 'show running-config'
+        if kwargs.get('include_defaults'):
+            cmd += ' all'
+        return self.execute([cmd])[0]
 
-    def replace_config(self, **kwargs):
-        raise NotImplementedError
-
-    def commit_config(self):
-        raise NotImplementedError
-
-    def abort_config(self):
-        raise NotImplementedError
+    def load_config(self, commands, **kwargs):
+        return self.configure(commands)
 
     def save_config(self):
         self.execute(['copy running-config startup-config'])
+
 Cli = register_transport('cli', default=True)(Cli)
 
 def prepare_config(commands):
@@ -290,12 +283,9 @@ def prepare_config(commands):
     commands.append('end')
     return commands
 
-
 def prepare_commands(commands):
     jsonify = lambda x: '%s | json' % x
     for cmd in to_list(commands):
         if cmd.output == 'json':
-            cmd = jsonify(cmd)
-        else:
-            cmd = str(cmd)
+            cmd.command = jsonify(cmd)
         yield cmd
diff --git a/lib/ansible/plugins/action/nxos_config.py b/lib/ansible/plugins/action/nxos_config.py
new file mode 100644
index 0000000000..ffcb0f057f
--- /dev/null
+++ b/lib/ansible/plugins/action/nxos_config.py
@@ -0,0 +1,28 @@
+#
+# Copyright 2015 Peter Sprygada <psprygada@ansible.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action import ActionBase
+from ansible.plugins.action.net_config import ActionModule as NetActionModule
+
+class ActionModule(NetActionModule, ActionBase):
+    pass
+
+