1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-28 03:08:14 +00:00

Changed the way vpcs closes by using the socket to send quit message instead of pid kill

This commit is contained in:
Joe Bowen 2014-05-12 08:16:37 -06:00
parent bbce6b2c5c
commit 588ee8eed0
5 changed files with 58 additions and 709 deletions

View File

@ -18,8 +18,10 @@
import sys import sys
from .base import IModule from .base import IModule
from .dynamips import Dynamips from .dynamips import Dynamips
from .vpcs import VPCS
MODULES = [Dynamips] MODULES = [Dynamips]
MODULES.append(VPCS)
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
# IOU runs only on Linux # IOU runs only on Linux

View File

@ -65,17 +65,17 @@ class VPCS(IModule):
# get the VPCS location # get the VPCS location
config = Config.instance() config = Config.instance()
VPCS_config = config.get_section_config(name.upper()) VPCS_config = config.get_section_config(name.upper())
self._VPCS = VPCS_config.get("VPCS") self._VPCS = VPCS_config.get("vpcs")
if not self._VPCS or not os.path.isfile(self._VPCS): if not self._VPCS or not os.path.isfile(self._VPCS):
VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") VPCS_in_cwd = os.path.join(os.getcwd(), "vpcs")
if os.path.isfile(VPCS_in_cwd): if os.path.isfile(VPCS_in_cwd):
self._VPCS = VPCS_in_cwd self._VPCS = VPCS_in_cwd
else: else:
# look for VPCS if none is defined or accessible # look for VPCS if none is defined or accessible
for path in os.environ["PATH"].split(":"): for path in os.environ["PATH"].split(":"):
try: try:
if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): if "vpcs" in os.listdir(path) and os.access(os.path.join(path, "vpcs"), os.X_OK):
self._VPCS = os.path.join(path, "VPCS") self._VPCS = os.path.join(path, "vpcs")
break break
except OSError: except OSError:
continue continue
@ -102,8 +102,8 @@ class VPCS(IModule):
self._VPCSrc = "" self._VPCSrc = ""
# check every 5 seconds # check every 5 seconds
self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) #self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000)
self._VPCS_callback.start() #self._VPCS_callback.start()
def stop(self, signum=None): def stop(self, signum=None):
""" """
@ -135,12 +135,12 @@ class VPCS(IModule):
"id": VPCS_id, "id": VPCS_id,
"name": VPCS_instance.name} "name": VPCS_instance.name}
if not VPCS_instance.is_running(): if not VPCS_instance.is_running():
stdout = VPCS_instance.read_VPCS_stdout() stdout = VPCS_instance.read_vpcs_stdout()
notification["message"] = "VPCS has stopped running" notification["message"] = "VPCS has stopped running"
notification["details"] = stdout notification["details"] = stdout
self.send_notification("{}.VPCS_stopped".format(self.name), notification) self.send_notification("{}.VPCS_stopped".format(self.name), notification)
elif not VPCS_instance.is_VPCS_running(): elif not VPCS_instance.is_VPCS_running():
stdout = VPCS_instance.read_VPCS_stdout() stdout = VPCS_instance.read_vpcs_stdout()
notification["message"] = "VPCS has stopped running" notification["message"] = "VPCS has stopped running"
notification["details"] = stdout notification["details"] = stdout
self.send_notification("{}.VPCS_stopped".format(self.name), notification) self.send_notification("{}.VPCS_stopped".format(self.name), notification)
@ -161,7 +161,7 @@ class VPCS(IModule):
return None return None
return self._VPCS_instances[VPCS_id] return self._VPCS_instances[VPCS_id]
@IModule.route("VPCS.reset") @IModule.route("vpcs.reset")
def reset(self, request): def reset(self, request):
""" """
Resets the module. Resets the module.
@ -184,7 +184,7 @@ class VPCS(IModule):
log.info("VPCS module has been reset") log.info("VPCS module has been reset")
@IModule.route("VPCS.settings") @IModule.route("vpcs.settings")
def settings(self, request): def settings(self, request):
""" """
Set or update settings. Set or update settings.
@ -247,7 +247,7 @@ class VPCS(IModule):
return {"result": result, "message": message} return {"result": result, "message": message}
@IModule.route("VPCS.test_settings") @IModule.route("vpcs.test_settings")
def test_settings(self, request): def test_settings(self, request):
""" """
""" """
@ -256,7 +256,7 @@ class VPCS(IModule):
self.send_response(response) self.send_response(response)
@IModule.route("VPCS.create") @IModule.route("vpcs.create")
def VPCS_create(self, request): def VPCS_create(self, request):
""" """
Creates a new VPCS instance. Creates a new VPCS instance.
@ -313,7 +313,7 @@ class VPCS(IModule):
self._VPCS_instances[VPCS_instance.id] = VPCS_instance self._VPCS_instances[VPCS_instance.id] = VPCS_instance
self.send_response(response) self.send_response(response)
@IModule.route("VPCS.delete") @IModule.route("vpcs.delete")
def VPCS_delete(self, request): def VPCS_delete(self, request):
""" """
Deletes an VPCS instance. Deletes an VPCS instance.
@ -345,7 +345,7 @@ class VPCS(IModule):
self.send_response(True) self.send_response(True)
@IModule.route("VPCS.update") @IModule.route("vpcs.update")
def VPCS_update(self, request): def VPCS_update(self, request):
""" """
Updates an VPCS instance Updates an VPCS instance
@ -355,7 +355,7 @@ class VPCS(IModule):
Optional request parameters: Optional request parameters:
- any setting to update - any setting to update
- startup_config_base64 (startup-config base64 encoded) - script_file_base64 (script-file base64 encoded)
Response parameters: Response parameters:
- updated settings - updated settings
@ -374,20 +374,20 @@ class VPCS(IModule):
response = {} response = {}
try: try:
# a new startup-config has been pushed # a new script-file has been pushed
if "startup_config_base64" in request: if "script_file_base64" in request:
config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "") config = "!\n" + config.replace("\r", "")
config = config.replace('%h', VPCS_instance.name) config = config.replace('%h', VPCS_instance.name)
config_path = os.path.join(VPCS_instance.working_dir, "startup-config") config_path = os.path.join(VPCS_instance.working_dir, "script-file")
try: try:
with open(config_path, "w") as f: with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(config_path)) log.info("saving script-file to {}".format(config_path))
f.write(config) f.write(config)
except OSError as e: except OSError as e:
raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) raise VPCSError("Could not save the configuration {}: {}".format(config_path, e))
# update the request with the new local startup-config path # update the request with the new local script-file path
request["startup_config"] = os.path.basename(config_path) request["script_file"] = os.path.basename(config_path)
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
@ -405,7 +405,7 @@ class VPCS(IModule):
self.send_response(response) self.send_response(response)
@IModule.route("VPCS.start") @IModule.route("vpcs.start")
def vm_start(self, request): def vm_start(self, request):
""" """
Starts an VPCS instance. Starts an VPCS instance.
@ -438,7 +438,7 @@ class VPCS(IModule):
return return
self.send_response(True) self.send_response(True)
@IModule.route("VPCS.stop") @IModule.route("vpcs.stop")
def vm_stop(self, request): def vm_stop(self, request):
""" """
Stops an VPCS instance. Stops an VPCS instance.
@ -468,7 +468,7 @@ class VPCS(IModule):
return return
self.send_response(True) self.send_response(True)
@IModule.route("VPCS.reload") @IModule.route("vpcs.reload")
def vm_reload(self, request): def vm_reload(self, request):
""" """
Reloads an VPCS instance. Reloads an VPCS instance.
@ -500,7 +500,7 @@ class VPCS(IModule):
return return
self.send_response(True) self.send_response(True)
@IModule.route("VPCS.allocate_udp_port") @IModule.route("vpcs.allocate_udp_port")
def allocate_udp_port(self, request): def allocate_udp_port(self, request):
""" """
Allocates a UDP port in order to create an UDP NIO. Allocates a UDP port in order to create an UDP NIO.
@ -573,7 +573,7 @@ class VPCS(IModule):
raise VPCSError("{} has no privileged access to {}.".format(self._VPCS, device)) raise VPCSError("{} has no privileged access to {}.".format(self._VPCS, device))
@IModule.route("VPCS.add_nio") @IModule.route("vpcs.add_nio")
def add_nio(self, request): def add_nio(self, request):
""" """
Adds an NIO (Network Input/Output) for an VPCS instance. Adds an NIO (Network Input/Output) for an VPCS instance.
@ -633,7 +633,7 @@ class VPCS(IModule):
self.send_response({"port_id": request["port_id"]}) self.send_response({"port_id": request["port_id"]})
@IModule.route("VPCS.delete_nio") @IModule.route("vpcs.delete_nio")
def delete_nio(self, request): def delete_nio(self, request):
""" """
Deletes an NIO (Network Input/Output). Deletes an NIO (Network Input/Output).
@ -668,7 +668,7 @@ class VPCS(IModule):
self.send_response(True) self.send_response(True)
@IModule.route("VPCS.echo") @IModule.route("vpcs.echo")
def echo(self, request): def echo(self, request):
""" """
Echo end point for testing purposes. Echo end point for testing purposes.

View File

@ -27,7 +27,8 @@ import subprocess
import argparse import argparse
import threading import threading
import configparser import configparser
from .vpcscon import start_vpcscon import sys
import socket
from .vpcs_error import VPCSError from .vpcs_error import VPCSError
from .adapters.ethernet_adapter import EthernetAdapter from .adapters.ethernet_adapter import EthernetAdapter
from .nios.nio_udp import NIO_UDP from .nios.nio_udp import NIO_UDP
@ -51,11 +52,11 @@ class VPCSDevice(object):
def __init__(self, path, working_dir, host="127.0.0.1", name=None): def __init__(self, path, working_dir, host="127.0.0.1", name=None):
# find an instance identifier (0 <= id < 255) # find an instance identifier (1 <= id <= 255)
# This 255 limit is due to a restriction on the number of possible # This 255 limit is due to a restriction on the number of possible
# mac addresses given in VPCS using the -m option # mac addresses given in VPCS using the -m option
self._id = 0 self._id = 0
for identifier in range(0, 255): for identifier in range(1, 256):
if identifier not in self._instances: if identifier not in self._instances:
self._id = identifier self._id = identifier
self._instances.append(self._id) self._instances.append(self._id)
@ -74,9 +75,7 @@ class VPCSDevice(object):
self._command = [] self._command = []
self._process = None self._process = None
self._vpcs_stdout_file = "" self._vpcs_stdout_file = ""
self._vpcscon_thead = None self._host = "127.0.0.1"
self._vpcscon_thread_stop_event = None
self._host = host
self._started = False self._started = False
# VPCS settings # VPCS settings
@ -252,19 +251,6 @@ class VPCSDevice(object):
return self._started return self._started
def _start_vpcscon(self):
"""
Starts vpcscon thread (for console connections).
"""
if not self._vpcscon_thead:
telnet_server = "{}:{}".format(self._host, self._console)
log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server))
args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server)
self._vpcscon_thread_stop_event = threading.Event()
self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event))
self._vpcscon_thead.start() ", ".join(missing_libs)))
def start(self): def start(self):
""" """
Starts the VPCS process. Starts the VPCS process.
@ -297,9 +283,6 @@ class VPCSDevice(object):
log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
# start console support
self._start_vpcscon()
def stop(self): def stop(self):
""" """
Stops the VPCS process. Stops the VPCS process.
@ -309,23 +292,16 @@ class VPCSDevice(object):
if self.is_running(): if self.is_running():
log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid))
try: try:
self._process.terminate() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._process.wait(1) sock.connect((self._host, self._console))
except subprocess.TimeoutExpired: sock.send(bytes("quit\n", 'UTF-8'))
self._process.kill() sock.close()
if self._process.poll() == None: except TypeError as e:
log.warn("VPCS instance {} PID={} is still running".format(self._id, log.warn("VPCS instance {} PID={} is still running. Error: {}".format(self._id,
self._process.pid)) self._process.pid, e))
self._process = None self._process = None
self._started = False self._started = False
# stop console support
if self._vpcscon_thead:
self._vpcscon_thread_stop_event.set()
if self._vpcscon_thead.is_alive():
self._vpcscon_thead.join(timeout=0.10)
self._vpcscon_thead = None
def read_vpcs_stdout(self): def read_vpcs_stdout(self):
""" """
@ -349,8 +325,16 @@ class VPCSDevice(object):
:returns: True or False :returns: True or False
""" """
if self._process and self._process.poll() == None: if self._process:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self._host, self._console))
sock.close()
return True return True
except:
e = sys.exc_info()[0]
log.warn("Could not connect to {}:{}. Error: {}".format(self._host, self._console, e))
return False
return False return False

View File

@ -1,642 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013, 2014 James E. Carpenter
#
# 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/>.
#
import socket
import sys
import os
import select
import fcntl
import struct
import termios
import tty
import time
import argparse
import traceback
import logging
log = logging.getLogger(__name__)
# Escape characters
ESC_CHAR = '^^' # can be overriden from command line
ESC_QUIT = 'q'
# VPCS seems to only send *1* byte at a time. If
# they ever fix that we'll be ready for it.
BUFFER_SIZE = 1024
# How long to wait before retrying a connection (seconds)
RETRY_DELAY = 3
# How often to test an idle connection (seconds)
POLL_TIMEOUT = 3
EXIT_SUCCESS = 0
EXIT_FAILURE = 1
EXIT_ABORT = 2
# Mostly from:
# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
#--[ Telnet Commands ]---------------------------------------------------------
SE = 240 # End of subnegotiation parameters
NOP = 241 # No operation
DATMK = 242 # Data stream portion of a sync.
BREAK = 243 # NVT Character BRK
IP = 244 # Interrupt Process
AO = 245 # Abort Output
AYT = 246 # Are you there
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # The Go Ahead Signal
SB = 250 # Sub-option to follow
WILL = 251 # Will; request or confirm option begin
WONT = 252 # Wont; deny option request
DO = 253 # Do = Request or confirm remote option
DONT = 254 # Don't = Demand or confirm option halt
IAC = 255 # Interpret as Command
SEND = 1 # Sub-process negotiation SEND command
IS = 0 # Sub-process negotiation IS command
#--[ Telnet Options ]----------------------------------------------------------
BINARY = 0 # Transmit Binary
ECHO = 1 # Echo characters back to sender
RECON = 2 # Reconnection
SGA = 3 # Suppress Go-Ahead
TMARK = 6 # Timing Mark
TTYPE = 24 # Terminal Type
NAWS = 31 # Negotiate About Window Size
LINEMO = 34 # Line Mode
class FileLock:
# struct flock { /* from fcntl(2) */
# ...
# short l_type; /* Type of lock: F_RDLCK,
# F_WRLCK, F_UNLCK */
# short l_whence; /* How to interpret l_start:
# SEEK_SET, SEEK_CUR, SEEK_END */
# off_t l_start; /* Starting offset for lock */
# off_t l_len; /* Number of bytes to lock */
# pid_t l_pid; /* PID of process blocking our lock
# (F_GETLK only) */
# ...
# };
_flock = struct.Struct('hhqql')
def __init__(self, fname=None):
self.fd = None
self.fname = fname
def get_lock(self):
flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET,
0, 0, os.getpid())
flk = self._flock.unpack(
fcntl.fcntl(self.fd, fcntl.F_GETLK, flk))
# If it's not locked (or is locked by us) then return None,
# otherwise return the PID of the owner.
if flk[0] == fcntl.F_UNLCK:
return None
return flk[4]
def lock(self):
try:
self.fd = open('{}.lck'.format(self.fname), 'a')
except Exception as e:
raise LockError("Couldn't get lock on {}: {}"
.format(self.fname, e))
flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0)
try:
fcntl.fcntl(self.fd, fcntl.F_SETLK, flk)
except BlockingIOError:
raise LockError("Already connected. PID {} has lock on {}"
.format(self.get_lock(), self.fname))
# If we got here then we must have the lock. Store the PID.
self.fd.truncate(0)
self.fd.write('{}\n'.format(os.getpid()))
self.fd.flush()
def unlock(self):
if self.fd:
# Deleting first prevents a race condition
os.unlink(self.fd.name)
self.fd.close()
def __enter__(self):
self.lock()
def __exit__(self, exc_type, exc_val, exc_tb):
self.unlock()
return False
class Console:
def fileno(self):
raise NotImplementedError("Only routers have fileno()")
class Router:
pass
class TTY(Console):
def read(self, fileno, bufsize):
return self.fd.read(bufsize)
def write(self, buf):
return self.fd.write(buf)
def register(self, epoll):
self.epoll = epoll
epoll.register(self.fd, select.EPOLLIN | select.EPOLLET)
def __enter__(self):
try:
self.fd = open('/dev/tty', 'r+b', buffering=0)
except OSError as e:
raise TTYError("Couldn't open controlling TTY: {}".format(e))
# Save original flags
self.termios = termios.tcgetattr(self.fd)
self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
# Update flags
tty.setraw(self.fd, termios.TCSANOW)
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Restore flags to original settings
termios.tcsetattr(self.fd, termios.TCSANOW, self.termios)
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl)
self.fd.close()
return False
class TelnetServer(Console):
def __init__(self, addr, port, stop_event):
self.addr = addr
self.port = port
self.fd_dict = {}
self.stop_event = stop_event
def read(self, fileno, bufsize):
# Someone wants to connect?
if fileno == self.sock_fd.fileno():
self._accept()
return None
self._cur_fileno = fileno
# Read a maximum of _bufsize_ bytes without blocking. When it
# would want to block it means there's no more data. An empty
# buffer normally means that we've been disconnected.
try:
buf = self._read_cur(bufsize, socket.MSG_DONTWAIT)
except BlockingIOError:
return None
if not buf:
self._disconnect(fileno)
# Process and remove any telnet commands from the buffer
if IAC in buf:
buf = self._IAC_parser(buf)
return buf
def write(self, buf):
for fd in self.fd_dict.values():
fd.send(buf)
def register(self, epoll):
self.epoll = epoll
epoll.register(self.sock_fd, select.EPOLLIN)
def _read_block(self, bufsize):
buf = self._read_cur(bufsize, socket.MSG_WAITALL)
# If we don't get everything we were looking for then the
# client probably disconnected.
if len(buf) < bufsize:
self._disconnect(self._cur_fileno)
return buf
def _read_cur(self, bufsize, flags):
return self.fd_dict[self._cur_fileno].recv(bufsize, flags)
def _write_cur(self, buf):
return self.fd_dict[self._cur_fileno].send(buf)
def _IAC_parser(self, buf):
skip_to = 0
while not self.stop_event.is_set():
# Locate an IAC to process
iac_loc = buf.find(IAC, skip_to)
if iac_loc < 0:
break
# Get the TELNET command
iac_cmd = bytearray([IAC])
try:
iac_cmd.append(buf[iac_loc + 1])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 1])
# Is this just a 2-byte TELNET command?
if iac_cmd[1] not in [WILL, WONT, DO, DONT]:
if iac_cmd[1] == AYT:
log.debug("Telnet server received Are-You-There (AYT)")
self._write_cur(
b'\r\nYour Are-You-There received. I am here.\r\n'
)
elif iac_cmd[1] == IAC:
# It's data, not an IAC
iac_cmd.pop()
# This prevents the 0xff from being
# interputed as yet another IAC
skip_to = iac_loc + 1
log.debug("Received IAC IAC")
elif iac_cmd[1] == NOP:
pass
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x}".format(*iac_cmd))
# This must be a 3-byte TELNET command
else:
try:
iac_cmd.append(buf[iac_loc + 2])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 2])
# We do ECHO, SGA, and BINARY. Period.
if (iac_cmd[1] == DO
and iac_cmd[2] not in [ECHO, SGA, BINARY]):
self._write_cur(bytes([IAC, WONT, iac_cmd[2]]))
log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x} {2:#x}".format(*iac_cmd))
# Remove the entire TELNET command from the buffer
buf = buf.replace(iac_cmd, b'', 1)
# Return the new copy of the buffer, minus telnet commands
return buf
def _accept(self):
fd, addr = self.sock_fd.accept()
self.fd_dict[fd.fileno()] = fd
self.epoll.register(fd, select.EPOLLIN | select.EPOLLET)
log.info("Telnet connection from {}:{}".format(addr[0], addr[1]))
# This is a one-way negotiation. This is very basic so there
# shouldn't be any problems with any decent client.
fd.send(bytes([IAC, WILL, ECHO,
IAC, WILL, SGA,
IAC, WILL, BINARY,
IAC, DO, BINARY]))
if args.telnet_limit and len(self.fd_dict) > args.telnet_limit:
fd.send(b'\r\nToo many connections\r\n')
self._disconnect(fd.fileno())
log.warn("Client disconnected because of too many connections. "
"(limit currently {})".format(args.telnet_limit))
def _disconnect(self, fileno):
fd = self.fd_dict.pop(fileno)
log.info("Telnet client disconnected")
fd.shutdown(socket.SHUT_RDWR)
fd.close()
def __enter__(self):
# Open a socket and start listening
sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock_fd.bind((self.addr, self.port))
except OSError:
raise TelnetServerError("Cannot bind to {}:{}"
.format(self.addr, self.port))
sock_fd.listen(socket.SOMAXCONN)
self.sock_fd = sock_fd
log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port))
return self
def __exit__(self, exc_type, exc_val, exc_tb):
for fileno in list(self.fd_dict.keys()):
self._disconnect(fileno)
self.sock_fd.close()
return False
class VPCS(Router):
def __init__(self, ttyC, ttyS, stop_event):
self.ttyC = ttyC
self.ttyS = ttyS
self.stop_event = stop_event
def read(self, bufsize):
try:
buf = self.fd.recv(bufsize)
except BlockingIOError:
return None
return buf
def write(self, buf):
self.fd.send(buf)
def _open(self):
self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.fd.setblocking(False)
def _bind(self):
try:
os.unlink(self.ttyC)
except FileNotFoundError:
pass
except Exception as e:
raise NetioError("Couldn't unlink socket {}: {}"
.format(self.ttyC, e))
try:
self.fd.bind(self.ttyC)
except Exception as e:
raise NetioError("Couldn't create socket {}: {}"
.format(self.ttyC, e))
def _connect(self):
# Keep trying until we connect or die trying
while not self.stop_event.is_set():
try:
self.fd.connect(self.ttyS)
except FileNotFoundError:
log.debug("Waiting to connect to {}".format(self.ttyS))
time.sleep(RETRY_DELAY)
except Exception as e:
raise NetioError("Couldn't connect to socket {}: {}"
.format(self.ttyS, e))
else:
break
def register(self, epoll):
self.epoll = epoll
epoll.register(self.fd, select.EPOLLIN | select.EPOLLET)
def fileno(self):
return self.fd.fileno()
def __enter__(self):
self._open()
self._bind()
self._connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
os.unlink(self.ttyC)
self.fd.close()
return False
class VPCSConError(Exception):
pass
class LockError(VPCSConError):
pass
class NetioError(VPCSConError):
pass
class TTYError(VPCSConError):
pass
class TelnetServerError(VPCSConError):
pass
class ConfigError(VPCSConError):
pass
def mkdir_netio(netio_dir):
try:
os.mkdir(netio_dir)
except FileExistsError:
pass
except Exception as e:
raise NetioError("Couldn't create directory {}: {}"
.format(netio_dir, e))
def send_recv_loop(console, router, esc_char, stop_event):
epoll = select.epoll()
router.register(epoll)
console.register(epoll)
router_fileno = router.fileno()
esc_quit = bytes(ESC_QUIT.upper(), 'ascii')
esc_state = False
while not stop_event.is_set():
event_list = epoll.poll(timeout=POLL_TIMEOUT)
# When/if the poll times out we send an empty datagram. If VPCS
# has gone away then this will toss a ConnectionRefusedError
# exception.
if not event_list:
router.write(b'')
continue
for fileno, event in event_list:
buf = bytearray()
# VPCS --> tty(s)
if fileno == router_fileno:
while not stop_event.is_set():
data = router.read(BUFFER_SIZE)
if not data:
break
buf.extend(data)
console.write(buf)
# tty --> VPCS
else:
while not stop_event.is_set():
data = console.read(fileno, BUFFER_SIZE)
if not data:
break
buf.extend(data)
# If we just received the escape character then
# enter the escape state.
#
# If we are in the escape state then check for a
# quit command. Or if it's the escape character then
# send the escape character. Else, send the escape
# character we ate earlier and whatever character we
# just got. Exit escape state.
#
# If we're not in the escape state and this isn't an
# escape character then just send it to VPCS.
if esc_state:
if buf.upper() == esc_quit:
sys.exit(EXIT_SUCCESS)
elif buf == esc_char:
router.write(esc_char)
else:
router.write(esc_char)
router.write(buf)
esc_state = False
elif buf == esc_char:
esc_state = True
else:
router.write(buf)
def get_args():
parser = argparse.ArgumentParser(
description='Connect to an VPCS console port.')
parser.add_argument('-d', '--debug', action='store_true',
help='display some debugging information')
parser.add_argument('-e', '--escape',
help='set escape character (default: %(default)s)',
default=ESC_CHAR, metavar='CHAR')
parser.add_argument('-t', '--telnet-server',
help='start telnet server listening on ADDR:PORT',
metavar='ADDR:PORT', default=False)
parser.add_argument('-l', '--telnet-limit',
help='maximum number of simultaneous '
'telnet connections (default: %(default)s)',
metavar='LIMIT', type=int, default=1)
parser.add_argument('appl_id', help='VPCS instance identifier')
return parser.parse_args()
def get_escape_character(escape):
# Figure out the escape character to use.
# Can be any ASCII character or a spelled out control
# character, like "^e". The string "none" disables it.
if escape.lower() == 'none':
esc_char = b''
elif len(escape) == 2 and escape[0] == '^':
c = ord(escape[1].upper()) - 0x40
if not 0 <= c <= 0x1f: # control code range
raise ConfigError("Invalid control code")
esc_char = bytes([c])
elif len(escape) == 1:
try:
esc_char = bytes(escape, 'ascii')
except ValueError as e:
raise ConfigError("Invalid escape character") from e
else:
raise ConfigError("Invalid length for escape character")
return esc_char
def start_VPCScon(cmdline_args, stop_event):
global args
args = cmdline_args
if args.debug:
logging.basicConfig(level=logging.DEBUG)
else:
# default logging level
logging.basicConfig(level=logging.INFO)
# Create paths for the Unix domain sockets
netio = '/tmp/netio{}'.format(os.getuid())
ttyC = '{}/ttyC{}'.format(netio, args.appl_id)
ttyS = '{}/ttyS{}'.format(netio, args.appl_id)
try:
mkdir_netio(netio)
with FileLock(ttyC):
esc_char = get_escape_character(args.escape)
if args.telnet_server:
addr, _, port = args.telnet_server.partition(':')
nport = 0
try:
nport = int(port)
except ValueError:
pass
if (addr == '' or nport == 0):
raise ConfigError('format for --telnet-server must be '
'ADDR:PORT (like 127.0.0.1:20000)')
while not stop_event.is_set():
try:
if args.telnet_server:
with TelnetServer(addr, nport, stop_event) as console:
with VPCS(ttyC, ttyS, stop_event) as router:
send_recv_loop(console, router, b'', stop_event)
else:
with VPCS(ttyC, ttyS, stop_event) as router, TTY() as console:
send_recv_loop(console, router, esc_char, stop_event)
except ConnectionRefusedError:
pass
except KeyboardInterrupt:
sys.exit(EXIT_ABORT)
finally:
# Put us at the beginning of a line
if not args.telnet_server:
print()
except VPCSConError as e:
if args.debug:
traceback.print_exc(file=sys.stderr)
else:
print(e, file=sys.stderr)
sys.exit(EXIT_FAILURE)
log.info("exiting...")
def main():
import threading
stop_event = threading.Event()
args = get_args()
start_VPCScon(args, stop_event)
if __name__ == '__main__':
main()

5
vpcs.hist Normal file
View File

@ -0,0 +1,5 @@
show
quit
show
quot
quit