From 817cdfb99d9c86cca54d69a9f951a3b492ae0c79 Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Fri, 14 Dec 2018 13:48:13 +0100 Subject: [PATCH 1/8] Added interactive execute on container to connect with WS Signed-off-by: Felix Engelmann --- pylxd/models/container.py | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pylxd/models/container.py b/pylxd/models/container.py index 354c8967..7abbc273 100644 --- a/pylxd/models/container.py +++ b/pylxd/models/container.py @@ -437,6 +437,46 @@ def execute( return _ContainerExecuteResult( operation.metadata['return'], stdout.data, stderr.data) + def interactive_execute( + self, commands, environment=None + ): + """Execute a command on the container interactively and return websocket. + + 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 (most likely a shell) + :type commands: [str] + :param environment: The environment variables to pass with the command + :type environment: {str: str} + :returns: A link to an interactive websocket and a control socket + :rtype: dict + """ + if not _ws4py_installed: + raise ValueError( + 'This feature requires the optional ws4py library.') + if isinstance(commands, six.string_types): + raise TypeError("First argument must be a list.") + + if environment is None: + environment = {} + + response = self.api['exec'].post(json={ + 'command': commands, + 'environment': environment, + 'wait-for-websocket': True, + 'interactive': True, + }) + + fds = response.json()['metadata']['metadata']['fds'] + operation_id = response.json()['operation']\ + .split('/')[-1].split('?')[0] + parsed = parse.urlparse( + self.client.api.operations[operation_id].websocket._api_endpoint) + + return {'ws': '{}?secret={}'.format(parsed.path, fds['0']), + 'control': '{}?secret={}'.format(parsed.path, fds['control']),} + def migrate(self, new_client, wait=False): """Migrate a container. From 244d3f6bc4e1ed69a0f8c0a18d3c12c2037f618d Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Sat, 15 Dec 2018 15:43:29 +0100 Subject: [PATCH 2/8] Cleanup of interactive_execute Signed-off-by: Felix Engelmann --- doc/source/containers.rst | 2 ++ pylxd/models/container.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/source/containers.rst b/doc/source/containers.rst index 864855ea..336ecf2b 100644 --- a/doc/source/containers.rst +++ b/doc/source/containers.rst @@ -64,6 +64,8 @@ Container methods a list, in the form of `subprocess.Popen` with each item of the command as a separate item in the list. Returns a tuple of `(exit_code, stdout, stderr)`. This method will block while the command is executed. + - `interactive_execute` - Execute a command on the container. It will return + an interactive websocket and the execution only starts after a client connected to the websocket. - `migrate` - Migrate the container. The first argument is a client connection to the destination server. This call is asynchronous, so `wait=True` is optional. The container on the new client is returned. diff --git a/pylxd/models/container.py b/pylxd/models/container.py index 7abbc273..e33e3c23 100644 --- a/pylxd/models/container.py +++ b/pylxd/models/container.py @@ -440,17 +440,15 @@ def execute( def interactive_execute( self, commands, environment=None ): - """Execute a command on the container interactively and return websocket. + """Execute a command on the container interactively and returns websockets. - 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 (most likely a shell) + :param commands: The command and arguments as a list of strings + (most likely a shell) :type commands: [str] :param environment: The environment variables to pass with the command :type environment: {str: str} - :returns: A link to an interactive websocket and a control socket - :rtype: dict + :returns: Two urls to an interactive websocket and a control socket + :rtype: {'ws':str,'control':str} """ if not _ws4py_installed: raise ValueError( @@ -475,7 +473,7 @@ def interactive_execute( self.client.api.operations[operation_id].websocket._api_endpoint) return {'ws': '{}?secret={}'.format(parsed.path, fds['0']), - 'control': '{}?secret={}'.format(parsed.path, fds['control']),} + 'control': '{}?secret={}'.format(parsed.path, fds['control'])} def migrate(self, new_client, wait=False): """Migrate a container. From 1e2444ac2bd29dd864660130fbb2079c71dd43b5 Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Sat, 15 Dec 2018 15:50:19 +0100 Subject: [PATCH 3/8] Added testcase for interactive execute Signed-off-by: Felix Engelmann --- pylxd/tests/models/test_container.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_container.py index c71097d1..10b88b08 100644 --- a/pylxd/tests/models/test_container.py +++ b/pylxd/tests/models/test_container.py @@ -236,6 +236,14 @@ def test_execute_string(self): self.assertRaises(TypeError, an_container.execute, 'apt-get update') + def test_interactive_execute(self): + an_container = models.Container(self.client, name='an-container') + + result = an_container.interactive_execute(['/bin/bash']) + + self.assertEqual(result['ws'],'/1.0/operations/operation-abc/websocket?secret=abc') + self.assertEqual(result['control'],'/1.0/operations/operation-abc/websocket?secret=jkl') + def test_migrate(self): """A container is migrated.""" from pylxd.client import Client From 4e930021457d2f1322a32920377395da14524786 Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Sat, 15 Dec 2018 15:57:58 +0100 Subject: [PATCH 4/8] Pep8 compliance Signed-off-by: Felix Engelmann --- pylxd/tests/models/test_container.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_container.py index 10b88b08..65720c96 100644 --- a/pylxd/tests/models/test_container.py +++ b/pylxd/tests/models/test_container.py @@ -241,8 +241,10 @@ def test_interactive_execute(self): result = an_container.interactive_execute(['/bin/bash']) - self.assertEqual(result['ws'],'/1.0/operations/operation-abc/websocket?secret=abc') - self.assertEqual(result['control'],'/1.0/operations/operation-abc/websocket?secret=jkl') + self.assertEqual(result['ws'], + '/1.0/operations/operation-abc/websocket?secret=abc') + self.assertEqual(result['control'], + '/1.0/operations/operation-abc/websocket?secret=jkl') def test_migrate(self): """A container is migrated.""" From 9cfafd20ac888c18a3629d01662bad32e55be5ea Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Sat, 15 Dec 2018 16:01:41 +0100 Subject: [PATCH 5/8] added testcase for string arguments Signed-off-by: Felix Engelmann --- pylxd/models/container.py | 3 --- pylxd/tests/models/test_container.py | 9 +++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pylxd/models/container.py b/pylxd/models/container.py index e33e3c23..96fd8403 100644 --- a/pylxd/models/container.py +++ b/pylxd/models/container.py @@ -450,9 +450,6 @@ def interactive_execute( :returns: Two urls to an interactive websocket and a control socket :rtype: {'ws':str,'control':str} """ - if not _ws4py_installed: - raise ValueError( - 'This feature requires the optional ws4py library.') if isinstance(commands, six.string_types): raise TypeError("First argument must be a list.") diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_container.py index 65720c96..16d1f2ec 100644 --- a/pylxd/tests/models/test_container.py +++ b/pylxd/tests/models/test_container.py @@ -246,6 +246,15 @@ def test_interactive_execute(self): self.assertEqual(result['control'], '/1.0/operations/operation-abc/websocket?secret=jkl') + def test_interactive_execute_string(self): + """A command passed as string raises a TypeError.""" + an_container = models.Container( + self.client, name='an-container') + + self.assertRaises(TypeError, + an_container.interactive_execute, + 'apt-get update') + def test_migrate(self): """A container is migrated.""" from pylxd.client import Client From 439381ffb95ad83def35027392935308e4fe63db Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Sat, 15 Dec 2018 16:08:10 +0100 Subject: [PATCH 6/8] Testing of environment Signed-off-by: Felix Engelmann --- pylxd/tests/models/test_container.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_container.py index 16d1f2ec..76263814 100644 --- a/pylxd/tests/models/test_container.py +++ b/pylxd/tests/models/test_container.py @@ -246,6 +246,16 @@ def test_interactive_execute(self): self.assertEqual(result['control'], '/1.0/operations/operation-abc/websocket?secret=jkl') + def test_interactive_execute_env(self): + an_container = models.Container(self.client, name='an-container') + + result = an_container.interactive_execute(['/bin/bash'], {"PATH": "/"}) + + self.assertEqual(result['ws'], + '/1.0/operations/operation-abc/websocket?secret=abc') + self.assertEqual(result['control'], + '/1.0/operations/operation-abc/websocket?secret=jkl') + def test_interactive_execute_string(self): """A command passed as string raises a TypeError.""" an_container = models.Container( From 5e502aec76547a9ad8f387dc574338d704876fd7 Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Fri, 18 Jan 2019 11:37:06 +0100 Subject: [PATCH 7/8] Added example on websocket use and clarified the urls are returned Signed-off-by: Felix Engelmann --- doc/source/containers.rst | 18 ++++++++++++++++-- pylxd/models/container.py | 8 ++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/doc/source/containers.rst b/doc/source/containers.rst index 336ecf2b..48d5ff92 100644 --- a/doc/source/containers.rst +++ b/doc/source/containers.rst @@ -64,8 +64,8 @@ Container methods a list, in the form of `subprocess.Popen` with each item of the command as a separate item in the list. Returns a tuple of `(exit_code, stdout, stderr)`. This method will block while the command is executed. - - `interactive_execute` - Execute a command on the container. It will return - an interactive websocket and the execution only starts after a client connected to the websocket. + - `raw_interactive_execute` - Execute a command on the container. It will return + an url to an interactive websocket and the execution only starts after a client connected to the websocket. - `migrate` - Migrate the container. The first argument is a client connection to the destination server. This call is asynchronous, so `wait=True` is optional. The container on the new client is returned. @@ -165,6 +165,20 @@ the source server has to be reachable by the destination server otherwise the mi This will migrate the container from source server to destination server +If you want an interactive shell in the container, you can attach to it via a websocket. + +.. code-block:: python + + >>> res = container.raw_interactive_execute(['/bin/bash']) + >>> res + { + "name": "container-name", + "ws": "/1.0/operations/adbaab82-afd2-450c-a67e-274726e875b1/websocket?secret=ef3dbdc103ec5c90fc6359c8e087dcaf1bc3eb46c76117289f34a8f949e08d87", + "control": "/1.0/operations/adbaab82-afd2-450c-a67e-274726e875b1/websocket?secret=dbbc67833009339d45140671773ac55b513e78b219f9f39609247a2d10458084" + } + +You can connect to this urls from e.g. https://xtermjs.org/ . + Container Snapshots ------------------- diff --git a/pylxd/models/container.py b/pylxd/models/container.py index 96fd8403..9cc878c2 100644 --- a/pylxd/models/container.py +++ b/pylxd/models/container.py @@ -437,10 +437,10 @@ def execute( return _ContainerExecuteResult( operation.metadata['return'], stdout.data, stderr.data) - def interactive_execute( - self, commands, environment=None - ): - """Execute a command on the container interactively and returns websockets. + def raw_interactive_execute(self, commands, environment=None): + """Execute a command on the container interactively and returns urls to websockets. + The urls contain a secret uuid, and can be accesses without further authentication. + The caller has to open and manage the websockets themselves. :param commands: The command and arguments as a list of strings (most likely a shell) From 6458c085fbdbb8753644ec0fbb41c66ad2881134 Mon Sep 17 00:00:00 2001 From: Felix Engelmann Date: Fri, 18 Jan 2019 11:47:09 +0100 Subject: [PATCH 8/8] fixed tests to raw_ and pep8 compliance Signed-off-by: Felix Engelmann --- pylxd/models/container.py | 7 ++++--- pylxd/tests/models/test_container.py | 13 +++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pylxd/models/container.py b/pylxd/models/container.py index 9cc878c2..d565d8a7 100644 --- a/pylxd/models/container.py +++ b/pylxd/models/container.py @@ -438,9 +438,10 @@ def execute( operation.metadata['return'], stdout.data, stderr.data) def raw_interactive_execute(self, commands, environment=None): - """Execute a command on the container interactively and returns urls to websockets. - The urls contain a secret uuid, and can be accesses without further authentication. - The caller has to open and manage the websockets themselves. + """Execute a command on the container interactively and returns + urls to websockets. The urls contain a secret uuid, and can be accesses + without further authentication. The caller has to open and manage + the websockets themselves. :param commands: The command and arguments as a list of strings (most likely a shell) diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_container.py index 76263814..795bebdc 100644 --- a/pylxd/tests/models/test_container.py +++ b/pylxd/tests/models/test_container.py @@ -236,33 +236,34 @@ def test_execute_string(self): self.assertRaises(TypeError, an_container.execute, 'apt-get update') - def test_interactive_execute(self): + def test_raw_interactive_execute(self): an_container = models.Container(self.client, name='an-container') - result = an_container.interactive_execute(['/bin/bash']) + result = an_container.raw_interactive_execute(['/bin/bash']) self.assertEqual(result['ws'], '/1.0/operations/operation-abc/websocket?secret=abc') self.assertEqual(result['control'], '/1.0/operations/operation-abc/websocket?secret=jkl') - def test_interactive_execute_env(self): + def test_raw_interactive_execute_env(self): an_container = models.Container(self.client, name='an-container') - result = an_container.interactive_execute(['/bin/bash'], {"PATH": "/"}) + result = an_container.raw_interactive_execute(['/bin/bash'], + {"PATH": "/"}) self.assertEqual(result['ws'], '/1.0/operations/operation-abc/websocket?secret=abc') self.assertEqual(result['control'], '/1.0/operations/operation-abc/websocket?secret=jkl') - def test_interactive_execute_string(self): + def test_raw_interactive_execute_string(self): """A command passed as string raises a TypeError.""" an_container = models.Container( self.client, name='an-container') self.assertRaises(TypeError, - an_container.interactive_execute, + an_container.raw_interactive_execute, 'apt-get update') def test_migrate(self):