From 21faf19abd2b36375b3ef3f38dce99edf45076fe Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 9 Feb 2026 11:13:21 +0100 Subject: [PATCH 01/29] board/common: mnt: drop tune2fs and speed up mounting Calling tune2fs for ext4 partitions at boot costs more than one second boot time on 32-bit Arm systems, with very little gain. Dropping this restores the default periodic fsck *and* saves boot time. Instead of calling sgdisk four times, call it only when resizing and instead use sysfs to find named paritions. Saves seconds on boards with weaker CPU and slow media. Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/libexec/infix/mnt | 124 +++++++++------------- 1 file changed, 50 insertions(+), 74 deletions(-) diff --git a/board/common/rootfs/usr/libexec/infix/mnt b/board/common/rootfs/usr/libexec/infix/mnt index 4421613f3..dfe732fb2 100755 --- a/board/common/rootfs/usr/libexec/infix/mnt +++ b/board/common/rootfs/usr/libexec/infix/mnt @@ -59,17 +59,9 @@ is_mmc() { [ -n "$mmc" ] && return $mmc - # Check if primary or secondary partition (our rootfs) is on MMC - for label in primary secondary; do - devname=$(find_partition_by_label "$label" 2>/dev/null) - if [ -n "$devname" ]; then - case "$devname" in - mmcblk*) - mmc=0 - return 0 - ;; - esac - fi + # Fast sysfs check — avoids triggering the slow partition scan + for d in /sys/class/block/mmcblk[0-9]; do + [ -d "$d" ] && mmc=0 && return 0 done mmc=1 @@ -78,78 +70,68 @@ is_mmc() wait_mmc() { - # Try up to 50 times with 0.2s sleep = 10 second timeout - for _ in $(seq 50); do - if ls /dev/mmcblk* >/dev/null 2>&1; then - logger $opt -p user.notice -t "$nm" "MMC device available after delay" - return 0 - fi - sleep .2 + tries=50 + while [ $tries -gt 0 ]; do + for d in /dev/mmcblk[0-9]; do + if [ -b "$d" ]; then + logger $opt -p user.notice -t "$nm" "MMC device available after delay" + return 0 + fi + done + sleep .2 + tries=$((tries - 1)) done logger $opt -p user.warn -t "$nm" "Timeout waiting for MMC device" return 1 } -# This early on we don't have the luxury of /dev/disk/by-label/$1 +# Read filesystem label from an ext2/3/4 formatted whole disk. +# Superblock is at byte 1024, magic 0xEF53 at offset 56, label at 120. +# Handles both LE and BE byte order (bi-endian MIPS, etc.) +read_ext_label() +{ + magic=$(dd if="$1" bs=1 skip=1080 count=2 2>/dev/null \ + | od -t x2 -An | tr -d ' \n') + case "$magic" in + ef53|53ef) ;; + *) return 1 ;; + esac + + dd if="$1" bs=1 skip=1144 count=16 2>/dev/null | tr -d '\000' +} + +# Look up a block device by its GPT partition name using the kernel's +# sysfs uevent data (zero forks). Falls back to reading the ext2/3/4 +# superblock for whole disks without GPT (e.g., virtual or USB setups). find_partition_by_label() { - label="$1" + # The kernel exposes GPT partition names as PARTNAME in uevent + for uevent in /sys/class/block/*/uevent; do + while IFS='=' read -r key val; do + if [ "$key" = "PARTNAME" ]; then + if [ "$val" = "$1" ]; then + devname="${uevent%/uevent}" + echo "${devname##*/}" + return 0 + fi + break + fi + done < "$uevent" + done + # Fallback: ext filesystem label on whole disk for diskpath in /sys/class/block/*; do - devname=$(basename "$diskpath") - - # Skip partitions, only check whole disks [ -f "$diskpath/partition" ] && continue - - # Skip ram, loop, and other virtual devices + devname="${diskpath##*/}" case "$devname" in - ram*|loop*|nullb*|dm-*) continue ;; + ram*|loop*|nullb*|dm-*|*boot[0-9]*|*rpmb) continue ;; esac - - disk="/dev/$devname" - - # - # 1. Try GPT/MBR partition label using sgdisk - # - result=$(sgdisk -p "$disk" 2>/dev/null | awk -v label="$label" -v devname="$devname" ' - /^ *[0-9]/ { - if ($7 == label) { - if (devname ~ /^(mmcblk|nvme|loop)/) - print devname "p" $1; - else - print devname $1; - exit 0; - } - } - ') - - if [ -n "$result" ]; then - echo "$result" + fslabel=$(read_ext_label "/dev/$devname") + if [ "$fslabel" = "$1" ]; then + echo "$devname" return 0 fi - - # - # 2. Fallback: Check if the whole disk is an ext4/ext2/ext3 filesystem - # - - # Check for ext4/ext2/ext3 magic number (0xEF53) at offset 1080 (1024+56). - magic_number=$(dd if="$disk" bs=1 skip=1080 count=2 2>/dev/null | od -t x2 -A n | tr -d ' \n') - - # Check for both Little-Endian ('53ef') and Big-Endian ('ef53') interpretations of 0xEF53. - # This supports bi-endian architectures like MIPS that may run in BE mode, - # as well as the LE mode which is standard for RISC-V and ext filesystems. - if [ "$magic_number" = "ef53" ] || [ "$magic_number" = "53ef" ]; then - - # Read the volume label from offset 1144 (1024+120) - fslabel=$(dd if="$disk" bs=1 skip=1144 count=16 2>/dev/null | tr -d '\000') - logger $opt -p user.notice -t "$nm" "Found label $fslabel on disk $disk ..." - - if [ "$fslabel" = "$label" ]; then - echo "$devname" - return 0 - fi - fi done return 1 @@ -319,12 +301,6 @@ mount_rw() fi fi - # TODO: Also look for UBI partitions - - # Disable periodic fsck, yet keeping safety checks on ext4 - if grep "LABEL=$label" /etc/fstab |grep -q ext4; then - tune2fs -c 0 -i 0 LABEL="$1" 2>/dev/null - fi mount LABEL="$1" 2>/dev/null && return 0 return 1 From 2fa6c06ee6ca2093d1754aa8b118103d03953b47 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 9 Feb 2026 11:15:49 +0100 Subject: [PATCH 02/29] board/common: optimize sysctl-sync-ip-conf, drop filtering loop Replace the costly read-only filter loop (awk+tr per line) with a stderr redirect, use /sys/class/net/ instead of ip+jq, and batch all sysctl writes into a single call. Reduces boot time by ~4s on 32-bit Arm systems. Signed-off-by: Joachim Wiberg --- .../infix/init.d/10-sysctl-sync-ip-conf | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf b/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf index d2bcd2fa4..d1a0df3b1 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf +++ b/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf @@ -5,24 +5,21 @@ # interfaces at boot, such that physical interfaces will start from # the same point as virtual ones. tmp=$(mktemp) -fil=$(mktemp) +out=$(mktemp) # Ignore unreadable entries, like net.ipv6.conf.default.stable_secret sysctl net.ipv4.conf.default >"$tmp" 2>/dev/null sysctl net.ipv6.conf.default >>"$tmp" 2>/dev/null -# Filter out read-only entries like net.ipv4.conf.default.mc_forwarding -# to prevent misleading error messages in syslog -while IFS= read -r line; do - entry=$(echo "$line" | awk '{print $1}') - path="/proc/sys/$(echo "$entry" | tr . /)" - if [ -w "$path" ]; then - echo "$line" >> "$fil" - fi -done < "$tmp" - -for iface in $(ip -j link show | jq -r .[].ifname); do - sed -e "s/.default./.${iface}./g" "$fil" | sysctl -q -p - +# Build a single sysctl input with settings for all interfaces +for dir in /sys/class/net/*/; do + iface=${dir%/} + iface=${iface##*/} + sed "s/.default./.${iface}./g" "$tmp" >> "$out" done -rm "$tmp" "$fil" +# Apply all at once, suppress errors from read-only entries +# (e.g., net.ipv4.conf.*.mc_forwarding) +sysctl -q -p - < "$out" 2>/dev/null + +rm "$tmp" "$out" From 08271d64c7cca4cd0baeab6ebde45a7a0ce17922 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 9 Feb 2026 11:17:05 +0100 Subject: [PATCH 03/29] board/common: optimize busybox for speed, drop fork+exec for applets - The biggest changes are syncing with latest BusyBox (busybox-update-config) - Disable optimize for size - Enable feature "SH_NOFORK" which allows /bin/sh to call applet_main() directly without having to fork+exec busybox Signed-off-by: Joachim Wiberg --- board/common/busybox_defconfig | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/board/common/busybox_defconfig b/board/common/busybox_defconfig index 1d10811be..739bbcf08 100644 --- a/board/common/busybox_defconfig +++ b/board/common/busybox_defconfig @@ -1,7 +1,7 @@ # # Automatically generated make config: don't edit -# Busybox version: 1.36.1 -# Sun Feb 9 12:25:37 2025 +# Busybox version: 1.37.0 +# Mon Feb 9 07:51:08 2026 # CONFIG_HAVE_DOT_CONFIG=y @@ -17,6 +17,7 @@ CONFIG_SHOW_USAGE=y CONFIG_FEATURE_VERBOSE_USAGE=y # CONFIG_FEATURE_COMPRESS_USAGE is not set CONFIG_LFS=y +CONFIG_TIME64=y CONFIG_PAM=y CONFIG_FEATURE_DEVPTS=y CONFIG_FEATURE_UTMP=y @@ -469,6 +470,7 @@ CONFIG_FEATURE_FIND_NEWER=y CONFIG_FEATURE_FIND_SAMEFILE=y CONFIG_FEATURE_FIND_EXEC=y CONFIG_FEATURE_FIND_EXEC_PLUS=y +CONFIG_FEATURE_FIND_EXEC_OK=y CONFIG_FEATURE_FIND_USER=y CONFIG_FEATURE_FIND_GROUP=y CONFIG_FEATURE_FIND_NOT=y @@ -795,6 +797,7 @@ CONFIG_FLASH_ERASEALL=y CONFIG_FLASH_LOCK=y CONFIG_FLASH_UNLOCK=y CONFIG_FLASHCP=y +CONFIG_GETFATTR=y CONFIG_HDPARM=y CONFIG_FEATURE_HDPARM_GET_IDENTITY=y # CONFIG_FEATURE_HDPARM_HDIO_SCAN_HWIF is not set @@ -933,6 +936,7 @@ CONFIG_IPRULE=y CONFIG_IPNEIGH=y CONFIG_FEATURE_IP_ADDRESS=y CONFIG_FEATURE_IP_LINK=y +CONFIG_FEATURE_IP_LINK_CAN=y CONFIG_FEATURE_IP_ROUTE=y CONFIG_FEATURE_IP_ROUTE_DIR="/etc/iproute2" CONFIG_FEATURE_IP_TUNNEL=y @@ -1007,6 +1011,7 @@ CONFIG_FEATURE_WGET_HTTPS=y CONFIG_WHOIS=y CONFIG_ZCIP=y CONFIG_UDHCPD=y +CONFIG_FEATURE_UDHCPD_BOOTP=y CONFIG_FEATURE_UDHCPD_BASE_IP_ON_MAC=y CONFIG_FEATURE_UDHCPD_WRITE_LEASES_EARLY=y CONFIG_DHCPD_LEASES_FILE="/var/lib/misc/udhcpd.leases" @@ -1016,7 +1021,12 @@ CONFIG_UDHCPC=y CONFIG_FEATURE_UDHCPC_ARPING=y CONFIG_FEATURE_UDHCPC_SANITIZEOPT=y CONFIG_UDHCPC_DEFAULT_SCRIPT="/usr/share/udhcpc/default.script" +CONFIG_UDHCPC6_DEFAULT_SCRIPT="" # CONFIG_UDHCPC6 is not set +# CONFIG_FEATURE_UDHCPC6_RFC3646 is not set +# CONFIG_FEATURE_UDHCPC6_RFC4704 is not set +# CONFIG_FEATURE_UDHCPC6_RFC4833 is not set +# CONFIG_FEATURE_UDHCPC6_RFC5970 is not set # # Common options for DHCP applets @@ -1027,7 +1037,7 @@ CONFIG_UDHCP_DEBUG=0 CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS=80 CONFIG_FEATURE_UDHCP_RFC3397=y CONFIG_FEATURE_UDHCP_8021Q=y -CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="-b -R -n -O search" +CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="" # # Print Utilities @@ -1132,7 +1142,7 @@ CONFIG_SH_IS_ASH=y CONFIG_BASH_IS_NONE=y CONFIG_SHELL_ASH=y CONFIG_ASH=y -CONFIG_ASH_OPTIMIZE_FOR_SIZE=y +# CONFIG_ASH_OPTIMIZE_FOR_SIZE is not set CONFIG_ASH_INTERNAL_GLOB=y CONFIG_ASH_BASH_COMPAT=y # CONFIG_ASH_BASH_SOURCE_CURDIR is not set @@ -1146,7 +1156,6 @@ CONFIG_ASH_IDLE_TIMEOUT=y CONFIG_ASH_ECHO=y CONFIG_ASH_PRINTF=y CONFIG_ASH_TEST=y -CONFIG_ASH_SLEEP=y CONFIG_ASH_HELP=y CONFIG_ASH_GETOPTS=y CONFIG_ASH_CMDCMD=y @@ -1197,7 +1206,7 @@ CONFIG_FEATURE_SH_MATH_64=y CONFIG_FEATURE_SH_MATH_BASE=y CONFIG_FEATURE_SH_EXTRA_QUIET=y # CONFIG_FEATURE_SH_STANDALONE is not set -# CONFIG_FEATURE_SH_NOFORK is not set +CONFIG_FEATURE_SH_NOFORK=y CONFIG_FEATURE_SH_READ_FRAC=y # CONFIG_FEATURE_SH_HISTFILESIZE is not set CONFIG_FEATURE_SH_EMBEDDED_SCRIPTS=y From 4d08a960bb2c61be927a3fcce47998d14df0e5d9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 9 Feb 2026 09:22:46 +0100 Subject: [PATCH 04/29] configs/sama7g54_ek_*_boot_defconfig: switch to upstream U-Boot The v2025.01 release supports the Microchip SamA7G5* eval kit(s), which means we can enjoy the same patch level of U-Boot as other Infix boards Signed-off-by: Joachim Wiberg --- configs/sama7g54_ek_emmc_boot_defconfig | 3 --- configs/sama7g54_ek_sd_boot_defconfig | 3 --- 2 files changed, 6 deletions(-) diff --git a/configs/sama7g54_ek_emmc_boot_defconfig b/configs/sama7g54_ek_emmc_boot_defconfig index 19f7f923c..18b0e1c93 100644 --- a/configs/sama7g54_ek_emmc_boot_defconfig +++ b/configs/sama7g54_ek_emmc_boot_defconfig @@ -20,9 +20,6 @@ BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL=y BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,at91bootstrap,v4.0.9)/at91bootstrap-v4.0.9.tar.gz" BR2_TARGET_AT91BOOTSTRAP3_DEFCONFIG="sama7g5ekemmc_uboot" BR2_TARGET_UBOOT=y -BR2_TARGET_UBOOT_BUILD_SYSTEM_KCONFIG=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,u-boot-at91,linux4microchip-2025.04)/u-boot-at91-linux4microchip-2025.04.tar.gz" BR2_TARGET_UBOOT_BOARD_DEFCONFIG="sama7g5ek_mmc" BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/emmc-extras.config" BR2_TARGET_UBOOT_NEEDS_DTC=y diff --git a/configs/sama7g54_ek_sd_boot_defconfig b/configs/sama7g54_ek_sd_boot_defconfig index 57274befb..135e26f0a 100644 --- a/configs/sama7g54_ek_sd_boot_defconfig +++ b/configs/sama7g54_ek_sd_boot_defconfig @@ -20,9 +20,6 @@ BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL=y BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,at91bootstrap,v4.0.9)/at91bootstrap-v4.0.9.tar.gz" BR2_TARGET_AT91BOOTSTRAP3_DEFCONFIG="sama7g5eksd_uboot" BR2_TARGET_UBOOT=y -BR2_TARGET_UBOOT_BUILD_SYSTEM_KCONFIG=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,u-boot-at91,linux4microchip-2025.04)/u-boot-at91-linux4microchip-2025.04.tar.gz" BR2_TARGET_UBOOT_BOARD_DEFCONFIG="sama7g5ek_mmc1" BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/sd-extras.config" BR2_TARGET_UBOOT_NEEDS_DTC=y From e145310cc17c98eb462144a7cea142d2b106504d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 6 Feb 2026 15:50:31 +0100 Subject: [PATCH 05/29] board/arm: update board list with Microchip SAMA7G54 eval kit Signed-off-by: Joachim Wiberg --- README.md | 1 + board/arm/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index dd1511d3e..44316e78a 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ containers for any custom functionality you need. - **x86_64** - Run in VMs or on mini PCs for development and testing - **Marvell CN9130 CRB, EspressoBIN** - High-performance ARM64 platforms - **Microchip SparX-5i** - Enterprise switching capabilities +- **Microchip SAMA7G54-EK** - ARM Cortex-A7 - **NXP i.MX8MP EVK** - Highly capable ARM64 SoC - **StarFive VisionFive2** - RISC-V architecture support diff --git a/board/arm/README.md b/board/arm/README.md index 2385ba84c..6058d7ee3 100644 --- a/board/arm/README.md +++ b/board/arm/README.md @@ -4,4 +4,5 @@ Arm 32-bit Board Specific Documentation ---------------------------- +- [Microchip SAMA7G54-EK (32-bit)](microchip-sama7g54-ek/) - [Raspberry Pi 2 Model B (32-bit)](raspberrypi-rpi2/) From 1391b960cf05770fa8ce142325e68f571eb9d1f7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 9 Feb 2026 09:20:56 +0100 Subject: [PATCH 06/29] board/arm: fix rauc system.conf --- .../rootfs/etc/rauc/system.conf | 12 -------- board/arm/rootfs/etc/rauc/system.conf | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) delete mode 100644 board/arm/microchip-sama7g54-ek/rootfs/etc/rauc/system.conf create mode 100644 board/arm/rootfs/etc/rauc/system.conf diff --git a/board/arm/microchip-sama7g54-ek/rootfs/etc/rauc/system.conf b/board/arm/microchip-sama7g54-ek/rootfs/etc/rauc/system.conf deleted file mode 100644 index dbdb8e70e..000000000 --- a/board/arm/microchip-sama7g54-ek/rootfs/etc/rauc/system.conf +++ /dev/null @@ -1,12 +0,0 @@ -[system] -compatible=infix-sama7g54-ek -bootloader=uboot -statusfile=/mnt/aux/rauc.status - -[slot.rootfs.0] -device=/dev/disk/by-partlabel/primary -bootname=primary - -[slot.rootfs.1] -device=/dev/disk/by-partlabel/secondary -bootname=secondary diff --git a/board/arm/rootfs/etc/rauc/system.conf b/board/arm/rootfs/etc/rauc/system.conf new file mode 100644 index 000000000..6b9b51b6f --- /dev/null +++ b/board/arm/rootfs/etc/rauc/system.conf @@ -0,0 +1,28 @@ +[system] +compatible=infix-arm +bootloader=uboot +statusfile=/mnt/aux/rauc.status +mountprefix=/var/lib/rauc/mnt +bundle-formats=-plain +max-bundle-download-size=1073741824 + +[log.event-log] +filename=/var/log/upgrade-json.log +format=json-pretty +max-size=1M +max-files=5 + +[keyring] +directory=/etc/rauc/keys + +[slot.rootfs.0] +device=/dev/disk/by-partlabel/primary +bootname=primary + +[slot.rootfs.1] +device=/dev/disk/by-partlabel/secondary +bootname=secondary + +[slot.net.0] +device=/dev/ram0 +bootname=net From b9325e6f40507da82169750ab2108bf396bf776a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 9 Feb 2026 14:48:11 +0100 Subject: [PATCH 07/29] board/arm: disable u-boot environment, use aux partition Signed-off-by: Joachim Wiberg --- board/arm/microchip-sama7g54-ek/uboot/extras.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/board/arm/microchip-sama7g54-ek/uboot/extras.config b/board/arm/microchip-sama7g54-ek/uboot/extras.config index 6d3cf0fc7..556649dcb 100644 --- a/board/arm/microchip-sama7g54-ek/uboot/extras.config +++ b/board/arm/microchip-sama7g54-ek/uboot/extras.config @@ -1,6 +1,8 @@ # Common U-Boot extras for SAMA7G54-EK CONFIG_EFI_PARTITION=y CONFIG_ENV_IMPORT_FDT=y +# CONFIG_ENV_IS_IN_FAT is not set +CONFIG_ENV_IS_NOWHERE=y CONFIG_FIT=y CONFIG_FIT_SIGNATURE=y CONFIG_RSA=y From a88dfa15678895d9e8d4f10e46be7f9d00d82780 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 11 Feb 2026 17:00:44 +0100 Subject: [PATCH 08/29] board/arm: sama7g54-ek: enable sdmcc high speed modes For details, see: - https://github.com/linux4sam/u-boot-at91/commit/23ac019 - https://github.com/linux4sam/linux-at91/commit/5b35500 U-Boot patches imported and refreshed in local KernelKi fork of U-Boot, see https://github.com/kernelkit/u-boot/tree/v2025.01-kkit Signed-off-by: Joachim Wiberg --- .../dts/microchip/at91-sama7g5ek.dts | 25 +++++++++++++++++++ ...a7g5ek-increase-clock-for-sdmmc-from.patch | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts b/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts index 503d70045..ebba2075a 100644 --- a/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts +++ b/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts @@ -16,3 +16,28 @@ &thermal_sensor { status = "disabled"; }; + +/* + * Enable SDMMC high speed mode, please note that neither 'mmc-ddr-1_8v' + * or 'mmc-hs200-1_8v' work, even though [1] states they are supported, + * the errata [2] tells a different story: "Using mode SDR104, HS200 or + * HS400 may lead to tuning issues, data read errors or clock switching + * failures." — empirical testing has proved this on a rev 5 board. + * + * [1]: https://github.com/linux4sam/linux-at91/commit/5b35500 + * [2]: https://ww1.microchip.com/downloads/en/DeviceDoc/SAMA7G5-Series-Silicon-Errata-and-Data-Sheet-Clarification-DS80001016A.pdf + */ +&sdmmc0 { + /delete-property/ sdhci-caps-mask; + cap-mmc-highspeed; + cap-mmc-hw-reset; +}; + +&sdmmc1 { + /delete-property/ no-1-8-v; + /delete-property/ sdhci-caps-mask; +}; + +&sdmmc2 { + /delete-property/ sdhci-caps-mask; +}; diff --git a/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch b/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch index 0e96b9214..e85ad96bc 100644 --- a/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch +++ b/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch @@ -16,7 +16,7 @@ improve the boot time when reading the kernel binary. Tested on sama7g5ek rev 5 using mmcinfo command. Signed-off-by: Mihai Sain -Signed-off-by: Mattias Walström +Signed-off-by: Joachim Wiberg --- arch/arm/dts/at91-sama7g5ek.dts | 4 ---- 1 file changed, 4 deletions(-) From 443aaec1eaaae15e175548ad471c27766d970b65 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 11 Feb 2026 17:36:28 +0100 Subject: [PATCH 09/29] board/arm: trim kernel defconfig Signed-off-by: Joachim Wiberg --- board/arm/linux_defconfig | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig index c54ed16fe..5ace00c2b 100644 --- a/board/arm/linux_defconfig +++ b/board/arm/linux_defconfig @@ -1,20 +1,19 @@ CONFIG_SYSVIPC=y CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y CONFIG_NO_HZ_IDLE=y CONFIG_HIGH_RES_TIMERS=y CONFIG_BPF_SYSCALL=y CONFIG_BPF_JIT=y -CONFIG_PREEMPT=y CONFIG_BSD_PROCESS_ACCT=y CONFIG_BSD_PROCESS_ACCT_V3=y CONFIG_TASKSTATS=y CONFIG_TASK_DELAY_ACCT=y CONFIG_TASK_XACCT=y CONFIG_TASK_IO_ACCOUNTING=y +# CONFIG_CPU_ISOLATION is not set CONFIG_IKCONFIG=y CONFIG_IKCONFIG_PROC=y -CONFIG_LOG_BUF_SHIFT=18 +CONFIG_LOG_BUF_SHIFT=16 CONFIG_MEMCG=y CONFIG_BLK_CGROUP=y CONFIG_CFS_BANDWIDTH=y @@ -31,17 +30,16 @@ CONFIG_SCHED_AUTOGROUP=y CONFIG_BLK_DEV_INITRD=y CONFIG_KALLSYMS_ALL=y CONFIG_PROFILING=y -CONFIG_ARCH_MULTI_V6=y -CONFIG_ARCH_VIRT=y CONFIG_ARCH_BCM=y CONFIG_ARCH_BCM2835=y +# CONFIG_ARM_ERRATA_643719 is not set CONFIG_SMP=y CONFIG_CPU_FREQ=y -CONFIG_CPU_FREQ_STAT=y -CONFIG_CPU_FREQ_DEFAULT_GOV_CONSERVATIVE=y +CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE=y CONFIG_CPU_FREQ_GOV_POWERSAVE=y CONFIG_CPU_FREQ_GOV_USERSPACE=y CONFIG_CPU_FREQ_GOV_ONDEMAND=y +CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y CONFIG_CPUFREQ_DT=y CONFIG_ARM_RASPBERRYPI_CPUFREQ=y CONFIG_VFP=y @@ -55,7 +53,11 @@ CONFIG_MODULES=y CONFIG_MODULE_UNLOAD=y CONFIG_PARTITION_ADVANCED=y # CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS is not set +CONFIG_ZSWAP=y +CONFIG_ZSWAP_DEFAULT_ON=y +CONFIG_ZSWAP_SHRINKER_DEFAULT_ON=y # CONFIG_COMPAT_BRK is not set +# CONFIG_BALLOON_COMPACTION is not set CONFIG_KSM=y CONFIG_CMA=y CONFIG_NET=y @@ -426,18 +428,13 @@ CONFIG_DMA_CMA=y CONFIG_CMA_SIZE_MBYTES=32 CONFIG_PRINTK_TIME=y CONFIG_DEBUG_KERNEL=y -CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y CONFIG_MAGIC_SYSRQ=y CONFIG_DEBUG_FS=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=20 -CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y CONFIG_HARDLOCKUP_DETECTOR=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_RCU_TRACE is not set CONFIG_FUNCTION_TRACER=y -CONFIG_MEMTEST=y +# CONFIG_RUNTIME_TESTING_MENU is not set From f842b7ba70d6b932a33066d0b60650976cbb98cd Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 6 Feb 2026 15:51:13 +0100 Subject: [PATCH 10/29] package/initviz: new package Signed-off-by: Joachim Wiberg --- package/Config.in | 1 + package/initviz/Config.in | 22 ++++++++++++++++++++++ package/initviz/initviz.hash | 3 +++ package/initviz/initviz.mk | 26 ++++++++++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 package/initviz/Config.in create mode 100644 package/initviz/initviz.hash create mode 100644 package/initviz/initviz.mk diff --git a/package/Config.in b/package/Config.in index 3ccb35d03..468435480 100644 --- a/package/Config.in +++ b/package/Config.in @@ -19,6 +19,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/firewall/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/greenpak-programmer/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/ifupdown-ng/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/iito/Config.in" +source "$BR2_EXTERNAL_INFIX_PATH/package/initviz/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/k8s-logger/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/keyack/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/klish-plugin-infix/Config.in" diff --git a/package/initviz/Config.in b/package/initviz/Config.in new file mode 100644 index 000000000..df811d260 --- /dev/null +++ b/package/initviz/Config.in @@ -0,0 +1,22 @@ +config BR2_PACKAGE_INITVIZ + bool "initviz" + depends on BR2_USE_MMU # fork() + help + InitViz is a performance analysis and visualization tool for the + boot process and system services. It consists of the bootchartd + data collection daemon (bootchartd) that runs during boot to + capture system activity, and InitViz the host visualization tool. + + InitViz is a reimplementation and successor to the bootchart2 + project, offering a more feature-rich solution compared to the + bootchartd subset available as a BusyBox applet. + + To profile the boot process, append the following to the kernel + command line: + + init=/sbin/bootchartd initcall_debug printk.time=y quiet + + The collected data can be visualized using the host-initviz + tool, initviz.py, which is currently not built here. + + https://github.com/finit-project/InitViz diff --git a/package/initviz/initviz.hash b/package/initviz/initviz.hash new file mode 100644 index 000000000..b0f379cbd --- /dev/null +++ b/package/initviz/initviz.hash @@ -0,0 +1,3 @@ +# Locally calculated +sha256 28a059ca6d3cbc5f65809a18167d089fd0dc2be13cd6c640c56ddae47be01849 initviz-1.0.0-rc1.tar.gz +sha256 54e1afa760fa3649fa47c7838ac937771e74af695d4cf7d907bc61c107c83dc9 COPYING diff --git a/package/initviz/initviz.mk b/package/initviz/initviz.mk new file mode 100644 index 000000000..15d28eb35 --- /dev/null +++ b/package/initviz/initviz.mk @@ -0,0 +1,26 @@ +################################################################################ +# +# initviz +# +################################################################################ + +INITVIZ_VERSION = 1.0.0-rc1 +INITVIZ_SITE = https://github.com/finit-project/InitViz/releases/download/$(INITVIZ_VERSION) +INITVIZ_SOURCE = initviz-$(INITVIZ_VERSION).tar.gz +INITVIZ_LICENSE = GPL-2.0-or-later +INITVIZ_LICENSE_FILES = COPYING + +# Target package: bootchartd collector daemon +define INITVIZ_BUILD_CMDS + $(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) \ + $(MAKE) -C $(@D) collector +endef + +define INITVIZ_INSTALL_TARGET_CMDS + $(TARGET_MAKE_ENV) $(MAKE) -C $(@D) \ + DESTDIR=$(TARGET_DIR) \ + EARLY_PREFIX= \ + install-collector +endef + +$(eval $(generic-package)) From 3dd360a7017bd790d6e1aec3d24a8c7567a0d635 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 9 Feb 2026 05:51:28 +0100 Subject: [PATCH 11/29] doc: minor fixes - Markdown syntax - Grammar fixes - Use lowdown's admonition syntax - Update examples Signed-off-by: Joachim Wiberg --- README.md | 6 +-- doc/cli/configure.md | 100 ++++++++++++++++++---------------------- doc/cli/introduction.md | 47 +++++++++---------- doc/cli/keybindings.md | 3 +- doc/cli/netcalc.md | 1 - doc/cli/quick.md | 8 ++-- doc/cli/upgrade.md | 4 +- 7 files changed, 75 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 44316e78a..4c335047b 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,12 @@ interfaces { admin@infix-12-34-56:/config/interface/eth0/> leave admin@infix-12-34-56:/> show interfaces INTERFACE PROTOCOL STATE DATA -eth0 ethernet UP 52:54:00:12:34:56 - ipv4 192.168.2.200/24 (static) - ipv6 fe80::5054:ff:fe12:3456/64 (link-layer) lo ethernet UP 00:00:00:00:00:00 ipv4 127.0.0.1/8 (static) ipv6 ::1/128 (static) +eth0 ethernet UP 52:54:00:12:34:56 + ipv4 192.168.2.200/24 (static) + ipv6 fe80::5054:ff:fe12:3456/64 (link-layer) admin@infix-12-34-56:/> copy running startup diff --git a/doc/cli/configure.md b/doc/cli/configure.md index 514e6aaec..b8300af45 100644 --- a/doc/cli/configure.md +++ b/doc/cli/configure.md @@ -40,16 +40,14 @@ admin@host:/config/interface/eth0/> up admin@host:/config/> ``` ----- - -> **Note:** commands in configure context are automatically generated -> from the system's YANG models, hence different products likely have a -> different set of commands. However, both the `ietf-interfaces.yang` -> and `ietf-ip.yang` models, for instance, that provide the networking +> **Note** +> +> Commands in configure context are automatically generated from the +> system's YANG models, hence different products likely have a different +> set of commands. However, both the `ietf-interfaces.yang` and +> `ietf-ip.yang` models, for instance, that provide the networking > support are common to all systems. ----- - ## Set IP Address on an Interface ``` @@ -74,7 +72,6 @@ interfaces { } ``` - ## Saving Changes Apply the changes (from candidate to `running-config`): @@ -85,12 +82,12 @@ admin@host:/> show running-config ... interfaces { interface eth0 { - type ethernetCsmacd; - ipv4 { - address 192.168.2.200 { - prefix-length 24; - } - } + type ethernetCsmacd; + ipv4 { + address 192.168.2.200 { + prefix-length 24; + } + } } ... ``` @@ -106,12 +103,10 @@ admin@host:/> copy running-config startup-config The `startup-config` can also be inspected with the `show` command to verify the changes are saved. ----- - -> **Note:** all commands need to be spelled out, no short forms are -> allowed in the CLI. Use the `TAB` key to make your life easier. - ----- +> **Important** +> +> All commands need to be spelled out, no short forms are allowed in the +> CLI. Use the `TAB` key to make your life easier. ## Changing Hostname @@ -128,12 +123,10 @@ admin@example:/> Notice how the hostname in the prompt does not change until the change is committed. ----- - -> **Note:** critical services like syslog, mDNS, LLDP, and similar that -> advertise the hostname, are restarted when the hostname is changed. - ----- +> **Note** +> +> Critical services like syslog, mDNS, LLDP, and similar that advertise +> the hostname, are restarted when the hostname is changed. ## Changing Password @@ -142,14 +135,14 @@ User management, including passwords, is also a part of `ietf-system`. ``` admin@host:/config/> edit system authentication user admin admin@host:/config/system/authentication/user/admin/> change password -New password: -Retype password: +New password: +Retype password: admin@host:/config/system/authentication/user/admin/> leave ``` The `change password` command starts an interactive dialogue that asks for the new password, with a confirmation, and then salts and encrypts -the password with sha512crypt. +the password with sha512crypt. It is also possible to use the `set password ...` command. This allows setting an already hashed password. To manually hash a password, use @@ -157,13 +150,11 @@ the `do password encrypt` command. This launches the admin-exec command to hash, and optionally salt, your password. This encrypted string can then be used with `set password ...`. ----- - -> **Tip:** if you are having trouble thinking of a password, there is -> also `do password generate`, which generates random but readable -> strings using the UNIX command `pwgen`. - ----- +> **Tip** +> +> If you are having trouble thinking of a password, there is also `do +> password generate`, which generates random but readable strings using +> the UNIX command `pwgen`. ## SSH Authorized Key @@ -185,11 +176,11 @@ key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTT admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> leave ``` ----- - -> **Note:** the `ssh-keygen` program already base64 encodes the public -> key data, so there is no need to use the `text-editor` command, `set` -> does the job. +> **Note** +> +> The `ssh-keygen` program already base64 encodes the public key data, +> so there is no need to use the `text-editor` command, `set` does the +> job. ---- @@ -229,13 +220,12 @@ admin@host:/config/> leave See the bridging example below for more. ----- - -> **Note:** in the CLI you do not have to create the `veth0b` interface. -> The system _infers_ this for you. When setting up a VETH pair using -> NETCONF, however, you must include the `veth0b` interface. - ----- +> **Tip** +> +> In the CLI you do not have to create the `veth0b` interface. The +> system _infers_ this for you. This does not apply when setting up a +> VETH pair using NETCONF or RESTCONF, then you must submit a complete +> configuration. ## Creating a Bridge @@ -291,10 +281,8 @@ the VETH pair from the previous example) are now bridged. Any traffic ingressing one port will egress the other. Only reserved IEEE multicast is filtered, except LLDP frames as shown above. ----- - -> **Note:** the bridge can be named anything, provided the interface -> name is not already taken. However, for any name outside the pattern -> `br[0-9]+`, you have to set the interface type manually to `bridge`. - ----- +> **Important** +> +> The bridge can be named anything, provided the interface name is not +> already taken. However, for any name outside the pattern `br[0-9]+`, +> you have to set the interface type manually to `bridge`. diff --git a/doc/cli/introduction.md b/doc/cli/introduction.md index a104f8a4b..4630c3562 100644 --- a/doc/cli/introduction.md +++ b/doc/cli/introduction.md @@ -34,13 +34,11 @@ admin@host-12-34-56:/> show # Try: Tab or ? admin@host-12-34-56:/> # Try: Tab or ? ``` ----- - -> **Note:** even on an empty command line, you can tap the `Tab` or `?` keys. +> **Tip** +> +> Even on an empty command line, you can tap the `Tab` or `?` keys. > See [`help keybindings`](keybindings.md) for more tips! ----- - ## Key Concepts The two modes in the CLI are the admin-exec and the configure context. @@ -78,15 +76,15 @@ and *running* that can be managed and inspected using the `copy`, `show`, and `configure` commands. The traditional names used in the CLI for these are listed below: - - `factory-config` the default configuration from factory for the - device, i.e., what the system returns to after a `factory-reset` - - `startup-config` created from `factory-config` at first boot after - factory reset. Loaded as the system configuration on each boot - - `running-config` what is actively running on the system. If no - changes have been made since boot, it is the same as `startup-config` - - `candidate-config` is created from `running-config` when entering the - configure context. Any changes made here can be discarded (`abort`, - `rollback`) or committed (`commit`, `leave`) to `running-config` +- `factory-config` the default configuration from factory for the + device, i.e., what the system returns to after a `factory-reset` +- `startup-config` created from `factory-config` at first boot after + factory reset. Loaded as the system configuration on each boot +- `running-config` what is actively running on the system. If no + changes have been made since boot, it is the same as `startup-config` +- `candidate-config` is created from `running-config` when entering the + configure context. Any changes made here can be discarded (`abort`, + `rollback`) or committed (`commit`, `leave`) to `running-config` Edit the *running* configuration using the `configure` command. This copies *running* to *candidate*, a temporary datastore, where changes @@ -131,16 +129,13 @@ In *configure context* the following commands are available: | `do command` | Call admin-exec command: `do show log` | | `commit` | | - ## Example Session ----- - +> **Tip** +> > Remember to use the `TAB` and `?` keys to speed up your navigation. > See [`help keybindings`](keybindings.md) for more tips! ----- - In this example we enter configure context to add an IPv4 address to interface `eth0`, then we apply the changes using the `leave` command. @@ -151,8 +146,8 @@ save the changes for the next reboot. admin@host-12-34-56:/> configure admin@host-12-34-56:/config/> edit interface eth0 admin@host-12-34-56:/config/interface/eth0/> set ipv4 - address autoconf bind-ni-name enabled - forwarding mtu neighbor + address autoconf bind-ni-name dhcp + enabled forwarding mtu neighbor admin@host-12-34-56:/config/interface/eth0/> set ipv4 address 192.168.2.200 prefix-length 24 admin@host-12-34-56:/config/interface/eth0/> show type ethernetCsmacd; @@ -193,10 +188,12 @@ admin@host-12-34-56:/> copy startup-config running-config Or restart the device, for example if the change to the configuration caused you to lose contact with the system (it happens to the best of -us). The system will start up from the last "save gave". +us). The system will start up from the last "save game". -> **Tip:** when restoring a backup of a configuration, or having manually -> edited a config file, you can validate it using system's YANG models, -> it is *not* applied if validation is successful: +> **Tip:** Restoring Backups +> +> When restoring a backup of a configuration, or having manually edited +> a config file, you can validate it using the system's YANG models, it +> is *not* applied if validation is successful: > > `copy /media/backup/old.cfg running-config validate` diff --git a/doc/cli/keybindings.md b/doc/cli/keybindings.md index 2633ccd9d..6687ccb1e 100644 --- a/doc/cli/keybindings.md +++ b/doc/cli/keybindings.md @@ -33,7 +33,7 @@ CLI has several keybindings, most significant first: | Ctrl-t | | Transpose/Swap characters before and at cursor | | Meta-# | Alt-Shift-3 | Prepend # to current line and submit to history | -## What is Meta? +## What is Meta The Meta key is called Alt on most modern keyboards. If you have neither, first tap the Esc key instead of holding down Alt/Meta. @@ -53,4 +53,3 @@ See possible arguments, with brief help text, to a command: ... Type the command, then tap the `?` key. - diff --git a/doc/cli/netcalc.md b/doc/cli/netcalc.md index 8514bb1fd..1c2e5416a 100644 --- a/doc/cli/netcalc.md +++ b/doc/cli/netcalc.md @@ -18,7 +18,6 @@ A subnet can be entered in two ways: An optional `split LEN` can be given as argument, the new length value must be bigger than the current prefix length. See example below. - ## Examples Its most commonly used features are to understand how many addresses an diff --git a/doc/cli/quick.md b/doc/cli/quick.md index dd8dfe02d..4762b3c06 100644 --- a/doc/cli/quick.md +++ b/doc/cli/quick.md @@ -23,10 +23,8 @@ keybindings are really useful to learn! | `help text-editor` | Help with the built-in text-editor command | | `help keybindings` | Lists available keybindings & other helpful tricks | ----- - -> In `configure` context the `help setting` command shows the YANG +> **Tip:** Online Help +> +> In `configure` context the `help ` command shows the YANG > description text for each node and container. To reach the admin > exec help from configure context, e.g., `do help text-editor` - ----- diff --git a/doc/cli/upgrade.md b/doc/cli/upgrade.md index 10cdf5c1b..09ef60ada 100644 --- a/doc/cli/upgrade.md +++ b/doc/cli/upgrade.md @@ -10,7 +10,7 @@ use the `upgrade` command and a URI to a ftp/tftp/sftp or http/https server that hosts the file: ``` -admin@host:/> upgrade tftp://192.168.122.1/firmware-x86_64-v23.11.pkg +admin@example:/> upgrade tftp://192.168.122.1/firmware-x86_64-v23.11.pkg installing 0% Installing 0% Determining slot states @@ -31,7 +31,7 @@ installing 99% Updating slots done. 100% Installing done. Installing `tftp://192.168.122.1/firmware-x86_64-v23.11.pkg` succeeded -admin@host:/> +admin@example:/> ``` The secondary partition (`rootfs.1`) has now been upgraded and will be used as From 363e3b9f52ef15724c53e362ec5fd1eb13c4effc Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 11 Feb 2026 06:50:59 +0100 Subject: [PATCH 12/29] confd: replace sysrepo-plugind + bootstrap + load with single daemon On single-core Cortex-A7, the YANG bootstrap and config loading is the dominant boot bottleneck. The current sequence spawns three serial phases (bootstrap, sysrepo-plugind, load), each performing independent sr_connect()/sr_disconnect() cycles, and every sysrepoctl/sysrepocfg invocation is a fork+exec that rebuilds SHM from scratch. Replace all three with a single confd binary that does one sr_connect() and performs all datastore operations in-process: - Wipe stale /dev/shm/sr_* for a clean slate - sr_install_factory_config() from the generated JSON - Smart migration: compare config version via libjansson, only fork+exec the migrate script when versions actually differ - Load startup-config (or test-config) via lyd_parse_data() + sr_replace_config(), mirroring what sysrepocfg -I does internally - On failure: revert to factory-default, load failure-config, set login banners (Fail Secure mode) - On first boot: copy factory-default to running, export to file - dlopen plugins and enter event loop The bootstrap shell script is split: config generation (gen-hostname, gen-interfaces, etc.) stays in the new gen-config script, while all sysrepo operations move into the C daemon. The finit boot sequence collapses from 5 stanzas to 2 (gen-config -> confd). Signed-off-by: Joachim Wiberg --- .../usr/libexec/infix/init.d/30-cfg-migrate | 20 - package/confd/confd.conf | 30 +- src/confd/bin/Makefile.am | 2 +- src/confd/bin/{bootstrap => gen-config} | 96 +- src/confd/bin/load | 143 --- src/confd/configure.ac | 3 +- src/confd/src/Makefile.am | 7 + src/confd/src/main.c | 840 ++++++++++++++++++ 8 files changed, 879 insertions(+), 262 deletions(-) delete mode 100755 board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate rename src/confd/bin/{bootstrap => gen-config} (60%) delete mode 100755 src/confd/bin/load create mode 100644 src/confd/src/main.c diff --git a/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate b/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate deleted file mode 100755 index def751055..000000000 --- a/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -# Check if /cfg/startup-config.cfg needs to be migrated to new syntax. -# Backup of the original is created in /cfg/backup/ for old versions, -# the migrate tool inserts old version in name before .cfg extension. -CONFIG_FILE="/cfg/startup-config.cfg" -BACKUP_FILE="/cfg/backup/startup-config.cfg" -BACKUP_DIR="$(dirname "$BACKUP_FILE")" - -mkdir -p "$BACKUP_DIR" -chown root:wheel "$BACKUP_DIR" -chmod 0770 "$BACKUP_DIR" - -if [ ! -f "$CONFIG_FILE" ]; then - logger -I $$ -k -p user.notice -t $(basename "$0") "No $(basename "$CONFIG_FILE" .cfg) yet, likely factory reset." - exit 0 -elif migrate -cq "$CONFIG_FILE"; then - exit 0 -fi - -migrate -i -b "$BACKUP_FILE" "$CONFIG_FILE" diff --git a/package/confd/confd.conf b/package/confd/confd.conf index 482ee26e8..c39ab50a7 100644 --- a/package/confd/confd.conf +++ b/package/confd/confd.conf @@ -1,27 +1,19 @@ #set DEBUG=1 +# Phase 1: Generate factory/failure/test configs (shell) run name:bootstrap log:prio:user.notice norestart \ - [S] /usr/libexec/confd/bootstrap \ - -- Bootstrapping YANG datastore + [S] /usr/libexec/confd/gen-config \ + -- Generating factory configuration run name:error :1 log:console norestart if: \ [S] /usr/libexec/confd/error -- -service name:confd log:prio:daemon.err \ - [S12345] sysrepo-plugind -f -p /run/confd.pid -n -v warning \ +# Phase 2: Single daemon handles datastore init, config load, and plugins +# log:prio:daemon.err +service name:confd env:/etc/default/confd \ + [S12345] confd -f -n -v warning -p /run/confd.pid \ + -F /etc/factory-config.cfg \ + -S /cfg/startup-config.cfg \ + -E /etc/failure-config.cfg \ + -t $CONFD_TIMEOUT \ -- Configuration daemon - -# Bootstrap system with startup-config -run name:startup log:prio:user.notice norestart env:/etc/default/confd \ - [S] /usr/libexec/confd/load -t $CONFD_TIMEOUT startup-config \ - -- Loading startup-config - -# Run if loading startup-config fails for some reason -run name:failure log:prio:user.crit norestart env:/etc/default/confd \ - if: \ - [S] /usr/libexec/confd/load -t $CONFD_TIMEOUT failure-config \ - -- Loading failure-config - -run name:error :2 log:console norestart \ - if: \ - [S] /usr/libexec/confd/error -- diff --git a/src/confd/bin/Makefile.am b/src/confd/bin/Makefile.am index 49bed009d..068f2b255 100644 --- a/src/confd/bin/Makefile.am +++ b/src/confd/bin/Makefile.am @@ -1,4 +1,4 @@ -pkglibexec_SCRIPTS = bootstrap error load gen-service gen-hostname \ +pkglibexec_SCRIPTS = gen-config error gen-service gen-hostname \ gen-interfaces gen-motd gen-hardware gen-version \ mstpd-wait-online wait-interface sbin_SCRIPTS = dagger migrate firewall diff --git a/src/confd/bin/bootstrap b/src/confd/bin/gen-config similarity index 60% rename from src/confd/bin/bootstrap rename to src/confd/bin/gen-config index 371d80286..d58e8b7b0 100755 --- a/src/confd/bin/bootstrap +++ b/src/confd/bin/gen-config @@ -1,42 +1,32 @@ #!/bin/sh -# Bootstrap system factory-config, failure-config, test-config and sysrepo db. +# Generate factory-config, failure-config, and test-config files. # -######################################################################## -# The system factory-config, failure-config and test-config are derived -# from default settings snippets, from /usr/share/confd/factory.d, and -# some generated snippets, e.g., hostname (based on base MAC address) -# and number of interfaces. +# These configs are derived from default settings snippets in +# /usr/share/confd/factory.d, and generated snippets (e.g., hostname +# based on base MAC address, number of interfaces). # -# The resulting factory-config is used to create the syrepo db (below) -# {factory} datastore. Hence, the factory-config file must match the -# the YANG models of the active image. +# The sysrepo datastore operations (loading factory defaults, startup +# config, migration) are handled by the confd daemon. ######################################################################## # NOTE: with the Infix defaults, a br2-external can provide a build-time # /etc/factory-config.cfg to override the behavior of this script. # # This applies also for /etc/failure-config.cfg, but we recommend # strongly that you instead provide gen-err-custom, see below. -# -# TODO: Look for statically defined factory-config, based on system's -# product ID, or just custom site-specific factory on /cfg. ######################################################################## -STATUS="" # Log functions -critical() +err() { - logger -i -p user.crit -t bootstrap "$1" 2>/dev/null || echo "$1" + logger -i -p user.err -t gen-config "$1" 2>/dev/null || echo "$1" } -err() +log() { - logger -i -p user.err -t bootstrap "$1" 2>/dev/null || echo "$1" + logger -i -p user.notice -t gen-config "$1" 2>/dev/null || echo "$1" } -# When logging errors, generating /etc/issue* or /etc/banner (SSH) -. /etc/os-release - -# /etc/confdrc controls the behavior or most of the gen-scripts, +# /etc/confdrc controls the behavior of most of the gen-scripts, # customize in an overlay when using Infix as an br2-external. RC=/etc/confdrc if [ "$1" = "-f" ] && [ -f "$2" ]; then @@ -79,25 +69,6 @@ collate() fi } -# Report error on console, syslog, and set login banners for getty + ssh -console_error() -{ - critical "$1" - - # shellcheck disable=SC3037 - /bin/echo -e "\n\n\e[31mCRITICAL BOOTSTRAP ERROR\n$1\e[0m\n" > /dev/console - - [ -z "$STATUS" ] || return - STATUS="CRITICAL ERROR: $1" - - printf "\n$STATUS\n" | tee -a \ - /etc/banner \ - /etc/issue \ - /etc/issue.net \ - >/dev/null - return 0 -} - gen_factory_cfg() { # Fetch defaults, simplifies sort in collate() @@ -145,49 +116,18 @@ gen_test_cfg() collate "$TEST_GEN" "$TEST_CFG" "$TEST_D" } -# Both factory-config and failure-config are generated every boot -# regardless if there is a static /etc/factory-config.cfg or not. +log "Starting up, calling gen_factory_cfg()" gen_factory_cfg +log "Starting up, calling gen_failure_cfg()" gen_failure_cfg if [ -f "/mnt/aux/test-mode" ]; then gen_test_cfg - sysrepoctl -c infix-test -e test-mode-enable -fi - -if [ -n "$TESTING" ]; then - echo "Done." - exit 0 fi -mkdir -p /etc/sysrepo/ -if [ -f "$FACTORY_CFG" ]; then - cp "$FACTORY_CFG" "$INIT_DATA" -else - cp "$FAILURE_CFG" "$INIT_DATA" -fi -rc=$? - -# Ensure 'admin' group users always have access -chgrp wheel "$CFG_PATH_" -chmod g+w "$CFG_PATH_" -# Ensure factory-config has correct syntax -if ! migrate -cq "$INIT_DATA"; then - if migrate -iq -b "${INIT_DATA%.*}.bak" "$INIT_DATA"; then - err "${INIT_DATA}: found and fixed old syntax!" - fi -fi - -if ! sysrepoctl -z "$INIT_DATA"; then - rc=$? - err "Failed loading factory-default datastore" -else - # Clear running-config so we can load/create startup in the next step - temp=$(mktemp) - echo "{}" > "$temp" - sysrepocfg -f json -I"$temp" -d running - rc=$? - rm "$temp" -fi +# Ensure 'admin' group users always have access to /cfg +mkdir -p "$CFG_PATH_" +chgrp wheel "$CFG_PATH_" 2>/dev/null +chmod g+w "$CFG_PATH_" 2>/dev/null -exit $rc +log "All done." diff --git a/src/confd/bin/load b/src/confd/bin/load deleted file mode 100755 index f1e157966..000000000 --- a/src/confd/bin/load +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/sh -# load [-b] -# -# Import a configuration to the sysrepo datastore using `sysrepocfg -Ifile` -# -# If the '-b' option is used we set the Finit condition if -# sysrepocfg returns OK. This to be able to detect and trigger the Infix -# Fail Secure Mode at boot. -# - -banner_append() -{ - printf "\n%s\n" "$*" | tee -a \ - /etc/banner \ - /etc/issue \ - /etc/issue.net \ - >/dev/null - return 0 -} - -# Ensure correct ownership and permissions, in particular after factory reset -# Created by the system, writable by any user in the admin group. -perms() -{ - chown root:wheel "$1" - chmod 0660 "$1" -} - -note() -{ - msg="$*" - logger -I $$ -p user.notice -t load -- "$msg" -} - -err() -{ - msg="$*" - logger -I $$ -p user.error -t load -- "$msg" -} - - -# shellcheck disable=SC1091 -. /etc/confdrc - -sysrepocfg=sysrepocfg -while getopts "t:" opt; do - case ${opt} in - t) - sysrepocfg="$sysrepocfg -t $OPTARG" - ;; - *) - ;; - esac -done -shift $((OPTIND - 1)) - -if [ $# -lt 1 ]; then - err "No configuration file supplied" - exit 1 -fi - - -config=$1 - -if [ -f "/mnt/aux/test-mode" ] && [ "$config" = "startup-config" ]; then - - if [ -f "/mnt/aux/test-override-startup" ]; then - rm -f "/mnt/aux/test-override-startup" - else - note "Test mode detected, switching to test-config" - config="test-config" - fi -fi - -if [ -f "$config" ]; then - fn="$config" -else - if [ -f "$CFG_PATH_/${config}.cfg" ]; then - fn="$CFG_PATH_/${config}.cfg" - else - fn="$SYS_PATH_/${config}.cfg" - fi -fi - -if [ ! -f "$fn" ]; then - case "$config" in - startup-config) - note "startup-config missing, initializing running datastore from factory-config" - $sysrepocfg -C factory-default - rc=$? - note "saving factory-config to $STARTUP_CFG ..." - $sysrepocfg -f json -X"$STARTUP_CFG" - perms "$STARTUP_CFG" - exit $rc - ;; - *) - err "No such file, $fn, aborting!" - exit 1 - ;; - esac -fi - -note "Loading $config ..." -if ! $sysrepocfg -v2 -I"$fn" -f json; then - case "$config" in - startup-config) - err "Failed loading $fn, reverting to Fail Secure mode!" - # On failure to load startup-config the system is in an undefined state - cat <<-EOF >/tmp/factory.json - { - "infix-factory-default:factory-default": {} - } - EOF - - if ! $sysrepocfg -f json -R /tmp/factory.json; then - rm -f /etc/sysrepo/data/*startup* - rm -f /etc/sysrepo/data/*running* - rm -f /dev/shm/sr_* - killall sysrepo-plugind - fi - ;; - failure-config) - err "Failed loading $fn, aborting!" - banner_append "CRITICAL ERROR: Logins are disabled, no credentials available" - initctl -nbq runlevel 9 - ;; - *) - err "Unknown config $config, aborting!" - ;; - esac - - exit 1 -else - note "Success, syncing with startup datastore." - $sysrepocfg -v2 -d startup -C running -fi - -note "Loaded $fn successfully." -if [ "$config" = "failure-config" ]; then - banner_append "ERROR: Corrupt startup-config, system has reverted to default login credentials" -else - perms "$fn" -fi diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 6266615be..72299e869 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -77,7 +77,8 @@ PKG_CHECK_MODULES([crypt], [libxcrypt >= 4.4.27]) PKG_CHECK_MODULES([glib], [glib-2.0 >= 2.50 gio-2.0 gio-unix-2.0]) PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0]) PKG_CHECK_MODULES([libite], [libite >= 2.6.1]) -PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36]) +PKG_CHECK_MODULES([sysrepo], [sysrepo >= 4.2.10]) +PKG_CHECK_MODULES([libyang], [libyang >= 4.2.2]) PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0]) # Control build with automake flags diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 08bf1cb73..67b2538f7 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -1,8 +1,15 @@ AM_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE -D_GNU_SOURCE -DYANG_PATH_=\"$(YANGDIR)\" +AM_CPPFLAGS += -DCONFD_VERSION=\"$(PACKAGE_VERSION)\" CLEANFILES = $(rauc_installer_sources) plugindir = $(srpdplugindir) plugin_LTLIBRARIES = confd-plugin.la +sbin_PROGRAMS = confd + +confd_CFLAGS = $(sysrepo_CFLAGS) $(libyang_CFLAGS) $(jansson_CFLAGS) $(libite_CFLAGS) +confd_LDADD = $(sysrepo_LIBS) $(libyang_LIBS) $(jansson_LIBS) $(libite_LIBS) -ldl +confd_SOURCES = main.c + confd_plugin_la_LDFLAGS = -module -avoid-version -shared confd_plugin_la_CFLAGS = \ diff --git a/src/confd/src/main.c b/src/confd/src/main.c new file mode 100644 index 000000000..dbf77e5a2 --- /dev/null +++ b/src/confd/src/main.c @@ -0,0 +1,840 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * confd - Infix configuration daemon + * + * Replaces sysrepo-plugind + bootstrap + load with a single binary. + * One sr_connect(), all datastore operations in-process, then load + * plugins and enter the event loop. + * + * Copyright (c) 2018 - 2021 Deutsche Telekom AG. + * Copyright (c) 2018 - 2021 CESNET, z.s.p.o. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* Callback type names from sysrepo plugin API */ +#define SRP_INIT_CB "sr_plugin_init_cb" +#define SRP_CLEANUP_CB "sr_plugin_cleanup_cb" + +#ifndef CONFD_VERSION +#define CONFD_VERSION PACKAGE_VERSION +#endif + +#ifndef SRPD_PLUGINS_PATH +#define SRPD_PLUGINS_PATH "/usr/lib/sysrepo-plugind/plugins" +#endif + +struct plugin { + void *handle; + char *name; + int (*init_cb)(sr_session_ctx_t *session, void **private_data); + void (*cleanup_cb)(sr_session_ctx_t *session, void *private_data); + void *private_data; + int initialized; +}; + +/* Protected flag for terminating */ +static int loop_finish; +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; + +static void error_print(int sr_error, const char *format, ...) +{ + va_list ap; + char msg[2048]; + + if (!sr_error) + snprintf(msg, sizeof(msg), "confd error: %s\n", format); + else + snprintf(msg, sizeof(msg), "confd error: %s (%s)\n", format, sr_strerror(sr_error)); + + va_start(ap, format); + vfprintf(stderr, msg, ap); + va_end(ap); +} + +static void version_print(void) +{ + printf("confd - Infix configuration daemon v%s, compiled with libsysrepo v%s\n\n", + CONFD_VERSION, SR_VERSION); +} + +static void help_print(void) +{ + printf( + "Usage:\n" + " confd [-h] [-V] [-v ] [-d] [-n] [-f] [-p pidfile]\n" + " [-F factory-config] [-S startup-config] [-E failure-config]\n" + " [-t timeout]\n" + "\n" + "Options:\n" + " -h, --help Prints usage help.\n" + " -V, --version Prints version information.\n" + " -v, --verbosity \n" + " Change verbosity to a level (none, error, warning, info, debug) or\n" + " number (0, 1, 2, 3, 4).\n" + " -d, --debug Debug mode - not daemonized and logs to stderr.\n" + " -n, --foreground Run in foreground and log to syslog.\n" + " -f, --fatal-plugin-fail\n" + " Terminate if any plugin initialization fails.\n" + " -p, --pid-file \n" + " Create a PID file at the specified path.\n" + " -F, --factory-config \n" + " Factory default config file (default: /etc/factory-config.cfg).\n" + " -S, --startup-config \n" + " Startup config file (default: /cfg/startup-config.cfg).\n" + " -E, --failure-config \n" + " Failure fallback config file (default: /etc/failure-config.cfg).\n" + " -t, --timeout Sysrepo operation timeout in seconds (default: 60).\n" + "\n" + "Environment variable $SRPD_PLUGINS_PATH overwrites the default plugins directory.\n" + "\n"); +} + +static void signal_handler(int sig) +{ + switch (sig) { + case SIGINT: + case SIGQUIT: + case SIGABRT: + case SIGTERM: + case SIGHUP: + pthread_mutex_lock(&lock); + if (!loop_finish) { + loop_finish = 1; + pthread_cond_signal(&cond); + } else { + error_print(0, "Exiting without a proper cleanup"); + exit(EXIT_FAILURE); + } + pthread_mutex_unlock(&lock); + break; + default: + error_print(0, "Exiting on receiving an unhandled signal"); + exit(EXIT_FAILURE); + } +} + +static void handle_signals(void) +{ + struct sigaction action; + sigset_t block_mask; + + sigfillset(&block_mask); + action.sa_handler = signal_handler; + action.sa_mask = block_mask; + action.sa_flags = 0; + sigaction(SIGINT, &action, NULL); + sigaction(SIGQUIT, &action, NULL); + sigaction(SIGABRT, &action, NULL); + sigaction(SIGTERM, &action, NULL); + sigaction(SIGHUP, &action, NULL); + + action.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &action, NULL); + sigaction(SIGTSTP, &action, NULL); + sigaction(SIGTTIN, &action, NULL); + sigaction(SIGTTOU, &action, NULL); +} + +static void daemon_init(int debug, sr_log_level_t log_level) +{ + pid_t pid = 0, sid = 0; + int fd = -1; + + if (debug) { + handle_signals(); + if (debug < 0) + goto done; + sr_log_stderr(log_level); + return; + } + + pid = fork(); + if (pid < 0) { + error_print(0, "fork() failed (%s).", strerror(errno)); + exit(EXIT_FAILURE); + } + if (pid > 0) + exit(EXIT_SUCCESS); + + handle_signals(); + + sid = setsid(); + if (sid < 0) { + error_print(0, "setsid() failed (%s).", strerror(errno)); + exit(EXIT_FAILURE); + } + + if (chdir("/") < 0) { + error_print(0, "chdir() failed (%s).", strerror(errno)); + exit(EXIT_FAILURE); + } + + fd = open("/dev/null", O_RDWR, 0); + if (fd != -1) { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + } + +done: + sr_log_syslog("confd", log_level); +} + +static int open_pidfile(const char *pidfile) +{ + int pidfd; + + pidfd = open(pidfile, O_RDWR | O_CREAT, 0640); + if (pidfd < 0) { + error_print(0, "Unable to open the PID file \"%s\" (%s).", pidfile, strerror(errno)); + return -1; + } + + if (lockf(pidfd, F_TLOCK, 0) < 0) { + if (errno == EACCES || errno == EAGAIN) + error_print(0, "Another instance of confd is running."); + else + error_print(0, "Unable to lock the PID file \"%s\" (%s).", pidfile, strerror(errno)); + close(pidfd); + return -1; + } + + return pidfd; +} + +static int write_pidfile(int pidfd) +{ + char pid[30] = {0}; + int pid_len; + + if (ftruncate(pidfd, 0)) { + error_print(0, "Failed to truncate pid file (%s).", strerror(errno)); + return -1; + } + + snprintf(pid, sizeof(pid) - 1, "%ld\n", (long)getpid()); + pid_len = strlen(pid); + if (write(pidfd, pid, pid_len) < pid_len) { + error_print(0, "Failed to write PID into pid file (%s).", strerror(errno)); + return -1; + } + + return 0; +} + +/* + * Plugin loading -- external .so files only (no internal plugins) + */ +static size_t path_len_no_ext(const char *path) +{ + const char *dot; + + dot = strrchr(path, '.'); + if (!dot || dot == path) + return 0; + + return dot - path; +} + +static int load_plugins(struct plugin **plugins, int *plugin_count) +{ + void *mem, *handle; + struct plugin *plugin; + DIR *dir; + struct dirent *ent; + const char *plugins_dir; + char *path; + size_t name_len; + int rc = 0; + + *plugins = NULL; + *plugin_count = 0; + + plugins_dir = getenv("SRPD_PLUGINS_PATH"); + if (!plugins_dir) + plugins_dir = SRPD_PLUGINS_PATH; + + dir = opendir(plugins_dir); + if (!dir) { + error_print(0, "Opening \"%s\" directory failed (%s).", plugins_dir, strerror(errno)); + return -1; + } + + while ((ent = readdir(dir))) { + if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) + continue; + + if (asprintf(&path, "%s/%s", plugins_dir, ent->d_name) == -1) { + error_print(0, "asprintf() failed (%s).", strerror(errno)); + rc = -1; + break; + } + handle = dlopen(path, RTLD_LAZY); + if (!handle) { + error_print(0, "Opening plugin \"%s\" failed (%s).", path, dlerror()); + free(path); + rc = -1; + break; + } + free(path); + + mem = realloc(*plugins, (*plugin_count + 1) * sizeof(**plugins)); + if (!mem) { + error_print(0, "realloc() failed (%s).", strerror(errno)); + dlclose(handle); + rc = -1; + break; + } + *plugins = mem; + plugin = &(*plugins)[*plugin_count]; + memset(plugin, 0, sizeof(*plugin)); + + *(void **)&plugin->init_cb = dlsym(handle, SRP_INIT_CB); + if (!plugin->init_cb) { + error_print(0, "Failed to find function \"%s\" in plugin \"%s\".", + SRP_INIT_CB, ent->d_name); + dlclose(handle); + rc = -1; + break; + } + + *(void **)&plugin->cleanup_cb = dlsym(handle, SRP_CLEANUP_CB); + if (!plugin->cleanup_cb) { + error_print(0, "Failed to find function \"%s\" in plugin \"%s\".", + SRP_CLEANUP_CB, ent->d_name); + dlclose(handle); + rc = -1; + break; + } + + plugin->handle = handle; + + name_len = path_len_no_ext(ent->d_name); + if (name_len == 0) { + error_print(0, "Wrong filename \"%s\".", ent->d_name); + dlclose(handle); + rc = -1; + break; + } + + plugin->name = strndup(ent->d_name, name_len); + if (!plugin->name) { + error_print(0, "strndup() failed."); + dlclose(handle); + rc = -1; + break; + } + + ++(*plugin_count); + } + + closedir(dir); + return rc; +} + +/* + * Wipe stale sysrepo SHM files for a clean slate every boot. + */ +static void wipe_sysrepo_shm(void) +{ + glob_t gl; + + if (glob("/dev/shm/sr_*", 0, NULL, &gl) == 0) { + for (size_t i = 0; i < gl.gl_pathc; i++) + unlink(gl.gl_pathv[i]); + globfree(&gl); + } +} + +const char *basenm(const char *path) +{ + const char *slash; + + if (!path) + return NULL; + + slash = strrchr(path, '/'); + if (slash) + return slash[1] ? slash + 1 : NULL; + + return path; +} + +/* + * Append error message to login banners. + */ +static void banner_append(const char *msg) +{ + const char *files[] = { + "/etc/banner", + "/etc/issue", + "/etc/issue.net", + }; + + for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); i++) { + FILE *fp = fopen(files[i], "a"); + + if (fp) { + fprintf(fp, "\n%s\n", msg); + fclose(fp); + } + } +} + +/* + * Smart migration: only fork+exec the migrate script if the version + * in the config file doesn't match the current confd version. + */ +static int maybe_migrate(const char *path) +{ + const char *backup_dir = "/cfg/backup"; + json_t *root, *meta, *ver; + const char *file_ver; + char backup[256]; + int rc; + + root = json_load_file(path, 0, NULL); + if (!root) + return -1; + + meta = json_object_get(root, "infix-meta:meta"); + ver = meta ? json_object_get(meta, "version") : NULL; + file_ver = ver ? json_string_value(ver) : "0.0"; + + if (!strcmp(file_ver, CONFD_VERSION)) { + json_decref(root); + return 0; + } + json_decref(root); + + SRPLG_LOG_INF("confd", "%s config version %s vs confd %s, migrating ...", + path, file_ver, CONFD_VERSION); + + mkpath(backup_dir, 0770); + chown(backup_dir, 0, 10); /* root:wheel */ + + snprintf(backup, sizeof(backup), "%s/%s", backup_dir, basenm(path)); + rc = systemf("migrate -i -b \"%s\" \"%s\"", backup, path); + if (rc) + SRPLG_LOG_ERR("confd", "Migration of %s failed (rc=%d)", path, rc); + + return rc; +} + +static int file_exists(const char *path) +{ + return access(path, F_OK) == 0; +} + +/* + * Load a JSON config file into the running datastore. + * Mirrors what sysrepocfg -I does: lyd_parse_data() + sr_replace_config(). + */ +static int load_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, + const char *path, uint32_t timeout_ms) +{ + const struct ly_ctx *ly_ctx; + struct lyd_node *data = NULL; + struct ly_in *in = NULL; + LY_ERR lyrc; + int r; + + ly_ctx = sr_acquire_context(conn); + + lyrc = ly_in_new_filepath(path, 0, &in); + if (lyrc == LY_EINVAL) { + /* empty file */ + char *empty = strdup(""); + + ly_in_new_memory(empty, &in); + } else if (lyrc) { + error_print(0, "Failed to open \"%s\" for reading", path); + sr_release_context(conn); + return -1; + } + + lyrc = lyd_parse_data(ly_ctx, NULL, in, LYD_JSON, + LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STRICT, + 0, &data); + ly_in_free(in, 1); + + if (lyrc) { + SRPLG_LOG_ERR("confd", "Parsing %s failed", path); + sr_release_context(conn); + return -1; + } + + sr_release_context(conn); + + r = sr_replace_config(sess, NULL, data, timeout_ms); + if (r != SR_ERR_OK) { + SRPLG_LOG_ERR("confd", "sr_replace_config failed: %s", sr_strerror(r)); + return -1; + } + + return 0; +} + +/* + * Export running datastore to a JSON file. + */ +static int export_running(sr_session_ctx_t *sess, const char *path, uint32_t timeout_ms) +{ + sr_data_t *data = NULL; + FILE *fp; + int r; + + r = sr_get_data(sess, "/*", 0, timeout_ms, 0, &data); + if (r != SR_ERR_OK) { + SRPLG_LOG_ERR("confd", "sr_get_data failed: %s", sr_strerror(r)); + return -1; + } + + fp = fopen(path, "w"); + if (!fp) { + SRPLG_LOG_ERR("confd", "Failed to open %s for writing: %s", path, strerror(errno)); + sr_release_data(data); + return -1; + } + + lyd_print_file(fp, data ? data->tree : NULL, LYD_JSON, LYD_PRINT_SIBLINGS); + fclose(fp); + sr_release_data(data); + + chown(path, 0, 0); /* root:root initially */ + chmod(path, 0660); + + /* Set group to 'wheel' for admin access */ + systemf("chgrp wheel '%s' 2>/dev/null", path); + + return 0; +} + +/* + * Handle startup-config load failure: revert to factory-default, + * then load failure-config, set error banners. + */ +static void handle_startup_failure(sr_session_ctx_t *sess, const char *failure_path, + sr_conn_ctx_t *conn, uint32_t timeout_ms) +{ + int r; + + SRPLG_LOG_ERR("confd", "Failed loading startup-config, reverting to Fail Secure mode!"); + + /* Reset to factory-default */ + r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms); + if (r != SR_ERR_OK) { + SRPLG_LOG_ERR("confd", "sr_copy_config(factory-default) failed: %s", sr_strerror(r)); + /* Nuclear option: wipe everything */ + systemf("rm -f /etc/sysrepo/data/*startup* /etc/sysrepo/data/*running* /dev/shm/sr_*"); + return; + } + + /* Load failure-config on top */ + if (file_exists(failure_path)) { + if (load_config(conn, sess, failure_path, timeout_ms)) { + SRPLG_LOG_ERR("confd", "Failed loading failure-config, aborting!"); + banner_append("CRITICAL ERROR: Logins are disabled, no credentials available"); + systemf("initctl -nbq runlevel 9"); + return; + } + } + + banner_append("ERROR: Corrupt startup-config, system has reverted to default login credentials"); +} + +/* + * Enable test-mode if the test-mode marker exists. + */ +static void maybe_enable_test_mode(void) +{ + if (file_exists("/mnt/aux/test-mode")) + systemf("sysrepoctl -c infix-test -e test-mode-enable"); +} + +/* + * Determine which config to load: + * - test-mode (unless override exists) + * - startup-config + * - first-boot from factory + */ +static int bootstrap_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, + const char *factory_path, const char *startup_path, + const char *failure_path, const char *test_path, + uint32_t timeout_ms) +{ + const char *config_path; + int r; + + /* Test mode support */ + if (file_exists("/mnt/aux/test-mode")) { + if (file_exists("/mnt/aux/test-override-startup")) { + unlink("/mnt/aux/test-override-startup"); + config_path = startup_path; + } else { + SRPLG_LOG_INF("confd", "Test mode detected, switching to test-config"); + config_path = test_path; + } + } else { + config_path = startup_path; + } + + if (file_exists(config_path)) { + /* Run migration if needed */ + maybe_migrate(config_path); + + /* Load startup (or test) config */ + SRPLG_LOG_INF("confd", "Loading %s ...", config_path); + if (load_config(conn, sess, config_path, timeout_ms)) { + handle_startup_failure(sess, failure_path, conn, timeout_ms); + return 0; /* continue running even in fail-secure */ + } + + SRPLG_LOG_INF("confd", "Loaded %s successfully, syncing startup datastore.", config_path); + sr_session_switch_ds(sess, SR_DS_STARTUP); + r = sr_copy_config(sess, NULL, SR_DS_RUNNING, timeout_ms); + sr_session_switch_ds(sess, SR_DS_RUNNING); + if (r != SR_ERR_OK) + SRPLG_LOG_WRN("confd", "Failed to sync startup datastore: %s", sr_strerror(r)); + + return 0; + } + + /* First boot: no startup-config, initialize from factory */ + SRPLG_LOG_INF("confd", "startup-config missing, initializing from factory-config"); + + r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms); + if (r != SR_ERR_OK) { + SRPLG_LOG_ERR("confd", "sr_copy_config(factory-default) failed: %s", sr_strerror(r)); + return -1; + } + + /* Export running → startup file */ + if (export_running(sess, startup_path, timeout_ms)) + SRPLG_LOG_WRN("confd", "Failed to export running to %s", startup_path); + + return 0; +} + +int main(int argc, char **argv) +{ + struct plugin *plugins = NULL; + sr_conn_ctx_t *conn = NULL; + sr_session_ctx_t *sess = NULL; + sr_log_level_t log_level = SR_LL_ERR; + int plugin_count = 0, i, r, rc = EXIT_FAILURE, opt, debug = 0; + int pidfd = -1, fatal_fail = 0; + const char *pidfile = NULL; + const char *factory_path = "/etc/factory-config.cfg"; + const char *startup_path = "/cfg/startup-config.cfg"; + const char *failure_path = "/etc/failure-config.cfg"; + const char *test_path = "/etc/test-config.cfg"; + uint32_t timeout_s = 60; + uint32_t timeout_ms; + + struct option options[] = { + {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, 'V'}, + {"verbosity", required_argument, NULL, 'v'}, + {"debug", no_argument, NULL, 'd'}, + {"foreground", no_argument, NULL, 'n'}, + {"pid-file", required_argument, NULL, 'p'}, + {"fatal-plugin-fail", no_argument, NULL, 'f'}, + {"factory-config", required_argument, NULL, 'F'}, + {"startup-config", required_argument, NULL, 'S'}, + {"failure-config", required_argument, NULL, 'E'}, + {"timeout", required_argument, NULL, 't'}, + {NULL, 0, NULL, 0}, + }; + + opterr = 0; + while ((opt = getopt_long(argc, argv, "hVv:dnp:fF:S:E:t:", options, NULL)) != -1) { + switch (opt) { + case 'h': + version_print(); + help_print(); + rc = EXIT_SUCCESS; + goto cleanup; + case 'V': + version_print(); + rc = EXIT_SUCCESS; + goto cleanup; + case 'v': + if (!strcmp(optarg, "none")) + log_level = SR_LL_NONE; + else if (!strcmp(optarg, "error")) + log_level = SR_LL_ERR; + else if (!strcmp(optarg, "warning")) + log_level = SR_LL_WRN; + else if (!strcmp(optarg, "info")) + log_level = SR_LL_INF; + else if (!strcmp(optarg, "debug")) + log_level = SR_LL_DBG; + else if (strlen(optarg) == 1 && optarg[0] >= '0' && optarg[0] <= '4') + log_level = atoi(optarg); + else { + error_print(0, "Invalid verbosity \"%s\"", optarg); + goto cleanup; + } + break; + case 'd': + debug = 1; + break; + case 'n': + debug = -1; + break; + case 'p': + pidfile = optarg; + break; + case 'f': + fatal_fail = 1; + break; + case 'F': + factory_path = optarg; + break; + case 'S': + startup_path = optarg; + break; + case 'E': + failure_path = optarg; + break; + case 't': + timeout_s = (uint32_t)atoi(optarg); + break; + default: + error_print(0, "Invalid option or missing argument: -%c", optopt); + goto cleanup; + } + } + + if (optind < argc) { + error_print(0, "Redundant parameters"); + goto cleanup; + } + + timeout_ms = timeout_s * 1000; + + if (pidfile && (pidfd = open_pidfile(pidfile)) < 0) + goto cleanup; + + /* Load plugins from disk (dlopen) before daemonizing */ + if (load_plugins(&plugins, &plugin_count)) + error_print(0, "load_plugins failed (continuing)"); + + /* Daemonize -- after this point, confd no longer logs to stderr */ + daemon_init(debug, log_level); + + /* Phase 1: Wipe stale SHM for a clean slate */ + wipe_sysrepo_shm(); + + /* Phase 2: Connect to sysrepo (rebuilds SHM from installed YANG modules) */ + r = sr_connect(0, &conn); + if (r != SR_ERR_OK) { + error_print(r, "Failed to connect"); + goto cleanup; + } + + /* Phase 3: Install factory defaults into all datastores */ + SRPLG_LOG_INF("confd", "Loading factory-default datastore from %s ...", factory_path); + r = sr_install_factory_config(conn, factory_path); + if (r != SR_ERR_OK) { + SRPLG_LOG_ERR("confd", "sr_install_factory_config failed: %s", sr_strerror(r)); + goto cleanup; + } + + /* Phase 4: Start running-datastore session */ + r = sr_session_start(conn, SR_DS_RUNNING, &sess); + if (r != SR_ERR_OK) { + error_print(r, "Failed to start new session"); + goto cleanup; + } + + /* Phase 4b: Clear running datastore so plugin init sees an empty + * tree. This matches the original bootstrap flow where running + * was cleared with '{}' before sysrepo-plugind started. When we + * later load startup-config, the diff will be all-create which is + * what the plugin callbacks expect. */ + r = sr_replace_config(sess, NULL, NULL, timeout_ms); + if (r != SR_ERR_OK) { + SRPLG_LOG_ERR("confd", "Failed to clear running datastore: %s", sr_strerror(r)); + goto cleanup; + } + + /* Enable test-mode YANG feature if needed */ + maybe_enable_test_mode(); + + /* Phase 5: Initialize plugins (subscribe to YANG module changes) */ + for (i = 0; i < plugin_count; i++) { + r = plugins[i].init_cb(sess, &plugins[i].private_data); + if (r) { + SRPLG_LOG_ERR("confd", "Plugin \"%s\" initialization failed (%s).", + plugins[i].name, sr_strerror(r)); + if (fatal_fail) + goto cleanup; + } else { + SRPLG_LOG_INF("confd", "Plugin \"%s\" initialized.", plugins[i].name); + plugins[i].initialized = 1; + } + } + + /* Phase 6: Load startup config -- plugins are now subscribed, so + * sr_replace_config() will trigger their change callbacks. */ + if (bootstrap_config(conn, sess, factory_path, startup_path, + failure_path, test_path, timeout_ms)) + goto cleanup; + + /* Write PID file after everything is ready */ + if (pidfile && write_pidfile(pidfd) < 0) + goto cleanup; + + /* Wait for a terminating signal */ + pthread_mutex_lock(&lock); + while (!loop_finish) + pthread_cond_wait(&cond, &lock); + pthread_mutex_unlock(&lock); + + rc = EXIT_SUCCESS; + +cleanup: + while (plugin_count > 0) { + if (plugins[plugin_count - 1].initialized) + plugins[plugin_count - 1].cleanup_cb(sess, plugins[plugin_count - 1].private_data); + if (plugins[plugin_count - 1].handle) + dlclose(plugins[plugin_count - 1].handle); + free(plugins[plugin_count - 1].name); + --plugin_count; + } + free(plugins); + + if (pidfd >= 0) { + close(pidfd); + unlink(pidfile); + } + + sr_disconnect(conn); + return rc; +} From 5ed1d14053c7f474184478a897404136555a6108 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 12 Feb 2026 14:25:49 +0100 Subject: [PATCH 13/29] confd: relocate generation of mdns service records to C Simplify and consolidate generation of mdns service records from an external script to C. This reduces fork + exec and saves two seconds of boot time on single core Cortex-A7 systems. Signed-off-by: Joachim Wiberg --- src/confd/bin/Makefile.am | 2 +- src/confd/bin/gen-service | 47 ----------------------- src/confd/src/services.c | 78 +++++++++++++++++++++++++++++++++++---- 3 files changed, 71 insertions(+), 56 deletions(-) delete mode 100755 src/confd/bin/gen-service diff --git a/src/confd/bin/Makefile.am b/src/confd/bin/Makefile.am index 068f2b255..364e7b629 100644 --- a/src/confd/bin/Makefile.am +++ b/src/confd/bin/Makefile.am @@ -1,4 +1,4 @@ -pkglibexec_SCRIPTS = gen-config error gen-service gen-hostname \ +pkglibexec_SCRIPTS = gen-config error gen-hostname \ gen-interfaces gen-motd gen-hardware gen-version \ mstpd-wait-online wait-interface sbin_SCRIPTS = dagger migrate firewall diff --git a/src/confd/bin/gen-service b/src/confd/bin/gen-service deleted file mode 100755 index 66d50ed27..000000000 --- a/src/confd/bin/gen-service +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -# Very basic avahi .service generator, works for tcp (http) services -. /etc/os-release - -cmd=$1 -host=$2 -name=$3 -type=$4 -port=$5 -desc=$6 -shift 6 -file="/etc/avahi/services/$name.service" - -case $cmd in - delete) - rm -f "$file" - exit 0 - ;; - update) - if [ ! -f "$file" ]; then - exit 0 - fi - ;; - *) - ;; -esac - -cat <"$file" - - - - $desc - - $type - $port - $host.local - vv=1 - vendor=$(jq -r .vendor /run/system.json) - product=$(jq -r '."product-name"' /run/system.json) - serial=$(jq -r '."serial-number"' /run/system.json) - deviceid=$(jq -r '."mac-address"' /run/system.json) - vn=$VENDOR_NAME - on=$NAME - ov=$VERSION_ID$(for txt in "$@"; do printf "\n %s" "$txt"; done) - - -EOF diff --git a/src/confd/src/services.c b/src/confd/src/services.c index 97e126997..31682334a 100644 --- a/src/confd/src/services.c +++ b/src/confd/src/services.c @@ -19,9 +19,13 @@ #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, +#define AVAHI_SVC_PATH "/etc/avahi/services" + #define LLDP_CONFIG "/etc/lldpd.d/confd.conf" #define LLDP_CONFIG_NEXT LLDP_CONFIG"+" +enum mdns_cmd { MDNS_ADD, MDNS_DELETE, MDNS_UPDATE }; + #define FOREACH_SVC(SVC) \ SVC(none) \ SVC(ssh) \ @@ -68,6 +72,13 @@ struct mdns_svc { { ssh, "ssh", "_ssh._tcp", 22, "Secure shell command line interface (CLI)", NULL }, }; +static const char *jgets(json_t *obj, const char *key) +{ + json_t *val = json_object_get(obj, key); + + return val ? json_string_value(val) : NULL; +} + /* * On hostname changes we need to update the mDNS records, in particular * the ones advertising an adminurl (standarized by Apple), because they @@ -77,27 +88,78 @@ struct mdns_svc { * adminurl to include 'admin@%s.local' to pre-populate the default * username in the login dialog. */ -static int mdns_records(const char *cmd, svc type) +static int mdns_records(int cmd, svc type) { char hostname[MAXHOSTNAMELEN + 1]; + const char *vendor, *product, *serial, *mac; + const char *vn, *on, *ov; if (gethostname(hostname, sizeof(hostname))) { ERRNO("failed getting system hostname"); return SR_ERR_SYS; } + vendor = jgets(confd.root, "vendor"); + product = jgets(confd.root, "product-name"); + serial = jgets(confd.root, "serial-number"); + mac = jgets(confd.root, "mac-address"); + + vn = fgetkey("/etc/os-release", "VENDOR_NAME"); + on = fgetkey("/etc/os-release", "NAME"); + ov = fgetkey("/etc/os-release", "VERSION_ID"); + for (size_t i = 0; i < NELEMS(services); i++) { struct mdns_svc *srv = &services[i]; - char buf[256] = ""; + FILE *fp; if (type != all && srv->svc != type) continue; - if (srv->text) - snprintf(buf, sizeof(buf), srv->text, hostname); + if (cmd == MDNS_DELETE) { + erasef(AVAHI_SVC_PATH "/%s.service", srv->name); + continue; + } + + if (cmd == MDNS_UPDATE && !fexistf(AVAHI_SVC_PATH "/%s.service", srv->name)) + continue; + + fp = fopenf("w", AVAHI_SVC_PATH "/%s.service", srv->name); + if (!fp) { + ERRNO("failed creating %s.service", srv->name); + continue; + } + + fprintf(fp, + "\n" + "\n" + "\n" + " %s\n" + " \n" + " %s\n" + " %d\n" + " %s.local\n" + " vv=1\n" + " vendor=%s\n" + " product=%s\n" + " serial=%s\n" + " deviceid=%s\n" + " vn=%s\n" + " on=%s\n" + " ov=%s\n", + srv->desc, srv->type, srv->port, hostname, + vendor ?: "", product ?: "", serial ?: "", mac ?: "", + vn ?: "", on ?: "", ov ?: ""); + + if (srv->text) { + fprintf(fp, " "); + fprintf(fp, srv->text, hostname); + fprintf(fp, "\n"); + } - systemf("/usr/libexec/confd/gen-service %s %s %s %s %d \"%s\" %s", cmd, - hostname, srv->name, srv->type, srv->port, srv->desc, buf); + fprintf(fp, + " \n" + "\n"); + fclose(fp); } return SR_ERR_OK; @@ -182,7 +244,7 @@ static void svc_enadis(int ena, svc type, const char *svc) } if (type != none) - mdns_records(ena ? "add" : "delete", type); + mdns_records(ena ? MDNS_ADD : MDNS_DELETE, type); systemf("initctl -nbq touch avahi"); systemf("initctl -nbq touch nginx"); @@ -295,7 +357,7 @@ static int mdns_change(sr_session_ctx_t *session, struct lyd_node *config, struc mdns_conf(srv); /* Generate/update basic mDNS service records */ - mdns_records("update", all); + mdns_records(MDNS_UPDATE, all); } svc_enadis(ena, none, "avahi"); From 09c25d281328637f718fff7852dacd2cb0f73676 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 12 Feb 2026 14:28:39 +0100 Subject: [PATCH 14/29] board/common: postpone start of dbus to after confd bootstrap Infix technically does not need to start dbus and dnsmasq before confd has loaded the system startup-config. So we move it in time to save some CPU cycles for confd & C:o. Signed-off-by: Joachim Wiberg --- board/common/rootfs/etc/finit.d/dbus.conf | 2 +- package/confd/resolvconf.conf | 5 ++--- .../skeleton/etc/finit.d/available/dnsmasq.conf | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/board/common/rootfs/etc/finit.d/dbus.conf b/board/common/rootfs/etc/finit.d/dbus.conf index 790c757a8..43b4eb67c 100644 --- a/board/common/rootfs/etc/finit.d/dbus.conf +++ b/board/common/rootfs/etc/finit.d/dbus.conf @@ -1,5 +1,5 @@ # Override Finit plugin service cgroup.system name:dbus notify:none pid:!/run/messagebus.pid \ - [S123456789] /usr/bin/dbus-daemon --nofork --system --syslog-only \ + [S123456789] /usr/bin/dbus-daemon --nofork --system --syslog-only \ -- D-Bus message bus daemon diff --git a/package/confd/resolvconf.conf b/package/confd/resolvconf.conf index e08a1acc1..1edc74b71 100644 --- a/package/confd/resolvconf.conf +++ b/package/confd/resolvconf.conf @@ -1,3 +1,2 @@ -# Create initial /etc/resolv.conf after successful bootstrap, regardless -# of startup-config or failure-config. Condition set by confd. -task [S12345] resolvconf -u -- Update DNS configuration +# Update /etc/resolv.conf after successful bootstrap and reconf. +task [S12345] resolvconf -u -- diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf index a14723e8a..c95bf5819 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf @@ -1 +1 @@ -service [S12345] dnsmasq -k -u root -- DHCP/DNS proxy +service [S12345] dnsmasq -k -u root -- DHCP/DNS proxy From e6854712dd69abbec34e77da511685f07edf88f3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 12 Feb 2026 14:31:44 +0100 Subject: [PATCH 15/29] statd: move start to runlevel 2 to save CPU cycles at bootstrap Also, we don't need to start a logger process for statd, it behaves nicely and uses syslog. Signed-off-by: Joachim Wiberg --- package/statd/statd.conf | 2 +- src/statd/python/yanger/__main__.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/package/statd/statd.conf b/package/statd/statd.conf index 53f5214da..d88025583 100644 --- a/package/statd/statd.conf +++ b/package/statd/statd.conf @@ -1,3 +1,3 @@ #set DEBUG=1 -service name:statd log [S12345] statd -f -p /run/statd.pid -n -- Status daemon +service name:statd [12345] statd -f -p /run/statd.pid -n -- Status daemon diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index e156174c4..eb6daec9f 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -82,7 +82,7 @@ def dirpath(path): from . import ietf_ntp yang_data = ietf_ntp.operational() elif args.model == 'ieee802-dot1ab-lldp': - from . import infix_lldp + from . import infix_lldp yang_data = infix_lldp.operational() elif args.model == 'infix-firewall': from . import infix_firewall @@ -90,9 +90,6 @@ def dirpath(path): elif args.model == 'ietf-bfd-ip-sh': from . import ietf_bfd_ip_sh yang_data = ietf_bfd_ip_sh.operational() - elif args.model == 'infix-wifi-radio': - from . import infix_wifi_radio - yang_data = infix_wifi_radio.operational() else: common.LOG.warning("Unsupported model %s", args.model) sys.exit(1) From ea44a85534c4f63f1546f4b55e3faa8c69f3ff53 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 12 Feb 2026 14:41:31 +0100 Subject: [PATCH 16/29] Drop useless logger processes, these services already use syslog Note, rousette does not support SIGHUP, so let's mark it as such. Signed-off-by: Joachim Wiberg --- board/common/rootfs/etc/finit.d/10-infix.conf | 2 +- board/common/rootfs/etc/finit.d/available/netconf.conf | 2 +- board/common/rootfs/etc/finit.d/available/restconf.conf | 4 ++-- package/klish/klish.svc | 2 +- package/mdns-alias/mdns-alias.svc | 5 +++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/board/common/rootfs/etc/finit.d/10-infix.conf b/board/common/rootfs/etc/finit.d/10-infix.conf index d2aa8fce5..fc65d79fb 100644 --- a/board/common/rootfs/etc/finit.d/10-infix.conf +++ b/board/common/rootfs/etc/finit.d/10-infix.conf @@ -1,3 +1,3 @@ -task name:ixinit log:tag:ixinit [S] \ +task name:ixinit [S] \ /usr/libexec/finit/runparts -bp /usr/libexec/infix/init.d \ -- Probing system diff --git a/board/common/rootfs/etc/finit.d/available/netconf.conf b/board/common/rootfs/etc/finit.d/available/netconf.conf index 656c67422..a591fb166 100644 --- a/board/common/rootfs/etc/finit.d/available/netconf.conf +++ b/board/common/rootfs/etc/finit.d/available/netconf.conf @@ -1,3 +1,3 @@ -service name:netopeer notify:none log env:/etc/default/confd \ +service name:netopeer notify:none log:null env:/etc/default/confd \ [12345] netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \ -- NETCONF server diff --git a/board/common/rootfs/etc/finit.d/available/restconf.conf b/board/common/rootfs/etc/finit.d/available/restconf.conf index 993fc0774..1ac89a1fc 100644 --- a/board/common/rootfs/etc/finit.d/available/restconf.conf +++ b/board/common/rootfs/etc/finit.d/available/restconf.conf @@ -1,3 +1,3 @@ -service name:rousette notify:none log:null env:/etc/default/confd \ - [12345] rousette --syslog -t $CONFD_TIMEOUT \ +service notify:none log:null env:/etc/default/confd \ + [12345] rousette --syslog -t $CONFD_TIMEOUT \ -- RESTCONF server diff --git a/package/klish/klish.svc b/package/klish/klish.svc index 6344c8a28..b044b89b2 100644 --- a/package/klish/klish.svc +++ b/package/klish/klish.svc @@ -1 +1 @@ -service log [2345] /usr/bin/klishd -d -- CLI backend daemon +service log:null [12345] /usr/bin/klishd -d -- CLI backend daemon diff --git a/package/mdns-alias/mdns-alias.svc b/package/mdns-alias/mdns-alias.svc index dfe65d8ff..376ccb586 100644 --- a/package/mdns-alias/mdns-alias.svc +++ b/package/mdns-alias/mdns-alias.svc @@ -1,2 +1,3 @@ -service env:-/etc/default/mdns-alias log:prio:daemon.debug,tag:mdns \ - [2345] mdns-alias $MDNS_ALIAS_ARGS -- mDNS alias advertiser +service log:null env:-/etc/default/mdns-alias \ + [2345] mdns-alias $MDNS_ALIAS_ARGS \ + -- mDNS alias advertiser From a7ebf9318341b803978c05a6d89c305f2a8096a6 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 13 Feb 2026 08:40:34 +0100 Subject: [PATCH 17/29] confd: consolidate gen-config to start earlier Allow confd to start even earlier at boot and call 'gen-config' as a background task, pending on it to complete before we load the sysrepo factory datastore. Also, add Finit style progress to console so users can see the phases of the bootstrap process. Signed-off-by: Joachim Wiberg --- package/confd/confd.conf | 15 ++--- src/confd/bin/Makefile.am | 2 +- src/confd/bin/error | 4 -- src/confd/src/core.c | 5 +- src/confd/src/main.c | 120 ++++++++++++++++++++++++++++++-------- 5 files changed, 102 insertions(+), 44 deletions(-) delete mode 100755 src/confd/bin/error diff --git a/package/confd/confd.conf b/package/confd/confd.conf index c39ab50a7..42385ad83 100644 --- a/package/confd/confd.conf +++ b/package/confd/confd.conf @@ -1,19 +1,12 @@ #set DEBUG=1 -# Phase 1: Generate factory/failure/test configs (shell) -run name:bootstrap log:prio:user.notice norestart \ - [S] /usr/libexec/confd/gen-config \ - -- Generating factory configuration - -run name:error :1 log:console norestart if: \ - [S] /usr/libexec/confd/error -- - -# Phase 2: Single daemon handles datastore init, config load, and plugins +# Single daemon handles gen-config, datastore init, config load, and plugins # log:prio:daemon.err -service name:confd env:/etc/default/confd \ - [S12345] confd -f -n -v warning -p /run/confd.pid \ +service log:console env:/etc/default/confd \ + [S12345] confd -f -n -v warning \ -F /etc/factory-config.cfg \ -S /cfg/startup-config.cfg \ -E /etc/failure-config.cfg \ + -p /run/confd.pid \ -t $CONFD_TIMEOUT \ -- Configuration daemon diff --git a/src/confd/bin/Makefile.am b/src/confd/bin/Makefile.am index 364e7b629..83ba7da5a 100644 --- a/src/confd/bin/Makefile.am +++ b/src/confd/bin/Makefile.am @@ -1,4 +1,4 @@ -pkglibexec_SCRIPTS = gen-config error gen-hostname \ +pkglibexec_SCRIPTS = gen-config gen-hostname \ gen-interfaces gen-motd gen-hardware gen-version \ mstpd-wait-online wait-interface sbin_SCRIPTS = dagger migrate firewall diff --git a/src/confd/bin/error b/src/confd/bin/error deleted file mode 100755 index 4ed1ca3e9..000000000 --- a/src/confd/bin/error +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -# Override using an overlay in your br2-external to change the behavior - -logger -sik -p user.error "The device has reached an unrecoverable error, please RMA." diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 4acce329a..b2bdc8432 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -431,11 +431,8 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod if (event == SR_EV_DONE) { /* skip reload in bootstrap, implicit reload in runlevel change */ - if (systemf("runlevel >/dev/null 2>&1")) { - /* trigger any tasks waiting for confd to have applied *-config */ - system("initctl -nbq cond set bootstrap"); + if (systemf("runlevel >/dev/null 2>&1")) return SR_ERR_OK; - } if (systemf("initctl -b reload")) { EMERG("initctl reload: failed applying new configuration!"); diff --git a/src/confd/src/main.c b/src/confd/src/main.c index dbf77e5a2..38966a473 100644 --- a/src/confd/src/main.c +++ b/src/confd/src/main.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +75,22 @@ static void error_print(int sr_error, const char *format, ...) va_end(ap); } +/* Finit style progress output on console */ +static void conout(int rc, const char *fmt, ...) +{ + const char *sta = "%s\e[1m[\e[1;%dm%s\e[0m\e[1m]\e[0m %s"; + const char *msg[] = { " OK ", "FAIL", "WARN", " ⋯ " }; + const int col[] = { 32, 31, 33, 33 }; + const char *cr = rc == 3 ? "" : "\r"; + char buf[80]; + va_list ap; + + snprintf(buf, sizeof(buf), sta, cr, col[rc], msg[rc], fmt); + va_start(ap, fmt); + vfprintf(stderr, buf, ap); + va_end(ap); +} + static void version_print(void) { printf("confd - Infix configuration daemon v%s, compiled with libsysrepo v%s\n\n", @@ -158,10 +175,26 @@ static void handle_signals(void) sigaction(SIGTTOU, &action, NULL); } +static void quiet_now(void) +{ + int fd = -1; + + fd = open("/dev/null", O_RDWR, 0); + if (fd != -1) { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + } + + nice(0); +} + static void daemon_init(int debug, sr_log_level_t log_level) { pid_t pid = 0, sid = 0; - int fd = -1; + + nice(-20); if (debug) { handle_signals(); @@ -192,14 +225,7 @@ static void daemon_init(int debug, sr_log_level_t log_level) exit(EXIT_FAILURE); } - fd = open("/dev/null", O_RDWR, 0); - if (fd != -1) { - dup2(fd, STDIN_FILENO); - dup2(fd, STDOUT_FILENO); - dup2(fd, STDERR_FILENO); - close(fd); - } - + quiet_now(); done: sr_log_syslog("confd", log_level); } @@ -514,6 +540,7 @@ static int export_running(sr_session_ctx_t *sess, const char *path, uint32_t tim return -1; } + umask(0006); fp = fopen(path, "w"); if (!fp) { SRPLG_LOG_ERR("confd", "Failed to open %s for writing: %s", path, strerror(errno)); @@ -525,11 +552,7 @@ static int export_running(sr_session_ctx_t *sess, const char *path, uint32_t tim fclose(fp); sr_release_data(data); - chown(path, 0, 0); /* root:root initially */ - chmod(path, 0660); - - /* Set group to 'wheel' for admin access */ - systemf("chgrp wheel '%s' 2>/dev/null", path); + chown(path, 0, 10); /* root:wheel for admin group access */ return 0; } @@ -572,8 +595,13 @@ static void handle_startup_failure(sr_session_ctx_t *sess, const char *failure_p */ static void maybe_enable_test_mode(void) { - if (file_exists("/mnt/aux/test-mode")) - systemf("sysrepoctl -c infix-test -e test-mode-enable"); + if (file_exists("/mnt/aux/test-mode")) { + int rc; + + conout(3, "Enbling test mode"); + rc = systemf("sysrepoctl -c infix-test -e test-mode-enable"); + conout(rc ? 1 : 0, "\n"); + } } /* @@ -640,6 +668,12 @@ static int bootstrap_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, return 0; } +static void *gen_config_thread(void *arg) +{ + (void)arg; + return (void *)(intptr_t)systemf("/usr/libexec/confd/gen-config"); +} + int main(int argc, char **argv) { struct plugin *plugins = NULL; @@ -648,6 +682,8 @@ int main(int argc, char **argv) sr_log_level_t log_level = SR_LL_ERR; int plugin_count = 0, i, r, rc = EXIT_FAILURE, opt, debug = 0; int pidfd = -1, fatal_fail = 0; + pthread_t tid; + void *tret; const char *pidfile = NULL; const char *factory_path = "/etc/factory-config.cfg"; const char *startup_path = "/cfg/startup-config.cfg"; @@ -748,6 +784,14 @@ int main(int argc, char **argv) /* Daemonize -- after this point, confd no longer logs to stderr */ daemon_init(debug, log_level); + /* Start gen-config in parallel -- thread is joined before we need the result */ + conout(3, "Generating factory-config and failure-config"); + if (pthread_create(&tid, NULL, gen_config_thread, NULL)) { + SRPLG_LOG_ERR("confd", "Failed to create gen-config thread: %s", strerror(errno)); + conout(1, "\n"); + goto cleanup; + } + /* Phase 1: Wipe stale SHM for a clean slate */ wipe_sysrepo_shm(); @@ -758,22 +802,34 @@ int main(int argc, char **argv) goto cleanup; } - /* Phase 3: Install factory defaults into all datastores */ + /* Phase 3: Wait for gen-config thread to finish */ + pthread_join(tid, &tret); + if ((intptr_t)tret != 0) { + SRPLG_LOG_ERR("confd", "gen-config failed (rc=%d)", (int)(intptr_t)tret); + conout(1, "\n"); + goto cleanup; + } + conout(0, "\n"); + + /* Phase 4: Install factory defaults into all datastores */ SRPLG_LOG_INF("confd", "Loading factory-default datastore from %s ...", factory_path); + conout(3, "Loading factory-default datastore"); r = sr_install_factory_config(conn, factory_path); if (r != SR_ERR_OK) { SRPLG_LOG_ERR("confd", "sr_install_factory_config failed: %s", sr_strerror(r)); + conout(1, "\n"); goto cleanup; } + conout(0, "\n"); - /* Phase 4: Start running-datastore session */ + /* Phase 5: Start running-datastore session */ r = sr_session_start(conn, SR_DS_RUNNING, &sess); if (r != SR_ERR_OK) { error_print(r, "Failed to start new session"); goto cleanup; } - /* Phase 4b: Clear running datastore so plugin init sees an empty + /* Phase 6: Clear running datastore so plugin init sees an empty * tree. This matches the original bootstrap flow where running * was cleared with '{}' before sysrepo-plugind started. When we * later load startup-config, the diff will be all-create which is @@ -787,25 +843,41 @@ int main(int argc, char **argv) /* Enable test-mode YANG feature if needed */ maybe_enable_test_mode(); - /* Phase 5: Initialize plugins (subscribe to YANG module changes) */ + /* Phase 7: Initialize plugins (subscribe to YANG module changes) */ + conout(3, "Loading confd plugins"); for (i = 0; i < plugin_count; i++) { r = plugins[i].init_cb(sess, &plugins[i].private_data); if (r) { SRPLG_LOG_ERR("confd", "Plugin \"%s\" initialization failed (%s).", plugins[i].name, sr_strerror(r)); - if (fatal_fail) + if (fatal_fail) { + conout(1, "\n"); goto cleanup; + } } else { SRPLG_LOG_INF("confd", "Plugin \"%s\" initialized.", plugins[i].name); plugins[i].initialized = 1; } } + conout(0, "\n"); - /* Phase 6: Load startup config -- plugins are now subscribed, so - * sr_replace_config() will trigger their change callbacks. */ + /* + * Phase 8: Load startup config -- plugins are now subscribed, so + * sr_replace_config() will trigger their change callbacks. + */ + conout(3, "Loading startup-config"); if (bootstrap_config(conn, sess, factory_path, startup_path, - failure_path, test_path, timeout_ms)) + failure_path, test_path, timeout_ms)) { + conout(1, "\n"); goto cleanup; + } + conout(0, "\n"); + + /* No more progress to show, go to quiet daemon mode */ + quiet_now(); + + /* Signal that bootstrap is complete (dbus, resolvconf depend on this) */ + symlink("/run/finit/cond/reconf", "/run/finit/cond/usr/bootstrap"); /* Write PID file after everything is ready */ if (pidfile && write_pidfile(pidfd) < 0) From 605d58b59aa4fee59184e6d426bfe40ef56e5638 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 13 Feb 2026 09:08:50 +0100 Subject: [PATCH 18/29] board/common: move hostname.d setup at runtime to build-time Follow-up to 90f619b which first introduced the /etc/hostname.d concept. This commit moves the setup of /etc/hostname.d to post-build.sh dropping the initial call to the hostname activation script a bit, since it is called anyway after bootstrap has finished. The scrip is also given a bit of a refrehs, reducing overhead and needless log messages. Signed-off-by: Joachim Wiberg --- board/common/post-build.sh | 4 ++ .../common/rootfs/usr/libexec/infix/hostname | 42 ++++++++----------- .../usr/libexec/infix/init.d/05-hostname | 18 -------- 3 files changed, 22 insertions(+), 42 deletions(-) delete mode 100755 board/common/rootfs/usr/libexec/infix/init.d/05-hostname diff --git a/board/common/post-build.sh b/board/common/post-build.sh index d707991f9..2f9e665ec 100755 --- a/board/common/post-build.sh +++ b/board/common/post-build.sh @@ -44,6 +44,10 @@ if [ -n "${ID_LIKE}" ]; then ID="${ID} ${ID_LIKE}" fi +# Initialize default hostname for hostname.d pattern +mkdir -p "$TARGET_DIR/etc/hostname.d" +cp "$TARGET_DIR/etc/hostname" "$TARGET_DIR/etc/hostname.d/10-default" + # This is a symlink to /usr/lib/os-release, so we remove this to keep # original Buildroot information. ixmsg "Creating /etc/os-release" diff --git a/board/common/rootfs/usr/libexec/infix/hostname b/board/common/rootfs/usr/libexec/infix/hostname index bfd1952b3..cefc86b5e 100755 --- a/board/common/rootfs/usr/libexec/infix/hostname +++ b/board/common/rootfs/usr/libexec/infix/hostname @@ -10,49 +10,43 @@ HOSTNAME_D="/etc/hostname.d" -# Ensure directory exists -mkdir -p "$HOSTNAME_D" - -# Find the highest priority file (reverse sort, take first) -hostname_file=$(ls -1 "$HOSTNAME_D" 2>/dev/null | sort -r | head -1) - -if [ -z "$hostname_file" ]; then +# Find highest priority file (reverse lexicographic) +set -- "$HOSTNAME_D"/* +if [ ! -e "$1" ]; then logger -it confd "No hostname sources found in $HOSTNAME_D" exit 1 fi -# Read hostname from the file (first line only, strip whitespace) -new_hostname=$(cat "$HOSTNAME_D/$hostname_file" | head -1 | tr -d '\n\r\t ') -if [ -z "$new_hostname" ]; then - logger -it confd "Empty hostname in $hostname_file" +file=$(printf '%s\n' "$@" | sort -r | head -n1) +IFS= read -r new < "$file" +new=$(printf '%s' "$new" | tr -d '\r\n\t ') +if [ -z "$new" ]; then + logger -it confd "Empty hostname in $file" exit 1 fi -if [ ${#new_hostname} -gt 64 ]; then - logger -it confd "Hostname too long (${#new_hostname} > 64) in $hostname_file" +if [ ${#new} -gt 64 ]; then + logger -it confd "Hostname too long (${#new} > 64) in $file" exit 1 fi # Check if hostname has actually changed -current_hostname=$(hostname) -if [ "$new_hostname" = "$current_hostname" ]; then +if [ "$new" = "$(hostname)" ]; then # No change needed, exit silently exit 0 fi # Set the hostname -logger -it confd "Setting hostname to '$new_hostname' from $hostname_file" -hostname "$new_hostname" - -# Update /etc/hostname (for persistence across reboots) -echo "$new_hostname" > /etc/hostname +if hostname "$new"; then + echo "$new" > /etc/hostname +fi # Update /etc/hosts (127.0.1.1 entry for proper name resolution) if grep -q "^127\.0\.1\.1" /etc/hosts; then - sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$new_hostname/" /etc/hosts + sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$new/" /etc/hosts else # Add entry if it doesn't exist - echo "127.0.1.1 $new_hostname" >> /etc/hosts + echo "127.0.1.1 $new" >> /etc/hosts fi # Notify services of hostname change, skip while in bootstrap @@ -61,8 +55,8 @@ if ! runlevel >/dev/null 2>&1; then exit 0 fi -initctl -bq status lldpd && lldpcli configure system hostname "$new_hostname" 2>/dev/null -initctl -bq status mdns && avahi-set-host-name "$new_hostname" 2>/dev/null +initctl -bq status lldpd && lldpcli configure system hostname "$new" 2>/dev/null +initctl -bq status mdns && avahi-set-host-name "$new" 2>/dev/null initctl -bq touch netbrowse 2>/dev/null # If called from dhcp script we need to reload to activate new name in syslogd diff --git a/board/common/rootfs/usr/libexec/infix/init.d/05-hostname b/board/common/rootfs/usr/libexec/infix/init.d/05-hostname deleted file mode 100755 index 2984ea3cc..000000000 --- a/board/common/rootfs/usr/libexec/infix/init.d/05-hostname +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# Initialize default hostname for hostname.d pattern -# This runs very early in boot to set up the default hostname entry - -HOSTNAME_D="/etc/hostname.d" - -# Ensure directory exists -mkdir -p "$HOSTNAME_D" - -# If no default exists yet, create it from /etc/hostname (from squashfs) -if [ ! -f "$HOSTNAME_D/10-default" ] && [ -f /etc/hostname ]; then - cp /etc/hostname "$HOSTNAME_D/10-default" -fi - -# Apply hostname using the deterministic helper -if [ -x /usr/libexec/infix/hostname ]; then - /usr/libexec/infix/hostname -fi From 6ab9e9cd7f57398f360e0daa91f5b10628b58e1e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 15 Feb 2026 05:51:01 +0100 Subject: [PATCH 19/29] board/common: probe for wifi radios Relocate probe of wifi radios from gen-hardware to 00-probe. This saves one python invocation and some precious CPU cycles at boot. Signed-off-by: Joachim Wiberg --- .../rootfs/usr/libexec/infix/init.d/00-probe | 44 +++++++++++++++++++ src/confd/bin/gen-hardware | 12 ++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/board/common/rootfs/usr/libexec/infix/init.d/00-probe b/board/common/rootfs/usr/libexec/infix/init.d/00-probe index d70671276..10f2ef53a 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/00-probe +++ b/board/common/rootfs/usr/libexec/infix/init.d/00-probe @@ -519,6 +519,48 @@ def probe_dtsystem(out): return 0 +def probe_wifi_radios(out): + """Probe wifi radios via sysfs/iw and store in output dict.""" + ieee80211 = "/sys/class/ieee80211" + if not os.path.exists(ieee80211): + return + + radios = sorted(os.listdir(ieee80211)) + if not radios: + return + + out["wifi-radios"] = [] + for phy in radios: + info = {"name": phy, "bands": []} + try: + result = subprocess.run( + ["iw", "phy", phy, "info"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode != 0: + continue + except Exception: + continue + + freqs = [] + for line in result.stdout.splitlines(): + stripped = line.strip() + if stripped.startswith("* ") and "MHz" in stripped: + try: + freqs.append(int(float(stripped.split()[1]))) + except (ValueError, IndexError): + pass + + if any(2400 <= f <= 2500 for f in freqs): + info["bands"].append({"name": "2.4GHz"}) + if any(5150 <= f <= 5900 for f in freqs): + info["bands"].append({"name": "5GHz"}) + if any(5955 <= f <= 7115 for f in freqs): + info["bands"].append({"name": "6GHz"}) + + out["wifi-radios"].append(info) + + def main(): out = { "vendor": None, @@ -544,6 +586,8 @@ def main(): if err: return err + probe_wifi_radios(out) + if not out["factory-password-hash"]: sys.stdout.write("\n\n\033[31mCRITICAL BOOTSTRAP ERROR\n" + "NO FACTORY PASSWORD FOUND\033[0m\n\n") diff --git a/src/confd/bin/gen-hardware b/src/confd/bin/gen-hardware index 33000a715..8679bc730 100755 --- a/src/confd/bin/gen-hardware +++ b/src/confd/bin/gen-hardware @@ -6,7 +6,11 @@ if jq -e '.["usb-ports"]' /run/system.json > /dev/null; then else usb_ports="" fi -wifi_radios=$(/usr/libexec/infix/iw.py list 2>/dev/null | jq -r '.[]' || echo "") +if jq -e '.["wifi-radios"]' /run/system.json > /dev/null 2>&1; then + wifi_radios=$(jq -r '.["wifi-radios"][].name' /run/system.json) +else + wifi_radios="" +fi gen_port() @@ -27,11 +31,9 @@ gen_radio() { radio="$1" - # Detect supported bands from iw.py info JSON output - phy_info=$(/usr/libexec/infix/iw.py info "$radio" 2>/dev/null || echo '{"bands":[]}') - # Check if 2.4GHz band exists (band name "2.4GHz") + # Read band info from system.json (probed at boot by 00-probe) + phy_info=$(jq -r --arg r "$radio" '.["wifi-radios"][] | select(.name == $r)' /run/system.json 2>/dev/null || echo '{"bands":[]}') has_2ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "2.4GHz")] | length') - # Check if 5GHz band exists (band name "5GHz") has_5ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "5GHz")] | length') # Determine band setting From af12d1535e719d863b2d19e9895bd3e066e93c23 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 14 Feb 2026 10:12:34 +0100 Subject: [PATCH 20/29] configs: drop wifi and gps support from minimal builds Neither the Raspberry Pi 2B or the Microchip SAMA7G54-EK board have WiFi hardware by default so drop WiFi, and GPS, support to allow for smaller builds with only the bare essentials in kernel and system. Minimal builds in general don't need WiFi or GPS either, so let's disable them from all. This may become a central theme going forward, keeping the minimal builds ... minimal. Signed-off-by: Joachim Wiberg --- board/aarch64/raspberrypi-rpi64/Config.in | 3 --- board/arm/raspberrypi-rpi2/Config.in | 3 --- configs/aarch64_defconfig | 3 +++ configs/aarch64_minimal_defconfig | 1 - configs/arm_minimal_defconfig | 1 - configs/x86_64_minimal_defconfig | 1 - 6 files changed, 3 insertions(+), 9 deletions(-) diff --git a/board/aarch64/raspberrypi-rpi64/Config.in b/board/aarch64/raspberrypi-rpi64/Config.in index cf2a63423..e2f3242b2 100644 --- a/board/aarch64/raspberrypi-rpi64/Config.in +++ b/board/aarch64/raspberrypi-rpi64/Config.in @@ -2,9 +2,6 @@ config BR2_PACKAGE_RASPBERRYPI_RPI64 bool "Raspberry Pi 64-bit (RPi3 and later)" depends on BR2_aarch64 select SDCARD_AUX - select BR2_PACKAGE_FEATURE_WIFI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI_WIFI select BR2_PACKAGE_LINUX_FIRMWARE_RTL_8169 help Raspberry Pi 64-bit adds support for the Raspberry Pi family of diff --git a/board/arm/raspberrypi-rpi2/Config.in b/board/arm/raspberrypi-rpi2/Config.in index 83daa3fb1..2a86b447c 100644 --- a/board/arm/raspberrypi-rpi2/Config.in +++ b/board/arm/raspberrypi-rpi2/Config.in @@ -2,9 +2,6 @@ config BR2_PACKAGE_RASPBERRYPI_RPI2 bool "Raspberry Pi 2 Model B (32-bit ARMv7)" depends on BR2_arm select SDCARD_AUX - select BR2_PACKAGE_FEATURE_WIFI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI_WIFI help Support for the 32-bit ARMv7 Raspberry Pi 2B single-board computer (SBC) with BCM2836 quad-core Cortex-A7 processor. diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index cd86f8f64..a25b2db9c 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -149,8 +149,11 @@ INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_GPS=y +BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y +BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI=y +BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI_WIFI=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_CURIOS_HTTPD=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index 50484286d..ee52686d9 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -126,7 +126,6 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" -BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index c836aa4ad..422e32721 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -126,7 +126,6 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" -BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 8b47bd513..c90fbe443 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -125,7 +125,6 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" -BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y From 923174cfef2d8dae3f177eb23504fc690a508829 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 14 Feb 2026 11:01:56 +0100 Subject: [PATCH 21/29] confd: add support for user configurable https certificate - Add x509-public-key-format identity to crypto-types - Add certificate node to web services container - Use certificate from ietf-keystore as web cert Signed-off-by: Joachim Wiberg --- .../rootfs/etc/finit.d/available/mkcert.conf | 1 - .../rootfs/etc/finit.d/enabled/mkcert.conf | 1 - board/common/rootfs/usr/libexec/infix/mkcert | 33 ++-- .../skeleton/etc/finit.d/available/nginx.conf | 2 +- .../share/factory.d/10-infix-services.json | 1 + src/confd/share/factory.d/10-keystore.json | 28 +++ .../share/factory.d/10-netconf-server.json | 14 -- src/confd/share/factory.d/Makefile.am | 3 +- .../share/failure.d/10-infix-services.json | 1 + src/confd/share/failure.d/10-keystore.json | 1 + src/confd/share/failure.d/Makefile.am | 3 +- src/confd/share/test.d/10-keystore.json | 1 + src/confd/share/test.d/Makefile.am | 4 +- src/confd/src/core.c | 10 ++ src/confd/src/core.h | 7 +- src/confd/src/keystore.c | 159 ++++++++++++++++-- src/confd/src/services.c | 45 +++++ src/confd/yang/confd.inc | 4 +- src/confd/yang/confd/infix-crypto-types.yang | 10 ++ ...ang => infix-crypto-types@2026-02-14.yang} | 0 src/confd/yang/confd/infix-services.yang | 10 ++ ...10.yang => infix-services@2026-02-14.yang} | 0 22 files changed, 279 insertions(+), 59 deletions(-) delete mode 100644 board/common/rootfs/etc/finit.d/available/mkcert.conf delete mode 120000 board/common/rootfs/etc/finit.d/enabled/mkcert.conf create mode 100644 src/confd/share/factory.d/10-keystore.json create mode 120000 src/confd/share/failure.d/10-keystore.json create mode 120000 src/confd/share/test.d/10-keystore.json rename src/confd/yang/confd/{infix-crypto-types@2025-11-09.yang => infix-crypto-types@2026-02-14.yang} (100%) rename src/confd/yang/confd/{infix-services@2025-12-10.yang => infix-services@2026-02-14.yang} (100%) diff --git a/board/common/rootfs/etc/finit.d/available/mkcert.conf b/board/common/rootfs/etc/finit.d/available/mkcert.conf deleted file mode 100644 index 125e831e0..000000000 --- a/board/common/rootfs/etc/finit.d/available/mkcert.conf +++ /dev/null @@ -1 +0,0 @@ -task [S] /usr/libexec/infix/mkcert -- Verifying self-signed https certificate diff --git a/board/common/rootfs/etc/finit.d/enabled/mkcert.conf b/board/common/rootfs/etc/finit.d/enabled/mkcert.conf deleted file mode 120000 index 166f9f03f..000000000 --- a/board/common/rootfs/etc/finit.d/enabled/mkcert.conf +++ /dev/null @@ -1 +0,0 @@ -../available/mkcert.conf \ No newline at end of file diff --git a/board/common/rootfs/usr/libexec/infix/mkcert b/board/common/rootfs/usr/libexec/infix/mkcert index 1bebec23b..6441a776c 100755 --- a/board/common/rootfs/usr/libexec/infix/mkcert +++ b/board/common/rootfs/usr/libexec/infix/mkcert @@ -1,7 +1,12 @@ #!/bin/sh +# Generate a self-signed TLS certificate. Called from confd keystore +# on first boot (or factory reset) when the gencert entry has empty +# keys. Output is written to a temporary directory that confd reads +# and then removes after importing into the keystore. -KEY=/cfg/ssl/private/self-signed.key -CRT=/cfg/ssl/certs/self-signed.crt +TMPDIR=/tmp/ssl +KEY=$TMPDIR/self-signed.key +CRT=$TMPDIR/self-signed.crt country=US state=California @@ -20,23 +25,11 @@ if [ -z "$cn" ]; then fi fi -generate() -{ - mkdir -p /cfg/ssl/private /cfg/ssl/certs - chmod 700 /cfg/ssl/private +mkdir -p "$TMPDIR" +chmod 700 "$TMPDIR" - gencert --country "$country" --state "$state" --city "$city" --organisation "$org" \ - --organisation-unit "$unit" --common-name "$cn" \ - --out-certificate $CRT --out-key $KEY -} +gencert --country "$country" --state "$state" --city "$city" --organisation "$org" \ + --organisation-unit "$unit" --common-name "$cn" \ + --out-certificate "$CRT" --out-key "$KEY" -CN=$(openssl x509 -noout -subject -in "${CRT}" 2>/dev/null |sed 's/.*CN=//') -if [ -z "$CN" ] || [ "$CN" != "$cn" ]; then - generate "$cn" -fi - -cp "${KEY}" "/etc/ssl/private/" -cp "${CRT}" "/etc/ssl/certs/" -initctl cond set mkcert - -exit 0 +exit $? diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf index 0286c2154..2905bb11e 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf @@ -1,2 +1,2 @@ -service env:-/etc/default/nginx \ +service env:-/etc/default/nginx \ [2345] nginx -g 'daemon off;' $NGINX_ARGS -- Web server diff --git a/src/confd/share/factory.d/10-infix-services.json b/src/confd/share/factory.d/10-infix-services.json index f7b36180f..5b7edadbd 100644 --- a/src/confd/share/factory.d/10-infix-services.json +++ b/src/confd/share/factory.d/10-infix-services.json @@ -22,6 +22,7 @@ "hostkey": [ "genkey" ] }, "infix-services:web": { + "certificate": "gencert", "enabled": true, "console": { "enabled": true diff --git a/src/confd/share/factory.d/10-keystore.json b/src/confd/share/factory.d/10-keystore.json new file mode 100644 index 000000000..e4bcc9beb --- /dev/null +++ b/src/confd/share/factory.d/10-keystore.json @@ -0,0 +1,28 @@ +{ + "ietf-keystore:keystore": { + "asymmetric-keys": { + "asymmetric-key": [ + { + "name": "genkey", + "public-key-format": "infix-crypto-types:ssh-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": {} + }, + { + "name": "gencert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": { + "certificate": [ + { "name": "self-signed", "cert-data": "" } + ] + } + } + ] + } + } +} diff --git a/src/confd/share/factory.d/10-netconf-server.json b/src/confd/share/factory.d/10-netconf-server.json index 0dcfdd2cb..1effd768c 100644 --- a/src/confd/share/factory.d/10-netconf-server.json +++ b/src/confd/share/factory.d/10-netconf-server.json @@ -1,18 +1,4 @@ { - "ietf-keystore:keystore": { - "asymmetric-keys": { - "asymmetric-key": [ - { - "name": "genkey", - "public-key-format": "infix-crypto-types:ssh-public-key-format", - "public-key": "", - "private-key-format": "infix-crypto-types:rsa-private-key-format", - "cleartext-private-key": "", - "certificates": {} - } - ] - } - }, "ietf-netconf-server:netconf-server": { "listen": { "endpoints": { diff --git a/src/confd/share/factory.d/Makefile.am b/src/confd/share/factory.d/Makefile.am index 72b403041..4256ab4c3 100644 --- a/src/confd/share/factory.d/Makefile.am +++ b/src/confd/share/factory.d/Makefile.am @@ -1,3 +1,4 @@ factorydir = $(pkgdatadir)/factory.d -dist_factory_DATA = 10-nacm.json 10-netconf-server.json \ +dist_factory_DATA = 10-keystore.json 10-nacm.json \ + 10-netconf-server.json \ 10-infix-services.json 10-system.json diff --git a/src/confd/share/failure.d/10-infix-services.json b/src/confd/share/failure.d/10-infix-services.json index a5ef51029..c34a89de0 100644 --- a/src/confd/share/failure.d/10-infix-services.json +++ b/src/confd/share/failure.d/10-infix-services.json @@ -6,6 +6,7 @@ "enabled": true }, "infix-services:web": { + "certificate": "gencert", "enabled": true, "restconf": { "enabled": true diff --git a/src/confd/share/failure.d/10-keystore.json b/src/confd/share/failure.d/10-keystore.json new file mode 120000 index 000000000..ae64a9eec --- /dev/null +++ b/src/confd/share/failure.d/10-keystore.json @@ -0,0 +1 @@ +../factory.d/10-keystore.json \ No newline at end of file diff --git a/src/confd/share/failure.d/Makefile.am b/src/confd/share/failure.d/Makefile.am index ae981ac3d..3b1f5f7da 100644 --- a/src/confd/share/failure.d/Makefile.am +++ b/src/confd/share/failure.d/Makefile.am @@ -1,4 +1,5 @@ failuredir = $(pkgdatadir)/failure.d -dist_failure_DATA = 10-nacm.json 10-netconf-server.json \ +dist_failure_DATA = 10-keystore.json 10-nacm.json \ + 10-netconf-server.json \ 10-infix-services.json 10-system.json diff --git a/src/confd/share/test.d/10-keystore.json b/src/confd/share/test.d/10-keystore.json new file mode 120000 index 000000000..ae64a9eec --- /dev/null +++ b/src/confd/share/test.d/10-keystore.json @@ -0,0 +1 @@ +../factory.d/10-keystore.json \ No newline at end of file diff --git a/src/confd/share/test.d/Makefile.am b/src/confd/share/test.d/Makefile.am index 89447af7d..66ac915fc 100644 --- a/src/confd/share/test.d/Makefile.am +++ b/src/confd/share/test.d/Makefile.am @@ -1,4 +1,4 @@ testdir = $(pkgdatadir)/test.d -dist_test_DATA = 10-nacm.json 10-netconf-server.json \ - 10-infix-services.json 10-system.json +dist_test_DATA = 10-keystore.json 10-nacm.json 10-netconf-server.json \ + 10-infix-services.json 10-system.json diff --git a/src/confd/src/core.c b/src/confd/src/core.c index b2bdc8432..ef958985d 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -99,6 +99,7 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd dkeys = lydx_get_descendant(*diff, "keystore", "asymmetric-keys", "asymmetric-key", NULL); LYX_LIST_FOR_EACH(dkeys, dkey, "asymmetric-key") { struct ly_set *hostkeys; + struct lyd_node *webcert; uint32_t i; key_name = lydx_get_cattr(dkey, "name"); @@ -116,6 +117,15 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd } ly_set_free(hostkeys, NULL); } + + webcert = lydx_get_xpathf(config, "/infix-services:web/certificate[.='%s']", key_name); + if (webcert) { + result = add_dependencies(diff, "/infix-services:web/certificate", key_name); + if (result == CONFD_DEP_ERROR) { + ERROR("Failed to add web certificate to diff for key %s", key_name); + return result; + } + } } hostname = lydx_get_xpathf(*diff, "/ietf-system:system/hostname"); diff --git a/src/confd/src/core.h b/src/confd/src/core.h index e65f27053..928584fec 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -31,6 +31,11 @@ #include "dagger.h" +#define SSH_HOSTKEYS "/etc/ssh/hostkeys" +#define SSH_HOSTKEYS_NEXT SSH_HOSTKEYS"+" +#define SSL_CERT_DIR "/etc/ssl/certs" +#define SSL_KEY_DIR "/etc/ssl/private" + #define CB_PRIO_PRIMARY 65535 #define CB_PRIO_PASSIVE 65000 @@ -238,8 +243,6 @@ int hardware_candidate_init(struct confd *confd); int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* keystore.c */ -#define SSH_HOSTKEYS "/etc/ssh/hostkeys" -#define SSH_HOSTKEYS_NEXT SSH_HOSTKEYS"+" int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* firewall.c */ diff --git a/src/confd/src/keystore.c b/src/confd/src/keystore.c index 069467485..6c24b80d9 100644 --- a/src/confd/src/keystore.c +++ b/src/confd/src/keystore.c @@ -14,6 +14,9 @@ #define XPATH_KEYSTORE_SYM "/ietf-keystore:keystore/symmetric-keys" #define SSH_PRIVATE_KEY "/tmp/ssh.key" #define SSH_PUBLIC_KEY "/tmp/ssh.pub" +#define TLS_TMPDIR "/tmp/ssl" +#define TLS_PRIVATE_KEY TLS_TMPDIR "/self-signed.key" +#define TLS_CERTIFICATE TLS_TMPDIR "/self-signed.crt" /* return file size */ static size_t filesz(const char *fn) @@ -70,6 +73,7 @@ static int gen_hostkey(const char *name, struct lyd_node *change) rc = SR_ERR_INTERNAL; } + AUDIT("Installing SSH host key \"%s\".", name); if (systemf("/usr/libexec/infix/mksshkey %s %s %s %s", name, SSH_HOSTKEYS_NEXT, public_key, private_key)) rc = SR_ERR_INTERNAL; @@ -77,6 +81,63 @@ static int gen_hostkey(const char *name, struct lyd_node *change) return rc; } +static int gen_webcert(const char *name, struct lyd_node *change) +{ + const char *private_key, *cert_data, *certname; + struct lyd_node *certs, *cert; + FILE *fp; + + erase("/run/finit/cond/usr/mkcert"); + + private_key = lydx_get_cattr(change, "cleartext-private-key"); + if (!private_key || !*private_key) { + ERROR("Cannot find private key for \"%s\"", name); + return SR_ERR_OK; + } + + certs = lydx_get_descendant(lyd_child(change), "certificates", "certificate", NULL); + if (!certs) { + ERROR("Cannot find any certificates for \"%s\"", name); + return SR_ERR_OK; + } + + cert = certs; /* Use first certificate */ + + certname = lydx_get_cattr(cert, "name"); + if (!certname || !*certname) { + ERROR("Cannot find certificate name for \"%s\"", name); + return SR_ERR_OK; + } + + cert_data = lydx_get_cattr(cert, "cert-data"); + if (!cert_data || !*cert_data) { + ERROR("Cannot find certificate data \"%s\"", name); + return SR_ERR_OK; + } + + AUDIT("Installing HTTPS %s certificate \"%s\"", name, certname); + fp = fopenf("w", "%s/%s.key", SSL_KEY_DIR, certname); + if (!fp) { + ERRNO("Failed creating key file for \"%s\"", certname); + return SR_ERR_INTERNAL; + } + fprintf(fp, "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----\n", private_key); + fclose(fp); + systemf("chmod 600 %s/%s.key", SSL_KEY_DIR, certname); + + fp = fopenf("w", "%s/%s.crt", SSL_CERT_DIR, certname); + if (!fp) { + ERRNO("Failed creating crt file for \"%s\"", certname); + return SR_ERR_INTERNAL; + } + fprintf(fp, "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", cert_data); + fclose(fp); + + symlink("/run/finit/cond/reconf", "/run/finit/cond/usr/mkcert"); + + return SR_ERR_OK; +} + static int keystore_update(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff) { const char *xpath = "/ietf-keystore:keystore/asymmetric-keys/asymmetric-key"; @@ -156,6 +217,84 @@ static int keystore_update(sr_session_ctx_t *session, struct lyd_node *config, s free(private_key_format); } + if (list) + sr_free_values(list, count); + + /* Second pass: generate X.509 certificates for TLS */ + list = NULL; + count = 0; + rc = sr_get_items(session, xpath, 0, 0, &list, &count); + if (rc != SR_ERR_OK) + return 0; + + for (size_t i = 0; i < count; i++) { + char *name = srx_get_str(session, "%s/name", list[i].xpath); + char *public_key_format = NULL, *private_key_format = NULL; + char *pub_key = NULL, *priv_key = NULL, *cert = NULL; + sr_val_t *entry = &list[i]; + + if (srx_isset(session, "%s/cleartext-private-key", entry->xpath) || + srx_isset(session, "%s/public-key", entry->xpath)) + goto next_x509; + + public_key_format = srx_get_str(session, "%s/public-key-format", entry->xpath); + if (!public_key_format) + goto next_x509; + + private_key_format = srx_get_str(session, "%s/private-key-format", entry->xpath); + if (!private_key_format) + goto next_x509; + + if (strcmp(private_key_format, "infix-crypto-types:rsa-private-key-format") || + strcmp(public_key_format, "infix-crypto-types:x509-public-key-format")) + goto next_x509; + + NOTE("X.509 certificate (%s) does not exist, generating...", name); + if (systemf("/usr/libexec/infix/mkcert")) { + ERROR("Failed generating X.509 certificate for %s", name); + goto next_x509; + } + + priv_key = filerd(TLS_PRIVATE_KEY, filesz(TLS_PRIVATE_KEY)); + if (!priv_key) + goto next_x509; + + pub_key = filerd(TLS_CERTIFICATE, filesz(TLS_CERTIFICATE)); + if (!pub_key) + goto next_x509; + + /* Use cert data also for public-key (X.509 SubjectPublicKeyInfo) */ + rc = srx_set_str(session, priv_key, 0, "%s/cleartext-private-key", entry->xpath); + if (rc) { + ERROR("Failed setting private key for %s... rc: %d", name, rc); + goto next_x509; + } + + rc = srx_set_str(session, pub_key, 0, "%s/public-key", entry->xpath); + if (rc != SR_ERR_OK) { + ERROR("Failed setting public key for %s... rc: %d", name, rc); + goto next_x509; + } + + cert = filerd(TLS_CERTIFICATE, filesz(TLS_CERTIFICATE)); + if (cert) { + rc = srx_set_str(session, cert, 0, + "%s/certificates/certificate[name='self-signed']/cert-data", + entry->xpath); + if (rc) + ERROR("Failed setting cert-data for %s... rc: %d", name, rc); + } + + next_x509: + rmrf(TLS_TMPDIR); + free(public_key_format); + free(private_key_format); + free(priv_key); + free(pub_key); + free(cert); + free(name); + } + if (list) sr_free_values(list, count); @@ -202,21 +341,13 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l changes = lydx_get_descendant(config, "keystore", "asymmetric-keys", "asymmetric-key", NULL); LYX_LIST_FOR_EACH(changes, change, "asymmetric-key") { const char *name = lydx_get_cattr(change, "name"); - const char *type; - - type = lydx_get_cattr(change, "private-key-format"); - if (strcmp(type, "infix-crypto-types:rsa-private-key-format")) { - INFO("Private key %s is not of SSH type (%s)", name, type); - continue; - } - - type = lydx_get_cattr(change, "public-key-format"); - if (strcmp(type, "infix-crypto-types:ssh-public-key-format")) { - INFO("Public key %s is not of SSH type (%s)", name, type); - continue; - } + const char *pubfmt; - gen_hostkey(name, change); + pubfmt = lydx_get_cattr(change, "public-key-format"); + if (!strcmp(pubfmt, "infix-crypto-types:ssh-public-key-format")) + gen_hostkey(name, change); + else if (!strcmp(pubfmt, "infix-crypto-types:x509-public-key-format")) + gen_webcert(name, change); } return rc; diff --git a/src/confd/src/services.c b/src/confd/src/services.c index 31682334a..173238e6c 100644 --- a/src/confd/src/services.c +++ b/src/confd/src/services.c @@ -19,6 +19,7 @@ #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, +#define NGINX_SSL_CONF "/etc/nginx/ssl.conf" #define AVAHI_SVC_PATH "/etc/avahi/services" #define LLDP_CONFIG "/etc/lldpd.d/confd.conf" @@ -547,6 +548,48 @@ static int ssh_change(sr_session_ctx_t *session, struct lyd_node *config, struct } +static void web_ssl_conf(struct lyd_node *srv, struct lyd_node *config) +{ + const char *keyref, *certname = "self-signed"; + struct lyd_node *key, *certs; + FILE *fp; + + keyref = lydx_get_cattr(srv, "certificate"); + if (!keyref) + keyref = "gencert"; + + key = lydx_get_xpathf(config, "/ietf-keystore:keystore/asymmetric-keys" + "/asymmetric-key[name='%s']", keyref); + if (key) { + certs = lydx_get_descendant(lyd_child(key), "certificates", "certificate", NULL); + if (certs) { + const char *name = lydx_get_cattr(certs, "name"); + + if (name && *name) + certname = name; + } + } + + fp = fopen(NGINX_SSL_CONF, "w"); + if (!fp) { + ERRNO("failed creating %s", NGINX_SSL_CONF); + return; + } + + fprintf(fp, + "ssl_certificate %s/%s.crt;\n" + "ssl_certificate_key %s/%s.key;\n" + "\n" + "ssl_protocols TLSv1.3 TLSv1.2;\n" + "ssl_ciphers HIGH:!aNULL:!MD5;\n" + "ssl_prefer_server_ciphers on;\n" + "\n" + "ssl_session_cache shared:SSL:1m;\n" + "ssl_session_timeout 5m;\n", + SSL_CERT_DIR, certname, SSL_KEY_DIR, certname); + fclose(fp); +} + static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { struct lyd_node *srv = NULL; @@ -560,6 +603,8 @@ static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct if (!cfg) return SR_ERR_OK; + web_ssl_conf(srv, config); + ena = lydx_is_enabled(srv, "enabled"); if (ena) { svc_enadis(srx_enabled(session, "%s/enabled", WEB_CONSOLE_XPATH), ttyd, "ttyd"); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 8efe9e3e1..a2e5426ac 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -43,13 +43,13 @@ MODULES=( "infix-firewall-icmp-types@2025-04-26.yang" "infix-meta@2025-12-10.yang" "infix-system@2025-12-02.yang" - "infix-services@2025-12-10.yang" + "infix-services@2026-02-14.yang" "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" "infix-interfaces@2025-11-06.yang -e vlan-filtering" "ietf-crypto-types -e cleartext-symmetric-keys" - "infix-crypto-types@2025-11-09.yang" + "infix-crypto-types@2026-02-14.yang" "ietf-keystore -e symmetric-keys" "infix-ntp@2026-02-08.yang" "infix-keystore@2025-12-17.yang" diff --git a/src/confd/yang/confd/infix-crypto-types.yang b/src/confd/yang/confd/infix-crypto-types.yang index ab7ab37ab..ada63afa1 100644 --- a/src/confd/yang/confd/infix-crypto-types.yang +++ b/src/confd/yang/confd/infix-crypto-types.yang @@ -6,6 +6,9 @@ module infix-crypto-types { prefix ct; } + revision 2026-02-14 { + description "Add X.509 public key format for TLS certificates."; + } revision 2025-11-09 { description "Add Wireguard public/private key and sha"; } @@ -44,6 +47,13 @@ module infix-crypto-types { Used for SSH host keys."; } + identity x509-public-key-format { + base public-key-format; + base ct:subject-public-key-info-format; + description + "X.509 SubjectPublicKeyInfo format. Used for TLS certificates."; + } + identity symmetric-key-format { base ct:symmetric-key-format; description diff --git a/src/confd/yang/confd/infix-crypto-types@2025-11-09.yang b/src/confd/yang/confd/infix-crypto-types@2026-02-14.yang similarity index 100% rename from src/confd/yang/confd/infix-crypto-types@2025-11-09.yang rename to src/confd/yang/confd/infix-crypto-types@2026-02-14.yang diff --git a/src/confd/yang/confd/infix-services.yang b/src/confd/yang/confd/infix-services.yang index a08135acd..6eec46b72 100644 --- a/src/confd/yang/confd/infix-services.yang +++ b/src/confd/yang/confd/infix-services.yang @@ -25,6 +25,10 @@ module infix-services { contact "kernelkit@googlegroups.com"; description "Infix services, generic."; + revision 2026-02-14 { + description "Add certificate leaf to web container for TLS keystore reference."; + reference "internal"; + } revision 2025-12-10 { description "Adapt to changes in final version of ietf-keystore"; reference "internal"; @@ -188,6 +192,12 @@ module infix-services { container web { description "Web services"; + leaf certificate { + description "Reference to asymmetric key in central keystore with an + associated certificate. By default 'gencert' is used."; + type ks:central-asymmetric-key-ref; + } + leaf enabled { description "Enable or disable on all web services. diff --git a/src/confd/yang/confd/infix-services@2025-12-10.yang b/src/confd/yang/confd/infix-services@2026-02-14.yang similarity index 100% rename from src/confd/yang/confd/infix-services@2025-12-10.yang rename to src/confd/yang/confd/infix-services@2026-02-14.yang From d9cdbdf5148579ef1333e50bedec44383757fdb3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 14 Feb 2026 11:54:13 +0100 Subject: [PATCH 22/29] confd: migrate https cert from /cfg/ssl to ietf-keystore Signed-off-by: Joachim Wiberg --- package/confd/confd.mk | 2 +- src/confd/configure.ac | 3 +- .../migrate/1.8/10-keystore-add-gencert.sh | 89 +++++++++++++++++++ src/confd/share/migrate/1.8/Makefile.am | 2 + src/confd/share/migrate/Makefile.am | 2 +- 5 files changed, 95 insertions(+), 3 deletions(-) create mode 100755 src/confd/share/migrate/1.8/10-keystore-add-gencert.sh create mode 100644 src/confd/share/migrate/1.8/Makefile.am diff --git a/package/confd/confd.mk b/package/confd/confd.mk index 7cfb2347f..35b4b3e33 100644 --- a/package/confd/confd.mk +++ b/package/confd/confd.mk @@ -4,7 +4,7 @@ # ################################################################################ -CONFD_VERSION = 1.7 +CONFD_VERSION = 1.8 CONFD_SITE_METHOD = local CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd CONFD_LICENSE = BSD-3-Clause diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 72299e869..f8d14cc4d 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.61) # confd version is same as system YANG model version, step on breaking changes -AC_INIT([confd], [1.7], [https://github.com/kernelkit/infix/issues]) +AC_INIT([confd], [1.8], [https://github.com/kernelkit/infix/issues]) AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) AM_SILENT_RULES(yes) @@ -21,6 +21,7 @@ AC_CONFIG_FILES([ share/migrate/1.5/Makefile share/migrate/1.6/Makefile share/migrate/1.7/Makefile + share/migrate/1.8/Makefile yang/Makefile yang/confd/Makefile yang/test-mode/Makefile diff --git a/src/confd/share/migrate/1.8/10-keystore-add-gencert.sh b/src/confd/share/migrate/1.8/10-keystore-add-gencert.sh new file mode 100755 index 000000000..7afea14fe --- /dev/null +++ b/src/confd/share/migrate/1.8/10-keystore-add-gencert.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# Migrate self-signed HTTPS certificate from /cfg/ssl/ files into the +# ietf-keystore in startup-config. Previously mkcert generated cert +# and key files on disk; now they are managed as a keystore entry +# called "gencert" alongside the SSH "genkey" entry. +# +# Also adds the "certificate": "gencert" leaf to the web container +# so nginx knows which keystore entry to use for TLS. +# +# After migration, /cfg/ssl/ is removed since cert/key are now stored +# in the keystore and written to /etc/ssl/ by confd at runtime. + +file=$1 +temp=${file}.tmp + +LEGACY_DIR=/cfg/ssl +LEGACY_KEY=$LEGACY_DIR/private/self-signed.key +LEGACY_CRT=$LEGACY_DIR/certs/self-signed.crt + +MKCERT_DIR=/tmp/ssl +MKCERT_KEY=$MKCERT_DIR/self-signed.key +MKCERT_CRT=$MKCERT_DIR/self-signed.crt + +# Read PEM files, strip markers and newlines to get raw base64 +read_pem() { + grep -v -- '-----' "$1" | tr -d '\n' +} + +if [ -f "$LEGACY_KEY" ] && [ -f "$LEGACY_CRT" ]; then + priv_key=$(read_pem "$LEGACY_KEY") + cert_data=$(read_pem "$LEGACY_CRT") +fi + +# Fallback: generate a fresh certificate if legacy files were missing +# or unreadable, same as keystore.c does on first boot. +if [ -z "$priv_key" ] || [ -z "$cert_data" ]; then + /usr/libexec/infix/mkcert + if [ -f "$MKCERT_KEY" ] && [ -f "$MKCERT_CRT" ]; then + priv_key=$(read_pem "$MKCERT_KEY") + cert_data=$(read_pem "$MKCERT_CRT") + rm -rf "$MKCERT_DIR" + fi +fi + +# If we still have no cert data, leave keys empty and let confd +# generate on boot via keystore_update(). +if [ -z "$priv_key" ]; then + priv_key="" + cert_data="" +fi + +jq --arg priv "$priv_key" --arg cert "$cert_data" ' +# Add gencert entry to keystore if not already present +if .["ietf-keystore:keystore"]?."asymmetric-keys"?."asymmetric-key" then + if (.["ietf-keystore:keystore"]."asymmetric-keys"."asymmetric-key" | map(select(.name == "gencert")) | length) == 0 then + .["ietf-keystore:keystore"]."asymmetric-keys"."asymmetric-key" += [{ + "name": "gencert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": $cert, + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": $priv, + "certificates": { + "certificate": [{ + "name": "self-signed", + "cert-data": $cert + }] + } + }] + else + . + end +else + . +end | + +# Add certificate reference to web container +if .["infix-services:web"] then + if .["infix-services:web"].certificate then + . + else + .["infix-services:web"].certificate = "gencert" + end +else + . +end +' "$file" > "$temp" && mv "$temp" "$file" + +# Cert/key now live in the keystore, wipe the legacy on-disk copy +rm -rf "$LEGACY_DIR" diff --git a/src/confd/share/migrate/1.8/Makefile.am b/src/confd/share/migrate/1.8/Makefile.am new file mode 100644 index 000000000..5586bcebc --- /dev/null +++ b/src/confd/share/migrate/1.8/Makefile.am @@ -0,0 +1,2 @@ +migratedir = $(pkgdatadir)/migrate/1.8 +dist_migrate_DATA = 10-keystore-add-gencert.sh diff --git a/src/confd/share/migrate/Makefile.am b/src/confd/share/migrate/Makefile.am index 0a7c2c82f..0a5c71ddd 100644 --- a/src/confd/share/migrate/Makefile.am +++ b/src/confd/share/migrate/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 +SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 migratedir = $(pkgdatadir)/migrate From 0eae3753130c55bff00b80ddb836c545b7bc8e6a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 14 Feb 2026 11:54:44 +0100 Subject: [PATCH 23/29] doc: document new 'gencert' default https cert Signed-off-by: Joachim Wiberg --- doc/branding.md | 35 ++++++++++++++++++++++++++-- doc/keystore.md | 58 +++++++++++++++++++++++++++++++++++++++++++---- doc/management.md | 18 +++++++++++++++ 3 files changed, 105 insertions(+), 6 deletions(-) diff --git a/doc/branding.md b/doc/branding.md index f98941126..7056c95fa 100644 --- a/doc/branding.md +++ b/doc/branding.md @@ -59,6 +59,8 @@ them: XPath: `/ietf-system:system/authentication/user/password` - **Default SSH and NETCONF hostkey:** `genkey` (regenerated at factory reset) XPath: `/ietf-keystore:keystore/asymmetric-keys/asymmetric-key[name='genkey']` +- **Default HTTPS certificate:** `gencert` (self-signed, regenerated at factory reset) + XPath: `/ietf-keystore:keystore/asymmetric-keys/asymmetric-key[name='gencert']` - **Hostname format specifiers:** XPath: `/ietf-system:system/hostname` - `%i`: OS ID, from `/etc/os-release`, from Menuconfig branding @@ -219,8 +221,12 @@ Notice how both the public and private keys are left empty here, this cause them to be always automatically regenerated after each factory reset. Keeping the `factory-config` snippet like this means we can use the same file on multiple devices, without risking them sharing the same host -keys. Sometimes you may want the same host keys, but that is the easy -use-case and not documented here. +keys or TLS certificates. Sometimes you may want the same keys, but +that is the easy use-case and not documented here. + +The `genkey` entry is the default SSH host key, and `gencert` is the +default self-signed HTTPS certificate used by the web server (nginx). +Both are regenerated on factory reset when their keys are empty. ```json "ietf-keystore:keystore": { @@ -233,6 +239,18 @@ use-case and not documented here. "private-key-format": "ietf-crypto-types:rsa-private-key-format", "cleartext-private-key": "", "certificates": {} + }, + { + "name": "gencert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": { + "certificate": [ + { "name": "self-signed", "cert-data": "" } + ] + } } ] } @@ -292,9 +310,22 @@ use-case and not documented here. "port": 22 } ] + }, + "infix-services:web": { + "certificate": "gencert", + "enabled": true, + "console": { "enabled": true }, + "netbrowse": { "enabled": true }, + "restconf": { "enabled": true } } ``` +The `certificate` leaf references an asymmetric key in the keystore +that has an associated certificate. The default `gencert` entry uses +a self-signed certificate. To use a custom (e.g., CA-signed) +certificate, create a new keystore entry with +`x509-public-key-format` and point the web `certificate` leaf to it. + ## Integration When integrating your software stack with Infix there may be protocols diff --git a/doc/keystore.md b/doc/keystore.md index 55da5961c..79db20def 100644 --- a/doc/keystore.md +++ b/doc/keystore.md @@ -11,6 +11,7 @@ The keystore supports two types of cryptographic keys: 1. **Asymmetric Keys** — public/private key pairs used for: - SSH host authentication (RSA keys) + - HTTPS/TLS certificates (X.509 keys) - WireGuard VPN tunnels (X25519 keys) 2. **Symmetric Keys** — shared secrets used for: @@ -22,10 +23,11 @@ managed via CLI, NETCONF, or RESTCONF. ### Supported Formats -| **Asymmetric Key Format** | **Use Case** | **Key Type** | -|----------------------------------------------------------|---------------|--------------| -| `rsa-private-key-format` / `ssh-public-key-format` | SSH host keys | RSA | -| `x25519-private-key-format` / `x25519-public-key-format` | WireGuard VPN | Curve25519 | +| **Asymmetric Key Format** | **Use Case** | **Key Type** | +|----------------------------------------------------------|--------------------|--------------| +| `rsa-private-key-format` / `ssh-public-key-format` | SSH host keys | RSA | +| `rsa-private-key-format` / `x509-public-key-format` | TLS certificates | RSA + X.509 | +| `x25519-private-key-format` / `x25519-public-key-format` | WireGuard VPN | Curve25519 | | **Symmetric Key Format** | **Use Case** | |-----------------------------|-----------------------------------| @@ -46,6 +48,53 @@ keystore with the name `genkey`. See [SSH Management](management.md) for details on generating and importing custom SSH host keys. +### TLS Certificates + +TLS certificates are used by the web server (nginx) for HTTPS connections. +The default certificate is a self-signed certificate automatically generated +on first boot and stored in the keystore with the name `gencert`. Like SSH +host keys, the certificate is regenerated on factory reset when its keys are +empty. + +The web server's `certificate` leaf references which keystore entry to use: + +```json +"infix-services:web": { + "certificate": "gencert", + "enabled": true +} +``` + +To use a custom (e.g., CA-signed) certificate, create a new asymmetric key +entry with `x509-public-key-format`, populate it with your certificate and +private key, then point the web `certificate` leaf to it: + +```json +"ietf-keystore:keystore": { + "asymmetric-keys": { + "asymmetric-key": [ + { + "name": "my-cert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": { + "certificate": [ + { "name": "ca-signed", "cert-data": "" } + ] + } + } + ] + } +} +``` + +> [!NOTE] +> The `public-key` and `cert-data` fields contain base64-encoded PEM data +> with the `-----BEGIN/END-----` markers stripped. The system reconstructs +> the PEM files when writing them to disk for nginx. + ### WireGuard Keys WireGuard uses X25519 elliptic curve cryptography for key exchange. Each @@ -116,6 +165,7 @@ wg-psk octet-string zYr83O4Ykj9i1gN+/aaosJxQx... Asymmetric Keys NAME TYPE PUBLIC KEY genkey rsa MIIBCgKCAQEAnj0YinjhYDgYbEGuh7... +gencert x509 MIIDXTCCAkWgAwIBAgIJAJC1HiIAZA... wg-tunnel x25519 bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1... diff --git a/doc/management.md b/doc/management.md index 1c3cb094c..ed478911e 100644 --- a/doc/management.md +++ b/doc/management.md @@ -136,6 +136,7 @@ the unit's neighbors, collected via mDNS (see
admin@example:/> configure
 admin@example:/config/> edit web
 admin@example:/config/web/> help
+  certificate                       Reference to asymmetric key in central keystore.
   enabled                           Enable or disable on all web services.
   console                           Web console interface.
   netbrowse                         mDNS Network Browser.
@@ -191,6 +192,23 @@ admin@example:/config/web/restconf/> no enabled
 admin@example:/config/web/restconf/>
 
+### HTTPS Certificate + +The Web server uses a TLS certificate from the central +[keystore](keystore.md). By default it uses `gencert`, a self-signed +certificate that is automatically generated on first boot. + +To use a different certificate, e.g., one signed by a CA, first add +it to the keystore as an asymmetric key with `x509-public-key-format`, +then point the web `certificate` leaf to it: + +
admin@example:/config/web/> set certificate my-cert
+admin@example:/config/web/>
+
+ +See [Keystore](keystore.md#tls-certificates) for details on managing +TLS certificates. + ## System Upgrade See [Upgrade & Boot Order](upgrade.md) for information on upgrading. From d0eaf115ee44a92d0595fe2c2eddc20d804ef20b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 14 Feb 2026 15:23:32 +0100 Subject: [PATCH 24/29] board/arm: relocate rpi2 kernel config to board support package Signed-off-by: Joachim Wiberg --- board/arm/linux_defconfig | 57 ++----------------- .../arm/raspberrypi-rpi2/raspberrypi-rpi2.mk | 34 ++++++++--- 2 files changed, 31 insertions(+), 60 deletions(-) diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig index 5ace00c2b..3c0639ad7 100644 --- a/board/arm/linux_defconfig +++ b/board/arm/linux_defconfig @@ -30,8 +30,6 @@ CONFIG_SCHED_AUTOGROUP=y CONFIG_BLK_DEV_INITRD=y CONFIG_KALLSYMS_ALL=y CONFIG_PROFILING=y -CONFIG_ARCH_BCM=y -CONFIG_ARCH_BCM2835=y # CONFIG_ARM_ERRATA_643719 is not set CONFIG_SMP=y CONFIG_CPU_FREQ=y @@ -41,7 +39,6 @@ CONFIG_CPU_FREQ_GOV_USERSPACE=y CONFIG_CPU_FREQ_GOV_ONDEMAND=y CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y CONFIG_CPUFREQ_DT=y -CONFIG_ARM_RASPBERRYPI_CPUFREQ=y CONFIG_VFP=y CONFIG_NEON=y CONFIG_KERNEL_MODE_NEON=y @@ -261,7 +258,6 @@ CONFIG_UEVENT_HELPER_PATH="/sbin/hotplug" CONFIG_DEVTMPFS=y CONFIG_DEVTMPFS_MOUNT=y # CONFIG_STANDALONE is not set -CONFIG_RASPBERRYPI_FIRMWARE=y CONFIG_FW_CFG_SYSFS=y CONFIG_FW_CFG_SYSFS_CMDLINE=y CONFIG_OF_OVERLAY=y @@ -303,63 +299,29 @@ CONFIG_VETH=m CONFIG_VIRTIO_NET=y CONFIG_NLMON=y CONFIG_NET_VRF=y -CONFIG_BCMGENET=y -CONFIG_SMSC911X=y -CONFIG_USB_LAN78XX=y CONFIG_USB_USBNET=y -CONFIG_USB_NET_SMSC95XX=y -CONFIG_INPUT_MOUSEDEV=m CONFIG_INPUT_EVDEV=y -CONFIG_INPUT_TOUCHSCREEN=y -CONFIG_TOUCHSCREEN_EDT_FT5X06=m # CONFIG_LEGACY_PTYS is not set -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_EXTENDED=y -CONFIG_SERIAL_8250_SHARE_IRQ=y -CONFIG_SERIAL_8250_BCM2835AUX=y -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y CONFIG_SERIAL_DEV_BUS=y CONFIG_VIRTIO_CONSOLE=y CONFIG_I2C_CHARDEV=y -CONFIG_I2C_BCM2835=m CONFIG_SPI=y -CONFIG_SPI_BCM2835=y -CONFIG_SPI_BCM2835AUX=y -CONFIG_SENSORS_RASPBERRYPI_HWMON=m CONFIG_THERMAL=y -CONFIG_BCM2711_THERMAL=y -CONFIG_BCM2835_THERMAL=m CONFIG_WATCHDOG=y CONFIG_I6300ESB_WDT=y -CONFIG_BCM2835_WDT=y CONFIG_MFD_SYSCON=y CONFIG_REGULATOR=y CONFIG_REGULATOR_FIXED_VOLTAGE=y -CONFIG_REGULATOR_GPIO=y CONFIG_MEDIA_SUPPORT=y -CONFIG_DRM=y -CONFIG_DRM_LOAD_EDID_FIRMWARE=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_PANEL_SIMPLE=m -CONFIG_DRM_TOSHIBA_TC358762=m -CONFIG_DRM_V3D=m -CONFIG_DRM_VC4=m -CONFIG_DRM_VC4_HDMI_CEC=y -CONFIG_FB=y -CONFIG_BACKLIGHT_CLASS_DEVICE=y -CONFIG_SOUND=y -CONFIG_SND=y -CONFIG_SND_SOC=y -CONFIG_SND_BCM2835_SOC_I2S=y -CONFIG_HID_GENERIC=m CONFIG_USB=y CONFIG_USB_ANNOUNCE_NEW_DEVICES=y CONFIG_USB_OTG=y +CONFIG_USB_EHCI_HCD=m +CONFIG_USB_EHCI_ROOT_HUB_TT=y +CONFIG_USB_EHCI_HCD_PLATFORM=m +CONFIG_USB_OHCI_HCD=m +CONFIG_USB_OHCI_HCD_PLATFORM=m CONFIG_USB_STORAGE=y -CONFIG_USB_DWC2=y -CONFIG_NOP_USB_XCEIV=y CONFIG_USB_GADGET=y CONFIG_USB_ETH=m CONFIG_USB_ETH_EEM=y @@ -367,8 +329,6 @@ CONFIG_USB_G_SERIAL=m CONFIG_MMC=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_PLTFM=y -CONFIG_MMC_SDHCI_IPROC=y -CONFIG_MMC_BCM2835=y CONFIG_NEW_LEDS=y CONFIG_LEDS_CLASS=y CONFIG_LEDS_GPIO=y @@ -382,20 +342,13 @@ CONFIG_LEDS_TRIGGER_DEFAULT_ON=y CONFIG_LEDS_TRIGGER_TRANSIENT=y CONFIG_LEDS_TRIGGER_CAMERA=y CONFIG_DMADEVICES=y -CONFIG_DMA_BCM2835=y CONFIG_VIRTIO_PCI=y CONFIG_VIRTIO_BALLOON=y CONFIG_VIRTIO_INPUT=y CONFIG_VIRTIO_MMIO=y -CONFIG_STAGING=y -CONFIG_SND_BCM2835=m -CONFIG_CLK_RASPBERRYPI=y CONFIG_MAILBOX=y -CONFIG_BCM2835_MBOX=y # CONFIG_IOMMU_SUPPORT is not set -CONFIG_RASPBERRYPI_POWER=y CONFIG_PWM=y -CONFIG_PWM_BCM2835=y CONFIG_EXT2_FS=y CONFIG_EXT2_FS_POSIX_ACL=y CONFIG_EXT4_FS=y diff --git a/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk b/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk index 73d4a0b63..a904a1385 100644 --- a/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk +++ b/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk @@ -1,13 +1,19 @@ # Raspberry Pi 2 Model B specific kernel configuration define RASPBERRYPI_RPI2_LINUX_CONFIG_FIXUPS + $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM) + $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM2835) + $(call KCONFIG_ENABLE_OPT,CONFIG_SOUND) $(call KCONFIG_ENABLE_OPT,CONFIG_SND) $(call KCONFIG_ENABLE_OPT,CONFIG_SND_SOC) + $(call KCONFIG_SET_OPT,CONFIG_SND_BCM2835_SOC_I2S,y) + $(call KCONFIG_SET_OPT,CONFIG_SND_BCM2835,m) $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_MOUSE) $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_KEYBOARD) $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_TOUCHSCREEN) $(call KCONFIG_SET_OPT,CONFIG_INPUT_MOUSEDEV,m) $(call KCONFIG_SET_OPT,CONFIG_HID_GENERIC,m) + $(call KCONFIG_SET_OPT,CONFIG_TOUCHSCREEN_EDT_FT5X06,m) $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM) $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM2835) @@ -15,46 +21,58 @@ define RASPBERRYPI_RPI2_LINUX_CONFIG_FIXUPS $(call KCONFIG_ENABLE_OPT,CONFIG_BCM2835_WDT) $(call KCONFIG_ENABLE_OPT,CONFIG_DMA_BCM2835) $(call KCONFIG_ENABLE_OPT,CONFIG_RASPBERRYPI_FIRMWARE) + $(call KCONFIG_ENABLE_OPT,CONFIG_RASPBERRYPI_POWER) + $(call KCONFIG_ENABLE_OPT,CONFIG_ARM_RASPBERRYPI_CPUFREQ) + $(call KCONFIG_ENABLE_OPT,CONFIG_CLK_RASPBERRYPI) $(call KCONFIG_ENABLE_OPT,CONFIG_PINCTRL_BCM2835) $(call KCONFIG_ENABLE_OPT,CONFIG_GPIO_BCM2835) + $(call KCONFIG_ENABLE_OPT,CONFIG_PWM_BCM2835) $(call KCONFIG_SET_OPT,CONFIG_BRCMFMAC,m) $(call KCONFIG_ENABLE_OPT,CONFIG_BRCMFMAC_SDIO) $(call KCONFIG_SET_OPT,CONFIG_I2C_BCM2835,m) + $(call KCONFIG_SET_OPT,CONFIG_SPI_BCM2835,y) + $(call KCONFIG_SET_OPT,CONFIG_SPI_BCM2835AUX,y) + $(call KCONFIG_SET_OPT,CONFIG_SENSORS_RASPBERRYPI_HWMON,m) + $(call KCONFIG_SET_OPT,CONFIG_BCM2711_THERMAL,y) $(call KCONFIG_SET_OPT,CONFIG_BCM2835_THERMAL,m) $(call KCONFIG_ENABLE_OPT,CONFIG_MMC_BCM2835) - $(call KCONFIG_ENABLE_OPT,CONFIG_RASPBERRYPI_POWER) + $(call KCONFIG_SET_OPT,CONFIG_MMC_SDHCI_IPROC,y) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_CONSOLE) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_BCM2835AUX) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_EXTENDED) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_SHARE_IRQ) + $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_AMBA_PL011) + $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_AMBA_PL011_CONSOLE) $(call KCONFIG_ENABLE_OPT,CONFIG_NET_VENDOR_BROADCOM) + $(call KCONFIG_SET_OPT,CONFIG_BCMGENET,y) $(call KCONFIG_SET_OPT,CONFIG_SMSC911X,y) + $(call KCONFIG_ENABLE_OPT,CONFIG_USB_USBNET) + $(call KCONFIG_SET_OPT,CONFIG_USB_LAN78XX,y) + $(call KCONFIG_SET_OPT,CONFIG_USB_NET_SMSC95XX,y) $(call KCONFIG_SET_OPT,CONFIG_REGULATOR_GPIO,y) $(call KCONFIG_ENABLE_OPT,CONFIG_COMMON_CLK_BCM2835) - $(call KCONFIG_ENABLE_OPT,CONFIG_CLK_RASPBERRYPI) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_KMS_HELPER) + $(call KCONFIG_SET_OPT,CONFIG_DRM_SIMPLEDRM,y) $(call KCONFIG_SET_OPT,CONFIG_DRM_V3D,m) $(call KCONFIG_SET_OPT,CONFIG_DRM_VC4,m) - $(call KCONFIG_SET_OPT,CONFIG_STAGING,y) - $(call KCONFIG_SET_OPT,CONFIG_SND_BCM2835,m) - $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_VC4_HDMI_CEC) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_LOAD_EDID_FIRMWARE) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_PANEL_BRIDGE) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_BRIDGE) $(call KCONFIG_SET_OPT,CONFIG_DRM_TOSHIBA_TC358762,m) $(call KCONFIG_SET_OPT,CONFIG_DRM_PANEL_SIMPLE,m) + $(call KCONFIG_SET_OPT,CONFIG_STAGING,y) $(call KCONFIG_ENABLE_OPT,CONFIG_FB) $(call KCONFIG_ENABLE_OPT,CONFIG_FRAMEBUFFER_CONSOLE) $(call KCONFIG_ENABLE_OPT,CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_FBDEV_EMULATION) - - $(call KCONFIG_SET_OPT,CONFIG_TOUCHSCREEN_EDT_FT5X06,m) - $(call KCONFIG_ENABLE_OPT,CONFIG_BACKLIGHT_CLASS_DEVICE) + + $(call KCONFIG_ENABLE_OPT,CONFIG_USB_DWC2) + $(call KCONFIG_ENABLE_OPT,CONFIG_NOP_USB_XCEIV) endef $(eval $(ix-board)) From 622a2f6c06af6a1b9aebf7d8311639a033831da5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 14 Feb 2026 15:24:08 +0100 Subject: [PATCH 25/29] board/aarch64: enable root hub transaction translator for ehci/ohci Signed-off-by: Joachim Wiberg --- board/aarch64/linux_defconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index b37b59593..b7c4fe051 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -502,6 +502,7 @@ CONFIG_USB_OTG=y CONFIG_USB_XHCI_HCD=m CONFIG_USB_XHCI_MVEBU=m CONFIG_USB_EHCI_HCD=m +CONFIG_USB_EHCI_ROOT_HUB_TT=y CONFIG_USB_EHCI_HCD_PLATFORM=m CONFIG_USB_OHCI_HCD=m CONFIG_USB_OHCI_HCD_PLATFORM=m From e29d6a4b88f24452351c4bd0677ef3a04ff68a44 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 14 Feb 2026 16:20:59 +0100 Subject: [PATCH 26/29] confd: replace sysrepo threads with libev event loop Use SR_SUBSCR_NO_THREAD for all subscriptions and integrate sysrepo event pipes into a libev event loop. This eliminates approximately 30 per-subscription threads, reducing overhead on embedded ARM hardware. A temporary poll-based "event pump" thread handles callback dispatch during bootstrap (where sr_replace_config blocks waiting for callbacks), then exits. After bootstrap, the single-threaded libev loop takes over for steady-state event processing. Note, the confd-test-mode plugin still requires use of threads so we do not create deadlocks when calling sr_replace_config(). Signed-off-by: Joachim Wiberg --- package/confd/confd.mk | 2 +- src/confd/configure.ac | 9 ++ src/confd/src/Makefile.am | 2 +- src/confd/src/core.c | 13 ++- src/confd/src/core.h | 8 +- src/confd/src/main.c | 191 ++++++++++++++++++++++++++++---------- 6 files changed, 168 insertions(+), 57 deletions(-) diff --git a/package/confd/confd.mk b/package/confd/confd.mk index 35b4b3e33..6f81deb0f 100644 --- a/package/confd/confd.mk +++ b/package/confd/confd.mk @@ -10,7 +10,7 @@ CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd CONFD_LICENSE = BSD-3-Clause CONFD_LICENSE_FILES = LICENSE CONFD_REDISTRIBUTE = NO -CONFD_DEPENDENCIES = host-sysrepo sysrepo rousette netopeer2 jansson libite sysrepo libsrx libglib2 +CONFD_DEPENDENCIES = host-sysrepo sysrepo rousette netopeer2 jansson libite sysrepo libsrx libglib2 libev CONFD_AUTORECONF = YES CONFD_CONF_OPTS += --disable-silent-rules --with-crypt=$(BR2_PACKAGE_CONFD_DEFAULT_CRYPT) CONFD_SYSREPO_SHM_PREFIX = sr_buildroot$(subst /,_,$(CONFIG_DIR))_confd diff --git a/src/confd/configure.ac b/src/confd/configure.ac index f8d14cc4d..bed811611 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -82,6 +82,15 @@ PKG_CHECK_MODULES([sysrepo], [sysrepo >= 4.2.10]) PKG_CHECK_MODULES([libyang], [libyang >= 4.2.2]) PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0]) +AC_CHECK_HEADER([ev.h], + [saved_LIBS="$LIBS" + AC_CHECK_LIB([ev], [ev_loop_new], + [EV_LIBS="-lev"], + [AC_MSG_ERROR("libev not found")]) + LIBS="$saved_LIBS"], + [AC_MSG_ERROR("ev.h not found")]) +AC_SUBST([EV_LIBS]) + # Control build with automake flags AM_CONDITIONAL(CONTAINERS, [test "x$enable_containers" != "xno"]) diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 67b2538f7..4ab20ba09 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -7,7 +7,7 @@ plugin_LTLIBRARIES = confd-plugin.la sbin_PROGRAMS = confd confd_CFLAGS = $(sysrepo_CFLAGS) $(libyang_CFLAGS) $(jansson_CFLAGS) $(libite_CFLAGS) -confd_LDADD = $(sysrepo_LIBS) $(libyang_LIBS) $(jansson_LIBS) $(libite_LIBS) -ldl +confd_LDADD = $(sysrepo_LIBS) $(libyang_LIBS) $(jansson_LIBS) $(libite_LIBS) $(EV_LIBS) -ldl confd_SOURCES = main.c confd_plugin_la_LDFLAGS = -module -avoid-version -shared diff --git a/src/confd/src/core.c b/src/confd/src/core.c index ef958985d..dd035a3a5 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -461,10 +461,10 @@ static inline int subscribe_model(char *model, struct confd *confd, int flags) { return sr_module_change_subscribe(confd->session, model, "//.", change_cb, confd, CB_PRIO_PRIMARY, SR_SUBSCR_CHANGE_ALL_MODULES | - SR_SUBSCR_DEFAULT | flags, &confd->sub) || + SR_SUBSCR_NO_THREAD | flags, &confd->sub) || sr_module_change_subscribe(confd->startup, model, "//.", startup_save, NULL, CB_PRIO_PASSIVE, SR_SUBSCR_CHANGE_ALL_MODULES | - SR_SUBSCR_PASSIVE, &confd->sub); + SR_SUBSCR_PASSIVE | SR_SUBSCR_NO_THREAD, &confd->sub); } int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) @@ -645,6 +645,15 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) return rc; } +void confd_get_subscriptions(void *priv, sr_subscription_ctx_t **out_sub, + sr_subscription_ctx_t **out_fsub) +{ + struct confd *c = (struct confd *)priv; + + *out_sub = c->sub; + *out_fsub = c->fsub; +} + void sr_plugin_cleanup_cb(sr_session_ctx_t *session, void *priv) { struct confd *ptr = (struct confd *)priv; diff --git a/src/confd/src/core.h b/src/confd/src/core.h index 928584fec..9262fde6a 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -145,7 +145,7 @@ static inline int register_change(sr_session_ctx_t *session, const char *module, int flags, sr_module_change_cb cb, void *arg, sr_subscription_ctx_t **sub) { int rc = sr_module_change_subscribe(session, module, xpath, cb, arg, - CB_PRIO_PRIMARY, flags | SR_SUBSCR_DEFAULT, sub); + CB_PRIO_PRIMARY, flags | SR_SUBSCR_NO_THREAD, sub); if (rc) { ERROR("failed subscribing to changes of %s: %s", xpath, sr_strerror(rc)); return rc; @@ -159,7 +159,7 @@ static inline int register_monitor(sr_session_ctx_t *session, const char *module int flags, sr_module_change_cb cb, void *arg, sr_subscription_ctx_t **sub) { int rc = sr_module_change_subscribe(session, module, xpath, cb, arg, - 0, flags | SR_SUBSCR_PASSIVE, sub); + 0, flags | SR_SUBSCR_PASSIVE | SR_SUBSCR_NO_THREAD, sub); if (rc) { ERROR("failed subscribing to monitor %s: %s", xpath, sr_strerror(rc)); return rc; @@ -172,7 +172,7 @@ static inline int register_oper(sr_session_ctx_t *session, const char *module, c sr_oper_get_items_cb cb, void *arg, int flags, sr_subscription_ctx_t **sub) { int rc = sr_oper_get_subscribe(session, module, xpath, cb, arg, - flags | SR_SUBSCR_DEFAULT, sub); + flags | SR_SUBSCR_NO_THREAD, sub); if (rc) ERROR("failed subscribing to %s oper: %s", xpath, sr_strerror(rc)); return rc; @@ -181,7 +181,7 @@ static inline int register_oper(sr_session_ctx_t *session, const char *module, c static inline int register_rpc(sr_session_ctx_t *session, const char *xpath, sr_rpc_cb cb, void *arg, sr_subscription_ctx_t **sub) { - int rc = sr_rpc_subscribe(session, xpath, cb, arg, 0, SR_SUBSCR_DEFAULT, sub); + int rc = sr_rpc_subscribe(session, xpath, cb, arg, 0, SR_SUBSCR_NO_THREAD, sub); if (rc) ERROR("failed subscribing to %s rpc: %s", xpath, sr_strerror(rc)); return rc; diff --git a/src/confd/src/main.c b/src/confd/src/main.c index 38966a473..7e454e713 100644 --- a/src/confd/src/main.c +++ b/src/confd/src/main.c @@ -13,9 +13,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -51,14 +53,15 @@ struct plugin { char *name; int (*init_cb)(sr_session_ctx_t *session, void **private_data); void (*cleanup_cb)(sr_session_ctx_t *session, void *private_data); + void (*get_subs)(void *priv, sr_subscription_ctx_t **sub, sr_subscription_ctx_t **fsub); void *private_data; + sr_subscription_ctx_t *sub; + sr_subscription_ctx_t *fsub; int initialized; }; -/* Protected flag for terminating */ -static int loop_finish; -static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; +/* Maximum number of sysrepo event pipe file descriptors across all plugins */ +#define MAX_EVENT_FDS 64 static void error_print(int sr_error, const char *format, ...) { @@ -129,45 +132,11 @@ static void help_print(void) "\n"); } -static void signal_handler(int sig) -{ - switch (sig) { - case SIGINT: - case SIGQUIT: - case SIGABRT: - case SIGTERM: - case SIGHUP: - pthread_mutex_lock(&lock); - if (!loop_finish) { - loop_finish = 1; - pthread_cond_signal(&cond); - } else { - error_print(0, "Exiting without a proper cleanup"); - exit(EXIT_FAILURE); - } - pthread_mutex_unlock(&lock); - break; - default: - error_print(0, "Exiting on receiving an unhandled signal"); - exit(EXIT_FAILURE); - } -} - -static void handle_signals(void) +static void ignore_signals(void) { struct sigaction action; - sigset_t block_mask; - - sigfillset(&block_mask); - action.sa_handler = signal_handler; - action.sa_mask = block_mask; - action.sa_flags = 0; - sigaction(SIGINT, &action, NULL); - sigaction(SIGQUIT, &action, NULL); - sigaction(SIGABRT, &action, NULL); - sigaction(SIGTERM, &action, NULL); - sigaction(SIGHUP, &action, NULL); + memset(&action, 0, sizeof(action)); action.sa_handler = SIG_IGN; sigaction(SIGPIPE, &action, NULL); sigaction(SIGTSTP, &action, NULL); @@ -175,6 +144,68 @@ static void handle_signals(void) sigaction(SIGTTOU, &action, NULL); } +/* libev callbacks for steady-state operation */ +static void signal_cb(struct ev_loop *loop, struct ev_signal *w, int revents) +{ + (void)revents; + (void)w; + ev_break(loop, EVBREAK_ALL); +} + +static void sr_event_cb(struct ev_loop *loop, struct ev_io *w, int revents) +{ + (void)loop; + (void)revents; + sr_subscription_process_events(w->data, NULL, NULL); +} + +/* + * Temporary event pump thread for bootstrap. + * + * With SR_SUBSCR_NO_THREAD, sysrepo writes events to a pipe and waits + * for the application to call sr_subscription_process_events(). During + * bootstrap, sr_replace_config() blocks waiting for callbacks — this + * thread ensures those callbacks get dispatched. + */ +struct event_pump { + struct plugin *plugins; + int plugin_count; + int running; +}; + +static void *event_pump_thread(void *arg) +{ + struct event_pump *pump = arg; + struct pollfd fds[MAX_EVENT_FDS]; + sr_subscription_ctx_t *subs[MAX_EVENT_FDS]; + int nfds = 0; + + for (int i = 0; i < pump->plugin_count; i++) { + struct plugin *p = &pump->plugins[i]; + + if (p->sub && sr_get_event_pipe(p->sub, &fds[nfds].fd) == SR_ERR_OK) { + fds[nfds].events = POLLIN; + subs[nfds] = p->sub; + nfds++; + } + if (p->fsub && sr_get_event_pipe(p->fsub, &fds[nfds].fd) == SR_ERR_OK) { + fds[nfds].events = POLLIN; + subs[nfds] = p->fsub; + nfds++; + } + } + + while (pump->running) { + if (poll(fds, nfds, 100) > 0) { + for (int i = 0; i < nfds; i++) + if (fds[i].revents & POLLIN) + sr_subscription_process_events(subs[i], NULL, NULL); + } + } + + return NULL; +} + static void quiet_now(void) { int fd = -1; @@ -196,8 +227,9 @@ static void daemon_init(int debug, sr_log_level_t log_level) nice(-20); + ignore_signals(); + if (debug) { - handle_signals(); if (debug < 0) goto done; sr_log_stderr(log_level); @@ -212,8 +244,6 @@ static void daemon_init(int debug, sr_log_level_t log_level) if (pid > 0) exit(EXIT_SUCCESS); - handle_signals(); - sid = setsid(); if (sid < 0) { error_print(0, "setsid() failed (%s).", strerror(errno)); @@ -357,6 +387,9 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) break; } + /* Optional: allows main to collect subscription contexts */ + *(void **)&plugin->get_subs = dlsym(handle, "confd_get_subscriptions"); + plugin->handle = handle; name_len = path_len_no_ext(ent->d_name); @@ -861,18 +894,47 @@ int main(int argc, char **argv) } conout(0, "\n"); + /* Phase 8: Collect subscription contexts from plugins */ + for (i = 0; i < plugin_count; i++) { + if (plugins[i].initialized && plugins[i].get_subs) + plugins[i].get_subs(plugins[i].private_data, + &plugins[i].sub, &plugins[i].fsub); + } + + /* Phase 9: Start event pump thread for bootstrap. + * With SR_SUBSCR_NO_THREAD, sr_replace_config() blocks waiting + * for callbacks. The pump thread processes those events. */ + struct event_pump pump = { + .plugins = plugins, + .plugin_count = plugin_count, + .running = 1, + }; + pthread_t pump_tid; + + if (pthread_create(&pump_tid, NULL, event_pump_thread, &pump)) { + SRPLG_LOG_ERR("confd", "Failed to create event pump thread: %s", strerror(errno)); + goto cleanup; + } + /* - * Phase 8: Load startup config -- plugins are now subscribed, so + * Phase 10: Load startup config -- plugins are now subscribed, so * sr_replace_config() will trigger their change callbacks. + * The event pump thread processes those callbacks. */ conout(3, "Loading startup-config"); if (bootstrap_config(conn, sess, factory_path, startup_path, failure_path, test_path, timeout_ms)) { + pump.running = 0; + pthread_join(pump_tid, NULL); conout(1, "\n"); goto cleanup; } conout(0, "\n"); + /* Phase 11: Stop event pump — bootstrap is done */ + pump.running = 0; + pthread_join(pump_tid, NULL); + /* No more progress to show, go to quiet daemon mode */ quiet_now(); @@ -883,11 +945,42 @@ int main(int argc, char **argv) if (pidfile && write_pidfile(pidfd) < 0) goto cleanup; - /* Wait for a terminating signal */ - pthread_mutex_lock(&lock); - while (!loop_finish) - pthread_cond_wait(&cond, &lock); - pthread_mutex_unlock(&lock); + /* Phase 12: Steady-state — libev event loop replaces pthread_cond_wait */ + { + struct ev_loop *loop = EV_DEFAULT; + struct ev_signal sigterm_w, sigint_w, sighup_w, sigquit_w; + struct ev_io io_watchers[MAX_EVENT_FDS]; + int nio = 0; + + ev_signal_init(&sigterm_w, signal_cb, SIGTERM); + ev_signal_init(&sigint_w, signal_cb, SIGINT); + ev_signal_init(&sighup_w, signal_cb, SIGHUP); + ev_signal_init(&sigquit_w, signal_cb, SIGQUIT); + ev_signal_start(loop, &sigterm_w); + ev_signal_start(loop, &sigint_w); + ev_signal_start(loop, &sighup_w); + ev_signal_start(loop, &sigquit_w); + + for (i = 0; i < plugin_count; i++) { + int fd; + + if (plugins[i].sub && sr_get_event_pipe(plugins[i].sub, &fd) == SR_ERR_OK) { + ev_io_init(&io_watchers[nio], sr_event_cb, fd, EV_READ); + io_watchers[nio].data = plugins[i].sub; + ev_io_start(loop, &io_watchers[nio]); + nio++; + } + if (plugins[i].fsub && sr_get_event_pipe(plugins[i].fsub, &fd) == SR_ERR_OK) { + ev_io_init(&io_watchers[nio], sr_event_cb, fd, EV_READ); + io_watchers[nio].data = plugins[i].fsub; + ev_io_start(loop, &io_watchers[nio]); + nio++; + } + } + + ev_run(loop, 0); + ev_loop_destroy(loop); + } rc = EXIT_SUCCESS; From b5518e54e0600868f086a5555b427d4b9b6f1a5b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 15 Feb 2026 06:34:21 +0100 Subject: [PATCH 27/29] confd: fix duplicate keystore log messages at boot Only install the keys on CHANGE event, fixes this annoying issue: Nov 5 01:32:10 ix confd[2011]: Installing HTTPS gencert certificate "self-signed" Nov 5 01:32:10 ix confd[2011]: Installing SSH host key "genkey". Nov 5 01:32:11 ix confd[2011]: Installing HTTPS gencert certificate "self-signed Nov 5 01:32:11 ix confd[2011]: Installing SSH host key "genkey". Signed-off-by: Joachim Wiberg --- src/confd/src/keystore.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/confd/src/keystore.c b/src/confd/src/keystore.c index 6c24b80d9..e48171a4d 100644 --- a/src/confd/src/keystore.c +++ b/src/confd/src/keystore.c @@ -313,8 +313,7 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l switch (event) { case SR_EV_UPDATE: - rc = keystore_update(session, config, diff); - break; + return keystore_update(session, config, diff); case SR_EV_CHANGE: if (diff && lydx_find_xpathf(diff, XPATH_KEYSTORE_SYM)) rc = interfaces_validate_keys(session, config); From 2a1e46dbcdb4c2d81c67f292b7b107bf23bbcae6 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 15 Feb 2026 08:22:57 +0100 Subject: [PATCH 28/29] statd: optimize yanger invocation time and reduce command overhead Replace logging + logging.handlers with a lightweight syslog wrapper, and argparse with manual argv parsing. On a sama7g54, this cuts yanger startup from ~770ms to ~470ms by eliminating ~300ms of stdlib imports. Also batch external command invocations: - ietf_routing: two sysctl calls instead of two per interface - ietf_hardware: one ls per hwmon device instead of six - bridge: fetch mctl querier data once instead of once per VLAN Signed-off-by: Joachim Wiberg --- src/statd/python/yanger/__main__.py | 143 +++++++++++------- src/statd/python/yanger/common.py | 44 +++++- src/statd/python/yanger/ietf_hardware.py | 15 +- .../python/yanger/ietf_interfaces/bridge.py | 19 ++- src/statd/python/yanger/ietf_routing.py | 29 +++- 5 files changed, 174 insertions(+), 76 deletions(-) diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index eb6daec9f..f7097c078 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -1,97 +1,130 @@ -import logging -import logging.handlers import json -import sys # (built-in module) import os -import argparse +import sys from . import common from . import host -def main(): - def dirpath(path): - if not os.path.isdir(path): - raise argparse.ArgumentTypeError(f"'{path}' is not a valid directory") - return path +USAGE = """\ +usage: yanger [-p PARAM] [-x PREFIX] [-r DIR | -c DIR] model - parser = argparse.ArgumentParser(description="YANG data creator") - parser.add_argument("model", help="YANG Model") - parser.add_argument("-p", "--param", - help="Model dependent parameter, e.g. interface name") - parser.add_argument("-x", "--cmd-prefix", metavar="PREFIX", - help="Use this prefix for all system commands, e.g. " + - "'ssh user@remotehost sudo'") +YANG data creator - rrparser = parser.add_mutually_exclusive_group() - rrparser.add_argument("-r", "--replay", type=dirpath, metavar="DIR", - help="Generate output based on recorded system commands from DIR, " + - "rather than querying the local system") - rrparser.add_argument("-c", "--capture", metavar="DIR", - help="Capture system command output in DIR, such that the current system " + - "state can be recreated offline (with --replay) for testing purposes") +positional arguments: + model YANG Model - args = parser.parse_args() - if args.replay and args.cmd_prefix: - parser.error("--cmd-prefix cannot be used with --replay") +options: + -p, --param PARAM Model dependent parameter, e.g. interface name + -x, --cmd-prefix PREFIX + Use this prefix for all system commands, e.g. + 'ssh user@remotehost sudo' + -r, --replay DIR Generate output based on recorded system commands + from DIR, rather than querying the local system + -c, --capture DIR Capture system command output in DIR, such that the + current system state can be recreated offline (with + --replay) for testing purposes +""" - # Set up syslog output for critical errors to aid debugging - common.LOG = logging.getLogger('yanger') - if os.path.exists('/dev/log'): - log = logging.handlers.SysLogHandler(address='/dev/log') - else: - # Use /dev/null as a fallback for unit tests - log = logging.FileHandler('/dev/null') +def _parse_args(argv): + model = None + param = None + cmd_prefix = None + replay = None + capture = None + + i = 1 + while i < len(argv): + arg = argv[i] + if arg in ('-h', '--help'): + sys.stdout.write(USAGE) + sys.exit(0) + elif arg in ('-p', '--param'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + param = argv[i] + elif arg in ('-x', '--cmd-prefix'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + cmd_prefix = argv[i] + elif arg in ('-r', '--replay'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + replay = argv[i] + if not os.path.isdir(replay): + sys.exit(f"error: '{replay}' is not a valid directory") + elif arg in ('-c', '--capture'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + capture = argv[i] + elif arg.startswith('-'): + sys.exit(f"error: unknown option: {arg}") + elif model is None: + model = arg + else: + sys.exit(f"error: unexpected argument: {arg}") + i += 1 - fmt = logging.Formatter('%(name)s[%(process)d]: %(message)s') - log.setFormatter(fmt) - common.LOG.setLevel(logging.INFO) - common.LOG.addHandler(log) + if model is None: + sys.exit("error: missing required argument: model") + if replay and cmd_prefix: + sys.exit("error: --cmd-prefix cannot be used with --replay") + if replay and capture: + sys.exit("error: --replay cannot be used with --capture") + + return model, param, cmd_prefix, replay, capture + +def main(): + model, param, cmd_prefix, replay, capture = _parse_args(sys.argv) - if args.cmd_prefix or args.capture: - host.HOST = host.Remotehost(args.cmd_prefix, args.capture) - elif args.replay: - host.HOST = host.Replayhost(args.replay) + if cmd_prefix or capture: + host.HOST = host.Remotehost(cmd_prefix, capture) + elif replay: + host.HOST = host.Replayhost(replay) else: host.HOST = host.Localhost() - if args.model == 'ietf-interfaces': + if model == 'ietf-interfaces': from . import ietf_interfaces - yang_data = ietf_interfaces.operational(args.param) - elif args.model == 'ietf-routing': + yang_data = ietf_interfaces.operational(param) + elif model == 'ietf-routing': from . import ietf_routing yang_data = ietf_routing.operational() - elif args.model == 'ietf-ospf': + elif model == 'ietf-ospf': from . import ietf_ospf yang_data = ietf_ospf.operational() - elif args.model == 'ietf-rip': + elif model == 'ietf-rip': from . import ietf_rip yang_data = ietf_rip.operational() - elif args.model == 'ietf-hardware': + elif model == 'ietf-hardware': from . import ietf_hardware yang_data = ietf_hardware.operational() - elif args.model == 'infix-containers': + elif model == 'infix-containers': from . import infix_containers yang_data = infix_containers.operational() - elif args.model == 'infix-dhcp-server': + elif model == 'infix-dhcp-server': from . import infix_dhcp_server yang_data = infix_dhcp_server.operational() - elif args.model == 'ietf-system': + elif model == 'ietf-system': from . import ietf_system yang_data = ietf_system.operational() - elif args.model == 'ietf-ntp': + elif model == 'ietf-ntp': from . import ietf_ntp yang_data = ietf_ntp.operational() - elif args.model == 'ieee802-dot1ab-lldp': + elif model == 'ieee802-dot1ab-lldp': from . import infix_lldp yang_data = infix_lldp.operational() - elif args.model == 'infix-firewall': + elif model == 'infix-firewall': from . import infix_firewall yang_data = infix_firewall.operational() - elif args.model == 'ietf-bfd-ip-sh': + elif model == 'ietf-bfd-ip-sh': from . import ietf_bfd_ip_sh yang_data = ietf_bfd_ip_sh.operational() else: - common.LOG.warning("Unsupported model %s", args.model) + common.LOG.warning("Unsupported model %s", model) sys.exit(1) print(json.dumps(yang_data, indent=2, ensure_ascii=False)) diff --git a/src/statd/python/yanger/common.py b/src/statd/python/yanger/common.py index be0c3ef90..a6cf8e506 100644 --- a/src/statd/python/yanger/common.py +++ b/src/statd/python/yanger/common.py @@ -1,8 +1,50 @@ +import syslog from datetime import timedelta from . import host -LOG = None + +class SysLog: + """Lightweight syslog wrapper replacing the logging module. + + Provides the same .error()/.warning()/.info()/.debug() interface + used throughout yanger, but uses the C syslog facility directly, + avoiding the ~374ms import overhead of logging + logging.handlers. + """ + + DEBUG = syslog.LOG_DEBUG + INFO = syslog.LOG_INFO + WARNING = syslog.LOG_WARNING + ERROR = syslog.LOG_ERR + + def __init__(self, name): + syslog.openlog(name, syslog.LOG_PID) + self._level = self.INFO + + def setLevel(self, level): + self._level = level + + def _log(self, level, msg, *args): + if level > self._level: + return + if args: + msg = msg % args + syslog.syslog(level, msg) + + def debug(self, msg, *args): + self._log(self.DEBUG, msg, *args) + + def info(self, msg, *args): + self._log(self.INFO, msg, *args) + + def warning(self, msg, *args): + self._log(self.WARNING, msg, *args) + + def error(self, msg, *args): + self._log(self.ERROR, msg, *args) + + +LOG = SysLog("yanger") class YangDate: def __init__(self, dt=None): diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index b68493c4e..1f210a7f4 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -265,8 +265,11 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): component["description"] = desc return component + # List hwmon directory once, reuse for all sensor types + all_entries = HOST.run(("ls", hwmon_path), default="").split() + # Temperature sensors - temp_entries = HOST.run(("ls", hwmon_path), default="").split() + temp_entries = all_entries temp_files = [os.path.join(hwmon_path, e) for e in temp_entries if e.startswith("temp") and e.endswith("_input")] for temp_file in temp_files: try: @@ -285,7 +288,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Fan sensors (RPM from tachometer) - fan_entries = HOST.run(("ls", hwmon_path), default="").split() + fan_entries = all_entries fan_files = [os.path.join(hwmon_path, e) for e in fan_entries if e.startswith("fan") and e.endswith("_input")] for fan_file in fan_files: try: @@ -307,7 +310,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): # Only add if no fan*_input exists for this device (avoid duplicates) has_rpm_sensor = bool(fan_files) if not has_rpm_sensor: - pwm_entries = HOST.run(("ls", hwmon_path), default="").split() + pwm_entries = all_entries pwm_files = [os.path.join(hwmon_path, e) for e in pwm_entries if e.startswith("pwm") and e[3:].replace('_', '').isdigit() if len(e) > 3] for pwm_file in pwm_files: # Skip pwm*_enable, pwm*_mode, etc. - only process pwm1, pwm2, etc. @@ -336,7 +339,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Voltage sensors - voltage_entries = HOST.run(("ls", hwmon_path), default="").split() + voltage_entries = all_entries voltage_files = [os.path.join(hwmon_path, e) for e in voltage_entries if e.startswith("in") and e.endswith("_input")] for voltage_file in voltage_files: try: @@ -356,7 +359,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Current sensors - current_entries = HOST.run(("ls", hwmon_path), default="").split() + current_entries = all_entries current_files = [os.path.join(hwmon_path, e) for e in current_entries if e.startswith("curr") and e.endswith("_input")] for current_file in current_files: try: @@ -376,7 +379,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Power sensors - power_entries = HOST.run(("ls", hwmon_path), default="").split() + power_entries = all_entries power_files = [os.path.join(hwmon_path, e) for e in power_entries if e.startswith("power") and e.endswith("_input")] for power_file in power_files: try: diff --git a/src/statd/python/yanger/ietf_interfaces/bridge.py b/src/statd/python/yanger/ietf_interfaces/bridge.py index b57536b4a..3c3bbd8c7 100644 --- a/src/statd/python/yanger/ietf_interfaces/bridge.py +++ b/src/statd/python/yanger/ietf_interfaces/bridge.py @@ -204,10 +204,13 @@ def mctlq2yang_mode(mctlq): return "off" -def mctl(ifname, vid): - mctl = HOST.run_json(["mctl", "-p", "show", "igmp", "json"], default={}) +def mctl_queriers(): + """Fetch all IGMP multicast querier data in one call""" + return HOST.run_json(["mctl", "-p", "show", "igmp", "json"], default={}) - for q in mctl.get("multicast-queriers", []): + +def mctl(ifname, vid, mctldata): + for q in mctldata.get("multicast-queriers", []): # TODO: Also need to match against VLAN uppers (e.g. br0.1337) if q.get("interface") == ifname and q.get("vid") == vid: return q @@ -239,8 +242,8 @@ def multicast_filters(iplink, vid): return { "multicast-filter": list(mdb.values()) } -def multicast(iplink, info): - mctlq = mctl(iplink["ifname"], info.get("vlan")) +def multicast(iplink, info, mctldata): + mctlq = mctl(iplink["ifname"], info.get("vlan"), mctldata) mcast = { "snooping": bool(info.get("mcast_snooping")), @@ -276,13 +279,15 @@ def vlans(iplink): if not (brgvlans := HOST.run_json(f"bridge -j vlan global show dev {iplink['ifname']}".split())): return [] + mctldata = mctl_queriers() + vlans = { v["vlan"]: { "vid": v["vlan"], "untagged": [], "tagged": [], - "multicast": multicast(iplink, v), + "multicast": multicast(iplink, v, mctldata), "multicast-filters": multicast_filters(iplink, v["vlan"]), } for v in brgvlans[0]["vlans"] @@ -307,7 +312,7 @@ def dbridge(iplink): info = iplink["linkinfo"]["info_data"] return { - "multicast": multicast(iplink, info), + "multicast": multicast(iplink, info, mctl_queriers()), "multicast-filters": multicast_filters(iplink, None), } diff --git a/src/statd/python/yanger/ietf_routing.py b/src/statd/python/yanger/ietf_routing.py index 9d7b1d9b9..da6fd1572 100644 --- a/src/statd/python/yanger/ietf_routing.py +++ b/src/statd/python/yanger/ietf_routing.py @@ -132,19 +132,34 @@ def get_routing_interfaces(): links_json = HOST.run(tuple(['ip', '-j', 'link', 'show']), default="[]") links = json.loads(links_json) + # Fetch all forwarding sysctls in two calls instead of 2 per interface + ipv4_sysctls = HOST.run(tuple(['sysctl', 'net.ipv4.conf']), default="") + ipv6_sysctls = HOST.run(tuple(['sysctl', 'net.ipv6.conf']), default="") + + # Parse "net.ipv4.conf..forwarding = 1" lines into a set + ipv4_fwd = set() + ipv6_fwd = set() + for line in ipv4_sysctls.splitlines(): + if '.forwarding = 1' in line: + # net.ipv4.conf.IFNAME.forwarding = 1 + parts = line.split('.') + if len(parts) >= 5: + ipv4_fwd.add(parts[3]) + + for line in ipv6_sysctls.splitlines(): + if '.force_forwarding = 1' in line: + # net.ipv6.conf.IFNAME.force_forwarding = 1 + parts = line.split('.') + if len(parts) >= 5: + ipv6_fwd.add(parts[3]) + routing_ifaces = [] for link in links: ifname = link.get('ifname') if not ifname: continue - # Check if IPv4 forwarding is enabled - ipv4_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv4.conf.{ifname}.forwarding']), default="0").strip() - - # Check if IPv6 force_forwarding is enabled (available since Linux 6.17) - ipv6_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv6.conf.{ifname}.force_forwarding']), default="0").strip() - - if ipv4_fwd == "1" or ipv6_fwd == "1": + if ifname in ipv4_fwd or ifname in ipv6_fwd: routing_ifaces.append(ifname) return routing_ifaces From f26a86971c5be9bdfbdafc8fc24394ba0e1b41f3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 16 Feb 2026 07:19:39 +0100 Subject: [PATCH 29/29] confd: clean up, simplify use common log framework - Use same log frameworks as reset of confd - Use existing primitives from libite + libsrx - Drop remaining pthreads - Coding style fixes Signed-off-by: Joachim Wiberg --- package/confd/confd.conf | 3 +- src/confd/src/Makefile.am | 4 +- src/confd/src/main.c | 459 +++++++++++++------------------------- 3 files changed, 158 insertions(+), 308 deletions(-) diff --git a/package/confd/confd.conf b/package/confd/confd.conf index 42385ad83..cc8d05720 100644 --- a/package/confd/confd.conf +++ b/package/confd/confd.conf @@ -3,10 +3,9 @@ # Single daemon handles gen-config, datastore init, config load, and plugins # log:prio:daemon.err service log:console env:/etc/default/confd \ - [S12345] confd -f -n -v warning \ + [S12345] confd -f -v warning \ -F /etc/factory-config.cfg \ -S /cfg/startup-config.cfg \ -E /etc/failure-config.cfg \ - -p /run/confd.pid \ -t $CONFD_TIMEOUT \ -- Configuration daemon diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 4ab20ba09..f457f1ad5 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -6,8 +6,8 @@ plugin_LTLIBRARIES = confd-plugin.la sbin_PROGRAMS = confd -confd_CFLAGS = $(sysrepo_CFLAGS) $(libyang_CFLAGS) $(jansson_CFLAGS) $(libite_CFLAGS) -confd_LDADD = $(sysrepo_LIBS) $(libyang_LIBS) $(jansson_LIBS) $(libite_LIBS) $(EV_LIBS) -ldl +confd_CFLAGS = $(sysrepo_CFLAGS) $(libyang_CFLAGS) $(jansson_CFLAGS) $(libite_CFLAGS) $(libsrx_CFLAGS) +confd_LDADD = $(sysrepo_LIBS) $(libyang_LIBS) $(jansson_LIBS) $(libite_LIBS) $(libsrx_LIBS) $(EV_LIBS) -ldl confd_SOURCES = main.c confd_plugin_la_LDFLAGS = -module -avoid-version -shared diff --git a/src/confd/src/main.c b/src/confd/src/main.c index 7e454e713..26566f5f7 100644 --- a/src/confd/src/main.c +++ b/src/confd/src/main.c @@ -18,10 +18,9 @@ #include #include #include -#include +#include #include #include -#include #include #include #include @@ -33,9 +32,13 @@ #include #include #include +#include #include #include +/* Maximum number of sysrepo event pipe file descriptors across all plugins */ +#define MAX_EVENT_FDS 64 + /* Callback type names from sysrepo plugin API */ #define SRP_INIT_CB "sr_plugin_init_cb" #define SRP_CLEANUP_CB "sr_plugin_cleanup_cb" @@ -60,31 +63,17 @@ struct plugin { int initialized; }; -/* Maximum number of sysrepo event pipe file descriptors across all plugins */ -#define MAX_EVENT_FDS 64 - -static void error_print(int sr_error, const char *format, ...) -{ - va_list ap; - char msg[2048]; +static sig_atomic_t pump_running = 1; +int debug = 0; - if (!sr_error) - snprintf(msg, sizeof(msg), "confd error: %s\n", format); - else - snprintf(msg, sizeof(msg), "confd error: %s (%s)\n", format, sr_strerror(sr_error)); - - va_start(ap, format); - vfprintf(stderr, msg, ap); - va_end(ap); -} /* Finit style progress output on console */ static void conout(int rc, const char *fmt, ...) { const char *sta = "%s\e[1m[\e[1;%dm%s\e[0m\e[1m]\e[0m %s"; const char *msg[] = { " OK ", "FAIL", "WARN", " ⋯ " }; - const int col[] = { 32, 31, 33, 33 }; const char *cr = rc == 3 ? "" : "\r"; + const int col[] = { 32, 31, 33, 33 }; char buf[80]; va_list ap; @@ -102,46 +91,28 @@ static void version_print(void) static void help_print(void) { - printf( - "Usage:\n" - " confd [-h] [-V] [-v ] [-d] [-n] [-f] [-p pidfile]\n" - " [-F factory-config] [-S startup-config] [-E failure-config]\n" - " [-t timeout]\n" - "\n" - "Options:\n" - " -h, --help Prints usage help.\n" - " -V, --version Prints version information.\n" - " -v, --verbosity \n" - " Change verbosity to a level (none, error, warning, info, debug) or\n" - " number (0, 1, 2, 3, 4).\n" - " -d, --debug Debug mode - not daemonized and logs to stderr.\n" - " -n, --foreground Run in foreground and log to syslog.\n" - " -f, --fatal-plugin-fail\n" - " Terminate if any plugin initialization fails.\n" - " -p, --pid-file \n" - " Create a PID file at the specified path.\n" - " -F, --factory-config \n" - " Factory default config file (default: /etc/factory-config.cfg).\n" - " -S, --startup-config \n" - " Startup config file (default: /cfg/startup-config.cfg).\n" - " -E, --failure-config \n" - " Failure fallback config file (default: /etc/failure-config.cfg).\n" - " -t, --timeout Sysrepo operation timeout in seconds (default: 60).\n" - "\n" - "Environment variable $SRPD_PLUGINS_PATH overwrites the default plugins directory.\n" - "\n"); -} - -static void ignore_signals(void) -{ - struct sigaction action; - - memset(&action, 0, sizeof(action)); - action.sa_handler = SIG_IGN; - sigaction(SIGPIPE, &action, NULL); - sigaction(SIGTSTP, &action, NULL); - sigaction(SIGTTIN, &action, NULL); - sigaction(SIGTTOU, &action, NULL); + printf("Usage:\n" + " confd [-h] [-V] [-v ] [-f]\n" + " [-F factory-config] [-S startup-config] [-E failure-config]\n" + " [-t timeout]\n" + "\n" + "Options:\n" + " -h, --help Prints usage help.\n" + " -V, --version Prints version information.\n" + " -v, --verbosity \n" + " Change verbosity to a level (none, error, warning, info, debug).\n" + " -f, --fatal-plugin-fail\n" + " Terminate if any plugin initialization fails.\n" + " -F, --factory-config \n" + " Factory default config file (default: /etc/factory-config.cfg).\n" + " -S, --startup-config \n" + " Startup config file (default: /cfg/startup-config.cfg).\n" + " -E, --failure-config \n" + " Failure fallback config file (default: /etc/failure-config.cfg).\n" + " -t, --timeout Sysrepo operation timeout in seconds (default: 60).\n" + "\n" + "Environment variable $SRPD_PLUGINS_PATH overwrites the default plugins directory.\n" + "\n"); } /* libev callbacks for steady-state operation */ @@ -160,28 +131,27 @@ static void sr_event_cb(struct ev_loop *loop, struct ev_io *w, int revents) } /* - * Temporary event pump thread for bootstrap. + * Temporary event pump process for bootstrap. * * With SR_SUBSCR_NO_THREAD, sysrepo writes events to a pipe and waits * for the application to call sr_subscription_process_events(). During * bootstrap, sr_replace_config() blocks waiting for callbacks — this - * thread ensures those callbacks get dispatched. + * child process ensures those callbacks get dispatched. */ -struct event_pump { - struct plugin *plugins; - int plugin_count; - int running; -}; +static void pump_sigterm(int sig) +{ + (void)sig; + pump_running = 0; +} -static void *event_pump_thread(void *arg) +static void event_pump(struct plugin *plugins, int plugin_count) { - struct event_pump *pump = arg; - struct pollfd fds[MAX_EVENT_FDS]; sr_subscription_ctx_t *subs[MAX_EVENT_FDS]; + struct pollfd fds[MAX_EVENT_FDS]; int nfds = 0; - for (int i = 0; i < pump->plugin_count; i++) { - struct plugin *p = &pump->plugins[i]; + for (int i = 0; i < plugin_count; i++) { + struct plugin *p = &plugins[i]; if (p->sub && sr_get_event_pipe(p->sub, &fds[nfds].fd) == SR_ERR_OK) { fds[nfds].events = POLLIN; @@ -195,7 +165,9 @@ static void *event_pump_thread(void *arg) } } - while (pump->running) { + signal(SIGTERM, pump_sigterm); + + while (pump_running) { if (poll(fds, nfds, 100) > 0) { for (int i = 0; i < nfds; i++) if (fds[i].revents & POLLIN) @@ -203,12 +175,12 @@ static void *event_pump_thread(void *arg) } } - return NULL; + _exit(0); } static void quiet_now(void) { - int fd = -1; + int fd; fd = open("/dev/null", O_RDWR, 0); if (fd != -1) { @@ -217,89 +189,6 @@ static void quiet_now(void) dup2(fd, STDERR_FILENO); close(fd); } - - nice(0); -} - -static void daemon_init(int debug, sr_log_level_t log_level) -{ - pid_t pid = 0, sid = 0; - - nice(-20); - - ignore_signals(); - - if (debug) { - if (debug < 0) - goto done; - sr_log_stderr(log_level); - return; - } - - pid = fork(); - if (pid < 0) { - error_print(0, "fork() failed (%s).", strerror(errno)); - exit(EXIT_FAILURE); - } - if (pid > 0) - exit(EXIT_SUCCESS); - - sid = setsid(); - if (sid < 0) { - error_print(0, "setsid() failed (%s).", strerror(errno)); - exit(EXIT_FAILURE); - } - - if (chdir("/") < 0) { - error_print(0, "chdir() failed (%s).", strerror(errno)); - exit(EXIT_FAILURE); - } - - quiet_now(); -done: - sr_log_syslog("confd", log_level); -} - -static int open_pidfile(const char *pidfile) -{ - int pidfd; - - pidfd = open(pidfile, O_RDWR | O_CREAT, 0640); - if (pidfd < 0) { - error_print(0, "Unable to open the PID file \"%s\" (%s).", pidfile, strerror(errno)); - return -1; - } - - if (lockf(pidfd, F_TLOCK, 0) < 0) { - if (errno == EACCES || errno == EAGAIN) - error_print(0, "Another instance of confd is running."); - else - error_print(0, "Unable to lock the PID file \"%s\" (%s).", pidfile, strerror(errno)); - close(pidfd); - return -1; - } - - return pidfd; -} - -static int write_pidfile(int pidfd) -{ - char pid[30] = {0}; - int pid_len; - - if (ftruncate(pidfd, 0)) { - error_print(0, "Failed to truncate pid file (%s).", strerror(errno)); - return -1; - } - - snprintf(pid, sizeof(pid) - 1, "%ld\n", (long)getpid()); - pid_len = strlen(pid); - if (write(pidfd, pid, pid_len) < pid_len) { - error_print(0, "Failed to write PID into pid file (%s).", strerror(errno)); - return -1; - } - - return 0; } /* @@ -318,14 +207,14 @@ static size_t path_len_no_ext(const char *path) static int load_plugins(struct plugin **plugins, int *plugin_count) { - void *mem, *handle; - struct plugin *plugin; - DIR *dir; - struct dirent *ent; const char *plugins_dir; - char *path; + struct dirent *ent; + struct plugin *plugin; + void *mem, *handle; size_t name_len; int rc = 0; + char *path; + DIR *dir; *plugins = NULL; *plugin_count = 0; @@ -336,7 +225,7 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) dir = opendir(plugins_dir); if (!dir) { - error_print(0, "Opening \"%s\" directory failed (%s).", plugins_dir, strerror(errno)); + ERRNO("Opening \"%s\" directory failed", plugins_dir); return -1; } @@ -345,13 +234,13 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) continue; if (asprintf(&path, "%s/%s", plugins_dir, ent->d_name) == -1) { - error_print(0, "asprintf() failed (%s).", strerror(errno)); + ERRNO("asprintf() failed"); rc = -1; break; } handle = dlopen(path, RTLD_LAZY); if (!handle) { - error_print(0, "Opening plugin \"%s\" failed (%s).", path, dlerror()); + ERROR("Opening plugin \"%s\" failed: %s", path, dlerror()); free(path); rc = -1; break; @@ -360,7 +249,7 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) mem = realloc(*plugins, (*plugin_count + 1) * sizeof(**plugins)); if (!mem) { - error_print(0, "realloc() failed (%s).", strerror(errno)); + ERRNO("realloc() failed"); dlclose(handle); rc = -1; break; @@ -371,8 +260,7 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) *(void **)&plugin->init_cb = dlsym(handle, SRP_INIT_CB); if (!plugin->init_cb) { - error_print(0, "Failed to find function \"%s\" in plugin \"%s\".", - SRP_INIT_CB, ent->d_name); + ERROR("Failed to find \"%s\" in plugin \"%s\".", SRP_INIT_CB, ent->d_name); dlclose(handle); rc = -1; break; @@ -380,8 +268,7 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) *(void **)&plugin->cleanup_cb = dlsym(handle, SRP_CLEANUP_CB); if (!plugin->cleanup_cb) { - error_print(0, "Failed to find function \"%s\" in plugin \"%s\".", - SRP_CLEANUP_CB, ent->d_name); + ERROR("Failed to find \"%s\" in plugin \"%s\".", SRP_CLEANUP_CB, ent->d_name); dlclose(handle); rc = -1; break; @@ -394,7 +281,7 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) name_len = path_len_no_ext(ent->d_name); if (name_len == 0) { - error_print(0, "Wrong filename \"%s\".", ent->d_name); + ERROR("Wrong filename \"%s\".", ent->d_name); dlclose(handle); rc = -1; break; @@ -402,7 +289,7 @@ static int load_plugins(struct plugin **plugins, int *plugin_count) plugin->name = strndup(ent->d_name, name_len); if (!plugin->name) { - error_print(0, "strndup() failed."); + ERRNO("strndup() failed"); dlclose(handle); rc = -1; break; @@ -490,8 +377,7 @@ static int maybe_migrate(const char *path) } json_decref(root); - SRPLG_LOG_INF("confd", "%s config version %s vs confd %s, migrating ...", - path, file_ver, CONFD_VERSION); + NOTE("%s config version %s vs confd %s, migrating ...", path, file_ver, CONFD_VERSION); mkpath(backup_dir, 0770); chown(backup_dir, 0, 10); /* root:wheel */ @@ -499,16 +385,11 @@ static int maybe_migrate(const char *path) snprintf(backup, sizeof(backup), "%s/%s", backup_dir, basenm(path)); rc = systemf("migrate -i -b \"%s\" \"%s\"", backup, path); if (rc) - SRPLG_LOG_ERR("confd", "Migration of %s failed (rc=%d)", path, rc); + ERROR("Migration of %s failed (rc=%d)", path, rc); return rc; } -static int file_exists(const char *path) -{ - return access(path, F_OK) == 0; -} - /* * Load a JSON config file into the running datastore. * Mirrors what sysrepocfg -I does: lyd_parse_data() + sr_replace_config(). @@ -531,18 +412,17 @@ static int load_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, ly_in_new_memory(empty, &in); } else if (lyrc) { - error_print(0, "Failed to open \"%s\" for reading", path); + ERROR("Failed to open \"%s\" for reading", path); sr_release_context(conn); return -1; } lyrc = lyd_parse_data(ly_ctx, NULL, in, LYD_JSON, - LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STRICT, - 0, &data); + LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STRICT, 0, &data); ly_in_free(in, 1); if (lyrc) { - SRPLG_LOG_ERR("confd", "Parsing %s failed", path); + ERROR("Parsing %s failed", path); sr_release_context(conn); return -1; } @@ -551,7 +431,7 @@ static int load_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, r = sr_replace_config(sess, NULL, data, timeout_ms); if (r != SR_ERR_OK) { - SRPLG_LOG_ERR("confd", "sr_replace_config failed: %s", sr_strerror(r)); + ERROR("sr_replace_config failed: %s", sr_strerror(r)); return -1; } @@ -569,14 +449,14 @@ static int export_running(sr_session_ctx_t *sess, const char *path, uint32_t tim r = sr_get_data(sess, "/*", 0, timeout_ms, 0, &data); if (r != SR_ERR_OK) { - SRPLG_LOG_ERR("confd", "sr_get_data failed: %s", sr_strerror(r)); + ERROR("sr_get_data failed: %s", sr_strerror(r)); return -1; } umask(0006); fp = fopen(path, "w"); if (!fp) { - SRPLG_LOG_ERR("confd", "Failed to open %s for writing: %s", path, strerror(errno)); + ERRNO("Failed to open %s for writing", path); sr_release_data(data); return -1; } @@ -599,21 +479,21 @@ static void handle_startup_failure(sr_session_ctx_t *sess, const char *failure_p { int r; - SRPLG_LOG_ERR("confd", "Failed loading startup-config, reverting to Fail Secure mode!"); + ERROR("Failed loading startup-config, reverting to Fail Secure mode!"); /* Reset to factory-default */ r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms); if (r != SR_ERR_OK) { - SRPLG_LOG_ERR("confd", "sr_copy_config(factory-default) failed: %s", sr_strerror(r)); + ERROR("sr_copy_config(factory-default) failed: %s", sr_strerror(r)); /* Nuclear option: wipe everything */ systemf("rm -f /etc/sysrepo/data/*startup* /etc/sysrepo/data/*running* /dev/shm/sr_*"); return; } /* Load failure-config on top */ - if (file_exists(failure_path)) { + if (fexist(failure_path)) { if (load_config(conn, sess, failure_path, timeout_ms)) { - SRPLG_LOG_ERR("confd", "Failed loading failure-config, aborting!"); + ERROR("Failed loading failure-config, aborting!"); banner_append("CRITICAL ERROR: Logins are disabled, no credentials available"); systemf("initctl -nbq runlevel 9"); return; @@ -628,10 +508,10 @@ static void handle_startup_failure(sr_session_ctx_t *sess, const char *failure_p */ static void maybe_enable_test_mode(void) { - if (file_exists("/mnt/aux/test-mode")) { + if (fexist("/mnt/aux/test-mode")) { int rc; - conout(3, "Enbling test mode"); + conout(3, "Enabling test mode"); rc = systemf("sysrepoctl -c infix-test -e test-mode-enable"); conout(rc ? 1 : 0, "\n"); } @@ -652,86 +532,78 @@ static int bootstrap_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, int r; /* Test mode support */ - if (file_exists("/mnt/aux/test-mode")) { - if (file_exists("/mnt/aux/test-override-startup")) { + if (fexist("/mnt/aux/test-mode")) { + if (fexist("/mnt/aux/test-override-startup")) { unlink("/mnt/aux/test-override-startup"); config_path = startup_path; } else { - SRPLG_LOG_INF("confd", "Test mode detected, switching to test-config"); + NOTE("Test mode detected, switching to test-config"); config_path = test_path; } } else { config_path = startup_path; } - if (file_exists(config_path)) { + if (fexist(config_path)) { /* Run migration if needed */ maybe_migrate(config_path); /* Load startup (or test) config */ - SRPLG_LOG_INF("confd", "Loading %s ...", config_path); + NOTE("Loading %s ...", config_path); if (load_config(conn, sess, config_path, timeout_ms)) { handle_startup_failure(sess, failure_path, conn, timeout_ms); return 0; /* continue running even in fail-secure */ } - SRPLG_LOG_INF("confd", "Loaded %s successfully, syncing startup datastore.", config_path); + NOTE("Loaded %s successfully, syncing startup datastore.", config_path); sr_session_switch_ds(sess, SR_DS_STARTUP); r = sr_copy_config(sess, NULL, SR_DS_RUNNING, timeout_ms); sr_session_switch_ds(sess, SR_DS_RUNNING); if (r != SR_ERR_OK) - SRPLG_LOG_WRN("confd", "Failed to sync startup datastore: %s", sr_strerror(r)); + WARN("Failed to sync startup datastore: %s", sr_strerror(r)); return 0; } /* First boot: no startup-config, initialize from factory */ - SRPLG_LOG_INF("confd", "startup-config missing, initializing from factory-config"); + NOTE("startup-config missing, initializing from factory-config"); r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms); if (r != SR_ERR_OK) { - SRPLG_LOG_ERR("confd", "sr_copy_config(factory-default) failed: %s", sr_strerror(r)); + ERROR("sr_copy_config(factory-default) failed: %s", sr_strerror(r)); return -1; } /* Export running → startup file */ if (export_running(sess, startup_path, timeout_ms)) - SRPLG_LOG_WRN("confd", "Failed to export running to %s", startup_path); + WARN("Failed to export running to %s", startup_path); return 0; } -static void *gen_config_thread(void *arg) -{ - (void)arg; - return (void *)(intptr_t)systemf("/usr/libexec/confd/gen-config"); -} - int main(int argc, char **argv) { - struct plugin *plugins = NULL; - sr_conn_ctx_t *conn = NULL; - sr_session_ctx_t *sess = NULL; - sr_log_level_t log_level = SR_LL_ERR; - int plugin_count = 0, i, r, rc = EXIT_FAILURE, opt, debug = 0; - int pidfd = -1, fatal_fail = 0; - pthread_t tid; - void *tret; - const char *pidfile = NULL; - const char *factory_path = "/etc/factory-config.cfg"; - const char *startup_path = "/cfg/startup-config.cfg"; const char *failure_path = "/etc/failure-config.cfg"; + const char *startup_path = "/cfg/startup-config.cfg"; + const char *factory_path = "/etc/factory-config.cfg"; const char *test_path = "/etc/test-config.cfg"; + int log_opts = LOG_PID | LOG_NDELAY; + int rc = EXIT_FAILURE, opt, i, r; + sr_session_ctx_t *sess = NULL; + struct plugin *plugins = NULL; + sr_conn_ctx_t *conn = NULL; + int log_level = LOG_ERR; + pid_t gen_pid, pump_pid; uint32_t timeout_s = 60; + int plugin_count = 0; + int fatal_fail = 0; uint32_t timeout_ms; + int status; struct option options[] = { {"help", no_argument, NULL, 'h'}, {"version", no_argument, NULL, 'V'}, {"verbosity", required_argument, NULL, 'v'}, - {"debug", no_argument, NULL, 'd'}, - {"foreground", no_argument, NULL, 'n'}, - {"pid-file", required_argument, NULL, 'p'}, {"fatal-plugin-fail", no_argument, NULL, 'f'}, {"factory-config", required_argument, NULL, 'F'}, {"startup-config", required_argument, NULL, 'S'}, @@ -741,44 +613,31 @@ int main(int argc, char **argv) }; opterr = 0; - while ((opt = getopt_long(argc, argv, "hVv:dnp:fF:S:E:t:", options, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "hVv:fF:S:E:t:", options, NULL)) != -1) { switch (opt) { case 'h': version_print(); help_print(); - rc = EXIT_SUCCESS; - goto cleanup; + return EXIT_SUCCESS; case 'V': version_print(); - rc = EXIT_SUCCESS; - goto cleanup; + return EXIT_SUCCESS; case 'v': if (!strcmp(optarg, "none")) - log_level = SR_LL_NONE; + log_level = LOG_EMERG; else if (!strcmp(optarg, "error")) - log_level = SR_LL_ERR; + log_level = LOG_ERR; else if (!strcmp(optarg, "warning")) - log_level = SR_LL_WRN; + log_level = LOG_WARNING; else if (!strcmp(optarg, "info")) - log_level = SR_LL_INF; + log_level = LOG_NOTICE; else if (!strcmp(optarg, "debug")) - log_level = SR_LL_DBG; - else if (strlen(optarg) == 1 && optarg[0] >= '0' && optarg[0] <= '4') - log_level = atoi(optarg); + log_level = LOG_DEBUG; else { - error_print(0, "Invalid verbosity \"%s\"", optarg); - goto cleanup; + fprintf(stderr, "confd error: Invalid verbosity \"%s\"\n", optarg); + return EXIT_FAILURE; } break; - case 'd': - debug = 1; - break; - case 'n': - debug = -1; - break; - case 'p': - pidfile = optarg; - break; case 'f': fatal_fail = 1; break; @@ -795,35 +654,44 @@ int main(int argc, char **argv) timeout_s = (uint32_t)atoi(optarg); break; default: - error_print(0, "Invalid option or missing argument: -%c", optopt); - goto cleanup; + fprintf(stderr, "confd error: Invalid option or missing argument: -%c\n", optopt); + return EXIT_FAILURE; } } if (optind < argc) { - error_print(0, "Redundant parameters"); - goto cleanup; + fprintf(stderr, "confd error: Redundant parameters\n"); + return EXIT_FAILURE; } timeout_ms = timeout_s * 1000; - if (pidfile && (pidfd = open_pidfile(pidfile)) < 0) - goto cleanup; + nice(-20); + signal(SIGPIPE, SIG_IGN); - /* Load plugins from disk (dlopen) before daemonizing */ - if (load_plugins(&plugins, &plugin_count)) - error_print(0, "load_plugins failed (continuing)"); + if (getenv("DEBUG")) { + log_opts |= LOG_PERROR; + debug = 1; + } + openlog("confd", log_opts, LOG_DAEMON); + setlogmask(LOG_UPTO(log_level)); - /* Daemonize -- after this point, confd no longer logs to stderr */ - daemon_init(debug, log_level); + pidfile(NULL); - /* Start gen-config in parallel -- thread is joined before we need the result */ + /* Load plugins from disk (dlopen) */ + if (load_plugins(&plugins, &plugin_count)) + ERROR("load_plugins failed (continuing)"); + + /* Start gen-config in parallel — child is reaped before we need the result */ conout(3, "Generating factory-config and failure-config"); - if (pthread_create(&tid, NULL, gen_config_thread, NULL)) { - SRPLG_LOG_ERR("confd", "Failed to create gen-config thread: %s", strerror(errno)); + gen_pid = fork(); + if (gen_pid < 0) { + ERRNO("Failed to fork gen-config"); conout(1, "\n"); goto cleanup; } + if (gen_pid == 0) + _exit(systemf("/usr/libexec/confd/gen-config")); /* Phase 1: Wipe stale SHM for a clean slate */ wipe_sysrepo_shm(); @@ -831,25 +699,25 @@ int main(int argc, char **argv) /* Phase 2: Connect to sysrepo (rebuilds SHM from installed YANG modules) */ r = sr_connect(0, &conn); if (r != SR_ERR_OK) { - error_print(r, "Failed to connect"); + ERROR("Failed to connect: %s", sr_strerror(r)); goto cleanup; } - /* Phase 3: Wait for gen-config thread to finish */ - pthread_join(tid, &tret); - if ((intptr_t)tret != 0) { - SRPLG_LOG_ERR("confd", "gen-config failed (rc=%d)", (int)(intptr_t)tret); + /* Phase 3: Wait for gen-config to finish */ + waitpid(gen_pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ERROR("gen-config failed (status=%d)", status); conout(1, "\n"); goto cleanup; } conout(0, "\n"); /* Phase 4: Install factory defaults into all datastores */ - SRPLG_LOG_INF("confd", "Loading factory-default datastore from %s ...", factory_path); + NOTE("Loading factory-default datastore from %s ...", factory_path); conout(3, "Loading factory-default datastore"); r = sr_install_factory_config(conn, factory_path); if (r != SR_ERR_OK) { - SRPLG_LOG_ERR("confd", "sr_install_factory_config failed: %s", sr_strerror(r)); + ERROR("sr_install_factory_config failed: %s", sr_strerror(r)); conout(1, "\n"); goto cleanup; } @@ -858,7 +726,7 @@ int main(int argc, char **argv) /* Phase 5: Start running-datastore session */ r = sr_session_start(conn, SR_DS_RUNNING, &sess); if (r != SR_ERR_OK) { - error_print(r, "Failed to start new session"); + ERROR("Failed to start new session: %s", sr_strerror(r)); goto cleanup; } @@ -869,7 +737,7 @@ int main(int argc, char **argv) * what the plugin callbacks expect. */ r = sr_replace_config(sess, NULL, NULL, timeout_ms); if (r != SR_ERR_OK) { - SRPLG_LOG_ERR("confd", "Failed to clear running datastore: %s", sr_strerror(r)); + ERROR("Failed to clear running datastore: %s", sr_strerror(r)); goto cleanup; } @@ -881,14 +749,13 @@ int main(int argc, char **argv) for (i = 0; i < plugin_count; i++) { r = plugins[i].init_cb(sess, &plugins[i].private_data); if (r) { - SRPLG_LOG_ERR("confd", "Plugin \"%s\" initialization failed (%s).", - plugins[i].name, sr_strerror(r)); + ERROR("Plugin \"%s\" initialization failed (%s).", plugins[i].name, sr_strerror(r)); if (fatal_fail) { conout(1, "\n"); goto cleanup; } } else { - SRPLG_LOG_INF("confd", "Plugin \"%s\" initialized.", plugins[i].name); + NOTE("Plugin \"%s\" initialized.", plugins[i].name); plugins[i].initialized = 1; } } @@ -897,43 +764,36 @@ int main(int argc, char **argv) /* Phase 8: Collect subscription contexts from plugins */ for (i = 0; i < plugin_count; i++) { if (plugins[i].initialized && plugins[i].get_subs) - plugins[i].get_subs(plugins[i].private_data, - &plugins[i].sub, &plugins[i].fsub); + plugins[i].get_subs(plugins[i].private_data, &plugins[i].sub, &plugins[i].fsub); } - /* Phase 9: Start event pump thread for bootstrap. + /* Phase 9: Fork event pump process for bootstrap. * With SR_SUBSCR_NO_THREAD, sr_replace_config() blocks waiting - * for callbacks. The pump thread processes those events. */ - struct event_pump pump = { - .plugins = plugins, - .plugin_count = plugin_count, - .running = 1, - }; - pthread_t pump_tid; - - if (pthread_create(&pump_tid, NULL, event_pump_thread, &pump)) { - SRPLG_LOG_ERR("confd", "Failed to create event pump thread: %s", strerror(errno)); + * for callbacks. The pump process processes those events. */ + pump_pid = fork(); + if (pump_pid < 0) { + ERRNO("Failed to fork event pump"); goto cleanup; } + if (pump_pid == 0) + event_pump(plugins, plugin_count); - /* - * Phase 10: Load startup config -- plugins are now subscribed, so + /* Phase 10: Load startup config -- plugins are now subscribed, so * sr_replace_config() will trigger their change callbacks. - * The event pump thread processes those callbacks. - */ + * The event pump process processes those callbacks. */ conout(3, "Loading startup-config"); if (bootstrap_config(conn, sess, factory_path, startup_path, failure_path, test_path, timeout_ms)) { - pump.running = 0; - pthread_join(pump_tid, NULL); + kill(pump_pid, SIGTERM); + waitpid(pump_pid, NULL, 0); conout(1, "\n"); goto cleanup; } conout(0, "\n"); /* Phase 11: Stop event pump — bootstrap is done */ - pump.running = 0; - pthread_join(pump_tid, NULL); + kill(pump_pid, SIGTERM); + waitpid(pump_pid, NULL, 0); /* No more progress to show, go to quiet daemon mode */ quiet_now(); @@ -941,15 +801,11 @@ int main(int argc, char **argv) /* Signal that bootstrap is complete (dbus, resolvconf depend on this) */ symlink("/run/finit/cond/reconf", "/run/finit/cond/usr/bootstrap"); - /* Write PID file after everything is ready */ - if (pidfile && write_pidfile(pidfd) < 0) - goto cleanup; - - /* Phase 12: Steady-state — libev event loop replaces pthread_cond_wait */ + /* Phase 12: Steady-state — libev event loop */ { - struct ev_loop *loop = EV_DEFAULT; struct ev_signal sigterm_w, sigint_w, sighup_w, sigquit_w; struct ev_io io_watchers[MAX_EVENT_FDS]; + struct ev_loop *loop = EV_DEFAULT; int nio = 0; ev_signal_init(&sigterm_w, signal_cb, SIGTERM); @@ -995,11 +851,6 @@ int main(int argc, char **argv) } free(plugins); - if (pidfd >= 0) { - close(pidfd); - unlink(pidfile); - } - sr_disconnect(conn); return rc; }