[lxc-devel] [pylxd/master] WIP: container.execute_interactive()

lingxiaoyang on Github lxc-bot at linuxcontainers.org
Thu Jul 20 19:42:20 UTC 2017


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 2339 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20170720/a74cd6d2/attachment.bin>
-------------- next part --------------
From 315aab47df2cd7bf3bec1a4e957c324d580cc832 Mon Sep 17 00:00:00 2001
From: Ling-Xiao Yang <ling-xiao.yang at savoirfairelinux.com>
Date: Wed, 19 Jul 2017 17:55:28 -0400
Subject: [PATCH] WIP: interactive exec.

Caveats:
1. bash color... it's weird because if I `vi` a file the colors are all correct.
2. Ctrl+D... for some reason I have to type another key to let the remote close.
---
 pylxd/models/container.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 100 insertions(+)

diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index 415ea37..adbe2a6 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -12,12 +12,21 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 import collections
+import fcntl
+import json
+import platform
+import termios
 import time
+import tty
+import signal
+import struct
+import sys
 
 import six
 from six.moves.urllib import parse
 try:
     from ws4py.client import WebSocketBaseClient
+    from ws4py.client.threadedclient import WebSocketClient
     from ws4py.manager import WebSocketManager
     _ws4py_installed = True
 except ImportError:  # pragma: no cover
@@ -250,6 +259,65 @@ def execute(self, commands, environment={}):
             return _ContainerExecuteResult(
                 operation.metadata['return'], stdout.data, stderr.data)
 
+    def execute_interactive(self, commands, environment={}):
+        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.")
+        rows, cols = _pty_size()
+        response = self.api['exec'].post(json={
+            'command': commands,
+            'environment': environment,
+            'wait-for-websocket': True,
+            'interactive': True,
+            'width': cols,
+            'height': rows,
+        })
+
+        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)
+
+        pts = _InteractiveWebsocket(self.client.websocket_url)
+        pts.resource = '{}?secret={}'.format(parsed.path, fds['0'])
+        pts.connect()
+
+        ctl = WebSocketClient(self.client.websocket_url)
+        ctl.resource = '{}?secret={}'.format(parsed.path, fds['control'])
+        ctl.connect()
+
+        oldtty = termios.tcgetattr(sys.stdin)
+        old_handler = signal.getsignal(signal.SIGWINCH)
+
+        def on_term_resize(signum, frame):
+            rows, cols = _pty_size()
+            # Refs:
+            # https://github.com/lxc/lxd/blob/master/lxd/container_exec.go#L190
+            # https://github.com/lxc/lxd/blob/master/shared/api/container_exec.go
+            ctl.send(json.dumps({
+                'command': 'window-resize',
+                'args': {
+                    'width': str(cols),
+                    'height': str(rows)
+                }
+            }))
+        signal.signal(signal.SIGWINCH, on_term_resize)
+
+        try:
+            tty.setraw(sys.stdin.fileno())
+            tty.setcbreak(sys.stdin.fileno())
+            pts.run_forever()
+        except Exception as e:
+            raise
+        finally:
+            termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
+            signal.signal(signal.SIGWINCH, old_handler)
+
+        operation = self.client.operations.get(operation_id)
+        return _ContainerExecuteResult(operation.metadata['return'], '', '')
+
     def migrate(self, new_client, wait=False):
         """Migrate a container.
 
@@ -356,6 +424,38 @@ def handshake_ok(self):
         self.close()
 
 
+class _InteractiveWebsocket(WebSocketClient):  # pragma: no cover
+    def received_message(self, message):
+        if message.encoding:
+            m = message.data.decode(message.encoding)
+        else:
+            m = message.data.decode('utf-8')
+        sys.stdout.write(m)
+        sys.stdout.flush()
+
+    def run_forever(self):
+        while not self.terminated:
+            if sys.stdin.isatty():
+                x = sys.stdin.buffer.read(1)
+                self.send(x, binary=True)
+            # The timeout should let cursor move fluidly in vim
+            self._th.join(timeout=0.01)
+
+
+def _pty_size():
+    """ From wssh.client._pty_size """
+    rows, cols = 24, 80
+    # Can't do much for Windows
+    if platform.system() == 'Windows':
+        return rows, cols
+    fmt = 'HH'
+    buffer = struct.pack(fmt, 0, 0)
+    result = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ,
+                         buffer)
+    rows, cols = struct.unpack(fmt, result)
+    return rows, cols
+
+
 class Snapshot(model.Model):
     """A container snapshot."""
 


More information about the lxc-devel mailing list