diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 4ce92d0b..28dcb813 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -2,7 +2,7 @@ CORE data objects. """ from dataclasses import dataclass, field -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, List, Optional, Tuple import netaddr @@ -176,6 +176,67 @@ class LinkOptions: key: int = None buffer: int = None + def update(self, options: "LinkOptions") -> bool: + """ + Updates current options with values from other options. + + :param options: options to update with + :return: True if any value has changed, False otherwise + """ + changed = False + if options.delay is not None and 0 <= options.delay != self.delay: + self.delay = options.delay + changed = True + if options.bandwidth is not None and 0 <= options.bandwidth != self.bandwidth: + self.bandwidth = options.bandwidth + changed = True + if options.loss is not None and 0 <= options.loss != self.loss: + self.loss = options.loss + changed = True + if options.dup is not None and 0 <= options.dup != self.dup: + self.dup = options.dup + changed = True + if options.jitter is not None and 0 <= options.jitter != self.jitter: + self.jitter = options.jitter + changed = True + if options.buffer is not None and 0 <= options.buffer != self.buffer: + self.buffer = options.buffer + changed = True + return changed + + def is_clear(self) -> bool: + """ + Checks if the current option values represent a clear state. + + :return: True if the current values should clear, False otherwise + """ + clear = self.delay is None or self.delay <= 0 + clear &= self.jitter is None or self.jitter <= 0 + clear &= self.loss is None or self.loss <= 0 + clear &= self.dup is None or self.dup <= 0 + clear &= self.bandwidth is None or self.bandwidth <= 0 + clear &= self.buffer is None or self.buffer <= 0 + return clear + + def __eq__(self, other: Any) -> bool: + """ + Custom logic to check if this link options is equivalent to another. + + :param other: other object to check + :return: True if they are both link options with the same values, + False otherwise + """ + if not isinstance(other, LinkOptions): + return False + return ( + self.delay == other.delay + and self.jitter == other.jitter + and self.loss == other.loss + and self.dup == other.dup + and self.bandwidth == other.bandwidth + and self.buffer == other.buffer + ) + @dataclass class LinkData: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index ca6a43ef..03979d01 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1049,11 +1049,10 @@ class CoreNetworkBase(NodeBase): :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(): - uni = False + unidirectional = 0 linked_node = iface.node if linked_node is None: # two layer-2 switches/hubs linked together via linknet() @@ -1062,53 +1061,29 @@ class CoreNetworkBase(NodeBase): linked_node = iface.othernet if linked_node.id == self.id: continue - iface.swapparams("_params_up") - upstream_params = iface.getparams() - iface.swapparams("_params_up") - if iface.getparams() != upstream_params: - uni = True - - unidirectional = 0 - if uni: - unidirectional = 1 - - mac = str(iface.mac) if iface.mac else None - iface2_data = InterfaceData( - id=linked_node.get_iface_id(iface), name=iface.name, mac=mac - ) - ip4 = iface.get_ip4() - if ip4: - iface2_data.ip4 = str(ip4.ip) - iface2_data.ip4_mask = ip4.prefixlen - ip6 = iface.get_ip6() - if ip6: - iface2_data.ip6 = str(ip6.ip) - iface2_data.ip6_mask = ip6.prefixlen - - options_data = iface.get_link_options(unidirectional) + 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=iface2_data, - options=options_data, + iface2=iface_data, + options=iface.local_options, ) + link_data.options.unidirectional = unidirectional all_links.append(link_data) - - if not uni: - continue - iface.swapparams("_params_up") - options_data = iface.get_link_options(unidirectional) - link_data = LinkData( - message_type=MessageFlags.NONE, - type=self.linktype, - node1_id=linked_node.id, - node2_id=self.id, - options=options_data, - ) - iface.swapparams("_params_up") - 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 diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 7fc0a386..70eb679f 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -6,12 +6,12 @@ import logging import math import time from pathlib import Path -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Callable, Dict, List, Optional import netaddr from core import utils -from core.emulator.data import LinkOptions +from core.emulator.data import InterfaceData, LinkOptions from core.emulator.enumerations import TransportType from core.errors import CoreCommandError, CoreError from core.executables import TC @@ -27,6 +27,50 @@ if TYPE_CHECKING: DEFAULT_MTU: int = 1500 +def tc_clear_cmd(name: str) -> str: + """ + Create tc command to clear device configuration. + + :param name: name of device to clear + :return: tc command + """ + return f"{TC} qdisc delete dev {name} root handle 10:" + + +def tc_cmd(name: str, options: LinkOptions, mtu: int) -> str: + """ + Create tc command to configure a device with given name and options. + + :param name: name of device to configure + :param options: options to configure with + :param mtu: mtu for configuration + :return: tc command + """ + netem = "" + if options.bandwidth is not None: + limit = 1000 + bw = options.bandwidth / 1000 + if options.buffer is not None and options.buffer > 0: + limit = options.buffer + elif options.delay and options.bandwidth: + delay = options.delay / 1000 + limit = max(2, math.ceil((2 * bw * delay) / (8 * mtu))) + netem += f" rate {bw}kbit" + netem += f" limit {limit}" + if options.delay is not None: + netem += f" delay {options.delay}us" + if options.jitter is not None: + if options.delay is None: + netem += f" delay 0us {options.jitter}us 25%" + else: + netem += f" {options.jitter}us 25%" + if options.loss is not None and options.loss > 0: + netem += f" loss {min(options.loss, 100)}%" + if options.dup is not None and options.dup > 0: + netem += f" duplicate {min(options.dup, 100)}%" + return f"{TC} qdisc replace dev {name} root handle 10: netem {netem}" + + class CoreInterface: """ Base class for network interfaces. @@ -63,7 +107,6 @@ class CoreInterface: self.mtu: int = mtu self.net: Optional[CoreNetworkBase] = None self.othernet: Optional[CoreNetworkBase] = None - self._params: Dict[str, float] = {} self.ip4s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = [] self.mac: Optional[netaddr.EUI] = None @@ -82,6 +125,11 @@ class CoreInterface: self.session.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() def host_cmd( self, @@ -221,89 +269,6 @@ class CoreInterface: except netaddr.AddrFormatError as e: raise CoreError(f"invalid mac address({mac}): {e}") - def getparam(self, key: str) -> float: - """ - Retrieve a parameter from the, or None if the parameter does not exist. - - :param key: parameter to get value for - :return: parameter value - """ - return self._params.get(key) - - def get_link_options(self, unidirectional: int) -> LinkOptions: - """ - Get currently set params as link options. - - :param unidirectional: unidirectional setting - :return: link options - """ - delay = self.getparam("delay") - if delay is not None: - delay = int(delay) - bandwidth = self.getparam("bw") - if bandwidth is not None: - bandwidth = int(bandwidth) - dup = self.getparam("duplicate") - if dup is not None: - dup = int(dup) - jitter = self.getparam("jitter") - if jitter is not None: - jitter = int(jitter) - buffer = self.getparam("buffer") - if buffer is not None: - buffer = int(buffer) - return LinkOptions( - delay=delay, - bandwidth=bandwidth, - dup=dup, - jitter=jitter, - loss=self.getparam("loss"), - buffer=buffer, - unidirectional=unidirectional, - ) - - def getparams(self) -> List[Tuple[str, float]]: - """ - Return (key, value) pairs for parameters. - """ - parameters = [] - for k in sorted(self._params.keys()): - parameters.append((k, self._params[k])) - return parameters - - def setparam(self, key: str, value: float) -> bool: - """ - Set a parameter value, returns True if the parameter has changed. - - :param key: parameter name to set - :param value: parameter value - :return: True if parameter changed, False otherwise - """ - # treat None and 0 as unchanged values - logger.debug("setting param: %s - %s", key, value) - if value is None or value < 0: - return False - current_value = self._params.get(key) - if current_value is not None and current_value == value: - return False - self._params[key] = value - return True - - def swapparams(self, name: str) -> None: - """ - Swap out parameters dict for name. If name does not exist, - intialize it. This is for supporting separate upstream/downstream - parameters when two layer-2 nodes are linked together. - - :param name: name of parameter to swap - :return: nothing - """ - tmp = self._params - if not hasattr(self, name): - setattr(self, name, {}) - self._params = getattr(self, name) - setattr(self, name, tmp) - def setposition(self) -> None: """ Dispatch position hook handler when possible. @@ -338,15 +303,6 @@ class CoreInterface: """ return self.transport_type == TransportType.VIRTUAL - def _set_params_change(self, **kwargs: float) -> bool: - """ - Set parameters to change. - - :param kwargs: parameter name and values to change - :return: True if any parameter changed, False otherwise - """ - return any([self.setparam(k, v) for k, v in kwargs.items()]) - def config(self, options: LinkOptions, use_local: bool = True) -> None: """ Configure interface using tc based on existing state and provided @@ -356,73 +312,55 @@ class CoreInterface: :param use_local: True to use localname for device, False for name :return: nothing """ - # determine if any settings have changed - if use_local: - devname = self.localname - changed = self._set_params_change( - bw=options.bandwidth, - delay=options.delay, - loss=options.loss, - duplicate=options.dup, - jitter=options.jitter, - buffer=options.buffer, - ) - else: - devname = self.name - changed = self._set_params_change( - n_bw=options.bandwidth, - n_delay=options.delay, - n_loss=options.loss, - n_duplicate=options.dup, - n_jitter=options.jitter, - n_buffer=options.buffer, - ) - if not changed: + # 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 - # delete tc configuration or create and add it - if all( - [ - options.delay is None or options.delay <= 0, - options.jitter is None or options.jitter <= 0, - options.loss is None or options.loss <= 0, - options.dup is None or options.dup <= 0, - options.bandwidth is None or options.bandwidth <= 0, - options.buffer is None or options.buffer <= 0, - ] - ): - if not self.getparam("has_netem"): - return - if self.up: - cmd = f"{TC} qdisc delete dev {devname} root handle 10:" + # 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) - self.setparam("has_netem", False) - else: - netem = "" - if options.bandwidth is not None: - limit = 1000 - bw = options.bandwidth / 1000 - if options.buffer is not None and options.buffer > 0: - limit = options.buffer - elif options.delay and options.bandwidth: - delay = options.delay / 1000 - limit = max(2, math.ceil((2 * bw * delay) / (8 * self.mtu))) - netem += f" rate {bw}kbit" - netem += f" limit {limit}" - if options.delay is not None: - netem += f" delay {options.delay}us" - if options.jitter is not None: - if options.delay is None: - netem += f" delay 0us {options.jitter}us 25%" + if use_local: + self.has_local_netem = False else: - netem += f" {options.jitter}us 25%" - if options.loss is not None and options.loss > 0: - netem += f" loss {min(options.loss, 100)}%" - if options.dup is not None and options.dup > 0: - netem += f" duplicate {min(options.dup, 100)}%" - if self.up: - cmd = f"{TC} qdisc replace dev {devname} root handle 10: netem {netem}" - self.host_cmd(cmd) - self.setparam("has_netem", True) + 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 + else: + self.has_netem = True + + def get_data(self) -> InterfaceData: + """ + Retrieve the data representation of this interface. + + :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 + ip6 = self.get_ip6() + if ip6: + data.ip6 = str(ip6.ip) + data.ip6_mask = ip6.prefixlen + return data class Veth(CoreInterface): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 2128c9b6..32d420dd 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -743,41 +743,12 @@ class PtpNet(CoreNetwork): all_links = [] if len(self.ifaces) != 2: return all_links - ifaces = self.get_ifaces() iface1 = ifaces[0] iface2 = ifaces[1] - unidirectional = 0 - if iface1.getparams() != iface2.getparams(): - unidirectional = 1 - - mac = str(iface1.mac) if iface1.mac else None - iface1_data = InterfaceData( - id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=mac - ) - ip4 = iface1.get_ip4() - if ip4: - iface1_data.ip4 = str(ip4.ip) - iface1_data.ip4_mask = ip4.prefixlen - ip6 = iface1.get_ip6() - if ip6: - iface1_data.ip6 = str(ip6.ip) - iface1_data.ip6_mask = ip6.prefixlen - - mac = str(iface2.mac) if iface2.mac else None - iface2_data = InterfaceData( - id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=mac - ) - ip4 = iface2.get_ip4() - if ip4: - iface2_data.ip4 = str(ip4.ip) - iface2_data.ip4_mask = ip4.prefixlen - ip6 = iface2.get_ip6() - if ip6: - iface2_data.ip6 = str(ip6.ip) - iface2_data.ip6_mask = ip6.prefixlen - - options_data = iface1.get_link_options(unidirectional) + 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, @@ -785,25 +756,23 @@ class PtpNet(CoreNetwork): node2_id=iface2.node.id, iface1=iface1_data, iface2=iface2_data, - options=options_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: - iface1_data = InterfaceData(id=iface2.node.get_iface_id(iface2)) - iface2_data = InterfaceData(id=iface1.node.get_iface_id(iface1)) - options_data = iface2.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, type=self.linktype, node1_id=iface2.node.id, node2_id=iface1.node.id, - iface1=iface1_data, - iface2=iface2_data, - options=options_data, + 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 diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 1342861b..d7a83452 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -130,12 +130,6 @@ class TestCore: assert 0 in node1.ifaces assert 0 in node2.ifaces - # check interface parameters - iface = node1.get_iface(0) - iface.setparam("test", 1) - assert iface.getparam("test") == 1 - assert iface.getparams() - # delete interface and test that if no longer exists node1.delete_iface(0) assert 0 not in node1.ifaces diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 1d9b54ce..791eb77a 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -6,7 +6,6 @@ from core.emulator.data import IpPrefixes, LinkOptions from core.emulator.session import Session from core.errors import CoreError from core.nodes.base import CoreNode -from core.nodes.interface import CoreInterface from core.nodes.network import SwitchNode INVALID_ID: int = 100 @@ -33,26 +32,6 @@ def create_ptp_network( return node1, node2 -def check_iface_match(iface: CoreInterface, options: LinkOptions) -> bool: - result = iface.getparam("delay") == options.delay - result &= iface.getparam("bw") == options.bandwidth - result &= iface.getparam("loss") == options.loss - result &= iface.getparam("duplicate") == options.dup - result &= iface.getparam("jitter") == options.jitter - result &= iface.getparam("buffer") == options.buffer - return result - - -def check_iface_diff(iface: CoreInterface, options: LinkOptions) -> bool: - result = iface.getparam("delay") != options.delay - result &= iface.getparam("bw") != options.bandwidth - result &= iface.getparam("loss") != options.loss - result &= iface.getparam("duplicate") != options.dup - result &= iface.getparam("jitter") != options.jitter - result &= iface.getparam("buffer") != options.buffer - return result - - class TestLinks: def test_add_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -71,8 +50,10 @@ class TestLinks: assert node2.get_iface(iface2_data.id) assert iface1 is not None assert iface2 is not None - assert check_iface_match(iface1, LINK_OPTIONS) - assert check_iface_match(iface2, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface2.local_options == LINK_OPTIONS + assert iface2.has_local_netem def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -89,7 +70,8 @@ class TestLinks: assert node2.links() assert node1.get_iface(iface1_data.id) assert iface is not None - assert check_iface_match(iface, LINK_OPTIONS) + assert iface.local_options == LINK_OPTIONS + assert iface.has_local_netem def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -106,7 +88,8 @@ class TestLinks: assert node1.links() assert node2.get_iface(iface2_data.id) assert iface is not None - assert check_iface_match(iface, LINK_OPTIONS) + assert iface.local_options == LINK_OPTIONS + assert iface.has_local_netem def test_add_net_to_net(self, session): # given @@ -119,7 +102,10 @@ class TestLinks: # then assert node1.links() assert iface is not None - assert check_iface_match(iface, LINK_OPTIONS) + assert iface.local_options == LINK_OPTIONS + assert iface.options == LINK_OPTIONS + assert iface.has_local_netem + assert iface.has_netem def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -159,8 +145,10 @@ class TestLinks: assert node2.get_iface(iface2_data.id) assert iface1 is not None assert iface2 is not None - assert check_iface_match(iface1, link_options1) - assert check_iface_match(iface2, link_options2) + assert iface1.local_options == link_options1 + assert iface1.has_local_netem + assert iface2.local_options == link_options2 + assert iface2.has_local_netem def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -168,7 +156,7 @@ class TestLinks: node2 = session.add_node(SwitchNode) iface1_data = ip_prefixes.create_iface(node1) iface1, _ = session.add_link(node1.id, node2.id, iface1_data) - assert check_iface_diff(iface1, LINK_OPTIONS) + assert iface1.local_options != LINK_OPTIONS # when session.update_link( @@ -176,7 +164,8 @@ class TestLinks: ) # then - assert check_iface_match(iface1, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -184,7 +173,7 @@ class TestLinks: node2 = session.add_node(CoreNode) iface2_data = ip_prefixes.create_iface(node2) _, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) - assert check_iface_diff(iface2, LINK_OPTIONS) + assert iface2.local_options != LINK_OPTIONS # when session.update_link( @@ -192,7 +181,8 @@ class TestLinks: ) # then - assert check_iface_match(iface2, LINK_OPTIONS) + assert iface2.local_options == LINK_OPTIONS + assert iface2.has_local_netem def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -201,8 +191,8 @@ class TestLinks: iface1_data = ip_prefixes.create_iface(node1) iface2_data = ip_prefixes.create_iface(node2) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) - assert check_iface_diff(iface1, LINK_OPTIONS) - assert check_iface_diff(iface2, LINK_OPTIONS) + assert iface1.local_options != LINK_OPTIONS + assert iface2.local_options != LINK_OPTIONS # when session.update_link( @@ -210,21 +200,46 @@ class TestLinks: ) # then - assert check_iface_match(iface1, LINK_OPTIONS) - assert check_iface_match(iface2, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface2.local_options == LINK_OPTIONS + assert iface2.has_local_netem def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) iface1, _ = session.add_link(node1.id, node2.id) - assert check_iface_diff(iface1, LINK_OPTIONS) + assert iface1.local_options != LINK_OPTIONS # when session.update_link(node1.id, node2.id, options=LINK_OPTIONS) # then - assert check_iface_match(iface1, LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + + def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) + iface1, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) + assert iface1.local_options == LINK_OPTIONS + assert iface1.has_local_netem + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + + # when + options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0) + session.update_link(node1.id, node2.id, options=options) + + # then + assert iface1.local_options.is_clear() + assert not iface1.has_local_netem + assert iface1.options.is_clear() + assert not iface1.has_netem def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given