Compare commits

...
Author SHA1 Message Date
Tobias WaldekranzandGitHub eb7c031270 Merge pull request #1332 from kernelkit/kernel-v25.02.x
Bump kernel and update ChangeLog for v25.08.2
2025-12-19 13:27:25 +01:00
Joachim Wiberg 1d23f73bfd doc: update ChangeLog for v25.08.2 release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-19 11:36:54 +01:00
ael-botandJoachim Wiberg 119b92f94e Upgrade Linux kernel to 6.12.63
Backported from origin/main a999a93e63

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-19 11:36:54 +01:00
Tobias WaldekranzandGitHub 96fad208cb Merge pull request #1328 from kernelkit/cand/v25.08.x
Candidate backports for v25.08.2
2025-12-19 10:34:57 +01:00
Tobias WaldekranzandJoachim Wiberg 0a3879f521 kernel: Enable test_lockup module
This will be us the ability to test a hardware watchdog's ability to
trigger on different kinds of lockups.
2025-12-18 14:12:03 +01:00
Tobias WaldekranzandJoachim Wiberg 6f6bb412f9 aarch64: kernel: Enable SBSA watchdog
Usable by all Server Base System Architecture (SBSA) compliant SoCs,
e.g., CN9130.
2025-12-18 14:12:02 +01:00
Joachim Wiberg b04bae1ad6 board/*/linux_defconfig: panic on lockups and hung tasks
Turn on OOPS-to-panic, soft/hard lockup panic, hung-task panic, and
extra workqueue watchdog reporting. This makes latent stalls visible
instead of silently freezing, improving diagnosis of issues like the
recent resource-pressure lockup.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:02 +01:00
Joachim Wiberg 73edf1bbd6 board/common: log disk/mem/filenr resource usage every hour
Dec  8 15:22:44 ix-00-00-00 watchdogd[2599]: Memory usage: 195036 kB, cached: 69740 kB, total: 423628 kB
Dec  8 15:22:44 ix-00-00-00 watchdogd[2599]: File system /var usage: blocks 4710/52564 inodes 80/65456
Dec  8 15:22:44 ix-00-00-00 watchdogd[2599]: File descriptor usage: 640/34603

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:01 +01:00
Joachim Wiberg 2557eb4900 confd: add support for toggling OSPF debug logs
Backported from origin/main 7edc3c19f7

Fixes #1281

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:01 +01:00
Joachim Wiberg f71722c111 bin: add system support data collection
Backported from origin/main 5420fb01f2

Includes the latest DRAM ECC status, with/without sudo, and temperature.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:00 +01:00
Joachim Wiberg 5ea99680c8 bin: backport fixes to shell/cli copy command
This is a backport of the following commits from origin/main: 3b24fab
e6a04fb 92e80b4 ad96965 3e03ece 21256a8 b7d91e4 d26e311 0977ab4 f83fbc6

---

cli: fix 'copy FILE running-config' use-case

When copying to the running datastore we cannot use sr_copy_config(),
instead we must use sr_replace_config().  This fix covers both the case
of 'copy startup-config running-config' and 'copy FILE running-config'.

Fixes #1203

---

cli: add 'validate', or '-n', dry run to copy command

This commit adds config file validation to the copy command, discussed
in #373.  Allowing users to test their config files before restoring a
backup.  The feature could also be used for the automatic rollback when
downgrading to an earlier version of the OS.

Fixes #373

---

cli: fix copy to missing startup-config file

Fixes #981

---

cli: restrict copy and erase commands

This is a follow-up to PR #717 where path traversal protection was
discussed.  A year later and it's clear that having a user-friendly
copy tool in the shell is a good thing, but that we proably want to
restrict what it can do when called from the CLI.

A sanitize flag (-s) is added to control the behavior, when used in the
shell without -s, both commands act like traditional UNIX tools and do
assume . for relative paths, and allow ../, whereas when running from
the CLI only /media/ is allowed and otherwise files are assumed to be
in $HOME or /cfg

---

cli: sanitize regular file to file copy

The regular file-to-file copy, was missing calls to cfg_adjust(), this
commit fixes that and adds some helpful comments for each use-case.

Also, drop insecure mktemp() in favor of our own version which uses the
basename of the remote source file.

---

bin: add bash completion for copy command

Add bash completion for the common datastores, like we already do in the
CLI, and update the usage text accordingly.

Also, make sure to install to /usr/bin, not /bin since we've now merged
the hierarchies since a while back.

---

bin: copy: Refactor

copy() made some...creative...use of control flow that made it quite
difficult to follow.

Take a first priciples approach to simplify the logic.

---

bin: copy: Always get startup from sysrepo

This will make sure to apply NACM rules for all the data. It also
makes it possible for a luser access a subset of the data, even if
they to do not have read access to /cfg/startup-config.cfg.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:12:00 +01:00
Joachim Wiberg 55b038d999 .github: only set latest tag on .0 releases
Prevent patch releases from stealing the "latest" tag from newer minor
versions. Only vXX.YY and vXX.YY.0 releases should be marked as latest.

Fixes #1187

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:11:59 +01:00
Joachim Wiberg 83b55ee4d8 Follw-up to d7abe46, use correct compatible string for override
Actually disable iitod on Styx DCP-SC-28p to work around #670, this
prohibits software control of LEDs, leaving the default HW control,
which has proven more stable on this platform.

Backported from origin/main b0dce8a05e

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-18 14:11:31 +01:00
Joachim WibergandGitHub 68e7167902 Merge pull request #1184 from kernelkit/releng
Release prep for v25.08.1
2025-10-03 16:22:07 +02:00
Joachim Wiberg 25a26fe7c6 doc: update release notes for v25.08.1
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 16:21:28 +02:00
Joachim Wiberg 6c8bd25cee .github; only publish to latest release from main branch
[skip ci]

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 16:21:27 +02:00
Joachim Wiberg ea300137ef test: relocate logic for creating test-report.pdf from workflow
It should be possible to create test reports manually, so logically
the GitHub workflow should call a make rule in test.mk

Untested: branding, or any case where Infix is used as a BR2_EXTERNAL

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:58 +02:00
Joachim Wiberg 44e9c87376 test/infamy: silence schema download in restconf backend
NETCONF backedn already silenced.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:58 +02:00
Joachim Wiberg e708f21444 test/infamy: refactor Furl class to support list of needles
Also, add some documentation to lower barrier of entry/use.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:58 +02:00
Joachim Wiberg 3508b8d0bb test: update container test specs.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:57 +02:00
Joachim Wiberg eec867bd5f test: verify ospf neighbors in setup with non-ospf interface
Extend OSPF basic with a regression test for issue #1169

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:57 +02:00
Joachim Wiberg daa0bd81e8 board/common: ensure efi-part.vfat is saved in build tree
Fixes #1132

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-03 15:03:56 +02:00
Joachim WibergandGitHub 4ccc70aeee Merge pull request #1172 from kernelkit/v25.08.x-backports
Backport fixes for v25.08.1

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 15:28:57 +02:00
Richard Alpe 923443b620 doc: update changelog for v25.08.1
Signed-off-by: Richard Alpe <richard@bit42.se>
2025-09-30 10:28:57 +02:00
Richard Alpe 9ad51c31bf OSPF: skip interfaces without OSPF enabled
Fix a bug where systems with OSPF enabled on some, but not all,
interfaces would cause the OSPF iterator to fail when accessing
iface['area'], which was missing. This caused the ospf_status.py
tool to return {}, resulting in empty OSPF data in sysrepo.

Fixes #1169 Expected neighbors not shown in sysrepocfg

Signed-off-by: Richard Alpe <richard@bit42.se>
2025-09-30 10:21:10 +02:00
Joachim WibergandRichard Alpe 6272c9c46c .github: prevent duplicate builds when ci:main label is added to PRs
Add concurrency control to trigger workflow to cancel in-progress builds
when new events (like adding ci:main label) occur on the same PR.  This
prevents resource waste from duplicate builds and handles the common
workflow where developers add the ci:main label after PR creation.

Fixes #1154
2025-09-30 10:21:09 +02:00
Joachim WibergandRichard Alpe 4750f96774 cli: fix 'show ospf' commands regression, introduced in 827dc098e
Fixes #1155

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:08 +02:00
Joachim WibergandRichard Alpe 81bc143883 board/common: set show-legacy wrapper as executable
Fix #1150

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:07 +02:00
Joachim WibergandRichard Alpe 58b3fb30be test/case/infix_services: refactor mdns-allow-deny
Major refactor to redesign how listeners and traffic is started in parallel.

Fixes #1130

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:06 +02:00
Joachim WibergandRichard Alpe 6dfc305f0b test/case/infix_container: refactor container-environment
Refactor test to use httpd container instead and return ENV with a CGI.

Fixes #1131

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 10:21:06 +02:00
Mattias WalströmandRichard Alpe 8ca3d3a3ff test: Add new sanity check tests
One to verify it is the correct version on the duts and one
that check that it is the correct bootorder.
2025-09-30 10:21:05 +02:00
Joachim Wiberg 7ae4de3a17 doc: prepare for v25.08.1
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-09-30 09:30:51 +02:00
87 changed files with 2232 additions and 718 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
if echo $ver | grep -qE 'v[0-9.]+(-alpha|-beta|-rc)[0-9]*'; then
echo "pre=true" >> $GITHUB_OUTPUT
echo "latest=false" >> $GITHUB_OUTPUT
elif echo $ver | grep -qE '^v[0-9.]+\.[0-9.]+(\.[0-9]+)?$'; then
elif echo $ver | grep -qE '^v[0-9]+\.[0-9]+(\.0)?$'; then
echo "pre=false" >> $GITHUB_OUTPUT
echo "latest=true" >> $GITHUB_OUTPUT
echo "cat=Releases" >> $GITHUB_OUTPUT
+4 -9
View File
@@ -39,7 +39,7 @@ env:
jobs:
test:
name: Regression Test ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.name || inputs.name }} ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.target || inputs.target }}
runs-on: [ self-hosted, regression ]
runs-on: [self-hosted, regression]
steps:
- name: Checkout infix repo
uses: actions/checkout@v4
@@ -87,7 +87,7 @@ jobs:
fi
make test
- name: Publish Test Result for ${{ env.TARGET }}
- name: Publish Test Result for ${{ env.TARGET }}
# Ensure this runs even if Regression Test fails
if: always()
run: cat $TEST_PATH/.log/last/result-gh.md >> $GITHUB_STEP_SUMMARY
@@ -96,15 +96,10 @@ jobs:
# Ensure this runs even if Regression Test fails
if: always()
run: |
asciidoctor-pdf \
--theme $TEST_PATH/9pm/report/theme.yml \
-a pdf-fontsdir=$TEST_PATH/9pm/report/fonts \
$TEST_PATH/.log/last/report.adoc \
-o $TEST_PATH/.log/last/report.pdf
make test-dir="$(pwd)/$TEST_PATH" test-report
- name: Upload Test Report as Artifact
uses: actions/upload-artifact@v4
with:
name: test-report
path: ${{ env.TEST_PATH }}/.log/last/report.pdf
path: output/images/test-report.pdf
+5 -1
View File
@@ -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/')
@@ -33,6 +37,6 @@ jobs:
name: "infix"
test-publish-x86_64:
if: startsWith(github.repository, 'kernelkit/')
if: ${{ github.repository_owner == 'kernelkit' && github.ref_name == 'main' }}
needs: test-run-x86_64
uses: ./.github/workflows/publish.yml
+9 -1
View File
@@ -401,6 +401,7 @@ CONFIG_WATCHDOG=y
CONFIG_WATCHDOG_SYSFS=y
CONFIG_SOFT_WATCHDOG=y
CONFIG_GPIO_WATCHDOG=y
CONFIG_ARM_SBSA_WATCHDOG=y
CONFIG_ARMADA_37XX_WATCHDOG=y
CONFIG_I6300ESB_WDT=y
CONFIG_MFD_MAX77620=y
@@ -549,7 +550,14 @@ CONFIG_MAGIC_SYSRQ=y
CONFIG_DEBUG_FS=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_DETECT_HUNG_TASK=y
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
CONFIG_WQ_WATCHDOG=y
CONFIG_WQ_CPU_INTENSIVE_REPORT=y
CONFIG_TEST_LOCKUP=m
# CONFIG_SCHED_DEBUG is not set
# CONFIG_RCU_TRACE is not set
CONFIG_FUNCTION_TRACER=y
@@ -0,0 +1,161 @@
#!/bin/sh
set -e
# Build a map of phandle -> device tree node path for all PHY nodes
build_phandle_map()
{
for phy_node in /sys/firmware/devicetree/base/cp*/config-space*/mdio*/switch*/mdio/ethernet-phy@* \
/sys/firmware/devicetree/base/cp*/config-space*/mdio*/ethernet-phy@*; do
[ -f "$phy_node/phandle" ] || continue
phandle=$(od -An -t x4 -N 4 "$phy_node/phandle" 2>/dev/null | tr -d ' ')
if [ -n "$phandle" ]; then
echo "$phandle:$phy_node"
fi
done
}
build_phy_map()
{
phandle_map=$(build_phandle_map)
# Build a mapping of PHY of_node path -> interface name
for iface in /sys/class/net/*; do
[ -d "$iface" ] || continue
iface_name=$(basename "$iface")
# Try regular phydev approach first (for non-DSA interfaces)
if [ -L "$iface/phydev/of_node" ]; then
phy_of_node=$(readlink -f "$iface/phydev/of_node" 2>/dev/null)
if [ -n "$phy_of_node" ]; then
echo "$phy_of_node:$iface_name"
continue
fi
fi
# For DSA interfaces, resolve via of_node's phy-handle
if [ -L "$iface/of_node" ]; then
iface_of_node=$(readlink -f "$iface/of_node" 2>/dev/null)
[ -n "$iface_of_node" ] || continue
# Try to read phy-handle property (4-byte phandle)
if [ -f "$iface_of_node/phy-handle" ]; then
phy_phandle=$(od -An -t x4 -N 4 "$iface_of_node/phy-handle" 2>/dev/null | tr -d ' ')
if [ -n "$phy_phandle" ]; then
# Look up the PHY node path from our phandle map
phy_of_node=$(echo "$phandle_map" | grep "^$phy_phandle:" | cut -d: -f2)
if [ -n "$phy_of_node" ]; then
echo "$phy_of_node:$iface_name"
fi
fi
fi
fi
done
}
zone_map()
{
type="$1"
case "$type" in
ap-ic-thermal)
echo "Application processor interconnect"
;;
ap-cpu[0-9]*-thermal)
cpu=${type#ap-cpu}
cpu=${cpu%-thermal}
echo "Application processor core $cpu"
;;
cp[0-9]*-ic-thermal)
cp=${type%%-*}
cp=${cp#cp}
echo "Communication processor $cp interconnect"
;;
*)
echo "$type"
;;
esac
}
thermal_zones()
{
echo "Thermal Zones"
echo "============="
echo
for zone in /sys/class/thermal/thermal_zone*; do
[ -d "$zone" ] || continue
name=$(basename "$zone")
type=$(cat "$zone/type" 2>/dev/null || echo "unknown")
data=$(cat "$zone/temp" 2>/dev/null)
desc=$(zone_map "$type")
if [ -n "$data" ] && [ "$data" != "N/A" ]; then
# Convert millidegrees to degrees Celsius
temp_c=$(awk "BEGIN {printf \"%.1f\", $data / 1000}")
printf "%-20s %8s°C %s\n" "$name" "$temp_c" "$desc"
else
printf "%-20s %8s %s\n" "$name" "N/A" "$desc"
fi
done
echo
}
hwmon()
{
tmpfile=$(mktemp)
echo "Hardware Monitors"
echo "================="
echo
phy_map=$(build_phy_map)
for hwmon in /sys/class/hwmon/hwmon*; do
[ -d "$hwmon" ] || continue
name=$(basename "$hwmon")
data=$(cat "$hwmon/temp1_input" 2>/dev/null)
# Try to find the associated network interface
iface=
if [ -L "$hwmon/of_node" ]; then
hwmon_of_node=$(readlink -f "$hwmon/of_node" 2>/dev/null)
if [ -n "$hwmon_of_node" ]; then
iface=$(echo "$phy_map" | grep "^$hwmon_of_node:" | cut -d: -f2)
fi
fi
if [ -n "$iface" ]; then
description="Phy $iface temperature"
else
description="N/A"
fi
if [ -n "$data" ] && [ "$data" != "N/A" ]; then
# Convert millidegrees to degrees Celsius
temp_c=$(awk "BEGIN {printf \"%.1f\", $data / 1000}")
# Format: sortkey|hwmon|temp|description (sortkey for natural sort by interface)
printf "%s|%-20s %8s°C %s\n" "$iface" "$name" "$temp_c" "$description" >> "$tmpfile"
else
printf "%s|%-20s %8s %s\n" "$iface" "$name" "N/A" "$description" >> "$tmpfile"
fi
done
# Sort by interface name naturally (e2 before e10), with N/A entries at the end
# Then strip the sort key before displaying
sort -V -t'|' -k1,1 "$tmpfile" | cut -d'|' -f2-
rm -f "$tmpfile"
echo
}
[ -n "$1" ] || { echo "usage: $0 OUT-DIR"; exit 1; }
work="$1"/system
mkdir -p "${work}"
thermal_zones > "${work}"/temperature.txt
hwmon >> "${work}"/temperature.txt
@@ -0,0 +1,26 @@
#!/bin/sh
set -e
ecc_stat()
{
local chan=
local base=
for chan in 0 1; do
base=$((0xf0020360 + 0x200 * chan))
echo "DRAM Channel $chan ECC Status"
echo -n " Log config: "; devmem $((base + 0x0)) 32
echo -n " 1b errors: "; devmem $((base + 0x4)) 32
echo -n " Info 0: "; devmem $((base + 0x8)) 32
echo -n " Info 1: "; devmem $((base + 0xc)) 32
echo
done
}
[ -n "$1" ] || { echo "usage: $0 OUT-DIR"; exit 1; }
work="$1"/marvell-cn913x
mkdir -p "${work}"
ecc_stat >"${work}"/ecc-stat
+2 -2
View File
@@ -83,7 +83,7 @@ genboot()
{
if [ -d "$bootdata" ]; then
bootimg=$(cat <<EOF
image efi-part.vfat {
image $BINARIES_DIR/efi-part.vfat {
size = $bootsize
vfat {
file EFI {
@@ -98,7 +98,7 @@ EOF
offset = $bootoffs
partition-type-uuid = U
bootable = true
image = efi-part.vfat
image = $BINARIES_DIR/efi-part.vfat
}
EOF
)
+33 -24
View File
@@ -90,9 +90,9 @@ reset-reason {
# Monitors file descriptor leaks based on /proc/sys/fs/file-nr
filenr {
# enabled = true
interval = 300
logmark = false
enabled = true
interval = 3600
logmark = true
warning = 0.9
critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
@@ -102,33 +102,42 @@ filenr {
# The script is called with fsmon as the first argument and there
# are two environment variables FSMON_NAME, for the monitored path,
# and FSMON_TYPE indicating either 'blocks' or 'inodes'.
#fsmon /var {
# enabled = true
# interval = 300
# logmark = false
# warning = 0.95
# critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
#}
# Monitors load average based on sysinfo() from /proc/loadavg
# The level is composed from the average of the 1 and 5 min marks.
loadavg {
# enabled = true
interval = 300
logmark = false
warning = 1.0
critical = 2.0
fsmon /var {
enabled = true
interval = 3600
logmark = true
warning = 0.95
critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
}
fsmon /tmp {
enabled = true
interval = 3600
logmark = true
warning = 0.95
critical = 1.0
# script = "/path/to/alt-reboot-action.sh"
}
# Monitors load average based on sysinfo() from /proc/loadavg
# The level is composed from the average of the 1 and 5 min marks.
#loadavg {
# enabled = true
# interval = 300
# logmark = true
# warning = 1.0
# critical = 2.0
# script = "/path/to/alt-reboot-action.sh"
#}
# Monitors free RAM based on data from /proc/meminfo
meminfo {
# enabled = true
interval = 300
logmark = false
enabled = true
interval = 3600
logmark = true
warning = 0.9
critical = 0.95
critical = 0.97
# script = "/path/to/alt-reboot-action.sh"
}
View File
+9 -1
View File
@@ -461,8 +461,16 @@ CONFIG_DEBUG_FS=y
# CONFIG_SLUB_DEBUG is not set
CONFIG_DEBUG_RODATA_TEST=y
CONFIG_DEBUG_WX=y
CONFIG_SOFTLOCKUP_DETECTOR=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
CONFIG_WQ_WATCHDOG=y
CONFIG_WQ_CPU_INTENSIVE_REPORT=y
CONFIG_TEST_LOCKUP=m
# CONFIG_SCHED_DEBUG is not set
CONFIG_STACKTRACE=y
CONFIG_RCU_CPU_STALL_TIMEOUT=60
+7 -1
View File
@@ -266,7 +266,13 @@ CONFIG_MAGIC_SYSRQ=y
CONFIG_DEBUG_FS=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_DETECT_HUNG_TASK=y
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y
CONFIG_HARDLOCKUP_DETECTOR=y
CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
CONFIG_WQ_WATCHDOG=y
CONFIG_WQ_CPU_INTENSIVE_REPORT=y
CONFIG_TEST_LOCKUP=m
CONFIG_FUNCTION_TRACER=y
CONFIG_UNWINDER_FRAME_POINTER=y
+7 -1
View File
@@ -27,7 +27,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -111,6 +111,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+7 -1
View File
@@ -27,7 +27,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -94,6 +94,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+6
View File
@@ -123,6 +123,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+7 -1
View File
@@ -26,7 +26,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INF
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -109,6 +109,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+7 -1
View File
@@ -27,7 +27,7 @@ BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INF
BR2_ROOTFS_POST_IMAGE_SCRIPT="${BR2_EXTERNAL_INFIX_PATH}/board/common/post-image.sh"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.44"
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.63"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/linux_defconfig"
BR2_LINUX_KERNEL_INSTALL_TARGET=y
@@ -93,6 +93,12 @@ BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_SYSKLOGD=y
BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_PACKAGE_WATCHDOGD=y
BR2_PACKAGE_WATCHDOGD_GENERIC=y
BR2_PACKAGE_WATCHDOGD_LOADAVG=y
BR2_PACKAGE_WATCHDOGD_FILENR=y
BR2_PACKAGE_WATCHDOGD_MEMINFO=y
BR2_PACKAGE_WATCHDOGD_FSMON=y
BR2_PACKAGE_WATCHDOGD_TEMPMON=y
BR2_PACKAGE_LESS=y
BR2_PACKAGE_MG=y
BR2_PACKAGE_NANO=y
+40
View File
@@ -3,6 +3,45 @@ Change Log
All notable changes to the project are documented in this file.
[v25.08.2][] - 2025-12-19
-------------------------
### Changes
- Upgrade Linux kernel to 6.12.63 (LTS)
- Enable workaround for issue #670 by disabling iitod on styx platform. This
prohibits software control of LEDs, leaving the default HW control, which
has proven more stable on this platform
- Add support for configurable OSPF debug logging, issue #1281. Debug options
can now be enabled per category (bfd, packet, ism, nsm, default-information,
nssa). All debug options are disabled by default to prevent log flooding in
production environments. See the documentation for usage examples
- Add support data collection script, useful when troubleshooting issues on
deployed systems. Gathers system information, logs, and more. Issue #1287
- Enable kernel panic on lockups + hung tasks => console log + reboot. Also,
enable watchdogd resource monitors, logs: memory/file system + descriptor
usage. Issue #1318
- Enable CN9130 HW watchdog, and kernel `test_lockup` module, issue #1320
### Fixes
- Fix #981: copying any file, including `running-config`, to the persistent
back-end store for `startup-config`, does not take
- Fix #1203: copying any file, including `startup-config`, to `running-config`
does not take
[v25.08.1][] - 2025-10-03
-------------------------
### Changes
- N/A
### Fixes
- Fix #1150: `show-legacy` wrapper permissions
- Fix #1155: `show ospf` commands regression
- Fix #1169: Expected OSPF neighbors not shown in `sysrepocfg` when the
system has at least one non-OSPF interface
[v25.08.0][] - 2025-09-01
-------------------------
@@ -1613,6 +1652,7 @@ Supported YANG models in addition to those used by sysrepo and netopeer:
[buildroot]: https://buildroot.org/
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.08.0...HEAD
[v25.08.1]: https://github.com/kernelkit/infix/compare/v25.08.0...v26.08.1
[v25.08.0]: https://github.com/kernelkit/infix/compare/v25.06.1...v26.08.0
[v25.06.0]: https://github.com/kernelkit/infix/compare/v25.05.1...v26.06.0
[v25.05.1]: https://github.com/kernelkit/infix/compare/v25.05.0...v25.05.1
+1 -1
View File
@@ -11,7 +11,7 @@ BIN_LICENSE = BSD-3-Clause
BIN_LICENSE_FILES = LICENSE
BIN_REDISTRIBUTE = NO
BIN_DEPENDENCIES = sysrepo libite
BIN_CONF_OPTS = --prefix= --disable-silent-rules
BIN_CONF_OPTS = --disable-silent-rules
BIN_AUTORECONF = YES
define BIN_CONF_ENV
@@ -1,4 +1,4 @@
From b4f8d056cfbf3af9116952f5f03ed2bfba13fcbb Mon Sep 17 00:00:00 2001
From c325dce2343f549b188f050ed6cc1fb6bdad824d Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 12 Mar 2024 10:27:24 +0100
Subject: [PATCH 01/30] [FIX] net: dsa: mv88e6xxx: Fix timeout on waiting for
@@ -1,4 +1,4 @@
From b9bf83b003601dc306d3a2cd7dd51f2795bbb451 Mon Sep 17 00:00:00 2001
From 1e235f31e3a37e1077a31136f9f1cbfcc41e2d1a Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 27 Mar 2024 15:52:43 +0100
Subject: [PATCH 02/30] net: dsa: mv88e6xxx: Improve indirect register access
@@ -1,4 +1,4 @@
From c6171e8b4f0a7acb1a8d57e9fe8900e132917e82 Mon Sep 17 00:00:00 2001
From 4cde6e8574c27bf2d1bb397c575e1d3a6d7c6e59 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Mon, 22 Apr 2024 23:18:01 +0200
Subject: [PATCH 03/30] net: dsa: mv88e6xxx: Honor ports being managed via
@@ -1,4 +1,4 @@
From 8138197f76730926e96d94b532d0215de20470af Mon Sep 17 00:00:00 2001
From 19290bb9a333978f38cf5790b5b568b2edae7824 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 24 Apr 2024 22:41:04 +0200
Subject: [PATCH 04/30] net: dsa: mv88e6xxx: Limit rsvd2cpu policy to user
@@ -1,4 +1,4 @@
From a02245c2aa6783aae016266ef3f226f85bf402da Mon Sep 17 00:00:00 2001
From 8a6823163110439b57cb88b9a370148fa34c6cd9 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Thu, 16 Nov 2023 19:44:32 +0100
Subject: [PATCH 05/30] net: dsa: mv88e6xxx: Add LED infrastructure
@@ -1,4 +1,4 @@
From cf091bc712f67eaa733194de9b84003d27221053 Mon Sep 17 00:00:00 2001
From dfdd2ba23cba1c48fe84201fd6ead98e5bda8bb3 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Thu, 16 Nov 2023 21:59:35 +0100
Subject: [PATCH 06/30] net: dsa: mv88e6xxx: Add LED support for 6393X
@@ -1,4 +1,4 @@
From 630151bff3fa0f6ef64c326ca874ef9858bdcb8a Mon Sep 17 00:00:00 2001
From 1874e05ed2d60863bc0d077596b4073f03fda1dc Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 28 May 2024 10:38:42 +0200
Subject: [PATCH 07/30] net: dsa: tag_dsa: Use tag priority as initial
@@ -1,4 +1,4 @@
From a87bfa946479faea1dce4d9ee1034922263dc758 Mon Sep 17 00:00:00 2001
From 001a92a13d26f1870b29b614120a1d1e2943906b Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 16 Jan 2024 16:00:55 +0100
Subject: [PATCH 08/30] net: dsa: Support MDB memberships whose L2 addresses
@@ -1,4 +1,4 @@
From 8465eec4cbeccb7eb683820e8e4f4601ce05509f Mon Sep 17 00:00:00 2001
From 7955dd42748c39e2dec0dd74b2275d36719470ea Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Thu, 21 Mar 2024 19:12:15 +0100
Subject: [PATCH 09/30] net: dsa: Support EtherType based priority overrides
@@ -1,4 +1,4 @@
From 6809a220eb75ef2ac5eed033028cd0a98948cca3 Mon Sep 17 00:00:00 2001
From 38de29685c16dca3a374d4762b34e5011335e6f7 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Fri, 22 Mar 2024 16:15:43 +0100
Subject: [PATCH 10/30] net: dsa: mv88e6xxx: Support EtherType based priority
@@ -1,4 +1,4 @@
From ecc33c81e71000a7db4abf1fd769783528883c0a Mon Sep 17 00:00:00 2001
From 3a1a9d89a6cd1e345ff40802ba06c4a0d7786b82 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 28 May 2024 11:04:22 +0200
Subject: [PATCH 11/30] net: dsa: mv88e6xxx: Add mqprio qdisc support
@@ -1,4 +1,4 @@
From 446ee49b7ecef20c17fa7fd8c2d196236fdbeb0f Mon Sep 17 00:00:00 2001
From 9ca8684220e48d43e8db03dbf58c84a7248af196 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 29 May 2024 13:20:41 +0200
Subject: [PATCH 12/30] net: dsa: mv88e6xxx: Use VLAN prio over IP when both
@@ -1,4 +1,4 @@
From 379d6362066d1e3944f2610b6769bcb170f5cfb5 Mon Sep 17 00:00:00 2001
From 6a03b1751882137bf8008d151f2d85b3975defe7 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 26 Nov 2024 19:45:59 +0100
Subject: [PATCH 13/30] [FIX] net: dsa: mv88e6xxx: Trap locally terminated
@@ -1,4 +1,4 @@
From c1c803aae5530c6ad8cba5e1b690309343d70581 Mon Sep 17 00:00:00 2001
From 8c1a4589df153e8a0029f5a44616c1e46f17a8d0 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 19 Sep 2023 18:38:10 +0200
Subject: [PATCH 14/30] net: phy: marvell10g: Support firmware loading on
@@ -1,4 +1,4 @@
From f78961c99e9027e093d5fc4b16b890acb0f4c906 Mon Sep 17 00:00:00 2001
From b252e0465607dd509b73e21404937875a9a11de4 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 21 Nov 2023 20:15:24 +0100
Subject: [PATCH 15/30] net: phy: marvell10g: Fix power-up when strapped to
@@ -1,4 +1,4 @@
From f91ec427a88888262de0e670ec1d6d5a2dd1ea19 Mon Sep 17 00:00:00 2001
From 361497a386716b9b9ab0822c5cb99165def31691 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 15 Nov 2023 20:58:42 +0100
Subject: [PATCH 16/30] net: phy: marvell10g: Add LED support for 88X3310
@@ -1,4 +1,4 @@
From 4bd8aa58f2cf82d8df62624a030ca822d1991e3f Mon Sep 17 00:00:00 2001
From bf56b0154d9ad9d5ab071596f67e533bd12da08d Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Tue, 12 Dec 2023 09:51:05 +0100
Subject: [PATCH 17/30] net: phy: marvell10g: Support LEDs tied to a single
@@ -1,4 +1,4 @@
From 462548c531a48ea97f16fe02f8214304bf5717eb Mon Sep 17 00:00:00 2001
From d73b87e8f153a1ec227ee5d764a223d71546d8d6 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 27 Mar 2024 10:10:19 +0100
Subject: [PATCH 18/30] net: phy: Do not resume PHY when attaching
@@ -1,4 +1,4 @@
From 550d6f5a0463f39f2959415495b86c79816d4055 Mon Sep 17 00:00:00 2001
From a16724e0f5ad607c5a066005870fffc90f5954e3 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Mon, 4 Mar 2024 16:47:28 +0100
Subject: [PATCH 19/30] net: bridge: avoid classifying unknown multicast as
@@ -1,4 +1,4 @@
From b1e6fb8acc320967fa2435dda641ec2031fd55cc Mon Sep 17 00:00:00 2001
From 66482102bdf087d6fdc14cb5ffada09e871736e8 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Tue, 5 Mar 2024 06:44:41 +0100
Subject: [PATCH 20/30] net: bridge: Ignore router ports when forwarding L2
@@ -1,4 +1,4 @@
From c0de0c952b98eea283925a7f15255a7501c7343c Mon Sep 17 00:00:00 2001
From dca2d29cf6473b4d01169b655801ab018edc6e87 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Thu, 4 Apr 2024 16:36:30 +0200
Subject: [PATCH 21/30] net: bridge: drop delay for applying strict multicast
@@ -1,4 +1,4 @@
From 778143529c0df2eeada5e2c682156d62103b92bc Mon Sep 17 00:00:00 2001
From 01ebb6534cdb82572dcd8b0c2ad84a96f7e3a80c Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Thu, 16 May 2024 14:51:54 +0200
Subject: [PATCH 22/30] net: bridge: Differentiate MDB additions from
@@ -1,4 +1,4 @@
From adc96c55eaea59edd98026c209f435238ceeac09 Mon Sep 17 00:00:00 2001
From 569aeb5b4438c403fba8e4ea64378a34cdb0c563 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Fri, 24 Nov 2023 23:29:55 +0100
Subject: [PATCH 23/30] nvmem: layouts: onie-tlv: Let device probe even when
@@ -1,4 +1,4 @@
From fecf7749e91bd1d2d54e2003daefe97ae449b04c Mon Sep 17 00:00:00 2001
From fd03c7f0132c458c7375d737f749d801a591aced Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Mon, 29 Apr 2024 15:14:51 +0200
Subject: [PATCH 24/30] usb: core: adjust log level for unauthorized devices
@@ -1,4 +1,4 @@
From c89e3e44747f2cddfaf47e0a55027a14579ca4ce Mon Sep 17 00:00:00 2001
From 0a112c6c854e7dba155d911c3ba72a3a2b6f206c Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Thu, 16 Jan 2025 12:35:12 +0100
Subject: [PATCH 25/30] net: dsa: mv88e6xxx: collapse disabled state into
@@ -1,4 +1,4 @@
From 34de4ecc90144fb98aae12cbc22c847eb0e1f92f Mon Sep 17 00:00:00 2001
From 5c939708c5195db4e0fc65f5e8d613cca5c6be26 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
Date: Wed, 12 Feb 2025 22:03:14 +0100
Subject: [PATCH 26/30] net: dsa: mv88e6xxx: Only activate LAG offloading when
@@ -1,4 +1,4 @@
From 10df1d67f996439e1f4dd41ec761c94631383930 Mon Sep 17 00:00:00 2001
From a2f7ac6427a3f2c0cc7783c67359ae956e374c26 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Sun, 11 Aug 2024 11:27:35 +0200
Subject: [PATCH 27/30] net: usb: r8152: add r8153b support for link/activity
@@ -1,4 +1,4 @@
From cd45846aea45d8a51d40472ee5993de43a98d4a6 Mon Sep 17 00:00:00 2001
From 7fa8a878f161e2f68ae8ac0fd1c2cb9e045162cb Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Sun, 10 Aug 2025 18:52:54 +0200
Subject: [PATCH 28/30] arm64: dts: mediatek: mt7986a: rename BPi R3 ports to
@@ -1,4 +1,4 @@
From b978886aa20a68f9d5be378797fa45054c249905 Mon Sep 17 00:00:00 2001
From fa784fa2fc2c80b516e723f91a327a1acaa86363 Mon Sep 17 00:00:00 2001
From: Mattias Walström <lazzer@gmail.com>
Date: Wed, 20 Aug 2025 21:38:24 +0200
Subject: [PATCH 29/30] drm/panel-simple: Add a timing for the Raspberry Pi 7"
@@ -1,4 +1,4 @@
From b6b066a94032f06e669685208969cf00172a361e Mon Sep 17 00:00:00 2001
From 92e8c7e06ba5f256cc36df4d0da4f9dd5566405e Mon Sep 17 00:00:00 2001
From: Mattias Walström <lazzer@gmail.com>
Date: Thu, 21 Aug 2025 11:20:23 +0200
Subject: [PATCH 30/30] input:touchscreen:edt-ft5x06: Add polled mode
+2 -2
View File
@@ -1,2 +1,2 @@
# Calculated with ../utils/kernel-refresh.sh
sha256 b650210ed3027b224969d148aa377452a9aad3ae7f2851abedd31adfef16bdae linux-6.12.44.tar.xz
# Calculated with utils/kernel-refresh.sh
sha256 9502c5ffe4b894383c97abfccf74430a84732f04ee476b9c0d87635b29df7db3 linux-6.12.63.tar.xz
+7 -2
View File
@@ -2,12 +2,17 @@ DISTCLEANFILES = *~ *.d
ACLOCAL_AMFLAGS = -I m4
bin_PROGRAMS = copy erase files
sbin_SCRIPTS = support
# Bash completion
bashcompdir = $(datadir)/bash-completion/completions
dist_bashcomp_DATA = copy.bash
copy_SOURCES = copy.c util.c util.h
copy_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
copy_CFLAGS = -W -Wall -Wextra
copy_CFLAGS += $(libite_CFLAGS) $(sysrepo_CFLAGS)
copy_LDADD = $(libite_LIBS) $(sysrepo_LIBS)
copy_CFLAGS += $(libite_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS)
copy_LDADD = $(libite_LIBS) $(libyang_LIBS) $(sysrepo_LIBS)
erase_SOURCES = erase.c util.c util.h
erase_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
+3 -2
View File
@@ -15,6 +15,7 @@ AC_PROG_INSTALL
PKG_PROG_PKG_CONFIG
PKG_CHECK_MODULES([libite], [libite >= 2.5.0])
PKG_CHECK_MODULES([libyang], [libyang >= 3.0.0])
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36])
# Misc variable replacements for below Summary
@@ -55,8 +56,8 @@ cat <<EOF
System environment....: ${sysconfig_path:-${sysconfig}}
C Compiler............: $CC $CFLAGS $CPPFLAGS $LDFLAGS $LIBS
Linker................: $LD $LLDP_LDFLAGS $LLDP_BIN_LDFLAGS $LDFLAGS $LIBS
CFLAGS................: $libite_CFLAGS $sysrepo_CFLAGS
LIBS..................: $libite_LIBS $sysrepo_LIBS
CFLAGS................: $libite_CFLAGS $libyang_CFLAGS $sysrepo_CFLAGS
LIBS..................: $libite_LIBS $libyang_LIBS $sysrepo_LIBS
------------- Compiler version --------------
$($CC --version || true)
+93
View File
@@ -0,0 +1,93 @@
# bash completion for copy command
# SPDX-License-Identifier: ISC
_copy_completion()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Options for the copy command
opts="-h -n -q -s -t -u -v"
local datastores_dst="running-config startup-config"
local datastores_src="factory-config operational-state running-config"
case "${prev}" in
-t)
# Timeout expects a number, no completion
return 0
;;
-u)
# Username, could complete with getent passwd but keep it simple
return 0
;;
esac
# If current word starts with -, complete options
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# Determine position (source or destination)
# Count non-option arguments before current position
local arg_count=0
local i
for ((i=1; i < COMP_CWORD; i++)); do
case "${COMP_WORDS[i]}" in
-h|-n|-q|-s|-v)
# Flag without argument
;;
-t|-u)
# Flag with argument, skip next word
((i++))
;;
-*)
# Unknown flag, might have argument
;;
*)
# Non-option argument
((arg_count++))
;;
esac
done
# Helper function to add non-hidden files/dirs, filtering out dotfiles unless explicitly requested
_add_files() {
local IFS=$'\n'
local files
# If user is explicitly typing a dotfile path, include dotfiles
if [[ ${cur} == .* || ${cur} == */.* ]]; then
files=( $(compgen -f -- "${cur}") )
else
# Only show non-hidden files and directories
files=( $(compgen -f -- "${cur}" | grep -v '/\.[^/]*$' | grep -v '^\.[^/]') )
fi
COMPREPLY+=( "${files[@]}" )
}
case ${arg_count} in
0)
# First argument (source): complete with files and all datastores including factory-config
COMPREPLY=( $(compgen -W "${datastores_src}" -- ${cur}) )
_add_files
;;
1)
# Second argument (destination): complete with files and limited datastores (no factory-config)
COMPREPLY=( $(compgen -W "${datastores_dst}" -- ${cur}) )
_add_files
;;
*)
# No more arguments expected
return 0
;;
esac
return 0
}
complete -F _copy_completion copy
+435 -230
View File
@@ -10,6 +10,7 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sysrepo.h>
#include <sysrepo/netconf_acm.h>
@@ -18,58 +19,29 @@
struct infix_ds {
char *name; /* startup-config, etc. */
char *sysrepocfg; /* ds name in sysrepocfg */
int datastore; /* sr_datastore_t and -1 */
int rw; /* read-write:1 or not:0 */
bool rw; /* read-write:1 or not:0 */
char *path; /* local path or NULL */
};
struct infix_ds infix_config[] = {
{ "startup-config", "startup", SR_DS_STARTUP, 1, "/cfg/startup-config.cfg" },
{ "running-config", "running", SR_DS_RUNNING, 1, NULL },
{ "candidate-config", "candidate", SR_DS_CANDIDATE, 1, NULL },
{ "operational-config", "operational", SR_DS_OPERATIONAL, 1, NULL },
{ "factory-config", "factory-default", SR_DS_FACTORY_DEFAULT, 0, NULL }
const struct infix_ds infix_config[] = {
{ "startup-config", SR_DS_STARTUP, true, "/cfg/startup-config.cfg" },
{ "running-config", SR_DS_RUNNING, true, NULL },
/* { "candidate-config", SR_DS_CANDIDATE, true, NULL }, */
{ "operational-state", SR_DS_OPERATIONAL, false, NULL },
{ "factory-config", SR_DS_FACTORY_DEFAULT, false, NULL }
};
static const char *prognm = "copy";
static const char *remote_user;
static int timeout;
/*
* Print sysrepo session errors followed by an optional string.
*/
static void emsg(sr_session_ctx_t *sess, const char *fmt, ...)
{
const sr_error_info_t *err = NULL;
va_list ap;
size_t i;
int rc;
if (!sess)
goto end;
rc = sr_session_get_error(sess, &err);
if ((rc != SR_ERR_OK) || !err)
goto end;
// Show the first error only. Because probably next errors are
// originated from internal sysrepo code but is not from subscribers.
// for (i = 0; i < err->err_count; i++)
for (i = 0; i < (err->err_count < 1 ? err->err_count : 1); i++)
fprintf(stderr, ERRMSG "%s\n", err->err[i].message);
end:
if (fmt) {
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
}
static int dry_run;
static int sanitize;
/*
* Current system user, same as sysrepo user
*/
static char *getuser(void)
static const char *getuser(void)
{
const struct passwd *pw;
uid_t uid;
@@ -163,7 +135,7 @@ static void set_owner(const char *fn, const char *user)
}
}
static const char *infix_ds(const char *text, struct infix_ds **ds)
static const char *infix_ds(const char *text, const struct infix_ds **ds)
{
size_t i, len = strlen(text);
@@ -174,209 +146,424 @@ static const char *infix_ds(const char *text, struct infix_ds **ds)
}
}
*ds = NULL;
return text;
}
static bool is_uri(const char *str)
{
return strstr(str, "://") != NULL;
}
static int copy(const char *src, const char *dst, const char *remote_user)
static char *mktmp(void)
{
struct infix_ds *srcds = NULL, *dstds = NULL;
char temp_file[20] = "/tmp/copy.XXXXXX";
const char *tmpfn = NULL;
sr_session_ctx_t *sess;
const char *fn = NULL;
sr_conn_ctx_t *conn;
const char *user;
char adjust[256];
mode_t oldmask;
int rc = 0;
char *path;
int fd;
path = strdup("/tmp/copy-XXXXXX");
if (!path)
goto err;
oldmask = umask(0077);
fd = mkstemp(path);
umask(oldmask);
if (fd < 0)
goto err;
close(fd);
return path;
err:
free(path);
return NULL;
}
static void rmtmp(const char *path)
{
if (remove(path)) {
if (errno == ENOENT)
return;
fprintf(stderr, ERRMSG "removal of temporary file %s failed\n", path);
}
}
static void sysrepo_print_error(sr_session_ctx_t *sess)
{
const sr_error_info_t *erri = NULL;
int err;
err = sr_session_get_error(sess, &erri);
if (err || !erri || !erri->err_count)
return;
fprintf(stderr, ERRMSG "%s (%d)\n", erri->err->message, erri->err->err_code);
}
static sr_session_ctx_t *sysrepo_session(const struct infix_ds *ds)
{
static sr_session_ctx_t *sess;
sr_subscription_ctx_t *sub = NULL;
const char *user = getuser();
sr_conn_ctx_t *conn = NULL;
int err;
if (!ds) {
if (!sess)
return NULL;
conn = sr_session_get_connection(sess);
sr_session_stop(sess);
sr_disconnect(conn);
return NULL;
}
if (!sess) {
err = sr_connect(0, &conn);
if (err != SR_ERR_OK) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "could not connect to %s\n", ds->name);
goto err;
}
/* Always open running, because sr_nacm_init() does not work
* against the factory DS.
*/
err = sr_session_start(conn, SR_DS_RUNNING, &sess);
if (err != SR_ERR_OK) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "%s session setup failed\n", ds->name);
goto err_disconnect;
}
err = sr_nacm_init(sess, 0, &sub);
if (err != SR_ERR_OK) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "%s NACM setup failed\n", ds->name);
goto err_stop;
}
err = sr_nacm_set_user(sess, user);
if (err != SR_ERR_OK) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "%s NACM setup for %s failed\n", ds->name, user);
goto err_nacm_destroy;
}
}
err = sr_session_switch_ds(sess, ds->datastore);
if (err) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "%s activation failed\n", ds->name);
return NULL;
}
return sess;
err_nacm_destroy:
sr_nacm_destroy();
err_stop:
sr_session_stop(sess);
err_disconnect:
sr_disconnect(conn);
err:
sess = NULL;
return NULL;
}
static int sysrepo_export(const struct infix_ds *ds, const char *path)
{
sr_session_ctx_t *sess;
sr_data_t *data;
int err;
sess = sysrepo_session(ds);
if (!sess)
return 1;
err = sr_get_data(sess, "/*", 0, timeout * 1000, SR_OPER_DEFAULT, &data);
if (err) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "retrieval of %s data failed\n", ds->name);
return err;
}
err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_WITHSIBLINGS);
sr_release_data(data);
if (err) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "failed to store %s data\n", ds->name);
return err;
}
return 0;
}
static int sysrepo_import(const struct infix_ds *ds, const char *path)
{
const struct ly_ctx *ly;
sr_session_ctx_t *sess;
struct lyd_node *data;
int err;
sess = sysrepo_session(ds);
if (!sess)
return 1;
ly = sr_acquire_context(sr_session_get_connection(sess));
err = lyd_parse_data_path(ly, path, LYD_JSON,
LYD_PARSE_NO_STATE | LYD_PARSE_ONLY |
LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT, 0, &data);
if (err) {
fprintf(stderr, ERRMSG "failed to parse %s data\n", ds->name);
goto out;
}
err = dry_run ? 0 : sr_replace_config(sess, NULL, data, timeout * 1000);
if (err) {
sysrepo_print_error(sess);
fprintf(stderr, ERRMSG "failed import %s data\n", ds->name);
}
out:
sr_release_context(sr_session_get_connection(sess));
return err ? 1 : 0;
/* return sysrepo_do(sysrepo_import_op, ds, path) ? 1 : 0; */
}
static int subprocess(char * const *argv)
{
int pid, status;
pid = fork();
if (!pid) {
execvp(argv[0], argv);
exit(1);
}
if (pid < 0)
return 1;
if (waitpid(pid, &status, 0) < 0)
return 1;
if (!WIFEXITED(status))
return 1;
return WEXITSTATUS(status);
}
static int curl(char *op, const char *path, const char *uri)
{
char *argv[] = {
"curl", "-L", op, NULL, NULL, NULL, NULL, NULL,
};
int err = 1;
argv[3] = strdup(path);
argv[4] = strdup(uri);
if (!(argv[3] && argv[4]))
goto out;
if (remote_user) {
argv[5] = strdup("-u");
argv[6] = strdup(remote_user);
if (!(argv[5] && argv[6]))
goto out;
}
err = subprocess(argv);
out:
free(argv[6]);
free(argv[5]);
free(argv[4]);
free(argv[3]);
return err;
}
static int curl_upload(const char *srcpath, const char *uri)
{
char upload[] = "-T";
if (curl(upload, srcpath, uri)) {
fprintf(stderr, ERRMSG "upload to %s failed\n", uri);
return 1;
}
return 0;
}
static int curl_download(const char *uri, const char *dstpath)
{
char download[] = "-o";
if (curl(download, dstpath, uri)) {
fprintf(stderr, ERRMSG "download of %s failed\n", uri);
return 1;
}
return 0;
}
static int cp(const char *srcpath, const char *dstpath)
{
char *argv[] = {
"cp", NULL, NULL, NULL,
};
int err = 1;
argv[1] = strdup(srcpath);
argv[2] = strdup(dstpath);
if (!(argv[1] && argv[2]))
goto out;
err = subprocess(argv);
if (err)
fprintf(stderr, ERRMSG "failed to save %s\n", dstpath);
out:
free(argv[2]);
free(argv[1]);
return err;
}
static int put(const char *srcpath, const char *dst,
const struct infix_ds *ds, const char *path)
{
int err = 0;
if (ds)
err = sysrepo_import(ds, srcpath);
else if (is_uri(dst))
err = curl_upload(srcpath, dst);
if (err)
return err;
if (path) {
err = cp(srcpath, path);
if (!err)
set_owner(path, getuser());
}
return 0;
}
static int get(const char *src, const struct infix_ds *ds, const char *path)
{
int err = 0;
if (ds)
err = sysrepo_export(ds, path);
else if (is_uri(src))
err = curl_download(src, path);
return err;
}
static int resolve_src(const char **src, const struct infix_ds **ds, char **path, bool *rm)
{
*src = infix_ds(*src, ds);
if (*ds || is_uri(*src)) {
*path = mktmp();
if (!*path)
return 1;
*rm = true;
return 0;
} else {
*path = cfg_adjust(*src, NULL, sanitize);
}
if (!*path) {
fprintf(stderr, ERRMSG "no such file %s.", *src);
return 1;
}
*rm = false;
return 0;
}
static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path)
{
*dst = infix_ds(*dst, ds);
if (*ds) {
if (!(*ds)->rw) {
fprintf(stderr, ERRMSG "%s is not writable", (*ds)->name);
return 1;
}
if (!(*ds)->path)
return 0;
*path = strdup((*ds)->path);
} else if (is_uri(*dst)) {
return 0;
} else {
*path = cfg_adjust(*dst, NULL, sanitize);
}
if (!*path) {
fprintf(stderr, ERRMSG "no such file: %s", *dst);
return 1;
}
if (!*ds && !access(*path, F_OK) && !yorn("Overwrite existing file %s", *path)) {
fprintf(stderr, "OK, aborting.\n");
return 1;
}
return 0;
}
static int copy(const char *src, const char *dst)
{
char *srcpath = NULL, *dstpath = NULL;
const struct infix_ds *srcds, *dstds;
bool rmsrc = false;
mode_t oldmask;
int err = 1;
/* rw for user and group only */
oldmask = umask(0006);
src = infix_ds(src, &srcds);
if (!src)
goto err;
dst = infix_ds(dst, &dstds);
if (!dst)
goto err;
if (!strcmp(src, dst)) {
fprintf(stderr, ERRMSG "source and destination are the same, aborting.\n");
goto err;
}
user = getuser();
err = resolve_src(&src, &srcds, &srcpath, &rmsrc);
if (err)
goto err;
/* 1. Regular ds copy */
if (srcds && dstds) {
/* Ensure the dst ds is writable */
if (!dstds->rw) {
fprintf(stderr, ERRMSG "not possible to write to \"%s\", skipping.\n", dst);
rc = 1;
goto err;
}
err = resolve_dst(&dst, &dstds, &dstpath);
if (err)
goto err;
if (sr_connect(SR_CONN_DEFAULT, &conn)) {
fprintf(stderr, ERRMSG "connection to datastore failed\n");
rc = 1;
goto err;
}
err = get(src, srcds, srcpath);
if (err)
goto err;
sr_log_syslog("klishd", SR_LL_WRN);
if (sr_session_start(conn, dstds->datastore, &sess)) {
fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst);
} else {
sr_nacm_set_user(sess, user);
rc = sr_copy_config(sess, NULL, srcds->datastore, timeout * 1000);
if (rc)
emsg(sess, ERRMSG "unable to copy configuration, err %d: %s\n",
rc, sr_strerror(rc));
else
set_owner(dstds->path, user);
}
rc = sr_disconnect(conn);
if (!srcds->path || !dstds->path)
goto err; /* done, not an error */
/* allow copy factory startup */
}
if (srcds) {
/* 2. Export from a datastore somewhere else */
if (strstr(dst, "://")) {
if (srcds->path)
fn = srcds->path;
else {
snprintf(adjust, sizeof(adjust), "/tmp/%s.cfg", srcds->name);
fn = tmpfn = adjust;
rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn);
}
if (rc)
fprintf(stderr, ERRMSG "failed exporting %s to %s\n", src, fn);
else {
rc = systemf("curl %s -LT %s %s", remote_user, fn, dst);
if (rc)
fprintf(stderr, ERRMSG "failed uploading %s to %s\n", src, dst);
else
set_owner(dst, user);
}
goto err;
}
if (dstds && dstds->path)
fn = dstds->path;
else
fn = cfg_adjust(dst, src, adjust, sizeof(adjust));
if (!fn) {
fprintf(stderr, ERRMSG "invalid destination path.\n");
rc = -1;
goto err;
}
if (!access(fn, F_OK) && !yorn("Overwrite existing file %s", fn)) {
fprintf(stderr, "OK, aborting.\n");
return 0;
}
if (srcds->path)
rc = systemf("cp %s %s", srcds->path, fn);
else
rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn);
if (rc)
fprintf(stderr, ERRMSG "failed copy %s to %s\n", src, fn);
else
set_owner(fn, user);
} else if (dstds) {
if (!dstds->sysrepocfg) {
fprintf(stderr, ERRMSG "not possible to import to this datastore.\n");
rc = 1;
goto err;
}
if (!dstds->rw) {
fprintf(stderr, ERRMSG "not possible to write to %s", dst);
goto err;
}
/* 3. Import from somewhere to a datastore */
if (strstr(src, "://")) {
tmpfn = mktemp(temp_file);
fn = tmpfn;
} else {
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
if (!fn) {
fprintf(stderr, ERRMSG "invalid source file location.\n");
rc = 1;
goto err;
}
}
if (tmpfn)
rc = systemf("curl %s -Lo %s %s", remote_user, fn, src);
if (rc) {
fprintf(stderr, ERRMSG "failed downloading %s", src);
} else {
rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn);
if (rc)
fprintf(stderr, ERRMSG "failed loading %s from %s", dst, src);
}
} else {
if (strstr(src, "://") && strstr(dst, "://")) {
fprintf(stderr, ERRMSG "copy from remote to remote is not supported.\n");
goto err;
}
if (strstr(src, "://")) {
fn = cfg_adjust(dst, src, adjust, sizeof(adjust));
if (!fn) {
fprintf(stderr, ERRMSG "invalid destination file location.\n");
rc = 1;
goto err;
}
if (!access(fn, F_OK)) {
if (!yorn("Overwrite existing file %s", fn)) {
fprintf(stderr, "OK, aborting.\n");
return 0;
}
}
rc = systemf("curl %s -Lo %s %s", remote_user, fn, src);
} else if (strstr(dst, "://")) {
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
if (!fn) {
fprintf(stderr, ERRMSG "invalid source file location.\n");
rc = 1;
goto err;
}
if (access(fn, F_OK))
fprintf(stderr, ERRMSG "no such file %s, aborting.", fn);
else
rc = systemf("curl %s -LT %s %s", remote_user, fn, dst);
} else {
if (!access(dst, F_OK)) {
if (!yorn("Overwrite existing file %s", dst)) {
fprintf(stderr, "OK, aborting.\n");
return 0;
}
}
rc = systemf("cp %s %s", src, dst);
}
}
err = put(srcpath, dst, dstds, dstpath);
err:
if (tmpfn)
rc = remove(tmpfn);
/* If either src or dst came from sysrepo, close the session */
sysrepo_session(NULL);
sync(); /* ensure command is flushed to disk */
if (rmsrc)
rmtmp(srcpath);
free(dstpath);
free(srcpath);
sync();
umask(oldmask);
return rc;
return err;
}
static int usage(int rc)
@@ -384,30 +571,48 @@ static int usage(int rc)
printf("Usage: %s [OPTIONS] SRC DST\n"
"\n"
"Options:\n"
" -h This help text\n"
" -u USER Username for remote commands, like scp\n"
" -t SEEC Timeout for the operation, or default %d sec\n"
" -v Show version\n", prognm, timeout);
" -h This help text\n"
" -n Dry-run, validate configuration without applying\n"
" -s Sanitize paths for CLI use (restrict path traversal)\n"
" -t SEC Timeout for the operation, or default %d sec\n"
" -u USER Username for remote commands, like scp\n"
" -v Show version\n"
"\n"
"Files:\n"
" SRC JSON configuration file, or a datastore\n"
" DST A file or datastore, except factory-config\n"
"\n"
"Datastores:\n"
" running-config The running datastore, current active config\n"
" startup-config The non-volatile config used at startup\n"
" factory-config The device's factory default configuration\n"
"\n", prognm, timeout);
return rc;
}
int main(int argc, char *argv[])
{
const char *user = NULL, *src = NULL, *dst = NULL;
const char *src = NULL, *dst = NULL;
int c;
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
while ((c = getopt(argc, argv, "ht:u:v")) != EOF) {
while ((c = getopt(argc, argv, "hnst:u:v")) != EOF) {
switch(c) {
case 'h':
return usage(0);
case 'n':
dry_run = 1;
break;
case 's':
sanitize = 1;
break;
case 't':
timeout = atoi(optarg);
break;
case 'u':
user = optarg;
remote_user = optarg;
break;
case 'v':
puts(PACKAGE_VERSION);
@@ -424,5 +629,5 @@ int main(int argc, char *argv[])
src = argv[optind++];
dst = argv[optind++];
return copy(src, dst, user);
return copy(src, dst);
}
+26 -24
View File
@@ -4,40 +4,38 @@
#include <alloca.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "util.h"
static const char *prognm = "erase";
static int sanitize;
static int do_erase(const char *path)
static int do_erase(const char *name)
{
char *fn;
char *path;
int rc = 0;
if (access(path, F_OK)) {
size_t len = strlen(path) + 10;
fn = alloca(len);
if (!fn) {
fprintf(stderr, ERRMSG "failed allocating memory.\n");
return -1;
}
cfg_adjust(path, NULL, fn, len);
} else
fn = (char *)path;
if (!yorn("Remove %s, are you sure", fn))
return 0;
if (remove(fn)) {
fprintf(stderr, ERRMSG "failed removing %s: %s\n", fn, strerror(errno));
return -1;
path = cfg_adjust(name, NULL, sanitize);
if (!path) {
fprintf(stderr, ERRMSG "file not found.\n");
rc = 1;
goto out;
}
return 0;
if (!yorn("Remove %s, are you sure?", path))
goto out;
if (remove(path)) {
fprintf(stderr, ERRMSG "failed removing %s: %s\n", path, strerror(errno));
rc = 11;
}
out:
free(path);
return rc;
}
static int usage(int rc)
@@ -46,6 +44,7 @@ static int usage(int rc)
"\n"
"Options:\n"
" -h This help text\n"
" -s Sanitize paths for CLI use (restrict path traversal)\n"
" -v Show version\n", prognm);
return rc;
@@ -55,10 +54,13 @@ int main(int argc, char *argv[])
{
int c;
while ((c = getopt(argc, argv, "hv")) != EOF) {
while ((c = getopt(argc, argv, "hsv")) != EOF) {
switch(c) {
case 'h':
return usage(0);
case 's':
sanitize = 1;
break;
case 'v':
puts(PACKAGE_VERSION);
return 0;
+635
View File
@@ -0,0 +1,635 @@
#!/bin/sh
# Support utilities for troubleshooting Infix systems
# Program name for usage messages (supports being renamed by users)
prognm=$(basename "$0")
#
# The collect command gathers system information and outputs a tarball.
# Data is collected to /var/lib/support (or $HOME as fallback) and then
# streamed to stdout. The temporary directory is cleaned up automatically.
#
# The following text is primarily intended for users of older Infix
# systems that do not yet have this script in the root fileystems.
#
# 1. Copy this script to the target device's home directory:
#
# scp support user@device:
#
# 2. SSH to the device and make it executable:
#
# ssh user@device chmod +x support
#
# 3. Run the script from your home directory:
#
# ./support collect > support-data.tar.gz
#
# Or directly via SSH from your workstation:
#
# ssh user@device './support collect' > support-data.tar.gz
#
# Optionally, the output can be encrypted with GPG using a password for
# secure transmission to support personnel, see below.
#
# Examples:
# ./support collect > support-data.tar.gz
# ./support collect -s 5 > support-data.tar.gz
# ./support collect -p > support-data.tar.gz.gpg
# ./support collect -p mypass > support-data.tar.gz.gpg
#
# ssh user@device ./support collect > support-data.tar.gz
# ssh user@device ./support collect -p mypass > support-data.tar.gz.gpg
#
# Note, interactive password prompt (-p without argument) may echo characters
# over SSH due to local terminal echo. Use -p PASSWORD for remote execution,
# or pipe the password: echo "password" | ssh user@device ./support collect -p
# meaning you can even: echo "$SECRET_VARIABLE" | ... which in some cases can
# come in handy.
#
# TODO:
# Add more commands, e.g., verify (run health checks), upload <file>,
# test <network|disk|routing>, backup, watch (dashboard view), diff
#
cmd_collect()
{
# Default values
LOG_TAIL_SEC=30
PASSWORD=""
# Parse options
while [ $# -gt 0 ]; do
case "$1" in
--log-sec|-s)
if [ -z "$2" ] || [ "$2" -le 0 ] 2>/dev/null; then
echo "Error: --log-sec requires a positive number" >&2
exit 1
fi
LOG_TAIL_SEC="$2"
shift 2
;;
--password|-p)
# If next arg exists and doesn't start with -, use it as password
if [ -n "$2" ] && [ "${2#-}" = "$2" ]; then
PASSWORD="$2"
shift 2
else
# Prompt for password interactively from stdin, no echo!
# Disable echo BEFORE printing the prompt
old_stty=$(stty -g 2>/dev/null)
stty -echo 2>/dev/null || true
printf "Enter encryption password: " >&2
read -r PASSWORD
echo "" >&2
# Restore terminal settings
if [ -n "$old_stty" ]; then
stty "$old_stty" 2>/dev/null || true
else
stty echo 2>/dev/null || true
fi
if [ -z "$PASSWORD" ]; then
echo "Error: Empty password not allowed" >&2
exit 1
fi
shift
fi
;;
*)
echo "Error: Unknown option '$1'" >&2
echo "Usage: $prognm collect [-s N] [-p PASSWORD]" >&2
exit 1
;;
esac
done
# If WORK_DIR not set globally, try /var/lib/support first (more space,
# persistent across user sessions). Fall back to $HOME if we can't create/write there
if [ -z "$WORK_DIR" ]; then
if [ -w /var/lib/support ] 2>/dev/null; then
# Already writable, use it
WORK_DIR="/var/lib/support"
elif [ ! -e /var/lib/support ]; then
# Doesn't exist, try to create it
if mkdir -p /var/lib/support 2>/dev/null; then
WORK_DIR="/var/lib/support"
fi
elif [ -d /var/lib/support ]; then
# Exists but not writable, try to fix permissions (requires root)
# Try chmod first (might just be permission issue), then chown if needed
if chmod 755 /var/lib/support 2>/dev/null && \
chown "$(id -u):$(id -g)" /var/lib/support 2>/dev/null; then
# Verify it's actually writable now
if [ -w /var/lib/support ] 2>/dev/null; then
WORK_DIR="/var/lib/support"
fi
fi
fi
# Fall back to $HOME if we couldn't set up /var/lib/support
if [ -z "$WORK_DIR" ]; then
WORK_DIR="${HOME}"
echo "Warning: Cannot write to /var/lib/support, using home directory instead." >&2
echo " (This may fill up your home directory on systems with limited space)" >&2
fi
fi
# Create unique collection directory
COLLECT_DIR="${WORK_DIR}/support-$(hostname -s)-$(date -Iseconds)"
EXEC_LOG="${COLLECT_DIR}/collection.log"
# Cleanup on exit
cleanup()
{
echo "[$(date -Iseconds)] Cleanup called (signal: ${1:-EXIT})" >> "${EXEC_LOG}" 2>&1 || echo "[$(date -Iseconds)] Cleanup called (signal: ${1:-EXIT})" >&2
if [ -d "${COLLECT_DIR}" ]; then
echo "[$(date -Iseconds)] Removing collection directory: ${COLLECT_DIR}" >> "${EXEC_LOG}" 2>&1 || echo "[$(date -Iseconds)] Removing: ${COLLECT_DIR}" >&2
rm -rf "${COLLECT_DIR}"
else
echo "[$(date -Iseconds)] Collection directory already gone: ${COLLECT_DIR}" >> "${EXEC_LOG}" 2>&1 || echo "[$(date -Iseconds)] Already gone: ${COLLECT_DIR}" >&2
fi
}
trap cleanup EXIT INT TERM
# Create collection directory
if ! mkdir -p "${COLLECT_DIR}"; then
echo "Error: Cannot create collection directory: ${COLLECT_DIR}" >&2
echo " Check permissions for ${WORK_DIR}" >&2
exit 1
fi
# Helper function to run commands with output to specific file
collect()
{
output_file="$1"
shift
cmd_desc="$*"
mkdir -p "${COLLECT_DIR}/$(dirname "$output_file")"
echo "[$(date -Iseconds)] Collecting: $cmd_desc -> ${output_file}" >> "${EXEC_LOG}" 2>&1
if "$@" > "${COLLECT_DIR}/${output_file}" 2>> "${EXEC_LOG}"; then
echo "[$(date -Iseconds)] Success: ${output_file}" >> "${EXEC_LOG}" 2>&1
else
exit_code=$?
echo "[$(date -Iseconds)] Failed (exit ${exit_code}): ${output_file}" >> "${EXEC_LOG}" 2>&1
# Create placeholder file indicating failure
echo "Command failed with exit code ${exit_code}: $cmd_desc" > "${COLLECT_DIR}/${output_file}"
fi
}
# Start collection log
echo "=== Infix Support Data Collection ===" > "${EXEC_LOG}"
echo "Started: $(date -Iseconds)" >> "${EXEC_LOG}"
echo "Hostname: $(hostname)" >> "${EXEC_LOG}"
echo "Work directory: ${WORK_DIR}" >> "${EXEC_LOG}"
echo "Collection directory: ${COLLECT_DIR}" >> "${EXEC_LOG}"
echo "" >> "${EXEC_LOG}"
# Inform user that collection is starting (to stderr for SSH visibility)
echo "Starting support data collection from $(hostname)..." >&2
echo "Collecting to: ${WORK_DIR}" >&2
echo "This may take up to a minute. Please wait..." >&2
# System identification
collect system-info.txt uname -a
collect hostname.txt hostname
collect uptime.txt uptime
# Configuration files
collect running-config.json sysrepocfg -f json -d running -X
collect operational-config.json sysrepocfg -f json -d operational -X
# Sysrepo YANG modules
if command -v sysrepoctl >/dev/null 2>&1; then
collect sysrepo-modules.txt sysrepoctl -l
fi
# Startup config (may not exist on first boot)
if [ -f /cfg/startup-config.cfg ]; then
cp /cfg/startup-config.cfg "${COLLECT_DIR}/startup-config.cfg" 2>> "${EXEC_LOG}"
else
echo "No startup-config.cfg found" > "${COLLECT_DIR}/startup-config.cfg"
fi
# System logs and runtime data
if [ -d /var/log ]; then
if ls -A /var/log >/dev/null 2>&1; then
tar czf "${COLLECT_DIR}/logs.tar.gz" -C / var/log 2>> "${EXEC_LOG}" || \
echo "Failed to collect /var/log" > "${COLLECT_DIR}/logs.tar.gz.error"
else
echo "No logs in /var/log" > "${COLLECT_DIR}/logs.tar.gz.error"
fi
fi
# Collect crash dumps if they exist
if [ -d /var/crash ]; then
if ls -A /var/crash >/dev/null 2>&1; then
tar czf "${COLLECT_DIR}/crash.tar.gz" -C / var/crash 2>> "${EXEC_LOG}" || \
echo "Failed to collect /var/crash" > "${COLLECT_DIR}/crash.tar.gz.error"
else
echo "No crash dumps in /var/crash" > "${COLLECT_DIR}/crash.tar.gz.error"
fi
fi
# Tail /var/log/messages to capture live logging
if [ -f /var/log/messages ]; then
echo "Tailing /var/log/messages for ${LOG_TAIL_SEC} seconds (please wait)..." >&2
{
echo "=== Starting tail of /var/log/messages for ${LOG_TAIL_SEC} seconds ==="
timeout "${LOG_TAIL_SEC}" tail -F /var/log/messages 2>/dev/null || true
echo ""
echo "=== End of ${LOG_TAIL_SEC}-second tail ==="
} > "${COLLECT_DIR}/logs-tail-${LOG_TAIL_SEC}s.txt" 2>> "${EXEC_LOG}"
echo "Log tail complete." >&2
fi
if [ -d /run/net ]; then
if ls -A /run/net >/dev/null 2>&1; then
tar czf "${COLLECT_DIR}/run-net.tar.gz" -C / run/net 2>> "${EXEC_LOG}" || \
echo "Failed to collect /run/net" > "${COLLECT_DIR}/run-net.tar.gz.error"
else
echo "No data in /run/net" > "${COLLECT_DIR}/run-net.tar.gz.error"
fi
fi
if [ -d /run/finit ]; then
if ls -A /run/finit >/dev/null 2>&1; then
tar czf "${COLLECT_DIR}/run-finit.tar.gz" -C / run/finit 2>> "${EXEC_LOG}" || \
echo "Failed to collect /run/finit" > "${COLLECT_DIR}/run-finit.tar.gz.error"
else
echo "No data in /run/finit" > "${COLLECT_DIR}/run-finit.tar.gz.error"
fi
fi
# Kernel and system state
collect system/dmesg.txt dmesg
collect system/free.txt free -h
collect system/stat.txt cat /proc/stat
collect system/softirqs.txt cat /proc/softirqs
collect system/locks.txt cat /proc/locks
collect system/meminfo.txt cat /proc/meminfo
collect system/file-nr.txt cat /proc/sys/fs/file-nr
collect system/ps.txt ps -o pid,rss,comm
collect system/df.txt df -h
# Finit/init state
if command -v initctl >/dev/null 2>&1; then
collect initctl/cgroup.txt initctl cgroup
collect initctl/status.txt initctl status
fi
# CPU statistics
if command -v mpstat >/dev/null 2>&1; then
collect system/mpstat.txt mpstat -P ALL 1 1
else
echo "mpstat not available" > "${COLLECT_DIR}/mpstat.txt"
fi
# Top output (two samples)
collect system/top.txt sh -c 'top -b -n 2 -d 2'
# Interrupt statistics (before and after 2 second delay)
collect system/interrupts1.txt cat /proc/interrupts
sleep 2
collect system/interrupts2.txt cat /proc/interrupts
# Network information
collect network/ip/addr.json ip -s -d -j addr show
collect network/ip/route.json ip -s -d -j route show
collect network/ip/link.json ip -s -d -j link show
collect network/ip/neigh.json ip -s -d -j neigh show
collect network/ifconfig.txt ifconfig -a
# Collect ethtool information for all ethernet interfaces
if command -v ethtool >/dev/null 2>&1; then
# Get list of ethernet interfaces (any interface with link/ether)
ip -o link show | grep 'link/ether' | awk -F': ' '{print $2}' > "${COLLECT_DIR}/.iface-list" 2>> "${EXEC_LOG}"
if [ -s "${COLLECT_DIR}/.iface-list" ]; then
while IFS= read -r iface; do
# ethtool typically needs root
collect "network/ethtool/${iface}.txt" ethtool "$iface"
collect "network/ethtool/stats-${iface}.txt" ethtool -S "$iface"
collect "network/ethtool/module-${iface}.txt" ethtool -m "$iface"
done < "${COLLECT_DIR}/.iface-list"
fi
rm -f "${COLLECT_DIR}/.iface-list"
fi
if command -v bridge >/dev/null 2>&1; then
collect network/bridge/link.json bridge -d -s -j link show
collect network/bridge/fdb.json bridge -d -s -j fdb show
fi
# Firewall rules
if command -v nft >/dev/null 2>&1; then
collect network/nftables.txt nft list ruleset
fi
# FRR routing information
if command -v vtysh >/dev/null 2>&1; then
collect frr/running-config.txt vtysh -c "show running-config"
collect frr/ip-route.txt vtysh -c "show ip route"
collect frr/ospf-interfaces.txt vtysh -c "show ip ospf interfaces"
collect frr/ospf-neighbor.txt vtysh -c "show ip ospf neighbor"
collect frr/ospf-routes.txt vtysh -c "show ip ospf routes"
collect frr/bgp-summary.txt vtysh -c "show ip bgp summary"
collect frr/bfd-peers.txt vtysh -c "show bfd peers"
fi
# Container information
if command -v podman >/dev/null 2>&1; then
# List all containers
collect podman/ps.txt podman ps -a
# Collect podman system info
collect podman/info.json podman info --format=json
# Get list of containers and inspect each
if podman ps -a --format '{{.Names}}' > "${COLLECT_DIR}/.container-list" 2>> "${EXEC_LOG}"; then
while IFS= read -r container; do
if [ -n "$container" ]; then
safe_name=$(echo "$container" | tr '/' '_')
collect "podman/inspect-${safe_name}.json" podman inspect "$container"
fi
done < "${COLLECT_DIR}/.container-list"
rm -f "${COLLECT_DIR}/.container-list"
fi
fi
# Additional system information
collect system/lsmod.txt lsmod
if command -v lspci >/dev/null 2>&1; then
collect system/lspci.txt lspci -v
else
echo "lspci not available" > "${COLLECT_DIR}/lspci.txt"
fi
if command -v lsusb >/dev/null 2>&1; then
collect system/lsusb.txt lsusb -v
else
echo "lsusb not available" > "${COLLECT_DIR}/lsusb.txt"
fi
# Disk I/O stats
if command -v iostat >/dev/null 2>&1; then
collect system/iostat.txt iostat -x 1 2
else
echo "iostat not available" > "${COLLECT_DIR}/iostat.txt"
fi
# Process resource usage
if command -v pstree >/dev/null 2>&1; then
collect system/pstree.txt pstree -p
else
collect system/pstree.txt ps fax
fi
# Environment and versions
collect system/env.txt env
# Network sockets
if command -v netstat >/dev/null 2>&1; then
collect system/netstat.txt netstat -tunlp
else
collect system/netstat.txt ss -tunlp
fi
for script in $(find "/etc/support.d" -type f -executable 2>/dev/null | sort); do
echo "[$(date -Iseconds)] Running ${script}..." >> "${EXEC_LOG}" 2>&1
if "${script}" "${COLLECT_DIR}" >> "${EXEC_LOG}" 2>&1; then
echo "[$(date -Iseconds)] Success: ${script}" >> "${EXEC_LOG}" 2>&1
else
exit_code=$?
echo "[$(date -Iseconds)] Failed (exit ${exit_code}): ${script}" >> "${EXEC_LOG}" 2>&1
fi
done
# Completion timestamp in log
echo "" >> "${EXEC_LOG}"
echo "Completed: $(date -Iseconds)" >> "${EXEC_LOG}"
# Notify user that collection is done
echo "Collection complete. Creating archive..." >&2
# Create final tar.gz and output to stdout
# Use -C to change to parent directory so paths in archive don't include full path
echo "[$(date -Iseconds)] Changing to work directory: ${WORK_DIR}" >> "${EXEC_LOG}" 2>&1
if ! cd "${WORK_DIR}"; then
echo "[$(date -Iseconds)] ERROR: Failed to cd to ${WORK_DIR}" >> "${EXEC_LOG}" 2>&1
echo "Error: Cannot change to work directory ${WORK_DIR}" >&2
exit 1
fi
echo "[$(date -Iseconds)] Successfully changed to: $(pwd)" >> "${EXEC_LOG}" 2>&1
echo "[$(date -Iseconds)] Creating archive from: $(basename "${COLLECT_DIR}")" >> "${EXEC_LOG}" 2>&1
# Check if password encryption is requested
if [ -n "$PASSWORD" ]; then
if ! command -v gpg >/dev/null 2>&1; then
echo "Error: --password specified but gpg is not available" >&2
exit 1
fi
echo "Encrypting with GPG..." >&2
echo "[$(date -Iseconds)] Starting tar with GPG encryption" >> "${EXEC_LOG}" 2>&1
tar czf - "$(basename "${COLLECT_DIR}")" 2>> "${EXEC_LOG}" | \
gpg --batch --yes --passphrase "$PASSWORD" --pinentry-mode loopback -c 2>> "${EXEC_LOG}"
tar_exit=$?
echo "[$(date -Iseconds)] tar+gpg pipeline exit code: $tar_exit" >> "${EXEC_LOG}" 2>&1
echo "" >&2
echo "WARNING: Remember to share the encryption password out-of-band!" >&2
echo " Do not send it in the same email as the encrypted file." >&2
if [ $tar_exit -ne 0 ]; then
echo "[$(date -Iseconds)] ERROR: tar+gpg failed with exit code $tar_exit" >> "${EXEC_LOG}" 2>&1
exit $tar_exit
fi
else
echo "[$(date -Iseconds)] Starting tar (no encryption)" >> "${EXEC_LOG}" 2>&1
tar czf - "$(basename "${COLLECT_DIR}")" 2>> "${EXEC_LOG}"
tar_exit=$?
echo "[$(date -Iseconds)] tar exit code: $tar_exit" >> "${EXEC_LOG}" 2>&1
if [ $tar_exit -ne 0 ]; then
echo "[$(date -Iseconds)] ERROR: tar failed with exit code $tar_exit" >> "${EXEC_LOG}" 2>&1
exit $tar_exit
fi
fi
echo "[$(date -Iseconds)] Archive creation completed successfully" >> "${EXEC_LOG}" 2>&1
}
cmd_clean()
{
dry_run=0
days=7
# Parse options
while [ $# -gt 0 ]; do
case "$1" in
--dry-run|-n)
dry_run=1
shift
;;
--days|-d)
if [ -z "$2" ] || [ "$2" -le 0 ] 2>/dev/null; then
echo "Error: --days requires a positive number" >&2
exit 1
fi
days="$2"
shift 2
;;
*)
echo "Error: Unknown option '$1'" >&2
echo "Usage: $prognm clean [--dry-run] [--days N]" >&2
exit 1
;;
esac
done
if [ "$dry_run" -eq 1 ]; then
echo "Dry run - no files will be deleted"
echo ""
fi
echo "Looking for support directories older than ${days} days..."
echo ""
# If WORK_DIR is set globally, only search there
# Otherwise search in both /var/lib/support and $HOME
if [ -n "$WORK_DIR" ]; then
search_dirs="$WORK_DIR"
else
search_dirs="/var/lib/support ${HOME}"
fi
total_count=0
for search_dir in $search_dirs; do
if [ ! -d "$search_dir" ]; then
continue
fi
# Use find to locate old support directories
# The pattern matches: support-YYYY-MM-DD* format
find "$search_dir" -maxdepth 1 -type d -name "support-*-20*" -mtime "+${days}" 2>/dev/null | while IFS= read -r dir; do
size=$(du -sh "$dir" 2>/dev/null | awk '{print $1}')
mtime=$(stat -c %y "$dir" 2>/dev/null | cut -d' ' -f1)
if [ "$dry_run" -eq 1 ]; then
echo "Would remove: $dir ($size, modified: $mtime)"
else
echo "Removing: $dir ($size, modified: $mtime)"
rm -rf "$dir"
fi
done
# Count directories found in this location
count=$(find "$search_dir" -maxdepth 1 -type d -name "support-*-20*" -mtime "+${days}" 2>/dev/null | wc -l)
total_count=$((total_count + count))
done
echo ""
if [ "$total_count" -eq 0 ]; then
echo "No support directories older than ${days} days found in /var/lib/support or home directory."
elif [ "$dry_run" -eq 1 ]; then
echo "Found ${total_count} directories. Run without --dry-run to remove them."
else
echo "Removed ${total_count} directories."
fi
}
usage()
{
echo "Usage: $prognm [global-options] <command> [options]"
echo ""
echo "Note: Run with 'sudo' for complete data collection (dmesg, ethtool, etc.)"
echo ""
echo "Global options:"
echo " -u, --unprivileged Allow running without root (some data will be missing)"
echo " -w, --work-dir PATH Use PATH as working directory for collection/cleanup"
echo " (default: /var/lib/support with fallback to \$HOME)"
echo ""
echo "Commands:"
echo " collect [options] Collect system information for support/troubleshooting"
echo " NOTE: may take up to a minute to finish, please wait!"
echo " clean [options] Remove old support collection directories"
echo ""
echo "Options for collect:"
echo " -s, --log-sec SEC Tail /var/log/messages for SEC seconds (default: 30)"
echo " -p, --password [PASS] Encrypt output with GPG. If PASS is omitted, prompts"
echo " interactively or reads from stdin, so possible to do"
echo " echo "\$MYSECRET" | ... (recommended for security)"
echo ""
echo "Options for clean:"
echo " -n, --dry-run Show what would be deleted without deleting"
echo " -d, --days N Remove directories older than N days (default: 7)"
echo ""
echo "Examples:"
echo " sudo $prognm collect > support-data.tar.gz"
echo " sudo $prognm collect -p > support-data.tar.gz.gpg"
echo " sudo $prognm collect --password mypass > support-data.tar.gz.gpg"
echo " sudo $prognm --work-dir /tmp/ram collect > support-data.tar.gz"
echo " ssh user@device 'sudo $prognm collect' > support-data.tar.gz"
echo " $prognm -u collect > support-data.tar.gz (degraded)"
echo " sudo $prognm clean --dry-run"
echo " sudo $prognm clean --days 30"
echo " sudo $prognm --work-dir /tmp/ram clean"
exit 1
}
# Main command dispatcher
# Parse global options first
WORK_DIR=""
ALLOW_UNPRIVILEGED=0
while [ $# -gt 0 ]; do
case "$1" in
-u|--unprivileged)
ALLOW_UNPRIVILEGED=1
shift
;;
-w|--work-dir)
if [ -z "$2" ]; then
echo "Error: --work-dir requires a path argument" >&2
exit 1
fi
WORK_DIR="$2"
shift 2
;;
-*)
# Unknown option - might be a command-specific option
break
;;
*)
# Not an option, must be the command
break
;;
esac
done
if [ $# -lt 1 ]; then
usage
fi
command="$1"
shift
case "$command" in
collect|clean)
# Check if running as root (uid 0)
if [ "$(id -u)" -ne 0 ] && [ "$ALLOW_UNPRIVILEGED" -eq 0 ]; then
echo "Error: This command should be run with 'sudo' for complete data collection." >&2
echo " Use -u/--unprivileged to run as a regular user in degraded mode." >&2
exit 1
fi
if [ "$command" = "collect" ]; then
cmd_collect "$@"
else
cmd_clean "$@"
fi
;;
help|--help|-h)
usage
;;
*)
echo "Error: Unknown command '$command'" >&2
echo "" >&2
usage
;;
esac
+83 -34
View File
@@ -5,6 +5,7 @@
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
@@ -67,52 +68,100 @@ int has_ext(const char *fn, const char *ext)
return 0;
}
static const char *basenm(const char *fn)
int dirlen(const char *path)
{
const char *ptr;
const char *slash;
if (!fn)
return "";
slash = strrchr(path, '/');
if (slash)
return slash - path;
ptr = strrchr(fn, '/');
if (!ptr)
ptr = fn;
return ptr;
return 0;
}
char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len)
const char *basenm(const char *path)
{
if (strstr(fn, "../"))
return NULL; /* relative paths not allowed */
const char *slash;
if (fn[0] == '/') {
strlcpy(buf, fn, len);
return buf; /* allow absolute paths */
}
if (!path)
return NULL;
/* Files in /cfg must end in .cfg */
if (!strncmp(fn, "/cfg/", 5)) {
strlcpy(buf, fn, len);
if (!has_ext(fn, ".cfg"))
strlcat(buf, ".cfg", len);
slash = strrchr(path, '/');
if (slash)
return slash[1] ? slash + 1 : NULL;
return buf;
return path;
}
static int path_allowed(const char *path)
{
const char *accepted[] = {
"/media/",
"/cfg/",
getenv("HOME"),
NULL
};
for (int i = 0; accepted[i]; i++) {
if (!strncmp(path, accepted[i], strlen(accepted[i])))
return 1;
}
return 0;
}
char *cfg_adjust(const char *path, const char *template, bool sanitize)
{
char *expanded = NULL, *resolved = NULL;
const char *basename;
int dlen;
dlen = dirlen(path);
basename = basenm(path) ? : basenm(template);
if (!basename)
goto err;
if (sanitize) {
if (strstr(path, "../"))
goto err;
if (path[0] == '/') {
if (!path_allowed(path))
goto err;
}
}
if (asprintf(&expanded, "%s%.*s/%s%s",
path[0] == '/' ? "" : "/cfg/",
dlen, path,
basename,
strchr(basename, '.') ? "" : ".cfg") < 0)
goto err;
if (sanitize) {
resolved = realpath(expanded, NULL);
if (!resolved) {
if (errno == ENOENT)
goto out;
else
goto err;
}
/* File exists, make sure that the resolved symlink
* still matches the whitelist.
*/
if (!path_allowed(resolved))
goto err;
/* Files ending with .cfg belong in /cfg */
if (has_ext(fn, ".cfg")) {
snprintf(buf, len, "/cfg/%s", fn);
return buf;
free(expanded);
expanded = resolved;
}
if (strlen(fn) > 0 && fn[0] == '.' && tmpl) {
if (fn[1] == '/' && fn[2] != 0)
strlcpy(buf, fn, len);
else
strlcpy(buf, basenm(tmpl), len);
} else
strlcpy(buf, fn, len);
out:
return expanded;
return buf;
err:
free(resolved);
free(expanded);
return NULL;
}
+6 -4
View File
@@ -1,17 +1,19 @@
/* SPDX-License-Identifier: ISC */
#ifndef BIN_UTIL_H_
#define BIN_UTIL_H_
#include <stdbool.h>
#include <stdio.h>
#include <libite/lite.h>
#define ERRMSG "Error: "
#define INFMSG "Note: "
int yorn (const char *fmt, ...);
int yorn (const char *fmt, ...);
int files (const char *path, const char *stripext);
int files (const char *path, const char *stripext);
int has_ext (const char *fn, const char *ext);
char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len);
const char *basenm (const char *fn);
int has_ext (const char *fn, const char *ext);
char *cfg_adjust (const char *path, const char *template, bool sanitize);
#endif /* BIN_UTIL_H_ */
+35 -10
View File
@@ -130,15 +130,7 @@ int parse_ospf_areas(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp
int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
{
const char *static_debug = "! OSPF default debug\
debug ospf bfd\n\
debug ospf packet all detail\n\
debug ospf ism\n\
debug ospf nsm\n\
debug ospf default-information\n\
debug ospf nssa\n\
! OSPF configuration\n";
struct lyd_node *areas, *default_route;
struct lyd_node *areas, *default_route, *debug;
const char *router_id;
int bfd_enabled = 0;
int num_areas = 0;
@@ -151,7 +143,40 @@ debug ospf nssa\n\
}
fputs(FRR_STATIC_CONFIG, fp);
fputs(static_debug, fp);
/* Handle OSPF debug configuration */
debug = lydx_get_child(ospf, "debug");
if (debug) {
int any_debug = 0;
if (lydx_get_bool(debug, "bfd")) {
fputs("debug ospf bfd\n", fp);
any_debug = 1;
}
if (lydx_get_bool(debug, "packet")) {
fputs("debug ospf packet all detail\n", fp);
any_debug = 1;
}
if (lydx_get_bool(debug, "ism")) {
fputs("debug ospf ism\n", fp);
any_debug = 1;
}
if (lydx_get_bool(debug, "nsm")) {
fputs("debug ospf nsm\n", fp);
any_debug = 1;
}
if (lydx_get_bool(debug, "default-information")) {
fputs("debug ospf default-information\n", fp);
any_debug = 1;
}
if (lydx_get_bool(debug, "nssa")) {
fputs("debug ospf nssa\n", fp);
any_debug = 1;
}
if (any_debug)
fputs("!\n", fp);
}
areas = lydx_get_child(ospf, "areas");
router_id = lydx_get_cattr(ospf, "explicit-router-id");
+1 -1
View File
@@ -22,7 +22,7 @@ MODULES=(
"ieee802-dot1q-types@2022-10-29.yang"
"infix-ip@2024-09-16.yang"
"infix-if-type@2025-02-12.yang"
"infix-routing@2024-11-27.yang"
"infix-routing@2025-12-02.yang"
"ieee802-dot1ab-lldp@2022-03-15.yang"
"infix-lldp@2025-05-05.yang"
"infix-dhcp-common@2025-01-29.yang"
+46
View File
@@ -20,6 +20,13 @@ module infix-routing {
contact "kernelkit@googlegroups.com";
description "Deviations and augments for ietf-routing and ietf-ospf.";
revision 2025-12-02 {
description "Add configurable OSPF debug logging container.
Allows users to enable specific OSPF debug categories
(bfd, packet, ism, nsm, default-information, nssa) for troubleshooting.
All debug options default to disabled to prevent log flooding.";
reference "Issue #1281";
}
revision 2024-11-27 {
description "Deviate address-family in OSPF";
reference "internal";
@@ -278,6 +285,45 @@ module infix-routing {
}
}
}
augment "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf" {
description "Add support for configurable OSPF debug logging.
Allows users to enable specific OSPF debug categories for troubleshooting.
All debug options default to disabled to prevent log flooding in
production environments.";
container debug {
description "OSPF debug logging configuration";
leaf bfd {
type boolean;
default false;
description "Enable OSPF BFD (Bidirectional Forwarding Detection) debug messages";
}
leaf packet {
type boolean;
default false;
description "Enable detailed OSPF packet debug messages (all packet types)";
}
leaf ism {
type boolean;
default false;
description "Enable OSPF Interface State Machine debug messages";
}
leaf nsm {
type boolean;
default false;
description "Enable OSPF Neighbor State Machine debug messages";
}
leaf default-information {
type boolean;
default false;
description "Enable OSPF default route origination debug messages";
}
leaf nssa {
type boolean;
default false;
description "Enable OSPF NSSA (Not-So-Stubby Area) debug messages";
}
}
}
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface" {
deviate add {
must "count(../../../../ospf:areas/ospf:area/ospf:interfaces/ospf:interface[ospf:name=current()/name]) <= 1" {
+11 -15
View File
@@ -28,7 +28,7 @@
const uint8_t kplugin_infix_major = 1;
const uint8_t kplugin_infix_minor = 0;
static void cd_home(kcontext_t *ctx)
static const char *cd_home(kcontext_t *ctx)
{
const char *user = "root";
ksession_t *session;
@@ -43,6 +43,8 @@ static void cd_home(kcontext_t *ctx)
pw = getpwnam(user);
chdir(pw->pw_dir);
return user;
}
static int systemf(const char *fmt, ...)
@@ -118,7 +120,7 @@ int infix_erase(kcontext_t *ctx)
cd_home(ctx);
return systemf("erase %s", path);
return systemf("erase -s %s", path);
}
int infix_files(kcontext_t *ctx)
@@ -146,8 +148,8 @@ int infix_copy(kcontext_t *ctx)
{
kpargv_t *pargv = kcontext_pargv(ctx);
const char *src, *dst;
const char *username;
char user[256] = "";
char validate[8] = "";
kparg_t *parg;
src = kparg_value(kpargv_find(pargv, "src"));
@@ -159,26 +161,20 @@ int infix_copy(kcontext_t *ctx)
if (parg)
snprintf(user, sizeof(user), "-u %s", kparg_value(parg));
cd_home(ctx);
username = ksession_user(kcontext_session(ctx));
parg = kpargv_find(pargv, "validate");
if (parg)
strlcpy(validate, "-n", sizeof(validate));
return systemf("sudo -u %s copy %s %s %s", username, user, src, dst);
/* Ensure we run the copy command as the logged-in user, not root (klishd) */
return systemf("doas -u %s copy -s %s %s %s %s", cd_home(ctx), validate, user, src, dst);
}
int infix_shell(kcontext_t *ctx)
{
const char *user = "root";
ksession_t *session;
const char *user = cd_home(ctx);
pid_t pid;
int rc;
session = kcontext_session(ctx);
if (session) {
user = ksession_user(session);
if (!user)
user = "root";
}
pid = fork();
if (pid == -1)
return -1;
+7 -6
View File
@@ -167,6 +167,7 @@
<PARAM name="src" ptype="/DATASTORE" help="Source datastore or file, e.g., tftp://ip/file"/>
<PARAM name="dst" ptype="/RW_DATASTORE" help="Destination datastore or file, e.g., scp://ip/file"/>
<SWITCH name="optional" min="0">
<COMMAND name="validate" help="Validate configuration without applying"/>
<PARAM name="user" ptype="/STRING" help="Remote username (interactive password)"/>
</SWITCH>
<ACTION sym="copy@infix" in="tty" out="tty" interrupt="true"/>
@@ -338,17 +339,17 @@
</PARAM>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
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
</ACTION>
</COMMAND>
@@ -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=""
+14
View File
@@ -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
@@ -1,7 +1,13 @@
=== OSPF Basic
==== Description
Very basic OSPF test just test that OSPF sends HELLO packets between the DUTs
and that they exchange routes, ending with a simple connectivity check.
Verifies basic OSPF functionality by configuring two routers (R1 and R2)
with OSPF on their interconnecting link. The test ensures OSPF
neighbors are established, routes are exchanged between the routers, and
end-to-end connectivity is achieved.
An end-device (HOST) is connected to R2 on an interface without OSPF enabled.
This verifies that OSPF status information remains accessible when a router
has non-OSPF interfaces.
==== Topology
ifdef::topdoc[]
@@ -19,6 +25,7 @@ endif::topdoc[]
. Set up topology and attach to target DUTs
. Configure targets
. Wait for OSPF routes
. Verify R2 OSPF neighbors with non-OSPF interface
. Test connectivity from PC:data to 192.168.200.1
+105 -63
View File
@@ -1,15 +1,24 @@
#!/usr/bin/env python3
"""
OSPF Basic
"""OSPF Basic
Verifies basic OSPF functionality by configuring two routers (R1 and R2)
with OSPF on their interconnecting link. The test ensures OSPF
neighbors are established, routes are exchanged between the routers, and
end-to-end connectivity is achieved.
An end-device (HOST) is connected to R2 on an interface without OSPF enabled.
This verifies that OSPF status information remains accessible when a router
has non-OSPF interfaces.
Very basic OSPF test just test that OSPF sends HELLO packets between the DUTs
and that they exchange routes, ending with a simple connectivity check.
"""
# TODO: Remove HOST node once Infamy supports unconnected ports in topologies
import infamy
import infamy.route as route
from infamy.util import until, parallel
def config_target1(target, data, link):
target.put_config_dict("ietf-interfaces", {
"interfaces": {
@@ -30,8 +39,8 @@ def config_target1(target, data, link):
"ipv4": {
"forwarding": True,
"address": [{
"ip": "192.168.50.1",
"prefix-length": 24
"ip": "192.168.50.1",
"prefix-length": 24
}]
}
},
@@ -40,8 +49,8 @@ def config_target1(target, data, link):
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.100.1",
"prefix-length": 32
"ip": "192.168.100.1",
"prefix-length": 32
}]
}
}
@@ -56,38 +65,34 @@ def config_target1(target, data, link):
target.put_config_dict("ietf-routing", {
"routing": {
"control-plane-protocols": {
"control-plane-protocol": [
{
"type": "infix-routing:static",
"name": "default",
"static-routes": {
"ipv4": {
"route": [{
"destination-prefix": "192.168.33.1/32",
"next-hop": {
"special-next-hop": "blackhole"
}
}]
}
"control-plane-protocol": [{
"type": "infix-routing:static",
"name": "default",
"static-routes": {
"ipv4": {
"route": [{
"destination-prefix": "192.168.33.1/32",
"next-hop": {
"special-next-hop": "blackhole"
}
}]
}
},
{
}
}, {
"type": "infix-routing:ospfv2",
"name": "default",
"ospf": {
"redistribute": {
"redistribute": [{
"protocol": "static"
},
{
}, {
"protocol": "connected"
}]
},
"areas": {
"area": [{
"area-id": "0.0.0.0",
"interfaces":
{
"interfaces": {
"interface": [{
"enabled": True,
"name": link,
@@ -103,35 +108,43 @@ def config_target1(target, data, link):
}
})
def config_target2(target, link):
def config_target2(target, link, data):
target.put_config_dict("ietf-interfaces", {
"interfaces": {
"interface": [
{
"name": link,
"enabled": True,
"ipv4": {
"forwarding": True,
"address": [{
"ip": "192.168.50.2",
"prefix-length": 24
}]
}
},
{
"name": "lo",
"enabled": True,
"forwarding": True,
"ipv4": {
"address": [{
"ip": "192.168.200.1",
"prefix-length": 32
}]
}
}
]
}
})
"interfaces": {
"interface": [{
"name": link,
"enabled": True,
"ipv4": {
"forwarding": True,
"address": [{
"ip": "192.168.50.2",
"prefix-length": 24
}]
}
}, {
"name": data,
"enabled": True,
"ipv4": {
"forwarding": True,
"address": [{
"ip": "192.168.60.1",
"prefix-length": 24
}]
}
}, {
"name": "lo",
"enabled": True,
"forwarding": True,
"ipv4": {
"address": [{
"ip": "192.168.200.1",
"prefix-length": 32
}]
}
}]
}
})
target.put_config_dict("ietf-system", {
"system": {
@@ -141,8 +154,7 @@ def config_target2(target, link):
target.put_config_dict("ietf-routing", {
"routing": {
"control-plane-protocols": {
"control-plane-protocol": [
{
"control-plane-protocol": [{
"type": "infix-routing:ospfv2",
"name": "default",
"ospf": {
@@ -154,7 +166,7 @@ def config_target2(target, link):
"areas": {
"area": [{
"area-id": "0.0.0.0",
"interfaces":{
"interfaces": {
"interface": [{
"enabled": True,
"name": link,
@@ -165,31 +177,61 @@ def config_target2(target, link):
}]
}
}
}
]
}]
}
}
})
def config_host(target, link):
target.put_config_dict("ietf-interfaces", {
"interfaces": {
"interface": [{
"name": link,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.60.2",
"prefix-length": 24
}]
}
}]
}
})
target.put_config_dict("ietf-system", {
"system": {
"hostname": "HOST"
}
})
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUTs"):
env = infamy.Env()
R1 = env.attach("R1", "mgmt")
R2 = env.attach("R2", "mgmt")
HOST = env.attach("HOST", "mgmt")
with test.step("Configure targets"):
_, R1data = env.ltop.xlate("R1", "data")
_, R2link = env.ltop.xlate("R2", "link")
_, R1link= env.ltop.xlate("R1", "link")
_, R1link = env.ltop.xlate("R1", "link")
_, R2data = env.ltop.xlate("R2", "data")
_, HOSTlink = env.ltop.xlate("HOST", "link")
parallel(config_target1(R1, R1data, R1link),
config_target2(R2, R2link))
config_target2(R2, R2link, R2data),
config_host(HOST, HOSTlink))
with test.step("Wait for OSPF routes"):
print("Waiting for OSPF routes..")
until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
until(lambda: route.ipv4_route_exist(R2, "192.168.100.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
until(lambda: route.ipv4_route_exist(R2, "192.168.33.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
with test.step("Verify R2 OSPF neighbors with non-OSPF interface"):
# Regression test for #1169
assert route.ospf_has_neighbors(R2)
with test.step("Test connectivity from PC:data to 192.168.200.1"):
_, hport0 = env.ltop.xlate("PC", "data")
with infamy.IsolatedMacVlan(hport0) as ns0:
+14 -6
View File
@@ -8,26 +8,34 @@ graph "2x2" {
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
PC [
label="PC | { <mgmt1> mgmt1 | <data> data | <mgmt2> mgmt2 }",
pos="20,80!",
label="PC | { <mgmt1> mgmt1 | <data> data | <mgmt2> mgmt2 | <mgmt3> mgmt3 }",
pos="20,30!",
requires="controller",
];
R1 [
label="{ <mgmt> mgmt | <data> data | <link> link} | R1 \n 192.168.100.1/32 \n(lo)",
pos="250,80!",
pos="160,60!",
requires="infix",
];
R2 [
label="{ <link> link | <mgmt> mgmt | <data> data } | R2 \n 192.168.200.1/32 \n(lo)",
pos="250,30!",
pos="160,30!",
requires="infix",
];
HOST [
label="{ <link> link | <mgmt> mgmt } | HOST \n end-device",
pos="153,0!",
requires="infix",
];
PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"]
PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"]
PC:data -- R1:data [color="black", headlabel="192.168.10.1/24", taillabel="192.168.10.2/24", fontcolor="black"]
PC:mgmt3 -- HOST:mgmt [requires="mgmt", color="lightgray"]
PC:data -- R1:data [color="black", headlabel="192.168.10.1/24", taillabel="192.168.10.2/24", labeldistance=6, fontcolor="black"]
R1:link -- R2:link [headlabel="192.168.50.2/24", taillabel="192.168.50.1/24", labeldistance=1, fontcolor="black", color="black"]
R2:data -- HOST:link [headlabel="192.168.60.2/24", taillabel="192.168.60.1/24", labeldistance=1, fontcolor="black", color="black"]
}
+62 -37
View File
@@ -3,74 +3,99 @@
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 2x2 Pages: 1 -->
<svg width="713pt" height="198pt"
viewBox="0.00 0.00 713.06 198.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 194.01)">
<svg width="720pt" height="306pt"
viewBox="0.00 0.00 720.00 306.08" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 302.52)">
<title>2x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-194.01 709.06,-194.01 709.06,4 -4,4"/>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-302.52 717.06,-302.52 717.06,4 -4,4"/>
<!-- PC -->
<g id="node1" class="node">
<title>PC</title>
<polygon fill="none" stroke="black" points="0,-120.51 0,-189.51 91,-189.51 91,-120.51 0,-120.51"/>
<text text-anchor="middle" x="16.5" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">PC</text>
<polyline fill="none" stroke="black" points="33,-120.51 33,-189.51 "/>
<polygon fill="none" stroke="black" points="0,-97.51 0,-189.51 91,-189.51 91,-97.51 0,-97.51"/>
<text text-anchor="middle" x="16.5" y="-139.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">PC</text>
<polyline fill="none" stroke="black" points="33,-97.51 33,-189.51 "/>
<text text-anchor="middle" x="62" y="-174.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="33,-166.51 91,-166.51 "/>
<text text-anchor="middle" x="62" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="33,-143.51 91,-143.51 "/>
<text text-anchor="middle" x="62" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
<polyline fill="none" stroke="black" points="33,-120.51 91,-120.51 "/>
<text text-anchor="middle" x="62" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt3</text>
</g>
<!-- R1 -->
<g id="node2" class="node">
<title>R1</title>
<polygon fill="none" stroke="black" points="490.06,-120.51 490.06,-189.51 705.06,-189.51 705.06,-120.51 490.06,-120.51"/>
<text text-anchor="middle" x="515.06" y="-174.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="490.06,-166.51 540.06,-166.51 "/>
<text text-anchor="middle" x="515.06" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="490.06,-143.51 540.06,-143.51 "/>
<text text-anchor="middle" x="515.06" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="540.06,-120.51 540.06,-189.51 "/>
<text text-anchor="middle" x="622.56" y="-166.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1 </text>
<text text-anchor="middle" x="622.56" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.100.1/32 </text>
<text text-anchor="middle" x="622.56" y="-136.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
<polygon fill="none" stroke="black" points="498.06,-229.02 498.06,-298.02 713.06,-298.02 713.06,-229.02 498.06,-229.02"/>
<text text-anchor="middle" x="523.06" y="-282.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="498.06,-275.02 548.06,-275.02 "/>
<text text-anchor="middle" x="523.06" y="-259.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="498.06,-252.02 548.06,-252.02 "/>
<text text-anchor="middle" x="523.06" y="-236.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="548.06,-229.02 548.06,-298.02 "/>
<text text-anchor="middle" x="630.56" y="-274.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1 </text>
<text text-anchor="middle" x="630.56" y="-259.82" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.100.1/32 </text>
<text text-anchor="middle" x="630.56" y="-244.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
</g>
<!-- PC&#45;&#45;R1 -->
<g id="edge1" class="edge">
<title>PC:mgmt1&#45;&#45;R1:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-178.01C91.5,-178.01 489.56,-178.01 489.56,-178.01"/>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-178.51C91.5,-178.51 497.56,-286.52 497.56,-286.52"/>
</g>
<!-- PC&#45;&#45;R1 -->
<g id="edge3" class="edge">
<g id="edge4" class="edge">
<title>PC:data&#45;&#45;R1:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M91.5,-155.01C91.5,-155.01 489.56,-155.01 489.56,-155.01"/>
<text text-anchor="middle" x="430.56" y="-158.81" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.1/24</text>
<text text-anchor="middle" x="150.5" y="-158.81" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.2/24</text>
<path fill="none" stroke="black" stroke-width="2" d="M91.5,-155.51C91.5,-155.51 497.56,-263.52 497.56,-263.52"/>
<text text-anchor="middle" x="438.49" y="-270.35" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.1/24</text>
<text text-anchor="middle" x="150.57" y="-141.29" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.2/24</text>
</g>
<!-- R2 -->
<g id="node3" class="node">
<title>R2</title>
<polygon fill="none" stroke="black" points="490.06,-0.5 490.06,-69.5 705.06,-69.5 705.06,-0.5 490.06,-0.5"/>
<text text-anchor="middle" x="515.06" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="490.06,-46.5 540.06,-46.5 "/>
<text text-anchor="middle" x="515.06" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="490.06,-23.5 540.06,-23.5 "/>
<text text-anchor="middle" x="515.06" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="540.06,-0.5 540.06,-69.5 "/>
<text text-anchor="middle" x="622.56" y="-46.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2 </text>
<text text-anchor="middle" x="622.56" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.200.1/32 </text>
<text text-anchor="middle" x="622.56" y="-16.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
<polygon fill="none" stroke="black" points="498.06,-109.01 498.06,-178.01 713.06,-178.01 713.06,-109.01 498.06,-109.01"/>
<text text-anchor="middle" x="523.06" y="-162.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="498.06,-155.01 548.06,-155.01 "/>
<text text-anchor="middle" x="523.06" y="-139.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="498.06,-132.01 548.06,-132.01 "/>
<text text-anchor="middle" x="523.06" y="-116.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="548.06,-109.01 548.06,-178.01 "/>
<text text-anchor="middle" x="630.56" y="-154.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2 </text>
<text text-anchor="middle" x="630.56" y="-139.81" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.200.1/32 </text>
<text text-anchor="middle" x="630.56" y="-124.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
</g>
<!-- PC&#45;&#45;R2 -->
<g id="edge2" class="edge">
<title>PC:mgmt2&#45;&#45;R2:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-132.01C91.5,-132.01 489.56,-35 489.56,-35"/>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-131.51C91.5,-131.51 497.56,-143.51 497.56,-143.51"/>
</g>
<!-- HOST -->
<g id="node4" class="node">
<title>HOST</title>
<polygon fill="none" stroke="black" points="499.05,-0.5 499.05,-46.5 656.05,-46.5 656.05,-0.5 499.05,-0.5"/>
<text text-anchor="middle" x="524.05" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="499.05,-23.5 549.05,-23.5 "/>
<text text-anchor="middle" x="524.05" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="549.05,-0.5 549.05,-46.5 "/>
<text text-anchor="middle" x="602.55" y="-27.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">HOST </text>
<text text-anchor="middle" x="602.55" y="-12.3" font-family="DejaVu Sans Mono, Book" font-size="14.00"> end&#45;device</text>
</g>
<!-- PC&#45;&#45;HOST -->
<g id="edge3" class="edge">
<title>PC:mgmt3&#45;&#45;HOST:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-108.51C91.5,-108.51 498.55,-11.5 498.55,-11.5"/>
</g>
<!-- R1&#45;&#45;R2 -->
<g id="edge4" class="edge">
<g id="edge5" class="edge">
<title>R1:link&#45;&#45;R2:link</title>
<path fill="none" stroke="black" stroke-width="2" d="M514.56,-120.01C514.56,-120.01 514.56,-70 514.56,-70"/>
<text text-anchor="middle" x="518.78" y="-75.36" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.2/24</text>
<text text-anchor="middle" x="510.33" y="-107.25" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.1/24</text>
<path fill="none" stroke="black" stroke-width="2" d="M522.56,-228.52C522.56,-228.52 522.56,-178.51 522.56,-178.51"/>
<text text-anchor="middle" x="526.78" y="-183.88" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.2/24</text>
<text text-anchor="middle" x="518.33" y="-215.76" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.1/24</text>
</g>
<!-- R2&#45;&#45;HOST -->
<g id="edge6" class="edge">
<title>R2:data&#45;&#45;HOST:link</title>
<path fill="none" stroke="black" stroke-width="2" d="M522.56,-108.51C522.56,-108.51 523.55,-46.5 523.55,-46.5"/>
<text text-anchor="middle" x="527.63" y="-51.93" font-family="DejaVu Serif, Book" font-size="14.00">192.168.60.2/24</text>
<text text-anchor="middle" x="518.48" y="-95.68" font-family="DejaVu Serif, Book" font-size="14.00">192.168.60.1/24</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -26,7 +26,6 @@ endif::topdoc[]
. Set hostname to 'container-host'
. Create enabled container from bundled OCI image
. Verify container has started
. Let container settle
. Disable container
. Verify container has stopped
. Re-enable container
@@ -1,14 +1,11 @@
=== Container environment variables
==== Description
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
==== Topology
ifdef::topdoc[]
@@ -25,11 +22,10 @@ endif::topdoc[]
==== Test sequence
. Set up topology and attach to target DUT
. Configure data interface with static IPv4
. Create container with environment variables
. Set up container with environment variables
. Verify container has started
. Verify environment variables are available via HTTP
. Verify basic connectivity to data interface
. Verify environment variables in HTTP response
. Verify environment variables in CGI response
<<<
@@ -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 infamy.Test() as test:
}
})
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
with test.step("Set up container with environment variables"):
cgi = [
'#!/bin/sh',
'# CGI script to output environment variables',
'echo "Content-Type: text/plain"',
'echo ""'
]
# 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
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 @@ done
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()
@@ -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
+2 -3
View File
@@ -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
@@ -10,22 +10,55 @@ with three scenarios:
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(ns, expr, must):
"""Wrap netns.must_receive() with common defaults"""
return ns.must_receive(expr, timeout=3, must=must)
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
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 @@ with infamy.Test() as test:
{
"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 @@ with infamy.Test() as test:
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()
+24
View File
@@ -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()
+24
View File
@@ -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()
-23
View File
@@ -13,27 +13,4 @@ with infamy.Test() as test:
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()
+2
View File
@@ -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 \
+55 -9
View File
@@ -1,26 +1,72 @@
"""Fugly URL fetcher"""
"""Simple HTTP URL fetcher and content checker
This module provides the a lightweight wrapper around urllib for testing
HTTP endpoints. It's designed specifically for test scenarios where you
need to:
- Fetch HTTP content and verify it contains expected strings
- Perform checks from within network namespaces
- Validate multiple content patterns in a single request
- Handle network errors gracefully in test environments
Example usage:
# Single needle check
url = Furl("http://example.com/api")
if url.check("success"):
print("API returned success")
# Multiple needle check (all must match)
if url.check(["user: alice", "status: active", "role: admin"]):
print("All user details found")
# Check from network namespace
with IsolatedMacVlan("eth0") as ns:
if url.nscheck(ns, "expected content"):
print("Content verified from namespace")
"""
import urllib.error
import urllib.parse
import urllib.request
class Furl:
"""Furl wraps urllib in a way similar to curl"""
def __init__(self, url):
"""Create new URL checker"""
self.url = urllib.parse.quote(url, safe='/:')
def check(self, needle, timeout=10):
"""Connect to web server URL, fetch body and check for needle"""
def check(self, needles, timeout=10):
"""Connect to web server URL, fetch body and check for needle(s)
Args:
needles: String or list of strings to search for in response
timeout: Request timeout in seconds
Returns:
bool: True if all needles found in response, False otherwise
"""
# Backwards compat, make needles a list for uniform processing
if isinstance(needles, str):
needles = [needles]
try:
with urllib.request.urlopen(self.url, timeout=timeout) as response:
text = response.read().decode('utf-8')
#print(text)
return needle in text
except urllib.error.URLError as _:
return all(needle in text for needle in needles)
except urllib.error.URLError:
return False
except ConnectionResetError:
return False
def nscheck(self, netns, needle):
""""Call check() from netns"""
return netns.call(lambda: self.check(needle))
def nscheck(self, netns, needles):
""""Call check() from netns
Args:
netns: Network namespace to call from
needles: String or list of strings to search for in response
Returns:
bool: True if all needles found in response, False otherwise
"""
return netns.call(lambda: self.check(needles))
-1
View File
@@ -150,7 +150,6 @@ class Device(Transport):
url = f"{self.yang_url}/{schema_name}"
data = self._get_raw(url=url, parse=False)
sys.stdout.write(f"Downloading YANG model {schema_name} ...\r\033[K")
data = data.decode('utf-8')
with open(f"{yangdir}/{schema_name}", 'w') as json_file:
json_file.write(data)
+10
View File
@@ -114,3 +114,13 @@ def ospf_is_area_nssa(target, area_id):
return True
return False
def ospf_has_neighbors(target):
ospf = _get_ospf_status(target)
for area in ospf.get("areas", {}).get("area", []):
for interface in area.get("interfaces", {}).get("interface", []):
if interface.get("neighbors"):
return True
return False
+7 -1
View File
@@ -131,6 +131,13 @@ class Topology:
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 @@ class Topology:
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))
+16 -7
View File
@@ -1,9 +1,10 @@
base-dir := $(lastword $(subst :, ,$(BR2_EXTERNAL)))
test-dir := $(BR2_EXTERNAL_INFIX_PATH)/test
test-dir ?= $(BR2_EXTERNAL_INFIX_PATH)/test
ninepm := $(BR2_EXTERNAL_INFIX_PATH)/test/9pm/9pm.py
NINEPM_PROJ_CONF ?= $(BR2_EXTERNAL_INFIX_PATH)/test/9pm-proj.yaml
spec-dir := $(test-dir)/spec
test-specification := $(O)/images/test-specification.pdf
test-specification := $(BINARIES_DIR)/test-specification.pdf
test-report := $(BINARIES_DIR)/test-report.pdf
UNIT_TESTS ?= $(test-dir)/case/all-repo.yaml $(test-dir)/case/all-unit.yaml
TESTS ?= $(test-dir)/case/all.yaml
@@ -14,8 +15,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
@@ -34,9 +34,18 @@ test-sh:
test-spec:
@esc_infix_name="$(echo $(INFIX_NAME) | sed 's/\//\\\//g')"; \
sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' $(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc
sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' \
$(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc
@$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH)
@asciidoctor-pdf --failure-level INFO --theme $(spec-dir)/theme.yml -a pdf-fontsdir=$(spec-dir)/fonts -o $(test-specification) $(spec-dir)/Readme.adoc
@asciidoctor-pdf --failure-level INFO --theme $(spec-dir)/theme.yml \
-a pdf-fontsdir=$(spec-dir)/fonts \
-o $(test-specification) $(spec-dir)/Readme.adoc
test-report:
asciidoctor-pdf --theme $(spec-dir)/theme.yml \
-a logo="image:$(LOGO)" \
-a pdf-fontsdir=$(spec-dir)/fonts \
-o $(test-report) $(test-dir)/.log/last/report.adoc
# Unit tests run with random (-r) hostname and container name to
# prevent race conditions when running in CI environments.
+2
View File
@@ -25,6 +25,7 @@ graph "dual" {
label="{ <e1> e1 | <e2> e2 | <e3> e3 } | dut1 | { <e4> e4 | <e5> e5 | <e6> 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> e1 | <e2> e2 | <e3> e3 } | dut2 | { <e4> e4 | <e5> e5 | <e6> e6 }",
pos="10,12!",
provides="infix",
expected_boot="primary",
qn_console=9002,
qn_mem="384M",
qn_usb="dut2.usb"
+4
View File
@@ -24,6 +24,7 @@ graph "quad" {
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut1 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> 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> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut2 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> 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> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut3 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> 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> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut4 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
pos="10,0!",
provides="infix",
expected_boot="primary",
qn_console=9004,
qn_mem="384M",
qn_usb="dut4.usb"