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:
Blake Harnden 2022-03-17 15:28:38 -07:00
parent d684b8eb5a
commit cd7f1a641e
19 changed files with 1393 additions and 1556 deletions

View file

@ -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:
"""