[lxc-devel] [pylxd/master] Execute interactive

rockstar on Github lxc-bot at linuxcontainers.org
Tue Jun 7 17:12:50 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 727 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160607/7e442d97/attachment.bin>
-------------- next part --------------
From 19da1f99addad2e62972731fb3f56ef83f7bb183 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Sat, 4 Jun 2016 22:52:20 -0600
Subject: [PATCH 1/4] Fix lint

---
 pylxd/container.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index 3408f52..3bb5a22 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -59,7 +59,7 @@ def get(self, filepath):
         '_client',
         'architecture', 'config', 'created_at', 'devices', 'ephemeral',
         'expanded_config', 'expanded_devices', 'name', 'profiles', 'status'
-        ]
+    ]
 
     @classmethod
     def get(cls, client, name):
@@ -164,7 +164,7 @@ def _set_state(self, state, timeout=30, force=True, wait=False):
             'action': state,
             'timeout': timeout,
             'force': force
-            })
+        })
         if wait:
             self.wait_for_operation(response.json()['operation'])
             self.fetch()
@@ -253,7 +253,7 @@ def execute(self, commands, environment={}):
             'environment': environment,
             'wait-for-websocket': False,
             'interactive': False,
-            })
+        })
         operation_id = response.json()['operation']
         self.wait_for_operation(operation_id)
 

From dab53d11808f5f7def6c82d342d03cc5c645056c Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Tue, 7 Jun 2016 10:46:58 -0600
Subject: [PATCH 2/4] Add integration test

---
 integration/test_containers.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/integration/test_containers.py b/integration/test_containers.py
index 156f50e..cc9053e 100644
--- a/integration/test_containers.py
+++ b/integration/test_containers.py
@@ -147,4 +147,6 @@ def test_execute(self):
         self.container.start(wait=True)
         self.addCleanup(self.container.stop, wait=True)
 
-        self.container.execute('ls /')
+        stdout, stderr = self.container.execute(['echo', 'test'])
+
+        self.assertEqual('test\n', stdout)

From d5fe0d3edcc2d174eae5fed92bf1645d70531648 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Tue, 7 Jun 2016 10:47:37 -0600
Subject: [PATCH 3/4] Unify the websocket url logic

---
 pylxd/client.py | 27 ++++++++++++++++-----------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index 8f95a52..b9f214d 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -197,6 +197,21 @@ def __init__(self, endpoint=None, version='1.0', cert=None, verify=True):
         self.operations = managers.OperationManager(self)
         self.profiles = managers.ProfileManager(self)
 
+    @property
+    def websocket_url(self):
+        parsed = parse.urlparse(self.api._api_endpoint)
+        if parsed.scheme in ('http', 'https'):
+            host = parsed.netloc
+            if parsed.scheme == 'http':
+                scheme = 'ws'
+            else:
+                scheme = 'wss'
+        else:
+            scheme = 'ws+unix'
+            host = parse.unquote(parsed.netloc)
+        url = parse.urlunparse((scheme, host, '', '', '', ''))
+        return url
+
     def events(self, websocket_client=None):
         """Get a websocket client for getting events.
 
@@ -212,18 +227,8 @@ def events(self, websocket_client=None):
         if websocket_client is None:
             websocket_client = _WebsocketClient
 
+        client = websocket_client(self.websocket_url)
         parsed = parse.urlparse(self.api.events._api_endpoint)
-        if parsed.scheme in ('http', 'https'):
-            host = parsed.netloc
-            if parsed.scheme == 'http':
-                scheme = 'ws'
-            else:
-                scheme = 'wss'
-        else:
-            scheme = 'ws+unix'
-            host = parse.unquote(parsed.netloc)
-        url = parse.urlunparse((scheme, host, '', '', '', ''))
-        client = websocket_client(url)
         client.resource = parsed.path
 
         return client

From fa01cc8b9da79396fb199d16d84a5cd3a8de5375 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Tue, 7 Jun 2016 10:48:59 -0600
Subject: [PATCH 4/4] Fix execute

---
 pylxd/container.py            | 70 ++++++++++++++++++++++++++++++++++++++++---
 pylxd/tests/mock_lxd.py       | 18 ++++++++++-
 pylxd/tests/test_container.py | 25 ++++++++++++++++
 3 files changed, 108 insertions(+), 5 deletions(-)

diff --git a/pylxd/container.py b/pylxd/container.py
index 3bb5a22..72504cf 100644
--- a/pylxd/container.py
+++ b/pylxd/container.py
@@ -11,7 +11,12 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import time
+
 import six
+from six.moves.urllib import parse
+from ws4py.manager import WebSocketManager
+from ws4py.client import WebSocketBaseClient
 
 from pylxd import exceptions, managers, mixin
 from pylxd.deprecation import deprecated
@@ -247,15 +252,72 @@ def execute(self, commands, environment={}):
         # design, for now. It needs to grow the ability to return web sockets
         # and perform interactive functions.
         if isinstance(commands, six.string_types):
-            commands = [commands]
+            raise TypeError("First argument must be a list.")
         response = self._client.api.containers[self.name]['exec'].post(json={
             'command': commands,
             'environment': environment,
-            'wait-for-websocket': False,
+            'wait-for-websocket': True,
             'interactive': False,
         })
-        operation_id = response.json()['operation']
-        self.wait_for_operation(operation_id)
+
+        fds = response.json()['metadata']['metadata']['fds']
+        operation_id = response.json()['operation'].split('/')[-1]
+        parsed = parse.urlparse(self._client.api.operations[operation_id].websocket._api_endpoint)
+
+        manager = WebSocketManager()
+
+        stdin = _StdinWebsocket(manager, self._client.websocket_url)
+        stdin.resource = '{}?secret={}'.format(parsed.path, fds['0'])
+        stdin.connect()
+        stdout = _CommandWebsocketClient(manager, self._client.websocket_url)
+        stdout.resource = '{}?secret={}'.format(parsed.path, fds['1'])
+        stdout.connect()
+        stderr = _CommandWebsocketClient(manager, self._client.websocket_url)
+        stderr.resource = '{}?secret={}'.format(parsed.path, fds['2'])
+        stderr.connect()
+
+        manager.start()
+
+        while True:  # pragma: no cover
+            for websocket in manager.websockets.values():
+                if not websocket.terminated:
+                    break
+            else:
+                break
+            time.sleep(1)
+
+        return stdout.data, stderr.data
+
+
+class _CommandWebsocketClient(WebSocketBaseClient):  # pragma: no cover
+    def __init__(self, manager, *args, **kwargs):
+        self.manager = manager
+        super(_CommandWebsocketClient, self).__init__(*args, **kwargs)
+
+    def handshake_ok(self):
+        self.manager.add(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'))
+
+    @property
+    def data(self):
+        return ''.join(self.buffer)
+
+
+class _StdinWebsocket(WebSocketBaseClient):  # pragma: no cover
+    """A websocket client for handling stdin."""
+    def __init__(self, manager, *args, **kwargs):
+        self.manager = manager
+        super(_StdinWebsocket, self).__init__(*args, **kwargs)
+
+    def handshake_ok(self):
+        self.manager.add(self)
+        self.close()
 
 
 class Snapshot(mixin.Waitable, mixin.Marshallable):
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 38f4930..4eb09f3 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -129,7 +129,23 @@ def profile_GET(request, context):
         'method': 'DELETE',
         'url': r'^http://pylxd.test/1.0/containers/an-container$',
     },
-
+    {
+        'json': {
+            'type': 'sync',  # This should be async
+            'metadata': {
+                'metadata': {
+                    'fds': {
+                        '0': 'abc',
+                        '1': 'def',
+                        '2': 'ghi',
+                        'control': 'jkl',
+                    }
+                },
+            },
+            'operation': 'operation-abc'},
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/containers/an-container/exec$',  # NOQA
+    },
 
     # Container Snapshots
     {
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
index 49b7c40..b10216a 100644
--- a/pylxd/tests/test_container.py
+++ b/pylxd/tests/test_container.py
@@ -1,5 +1,7 @@
 import json
 
+import mock
+
 from pylxd import container, exceptions
 from pylxd.tests import testing
 
@@ -180,6 +182,29 @@ def test_delete(self):
 
         an_container.delete(wait=True)
 
+    @mock.patch('pylxd.container._StdinWebsocket')
+    @mock.patch('pylxd.container._CommandWebsocketClient')
+    def test_execute(self, _CommandWebsocketClient, _StdinWebsocket):
+        """A command is executed on a container."""
+        fake_websocket = mock.Mock()
+        fake_websocket.data = 'test\n'
+        _StdinWebsocket.return_value = fake_websocket
+        _CommandWebsocketClient.return_value = fake_websocket
+
+        an_container = container.Container(
+            name='an-container', _client=self.client)
+
+        stdout, _ = an_container.execute(['echo', 'test'])
+
+        self.assertEqual('test\n', stdout)
+
+    def test_execute_string(self):
+        """A command passed as string raises a TypeError."""
+        an_container = container.Container(
+            name='an-container', _client=self.client)
+
+        self.assertRaises(TypeError, an_container.execute, 'apt-get update')
+
 
 class TestContainerState(testing.PyLXDTestCase):
     """Tests for pylxd.container.ContainerState."""


More information about the lxc-devel mailing list