mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Merge remote-tracking branch 'origin/asyncio' into asyncio
This commit is contained in:
commit
0a5aaedc7c
44 files changed
+1419
-5194
No files matched your search
@@ -18,5 +18,6 @@
|
||||
from .vpcs import VPCS
|
||||
from .virtualbox import VirtualBox
|
||||
from .dynamips import Dynamips
|
||||
from .iou import IOU
|
||||
|
||||
MODULES = [VPCS, VirtualBox, Dynamips]
|
||||
MODULES = [VPCS, VirtualBox, Dynamips, IOU]
|
||||
@@ -29,4 +29,4 @@ class EthernetAdapter(Adapter):
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "VPCS Ethernet adapter"
|
||||
return "Ethernet adapter"
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from .adapter import Adapter
|
||||
|
||||
|
||||
class SerialAdapter(Adapter):
|
||||
|
||||
"""
|
||||
VPCS Ethernet adapter.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Adapter.__init__(self, interfaces=1)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Serial adapter"
|
||||
@@ -32,8 +32,8 @@ from ..config import Config
|
||||
from ..utils.asyncio import wait_run_in_executor
|
||||
from .project_manager import ProjectManager
|
||||
|
||||
from .nios.nio_udp import NIOUDP
|
||||
from .nios.nio_tap import NIOTAP
|
||||
from .nios.nio_udp import NIO_UDP
|
||||
from .nios.nio_tap import NIO_TAP
|
||||
|
||||
|
||||
class BaseManager:
|
||||
@@ -274,11 +274,11 @@ class BaseManager:
|
||||
sock.connect((rhost, rport))
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
nio = NIOUDP(lport, rhost, rport)
|
||||
nio = NIO_UDP(lport, rhost, rport)
|
||||
elif nio_settings["type"] == "nio_tap":
|
||||
tap_device = nio_settings["tap_device"]
|
||||
if not self._has_privileged_access(executable):
|
||||
raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device))
|
||||
nio = NIOTAP(tap_device)
|
||||
nio = NIO_TAP(tap_device)
|
||||
assert nio is not None
|
||||
return nio
|
||||
@@ -24,18 +24,4 @@ from ..vm_error import VMError
|
||||
|
||||
class DynamipsError(VMError):
|
||||
|
||||
def __init__(self, message, original_exception=None):
|
||||
|
||||
Exception.__init__(self, message)
|
||||
if isinstance(message, Exception):
|
||||
message = str(message)
|
||||
self._message = message
|
||||
self._original_exception = original_exception
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return self._message
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self._message
|
||||
pass
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 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/>.
|
||||
|
||||
"""
|
||||
IOU server module.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from ..base_manager import BaseManager
|
||||
from .iou_error import IOUError
|
||||
from .iou_vm import IOUVM
|
||||
|
||||
|
||||
class IOU(BaseManager):
|
||||
_VM_CLASS = IOUVM
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._free_application_ids = list(range(1, 512))
|
||||
self._used_application_ids = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def create_vm(self, *args, **kwargs):
|
||||
|
||||
vm = yield from super().create_vm(*args, **kwargs)
|
||||
try:
|
||||
self._used_application_ids[vm.id] = self._free_application_ids.pop(0)
|
||||
except IndexError:
|
||||
raise IOUError("No mac address available")
|
||||
return vm
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete_vm(self, vm_id, *args, **kwargs):
|
||||
|
||||
vm = self.get_vm(vm_id)
|
||||
i = self._used_application_ids[vm_id]
|
||||
self._free_application_ids.insert(0, i)
|
||||
del self._used_application_ids[vm_id]
|
||||
yield from super().delete_vm(vm_id, *args, **kwargs)
|
||||
|
||||
def get_application_id(self, vm_id):
|
||||
"""
|
||||
Get an unique IOU mac id
|
||||
|
||||
:param vm_id: ID of the IOU VM
|
||||
:returns: IOU MAC id
|
||||
"""
|
||||
|
||||
return self._used_application_ids.get(vm_id, 1)
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 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/>.
|
||||
|
||||
"""
|
||||
Custom exceptions for IOU module.
|
||||
"""
|
||||
|
||||
from ..vm_error import VMError
|
||||
|
||||
|
||||
class IOUError(VMError):
|
||||
pass
|
||||
@@ -0,0 +1,761 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 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/>.
|
||||
|
||||
"""
|
||||
IOU VM management (creates command line, processes, files etc.) in
|
||||
order to run an IOU instance.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
import re
|
||||
import asyncio
|
||||
import shutil
|
||||
import argparse
|
||||
import threading
|
||||
import configparser
|
||||
|
||||
from pkg_resources import parse_version
|
||||
from .iou_error import IOUError
|
||||
from ..adapters.ethernet_adapter import EthernetAdapter
|
||||
from ..adapters.serial_adapter import SerialAdapter
|
||||
from ..nios.nio_udp import NIO_UDP
|
||||
from ..nios.nio_tap import NIO_TAP
|
||||
from ..base_vm import BaseVM
|
||||
from .ioucon import start_ioucon
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IOUVM(BaseVM):
|
||||
module_name = 'iou'
|
||||
|
||||
"""
|
||||
IOU vm implementation.
|
||||
|
||||
:param name: name of this IOU vm
|
||||
:param vm_id: IOU instance identifier
|
||||
:param project: Project instance
|
||||
:param manager: parent VM Manager
|
||||
:param console: TCP console port
|
||||
:params console_host: TCP console host IP
|
||||
:params ethernet_adapters: Number of ethernet adapters
|
||||
:params serial_adapters: Number of serial adapters
|
||||
:params ram: Ram MB
|
||||
:params nvram: Nvram KB
|
||||
"""
|
||||
|
||||
def __init__(self, name, vm_id, project, manager,
|
||||
console=None,
|
||||
console_host="0.0.0.0",
|
||||
ram=None,
|
||||
nvram=None,
|
||||
ethernet_adapters=None,
|
||||
serial_adapters=None):
|
||||
|
||||
super().__init__(name, vm_id, project, manager)
|
||||
|
||||
self._console = console
|
||||
self._command = []
|
||||
self._iouyap_process = None
|
||||
self._iou_process = None
|
||||
self._iou_stdout_file = ""
|
||||
self._started = False
|
||||
self._path = None
|
||||
self._iourc_path = None
|
||||
self._ioucon_thread = None
|
||||
self._console_host = console_host
|
||||
|
||||
# IOU settings
|
||||
self._ethernet_adapters = []
|
||||
self._serial_adapters = []
|
||||
self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces
|
||||
self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces
|
||||
self._use_default_iou_values = True # for RAM & NVRAM values
|
||||
self._nvram = 128 if nvram is None else nvram # Kilobytes
|
||||
self._initial_config = ""
|
||||
self._ram = 256 if ram is None else ram # Megabytes
|
||||
self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes).
|
||||
|
||||
if self._console is not None:
|
||||
self._console = self._manager.port_manager.reserve_console_port(self._console)
|
||||
else:
|
||||
self._console = self._manager.port_manager.get_free_console_port()
|
||||
|
||||
def close(self):
|
||||
|
||||
if self._console:
|
||||
self._manager.port_manager.release_console_port(self._console)
|
||||
self._console = None
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Path of the iou binary"""
|
||||
|
||||
return self._path
|
||||
|
||||
@path.setter
|
||||
def path(self, path):
|
||||
"""
|
||||
Path of the iou binary
|
||||
|
||||
:params path: Path to the binary
|
||||
"""
|
||||
|
||||
self._path = path
|
||||
if not os.path.isfile(self._path) or not os.path.exists(self._path):
|
||||
if os.path.islink(self._path):
|
||||
raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path)))
|
||||
else:
|
||||
raise IOUError("IOU image '{}' is not accessible".format(self._path))
|
||||
|
||||
try:
|
||||
with open(self._path, "rb") as f:
|
||||
# read the first 7 bytes of the file.
|
||||
elf_header_start = f.read(7)
|
||||
except OSError as e:
|
||||
raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e))
|
||||
|
||||
# IOU images must start with the ELF magic number, be 32-bit, little endian
|
||||
# and have an ELF version of 1 normal IOS image are big endian!
|
||||
if elf_header_start != b'\x7fELF\x01\x01\x01':
|
||||
raise IOUError("'{}' is not a valid IOU image".format(self._path))
|
||||
|
||||
if not os.access(self._path, os.X_OK):
|
||||
raise IOUError("IOU image '{}' is not executable".format(self._path))
|
||||
|
||||
@property
|
||||
def iourc_path(self):
|
||||
"""
|
||||
Returns the path to the iourc file.
|
||||
:returns: path to the iourc file
|
||||
"""
|
||||
|
||||
return self._iourc_path
|
||||
|
||||
@iourc_path.setter
|
||||
def iourc_path(self, path):
|
||||
"""
|
||||
Set path to IOURC file
|
||||
"""
|
||||
|
||||
self._iourc_path = path
|
||||
log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name,
|
||||
id=self._id,
|
||||
path=self._iourc_path))
|
||||
|
||||
@property
|
||||
def use_default_iou_values(self):
|
||||
"""
|
||||
Returns if this device uses the default IOU image values.
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._use_default_iou_values
|
||||
|
||||
@use_default_iou_values.setter
|
||||
def use_default_iou_values(self, state):
|
||||
"""
|
||||
Sets if this device uses the default IOU image values.
|
||||
:param state: boolean
|
||||
"""
|
||||
|
||||
self._use_default_iou_values = state
|
||||
if state:
|
||||
log.info("IOU {name} [id={id}]: uses the default IOU image values".format(name=self._name, id=self._id))
|
||||
else:
|
||||
log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id))
|
||||
|
||||
def _check_requirements(self):
|
||||
"""
|
||||
Check if IOUYAP is available
|
||||
"""
|
||||
path = self.iouyap_path
|
||||
if not path:
|
||||
raise IOUError("No path to a IOU executable has been set")
|
||||
|
||||
if not os.path.isfile(path):
|
||||
raise IOUError("IOU program '{}' is not accessible".format(path))
|
||||
|
||||
if not os.access(path, os.X_OK):
|
||||
raise IOUError("IOU program '{}' is not executable".format(path))
|
||||
|
||||
def __json__(self):
|
||||
|
||||
return {"name": self.name,
|
||||
"vm_id": self.id,
|
||||
"console": self._console,
|
||||
"project_id": self.project.id,
|
||||
"path": self.path,
|
||||
"ethernet_adapters": len(self._ethernet_adapters),
|
||||
"serial_adapters": len(self._serial_adapters),
|
||||
"ram": self._ram,
|
||||
"nvram": self._nvram
|
||||
}
|
||||
|
||||
@property
|
||||
def iouyap_path(self):
|
||||
"""
|
||||
Returns the IOUYAP executable path.
|
||||
|
||||
:returns: path to IOUYAP
|
||||
"""
|
||||
|
||||
path = self._manager.config.get_section_config("IOU").get("iouyap_path", "iouyap")
|
||||
if path == "iouyap":
|
||||
path = shutil.which("iouyap")
|
||||
return path
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
"""
|
||||
Returns the console port of this IOU vm.
|
||||
|
||||
:returns: console port
|
||||
"""
|
||||
|
||||
return self._console
|
||||
|
||||
@console.setter
|
||||
def console(self, console):
|
||||
"""
|
||||
Change console port
|
||||
|
||||
:params console: Console port (integer)
|
||||
"""
|
||||
|
||||
if console == self._console:
|
||||
return
|
||||
if self._console:
|
||||
self._manager.port_manager.release_console_port(self._console)
|
||||
self._console = self._manager.port_manager.reserve_console_port(console)
|
||||
|
||||
@property
|
||||
def ram(self):
|
||||
"""
|
||||
Returns the amount of RAM allocated to this IOU instance.
|
||||
:returns: amount of RAM in Mbytes (integer)
|
||||
"""
|
||||
|
||||
return self._ram
|
||||
|
||||
@ram.setter
|
||||
def ram(self, ram):
|
||||
"""
|
||||
Sets amount of RAM allocated to this IOU instance.
|
||||
:param ram: amount of RAM in Mbytes (integer)
|
||||
"""
|
||||
|
||||
if self._ram == ram:
|
||||
return
|
||||
|
||||
log.info("IOU {name} [id={id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name,
|
||||
id=self._id,
|
||||
old_ram=self._ram,
|
||||
new_ram=ram))
|
||||
|
||||
self._ram = ram
|
||||
|
||||
@property
|
||||
def nvram(self):
|
||||
"""
|
||||
Returns the mount of NVRAM allocated to this IOU instance.
|
||||
:returns: amount of NVRAM in Kbytes (integer)
|
||||
"""
|
||||
|
||||
return self._nvram
|
||||
|
||||
@nvram.setter
|
||||
def nvram(self, nvram):
|
||||
"""
|
||||
Sets amount of NVRAM allocated to this IOU instance.
|
||||
:param nvram: amount of NVRAM in Kbytes (integer)
|
||||
"""
|
||||
|
||||
if self._nvram == nvram:
|
||||
return
|
||||
|
||||
log.info("IOU {name} [id={id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name,
|
||||
id=self._id,
|
||||
old_nvram=self._nvram,
|
||||
new_nvram=nvram))
|
||||
self._nvram = nvram
|
||||
|
||||
@property
|
||||
def application_id(self):
|
||||
return self._manager.get_application_id(self.id)
|
||||
|
||||
# TODO: ASYNCIO
|
||||
def _library_check(self):
|
||||
"""
|
||||
Checks for missing shared library dependencies in the IOU image.
|
||||
"""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(["ldd", self._path])
|
||||
except (FileNotFoundError, subprocess.SubprocessError) as e:
|
||||
log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e))
|
||||
return
|
||||
|
||||
p = re.compile("([\.\w]+)\s=>\s+not found")
|
||||
missing_libs = p.findall(output.decode("utf-8"))
|
||||
if missing_libs:
|
||||
raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path,
|
||||
", ".join(missing_libs)))
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self):
|
||||
"""
|
||||
Starts the IOU process.
|
||||
"""
|
||||
|
||||
self._check_requirements()
|
||||
if not self.is_running():
|
||||
|
||||
# TODO: ASYNC
|
||||
# self._library_check()
|
||||
|
||||
if self._iourc_path and not os.path.isfile(self._iourc_path):
|
||||
raise IOUError("A valid iourc file is necessary to start IOU")
|
||||
|
||||
iouyap_path = self.iouyap_path
|
||||
if not iouyap_path or not os.path.isfile(iouyap_path):
|
||||
raise IOUError("iouyap is necessary to start IOU")
|
||||
|
||||
self._create_netmap_config()
|
||||
# created a environment variable pointing to the iourc file.
|
||||
env = os.environ.copy()
|
||||
if self._iourc_path:
|
||||
env["IOURC"] = self._iourc_path
|
||||
self._command = self._build_command()
|
||||
try:
|
||||
log.info("Starting IOU: {}".format(self._command))
|
||||
self._iou_stdout_file = os.path.join(self.working_dir, "iou.log")
|
||||
log.info("Logging to {}".format(self._iou_stdout_file))
|
||||
with open(self._iou_stdout_file, "w") as fd:
|
||||
self._iou_process = yield from asyncio.create_subprocess_exec(*self._command,
|
||||
stdout=fd,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=self.working_dir,
|
||||
env=env)
|
||||
log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid))
|
||||
self._started = True
|
||||
except FileNotFoundError as e:
|
||||
raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e))
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
iou_stdout = self.read_iou_stdout()
|
||||
log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
|
||||
raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
|
||||
|
||||
# start console support
|
||||
self._start_ioucon()
|
||||
# connections support
|
||||
self._start_iouyap()
|
||||
|
||||
def _start_iouyap(self):
|
||||
"""
|
||||
Starts iouyap (handles connections to and from this IOU device).
|
||||
"""
|
||||
|
||||
try:
|
||||
self._update_iouyap_config()
|
||||
command = [self.iouyap_path, "-q", str(self.application_id + 512)] # iouyap has always IOU ID + 512
|
||||
log.info("starting iouyap: {}".format(command))
|
||||
self._iouyap_stdout_file = os.path.join(self.working_dir, "iouyap.log")
|
||||
log.info("logging to {}".format(self._iouyap_stdout_file))
|
||||
with open(self._iouyap_stdout_file, "w") as fd:
|
||||
self._iouyap_process = subprocess.Popen(command,
|
||||
stdout=fd,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=self.working_dir)
|
||||
|
||||
log.info("iouyap started PID={}".format(self._iouyap_process.pid))
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
iouyap_stdout = self.read_iouyap_stdout()
|
||||
log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout))
|
||||
raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout))
|
||||
|
||||
def _update_iouyap_config(self):
|
||||
"""
|
||||
Updates the iouyap.ini file.
|
||||
"""
|
||||
|
||||
iouyap_ini = os.path.join(self.working_dir, "iouyap.ini")
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config["default"] = {"netmap": "NETMAP",
|
||||
"base_port": "49000"}
|
||||
|
||||
bay_id = 0
|
||||
for adapter in self._slots:
|
||||
unit_id = 0
|
||||
for unit in adapter.ports.keys():
|
||||
nio = adapter.get_nio(unit)
|
||||
if nio:
|
||||
connection = None
|
||||
if isinstance(nio, NIO_UDP):
|
||||
# UDP tunnel
|
||||
connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport,
|
||||
rhost=nio.rhost,
|
||||
rport=nio.rport)}
|
||||
elif isinstance(nio, NIO_TAP):
|
||||
# TAP interface
|
||||
connection = {"tap_dev": "{tap_device}".format(tap_device=nio.tap_device)}
|
||||
|
||||
elif isinstance(nio, NIO_GenericEthernet):
|
||||
# Ethernet interface
|
||||
connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)}
|
||||
|
||||
if connection:
|
||||
interface = "{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self.application_id + 512), bay=bay_id, unit=unit_id)
|
||||
config[interface] = connection
|
||||
|
||||
if nio.capturing:
|
||||
pcap_data_link_type = nio.pcap_data_link_type.upper()
|
||||
if pcap_data_link_type == "DLT_PPP_SERIAL":
|
||||
pcap_protocol = "ppp"
|
||||
elif pcap_data_link_type == "DLT_C_HDLC":
|
||||
pcap_protocol = "hdlc"
|
||||
elif pcap_data_link_type == "DLT_FRELAY":
|
||||
pcap_protocol = "fr"
|
||||
else:
|
||||
pcap_protocol = "ethernet"
|
||||
capture_info = {"pcap_file": "{pcap_file}".format(pcap_file=nio.pcap_output_file),
|
||||
"pcap_protocol": pcap_protocol,
|
||||
"pcap_overwrite": "y"}
|
||||
config[interface].update(capture_info)
|
||||
|
||||
unit_id += 1
|
||||
bay_id += 1
|
||||
|
||||
try:
|
||||
with open(iouyap_ini, "w") as config_file:
|
||||
config.write(config_file)
|
||||
log.info("IOU {name} [id={id}]: iouyap.ini updated".format(name=self._name,
|
||||
id=self._id))
|
||||
except OSError as e:
|
||||
raise IOUError("Could not create {}: {}".format(iouyap_ini, e))
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the IOU process.
|
||||
"""
|
||||
|
||||
# stop console support
|
||||
if self._ioucon_thread:
|
||||
self._ioucon_thread_stop_event.set()
|
||||
if self._ioucon_thread.is_alive():
|
||||
self._ioucon_thread.join(timeout=3.0) # wait for the thread to free the console port
|
||||
self._ioucon_thread = None
|
||||
|
||||
if self.is_running():
|
||||
self._terminate_process_iou()
|
||||
try:
|
||||
yield from asyncio.wait_for(self._iou_process.wait(), timeout=3)
|
||||
except asyncio.TimeoutError:
|
||||
self._iou_process.kill()
|
||||
if self._iou_process.returncode is None:
|
||||
log.warn("IOU process {} is still running".format(self._iou_process.pid))
|
||||
|
||||
self._iou_process = None
|
||||
|
||||
if self._iouyap_process is not None:
|
||||
self._terminate_process_iouyap()
|
||||
try:
|
||||
yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3)
|
||||
except asyncio.TimeoutError:
|
||||
self._iou_process.kill()
|
||||
if self._iouyap_process.returncode is None:
|
||||
log.warn("IOUYAP process {} is still running".format(self._iou_process.pid))
|
||||
|
||||
self._started = False
|
||||
|
||||
def _terminate_process_iouyap(self):
|
||||
"""Terminate the process if running"""
|
||||
|
||||
if self._iou_process:
|
||||
log.info("Stopping IOUYAP instance {} PID={}".format(self.name, self._iouyap_process.pid))
|
||||
try:
|
||||
self._iouyap_process.terminate()
|
||||
# Sometime the process can already be dead when we garbage collect
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
def _terminate_process_iou(self):
|
||||
"""Terminate the process if running"""
|
||||
|
||||
if self._iou_process:
|
||||
log.info("Stopping IOU instance {} PID={}".format(self.name, self._iou_process.pid))
|
||||
try:
|
||||
self._iou_process.terminate()
|
||||
# Sometime the process can already be dead when we garbage collect
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload(self):
|
||||
"""
|
||||
Reload the IOU process. (Stop / Start)
|
||||
"""
|
||||
|
||||
yield from self.stop()
|
||||
yield from self.start()
|
||||
|
||||
def is_running(self):
|
||||
"""
|
||||
Checks if the IOU process is running
|
||||
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
if self._iou_process:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_iouyap_running(self):
|
||||
"""
|
||||
Checks if the IOUYAP process is running
|
||||
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
if self._iouyap_process:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _create_netmap_config(self):
|
||||
"""
|
||||
Creates the NETMAP file.
|
||||
"""
|
||||
|
||||
netmap_path = os.path.join(self.working_dir, "NETMAP")
|
||||
try:
|
||||
with open(netmap_path, "w") as f:
|
||||
for bay in range(0, 16):
|
||||
for unit in range(0, 4):
|
||||
f.write("{iouyap_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format(iouyap_id=str(self.application_id + 512),
|
||||
bay=bay,
|
||||
unit=unit,
|
||||
iou_id=self.application_id))
|
||||
log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name,
|
||||
id=self._id))
|
||||
except OSError as e:
|
||||
raise IOUError("Could not create {}: {}".format(netmap_path, e))
|
||||
|
||||
def _build_command(self):
|
||||
"""
|
||||
Command to start the IOU process.
|
||||
(to be passed to subprocess.Popen())
|
||||
IOU command line:
|
||||
Usage: <image> [options] <application id>
|
||||
<image>: unix-js-m | unix-is-m | unix-i-m | ...
|
||||
<application id>: instance identifier (0 < id <= 1024)
|
||||
Options:
|
||||
-e <n> Number of Ethernet interfaces (default 2)
|
||||
-s <n> Number of Serial interfaces (default 2)
|
||||
-n <n> Size of nvram in Kb (default 64KB)
|
||||
-b <string> IOS debug string
|
||||
-c <name> Configuration file name
|
||||
-d Generate debug information
|
||||
-t Netio message trace
|
||||
-q Suppress informational messages
|
||||
-h Display this help
|
||||
-C Turn off use of host clock
|
||||
-m <n> Megabytes of router memory (default 256MB)
|
||||
-L Disable local console, use remote console
|
||||
-l Enable Layer 1 keepalive messages
|
||||
-u <n> UDP port base for distributed networks
|
||||
-R Ignore options from the IOURC file
|
||||
-U Disable unix: file system location
|
||||
-W Disable watchdog timer
|
||||
-N Ignore the NETMAP file
|
||||
"""
|
||||
|
||||
command = [self._path]
|
||||
if len(self._ethernet_adapters) != 2:
|
||||
command.extend(["-e", str(len(self._ethernet_adapters))])
|
||||
if len(self._serial_adapters) != 2:
|
||||
command.extend(["-s", str(len(self._serial_adapters))])
|
||||
if not self.use_default_iou_values:
|
||||
command.extend(["-n", str(self._nvram)])
|
||||
command.extend(["-m", str(self._ram)])
|
||||
command.extend(["-L"]) # disable local console, use remote console
|
||||
if self._initial_config:
|
||||
command.extend(["-c", self._initial_config])
|
||||
if self._l1_keepalives:
|
||||
self._enable_l1_keepalives(command)
|
||||
command.extend([str(self.application_id)])
|
||||
return command
|
||||
|
||||
def read_iou_stdout(self):
|
||||
"""
|
||||
Reads the standard output of the IOU process.
|
||||
Only use when the process has been stopped or has crashed.
|
||||
"""
|
||||
|
||||
output = ""
|
||||
if self._iou_stdout_file:
|
||||
try:
|
||||
with open(self._iou_stdout_file, errors="replace") as file:
|
||||
output = file.read()
|
||||
except OSError as e:
|
||||
log.warn("could not read {}: {}".format(self._iou_stdout_file, e))
|
||||
return output
|
||||
|
||||
def read_iouyap_stdout(self):
|
||||
"""
|
||||
Reads the standard output of the iouyap process.
|
||||
Only use when the process has been stopped or has crashed.
|
||||
"""
|
||||
|
||||
output = ""
|
||||
if self._iouyap_stdout_file:
|
||||
try:
|
||||
with open(self._iouyap_stdout_file, errors="replace") as file:
|
||||
output = file.read()
|
||||
except OSError as e:
|
||||
log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e))
|
||||
return output
|
||||
|
||||
def _start_ioucon(self):
|
||||
"""
|
||||
Starts ioucon thread (for console connections).
|
||||
"""
|
||||
|
||||
if not self._ioucon_thread:
|
||||
telnet_server = "{}:{}".format(self._console_host, self.console)
|
||||
log.info("Starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server))
|
||||
args = argparse.Namespace(appl_id=str(self.application_id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server)
|
||||
self._ioucon_thread_stop_event = threading.Event()
|
||||
self._ioucon_thread = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event))
|
||||
self._ioucon_thread.start()
|
||||
|
||||
@property
|
||||
def ethernet_adapters(self):
|
||||
"""
|
||||
Returns the number of Ethernet adapters for this IOU instance.
|
||||
:returns: number of adapters
|
||||
"""
|
||||
|
||||
return len(self._ethernet_adapters)
|
||||
|
||||
@ethernet_adapters.setter
|
||||
def ethernet_adapters(self, ethernet_adapters):
|
||||
"""
|
||||
Sets the number of Ethernet adapters for this IOU instance.
|
||||
:param ethernet_adapters: number of adapters
|
||||
"""
|
||||
|
||||
self._ethernet_adapters.clear()
|
||||
for _ in range(0, ethernet_adapters):
|
||||
self._ethernet_adapters.append(EthernetAdapter())
|
||||
|
||||
log.info("IOU {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
|
||||
id=self._id,
|
||||
adapters=len(self._ethernet_adapters)))
|
||||
|
||||
self._slots = self._ethernet_adapters + self._serial_adapters
|
||||
|
||||
@property
|
||||
def serial_adapters(self):
|
||||
"""
|
||||
Returns the number of Serial adapters for this IOU instance.
|
||||
:returns: number of adapters
|
||||
"""
|
||||
|
||||
return len(self._serial_adapters)
|
||||
|
||||
@serial_adapters.setter
|
||||
def serial_adapters(self, serial_adapters):
|
||||
"""
|
||||
Sets the number of Serial adapters for this IOU instance.
|
||||
:param serial_adapters: number of adapters
|
||||
"""
|
||||
|
||||
self._serial_adapters.clear()
|
||||
for _ in range(0, serial_adapters):
|
||||
self._serial_adapters.append(SerialAdapter())
|
||||
|
||||
log.info("IOU {name} [id={id}]: number of Serial adapters changed to {adapters}".format(name=self._name,
|
||||
id=self._id,
|
||||
adapters=len(self._serial_adapters)))
|
||||
|
||||
self._slots = self._ethernet_adapters + self._serial_adapters
|
||||
|
||||
def slot_add_nio_binding(self, slot_id, port_id, nio):
|
||||
"""
|
||||
Adds a slot NIO binding.
|
||||
:param slot_id: slot ID
|
||||
:param port_id: port ID
|
||||
:param nio: NIO instance to add to the slot/port
|
||||
"""
|
||||
|
||||
try:
|
||||
adapter = self._slots[slot_id]
|
||||
except IndexError:
|
||||
raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name,
|
||||
slot_id=slot_id))
|
||||
|
||||
if not adapter.port_exists(port_id):
|
||||
raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
|
||||
port_id=port_id))
|
||||
|
||||
adapter.add_nio(port_id, nio)
|
||||
log.info("IOU {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name,
|
||||
id=self._id,
|
||||
nio=nio,
|
||||
slot_id=slot_id,
|
||||
port_id=port_id))
|
||||
if self.is_iouyap_running():
|
||||
self._update_iouyap_config()
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
|
||||
def slot_remove_nio_binding(self, slot_id, port_id):
|
||||
"""
|
||||
Removes a slot NIO binding.
|
||||
:param slot_id: slot ID
|
||||
:param port_id: port ID
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
try:
|
||||
adapter = self._slots[slot_id]
|
||||
except IndexError:
|
||||
raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name,
|
||||
slot_id=slot_id))
|
||||
|
||||
if not adapter.port_exists(port_id):
|
||||
raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
|
||||
port_id=port_id))
|
||||
|
||||
nio = adapter.get_nio(port_id)
|
||||
adapter.remove_nio(port_id)
|
||||
log.info("IOU {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name,
|
||||
id=self._id,
|
||||
nio=nio,
|
||||
slot_id=slot_id,
|
||||
port_id=port_id))
|
||||
if self.is_iouyap_running():
|
||||
self._update_iouyap_config()
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
|
||||
return nio
|
||||
@@ -0,0 +1,648 @@
|
||||
#!/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'
|
||||
|
||||
# IOU 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 sub-negotiation 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
|
||||
try:
|
||||
os.unlink(self.fd.name)
|
||||
except FileNotFoundError as e:
|
||||
log.debug("{}".format(e))
|
||||
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
|
||||
except ConnectionResetError:
|
||||
buf = b''
|
||||
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")
|
||||
try:
|
||||
fd.shutdown(socket.SHUT_RDWR)
|
||||
except OSError as e:
|
||||
log.warn("shutdown: {}".format(e))
|
||||
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 IOU(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):
|
||||
try:
|
||||
self.fd.send(buf)
|
||||
except BlockingIOError:
|
||||
return
|
||||
|
||||
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 IOUConError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LockError(IOUConError):
|
||||
pass
|
||||
|
||||
|
||||
class NetioError(IOUConError):
|
||||
pass
|
||||
|
||||
|
||||
class TTYError(IOUConError):
|
||||
pass
|
||||
|
||||
|
||||
class TelnetServerError(IOUConError):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(IOUConError):
|
||||
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 IOU
|
||||
# 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()
|
||||
|
||||
# IOU --> 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 --> IOU
|
||||
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 IOU.
|
||||
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 IOU 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='IOU 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_ioucon(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 IOU(ttyC, ttyS, stop_event) as router:
|
||||
send_recv_loop(console, router, b'', stop_event)
|
||||
else:
|
||||
with IOU(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 IOUConError as e:
|
||||
if args.debug:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
else:
|
||||
log.error("ioucon: {}".format(e))
|
||||
sys.exit(EXIT_FAILURE)
|
||||
|
||||
log.info("exiting...")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
import threading
|
||||
stop_event = threading.Event()
|
||||
args = get_args()
|
||||
start_ioucon(args, stop_event)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only).
|
||||
from .nio import NIO
|
||||
|
||||
|
||||
class NIOTAP(NIO):
|
||||
class NIO_TAP(NIO):
|
||||
|
||||
"""
|
||||
TAP NIO.
|
||||
|
||||
@@ -22,7 +22,7 @@ Interface for UDP NIOs.
|
||||
from .nio import NIO
|
||||
|
||||
|
||||
class NIOUDP(NIO):
|
||||
class NIO_UDP(NIO):
|
||||
|
||||
"""
|
||||
UDP NIO.
|
||||
|
||||
@@ -24,18 +24,4 @@ from ..vm_error import VMError
|
||||
|
||||
class VirtualBoxError(VMError):
|
||||
|
||||
def __init__(self, message, original_exception=None):
|
||||
|
||||
Exception.__init__(self, message)
|
||||
if isinstance(message, Exception):
|
||||
message = str(message)
|
||||
self._message = message
|
||||
self._original_exception = original_exception
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return self._message
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self._message
|
||||
pass
|
||||
@@ -17,4 +17,19 @@
|
||||
|
||||
|
||||
class VMError(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, message, original_exception=None):
|
||||
|
||||
Exception.__init__(self, message)
|
||||
if isinstance(message, Exception):
|
||||
message = str(message)
|
||||
self._message = message
|
||||
self._original_exception = original_exception
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return self._message
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self._message
|
||||
@@ -24,18 +24,4 @@ from ..vm_error import VMError
|
||||
|
||||
class VPCSError(VMError):
|
||||
|
||||
def __init__(self, message, original_exception=None):
|
||||
|
||||
Exception.__init__(self, message)
|
||||
if isinstance(message, Exception):
|
||||
message = str(message)
|
||||
self._message = message
|
||||
self._original_exception = original_exception
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return self._message
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self._message
|
||||
pass
|
||||
Reference in new issue
Block a user