diff --git a/board/common/rootfs/usr/libexec/infix/hostname b/board/common/rootfs/usr/libexec/infix/hostname new file mode 100755 index 000000000..bfd1952b3 --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/hostname @@ -0,0 +1,74 @@ +#!/bin/sh +# Deterministically set system hostname from /etc/hostname.d/ +# +# Highest numbered file wins (lexicographic sort, 90-dhcp > 50-configured > 10-default) +# +# Priority scheme: +# 10-default - Bootstrap/factory default (%h-%m format) +# 50-configured - From confd /system/hostname +# 90-dhcp- - From DHCP clietn on interface (highest priority) + +HOSTNAME_D="/etc/hostname.d" + +# Ensure directory exists +mkdir -p "$HOSTNAME_D" + +# Find the highest priority file (reverse sort, take first) +hostname_file=$(ls -1 "$HOSTNAME_D" 2>/dev/null | sort -r | head -1) + +if [ -z "$hostname_file" ]; then + logger -it confd "No hostname sources found in $HOSTNAME_D" + exit 1 +fi + +# Read hostname from the file (first line only, strip whitespace) +new_hostname=$(cat "$HOSTNAME_D/$hostname_file" | head -1 | tr -d '\n\r\t ') +if [ -z "$new_hostname" ]; then + logger -it confd "Empty hostname in $hostname_file" + exit 1 +fi + +if [ ${#new_hostname} -gt 64 ]; then + logger -it confd "Hostname too long (${#new_hostname} > 64) in $hostname_file" + exit 1 +fi + +# Check if hostname has actually changed +current_hostname=$(hostname) +if [ "$new_hostname" = "$current_hostname" ]; then + # No change needed, exit silently + exit 0 +fi + +# Set the hostname +logger -it confd "Setting hostname to '$new_hostname' from $hostname_file" +hostname "$new_hostname" + +# Update /etc/hostname (for persistence across reboots) +echo "$new_hostname" > /etc/hostname + +# Update /etc/hosts (127.0.1.1 entry for proper name resolution) +if grep -q "^127\.0\.1\.1" /etc/hosts; then + sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$new_hostname/" /etc/hosts +else + # Add entry if it doesn't exist + echo "127.0.1.1 $new_hostname" >> /etc/hosts +fi + +# Notify services of hostname change, skip while in bootstrap +initctl -nbq touch sysklogd +if ! runlevel >/dev/null 2>&1; then + exit 0 +fi + +initctl -bq status lldpd && lldpcli configure system hostname "$new_hostname" 2>/dev/null +initctl -bq status mdns && avahi-set-host-name "$new_hostname" 2>/dev/null +initctl -bq touch netbrowse 2>/dev/null + +# If called from dhcp script we need to reload to activate new name in syslogd +# Otherwise we're called from confd, which does the reload when all is done. +if [ -n "$1" ]; then + initctl -b reload +fi + +exit 0 diff --git a/board/common/rootfs/usr/libexec/infix/init.d/05-hostname b/board/common/rootfs/usr/libexec/infix/init.d/05-hostname new file mode 100755 index 000000000..2984ea3cc --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/init.d/05-hostname @@ -0,0 +1,18 @@ +#!/bin/sh +# Initialize default hostname for hostname.d pattern +# This runs very early in boot to set up the default hostname entry + +HOSTNAME_D="/etc/hostname.d" + +# Ensure directory exists +mkdir -p "$HOSTNAME_D" + +# If no default exists yet, create it from /etc/hostname (from squashfs) +if [ ! -f "$HOSTNAME_D/10-default" ] && [ -f /etc/hostname ]; then + cp /etc/hostname "$HOSTNAME_D/10-default" +fi + +# Apply hostname using the deterministic helper +if [ -x /usr/libexec/infix/hostname ]; then + /usr/libexec/infix/hostname +fi diff --git a/board/common/rootfs/usr/share/udhcpc/default.script b/board/common/rootfs/usr/share/udhcpc/default.script index 4ebb3ac58..1ea929a93 100755 --- a/board/common/rootfs/usr/share/udhcpc/default.script +++ b/board/common/rootfs/usr/share/udhcpc/default.script @@ -50,6 +50,26 @@ wait_for_ipv6_default_route() err "Timed out waiting for IPv6 default route!" } +# Check if a DHCP option was requested in the parameter request list +# Returns: 0 if requested, 1 if not requested or config unavailable +was_option_requested() +{ + local opt_num="$1" + local config="/etc/finit.d/available/dhcp-client-${interface}.conf" + + if [ ! -f "$config" ]; then + dbg "config file not found: $config" + return 1 + fi + + # Extract udhcpc command line and check for -O + if grep -q -- "-O ${opt_num}\b" "$config"; then + return 0 + fi + + return 1 +} + # RFC3442: If the DHCP server returns both a Classless # Static Routes option and a Router option, the DHCP # client MUST ignore the Router option. @@ -57,17 +77,25 @@ set_dhcp_routes() { echo "! Generated by udhcpc" > "$NEXT" if [ -n "$staticroutes" ]; then - # format: dest1/mask gw1 ... destn/mask gwn - set -- $staticroutes - while [ -n "$1" -a -n "$2" ]; do - dbg "adding route $1 via $2 metric $metric tag 100" - echo "ip route $1 $2 $metric tag 100" >> "$NEXT" - shift 2 - done + if was_option_requested 121; then + # format: dest1/mask gw1 ... destn/mask gwn + set -- $staticroutes + while [ -n "$1" -a -n "$2" ]; do + dbg "adding route $1 via $2 metric $metric tag 100" + echo "ip route $1 $2 $metric tag 100" >> "$NEXT" + shift 2 + done + else + log "ignoring unrequested staticroutes (option 121)" + fi elif [ -n "$router" ] ; then - for i in $router ; do - echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT" - done + if was_option_requested 3; then + for i in $router ; do + echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT" + done + else + log "ignoring unrequested router (option 3)" + fi fi # Reduce changes needed by comparing with previous route(s) @@ -109,6 +137,11 @@ case "$ACTION" in # drop info from this interface rm -f "$RESOLV_CONF" rm -f "$NTPFILE" + if [ -f "/etc/hostname.d/90-dhcp-${interface}" ]; then + log "removing /etc/hostname.d/90-dhcp-${interface}" + rm -f "/etc/hostname.d/90-dhcp-${interface}" + /usr/libexec/infix/hostname dhcp + fi if [ -x /usr/sbin/avahi-autoipd ]; then /usr/sbin/avahi-autoipd -c $interface && /usr/sbin/avahi-autoipd -k $interface fi @@ -134,21 +167,35 @@ case "$ACTION" in set_dhcp_routes - # set hostname if given + # set hostname if given and requested if [ -n "$hostname" ]; then - log "setting new hostname: $hostname" - hostname "$hostname" - sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$hostname/" /etc/hosts + if was_option_requested 12; then + log "received DHCP hostname: $hostname" + mkdir -p /etc/hostname.d + echo "$hostname" > "/etc/hostname.d/90-dhcp-${interface}" + /usr/libexec/infix/hostname dhcp + else + log "ignoring unrequested hostname (option 12): $hostname" + fi fi # drop info from this interface truncate -s 0 "$RESOLV_CONF" # prefer rfc3397 domain search list (option 119) if available + search_list="" if [ -n "$search" ]; then - search_list=$search + if was_option_requested 119; then + search_list=$search + else + log "ignoring unrequested search (option 119): $search" + fi elif [ -n "$domain" ]; then - search_list=$domain + if was_option_requested 15; then + search_list=$domain + else + log "ignoring unrequested domain (option 15): $domain" + fi fi if [ -n "$search_list" ]; then @@ -156,19 +203,29 @@ case "$ACTION" in echo "search $search_list # $interface" >> $RESOLV_CONF fi - for i in $dns ; do - dbg "adding dns $i" - echo "nameserver $i # $interface" >> $RESOLV_CONF - resolvconf -u - done + if [ -n "$dns" ]; then + if was_option_requested 6; then + for i in $dns ; do + dbg "adding dns $i" + echo "nameserver $i # $interface" >> $RESOLV_CONF + resolvconf -u + done + else + log "ignoring unrequested dns (option 6): $dns" + fi + fi if [ -n "$ntpsrv" ]; then - truncate -s 0 "$NTPFILE" - for srv in $ntpsrv; do - dbg "got NTP server $srv" - echo "server $srv iburst" >> "$NTPFILE" - done - chronyc reload sources >/dev/null + if was_option_requested 42; then + truncate -s 0 "$NTPFILE" + for srv in $ntpsrv; do + dbg "got NTP server $srv" + echo "server $srv iburst" >> "$NTPFILE" + done + chronyc reload sources >/dev/null + else + log "ignoring unrequested ntpsrv (option 42): $ntpsrv" + fi fi esac diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 497268489..ed47340cb 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -48,11 +48,13 @@ All notable changes to the project are documented in this file. ### Fixes - Fix #855: User admin sometimes fails to be added to `wheel` group +- Fix #1112: setting hostname via DHCP client sometimes gets overridden by the + configured system hostname - Fix #1247: Prevent invalid configuration of OSPF backbone area (0.0.0.0) as stub or NSSA. The backbone must always be a normal area per RFC 2328. Any existing invalid configurations are automatically corrected during upgrade -- Fix serious regression in boot time, introduced in v25.10, delays the - boot step "Mounting filesystems ..." with up to 30 seconds! +- Fix #1255: serious regression in boot time, introduced in v25.10, delays the + boot step "Mounting filesystems ...", from 30 seconds up to five minutes! - Fix broken intra-document links in container and tunnel documentation [lastest-boot]: https://github.com/kernelkit/infix/releases/latest-boot diff --git a/src/confd/src/ietf-system.c b/src/confd/src/ietf-system.c index 1a6b7eb57..eedd9427a 100644 --- a/src/confd/src/ietf-system.c +++ b/src/confd/src/ietf-system.c @@ -206,11 +206,6 @@ static int rpc_set_datetime(sr_session_ctx_t *session, uint32_t sub_id, return rc; } -static int sys_reload_services(void) -{ - return systemf("initctl -nbq touch sysklogd"); -} - #define TIMEZONE_CONF "/etc/timezone" #define TIMEZONE_PREV TIMEZONE_CONF "-" @@ -1558,11 +1553,10 @@ int hostnamefmt(struct confd *confd, const char *fmt, char *hostnm, size_t hostl static int change_hostname(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { - const char *hostip = "127.0.1.1"; char hostnm[65], domain[65]; - char buf[256], *fmt; - FILE *nfp, *fp; - int err, fd; + int rc = SR_ERR_OK; + char *fmt; + FILE *fp; if (event != SR_EV_DONE || !lydx_get_xpathf(diff, XPATH_HOSTNAME_)) return SR_ERR_OK; @@ -1572,82 +1566,36 @@ static int change_hostname(sr_session_ctx_t *session, struct lyd_node *config, s fmt = strdup(nm); if (hostnamefmt(confd, fmt, hostnm, sizeof(hostnm), domain, sizeof(domain))) { - err = SR_ERR_SYS; - goto err; - } - - err = sethostname(hostnm, strlen(hostnm)); - if (err) { - ERROR("failed setting hostname"); - err = SR_ERR_SYS; - goto err; + rc = SR_ERR_SYS; + goto failed; } - if (domain[0] && setdomainname(domain, strlen(domain))) { - ERROR("failed setting domain name"); - /* Not cause for failing this function */ - } + /* Use hostname.d for deterministic hostname management */ + systemf("mkdir -p /etc/hostname.d"); - fp = fopen(_PATH_HOSTNAME, "w"); - if (!fp) { - err = SR_ERR_INTERNAL; - goto err; - } + fp = fopen("/etc/hostname.d/50-configured", "w"); + if (!fp) + goto failed; fprintf(fp, "%s\n", hostnm); fclose(fp); - nfp = fopen(_PATH_HOSTS "+", "w"); - if (!nfp) { - err = SR_ERR_INTERNAL; - goto err; - } - fd = fileno(nfp); - if (fd == -1 || fchown(fd, 0, 0) || fchmod(fd, 0644)) { - fclose(nfp); - goto err; - } - - fp = fopen(_PATH_HOSTS, "r"); - if (!fp) { - err = SR_ERR_INTERNAL; - fclose(nfp); - goto err; + /* Handle domain name if present */ + if (domain[0] && setdomainname(domain, strlen(domain))) { + ERROR("failed setting domain name"); + /* Not cause for failing this function */ } - while (fgets(buf, sizeof(buf), fp)) { - if (!strncmp(buf, hostip, strlen(hostip))) { - if (domain[0]) - snprintf(buf, sizeof(buf), "%s\t%s.%s %s\n", hostip, hostnm, domain, hostnm); - else - snprintf(buf, sizeof(buf), "%s\t%s\n", hostip, hostnm); - } - fputs(buf, nfp); + if (systemf("/usr/libexec/infix/hostname")) { + failed: + ERROR("failed setting hostname"); + rc = SR_ERR_SYS; } - fclose(fp); - fclose(nfp); - if (rename(_PATH_HOSTS "+", _PATH_HOSTS)) - ERRNO("Failed activating changes to "_PATH_HOSTS); - - /* skip in bootstrap, lldpd and avahi have not started yet */ - if (systemf("runlevel >/dev/null 2>&1")) - goto err; - - /* Inform any running lldpd and avahi of the change ... */ - systemf("initctl -bq status lldpd && lldpcli configure system hostname %s", hostnm); - systemf("initctl -bq status mdns && avahi-set-host-name %s", hostnm); - systemf("initctl -bq touch netbrowse"); -err: if (fmt) free(fmt); - - if (err) { - ERROR("Failed activating changes."); - return err; - } - if (sys_reload_services()) - return SR_ERR_SYS; + if (rc) + return rc; return SR_ERR_OK; } @@ -1656,6 +1604,7 @@ static int change_hostname(sr_session_ctx_t *session, struct lyd_node *config, s int ietf_system_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { int rc = SR_ERR_OK; + if ((rc = change_auth(session, config, diff, event, confd))) return rc; if ((rc = change_ntp(session, config, diff, event, confd))) diff --git a/test/case/infix_dhcp/client_hostname/Readme.adoc b/test/case/infix_dhcp/client_hostname/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/infix_dhcp/client_hostname/test.adoc b/test/case/infix_dhcp/client_hostname/test.adoc new file mode 100644 index 000000000..55756a6c9 --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/test.adoc @@ -0,0 +1,26 @@ +=== DHCP Hostname Priority + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_dhcp/client_hostname] + +==== Description + +Verify deterministic hostname management: a DHCP acquired hostname takes +precedence over a configured hostname. When a DHCP lease ends, or the +hostname option is removed, the system should revert to the configured +hostname. + +==== Topology + +image::topology.svg[DHCP Hostname Priority topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure static system hostname +. Verify configured hostname is set +. Enable DHCP client requesting hostname option +. Verify DHCP hostname takes precedence +. Drop hostname option from client request +. Verify hostname reverts to configured value + + diff --git a/test/case/infix_dhcp/client_hostname/test.py b/test/case/infix_dhcp/client_hostname/test.py new file mode 100755 index 000000000..003b15132 --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/test.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""DHCP Hostname Priority + +Verify deterministic hostname management: a DHCP acquired hostname takes +precedence over a configured hostname. When a DHCP lease ends, or the +hostname option is removed, the system should revert to the configured +hostname. + +""" + +import infamy, infamy.dhcp +from infamy.util import until + + +def verify_hostname(node, expected): + """Verify operational hostname matches expected value""" + data = node.get_data("/ietf-system:system") + return data["system"]["hostname"] == expected + + +with infamy.Test() as test: + DHCP_HOSTNAME = "dhcp-assigned" + CONF_HOSTNAME = "configured-host" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + client = env.attach("client", "mgmt") + _, host = env.ltop.xlate("host", "mgmt") + _, port = env.ltop.xlate("client", "mgmt") + + with test.step("Configure static system hostname"): + client.put_config_dict("ietf-system", { + "system": { + "hostname": CONF_HOSTNAME + } + }) + + with test.step("Verify configured hostname is set"): + until(lambda: verify_hostname(client, CONF_HOSTNAME)) + + with infamy.IsolatedMacVlan(host, mode="private") as netns: + netns.addip("10.0.0.1") + with infamy.dhcp.Server(netns, ip="10.0.0.42", hostname=DHCP_HOSTNAME): + with test.step("Enable DHCP client requesting hostname option"): + client.put_config_dict("ietf-interfaces", { + "interfaces": { + "interface": [{ + "name": port, + "ipv4": { + "infix-dhcp-client:dhcp": { + "option": [ + {"id": "hostname"}, + {"id": "netmask"}, + {"id": "router"} + ] + } + } + }] + } + }) + + with test.step("Verify DHCP hostname takes precedence"): + until(lambda: verify_hostname(client, DHCP_HOSTNAME)) + + with test.step("Drop hostname option from client request"): + path = f"/ietf-interfaces:interfaces/interface[name='{port}']" \ + + "/ietf-ip:ipv4/infix-dhcp-client:dhcp/option[id='hostname']" + client.delete_xpath(path) + + with test.step("Verify hostname reverts to configured value"): + until(lambda: verify_hostname(client, CONF_HOSTNAME)) + + test.succeed() diff --git a/test/case/infix_dhcp/client_hostname/topology.dot b/test/case/infix_dhcp/client_hostname/topology.dot new file mode 100644 index 000000000..d2b1b2bcb --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/topology.dot @@ -0,0 +1,22 @@ +graph "1x1" { + layout="neato"; + overlap="false"; + esep="+100"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt }", + pos="0,20!", + requires="controller", + ]; + + client [ + label="{ mgmt } | client", + pos="200,20!", + requires="infix", + ]; + + host:mgmt -- client:mgmt [requires="mgmt", color=lightgrey] +} diff --git a/test/case/infix_dhcp/client_hostname/topology.svg b/test/case/infix_dhcp/client_hostname/topology.svg new file mode 100644 index 000000000..20a632e9f --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/topology.svg @@ -0,0 +1,33 @@ + + + + + + +1x1 + + + +host + +host + +mgmt + + + +client + +mgmt + +client + + + +host:mgmt--client:mgmt + + + + diff --git a/test/case/infix_dhcp/dhcp_client.yaml b/test/case/infix_dhcp/dhcp_client.yaml index d2244bad0..98bbe3a77 100644 --- a/test/case/infix_dhcp/dhcp_client.yaml +++ b/test/case/infix_dhcp/dhcp_client.yaml @@ -10,3 +10,6 @@ - name: DHCP option 121 vs option 3 case: client_routes/test.py + +- name: DHCP Hostname Priority + case: client_hostname/test.py diff --git a/test/env b/test/env index b5e651a5d..b89e21ea1 100755 --- a/test/env +++ b/test/env @@ -228,6 +228,7 @@ if [ "$containerize" ]; then --cap-add=NET_ADMIN \ --device=/dev/net/tun \ --env PYTHONHASHSEED=${PYTHONHASHSEED:-$(shuf -i 0-$(((1 << 32) - 1)) -n 1)} \ + --env PYTHONPYCACHEDIR=/tmp/__pycache__ \ --env VIRTUAL_ENV_DISABLE_PROMPT=yes \ --env INFAMY_ARGS="$INFAMY_ARGS" \ --env INFAMY_EXTRA_ARGS="$INFAMY_EXTRA_ARGS" \ diff --git a/test/infamy/dhcp.py b/test/infamy/dhcp.py index 7edaa41f8..62731e3c5 100644 --- a/test/infamy/dhcp.py +++ b/test/infamy/dhcp.py @@ -5,11 +5,13 @@ class Server: config_file = '/tmp/udhcpd.conf' leases_file = '/tmp/udhcpd.leases' - def __init__(self, netns, start='192.168.0.100', end='192.168.0.110', netmask='255.255.255.0', ip=None, router=None, prefix=None, iface="iface"): + def __init__(self, netns, start='192.168.0.100', end='192.168.0.110', + netmask='255.255.255.0', ip=None, router=None, prefix=None, + hostname=None, iface="iface"): self.process = None self.netns = netns self.iface = iface - self._create_files(start, end, netmask, ip, router, prefix) + self._create_files(start, end, netmask, ip, router, prefix, hostname) def __del__(self): #print(self.config_file) @@ -23,7 +25,7 @@ def __enter__(self): def __exit__(self, _, __, ___): self.stop() - def _create_files(self, start, end, netmask, ip, router, prefix): + def _create_files(self, start, end, netmask, ip, router, prefix, hostname): f = open(self.leases_file, "w") f.close() @@ -44,6 +46,8 @@ def _create_files(self, start, end, netmask, ip, router, prefix): f.write(f"option router {router}\n") if prefix and router: f.write(f"option staticroutes {prefix} {router}\n") + if hostname: + f.write(f"option hostname {hostname}\n") def get_pid(self): return self.process.pid diff --git a/test/infamy/netns.py b/test/infamy/netns.py index 317e006db..8ac0414dc 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -28,6 +28,17 @@ class IsolatedMacVlans: NOTE: For the simple case when only one interface needs to be mapped, see IsolatedMacVlan below. + Args: + ifmap: Dictionary mapping parent interface names to MACVLAN names + lo: Enable loopback interface in the namespace (default: True) + set_up: Automatically bring up the interfaces (default: True) + mode: MACVLAN mode to use (default: "passthru") + - "passthru": Exclusive access, single MACVLAN per parent. + Parent interface becomes promiscuous. + - "bridge": Shared access, allows multiple MACVLANs to + communicate. Required for layer-2 tests that + need full control of all frames. + Example: netns = IsolatedMacVlans({ "eth2": "a", "eth3": "b" }) @@ -46,9 +57,10 @@ def Cleanup(): for ns in list(IsolatedMacVlans.Instances): ns.stop() - def __init__(self, ifmap, lo=True, set_up=True): + def __init__(self, ifmap, lo=True, set_up=True, mode="passthru"): self.sleeper = None self.ifmap, self.lo, self.set_up = ifmap, lo, set_up + self.mode = mode self.ping_timeout = env.ENV.attr("ping_timeout", 5) def start(self): @@ -64,7 +76,8 @@ def start(self): "link", parent, "address", self._stable_mac(parent), "netns", str(self.sleeper.pid), - "type", "macvlan", "mode", "passthru"], check=True) + "type", "macvlan", "mode", self.mode], + check=True) self.runsh(f""" while ! ip link show dev {ifname}; do sleep 0.1 @@ -287,6 +300,17 @@ class IsolatedMacVlan(IsolatedMacVlans): moves that interface to a separate namespace, isolating it from all other interfaces. + Args: + parent: Name of the parent interface on the controller + ifname: Name of the MACVLAN interface in the namespace (default: "iface") + lo: Enable loopback interface in the namespace (default: True) + set_up: Automatically bring up the interface (default: True) + mode: MACVLAN mode to use (default: "passthru") + - "passthru": Exclusive access, single MACVLAN per parent. + - "bridge": Shared access, required for layer-2 tests that + need to communicate with other MACVLANs on the + same parent or control all frames. + Example: netns = IsolatedMacVlan("eth3") @@ -298,13 +322,19 @@ class IsolatedMacVlan(IsolatedMacVlans): | eth0 eth1 eth2 eth3 + Example with bridge mode: + + netns = IsolatedMacVlan("eth3", mode="bridge") + """ - def __init__(self, parent, ifname="iface", lo=True, set_up=True): + def __init__(self, parent, ifname="iface", lo=True, set_up=True, mode="passthru"): self._ifname = ifname - return super().__init__(ifmap={ parent: ifname }, lo=lo, set_up=set_up) + return super().__init__(ifmap={parent: ifname}, lo=lo, set_up=set_up, + mode=mode) def addip(self, addr, prefix_length=24, proto="ipv4"): - return super().addip(ifname=self._ifname, addr=addr, prefix_length=prefix_length, proto=proto) + return super().addip(ifname=self._ifname, addr=addr, + prefix_length=prefix_length, proto=proto) def must_receive(self, expr, timeout=None, ifname=None, must=True): ifname = ifname if ifname else self._ifname @@ -387,10 +417,19 @@ class TPMR(IsolatedMacVlans): This is useful to verify the correctness of fail-over behavior in various protocols. See ospf_bfd for a usage example. + + Args: + a: Name of the first parent interface on the controller + b: Name of the second parent interface on the controller + mode: MACVLAN mode to use (default: "passthru") + - "passthru": Exclusive access (default) + - "bridge": Shared access, allows communication between MACVLANs + and full control of all frames. May be required for + proper layer-2 relay functionality in some tests. """ - def __init__(self, a, b): - super().__init__(ifmap={ a: "a", b: "b" }, lo=False) + def __init__(self, a, b, mode="passthru"): + super().__init__(ifmap={ a: "a", b: "b" }, lo=False, mode=mode) def start(self, forward=True): ret = super().start()