Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive Execute on Container #348

Merged
merged 8 commits into from
Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -437,6 +437,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.

felix-engelmann marked this conversation as resolved.
Show resolved Hide resolved
: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