Repository: Juniper/py-junos-eznc

Issue:
Config.lock() return exception when normalize is on
>>> dev=Device('172.27.62.164',user='root',passwd='lab123',normalize=True)
>>> dev.open()
Device(172.27.62.164)
>>> cu=Config(dev)
>>> cu.lock()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/root/pyez2.0/lib/python2.7/site-packages/jnpr/junos/utils/config.py", line 467, in lock
    raise LockError(rsp=JXML.remove_namespaces(err.xml))
AttributeError: 'exceptions.AttributeError' object has no attribute 'xml'

If normalize is not enabled, no problem
This problem is introduced in pyez 2.0

Discussion:

Patch:
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 1069b0df..00000000
--- a/.dockerignore
+++ /dev/null
@@ -1,10 +0,0 @@
-/docs/*
-/tests/*
-/vagrant/*
-INSTALL*
-COPYRIGHT
-LICENSE
-MANIFEST.md
-RELEASE-NOTES.md
-development.txt
-docreq.txt
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 11f1f000..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,30 +0,0 @@
-FROM alpine:3.4
-
-MAINTAINER ssteiner@juniper.net
-
-RUN mkdir /source \
-    && mkdir /scripts
-
-WORKDIR /source
-
-## Copy project inside the container
-ADD setup.py setup.py
-ADD requirements.txt requirements.txt
-ADD lib lib
-
-## Install dependancies and Pyez
-RUN apk update \
-    && apk upgrade \
-    && apk add build-base gcc g++ make python-dev py-pip py-lxml \
-    libxslt-dev libxml2-dev libffi-dev openssl-dev curl \
-    && pip install -r requirements.txt \
-    && apk del -r --purge gcc make g++ \
-    && python setup.py install \
-    && rm -rf /source/* \
-    && rm -rf /var/cache/apk/*
-
-WORKDIR /scripts
-
-VOLUME ["$PWD:/scripts"]
-
-CMD sh
diff --git a/lib/jnpr/junos/device.py b/lib/jnpr/junos/device.py
index 9c335e50..c2bd3712 100644
--- a/lib/jnpr/junos/device.py
+++ b/lib/jnpr/junos/device.py
@@ -919,7 +919,7 @@ class Device(_Connection):
             #    protocol: operation-failed
             #    error: device asdf not found
             # </rpc-reply>
-            if rpc_rsp_e.text.strip() is not '':
+            if rpc_rsp_e.text is not None and rpc_rsp_e.text.strip() is not '':
                 return rpc_rsp_e
             # no children, so assume it means we are OK
             return True
diff --git a/lib/jnpr/junos/utils/scp.py b/lib/jnpr/junos/utils/scp.py
index 8a150d6c..adc49fd3 100644
--- a/lib/jnpr/junos/utils/scp.py
+++ b/lib/jnpr/junos/utils/scp.py
@@ -73,8 +73,11 @@ class SCP(object):
 
         .. note:: This method uses the same username/password authentication
                    credentials as used by :class:`jnpr.junos.device.Device`.
-                   It can also use ``ssh_private_key_file`` option if provided
-                   to the :class:`jnpr.junos.device.Device` 
+
+        .. warning:: The :class:`jnpr.junos.device.Device` ``ssh_private_key_file``
+                     option is currently **not** supported.
+
+        .. todo:: add support for ``ssh_private_key_file``.
 
         :returns: SCPClient object
         """
@@ -89,7 +92,6 @@ class SCP(object):
         # through a jumphost.
 
         config = {}
-        kwargs = {}
         ssh_config = getattr(junos, '_sshconf_path')
         if ssh_config:
             config = paramiko.SSHConfig()
@@ -99,9 +101,6 @@ class SCP(object):
         if config.get("proxycommand"):
             sock = paramiko.proxy.ProxyCommand(config.get("proxycommand"))
 
-        if self._junos._ssh_private_key_file is not None:
-            kwargs['key_filename']=self._junos._ssh_private_key_file
-
         self._ssh.connect(hostname=junos._hostname,
                           port=(
                               22, int(
@@ -109,7 +108,7 @@ class SCP(object):
                               junos._hostname == 'localhost'],
                           username=junos._auth_user,
                           password=junos._auth_password,
-                          sock=sock, **kwargs
+                          sock=sock
                           )
         return SCPClient(self._ssh.get_transport(), **scpargs)
 
diff --git a/lib/jnpr/junos/utils/start_shell.py b/lib/jnpr/junos/utils/start_shell.py
index 07956c0e..67a0e1e1 100644
--- a/lib/jnpr/junos/utils/start_shell.py
+++ b/lib/jnpr/junos/utils/start_shell.py
@@ -1,7 +1,6 @@
 import paramiko
 from select import select
 import re
-import datetime
 
 _JUNOS_PROMPT = '> '
 _SHELL_PROMPT = '(%|#)\s'
@@ -22,28 +21,20 @@ class StartShell(object):
 
     """
 
-    def __init__(self, nc, timeout=30):
+    def __init__(self, nc):
         """
         Utility Constructor
 
         :param Device nc: The Device object
-
-        :param int timeout:
-          Timeout value in seconds to wait for expected string/pattern.
         """
         self._nc = nc
-        self.timeout = timeout
 
-    def wait_for(self, this=_SHELL_PROMPT, timeout=0):
+    def wait_for(self, this=_SHELL_PROMPT):
         """
         Wait for the result of the command, expecting **this** prompt.
 
         :param str this: expected string/pattern.
 
-        :param int timeout:
-          Timeout value in seconds to wait for expected string/pattern.
-          If not specified defaults to self.timeout.
-
         :returns: resulting string of data in a list
         :rtype: list
 
@@ -51,15 +42,12 @@ class StartShell(object):
         """
         chan = self._chan
         got = []
-        timeout = timeout or self.timeout
-        timeout = datetime.datetime.now()+datetime.timedelta(
-            seconds=timeout)
-        while timeout > datetime.datetime.now():
+        while True:
             rd, wr, err = select([chan], [], [], _SELECT_WAIT)
             if rd:
                 data = chan.recv(_RECVSZ)
                 if isinstance(data, bytes):
-                    data = data.decode('utf-8', 'replace')
+                    data = data.decode('utf-8')
                 got.append(data)
                 if re.search(r'{0}\s?$'.format(this), data):
                     break
@@ -106,7 +94,7 @@ class StartShell(object):
         self._chan.close()
         self._client.close()
 
-    def run(self, command, this=_SHELL_PROMPT, timeout=0):
+    def run(self, command, this=_SHELL_PROMPT):
         """
         Run a shell command and wait for the response.  The return is a
         tuple. The first item is True/False if exit-code is 0.  The second
@@ -114,30 +102,22 @@ class StartShell(object):
 
         :param str command: the shell command to execute
         :param str this: the exected shell-prompt to wait for
-        :param int timeout:
-          Timeout value in seconds to wait for expected string/pattern (this).
-          If not specified defaults to self.timeout. This timeout is specific
-          to individual run call.
 
         :returns: (last_ok, result of the executed shell command (str) )
 
         .. note:: as a *side-effect* this method will set the ``self.last_ok``
                   property.  This property is set to ``True`` if ``$?`` is
-                  "0"; indicating the last shell command was successful else
-                  False
+                  "0"; indicating the last shell command was successful.
         """
-        timeout = timeout or self.timeout
         # run the command and capture the output
         self.send(command)
-        got = ''.join(self.wait_for(this, timeout))
-        self.last_ok = False
-        if this != _SHELL_PROMPT:
-            self.last_ok = re.search(r'{0}\s?$'.format(this), got) is not None
-        elif re.search(r'{0}\s?$'.format(_SHELL_PROMPT), got) is not None:
-            # use $? to get the exit code of the command
-            self.send('echo $?')
-            rc = ''.join(self.wait_for(_SHELL_PROMPT))
-            self.last_ok = rc.find('0') > 0
+        got = ''.join(self.wait_for(this))
+
+        # use $? to get the exit code of the command
+        self.send('echo $?')
+        rc = ''.join(self.wait_for(this))
+        self.last_ok = True if rc.find('0') > 0 else False
+
         return (self.last_ok, got)
 
     # -------------------------------------------------------------------------
@@ -150,4 +130,3 @@ class StartShell(object):
 
     def __exit__(self, exc_ty, exc_val, exc_tb):
         self.close()
-
diff --git a/lib/jnpr/junos/version.py b/lib/jnpr/junos/version.py
index 55ee6d04..95a27f1a 100644
--- a/lib/jnpr/junos/version.py
+++ b/lib/jnpr/junos/version.py
@@ -1,2 +1,2 @@
-VERSION = "2.0.1"
+VERSION = "2.0.0"
 DATE = "2016-Sept-01"
diff --git a/setup.py b/setup.py
index 25a7e84c..53fa4682 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@ if sys.version_info[:2] == (2, 6):
 setup(
     name="junos-eznc",
     namespace_packages=['jnpr'],
-    version="2.0.1",
+    version="2.0.0",
     author="Jeremy Schulman, Nitin Kumar",
     author_email="jnpr-community-netdev@juniper.net",
     description=("Junos 'EZ' automation for non-programmers"),


Test patch:
diff --git a/tests/unit/utils/test_scp.py b/tests/unit/utils/test_scp.py
index d72571c8..c0ddd22e 100644
--- a/tests/unit/utils/test_scp.py
+++ b/tests/unit/utils/test_scp.py
@@ -99,22 +99,6 @@ class TestScp(unittest.TestCase):
         self.assertEqual(mock_scpclient.mock_calls[0][2]['progress'].__name__,
                          '_scp_progress')
 
-    @patch('ncclient.manager.connect')
-    @patch('paramiko.SSHClient.connect')
-    @patch('scp.SCPClient.put')
-    @patch('scp.SCPClient.__init__')
-    def test_ssh_private_key_file(self, mock_scpclient, mock_put,
-                                  mock_sshclient, mock_ncclient):
-        mock_scpclient.return_value = None
-        package = 'test.tgz'
-        dev = Device(host='1.1.1.1', user='user',
-                     ssh_private_key_file='/Users/test/testkey')
-        dev.open(gather_facts=False)
-        with SCP(dev) as scp:
-            scp.put(package)
-        self.assertEqual(mock_sshclient.mock_calls[0][2]['key_filename'],
-                         '/Users/test/testkey')
-
     @contextmanager
     def capture(self, command, *args, **kwargs):
         out, sys.stdout = sys.stdout, StringIO()
diff --git a/tests/unit/utils/test_start_shell.py b/tests/unit/utils/test_start_shell.py
index d429748d..eb6dcdca 100644
--- a/tests/unit/utils/test_start_shell.py
+++ b/tests/unit/utils/test_start_shell.py
@@ -42,7 +42,6 @@ class TestStartShell(unittest.TestCase):
     @patch('jnpr.junos.utils.start_shell.StartShell.wait_for')
     def test_startshell_run(self, mock_wait):
         self.shell._chan = MagicMock()
-        mock_wait.return_value = ["user % "]
         self.shell.run('ls')
         self.assertTrue(call.send('echo $?') in self.shell._chan.mock_calls)