Commit 4be7e906 authored by Sébastien Luttringer's avatar Sébastien Luttringer
Browse files

Reimplement console and rshell

parent 2a187b83
Loading
Loading
Loading
Loading
+0 −43
Original line number Diff line number Diff line
@@ -414,46 +414,3 @@ class TqlCommand(RemoteCommand):
            args2[self.options.tql_index] = "id=%s"%obj["id"]
            # now we can make call
            self._unsecure_rpccall(args2, **kwargs)

class ConsoleCommand(TqlCommand):

    def __init__(self, cli, argv0):
        TqlCommand.__init__(self, cli, argv0)

    def start_console(self, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr):
        self.stdin = stdin.fileno()
        self.stdout = stdout.fileno()
        self.stderr = stderr.fileno()
        if self.stdin == sys.stdin.fileno():
            sys.stdin.flush()
        if self.stdout == sys.stdout.fileno():
            sys.stdout.flush()
        if self.stderr == sys.stderr.fileno():
            sys.stderr.flush()
        self._old_settings = termios.tcgetattr(self.stdout)
        tty.setraw(self.stdin)
        self.running = True

    def stop_console(self):
        # Reset terminal attributes.
        termios.tcsetattr(sys.stdout, termios.TCSADRAIN, self._old_settings)
        self.running = False

    def recv(self, size):
        return os.read(self.stdin, 1024)

    def send(self, data):
        return os.write(self.stdout, data)

    def fileno(self):
        return self.stdin

    def setblocking(self, blocking):
        # Do NOT be in non-blocking mode : non-blocking mode
        # causes rshell to get EAGAIN errors when having to
        # output loads of data, causing the TTY to break.
        return

    def close(self):
        # Reset terminal attributes.
        termios.tcsetattr(sys.stdout, termios.TCSADRAIN, self._old_settings)
+123 −49
Original line number Diff line number Diff line
@@ -2,32 +2,71 @@
#coding=utf8

'''
Connection to a remote shell
CloudControl Console related commands
'''

import os
import sys
import tty
from cccli.command import TqlCommand
from cccli.exception import *
from cccli.printer import Printer, color
from sjrpc.core import AsyncWatcher
from sjrpc.core.exceptions import *
from sjrpc.core.protocols import TunnelProtocol
import fcntl
import os
import signal
import struct
import sys
import termios
import threading
import time
import tty

from cccli.exception import *
from sjrpc.core.exceptions import *
from cccli.printer import Printer, color
from cccli.command import ConsoleCommand
class FakeTtySocket(object):

from sjrpc.core.protocols import TunnelProtocol
from sjrpc.core import AsyncWatcher
    '''Give a tty a socket interface

    This object cannot handle sigwinch directly because close
    can be called by remote RPC and by the way, called outside the main
    thread and fail.
    '''
    def __init__(self):
        if not os.isatty(0):
            raise cmdError("stdin is not tty")
        # flush stdin
        sys.stdin.flush()
        # save termios settings
        self._tc_attr = termios.tcgetattr(0)
        # set tty in raw mode
        tty.setraw(0)

    def recv(self, size):
        return os.read(0, 1024)

    def send(self, data):
        return os.write(0, data)

    def fileno(self):
        return 0

    def setblocking(self, blocking):
        # Do NOT be in non-blocking mode : non-blocking mode
        # causes rshell to get EAGAIN errors when having to
        # output loads of data, causing the TTY to break.
        return

    def close(self):
        # reset tty original state
        termios.tcsetattr(0, termios.TCSADRAIN, self._tc_attr)

class Command_console(ConsoleCommand):
    '''Start a remote shell on the provided host'''

class Command_rshell(TqlCommand):
    '''Start a shell on the provided host'''

    def __init__(self, cli, argv0):
        ConsoleCommand.__init__(self, cli, argv0)
        #self.tql_filter = "&con"
        TqlCommand.__init__(self, cli, argv0)
        self.remove_option("--direct")
        self.remove_option("--quiet")
        self.tql_filter = "&con"

    def __call__(self, argv):
        # args parse
@@ -35,42 +74,77 @@ class Command_console(ConsoleCommand):
        if len(self.args) != 1:
            raise cmdBadArgument()
        # rpccall
        self.rpccall("console", self.args[0], _callback=self._cb_open_shell,
                     _status=True, _direct=True)

    def _cb_open_shell(self, response):
        if not 'objects' in response:
            return
        response = response['objects'][0]
        if not 'status' in response:
            return
        if response['status'] == 'success':
            label = response['output']
        ans = self.rpccall("rshell", self.args[0], _status=False, _direct=True)
        # alias first object
        obj = ans['objects'][0]
        # check response is successful
        if obj['status'] != 'success':
            raise cmdError("%s: %s" % (obj["status"], obj["message"]))
        # create a callbeck function for sigwinch
        def _cb_sig_resize(*args):
            '''Callback for terminal resized'''
            size = struct.pack('HHHH', 0, 0, 0, 0)
            size = fcntl.ioctl(0, termios.TIOCGWINSZ, size)
            size = struct.unpack('HHHH', size)
            try:
                size = 0
                self.start_console(sys.stdin, sys.stdout, sys.stderr)
                self.rpc.call('rshell_resize', obj["output"], *size)
            except RpcError as e:
                # we dont care of failure of this
                self.printer.debug("Terminal resize failed: %s" % e)
        # Connect to remote end using sjrpc
                tun = self.rpc.create_tunnel(endpoint=self)
                # Save current terminal size
                size = struct.pack('HHHH', 0, 0, 0, 0)
                size = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, size)
                while self.running:
                    time.sleep(1)
            except Exception:
                # Cleanup
                self.stop_console()
                tun.shutdown()
                tun.close()
                # Restore terminal size
                fcntl.ioctl(sys.stdout.fileno(), termios.TIOCSWINSZ, size)
                raise
            else:
                # Cleanup
                self.stop_console()
                tun.shutdown()
        tun = self.rpc.create_tunnel(obj["output"], endpoint=FakeTtySocket())
        try:
            signal.signal(signal.SIGWINCH, _cb_sig_resize)
            # we need an async watcher to be able to handle sigwinch
            watcher = AsyncWatcher()
            watcher.register(self.rpc.rpc, "rshell_wait", obj["output"])
            watcher.wait()
        finally:
            # restore original signal
            signal.signal(signal.SIGWINCH, signal.SIG_DFL)
            # shutdown is empty, useless
            #tun.shutdown()
            # close tunnel if sjrpc doesn't do it
            tun.close()
                # Restore terminal size
                fcntl.ioctl(sys.stdout.fileno(), termios.TIOCSWINSZ, size)

    def remote_functions(self):
        return set(("rshell",))


class Command_console(TqlCommand):
    '''Start a vm console using libvirt'''

    def __init__(self, cli, argv0):
        TqlCommand.__init__(self, cli, argv0)
        self.remove_option("--direct")
        self.remove_option("--quiet")
        self.tql_filter = "&r=vm&status=running"

    def __call__(self, argv):
        # args parse
        self.parse_args(argv)
        if len(self.args) != 1:
            raise cmdBadArgument()
        # rpccall
        ans = self.rpccall("console", self.args[0], _status=False, _direct=True)
        # alias first object
        obj = ans['objects'][0]
        # check everything is ok
        if obj['status'] != 'success':
            raise cmdError("%s: %s" % (obj["status"], obj["message"]))
        # Connect to remote end using sjrpc
        ev = threading.Event()
        def cb_on_close(tun):
            tun.cb_default_on_close(tun)
            ev.set()
        try:
            tun = self.rpc.create_tunnel(obj["output"],
                                         endpoint=FakeTtySocket(),
                                         on_close=cb_on_close)
            ev.wait()
        finally:
            # see tun.close in previous command
            tun.close()

    def remote_functions(self):
        return set(("console",))

cccli/commands/rshell.py

deleted100644 → 0
+0 −95
Original line number Diff line number Diff line
#!/usr/bin/env python
#coding=utf8

'''
Connection to a remote shell
'''

import os
import sys
import tty
import fcntl
import signal
import struct
import termios

from cccli.exception import *
from sjrpc.core.exceptions import *
from cccli.printer import Printer, color
from cccli.command import ConsoleCommand

from sjrpc.core.protocols import TunnelProtocol
from sjrpc.core import AsyncWatcher

class Command_rshell(ConsoleCommand):
    '''Start a remote shell on the provided host'''

    def __init__(self, cli, argv0):
        ConsoleCommand.__init__(self, cli, argv0)
        self.tql_filter = "&con"

    def __call__(self, argv):
        # args parse
        self.parse_args(argv)
        if len(self.args) != 1:
            raise cmdBadArgument()
        # rpccall
        self.rpccall("rshell", self.args[0], _callback=self._cb_open_shell,
                     _status=True, _direct=True)

    def _cb_open_shell(self, response):
        tun = None
        if not 'objects' in response:
            return
        response = response['objects'][0]
        if not 'status' in response:
            return
        if response['status'] == 'success':
            label = response['output']
            try:
                # Create terminal
                self.start_console(sys.stdin, sys.stdout, sys.stderr)
                # Connect to remote end using sjrpc
                tun = self.rpc.create_tunnel(endpoint=self)
                # Terminal resizing event handler
                def sig_resize(signum, frame):
                    size = struct.pack('HHHH', 0, 0, 0, 0)
                    size = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, size)
                    size = struct.unpack('HHHH', size)
                    self.rpc.call('rshell_resize', label, *size)
                signal.signal(signal.SIGWINCH, sig_resize)
                # Init size for term
                size = struct.pack('HHHH', 0, 0, 0, 0)
                size = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, size)
                size = struct.unpack('HHHH', size)
                self.rpc.call('rshell_resize', label, *size)
                # Setup and run event loop
                watcher = AsyncWatcher()
                watcher.register(self.rpc.rpc, 'rshell_wait', label)
                while self.running:
                    returned = watcher.wait(1)
                    if returned:
                        if 'return' in returned[0]:
                            rcode = returned[0]['return']
                            break
                        elif 'error' in returned[0]:
                            raise RpcError(Exception(returned[0]['error']['message']),
                                           Exception(returned[0]['error']['message']))
                # Clear signal handlers
                signal.signal(signal.SIGWINCH, signal.SIG_IGN)
            except Exception:
                # Cleanup
                self.stop_console()
                if tun:
                    tun.shutdown()
                    tun.close()
                raise
            else:
                # Cleanup
                self.stop_console()
                if tun:
                    tun.shutdown()
                    tun.close()

    def remote_functions(self):
        return set(("rshell",))