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