Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 170 additions & 3 deletions gns3server/compute/iou/iou_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,85 @@
log = logging.getLogger(__name__)


class IOUL1KeepaliveProtocol(asyncio.DatagramProtocol):
"""Handle IOU/IOL Layer 1 keepalives for connected interfaces."""

_header = struct.Struct("!HHBBBB")
_message_type = 3

def __init__(self, vm):
self._vm = vm
self.transport = None

def connection_made(self, transport):
self.transport = transport

@staticmethod
def encode_interface(adapter_number, port_number):
"""Encode an IOU bay/unit for the L1 keepalive protocol."""

# IOU stores the zero-based unit in the high nibble and the
# zero-based bay in the low nibble.
return (port_number << 4) | adapter_number

@staticmethod
def decode_interface(interface):
"""Decode an L1 keepalive interface into an IOU bay/unit."""

return interface & 0x0F, interface >> 4

def datagram_received(self, data, address):
if len(data) != self._header.size:
log.debug('IOU "%s": ignored malformed L1 keepalive of %d bytes', self._vm.name, len(data))
return

destination, source, destination_interface, source_interface, message_type, channel = self._header.unpack(data)
if (
destination != self._vm.l1_bridge_id
or source != self._vm.application_id
or message_type != self._message_type
or not self._vm.has_nio_for_iou_interface(source_interface)
):
return

response = self._header.pack(
source,
destination,
source_interface,
destination_interface,
message_type,
channel,
)
try:
self.transport.sendto(response, self._vm.l1_iou_socket_path)
except OSError as e:
# IOU creates its endpoint during startup and removes it on stop.
# Dropping a keepalive during either transition is harmless.
log.debug('IOU "%s": could not send an L1 keepalive response: %s', self._vm.name, e)

def send_keepalives(self):
"""Tell IOU that every interface with an attached NIO has Layer 1 connectivity."""

for adapter_number, adapter in enumerate(self._vm.adapters):
for port_number, nio in adapter.ports.items():
if nio is None:
continue
interface = self.encode_interface(adapter_number, port_number)
keepalive = self._header.pack(
self._vm.application_id,
self._vm.l1_bridge_id,
interface,
interface,
self._message_type,
0,
)
try:
self.transport.sendto(keepalive, self._vm.l1_iou_socket_path)
except OSError as e:
# The IOU endpoint does not exist until the image has started.
log.debug('IOU "%s": could not send an L1 keepalive: %s', self._vm.name, e)


class IOUVM(BaseNode):
module_name = "iou"

Expand Down Expand Up @@ -98,6 +177,8 @@ def __init__(
self._lib_base = self.manager.get_images_directory()
self._loader = None
self._license_check = True
self._l1_keepalive_transport = None
self._l1_keepalive_task = None

# IOU settings
self._ethernet_adapters = []
Expand All @@ -110,7 +191,7 @@ def __init__(
self._private_config = ""
self._ram = 1024 # Megabytes
self._application_id = application_id
self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes).
self._l1_keepalives = False

def _nvram_changed(self, path):
"""
Expand Down Expand Up @@ -637,6 +718,10 @@ async def start(self):
raise IOUError(f"Could not create symbolic link: {e}")

command = await self._build_command()
# Only start the responder when the capability probe actually
# enabled IOU's L1 protocol on the command line.
if "-l" in command:
await self._start_l1_keepalive_responder()
try:
if self._loader:
log.info(f"Starting IOU: {command} with loader {self._loader}")
Expand All @@ -657,8 +742,10 @@ async def start(self):
callback = functools.partial(self._termination_callback, "IOU")
gns3server.utils.asyncio.monitor_process(self._iou_process, callback)
except FileNotFoundError as e:
self._stop_l1_keepalive_responder()
raise IOUError(f"Could not start IOU: {e}: 32-bit binary support is probably not installed, it is recommended to use a 64-bit image instead")
except (OSError, subprocess.SubprocessError) as e:
self._stop_l1_keepalive_responder()
iou_stdout = self.read_iou_stdout()
log.error(f"Could not start IOU {self._path}: {e}\n{iou_stdout}")
raise IOUError(f"Could not start IOU {self._path}: {e}\n{iou_stdout}")
Expand Down Expand Up @@ -760,6 +847,7 @@ def _termination_callback(self, process_name, returncode):
"""

self._terminate_process_iou()
self._stop_l1_keepalive_responder()
if returncode != 0:
if returncode == -11:
message = 'IOU VM "{}" process has stopped with return code: {} (segfault). This could be an issue with the IOU image, using a different image may fix this.\n{}'.format(
Expand Down Expand Up @@ -792,6 +880,7 @@ async def stop(self):
Stops the IOU process.
"""

self._stop_l1_keepalive_responder()
await self._stop_ubridge()
if self._nvram_watcher:
self._nvram_watcher.close()
Expand Down Expand Up @@ -894,6 +983,83 @@ def _create_netmap_config(self):
except OSError as e:
raise IOUError(f"Could not create {netmap_path}: {e}")

@property
def l1_bridge_id(self):
return self.application_id + 512

@property
def l1_socket_directory(self):
# IOU hard-codes this directory independently from TMPDIR.
return os.path.join("/tmp", f"netl1{os.geteuid()}")

@property
def l1_bridge_socket_path(self):
return os.path.join(self.l1_socket_directory, f"L1{self.l1_bridge_id}")

@property
def l1_iou_socket_path(self):
return os.path.join(self.l1_socket_directory, f"L1{self.application_id}")

def has_nio_for_iou_interface(self, interface):
"""Return whether the IOU bay/unit encoded in one byte is connected."""

adapter_number, port_number = IOUL1KeepaliveProtocol.decode_interface(interface)
if adapter_number >= len(self._adapters):
return False
adapter = self._adapters[adapter_number]
return adapter.port_exists(port_number) and adapter.get_nio(port_number) is not None

async def _start_l1_keepalive_responder(self):
"""Create the bridge-side UNIX datagram endpoint used by IOU's ``-l`` option."""

if self._l1_keepalive_transport is not None:
return

socket_directory = self.l1_socket_directory
try:
os.makedirs(socket_directory, mode=0o755, exist_ok=True)
if os.path.islink(socket_directory) or os.stat(socket_directory).st_uid != os.geteuid():
raise IOUError(f"Unsafe IOU L1 keepalive directory '{socket_directory}'")
if os.path.lexists(self.l1_bridge_socket_path):
os.unlink(self.l1_bridge_socket_path)
loop = asyncio.get_running_loop()
transport, protocol = await loop.create_datagram_endpoint(
lambda: IOUL1KeepaliveProtocol(self),
local_addr=self.l1_bridge_socket_path,
family=socket.AF_UNIX,
)
self._l1_keepalive_transport = transport
self._l1_keepalive_task = asyncio.create_task(self._send_l1_keepalives(protocol))
log.info(
'IOU "%s" [%s]: L1 keepalive responder listening on %s',
self._name,
self._id,
self.l1_bridge_socket_path,
)
except (OSError, RuntimeError) as e:
self._stop_l1_keepalive_responder()
raise IOUError(f"Could not start IOU L1 keepalive responder: {e}")

async def _send_l1_keepalives(self, protocol):
while self._l1_keepalive_transport is not None:
protocol.send_keepalives()
await asyncio.sleep(1)

def _stop_l1_keepalive_responder(self):
"""Stop the L1 endpoint and remove its bridge-side socket."""

if self._l1_keepalive_task is not None:
self._l1_keepalive_task.cancel()
self._l1_keepalive_task = None
if self._l1_keepalive_transport is not None:
self._l1_keepalive_transport.close()
self._l1_keepalive_transport = None
try:
if os.path.lexists(self.l1_bridge_socket_path):
os.unlink(self.l1_bridge_socket_path)
except OSError as e:
log.warning('Could not remove IOU L1 keepalive socket "%s": %s', self.l1_bridge_socket_path, e)

async def _build_command(self):
"""
Command to start the IOU process.
Expand Down Expand Up @@ -1268,8 +1434,9 @@ async def _enable_l1_keepalives(self, command):
"""

env = os.environ.copy()
if "IOURC" not in os.environ:
env["IOURC"] = self.iourc_path
iourc_path = self.iourc_path
if "IOURC" not in os.environ and iourc_path:
env["IOURC"] = iourc_path
try:
output = await gns3server.utils.asyncio.subprocess_check_output(
*self._loader, self._path, "-h", cwd=self.working_dir, env=env, stderr=True
Expand Down
5 changes: 4 additions & 1 deletion gns3server/schemas/compute/iou_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ class IOUBase(BaseModel):
ethernet_adapters: Optional[int] = Field(None, description="How many Ethernet adapters are connected to IOU")
ram: Optional[int] = Field(None, gt=0, description="Amount of RAM in MB")
nvram: Optional[int] = Field(None, gt=0, description="Amount of NVRAM in KB")
l1_keepalives: Optional[bool] = Field(None, description="Use default IOU values")
l1_keepalives: Optional[bool] = Field(
None,
description="Enable Layer 1 keepalives so IOU interfaces report accurate link state",
)
use_default_iou_values: Optional[bool] = Field(None, description="Use default IOU values")
startup_config_content: Optional[str] = Field(None, description="Content of IOU startup configuration file")
private_config_content: Optional[str] = Field(None, description="Content of IOU private configuration file")
Expand Down
5 changes: 4 additions & 1 deletion gns3server/schemas/controller/templates/iou_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ class IOUTemplate(TemplateBase):
use_default_iou_values: Optional[bool] = Field(False, description="Use default IOU values")
startup_config: Optional[str] = Field("iou_l3_base_startup-config.txt", description="Startup-config of IOU")
private_config: Optional[str] = Field("", description="Private-config of IOU")
l1_keepalives: Optional[bool] = Field(False, description="Always keep up Ethernet interface (does not always work)")
l1_keepalives: Optional[bool] = Field(
False,
description="Enable Layer 1 keepalives so IOU interfaces report accurate link state",
)
console_type: Optional[ConsoleType] = Field(ConsoleType.telnet, description="Console type")
console_auto_start: Optional[bool] = Field(
False, description="Automatically start the console when the node has started"
Expand Down
Loading