confd: replace sysrepo-plugind + bootstrap + load with single daemon

On single-core Cortex-A7, the YANG bootstrap and config loading is the
dominant boot bottleneck.  The current sequence spawns three serial
phases (bootstrap, sysrepo-plugind, load), each performing independent
sr_connect()/sr_disconnect() cycles, and every sysrepoctl/sysrepocfg
invocation is a fork+exec that rebuilds SHM from scratch.

Replace all three with a single confd binary that does one sr_connect()
and performs all datastore operations in-process:

 - Wipe stale /dev/shm/sr_* for a clean slate
 - sr_install_factory_config() from the generated JSON
 - Smart migration: compare config version via libjansson, only
   fork+exec the migrate script when versions actually differ
 - Load startup-config (or test-config) via lyd_parse_data() +
   sr_replace_config(), mirroring what sysrepocfg -I does internally
 - On failure: revert to factory-default, load failure-config, set
   login banners (Fail Secure mode)
 - On first boot: copy factory-default to running, export to file
 - dlopen plugins and enter event loop

The bootstrap shell script is split: config generation (gen-hostname,
gen-interfaces, etc.) stays in the new gen-config script, while all
sysrepo operations move into the C daemon.  The finit boot sequence
collapses from 5 stanzas to 2 (gen-config -> confd).

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-03-20 16:18:03 +01:00
parent 3b85fdff87
commit 77a7915e1f
8 changed files with 881 additions and 265 deletions
@@ -1,20 +0,0 @@
#!/bin/sh
# Check if /cfg/startup-config.cfg needs to be migrated to new syntax.
# Backup of the original is created in /cfg/backup/ for old versions,
# the migrate tool inserts old version in name before .cfg extension.
CONFIG_FILE="/cfg/startup-config.cfg"
BACKUP_FILE="/cfg/backup/startup-config.cfg"
BACKUP_DIR="$(dirname "$BACKUP_FILE")"
mkdir -p "$BACKUP_DIR"
chown root:wheel "$BACKUP_DIR"
chmod 0770 "$BACKUP_DIR"
if [ ! -f "$CONFIG_FILE" ]; then
logger -I $$ -k -p user.notice -t $(basename "$0") "No $(basename "$CONFIG_FILE" .cfg) yet, likely factory reset."
exit 0
elif migrate -cq "$CONFIG_FILE"; then
exit 0
fi
migrate -i -b "$BACKUP_FILE" "$CONFIG_FILE"
+11 -19
View File
@@ -1,27 +1,19 @@
#set DEBUG=1 #set DEBUG=1
# Phase 1: Generate factory/failure/test configs (shell)
run name:bootstrap log:prio:user.notice norestart <usr/ixinit> \ run name:bootstrap log:prio:user.notice norestart <usr/ixinit> \
[S] /usr/libexec/confd/bootstrap \ [S] /usr/libexec/confd/gen-config \
-- Bootstrapping YANG datastore -- Generating factory configuration
run name:error :1 log:console norestart if:<run/bootstrap/failure> \ run name:error :1 log:console norestart if:<run/bootstrap/failure> \
[S] /usr/libexec/confd/error -- [S] /usr/libexec/confd/error --
service name:confd log:prio:daemon.err <run/bootstrap/success> \ # Phase 2: Single daemon handles datastore init, config load, and plugins
[S12345] sysrepo-plugind -f -p /run/confd.pid -n -v warning \ # log:prio:daemon.err
service name:confd <run/bootstrap/success> env:/etc/default/confd \
[S12345] confd -f -n -v warning -p /run/confd.pid \
-F /etc/factory-config.cfg \
-S /cfg/startup-config.cfg \
-E /etc/failure-config.cfg \
-t $CONFD_TIMEOUT \
-- Configuration daemon -- Configuration daemon
# Bootstrap system with startup-config
run name:startup log:prio:user.notice norestart <pid/confd> 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:<run/startup/failure> \
[S] <pid/confd> /usr/libexec/confd/load -t $CONFD_TIMEOUT failure-config \
-- Loading failure-config
run name:error :2 log:console norestart \
if:<run/failure/failure> \
[S] /usr/libexec/confd/error --
+2 -3
View File
@@ -1,4 +1,3 @@
pkglibexec_SCRIPTS = bootstrap error load gen-hostname \ pkglibexec_SCRIPTS = gen-hostname gen-interfaces gen-motd gen-hardware \
gen-interfaces gen-motd gen-hardware gen-version \ gen-version mstpd-wait-online wait-interface
mstpd-wait-online wait-interface
sbin_SCRIPTS = dagger migrate firewall sbin_SCRIPTS = dagger migrate firewall
@@ -1,42 +1,32 @@
#!/bin/sh #!/bin/sh
# Bootstrap system factory-config, failure-config, test-config and sysrepo db. # Generate factory-config, failure-config, and test-config files.
# #
######################################################################## # These configs are derived from default settings snippets in
# The system factory-config, failure-config and test-config are derived # /usr/share/confd/factory.d, and generated snippets (e.g., hostname
# from default settings snippets, from /usr/share/confd/factory.d, and # based on base MAC address, number of interfaces).
# some generated snippets, e.g., hostname (based on base MAC address)
# and number of interfaces.
# #
# The resulting factory-config is used to create the syrepo db (below) # The sysrepo datastore operations (loading factory defaults, startup
# {factory} datastore. Hence, the factory-config file must match the # config, migration) are handled by the confd daemon.
# the YANG models of the active image.
######################################################################## ########################################################################
# NOTE: with the Infix defaults, a br2-external can provide a build-time # NOTE: with the Infix defaults, a br2-external can provide a build-time
# /etc/factory-config.cfg to override the behavior of this script. # /etc/factory-config.cfg to override the behavior of this script.
# #
# This applies also for /etc/failure-config.cfg, but we recommend # This applies also for /etc/failure-config.cfg, but we recommend
# strongly that you instead provide gen-err-custom, see below. # 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 # Log functions
critical()
{
logger -i -p user.crit -t bootstrap "$1" 2>/dev/null || echo "$1"
}
err() err()
{ {
logger -i -p user.err -t bootstrap "$1" 2>/dev/null || echo "$1" logger -i -p user.err -t gen-config "$1" 2>/dev/null || echo "$1"
} }
# When logging errors, generating /etc/issue* or /etc/banner (SSH) log()
. /etc/os-release {
logger -i -p user.notice -t gen-config "$1" 2>/dev/null || echo "$1"
}
# /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. # customize in an overlay when using Infix as an br2-external.
RC=/etc/confdrc RC=/etc/confdrc
if [ "$1" = "-f" ] && [ -f "$2" ]; then if [ "$1" = "-f" ] && [ -f "$2" ]; then
@@ -79,25 +69,6 @@ collate()
fi 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() gen_factory_cfg()
{ {
# Fetch defaults, simplifies sort in collate() # Fetch defaults, simplifies sort in collate()
@@ -145,49 +116,18 @@ gen_test_cfg()
collate "$TEST_GEN" "$TEST_CFG" "$TEST_D" collate "$TEST_GEN" "$TEST_CFG" "$TEST_D"
} }
# Both factory-config and failure-config are generated every boot log "Starting up, calling gen_factory_cfg()"
# regardless if there is a static /etc/factory-config.cfg or not.
gen_factory_cfg gen_factory_cfg
log "Starting up, calling gen_failure_cfg()"
gen_failure_cfg gen_failure_cfg
if [ -f "/mnt/aux/test-mode" ]; then if [ -f "/mnt/aux/test-mode" ]; then
gen_test_cfg gen_test_cfg
sysrepoctl -c infix-test -e test-mode-enable
fi fi
if [ -n "$TESTING" ]; then # Ensure 'admin' group users always have access to /cfg
echo "Done." mkdir -p "$CFG_PATH_"
exit 0 chgrp wheel "$CFG_PATH_" 2>/dev/null
fi chmod g+w "$CFG_PATH_" 2>/dev/null
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 log "All done."
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
exit $rc
-143
View File
@@ -1,143 +0,0 @@
#!/bin/sh
# load [-b] <startup-config | failure-config | test-config>
#
# Import a configuration to the sysrepo datastore using `sysrepocfg -Ifile`
#
# If the '-b' option is used we set the Finit <usr/bootstrap> 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
+2 -1
View File
@@ -84,7 +84,8 @@ PKG_CHECK_MODULES([crypt], [libxcrypt >= 4.4.27])
PKG_CHECK_MODULES([glib], [glib-2.0 >= 2.50 gio-2.0 gio-unix-2.0]) PKG_CHECK_MODULES([glib], [glib-2.0 >= 2.50 gio-2.0 gio-unix-2.0])
PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0]) PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0])
PKG_CHECK_MODULES([libite], [libite >= 2.6.1]) 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]) PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
# Control build with automake flags # Control build with automake flags
+7
View File
@@ -1,8 +1,15 @@
AM_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE -D_GNU_SOURCE -DYANG_PATH_=\"$(YANGDIR)\" AM_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE -D_GNU_SOURCE -DYANG_PATH_=\"$(YANGDIR)\"
AM_CPPFLAGS += -DCONFD_VERSION=\"$(PACKAGE_VERSION)\"
CLEANFILES = $(rauc_installer_sources) CLEANFILES = $(rauc_installer_sources)
plugindir = $(srpdplugindir) plugindir = $(srpdplugindir)
plugin_LTLIBRARIES = confd-plugin.la plugin_LTLIBRARIES = confd-plugin.la
sbin_PROGRAMS = confd
confd_CFLAGS = $(sysrepo_CFLAGS) $(libyang_CFLAGS) $(jansson_CFLAGS) $(libite_CFLAGS)
confd_LDADD = $(sysrepo_LIBS) $(libyang_LIBS) $(jansson_LIBS) $(libite_LIBS) -ldl
confd_SOURCES = main.c
confd_plugin_la_LDFLAGS = -module -avoid-version -shared confd_plugin_la_LDFLAGS = -module -avoid-version -shared
confd_plugin_la_CFLAGS = \ confd_plugin_la_CFLAGS = \
+840
View File
@@ -0,0 +1,840 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* confd - Infix configuration daemon
*
* Replaces sysrepo-plugind + bootstrap + load with a single binary.
* One sr_connect(), all datastore operations in-process, then load
* plugins and enter the event loop.
*
* Copyright (c) 2018 - 2021 Deutsche Telekom AG.
* Copyright (c) 2018 - 2021 CESNET, z.s.p.o.
*/
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <glob.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <jansson.h>
#include <libyang/libyang.h>
#include <libite/lite.h>
#include <sysrepo.h>
#include <sysrepo/version.h>
/* Callback type names from sysrepo plugin API */
#define SRP_INIT_CB "sr_plugin_init_cb"
#define SRP_CLEANUP_CB "sr_plugin_cleanup_cb"
#ifndef CONFD_VERSION
#define CONFD_VERSION PACKAGE_VERSION
#endif
#ifndef SRPD_PLUGINS_PATH
#define SRPD_PLUGINS_PATH "/usr/lib/sysrepo-plugind/plugins"
#endif
struct plugin {
void *handle;
char *name;
int (*init_cb)(sr_session_ctx_t *session, void **private_data);
void (*cleanup_cb)(sr_session_ctx_t *session, void *private_data);
void *private_data;
int initialized;
};
/* Protected flag for terminating */
static int loop_finish;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static void error_print(int sr_error, const char *format, ...)
{
va_list ap;
char msg[2048];
if (!sr_error)
snprintf(msg, sizeof(msg), "confd error: %s\n", format);
else
snprintf(msg, sizeof(msg), "confd error: %s (%s)\n", format, sr_strerror(sr_error));
va_start(ap, format);
vfprintf(stderr, msg, ap);
va_end(ap);
}
static void version_print(void)
{
printf("confd - Infix configuration daemon v%s, compiled with libsysrepo v%s\n\n",
CONFD_VERSION, SR_VERSION);
}
static void help_print(void)
{
printf(
"Usage:\n"
" confd [-h] [-V] [-v <level>] [-d] [-n] [-f] [-p pidfile]\n"
" [-F factory-config] [-S startup-config] [-E failure-config]\n"
" [-t timeout]\n"
"\n"
"Options:\n"
" -h, --help Prints usage help.\n"
" -V, --version Prints version information.\n"
" -v, --verbosity <level>\n"
" Change verbosity to a level (none, error, warning, info, debug) or\n"
" number (0, 1, 2, 3, 4).\n"
" -d, --debug Debug mode - not daemonized and logs to stderr.\n"
" -n, --foreground Run in foreground and log to syslog.\n"
" -f, --fatal-plugin-fail\n"
" Terminate if any plugin initialization fails.\n"
" -p, --pid-file <path>\n"
" Create a PID file at the specified path.\n"
" -F, --factory-config <path>\n"
" Factory default config file (default: /etc/factory-config.cfg).\n"
" -S, --startup-config <path>\n"
" Startup config file (default: /cfg/startup-config.cfg).\n"
" -E, --failure-config <path>\n"
" Failure fallback config file (default: /etc/failure-config.cfg).\n"
" -t, --timeout <ms> Sysrepo operation timeout in seconds (default: 60).\n"
"\n"
"Environment variable $SRPD_PLUGINS_PATH overwrites the default plugins directory.\n"
"\n");
}
static void signal_handler(int sig)
{
switch (sig) {
case SIGINT:
case SIGQUIT:
case SIGABRT:
case SIGTERM:
case SIGHUP:
pthread_mutex_lock(&lock);
if (!loop_finish) {
loop_finish = 1;
pthread_cond_signal(&cond);
} else {
error_print(0, "Exiting without a proper cleanup");
exit(EXIT_FAILURE);
}
pthread_mutex_unlock(&lock);
break;
default:
error_print(0, "Exiting on receiving an unhandled signal");
exit(EXIT_FAILURE);
}
}
static void handle_signals(void)
{
struct sigaction action;
sigset_t block_mask;
sigfillset(&block_mask);
action.sa_handler = signal_handler;
action.sa_mask = block_mask;
action.sa_flags = 0;
sigaction(SIGINT, &action, NULL);
sigaction(SIGQUIT, &action, NULL);
sigaction(SIGABRT, &action, NULL);
sigaction(SIGTERM, &action, NULL);
sigaction(SIGHUP, &action, NULL);
action.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &action, NULL);
sigaction(SIGTSTP, &action, NULL);
sigaction(SIGTTIN, &action, NULL);
sigaction(SIGTTOU, &action, NULL);
}
static void daemon_init(int debug, sr_log_level_t log_level)
{
pid_t pid = 0, sid = 0;
int fd = -1;
if (debug) {
handle_signals();
if (debug < 0)
goto done;
sr_log_stderr(log_level);
return;
}
pid = fork();
if (pid < 0) {
error_print(0, "fork() failed (%s).", strerror(errno));
exit(EXIT_FAILURE);
}
if (pid > 0)
exit(EXIT_SUCCESS);
handle_signals();
sid = setsid();
if (sid < 0) {
error_print(0, "setsid() failed (%s).", strerror(errno));
exit(EXIT_FAILURE);
}
if (chdir("/") < 0) {
error_print(0, "chdir() failed (%s).", strerror(errno));
exit(EXIT_FAILURE);
}
fd = open("/dev/null", O_RDWR, 0);
if (fd != -1) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
}
done:
sr_log_syslog("confd", log_level);
}
static int open_pidfile(const char *pidfile)
{
int pidfd;
pidfd = open(pidfile, O_RDWR | O_CREAT, 0640);
if (pidfd < 0) {
error_print(0, "Unable to open the PID file \"%s\" (%s).", pidfile, strerror(errno));
return -1;
}
if (lockf(pidfd, F_TLOCK, 0) < 0) {
if (errno == EACCES || errno == EAGAIN)
error_print(0, "Another instance of confd is running.");
else
error_print(0, "Unable to lock the PID file \"%s\" (%s).", pidfile, strerror(errno));
close(pidfd);
return -1;
}
return pidfd;
}
static int write_pidfile(int pidfd)
{
char pid[30] = {0};
int pid_len;
if (ftruncate(pidfd, 0)) {
error_print(0, "Failed to truncate pid file (%s).", strerror(errno));
return -1;
}
snprintf(pid, sizeof(pid) - 1, "%ld\n", (long)getpid());
pid_len = strlen(pid);
if (write(pidfd, pid, pid_len) < pid_len) {
error_print(0, "Failed to write PID into pid file (%s).", strerror(errno));
return -1;
}
return 0;
}
/*
* Plugin loading -- external .so files only (no internal plugins)
*/
static size_t path_len_no_ext(const char *path)
{
const char *dot;
dot = strrchr(path, '.');
if (!dot || dot == path)
return 0;
return dot - path;
}
static int load_plugins(struct plugin **plugins, int *plugin_count)
{
void *mem, *handle;
struct plugin *plugin;
DIR *dir;
struct dirent *ent;
const char *plugins_dir;
char *path;
size_t name_len;
int rc = 0;
*plugins = NULL;
*plugin_count = 0;
plugins_dir = getenv("SRPD_PLUGINS_PATH");
if (!plugins_dir)
plugins_dir = SRPD_PLUGINS_PATH;
dir = opendir(plugins_dir);
if (!dir) {
error_print(0, "Opening \"%s\" directory failed (%s).", plugins_dir, strerror(errno));
return -1;
}
while ((ent = readdir(dir))) {
if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, ".."))
continue;
if (asprintf(&path, "%s/%s", plugins_dir, ent->d_name) == -1) {
error_print(0, "asprintf() failed (%s).", strerror(errno));
rc = -1;
break;
}
handle = dlopen(path, RTLD_LAZY);
if (!handle) {
error_print(0, "Opening plugin \"%s\" failed (%s).", path, dlerror());
free(path);
rc = -1;
break;
}
free(path);
mem = realloc(*plugins, (*plugin_count + 1) * sizeof(**plugins));
if (!mem) {
error_print(0, "realloc() failed (%s).", strerror(errno));
dlclose(handle);
rc = -1;
break;
}
*plugins = mem;
plugin = &(*plugins)[*plugin_count];
memset(plugin, 0, sizeof(*plugin));
*(void **)&plugin->init_cb = dlsym(handle, SRP_INIT_CB);
if (!plugin->init_cb) {
error_print(0, "Failed to find function \"%s\" in plugin \"%s\".",
SRP_INIT_CB, ent->d_name);
dlclose(handle);
rc = -1;
break;
}
*(void **)&plugin->cleanup_cb = dlsym(handle, SRP_CLEANUP_CB);
if (!plugin->cleanup_cb) {
error_print(0, "Failed to find function \"%s\" in plugin \"%s\".",
SRP_CLEANUP_CB, ent->d_name);
dlclose(handle);
rc = -1;
break;
}
plugin->handle = handle;
name_len = path_len_no_ext(ent->d_name);
if (name_len == 0) {
error_print(0, "Wrong filename \"%s\".", ent->d_name);
dlclose(handle);
rc = -1;
break;
}
plugin->name = strndup(ent->d_name, name_len);
if (!plugin->name) {
error_print(0, "strndup() failed.");
dlclose(handle);
rc = -1;
break;
}
++(*plugin_count);
}
closedir(dir);
return rc;
}
/*
* Wipe stale sysrepo SHM files for a clean slate every boot.
*/
static void wipe_sysrepo_shm(void)
{
glob_t gl;
if (glob("/dev/shm/sr_*", 0, NULL, &gl) == 0) {
for (size_t i = 0; i < gl.gl_pathc; i++)
unlink(gl.gl_pathv[i]);
globfree(&gl);
}
}
const char *basenm(const char *path)
{
const char *slash;
if (!path)
return NULL;
slash = strrchr(path, '/');
if (slash)
return slash[1] ? slash + 1 : NULL;
return path;
}
/*
* Append error message to login banners.
*/
static void banner_append(const char *msg)
{
const char *files[] = {
"/etc/banner",
"/etc/issue",
"/etc/issue.net",
};
for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); i++) {
FILE *fp = fopen(files[i], "a");
if (fp) {
fprintf(fp, "\n%s\n", msg);
fclose(fp);
}
}
}
/*
* Smart migration: only fork+exec the migrate script if the version
* in the config file doesn't match the current confd version.
*/
static int maybe_migrate(const char *path)
{
const char *backup_dir = "/cfg/backup";
json_t *root, *meta, *ver;
const char *file_ver;
char backup[256];
int rc;
root = json_load_file(path, 0, NULL);
if (!root)
return -1;
meta = json_object_get(root, "infix-meta:meta");
ver = meta ? json_object_get(meta, "version") : NULL;
file_ver = ver ? json_string_value(ver) : "0.0";
if (!strcmp(file_ver, CONFD_VERSION)) {
json_decref(root);
return 0;
}
json_decref(root);
SRPLG_LOG_INF("confd", "%s config version %s vs confd %s, migrating ...",
path, file_ver, CONFD_VERSION);
mkpath(backup_dir, 0770);
chown(backup_dir, 0, 10); /* root:wheel */
snprintf(backup, sizeof(backup), "%s/%s", backup_dir, basenm(path));
rc = systemf("migrate -i -b \"%s\" \"%s\"", backup, path);
if (rc)
SRPLG_LOG_ERR("confd", "Migration of %s failed (rc=%d)", path, rc);
return rc;
}
static int file_exists(const char *path)
{
return access(path, F_OK) == 0;
}
/*
* Load a JSON config file into the running datastore.
* Mirrors what sysrepocfg -I does: lyd_parse_data() + sr_replace_config().
*/
static int load_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess,
const char *path, uint32_t timeout_ms)
{
const struct ly_ctx *ly_ctx;
struct lyd_node *data = NULL;
struct ly_in *in = NULL;
LY_ERR lyrc;
int r;
ly_ctx = sr_acquire_context(conn);
lyrc = ly_in_new_filepath(path, 0, &in);
if (lyrc == LY_EINVAL) {
/* empty file */
char *empty = strdup("");
ly_in_new_memory(empty, &in);
} else if (lyrc) {
error_print(0, "Failed to open \"%s\" for reading", path);
sr_release_context(conn);
return -1;
}
lyrc = lyd_parse_data(ly_ctx, NULL, in, LYD_JSON,
LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STRICT,
0, &data);
ly_in_free(in, 1);
if (lyrc) {
SRPLG_LOG_ERR("confd", "Parsing %s failed", path);
sr_release_context(conn);
return -1;
}
sr_release_context(conn);
r = sr_replace_config(sess, NULL, data, timeout_ms);
if (r != SR_ERR_OK) {
SRPLG_LOG_ERR("confd", "sr_replace_config failed: %s", sr_strerror(r));
return -1;
}
return 0;
}
/*
* Export running datastore to a JSON file.
*/
static int export_running(sr_session_ctx_t *sess, const char *path, uint32_t timeout_ms)
{
sr_data_t *data = NULL;
FILE *fp;
int r;
r = sr_get_data(sess, "/*", 0, timeout_ms, 0, &data);
if (r != SR_ERR_OK) {
SRPLG_LOG_ERR("confd", "sr_get_data failed: %s", sr_strerror(r));
return -1;
}
fp = fopen(path, "w");
if (!fp) {
SRPLG_LOG_ERR("confd", "Failed to open %s for writing: %s", path, strerror(errno));
sr_release_data(data);
return -1;
}
lyd_print_file(fp, data ? data->tree : NULL, LYD_JSON, LYD_PRINT_SIBLINGS);
fclose(fp);
sr_release_data(data);
chown(path, 0, 0); /* root:root initially */
chmod(path, 0660);
/* Set group to 'wheel' for admin access */
systemf("chgrp wheel '%s' 2>/dev/null", path);
return 0;
}
/*
* Handle startup-config load failure: revert to factory-default,
* then load failure-config, set error banners.
*/
static void handle_startup_failure(sr_session_ctx_t *sess, const char *failure_path,
sr_conn_ctx_t *conn, uint32_t timeout_ms)
{
int r;
SRPLG_LOG_ERR("confd", "Failed loading startup-config, reverting to Fail Secure mode!");
/* Reset to factory-default */
r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms);
if (r != SR_ERR_OK) {
SRPLG_LOG_ERR("confd", "sr_copy_config(factory-default) failed: %s", sr_strerror(r));
/* Nuclear option: wipe everything */
systemf("rm -f /etc/sysrepo/data/*startup* /etc/sysrepo/data/*running* /dev/shm/sr_*");
return;
}
/* Load failure-config on top */
if (file_exists(failure_path)) {
if (load_config(conn, sess, failure_path, timeout_ms)) {
SRPLG_LOG_ERR("confd", "Failed loading failure-config, aborting!");
banner_append("CRITICAL ERROR: Logins are disabled, no credentials available");
systemf("initctl -nbq runlevel 9");
return;
}
}
banner_append("ERROR: Corrupt startup-config, system has reverted to default login credentials");
}
/*
* Enable test-mode if the test-mode marker exists.
*/
static void maybe_enable_test_mode(void)
{
if (file_exists("/mnt/aux/test-mode"))
systemf("sysrepoctl -c infix-test -e test-mode-enable");
}
/*
* Determine which config to load:
* - test-mode (unless override exists)
* - startup-config
* - first-boot from factory
*/
static int bootstrap_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess,
const char *factory_path, const char *startup_path,
const char *failure_path, const char *test_path,
uint32_t timeout_ms)
{
const char *config_path;
int r;
/* Test mode support */
if (file_exists("/mnt/aux/test-mode")) {
if (file_exists("/mnt/aux/test-override-startup")) {
unlink("/mnt/aux/test-override-startup");
config_path = startup_path;
} else {
SRPLG_LOG_INF("confd", "Test mode detected, switching to test-config");
config_path = test_path;
}
} else {
config_path = startup_path;
}
if (file_exists(config_path)) {
/* Run migration if needed */
maybe_migrate(config_path);
/* Load startup (or test) config */
SRPLG_LOG_INF("confd", "Loading %s ...", config_path);
if (load_config(conn, sess, config_path, timeout_ms)) {
handle_startup_failure(sess, failure_path, conn, timeout_ms);
return 0; /* continue running even in fail-secure */
}
SRPLG_LOG_INF("confd", "Loaded %s successfully, syncing startup datastore.", config_path);
sr_session_switch_ds(sess, SR_DS_STARTUP);
r = sr_copy_config(sess, NULL, SR_DS_RUNNING, timeout_ms);
sr_session_switch_ds(sess, SR_DS_RUNNING);
if (r != SR_ERR_OK)
SRPLG_LOG_WRN("confd", "Failed to sync startup datastore: %s", sr_strerror(r));
return 0;
}
/* First boot: no startup-config, initialize from factory */
SRPLG_LOG_INF("confd", "startup-config missing, initializing from factory-config");
r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms);
if (r != SR_ERR_OK) {
SRPLG_LOG_ERR("confd", "sr_copy_config(factory-default) failed: %s", sr_strerror(r));
return -1;
}
/* Export running → startup file */
if (export_running(sess, startup_path, timeout_ms))
SRPLG_LOG_WRN("confd", "Failed to export running to %s", startup_path);
return 0;
}
int main(int argc, char **argv)
{
struct plugin *plugins = NULL;
sr_conn_ctx_t *conn = NULL;
sr_session_ctx_t *sess = NULL;
sr_log_level_t log_level = SR_LL_ERR;
int plugin_count = 0, i, r, rc = EXIT_FAILURE, opt, debug = 0;
int pidfd = -1, fatal_fail = 0;
const char *pidfile = NULL;
const char *factory_path = "/etc/factory-config.cfg";
const char *startup_path = "/cfg/startup-config.cfg";
const char *failure_path = "/etc/failure-config.cfg";
const char *test_path = "/etc/test-config.cfg";
uint32_t timeout_s = 60;
uint32_t timeout_ms;
struct option options[] = {
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'V'},
{"verbosity", required_argument, NULL, 'v'},
{"debug", no_argument, NULL, 'd'},
{"foreground", no_argument, NULL, 'n'},
{"pid-file", required_argument, NULL, 'p'},
{"fatal-plugin-fail", no_argument, NULL, 'f'},
{"factory-config", required_argument, NULL, 'F'},
{"startup-config", required_argument, NULL, 'S'},
{"failure-config", required_argument, NULL, 'E'},
{"timeout", required_argument, NULL, 't'},
{NULL, 0, NULL, 0},
};
opterr = 0;
while ((opt = getopt_long(argc, argv, "hVv:dnp:fF:S:E:t:", options, NULL)) != -1) {
switch (opt) {
case 'h':
version_print();
help_print();
rc = EXIT_SUCCESS;
goto cleanup;
case 'V':
version_print();
rc = EXIT_SUCCESS;
goto cleanup;
case 'v':
if (!strcmp(optarg, "none"))
log_level = SR_LL_NONE;
else if (!strcmp(optarg, "error"))
log_level = SR_LL_ERR;
else if (!strcmp(optarg, "warning"))
log_level = SR_LL_WRN;
else if (!strcmp(optarg, "info"))
log_level = SR_LL_INF;
else if (!strcmp(optarg, "debug"))
log_level = SR_LL_DBG;
else if (strlen(optarg) == 1 && optarg[0] >= '0' && optarg[0] <= '4')
log_level = atoi(optarg);
else {
error_print(0, "Invalid verbosity \"%s\"", optarg);
goto cleanup;
}
break;
case 'd':
debug = 1;
break;
case 'n':
debug = -1;
break;
case 'p':
pidfile = optarg;
break;
case 'f':
fatal_fail = 1;
break;
case 'F':
factory_path = optarg;
break;
case 'S':
startup_path = optarg;
break;
case 'E':
failure_path = optarg;
break;
case 't':
timeout_s = (uint32_t)atoi(optarg);
break;
default:
error_print(0, "Invalid option or missing argument: -%c", optopt);
goto cleanup;
}
}
if (optind < argc) {
error_print(0, "Redundant parameters");
goto cleanup;
}
timeout_ms = timeout_s * 1000;
if (pidfile && (pidfd = open_pidfile(pidfile)) < 0)
goto cleanup;
/* Load plugins from disk (dlopen) before daemonizing */
if (load_plugins(&plugins, &plugin_count))
error_print(0, "load_plugins failed (continuing)");
/* Daemonize -- after this point, confd no longer logs to stderr */
daemon_init(debug, log_level);
/* Phase 1: Wipe stale SHM for a clean slate */
wipe_sysrepo_shm();
/* Phase 2: Connect to sysrepo (rebuilds SHM from installed YANG modules) */
r = sr_connect(0, &conn);
if (r != SR_ERR_OK) {
error_print(r, "Failed to connect");
goto cleanup;
}
/* Phase 3: Install factory defaults into all datastores */
SRPLG_LOG_INF("confd", "Loading factory-default datastore from %s ...", factory_path);
r = sr_install_factory_config(conn, factory_path);
if (r != SR_ERR_OK) {
SRPLG_LOG_ERR("confd", "sr_install_factory_config failed: %s", sr_strerror(r));
goto cleanup;
}
/* Phase 4: Start running-datastore session */
r = sr_session_start(conn, SR_DS_RUNNING, &sess);
if (r != SR_ERR_OK) {
error_print(r, "Failed to start new session");
goto cleanup;
}
/* Phase 4b: Clear running datastore so plugin init sees an empty
* tree. This matches the original bootstrap flow where running
* was cleared with '{}' before sysrepo-plugind started. When we
* later load startup-config, the diff will be all-create which is
* what the plugin callbacks expect. */
r = sr_replace_config(sess, NULL, NULL, timeout_ms);
if (r != SR_ERR_OK) {
SRPLG_LOG_ERR("confd", "Failed to clear running datastore: %s", sr_strerror(r));
goto cleanup;
}
/* Enable test-mode YANG feature if needed */
maybe_enable_test_mode();
/* Phase 5: Initialize plugins (subscribe to YANG module changes) */
for (i = 0; i < plugin_count; i++) {
r = plugins[i].init_cb(sess, &plugins[i].private_data);
if (r) {
SRPLG_LOG_ERR("confd", "Plugin \"%s\" initialization failed (%s).",
plugins[i].name, sr_strerror(r));
if (fatal_fail)
goto cleanup;
} else {
SRPLG_LOG_INF("confd", "Plugin \"%s\" initialized.", plugins[i].name);
plugins[i].initialized = 1;
}
}
/* Phase 6: Load startup config -- plugins are now subscribed, so
* sr_replace_config() will trigger their change callbacks. */
if (bootstrap_config(conn, sess, factory_path, startup_path,
failure_path, test_path, timeout_ms))
goto cleanup;
/* Write PID file after everything is ready */
if (pidfile && write_pidfile(pidfd) < 0)
goto cleanup;
/* Wait for a terminating signal */
pthread_mutex_lock(&lock);
while (!loop_finish)
pthread_cond_wait(&cond, &lock);
pthread_mutex_unlock(&lock);
rc = EXIT_SUCCESS;
cleanup:
while (plugin_count > 0) {
if (plugins[plugin_count - 1].initialized)
plugins[plugin_count - 1].cleanup_cb(sess, plugins[plugin_count - 1].private_data);
if (plugins[plugin_count - 1].handle)
dlclose(plugins[plugin_count - 1].handle);
free(plugins[plugin_count - 1].name);
--plugin_count;
}
free(plugins);
if (pidfd >= 0) {
close(pidfd);
unlink(pidfile);
}
sr_disconnect(conn);
return rc;
}