daemon: refactored interfaces to store configuration options as link options, instead of using a dictionary

This commit is contained in:
Blake Harnden 2022-01-25 21:39:52 -08:00
parent e9b83b0d28
commit 6791269eeb
6 changed files with 238 additions and 286 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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