mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7c031270 | ||
|
|
1d23f73bfd | ||
|
|
119b92f94e | ||
|
|
96fad208cb | ||
|
|
0a3879f521 | ||
|
|
6f6bb412f9 | ||
|
|
b04bae1ad6 | ||
|
|
73edf1bbd6 | ||
|
|
2557eb4900 | ||
|
|
f71722c111 | ||
|
|
5ea99680c8 | ||
|
|
55b038d999 | ||
|
|
83b55ee4d8 | ||
|
|
68e7167902 | ||
|
|
25a26fe7c6 | ||
|
|
6c8bd25cee | ||
|
|
ea300137ef | ||
|
|
44e9c87376 | ||
|
|
e708f21444 | ||
|
|
3508b8d0bb | ||
|
|
eec867bd5f | ||
|
|
daa0bd81e8 | ||
|
|
4ccc70aeee | ||
|
|
923443b620 | ||
|
|
9ad51c31bf | ||
|
|
6272c9c46c | ||
|
|
4750f96774 | ||
|
|
81bc143883 | ||
|
|
58b3fb30be | ||
|
|
6dfc305f0b | ||
|
|
8ca3d3a3ff | ||
|
|
7ae4de3a17 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+161
@@ -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
|
||||
Executable
+26
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Regular → Executable
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Executable
+635
@@ -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
@@ -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
@@ -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_ */
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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=""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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--R1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>PC:mgmt1--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--R1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<g id="edge4" class="edge">
|
||||
<title>PC:data--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--R2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>PC:mgmt2--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-device</text>
|
||||
</g>
|
||||
<!-- PC--HOST -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>PC:mgmt3--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--R2 -->
|
||||
<g id="edge4" class="edge">
|
||||
<g id="edge5" class="edge">
|
||||
<title>R1:link--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--HOST -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>R2:data--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,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()
|
||||
|
||||
Executable
+24
@@ -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()
|
||||
Executable
+24
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user