Add support for attach detach and resize
Also add a websocket client Change-Id: Ie69232351db67a3a541ba44a0c74f01228539066 Partially-Implements: BP support-interactive-mode
This commit is contained in:
parent
74c06b5166
commit
9d70624b57
@ -7,3 +7,4 @@ PrettyTable>=0.7.1,<0.8 # BSD
|
|||||||
osc-lib>=1.2.0 # Apache-2.0
|
osc-lib>=1.2.0 # Apache-2.0
|
||||||
oslo.i18n>=2.1.0 # Apache-2.0
|
oslo.i18n>=2.1.0 # Apache-2.0
|
||||||
oslo.utils>=3.16.0 # Apache-2.0
|
oslo.utils>=3.16.0 # Apache-2.0
|
||||||
|
websocket-client>=0.32.0 # LGPLv2+
|
||||||
|
0
zunclient/common/websocketclient/__init__.py
Normal file
0
zunclient/common/websocketclient/__init__.py
Normal file
43
zunclient/common/websocketclient/exceptions.py
Normal file
43
zunclient/common/websocketclient/exceptions.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Copyright 2014
|
||||||
|
# The Cloudscaling Group, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
# use this file except in compliance with the License. You may obtain a copy
|
||||||
|
# of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerWebSocketException(Exception):
|
||||||
|
'base for all ContainerWebSocket interactive generated exceptions'
|
||||||
|
def __init__(self, wrapped=None, message=None):
|
||||||
|
self.wrapped = wrapped
|
||||||
|
if message:
|
||||||
|
self.message = message
|
||||||
|
if wrapped:
|
||||||
|
formatted_string = "%s:%s" % (self.message, str(self.wrapped))
|
||||||
|
else:
|
||||||
|
formatted_string = "%s" % self.message
|
||||||
|
super(ContainerWebSocketException, self).__init__(formatted_string)
|
||||||
|
|
||||||
|
|
||||||
|
class UserExit(ContainerWebSocketException):
|
||||||
|
message = "User requested disconnect the container"
|
||||||
|
|
||||||
|
|
||||||
|
class Disconnected(ContainerWebSocketException):
|
||||||
|
message = "Remote host closed connection"
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionFailed(ContainerWebSocketException):
|
||||||
|
message = "Failed to connect to remote host"
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidWebSocketLink(ContainerWebSocketException):
|
||||||
|
message = "Invalid websocket link when attach container"
|
330
zunclient/common/websocketclient/websocketclient.py
Normal file
330
zunclient/common/websocketclient/websocketclient.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 OpenStack Foundation
|
||||||
|
# Copyright 2012 Grid Dynamics
|
||||||
|
# Copyright 2013 OpenStack Foundation
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import fcntl
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import select
|
||||||
|
import signal
|
||||||
|
import six
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import termios
|
||||||
|
import time
|
||||||
|
import tty
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
from zunclient.common.websocketclient import exceptions
|
||||||
|
from zunclient.v1 import client
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_API_VERSION = '1'
|
||||||
|
DEFAULT_ENDPOINT_TYPE = 'publicURL'
|
||||||
|
DEFAULT_SERVICE_TYPE = 'container'
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClient(object):
|
||||||
|
|
||||||
|
def __init__(self, host_url, id, escape='~',
|
||||||
|
close_wait=0.5):
|
||||||
|
self.id = id
|
||||||
|
self.escape = escape
|
||||||
|
self.close_wait = close_wait
|
||||||
|
self.host_url = host_url
|
||||||
|
self.cs = None
|
||||||
|
|
||||||
|
def init_httpclient(self):
|
||||||
|
"""Initialize the httpclient
|
||||||
|
|
||||||
|
Websocket client need to call httpclient to send the resize
|
||||||
|
command to Zun API server
|
||||||
|
"""
|
||||||
|
os_username = os.environ.get('OS_USERNAME')
|
||||||
|
os_password = os.environ.get('OS_PASSWORD')
|
||||||
|
os_project_name = os.environ.get('OS_PROJECT_NAME')
|
||||||
|
os_project_id = os.environ.get('OS_PROJECT_ID')
|
||||||
|
os_user_domain_id = os.environ.get('OS_USER_DOMAIN_ID')
|
||||||
|
os_user_domain_name = os.environ.get('OS_USER_DOMAIN_NAME')
|
||||||
|
os_project_domain_id = os.environ.get('OS_PROJECT_DOMAIN_ID')
|
||||||
|
os_project_domain_name = os.environ.get('OS_PROJECT_DOMAIN_NAME')
|
||||||
|
os_auth_url = os.environ.get('OS_AUTH_URL')
|
||||||
|
endpoint_type = os.environ.get('ENDPOINT_TYPE')
|
||||||
|
service_type = os.environ.get('SERVICE_TYPE')
|
||||||
|
os_region_name = os.environ.get('OS_REGION_NAME')
|
||||||
|
bypass_url = os.environ.get('BYPASS_URL')
|
||||||
|
insecure = os.environ.get('INSECURE')
|
||||||
|
if not endpoint_type:
|
||||||
|
endpoint_type = DEFAULT_ENDPOINT_TYPE
|
||||||
|
|
||||||
|
if not service_type:
|
||||||
|
service_type = DEFAULT_SERVICE_TYPE
|
||||||
|
|
||||||
|
self.cs = client.Client(username=os_username,
|
||||||
|
api_key=os_password,
|
||||||
|
project_id=os_project_id,
|
||||||
|
project_name=os_project_name,
|
||||||
|
user_domain_id=os_user_domain_id,
|
||||||
|
user_domain_name=os_user_domain_name,
|
||||||
|
project_domain_id=os_project_domain_id,
|
||||||
|
project_domain_name=os_project_domain_name,
|
||||||
|
auth_url=os_auth_url,
|
||||||
|
service_type=service_type,
|
||||||
|
region_name=os_region_name,
|
||||||
|
zun_url=bypass_url,
|
||||||
|
endpoint_type=endpoint_type,
|
||||||
|
insecure=insecure)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
url = self.host_url
|
||||||
|
LOG.debug('connecting to: %s', url)
|
||||||
|
try:
|
||||||
|
self.ws = websocket.create_connection(url,
|
||||||
|
skip_utf8_validation=True)
|
||||||
|
print('connected to %s ,press Enter to continue' % self.id)
|
||||||
|
print('type %s. to disconnect' % self.escape)
|
||||||
|
except socket.error as e:
|
||||||
|
raise exceptions.ConnectionFailed(e)
|
||||||
|
except websocket.WebSocketConnectionClosedException as e:
|
||||||
|
raise exceptions.ConnectionFailed(e)
|
||||||
|
except websocket.WebSocketBadStatusException as e:
|
||||||
|
raise exceptions.ConnectionFailed(e)
|
||||||
|
|
||||||
|
def start_loop(self):
|
||||||
|
self.poll = select.poll()
|
||||||
|
self.poll.register(sys.stdin,
|
||||||
|
select.POLLIN | select.POLLHUP | select.POLLPRI)
|
||||||
|
self.poll.register(self.ws,
|
||||||
|
select.POLLIN | select.POLLHUP | select.POLLPRI)
|
||||||
|
|
||||||
|
self.start_of_line = False
|
||||||
|
self.read_escape = False
|
||||||
|
with WINCHHandler(self):
|
||||||
|
try:
|
||||||
|
self.setup_tty()
|
||||||
|
self.run_forever()
|
||||||
|
except socket.error as e:
|
||||||
|
raise exceptions.ConnectionFailed(e)
|
||||||
|
except websocket.WebSocketConnectionClosedException as e:
|
||||||
|
raise exceptions.Disconnected(e)
|
||||||
|
finally:
|
||||||
|
self.restore_tty()
|
||||||
|
|
||||||
|
def run_forever(self):
|
||||||
|
LOG.debug('starting main loop in client')
|
||||||
|
self.quit = False
|
||||||
|
quitting = False
|
||||||
|
when = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
for fd, event in self.poll.poll(500):
|
||||||
|
if fd == self.ws.fileno():
|
||||||
|
self.handle_websocket(event)
|
||||||
|
elif fd == sys.stdin.fileno():
|
||||||
|
self.handle_stdin(event)
|
||||||
|
except select.error as e:
|
||||||
|
# POSIX signals interrupt select()
|
||||||
|
no = e.errno if six.PY3 else e[0]
|
||||||
|
if no == errno.EINTR:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if self.quit and not quitting:
|
||||||
|
self.log.debug('entering close_wait')
|
||||||
|
quitting = True
|
||||||
|
when = time.time() + self.close_wait
|
||||||
|
|
||||||
|
if quitting and time.time() > when:
|
||||||
|
self.log.debug('quitting')
|
||||||
|
break
|
||||||
|
|
||||||
|
def setup_tty(self):
|
||||||
|
if os.isatty(sys.stdin.fileno()):
|
||||||
|
LOG.debug('putting tty into raw mode')
|
||||||
|
self.old_settings = termios.tcgetattr(sys.stdin)
|
||||||
|
tty.setraw(sys.stdin)
|
||||||
|
|
||||||
|
def restore_tty(self):
|
||||||
|
if os.isatty(sys.stdin.fileno()):
|
||||||
|
LOG.debug('restoring tty configuration')
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN,
|
||||||
|
self.old_settings)
|
||||||
|
|
||||||
|
def handle_stdin(self, event):
|
||||||
|
if event in (select.POLLHUP, select.POLLNVAL):
|
||||||
|
LOG.debug('event %d on stdin', event)
|
||||||
|
|
||||||
|
LOG.debug('eof on stdin')
|
||||||
|
self.poll.unregister(sys.stdin)
|
||||||
|
self.quit = True
|
||||||
|
|
||||||
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
|
LOG.debug('read %s (%d bytes) from stdin', repr(data), len(data))
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.start_of_line and data == self.escape:
|
||||||
|
self.read_escape = True
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.read_escape and data == '.':
|
||||||
|
LOG.debug('exit by local escape code')
|
||||||
|
raise exceptions.UserExit()
|
||||||
|
elif self.read_escape:
|
||||||
|
self.read_escape = False
|
||||||
|
self.ws.send(self.escape)
|
||||||
|
|
||||||
|
self.ws.send(data)
|
||||||
|
|
||||||
|
if data == '\r':
|
||||||
|
self.start_of_line = True
|
||||||
|
else:
|
||||||
|
self.start_of_line = False
|
||||||
|
|
||||||
|
def handle_websocket(self, event):
|
||||||
|
if event in (select.POLLHUP, select.POLLNVAL):
|
||||||
|
LOG.debug('event %d on websocket', event)
|
||||||
|
|
||||||
|
LOG.debug('eof on websocket')
|
||||||
|
self.poll.unregister(self.ws)
|
||||||
|
self.quit = True
|
||||||
|
|
||||||
|
data = self.ws.recv()
|
||||||
|
LOG.debug('read %s (%d bytes) from websocket from container',
|
||||||
|
repr(data), len(data))
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
sys.stdout.write(data)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def handle_resize(self):
|
||||||
|
"""send the POST to resize the tty session size in container.
|
||||||
|
|
||||||
|
Resize the container's PTY.
|
||||||
|
If `size` is not None, it must be a tuple of (height,width), otherwise
|
||||||
|
it will be determined by the size of the current TTY.
|
||||||
|
"""
|
||||||
|
size = self.tty_size(sys.stdout)
|
||||||
|
|
||||||
|
if size is not None:
|
||||||
|
rows, cols = size
|
||||||
|
try:
|
||||||
|
self.tty_resize(height=rows, width=cols)
|
||||||
|
except IOError: # Container already exited
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tty_size(self, fd):
|
||||||
|
"""Get the tty size
|
||||||
|
|
||||||
|
Return a tuple (rows,cols) representing the size of the TTY `fd`.
|
||||||
|
|
||||||
|
The provided file descriptor should be the stdout stream of the TTY.
|
||||||
|
|
||||||
|
If the TTY size cannot be determined, returns None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not os.isatty(fd.fileno()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
dims = struct.unpack('hh', fcntl.ioctl(fd,
|
||||||
|
termios.TIOCGWINSZ,
|
||||||
|
'hhhh'))
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
dims = (os.environ['LINES'], os.environ['COLUMNS'])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return dims
|
||||||
|
|
||||||
|
def tty_resize(self, height, width):
|
||||||
|
"""Resize the tty session
|
||||||
|
|
||||||
|
Get the client and send the tty size data to zun api server
|
||||||
|
The environment variables need to get when implement sending
|
||||||
|
operation.
|
||||||
|
"""
|
||||||
|
height = str(height)
|
||||||
|
width = str(width)
|
||||||
|
|
||||||
|
self.cs.containers.resize(self.id, width, height)
|
||||||
|
|
||||||
|
|
||||||
|
class WINCHHandler(object):
|
||||||
|
"""WINCH Signal handler
|
||||||
|
|
||||||
|
WINCH Signal handler to keep the PTY correctly sized.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, client):
|
||||||
|
"""Initialize a new WINCH handler for the given PTY.
|
||||||
|
|
||||||
|
Initializing a handler has no immediate side-effects. The `start()`
|
||||||
|
method must be invoked for the signals to be trapped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.client = client
|
||||||
|
self.original_handler = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Enter
|
||||||
|
|
||||||
|
Invoked on entering a `with` block.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.start()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_):
|
||||||
|
"""Exit
|
||||||
|
|
||||||
|
Invoked on exiting a `with` block.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start
|
||||||
|
|
||||||
|
Start trapping WINCH signals and resizing the PTY.
|
||||||
|
This method saves the previous WINCH handler so it can be restored on
|
||||||
|
`stop()`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle(signum, frame):
|
||||||
|
if signum == signal.SIGWINCH:
|
||||||
|
LOG.debug("Send command to resize the tty session")
|
||||||
|
self.client.handle_resize()
|
||||||
|
|
||||||
|
self.original_handler = signal.signal(signal.SIGWINCH, handle)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""stop
|
||||||
|
|
||||||
|
Stop trapping WINCH signals and restore the previous WINCH handler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.original_handler is not None:
|
||||||
|
signal.signal(signal.SIGWINCH, self.original_handler)
|
51
zunclient/tests/test_websocketclient.py
Normal file
51
zunclient/tests/test_websocketclient.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Copyright 2015 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from zunclient.common.websocketclient import websocketclient
|
||||||
|
|
||||||
|
CONTAINER_ID = "0f96db5a-26dc-4550-b1a8-b110bd9247cb"
|
||||||
|
ESCAPE_FLAG = "~"
|
||||||
|
URL = "ws://localhost:2375/v1.17/containers/201e4e22c5b2/" \
|
||||||
|
"attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1"
|
||||||
|
URL1 = "ws://10.10.10.10:2375/v1.17/containers/***********/" \
|
||||||
|
"attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1"
|
||||||
|
WAIT_TIME = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClientTest(testtools.TestCase):
|
||||||
|
|
||||||
|
def test_websocketclient_variables(self):
|
||||||
|
wsclient = websocketclient.WebSocketClient(host_url=URL,
|
||||||
|
id=CONTAINER_ID,
|
||||||
|
escape=ESCAPE_FLAG,
|
||||||
|
close_wait=WAIT_TIME)
|
||||||
|
self.assertEqual(wsclient.host_url, URL)
|
||||||
|
self.assertEqual(wsclient.id, CONTAINER_ID)
|
||||||
|
self.assertEqual(wsclient.escape, ESCAPE_FLAG)
|
||||||
|
self.assertEqual(wsclient.close_wait, WAIT_TIME)
|
||||||
|
|
||||||
|
@mock.patch('zunclient.v1.client.Client')
|
||||||
|
def test_init_httpclient(self, mock_client):
|
||||||
|
wsclient = websocketclient.WebSocketClient(host_url=URL,
|
||||||
|
id=CONTAINER_ID,
|
||||||
|
escape=ESCAPE_FLAG,
|
||||||
|
close_wait=WAIT_TIME)
|
||||||
|
mock_client.return_value = 'Client Object'
|
||||||
|
wsclient.init_httpclient()
|
||||||
|
self.assertEqual(wsclient.cs, 'Client Object')
|
||||||
|
self.assertTrue(mock_client.called)
|
@ -55,6 +55,8 @@ force_delete2 = True
|
|||||||
signal = "SIGTERM"
|
signal = "SIGTERM"
|
||||||
name = "new-name"
|
name = "new-name"
|
||||||
timeout = 10
|
timeout = 10
|
||||||
|
tty_height = "56"
|
||||||
|
tty_weight = "121"
|
||||||
|
|
||||||
fake_responses = {
|
fake_responses = {
|
||||||
'/v1/containers':
|
'/v1/containers':
|
||||||
@ -211,6 +213,22 @@ fake_responses = {
|
|||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
'/v1/containers/%s/attach' % CONTAINER1['id']:
|
||||||
|
{
|
||||||
|
'GET': (
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'/v1/containers/%s/resize?%s'
|
||||||
|
% (CONTAINER1['id'], parse.urlencode({'h': tty_height,
|
||||||
|
'w': tty_weight})):
|
||||||
|
{
|
||||||
|
'POST': (
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -445,3 +463,23 @@ class ContainerManagerTest(testtools.TestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(expect, self.api.calls)
|
self.assertEqual(expect, self.api.calls)
|
||||||
self.assertTrue(containers)
|
self.assertTrue(containers)
|
||||||
|
|
||||||
|
def test_containers_attach(self):
|
||||||
|
containers = self.mgr.attach(CONTAINER1['id'])
|
||||||
|
expect = [
|
||||||
|
('GET', '/v1/containers/%s/attach' % CONTAINER1['id'],
|
||||||
|
{'Content-Length': '0'}, None)
|
||||||
|
]
|
||||||
|
self.assertEqual(expect, self.api.calls)
|
||||||
|
self.assertIsNone(containers)
|
||||||
|
|
||||||
|
def test_containers_resize(self):
|
||||||
|
containers = self.mgr.resize(CONTAINER1['id'], tty_weight, tty_height)
|
||||||
|
expect = [
|
||||||
|
('POST', '/v1/containers/%s/resize?%s'
|
||||||
|
% (CONTAINER1['id'], parse.urlencode({'h': tty_height,
|
||||||
|
'w': tty_weight})),
|
||||||
|
{'Content-Length': '0'}, None)
|
||||||
|
]
|
||||||
|
self.assertEqual(expect, self.api.calls)
|
||||||
|
self.assertIsNone(containers)
|
||||||
|
@ -158,3 +158,10 @@ class ContainerManager(base.Manager):
|
|||||||
|
|
||||||
def update(self, id, **patch):
|
def update(self, id, **patch):
|
||||||
return self._update(self._path(id), patch)
|
return self._update(self._path(id), patch)
|
||||||
|
|
||||||
|
def attach(self, id):
|
||||||
|
return self._action(id, '/attach', method='GET')[1]
|
||||||
|
|
||||||
|
def resize(self, id, width, height):
|
||||||
|
return self._action(id, '/resize',
|
||||||
|
qparams={'w': width, 'h': height})[1]
|
||||||
|
@ -17,6 +17,8 @@ import json
|
|||||||
|
|
||||||
from zunclient.common import cliutils as utils
|
from zunclient.common import cliutils as utils
|
||||||
from zunclient.common import utils as zun_utils
|
from zunclient.common import utils as zun_utils
|
||||||
|
from zunclient.common.websocketclient import exceptions
|
||||||
|
from zunclient.common.websocketclient import websocketclient
|
||||||
from zunclient import exceptions as exc
|
from zunclient import exceptions as exc
|
||||||
|
|
||||||
|
|
||||||
@ -429,3 +431,26 @@ def do_update(cs, args):
|
|||||||
raise exc.CommandError("You must update at least one property")
|
raise exc.CommandError("You must update at least one property")
|
||||||
container = cs.containers.update(args.container, **opts)
|
container = cs.containers.update(args.container, **opts)
|
||||||
_show_container(container)
|
_show_container(container)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('container',
|
||||||
|
metavar='<container>',
|
||||||
|
help='ID or name of the container to be attahed to.')
|
||||||
|
def do_attach(cs, args):
|
||||||
|
"""Attach to a container."""
|
||||||
|
response = cs.containers.attach(args.container)
|
||||||
|
if response.startswith("ws://"):
|
||||||
|
try:
|
||||||
|
wscls = websocketclient.WebSocketClient(host_url=response,
|
||||||
|
id=args.container,
|
||||||
|
escape="~",
|
||||||
|
close_wait=0.5)
|
||||||
|
wscls.init_httpclient()
|
||||||
|
wscls.connect()
|
||||||
|
wscls.handle_resize()
|
||||||
|
wscls.start_loop()
|
||||||
|
except exceptions.ContainerWebSocketException as e:
|
||||||
|
print("%(e)s:%(container)s" %
|
||||||
|
{'e': e, 'container': args.container})
|
||||||
|
else:
|
||||||
|
raise exceptions.InvalidWebSocketLink(args.container)
|
||||||
|
Loading…
Reference in New Issue
Block a user