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. CORE data objects.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, Any, List, Optional, Tuple
import netaddr import netaddr
@ -176,6 +176,67 @@ class LinkOptions:
key: int = None key: int = None
buffer: 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 @dataclass
class LinkData: class LinkData:

View file

@ -1049,11 +1049,10 @@ class CoreNetworkBase(NodeBase):
:return: list of link data :return: list of link data
""" """
all_links = [] all_links = []
# build a link message from this network node to each node having a # build a link message from this network node to each node having a
# connected interface # connected interface
for iface in self.get_ifaces(): for iface in self.get_ifaces():
uni = False unidirectional = 0
linked_node = iface.node linked_node = iface.node
if linked_node is None: if linked_node is None:
# two layer-2 switches/hubs linked together via linknet() # two layer-2 switches/hubs linked together via linknet()
@ -1062,53 +1061,29 @@ class CoreNetworkBase(NodeBase):
linked_node = iface.othernet linked_node = iface.othernet
if linked_node.id == self.id: if linked_node.id == self.id:
continue continue
iface.swapparams("_params_up") if iface.local_options != iface.options:
upstream_params = iface.getparams() unidirectional = 1
iface.swapparams("_params_up") iface_data = iface.get_data()
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)
link_data = LinkData( link_data = LinkData(
message_type=flags, message_type=flags,
type=self.linktype, type=self.linktype,
node1_id=self.id, node1_id=self.id,
node2_id=linked_node.id, node2_id=linked_node.id,
iface2=iface2_data, iface2=iface_data,
options=options_data, options=iface.local_options,
) )
link_data.options.unidirectional = unidirectional
all_links.append(link_data) all_links.append(link_data)
if unidirectional:
if not uni: link_data = LinkData(
continue message_type=MessageFlags.NONE,
iface.swapparams("_params_up") type=self.linktype,
options_data = iface.get_link_options(unidirectional) node1_id=linked_node.id,
link_data = LinkData( node2_id=self.id,
message_type=MessageFlags.NONE, options=iface.options,
type=self.linktype, )
node1_id=linked_node.id, link_data.options.unidirectional = unidirectional
node2_id=self.id, all_links.append(link_data)
options=options_data,
)
iface.swapparams("_params_up")
all_links.append(link_data)
return all_links return all_links

View file

@ -6,12 +6,12 @@ import logging
import math import math
import time import time
from pathlib import Path 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 import netaddr
from core import utils 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.emulator.enumerations import TransportType
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import TC from core.executables import TC
@ -27,6 +27,50 @@ if TYPE_CHECKING:
DEFAULT_MTU: int = 1500 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: class CoreInterface:
""" """
Base class for network interfaces. Base class for network interfaces.
@ -63,7 +107,6 @@ class CoreInterface:
self.mtu: int = mtu self.mtu: int = mtu
self.net: Optional[CoreNetworkBase] = None self.net: Optional[CoreNetworkBase] = None
self.othernet: Optional[CoreNetworkBase] = None self.othernet: Optional[CoreNetworkBase] = None
self._params: Dict[str, float] = {}
self.ip4s: List[netaddr.IPNetwork] = [] self.ip4s: List[netaddr.IPNetwork] = []
self.ip6s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = []
self.mac: Optional[netaddr.EUI] = None self.mac: Optional[netaddr.EUI] = None
@ -82,6 +125,11 @@ class CoreInterface:
self.session.use_ovs(), self.host_cmd self.session.use_ovs(), self.host_cmd
) )
self.control: bool = False 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( def host_cmd(
self, self,
@ -221,89 +269,6 @@ class CoreInterface:
except netaddr.AddrFormatError as e: except netaddr.AddrFormatError as e:
raise CoreError(f"invalid mac address({mac}): {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: def setposition(self) -> None:
""" """
Dispatch position hook handler when possible. Dispatch position hook handler when possible.
@ -338,15 +303,6 @@ class CoreInterface:
""" """
return self.transport_type == TransportType.VIRTUAL 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: def config(self, options: LinkOptions, use_local: bool = True) -> None:
""" """
Configure interface using tc based on existing state and provided 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 :param use_local: True to use localname for device, False for name
:return: nothing :return: nothing
""" """
# determine if any settings have changed # determine name, options, and if anything has changed
if use_local: name = self.localname if use_local else self.name
devname = self.localname current_options = self.local_options if use_local else self.options
changed = self._set_params_change( changed = current_options.update(options)
bw=options.bandwidth, # nothing more to do when nothing has changed or not up
delay=options.delay, if not changed or not self.up:
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:
return return
# delete tc configuration or create and add it # clear current settings
if all( if current_options.is_clear():
[ clear_local_netem = use_local and self.has_local_netem
options.delay is None or options.delay <= 0, clear_netem = not use_local and self.has_netem
options.jitter is None or options.jitter <= 0, if clear_local_netem or clear_netem:
options.loss is None or options.loss <= 0, cmd = tc_clear_cmd(name)
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:"
self.host_cmd(cmd) self.host_cmd(cmd)
self.setparam("has_netem", False) if use_local:
else: self.has_local_netem = False
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%"
else: else:
netem += f" {options.jitter}us 25%" self.has_netem = False
if options.loss is not None and options.loss > 0: # set updated settings
netem += f" loss {min(options.loss, 100)}%" else:
if options.dup is not None and options.dup > 0: cmd = tc_cmd(name, current_options, self.mtu)
netem += f" duplicate {min(options.dup, 100)}%" self.host_cmd(cmd)
if self.up: if use_local:
cmd = f"{TC} qdisc replace dev {devname} root handle 10: netem {netem}" self.has_local_netem = True
self.host_cmd(cmd) else:
self.setparam("has_netem", True) 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): class Veth(CoreInterface):

View file

@ -743,41 +743,12 @@ class PtpNet(CoreNetwork):
all_links = [] all_links = []
if len(self.ifaces) != 2: if len(self.ifaces) != 2:
return all_links return all_links
ifaces = self.get_ifaces() ifaces = self.get_ifaces()
iface1 = ifaces[0] iface1 = ifaces[0]
iface2 = ifaces[1] iface2 = ifaces[1]
unidirectional = 0 unidirectional = 0 if iface1.local_options == iface2.local_options else 1
if iface1.getparams() != iface2.getparams(): iface1_data = iface1.get_data()
unidirectional = 1 iface2_data = iface2.get_data()
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)
link_data = LinkData( link_data = LinkData(
message_type=flags, message_type=flags,
type=self.linktype, type=self.linktype,
@ -785,25 +756,23 @@ class PtpNet(CoreNetwork):
node2_id=iface2.node.id, node2_id=iface2.node.id,
iface1=iface1_data, iface1=iface1_data,
iface2=iface2_data, iface2=iface2_data,
options=options_data, options=iface1.local_options,
) )
link_data.options.unidirectional = unidirectional
all_links.append(link_data) all_links.append(link_data)
# build a 2nd link message for the upstream link parameters # build a 2nd link message for the upstream link parameters
# (swap if1 and if2) # (swap if1 and if2)
if unidirectional: 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( link_data = LinkData(
message_type=MessageFlags.NONE, message_type=MessageFlags.NONE,
type=self.linktype, type=self.linktype,
node1_id=iface2.node.id, node1_id=iface2.node.id,
node2_id=iface1.node.id, node2_id=iface1.node.id,
iface1=iface1_data, iface1=InterfaceData(id=iface2_data.id),
iface2=iface2_data, iface2=InterfaceData(id=iface1_data.id),
options=options_data, options=iface2.local_options,
) )
link_data.options.unidirectional = unidirectional
all_links.append(link_data) all_links.append(link_data)
return all_links return all_links

View file

@ -130,12 +130,6 @@ class TestCore:
assert 0 in node1.ifaces assert 0 in node1.ifaces
assert 0 in node2.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 # delete interface and test that if no longer exists
node1.delete_iface(0) node1.delete_iface(0)
assert 0 not in node1.ifaces 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.emulator.session import Session
from core.errors import CoreError from core.errors import CoreError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
from core.nodes.interface import CoreInterface
from core.nodes.network import SwitchNode from core.nodes.network import SwitchNode
INVALID_ID: int = 100 INVALID_ID: int = 100
@ -33,26 +32,6 @@ def create_ptp_network(
return node1, node2 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: class TestLinks:
def test_add_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_add_node_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -71,8 +50,10 @@ class TestLinks:
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface1 is not None assert iface1 is not None
assert iface2 is not None assert iface2 is not None
assert check_iface_match(iface1, LINK_OPTIONS) assert iface1.local_options == LINK_OPTIONS
assert check_iface_match(iface2, 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): def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -89,7 +70,8 @@ class TestLinks:
assert node2.links() assert node2.links()
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert iface is not None 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): def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -106,7 +88,8 @@ class TestLinks:
assert node1.links() assert node1.links()
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface is not None 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): def test_add_net_to_net(self, session):
# given # given
@ -119,7 +102,10 @@ class TestLinks:
# then # then
assert node1.links() assert node1.links()
assert iface is not None 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): def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -159,8 +145,10 @@ class TestLinks:
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface1 is not None assert iface1 is not None
assert iface2 is not None assert iface2 is not None
assert check_iface_match(iface1, link_options1) assert iface1.local_options == link_options1
assert check_iface_match(iface2, link_options2) 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): def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -168,7 +156,7 @@ class TestLinks:
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface1, _ = session.add_link(node1.id, node2.id, iface1_data) iface1, _ = session.add_link(node1.id, node2.id, iface1_data)
assert check_iface_diff(iface1, LINK_OPTIONS) assert iface1.local_options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(
@ -176,7 +164,8 @@ class TestLinks:
) )
# then # 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): def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -184,7 +173,7 @@ class TestLinks:
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) _, 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 # when
session.update_link( session.update_link(
@ -192,7 +181,8 @@ class TestLinks:
) )
# then # 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): def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -201,8 +191,8 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert check_iface_diff(iface1, LINK_OPTIONS) assert iface1.local_options != LINK_OPTIONS
assert check_iface_diff(iface2, LINK_OPTIONS) assert iface2.local_options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(
@ -210,21 +200,46 @@ class TestLinks:
) )
# then # then
assert check_iface_match(iface1, LINK_OPTIONS) assert iface1.local_options == LINK_OPTIONS
assert check_iface_match(iface2, 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): def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1, _ = session.add_link(node1.id, node2.id) iface1, _ = session.add_link(node1.id, node2.id)
assert check_iface_diff(iface1, LINK_OPTIONS) assert iface1.local_options != LINK_OPTIONS
# when # when
session.update_link(node1.id, node2.id, options=LINK_OPTIONS) session.update_link(node1.id, node2.id, options=LINK_OPTIONS)
# then # 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): def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given