Skip to content

Commit

Permalink
Interactive Execute on Container (#348)
Browse files Browse the repository at this point in the history
* Added interactive execute on container to connect with WS
* Added example on websocket use and clarified the urls are returned

Signed-off-by: Felix Engelmann <[email protected]>
  • Loading branch information
felix-engelmann authored and ajkavanagh committed Jan 18, 2019
1 parent 7897af7 commit d5d47a4
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 0 deletions.
16 changes: 16 additions & 0 deletions doc/source/containers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- `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.
Expand Down Expand Up @@ -163,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
-------------------

Expand Down
36 changes: 36 additions & 0 deletions pylxd/models/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,42 @@ def execute(
return _ContainerExecuteResult(
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.
: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: Two urls to an interactive websocket and a control socket
:rtype: {'ws':str,'control':str}
"""
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.
Expand Down
30 changes: 30 additions & 0 deletions pylxd/tests/models/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,36 @@ def test_execute_string(self):

self.assertRaises(TypeError, an_container.execute, 'apt-get update')

def test_raw_interactive_execute(self):
an_container = models.Container(self.client, name='an-container')

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_raw_interactive_execute_env(self):
an_container = models.Container(self.client, name='an-container')

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_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.raw_interactive_execute,
'apt-get update')

def test_migrate(self):
"""A container is migrated."""
from pylxd.client import Client
Expand Down

0 comments on commit d5d47a4

Please sign in to comment.