daemon: initial pass to revamp how node linking and link management is done, provides a consistent way to link all wired nodes and allows them to be configured for tc for the same behavior across the board
This commit is contained in:
parent
d684b8eb5a
commit
cd7f1a641e
19 changed files with 1393 additions and 1556 deletions
|
@ -4,7 +4,6 @@ virtual ethernet classes that implement the interfaces available under Linux.
|
|||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
||||
|
||||
|
@ -20,11 +19,12 @@ from core.nodes.netclient import LinuxNetClient, get_net_client
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.session import Session
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase
|
||||
|
||||
DEFAULT_MTU: int = 1500
|
||||
IFACE_NAME_LENGTH: int = 15
|
||||
|
||||
|
||||
def tc_clear_cmd(name: str) -> str:
|
||||
|
@ -78,35 +78,42 @@ class CoreInterface:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
session: "Session",
|
||||
_id: int,
|
||||
name: str,
|
||||
localname: str,
|
||||
use_ovs: bool,
|
||||
mtu: int = DEFAULT_MTU,
|
||||
node: "NodeBase" = None,
|
||||
server: "DistributedServer" = None,
|
||||
node: "CoreNode" = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates a CoreInterface instance.
|
||||
|
||||
:param session: core session instance
|
||||
:param _id: interface id for associated node
|
||||
:param name: interface name
|
||||
:param localname: interface local name
|
||||
:param use_ovs: True to use ovs, False otherwise
|
||||
:param mtu: mtu value
|
||||
:param node: node associated with this interface
|
||||
:param server: remote server node will run on, default is None for localhost
|
||||
:param node: node for interface
|
||||
"""
|
||||
if len(name) >= 16:
|
||||
raise CoreError(f"interface name ({name}) too long, max 16")
|
||||
if len(localname) >= 16:
|
||||
raise CoreError(f"interface local name ({localname}) too long, max 16")
|
||||
self.session: "Session" = session
|
||||
self.node: Optional["CoreNode"] = node
|
||||
if len(name) >= IFACE_NAME_LENGTH:
|
||||
raise CoreError(
|
||||
f"interface name ({name}) too long, max {IFACE_NAME_LENGTH}"
|
||||
)
|
||||
if len(localname) >= IFACE_NAME_LENGTH:
|
||||
raise CoreError(
|
||||
f"interface local name ({localname}) too long, max {IFACE_NAME_LENGTH}"
|
||||
)
|
||||
self.id: int = _id
|
||||
self.node: Optional["NodeBase"] = node
|
||||
# id of interface for network, used by wlan/emane
|
||||
self.net_id: Optional[int] = None
|
||||
self.name: str = name
|
||||
self.localname: str = localname
|
||||
self.up: bool = False
|
||||
self.mtu: int = mtu
|
||||
self.net: Optional[CoreNetworkBase] = None
|
||||
self.othernet: Optional[CoreNetworkBase] = None
|
||||
self.ip4s: List[netaddr.IPNetwork] = []
|
||||
self.ip6s: List[netaddr.IPNetwork] = []
|
||||
self.mac: Optional[netaddr.EUI] = None
|
||||
|
@ -114,20 +121,12 @@ class CoreInterface:
|
|||
self.poshook: Callable[[CoreInterface], None] = lambda x: None
|
||||
# used with EMANE
|
||||
self.transport_type: TransportType = TransportType.VIRTUAL
|
||||
# id of interface for node
|
||||
self.node_id: Optional[int] = None
|
||||
# id of interface for network
|
||||
self.net_id: Optional[int] = None
|
||||
# id used to find flow data
|
||||
self.flow_id: Optional[int] = None
|
||||
self.server: Optional["DistributedServer"] = server
|
||||
self.net_client: LinuxNetClient = get_net_client(
|
||||
self.session.use_ovs(), self.host_cmd
|
||||
)
|
||||
self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd)
|
||||
self.control: bool = False
|
||||
# configuration data
|
||||
self.has_local_netem: bool = False
|
||||
self.local_options: LinkOptions = LinkOptions()
|
||||
self.has_netem: bool = False
|
||||
self.options: LinkOptions = LinkOptions()
|
||||
|
||||
|
@ -161,7 +160,13 @@ class CoreInterface:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
pass
|
||||
self.net_client.create_veth(self.localname, self.name)
|
||||
if self.mtu > 0:
|
||||
self.net_client.set_mtu(self.name, self.mtu)
|
||||
self.net_client.set_mtu(self.localname, self.mtu)
|
||||
self.net_client.device_up(self.name)
|
||||
self.net_client.device_up(self.localname)
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
|
@ -169,29 +174,14 @@ class CoreInterface:
|
|||
|
||||
:return: nothing
|
||||
"""
|
||||
pass
|
||||
|
||||
def attachnet(self, net: "CoreNetworkBase") -> None:
|
||||
"""
|
||||
Attach network.
|
||||
|
||||
:param net: network to attach
|
||||
:return: nothing
|
||||
"""
|
||||
if self.net:
|
||||
self.detachnet()
|
||||
self.net = None
|
||||
net.attach(self)
|
||||
self.net = net
|
||||
|
||||
def detachnet(self) -> None:
|
||||
"""
|
||||
Detach from a network.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if self.net is not None:
|
||||
self.net.detach(self)
|
||||
if not self.up:
|
||||
return
|
||||
if self.localname:
|
||||
try:
|
||||
self.net_client.delete_device(self.localname)
|
||||
except CoreCommandError:
|
||||
pass
|
||||
self.up = False
|
||||
|
||||
def add_ip(self, ip: str) -> None:
|
||||
"""
|
||||
|
@ -303,41 +293,24 @@ class CoreInterface:
|
|||
"""
|
||||
return self.transport_type == TransportType.VIRTUAL
|
||||
|
||||
def config(self, options: LinkOptions, use_local: bool = True) -> None:
|
||||
"""
|
||||
Configure interface using tc based on existing state and provided
|
||||
link options.
|
||||
|
||||
:param options: options to configure with
|
||||
:param use_local: True to use localname for device, False for name
|
||||
:return: nothing
|
||||
"""
|
||||
# determine name, options, and if anything has changed
|
||||
name = self.localname if use_local else self.name
|
||||
current_options = self.local_options if use_local else self.options
|
||||
changed = current_options.update(options)
|
||||
# nothing more to do when nothing has changed or not up
|
||||
if not changed or not self.up:
|
||||
return
|
||||
def set_config(self, node: "CoreNode" = None) -> None:
|
||||
# clear current settings
|
||||
if current_options.is_clear():
|
||||
clear_local_netem = use_local and self.has_local_netem
|
||||
clear_netem = not use_local and self.has_netem
|
||||
if clear_local_netem or clear_netem:
|
||||
cmd = tc_clear_cmd(name)
|
||||
self.host_cmd(cmd)
|
||||
if use_local:
|
||||
self.has_local_netem = False
|
||||
if self.options.is_clear():
|
||||
if self.has_netem:
|
||||
cmd = tc_clear_cmd(self.name)
|
||||
if node:
|
||||
node.cmd(cmd)
|
||||
else:
|
||||
self.has_netem = False
|
||||
self.host_cmd(cmd)
|
||||
self.has_netem = False
|
||||
# set updated settings
|
||||
else:
|
||||
cmd = tc_cmd(name, current_options, self.mtu)
|
||||
self.host_cmd(cmd)
|
||||
if use_local:
|
||||
self.has_local_netem = True
|
||||
cmd = tc_cmd(self.name, self.options, self.mtu)
|
||||
if node:
|
||||
node.cmd(cmd)
|
||||
else:
|
||||
self.has_netem = True
|
||||
self.host_cmd(cmd)
|
||||
self.has_netem = True
|
||||
|
||||
def get_data(self) -> InterfaceData:
|
||||
"""
|
||||
|
@ -345,231 +318,22 @@ class CoreInterface:
|
|||
|
||||
:return: interface data
|
||||
"""
|
||||
if self.node:
|
||||
iface_id = self.node.get_iface_id(self)
|
||||
else:
|
||||
iface_id = self.othernet.get_iface_id(self)
|
||||
data = InterfaceData(
|
||||
id=iface_id, name=self.name, mac=str(self.mac) if self.mac else None
|
||||
)
|
||||
ip4 = self.get_ip4()
|
||||
if ip4:
|
||||
data.ip4 = str(ip4.ip)
|
||||
data.ip4_mask = ip4.prefixlen
|
||||
ip4_addr = str(ip4.ip) if ip4 else None
|
||||
ip4_mask = ip4.prefixlen if ip4 else None
|
||||
ip6 = self.get_ip6()
|
||||
if ip6:
|
||||
data.ip6 = str(ip6.ip)
|
||||
data.ip6_mask = ip6.prefixlen
|
||||
return data
|
||||
|
||||
|
||||
class Veth(CoreInterface):
|
||||
"""
|
||||
Provides virtual ethernet functionality for core nodes.
|
||||
"""
|
||||
|
||||
def adopt_node(self, iface_id: int, name: str, start: bool) -> None:
|
||||
"""
|
||||
Adopt this interface to the provided node, configuring and associating
|
||||
with the node as needed.
|
||||
|
||||
:param iface_id: interface id for node
|
||||
:param name: name of interface fo rnode
|
||||
:param start: True to start interface, False otherwise
|
||||
:return: nothing
|
||||
"""
|
||||
if start:
|
||||
self.startup()
|
||||
self.net_client.device_ns(self.name, str(self.node.pid))
|
||||
self.node.node_net_client.checksums_off(self.name)
|
||||
self.flow_id = self.node.node_net_client.get_ifindex(self.name)
|
||||
logger.debug("interface flow index: %s - %s", self.name, self.flow_id)
|
||||
mac = self.node.node_net_client.get_mac(self.name)
|
||||
logger.debug("interface mac: %s - %s", self.name, mac)
|
||||
self.set_mac(mac)
|
||||
self.node.node_net_client.device_name(self.name, name)
|
||||
self.name = name
|
||||
try:
|
||||
self.node.add_iface(self, iface_id)
|
||||
except CoreError as e:
|
||||
self.shutdown()
|
||||
raise e
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
Interface startup logic.
|
||||
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when there is a command exception
|
||||
"""
|
||||
self.net_client.create_veth(self.localname, self.name)
|
||||
if self.mtu > 0:
|
||||
self.net_client.set_mtu(self.name, self.mtu)
|
||||
self.net_client.set_mtu(self.localname, self.mtu)
|
||||
self.net_client.device_up(self.localname)
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Interface shutdown logic.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.up:
|
||||
return
|
||||
if self.node:
|
||||
try:
|
||||
self.node.node_net_client.device_flush(self.name)
|
||||
except CoreCommandError:
|
||||
pass
|
||||
if self.localname:
|
||||
try:
|
||||
self.net_client.delete_device(self.localname)
|
||||
except CoreCommandError:
|
||||
pass
|
||||
self.up = False
|
||||
|
||||
|
||||
class TunTap(CoreInterface):
|
||||
"""
|
||||
TUN/TAP virtual device in TAP mode
|
||||
"""
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
Startup logic for a tunnel tap.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
# TODO: more sophisticated TAP creation here
|
||||
# Debian does not support -p (tap) option, RedHat does.
|
||||
# For now, this is disabled to allow the TAP to be created by another
|
||||
# system (e.g. EMANE"s emanetransportd)
|
||||
# check_call(["tunctl", "-t", self.name])
|
||||
# self.install()
|
||||
self.up = True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Shutdown functionality for a tunnel tap.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.up:
|
||||
return
|
||||
try:
|
||||
self.node.node_net_client.device_flush(self.name)
|
||||
except CoreCommandError:
|
||||
logger.exception("error shutting down tunnel tap")
|
||||
self.up = False
|
||||
|
||||
def waitfor(
|
||||
self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25
|
||||
) -> bool:
|
||||
"""
|
||||
Wait for func() to return zero with exponential backoff.
|
||||
|
||||
:param func: function to wait for a result of zero
|
||||
:param attempts: number of attempts to wait for a zero result
|
||||
:param maxretrydelay: maximum retry delay
|
||||
:return: True if wait succeeded, False otherwise
|
||||
"""
|
||||
delay = 0.01
|
||||
result = False
|
||||
for i in range(1, attempts + 1):
|
||||
r = func()
|
||||
if r == 0:
|
||||
result = True
|
||||
break
|
||||
msg = f"attempt {i} failed with nonzero exit status {r}"
|
||||
if i < attempts + 1:
|
||||
msg += ", retrying..."
|
||||
logger.info(msg)
|
||||
time.sleep(delay)
|
||||
delay += delay
|
||||
if delay > maxretrydelay:
|
||||
delay = maxretrydelay
|
||||
else:
|
||||
msg += ", giving up"
|
||||
logger.info(msg)
|
||||
|
||||
return result
|
||||
|
||||
def waitfordevicelocal(self) -> None:
|
||||
"""
|
||||
Check for presence of a local device - tap device may not
|
||||
appear right away waits
|
||||
|
||||
:return: wait for device local response
|
||||
"""
|
||||
logger.debug("waiting for device local: %s", self.localname)
|
||||
|
||||
def localdevexists():
|
||||
try:
|
||||
self.net_client.device_show(self.localname)
|
||||
return 0
|
||||
except CoreCommandError:
|
||||
return 1
|
||||
|
||||
self.waitfor(localdevexists)
|
||||
|
||||
def waitfordevicenode(self) -> None:
|
||||
"""
|
||||
Check for presence of a node device - tap device may not appear right away waits.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
logger.debug("waiting for device node: %s", self.name)
|
||||
|
||||
def nodedevexists():
|
||||
try:
|
||||
self.node.node_net_client.device_show(self.name)
|
||||
return 0
|
||||
except CoreCommandError:
|
||||
return 1
|
||||
|
||||
count = 0
|
||||
while True:
|
||||
result = self.waitfor(nodedevexists)
|
||||
if result:
|
||||
break
|
||||
|
||||
# TODO: emane specific code
|
||||
# check if this is an EMANE interface; if so, continue
|
||||
# waiting if EMANE is still running
|
||||
should_retry = count < 5
|
||||
is_emane = self.session.emane.is_emane_net(self.net)
|
||||
is_emane_running = self.session.emane.emanerunning(self.node)
|
||||
if all([should_retry, is_emane, is_emane_running]):
|
||||
count += 1
|
||||
else:
|
||||
raise RuntimeError("node device failed to exist")
|
||||
|
||||
def install(self) -> None:
|
||||
"""
|
||||
Install this TAP into its namespace. This is not done from the
|
||||
startup() method but called at a later time when a userspace
|
||||
program (running on the host) has had a chance to open the socket
|
||||
end of the TAP.
|
||||
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when there is a command exception
|
||||
"""
|
||||
self.waitfordevicelocal()
|
||||
netns = str(self.node.pid)
|
||||
self.net_client.device_ns(self.localname, netns)
|
||||
self.node.node_net_client.device_name(self.localname, self.name)
|
||||
self.node.node_net_client.device_up(self.name)
|
||||
|
||||
def set_ips(self) -> None:
|
||||
"""
|
||||
Set interface ip addresses.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.waitfordevicenode()
|
||||
for ip in self.ips():
|
||||
self.node.node_net_client.create_address(self.name, str(ip))
|
||||
ip6_addr = str(ip6.ip) if ip6 else None
|
||||
ip6_mask = ip6.prefixlen if ip6 else None
|
||||
mac = str(self.mac) if self.mac else None
|
||||
return InterfaceData(
|
||||
id=self.id,
|
||||
name=self.name,
|
||||
mac=mac,
|
||||
ip4=ip4_addr,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6_addr,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
|
||||
|
||||
class GreTap(CoreInterface):
|
||||
|
@ -594,7 +358,7 @@ class GreTap(CoreInterface):
|
|||
"""
|
||||
Creates a GreTap instance.
|
||||
|
||||
:param session: core session instance
|
||||
:param session: session for this gre tap
|
||||
:param remoteip: remote address
|
||||
:param key: gre tap key
|
||||
:param node: related core node
|
||||
|
@ -612,7 +376,7 @@ class GreTap(CoreInterface):
|
|||
sessionid = session.short_session_id()
|
||||
localname = f"gt.{self.id}.{sessionid}"
|
||||
name = f"{localname}p"
|
||||
super().__init__(session, name, localname, mtu, server, node)
|
||||
super().__init__(0, name, localname, session.use_ovs(), mtu, node, server)
|
||||
self.transport_type: TransportType = TransportType.RAW
|
||||
self.remote_ip: str = remoteip
|
||||
self.ttl: int = ttl
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue