[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