" }
+ ]
+ }
+ }
+ ]
+ }
+}
+```
+
+> [!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
genkey rsa MIIBCgKCAQEAnj0YinjhYDgYbEGuh7...
+gencert x509 MIIDXTCCAkWgAwIBAgIJAJC1HiIAZA...
wg-tunnel x25519 bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1...
diff --git a/doc/management.md b/doc/management.md
index 486cd6846..2a75214cb 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.
diff --git a/package/Config.in b/package/Config.in
index 0c0115b48..110d247b2 100644
--- a/package/Config.in
+++ b/package/Config.in
@@ -20,6 +20,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/confd/confd.conf b/package/confd/confd.conf
index 482ee26e8..cc8d05720 100644
--- a/package/confd/confd.conf
+++ b/package/confd/confd.conf
@@ -1,27 +1,11 @@
#set DEBUG=1
-run name:bootstrap log:prio:user.notice norestart \
- [S] /usr/libexec/confd/bootstrap \
- -- Bootstrapping YANG datastore
-
-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 \
+# 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 -v warning \
+ -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/package/confd/confd.mk b/package/confd/confd.mk
index 7cfb2347f..6f81deb0f 100644
--- a/package/confd/confd.mk
+++ b/package/confd/confd.mk
@@ -4,13 +4,13 @@
#
################################################################################
-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
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/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/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))
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
diff --git a/package/skeleton-init-finit/skeleton/etc/default/zebra b/package/skeleton-init-finit/skeleton/etc/default/zebra
index 4467b9af2..19e0897c7 100644
--- a/package/skeleton-init-finit/skeleton/etc/default/zebra
+++ b/package/skeleton-init-finit/skeleton/etc/default/zebra
@@ -1,2 +1,2 @@
-# --log-level debug
-ZEBRA_ARGS="-A 127.0.0.1 -u frr -g frr --log syslog --log-level err"
+# --log-level debug --graceful_restart 60
+ZEBRA_ARGS="-A 127.0.0.1 -a -s 90000000 -u frr -g frr --log syslog --log-level err"
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
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf
index 825aff9cc..fbf2c0861 100644
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf
@@ -1,2 +1,3 @@
service pid:!/run/frr/mgmtd.pid env:-/etc/default/mgmtd \
- [2345] mgmtd $MGMTD_ARGS -- FRR MGMT daemon
+ [2345] mgmtd $MGMTD_ARGS \
+ -- FRR MGMT daemon
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf
index e4652ff1f..20b523396 100644
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf
@@ -1,2 +1,3 @@
-service env:-/etc/default/ospfd \
- [2345] ospfd $OSPFD_ARGS -- OSPF daemon
+service pid:!/run/frr/ospfd.pid env:-/etc/default/ospfd \
+ [2345] ospfd $OSPFD_ARGS \
+ -- OSPF daemon
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf
index bb311b582..8a8f93308 100644
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf
@@ -1,3 +1,3 @@
-service env:-/etc/default/ripd \
- [2345] ripd $RIPD_ARGS
+service pid:!/run/frr/ripd.pid env:-/etc/default/ripd \
+ [2345] ripd $RIPD_ARGS \
-- RIP daemon
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf
index dce1abcac..d423b539c 100644
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf
@@ -1,3 +1,5 @@
-service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
- [2345] zebra $ZEBRA_ARGS
+# Unfortunately Zebra in Frr v10.5.1 is a bit buggy and does not
+# properly flush unused routes the kernel has removed. <~pid/netd>
+service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
+ [2345] zebra $ZEBRA_ARGS \
-- Zebra routing daemon
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/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/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(-)
diff --git a/src/confd/bin/Makefile.am b/src/confd/bin/Makefile.am
index 49bed009d..83ba7da5a 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 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/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/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
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/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..bed811611 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
@@ -77,9 +78,19 @@ 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])
+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/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/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
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/Makefile.am b/src/confd/src/Makefile.am
index 08bf1cb73..f457f1ad5 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) $(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
confd_plugin_la_CFLAGS = \
diff --git a/src/confd/src/core.c b/src/confd/src/core.c
index 4acce329a..dd035a3a5 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");
@@ -431,11 +441,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!");
@@ -454,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)
@@ -638,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 e65f27053..9262fde6a 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
@@ -140,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;
@@ -154,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;
@@ -167,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;
@@ -176,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;
@@ -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 6f493e5c0..7cfa93bbf 100644
--- a/src/confd/src/keystore.c
+++ b/src/confd/src/keystore.c
@@ -12,6 +12,11 @@
#define XPATH_KEYSTORE_ASYM "/ietf-keystore:keystore/asymmetric-keys"
#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)
@@ -68,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;
@@ -75,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";
@@ -163,6 +226,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);
@@ -181,8 +322,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);
@@ -209,21 +349,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/main.c b/src/confd/src/main.c
new file mode 100644
index 000000000..26566f5f7
--- /dev/null
+++ b/src/confd/src/main.c
@@ -0,0 +1,856 @@
+/* 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
+#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"
+
+#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 (*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;
+};
+
+static sig_atomic_t pump_running = 1;
+int debug = 0;
+
+
+/* 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 char *cr = rc == 3 ? "" : "\r";
+ const int col[] = { 32, 31, 33, 33 };
+ 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",
+ CONFD_VERSION, SR_VERSION);
+}
+
+static void help_print(void)
+{
+ 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 */
+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 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
+ * child process ensures those callbacks get dispatched.
+ */
+static void pump_sigterm(int sig)
+{
+ (void)sig;
+ pump_running = 0;
+}
+
+static void event_pump(struct plugin *plugins, int plugin_count)
+{
+ sr_subscription_ctx_t *subs[MAX_EVENT_FDS];
+ struct pollfd fds[MAX_EVENT_FDS];
+ int nfds = 0;
+
+ 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;
+ 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++;
+ }
+ }
+
+ 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)
+ sr_subscription_process_events(subs[i], NULL, NULL);
+ }
+ }
+
+ _exit(0);
+}
+
+static void quiet_now(void)
+{
+ int fd;
+
+ fd = open("/dev/null", O_RDWR, 0);
+ if (fd != -1) {
+ dup2(fd, STDIN_FILENO);
+ dup2(fd, STDOUT_FILENO);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+ }
+}
+
+/*
+ * 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)
+{
+ const char *plugins_dir;
+ 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;
+
+ plugins_dir = getenv("SRPD_PLUGINS_PATH");
+ if (!plugins_dir)
+ plugins_dir = SRPD_PLUGINS_PATH;
+
+ dir = opendir(plugins_dir);
+ if (!dir) {
+ ERRNO("Opening \"%s\" directory failed", plugins_dir);
+ 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) {
+ ERRNO("asprintf() failed");
+ rc = -1;
+ break;
+ }
+ handle = dlopen(path, RTLD_LAZY);
+ if (!handle) {
+ ERROR("Opening plugin \"%s\" failed: %s", path, dlerror());
+ free(path);
+ rc = -1;
+ break;
+ }
+ free(path);
+
+ mem = realloc(*plugins, (*plugin_count + 1) * sizeof(**plugins));
+ if (!mem) {
+ ERRNO("realloc() failed");
+ 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("Failed to find \"%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("Failed to find \"%s\" in plugin \"%s\".", SRP_CLEANUP_CB, ent->d_name);
+ dlclose(handle);
+ rc = -1;
+ 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);
+ if (name_len == 0) {
+ ERROR("Wrong filename \"%s\".", ent->d_name);
+ dlclose(handle);
+ rc = -1;
+ break;
+ }
+
+ plugin->name = strndup(ent->d_name, name_len);
+ if (!plugin->name) {
+ ERRNO("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);
+
+ 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 */
+
+ snprintf(backup, sizeof(backup), "%s/%s", backup_dir, basenm(path));
+ rc = systemf("migrate -i -b \"%s\" \"%s\"", backup, path);
+ if (rc)
+ ERROR("Migration of %s failed (rc=%d)", path, rc);
+
+ return rc;
+}
+
+/*
+ * 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("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) {
+ ERROR("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) {
+ ERROR("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) {
+ ERROR("sr_get_data failed: %s", sr_strerror(r));
+ return -1;
+ }
+
+ umask(0006);
+ fp = fopen(path, "w");
+ if (!fp) {
+ ERRNO("Failed to open %s for writing", path);
+ 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, 10); /* root:wheel for admin group access */
+
+ 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;
+
+ 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) {
+ 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 (fexist(failure_path)) {
+ if (load_config(conn, sess, failure_path, timeout_ms)) {
+ ERROR("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 (fexist("/mnt/aux/test-mode")) {
+ int rc;
+
+ conout(3, "Enabling test mode");
+ rc = systemf("sysrepoctl -c infix-test -e test-mode-enable");
+ conout(rc ? 1 : 0, "\n");
+ }
+}
+
+/*
+ * 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 (fexist("/mnt/aux/test-mode")) {
+ if (fexist("/mnt/aux/test-override-startup")) {
+ unlink("/mnt/aux/test-override-startup");
+ config_path = startup_path;
+ } else {
+ NOTE("Test mode detected, switching to test-config");
+ config_path = test_path;
+ }
+ } else {
+ config_path = startup_path;
+ }
+
+ if (fexist(config_path)) {
+ /* Run migration if needed */
+ maybe_migrate(config_path);
+
+ /* Load startup (or test) config */
+ 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 */
+ }
+
+ 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)
+ WARN("Failed to sync startup datastore: %s", sr_strerror(r));
+
+ return 0;
+ }
+
+ /* First boot: no startup-config, initialize from factory */
+ 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) {
+ 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))
+ WARN("Failed to export running to %s", startup_path);
+
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ 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'},
+ {"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:fF:S:E:t:", options, NULL)) != -1) {
+ switch (opt) {
+ case 'h':
+ version_print();
+ help_print();
+ return EXIT_SUCCESS;
+ case 'V':
+ version_print();
+ return EXIT_SUCCESS;
+ case 'v':
+ if (!strcmp(optarg, "none"))
+ log_level = LOG_EMERG;
+ else if (!strcmp(optarg, "error"))
+ log_level = LOG_ERR;
+ else if (!strcmp(optarg, "warning"))
+ log_level = LOG_WARNING;
+ else if (!strcmp(optarg, "info"))
+ log_level = LOG_NOTICE;
+ else if (!strcmp(optarg, "debug"))
+ log_level = LOG_DEBUG;
+ else {
+ fprintf(stderr, "confd error: Invalid verbosity \"%s\"\n", optarg);
+ return EXIT_FAILURE;
+ }
+ 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:
+ fprintf(stderr, "confd error: Invalid option or missing argument: -%c\n", optopt);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (optind < argc) {
+ fprintf(stderr, "confd error: Redundant parameters\n");
+ return EXIT_FAILURE;
+ }
+
+ timeout_ms = timeout_s * 1000;
+
+ nice(-20);
+ signal(SIGPIPE, SIG_IGN);
+
+ if (getenv("DEBUG")) {
+ log_opts |= LOG_PERROR;
+ debug = 1;
+ }
+ openlog("confd", log_opts, LOG_DAEMON);
+ setlogmask(LOG_UPTO(log_level));
+
+ pidfile(NULL);
+
+ /* 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");
+ 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();
+
+ /* Phase 2: Connect to sysrepo (rebuilds SHM from installed YANG modules) */
+ r = sr_connect(0, &conn);
+ if (r != SR_ERR_OK) {
+ ERROR("Failed to connect: %s", sr_strerror(r));
+ goto cleanup;
+ }
+
+ /* 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 */
+ 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) {
+ ERROR("sr_install_factory_config failed: %s", sr_strerror(r));
+ conout(1, "\n");
+ goto cleanup;
+ }
+ conout(0, "\n");
+
+ /* Phase 5: Start running-datastore session */
+ r = sr_session_start(conn, SR_DS_RUNNING, &sess);
+ if (r != SR_ERR_OK) {
+ ERROR("Failed to start new session: %s", sr_strerror(r));
+ goto cleanup;
+ }
+
+ /* 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
+ * what the plugin callbacks expect. */
+ r = sr_replace_config(sess, NULL, NULL, timeout_ms);
+ if (r != SR_ERR_OK) {
+ ERROR("Failed to clear running datastore: %s", sr_strerror(r));
+ goto cleanup;
+ }
+
+ /* Enable test-mode YANG feature if needed */
+ maybe_enable_test_mode();
+
+ /* 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) {
+ ERROR("Plugin \"%s\" initialization failed (%s).", plugins[i].name, sr_strerror(r));
+ if (fatal_fail) {
+ conout(1, "\n");
+ goto cleanup;
+ }
+ } else {
+ NOTE("Plugin \"%s\" initialized.", plugins[i].name);
+ plugins[i].initialized = 1;
+ }
+ }
+ 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: Fork event pump process for bootstrap.
+ * With SR_SUBSCR_NO_THREAD, sr_replace_config() blocks waiting
+ * 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
+ * sr_replace_config() will trigger their change 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)) {
+ 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 */
+ kill(pump_pid, SIGTERM);
+ waitpid(pump_pid, NULL, 0);
+
+ /* 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");
+
+ /* Phase 12: Steady-state — libev event loop */
+ {
+ 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);
+ 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;
+
+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);
+
+ sr_disconnect(conn);
+ return rc;
+}
diff --git a/src/confd/src/services.c b/src/confd/src/services.c
index 97e126997..173238e6c 100644
--- a/src/confd/src/services.c
+++ b/src/confd/src/services.c
@@ -19,9 +19,14 @@
#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"
#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 +73,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 +89,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 +245,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 +358,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");
@@ -485,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;
@@ -498,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
diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py
index e156174c4..f7097c078 100644
--- a/src/statd/python/yanger/__main__.py
+++ b/src/statd/python/yanger/__main__.py
@@ -1,100 +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':
- from . import infix_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()
- 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)
+ 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
diff --git a/test/case/interfaces/iface_phys_address/test.py b/test/case/interfaces/iface_phys_address/test.py
index c6a7d27a6..0a11a82c7 100755
--- a/test/case/interfaces/iface_phys_address/test.py
+++ b/test/case/interfaces/iface_phys_address/test.py
@@ -35,15 +35,14 @@ def reset_mac(tgt, port):
with infamy.Test() as test:
- CMD = "jq -r '.[\"mac-address\"]' /run/system.json"
-
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
- tgtssh = env.attach("target", "mgmt", "ssh")
_, tport = env.ltop.xlate("target", "data")
pmac = iface.get_phys_address(target, tport)
- cmac = tgtssh.runsh(CMD).stdout.strip()
+ data = target.get_data("/ietf-hardware:hardware/component[name='mainboard']")
+ cmac = data.get("hardware", {}).get("component", {}) \
+ .get("mainboard", {}).get("infix-hardware:phys-address", "")
STATIC = "02:01:00:c0:ff:ee"
OFFSET = "00:00:00:00:ff:aa"
@@ -88,9 +87,7 @@ def reset_mac(tgt, port):
target.put_config_dict("ietf-interfaces", config)
with test.step("Verify target:data has chassis MAC"):
- mac = iface.get_phys_address(target, tport)
- print(f"Current MAC: {mac}, should be: {cmac}")
- assert mac == cmac
+ until(lambda: iface.get_phys_address(target, tport) == cmac)
with test.step("Set target:data to chassis MAC + offset"):
print(f"Setting chassis MAC {cmac} + offset {OFFSET}")
@@ -109,10 +106,8 @@ def reset_mac(tgt, port):
target.put_config_dict("ietf-interfaces", config)
with test.step("Verify target:data has chassis MAC + offset"):
- mac = iface.get_phys_address(target, tport)
BMAC = calc_mac(cmac, OFFSET)
- print(f"Current MAC: {mac}, should be: {BMAC} (calculated)")
- assert mac == BMAC
+ until(lambda: iface.get_phys_address(target, tport) == BMAC)
with test.step("Reset target:data MAC address to default"):
reset_mac(target, tport)
diff --git a/test/infamy/tap.py b/test/infamy/tap.py
index b19383265..42b7e10b0 100644
--- a/test/infamy/tap.py
+++ b/test/infamy/tap.py
@@ -56,6 +56,8 @@ def __exit__(self, t, e, tb):
elif len(e.args) and type(e.args[0]) is subprocess.CompletedProcess:
print("Failing subprocess stdout:\n", e.args[0].stdout)
+ self.out.write(f"1..{self.steps}\n")
+ self.out.flush()
raise SystemExit(1)
@contextlib.contextmanager