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
|
@ -13,11 +13,11 @@ import netaddr
|
|||
|
||||
from core import utils
|
||||
from core.configservice.dependencies import ConfigServiceDependencies
|
||||
from core.emulator.data import InterfaceData, LinkData
|
||||
from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
|
||||
from core.emulator.data import InterfaceData, LinkOptions
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import BASH, MOUNT, TEST, VCMD, VNODED
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface, TunTap, Veth
|
||||
from core.nodes.interface import DEFAULT_MTU, CoreInterface
|
||||
from core.nodes.netclient import LinuxNetClient, get_net_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -74,6 +74,7 @@ class NodeBase(abc.ABC):
|
|||
self.icon: Optional[str] = None
|
||||
self.position: Position = Position()
|
||||
self.up: bool = False
|
||||
self.lock: RLock = RLock()
|
||||
self.net_client: LinuxNetClient = get_net_client(
|
||||
self.session.use_ovs(), self.host_cmd
|
||||
)
|
||||
|
@ -96,6 +97,18 @@ class NodeBase(abc.ABC):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||
"""
|
||||
Adopt an interface, placing within network namespacing for containers
|
||||
and setting to bridge masters for network like nodes.
|
||||
|
||||
:param iface: interface to adopt
|
||||
:param name: proper name to use for interface
|
||||
:return: nothing
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def host_cmd(
|
||||
self,
|
||||
args: str,
|
||||
|
@ -139,6 +152,70 @@ class NodeBase(abc.ABC):
|
|||
"""
|
||||
return self.position.get()
|
||||
|
||||
def create_iface(
|
||||
self, iface_data: InterfaceData = None, options: LinkOptions = None
|
||||
) -> CoreInterface:
|
||||
"""
|
||||
Creates an interface and adopts it to a node.
|
||||
|
||||
:param iface_data: data to create interface with
|
||||
:param options: options to create interface with
|
||||
:return: created interface
|
||||
"""
|
||||
with self.lock:
|
||||
if iface_data and iface_data.id is not None:
|
||||
if iface_data.id in self.ifaces:
|
||||
raise CoreError(
|
||||
f"node({self.id}) interface({iface_data.id}) already exists"
|
||||
)
|
||||
iface_id = iface_data.id
|
||||
else:
|
||||
iface_id = self.next_iface_id()
|
||||
mtu = DEFAULT_MTU
|
||||
if iface_data and iface_data.mtu is not None:
|
||||
mtu = iface_data.mtu
|
||||
name = f"veth{self.id}.{iface_id}.{self.session.short_session_id()}"
|
||||
localname = f"{name}p"
|
||||
iface = CoreInterface(
|
||||
iface_id,
|
||||
name,
|
||||
localname,
|
||||
self.session.use_ovs(),
|
||||
mtu,
|
||||
node=self,
|
||||
server=self.server,
|
||||
)
|
||||
if iface_data:
|
||||
if iface_data.mac:
|
||||
iface.set_mac(iface_data.mac)
|
||||
for ip in iface_data.get_ips():
|
||||
iface.add_ip(ip)
|
||||
if options:
|
||||
iface.options.update(options)
|
||||
self.ifaces[iface_id] = iface
|
||||
if self.up:
|
||||
iface.startup()
|
||||
if iface_data and iface_data.name is not None:
|
||||
name = iface_data.name
|
||||
else:
|
||||
name = iface.name
|
||||
self.adopt_iface(iface, name)
|
||||
return iface
|
||||
|
||||
def delete_iface(self, iface_id: int) -> CoreInterface:
|
||||
"""
|
||||
Delete an interface.
|
||||
|
||||
:param iface_id: interface id to delete
|
||||
:return: the removed interface
|
||||
"""
|
||||
if iface_id not in self.ifaces:
|
||||
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
|
||||
iface = self.ifaces.pop(iface_id)
|
||||
logger.info("node(%s) removing interface(%s)", self.name, iface.name)
|
||||
iface.shutdown()
|
||||
return iface
|
||||
|
||||
def get_iface(self, iface_id: int) -> CoreInterface:
|
||||
"""
|
||||
Retrieve interface based on id.
|
||||
|
@ -191,15 +268,6 @@ class NodeBase(abc.ABC):
|
|||
self.iface_id += 1
|
||||
return iface_id
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||
"""
|
||||
Build link data for this node.
|
||||
|
||||
:param flags: message flags
|
||||
:return: list of link data
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class CoreNodeBase(NodeBase):
|
||||
"""
|
||||
|
@ -227,14 +295,6 @@ class CoreNodeBase(NodeBase):
|
|||
self.directory: Optional[Path] = None
|
||||
self.tmpnodedir: bool = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def shutdown(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_dir(self, dir_path: Path) -> None:
|
||||
"""
|
||||
|
@ -293,19 +353,6 @@ class CoreNodeBase(NodeBase):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def new_iface(
|
||||
self, net: "CoreNetworkBase", iface_data: InterfaceData
|
||||
) -> CoreInterface:
|
||||
"""
|
||||
Create a new interface.
|
||||
|
||||
:param net: network to associate with
|
||||
:param iface_data: interface data for new interface
|
||||
:return: interface index
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def path_exists(self, path: str) -> bool:
|
||||
"""
|
||||
|
@ -318,7 +365,7 @@ class CoreNodeBase(NodeBase):
|
|||
|
||||
def host_path(self, path: Path, is_dir: bool = False) -> Path:
|
||||
"""
|
||||
Return the name of a node"s file on the host filesystem.
|
||||
Return the name of a node's file on the host filesystem.
|
||||
|
||||
:param path: path to translate to host path
|
||||
:param is_dir: True if path is a directory path, False otherwise
|
||||
|
@ -393,54 +440,6 @@ class CoreNodeBase(NodeBase):
|
|||
if self.tmpnodedir:
|
||||
self.host_cmd(f"rm -rf {self.directory}")
|
||||
|
||||
def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
|
||||
"""
|
||||
Add network interface to node and set the network interface index if successful.
|
||||
|
||||
:param iface: network interface to add
|
||||
:param iface_id: interface id
|
||||
:return: nothing
|
||||
"""
|
||||
if iface_id in self.ifaces:
|
||||
raise CoreError(f"interface({iface_id}) already exists")
|
||||
self.ifaces[iface_id] = iface
|
||||
iface.node_id = iface_id
|
||||
|
||||
def delete_iface(self, iface_id: int) -> None:
|
||||
"""
|
||||
Delete a network interface
|
||||
|
||||
:param iface_id: interface index to delete
|
||||
:return: nothing
|
||||
"""
|
||||
if iface_id not in self.ifaces:
|
||||
raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
|
||||
iface = self.ifaces.pop(iface_id)
|
||||
logger.info("node(%s) removing interface(%s)", self.name, iface.name)
|
||||
iface.detachnet()
|
||||
iface.shutdown()
|
||||
|
||||
def attachnet(self, iface_id: int, net: "CoreNetworkBase") -> None:
|
||||
"""
|
||||
Attach a network.
|
||||
|
||||
:param iface_id: interface of index to attach
|
||||
:param net: network to attach
|
||||
:return: nothing
|
||||
"""
|
||||
iface = self.get_iface(iface_id)
|
||||
iface.attachnet(net)
|
||||
|
||||
def detachnet(self, iface_id: int) -> None:
|
||||
"""
|
||||
Detach network interface.
|
||||
|
||||
:param iface_id: interface id to detach
|
||||
:return: nothing
|
||||
"""
|
||||
iface = self.get_iface(iface_id)
|
||||
iface.detachnet()
|
||||
|
||||
def setposition(self, x: float = None, y: float = None, z: float = None) -> None:
|
||||
"""
|
||||
Set position.
|
||||
|
@ -455,25 +454,6 @@ class CoreNodeBase(NodeBase):
|
|||
for iface in self.get_ifaces():
|
||||
iface.setposition()
|
||||
|
||||
def commonnets(
|
||||
self, node: "CoreNodeBase", want_ctrl: bool = False
|
||||
) -> List[Tuple["CoreNetworkBase", CoreInterface, CoreInterface]]:
|
||||
"""
|
||||
Given another node or net object, return common networks between
|
||||
this node and that object. A list of tuples is returned, with each tuple
|
||||
consisting of (network, interface1, interface2).
|
||||
|
||||
:param node: node to get common network with
|
||||
:param want_ctrl: flag set to determine if control network are wanted
|
||||
:return: tuples of common networks
|
||||
"""
|
||||
common = []
|
||||
for iface1 in self.get_ifaces(control=want_ctrl):
|
||||
for iface2 in node.get_ifaces():
|
||||
if iface1.net == iface2.net:
|
||||
common.append((iface1.net, iface1, iface2))
|
||||
return common
|
||||
|
||||
|
||||
class CoreNode(CoreNodeBase):
|
||||
"""
|
||||
|
@ -504,7 +484,6 @@ class CoreNode(CoreNodeBase):
|
|||
self.directory: Optional[Path] = directory
|
||||
self.ctrlchnlname: Path = self.session.directory / self.name
|
||||
self.pid: Optional[int] = None
|
||||
self.lock: RLock = RLock()
|
||||
self._mounts: List[Tuple[Path, Path]] = []
|
||||
self.node_net_client: LinuxNetClient = self.create_node_net_client(
|
||||
self.session.use_ovs()
|
||||
|
@ -585,6 +564,10 @@ class CoreNode(CoreNodeBase):
|
|||
self._mounts = []
|
||||
# shutdown all interfaces
|
||||
for iface in self.get_ifaces():
|
||||
try:
|
||||
self.node_net_client.device_flush(iface.name)
|
||||
except CoreCommandError:
|
||||
pass
|
||||
iface.shutdown()
|
||||
# kill node process if present
|
||||
try:
|
||||
|
@ -691,150 +674,6 @@ class CoreNode(CoreNodeBase):
|
|||
self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
|
||||
self._mounts.append((src_path, target_path))
|
||||
|
||||
def next_iface_id(self) -> int:
|
||||
"""
|
||||
Retrieve a new interface index.
|
||||
|
||||
:return: new interface index
|
||||
"""
|
||||
with self.lock:
|
||||
return super().next_iface_id()
|
||||
|
||||
def newveth(self, iface_id: int = None, ifname: str = None, mtu: int = None) -> int:
|
||||
"""
|
||||
Create a new interface.
|
||||
|
||||
:param iface_id: id for the new interface
|
||||
:param ifname: name for the new interface
|
||||
:param mtu: mtu for interface
|
||||
:return: nothing
|
||||
"""
|
||||
with self.lock:
|
||||
mtu = mtu if mtu is not None else DEFAULT_MTU
|
||||
iface_id = iface_id if iface_id is not None else self.next_iface_id()
|
||||
ifname = ifname if ifname is not None else f"eth{iface_id}"
|
||||
sessionid = self.session.short_session_id()
|
||||
try:
|
||||
suffix = f"{self.id:x}.{iface_id}.{sessionid}"
|
||||
except TypeError:
|
||||
suffix = f"{self.id}.{iface_id}.{sessionid}"
|
||||
localname = f"veth{suffix}"
|
||||
name = f"{localname}p"
|
||||
veth = Veth(self.session, name, localname, mtu, self.server, self)
|
||||
veth.adopt_node(iface_id, ifname, self.up)
|
||||
return iface_id
|
||||
|
||||
def newtuntap(self, iface_id: int = None, ifname: str = None) -> int:
|
||||
"""
|
||||
Create a new tunnel tap.
|
||||
|
||||
:param iface_id: interface id
|
||||
:param ifname: interface name
|
||||
:return: interface index
|
||||
"""
|
||||
with self.lock:
|
||||
iface_id = iface_id if iface_id is not None else self.next_iface_id()
|
||||
ifname = ifname if ifname is not None else f"eth{iface_id}"
|
||||
sessionid = self.session.short_session_id()
|
||||
localname = f"tap{self.id}.{iface_id}.{sessionid}"
|
||||
name = ifname
|
||||
tuntap = TunTap(self.session, name, localname, node=self)
|
||||
if self.up:
|
||||
tuntap.startup()
|
||||
try:
|
||||
self.add_iface(tuntap, iface_id)
|
||||
except CoreError as e:
|
||||
tuntap.shutdown()
|
||||
raise e
|
||||
return iface_id
|
||||
|
||||
def set_mac(self, iface_id: int, mac: str) -> None:
|
||||
"""
|
||||
Set hardware address for an interface.
|
||||
|
||||
:param iface_id: id of interface to set hardware address for
|
||||
:param mac: mac address to set
|
||||
:return: nothing
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
iface = self.get_iface(iface_id)
|
||||
iface.set_mac(mac)
|
||||
if self.up:
|
||||
self.node_net_client.device_mac(iface.name, str(iface.mac))
|
||||
|
||||
def add_ip(self, iface_id: int, ip: str) -> None:
|
||||
"""
|
||||
Add an ip address to an interface in the format "10.0.0.1/24".
|
||||
|
||||
:param iface_id: id of interface to add address to
|
||||
:param ip: address to add to interface
|
||||
:return: nothing
|
||||
:raises CoreError: when ip address provided is invalid
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
iface = self.get_iface(iface_id)
|
||||
iface.add_ip(ip)
|
||||
if self.up:
|
||||
# ipv4 check
|
||||
broadcast = None
|
||||
if netaddr.valid_ipv4(ip):
|
||||
broadcast = "+"
|
||||
self.node_net_client.create_address(iface.name, ip, broadcast)
|
||||
|
||||
def remove_ip(self, iface_id: int, ip: str) -> None:
|
||||
"""
|
||||
Remove an ip address from an interface in the format "10.0.0.1/24".
|
||||
|
||||
:param iface_id: id of interface to delete address from
|
||||
:param ip: ip address to remove from interface
|
||||
:return: nothing
|
||||
:raises CoreError: when ip address provided is invalid
|
||||
:raises CoreCommandError: when a non-zero exit status occurs
|
||||
"""
|
||||
iface = self.get_iface(iface_id)
|
||||
iface.remove_ip(ip)
|
||||
if self.up:
|
||||
self.node_net_client.delete_address(iface.name, ip)
|
||||
|
||||
def ifup(self, iface_id: int) -> None:
|
||||
"""
|
||||
Bring an interface up.
|
||||
|
||||
:param iface_id: index of interface to bring up
|
||||
:return: nothing
|
||||
"""
|
||||
if self.up:
|
||||
iface = self.get_iface(iface_id)
|
||||
self.node_net_client.device_up(iface.name)
|
||||
|
||||
def new_iface(
|
||||
self, net: "CoreNetworkBase", iface_data: InterfaceData
|
||||
) -> CoreInterface:
|
||||
"""
|
||||
Create a new network interface.
|
||||
|
||||
:param net: network to associate with
|
||||
:param iface_data: interface data for new interface
|
||||
:return: interface index
|
||||
"""
|
||||
with self.lock:
|
||||
if net.has_custom_iface:
|
||||
return net.custom_iface(self, iface_data)
|
||||
else:
|
||||
iface_id = iface_data.id
|
||||
if iface_id is not None and iface_id in self.ifaces:
|
||||
raise CoreError(
|
||||
f"node({self.name}) already has interface({iface_id})"
|
||||
)
|
||||
iface_id = self.newveth(iface_id, iface_data.name, iface_data.mtu)
|
||||
self.attachnet(iface_id, net)
|
||||
if iface_data.mac:
|
||||
self.set_mac(iface_id, iface_data.mac)
|
||||
for ip in iface_data.get_ips():
|
||||
self.add_ip(iface_id, ip)
|
||||
self.ifup(iface_id)
|
||||
return self.get_iface(iface_id)
|
||||
|
||||
def _find_parent_path(self, path: Path) -> Optional[Path]:
|
||||
"""
|
||||
Check if there is a mounted parent directory created for this node.
|
||||
|
@ -910,6 +749,47 @@ class CoreNode(CoreNodeBase):
|
|||
if mode is not None:
|
||||
self.host_cmd(f"chmod {mode:o} {host_path}")
|
||||
|
||||
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||
"""
|
||||
Adopt interface to the network namespace of the node and setting
|
||||
the proper name provided.
|
||||
|
||||
:param iface: interface to adopt
|
||||
:param name: proper name for interface
|
||||
:return: nothing
|
||||
"""
|
||||
# TODO: container, checksums off (container only?)
|
||||
# TODO: container, get flow id (container only?)
|
||||
# validate iface belongs to node and get id
|
||||
iface_id = self.get_iface_id(iface)
|
||||
if iface_id == -1:
|
||||
raise CoreError(f"adopting unknown iface({iface.name})")
|
||||
# add iface to container namespace
|
||||
self.net_client.device_ns(iface.name, str(self.pid))
|
||||
# update iface name to container name
|
||||
name = name if name else f"eth{iface_id}"
|
||||
self.node_net_client.device_name(iface.name, name)
|
||||
iface.name = name
|
||||
# turn checksums off
|
||||
self.node_net_client.checksums_off(iface.name)
|
||||
# retrieve flow id for container
|
||||
iface.flow_id = self.node_net_client.get_ifindex(iface.name)
|
||||
logger.debug("interface flow index: %s - %s", iface.name, iface.flow_id)
|
||||
# set mac address
|
||||
self.node_net_client.device_mac(iface.name, str(iface.mac))
|
||||
logger.debug("interface mac: %s - %s", iface.name, iface.mac)
|
||||
# set all addresses
|
||||
for ip in iface.ips():
|
||||
# ipv4 check
|
||||
broadcast = None
|
||||
if netaddr.valid_ipv4(ip):
|
||||
broadcast = "+"
|
||||
self.node_net_client.create_address(iface.name, str(ip), broadcast)
|
||||
# configure iface options
|
||||
iface.set_config(self)
|
||||
# set iface up
|
||||
self.node_net_client.device_up(iface.name)
|
||||
|
||||
|
||||
class CoreNetworkBase(NodeBase):
|
||||
"""
|
||||
|
@ -917,7 +797,6 @@ class CoreNetworkBase(NodeBase):
|
|||
"""
|
||||
|
||||
linktype: LinkTypes = LinkTypes.WIRED
|
||||
has_custom_iface: bool = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -941,57 +820,6 @@ class CoreNetworkBase(NodeBase):
|
|||
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
|
||||
self.linked_lock: threading.Lock = threading.Lock()
|
||||
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
Each object implements its own startup method.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Each object implements its own shutdown method.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
|
||||
"""
|
||||
Link network to another.
|
||||
|
||||
:param net: network to link with
|
||||
:return: created interface
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
"""
|
||||
Defines custom logic for creating an interface, if required.
|
||||
|
||||
:param node: node to create interface for
|
||||
:param iface_data: data for creating interface
|
||||
:return: created interface
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]:
|
||||
"""
|
||||
Return the interface that links this net with another net.
|
||||
|
||||
:param net: interface to get link for
|
||||
:return: interface the provided network is linked to
|
||||
"""
|
||||
for iface in self.get_ifaces():
|
||||
if iface.othernet == net:
|
||||
return iface
|
||||
return None
|
||||
|
||||
def attach(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
Attach network interface.
|
||||
|
@ -1001,6 +829,7 @@ class CoreNetworkBase(NodeBase):
|
|||
"""
|
||||
i = self.next_iface_id()
|
||||
self.ifaces[i] = iface
|
||||
iface.net = self
|
||||
iface.net_id = i
|
||||
with self.linked_lock:
|
||||
self.linked[iface] = {}
|
||||
|
@ -1013,56 +842,11 @@ class CoreNetworkBase(NodeBase):
|
|||
:return: nothing
|
||||
"""
|
||||
del self.ifaces[iface.net_id]
|
||||
iface.net = None
|
||||
iface.net_id = None
|
||||
with self.linked_lock:
|
||||
del self.linked[iface]
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||
"""
|
||||
Build link data objects for this network. Each link object describes a link
|
||||
between this network and a node.
|
||||
|
||||
:param flags: message type
|
||||
:return: list of link data
|
||||
"""
|
||||
all_links = []
|
||||
# build a link message from this network node to each node having a
|
||||
# connected interface
|
||||
for iface in self.get_ifaces():
|
||||
unidirectional = 0
|
||||
linked_node = iface.node
|
||||
if linked_node is None:
|
||||
# two layer-2 switches/hubs linked together
|
||||
if not iface.othernet:
|
||||
continue
|
||||
linked_node = iface.othernet
|
||||
if linked_node.id == self.id:
|
||||
continue
|
||||
if iface.local_options != iface.options:
|
||||
unidirectional = 1
|
||||
iface_data = iface.get_data()
|
||||
link_data = LinkData(
|
||||
message_type=flags,
|
||||
type=self.linktype,
|
||||
node1_id=self.id,
|
||||
node2_id=linked_node.id,
|
||||
iface2=iface_data,
|
||||
options=iface.local_options,
|
||||
)
|
||||
link_data.options.unidirectional = unidirectional
|
||||
all_links.append(link_data)
|
||||
if unidirectional:
|
||||
link_data = LinkData(
|
||||
message_type=MessageFlags.NONE,
|
||||
type=self.linktype,
|
||||
node1_id=linked_node.id,
|
||||
node2_id=self.id,
|
||||
options=iface.options,
|
||||
)
|
||||
link_data.options.unidirectional = unidirectional
|
||||
all_links.append(link_data)
|
||||
return all_links
|
||||
|
||||
|
||||
class Position:
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -22,8 +22,8 @@ from core.emulator.enumerations import (
|
|||
)
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.executables import NFTABLES
|
||||
from core.nodes.base import CoreNetworkBase, CoreNode
|
||||
from core.nodes.interface import CoreInterface, GreTap, Veth
|
||||
from core.nodes.base import CoreNetworkBase
|
||||
from core.nodes.interface import CoreInterface, GreTap
|
||||
from core.nodes.netclient import get_net_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -280,6 +280,17 @@ class CoreNetwork(CoreNetworkBase):
|
|||
self.up = True
|
||||
nft_queue.start()
|
||||
|
||||
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
|
||||
"""
|
||||
Adopt interface and set it to use this bridge as master.
|
||||
|
||||
:param iface: interface to adpopt
|
||||
:param name: formal name for interface
|
||||
:return: nothing
|
||||
"""
|
||||
iface.net_client.set_iface_master(self.brname, iface.name)
|
||||
iface.set_config()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Linux bridge shutdown logic.
|
||||
|
@ -309,9 +320,9 @@ class CoreNetwork(CoreNetworkBase):
|
|||
:param iface: network interface to attach
|
||||
:return: nothing
|
||||
"""
|
||||
super().attach(iface)
|
||||
if self.up:
|
||||
iface.net_client.set_iface_master(self.brname, iface.localname)
|
||||
super().attach(iface)
|
||||
|
||||
def detach(self, iface: CoreInterface) -> None:
|
||||
"""
|
||||
|
@ -320,9 +331,9 @@ class CoreNetwork(CoreNetworkBase):
|
|||
:param iface: network interface to detach
|
||||
:return: nothing
|
||||
"""
|
||||
super().detach(iface)
|
||||
if self.up:
|
||||
iface.net_client.delete_iface(self.brname, iface.localname)
|
||||
super().detach(iface)
|
||||
|
||||
def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
|
||||
"""
|
||||
|
@ -378,67 +389,6 @@ class CoreNetwork(CoreNetworkBase):
|
|||
self.linked[iface1][iface2] = True
|
||||
nft_queue.update(self)
|
||||
|
||||
def linknet(self, net: CoreNetworkBase) -> CoreInterface:
|
||||
"""
|
||||
Link this bridge with another by creating a veth pair and installing
|
||||
each device into each bridge.
|
||||
|
||||
:param net: network to link with
|
||||
:return: created interface
|
||||
"""
|
||||
sessionid = self.session.short_session_id()
|
||||
try:
|
||||
_id = f"{self.id:x}"
|
||||
except TypeError:
|
||||
_id = str(self.id)
|
||||
try:
|
||||
net_id = f"{net.id:x}"
|
||||
except TypeError:
|
||||
net_id = str(net.id)
|
||||
localname = f"veth{_id}.{net_id}.{sessionid}"
|
||||
name = f"veth{net_id}.{_id}.{sessionid}"
|
||||
iface = Veth(self.session, name, localname)
|
||||
if self.up:
|
||||
iface.startup()
|
||||
self.attach(iface)
|
||||
if net.up and net.brname:
|
||||
iface.net_client.set_iface_master(net.brname, iface.name)
|
||||
i = net.next_iface_id()
|
||||
net.ifaces[i] = iface
|
||||
with net.linked_lock:
|
||||
net.linked[iface] = {}
|
||||
iface.net = self
|
||||
iface.othernet = net
|
||||
return iface
|
||||
|
||||
def get_linked_iface(self, net: CoreNetworkBase) -> Optional[CoreInterface]:
|
||||
"""
|
||||
Return the interface of that links this net with another net
|
||||
(that were linked using linknet()).
|
||||
|
||||
:param net: interface to get link for
|
||||
:return: interface the provided network is linked to
|
||||
"""
|
||||
for iface in self.get_ifaces():
|
||||
if iface.othernet == net:
|
||||
return iface
|
||||
return None
|
||||
|
||||
def add_ips(self, ips: List[str]) -> None:
|
||||
"""
|
||||
Add ip addresses on the bridge in the format "10.0.0.1/24".
|
||||
|
||||
:param ips: ip address to add
|
||||
:return: nothing
|
||||
"""
|
||||
if not self.up:
|
||||
return
|
||||
for ip in ips:
|
||||
self.net_client.create_address(self.brname, ip)
|
||||
|
||||
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
|
||||
raise CoreError(f"{type(self).__name__} does not support, custom interfaces")
|
||||
|
||||
|
||||
class GreTapBridge(CoreNetwork):
|
||||
"""
|
||||
|
@ -686,15 +636,6 @@ class CtrlNet(CoreNetwork):
|
|||
|
||||
super().shutdown()
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||
"""
|
||||
Do not include CtrlNet in link messages describing this session.
|
||||
|
||||
:param flags: message flags
|
||||
:return: list of link data
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class PtpNet(CoreNetwork):
|
||||
"""
|
||||
|
@ -714,50 +655,6 @@ class PtpNet(CoreNetwork):
|
|||
raise CoreError("ptp links support at most 2 network interfaces")
|
||||
super().attach(iface)
|
||||
|
||||
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
|
||||
"""
|
||||
Build CORE API TLVs for a point-to-point link. One Link message
|
||||
describes this network.
|
||||
|
||||
:param flags: message flags
|
||||
:return: list of link data
|
||||
"""
|
||||
all_links = []
|
||||
if len(self.ifaces) != 2:
|
||||
return all_links
|
||||
ifaces = self.get_ifaces()
|
||||
iface1 = ifaces[0]
|
||||
iface2 = ifaces[1]
|
||||
unidirectional = 0 if iface1.local_options == iface2.local_options else 1
|
||||
iface1_data = iface1.get_data()
|
||||
iface2_data = iface2.get_data()
|
||||
link_data = LinkData(
|
||||
message_type=flags,
|
||||
type=self.linktype,
|
||||
node1_id=iface1.node.id,
|
||||
node2_id=iface2.node.id,
|
||||
iface1=iface1_data,
|
||||
iface2=iface2_data,
|
||||
options=iface1.local_options,
|
||||
)
|
||||
link_data.options.unidirectional = unidirectional
|
||||
all_links.append(link_data)
|
||||
# build a 2nd link message for the upstream link parameters
|
||||
# (swap if1 and if2)
|
||||
if unidirectional:
|
||||
link_data = LinkData(
|
||||
message_type=MessageFlags.NONE,
|
||||
type=self.linktype,
|
||||
node1_id=iface2.node.id,
|
||||
node2_id=iface1.node.id,
|
||||
iface1=InterfaceData(id=iface2_data.id),
|
||||
iface2=InterfaceData(id=iface1_data.id),
|
||||
options=iface2.local_options,
|
||||
)
|
||||
link_data.options.unidirectional = unidirectional
|
||||
all_links.append(link_data)
|
||||
return all_links
|
||||
|
||||
|
||||
class SwitchNode(CoreNetwork):
|
||||
"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue