diff --git a/zun/compute/api.py b/zun/compute/api.py index 6b8445797..b0e6889b3 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -133,7 +133,15 @@ class API(object): timestamps, tail, since) def container_exec(self, context, container, *args): - return self.rpcapi.container_exec(context, container, *args) + data = self.rpcapi.container_exec(context, container, *args) + token = data.pop('token', None) + exec_id = data.get('exec_id') + if token: + data['proxy_url'] = '%s?token=%s&uuid=%s&exec_id=%s' % ( + CONF.websocket_proxy.base_url, token, container.uuid, exec_id) + else: + data['proxy_url'] = None + return data def container_exec_resize(self, context, container, *args): return self.rpcapi.container_exec_resize(context, container, *args) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 2c185c5f0..af800bc5f 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -730,12 +730,26 @@ class Manager(periodic_task.PeriodicTasks): exec_id = self.driver.execute_create(context, container, command, interactive) if run: - return self.driver.execute_run(exec_id, command) + output, exit_code = self.driver.execute_run(exec_id, command) + # TODO(hongbin): remove url once bug #1735076 is fixed + return {"output": output, + "exit_code": exit_code, + "exec_id": None, + "url": None, + "token": None} else: + token = uuidutils.generate_uuid() + url = CONF.docker.docker_remote_api_url + exec_instace = objects.ExecInstance( + context, container_id=container.id, exec_id=exec_id, + url=url, token=token) + exec_instace.create(context) + # TODO(hongbin): remove url once bug #1735076 is fixed return {'output': None, 'exit_code': None, 'exec_id': exec_id, - 'url': CONF.docker.docker_remote_api_url} + 'url': url, + 'token': token} except exception.DockerError as e: LOG.error("Error occurred while calling Docker exec API: %s", six.text_type(e)) diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index 4f0f8e943..949717ed5 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -767,10 +767,7 @@ class DockerDriver(driver.ContainerDriver): raise exception.Conflict(_( "Timeout on executing command: %s") % command) inspect_res = docker.exec_inspect(exec_id) - return {"output": output, - "exit_code": inspect_res['ExitCode'], - "exec_id": None, - "url": None} + return output, inspect_res['ExitCode'] def execute_resize(self, exec_id, height, width): height = int(height) diff --git a/zun/tests/unit/compute/test_compute_api.py b/zun/tests/unit/compute/test_compute_api.py index 79828954f..b74bd5613 100644 --- a/zun/tests/unit/compute/test_compute_api.py +++ b/zun/tests/unit/compute/test_compute_api.py @@ -237,6 +237,28 @@ class TestAPI(base.TestCase): container=container, command="/bin/bash", run=True, interactive=True) + @mock.patch('zun.compute.rpcapi.API._call') + @mock.patch('zun.api.servicegroup.ServiceGroup.service_is_up') + @mock.patch('zun.objects.ZunService.list_by_binary') + def test_container_exec_interactive( + self, mock_srv_list, mock_srv_up, mock_call): + mock_call.return_value = {'token': 'fake-token', + 'exec_id': 'fake-exec-id'} + container = self.container + srv = objects.ZunService( + self.context, + **utils.get_test_zun_service(host=container.host)) + mock_srv_list.return_value = [srv] + mock_srv_up.return_value = True + result = self.compute_api.container_exec( + self.context, container, "/bin/bash", True, True) + self.assertIn('fake-token', result['proxy_url']) + self.assertIn('fake-exec-id', result['proxy_url']) + mock_call.assert_called_once_with( + container.host, "container_exec", + container=container, command="/bin/bash", + run=True, interactive=True) + @mock.patch('zun.compute.rpcapi.API._call') @mock.patch('zun.api.servicegroup.ServiceGroup.service_is_up') @mock.patch('zun.objects.ZunService.list_by_binary') diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index 22a076060..452186bd7 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -23,6 +23,7 @@ from zun.compute import manager import zun.conf from zun.objects.container import Container from zun.objects.container_action import ContainerActionEvent +from zun.objects.exec_instance import ExecInstance from zun.objects.image import Image from zun.objects.network import Network from zun.objects.volume_mapping import VolumeMapping @@ -973,13 +974,35 @@ class TestManager(base.TestCase): @mock.patch.object(fake_driver, 'execute_create') def test_container_execute(self, mock_execute_create, mock_execute_run): mock_execute_create.return_value = 'fake_exec_id' + mock_execute_run.return_value = 'fake_output', 'fake_exit_code' container = Container(self.context, **utils.get_test_container()) - self.compute_manager.container_exec( + result = self.compute_manager.container_exec( self.context, container, 'fake_cmd', True, False) + self.assertEqual('fake_output', result.get('output')) + self.assertEqual('fake_exit_code', result.get('exit_code')) + self.assertIsNone(result.get('exec_id')) + self.assertIsNone(result.get('token')) mock_execute_create.assert_called_once_with( self.context, container, 'fake_cmd', False) mock_execute_run.assert_called_once_with('fake_exec_id', 'fake_cmd') + @mock.patch.object(ExecInstance, 'create') + @mock.patch.object(fake_driver, 'execute_run') + @mock.patch.object(fake_driver, 'execute_create') + def test_container_execute_interactive( + self, mock_execute_create, mock_execute_run, mock_create): + mock_execute_create.return_value = 'fake_exec_id' + container = Container(self.context, **utils.get_test_container()) + result = self.compute_manager.container_exec( + self.context, container, 'fake_cmd', False, True) + self.assertIsNone(result.get('output')) + self.assertIsNone(result.get('exit_code')) + self.assertEqual('fake_exec_id', result.get('exec_id')) + self.assertIsNotNone(result.get('token')) + mock_execute_create.assert_called_once_with( + self.context, container, 'fake_cmd', True) + mock_execute_run.assert_not_called() + @mock.patch.object(fake_driver, 'execute_create') def test_container_execute_failed(self, mock_execute_create): container = Container(self.context, **utils.get_test_container()) diff --git a/zun/websocket/websocketproxy.py b/zun/websocket/websocketproxy.py index 4caddd082..3c95372d7 100644 --- a/zun/websocket/websocketproxy.py +++ b/zun/websocket/websocketproxy.py @@ -23,11 +23,12 @@ import socket import sys import time - +import docker from oslo_log import log as logging from oslo_utils import uuidutils import six.moves.urllib.parse as urlparse import websockify + from zun.common import context from zun.common import exception from zun.common.i18n import _ @@ -121,7 +122,7 @@ class ZunProxyRequestHandlerBase(object): raise self.CClose(1000, "Target closed") self.cqueue.append(buf) - def do_proxy(self, target): + def do_websocket_proxy(self, target): """Proxy websocket link Proxy client WebSocket to normal target socket. @@ -189,6 +190,7 @@ class ZunProxyRequestHandlerBase(object): query = parse.query token = urlparse.parse_qs(query).get("token", [""]).pop() uuid = urlparse.parse_qs(query).get("uuid", [""]).pop() + exec_id = urlparse.parse_qs(query).get("exec_id", [""]).pop() ctx = context.get_admin_context(all_projects=True) @@ -197,12 +199,66 @@ class ZunProxyRequestHandlerBase(object): else: container = objects.Container.get_by_name(ctx, uuid) + if exec_id: + self._new_exec_client(container, token, uuid, exec_id) + else: + self._new_websocket_client(container, token, uuid) + + def _new_websocket_client(self, container, token, uuid): if token != container.websocket_token: raise exception.InvalidWebsocketToken(token) access_url = '%s?token=%s&uuid=%s' % (CONF.websocket_proxy.base_url, token, uuid) + self._verify_origin(access_url) + + if container.websocket_url: + target_url = container.websocket_url + escape = "~" + close_wait = 0.5 + wscls = WebSocketClient(host_url=target_url, escape=escape, + close_wait=close_wait) + wscls.connect() + self.target = wscls + else: + raise exception.InvalidWebsocketUrl() + + # Start proxying + try: + self.do_websocket_proxy(self.target.ws) + except Exception: + if self.target.ws: + self.target.ws.close() + self.vmsg(_("Websocket client or target closed")) + raise + + def _new_exec_client(self, container, token, uuid, exec_id): + exec_instance = None + for e in container.exec_instances: + if token == e.token and exec_id == e.exec_id: + exec_instance = e + + if not exec_instance: + raise exception.InvalidWebsocketToken(token) + + access_url = '%s?token=%s&uuid=%s' % (CONF.websocket_proxy.base_url, + token, uuid) + + self._verify_origin(access_url) + + client = docker.APIClient(base_url=exec_instance.url) + tsock = client.exec_start(exec_id, socket=True, tty=True) + + try: + self.do_proxy(tsock) + finally: + if tsock: + tsock.shutdown(socket.SHUT_RDWR) + tsock.close() + self.vmsg(_("%s: Closed target") % exec_instance.url) + + def _verify_origin(self, access_url): # Verify Origin expected_origin_hostname = self.headers.get('Host') if ':' in expected_origin_hostname: @@ -230,26 +286,6 @@ class ZunProxyRequestHandlerBase(object): detail = _("Origin header protocol does not match this host.") raise exception.ValidationError(detail) - if container.websocket_url: - target_url = container.websocket_url - escape = "~" - close_wait = 0.5 - wscls = WebSocketClient(host_url=target_url, escape=escape, - close_wait=close_wait) - wscls.connect() - self.target = wscls - else: - raise exception.InvalidWebsocketUrl() - - # Start proxying - try: - self.do_proxy(self.target.ws) - except Exception as e: - if self.target.ws: - self.target.ws.close() - self.vmsg(_("%Websocket client or target closed")) - raise - class ZunProxyRequestHandler(ZunProxyRequestHandlerBase, websockify.ProxyRequestHandler):