diff --git a/.github/workflows/trigger.yml b/.github/workflows/trigger.yml index c0db755ff..ed1f274da 100644 --- a/.github/workflows/trigger.yml +++ b/.github/workflows/trigger.yml @@ -9,6 +9,10 @@ on: - ci-work workflow_dispatch: +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build-x86_64: if: startsWith(github.repository, 'kernelkit/') diff --git a/board/common/rootfs/usr/bin/show-legacy b/board/common/rootfs/usr/bin/show-legacy old mode 100644 new mode 100755 diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index acb7e7b80..9a911897a 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -3,7 +3,7 @@ Change Log All notable changes to the project are documented in this file. -[v25.08.1][] - 2025-10-01 +[v25.08.1][] - 2025-09-30 ------------------------- ### Changes @@ -12,6 +12,8 @@ All notable changes to the project are documented in this file. ### Fixes - Fix #1150: `show-legacy` wrapper permissions - Fix #1155: `show ospf` commands regression +- Fix #1169: Expected neighbors not shown in sysrepocfg +- Various test stability improvments [v25.08.0][] - 2025-09-01 ------------------------- diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index 1ce5ff1a4..8da12d07a 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -338,17 +338,17 @@ if [ -z "$KLISH_PARAM_name" ]; then - doas vtysh -c "show-legacy ip ospf" |pager + doas vtysh -c "show ip ospf" |pager elif [ "$KLISH_PARAM_name" == "neighbor" ];then - doas vtysh -c "show-legacy ip ospf neighbor" |pager + doas vtysh -c "show ip ospf neighbor" |pager elif [ "$KLISH_PARAM_name" == "interfaces" ];then - doas vtysh -c "show-legacy ip ospf interface" |pager + doas vtysh -c "show ip ospf interface" |pager elif [ "$KLISH_PARAM_name" == "routes" ];then - doas vtysh -c "show-legacy ip ospf route" |pager + doas vtysh -c "show ip ospf route" |pager elif [ "$KLISH_PARAM_name" == "database" ];then - doas vtysh -c "show-legacy ip ospf database" |pager + doas vtysh -c "show ip ospf database" |pager elif [ "$KLISH_PARAM_name" == "bfd" ];then - doas vtysh -c "show-legacy bfd peers" |pager + doas vtysh -c "show bfd peers" |pager fi diff --git a/src/statd/python/ospf_status/ospf_status.py b/src/statd/python/ospf_status/ospf_status.py index 09bcff842..ecc89217e 100755 --- a/src/statd/python/ospf_status/ospf_status.py +++ b/src/statd/python/ospf_status/ospf_status.py @@ -40,6 +40,11 @@ def main(): for ifname, iface in interfaces["interfaces"].items(): iface["name"] = ifname iface["neighbors"] = [] + + # Skip interfaces that don't have OSPF enabled or area configured + if not iface.get("ospfEnabled", False) or not iface.get("area"): + continue + for area_id in ospf["areas"]: area_type="" diff --git a/test/case/all.yaml b/test/case/all.yaml index 22cbecec0..74174a306 100644 --- a/test/case/all.yaml +++ b/test/case/all.yaml @@ -6,12 +6,26 @@ infamy: specification: False +- case: meta/bootorder.py + infamy: + specification: False + +- case: meta/check-version.py + infamy: + specification: False + + - name: Misc tests suite: misc/misc.yaml - name: ietf-system suite: ietf_system/ietf_system.yaml +# Upgrade may leave wrong boot order +- case: meta/bootorder.py + infamy: + specification: False + - name: ietf-syslog suite: ietf_syslog/ietf_syslog.yaml diff --git a/test/case/infix_containers/container_environment/test.py b/test/case/infix_containers/container_environment/test.py index f571fcfbe..eb2d5189c 100755 --- a/test/case/infix_containers/container_environment/test.py +++ b/test/case/infix_containers/container_environment/test.py @@ -3,27 +3,32 @@ Container environment variables Verify that environment variables can be set in container configuration -and are available inside the running container. Tests the 'env' list -functionality by: +and are available inside the running container. -1. Creating a container with multiple environment variables -2. Using a custom script to extract env vars and serve them via HTTP -3. Fetching the served content to verify environment variables are set correctly - -Uses the nftables container image with custom rc.local script. +1 Set up a container config with multiple environment variables +2. Serve variables back to host using a CGI script in container +3. Verify served content against environment variables """ import infamy from infamy.util import until, to_binary with infamy.Test() as test: + ENV_VARS = [ + {"key": "TEST_VAR", "value": "hello-world"}, + {"key": "APP_PORT", "value": "8080"}, + {"key": "DEBUG_MODE", "value": "true"}, + {"key": "PATH_WITH_SPACES", "value": "/path with spaces/test"} + ] NAME = "web-env" DUTIP = "10.0.0.2" OURIP = "10.0.0.1" + url = infamy.Furl(f"http://{DUTIP}:8080/cgi-bin/env.cgi") with test.step("Set up topology and attach to target DUT"): env = infamy.Env() target = env.attach("target", "mgmt") + _, hport = env.ltop.xlate("host", "data") if not target.has_model("infix-containers"): test.skip() @@ -44,44 +49,33 @@ } }) - with test.step("Create container with environment variables"): - script = to_binary("""#!/bin/sh -# Create HTTP response with environment variables -printf "HTTP/1.1 200 OK\\r\\n" > /var/www/response.txt -printf "Content-Type: text/plain\\r\\n" >> /var/www/response.txt -printf "Connection: close\\r\\n\\r\\n" >> /var/www/response.txt - -# Add environment variables using printf to control encoding -printf "TEST_VAR=\\"%s\\"\\n" "$TEST_VAR" >> /var/www/response.txt -printf "APP_PORT=%s\\n" "$APP_PORT" >> /var/www/response.txt -printf "DEBUG_MODE=\\"%s\\"\\n" "$DEBUG_MODE" >> /var/www/response.txt -printf "PATH_WITH_SPACES=\\"%s\\"\\n" "$PATH_WITH_SPACES" >> /var/www/response.txt + with test.step("Set up container with environment variables"): + cgi = [ + '#!/bin/sh', + '# CGI script to output environment variables', + 'echo "Content-Type: text/plain"', + 'echo ""' + ] -while true; do - nc -l -p 8080 < /var/www/response.txt 2>>/var/www/debug.log || sleep 1 -done -""") + for var in ENV_VARS: + cgi.append(f'echo "{var["key"]}=${var["key"]}"') target.put_config_dict("infix-containers", { "containers": { "container": [ { "name": f"{NAME}", - "image": f"oci-archive:{infamy.Container.NFTABLES_IMAGE}", - "env": [ - {"key": "TEST_VAR", "value": "hello-world"}, - {"key": "APP_PORT", "value": "8080"}, - {"key": "DEBUG_MODE", "value": "true"}, - {"key": "PATH_WITH_SPACES", "value": "/path with spaces/test"} - ], + "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", + "command": "/usr/sbin/httpd -f -v -p 8080", + "env": ENV_VARS, "network": { "host": True }, "mount": [ { - "name": "rc.local", - "content": script, - "target": "/etc/rc.local", + "name": "env.cgi", + "content": to_binary('\n'.join(cgi) + '\n'), + "target": "/var/www/cgi-bin/env.cgi", "mode": "0755" } ], @@ -98,20 +92,17 @@ c = infamy.Container(target) until(lambda: c.running(NAME), attempts=60) - with test.step("Verify environment variables are available via HTTP"): - _, hport = env.ltop.xlate("host", "data") - url = infamy.Furl(f"http://{DUTIP}:8080/env.html") + with infamy.IsolatedMacVlan(hport) as ns: + ns.addip(OURIP) - with infamy.IsolatedMacVlan(hport) as ns: - ns.addip(OURIP) + with test.step("Verify basic connectivity to data interface"): + ns.must_reach(DUTIP) - with test.step("Verify basic connectivity to data interface"): - ns.must_reach(DUTIP) + with test.step("Verify environment variables in CGI response"): + expected_strings = [] + for var in ENV_VARS: + expected_strings.append(f'{var["key"]}={var["value"]}') - with test.step("Verify environment variables in HTTP response"): - until(lambda: url.nscheck(ns, "TEST_VAR=\"hello-world\""), attempts=10) - until(lambda: url.nscheck(ns, "APP_PORT=8080"), attempts=10) - until(lambda: url.nscheck(ns, "DEBUG_MODE=\"true\""), attempts=10) - until(lambda: url.nscheck(ns, "PATH_WITH_SPACES=\"/path with spaces/test\""), attempts=10) + until(lambda: url.nscheck(ns, expected_strings)) test.succeed() diff --git a/test/case/infix_containers/infix_containers.yaml b/test/case/infix_containers/infix_containers.yaml index 83d07fd74..b3f65e959 100644 --- a/test/case/infix_containers/infix_containers.yaml +++ b/test/case/infix_containers/infix_containers.yaml @@ -6,9 +6,8 @@ - name: container_enabled case: container_enabled/test.py -# Disabled for v25.08, new/unstable -# - name: container_environment -# case: container_environment/test.py +- name: container_environment + case: container_environment/test.py - name: container_bridge case: container_bridge/test.py diff --git a/test/case/infix_services/mdns/mdns.yaml b/test/case/infix_services/mdns/mdns.yaml index 09c785466..248c901f5 100644 --- a/test/case/infix_services/mdns/mdns.yaml +++ b/test/case/infix_services/mdns/mdns.yaml @@ -2,6 +2,5 @@ - name: mdns_enable_disable case: mdns_enable_disable/test.py -# Disabled for v25.08, unstable -# - name: mdns_allow_deny -# case: mdns_allow_deny/test.py +- name: mdns_allow_deny + case: mdns_allow_deny/test.py diff --git a/test/case/infix_services/mdns/mdns_allow_deny/test.py b/test/case/infix_services/mdns/mdns_allow_deny/test.py index f53adbb65..8fa5707aa 100755 --- a/test/case/infix_services/mdns/mdns_allow_deny/test.py +++ b/test/case/infix_services/mdns/mdns_allow_deny/test.py @@ -10,22 +10,55 @@ 3. Allow p1 and p3, deny p2 and p3, traffic only on p1 """ - -import time +import re import infamy -from infamy.util import parallel -def mdns_scan(tgt): - """Trigger Avahi to send traffic on allowed interfaces""" - time.sleep(2) - tgt.runsh("logger -t scan 'calling avahi-browse ...'") - tgt.runsh("avahi-browse -lat") +def mdns_scan(): + """Start packet captures, trigger mDNS scan, return capture results""" + pcap1 = ns1.pcap("host 10.0.1.1 and port 5353") + pcap2 = ns2.pcap("host 10.0.2.1 and port 5353") + pcap3 = ns3.pcap("host 10.0.3.1 and port 5353") + + with pcap1, pcap2, pcap3: + ssh.runsh("logger -t scan 'calling avahi-browse ...'") + ssh.runsh("avahi-browse -lat") + + def has_packets(output): + if not output: + return False + lines = output.strip().split('\n') + m = re.search(r'^(\d+) packets.*', lines[1]) + if m and int(m.group(1)) > 0: + return True + return False + + out1 = pcap1.tcpdump("--count") + out2 = pcap2.tcpdump("--count") + out3 = pcap3.tcpdump("--count") + + return (has_packets(out1), has_packets(out2), has_packets(out3)) + +def check(expected, allow=None, deny=None): + """Execute complete mDNS test scenario""" + try: + dut.delete_xpath("/infix-services:mdns/interfaces") + except ValueError: + # Ignore if xpath doesn't exist (first run) + pass -def check(ns, expr, must): - """Wrap netns.must_receive() with common defaults""" - return ns.must_receive(expr, timeout=3, must=must) + mdns_config = {"mdns": {"interfaces": {}}} + if allow: + mdns_config["mdns"]["interfaces"]["allow"] = allow + if deny: + mdns_config["mdns"]["interfaces"]["deny"] = deny + + dut.put_config_dict("infix-services", mdns_config) + + actual = mdns_scan() + if actual != tuple(expected): + raise AssertionError(f"Expected {expected}, got {actual}") with infamy.Test() as test: @@ -45,58 +78,41 @@ def check(ns, expr, must): { "ietf-interfaces": { "interfaces": { - "interface": [ - { - "name": p1, - "enabled": True, - "ipv4": { - "address": [ - { - "ip": "10.0.1.1", - "prefix-length": 24 - } - ] - } - - }, - { - "name": p2, - "enabled": True, - "ipv4": { - "address": [ - { - "ip": "10.0.2.1", - "prefix-length": 24 - } - ] - } - - }, - { - "name": p3, - "enabled": True, - "ipv4": { - "address": [ - { - "ip": "10.0.3.1", - "prefix-length": 24 - } - ] - } - - }, - ] - } + "interface": [{ + "name": p1, + "enabled": True, + "ipv4": { + "address": [{ + "ip": "10.0.1.1", + "prefix-length": 24 + }] + } + }, { + "name": p2, + "enabled": True, + "ipv4": { + "address": [{ + "ip": "10.0.2.1", + "prefix-length": 24 + }] + } + }, { + "name": p3, + "enabled": True, + "ipv4": { + "address": [{ + "ip": "10.0.3.1", + "prefix-length": 24 + }] + } + }] + } }, "ietf-system": { - "system": { - "hostname": "dut" - } + "system": {"hostname": "dut"} }, "infix-services": { - "mdns": { - "enabled": True - } + "mdns": {"enabled": True} } } ) @@ -108,53 +124,16 @@ def check(ns, expr, must): ns2.addip("10.0.2.2") ns3.addip("10.0.3.2") - EXPR1 = "host 10.0.1.1 and port 5353" - EXPR2 = "host 10.0.2.1 and port 5353" - EXPR3 = "host 10.0.3.1 and port 5353" - with test.step("Allow mDNS on a single interface: p2"): - dut.put_config_dict("infix-services", { - "mdns": { - "interfaces": { - "allow": [p2], - } - } - }) - - parallel(lambda: mdns_scan(ssh), - lambda: check(ns1, EXPR1, False), - lambda: check(ns2, EXPR2, True), - lambda: check(ns3, EXPR3, False)) + # p1:no, p2:yes, p3:no + check([False, True, False], allow=[p2]) with test.step("Deny mDNS on a single interface: p2"): - dut.delete_xpath("/infix-services:mdns/interfaces") - dut.put_config_dict("infix-services", { - "mdns": { - "interfaces": { - "deny": [p2], - } - } - }) - - parallel(lambda: mdns_scan(ssh), - lambda: check(ns1, EXPR1, True), - lambda: check(ns2, EXPR2, False), - lambda: check(ns3, EXPR3, True)) + # p1:yes, p2:no, p3:yes + check([True, False, True], deny=[p2]) with test.step("Allow mDNS on p1, p3 deny on p2, p3"): - dut.delete_xpath("/infix-services:mdns/interfaces") - dut.put_config_dict("infix-services", { - "mdns": { - "interfaces": { - "allow": [p1, p3], - "deny": [p2, p3], - } - } - }) - - parallel(lambda: mdns_scan(ssh), - lambda: check(ns1, EXPR1, True), - lambda: check(ns2, EXPR2, False), - lambda: check(ns3, EXPR3, False)) + # p1:yes, p2:no, p3:no (deny overrides allow) + check([True, False, False], allow=[p1, p3], deny=[p2, p3]) test.succeed() diff --git a/test/case/meta/bootorder.py b/test/case/meta/bootorder.py new file mode 100755 index 000000000..a82d4c7b1 --- /dev/null +++ b/test/case/meta/bootorder.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import infamy + +with infamy.Test() as test: + with test.step("Discover topology and attach to available DUTs"): + env = infamy.Env(False) + ctrl = env.ptop.get_ctrl() + duts = {} + duts_state = {} + for ix in env.ptop.get_infixen(): + cport, ixport = env.ptop.get_mgmt_link(ctrl, ix) + print(f"Attaching to {ix}:{ixport} via {ctrl}:{cport}") + duts[ix] = env.attach(ix, ixport) + + with test.step("Verify bootorder"): + for name, tgt in duts.items(): + expected = env.ptop.get_expected_boot(name) + running = tgt.get_data("/ietf-system:system-state") + running = running['system-state']['software']['booted'] + print(f"{name}: booted: {running} expected: {expected}") + + if running != expected: + test.fail() + test.succeed() diff --git a/test/case/meta/check-version.py b/test/case/meta/check-version.py new file mode 100755 index 000000000..1ec8570a3 --- /dev/null +++ b/test/case/meta/check-version.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import infamy +import os + +with infamy.Test() as test: + with test.step("Discover topology and attach to available DUTs"): + env = infamy.Env(False) + ctrl = env.ptop.get_ctrl() + duts = {} + duts_state = {} + for ix in env.ptop.get_infixen(): + cport, ixport = env.ptop.get_mgmt_link(ctrl, ix) + print(f"Attaching to {ix}:{ixport} via {ctrl}:{cport}") + duts[ix] = env.attach(ix, ixport) + + with test.step("Verify software version"): + expected=os.environ.get("VERSION") + for name, tgt in duts.items(): + running = tgt.get_data("/ietf-system:system-state") + running = running['system-state']['platform']['os-version'] + print(f"{name}: booted: {running} expected {expected}") + if running != expected: + test.fail() + test.succeed() diff --git a/test/case/meta/reproducible.py b/test/case/meta/reproducible.py index e6f68515a..6e2ad565a 100755 --- a/test/case/meta/reproducible.py +++ b/test/case/meta/reproducible.py @@ -13,27 +13,4 @@ else: print(f"Specify PYTHONHASHSEED={seed} to reproduce this test environment") - with test.step("Discover topology and attach to available DUTs"): - env = infamy.Env(False) - ctrl = env.ptop.get_ctrl() - - duts = {} - for ix in env.ptop.get_infixen(): - cport, ixport = env.ptop.get_mgmt_link(ctrl, ix) - print(f"Attaching to {ix}:{ixport} via {ctrl}:{cport}") - duts[ix] = env.attach(ix, ixport) - - with test.step("Log running software versions"): - for name, tgt in duts.items(): - sys = tgt.get_data("/ietf-system:system-state") - sw = sys["system-state"]["software"] - plt = sys["system-state"]["platform"] - - print(f"{name}:") - for k,v in plt.items(): - print(f" {k:<16s} {v}") - - for k in ("compatible", "booted"): - print(f" {k:<16s} {sw[k]}") - test.succeed() diff --git a/test/env b/test/env index 10eed98fe..b5e651a5d 100755 --- a/test/env +++ b/test/env @@ -184,6 +184,7 @@ while getopts "b:cCDf:hiKp:q:rt:" opt; do kvm= ;; p) + version=$(unsquashfs -cat $OPTARG manifest.raucm | awk -F'=' '/^version=/ {print $2}') INFAMY_ARGS="$INFAMY_ARGS -p $OPTARG" ;; q) @@ -234,6 +235,7 @@ if [ "$containerize" ]; then --env NINEPM_PROJ_CONFIG="$NINEPM_PROJ_CONFIG" \ --env QENETH_PATH="$testdir/templates:$testdir" \ --env PS1="$(build_ps1)" \ + --env VERSION="$version" \ $extra_env \ --expose 9001-9010 --publish-all \ -v "$HOME/.infix/.ash_history":/root/.ash_history \ diff --git a/test/infamy/topology.py b/test/infamy/topology.py index 00f3efcfb..2332945ea 100644 --- a/test/infamy/topology.py +++ b/test/infamy/topology.py @@ -131,6 +131,13 @@ def get_password(self, node): return qstrip(password) if password is not None else "admin" + def get_expected_boot(self, node): + n = self.dotg.get_node(node) + b = n[0] if n else {} + boot = b.get("expected_boot") + + return _qstrip(boot) + def get_link(self, src, dst, flt=lambda _: True): es = self.g.get_edge_data(src, dst) for e in es.values(): @@ -150,7 +157,6 @@ def get_ctrl(self): def get_infixen(self): return self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": {"infix"}})) - def get_attr(self, name, default=None): return _qstrip(self.dotg.get_attributes().get(name, default)) diff --git a/test/test.mk b/test/test.mk index 53c3391d6..0d75fb182 100644 --- a/test/test.mk +++ b/test/test.mk @@ -14,8 +14,7 @@ mode-qeneth := -q $(test-dir)/virt/quad mode-host := -t $(or $(TOPOLOGY),/etc/infamy.dot) mode-run := -t $(BINARIES_DIR)/qemu.dot mode := $(mode-$(TEST_MODE)) - -INFIX_IMAGE_ID := $(call qstrip,$(INFIX_IMAGE_ID)) +INFIX_IMAGE_ID := $(call qstrip,$(INFIX_IMAGE_ID)) binaries-$(ARCH) := $(addprefix $(INFIX_IMAGE_ID),.img -disk.qcow2) pkg-$(ARCH) := -p $(O)/images/$(addprefix $(INFIX_IMAGE_ID),.pkg) binaries-x86_64 += OVMF.fd diff --git a/test/virt/dual/topology.dot.in b/test/virt/dual/topology.dot.in index 3ce0fab1b..736f818c0 100644 --- a/test/virt/dual/topology.dot.in +++ b/test/virt/dual/topology.dot.in @@ -25,6 +25,7 @@ graph "dual" { label="{ e1 | e2 | e3 } | dut1 | { e4 | e5 | e6 }", pos="10,18!", provides="infix", + expected_boot="primary", qn_console=9001, qn_mem="384M", qn_usb="dut1.usb" @@ -33,6 +34,7 @@ graph "dual" { label="{ e1 | e2 | e3 } | dut2 | { e4 | e5 | e6 }", pos="10,12!", provides="infix", + expected_boot="primary", qn_console=9002, qn_mem="384M", qn_usb="dut2.usb" diff --git a/test/virt/quad/topology.dot.in b/test/virt/quad/topology.dot.in index a2b3fb100..549456d49 100644 --- a/test/virt/quad/topology.dot.in +++ b/test/virt/quad/topology.dot.in @@ -24,6 +24,7 @@ graph "quad" { label="{ e1 | e2 | e3 | e4 } | dut1 | { e5 | e6 | e7 | e8}", pos="10,30!", provides="infix", + expected_boot="primary", qn_console=9001, qn_mem="384M", qn_usb="dut1.usb" @@ -32,6 +33,7 @@ graph "quad" { label="{ e1 | e2 | e3 | e4 } | dut2 | { e5 | e6 | e7 | e8}", pos="0,20!", provides="infix", + expected_boot="primary", qn_console=9002, qn_mem="384M", qn_usb="dut2.usb" @@ -40,6 +42,7 @@ graph "quad" { label="{ e1 | e2 | e3 | e4 } | dut3 | { e5 | e6 | e7 | e8}", pos="0,10!", provides="infix", + expected_boot="primary", qn_console=9003, qn_mem="384M", qn_usb="dut3.usb" @@ -49,6 +52,7 @@ graph "quad" { label="{ e1 | e2 | e3 | e4 } | dut4 | { e5 | e6 | e7 | e8}", pos="10,0!", provides="infix", + expected_boot="primary", qn_console=9004, qn_mem="384M", qn_usb="dut4.usb"