1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-24 09:18:08 +00:00

Telnet to pipe support for VirtualBox.

This commit is contained in:
grossmj 2014-07-17 20:02:18 -06:00
parent 9ac2716826
commit 65d70bacfa
4 changed files with 532 additions and 9 deletions

View File

@ -521,7 +521,7 @@ class IOUDevice(object):
try:
output = subprocess.check_output(["ldd", self._path])
except (subprocess.SubprocessError, FileNotFoundError) as e:
except (FileNotFoundError, subprocess.CalledProcessError) as e:
log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e))
return
@ -761,7 +761,7 @@ class IOUDevice(object):
command.extend(["-l"])
else:
raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path)))
except OSError as e:
except (OSError, subprocess.CalledProcessError) as e:
log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e))
def _build_command(self):

View File

@ -0,0 +1,477 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Parts of this code have been taken from Pyserial project (http://pyserial.sourceforge.net/) under Python license
import sys
import time
import threading
import socket
import select
if sys.platform.startswith("win"):
import win32pipe
import win32file
class PipeProxy(threading.Thread):
def __init__(self, name, pipe, host, port):
self.devname = name
self.pipe = pipe
self.host = host
self.port = port
self.server = None
self.reader_thread = None
self.use_thread = False
self._write_lock = threading.Lock()
self.clients = {}
self.timeout = 0.1
self.alive = True
if sys.platform.startswith("win"):
# we must a thread for reading the pipe on Windows because it is a Named Pipe and it cannot be monitored by select()
self.use_thread = True
try:
if self.host.__contains__(':'):
# IPv6 address support
self.server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((self.host, int(self.port)))
self.server.listen(5)
except socket.error as msg:
self.error("unable to create the socket server %s" % msg)
return
threading.Thread.__init__(self)
self.debug("initialized, waiting for clients on %s:%i..." % (self.host, self.port))
def error(self, msg):
sys.stderr.write("ERROR -> %s PIPE PROXY: %s\n" % (self.devname, msg))
def debug(self, msg):
sys.stdout.write("INFO -> %s PIPE PROXY: %s\n" % (self.devname, msg))
def run(self):
while True:
recv_list = [self.server.fileno()]
if not self.use_thread:
recv_list.append(self.pipe.fileno())
for client in self.clients.values():
if client.active:
recv_list.append(client.fileno)
else:
self.debug("lost client %s" % client.addrport())
try:
client.sock.close()
except:
pass
del self.clients[client.fileno]
try:
rlist, slist, elist = select.select(recv_list, [], [], self.timeout)
except select.error as err:
self.error("fatal select error %d:%s" % (err[0], err[1]))
return False
if not self.alive:
self.debug('Exiting ...')
return True
for sock_fileno in rlist:
if sock_fileno == self.server.fileno():
try:
sock, addr = self.server.accept()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.debug("new client %s:%s" % (addr[0], addr[1]))
except socket.error as err:
self.error("accept error %d:%s" % (err[0], err[1]))
continue
new_client = TelnetClient(sock, addr)
self.clients[new_client.fileno] = new_client
welcome_msg = "%s console is now available ... Press RETURN to get started.\r\n" % self.devname
sock.send(welcome_msg.encode('utf-8'))
if self.use_thread and not self.reader_thread:
self.reader_thread = threading.Thread(target=self.reader)
self.reader_thread.setDaemon(True)
self.reader_thread.setName('pipe->socket')
self.reader_thread.start()
elif not self.use_thread and sock_fileno == self.pipe.fileno():
data = self.read_from_pipe()
if not data:
self.debug("pipe has been closed!")
return False
for client in self.clients.values():
try:
client.send(data)
except:
self.debug(msg)
client.deactivate()
elif sock_fileno in self.clients:
try:
data = self.clients[sock_fileno].socket_recv()
# For some reason, windows likes to send "cr/lf" when you send a "cr".
# Strip that so we don't get a double prompt.
#data = string.replace(data, chr(13) + chr(10), chr(13))
data = data.replace(bytearray([13, 10]), bytes(13))
self.write_to_pipe(data)
except Exception as msg:
self.debug(msg)
self.clients[sock_fileno].deactivate()
def write_to_pipe(self, data):
if sys.platform.startswith('win'):
win32file.WriteFile(self.pipe, data)
else:
self.pipe.sendall(data)
def read_from_pipe(self):
if sys.platform.startswith('win'):
(read, num_avail, num_message) = win32pipe.PeekNamedPipe(self.pipe, 0)
if num_avail > 0:
(error_code, output) = win32file.ReadFile(self.pipe, num_avail, None)
return output
return ""
else:
return self.pipe.recv(1024)
def reader(self):
"""loop forever and copy pipe->socket"""
self.debug("reader thread started")
while self.alive:
try:
data = self.read_from_pipe()
if not data and not sys.platform.startswith('win'):
self.debug("pipe has been closed!")
break
self._write_lock.acquire()
try:
for client in self.clients.values():
client.send(data)
finally:
self._write_lock.release()
if sys.platform.startswith('win'):
# sleep every 10 ms
time.sleep(0.01)
except:
self.debug("pipe has been closed!")
break
self.debug("reader thread exited")
self.stop()
def stop(self):
"""Stop copying"""
if self.alive:
self.alive = False
for client in self.clients.values():
client.sock.close()
client.deactivate()
# telnet protocol characters
IAC = 255 # Interpret As Command
DONT = 254
DO = 253
WONT = 252
WILL = 251
IAC_DOUBLED = [IAC, IAC]
SE = 240 # Subnegotiation End
NOP = 241 # No Operation
DM = 242 # Data Mark
BRK = 243 # Break
IP = 244 # Interrupt process
AO = 245 # Abort output
AYT = 246 # Are You There
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # Go Ahead
SB = 250 # Subnegotiation Begin
# selected telnet options
ECHO = 1 # echo
SGA = 3 # suppress go ahead
LINEMODE = 34 # line mode
TERMTYPE = 24 # terminal type
# Telnet filter states
M_NORMAL = 0
M_IAC_SEEN = 1
M_NEGOTIATE = 2
# TelnetOption and TelnetSubnegotiation states
REQUESTED = 'REQUESTED'
ACTIVE = 'ACTIVE'
INACTIVE = 'INACTIVE'
REALLY_INACTIVE = 'REALLY_INACTIVE'
class TelnetOption(object):
"""Manage a single telnet option, keeps track of DO/DONT WILL/WONT."""
def __init__(self, connection, name, option, send_yes, send_no, ack_yes, ack_no, initial_state, activation_callback=None):
"""Init option.
:param connection: connection used to transmit answers
:param name: a readable name for debug outputs
:param send_yes: what to send when option is to be enabled.
:param send_no: what to send when option is to be disabled.
:param ack_yes: what to expect when remote agrees on option.
:param ack_no: what to expect when remote disagrees on option.
:param initial_state: options initialized with REQUESTED are tried to
be enabled on startup. use INACTIVE for all others.
"""
self.connection = connection
self.name = name
self.option = option
self.send_yes = send_yes
self.send_no = send_no
self.ack_yes = ack_yes
self.ack_no = ack_no
self.state = initial_state
self.active = False
self.activation_callback = activation_callback
def __repr__(self):
"""String for debug outputs"""
return "%s:%s(%s)" % (self.name, self.active, self.state)
def process_incoming(self, command):
"""A DO/DONT/WILL/WONT was received for this option, update state and
answer when needed."""
if command == self.ack_yes:
if self.state is REQUESTED:
self.state = ACTIVE
self.active = True
if self.activation_callback is not None:
self.activation_callback()
elif self.state is ACTIVE:
pass
elif self.state is INACTIVE:
self.state = ACTIVE
self.connection.telnetSendOption(self.send_yes, self.option)
self.active = True
if self.activation_callback is not None:
self.activation_callback()
elif self.state is REALLY_INACTIVE:
self.connection.telnetSendOption(self.send_no, self.option)
else:
raise ValueError('option in illegal state %r' % self)
elif command == self.ack_no:
if self.state is REQUESTED:
self.state = INACTIVE
self.active = False
elif self.state is ACTIVE:
self.state = INACTIVE
self.connection.telnetSendOption(self.send_no, self.option)
self.active = False
elif self.state is INACTIVE:
pass
elif self.state is REALLY_INACTIVE:
pass
else:
raise ValueError('option in illegal state %r' % self)
class TelnetClient(object):
"""
Represents a client connection via Telnet.
First argument is the socket discovered by the Telnet Server.
Second argument is the tuple (ip address, port number).
"""
def __init__(self, sock, addr_tup):
self.active = True # Turns False when the connection is lost
self.sock = sock # The connection's socket
self.fileno = sock.fileno() # The socket's file descriptor
self.address = addr_tup[0] # The client's remote TCP/IP address
self.port = addr_tup[1] # The client's remote port
# filter state machine
self.mode = M_NORMAL
self.suboption = None
self.telnet_command = None
# all supported telnet options
self._telnet_options = [
TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED),
TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED),
TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE),
TelnetOption(self, 'LINEMODE', LINEMODE, DONT, DONT, WILL, WONT, REQUESTED),
TelnetOption(self, 'TERMTYPE', TERMTYPE, DO, DONT, WILL, WONT, REQUESTED),
]
for option in self._telnet_options:
if option.state is REQUESTED:
self.telnetSendOption(option.send_yes, option.option)
def telnetSendOption(self, action, option):
"""Send DO, DONT, WILL, WONT."""
self.sock.sendall(bytes([IAC, action, option]))
def escape(self, data):
""" All outgoing data has to be properly escaped, so that no IAC character
in the data stream messes up the Telnet state machine in the server.
"""
for byte in data:
if byte == IAC:
yield IAC
yield IAC
else:
yield byte
def filter(self, data):
""" handle a bunch of incoming bytes. this is a generator. it will yield
all characters not of interest for Telnet
"""
for byte in data:
if self.mode == M_NORMAL:
# interpret as command or as data
if byte == IAC:
self.mode = M_IAC_SEEN
else:
# store data in sub option buffer or pass it to our
# consumer depending on state
if self.suboption is not None:
self.suboption.append(byte)
else:
yield byte
elif self.mode == M_IAC_SEEN:
if byte == IAC:
# interpret as command doubled -> insert character
# itself
if self.suboption is not None:
self.suboption.append(byte)
else:
yield byte
self.mode = M_NORMAL
elif byte == SB:
# sub option start
self.suboption = bytearray()
self.mode = M_NORMAL
elif byte == SE:
# sub option end -> process it now
#self._telnetProcessSubnegotiation(bytes(self.suboption))
self.suboption = None
self.mode = M_NORMAL
elif byte in (DO, DONT, WILL, WONT):
# negotiation
self.telnet_command = byte
self.mode = M_NEGOTIATE
else:
# other telnet commands are ignored!
self.mode = M_NORMAL
elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
self._telnetNegotiateOption(self.telnet_command, byte)
self.mode = M_NORMAL
def _telnetNegotiateOption(self, command, option):
"""Process incoming DO, DONT, WILL, WONT."""
# check our registered telnet options and forward command to them
# they know themselves if they have to answer or not
known = False
for item in self._telnet_options:
# can have more than one match! as some options are duplicated for
# 'us' and 'them'
if item.option == option:
item.process_incoming(command)
known = True
if not known:
# handle unknown options
# only answer to positive requests and deny them
if command == WILL or command == DO:
self.telnetSendOption((command == WILL and DONT or WONT), option)
def send(self, data):
"""
Send data to the distant end.
"""
try:
self.sock.sendall(bytes(self.escape(data)))
except socket.error as ex:
self.active = False
raise Exception("socket.sendall() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport()))
def deactivate(self):
"""
Set the client to disconnect on the next server poll.
"""
self.active = False
def addrport(self):
"""
Return the DE's IP address and port number as a string.
"""
return "%s:%s" % (self.address, self.port)
def socket_recv(self):
"""
Called by TelnetServer when recv data is ready.
"""
try:
data = self.sock.recv(4096)
except socket.error as ex:
raise Exception("socket.recv() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport()))
## Did they close the connection?
size = len(data)
if size == 0:
raise Exception("connection closed by %s" % self.addrport())
return bytes(self.filter(data))
if __name__ == '__main__':
if sys.platform.startswith('win'):
import msvcrt
pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_3.8.2'
pipe = open(pipe_name, 'a+b')
pipe_proxy = PipeProxy("VBOX", msvcrt.get_osfhandle(pipe.fileno()), '127.0.0.1', 3900)
else:
try:
unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
#unix_socket.settimeout(0.1)
unix_socket.connect("/tmp/pipe_test")
except socket.error as err:
print("Socket error -> %d:%s" % (err[0], err[1]))
sys.exit(False)
pipe_proxy = PipeProxy('VBOX', unix_socket, '127.0.0.1', 3900)
pipe_proxy.setDaemon(True)
pipe_proxy.start()
pipe.proxy.stop()
pipe_proxy.join()

View File

@ -25,14 +25,21 @@ import shutil
import tempfile
import re
import time
import socket
from .pipe_proxy import PipeProxy
from .virtualbox_error import VirtualBoxError
from .adapters.ethernet_adapter import EthernetAdapter
from ..attic import find_unused_port
if sys.platform.startswith('win'):
import msvcrt
import win32file
import logging
log = logging.getLogger(__name__)
class VirtualBoxVM(object):
"""
VirtualBox VM implementation.
@ -91,6 +98,10 @@ class VirtualBoxVM(object):
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
# Telnet to pipe mini-server
self._serial_pipe_thread = None
self._serial_pipe = None
# VirtualBox API variables
self._machine = None
self._session = None
@ -464,6 +475,26 @@ class VirtualBoxVM(object):
except Exception:
pass
# starts the Telnet to pipe thread
pipe_name = self._get_pipe_name()
if sys.platform.startswith('win'):
try:
self._serial_pipe = open(pipe_name, "a+b")
except OSError as e:
raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e))
self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console)
self._serial_pipe_thread.setDaemon(True)
self._serial_pipe_thread.start()
else:
try:
self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._serial_pipe.connect(pipe_name)
except OSError as e:
raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e))
self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console)
self._serial_pipe_thread.setDaemon(True)
self._serial_pipe_thread.start()
def stop(self):
"""
Stops this VirtualBox VM.
@ -489,6 +520,18 @@ class VirtualBoxVM(object):
# This can happen, if user manually kills VBox VM.
log.warn("could not stop VM for {}: {}".format(self._vmname, e))
return
finally:
if self._serial_pipe_thread:
self._serial_pipe_thread.stop()
self._serial_pipe_thread.join()
self._serial_pipe_thread = None
if self._serial_pipe:
if sys.platform.startswith('win'):
win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno()))
else:
self._serial_pipe.close()
self._serial_pipe = None
def suspend(self):
"""
@ -835,19 +878,22 @@ class VirtualBoxVM(object):
time.sleep(0.75)
continue
def _set_console_options(self):
def _get_pipe_name(self):
log.info("setting console options for {}".format(self.vmname))
self._lock_machine()
# pick a pipe name
p = re.compile('\s+', re.UNICODE)
pipe_name = p.sub("_", self._vmname)
if sys.platform.startswith('win'):
pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name)
else:
pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name))
return pipe_name
def _set_console_options(self):
log.info("setting console options for {}".format(self.vmname))
self._lock_machine()
pipe_name = self._get_pipe_name()
try:
serial_port = self._session.machine.getSerialPort(0)

View File

@ -348,7 +348,7 @@ class VPCSDevice(object):
raise VPCSError("VPCS executable version must be >= 0.5b1")
else:
raise VPCSError("Could not determine the VPCS version for {}".format(self._path))
except OSError as e:
except (OSError, subprocess.CalledProcessError) as e:
raise VPCSError("Error while looking for the VPCS version: {}".format(e))
def start(self):