[lxc-devel] [pylxd/master] Enable decoding control over stdout/stderr on execute

ajkavanagh on Github lxc-bot at linuxcontainers.org
Tue Mar 20 17:14:43 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 788 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20180320/9f8019ec/attachment.bin>
-------------- next part --------------
From 201f0e2d61fa3d77eb8485c0594542c74c31bee3 Mon Sep 17 00:00:00 2001
From: Alex Kavanagh <alex at ajkavanagh.co.uk>
Date: Tue, 20 Mar 2018 17:11:50 +0000
Subject: [PATCH] Enable decoding control over stdout/stderr on execute

This patch adds support to control the decoding of the stdout
and stderr when executing commands in a container.  It is possible to
force a particular decode, or just get the raw buffers.  The existing
behaviour of trying to decode to utf-8 is retained if no decode or
encoding paramters are passed to the execute method.

Tests are in the integration/test_containers.py file as it's tricky to
test properly in a unit test.

Signed-off-by: Alex Kavanagh <alex at ajkavanagh.co.uk>
---
 integration/test_containers.py | 24 +++++++++++++++++++++
 pylxd/models/container.py      | 47 +++++++++++++++++++++++++++++++++++-------
 2 files changed, 63 insertions(+), 8 deletions(-)

diff --git a/integration/test_containers.py b/integration/test_containers.py
index 75c6df8..ae68109 100644
--- a/integration/test_containers.py
+++ b/integration/test_containers.py
@@ -155,6 +155,30 @@ def test_execute(self):
         self.assertEqual('test\n', result.stdout)
         self.assertEqual('', result.stderr)
 
+    def test_execute_no_decode(self):
+        """A command is executed on the container that isn't utf-8 decodable
+        """
+        self.container.start(wait=True)
+        self.addCleanup(self.container.stop, wait=True)
+
+        result = self.container.execute(['printf', '\\xff'], decode=None)
+
+        self.assertEqual(0, result.exit_code)
+        self.assertEqual(b'\xff', result.stdout)
+        self.assertEqual(b'', result.stderr)
+
+    def test_execute_force_decode(self):
+        """A command is executed and force output to ascii"""
+        self.container.start(wait=True)
+        self.addCleanup(self.container.stop, wait=True)
+
+        result = self.container.execute(['printf', 'qu\\xe9'], decode=True,
+                                        encoding='latin1')
+
+        self.assertEqual(0, result.exit_code)
+        self.assertEqual('qué', result.stdout)
+        self.assertEqual('', result.stderr)
+
     def test_publish(self):
         """A container is published."""
         image = self.container.publish(wait=True)
diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index ca03c2a..4034cb5 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -255,11 +255,26 @@ def unfreeze(self, timeout=30, force=True, wait=False):
                                force=force,
                                wait=wait)
 
-    def execute(self, commands, environment={}):
+    def execute(self, commands, environment={}, encoding=None, decode=True):
         """Execute a command on the container.
 
         In pylxd 2.2, this method will be renamed `execute` and the existing
         `execute` method removed.
+
+        :param commands: The command and arguments as a list of strings
+        :type commands: [str]
+        :param environment: The environment variables to pass with the command
+        :type environment: {str: str}
+        :param encoding: The encoding to use for stdout/stderr if the param
+            decode is True.  If encoding is None, then no override is
+            performed and whatever the existing encoding from LXD is used.
+        :type encoding: str
+        :param decode: Whether to decode the stdout/stderr or just return the
+            raw buffers.
+        :type decode: bool
+        :raises ValueError: if the ws4py library is not installed.
+        :returns: The return value, stdout and stdin
+        :rtype: _ContainerExecuteResult() namedtuple
         """
         if not _ws4py_installed:
             raise ValueError(
@@ -283,11 +298,13 @@ def execute(self, commands, environment={}):
             stdin.resource = '{}?secret={}'.format(parsed.path, fds['0'])
             stdin.connect()
             stdout = _CommandWebsocketClient(
-                manager, self.client.websocket_url)
+                manager, self.client.websocket_url,
+                encoding=encoding, decode=decode)
             stdout.resource = '{}?secret={}'.format(parsed.path, fds['1'])
             stdout.connect()
             stderr = _CommandWebsocketClient(
-                manager, self.client.websocket_url)
+                manager, self.client.websocket_url,
+                encoding=encoding, decode=decode)
             stderr.resource = '{}?secret={}'.format(parsed.path, fds['2'])
             stderr.connect()
 
@@ -383,8 +400,15 @@ def publish(self, public=False, wait=False):
 
 
 class _CommandWebsocketClient(WebSocketBaseClient):  # pragma: no cover
+    """Handle a websocket for container.execute(...) and manage decoding of the
+    returned values depending on 'decode' and 'encoding' parameters.
+    """
+
     def __init__(self, manager, *args, **kwargs):
         self.manager = manager
+        self.decode = kwargs.pop('decode', True)
+        self.encoding = kwargs.pop('encoding', None)
+        self.message_encoding = None
         super(_CommandWebsocketClient, self).__init__(*args, **kwargs)
 
     def handshake_ok(self):
@@ -392,14 +416,21 @@ def handshake_ok(self):
         self.buffer = []
 
     def received_message(self, message):
-        if message.encoding:
-            self.buffer.append(message.data.decode(message.encoding))
-        else:
-            self.buffer.append(message.data.decode('utf-8'))
+        if message.encoding and self.message_encoding is None:
+            self.message_encoding = message.encoding
+        self.buffer.append(message.data)
 
     @property
     def data(self):
-        return ''.join(self.buffer)
+        buffer = b''.join(self.buffer)
+        if self.decode:
+            if self.encoding:
+                return buffer.decode(self.encoding)
+            if self.message_encoding:
+                return buffer.decode(self.message_encoding)
+            # This is the backwards compatible "always decode to utf-8"
+            return buffer.decode('utf-8')
+        return buffer
 
 
 class _StdinWebsocket(WebSocketBaseClient):  # pragma: no cover


More information about the lxc-devel mailing list