diff --git a/munet/base.py b/munet/base.py index 01d7a00..d5f14b3 100644 --- a/munet/base.py +++ b/munet/base.py @@ -1611,6 +1611,7 @@ def __init__(self, *args, **kwargs): # logging.warning("InterfaceMixin: args: %s kwargs: %s", args, kwargs) self._intf_addrs = defaultdict(lambda: [None, None]) + self._peer_intf_addrs = defaultdict(lambda: [None, None]) self.net_intfs = {} self.next_intf_index = 0 self.basename = "eth" @@ -1630,9 +1631,16 @@ def get_intf_addr(self, ifname, ipv6=False): return None return self._intf_addrs[ifname][bool(ipv6)] - def set_intf_addr(self, ifname, ifaddr): + def get_peer_intf_addr(self, ifname, ipv6=False): + if ifname not in self._peer_intf_addrs: + return None + return self._peer_intf_addrs[ifname][bool(ipv6)] + + def set_intf_addr(self, ifname, ifaddr, peer_ifaddr=None): ifaddr = ipaddress.ip_interface(ifaddr) self._intf_addrs[ifname][ifaddr.version == 6] = ifaddr + if peer_ifaddr is not None: + self._peer_intf_addrs[ifname][peer_ifaddr.version == 6] = peer_ifaddr def net_addr(self, netname, ipv6=False): if netname not in self.net_intfs: @@ -2883,6 +2891,33 @@ def add_host(self, name, cls=LinuxNamespace, **kwargs): return self.hosts[name] + def add_dummy(self, node1, if1, mtu=None, **intf_constraints): + """Add a dummy for an interface with no link.""" + try: + name1 = node1.name + except AttributeError: + if node1 in self.switches: + node1 = self.switches[node1] + else: + node1 = self.hosts[node1] + name1 = node1.name + + lname = "{}:{}".format(name1, if1) + self.logger.debug("%s: add_dummy %s", self, lname) + lhost = self.hosts[name1] + + nsif1 = lhost.get_ns_ifname(if1) + lhost.cmd_raises_nsonly(f"ip link add name {nsif1} type dummy") + + if mtu: + lhost.cmd_raises_nsonly(f"ip link set {nsif1} mtu {mtu}") + lhost.cmd_raises_nsonly(f"ip link set {nsif1} up") + lhost.register_interface(if1) + + # Setup interface constraints if provided + if intf_constraints: + node1.set_intf_constraints(if1, **intf_constraints) + def add_link(self, node1, node2, if1, if2, mtu=None, **intf_constraints): """Add a link between switch and node or 2 nodes. diff --git a/munet/native.py b/munet/native.py index da85035..3d71d14 100644 --- a/munet/native.py +++ b/munet/native.py @@ -980,6 +980,31 @@ def get_ifname(self, netname): return c["name"] return None + def set_dummy_addr(self, cconf): + if ip := cconf.get("ip"): + ipaddr = ipaddress.ip_interface(ip) + assert ipaddr.version == 4 + else: + ipaddr = None + + if ip := cconf.get("ipv6"): + ip6addr = ipaddress.ip_interface(ip) + assert ip6addr.version == 6 + else: + ip6addr = None + + if "physical" in cconf or self.is_vm: + return + + ifname = cconf["name"] + for ip in (ipaddr, ip6addr): + if ip is None: + continue + self.set_intf_addr(ifname, ip) + ipcmd = "ip " if ip.version == 4 else "ip -6 " + self.logger.debug("%s: adding %s to unconnected intf %s", self, ip, ifname) + self.intf_ip_cmd(ifname, ipcmd + f"addr add {ip} dev {ifname}") + def set_lan_addr(self, switch, cconf): if ip := cconf.get("ip"): ipaddr = ipaddress.ip_interface(ip) @@ -1055,20 +1080,42 @@ def _set_p2p_addr(self, other, cconf, occonf, ipv6=False): return if ipaddr: + # Check if the two sides of this link are assigned + # different subnets. If so, set the peer address. + set_peer = False + if oipaddr and ipaddr.network != oipaddr.network: + set_peer = True ifname = cconf["name"] - self.set_intf_addr(ifname, ipaddr) + self.set_intf_addr(ifname, ipaddr, oipaddr) self.logger.debug("%s: adding %s to p2p intf %s", self, ipaddr, ifname) if "physical" not in cconf and not self.is_vm: - self.intf_ip_cmd(ifname, f"ip addr add {ipaddr} dev {ifname}") + if set_peer: + self.logger.debug("%s: setting peer address %s", self, oipaddr) + self.intf_ip_cmd( + ifname, + f"ip addr add {ipaddr.ip} peer {oipaddr.network} dev {ifname}", + ) + else: + self.intf_ip_cmd(ifname, f"ip addr add {ipaddr} dev {ifname}") if oipaddr: + set_peer = False + if ipaddr and ipaddr.network != oipaddr.network: + set_peer = True oifname = occonf["name"] - other.set_intf_addr(oifname, oipaddr) + other.set_intf_addr(oifname, oipaddr, ipaddr) self.logger.debug( "%s: adding %s to other p2p intf %s", other, oipaddr, oifname ) if "physical" not in occonf and not other.is_vm: - other.intf_ip_cmd(oifname, f"ip addr add {oipaddr} dev {oifname}") + if set_peer: + other.logger.debug("%s: setting peer address %s", other, ipaddr) + other.intf_ip_cmd( + oifname, + f"ip addr add {oipaddr.ip} peer {ipaddr.network} dev {oifname}", + ) + else: + other.intf_ip_cmd(oifname, f"ip addr add {oipaddr} dev {oifname}") def set_p2p_addr(self, other, cconf, occonf): self._set_p2p_addr(other, cconf, occonf, ipv6=False) @@ -2225,13 +2272,33 @@ async def renumber_interfaces(self): con.cmd_raises(f"ip -4 addr flush dev {ifname}") sw_is_nat = switch and hasattr(switch, "is_nat") and switch.is_nat if ifaddr := self.get_intf_addr(ifname, ipv6=False): - con.cmd_raises(f"ip addr add {ifaddr} dev {ifname}") + oifaddr = self.get_peer_intf_addr(ifname, ipv6=False) + if ( + not switch + and oifaddr is not None + and ifaddr.network != oifaddr.network + ): + con.cmd_raises( + f"ip addr add {ifaddr.ip} peer {oifaddr.network} dev {ifname}" + ) + else: + con.cmd_raises(f"ip addr add {ifaddr} dev {ifname}") if sw_is_nat: # In case there was some preconfig e.g., cloud-init con.cmd_raises("ip route flush exact default") con.cmd_raises(f"ip route add default via {switch.ip_address}") if ifaddr := self.get_intf_addr(ifname, ipv6=True): - con.cmd_raises(f"ip -6 addr add {ifaddr} dev {ifname}") + oifaddr = self.get_peer_intf_addr(ifname, ipv6=True) + if ( + not switch + and oifaddr is not None + and ifaddr.network != oifaddr.network + ): + con.cmd_raises( + f"ip addr add {ifaddr.ip} peer {oifaddr.network} dev {ifname}" + ) + else: + con.cmd_raises(f"ip -6 addr add {ifaddr} dev {ifname}") if sw_is_nat: # In case there was some preconfig e.g., cloud-init con.cmd_raises("ip -6 route flush exact default") @@ -3204,8 +3271,9 @@ async def _async_build(self, logger=None): if "connections" not in nconf: continue for cconf in nconf["connections"]: - # Eventually can add support for unconnected intf here. if "to" not in cconf: + # unconnected intf + await self.add_dummy_link(node, cconf) continue to = cconf["to"] if to in self.switches: @@ -3236,6 +3304,25 @@ def autonumber(self): def autonumber(self, value): self.topoconf["networks-autonumber"] = bool(value) + async def add_dummy_link(self, node1, c1=None): + c1 = {} if c1 is None else c1 + + if "name" not in c1: + c1["name"] = node1.get_next_intf_name() + if1 = c1["name"] + + do_add_dummy = True + if "hostintf" in c1: + await node1.add_host_intf(c1["hostintf"], c1["name"], mtu=c1.get("mtu")) + do_add_dummy = False + elif "physical" in c1: + await node1.add_phy_intf(c1["physical"], c1["name"]) + do_add_dummy = False + + if do_add_dummy: + super().add_dummy(node1, if1, **c1) + node1.set_dummy_addr(c1) + async def add_native_link(self, node1, node2, c1=None, c2=None): """Add a link between switch and node or 2 nodes.""" isp2p = False diff --git a/tests/config/p2p-addr/munet.yaml b/tests/config/p2p-addr/munet.yaml new file mode 100644 index 0000000..02c75ee --- /dev/null +++ b/tests/config/p2p-addr/munet.yaml @@ -0,0 +1,28 @@ +version: 1 +topology: + ipv6-enable: true + nodes: + - name: r1 + connections: + - to: r2 + name: eth0 + mtu: 4500 + ip: 172.16.0.1/32 + ipv6: 2001:db8::1/128 + cmd: | + ip addr show + ip -6 addr show + which ping + tail -f /dev/null + - name: r2 + connections: + - to: r1 + name: eth0 + mtu: 4500 + ip: 172.16.1.2/24 + ipv6: 2001:db8::1:1/112 + cmd: | + ip addr show + ip -6 addr show + which ping + tail -f /dev/null diff --git a/tests/config/p2p-addr/test_p2p_peer_addr.py b/tests/config/p2p-addr/test_p2p_peer_addr.py new file mode 100644 index 0000000..15aa5d3 --- /dev/null +++ b/tests/config/p2p-addr/test_p2p_peer_addr.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# SPDX-License-Identifier: GPL-2.0-or-later +# +# June 28 2023, Eric Kinzie +# +# Copyright 2023, LabN Consulting, L.L.C. +# +"Test p2p peer addresses" +import logging +import pytest + +# All tests are coroutines +pytestmark = pytest.mark.asyncio + + +async def test_peer_address(unet): + rc, o, e = await unet.hosts["r1"].async_cmd_status(f"ip addr show dev eth0") + assert rc == 0 + assert o.find("mtu 4500") > -1 + assert o.find("inet 172.16.0.1 peer 172.16.1.0/24") > -1 + assert o.find("inet6 2001:db8::1 peer 2001:db8::1:0/112") > -1 + + rc, o, e = await unet.hosts["r2"].async_cmd_status(f"ip addr show dev eth0") + assert rc == 0 + assert o.find("mtu 4500") > -1 + assert o.find("inet 172.16.1.2 peer 172.16.0.1/32") > -1 + assert o.find("inet6 2001:db8::1:1 peer 2001:db8::1/128") > -1 + + +async def test_peer_ping(unet): + r1eth0 = unet.hosts["r1"].get_intf_addr("eth0").ip + logging.debug("r1eth0 is %s", r1eth0) + o = await unet.hosts["r2"].async_cmd_raises(f"ping -w1 -c1 172.16.0.1") + logging.debug("ping r2 output: %s", o) diff --git a/tests/config/unconnected/munet.yaml b/tests/config/unconnected/munet.yaml new file mode 100644 index 0000000..90a4961 --- /dev/null +++ b/tests/config/unconnected/munet.yaml @@ -0,0 +1,31 @@ +version: 1 +topology: + networks-autonumber: true + ipv6-enable: true + networks: + - name: net0 + mtu: 5000 + nodes: + - name: r1 + connections: + - to: net0 + name: eth0 + mtu: 4500 + - name: unconnected + ip: 172.16.0.1/24 + mtu: 9000 + cmd: | + ip addr show + ip -6 addr show + which ping + tail -f /dev/null + - name: r2 + connections: + - to: "net0" + name: eth0 + mtu: 4500 + cmd: | + ip addr show + ip -6 addr show + which ping + tail -f /dev/null diff --git a/tests/config/unconnected/test_unconnected.py b/tests/config/unconnected/test_unconnected.py new file mode 100644 index 0000000..8a0d7fd --- /dev/null +++ b/tests/config/unconnected/test_unconnected.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# SPDX-License-Identifier: GPL-2.0-or-later +# +# June 28 2023, Eric Kinzie +# +# Copyright 2023, LabN Consulting, L.L.C. +# +"Testing of unconnected interface." +import logging + +import pytest + +# All tests are coroutines +pytestmark = pytest.mark.asyncio + + +async def test_unconnected_presence(unet): + rc, o, e = await unet.hosts["r1"].async_cmd_status(f"ip addr show dev unconnected") + assert rc == 0 + assert o.find("mtu 9000") > -1 + assert o.find("inet 172.16.0.1/24") > -1 + + +async def test_basic_ping(unet): + r1eth0 = unet.hosts["r1"].get_intf_addr("eth0").ip + logging.debug("r1eth0 is %s", r1eth0) + rc, o, e = await unet.hosts["r2"].async_cmd_status( + f"ip ro add 172.16.0.0/24 via {r1eth0}" + ) + assert rc == 0 + o = await unet.hosts["r2"].async_cmd_raises(f"ping -w1 -c1 172.16.0.1") + logging.debug("ping r2 output: %s", o)