ssh: don't quote ~ in remotepaths 33/26333/2
authorRoss Brattain <ross.b.brattain@intel.com>
Wed, 21 Dec 2016 05:40:52 +0000 (21:40 -0800)
committerRoss Brattain <ross.b.brattain@intel.com>
Wed, 21 Dec 2016 05:47:37 +0000 (21:47 -0800)
~ is not expanded in double quotes, so we have a dilemma.

We need to quote in order to preserve filenames with spaces,
but we have to make sure we don't quote the ~ so it can be expanded.

To resolve this we use a regex to search for tidle-prefixes
and excluded them from quotes.

Added unittests for the cases:
path with tilde
path with space
path with tilde and space

see bash man page for details of tidle expansion

Tilde Expansion

    If  a word begins with an unquoted tilde character (`~'), all of the
characters preceding the first unquoted slash (or all characters, if there is
no unquoted slash) are considered a tilde-prefix.  If none of the characters in
the tilde-prefix are quoted, the characters in the tilde-prefix following the
tilde are treated as a possible login name.  If this login name is the null
string, the tilde is replaced with the value of the shell parameter HOME.  If
HOME is unset, the home directory of the user executing the shell is
substituted instead.  Otherwise, the  tilde-prefix  is  replaced  with  the
home directory associated with the specified login name.

JIRA: YARDSTICK-501

Change-Id: I324be20aba0dbd50434fbd8081685c598ebd8a84
Signed-off-by: Ross Brattain <ross.b.brattain@intel.com>
tests/unit/test_ssh.py
yardstick/ssh.py

index 8b828ed..045ac0f 100644 (file)
@@ -310,12 +310,38 @@ class SSHRunTestCase(unittest.TestCase):
 
     @mock.patch("yardstick.ssh.open", create=True)
     def test__put_file_shell(self, mock_open):
-        self.test_client.run = mock.Mock()
-        self.test_client._put_file_shell("localfile", "remotefile", 0o42)
+        with mock.patch.object(self.test_client, "run") as run_mock:
+            self.test_client._put_file_shell("localfile", "remotefile", 0o42)
+            run_mock.assert_called_once_with(
+                'cat > "remotefile"&& chmod -- 042 "remotefile"',
+                stdin=mock_open.return_value.__enter__.return_value)
 
-        self.test_client.run.assert_called_once_with(
-            'cat > remotefile && chmod -- 042 remotefile',
-            stdin=mock_open.return_value.__enter__.return_value)
+    @mock.patch("yardstick.ssh.open", create=True)
+    def test__put_file_shell_space(self, mock_open):
+        with mock.patch.object(self.test_client, "run") as run_mock:
+            self.test_client._put_file_shell("localfile",
+                                             "filename with space", 0o42)
+            run_mock.assert_called_once_with(
+                'cat > "filename with space"&& chmod -- 042 "filename with '
+                'space"',
+                stdin=mock_open.return_value.__enter__.return_value)
+
+    @mock.patch("yardstick.ssh.open", create=True)
+    def test__put_file_shell_tilde(self, mock_open):
+        with mock.patch.object(self.test_client, "run") as run_mock:
+            self.test_client._put_file_shell("localfile", "~/remotefile", 0o42)
+            run_mock.assert_called_once_with(
+                'cat > ~/"remotefile"&& chmod -- 042 ~/"remotefile"',
+                stdin=mock_open.return_value.__enter__.return_value)
+
+    @mock.patch("yardstick.ssh.open", create=True)
+    def test__put_file_shell_tilde_spaces(self, mock_open):
+        with mock.patch.object(self.test_client, "run") as run_mock:
+            self.test_client._put_file_shell("localfile", "~/file with space",
+                                             0o42)
+            run_mock.assert_called_once_with(
+                'cat > ~/"file with space"&& chmod -- 042 ~/"file with space"',
+                stdin=mock_open.return_value.__enter__.return_value)
 
     @mock.patch("yardstick.ssh.os.stat")
     def test__put_file_sftp(self, mock_stat):
index 3081001..927ca94 100644 (file)
@@ -66,6 +66,7 @@ import os
 import select
 import socket
 import time
+import re
 
 import logging
 import paramiko
@@ -252,7 +253,7 @@ class SSH(object):
                 raise SSHError("Socket error.")
 
         exit_status = session.recv_exit_status()
-        if 0 != exit_status and raise_on_error:
+        if exit_status != 0 and raise_on_error:
             fmt = "Command '%(cmd)s' failed with exit_status %(status)d."
             details = fmt % {"cmd": cmd, "status": exit_status}
             if stderr_data:
@@ -311,17 +312,21 @@ class SSH(object):
                 mode = 0o777 & os.stat(localpath).st_mode
             sftp.chmod(remotepath, mode)
 
+    TILDE_EXPANSIONS_RE = re.compile("(^~[^/]*/)?(.*)")
+
     def _put_file_shell(self, localpath, remotepath, mode=None):
         # quote to stop wordpslit
-        cmd = ['cat > %s' % remotepath]
+        tilde, remotepath = self.TILDE_EXPANSIONS_RE.match(remotepath).groups()
+        if not tilde:
+            tilde = ''
+        cmd = ['cat > %s"%s"' % (tilde, remotepath)]
         if mode is not None:
             # use -- so no options
-            cmd.append('chmod -- 0%o %s' % (mode, remotepath))
+            cmd.append('chmod -- 0%o %s"%s"' % (mode, tilde, remotepath))
 
         with open(localpath, "rb") as localfile:
             # only chmod on successful cat
-            cmd = " && ".join(cmd)
-            self.run(cmd, stdin=localfile)
+            self.run("&& ".join(cmd), stdin=localfile)
 
     def put_file(self, localpath, remotepath, mode=None):
         """Copy specified local file to the server.
@@ -330,7 +335,6 @@ class SSH(object):
         :param remotepath:  Remote filename.
         :param mode:        Permissions to set after upload
         """
-        import socket
         try:
             self._put_file_sftp(localpath, remotepath, mode=mode)
         except (paramiko.SSHException, socket.error):