diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index c1ad28ae..437cae37 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -582,6 +582,7 @@ def clear(self) -> None: self.location.reset() self.mobility.config_reset() self.link_colors.clear() + self.control_net_manager.remove_nets() def set_location(self, lat: float, lon: float, alt: float, scale: float) -> None: """ @@ -1035,9 +1036,6 @@ def data_collect(self) -> None: # update control interface hosts self.control_net_manager.clear_etc_hosts() - # remove control networks - self.control_net_manager.remove_nets() - def short_session_id(self) -> str: """ Return a shorter version of the session ID, appropriate for diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 1a03aced..a527b670 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -228,6 +228,19 @@ def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: """ return self.host_cmd(args, wait=wait, shell=shell) + def net_cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: + """ + Runs a network command that is in the context of a node, default is to run a + standard host command. + + :param args: command to run + :param wait: True to wait for status, False otherwise + :param shell: True to use shell, False otherwise + :return: combined stdout and stderr + :raises CoreCommandError: when a non-zero exit status occurs + """ + return self.cmd(args, wait, shell) + def setposition(self, x: float = None, y: float = None, z: float = None) -> bool: """ Set the (x,y,z) position of the object. diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 061701fb..85dc14d4 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -13,6 +13,7 @@ from core.errors import CoreCommandError, CoreError from core.executables import BASH from core.nodes.base import CoreNode, CoreNodeOptions +from core.nodes.netclient import LinuxNetClient, get_net_client logger = logging.getLogger(__name__) @@ -99,6 +100,16 @@ def create_options(cls) -> DockerOptions: """ return DockerOptions() + def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: + """ + Create node network client for running network commands within the nodes + container. + + :param use_ovs: True for OVS bridges, False for Linux bridges + :return: node network client + """ + return get_net_client(use_ovs, self.cmd, self.net_cmd) + def create_cmd(self, args: str, shell: bool = False) -> str: """ Create command used to run commands within the context of a node. @@ -128,6 +139,35 @@ def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: else: return self.server.remote_cmd(args, wait=wait, env=self.env) + def create_net_cmd(self, args: str, shell: bool = False) -> str: + """ + Create command used to run network commands within the context of a node. + + :param args: command arguments + :param shell: True to run shell like, False otherwise + :return: node command + """ + if shell: + args = f"{BASH} -c {shlex.quote(args)}" + return f"nsenter -t {self.pid} -n -- {args}" + + def net_cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: + """ + Runs a command that is used to configure and setup the network within a + node. + + :param args: command to run + :param wait: True to wait for status, False otherwise + :param shell: True to use shell, False otherwise + :return: combined stdout and stderr + :raises CoreCommandError: when a non-zero exit status occurs + """ + args = self.create_net_cmd(args, shell) + if self.server is None: + return utils.cmd(args, wait=wait, shell=shell, env=self.env) + else: + return self.server.remote_cmd(args, wait=wait, env=self.env) + def _unique_name(self, name: str) -> str: """ Creates a session/node unique prefixed name for the provided input. diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index eba60e96..5520feab 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -317,7 +317,7 @@ def set_config(self) -> None: if self.has_netem: cmd = tc_clear_cmd(self.name) if self.node: - self.node.cmd(cmd) + self.node.net_cmd(cmd) else: self.host_cmd(cmd) self.has_netem = False @@ -325,7 +325,7 @@ def set_config(self) -> None: else: cmd = tc_cmd(self.name, self.options, self.mtu) if self.node: - self.node.cmd(cmd) + self.node.net_cmd(cmd) else: self.host_cmd(cmd) self.has_netem = True diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 74087e31..398eacde 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -14,13 +14,15 @@ class LinuxNetClient: Client for creating Linux bridges and ip interfaces for nodes. """ - def __init__(self, run: Callable[..., str]) -> None: + def __init__(self, run: Callable[..., str], run_net: Callable[..., str]) -> None: """ Create LinuxNetClient instance. - :param run: function to run commands with + :param run: function to run commands within node context + :param run_net: function to run commands within network context only """ self.run: Callable[..., str] = run + self.run_net: Callable[..., str] = run_net def set_hostname(self, name: str) -> None: """ @@ -40,7 +42,7 @@ def create_route(self, route: str, device: str) -> None: :param device: device to add route to :return: nothing """ - self.run(f"{IP} route replace {route} dev {device}") + self.run_net(f"{IP} route replace {route} dev {device}") def device_up(self, device: str) -> None: """ @@ -49,7 +51,7 @@ def device_up(self, device: str) -> None: :param device: device to bring up :return: nothing """ - self.run(f"{IP} link set {device} up") + self.run_net(f"{IP} link set {device} up") def device_down(self, device: str) -> None: """ @@ -58,7 +60,7 @@ def device_down(self, device: str) -> None: :param device: device to bring down :return: nothing """ - self.run(f"{IP} link set {device} down") + self.run_net(f"{IP} link set {device} down") def device_name(self, device: str, name: str) -> None: """ @@ -68,7 +70,7 @@ def device_name(self, device: str, name: str) -> None: :param name: name to set :return: nothing """ - self.run(f"{IP} link set {device} name {name}") + self.run_net(f"{IP} link set {device} name {name}") def device_show(self, device: str) -> str: """ @@ -77,7 +79,7 @@ def device_show(self, device: str) -> str: :param device: device to get information for :return: device information """ - return self.run(f"{IP} link show {device}") + return self.run_net(f"{IP} link show {device}") def address_show(self, device: str) -> str: """ @@ -86,7 +88,7 @@ def address_show(self, device: str) -> str: :param device: device name :return: address information """ - return self.run(f"{IP} address show {device}") + return self.run_net(f"{IP} address show {device}") def get_mac(self, device: str) -> str: """ @@ -114,7 +116,7 @@ def device_ns(self, device: str, namespace: str) -> None: :param namespace: namespace to set device to :return: nothing """ - self.run(f"{IP} link set {device} netns {namespace}") + self.run_net(f"{IP} link set {device} netns {namespace}") def device_flush(self, device: str) -> None: """ @@ -123,7 +125,7 @@ def device_flush(self, device: str) -> None: :param device: device to flush :return: nothing """ - self.run(f"{IP} address flush dev {device}") + self.run_net(f"{IP} address flush dev {device}") def device_mac(self, device: str, mac: str) -> None: """ @@ -133,7 +135,7 @@ def device_mac(self, device: str, mac: str) -> None: :param mac: mac to set :return: nothing """ - self.run(f"{IP} link set dev {device} address {mac}") + self.run_net(f"{IP} link set dev {device} address {mac}") def delete_device(self, device: str) -> None: """ @@ -142,7 +144,7 @@ def delete_device(self, device: str) -> None: :param device: device to delete :return: nothing """ - self.run(f"{IP} link delete {device}") + self.run_net(f"{IP} link delete {device}") def delete_tc(self, device: str) -> None: """ @@ -151,7 +153,7 @@ def delete_tc(self, device: str) -> None: :param device: device to remove tc :return: nothing """ - self.run(f"{TC} qdisc delete dev {device} root") + self.run_net(f"{TC} qdisc delete dev {device} root") def checksums_off(self, iface_name: str) -> None: """ @@ -160,7 +162,7 @@ def checksums_off(self, iface_name: str) -> None: :param iface_name: interface to update :return: nothing """ - self.run(f"{ETHTOOL} -K {iface_name} rx off tx off") + self.run_net(f"{ETHTOOL} -K {iface_name} rx off tx off") def create_address(self, device: str, address: str, broadcast: str = None) -> None: """ @@ -172,9 +174,11 @@ def create_address(self, device: str, address: str, broadcast: str = None) -> No :return: nothing """ if broadcast is not None: - self.run(f"{IP} address add {address} broadcast {broadcast} dev {device}") + self.run_net( + f"{IP} address add {address} broadcast {broadcast} dev {device}" + ) else: - self.run(f"{IP} address add {address} dev {device}") + self.run_net(f"{IP} address add {address} dev {device}") if netaddr.valid_ipv6(address.split("/")[0]): # IPv6 addresses are removed by default on interface down. # Make sure that the IPv6 address we add is not removed @@ -189,7 +193,7 @@ def delete_address(self, device: str, address: str) -> None: :param address: address to remove :return: nothing """ - self.run(f"{IP} address delete {address} dev {device}") + self.run_net(f"{IP} address delete {address} dev {device}") def create_veth(self, name: str, peer: str) -> None: """ @@ -199,7 +203,7 @@ def create_veth(self, name: str, peer: str) -> None: :param peer: peer name :return: nothing """ - self.run(f"{IP} link add name {name} type veth peer name {peer}") + self.run_net(f"{IP} link add name {name} type veth peer name {peer}") def create_gretap( self, device: str, address: str, local: str, ttl: int, key: int @@ -221,7 +225,7 @@ def create_gretap( cmd += f" ttl {ttl}" if key is not None: cmd += f" key {key}" - self.run(cmd) + self.run_net(cmd) def create_bridge(self, name: str) -> None: """ @@ -230,11 +234,11 @@ def create_bridge(self, name: str) -> None: :param name: bridge name :return: nothing """ - self.run(f"{IP} link add name {name} type bridge") - self.run(f"{IP} link set {name} type bridge stp_state 0") - self.run(f"{IP} link set {name} type bridge forward_delay 0") - self.run(f"{IP} link set {name} type bridge mcast_snooping 0") - self.run(f"{IP} link set {name} type bridge group_fwd_mask 65528") + self.run_net(f"{IP} link add name {name} type bridge") + self.run_net(f"{IP} link set {name} type bridge stp_state 0") + self.run_net(f"{IP} link set {name} type bridge forward_delay 0") + self.run_net(f"{IP} link set {name} type bridge mcast_snooping 0") + self.run_net(f"{IP} link set {name} type bridge group_fwd_mask 65528") self.device_up(name) def delete_bridge(self, name: str) -> None: @@ -245,7 +249,7 @@ def delete_bridge(self, name: str) -> None: :return: nothing """ self.device_down(name) - self.run(f"{IP} link delete {name} type bridge") + self.run_net(f"{IP} link delete {name} type bridge") def set_iface_master(self, bridge_name: str, iface_name: str) -> None: """ @@ -255,7 +259,7 @@ def set_iface_master(self, bridge_name: str, iface_name: str) -> None: :param iface_name: interface name :return: nothing """ - self.run(f"{IP} link set dev {iface_name} master {bridge_name}") + self.run_net(f"{IP} link set dev {iface_name} master {bridge_name}") self.device_up(iface_name) def delete_iface(self, bridge_name: str, iface_name: str) -> None: @@ -266,7 +270,7 @@ def delete_iface(self, bridge_name: str, iface_name: str) -> None: :param iface_name: interface name :return: nothing """ - self.run(f"{IP} link set dev {iface_name} nomaster") + self.run_net(f"{IP} link set dev {iface_name} nomaster") def existing_bridges(self, _id: int) -> bool: """ @@ -275,7 +279,7 @@ def existing_bridges(self, _id: int) -> bool: :param _id: node id to check bridges for :return: True if there are existing bridges, False otherwise """ - output = self.run(f"{IP} -o link show type bridge") + output = self.run_net(f"{IP} -o link show type bridge") lines = output.split("\n") for line in lines: values = line.split(":") @@ -297,7 +301,7 @@ def set_mac_learning(self, name: str, value: int) -> None: :param value: ageing time value :return: nothing """ - self.run(f"{IP} link set {name} type bridge ageing_time {value}") + self.run_net(f"{IP} link set {name} type bridge ageing_time {value}") def set_mtu(self, name: str, value: int) -> None: """ @@ -307,7 +311,7 @@ def set_mtu(self, name: str, value: int) -> None: :param value: mtu value to set :return: nothing """ - self.run(f"{IP} link set {name} mtu {value}") + self.run_net(f"{IP} link set {name} mtu {value}") class OvsNetClient(LinuxNetClient): @@ -385,15 +389,20 @@ def set_mac_learning(self, name: str, value: int) -> None: self.run(f"{OVS_VSCTL} set bridge {name} other_config:mac-aging-time={value}") -def get_net_client(use_ovs: bool, run: Callable[..., str]) -> LinuxNetClient: +def get_net_client( + use_ovs: bool, run: Callable[..., str], run_net: Callable[..., str] = None +) -> LinuxNetClient: """ Retrieve desired net client for running network commands. :param use_ovs: True for OVS bridges, False for Linux bridges - :param run: function used to run net client commands + :param run: function to run commands within node context + :param run_net: function to run commands within network context only :return: net client class """ + if run_net is None: + run_net = run if use_ovs: - return OvsNetClient(run) + return OvsNetClient(run, run_net) else: - return LinuxNetClient(run) + return LinuxNetClient(run, run_net) diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index d2132143..04deb307 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -46,6 +46,7 @@ def test_xml_hooks(self, session: Session, tmpdir: TemporaryFile): session.shutdown() # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # verify nodes have been recreated @@ -96,6 +97,7 @@ def test_xml_ptp( assert len(session.link_manager.links()) == 0 # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # verify nodes have been recreated @@ -150,6 +152,7 @@ def test_xml_ptp_services( assert not session.get_node(node2.id, CoreNode) # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # retrieve custom service @@ -209,6 +212,7 @@ def test_xml_mobility( assert not session.get_node(node2.id, CoreNode) # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # retrieve configuration we set originally @@ -256,6 +260,7 @@ def test_network_to_network(self, session: Session, tmpdir: TemporaryFile): assert not session.get_node(switch2.id, SwitchNode) # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # verify nodes have been recreated @@ -312,6 +317,7 @@ def test_link_options( assert not session.get_node(switch.id, SwitchNode) # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # verify nodes have been recreated @@ -375,6 +381,7 @@ def test_link_options_ptp( assert not session.get_node(node2.id, CoreNode) # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # verify nodes have been recreated @@ -450,6 +457,7 @@ def test_link_options_bidirectional( assert not session.get_node(node2.id, CoreNode) # load saved xml + session.directory.mkdir() session.open_xml(file_path, start=True) # verify nodes have been recreated