" }
+ ]
+ }
+ }
+ ]
+ }
+}
+```
+
+> [!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/doc/test-arch.md b/doc/test-arch.md
index 7b1f41974..dd8437b92 100644
--- a/doc/test-arch.md
+++ b/doc/test-arch.md
@@ -243,20 +243,59 @@ spanning tree matches the expected one.
Integration to Infix
--------------------
-When the test environment is started with Qeneth, it doesn't use the
-base image directly. Instead, it creates a copy and inserts a `test-mode`
-flag into it. During the bootstrap phase, the system checks for the
-presence of the test-mode flag (file).
-
-If the flag exists, a 'test-config.cfg' file is generated. In the
-following step, the system loads the 'test-config' instead of the
-standard `startup-config` (or `factory-config`). This configuration
-is simple and safe, equivalent to the one used in 'Secure Mode'
-(also known as 'failure-config').
-
-Additionally, the configuration enables extra RPCs related to system
-restart and configuration overrides, allowing tests to be run even on
-systems where the factory configuration may potentially create L2 loops.
+
+### Test Mode
+
+Infix supports a *test mode* that makes devices safe and predictable to
+test against. When active, each boot starts from a known-good
+`test-config` (equivalent to the failure/secure config) instead of the
+stored startup or factory configuration. This prevents any persistent
+configuration from interfering with tests and avoids L2 loops that a
+factory config might create. It also enables two extra RPCs used by
+Infamy:
+
+- `infix-test:test/reset` — reload the `test-config` into the running
+ datastore (called by `env.attach()` at the start of each test)
+- `infix-test:test/override-startup` — boot from the startup datastore
+ once, instead of the `test-config` (used by tests that verify
+ save-and-reboot behaviour)
+
+Test mode is activated by the presence of an empty file named
+`test-mode` on the device's `aux` partition (`/mnt/aux/test-mode`).
+
+### Test Mode on Virtual Devices
+
+When the test environment is started with Qeneth, the `inject-test-mode`
+script (`test/inject-test-mode`) is run automatically. It copies the
+base disk image and writes the `test-mode` marker into the `aux`
+partition of that copy, which is then used as the Qeneth backing image.
+
+### Test Mode on Physical Devices
+
+For a physical device, log in as `root` (or `admin`) and create the
+marker file on the `aux` partition, then reboot:
+
+```
+# touch /mnt/aux/test-mode
+# reboot
+```
+
+Alternatively, if you want to prepare the disk image before flashing,
+use the `inject-test-mode` script:
+
+```
+$ test/inject-test-mode -b infix-.img -o infix--test.img
+```
+
+Flash `infix--test.img` to the device instead of the plain
+image. The device will boot into test mode on first power-on.
+
+> [!NOTE]
+> Test mode persists across reboots until the marker file is removed
+> (`rm /mnt/aux/test-mode`). A device in test mode always starts from
+> the `test-config`, so any configuration changes are lost on reboot
+> unless `startup_override()` is called first (or the marker is removed
+> and a normal startup config is saved).
[9PM]: https://github.com/rical/9pm
[Qeneth]: https://github.com/wkz/qeneth
diff --git a/doc/testing.md b/doc/testing.md
index 9a71917fe..eb3d14760 100644
--- a/doc/testing.md
+++ b/doc/testing.md
@@ -37,7 +37,7 @@ $ make TEST_MODE=host test
...
```
-This typically used when testing on physical hardware. By default the
+This is typically used when testing on physical hardware. By default the
topology will be sourced from `/etc/infamy.dot`, but this can be
overwritten by setting the `TOPOLOGY` variable:
@@ -46,6 +46,12 @@ $ make TEST_MODE=host TOPOLOGY=~/my-topology.dot test
...
```
+> [!IMPORTANT]
+> Physical devices must be put into *test mode* before running tests.
+> See [Test Mode](test-arch.md#test-mode) in the architecture document
+> for details. The short version: log in to the device and run
+> `touch /mnt/aux/test-mode && reboot`.
+
### `make run` Devices
Some tests only require a single DUT. These can therefore be run
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 e01756ca6..ccb50d2b0 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
@@ -42,7 +42,7 @@ define CONFD_INSTALL_EXTRA
done
cp $(CONFD_PKGDIR)/tmpfiles.conf $(TARGET_DIR)/etc/tmpfiles.d/confd.conf
mkdir -p $(TARGET_DIR)/etc/avahi/services
- cp $(CONFD_PKGDIR)/avahi.service $(TARGET_DIR)/etc/avahi/services/netconf.service
+ cp $(CONFD_PKGDIR)/netconf.service $(TARGET_DIR)/etc/avahi/services/
endef
NETOPEER2_SEARCHPATH=$(TARGET_DIR)/usr/share/yang/modules/netopeer2/
diff --git a/package/confd/avahi.service b/package/confd/netconf.service
similarity index 100%
rename from package/confd/avahi.service
rename to package/confd/netconf.service
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/finit/0001-Remove-redundant-global-path-var-and-fix-memory-corr.patch b/package/finit/0001-Remove-redundant-global-path-var-and-fix-memory-corr.patch
new file mode 100644
index 000000000..e10e66557
--- /dev/null
+++ b/package/finit/0001-Remove-redundant-global-path-var-and-fix-memory-corr.patch
@@ -0,0 +1,54 @@
+From 184c079c08387d1b0f74de25b63c56304d2156d0 Mon Sep 17 00:00:00 2001
+From: bazub
+Date: Tue, 3 Mar 2026 20:31:33 +0000
+Subject: [PATCH 1/3] Remove redundant global path var and fix memory
+ corruption
+Organization: Wires
+
+Signed-off-by: Joachim Wiberg
+---
+ src/conf.c | 13 ++-----------
+ 1 file changed, 2 insertions(+), 11 deletions(-)
+
+diff --git a/src/conf.c b/src/conf.c
+index d0abb61d..6e8ec834 100644
+--- a/src/conf.c
++++ b/src/conf.c
+@@ -121,9 +121,6 @@ static uev_t etcw;
+
+ static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list);
+
+-static char *path;
+-static char *shell;
+-
+ static int parse_conf(char *file, int is_rcsd);
+ static void drop_changes(void);
+
+@@ -377,8 +374,6 @@ void conf_parse_cmdline(int argc, char *argv[])
+ fstab = strdup(ptr);
+ finit_conf = strdup(FINIT_CONF);
+ finit_rcsd = strdup(FINIT_RCSD);
+- path = getenv("PATH");
+- shell = getenv("SHELL");
+
+ for (int i = 1; i < argc; i++)
+ parse_arg(argv[i]);
+@@ -404,13 +399,9 @@ void conf_reset_env(void)
+ free(node);
+ }
+
+- if (path)
+- setenv("PATH", path, 1);
+- else
++ if (!getenv("PATH"))
+ setenv("PATH", _PATH_STDPATH, 1);
+- if (shell)
+- setenv("SHELL", shell, 1);
+- else
++ if (!getenv("SHELL"))
+ setenv("SHELL", _PATH_BSHELL, 1);
+ setenv("LOGNAME", "root", 1);
+ setenv("USER", "root", 1);
+--
+2.43.0
+
diff --git a/package/finit/0002-Use-explicit-plugin-names-to-prevent-subtle-macro-pr.patch b/package/finit/0002-Use-explicit-plugin-names-to-prevent-subtle-macro-pr.patch
new file mode 100644
index 000000000..1c9bdb721
--- /dev/null
+++ b/package/finit/0002-Use-explicit-plugin-names-to-prevent-subtle-macro-pr.patch
@@ -0,0 +1,318 @@
+From 662293e194811213b9b387163dfe8499e3300ccf Mon Sep 17 00:00:00 2001
+From: bazub
+Date: Fri, 6 Mar 2026 20:36:23 +0000
+Subject: [PATCH 2/3] Use explicit plugin names to prevent subtle macro
+ processing bugs
+Organization: Wires
+
+Signed-off-by: Joachim Wiberg
+---
+ plugins/alsa-utils.c | 6 +++---
+ plugins/bootmisc.c | 2 +-
+ plugins/dbus.c | 4 ++--
+ plugins/hook-scripts.c | 2 +-
+ plugins/modprobe.c | 2 +-
+ plugins/modules-load.c | 2 +-
+ plugins/netlink.c | 2 +-
+ plugins/pidfile.c | 2 +-
+ plugins/procps.c | 4 ++--
+ plugins/resolvconf.c | 2 +-
+ plugins/rtc.c | 6 +++---
+ plugins/sys.c | 2 +-
+ plugins/tty.c | 1 +
+ plugins/urandom.c | 6 +++---
+ plugins/usr.c | 2 +-
+ plugins/x11-common.c | 4 ++--
+ 16 files changed, 25 insertions(+), 24 deletions(-)
+
+diff --git a/plugins/alsa-utils.c b/plugins/alsa-utils.c
+index 6b2c3603..a8a967b0 100644
+--- a/plugins/alsa-utils.c
++++ b/plugins/alsa-utils.c
+@@ -38,7 +38,7 @@
+ static void save(void *arg)
+ {
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "alsa-utils");
+ return;
+ }
+
+@@ -51,7 +51,7 @@ static void save(void *arg)
+ static void restore(void *arg)
+ {
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "alsa-utils");
+ return;
+ }
+
+@@ -62,7 +62,7 @@ static void restore(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "alsa-utils",
+ .hook[HOOK_BASEFS_UP] = { .cb = restore },
+ .hook[HOOK_SHUTDOWN] = { .cb = save }
+ };
+diff --git a/plugins/bootmisc.c b/plugins/bootmisc.c
+index 701f73f0..a8ba3808 100644
+--- a/plugins/bootmisc.c
++++ b/plugins/bootmisc.c
+@@ -172,7 +172,7 @@ static void setup(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "bootmisc",
+ .hook[HOOK_MOUNT_POST] = { .cb = clean },
+ .hook[HOOK_BASEFS_UP] = { .cb = setup },
+ .depends = { "pidfile" },
+diff --git a/plugins/dbus.c b/plugins/dbus.c
+index bbf55bc2..a8a155a1 100644
+--- a/plugins/dbus.c
++++ b/plugins/dbus.c
+@@ -106,7 +106,7 @@ static void setup(void *arg)
+ char *cmd;
+
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "dbus");
+ return;
+ }
+
+@@ -164,7 +164,7 @@ static void setup(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "dbus",
+ .hook[HOOK_SVC_PLUGIN] = { .cb = setup },
+ };
+
+diff --git a/plugins/hook-scripts.c b/plugins/hook-scripts.c
+index 9aa78173..e75808c5 100644
+--- a/plugins/hook-scripts.c
++++ b/plugins/hook-scripts.c
+@@ -79,7 +79,7 @@ static void hscript_shutdown(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "hook-scripts",
+ .hook[HOOK_BANNER] = { .cb = hscript_banner },
+ .hook[HOOK_ROOTFS_UP] = { .cb = hscript_rootfs_up },
+ .hook[HOOK_MOUNT_ERROR] = { .cb = hscript_mount_error },
+diff --git a/plugins/modprobe.c b/plugins/modprobe.c
+index b6b7e7bb..e52bf228 100644
+--- a/plugins/modprobe.c
++++ b/plugins/modprobe.c
+@@ -228,7 +228,7 @@ static void coldplug(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "modprobe",
+ .hook[HOOK_BASEFS_UP] = { .cb = coldplug },
+ .depends = { "bootmisc", }
+ };
+diff --git a/plugins/modules-load.c b/plugins/modules-load.c
+index 53aef82f..e2e45c72 100644
+--- a/plugins/modules-load.c
++++ b/plugins/modules-load.c
+@@ -213,7 +213,7 @@ static void load(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "modules-load",
+ .hook[HOOK_SVC_PLUGIN] = { .cb = load },
+ };
+
+diff --git a/plugins/netlink.c b/plugins/netlink.c
+index a70b01e8..625cbd1b 100644
+--- a/plugins/netlink.c
++++ b/plugins/netlink.c
+@@ -426,7 +426,7 @@ static void nl_enumerate(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "netlink",
+ .hook[HOOK_SVC_RECONF] = { .cb = nl_reconf },
+ .hook[HOOK_SVC_PLUGIN] = { .cb = nl_enumerate },
+ .io = {
+diff --git a/plugins/pidfile.c b/plugins/pidfile.c
+index ae0fdeea..f1232ab6 100644
+--- a/plugins/pidfile.c
++++ b/plugins/pidfile.c
+@@ -335,7 +335,7 @@ static void pidfile_init(void *arg)
+ * SIGSTP:ed (in state PAUSED) waiting for .
+ */
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "pidfile",
+ .hook[HOOK_BASEFS_UP] = { .cb = pidfile_init },
+ .hook[HOOK_SVC_RECONF] = { .cb = pidfile_reconf },
+ .depends = { "netlink" }, /* bootmisc depends on us */
+diff --git a/plugins/procps.c b/plugins/procps.c
+index 826e38ad..5a178d75 100644
+--- a/plugins/procps.c
++++ b/plugins/procps.c
+@@ -43,7 +43,7 @@ static void setup(void *arg)
+ glob_t gl;
+
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "procps");
+ return;
+ }
+
+@@ -69,7 +69,7 @@ static void setup(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "procps",
+ .hook[HOOK_BASEFS_UP] = {
+ .cb = setup
+ },
+diff --git a/plugins/resolvconf.c b/plugins/resolvconf.c
+index 1ac29dae..b98e9214 100644
+--- a/plugins/resolvconf.c
++++ b/plugins/resolvconf.c
+@@ -51,7 +51,7 @@ static void setup(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "resolvconf",
+ .hook[HOOK_BASEFS_UP] = {
+ .cb = setup
+ },
+diff --git a/plugins/rtc.c b/plugins/rtc.c
+index faf69415..cf734763 100644
+--- a/plugins/rtc.c
++++ b/plugins/rtc.c
+@@ -236,7 +236,7 @@ static void rtc_save(void *arg)
+ int fd, rc = 0;
+
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "rtc");
+ return;
+ }
+
+@@ -266,7 +266,7 @@ static void rtc_restore(void *arg)
+ int fd, rc = 0;
+
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "rtc");
+ return;
+ }
+
+@@ -321,7 +321,7 @@ static void update(uev_t *w, void *arg, int events)
+
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "rtc",
+ .hook[HOOK_BASEFS_UP] = {
+ .cb = rtc_restore
+ },
+diff --git a/plugins/sys.c b/plugins/sys.c
+index 438fdfd5..484adc05 100644
+--- a/plugins/sys.c
++++ b/plugins/sys.c
+@@ -180,7 +180,7 @@ static void sys_init(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "sys",
+ .hook[HOOK_BASEFS_UP] = { .cb = sys_init },
+ .depends = { "bootmisc", },
+ };
+diff --git a/plugins/tty.c b/plugins/tty.c
+index b3255f9f..c6a750b0 100644
+--- a/plugins/tty.c
++++ b/plugins/tty.c
+@@ -43,6 +43,7 @@
+ static void tty_watcher(void *arg, int fd, int events);
+
+ static plugin_t plugin = {
++ .name = "tty",
+ .io = {
+ .cb = tty_watcher,
+ .flags = PLUGIN_IO_READ,
+diff --git a/plugins/urandom.c b/plugins/urandom.c
+index 410d0660..9bb6252b 100644
+--- a/plugins/urandom.c
++++ b/plugins/urandom.c
+@@ -87,7 +87,7 @@ static void setup(void *arg)
+ int fd, err;
+
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "urandom");
+ return;
+ }
+
+@@ -188,7 +188,7 @@ static void save(void *arg)
+ mode_t prev;
+
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "urandom");
+ return;
+ }
+
+@@ -202,7 +202,7 @@ static void save(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "urandom",
+ .hook[HOOK_BASEFS_UP] = { .cb = setup },
+ .hook[HOOK_SHUTDOWN] = { .cb = save },
+ .depends = { "bootmisc", }
+diff --git a/plugins/usr.c b/plugins/usr.c
+index d30d9440..496facc2 100644
+--- a/plugins/usr.c
++++ b/plugins/usr.c
+@@ -104,7 +104,7 @@ static void usr_init(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "usr",
+ .hook[HOOK_BASEFS_UP] = { .cb = usr_init },
+ .depends = { "bootmisc", },
+ };
+diff --git a/plugins/x11-common.c b/plugins/x11-common.c
+index f65331a0..75825a36 100644
+--- a/plugins/x11-common.c
++++ b/plugins/x11-common.c
+@@ -39,7 +39,7 @@
+ static void setup(void *arg)
+ {
+ if (rescue) {
+- dbg("Skipping %s plugin in rescue mode.", __FILE__);
++ dbg("Skipping %s plugin in rescue mode.", "x11-common");
+ return;
+ }
+
+@@ -48,7 +48,7 @@ static void setup(void *arg)
+ }
+
+ static plugin_t plugin = {
+- .name = __FILE__,
++ .name = "x11-common",
+ .hook[HOOK_SVC_PLUGIN] = { .cb = setup },
+ };
+
+--
+2.43.0
+
diff --git a/package/finit/0003-service-clear-condition-before-stopping-rdeps-on-rel.patch b/package/finit/0003-service-clear-condition-before-stopping-rdeps-on-rel.patch
new file mode 100644
index 000000000..0925b673c
--- /dev/null
+++ b/package/finit/0003-service-clear-condition-before-stopping-rdeps-on-rel.patch
@@ -0,0 +1,66 @@
+From c01faef99b7e4ff9c39f29ad5648db61a4742539 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 19 Mar 2026 06:37:50 +0100
+Subject: [PATCH 3/3] service: clear condition before stopping rdeps on reload
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
+
+When a service without SIGHUP reload support (noreload) is touched and
+'initctl reload' is called, service_update_rdeps() correctly identifies
+its reverse dependencies but only marks them dirty. It does not clear
+the service's condition, so when service_step_all() runs:
+
+ - rdeps supporting SIGHUP hit the sm_in_reload() guard and break early,
+ left running while their dependency is being killed.
+ - rdeps without SIGHUP support may receive SIGTERM too late, after the
+ dependency has already died and broken their connection, causing them
+ to exit from RUNNING state and have their restart counter incremented.
+
+Fix by calling cond_clear() on the service's condition immediately in
+service_update_rdeps(), before service_step_all() runs. cond_clear()
+calls cond_update() which calls service_step() inline on all affected
+services, which see COND_OFF and transition to STOPPING_STATE — all
+before SIGTERM is ever sent to the dependency itself.
+
+This mirrors the pattern already used in api.c:do_reload() for direct
+'initctl reload ' calls.
+
+Fixes: avahi/mdns stop causing mdns-alias restart counter increment
+
+Signed-off-by: Joachim Wiberg
+---
+ src/service.c | 16 +++++++++++++++-
+ 1 file changed, 15 insertions(+), 1 deletion(-)
+
+diff --git a/src/service.c b/src/service.c
+index b2c6c15b..016b5d2f 100644
+--- a/src/service.c
++++ b/src/service.c
+@@ -2418,7 +2418,21 @@ void service_update_rdeps(void)
+ if (!svc_is_noreload(svc))
+ continue; /* Yup, no need to stop start rdeps */
+
+- svc_mark_affected(mkcond(svc, cond, sizeof(cond)));
++ /*
++ * Clear the condition immediately, before service_step_all()
++ * runs. cond_clear() calls cond_update() which calls
++ * service_step() on all affected services right now. Those
++ * services see COND_OFF and get service_stop() called,
++ * transitioning to STOPPING_STATE before we ever send SIGTERM
++ * to this service. Without this, the condition is only cleared
++ * after the service dies, by which time reverse-dependencies
++ * may have already crashed due to the lost connection.
++ * See also: api.c do_reload() which does the same for direct
++ * 'initctl reload ' calls.
++ */
++ mkcond(svc, cond, sizeof(cond));
++ cond_clear(cond);
++ svc_mark_affected(cond);
+ }
+ }
+
+--
+2.43.0
+
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 298317cb3..241edc618 100644
--- a/package/mdns-alias/mdns-alias.svc
+++ b/package/mdns-alias/mdns-alias.svc
@@ -1,5 +1,5 @@
# Avahi advertises the system default hostname, this service advertises
# /etc/hostname (-H) and, optionally, network.local as CNAMEs. Changes
# to /etc/default/mdns-alias will cause Finit to restart not reload.
-service env:-/etc/default/mdns-alias \
+service env:-/etc/default/mdns-alias \
[2345] mdns-alias -H $MDNS_ALIAS_ARGS --
diff --git a/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch b/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch
index 42e9504b7..36b374b38 100644
--- a/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch
+++ b/package/rousette/0001-Log-HTTP-headers-and-the-input-data-payload.patch
@@ -1,7 +1,7 @@
-From a4136d889237dadb9253ea7eb668a525dd779e6d Mon Sep 17 00:00:00 2001
+From 65a99da7e857d1f6fd111d0428622563fd895810 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Thu, 12 Jun 2025 10:33:42 +0100
-Subject: [PATCH 01/38] Log HTTP headers and the input data payload
+Subject: [PATCH 01/42] Log HTTP headers and the input data payload
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -12,6 +12,7 @@ Hažlinský just found.
Change-Id: I2a930a02c7d30c051390fe73e6af9849edd580b4
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch b/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch
index 4aa9089cd..7f1aa37b3 100644
--- a/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch
+++ b/package/rousette/0002-restconf-prevent-throwing-exception-in-withRestconfE.patch
@@ -1,7 +1,7 @@
-From 6c5b482ea5c9fbc1149a0864b05d1bb1fa7100bf Mon Sep 17 00:00:00 2001
+From 729bb0619d7cb14ca3bf97f56d3c6550bd2e75f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Fri, 13 Jun 2025 10:47:55 +0200
-Subject: [PATCH 02/38] restconf: prevent throwing exception in
+Subject: [PATCH 02/42] restconf: prevent throwing exception in
withRestconfExceptions wrapper
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -23,6 +23,7 @@ handle such situations.
Bug: https://github.com/CESNET/rousette/issues/19
Change-Id: Ifbd74b9bdc0ca66c4e5449a7673ef2f12ae9215e
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 2 +-
tests/restconf-plain-patch.cpp | 24 ++++++++++++++++++++++++
diff --git a/package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch b/package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch
index 272c73c03..b523926df 100644
--- a/package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch
+++ b/package/rousette/0003-CI-switch-to-the-new-cloud-s-Swift-URL.patch
@@ -1,7 +1,7 @@
-From 41c9d9cab47a88ee6c70ab8009b789226c0982fe Mon Sep 17 00:00:00 2001
+From f548df0c693ffd1625fbcb843c6f914972315179 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Tue, 17 Jun 2025 12:46:27 +0200
-Subject: [PATCH 03/38] CI: switch to the new cloud's Swift URL
+Subject: [PATCH 03/42] CI: switch to the new cloud's Swift URL
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -9,6 +9,7 @@ Organization: Wires
Change-Id: I69f8351394262a2a9b691422592741bfb40a8e38
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
ci/build.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch b/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch
index 6086d3114..a43db45c7 100644
--- a/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch
+++ b/package/rousette/0004-restconf-crash-instead-of-a-deadlock-when-the-handle.patch
@@ -1,7 +1,7 @@
-From ec8673126929b6459fcd99c84a79993a725b40e1 Mon Sep 17 00:00:00 2001
+From 8c441795f1050412fc953518917f3d494edd6be1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Fri, 13 Jun 2025 10:47:55 +0200
-Subject: [PATCH 04/38] restconf: crash instead of a deadlock when the handler
+Subject: [PATCH 04/42] restconf: crash instead of a deadlock when the handler
throws
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -38,6 +38,7 @@ Signed-off-by: Jan Kundrát
Bug: https://github.com/CESNET/rousette/issues/19
Change-Id: I2c090b9a76b062101ba422a7d50e8e699779e203
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 28 ++++++++++++++++++++++++++++
src/restconf/Server.h | 4 ++++
diff --git a/package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch b/package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch
index 301a57eca..b4b1a073c 100644
--- a/package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch
+++ b/package/rousette/0005-doc-let-s-stop-calling-this-an-almost-RESTCONF-serve.patch
@@ -1,7 +1,7 @@
-From 37ca95c387d76c3f296a4e44b211772a1ca155ab Mon Sep 17 00:00:00 2001
+From 130a5f43c3cb06e0a97e8eb580f97a526c8c9d8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Wed, 18 Jun 2025 12:01:04 +0200
-Subject: [PATCH 05/38] doc: let's stop calling this "an almost-RESTCONF
+Subject: [PATCH 05/42] doc: let's stop calling this "an almost-RESTCONF
server"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -10,6 +10,7 @@ Organization: Wires
Change-Id: If55ace481c78d838a811ded76a564f8fb59f9233
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch b/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch
index 37ffabacd..72f74c282 100644
--- a/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch
+++ b/package/rousette/0006-close-long-lived-connections-on-SIGTERM.patch
@@ -1,7 +1,7 @@
-From 4eae6200aa812950ebbac1660a1899f4edf41e11 Mon Sep 17 00:00:00 2001
+From 7b8c1d90594dbc65eeb0ea767e3a50b92db8aea3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Wed, 7 Aug 2024 19:07:35 +0200
-Subject: [PATCH 06/38] close long-lived connections on SIGTERM
+Subject: [PATCH 06/42] close long-lived connections on SIGTERM
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -24,6 +24,7 @@ hope it is).
Change-Id: If442134783ba1d699de47c51a9068378f53e8339
Co-authored-by: Tomas Pecka
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 1 +
src/clock.cpp | 7 +--
diff --git a/package/rousette/0007-restconf-refactor-server-stop.patch b/package/rousette/0007-restconf-refactor-server-stop.patch
index 065c2c21e..7ffd4cd33 100644
--- a/package/rousette/0007-restconf-refactor-server-stop.patch
+++ b/package/rousette/0007-restconf-refactor-server-stop.patch
@@ -1,7 +1,7 @@
-From ca2894d4888c673d227fc196a25f83ded20e8f04 Mon Sep 17 00:00:00 2001
+From be28d13811e617d9cdb74bd6d27f4c8634fe4d02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 30 Jun 2025 15:38:02 +0200
-Subject: [PATCH 07/38] restconf: refactor server stop
+Subject: [PATCH 07/42] restconf: refactor server stop
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -21,6 +21,7 @@ does not seem like it should be called from *every* io_service.)
Change-Id: I2f33c38a78dce4081a03326c9a9bb25817fc9d2f
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/package/rousette/0008-tests-use-std-string-starts_with.patch b/package/rousette/0008-tests-use-std-string-starts_with.patch
index a397d617a..a6583d5cc 100644
--- a/package/rousette/0008-tests-use-std-string-starts_with.patch
+++ b/package/rousette/0008-tests-use-std-string-starts_with.patch
@@ -1,7 +1,7 @@
-From 9961430bacdd8dbac64a01b2a2ffb6a4b7e806b6 Mon Sep 17 00:00:00 2001
+From edc6f47fe49d9873f9c7257d66fbf8b362a6c7ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 13 May 2025 13:48:35 +0200
-Subject: [PATCH 08/38] tests: use std::string::starts_with
+Subject: [PATCH 08/42] tests: use std::string::starts_with
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -11,6 +11,7 @@ We are C++20, so we can use line, which is more readable.
Change-Id: I40d4038b421f6bc1fcf320f609b50d5ce7018a45
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
tests/restconf_utils.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch b/package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch
index 5558b3133..1a2ef476b 100644
--- a/package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch
+++ b/package/rousette/0009-tests-add-SSE-event-watcher-for-comments.patch
@@ -1,7 +1,7 @@
-From 746c0cdfef6808f393be7946630b8acbb0636706 Mon Sep 17 00:00:00 2001
+From 8f6ce5d8efd7e5ce4adb6e188dbec14ad86fa0e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 22 Jul 2025 17:47:50 +0200
-Subject: [PATCH 09/38] tests: add SSE event watcher for comments
+Subject: [PATCH 09/42] tests: add SSE event watcher for comments
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -12,6 +12,7 @@ which should be ignored, i.e., lines starting with colon.
Change-Id: If54f0af05b4884aab01325f12fd0a6859791b41b
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
tests/event_watchers.cpp | 7 ++++++-
tests/event_watchers.h | 4 +++-
diff --git a/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch b/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch
index 696b1d1d8..5bcb1f73b 100644
--- a/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch
+++ b/package/rousette/0010-http-send-keep-alive-pings-from-EventStream.patch
@@ -1,7 +1,7 @@
-From 5becffe8a1dd47c8836ce1800a1b72acdf86021f Mon Sep 17 00:00:00 2001
+From fc240be374f70b0f006bb83152364678ffdbfa86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 12 May 2025 14:56:54 +0200
-Subject: [PATCH 10/38] http: send keep-alive pings from EventStream
+Subject: [PATCH 10/42] http: send keep-alive pings from EventStream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -32,6 +32,7 @@ to the server when the server sends these "keep-alive comments".
Change-Id: I57e510d0b61ac7ed032c582779780c64768b7d53
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/clock.cpp | 4 +-
src/http/EventStream.cpp | 44 +++++++++++++--
diff --git a/package/rousette/0011-refactor-event-streams-use-named-constructors.patch b/package/rousette/0011-refactor-event-streams-use-named-constructors.patch
index 35d5fe1ac..c7f09cf86 100644
--- a/package/rousette/0011-refactor-event-streams-use-named-constructors.patch
+++ b/package/rousette/0011-refactor-event-streams-use-named-constructors.patch
@@ -1,7 +1,7 @@
-From ff4ff1c193083feca76d9f0f4485e4b175c373c2 Mon Sep 17 00:00:00 2001
+From 1375ecb15ef534ca2856f46f957686ccc23ab7fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Wed, 23 Jul 2025 14:27:26 +0200
-Subject: [PATCH 11/38] refactor: event streams use named constructors
+Subject: [PATCH 11/42] refactor: event streams use named constructors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -18,6 +18,7 @@ This way, the code is more readable and the intention should be clearer.
Change-Id: Iac96c49c20670dfe924d7c8db33328ed9c2fc9dd
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/clock.cpp | 3 +--
src/http/EventStream.cpp | 20 ++++++++++++++++++++
diff --git a/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch b/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch
index 9c5b51ee6..081678985 100644
--- a/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch
+++ b/package/rousette/0012-refactor-a-better-convention-for-weak_from_this-lock.patch
@@ -1,7 +1,7 @@
-From f4602a03adc9134a9b7a9d338e900b40557da6c4 Mon Sep 17 00:00:00 2001
+From fd0dbd116f249cd8a4d00b677ed2976b19a28b1e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Thu, 7 Aug 2025 12:14:02 +0200
-Subject: [PATCH 12/38] refactor: a better convention for weak_from_this->lock
+Subject: [PATCH 12/42] refactor: a better convention for weak_from_this->lock
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -11,6 +11,7 @@ It is not a "client", so let's stop calling it a "client". My bad.
Change-Id: Id8dc4d92c3ade8d86697366d0102e84bd466f504
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/http/EventStream.cpp | 44 ++++++++++++++++++++--------------------
1 file changed, 22 insertions(+), 22 deletions(-)
diff --git a/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch b/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch
index 4a4380d92..08c360a5a 100644
--- a/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch
+++ b/package/rousette/0013-fix-a-possible-bad_weak_ptr-exception.patch
@@ -1,7 +1,7 @@
-From 4e9b535a59861f25c0602eaa1fc39126d7cd9899 Mon Sep 17 00:00:00 2001
+From cb0c00ceba7ac4b225382ffacef919f2573dc5cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Thu, 7 Aug 2025 12:21:54 +0200
-Subject: [PATCH 13/38] fix a possible bad_weak_ptr exception
+Subject: [PATCH 13/42] fix a possible bad_weak_ptr exception
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -10,6 +10,7 @@ Organization: Wires
Change-Id: I8c7f7a943a1d848f15527988cb76c2a0a10089e6
Fixes: 4eae6200 (close long-lived connections on SIGTERM)
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/http/EventStream.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch b/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch
index 50abb38bb..7f813ffa8 100644
--- a/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch
+++ b/package/rousette/0014-http-add-optional-callbacks-to-EventStream.patch
@@ -1,7 +1,7 @@
-From 6bca750f866b5b14c4d9c3da68e5c8f1e4eee36c Mon Sep 17 00:00:00 2001
+From 91e8e41b5f22629820799b48977a90f57f01f424 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 19 May 2025 12:11:09 +0200
-Subject: [PATCH 14/38] http: add optional callbacks to EventStream
+Subject: [PATCH 14/42] http: add optional callbacks to EventStream
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -15,6 +15,7 @@ disconnects.
Change-Id: Icfc2959e38b812b7c18f45976415209b29151c7b
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/http/EventStream.cpp | 18 +++++++++++++++---
src/http/EventStream.h | 10 ++++++++--
diff --git a/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch b/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch
index 2a151cc5a..b0368622b 100644
--- a/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch
+++ b/package/rousette/0015-restconf-add-internal-RPC-handler-dispatcher.patch
@@ -1,7 +1,7 @@
-From f6ee629de8abef42a24c42b185052b0c8e78bd6b Mon Sep 17 00:00:00 2001
+From 39f43493339984e3a3c256bc818f11aee71375f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 19 May 2025 12:23:16 +0200
-Subject: [PATCH 15/38] restconf: add internal RPC handler dispatcher
+Subject: [PATCH 15/42] restconf: add internal RPC handler dispatcher
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -15,6 +15,7 @@ ietf-subscribed-notifications:establish-subscription RPC is coming soon.
Change-Id: I99121a511011229e4098f95e91601b39d333444a
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 24 ++++++++++++++++++++----
tests/restconf-rpc.cpp | 18 ++++++++++++++++++
diff --git a/package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch b/package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch
index 5b462f46a..c97bd71e8 100644
--- a/package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch
+++ b/package/rousette/0016-tests-processing-incomplete-events-in-SSE-client.patch
@@ -1,7 +1,7 @@
-From b7966613b43b01402c9f0af286a0b3237161779d Mon Sep 17 00:00:00 2001
+From fe57a9ac8024e4115c45d9c256d5048e17fe9fdb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 2 Sep 2025 15:33:43 +0200
-Subject: [PATCH 16/38] tests: processing incomplete events in SSE client
+Subject: [PATCH 16/42] tests: processing incomplete events in SSE client
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -14,6 +14,7 @@ a complete event, once available.
Change-Id: Ied07e69e8b518f20fcc82134a4c041e7ec3a06d6
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
tests/restconf_utils.cpp | 53 +++++++++++++++++++++++-----------------
tests/restconf_utils.h | 3 ++-
diff --git a/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch b/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch
index 5bc8e3450..0500ce7ad 100644
--- a/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch
+++ b/package/rousette/0017-EventStream-fix-possible-heap-use-after-free.patch
@@ -1,7 +1,7 @@
-From 1067e05674633b97d64b428686aff44822230c5f Mon Sep 17 00:00:00 2001
+From 5686bf2fcf351e999043fe3e6e788e1ed502116a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 8 Sep 2025 20:24:15 +0200
-Subject: [PATCH 17/38] EventStream: fix possible heap-use-after-free
+Subject: [PATCH 17/42] EventStream: fix possible heap-use-after-free
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -65,6 +65,7 @@ before actually resuming the response.
Change-Id: Ifdb1f8610cacffca3bb49da17aa9b1d267cdd472
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/http/EventStream.cpp | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch b/package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch
index 620c7af2a..50a434882 100644
--- a/package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch
+++ b/package/rousette/0018-Use-a-working-version-of-nghttp2-asio.patch
@@ -1,7 +1,7 @@
-From fff342945eb3c8deb6f7aa67aca54f9c9f87f3ec Mon Sep 17 00:00:00 2001
+From 07131a39ca9c5a27fe7b1a5c628f3519c2efc0e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Thu, 11 Sep 2025 18:56:31 +0200
-Subject: [PATCH 18/38] Use a working version of nghttp2-asio
+Subject: [PATCH 18/42] Use a working version of nghttp2-asio
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -15,6 +15,7 @@ Link: https://github.com/nghttp2/nghttp2-asio/pull/9
Link: https://github.com/nghttp2/nghttp2-asio/pull/25
Change-Id: Iec9619ce45e7e76f0781d39966d0b6c7cc6fa778
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
.zuul.yaml | 2 +-
README.md | 3 ++-
diff --git a/package/rousette/0019-ping-use-a-monotonic-timer.patch b/package/rousette/0019-ping-use-a-monotonic-timer.patch
index 7f6280436..a527e4786 100644
--- a/package/rousette/0019-ping-use-a-monotonic-timer.patch
+++ b/package/rousette/0019-ping-use-a-monotonic-timer.patch
@@ -1,7 +1,7 @@
-From ffc52b2e454ae4ac4fa1948cb76a9469026d453e Mon Sep 17 00:00:00 2001
+From f71d4467409885078395c8d59c4390ddc1782d33 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Mon, 6 Oct 2025 22:35:27 +0200
-Subject: [PATCH 19/38] ping: use a monotonic timer
+Subject: [PATCH 19/42] ping: use a monotonic timer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -16,6 +16,7 @@ deprecated, use the other function.
Change-Id: I40383721ecb0f12bfcb3f638124a32150a29bf48
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/http/EventStream.cpp | 2 +-
src/http/EventStream.h | 4 ++--
diff --git a/package/rousette/0020-tests-use-a-monotonic-timer.patch b/package/rousette/0020-tests-use-a-monotonic-timer.patch
index 9b7f7d69e..a017ea1ed 100644
--- a/package/rousette/0020-tests-use-a-monotonic-timer.patch
+++ b/package/rousette/0020-tests-use-a-monotonic-timer.patch
@@ -1,7 +1,7 @@
-From 4493316404f31354ea18bc92a1706a3834ad972b Mon Sep 17 00:00:00 2001
+From f842f027aa462486c1cc5cb9c47e0398b3242fe7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Mon, 6 Oct 2025 22:50:11 +0200
-Subject: [PATCH 20/38] tests: use a monotonic timer
+Subject: [PATCH 20/42] tests: use a monotonic timer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -9,6 +9,7 @@ Organization: Wires
Change-Id: I89cdd08082b025643dac81788ca174d3c2177089
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
tests/restconf-eventstream.cpp | 2 +-
tests/restconf_utils.cpp | 6 +++---
diff --git a/package/rousette/0021-port-to-libyang-v4.patch b/package/rousette/0021-port-to-libyang-v4.patch
index 34db989eb..72863a6d0 100644
--- a/package/rousette/0021-port-to-libyang-v4.patch
+++ b/package/rousette/0021-port-to-libyang-v4.patch
@@ -1,7 +1,7 @@
-From 8077a63ac97529d56782d2315f23c44f6036b85f Mon Sep 17 00:00:00 2001
+From 462dca24ef3cb3ae8bb5c6f7a349b94308281f9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Thu, 23 Oct 2025 18:17:37 +0200
-Subject: [PATCH 21/38] port to libyang v4
+Subject: [PATCH 21/42] port to libyang v4
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -41,6 +41,7 @@ Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/8990
Bug: https://github.com/CESNET/libyang/pull/2448
Depends-on: https://github.com/sysrepo/sysrepo/commit/6dc5641762962b93d54a54443e2fd43aa319a7a6
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 6 +++---
src/restconf/Server.cpp | 2 +-
diff --git a/package/rousette/0022-restconf-prevent-lock-order-inversion.patch b/package/rousette/0022-restconf-prevent-lock-order-inversion.patch
index 1e05a8c19..0ee4488bf 100644
--- a/package/rousette/0022-restconf-prevent-lock-order-inversion.patch
+++ b/package/rousette/0022-restconf-prevent-lock-order-inversion.patch
@@ -1,7 +1,7 @@
-From b4bf655502d949a5f351e6acd313f47eecefb062 Mon Sep 17 00:00:00 2001
+From 4b08c23436972e80760abe397806867a1d4d486c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 13 Oct 2025 12:26:46 +0200
-Subject: [PATCH 22/38] restconf: prevent lock order inversion
+Subject: [PATCH 22/42] restconf: prevent lock order inversion
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -29,6 +29,7 @@ callbacks to EventStream"), I am not sure.
See-also: 6bca750f866b5b14c4d9c3da68e5c8f1e4eee36c ("http: add optional callbacks to EventStream")
Change-Id: Ib6626c127ff2eb5feb1d17dece28e70265749df4
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/http/EventStream.cpp | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch b/package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch
index 38ebfb387..ac34988e3 100644
--- a/package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch
+++ b/package/rousette/0023-refactor-do-not-pollute-scope-with-unnecessary-varia.patch
@@ -1,7 +1,7 @@
-From 27cbe6750bdfc9f346421e55ebe555b8c036d77c Mon Sep 17 00:00:00 2001
+From 24a35b1f1424826202ecaebf7aa04dccdf6ad2f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 11 Nov 2025 15:01:21 +0100
-Subject: [PATCH 23/38] refactor: do not pollute scope with unnecessary
+Subject: [PATCH 23/42] refactor: do not pollute scope with unnecessary
variables
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -13,6 +13,7 @@ nothing.
Change-Id: I5b9b1a077d44d088084c3b2b93359ab7464ec8aa
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch b/package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch
index f2fcc9f5d..8ab3744b9 100644
--- a/package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch
+++ b/package/rousette/0024-restconf-validate-input-for-internal-rpcs.patch
@@ -1,7 +1,7 @@
-From 5592ae49cd7607fdcb2c9e554a521b53545e761b Mon Sep 17 00:00:00 2001
+From 228aa9ea848d437565b5f90972a0c4b23b279337 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 21 Oct 2025 17:23:44 +0200
-Subject: [PATCH 24/38] restconf: validate input for internal rpcs
+Subject: [PATCH 24/42] restconf: validate input for internal rpcs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -23,6 +23,7 @@ the RPC.
Change-Id: I55296895e6993b4ca27d21ce2a64ec2d159a35fc
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 36 +++++++++++++++++++++++++++++-------
1 file changed, 29 insertions(+), 7 deletions(-)
diff --git a/package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch b/package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch
index 00a0ba90d..4ff4949a5 100644
--- a/package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch
+++ b/package/rousette/0025-tests-allow-read-from-parts-ietf-subscribed-notifica.patch
@@ -1,7 +1,7 @@
-From c2413237501eea3125fe31a9701919d14402737f Mon Sep 17 00:00:00 2001
+From 163ebff977aff8fea8e342f35314697f89e5bb82 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 11 Nov 2025 17:30:33 +0100
-Subject: [PATCH 25/38] tests: allow read from parts
+Subject: [PATCH 25/42] tests: allow read from parts
ietf-subscribed-notifications for anonymous user
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -16,6 +16,7 @@ access for users.
Change-Id: I50e49c8c60b7e6be47c51f3d8cd546fbb6f49294
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
tests/restconf-reading.cpp | 5 ++++-
tests/restconf_utils.cpp | 4 ++++
diff --git a/package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch b/package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch
index 1231a32e2..9e7757088 100644
--- a/package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch
+++ b/package/rousette/0026-restconf-establish-subscription-RPC-for-subscribed-n.patch
@@ -1,7 +1,7 @@
-From aa04b0a99fb51aed998aa9f1ab0a527cc09e647f Mon Sep 17 00:00:00 2001
+From 0f13a7262ddf58f400a53cfedb038344abb929f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 19 May 2025 12:11:09 +0200
-Subject: [PATCH 26/38] restconf: establish-subscription RPC for subscribed
+Subject: [PATCH 26/42] restconf: establish-subscription RPC for subscribed
notifications
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -21,6 +21,7 @@ the follow-up patch.
Change-Id: I0217e5abc56cfd73859dbcc610fb1f342dc33a10
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 8 +-
src/restconf/DynamicSubscriptions.cpp | 177 ++++++++++++++
diff --git a/package/rousette/0027-refactor-restconf-stream-request-types.patch b/package/rousette/0027-refactor-restconf-stream-request-types.patch
index bff321f03..8592b20b3 100644
--- a/package/rousette/0027-refactor-restconf-stream-request-types.patch
+++ b/package/rousette/0027-refactor-restconf-stream-request-types.patch
@@ -1,7 +1,7 @@
-From e2c8036130a2265476579ffa9f4354741b54bbd5 Mon Sep 17 00:00:00 2001
+From b2be7b3b43cb95b5bfc1e0b774de7dbac4376809 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 13 Oct 2025 16:10:51 +0200
-Subject: [PATCH 27/38] refactor: restconf stream request types
+Subject: [PATCH 27/42] refactor: restconf stream request types
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -21,6 +21,7 @@ will not have it.
See-also: 97ceef119c900c37bbaa27860c3b43cfa6d69f95 ("restconf: refactor uri parser for stream URIs")
Change-Id: Idc1e05f7ff05cea84b5806f60e217605f75e7a9f
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/Server.cpp | 2 +-
src/restconf/uri.cpp | 15 ++++++++-------
diff --git a/package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch b/package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch
index 23879906f..67774ebf1 100644
--- a/package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch
+++ b/package/rousette/0028-restconf-endpoint-for-subscribed-notifications.patch
@@ -1,7 +1,7 @@
-From b3a55ae0a1ef9e092b0c8f0d7c3dbd7baa708cc8 Mon Sep 17 00:00:00 2001
+From 5f4c781728d888d3e1c8db9c1c78c545caae824c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 29 Jul 2025 11:39:21 +0200
-Subject: [PATCH 28/38] restconf: endpoint for subscribed notifications
+Subject: [PATCH 28/42] restconf: endpoint for subscribed notifications
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -27,6 +27,7 @@ of boost::asio is not enough.
Change-Id: I07dcdd3fb9ef4f05f93ae6dda0e0d71094a0bbfc
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 1 +
src/restconf/DynamicSubscriptions.cpp | 127 ++++++++++++-
diff --git a/package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch b/package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch
index 009ad42bb..4c041aac7 100644
--- a/package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch
+++ b/package/rousette/0029-restconf-dynamic-subscriptions-shutdown-method.patch
@@ -1,7 +1,7 @@
-From 9c39cdd8a8a6bb8fb94d4f943313ec8c4a1823d7 Mon Sep 17 00:00:00 2001
+From dc462f432a2360f4ed9c7b5e2a17051074674f01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 10 Nov 2025 19:58:36 +0100
-Subject: [PATCH 29/38] restconf: dynamic subscriptions shutdown method
+Subject: [PATCH 29/42] restconf: dynamic subscriptions shutdown method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -13,6 +13,7 @@ cannot call its terminate() method.
Change-Id: I4532bbafff7c7386a18ed0636f1102842d2749fa
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/DynamicSubscriptions.cpp | 10 ++++++++++
src/restconf/DynamicSubscriptions.h | 1 +
diff --git a/package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch b/package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch
index 494ed1a13..3b5664337 100644
--- a/package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch
+++ b/package/rousette/0030-restconf-terminate-subsc.-notification-streams-after.patch
@@ -1,7 +1,7 @@
-From 3154d9209dc876fee43d9e83907ab44e06008c65 Mon Sep 17 00:00:00 2001
+From ac1097419badcee117d5c90331db9fa5771bb842 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 14 Oct 2025 12:25:20 +0200
-Subject: [PATCH 30/38] restconf: terminate subsc. notification streams after
+Subject: [PATCH 30/42] restconf: terminate subsc. notification streams after
60s with no client
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -18,6 +18,7 @@ is 60 seconds.
Change-Id: Id399d108069704fa8ab79b9ef6ab855dd62cfc8d
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/DynamicSubscriptions.cpp | 72 ++++++++++++++++++---
src/restconf/DynamicSubscriptions.h | 22 ++++++-
diff --git a/package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch b/package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch
index 49589dd58..c66b04bb3 100644
--- a/package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch
+++ b/package/rousette/0031-restconf-add-kill-delete-subscription-RPCs.patch
@@ -1,7 +1,7 @@
-From 22cfcbf52be604afe0caafc892f4b7befe03b9db Mon Sep 17 00:00:00 2001
+From f8d6053c6777476384d9c32564e5faf355edbafa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 8 Apr 2025 17:00:59 +0200
-Subject: [PATCH 31/38] restconf: add {kill,delete}-subscription RPCs
+Subject: [PATCH 31/42] restconf: add {kill,delete}-subscription RPCs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -21,6 +21,7 @@ disconnects.
Change-Id: I443d6fd88f2797045b5ae0a7d7e1a761fe74e7cb
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/DynamicSubscriptions.cpp | 44 +++++
src/restconf/DynamicSubscriptions.h | 2 +
diff --git a/package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch b/package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch
index 86a6f7c08..6f9de49a5 100644
--- a/package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch
+++ b/package/rousette/0032-restconf-add-subscribed-notifications-filtering.patch
@@ -1,7 +1,7 @@
-From c5558b12ee67280b6281b639c3d8652cec3c7fa6 Mon Sep 17 00:00:00 2001
+From da35875a545a4709da0662bb6ea104dcdba85582 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Wed, 19 Mar 2025 14:39:05 +0100
-Subject: [PATCH 32/38] restconf: add subscribed notifications filtering
+Subject: [PATCH 32/42] restconf: add subscribed notifications filtering
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -12,6 +12,7 @@ filters.
Change-Id: Iee973a6a2d03b4a0d90f952afe5436ce701787ae
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 2 +-
src/restconf/DynamicSubscriptions.cpp | 13 ++++-
diff --git a/package/rousette/0033-cmake-wrap-long-line.patch b/package/rousette/0033-cmake-wrap-long-line.patch
index d4850a268..2e1ef6196 100644
--- a/package/rousette/0033-cmake-wrap-long-line.patch
+++ b/package/rousette/0033-cmake-wrap-long-line.patch
@@ -1,7 +1,7 @@
-From fe91af1eb38888d7eacb36cb5a299bd3163f7c60 Mon Sep 17 00:00:00 2001
+From 36b8ec2f65a456bf97b97ed4b3d74bb323cf85b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 14 Oct 2025 17:22:00 +0200
-Subject: [PATCH 33/38] cmake: wrap long line
+Subject: [PATCH 33/42] cmake: wrap long line
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -9,6 +9,7 @@ Organization: Wires
Change-Id: Id94e18d581dbfbe2e6d386be6a232544f9344eec
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch b/package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch
index c3d211e11..f908d13f2 100644
--- a/package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch
+++ b/package/rousette/0034-restconf-support-replays-in-subscribed-notifications.patch
@@ -1,7 +1,7 @@
-From 817debab72d10f2f30e83f1630bebdcdf3ec9455 Mon Sep 17 00:00:00 2001
+From f633e7ffd39955051d886b7977e8a8ecfffda2b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Mon, 10 Mar 2025 15:33:08 +0100
-Subject: [PATCH 34/38] restconf: support replays in subscribed notifications
+Subject: [PATCH 34/42] restconf: support replays in subscribed notifications
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -9,6 +9,7 @@ Organization: Wires
Change-Id: Idfc3ceb99c8111c5575c1c8b94d539f234fa43be
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 1 +
src/restconf/DynamicSubscriptions.cpp | 24 ++++-
diff --git a/package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch b/package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch
index 42fe80fc8..8f8b8aa32 100644
--- a/package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch
+++ b/package/rousette/0035-restconf-refactor-getting-datastore-from-string.patch
@@ -1,7 +1,7 @@
-From 7d6b1a463fdacefaf7a7057564f6f4d4a8cb4632 Mon Sep 17 00:00:00 2001
+From f58dc52c4e7445a06484bce0bbfeac831004ce02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?=
Date: Tue, 25 Mar 2025 11:06:26 +0100
-Subject: [PATCH 35/38] restconf: refactor getting datastore from string
+Subject: [PATCH 35/42] restconf: refactor getting datastore from string
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -11,6 +11,7 @@ This will be useful when processing YANG push requests.
Change-Id: I1e76335c8d674e81890d6cd1e973adaf521eb205
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
src/restconf/uri.cpp | 19 +++++--------------
src/restconf/utils/sysrepo.cpp | 16 ++++++++++++++++
diff --git a/package/rousette/0036-build-remove-duplicate-line.patch b/package/rousette/0036-build-remove-duplicate-line.patch
index 2b3090e18..da84375fe 100644
--- a/package/rousette/0036-build-remove-duplicate-line.patch
+++ b/package/rousette/0036-build-remove-duplicate-line.patch
@@ -1,7 +1,7 @@
-From c1d5d143020ddc51c3886b82e87d463cf3211fca Mon Sep 17 00:00:00 2001
+From 8a541a83176eb44fbe501a91b7a4ef0576f60ba3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Fri, 21 Nov 2025 10:44:47 +0100
-Subject: [PATCH 36/38] build: remove duplicate line
+Subject: [PATCH 36/42] build: remove duplicate line
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -10,6 +10,7 @@ Organization: Wires
Fixes: 817debab restconf: support replays in subscribed notifications
Change-Id: I4d2ea8429a4cb7b77f8739aff7a26a03cf535756
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/package/rousette/0037-remove-obsolete-targets.patch b/package/rousette/0037-remove-obsolete-targets.patch
index 4320d1523..5ffeca475 100644
--- a/package/rousette/0037-remove-obsolete-targets.patch
+++ b/package/rousette/0037-remove-obsolete-targets.patch
@@ -1,7 +1,7 @@
-From ed62a496744de0e0dc31171167d5d6f69f54af93 Mon Sep 17 00:00:00 2001
+From 538dbbff9604c00fd3875c88d0ed19074dfa0bfb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Fri, 21 Nov 2025 12:54:44 +0100
-Subject: [PATCH 37/38] remove obsolete targets
+Subject: [PATCH 37/42] remove obsolete targets
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -9,6 +9,7 @@ Organization: Wires
Change-Id: Idde217a4ae1bc6f03657d9609e0ec1b0a47a9747
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 9 --------
src/clock.cpp | 62 --------------------------------------------------
diff --git a/package/rousette/0038-port-to-libyang-v4.2.patch b/package/rousette/0038-port-to-libyang-v4.2.patch
index 36494c213..3026a51a6 100644
--- a/package/rousette/0038-port-to-libyang-v4.2.patch
+++ b/package/rousette/0038-port-to-libyang-v4.2.patch
@@ -1,7 +1,7 @@
-From 5a1d0dac28c1a78e90d3906693228584adcf3ab1 Mon Sep 17 00:00:00 2001
+From 6adfafa9324ee0bfd50d0dab7c7657138a4a55a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kundr=C3=A1t?=
Date: Thu, 20 Nov 2025 16:13:43 +0100
-Subject: [PATCH 38/38] port to libyang v4.2
+Subject: [PATCH 38/42] port to libyang v4.2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@@ -10,6 +10,7 @@ Organization: Wires
Depends-on: https://gerrit.cesnet.cz/c/CzechLight/dependencies/+/9012
Change-Id: I1ac6b25a3c1034f06917594732f066fd77d4b26b
Signed-off-by: Mattias Walström
+Signed-off-by: Joachim Wiberg
---
CMakeLists.txt | 2 +-
src/restconf/Server.cpp | 10 +++++-----
diff --git a/package/rousette/0039-Add-log-level-LEVEL-command-line-option.patch b/package/rousette/0039-Add-log-level-LEVEL-command-line-option.patch
new file mode 100644
index 000000000..ba6aa3582
--- /dev/null
+++ b/package/rousette/0039-Add-log-level-LEVEL-command-line-option.patch
@@ -0,0 +1,88 @@
+From c617503c76dcfe75e73c5f1af3dd6c17c2ca3fd4 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Sun, 28 Sep 2025 06:04:12 +0200
+Subject: [PATCH 39/42] Add '--log-level LEVEL' command line option
+Organization: Wires
+
+The default 'trace' log level is quite verbose for production systems.
+This commit changes the default to 'info' and adds a command line option
+to control the log level.
+
+Signed-off-by: Joachim Wiberg
+---
+ src/restconf/main.cpp | 30 ++++++++++++++++++++++++++++--
+ 1 file changed, 28 insertions(+), 2 deletions(-)
+
+diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp
+index b1e2bc6..66c782d 100644
+--- a/src/restconf/main.cpp
++++ b/src/restconf/main.cpp
+@@ -19,6 +19,7 @@
+ #include
+ #include
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -27,11 +28,12 @@
+ static const char usage[] =
+ R"(Rousette - RESTCONF server
+ Usage:
+- rousette [--syslog] [--timeout ] [--help]
++ rousette [--syslog] [--timeout ] [--log-level ] [--help]
+ Options:
+ -h --help Show this screen.
+ -t --timeout Change default timeout in sysrepo (if not set, use sysrepo internal).
+ --syslog Log to syslog.
++ --log-level Set log level (trace, debug, info, warn, error, critical, off) [default: info].
+ )";
+ #ifdef HAVE_SYSTEMD
+
+@@ -76,10 +78,34 @@ int main(int argc, char* argv [])
+ {
+ auto args = docopt::docopt(usage, {argv + 1, argv + argc}, true,""/* version */, true);
+ auto timeout = std::chrono::milliseconds{0};
++ auto logLevel = spdlog::level::info;
+
+ if (args["--timeout"]) {
+ timeout = std::chrono::milliseconds{args["--timeout"].asLong() * 1000};
+ }
++
++ if (args["--log-level"]) {
++ auto levelStr = args["--log-level"].asString();
++ if (levelStr == "trace") {
++ logLevel = spdlog::level::trace;
++ } else if (levelStr == "debug") {
++ logLevel = spdlog::level::debug;
++ } else if (levelStr == "info") {
++ logLevel = spdlog::level::info;
++ } else if (levelStr == "warn" || levelStr == "warning") {
++ logLevel = spdlog::level::warn;
++ } else if (levelStr == "error" || levelStr == "err") {
++ logLevel = spdlog::level::err;
++ } else if (levelStr == "critical") {
++ logLevel = spdlog::level::critical;
++ } else if (levelStr == "off") {
++ logLevel = spdlog::level::off;
++ } else {
++ std::cerr << "Invalid log level: " << levelStr << std::endl;
++ std::cerr << "Valid levels: trace, debug, info, warn, error, critical, off" << std::endl;
++ return 1;
++ }
++ }
+ if (args["--syslog"].asBool()) {
+ auto syslog_sink = std::make_shared("rousette", LOG_PID, LOG_USER, true);
+ auto logger = std::make_shared("rousette", syslog_sink);
+@@ -95,7 +121,7 @@ int main(int argc, char* argv [])
+ auto logger = std::make_shared("rousette", stdout_sink);
+ spdlog::set_default_logger(logger);
+ }
+- spdlog::set_level(spdlog::level::trace);
++ spdlog::set_level(logLevel);
+
+ /* We will parse URIs using boost::spirit's alnum/alpha/... matchers which are locale-dependent.
+ * Let's use something stable no matter what the system is using
+--
+2.43.0
+
diff --git a/package/rousette/0040-sr-lower-log-message-severity-to-debug.patch b/package/rousette/0040-sr-lower-log-message-severity-to-debug.patch
new file mode 100644
index 000000000..97f9a9853
--- /dev/null
+++ b/package/rousette/0040-sr-lower-log-message-severity-to-debug.patch
@@ -0,0 +1,46 @@
+From 3d4262c6c2e334fa6577a69e0483a19f638e43fd Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 19 Mar 2026 17:21:20 +0100
+Subject: [PATCH 40/42] sr: lower log message severity to debug
+Organization: Wires
+
+On non-CzechLight hardware this message fires on every startup. It is
+not a warning condition, and at debug level it won't clutter the log at
+the default info level. Also lower the NACM startup diagnostic from
+info to debug for the same reason.
+
+Signed-off-by: Joachim Wiberg
+---
+ src/auth/Nacm.cpp | 2 +-
+ src/sr/OpticalEvents.cpp | 2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/src/auth/Nacm.cpp b/src/auth/Nacm.cpp
+index 284fa85..a4cc8cc 100644
+--- a/src/auth/Nacm.cpp
++++ b/src/auth/Nacm.cpp
+@@ -98,7 +98,7 @@ Nacm::Nacm(sysrepo::Connection conn)
+ m_srSub.onModuleChange(
+ "ietf-netconf-acm", [&](auto session, auto, auto, auto, auto, auto) {
+ m_anonymousEnabled = validAnonymousNacmRules(session, ANONYMOUS_USER_GROUP);
+- spdlog::info("NACM config validation: Anonymous user access {}", m_anonymousEnabled ? "enabled" : "disabled");
++ spdlog::debug("NACM config validation: Anonymous user access {}", m_anonymousEnabled ? "enabled" : "disabled");
+ return sysrepo::ErrorCode::Ok;
+ },
+ std::nullopt,
+diff --git a/src/sr/OpticalEvents.cpp b/src/sr/OpticalEvents.cpp
+index 3f78ddc..a2cefe7 100644
+--- a/src/sr/OpticalEvents.cpp
++++ b/src/sr/OpticalEvents.cpp
+@@ -47,7 +47,7 @@ OpticalEvents::OpticalEvents(sysrepo::Session session)
+ }
+ }
+
+- spdlog::warn("Telemetry disabled. No CzechLight YANG modules found.");
++ spdlog::debug("Telemetry disabled. No CzechLight YANG modules found.");
+ }
+
+ sysrepo::ErrorCode OpticalEvents::onChange(sysrepo::Session session, const std::string& module)
+--
+2.43.0
+
diff --git a/package/rousette/0041-restconf-add-audit-trail-for-datastore-write-operati.patch b/package/rousette/0041-restconf-add-audit-trail-for-datastore-write-operati.patch
new file mode 100644
index 000000000..c66788fd2
--- /dev/null
+++ b/package/rousette/0041-restconf-add-audit-trail-for-datastore-write-operati.patch
@@ -0,0 +1,134 @@
+From 2877c5a6a139b15e42f99c5aba4127b427154707 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 19 Mar 2026 17:21:25 +0100
+Subject: [PATCH 41/42] restconf: add audit trail for datastore write
+ operations
+Organization: Wires
+
+Log an info message before and after committing changes to sysrepo,
+matching the style of netopeer2's audit trail.
+
+Signed-off-by: Joachim Wiberg
+---
+ src/restconf/Server.cpp | 14 ++++++++++++++
+ src/restconf/utils/sysrepo.cpp | 17 +++++++++++++++++
+ src/restconf/utils/sysrepo.h | 1 +
+ 3 files changed, 32 insertions(+)
+
+diff --git a/src/restconf/Server.cpp b/src/restconf/Server.cpp
+index 3c7c3c7..15eec92 100644
+--- a/src/restconf/Server.cpp
++++ b/src/restconf/Server.cpp
+@@ -17,10 +17,12 @@
+ #include "restconf/Exceptions.h"
+ #include "restconf/NotificationStream.h"
+ #include "auth/Http.h"
++#include "NacmIdentities.h"
+ #include "restconf/Server.h"
+ #include "restconf/YangSchemaLocations.h"
+ #include "restconf/uri.h"
+ #include "restconf/utils/dataformat.h"
++#include "restconf/utils/sysrepo.h"
+ #include "restconf/utils/yang.h"
+ #include "sr/OpticalEvents.h"
+
+@@ -594,8 +596,10 @@ void processPost(std::shared_ptr requestCtx, const std::chrono::
+ createdNodes.begin()->newMeta(*modNetconf, "operation", "create");
+ yangInsert(*requestCtx, *createdNodes.begin());
+
++ spdlog::info("user \"{}\" committing changes to {} ...", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+ requestCtx->sess.editBatch(*edit, sysrepo::DefaultOperation::Merge);
+ requestCtx->sess.applyChanges(timeout);
++ spdlog::info("user \"{}\" committed changes to {}.", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+
+ requestCtx->res.write_head(201,
+ {
+@@ -682,8 +686,10 @@ void processYangPatchImpl(const std::shared_ptr& requestCtx, con
+ }
+
+ if (mergedEdits) {
++ spdlog::info("user \"{}\" committing changes to {} ...", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+ requestCtx->sess.editBatch(*mergedEdits, sysrepo::DefaultOperation::Merge);
+ requestCtx->sess.applyChanges(timeout);
++ spdlog::info("user \"{}\" committed changes to {}.", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+ }
+ }
+
+@@ -726,12 +732,16 @@ void processPutOrPlainPatch(std::shared_ptr requestCtx, const st
+ validateInputMetaAttributes(ctx, *edit);
+
+ if (requestCtx->req.method() == "PUT") {
++ spdlog::info("user \"{}\" committing changes to {} ...", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+ requestCtx->sess.replaceConfig(edit, std::nullopt, timeout);
++ spdlog::info("user \"{}\" committed changes to {}.", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+
+ requestCtx->res.write_head(edit ? 201 : 204, {CORS});
+ } else {
++ spdlog::info("user \"{}\" committing changes to {} ...", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+ requestCtx->sess.editBatch(*edit, sysrepo::DefaultOperation::Merge);
+ requestCtx->sess.applyChanges(timeout);
++ spdlog::info("user \"{}\" committed changes to {}.", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+ requestCtx->res.write_head(204, {CORS});
+ }
+ requestCtx->res.end();
+@@ -763,8 +773,10 @@ void processPutOrPlainPatch(std::shared_ptr requestCtx, const st
+ yangInsert(*requestCtx, *replacementNode);
+ }
+
++ spdlog::info("user \"{}\" committing changes to {} ...", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+ requestCtx->sess.editBatch(*edit, sysrepo::DefaultOperation::Merge);
+ requestCtx->sess.applyChanges(timeout);
++ spdlog::info("user \"{}\" committed changes to {}.", requestCtx->sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(requestCtx->sess.activeDatastore()));
+
+ if (requestCtx->req.method() == "PUT") {
+ requestCtx->res.write_head(nodeExisted ? 204 : 201, {CORS});
+@@ -1219,8 +1231,10 @@ Server::Server(
+ deletedNode->newMeta(*netconf, "operation", "delete");
+ }
+
++ spdlog::info("user \"{}\" committing changes to {} ...", sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(sess.activeDatastore()));
+ sess.editBatch(*edit, sysrepo::DefaultOperation::Merge);
+ sess.applyChanges(timeout);
++ spdlog::info("user \"{}\" committed changes to {}.", sess.getNacmUser().value_or(ANONYMOUS_USER), datastoreToString(sess.activeDatastore()));
+ } catch (const sysrepo::ErrorWithCode& e) {
+ if (e.code() == sysrepo::ErrorCode::Unauthorized) {
+ throw ErrorResponse(403, "application", "access-denied", "Access denied.", restconfRequest.path);
+diff --git a/src/restconf/utils/sysrepo.cpp b/src/restconf/utils/sysrepo.cpp
+index 9f14cc7..d3bcc39 100644
+--- a/src/restconf/utils/sysrepo.cpp
++++ b/src/restconf/utils/sysrepo.cpp
+@@ -38,4 +38,21 @@ sysrepo::Datastore datastoreFromString(const std::string& datastore)
+
+ throw std::runtime_error("Unknown datastore '" + datastore + "'");
+ }
++
++std::string datastoreToString(sysrepo::Datastore datastore)
++{
++ switch (datastore) {
++ case sysrepo::Datastore::Running:
++ return "running";
++ case sysrepo::Datastore::Operational:
++ return "operational";
++ case sysrepo::Datastore::Candidate:
++ return "candidate";
++ case sysrepo::Datastore::Startup:
++ return "startup";
++ case sysrepo::Datastore::FactoryDefault:
++ return "factory-default";
++ }
++ throw std::runtime_error("Unknown datastore");
++}
+ }
+diff --git a/src/restconf/utils/sysrepo.h b/src/restconf/utils/sysrepo.h
+index 0eced9e..b8dd399 100644
+--- a/src/restconf/utils/sysrepo.h
++++ b/src/restconf/utils/sysrepo.h
+@@ -27,4 +27,5 @@ public:
+ };
+
+ sysrepo::Datastore datastoreFromString(const std::string& datastore);
++std::string datastoreToString(sysrepo::Datastore datastore);
+ }
+--
+2.43.0
+
diff --git a/package/rousette/0042-restconf-strip-redundant-fields-from-syslog-journald.patch b/package/rousette/0042-restconf-strip-redundant-fields-from-syslog-journald.patch
new file mode 100644
index 000000000..ffec0c7c0
--- /dev/null
+++ b/package/rousette/0042-restconf-strip-redundant-fields-from-syslog-journald.patch
@@ -0,0 +1,37 @@
+From 87f1984ea8d07d7e55c0ad08e24ba9adeefd6aca Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 19 Mar 2026 17:54:11 +0100
+Subject: [PATCH 42/42] restconf: strip redundant fields from syslog/journald
+ log messages
+Organization: Wires
+
+When logging to syslog or journald, the timestamp, program name, and log
+level are all provided by the logging infrastructure itself. Use a bare
+message-only pattern to avoid the duplication.
+
+Signed-off-by: Joachim Wiberg
+---
+ src/restconf/main.cpp | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/src/restconf/main.cpp b/src/restconf/main.cpp
+index 66c782d..63f8760 100644
+--- a/src/restconf/main.cpp
++++ b/src/restconf/main.cpp
+@@ -110,11 +110,13 @@ int main(int argc, char* argv [])
+ auto syslog_sink = std::make_shared("rousette", LOG_PID, LOG_USER, true);
+ auto logger = std::make_shared("rousette", syslog_sink);
+ spdlog::set_default_logger(logger);
++ spdlog::set_pattern("%v");
+ #ifdef HAVE_SYSTEMD
+ } else if (is_journald_active()) {
+ auto sink = std::make_shared>();
+ auto logger = std::make_shared("rousette", sink);
+ spdlog::set_default_logger(logger);
++ spdlog::set_pattern("%v");
+ #endif
+ } else {
+ auto stdout_sink = std::make_shared();
+--
+2.43.0
+
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf
index 916d85fb2..dfcf76ba5 100644
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/avahi.conf
@@ -1,2 +1,2 @@
-service name:mdns env:-/etc/default/avahi \
+service name:mdns env:-/etc/default/avahi \
[2345] avahi-daemon -s $AVAHI_ARGS -- Avahi mDNS-SD daemon
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/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/libnetconf2/4.1.2/0001-server_config-use-YANG-identity-derivation-for-key-f.patch b/patches/libnetconf2/4.1.2/0001-server_config-use-YANG-identity-derivation-for-key-f.patch
new file mode 100644
index 000000000..30fa8129a
--- /dev/null
+++ b/patches/libnetconf2/4.1.2/0001-server_config-use-YANG-identity-derivation-for-key-f.patch
@@ -0,0 +1,217 @@
+From dfa37a99b335b99e89df1be87b240123178cf1f0 Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 19 Mar 2026 15:15:52 +0100
+Subject: [PATCH 1/2] server_config: use YANG identity derivation for key
+ formats
+Organization: Wires
+
+Replace plain string comparison of public/private key format identities
+with proper derived-from-or-self() semantics. This allows vendor-extended
+identities (e.g. an identity with base ct:subject-public-key-info-format)
+to be silently mapped to the correct internal type without any warning.
+
+Also silence the spurious warning for symmetric-keys: the feature is
+intentionally disabled for NETCONF, but a shared keystore may legitimately
+contain symmetric keys used by other applications, e.g, Wi-Fi passphrases,
+WireGuard PSKs, etc.
+
+Signed-off-by: Joachim Wiberg
+---
+ src/server_config.c | 131 ++++++++++++++++++++++++++++++++++++--------
+ 1 file changed, 107 insertions(+), 24 deletions(-)
+
+diff --git a/src/server_config.c b/src/server_config.c
+index 9f85655..54c470c 100644
+--- a/src/server_config.c
++++ b/src/server_config.c
+@@ -110,6 +110,64 @@ nc_lyd_find_child(const struct lyd_node *node, const char *child, int fail_if_no
+ return 0;
+ }
+
++/**
++ * @brief Recursively check if @p ident is in the derived tree of @p base.
++ *
++ * @param[in] base Base identity to search from.
++ * @param[in] ident Identity to find.
++ * @return 1 if @p ident is @p base or derived from it, 0 otherwise.
++ */
++static int
++nc_ident_derived_or_self(const struct lysc_ident *base, const struct lysc_ident *ident)
++{
++ LY_ARRAY_COUNT_TYPE i;
++
++ if (base == ident)
++ return 1;
++
++ LY_ARRAY_FOR(base->derived, i) {
++ if (nc_ident_derived_or_self(base->derived[i], ident))
++ return 1;
++ }
++
++ return 0;
++}
++
++/**
++ * @brief Check if a YANG identity derives from (or is) a named identity in a given module.
++ *
++ * Implements YANG derived-from-or-self() semantics so that vendor-extended
++ * format identities (e.g. an identity with base ct:subject-public-key-info-format)
++ * are treated the same as their standard base.
++ *
++ * @param[in] ctx libyang context used to look up the base module.
++ * @param[in] ident Identity value from the data node.
++ * @param[in] base_mod Name of the module that defines the base identity.
++ * @param[in] base_name Name of the base identity.
++ * @return 1 if @p ident is or derives from the named base, 0 otherwise.
++ */
++static int
++nc_ident_is_derived_from(const struct ly_ctx *ctx, const struct lysc_ident *ident,
++ const char *base_mod, const char *base_name)
++{
++ const struct lysc_ident *base;
++ const struct lys_module *mod;
++ LY_ARRAY_COUNT_TYPE i;
++
++ mod = ly_ctx_get_module_implemented(ctx, base_mod);
++ if (!mod)
++ return 0;
++
++ LY_ARRAY_FOR(mod->identities, i) {
++ if (!strcmp(mod->identities[i].name, base_name)) {
++ base = &mod->identities[i];
++ return nc_ident_derived_or_self(base, ident);
++ }
++ }
++
++ return 0;
++}
++
+ #ifdef NC_ENABLED_SSH_TLS
+
+ /**
+@@ -746,23 +804,32 @@ static int
+ config_pubkey_format(const struct lyd_node *node, enum nc_operation parent_op, struct nc_public_key *pubkey)
+ {
+ enum nc_operation op;
+- const char *format;
++ const struct lysc_ident *ident;
+
+ NC_NODE_GET_OP(node, parent_op, &op);
+
+ if (op == NC_OP_DELETE) {
+ pubkey->type = NC_PUBKEY_FORMAT_UNKNOWN;
+ } else if ((op == NC_OP_CREATE) || (op == NC_OP_REPLACE)) {
+- format = ((struct lyd_node_term *)node)->value.ident->name;
+- assert(format);
+- if (!strcmp(format, "ssh-public-key-format")) {
++ ident = ((struct lyd_node_term *)node)->value.ident;
++ assert(ident);
++
++ /*
++ * Use derived-from-or-self semantics so that vendor-extended identities
++ * (e.g. an identity with base ct:ssh-public-key-format) are correctly
++ * mapped without requiring explicit registration in libnetconf2.
++ */
++ if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "ssh-public-key-format")) {
+ pubkey->type = NC_PUBKEY_FORMAT_SSH;
+- } else if (!strcmp(format, "subject-public-key-info-format")) {
++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "subject-public-key-info-format")) {
+ pubkey->type = NC_PUBKEY_FORMAT_X509;
+ } else {
+- /* do not fail, the key may still be usable, or it may have come from a keystore/truststore
+- * and have a different purpose other than NETCONF */
+- WRN(NULL, "Public key format \"%s\" not supported. The key may not be usable.", format);
++ /*
++ * The key format is not one used by NETCONF - it may be present in the
++ * keystore for other purposes (e.g. TLS, WireGuard, Wi-Fi). Do not
++ * fail; just mark it unusable for NETCONF and continue.
++ */
++ VRB(NULL, "Public key format \"%s\" not used by NETCONF, ignoring.", ident->name);
+ pubkey->type = NC_PUBKEY_FORMAT_UNKNOWN;
+ }
+ }
+@@ -792,29 +859,35 @@ config_pubkey_data(const struct lyd_node *node, enum nc_operation parent_op, str
+ static int
+ config_privkey_format(const struct lyd_node *node, enum nc_operation parent_op, struct nc_private_key *privkey)
+ {
++ const struct lysc_ident *ident;
+ enum nc_operation op;
+- const char *format;
+
+ NC_NODE_GET_OP(node, parent_op, &op);
+
+ if (op == NC_OP_DELETE) {
+ privkey->type = NC_PRIVKEY_FORMAT_UNKNOWN;
+ } else if ((op == NC_OP_CREATE) || (op == NC_OP_REPLACE)) {
+- format = ((struct lyd_node_term *)node)->value.ident->name;
+- assert(format);
+-
+- if (!strcmp(format, "rsa-private-key-format")) {
++ ident = ((struct lyd_node_term *)node)->value.ident;
++ assert(ident);
++ /*
++ * Use derived-from-or-self semantics so that vendor-extended identities
++ * are correctly mapped without requiring explicit registration here.
++ */
++ if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "rsa-private-key-format")) {
+ privkey->type = NC_PRIVKEY_FORMAT_RSA;
+- } else if (!strcmp(format, "ec-private-key-format")) {
++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "ec-private-key-format")) {
+ privkey->type = NC_PRIVKEY_FORMAT_EC;
+- } else if (!strcmp(format, "private-key-info-format")) {
++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "private-key-info-format")) {
+ privkey->type = NC_PRIVKEY_FORMAT_X509;
+- } else if (!strcmp(format, "openssh-private-key-format")) {
++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "openssh-private-key-format")) {
+ privkey->type = NC_PRIVKEY_FORMAT_OPENSSH;
+ } else {
+- /* do not fail, the key may still be usable, or it may have come from a keystore/truststore
+- * and have a different purpose other than NETCONF */
+- WRN(NULL, "Private key format \"%s\" not supported. The key may not be usable.", format);
++ /*
++ * The key format is not one used by NETCONF - it may be present in the
++ * keystore for other purposes. Do not fail; just mark it unusable for
++ * NETCONF and continue.
++ */
++ VRB(NULL, "Private key format \"%s\" not used by NETCONF, ignoring.", ident->name);
+ privkey->type = NC_PRIVKEY_FORMAT_UNKNOWN;
+ }
+ }
+@@ -4223,9 +4296,14 @@ config_asymmetric_key(const struct lyd_node *node, enum nc_operation parent_op,
+ NC_CHECK_RET(config_encrypted_privkey_data(encrypted, op));
+ }
+
+- /* config asymmetric key certificates */
+- NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 1, &n));
+- NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry));
++ /*
++ * config asymmetric key certificates (optional: keys shared with other
++ * applications may legitimately have no certificates node)
++ */
++ NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 0, &n));
++ if (n) {
++ NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry));
++ }
+
+ /* config generate csr */
+ NC_CHECK_RET(nc_lyd_find_child(node, "generate-csr", 0, &n));
+@@ -4261,9 +4339,14 @@ config_asymmetric_keys(const struct lyd_node *node, enum nc_operation parent_op,
+ }
+
+ static int
+-config_symmetric_keys(const struct lyd_node *node, enum nc_operation UNUSED(parent_op))
++config_symmetric_keys(const struct lyd_node *UNUSED(node), enum nc_operation UNUSED(parent_op))
+ {
+- CONFIG_LOG_UNSUPPORTED(node);
++ /*
++ * Symmetric keys are not supported for NETCONF. They may legitimately
++ * be present in a shared keystore used by other applications, e.g.,
++ * Wi-Fi passphrases, WireGuard PSKs. Silently ignore them rather than
++ * emitting a warning that would confuse operators.
++ */
+ return 0;
+ }
+
+--
+2.43.0
+
diff --git a/patches/libnetconf2/4.1.2/0002-session-silence-spurious-errors-on-client-disconnect.patch b/patches/libnetconf2/4.1.2/0002-session-silence-spurious-errors-on-client-disconnect.patch
new file mode 100644
index 000000000..8d7af8a46
--- /dev/null
+++ b/patches/libnetconf2/4.1.2/0002-session-silence-spurious-errors-on-client-disconnect.patch
@@ -0,0 +1,83 @@
+From 7988dc8bb8b5dfd3ef1c783164f067a05dd5861b Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 19 Mar 2026 16:38:33 +0100
+Subject: [PATCH 2/2] session: silence spurious errors on client disconnect
+Organization: Wires
+
+When a client drops the TCP connection, ssh_channel_poll_timeout()
+returns SSH_ERROR with "Socket error: disconnected". This is normal
+and should not be logged as an error.
+
+Use ssh_is_connected() to distinguish a peer disconnect from a real
+channel error. On disconnect, set the termination reason to DROPPED
+and omit the SESSION_ERROR flag so no ERR message is emitted. Real
+channel errors retain the existing ERR log and TERM_OTHER reason.
+
+SSH_EOF (clean channel close by the peer) is treated the same way:
+downgraded from ERR to VRB, since it too is an expected condition.
+
+Signed-off-by: Joachim Wiberg
+---
+ src/io.c | 11 ++++++++---
+ src/session_server.c | 16 +++++++++++-----
+ 2 files changed, 19 insertions(+), 8 deletions(-)
+
+diff --git a/src/io.c b/src/io.c
+index b9837c6..9d55a0f 100644
+--- a/src/io.c
++++ b/src/io.c
+@@ -376,12 +376,17 @@ nc_read_poll(struct nc_session *session, int io_timeout)
+ /* EINTR is handled, it resumes waiting */
+ ret = ssh_channel_poll_timeout(session->ti.libssh.channel, io_timeout, 0);
+ if (ret == SSH_ERROR) {
+- ERR(session, "SSH channel poll error (%s).", ssh_get_error(session->ti.libssh.session));
++ if (!ssh_is_connected(session->ti.libssh.session)) {
++ VRB(session, "SSH session disconnected.");
++ session->term_reason = NC_SESSION_TERM_DROPPED;
++ } else {
++ ERR(session, "SSH channel poll error (%s).", ssh_get_error(session->ti.libssh.session));
++ session->term_reason = NC_SESSION_TERM_OTHER;
++ }
+ session->status = NC_STATUS_INVALID;
+- session->term_reason = NC_SESSION_TERM_OTHER;
+ return -1;
+ } else if (ret == SSH_EOF) {
+- ERR(session, "SSH channel unexpected EOF.");
++ VRB(session, "SSH channel closed by the other side.");
+ session->status = NC_STATUS_INVALID;
+ session->term_reason = NC_SESSION_TERM_DROPPED;
+ return -1;
+diff --git a/src/session_server.c b/src/session_server.c
+index 929f8bf..710d66c 100644
+--- a/src/session_server.c
++++ b/src/session_server.c
+@@ -2097,15 +2097,21 @@ nc_ps_poll_session_io(struct nc_session *session, int io_timeout, time_t now_mon
+
+ r = ssh_channel_poll_timeout(session->ti.libssh.channel, 0, 0);
+ if (r == SSH_EOF) {
+- sprintf(msg, "SSH channel unexpected EOF");
++ sprintf(msg, "SSH channel closed by the other side");
+ session->status = NC_STATUS_INVALID;
+ session->term_reason = NC_SESSION_TERM_DROPPED;
+- ret = NC_PSPOLL_SESSION_TERM | NC_PSPOLL_SESSION_ERROR;
++ ret = NC_PSPOLL_SESSION_TERM;
+ } else if (r == SSH_ERROR) {
+- sprintf(msg, "SSH channel poll error (%s)", ssh_get_error(session->ti.libssh.session));
++ if (!ssh_is_connected(session->ti.libssh.session)) {
++ sprintf(msg, "SSH session disconnected");
++ session->term_reason = NC_SESSION_TERM_DROPPED;
++ ret = NC_PSPOLL_SESSION_TERM;
++ } else {
++ sprintf(msg, "SSH channel poll error (%s)", ssh_get_error(session->ti.libssh.session));
++ session->term_reason = NC_SESSION_TERM_OTHER;
++ ret = NC_PSPOLL_SESSION_TERM | NC_PSPOLL_SESSION_ERROR;
++ }
+ session->status = NC_STATUS_INVALID;
+- session->term_reason = NC_SESSION_TERM_OTHER;
+- ret = NC_PSPOLL_SESSION_TERM | NC_PSPOLL_SESSION_ERROR;
+ } else if (!r) {
+ /* no application data received */
+ ret = NC_PSPOLL_TIMEOUT;
+--
+2.43.0
+
diff --git a/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch b/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch
deleted file mode 100644
index 95c684a99..000000000
--- a/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch
+++ /dev/null
@@ -1,22 +0,0 @@
-diff --git a/src/server_config.c b/src/server_config.c
-index 48d362e..dc9f59d 100644
---- a/src/server_config.c
-+++ b/src/server_config.c
-@@ -4151,6 +4151,8 @@ config_asymmetric_key_certs(const struct lyd_node *node, enum nc_operation paren
- struct lyd_node *n;
- enum nc_operation op;
-
-+ if (!node)
-+ return 0;
- NC_NODE_GET_OP(node, parent_op, &op);
-
- /* configure all certificates */
-@@ -4225,7 +4227,7 @@ config_asymmetric_key(const struct lyd_node *node, enum nc_operation parent_op,
- }
-
- /* config asymmetric key certificates */
-- NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 1, &n));
-+ NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 0, &n));
- NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry));
-
- /* config generate csr */
diff --git a/src/confd/bin/Makefile.am b/src/confd/bin/Makefile.am
index 69b28e5ac..9df1ebd62 100644
--- a/src/confd/bin/Makefile.am
+++ b/src/confd/bin/Makefile.am
@@ -1,4 +1,3 @@
-pkglibexec_SCRIPTS = bootstrap error load gen-hostname \
- gen-interfaces gen-motd gen-hardware gen-version \
- mstpd-wait-online wait-interface
+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/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 f8071c2c3..71e4717a5 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
@@ -84,9 +85,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/containers.c b/src/confd/src/containers.c
index 59eae7fbd..8dabcc690 100644
--- a/src/confd/src/containers.c
+++ b/src/confd/src/containers.c
@@ -322,9 +322,12 @@ static int add(const char *name, struct lyd_node *cif)
fchmod(fileno(fp), 0700);
fclose(fp);
- systemf("initctl -bnq touch container@%s.conf", name);
- systemf("initctl -bnq %s container@%s.conf", lydx_is_enabled(cif, "enabled")
- ? "enable" : "disable", name);
+ if (lydx_is_enabled(cif, "enabled")) {
+ finit_enablef("container@%s", name);
+ finit_reloadf("container@%s", name);
+ } else {
+ finit_disablef("container@%s", name);
+ }
return 0;
}
@@ -341,11 +344,11 @@ static int del(const char *name)
FILE *pp;
erasef("%s/%s.sh", _PATH_CONT, name);
- systemf("initctl -bnq disable container@%s.conf", name);
+ finit_disablef("container@%s", name);
/* Schedule a cleanup job for this container as soon as it has stopped */
snprintf(prune_dir, sizeof(prune_dir), "%s/%s", _PATH_CLEAN, name);
- systemf("mkdir -p %s", prune_dir);
+ mkpath(prune_dir, 0755);
/* Finit cleanup:script runs when container is deleted, it will remove any image by-ID */
pp = popenf("r", "podman inspect %s 2>/dev/null | jq -r '.[].Id' 2>/dev/null", name);
diff --git a/src/confd/src/core.c b/src/confd/src/core.c
index 4acce329a..5e4fddaa2 100644
--- a/src/confd/src/core.c
+++ b/src/confd/src/core.c
@@ -9,6 +9,129 @@
struct confd confd;
+/*
+ * Touch a Finit service .conf file to schedule a synchronized reload.
+ * Equivalent to 'initctl touch ' but without the fork+exec overhead.
+ * The service name should be given without the .conf suffix.
+ */
+int finit_reload(const char *svc)
+{
+ char path[256];
+
+ /* Prefer the enabled/ symlink -- that's what Finit watches */
+ snprintf(path, sizeof(path), FINIT_RCSD "/enabled/%s.conf", svc);
+ if (!utimensat(AT_FDCWD, path, NULL, AT_SYMLINK_NOFOLLOW))
+ return 0;
+
+ snprintf(path, sizeof(path), FINIT_RCSD "/available/%s.conf", svc);
+ if (!utimensat(AT_FDCWD, path, NULL, AT_SYMLINK_NOFOLLOW))
+ return 0;
+
+ ERRNO("failed marking %s for reload", svc);
+ return -1;
+}
+
+int finit_reloadf(const char *fmt, ...)
+{
+ char svc[64];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(svc, sizeof(svc), fmt, ap);
+ va_end(ap);
+
+ return finit_reload(svc);
+}
+
+int finit_enable(const char *svc)
+{
+ char src[256], dst[256];
+ const char *at;
+
+ snprintf(src, sizeof(src), FINIT_RCSD "/available/%s.conf", svc);
+ if (!fexist(src)) {
+ /* Template instance (e.g. container@foo): point to base template */
+ at = strchr(svc, '@');
+ if (at)
+ snprintf(src, sizeof(src), FINIT_RCSD "/available/%.*s@.conf",
+ (int)(at - svc), svc);
+ }
+
+ snprintf(dst, sizeof(dst), FINIT_RCSD "/enabled/%s.conf", svc);
+ if (symlink(src, dst) && errno != EEXIST) {
+ ERRNO("failed enabling %s", svc);
+ return -1;
+ }
+ return 0;
+}
+
+int finit_disable(const char *svc)
+{
+ char path[256];
+
+ snprintf(path, sizeof(path), FINIT_RCSD "/enabled/%s.conf", svc);
+ if (remove(path) && errno != ENOENT) {
+ ERRNO("failed disabling %s", svc);
+ return -1;
+ }
+ return 0;
+}
+
+int finit_delete(const char *svc)
+{
+ char path[256];
+
+ snprintf(path, sizeof(path), FINIT_RCSD "/enabled/%s.conf", svc);
+ if (remove(path) && errno != ENOENT) {
+ ERRNO("failed removing enabled symlink for %s", svc);
+ return -1;
+ }
+
+ snprintf(path, sizeof(path), FINIT_RCSD "/available/%s.conf", svc);
+ if (remove(path) && errno != ENOENT) {
+ ERRNO("failed removing available conf for %s", svc);
+ return -1;
+ }
+
+ return 0;
+}
+
+int finit_deletef(const char *fmt, ...)
+{
+ char svc[64];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(svc, sizeof(svc), fmt, ap);
+ va_end(ap);
+
+ return finit_delete(svc);
+}
+
+int finit_enablef(const char *fmt, ...)
+{
+ char svc[64];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(svc, sizeof(svc), fmt, ap);
+ va_end(ap);
+
+ return finit_enable(svc);
+}
+
+int finit_disablef(const char *fmt, ...)
+{
+ char svc[64];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(svc, sizeof(svc), fmt, ap);
+ va_end(ap);
+
+ return finit_disable(svc);
+}
+
static int startup_save(sr_session_ctx_t *session, uint32_t sub_id, const char *model,
const char *xpath, sr_event_t event, unsigned request_id, void *priv)
@@ -99,6 +222,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 +240,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");
@@ -423,7 +556,7 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
client = srx_enabled(session, "/ietf-system:system/ntp/enabled");
server = lydx_get_xpathf(config, "/ietf-ntp:ntp") != NULL;
- systemf("initctl -nbq %s chronyd", client || server ? "enable" : "disable");
+ (client || server) ? finit_enable("chronyd") : finit_disable("chronyd");
}
if (cfg)
@@ -431,11 +564,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 +584,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 +768,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..f25d91bd4 100644
--- a/src/confd/src/core.h
+++ b/src/confd/src/core.h
@@ -4,11 +4,14 @@
#define CONFD_CORE_H_
#include
+#include
+#include
#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -31,6 +34,13 @@
#include "dagger.h"
+#define FINIT_RCSD "/etc/finit.d"
+
+#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 +150,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 +164,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 +177,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,13 +186,23 @@ 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;
}
+/* core.c */
+int finit_enable(const char *svc);
+int finit_disable(const char *svc);
+int finit_delete(const char *svc);
+int finit_reload(const char *svc);
+int finit_enablef(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
+int finit_disablef(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
+int finit_deletef(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
+int finit_reloadf(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
+
/* interfaces.c */
int interfaces_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
int interfaces_cand_init(struct confd *confd);
@@ -238,8 +258,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/dagger.c b/src/confd/src/dagger.c
index a8aea6f56..3dafa8770 100644
--- a/src/confd/src/dagger.c
+++ b/src/confd/src/dagger.c
@@ -21,7 +21,7 @@ static FILE *dagger_fopen(struct dagger *d, int gen, const char *action,
return NULL;
}
- if (systemf("mkdir -p " PATH_ACTION_, d->path, gen, action, node))
+ if (fmkpath(0755, PATH_ACTION_, d->path, gen, action, node))
return NULL;
if (asprintf(&path, PATH_ACTION_"/%02u-%s", d->path, gen, action, node, prio, script) == -1)
@@ -117,15 +117,19 @@ int dagger_add_dep(const struct dagger *d, const char *depender, const char *dep
int dagger_add_node(struct dagger *d, const char *node)
{
- return systemf("mkdir -p %s/%d/dag/%s", d->path, d->next, node);
+ return fmkpath(0755, "%s/%d/dag/%s", d->path, d->next, node);
}
int dagger_abandon(struct dagger *d)
{
int exitcode;
+ if (!d->next_fp)
+ return 0;
+
fprintf(d->next_fp, "%d\n", d->next);
fclose(d->next_fp);
+ d->next_fp = NULL;
exitcode = systemf("dagger -C %s abandon", d->path);
DEBUG("dagger(%d->%d): abandon: exitcode=%d\n",
@@ -134,12 +138,13 @@ int dagger_abandon(struct dagger *d)
return exitcode;
}
-int dagger_evolve(struct dagger *d)
+static int dagger_evolve(struct dagger *d)
{
int exitcode;
fprintf(d->next_fp, "%d\n", d->next);
fclose(d->next_fp);
+ d->next_fp = NULL;
exitcode = systemf("dagger -C %s evolve", d->path);
DEBUG("dagger(%d->%d): evolve: exitcode=%d\n",
@@ -163,6 +168,9 @@ int dagger_evolve_or_abandon(struct dagger *d)
{
int exitcode, err;
+ if (!d->next_fp)
+ return 0;
+
exitcode = dagger_evolve(d);
dagger_prune(d);
if (!exitcode)
@@ -184,13 +192,12 @@ int dagger_is_bootstrap(struct dagger *d)
int dagger_claim(struct dagger *d, const char *path)
{
- int err;
+ char lnk[strlen(path) + 32];
memset(d, 0, sizeof(*d));
- err = systemf("mkdir -p %s", path);
- if (err)
- return err;
+ if (mkpath(path, 0755))
+ return -1;
d->next_fp = fopenf("wx", "%s/next", path);
if (!d->next_fp) {
@@ -201,23 +208,22 @@ int dagger_claim(struct dagger *d, const char *path)
if (readdf(&d->current, "%s/current", path)) {
d->current = -1;
} else {
- err = systemf("mkdir -p %s/%d/action/exit"
- " && "
- "ln -sf ../../top-down-order %s/%d/action/exit/order",
- path, d->current, path, d->current);
- if (err)
- return err;
+ if (fmkpath(0755, "%s/%d/action/exit", path, d->current))
+ return -1;
+ snprintf(lnk, sizeof(lnk), "%s/%d/action/exit/order", path, d->current);
+ erase(lnk);
+ if (symlink("../../top-down-order", lnk))
+ return -1;
}
d->next = d->current + 1;
- err = systemf("mkdir -p %s/%d/action/init"
- " && "
- "mkdir -p %s/%d/skip"
- " && "
- "ln -s ../../bottom-up-order %s/%d/action/init/order",
- path, d->next, path, d->next, path, d->next);
- if (err)
- return err;
+ if (fmkpath(0755, "%s/%d/action/init", path, d->next))
+ return -1;
+ if (fmkpath(0755, "%s/%d/skip", path, d->next))
+ return -1;
+ snprintf(lnk, sizeof(lnk), "%s/%d/action/init/order", path, d->next);
+ if (symlink("../../bottom-up-order", lnk) && errno != EEXIST)
+ return -1;
strlcpy(d->path, path, sizeof(d->path));
return 0;
diff --git a/src/confd/src/dagger.h b/src/confd/src/dagger.h
index c08217e2b..bf305c6a2 100644
--- a/src/confd/src/dagger.h
+++ b/src/confd/src/dagger.h
@@ -24,7 +24,6 @@ FILE *dagger_fopen_current(struct dagger *d, const char *action, const char *nod
int dagger_add_dep(const struct dagger *d, const char *depender, const char *dependee);
int dagger_add_node(struct dagger *d, const char *node);
int dagger_abandon(struct dagger *d);
-int dagger_evolve(struct dagger *d);
int dagger_evolve_or_abandon(struct dagger *d);
int dagger_is_bootstrap(struct dagger *d);
diff --git a/src/confd/src/dhcp-client.c b/src/confd/src/dhcp-client.c
index 1d0f347aa..2ef8d1cc4 100644
--- a/src/confd/src/dhcp-client.c
+++ b/src/confd/src/dhcp-client.c
@@ -99,7 +99,7 @@ static void add(const char *ifname, struct lyd_node *cfg)
const char *metric = lydx_get_cattr(cfg, "route-preference");
const char *client_id = lydx_get_cattr(cfg, "client-id");
char *cid = NULL, *options = NULL;
- const char *action = "disable";
+ int ena = 0;
const char *vendor_class;
char vendor[128] = { 0 };
char do_arp[20] = { 0 };
@@ -153,9 +153,10 @@ static void add(const char *ifname, struct lyd_node *cfg)
options ? "-o " : "", options,
ifname, cid ?: "", vendor, ifname);
fclose(fp);
- action = "enable";
+ ena = 1;
err:
- systemf("initctl -bfqn %s dhcp-client-%s", action, ifname);
+ ena ? finit_enablef("dhcp-client-%s", ifname)
+ : finit_disablef("dhcp-client-%s", ifname);
if (options)
free(options);
if (cid)
@@ -164,7 +165,7 @@ static void add(const char *ifname, struct lyd_node *cfg)
static void del(const char *ifname)
{
- systemf("initctl -bfq delete dhcp-client-%s", ifname);
+ finit_deletef("dhcp-client-%s", ifname);
}
int dhcp_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff,
diff --git a/src/confd/src/dhcp-server.c b/src/confd/src/dhcp-server.c
index cdf641f35..8e464d095 100644
--- a/src/confd/src/dhcp-server.c
+++ b/src/confd/src/dhcp-server.c
@@ -377,7 +377,7 @@ int dhcp_server_change(sr_session_ctx_t *session, struct lyd_node *config, struc
err_done:
if (added || deleted)
- system("initctl -nbq touch dnsmasq");
+ finit_reload("dnsmasq");
return err;
}
diff --git a/src/confd/src/dhcpv6-client.c b/src/confd/src/dhcpv6-client.c
index d29426277..69746acf3 100644
--- a/src/confd/src/dhcpv6-client.c
+++ b/src/confd/src/dhcpv6-client.c
@@ -80,7 +80,7 @@ static void add_v6(const char *ifname, struct lyd_node *cfg)
const char *metric = lydx_get_cattr(cfg, "route-preference");
const char *duid = lydx_get_cattr(cfg, "duid");
char *client_duid = NULL, *options = NULL;
- const char *action = "disable";
+ int ena = 0;
const char *addr_mode = "-N try"; /* Default: stateful mode */
char prefix_del[16] = { 0 };
bool request_pd = false;
@@ -127,9 +127,10 @@ static void add_v6(const char *ifname, struct lyd_node *cfg)
options ?: "", client_duid ?: "",
ifname, ifname);
fclose(fp);
- action = "enable";
+ ena = 1;
err:
- systemf("initctl -bfqn %s dhcpv6-client-%s", action, ifname);
+ ena ? finit_enablef("dhcpv6-client-%s", ifname)
+ : finit_disablef("dhcpv6-client-%s", ifname);
if (options)
free(options);
if (client_duid)
@@ -138,7 +139,7 @@ static void add_v6(const char *ifname, struct lyd_node *cfg)
static void del_v6(const char *ifname)
{
- systemf("initctl -bfq delete dhcpv6-client-%s", ifname);
+ finit_deletef("dhcpv6-client-%s", ifname);
}
int dhcpv6_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff,
diff --git a/src/confd/src/firewall.c b/src/confd/src/firewall.c
index b6653d870..ba6969e84 100644
--- a/src/confd/src/firewall.c
+++ b/src/confd/src/firewall.c
@@ -504,25 +504,25 @@ int firewall_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
break;
case SR_EV_ABORT:
- systemf("rm -rf " FIREWALLD_DIR_NEXT);
+ rmrf(FIREWALLD_DIR_NEXT);
return SR_ERR_OK;
case SR_EV_DONE:
if (!fisdir(FIREWALLD_DIR_NEXT)) {
/* Firewall is disabled */
- systemf("initctl -nbq disable firewalld");
+ finit_disable("firewalld");
return SR_ERR_OK;
}
/* Firewall is enabled, roll in new configuration */
- systemf("rm -rf " FIREWALLD_DIR);
+ rmrf(FIREWALLD_DIR);
if (rename(FIREWALLD_DIR_NEXT, FIREWALLD_DIR)) {
ERRNO("Failed rolling in firewalld configuration");
return SR_ERR_SYS;
}
- systemf("initctl -nbq touch firewalld");
- systemf("initctl -nbq enable firewalld");
+ finit_reload("firewalld");
+ finit_enable("firewalld");
return SR_ERR_OK;
default:
@@ -533,7 +533,7 @@ int firewall_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
global = lydx_get_descendant(tree, "firewall", NULL);
/* Clean up any stale /etc/firewalld+ first */
- systemf("rm -rf " FIREWALLD_DIR_NEXT);
+ rmrf(FIREWALLD_DIR_NEXT);
/* If firewall is disabled or not enabled, don't generate config */
if (!global || !lydx_is_enabled(global, "enabled")) {
diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c
index 11fa7ed43..5a5be02c0 100644
--- a/src/confd/src/hardware.c
+++ b/src/confd/src/hardware.c
@@ -685,14 +685,14 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
ap_interfaces++;
if (running)
- systemf("initctl -bfq touch hostapd@%s", name);
+ finit_reloadf("hostapd@%s", name);
else
- systemf("initctl -bfq enable hostapd@%s", name);
+ finit_enablef("hostapd@%s", name);
}
}
}
if (!ap_interfaces) {
- systemf("initctl -bfq disable hostapd@%s", name);
+ finit_disablef("hostapd@%s", name);
erasef(HOSTAPD_CONF, name);
erasef(HOSTAPD_CONF_NEXT, name);
}
@@ -784,10 +784,10 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
if (fexist(GPSD_CONF_NEXT)) {
unlink(GPSD_CONF);
rename(GPSD_CONF_NEXT, GPSD_CONF);
- systemf("initctl -nbq enable gpsd");
+ finit_enable("gpsd");
} else {
unlink(GPSD_CONF);
- systemf("initctl -nbq disable gpsd");
+ finit_disable("gpsd");
}
break;
}
diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c
index ba1e04a0c..a50879e68 100644
--- a/src/confd/src/interfaces.c
+++ b/src/confd/src/interfaces.c
@@ -774,7 +774,8 @@ static sr_error_t ifchange_post(sr_session_ctx_t *session, struct dagger *net,
return err ? SR_ERR_INTERNAL : SR_ERR_OK;
}
-int interfaces_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
+int interfaces_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff,
+ sr_event_t event, struct confd *confd)
{
struct lyd_node *cifs, *difs, *cif, *dif;
sr_error_t err;
@@ -794,12 +795,18 @@ int interfaces_change(sr_session_ctx_t *session, struct lyd_node *config, struct
return SR_ERR_OK;
}
+ difs = lydx_get_descendant(diff, "interfaces", "interface", NULL);
+ if (!difs) {
+ /* No interface changes, skip to prevent another dagger generation */
+ return SR_ERR_OK;
+ }
+
err = dagger_claim(&confd->netdag, "/run/net");
if (err)
return err;
cifs = lydx_get_descendant(config, "interfaces", "interface", NULL);
- difs = lydx_get_descendant(diff, "interfaces", "interface", NULL);
+
err = netdag_init(session, &confd->netdag, cifs, difs);
if (err)
goto err_out;
diff --git a/src/confd/src/keystore.c b/src/confd/src/keystore.c
index 6f493e5c0..296f63988 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,72 @@ 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;
+ int new_cert;
+ FILE *fp;
+
+ 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;
+ }
+
+ /*
+ * Only stop nginx (by clearing usr/mkcert) if the cert file does not
+ * yet exist. When updating an existing cert, nginx can keep running
+ * and will reload via SIGHUP from finit_reload("nginx") in web_change.
+ */
+ new_cert = !fexistf("%s/%s.crt", SSL_CERT_DIR, certname);
+ if (new_cert)
+ erase("/run/finit/cond/usr/mkcert");
+
+ 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);
+
+ if (new_cert)
+ 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 +235,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 +331,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 +358,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..8f9863391
--- /dev/null
+++ b/src/confd/src/main.c
@@ -0,0 +1,902 @@
+/* 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
+
+/*
+ * Restart sentinel: written to tmpfs after a successful bootstrap.
+ * Survives crash and clean exit within the same boot session (cleared by
+ * reboot since /run is tmpfs). On restart confd detects it and skips the
+ * destructive bootstrap phases (gen-config, SHM wipe, sr_install_factory_config,
+ * clearing the running datastore, loading startup-config), letting the already-
+ * consistent sysrepo state drive a lightweight re-attach.
+ */
+#define SENTINEL_PATH "/run/confd.boot"
+
+/* 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;
+static int restart; /* set when sentinel found; suppresses conout() */
+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;
+
+ if (restart)
+ return;
+
+ 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 = -1, pump_pid = -1;
+ 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)");
+
+ /*
+ * Check for restart sentinel. If present, sysrepo and the system are
+ * already in a consistent state from a prior completed bootstrap — skip
+ * all destructive phases and re-attach to the live datastore instead.
+ */
+ restart = fexist(SENTINEL_PATH);
+ if (restart)
+ NOTE("Restart sentinel found, skipping bootstrap and re-attaching to sysrepo");
+
+ if (!restart) {
+ /* 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;
+ }
+
+ if (!restart) {
+ /* 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;
+ }
+
+ if (!restart) {
+ /* 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);
+ }
+
+ if (!restart) {
+ /* 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");
+
+ /*
+ * Write restart sentinel. From this point on, sysrepo and the system
+ * are in a consistent state. If confd exits for any reason (crash or
+ * clean shutdown) and Finit restarts it, the sentinel tells it to skip
+ * the destructive bootstrap phases and re-attach to the live datastore.
+ * The sentinel lives in /run (tmpfs) so a real reboot always starts clean.
+ */
+ {
+ int sfd = open(SENTINEL_PATH, O_CREAT | O_WRONLY | O_TRUNC, 0644);
+ if (sfd >= 0)
+ close(sfd);
+ else
+ WARN("Failed to write restart sentinel %s: %m", SENTINEL_PATH);
+ }
+
+ /* 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/ntp.c b/src/confd/src/ntp.c
index 618de3393..818138dee 100644
--- a/src/confd/src/ntp.c
+++ b/src/confd/src/ntp.c
@@ -48,7 +48,7 @@ static int change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd
if (systemf("chronyc reload sources >/dev/null 2>&1"))
ERRNO("Failed reloading chronyd sources");
- systemf("initctl -nbq touch chronyd");
+ finit_reload("chronyd");
return SR_ERR_OK;
default:
diff --git a/src/confd/src/routing.c b/src/confd/src/routing.c
index 8c01c9323..c9b2a24d2 100644
--- a/src/confd/src/routing.c
+++ b/src/confd/src/routing.c
@@ -605,17 +605,17 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
/* Enable/disable FRR daemons as standalone finit services.
* Harmless no-op when using watchfrr (services not installed). */
- systemf("initctl -nbq %s ospfd", ospfd_enabled ? "enable" : "disable");
+ ospfd_enabled ? finit_enable("ospfd") : finit_disable("ospfd");
if (ospfd_enabled)
- systemf("initctl -nbq touch ospfd");
- systemf("initctl -nbq %s ripd", ripd_enabled ? "enable" : "disable");
- systemf("initctl -nbq %s bfdd", bfdd_enabled ? "enable" : "disable");
+ finit_reload("ospfd");
+ ripd_enabled ? finit_enable("ripd") : finit_disable("ripd");
+ bfdd_enabled ? finit_enable("bfdd") : finit_disable("bfdd");
/*
* Signal netd to reload - it assembles /etc/frr/frr.conf and
* Finit propagates the restart to the frr sysv service
*/
- if (systemf("initctl -bfq touch netd"))
+ if (finit_reload("netd"))
ERROR("Failed to signal netd for reload");
return rc;
diff --git a/src/confd/src/services.c b/src/confd/src/services.c
index 0f487f13b..0981f80df 100644
--- a/src/confd/src/services.c
+++ b/src/confd/src/services.c
@@ -11,7 +11,6 @@
#include
#include
#include
-#include
#include "core.h"
#include
@@ -19,6 +18,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"
@@ -273,55 +273,46 @@ static int put(sr_data_t *cfg)
return SR_ERR_OK;
}
-static int svc_change(sr_session_ctx_t *session, sr_event_t event, const char *xpath,
- const char *name, const char *svc)
-{
- struct lyd_node *srv = NULL;
- sr_data_t *cfg;
- int ena;
-
- cfg = get(session, event, xpath, &srv, name, NULL);
- if (!cfg)
- return SR_ERR_OK;
-
- ena = lydx_is_enabled(srv, "enabled");
- if (systemf("initctl -nbq %s %s", ena ? "enable" : "disable", svc))
- ERROR("Failed %s %s", ena ? "enabling" : "disabling", name);
- if (ena)
- systemf("initctl -nbq touch %s", svc); /* in case already enabled */
-
- return put(cfg);
-}
-
-static void svc_enadis(int ena, svc type, const char *svc)
+/*
+ * Enable or disable a named service: manage nginx symlinks and mDNS
+ * records, then start or stop via initctl. Does NOT touch (restart)
+ * the service -- call finit_reload() separately when only config
+ * changes and the service is already running.
+ */
+static void svc_enable(int ena, svc type, const char *svcname)
{
- int isweb, isapp;
+ if (!svcname)
+ svcname = name[type];
- if (!svc)
- svc = name[type];
- isweb = fexistf("/etc/nginx/available/%s.conf", svc);
- isapp = fexistf("/etc/nginx/%s.app", svc);
+ if (fexistf("/etc/nginx/available/%s.conf", svcname)) {
+ char src[256], dst[256];
- if (ena) {
- if (isweb)
- systemf("ln -sf ../available/%s.conf /etc/nginx/enabled/", svc);
- if (isapp)
- systemf("ln -sf ../%s.app /etc/nginx/app/%s.conf", svc, svc);
- systemf("initctl -nbq enable %s", svc);
- systemf("initctl -nbq touch %s", svc); /* in case already enabled */
- } else {
- if (isweb)
- systemf("rm -f /etc/nginx/enabled/%s.conf", svc);
- if (isapp)
- systemf("rm -f /etc/nginx/app/%s.conf", svc);
- systemf("initctl -nbq disable %s", svc);
+ snprintf(dst, sizeof(dst), "/etc/nginx/enabled/%s.conf", svcname);
+ if (ena) {
+ snprintf(src, sizeof(src), "../available/%s.conf", svcname);
+ erase(dst);
+ symlink(src, dst);
+ } else {
+ erase(dst);
+ }
}
+ if (fexistf("/etc/nginx/%s.app", svcname)) {
+ char src[256], dst[256];
+
+ snprintf(dst, sizeof(dst), "/etc/nginx/app/%s.conf", svcname);
+ if (ena) {
+ snprintf(src, sizeof(src), "../%s.app", svcname);
+ erase(dst);
+ symlink(src, dst);
+ } else {
+ erase(dst);
+ }
+ }
+
+ ena ? finit_enable(svcname) : finit_disable(svcname);
if (type != none)
mdns_records(ena ? MDNS_ADD : MDNS_DELETE, type);
-
- systemf("initctl -nbq touch avahi");
- systemf("initctl -nbq touch nginx");
}
static void fput_list(FILE *fp, struct lyd_node *cfg, const char *list, const char *heading)
@@ -394,28 +385,6 @@ static void mdns_conf(struct confd *confd, struct lyd_node *cfg)
fclose(fp);
}
-static void mdns_cname(sr_session_ctx_t *session)
-{
- int ena = srx_enabled(session, "/infix-services:mdns/enabled");
-
- if (ena) {
- int www = srx_enabled(session, "/infix-services:web/netbrowse/enabled");
- FILE *fp;
-
- fp = fopen("/etc/default/mdns-alias", "w");
- if (fp) {
- fprintf(fp, "MDNS_ALIAS_ARGS=\"%s\"\n",
- www ? "network.local" : "");
- fclose(fp);
- } else {
- ERRNO("failed updating mDNS aliases");
- ena = 0;
- }
- }
-
- svc_enadis(ena, none, "mdns-alias");
-}
-
static int mdns_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;
@@ -431,15 +400,16 @@ static int mdns_change(sr_session_ctx_t *session, struct lyd_node *config, struc
ena = lydx_is_enabled(srv, "enabled");
if (ena) {
- /* Generate/update avahi-daemon.conf */
mdns_conf(confd, srv);
-
- /* Generate/update basic mDNS service records */
mdns_records(MDNS_UPDATE, all);
}
- svc_enadis(ena, none, "avahi");
- mdns_cname(session);
+ if (lydx_get_xpathf(diff, MDNS_XPATH "/enabled")) {
+ svc_enable(ena, none, "avahi");
+ svc_enable(ena, none, "mdns-alias");
+ }
+ if (ena)
+ finit_reload("avahi");
return put(cfg);
}
@@ -493,7 +463,15 @@ static int lldp_change(sr_session_ctx_t *session, struct lyd_node *config, struc
if (erase(LLDP_CONFIG))
ERRNO("Failed to remove old %s", LLDP_CONFIG);
- svc_change(session, event, LLDP_XPATH, "lldp", "lldpd");
+ {
+ struct lyd_node *lldp = lydx_get_xpathf(config, LLDP_XPATH);
+ int lldp_ena = lydx_is_enabled(lldp, "enabled");
+
+ if (lydx_get_xpathf(diff, LLDP_XPATH "/enabled"))
+ lldp_ena ? finit_enable("lldpd") : finit_disable("lldpd");
+ else if (lldp_ena)
+ finit_reload("lldpd");
+ }
break;
case SR_EV_ABORT:
@@ -511,33 +489,55 @@ static int ttyd_change(sr_session_ctx_t *session, struct lyd_node *config, struc
{
struct lyd_node *srv = NULL;
sr_data_t *cfg;
+ int ena;
- if (event != SR_EV_DONE || !lydx_get_xpathf(diff, WEB_CONSOLE_XPATH))
+ if (event != SR_EV_DONE || !lydx_get_xpathf(diff, WEB_CONSOLE_XPATH "/enabled"))
return SR_ERR_OK;
cfg = get(session, event, WEB_XPATH, &srv, "web", "console", NULL);
if (!cfg)
return SR_ERR_OK;
- svc_enadis(lydx_is_enabled(srv, "enabled"), ttyd, NULL);
+ ena = lydx_is_enabled(srv, "enabled") &&
+ lydx_is_enabled(lydx_get_xpathf(config, WEB_XPATH), "enabled");
+ svc_enable(ena, ttyd, NULL);
+ finit_reload("nginx");
return put(cfg);
}
-static int netbrowse_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
+static void mdns_alias_conf(int ena)
+{
+ FILE *fp = fopen("/etc/default/mdns-alias", "w");
+
+ if (fp) {
+ fprintf(fp, "MDNS_ALIAS_ARGS=\"%s\"\n", ena ? "network.local" : "");
+ fclose(fp);
+ } else {
+ ERRNO("failed updating mDNS aliases");
+ }
+}
+
+static int netbrowse_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;
sr_data_t *cfg;
+ int ena;
- if (event != SR_EV_DONE || !lydx_get_xpathf(diff, WEB_NETBROWSE_XPATH))
+ if (event != SR_EV_DONE || !lydx_get_xpathf(diff, WEB_NETBROWSE_XPATH "/enabled"))
return SR_ERR_OK;
cfg = get(session, event, WEB_XPATH, &srv, "web", "netbrowse", NULL);
if (!cfg)
return SR_ERR_OK;
- svc_enadis(lydx_is_enabled(srv, "enabled"), netbrowse, NULL);
- mdns_cname(session);
+ ena = lydx_is_enabled(srv, "enabled") &&
+ lydx_is_enabled(lydx_get_xpathf(config, WEB_XPATH), "enabled");
+ svc_enable(ena, netbrowse, NULL);
+ mdns_alias_conf(ena);
+ finit_reload("nginx");
+ finit_reload("mdns-alias");
return put(cfg);
}
@@ -546,15 +546,19 @@ static int restconf_change(sr_session_ctx_t *session, struct lyd_node *config, s
{
struct lyd_node *srv = NULL;
sr_data_t *cfg;
+ int ena;
- if (event != SR_EV_DONE || !lydx_get_xpathf(diff, WEB_RESTCONF_XPATH))
+ if (event != SR_EV_DONE || !lydx_get_xpathf(diff, WEB_RESTCONF_XPATH "/enabled"))
return SR_ERR_OK;
cfg = get(session, event, WEB_XPATH, &srv, "web", "restconf", NULL);
if (!cfg)
return SR_ERR_OK;
- svc_enadis(lydx_is_enabled(srv, "enabled"), restconf, NULL);
+ ena = lydx_is_enabled(srv, "enabled") &&
+ lydx_is_enabled(lydx_get_xpathf(config, WEB_XPATH), "enabled");
+ svc_enable(ena, restconf, "restconf");
+ finit_reload("nginx");
return put(cfg);
}
@@ -570,7 +574,16 @@ static int ssh_change(sr_session_ctx_t *session, struct lyd_node *config, struct
switch (event) {
case SR_EV_DONE:
- return svc_change(session, event, SSH_XPATH, "ssh", "sshd");
+ {
+ struct lyd_node *ssh = lydx_get_xpathf(config, SSH_XPATH);
+ int ssh_ena = lydx_is_enabled(ssh, "enabled");
+
+ if (lydx_get_xpathf(diff, SSH_XPATH "/enabled"))
+ ssh_ena ? finit_enable("sshd") : finit_disable("sshd");
+ else if (ssh_ena)
+ finit_reload("sshd");
+ }
+ return SR_ERR_OK;
case SR_EV_ENABLED:
case SR_EV_CHANGE:
break;
@@ -625,6 +638,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;
@@ -639,18 +694,26 @@ static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct
return SR_ERR_OK;
ena = lydx_is_enabled(srv, "enabled");
- if (ena) {
- svc_enadis(srx_enabled(session, "%s/enabled", WEB_CONSOLE_XPATH), ttyd, "ttyd");
- svc_enadis(srx_enabled(session, "%s/enabled", WEB_NETBROWSE_XPATH), netbrowse, "netbrowse");
- svc_enadis(srx_enabled(session, "%s/enabled", WEB_RESTCONF_XPATH), restconf, "restconf");
- } else {
- svc_enadis(0, ttyd, NULL);
- svc_enadis(0, netbrowse, NULL);
- svc_enadis(0, restconf, NULL);
+
+ /* Certificate changed: regenerate ssl.conf and reload nginx */
+ if (lydx_get_xpathf(diff, WEB_XPATH "/certificate")) {
+ web_ssl_conf(srv, config);
+ finit_reload("nginx");
}
- svc_enadis(ena, web, "nginx");
- mdns_cname(session);
+ /* Web master on/off: propagate to nginx and all sub-services */
+ if (lydx_get_xpathf(diff, WEB_XPATH "/enabled")) {
+ int nb_ena = ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_NETBROWSE_XPATH), "enabled");
+
+ svc_enable(ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_CONSOLE_XPATH), "enabled"),
+ ttyd, "ttyd");
+ svc_enable(nb_ena, netbrowse, "netbrowse");
+ svc_enable(ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_RESTCONF_XPATH), "enabled"),
+ restconf, "restconf");
+ svc_enable(ena, web, "nginx");
+ mdns_alias_conf(nb_ena);
+ finit_reload("mdns-alias");
+ }
return put(cfg);
}
diff --git a/src/confd/src/syslog.c b/src/confd/src/syslog.c
index af8921fbc..8f621dce3 100644
--- a/src/confd/src/syslog.c
+++ b/src/confd/src/syslog.c
@@ -344,7 +344,7 @@ static int file_change(sr_session_ctx_t *session, struct lyd_node *config,struct
}
srx_free_changes(tree);
- systemf("initctl -nbq touch sysklogd");
+ finit_reload("sysklogd");
return SR_ERR_OK;
}
@@ -377,7 +377,7 @@ static int remote_change(sr_session_ctx_t *session, struct lyd_node *config, str
}
}
- systemf("initctl -nbq touch sysklogd");
+ finit_reload("sysklogd");
return SR_ERR_OK;
}
@@ -410,7 +410,7 @@ static int rotate_change(sr_session_ctx_t *session, struct lyd_node *config, str
}
fclose(fp);
- systemf("initctl -nbq touch sysklogd");
+ finit_reload("sysklogd");
return SR_ERR_OK;
}
@@ -458,7 +458,7 @@ static int server_change(sr_session_ctx_t *session, struct lyd_node *config, str
sr_free_values(list, count);
fclose(fp);
done:
- systemf("initctl -nbq touch sysklogd");
+ finit_reload("sysklogd");
return SR_ERR_OK;
}
diff --git a/src/confd/src/system.c b/src/confd/src/system.c
index b0b3701a1..9286afdf0 100644
--- a/src/confd/src/system.c
+++ b/src/confd/src/system.c
@@ -268,8 +268,11 @@ static int change_clock(sr_session_ctx_t *session, struct lyd_node *config, stru
}
}
+ char zonelink[256];
+
+ snprintf(zonelink, sizeof(zonelink), "/usr/share/zoneinfo/%s", timezone);
(void)remove("/etc/localtime+");
- if (systemf("ln -sf /usr/share/zoneinfo/%s /etc/localtime+", timezone)) {
+ if (symlink(zonelink, "/etc/localtime+")) {
ERROR("No such timezone %s", timezone);
rc = SR_ERR_VALIDATION_FAILED;
}
@@ -306,9 +309,10 @@ static int change_ntp_client(sr_session_ctx_t *session, struct lyd_node *config,
case SR_EV_DONE:
if (!srx_enabled(session, XPATH_NTP_"/enabled")) {
(void)remove(NTP_CLIENT_CONF);
- systemf("rm -f /etc/chrony/sources.d/*");
+ rmrf("/etc/chrony/sources.d");
+ mkpath("/etc/chrony/sources.d", 0755);
/* Note: chronyd enable/disable is managed centrally in core.c */
- systemf("initctl -nbq touch chronyd");
+ finit_reload("chronyd");
return SR_ERR_OK;
}
@@ -318,7 +322,7 @@ static int change_ntp_client(sr_session_ctx_t *session, struct lyd_node *config,
}
/* Note: chronyd enable/disable is managed centrally in core.c */
- systemf("initctl -nbq touch chronyd");
+ finit_reload("chronyd");
return SR_ERR_OK;
default:
@@ -466,7 +470,7 @@ static int change_dns(sr_session_ctx_t *session, struct lyd_node *config, struct
(void)rename(RESOLV_NEXT, RESOLV_CONF);
}
- systemf("initctl -bq touch resolvconf");
+ finit_reload("resolvconf");
return SR_ERR_OK;
@@ -701,7 +705,9 @@ static int sys_del_user(char *user, bool silent)
ERROR("Error deleting user \"%s\"", user);
/* Ensure $HOME is removed at least. */
- systemf("rm -rf /home/%s", user);
+ char home[256];
+ snprintf(home, sizeof(home), "/home/%s", user);
+ rmrf(home);
return SR_ERR_SYS;
}
@@ -825,9 +831,14 @@ static int sys_add_user(sr_session_ctx_t *sess, char *name)
* /home/%s/.ssh/authorized_keys file. This creates a both the
* directory and the symlink owned by root to prevent tampering.
*/
+ char src[256], dst[256];
+
DEBUG("Adding secure /home/%s/.ssh directory.", name);
fmkpath(0750, "/home/%s/.ssh", name);
- systemf("ln -sf /var/run/sshd/%s.keys /home/%s/.ssh/authorized_keys", name, name);
+ snprintf(src, sizeof(src), "/var/run/sshd/%s.keys", name);
+ snprintf(dst, sizeof(dst), "/home/%s/.ssh/authorized_keys", name);
+ erasef("/home/%s/.ssh/authorized_keys", name);
+ symlink(src, dst);
return SR_ERR_OK;
}
@@ -1491,7 +1502,7 @@ static int change_editor(sr_session_ctx_t *session, struct lyd_node *config, str
continue;
erase(alt);
- rc = systemf("ln -s %s %s", map[i].path, alt);
+ rc = symlink(map[i].path, alt);
if (rc)
ERROR("Failed setting system editor '%s'", map[i].editor);
}
@@ -1611,7 +1622,7 @@ static int change_hostname(sr_session_ctx_t *session, struct lyd_node *config, s
}
/* Use hostname.d for deterministic hostname management */
- systemf("mkdir -p /etc/hostname.d");
+ mkpath("/etc/hostname.d", 0755);
fp = fopen("/etc/hostname.d/50-configured", "w");
if (!fp)
diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc
index 6f9c6af55..27bc3fe9b 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@2026-03-09.yang"
- "infix-services@2026-03-04.yang"
+ "infix-services@2026-03-20.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-03-09.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-ntp.yang b/src/confd/yang/confd/infix-ntp.yang
index 19addf890..9a2582b8c 100644
--- a/src/confd/yang/confd/infix-ntp.yang
+++ b/src/confd/yang/confd/infix-ntp.yang
@@ -33,8 +33,10 @@ module infix-ntp {
description "Add stratumweight to NTP server configuration.
Allows tuning the weight of stratum in NTP source selection
- relative to measured distance. Setting to 0.0 ensures lower
- stratum sources are always preferred regardless of distance.";
+ relative to measured distance. Higher values give more preference
+ to lower-stratum sources; use 1.0 to guarantee stratum-based
+ selection on local networks. Setting to 0.0 disables stratum
+ weighting (pure distance-based selection).";
reference "internal";
}
@@ -75,13 +77,19 @@ module infix-ntp {
description
"Weight of stratum in NTP source selection relative to measured distance.
- A one-stratum difference counts as this many seconds of additional
- distance when comparing candidates.
+ Each stratum level adds this many seconds of effective distance when
+ comparing candidates. Higher values give more preference to lower-stratum
+ sources.
- The default (0.001 = 1ms) means stratum only wins when distance
- differences are less than 1ms. Setting it to 0.0 ensures that a
- lower-stratum source is always preferred over a higher-stratum source,
- regardless of their measured distances.";
+ The default (0.001 = 1ms per stratum level) is suitable for most networks.
+ On a LAN where all servers have similar round-trip times, a lower-stratum
+ source wins unless its synchronization distance degrades (e.g., due to
+ missed polls) by more than this amount.
+
+ Use 1.0 or larger to guarantee stratum-based selection on local networks
+ regardless of transient distance fluctuations. Setting it to 0.0 disables
+ stratum weighting; selection is then based purely on measured
+ synchronization distance.";
}
container makestep {
diff --git a/src/confd/yang/confd/infix-services.yang b/src/confd/yang/confd/infix-services.yang
index 4f87b5bb8..7d64f8489 100644
--- a/src/confd/yang/confd/infix-services.yang
+++ b/src/confd/yang/confd/infix-services.yang
@@ -31,9 +31,11 @@ module infix-services {
contact "kernelkit@googlegroups.com";
description "Infix services, generic.";
- revision 2026-03-04 {
- description "Add hostname leaf to mdns container for avahi host-name override.
- Add neighbors container to mdns for mDNS-SD neighbor table.";
+ revision 2026-03-20 {
+ description "Add hostname leaf to mdns container for avahi host-name override.
+ Add neighbors container to mdns for mDNS-SD neighbor table.
+ 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";
@@ -266,6 +268,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@2026-03-04.yang b/src/confd/yang/confd/infix-services@2026-03-20.yang
similarity index 100%
rename from src/confd/yang/confd/infix-services@2026-03-04.yang
rename to src/confd/yang/confd/infix-services@2026-03-20.yang
diff --git a/src/confd/yang/confd/infix-system.yang b/src/confd/yang/confd/infix-system.yang
index 16c8de18e..3e60a5b84 100644
--- a/src/confd/yang/confd/infix-system.yang
+++ b/src/confd/yang/confd/infix-system.yang
@@ -32,8 +32,10 @@ module infix-system {
description "Add stratumweight to NTP client configuration.
Allows tuning the weight of stratum in NTP source selection
- relative to measured distance. Setting to 0.0 ensures lower
- stratum sources are always preferred regardless of distance.";
+ relative to measured distance. Higher values give more preference
+ to lower-stratum sources; use 1.0 to guarantee stratum-based
+ selection on local networks. Setting to 0.0 disables stratum
+ weighting (pure distance-based selection).";
reference "internal";
}
@@ -327,13 +329,19 @@ module infix-system {
description
"Weight of stratum in NTP source selection relative to measured distance.
- A one-stratum difference counts as this many seconds of additional
- distance when comparing candidates.
+ Each stratum level adds this many seconds of effective distance when
+ comparing candidates. Higher values give more preference to lower-stratum
+ sources.
- The default (0.001 = 1ms) means stratum only wins when distance
- differences are less than 1ms. Setting it to 0.0 ensures that a
- lower-stratum source is always preferred over a higher-stratum source,
- regardless of their measured distances.";
+ The default (0.001 = 1ms per stratum level) is suitable for most networks.
+ On a LAN where all servers have similar round-trip times, a lower-stratum
+ source wins unless its synchronization distance degrades (e.g., due to
+ missed polls) by more than this amount.
+
+ Use 1.0 or larger to guarantee stratum-based selection on local networks
+ regardless of transient distance fluctuations. Setting it to 0.0 disables
+ stratum weighting; selection is then based purely on measured
+ synchronization distance.";
}
}
diff --git a/src/statd/avahi.c b/src/statd/avahi.c
index ced30cdf7..4d27a8ecc 100644
--- a/src/statd/avahi.c
+++ b/src/statd/avahi.c
@@ -16,6 +16,8 @@
* (same thread — no locking required).
*/
+#include
+#include
#include
#include
#include
@@ -41,14 +43,14 @@ struct AvahiWatch {
AvahiWatchEvent last_event;
AvahiWatchCallback callback;
void *userdata;
- struct avahi_ctx *ctx;
+ struct mdns_ctx *ctx;
};
struct AvahiTimeout {
ev_timer timer; /* MUST be first */
AvahiTimeoutCallback callback;
void *userdata;
- struct avahi_ctx *ctx;
+ struct mdns_ctx *ctx;
};
/* --------------------------------------------------------------------------
@@ -71,7 +73,7 @@ static void watch_io_cb(struct ev_loop *loop, ev_io *w, int events)
static AvahiWatch *watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent event,
AvahiWatchCallback callback, void *userdata)
{
- struct avahi_ctx *ctx = api->userdata;
+ struct mdns_ctx *ctx = api->userdata;
struct AvahiWatch *w;
int ev_events = 0;
@@ -128,7 +130,7 @@ static void timeout_cb(struct ev_loop *loop, ev_timer *t, int events)
static AvahiTimeout *timeout_new(const AvahiPoll *api, const struct timeval *tv,
AvahiTimeoutCallback callback, void *userdata)
{
- struct avahi_ctx *ctx = api->userdata;
+ struct mdns_ctx *ctx = api->userdata;
struct AvahiTimeout *t;
t = calloc(1, sizeof(*t));
@@ -186,7 +188,7 @@ static void timeout_free(AvahiTimeout *t)
* In-memory state helpers
* -------------------------------------------------------------------------- */
-static struct avahi_neighbor *find_neighbor(struct avahi_ctx *ctx, const char *hostname)
+static struct avahi_neighbor *find_neighbor(struct mdns_ctx *ctx, const char *hostname)
{
struct avahi_neighbor *n;
@@ -197,7 +199,7 @@ static struct avahi_neighbor *find_neighbor(struct avahi_ctx *ctx, const char *h
return NULL;
}
-static struct avahi_neighbor *get_neighbor(struct avahi_ctx *ctx, const char *hostname)
+static struct avahi_neighbor *get_neighbor(struct mdns_ctx *ctx, const char *hostname)
{
struct avahi_neighbor *n = find_neighbor(ctx, hostname);
@@ -239,7 +241,7 @@ static void add_addr(struct avahi_neighbor *n, const char *addr)
/*
* Find service in flat list by 5-tuple (ifindex, proto, name, type, domain).
*/
-static struct avahi_service *find_service(struct avahi_ctx *ctx,
+static struct avahi_service *find_service(struct mdns_ctx *ctx,
int ifindex, AvahiProtocol proto,
const char *name, const char *type,
const char *domain)
@@ -260,7 +262,7 @@ static struct avahi_service *find_service(struct avahi_ctx *ctx,
* used after removing one 5-tuple entry to decide if the DS entry should
* be removed too (another interface may still have the same service).
*/
-static int svc_ds_entry_exists(struct avahi_ctx *ctx, const char *hostname, const char *name)
+static int svc_ds_entry_exists(struct mdns_ctx *ctx, const char *hostname, const char *name)
{
struct avahi_service *s;
@@ -271,7 +273,7 @@ static int svc_ds_entry_exists(struct avahi_ctx *ctx, const char *hostname, cons
return 0;
}
-static int neighbor_has_services(struct avahi_ctx *ctx, const char *hostname)
+static int neighbor_has_services(struct mdns_ctx *ctx, const char *hostname)
{
struct avahi_service *s;
@@ -313,7 +315,7 @@ static void free_neighbor(struct avahi_neighbor *n)
free(n);
}
-static void free_all(struct avahi_ctx *ctx)
+static void free_all(struct mdns_ctx *ctx)
{
struct avahi_service *s;
struct avahi_neighbor *n;
@@ -348,7 +350,7 @@ static int sr_setstr(sr_session_ctx_t *ses, const char *xpath, const char *val)
int err = sr_set_item_str(ses, xpath, val, NULL, 0);
if (err)
- ERROR("avahi: sr_set_item_str(%s): %s", xpath, sr_strerror(err));
+ ERROR("mdns: sr_set_item_str(%s): %s", xpath, sr_strerror(err));
return err;
}
@@ -370,7 +372,7 @@ static const char *xpath_str(char *buf, size_t sz, const char *val)
* Push a resolver result to the operational DS.
* new_addr is non-NULL only when a new address was just added in memory.
*/
-static void ds_push_resolver(struct avahi_ctx *ctx, struct avahi_service *svc,
+static void ds_push_resolver(struct mdns_ctx *ctx, struct avahi_service *svc,
const char *new_addr)
{
char qname[258]; /* quoted svc->name for safe XPath predicates */
@@ -441,10 +443,10 @@ static void ds_push_resolver(struct avahi_ctx *ctx, struct avahi_service *svc,
err = sr_apply_changes(ctx->sr_ses, 0);
if (err)
- ERROR("avahi: sr_apply_changes: %s", sr_strerror(err));
+ ERROR("mdns: sr_apply_changes: %s", sr_strerror(err));
}
-static void ds_delete_service(struct avahi_ctx *ctx, const char *hostname, const char *name)
+static void ds_delete_service(struct mdns_ctx *ctx, const char *hostname, const char *name)
{
char qname[258];
char xpath[512];
@@ -456,7 +458,7 @@ static void ds_delete_service(struct avahi_ctx *ctx, const char *hostname, const
sr_delete_item(ctx->sr_ses, xpath, 0);
}
-static void ds_delete_neighbor(struct avahi_ctx *ctx, const char *hostname)
+static void ds_delete_neighbor(struct mdns_ctx *ctx, const char *hostname)
{
char xpath[512];
@@ -465,7 +467,7 @@ static void ds_delete_neighbor(struct avahi_ctx *ctx, const char *hostname)
sr_delete_item(ctx->sr_ses, xpath, 0);
}
-static void ds_clear_all(struct avahi_ctx *ctx)
+static void ds_clear_all(struct mdns_ctx *ctx)
{
sr_delete_item(ctx->sr_ses, XPATH_BASE, 0);
sr_apply_changes(ctx->sr_ses, 0);
@@ -484,7 +486,7 @@ static void resolver_cb(AvahiServiceResolver *r,
AvahiLookupResultFlags flags,
void *userdata)
{
- struct avahi_ctx *ctx = userdata;
+ struct mdns_ctx *ctx = userdata;
char addrstr[AVAHI_ADDRESS_STR_MAX] = "";
struct avahi_neighbor *n;
struct avahi_service *svc;
@@ -508,7 +510,7 @@ static void resolver_cb(AvahiServiceResolver *r,
/* Find or create neighbor (tracks addresses) */
n = get_neighbor(ctx, hostname);
if (!n) {
- ERROR("avahi: out of memory for neighbor '%s'", hostname);
+ ERROR("mdns: out of memory for neighbor '%s'", hostname);
goto done;
}
@@ -523,7 +525,7 @@ static void resolver_cb(AvahiServiceResolver *r,
if (!svc) {
svc = calloc(1, sizeof(*svc));
if (!svc) {
- ERROR("avahi: out of memory for service '%s'", name);
+ ERROR("mdns: out of memory for service '%s'", name);
goto done;
}
svc->ifindex = iface;
@@ -565,7 +567,7 @@ static void service_browser_cb(AvahiServiceBrowser *b,
AvahiLookupResultFlags flags,
void *userdata)
{
- struct avahi_ctx *ctx = userdata;
+ struct mdns_ctx *ctx = userdata;
(void)b;
(void)flags;
@@ -576,7 +578,7 @@ static void service_browser_cb(AvahiServiceBrowser *b,
name, type, domain,
AVAHI_PROTO_UNSPEC, 0,
resolver_cb, ctx))
- DEBUG("avahi: resolver_new(%s) failed: %s", name,
+ DEBUG("mdns: resolver_new(%s) failed: %s", name,
avahi_strerror(avahi_client_errno(ctx->client)));
break;
@@ -624,7 +626,7 @@ static void type_browser_cb(AvahiServiceTypeBrowser *b,
AvahiLookupResultFlags flags,
void *userdata)
{
- struct avahi_ctx *ctx = userdata;
+ struct mdns_ctx *ctx = userdata;
(void)b;
(void)flags;
@@ -651,14 +653,14 @@ static void type_browser_cb(AvahiServiceTypeBrowser *b,
0,
service_browser_cb, ctx);
if (!te->browser) {
- DEBUG("avahi: service_browser_new(%s) failed: %s", type,
+ DEBUG("mdns: service_browser_new(%s) failed: %s", type,
avahi_strerror(avahi_client_errno(ctx->client)));
free(te);
return;
}
LIST_INSERT_HEAD(&ctx->type_entries, te, link);
- DEBUG("avahi: browsing service type %s", type);
+ DEBUG("mdns: browsing service type %s", type);
break;
}
@@ -683,15 +685,76 @@ static void type_browser_cb(AvahiServiceTypeBrowser *b,
}
}
+/*
+ * Check if mDNS is enabled in the running datastore.
+ * Opens a temporary session to avoid disturbing the operational-DS session.
+ * Returns true if enabled or if the check cannot be performed (fail-safe).
+ */
+static bool mdns_is_enabled(struct mdns_ctx *ctx)
+{
+ sr_session_ctx_t *sess = NULL;
+ sr_data_t *data = NULL;
+ const char *s;
+ bool enabled = true; /* fail-safe: assume enabled */
+
+ if (!ctx->sr_conn)
+ return true;
+
+ if (sr_session_start(ctx->sr_conn, SR_DS_RUNNING, &sess))
+ return true;
+
+ if (!sr_get_node(sess, "/infix-services:mdns/enabled", 0, &data) && data) {
+ s = lyd_get_value(data->tree);
+ if (s && !strcmp(s, "false"))
+ enabled = false;
+ sr_release_data(data);
+ }
+
+ sr_session_stop(sess);
+ return enabled;
+}
+
+/*
+ * Retry timer callback: fires 2 s after AVAHI_CLIENT_FAILURE (and repeats up
+ * to 3 times). Only logs ERROR once all retries are exhausted AND mDNS is
+ * enabled in the running config — this avoids noisy errors when the operator
+ * has simply disabled the mDNS service.
+ *
+ * Example log (mDNS enabled, daemon stays down):
+ * avahi: mDNS daemon not responding (attempt 3/3) — check that the mdns
+ * service is running
+ */
+static void mdns_retry_cb(struct ev_loop *loop, ev_timer *w, int revents)
+{
+ struct mdns_ctx *ctx = (struct mdns_ctx *)
+ ((char *)w - offsetof(struct mdns_ctx, retry_timer));
+
+ ctx->fail_count++;
+ if (ctx->fail_count < 3) {
+ ev_timer_set(w, 2.0, 0.0);
+ ev_timer_start(loop, w);
+ return;
+ }
+
+ if (mdns_is_enabled(ctx))
+ ERROR("mdns: mDNS daemon not responding (attempt %d/3) — "
+ "check that the mdns service is running", ctx->fail_count);
+}
+
static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
{
- struct avahi_ctx *ctx = userdata;
+ struct mdns_ctx *ctx = userdata;
ctx->client = c;
switch (state) {
case AVAHI_CLIENT_S_RUNNING:
- INFO("avahi: client running");
+ if (ctx->fail_count > 0) {
+ ev_timer_stop(ctx->loop, &ctx->retry_timer);
+ NOTE("mdns: mDNS daemon reconnected");
+ ctx->fail_count = 0;
+ }
+ INFO("mdns: client running");
if (ctx->type_browser)
break; /* Already browsing */
@@ -702,18 +765,25 @@ static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
0,
type_browser_cb, ctx);
if (!ctx->type_browser)
- ERROR("avahi: service_type_browser_new failed: %s",
+ ERROR("mdns: service_type_browser_new failed: %s",
avahi_strerror(avahi_client_errno(ctx->client)));
break;
case AVAHI_CLIENT_FAILURE:
- ERROR("avahi: client failure: %s",
- avahi_strerror(avahi_client_errno(c)));
-
/*
- * Browsers are internally invalidated when the daemon dies.
- * Free them explicitly here so they're recreated on reconnect.
+ * The daemon went away. AVAHI_CLIENT_NO_FAIL means the client
+ * will reconnect automatically — we just need to clean up the
+ * now-invalid browsers so they're recreated on reconnect.
+ *
+ * Suppress the immediate ERROR; start a 2-second timer that
+ * will log only if the daemon stays down for 3 attempts (~6 s)
+ * and mDNS is enabled in the running config.
*/
+ if (!ev_is_active(&ctx->retry_timer)) {
+ ev_timer_init(&ctx->retry_timer, mdns_retry_cb, 2.0, 0.0);
+ ev_timer_start(ctx->loop, &ctx->retry_timer);
+ }
+
{
struct avahi_type_entry *te;
@@ -744,12 +814,13 @@ static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata)
* Public interface
* -------------------------------------------------------------------------- */
-int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn)
+int mdns_ctx_init(struct mdns_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn)
{
int avahi_err;
memset(ctx, 0, sizeof(*ctx));
- ctx->loop = loop;
+ ctx->loop = loop;
+ ctx->sr_conn = sr_conn;
LIST_INIT(&ctx->neighbors);
LIST_INIT(&ctx->services);
LIST_INIT(&ctx->type_entries);
@@ -757,7 +828,7 @@ int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *s
/* Dedicated operational session for push writes (avoids sharing
* sr_query_ses which the journal thread also uses). */
if (sr_session_start(sr_conn, SR_DS_OPERATIONAL, &ctx->sr_ses)) {
- ERROR("avahi: failed to start sysrepo session");
+ ERROR("mdns: failed to start sysrepo session");
return -1;
}
@@ -776,20 +847,23 @@ int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *s
client_cb, ctx,
&avahi_err);
if (!ctx->client) {
- ERROR("avahi: client_new failed: %s", avahi_strerror(avahi_err));
+ ERROR("mdns: client_new failed: %s", avahi_strerror(avahi_err));
sr_session_stop(ctx->sr_ses);
ctx->sr_ses = NULL;
return -1;
}
- INFO("avahi: mDNS neighbor monitor initialized");
+ INFO("mdns: mDNS neighbor monitor initialized");
return 0;
}
-void avahi_ctx_exit(struct avahi_ctx *ctx)
+void mdns_ctx_exit(struct mdns_ctx *ctx)
{
struct avahi_type_entry *te;
+ if (ev_is_active(&ctx->retry_timer))
+ ev_timer_stop(ctx->loop, &ctx->retry_timer);
+
/* Free browsers explicitly before freeing the client */
while (!LIST_EMPTY(&ctx->type_entries)) {
te = LIST_FIRST(&ctx->type_entries);
@@ -813,5 +887,5 @@ void avahi_ctx_exit(struct avahi_ctx *ctx)
}
free_all(ctx);
- INFO("avahi: mDNS neighbor monitor stopped");
+ INFO("mdns: mDNS neighbor monitor stopped");
}
diff --git a/src/statd/avahi.h b/src/statd/avahi.h
index 4e94abd8e..fde93e368 100644
--- a/src/statd/avahi.h
+++ b/src/statd/avahi.h
@@ -51,18 +51,21 @@ struct avahi_type_entry {
LIST_ENTRY(avahi_type_entry) link;
};
-struct avahi_ctx {
+struct mdns_ctx {
struct ev_loop *loop;
+ sr_conn_ctx_t *sr_conn; /* Connection for running-DS config queries */
sr_session_ctx_t *sr_ses; /* Dedicated operational DS write session */
AvahiClient *client;
AvahiServiceTypeBrowser *type_browser;
AvahiPoll poll_api; /* libev-backed vtable */
+ unsigned int fail_count; /* Consecutive avahi-daemon connection failures */
+ ev_timer retry_timer; /* Deferred error-log timer */
LIST_HEAD(, avahi_neighbor) neighbors;
LIST_HEAD(, avahi_service) services; /* Flat list; keyed by 5-tuple */
LIST_HEAD(, avahi_type_entry) type_entries;
};
-int avahi_ctx_init(struct avahi_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn);
-void avahi_ctx_exit(struct avahi_ctx *ctx);
+int mdns_ctx_init(struct mdns_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *sr_conn);
+void mdns_ctx_exit(struct mdns_ctx *ctx);
#endif
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 f1fda8283..62e32cd73 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/src/statd/statd.c b/src/statd/statd.c
index d9920076a..532760730 100644
--- a/src/statd/statd.c
+++ b/src/statd/statd.c
@@ -70,7 +70,7 @@ struct statd {
sr_conn_ctx_t *sr_conn; /* Connection (owns YANG context) */
struct ev_loop *ev_loop;
struct journal_ctx journal; /* Journal thread context */
- struct avahi_ctx avahi; /* mDNS neighbor monitor */
+ struct mdns_ctx mdns; /* mDNS neighbor monitor */
};
static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent,
@@ -524,7 +524,7 @@ int main(int argc, char *argv[])
return EXIT_FAILURE;
}
- if (avahi_ctx_init(&statd.avahi, statd.ev_loop, statd.sr_conn))
+ if (mdns_ctx_init(&statd.mdns, statd.ev_loop, statd.sr_conn))
INFO("mDNS neighbor monitoring not available");
/* Signal readiness to Finit */
@@ -536,7 +536,7 @@ int main(int argc, char *argv[])
/* We should never get here during normal operation */
INFO("Status daemon shutting down");
- avahi_ctx_exit(&statd.avahi);
+ mdns_ctx_exit(&statd.mdns);
journal_stop(&statd.journal);
unsub_to_all(&statd);
diff --git a/test/case/hardware/gps_simple/test.py b/test/case/hardware/gps_simple/test.py
index beb872676..5f6393f95 100755
--- a/test/case/hardware/gps_simple/test.py
+++ b/test/case/hardware/gps_simple/test.py
@@ -85,8 +85,8 @@ def verify_position(target, name="gps0"):
until(lambda: gps.is_activated(target, "gps1"), attempts=500)
with test.step("Verify both GPS receivers have a fix"):
- until(lambda: gps.has_fix(target, "gps0"), attempts=60)
- until(lambda: gps.has_fix(target, "gps1"), attempts=60)
+ until(lambda: gps.has_position(target, "gps0"), attempts=60)
+ until(lambda: gps.has_position(target, "gps1"), attempts=60)
with test.step("Verify gps0 position is near the coordinates"):
verify_position(target, "gps0")
@@ -107,8 +107,8 @@ def verify_position(target, name="gps0"):
until(lambda: gps.is_activated(target, "gps1"), attempts=500)
with test.step("Verify both GPS receivers have a fix"):
- until(lambda: gps.has_fix(target, "gps0"), attempts=60)
- until(lambda: gps.has_fix(target, "gps1"), attempts=60)
+ until(lambda: gps.has_position(target, "gps0"), attempts=60)
+ until(lambda: gps.has_position(target, "gps1"), attempts=60)
with test.step("Verify gps0 position is near the coordinates"):
verify_position(target, "gps0")
diff --git a/test/case/interfaces/iface_phys_address/test.py b/test/case/interfaces/iface_phys_address/test.py
index c6a7d27a6..260e1f698 100755
--- a/test/case/interfaces/iface_phys_address/test.py
+++ b/test/case/interfaces/iface_phys_address/test.py
@@ -35,15 +35,15 @@ 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']")
+ components = data.get("hardware", {}).get("component", {})
+ cmac = components["mainboard"].get("phys-address", "") \
+ if "mainboard" in components else ""
STATIC = "02:01:00:c0:ff:ee"
OFFSET = "00:00:00:00:ff:aa"
@@ -88,9 +88,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 +107,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/case/ntp/client_stratum_selection/test.py b/test/case/ntp/client_stratum_selection/test.py
index 073d4a7e8..f52be5461 100755
--- a/test/case/ntp/client_stratum_selection/test.py
+++ b/test/case/ntp/client_stratum_selection/test.py
@@ -107,7 +107,7 @@
"system": {
"ntp": {
"enabled": True,
- "infix-system:stratum-weight": 0.0,
+ "infix-system:stratum-weight": 1.0,
"server": [{
"name": "srv1",
"udp": {
diff --git a/test/infamy/env.py b/test/infamy/env.py
index 19ca29577..6a6359ac8 100644
--- a/test/infamy/env.py
+++ b/test/infamy/env.py
@@ -166,6 +166,7 @@ def attach(self, node, port="mgmt", protocol=None, test_reset=True, username=Non
yangdir=self.args.yangdir)
if test_reset:
dev.test_reset()
+ util.until(lambda: self.is_reachable(node, cport), 30)
return dev
if protocol == "ssh":
@@ -181,6 +182,7 @@ def attach(self, node, port="mgmt", protocol=None, test_reset=True, username=Non
yangdir=self.args.yangdir)
if test_reset:
dev.test_reset()
+ util.until(lambda: self.is_reachable(node, cport), 30)
return dev
raise Exception(f"Unsupported management procotol \"{protocol}\"")
diff --git a/test/infamy/gps.py b/test/infamy/gps.py
index d3743a50c..3ec9e31d7 100644
--- a/test/infamy/gps.py
+++ b/test/infamy/gps.py
@@ -208,3 +208,13 @@ def has_fix(target, name="gps0"):
if not state:
return False
return state.get("fix-mode") in ("2d", "3d")
+
+
+def has_position(target, name="gps0"):
+ """Check if GPS has a fix and all position fields are populated."""
+ state = get_gps_state(target, name)
+ if not state:
+ return False
+ if state.get("fix-mode") not in ("2d", "3d"):
+ return False
+ return all(k in state for k in ("latitude", "longitude", "altitude", "satellites-used"))
diff --git a/test/infamy/restconf.py b/test/infamy/restconf.py
index 1dc4453d9..b59f6407b 100644
--- a/test/infamy/restconf.py
+++ b/test/infamy/restconf.py
@@ -489,10 +489,20 @@ def get_data(self, xpath=None, parse=True):
return data
- def copy(self, source, target):
+ def copy(self, source, target, retries=3):
factory = self.get_datastore(source)
data = factory.print_mem("json", with_siblings=True, pretty=False)
- self.put_datastore(target, json.loads(data))
+ last_error = None
+ for attempt in range(0, retries):
+ try:
+ self.put_datastore(target, json.loads(data))
+ return
+ except requests.exceptions.ConnectionError as e:
+ last_error = e
+ if attempt < retries - 1:
+ print(f"Failed copy {source}->{target}: {e} Retrying ...")
+ time.sleep(1)
+ raise last_error
def reboot(self):
self.call_rpc("ietf-system:system-restart")