From 83b55ee4d86b9de45e4de5bc52f7dc9dce54210c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 18 Dec 2025 08:27:14 +0100 Subject: [PATCH 1/9] Follw-up to d7abe46, use correct compatible string for override Actually disable iitod on Styx DCP-SC-28p to work around #670, this prohibits software control of LEDs, leaving the default HW control, which has proven more stable on this platform. Backported from origin/main b0dce8a05e9f1c6fca5c64cf9c4090bb89ba2499 Signed-off-by: Joachim Wiberg --- .../etc/finit.d/available/iitod.conf | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/board/styx-dcp-sc-28p/rootfs/usr/share/product/{dcp-sc-28p => styx,dcp-sc-28p}/etc/finit.d/available/iitod.conf (100%) diff --git a/src/board/styx-dcp-sc-28p/rootfs/usr/share/product/dcp-sc-28p/etc/finit.d/available/iitod.conf b/src/board/styx-dcp-sc-28p/rootfs/usr/share/product/styx,dcp-sc-28p/etc/finit.d/available/iitod.conf similarity index 100% rename from src/board/styx-dcp-sc-28p/rootfs/usr/share/product/dcp-sc-28p/etc/finit.d/available/iitod.conf rename to src/board/styx-dcp-sc-28p/rootfs/usr/share/product/styx,dcp-sc-28p/etc/finit.d/available/iitod.conf From 55b038d9995ff21bf924161e551287b3979a2e69 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 8 Nov 2025 11:45:03 +0100 Subject: [PATCH 2/9] .github: only set latest tag on .0 releases Prevent patch releases from stealing the "latest" tag from newer minor versions. Only vXX.YY and vXX.YY.0 releases should be marked as latest. Fixes #1187 Signed-off-by: Joachim Wiberg --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea785456e..63beef05e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,7 +125,7 @@ jobs: if echo $ver | grep -qE 'v[0-9.]+(-alpha|-beta|-rc)[0-9]*'; then echo "pre=true" >> $GITHUB_OUTPUT echo "latest=false" >> $GITHUB_OUTPUT - elif echo $ver | grep -qE '^v[0-9.]+\.[0-9.]+(\.[0-9]+)?$'; then + elif echo $ver | grep -qE '^v[0-9]+\.[0-9]+(\.0)?$'; then echo "pre=false" >> $GITHUB_OUTPUT echo "latest=true" >> $GITHUB_OUTPUT echo "cat=Releases" >> $GITHUB_OUTPUT From 5ea99680c851b19f76ce5bf38615d7d43d3cf015 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 20 Oct 2025 14:39:55 +0200 Subject: [PATCH 3/9] bin: backport fixes to shell/cli copy command This is a backport of the following commits from origin/main: 3b24fab e6a04fb 92e80b4 ad96965 3e03ece 21256a8 b7d91e4 d26e311 0977ab4 f83fbc6 --- cli: fix 'copy FILE running-config' use-case When copying to the running datastore we cannot use sr_copy_config(), instead we must use sr_replace_config(). This fix covers both the case of 'copy startup-config running-config' and 'copy FILE running-config'. Fixes #1203 --- cli: add 'validate', or '-n', dry run to copy command This commit adds config file validation to the copy command, discussed in #373. Allowing users to test their config files before restoring a backup. The feature could also be used for the automatic rollback when downgrading to an earlier version of the OS. Fixes #373 --- cli: fix copy to missing startup-config file Fixes #981 --- cli: restrict copy and erase commands This is a follow-up to PR #717 where path traversal protection was discussed. A year later and it's clear that having a user-friendly copy tool in the shell is a good thing, but that we proably want to restrict what it can do when called from the CLI. A sanitize flag (-s) is added to control the behavior, when used in the shell without -s, both commands act like traditional UNIX tools and do assume . for relative paths, and allow ../, whereas when running from the CLI only /media/ is allowed and otherwise files are assumed to be in $HOME or /cfg --- cli: sanitize regular file to file copy The regular file-to-file copy, was missing calls to cfg_adjust(), this commit fixes that and adds some helpful comments for each use-case. Also, drop insecure mktemp() in favor of our own version which uses the basename of the remote source file. --- bin: add bash completion for copy command Add bash completion for the common datastores, like we already do in the CLI, and update the usage text accordingly. Also, make sure to install to /usr/bin, not /bin since we've now merged the hierarchies since a while back. --- bin: copy: Refactor copy() made some...creative...use of control flow that made it quite difficult to follow. Take a first priciples approach to simplify the logic. --- bin: copy: Always get startup from sysrepo This will make sure to apply NACM rules for all the data. It also makes it possible for a luser access a subset of the data, even if they to do not have read access to /cfg/startup-config.cfg. Signed-off-by: Joachim Wiberg --- package/bin/bin.mk | 2 +- src/bin/Makefile.am | 8 +- src/bin/configure.ac | 5 +- src/bin/copy.bash | 93 ++++ src/bin/copy.c | 635 ++++++++++++++++++--------- src/bin/erase.c | 48 +- src/bin/util.c | 115 +++-- src/bin/util.h | 10 +- src/klish-plugin-infix/src/infix.c | 26 +- src/klish-plugin-infix/xml/infix.xml | 1 + 10 files changed, 648 insertions(+), 295 deletions(-) create mode 100644 src/bin/copy.bash diff --git a/package/bin/bin.mk b/package/bin/bin.mk index 5ac94ac9e..15d276a94 100644 --- a/package/bin/bin.mk +++ b/package/bin/bin.mk @@ -11,7 +11,7 @@ BIN_LICENSE = BSD-3-Clause BIN_LICENSE_FILES = LICENSE BIN_REDISTRIBUTE = NO BIN_DEPENDENCIES = sysrepo libite -BIN_CONF_OPTS = --prefix= --disable-silent-rules +BIN_CONF_OPTS = --disable-silent-rules BIN_AUTORECONF = YES define BIN_CONF_ENV diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am index 15ede0cf1..f33dd3e9e 100644 --- a/src/bin/Makefile.am +++ b/src/bin/Makefile.am @@ -3,11 +3,15 @@ ACLOCAL_AMFLAGS = -I m4 bin_PROGRAMS = copy erase files +# Bash completion +bashcompdir = $(datadir)/bash-completion/completions +dist_bashcomp_DATA = copy.bash + copy_SOURCES = copy.c util.c util.h copy_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE copy_CFLAGS = -W -Wall -Wextra -copy_CFLAGS += $(libite_CFLAGS) $(sysrepo_CFLAGS) -copy_LDADD = $(libite_LIBS) $(sysrepo_LIBS) +copy_CFLAGS += $(libite_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS) +copy_LDADD = $(libite_LIBS) $(libyang_LIBS) $(sysrepo_LIBS) erase_SOURCES = erase.c util.c util.h erase_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE diff --git a/src/bin/configure.ac b/src/bin/configure.ac index 10aaf0a8f..8b5c12c81 100644 --- a/src/bin/configure.ac +++ b/src/bin/configure.ac @@ -15,6 +15,7 @@ AC_PROG_INSTALL PKG_PROG_PKG_CONFIG PKG_CHECK_MODULES([libite], [libite >= 2.5.0]) +PKG_CHECK_MODULES([libyang], [libyang >= 3.0.0]) PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36]) # Misc variable replacements for below Summary @@ -55,8 +56,8 @@ cat < #include +#include #include #include @@ -18,58 +19,29 @@ struct infix_ds { char *name; /* startup-config, etc. */ - char *sysrepocfg; /* ds name in sysrepocfg */ int datastore; /* sr_datastore_t and -1 */ - int rw; /* read-write:1 or not:0 */ + bool rw; /* read-write:1 or not:0 */ char *path; /* local path or NULL */ }; -struct infix_ds infix_config[] = { - { "startup-config", "startup", SR_DS_STARTUP, 1, "/cfg/startup-config.cfg" }, - { "running-config", "running", SR_DS_RUNNING, 1, NULL }, - { "candidate-config", "candidate", SR_DS_CANDIDATE, 1, NULL }, - { "operational-config", "operational", SR_DS_OPERATIONAL, 1, NULL }, - { "factory-config", "factory-default", SR_DS_FACTORY_DEFAULT, 0, NULL } +const struct infix_ds infix_config[] = { + { "startup-config", SR_DS_STARTUP, true, "/cfg/startup-config.cfg" }, + { "running-config", SR_DS_RUNNING, true, NULL }, + /* { "candidate-config", SR_DS_CANDIDATE, true, NULL }, */ + { "operational-state", SR_DS_OPERATIONAL, false, NULL }, + { "factory-config", SR_DS_FACTORY_DEFAULT, false, NULL } }; static const char *prognm = "copy"; +static const char *remote_user; static int timeout; - - -/* - * Print sysrepo session errors followed by an optional string. - */ -static void emsg(sr_session_ctx_t *sess, const char *fmt, ...) -{ - const sr_error_info_t *err = NULL; - va_list ap; - size_t i; - int rc; - - if (!sess) - goto end; - - rc = sr_session_get_error(sess, &err); - if ((rc != SR_ERR_OK) || !err) - goto end; - - // Show the first error only. Because probably next errors are - // originated from internal sysrepo code but is not from subscribers. -// for (i = 0; i < err->err_count; i++) - for (i = 0; i < (err->err_count < 1 ? err->err_count : 1); i++) - fprintf(stderr, ERRMSG "%s\n", err->err[i].message); -end: - if (fmt) { - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - } -} +static int dry_run; +static int sanitize; /* * Current system user, same as sysrepo user */ -static char *getuser(void) +static const char *getuser(void) { const struct passwd *pw; uid_t uid; @@ -163,7 +135,7 @@ static void set_owner(const char *fn, const char *user) } } -static const char *infix_ds(const char *text, struct infix_ds **ds) +static const char *infix_ds(const char *text, const struct infix_ds **ds) { size_t i, len = strlen(text); @@ -174,209 +146,424 @@ static const char *infix_ds(const char *text, struct infix_ds **ds) } } + *ds = NULL; return text; } +static bool is_uri(const char *str) +{ + return strstr(str, "://") != NULL; +} -static int copy(const char *src, const char *dst, const char *remote_user) +static char *mktmp(void) { - struct infix_ds *srcds = NULL, *dstds = NULL; - char temp_file[20] = "/tmp/copy.XXXXXX"; - const char *tmpfn = NULL; - sr_session_ctx_t *sess; - const char *fn = NULL; - sr_conn_ctx_t *conn; - const char *user; - char adjust[256]; mode_t oldmask; - int rc = 0; - - /* rw for user and group only */ - oldmask = umask(0006); + char *path; + int fd; - src = infix_ds(src, &srcds); - if (!src) - goto err; - dst = infix_ds(dst, &dstds); - if (!dst) + path = strdup("/tmp/copy-XXXXXX"); + if (!path) goto err; - if (!strcmp(src, dst)) { - fprintf(stderr, ERRMSG "source and destination are the same, aborting.\n"); + oldmask = umask(0077); + fd = mkstemp(path); + umask(oldmask); + + if (fd < 0) goto err; + + close(fd); + return path; +err: + free(path); + return NULL; +} + +static void rmtmp(const char *path) +{ + if (remove(path)) { + if (errno == ENOENT) + return; + + fprintf(stderr, ERRMSG "removal of temporary file %s failed\n", path); } +} - user = getuser(); - /* 1. Regular ds copy */ - if (srcds && dstds) { - /* Ensure the dst ds is writable */ - if (!dstds->rw) { - fprintf(stderr, ERRMSG "not possible to write to \"%s\", skipping.\n", dst); - rc = 1; - goto err; - } +static void sysrepo_print_error(sr_session_ctx_t *sess) +{ + const sr_error_info_t *erri = NULL; + int err; - if (sr_connect(SR_CONN_DEFAULT, &conn)) { - fprintf(stderr, ERRMSG "connection to datastore failed\n"); - rc = 1; - goto err; - } + err = sr_session_get_error(sess, &erri); + if (err || !erri || !erri->err_count) + return; - sr_log_syslog("klishd", SR_LL_WRN); - - if (sr_session_start(conn, dstds->datastore, &sess)) { - fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst); - } else { - sr_nacm_set_user(sess, user); - rc = sr_copy_config(sess, NULL, srcds->datastore, timeout * 1000); - if (rc) - emsg(sess, ERRMSG "unable to copy configuration, err %d: %s\n", - rc, sr_strerror(rc)); - else - set_owner(dstds->path, user); - } - rc = sr_disconnect(conn); + fprintf(stderr, ERRMSG "%s (%d)\n", erri->err->message, erri->err->err_code); +} - if (!srcds->path || !dstds->path) - goto err; /* done, not an error */ +static sr_session_ctx_t *sysrepo_session(const struct infix_ds *ds) +{ + static sr_session_ctx_t *sess; - /* allow copy factory startup */ - } + sr_subscription_ctx_t *sub = NULL; + const char *user = getuser(); + sr_conn_ctx_t *conn = NULL; + int err; - if (srcds) { - /* 2. Export from a datastore somewhere else */ - if (strstr(dst, "://")) { - if (srcds->path) - fn = srcds->path; - else { - snprintf(adjust, sizeof(adjust), "/tmp/%s.cfg", srcds->name); - fn = tmpfn = adjust; - rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn); - } - - if (rc) - fprintf(stderr, ERRMSG "failed exporting %s to %s\n", src, fn); - else { - rc = systemf("curl %s -LT %s %s", remote_user, fn, dst); - if (rc) - fprintf(stderr, ERRMSG "failed uploading %s to %s\n", src, dst); - else - set_owner(dst, user); - } - goto err; - } + if (!ds) { + if (!sess) + return NULL; - if (dstds && dstds->path) - fn = dstds->path; - else - fn = cfg_adjust(dst, src, adjust, sizeof(adjust)); + conn = sr_session_get_connection(sess); + sr_session_stop(sess); + sr_disconnect(conn); + return NULL; + } - if (!fn) { - fprintf(stderr, ERRMSG "invalid destination path.\n"); - rc = -1; + if (!sess) { + err = sr_connect(0, &conn); + if (err != SR_ERR_OK) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "could not connect to %s\n", ds->name); goto err; } - if (!access(fn, F_OK) && !yorn("Overwrite existing file %s", fn)) { - fprintf(stderr, "OK, aborting.\n"); - return 0; + /* Always open running, because sr_nacm_init() does not work + * against the factory DS. + */ + err = sr_session_start(conn, SR_DS_RUNNING, &sess); + if (err != SR_ERR_OK) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "%s session setup failed\n", ds->name); + goto err_disconnect; } - if (srcds->path) - rc = systemf("cp %s %s", srcds->path, fn); - else - rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn); - if (rc) - fprintf(stderr, ERRMSG "failed copy %s to %s\n", src, fn); - else - set_owner(fn, user); - } else if (dstds) { - if (!dstds->sysrepocfg) { - fprintf(stderr, ERRMSG "not possible to import to this datastore.\n"); - rc = 1; - goto err; - } - if (!dstds->rw) { - fprintf(stderr, ERRMSG "not possible to write to %s", dst); - goto err; + err = sr_nacm_init(sess, 0, &sub); + if (err != SR_ERR_OK) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "%s NACM setup failed\n", ds->name); + goto err_stop; } - /* 3. Import from somewhere to a datastore */ - if (strstr(src, "://")) { - tmpfn = mktemp(temp_file); - fn = tmpfn; - } else { - fn = cfg_adjust(src, NULL, adjust, sizeof(adjust)); - if (!fn) { - fprintf(stderr, ERRMSG "invalid source file location.\n"); - rc = 1; - goto err; - } + err = sr_nacm_set_user(sess, user); + if (err != SR_ERR_OK) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "%s NACM setup for %s failed\n", ds->name, user); + goto err_nacm_destroy; } + } - if (tmpfn) - rc = systemf("curl %s -Lo %s %s", remote_user, fn, src); - if (rc) { - fprintf(stderr, ERRMSG "failed downloading %s", src); - } else { - rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn); - if (rc) - fprintf(stderr, ERRMSG "failed loading %s from %s", dst, src); - } + err = sr_session_switch_ds(sess, ds->datastore); + if (err) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "%s activation failed\n", ds->name); + return NULL; + } + + return sess; + +err_nacm_destroy: + sr_nacm_destroy(); +err_stop: + sr_session_stop(sess); +err_disconnect: + sr_disconnect(conn); +err: + sess = NULL; + return NULL; +} + +static int sysrepo_export(const struct infix_ds *ds, const char *path) +{ + sr_session_ctx_t *sess; + sr_data_t *data; + int err; + + sess = sysrepo_session(ds); + if (!sess) + return 1; + + err = sr_get_data(sess, "/*", 0, timeout * 1000, SR_OPER_DEFAULT, &data); + if (err) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "retrieval of %s data failed\n", ds->name); + return err; + } + + err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_WITHSIBLINGS); + sr_release_data(data); + if (err) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "failed to store %s data\n", ds->name); + return err; + } + + return 0; +} + +static int sysrepo_import(const struct infix_ds *ds, const char *path) +{ + const struct ly_ctx *ly; + sr_session_ctx_t *sess; + struct lyd_node *data; + int err; + + sess = sysrepo_session(ds); + if (!sess) + return 1; + + ly = sr_acquire_context(sr_session_get_connection(sess)); + + err = lyd_parse_data_path(ly, path, LYD_JSON, + LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | + LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT, 0, &data); + if (err) { + fprintf(stderr, ERRMSG "failed to parse %s data\n", ds->name); + goto out; + } + + err = dry_run ? 0 : sr_replace_config(sess, NULL, data, timeout * 1000); + if (err) { + sysrepo_print_error(sess); + fprintf(stderr, ERRMSG "failed import %s data\n", ds->name); + } + +out: + sr_release_context(sr_session_get_connection(sess)); + return err ? 1 : 0; + /* return sysrepo_do(sysrepo_import_op, ds, path) ? 1 : 0; */ +} + +static int subprocess(char * const *argv) +{ + int pid, status; + + pid = fork(); + if (!pid) { + execvp(argv[0], argv); + exit(1); + } + + if (pid < 0) + return 1; + + if (waitpid(pid, &status, 0) < 0) + return 1; + + if (!WIFEXITED(status)) + return 1; + + return WEXITSTATUS(status); +} + +static int curl(char *op, const char *path, const char *uri) +{ + char *argv[] = { + "curl", "-L", op, NULL, NULL, NULL, NULL, NULL, + }; + int err = 1; + + argv[3] = strdup(path); + argv[4] = strdup(uri); + if (!(argv[3] && argv[4])) + goto out; + + if (remote_user) { + argv[5] = strdup("-u"); + argv[6] = strdup(remote_user); + if (!(argv[5] && argv[6])) + goto out; + } + err = subprocess(argv); + +out: + free(argv[6]); + free(argv[5]); + free(argv[4]); + free(argv[3]); + return err; +} + +static int curl_upload(const char *srcpath, const char *uri) +{ + char upload[] = "-T"; + + if (curl(upload, srcpath, uri)) { + fprintf(stderr, ERRMSG "upload to %s failed\n", uri); + return 1; + } + + return 0; +} + +static int curl_download(const char *uri, const char *dstpath) +{ + char download[] = "-o"; + + if (curl(download, dstpath, uri)) { + fprintf(stderr, ERRMSG "download of %s failed\n", uri); + return 1; + } + + return 0; +} + +static int cp(const char *srcpath, const char *dstpath) +{ + char *argv[] = { + "cp", NULL, NULL, NULL, + }; + int err = 1; + + argv[1] = strdup(srcpath); + argv[2] = strdup(dstpath); + if (!(argv[1] && argv[2])) + goto out; + + err = subprocess(argv); + if (err) + fprintf(stderr, ERRMSG "failed to save %s\n", dstpath); +out: + free(argv[2]); + free(argv[1]); + return err; +} + +static int put(const char *srcpath, const char *dst, + const struct infix_ds *ds, const char *path) +{ + int err = 0; + + if (ds) + err = sysrepo_import(ds, srcpath); + else if (is_uri(dst)) + err = curl_upload(srcpath, dst); + + if (err) + return err; + + if (path) { + err = cp(srcpath, path); + if (!err) + set_owner(path, getuser()); + } + + return 0; +} + +static int get(const char *src, const struct infix_ds *ds, const char *path) +{ + int err = 0; + + if (ds) + err = sysrepo_export(ds, path); + else if (is_uri(src)) + err = curl_download(src, path); + + return err; +} + +static int resolve_src(const char **src, const struct infix_ds **ds, char **path, bool *rm) +{ + *src = infix_ds(*src, ds); + + if (*ds || is_uri(*src)) { + *path = mktmp(); + if (!*path) + return 1; + + *rm = true; + return 0; } else { - if (strstr(src, "://") && strstr(dst, "://")) { - fprintf(stderr, ERRMSG "copy from remote to remote is not supported.\n"); - goto err; - } + *path = cfg_adjust(*src, NULL, sanitize); + } + + if (!*path) { + fprintf(stderr, ERRMSG "no such file %s.", *src); + return 1; + } + + *rm = false; + return 0; +} - if (strstr(src, "://")) { - fn = cfg_adjust(dst, src, adjust, sizeof(adjust)); - if (!fn) { - fprintf(stderr, ERRMSG "invalid destination file location.\n"); - rc = 1; - goto err; - } - - if (!access(fn, F_OK)) { - if (!yorn("Overwrite existing file %s", fn)) { - fprintf(stderr, "OK, aborting.\n"); - return 0; - } - } - - rc = systemf("curl %s -Lo %s %s", remote_user, fn, src); - } else if (strstr(dst, "://")) { - fn = cfg_adjust(src, NULL, adjust, sizeof(adjust)); - if (!fn) { - fprintf(stderr, ERRMSG "invalid source file location.\n"); - rc = 1; - goto err; - } - - if (access(fn, F_OK)) - fprintf(stderr, ERRMSG "no such file %s, aborting.", fn); - else - rc = systemf("curl %s -LT %s %s", remote_user, fn, dst); - } else { - if (!access(dst, F_OK)) { - if (!yorn("Overwrite existing file %s", dst)) { - fprintf(stderr, "OK, aborting.\n"); - return 0; - } - } - rc = systemf("cp %s %s", src, dst); +static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path) +{ + *dst = infix_ds(*dst, ds); + + if (*ds) { + if (!(*ds)->rw) { + fprintf(stderr, ERRMSG "%s is not writable", (*ds)->name); + return 1; } + + if (!(*ds)->path) + return 0; + + *path = strdup((*ds)->path); + } else if (is_uri(*dst)) { + return 0; + } else { + *path = cfg_adjust(*dst, NULL, sanitize); + } + + if (!*path) { + fprintf(stderr, ERRMSG "no such file: %s", *dst); + return 1; + } + + if (!*ds && !access(*path, F_OK) && !yorn("Overwrite existing file %s", *path)) { + fprintf(stderr, "OK, aborting.\n"); + return 1; + } + + return 0; +} + +static int copy(const char *src, const char *dst) +{ + char *srcpath = NULL, *dstpath = NULL; + const struct infix_ds *srcds, *dstds; + bool rmsrc = false; + mode_t oldmask; + int err = 1; + + /* rw for user and group only */ + oldmask = umask(0006); + + if (!strcmp(src, dst)) { + fprintf(stderr, ERRMSG "source and destination are the same, aborting.\n"); + goto err; } + err = resolve_src(&src, &srcds, &srcpath, &rmsrc); + if (err) + goto err; + + err = resolve_dst(&dst, &dstds, &dstpath); + if (err) + goto err; + + err = get(src, srcds, srcpath); + if (err) + goto err; + + err = put(srcpath, dst, dstds, dstpath); + err: - if (tmpfn) - rc = remove(tmpfn); + /* If either src or dst came from sysrepo, close the session */ + sysrepo_session(NULL); - sync(); /* ensure command is flushed to disk */ - umask(oldmask); + if (rmsrc) + rmtmp(srcpath); - return rc; + free(dstpath); + free(srcpath); + + sync(); + umask(oldmask); + return err; } static int usage(int rc) @@ -384,30 +571,48 @@ static int usage(int rc) printf("Usage: %s [OPTIONS] SRC DST\n" "\n" "Options:\n" - " -h This help text\n" - " -u USER Username for remote commands, like scp\n" - " -t SEEC Timeout for the operation, or default %d sec\n" - " -v Show version\n", prognm, timeout); + " -h This help text\n" + " -n Dry-run, validate configuration without applying\n" + " -s Sanitize paths for CLI use (restrict path traversal)\n" + " -t SEC Timeout for the operation, or default %d sec\n" + " -u USER Username for remote commands, like scp\n" + " -v Show version\n" + "\n" + "Files:\n" + " SRC JSON configuration file, or a datastore\n" + " DST A file or datastore, except factory-config\n" + "\n" + "Datastores:\n" + " running-config The running datastore, current active config\n" + " startup-config The non-volatile config used at startup\n" + " factory-config The device's factory default configuration\n" + "\n", prognm, timeout); return rc; } int main(int argc, char *argv[]) { - const char *user = NULL, *src = NULL, *dst = NULL; + const char *src = NULL, *dst = NULL; int c; timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT"); - while ((c = getopt(argc, argv, "ht:u:v")) != EOF) { + while ((c = getopt(argc, argv, "hnst:u:v")) != EOF) { switch(c) { case 'h': return usage(0); + case 'n': + dry_run = 1; + break; + case 's': + sanitize = 1; + break; case 't': timeout = atoi(optarg); break; case 'u': - user = optarg; + remote_user = optarg; break; case 'v': puts(PACKAGE_VERSION); @@ -424,5 +629,5 @@ int main(int argc, char *argv[]) src = argv[optind++]; dst = argv[optind++]; - return copy(src, dst, user); + return copy(src, dst); } diff --git a/src/bin/erase.c b/src/bin/erase.c index 716c310b3..d78ba1a40 100644 --- a/src/bin/erase.c +++ b/src/bin/erase.c @@ -4,40 +4,38 @@ #include #include #include +#include #include #include #include "util.h" static const char *prognm = "erase"; +static int sanitize; - -static int do_erase(const char *path) +static int do_erase(const char *name) { - char *fn; - - if (access(path, F_OK)) { - size_t len = strlen(path) + 10; - - fn = alloca(len); - if (!fn) { - fprintf(stderr, ERRMSG "failed allocating memory.\n"); - return -1; - } - - cfg_adjust(path, NULL, fn, len); - } else - fn = (char *)path; + char *path; + int rc = 0; + + path = cfg_adjust(name, NULL, sanitize); + if (!path) { + fprintf(stderr, ERRMSG "file not found.\n"); + rc = 1; + goto out; + } - if (!yorn("Remove %s, are you sure", fn)) - return 0; + if (!yorn("Remove %s, are you sure?", path)) + goto out; - if (remove(fn)) { - fprintf(stderr, ERRMSG "failed removing %s: %s\n", fn, strerror(errno)); - return -1; + if (remove(path)) { + fprintf(stderr, ERRMSG "failed removing %s: %s\n", path, strerror(errno)); + rc = 11; } - return 0; +out: + free(path); + return rc; } static int usage(int rc) @@ -46,6 +44,7 @@ static int usage(int rc) "\n" "Options:\n" " -h This help text\n" + " -s Sanitize paths for CLI use (restrict path traversal)\n" " -v Show version\n", prognm); return rc; @@ -55,10 +54,13 @@ int main(int argc, char *argv[]) { int c; - while ((c = getopt(argc, argv, "hv")) != EOF) { + while ((c = getopt(argc, argv, "hsv")) != EOF) { switch(c) { case 'h': return usage(0); + case 's': + sanitize = 1; + break; case 'v': puts(PACKAGE_VERSION); return 0; diff --git a/src/bin/util.c b/src/bin/util.c index 0e739b1ef..f922f4841 100644 --- a/src/bin/util.c +++ b/src/bin/util.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -67,52 +68,100 @@ int has_ext(const char *fn, const char *ext) return 0; } -static const char *basenm(const char *fn) +int dirlen(const char *path) { - const char *ptr; + const char *slash; - if (!fn) - return ""; + slash = strrchr(path, '/'); + if (slash) + return slash - path; - ptr = strrchr(fn, '/'); - if (!ptr) - ptr = fn; - - return ptr; + return 0; } -char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len) +const char *basenm(const char *path) { - if (strstr(fn, "../")) - return NULL; /* relative paths not allowed */ + const char *slash; + + if (!path) + return NULL; + + slash = strrchr(path, '/'); + if (slash) + return slash[1] ? slash + 1 : NULL; + + return path; +} - if (fn[0] == '/') { - strlcpy(buf, fn, len); - return buf; /* allow absolute paths */ +static int path_allowed(const char *path) +{ + const char *accepted[] = { + "/media/", + "/cfg/", + getenv("HOME"), + NULL + }; + + for (int i = 0; accepted[i]; i++) { + if (!strncmp(path, accepted[i], strlen(accepted[i]))) + return 1; } - /* Files in /cfg must end in .cfg */ - if (!strncmp(fn, "/cfg/", 5)) { - strlcpy(buf, fn, len); - if (!has_ext(fn, ".cfg")) - strlcat(buf, ".cfg", len); + return 0; +} - return buf; +char *cfg_adjust(const char *path, const char *template, bool sanitize) +{ + char *expanded = NULL, *resolved = NULL; + const char *basename; + int dlen; + + dlen = dirlen(path); + basename = basenm(path) ? : basenm(template); + if (!basename) + goto err; + + if (sanitize) { + if (strstr(path, "../")) + goto err; + + if (path[0] == '/') { + if (!path_allowed(path)) + goto err; + } } - /* Files ending with .cfg belong in /cfg */ - if (has_ext(fn, ".cfg")) { - snprintf(buf, len, "/cfg/%s", fn); - return buf; + if (asprintf(&expanded, "%s%.*s/%s%s", + path[0] == '/' ? "" : "/cfg/", + dlen, path, + basename, + strchr(basename, '.') ? "" : ".cfg") < 0) + goto err; + + if (sanitize) { + resolved = realpath(expanded, NULL); + if (!resolved) { + if (errno == ENOENT) + goto out; + else + goto err; + } + + /* File exists, make sure that the resolved symlink + * still matches the whitelist. + */ + if (!path_allowed(resolved)) + goto err; + + free(expanded); + expanded = resolved; } - if (strlen(fn) > 0 && fn[0] == '.' && tmpl) { - if (fn[1] == '/' && fn[2] != 0) - strlcpy(buf, fn, len); - else - strlcpy(buf, basenm(tmpl), len); - } else - strlcpy(buf, fn, len); +out: + return expanded; - return buf; +err: + free(resolved); + free(expanded); + return NULL; } diff --git a/src/bin/util.h b/src/bin/util.h index 6c5ded9d8..e9a5b6f8a 100644 --- a/src/bin/util.h +++ b/src/bin/util.h @@ -1,17 +1,19 @@ /* SPDX-License-Identifier: ISC */ #ifndef BIN_UTIL_H_ #define BIN_UTIL_H_ +#include #include #include #define ERRMSG "Error: " #define INFMSG "Note: " -int yorn (const char *fmt, ...); +int yorn (const char *fmt, ...); -int files (const char *path, const char *stripext); +int files (const char *path, const char *stripext); -int has_ext (const char *fn, const char *ext); -char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len); +const char *basenm (const char *fn); +int has_ext (const char *fn, const char *ext); +char *cfg_adjust (const char *path, const char *template, bool sanitize); #endif /* BIN_UTIL_H_ */ diff --git a/src/klish-plugin-infix/src/infix.c b/src/klish-plugin-infix/src/infix.c index 54bae6554..b041da200 100644 --- a/src/klish-plugin-infix/src/infix.c +++ b/src/klish-plugin-infix/src/infix.c @@ -28,7 +28,7 @@ const uint8_t kplugin_infix_major = 1; const uint8_t kplugin_infix_minor = 0; -static void cd_home(kcontext_t *ctx) +static const char *cd_home(kcontext_t *ctx) { const char *user = "root"; ksession_t *session; @@ -43,6 +43,8 @@ static void cd_home(kcontext_t *ctx) pw = getpwnam(user); chdir(pw->pw_dir); + + return user; } static int systemf(const char *fmt, ...) @@ -118,7 +120,7 @@ int infix_erase(kcontext_t *ctx) cd_home(ctx); - return systemf("erase %s", path); + return systemf("erase -s %s", path); } int infix_files(kcontext_t *ctx) @@ -146,8 +148,8 @@ int infix_copy(kcontext_t *ctx) { kpargv_t *pargv = kcontext_pargv(ctx); const char *src, *dst; - const char *username; char user[256] = ""; + char validate[8] = ""; kparg_t *parg; src = kparg_value(kpargv_find(pargv, "src")); @@ -159,26 +161,20 @@ int infix_copy(kcontext_t *ctx) if (parg) snprintf(user, sizeof(user), "-u %s", kparg_value(parg)); - cd_home(ctx); - username = ksession_user(kcontext_session(ctx)); + parg = kpargv_find(pargv, "validate"); + if (parg) + strlcpy(validate, "-n", sizeof(validate)); - return systemf("sudo -u %s copy %s %s %s", username, user, src, dst); + /* Ensure we run the copy command as the logged-in user, not root (klishd) */ + return systemf("doas -u %s copy -s %s %s %s %s", cd_home(ctx), validate, user, src, dst); } int infix_shell(kcontext_t *ctx) { - const char *user = "root"; - ksession_t *session; + const char *user = cd_home(ctx); pid_t pid; int rc; - session = kcontext_session(ctx); - if (session) { - user = ksession_user(session); - if (!user) - user = "root"; - } - pid = fork(); if (pid == -1) return -1; diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index 8da12d07a..52210a2e3 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -167,6 +167,7 @@ + From f71722c1113f2f6dd8308a90dd7c0648fb0f378a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 28 Nov 2025 16:25:45 +0100 Subject: [PATCH 4/9] bin: add system support data collection Backported from origin/main 5420fb01f2331789bdd6337f411942ca9c87e782 Includes the latest DRAM ECC status, with/without sudo, and temperature. Signed-off-by: Joachim Wiberg --- .../etc/support.d/10-temperature.sh | 161 +++++ .../etc/support.d/50-marvell-cn913x.sh | 26 + src/bin/Makefile.am | 1 + src/bin/support | 635 ++++++++++++++++++ 4 files changed, 823 insertions(+) create mode 100755 board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/10-temperature.sh create mode 100755 board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/50-marvell-cn913x.sh create mode 100755 src/bin/support diff --git a/board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/10-temperature.sh b/board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/10-temperature.sh new file mode 100755 index 000000000..ab44e0492 --- /dev/null +++ b/board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/10-temperature.sh @@ -0,0 +1,161 @@ +#!/bin/sh + +set -e + +# Build a map of phandle -> device tree node path for all PHY nodes +build_phandle_map() +{ + for phy_node in /sys/firmware/devicetree/base/cp*/config-space*/mdio*/switch*/mdio/ethernet-phy@* \ + /sys/firmware/devicetree/base/cp*/config-space*/mdio*/ethernet-phy@*; do + [ -f "$phy_node/phandle" ] || continue + + phandle=$(od -An -t x4 -N 4 "$phy_node/phandle" 2>/dev/null | tr -d ' ') + if [ -n "$phandle" ]; then + echo "$phandle:$phy_node" + fi + done +} + +build_phy_map() +{ + phandle_map=$(build_phandle_map) + + # Build a mapping of PHY of_node path -> interface name + for iface in /sys/class/net/*; do + [ -d "$iface" ] || continue + + iface_name=$(basename "$iface") + + # Try regular phydev approach first (for non-DSA interfaces) + if [ -L "$iface/phydev/of_node" ]; then + phy_of_node=$(readlink -f "$iface/phydev/of_node" 2>/dev/null) + if [ -n "$phy_of_node" ]; then + echo "$phy_of_node:$iface_name" + continue + fi + fi + + # For DSA interfaces, resolve via of_node's phy-handle + if [ -L "$iface/of_node" ]; then + iface_of_node=$(readlink -f "$iface/of_node" 2>/dev/null) + [ -n "$iface_of_node" ] || continue + + # Try to read phy-handle property (4-byte phandle) + if [ -f "$iface_of_node/phy-handle" ]; then + phy_phandle=$(od -An -t x4 -N 4 "$iface_of_node/phy-handle" 2>/dev/null | tr -d ' ') + + if [ -n "$phy_phandle" ]; then + # Look up the PHY node path from our phandle map + phy_of_node=$(echo "$phandle_map" | grep "^$phy_phandle:" | cut -d: -f2) + if [ -n "$phy_of_node" ]; then + echo "$phy_of_node:$iface_name" + fi + fi + fi + fi + done +} + +zone_map() +{ + type="$1" + + case "$type" in + ap-ic-thermal) + echo "Application processor interconnect" + ;; + ap-cpu[0-9]*-thermal) + cpu=${type#ap-cpu} + cpu=${cpu%-thermal} + echo "Application processor core $cpu" + ;; + cp[0-9]*-ic-thermal) + cp=${type%%-*} + cp=${cp#cp} + echo "Communication processor $cp interconnect" + ;; + *) + echo "$type" + ;; + esac +} + +thermal_zones() +{ + echo "Thermal Zones" + echo "=============" + echo + + for zone in /sys/class/thermal/thermal_zone*; do + [ -d "$zone" ] || continue + + name=$(basename "$zone") + type=$(cat "$zone/type" 2>/dev/null || echo "unknown") + data=$(cat "$zone/temp" 2>/dev/null) + desc=$(zone_map "$type") + + if [ -n "$data" ] && [ "$data" != "N/A" ]; then + # Convert millidegrees to degrees Celsius + temp_c=$(awk "BEGIN {printf \"%.1f\", $data / 1000}") + printf "%-20s %8s°C %s\n" "$name" "$temp_c" "$desc" + else + printf "%-20s %8s %s\n" "$name" "N/A" "$desc" + fi + done + echo +} + +hwmon() +{ + tmpfile=$(mktemp) + + echo "Hardware Monitors" + echo "=================" + echo + + phy_map=$(build_phy_map) + + for hwmon in /sys/class/hwmon/hwmon*; do + [ -d "$hwmon" ] || continue + + name=$(basename "$hwmon") + data=$(cat "$hwmon/temp1_input" 2>/dev/null) + + # Try to find the associated network interface + iface= + if [ -L "$hwmon/of_node" ]; then + hwmon_of_node=$(readlink -f "$hwmon/of_node" 2>/dev/null) + if [ -n "$hwmon_of_node" ]; then + iface=$(echo "$phy_map" | grep "^$hwmon_of_node:" | cut -d: -f2) + fi + fi + + if [ -n "$iface" ]; then + description="Phy $iface temperature" + else + description="N/A" + fi + + if [ -n "$data" ] && [ "$data" != "N/A" ]; then + # Convert millidegrees to degrees Celsius + temp_c=$(awk "BEGIN {printf \"%.1f\", $data / 1000}") + # Format: sortkey|hwmon|temp|description (sortkey for natural sort by interface) + printf "%s|%-20s %8s°C %s\n" "$iface" "$name" "$temp_c" "$description" >> "$tmpfile" + else + printf "%s|%-20s %8s %s\n" "$iface" "$name" "N/A" "$description" >> "$tmpfile" + fi + done + + # Sort by interface name naturally (e2 before e10), with N/A entries at the end + # Then strip the sort key before displaying + sort -V -t'|' -k1,1 "$tmpfile" | cut -d'|' -f2- + rm -f "$tmpfile" + echo +} + +[ -n "$1" ] || { echo "usage: $0 OUT-DIR"; exit 1; } +work="$1"/system +mkdir -p "${work}" + +thermal_zones > "${work}"/temperature.txt +hwmon >> "${work}"/temperature.txt diff --git a/board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/50-marvell-cn913x.sh b/board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/50-marvell-cn913x.sh new file mode 100755 index 000000000..0aa3168e4 --- /dev/null +++ b/board/aarch64/rootfs/usr/share/product/marvell,armada-ap807/etc/support.d/50-marvell-cn913x.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -e + +ecc_stat() +{ + local chan= + local base= + + for chan in 0 1; do + base=$((0xf0020360 + 0x200 * chan)) + + echo "DRAM Channel $chan ECC Status" + echo -n " Log config: "; devmem $((base + 0x0)) 32 + echo -n " 1b errors: "; devmem $((base + 0x4)) 32 + echo -n " Info 0: "; devmem $((base + 0x8)) 32 + echo -n " Info 1: "; devmem $((base + 0xc)) 32 + echo + done +} + +[ -n "$1" ] || { echo "usage: $0 OUT-DIR"; exit 1; } +work="$1"/marvell-cn913x +mkdir -p "${work}" + +ecc_stat >"${work}"/ecc-stat diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am index f33dd3e9e..e2c4ebf83 100644 --- a/src/bin/Makefile.am +++ b/src/bin/Makefile.am @@ -2,6 +2,7 @@ DISTCLEANFILES = *~ *.d ACLOCAL_AMFLAGS = -I m4 bin_PROGRAMS = copy erase files +sbin_SCRIPTS = support # Bash completion bashcompdir = $(datadir)/bash-completion/completions diff --git a/src/bin/support b/src/bin/support new file mode 100755 index 000000000..ba642a227 --- /dev/null +++ b/src/bin/support @@ -0,0 +1,635 @@ +#!/bin/sh +# Support utilities for troubleshooting Infix systems + +# Program name for usage messages (supports being renamed by users) +prognm=$(basename "$0") + +# +# The collect command gathers system information and outputs a tarball. +# Data is collected to /var/lib/support (or $HOME as fallback) and then +# streamed to stdout. The temporary directory is cleaned up automatically. +# +# The following text is primarily intended for users of older Infix +# systems that do not yet have this script in the root fileystems. +# +# 1. Copy this script to the target device's home directory: +# +# scp support user@device: +# +# 2. SSH to the device and make it executable: +# +# ssh user@device chmod +x support +# +# 3. Run the script from your home directory: +# +# ./support collect > support-data.tar.gz +# +# Or directly via SSH from your workstation: +# +# ssh user@device './support collect' > support-data.tar.gz +# +# Optionally, the output can be encrypted with GPG using a password for +# secure transmission to support personnel, see below. +# +# Examples: +# ./support collect > support-data.tar.gz +# ./support collect -s 5 > support-data.tar.gz +# ./support collect -p > support-data.tar.gz.gpg +# ./support collect -p mypass > support-data.tar.gz.gpg +# +# ssh user@device ./support collect > support-data.tar.gz +# ssh user@device ./support collect -p mypass > support-data.tar.gz.gpg +# +# Note, interactive password prompt (-p without argument) may echo characters +# over SSH due to local terminal echo. Use -p PASSWORD for remote execution, +# or pipe the password: echo "password" | ssh user@device ./support collect -p +# meaning you can even: echo "$SECRET_VARIABLE" | ... which in some cases can +# come in handy. +# +# TODO: +# Add more commands, e.g., verify (run health checks), upload , +# test , backup, watch (dashboard view), diff +# + +cmd_collect() +{ + # Default values + LOG_TAIL_SEC=30 + PASSWORD="" + + # Parse options + while [ $# -gt 0 ]; do + case "$1" in + --log-sec|-s) + if [ -z "$2" ] || [ "$2" -le 0 ] 2>/dev/null; then + echo "Error: --log-sec requires a positive number" >&2 + exit 1 + fi + LOG_TAIL_SEC="$2" + shift 2 + ;; + --password|-p) + # If next arg exists and doesn't start with -, use it as password + if [ -n "$2" ] && [ "${2#-}" = "$2" ]; then + PASSWORD="$2" + shift 2 + else + # Prompt for password interactively from stdin, no echo! + # Disable echo BEFORE printing the prompt + old_stty=$(stty -g 2>/dev/null) + stty -echo 2>/dev/null || true + printf "Enter encryption password: " >&2 + read -r PASSWORD + echo "" >&2 + # Restore terminal settings + if [ -n "$old_stty" ]; then + stty "$old_stty" 2>/dev/null || true + else + stty echo 2>/dev/null || true + fi + if [ -z "$PASSWORD" ]; then + echo "Error: Empty password not allowed" >&2 + exit 1 + fi + shift + fi + ;; + *) + echo "Error: Unknown option '$1'" >&2 + echo "Usage: $prognm collect [-s N] [-p PASSWORD]" >&2 + exit 1 + ;; + esac + done + + # If WORK_DIR not set globally, try /var/lib/support first (more space, + # persistent across user sessions). Fall back to $HOME if we can't create/write there + if [ -z "$WORK_DIR" ]; then + if [ -w /var/lib/support ] 2>/dev/null; then + # Already writable, use it + WORK_DIR="/var/lib/support" + elif [ ! -e /var/lib/support ]; then + # Doesn't exist, try to create it + if mkdir -p /var/lib/support 2>/dev/null; then + WORK_DIR="/var/lib/support" + fi + elif [ -d /var/lib/support ]; then + # Exists but not writable, try to fix permissions (requires root) + # Try chmod first (might just be permission issue), then chown if needed + if chmod 755 /var/lib/support 2>/dev/null && \ + chown "$(id -u):$(id -g)" /var/lib/support 2>/dev/null; then + # Verify it's actually writable now + if [ -w /var/lib/support ] 2>/dev/null; then + WORK_DIR="/var/lib/support" + fi + fi + fi + + # Fall back to $HOME if we couldn't set up /var/lib/support + if [ -z "$WORK_DIR" ]; then + WORK_DIR="${HOME}" + echo "Warning: Cannot write to /var/lib/support, using home directory instead." >&2 + echo " (This may fill up your home directory on systems with limited space)" >&2 + fi + fi + + # Create unique collection directory + COLLECT_DIR="${WORK_DIR}/support-$(hostname -s)-$(date -Iseconds)" + EXEC_LOG="${COLLECT_DIR}/collection.log" + + # Cleanup on exit + cleanup() + { + echo "[$(date -Iseconds)] Cleanup called (signal: ${1:-EXIT})" >> "${EXEC_LOG}" 2>&1 || echo "[$(date -Iseconds)] Cleanup called (signal: ${1:-EXIT})" >&2 + if [ -d "${COLLECT_DIR}" ]; then + echo "[$(date -Iseconds)] Removing collection directory: ${COLLECT_DIR}" >> "${EXEC_LOG}" 2>&1 || echo "[$(date -Iseconds)] Removing: ${COLLECT_DIR}" >&2 + rm -rf "${COLLECT_DIR}" + else + echo "[$(date -Iseconds)] Collection directory already gone: ${COLLECT_DIR}" >> "${EXEC_LOG}" 2>&1 || echo "[$(date -Iseconds)] Already gone: ${COLLECT_DIR}" >&2 + fi + } + trap cleanup EXIT INT TERM + + # Create collection directory + if ! mkdir -p "${COLLECT_DIR}"; then + echo "Error: Cannot create collection directory: ${COLLECT_DIR}" >&2 + echo " Check permissions for ${WORK_DIR}" >&2 + exit 1 + fi + + # Helper function to run commands with output to specific file + collect() + { + output_file="$1" + shift + cmd_desc="$*" + + mkdir -p "${COLLECT_DIR}/$(dirname "$output_file")" + echo "[$(date -Iseconds)] Collecting: $cmd_desc -> ${output_file}" >> "${EXEC_LOG}" 2>&1 + if "$@" > "${COLLECT_DIR}/${output_file}" 2>> "${EXEC_LOG}"; then + echo "[$(date -Iseconds)] Success: ${output_file}" >> "${EXEC_LOG}" 2>&1 + else + exit_code=$? + echo "[$(date -Iseconds)] Failed (exit ${exit_code}): ${output_file}" >> "${EXEC_LOG}" 2>&1 + # Create placeholder file indicating failure + echo "Command failed with exit code ${exit_code}: $cmd_desc" > "${COLLECT_DIR}/${output_file}" + fi + } + + # Start collection log + echo "=== Infix Support Data Collection ===" > "${EXEC_LOG}" + echo "Started: $(date -Iseconds)" >> "${EXEC_LOG}" + echo "Hostname: $(hostname)" >> "${EXEC_LOG}" + echo "Work directory: ${WORK_DIR}" >> "${EXEC_LOG}" + echo "Collection directory: ${COLLECT_DIR}" >> "${EXEC_LOG}" + echo "" >> "${EXEC_LOG}" + + # Inform user that collection is starting (to stderr for SSH visibility) + echo "Starting support data collection from $(hostname)..." >&2 + echo "Collecting to: ${WORK_DIR}" >&2 + echo "This may take up to a minute. Please wait..." >&2 + + # System identification + collect system-info.txt uname -a + collect hostname.txt hostname + collect uptime.txt uptime + + # Configuration files + collect running-config.json sysrepocfg -f json -d running -X + collect operational-config.json sysrepocfg -f json -d operational -X + + # Sysrepo YANG modules + if command -v sysrepoctl >/dev/null 2>&1; then + collect sysrepo-modules.txt sysrepoctl -l + fi + + # Startup config (may not exist on first boot) + if [ -f /cfg/startup-config.cfg ]; then + cp /cfg/startup-config.cfg "${COLLECT_DIR}/startup-config.cfg" 2>> "${EXEC_LOG}" + else + echo "No startup-config.cfg found" > "${COLLECT_DIR}/startup-config.cfg" + fi + + # System logs and runtime data + if [ -d /var/log ]; then + if ls -A /var/log >/dev/null 2>&1; then + tar czf "${COLLECT_DIR}/logs.tar.gz" -C / var/log 2>> "${EXEC_LOG}" || \ + echo "Failed to collect /var/log" > "${COLLECT_DIR}/logs.tar.gz.error" + else + echo "No logs in /var/log" > "${COLLECT_DIR}/logs.tar.gz.error" + fi + fi + + # Collect crash dumps if they exist + if [ -d /var/crash ]; then + if ls -A /var/crash >/dev/null 2>&1; then + tar czf "${COLLECT_DIR}/crash.tar.gz" -C / var/crash 2>> "${EXEC_LOG}" || \ + echo "Failed to collect /var/crash" > "${COLLECT_DIR}/crash.tar.gz.error" + else + echo "No crash dumps in /var/crash" > "${COLLECT_DIR}/crash.tar.gz.error" + fi + fi + + # Tail /var/log/messages to capture live logging + if [ -f /var/log/messages ]; then + echo "Tailing /var/log/messages for ${LOG_TAIL_SEC} seconds (please wait)..." >&2 + { + echo "=== Starting tail of /var/log/messages for ${LOG_TAIL_SEC} seconds ===" + timeout "${LOG_TAIL_SEC}" tail -F /var/log/messages 2>/dev/null || true + echo "" + echo "=== End of ${LOG_TAIL_SEC}-second tail ===" + } > "${COLLECT_DIR}/logs-tail-${LOG_TAIL_SEC}s.txt" 2>> "${EXEC_LOG}" + echo "Log tail complete." >&2 + fi + + if [ -d /run/net ]; then + if ls -A /run/net >/dev/null 2>&1; then + tar czf "${COLLECT_DIR}/run-net.tar.gz" -C / run/net 2>> "${EXEC_LOG}" || \ + echo "Failed to collect /run/net" > "${COLLECT_DIR}/run-net.tar.gz.error" + else + echo "No data in /run/net" > "${COLLECT_DIR}/run-net.tar.gz.error" + fi + fi + + if [ -d /run/finit ]; then + if ls -A /run/finit >/dev/null 2>&1; then + tar czf "${COLLECT_DIR}/run-finit.tar.gz" -C / run/finit 2>> "${EXEC_LOG}" || \ + echo "Failed to collect /run/finit" > "${COLLECT_DIR}/run-finit.tar.gz.error" + else + echo "No data in /run/finit" > "${COLLECT_DIR}/run-finit.tar.gz.error" + fi + fi + + # Kernel and system state + collect system/dmesg.txt dmesg + collect system/free.txt free -h + collect system/stat.txt cat /proc/stat + collect system/softirqs.txt cat /proc/softirqs + collect system/locks.txt cat /proc/locks + collect system/meminfo.txt cat /proc/meminfo + collect system/file-nr.txt cat /proc/sys/fs/file-nr + collect system/ps.txt ps -o pid,rss,comm + collect system/df.txt df -h + + # Finit/init state + if command -v initctl >/dev/null 2>&1; then + collect initctl/cgroup.txt initctl cgroup + collect initctl/status.txt initctl status + fi + + # CPU statistics + if command -v mpstat >/dev/null 2>&1; then + collect system/mpstat.txt mpstat -P ALL 1 1 + else + echo "mpstat not available" > "${COLLECT_DIR}/mpstat.txt" + fi + + # Top output (two samples) + collect system/top.txt sh -c 'top -b -n 2 -d 2' + + # Interrupt statistics (before and after 2 second delay) + collect system/interrupts1.txt cat /proc/interrupts + sleep 2 + collect system/interrupts2.txt cat /proc/interrupts + + # Network information + collect network/ip/addr.json ip -s -d -j addr show + collect network/ip/route.json ip -s -d -j route show + collect network/ip/link.json ip -s -d -j link show + collect network/ip/neigh.json ip -s -d -j neigh show + collect network/ifconfig.txt ifconfig -a + + # Collect ethtool information for all ethernet interfaces + if command -v ethtool >/dev/null 2>&1; then + # Get list of ethernet interfaces (any interface with link/ether) + ip -o link show | grep 'link/ether' | awk -F': ' '{print $2}' > "${COLLECT_DIR}/.iface-list" 2>> "${EXEC_LOG}" + if [ -s "${COLLECT_DIR}/.iface-list" ]; then + while IFS= read -r iface; do + # ethtool typically needs root + collect "network/ethtool/${iface}.txt" ethtool "$iface" + collect "network/ethtool/stats-${iface}.txt" ethtool -S "$iface" + collect "network/ethtool/module-${iface}.txt" ethtool -m "$iface" + done < "${COLLECT_DIR}/.iface-list" + fi + rm -f "${COLLECT_DIR}/.iface-list" + fi + + if command -v bridge >/dev/null 2>&1; then + collect network/bridge/link.json bridge -d -s -j link show + collect network/bridge/fdb.json bridge -d -s -j fdb show + fi + + # Firewall rules + if command -v nft >/dev/null 2>&1; then + collect network/nftables.txt nft list ruleset + fi + + # FRR routing information + if command -v vtysh >/dev/null 2>&1; then + collect frr/running-config.txt vtysh -c "show running-config" + collect frr/ip-route.txt vtysh -c "show ip route" + collect frr/ospf-interfaces.txt vtysh -c "show ip ospf interfaces" + collect frr/ospf-neighbor.txt vtysh -c "show ip ospf neighbor" + collect frr/ospf-routes.txt vtysh -c "show ip ospf routes" + collect frr/bgp-summary.txt vtysh -c "show ip bgp summary" + collect frr/bfd-peers.txt vtysh -c "show bfd peers" + fi + + # Container information + if command -v podman >/dev/null 2>&1; then + # List all containers + collect podman/ps.txt podman ps -a + + # Collect podman system info + collect podman/info.json podman info --format=json + + # Get list of containers and inspect each + if podman ps -a --format '{{.Names}}' > "${COLLECT_DIR}/.container-list" 2>> "${EXEC_LOG}"; then + while IFS= read -r container; do + if [ -n "$container" ]; then + safe_name=$(echo "$container" | tr '/' '_') + collect "podman/inspect-${safe_name}.json" podman inspect "$container" + fi + done < "${COLLECT_DIR}/.container-list" + rm -f "${COLLECT_DIR}/.container-list" + fi + fi + + # Additional system information + collect system/lsmod.txt lsmod + if command -v lspci >/dev/null 2>&1; then + collect system/lspci.txt lspci -v + else + echo "lspci not available" > "${COLLECT_DIR}/lspci.txt" + fi + + if command -v lsusb >/dev/null 2>&1; then + collect system/lsusb.txt lsusb -v + else + echo "lsusb not available" > "${COLLECT_DIR}/lsusb.txt" + fi + + # Disk I/O stats + if command -v iostat >/dev/null 2>&1; then + collect system/iostat.txt iostat -x 1 2 + else + echo "iostat not available" > "${COLLECT_DIR}/iostat.txt" + fi + + # Process resource usage + if command -v pstree >/dev/null 2>&1; then + collect system/pstree.txt pstree -p + else + collect system/pstree.txt ps fax + fi + + # Environment and versions + collect system/env.txt env + + # Network sockets + if command -v netstat >/dev/null 2>&1; then + collect system/netstat.txt netstat -tunlp + else + collect system/netstat.txt ss -tunlp + fi + + for script in $(find "/etc/support.d" -type f -executable 2>/dev/null | sort); do + echo "[$(date -Iseconds)] Running ${script}..." >> "${EXEC_LOG}" 2>&1 + + if "${script}" "${COLLECT_DIR}" >> "${EXEC_LOG}" 2>&1; then + echo "[$(date -Iseconds)] Success: ${script}" >> "${EXEC_LOG}" 2>&1 + else + exit_code=$? + echo "[$(date -Iseconds)] Failed (exit ${exit_code}): ${script}" >> "${EXEC_LOG}" 2>&1 + fi + done + + # Completion timestamp in log + echo "" >> "${EXEC_LOG}" + echo "Completed: $(date -Iseconds)" >> "${EXEC_LOG}" + + # Notify user that collection is done + echo "Collection complete. Creating archive..." >&2 + + # Create final tar.gz and output to stdout + # Use -C to change to parent directory so paths in archive don't include full path + echo "[$(date -Iseconds)] Changing to work directory: ${WORK_DIR}" >> "${EXEC_LOG}" 2>&1 + if ! cd "${WORK_DIR}"; then + echo "[$(date -Iseconds)] ERROR: Failed to cd to ${WORK_DIR}" >> "${EXEC_LOG}" 2>&1 + echo "Error: Cannot change to work directory ${WORK_DIR}" >&2 + exit 1 + fi + echo "[$(date -Iseconds)] Successfully changed to: $(pwd)" >> "${EXEC_LOG}" 2>&1 + echo "[$(date -Iseconds)] Creating archive from: $(basename "${COLLECT_DIR}")" >> "${EXEC_LOG}" 2>&1 + + # Check if password encryption is requested + if [ -n "$PASSWORD" ]; then + if ! command -v gpg >/dev/null 2>&1; then + echo "Error: --password specified but gpg is not available" >&2 + exit 1 + fi + echo "Encrypting with GPG..." >&2 + echo "[$(date -Iseconds)] Starting tar with GPG encryption" >> "${EXEC_LOG}" 2>&1 + tar czf - "$(basename "${COLLECT_DIR}")" 2>> "${EXEC_LOG}" | \ + gpg --batch --yes --passphrase "$PASSWORD" --pinentry-mode loopback -c 2>> "${EXEC_LOG}" + tar_exit=$? + echo "[$(date -Iseconds)] tar+gpg pipeline exit code: $tar_exit" >> "${EXEC_LOG}" 2>&1 + echo "" >&2 + echo "WARNING: Remember to share the encryption password out-of-band!" >&2 + echo " Do not send it in the same email as the encrypted file." >&2 + if [ $tar_exit -ne 0 ]; then + echo "[$(date -Iseconds)] ERROR: tar+gpg failed with exit code $tar_exit" >> "${EXEC_LOG}" 2>&1 + exit $tar_exit + fi + else + echo "[$(date -Iseconds)] Starting tar (no encryption)" >> "${EXEC_LOG}" 2>&1 + tar czf - "$(basename "${COLLECT_DIR}")" 2>> "${EXEC_LOG}" + tar_exit=$? + echo "[$(date -Iseconds)] tar exit code: $tar_exit" >> "${EXEC_LOG}" 2>&1 + if [ $tar_exit -ne 0 ]; then + echo "[$(date -Iseconds)] ERROR: tar failed with exit code $tar_exit" >> "${EXEC_LOG}" 2>&1 + exit $tar_exit + fi + fi + echo "[$(date -Iseconds)] Archive creation completed successfully" >> "${EXEC_LOG}" 2>&1 +} + +cmd_clean() +{ + dry_run=0 + days=7 + + # Parse options + while [ $# -gt 0 ]; do + case "$1" in + --dry-run|-n) + dry_run=1 + shift + ;; + --days|-d) + if [ -z "$2" ] || [ "$2" -le 0 ] 2>/dev/null; then + echo "Error: --days requires a positive number" >&2 + exit 1 + fi + days="$2" + shift 2 + ;; + *) + echo "Error: Unknown option '$1'" >&2 + echo "Usage: $prognm clean [--dry-run] [--days N]" >&2 + exit 1 + ;; + esac + done + + if [ "$dry_run" -eq 1 ]; then + echo "Dry run - no files will be deleted" + echo "" + fi + + echo "Looking for support directories older than ${days} days..." + echo "" + + # If WORK_DIR is set globally, only search there + # Otherwise search in both /var/lib/support and $HOME + if [ -n "$WORK_DIR" ]; then + search_dirs="$WORK_DIR" + else + search_dirs="/var/lib/support ${HOME}" + fi + total_count=0 + + for search_dir in $search_dirs; do + if [ ! -d "$search_dir" ]; then + continue + fi + + # Use find to locate old support directories + # The pattern matches: support-YYYY-MM-DD* format + find "$search_dir" -maxdepth 1 -type d -name "support-*-20*" -mtime "+${days}" 2>/dev/null | while IFS= read -r dir; do + size=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') + mtime=$(stat -c %y "$dir" 2>/dev/null | cut -d' ' -f1) + + if [ "$dry_run" -eq 1 ]; then + echo "Would remove: $dir ($size, modified: $mtime)" + else + echo "Removing: $dir ($size, modified: $mtime)" + rm -rf "$dir" + fi + done + + # Count directories found in this location + count=$(find "$search_dir" -maxdepth 1 -type d -name "support-*-20*" -mtime "+${days}" 2>/dev/null | wc -l) + total_count=$((total_count + count)) + done + + echo "" + if [ "$total_count" -eq 0 ]; then + echo "No support directories older than ${days} days found in /var/lib/support or home directory." + elif [ "$dry_run" -eq 1 ]; then + echo "Found ${total_count} directories. Run without --dry-run to remove them." + else + echo "Removed ${total_count} directories." + fi +} + +usage() +{ + echo "Usage: $prognm [global-options] [options]" + echo "" + echo "Note: Run with 'sudo' for complete data collection (dmesg, ethtool, etc.)" + echo "" + echo "Global options:" + echo " -u, --unprivileged Allow running without root (some data will be missing)" + echo " -w, --work-dir PATH Use PATH as working directory for collection/cleanup" + echo " (default: /var/lib/support with fallback to \$HOME)" + echo "" + echo "Commands:" + echo " collect [options] Collect system information for support/troubleshooting" + echo " NOTE: may take up to a minute to finish, please wait!" + echo " clean [options] Remove old support collection directories" + echo "" + echo "Options for collect:" + echo " -s, --log-sec SEC Tail /var/log/messages for SEC seconds (default: 30)" + echo " -p, --password [PASS] Encrypt output with GPG. If PASS is omitted, prompts" + echo " interactively or reads from stdin, so possible to do" + echo " echo "\$MYSECRET" | ... (recommended for security)" + echo "" + echo "Options for clean:" + echo " -n, --dry-run Show what would be deleted without deleting" + echo " -d, --days N Remove directories older than N days (default: 7)" + echo "" + echo "Examples:" + echo " sudo $prognm collect > support-data.tar.gz" + echo " sudo $prognm collect -p > support-data.tar.gz.gpg" + echo " sudo $prognm collect --password mypass > support-data.tar.gz.gpg" + echo " sudo $prognm --work-dir /tmp/ram collect > support-data.tar.gz" + echo " ssh user@device 'sudo $prognm collect' > support-data.tar.gz" + echo " $prognm -u collect > support-data.tar.gz (degraded)" + echo " sudo $prognm clean --dry-run" + echo " sudo $prognm clean --days 30" + echo " sudo $prognm --work-dir /tmp/ram clean" + exit 1 +} + +# Main command dispatcher +# Parse global options first +WORK_DIR="" +ALLOW_UNPRIVILEGED=0 + +while [ $# -gt 0 ]; do + case "$1" in + -u|--unprivileged) + ALLOW_UNPRIVILEGED=1 + shift + ;; + -w|--work-dir) + if [ -z "$2" ]; then + echo "Error: --work-dir requires a path argument" >&2 + exit 1 + fi + WORK_DIR="$2" + shift 2 + ;; + -*) + # Unknown option - might be a command-specific option + break + ;; + *) + # Not an option, must be the command + break + ;; + esac +done + +if [ $# -lt 1 ]; then + usage +fi + +command="$1" +shift + +case "$command" in + collect|clean) + # Check if running as root (uid 0) + if [ "$(id -u)" -ne 0 ] && [ "$ALLOW_UNPRIVILEGED" -eq 0 ]; then + echo "Error: This command should be run with 'sudo' for complete data collection." >&2 + echo " Use -u/--unprivileged to run as a regular user in degraded mode." >&2 + exit 1 + fi + + if [ "$command" = "collect" ]; then + cmd_collect "$@" + else + cmd_clean "$@" + fi + ;; + help|--help|-h) + usage + ;; + *) + echo "Error: Unknown command '$command'" >&2 + echo "" >&2 + usage + ;; +esac From 2557eb490079d3e495ac6c187c2689e5c80a2f88 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 2 Dec 2025 13:35:56 +0100 Subject: [PATCH 5/9] confd: add support for toggling OSPF debug logs Backported from origin/main 7edc3c19f7c63eca08018984d6e3487a7b465107 Fixes #1281 Signed-off-by: Joachim Wiberg --- src/confd/src/ietf-routing.c | 45 ++++++++++++++---- src/confd/yang/confd.inc | 2 +- src/confd/yang/confd/infix-routing.yang | 46 +++++++++++++++++++ ...-27.yang => infix-routing@2025-12-02.yang} | 0 4 files changed, 82 insertions(+), 11 deletions(-) rename src/confd/yang/confd/{infix-routing@2024-11-27.yang => infix-routing@2025-12-02.yang} (100%) diff --git a/src/confd/src/ietf-routing.c b/src/confd/src/ietf-routing.c index 9e0a6ae90..cbed53629 100644 --- a/src/confd/src/ietf-routing.c +++ b/src/confd/src/ietf-routing.c @@ -130,15 +130,7 @@ int parse_ospf_areas(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf) { - const char *static_debug = "! OSPF default debug\ -debug ospf bfd\n\ -debug ospf packet all detail\n\ -debug ospf ism\n\ -debug ospf nsm\n\ -debug ospf default-information\n\ -debug ospf nssa\n\ -! OSPF configuration\n"; - struct lyd_node *areas, *default_route; + struct lyd_node *areas, *default_route, *debug; const char *router_id; int bfd_enabled = 0; int num_areas = 0; @@ -151,7 +143,40 @@ debug ospf nssa\n\ } fputs(FRR_STATIC_CONFIG, fp); - fputs(static_debug, fp); + + /* Handle OSPF debug configuration */ + debug = lydx_get_child(ospf, "debug"); + if (debug) { + int any_debug = 0; + + if (lydx_get_bool(debug, "bfd")) { + fputs("debug ospf bfd\n", fp); + any_debug = 1; + } + if (lydx_get_bool(debug, "packet")) { + fputs("debug ospf packet all detail\n", fp); + any_debug = 1; + } + if (lydx_get_bool(debug, "ism")) { + fputs("debug ospf ism\n", fp); + any_debug = 1; + } + if (lydx_get_bool(debug, "nsm")) { + fputs("debug ospf nsm\n", fp); + any_debug = 1; + } + if (lydx_get_bool(debug, "default-information")) { + fputs("debug ospf default-information\n", fp); + any_debug = 1; + } + if (lydx_get_bool(debug, "nssa")) { + fputs("debug ospf nssa\n", fp); + any_debug = 1; + } + + if (any_debug) + fputs("!\n", fp); + } areas = lydx_get_child(ospf, "areas"); router_id = lydx_get_cattr(ospf, "explicit-router-id"); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index f260c53a6..f0b785f90 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -22,7 +22,7 @@ MODULES=( "ieee802-dot1q-types@2022-10-29.yang" "infix-ip@2024-09-16.yang" "infix-if-type@2025-02-12.yang" - "infix-routing@2024-11-27.yang" + "infix-routing@2025-12-02.yang" "ieee802-dot1ab-lldp@2022-03-15.yang" "infix-lldp@2025-05-05.yang" "infix-dhcp-common@2025-01-29.yang" diff --git a/src/confd/yang/confd/infix-routing.yang b/src/confd/yang/confd/infix-routing.yang index effdcff56..aa90d2ba1 100644 --- a/src/confd/yang/confd/infix-routing.yang +++ b/src/confd/yang/confd/infix-routing.yang @@ -20,6 +20,13 @@ module infix-routing { contact "kernelkit@googlegroups.com"; description "Deviations and augments for ietf-routing and ietf-ospf."; + revision 2025-12-02 { + description "Add configurable OSPF debug logging container. + Allows users to enable specific OSPF debug categories + (bfd, packet, ism, nsm, default-information, nssa) for troubleshooting. + All debug options default to disabled to prevent log flooding."; + reference "Issue #1281"; + } revision 2024-11-27 { description "Deviate address-family in OSPF"; reference "internal"; @@ -278,6 +285,45 @@ module infix-routing { } } } + augment "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf" { + description "Add support for configurable OSPF debug logging. + Allows users to enable specific OSPF debug categories for troubleshooting. + All debug options default to disabled to prevent log flooding in + production environments."; + container debug { + description "OSPF debug logging configuration"; + leaf bfd { + type boolean; + default false; + description "Enable OSPF BFD (Bidirectional Forwarding Detection) debug messages"; + } + leaf packet { + type boolean; + default false; + description "Enable detailed OSPF packet debug messages (all packet types)"; + } + leaf ism { + type boolean; + default false; + description "Enable OSPF Interface State Machine debug messages"; + } + leaf nsm { + type boolean; + default false; + description "Enable OSPF Neighbor State Machine debug messages"; + } + leaf default-information { + type boolean; + default false; + description "Enable OSPF default route origination debug messages"; + } + leaf nssa { + type boolean; + default false; + description "Enable OSPF NSSA (Not-So-Stubby Area) debug messages"; + } + } + } deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface" { deviate add { must "count(../../../../ospf:areas/ospf:area/ospf:interfaces/ospf:interface[ospf:name=current()/name]) <= 1" { diff --git a/src/confd/yang/confd/infix-routing@2024-11-27.yang b/src/confd/yang/confd/infix-routing@2025-12-02.yang similarity index 100% rename from src/confd/yang/confd/infix-routing@2024-11-27.yang rename to src/confd/yang/confd/infix-routing@2025-12-02.yang From 73edf1bbd68bcfe04943bbb2cc569ee486186892 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 8 Dec 2025 16:30:42 +0100 Subject: [PATCH 6/9] board/common: log disk/mem/filenr resource usage every hour Dec 8 15:22:44 ix-00-00-00 watchdogd[2599]: Memory usage: 195036 kB, cached: 69740 kB, total: 423628 kB Dec 8 15:22:44 ix-00-00-00 watchdogd[2599]: File system /var usage: blocks 4710/52564 inodes 80/65456 Dec 8 15:22:44 ix-00-00-00 watchdogd[2599]: File descriptor usage: 640/34603 Signed-off-by: Joachim Wiberg --- board/common/rootfs/etc/watchdogd.conf | 49 +++++++++++++++----------- configs/aarch64_defconfig | 6 ++++ configs/aarch64_minimal_defconfig | 6 ++++ configs/riscv64_defconfig | 6 ++++ configs/x86_64_defconfig | 6 ++++ configs/x86_64_minimal_defconfig | 6 ++++ 6 files changed, 59 insertions(+), 20 deletions(-) diff --git a/board/common/rootfs/etc/watchdogd.conf b/board/common/rootfs/etc/watchdogd.conf index e4c4bdbe8..b2c328ff5 100644 --- a/board/common/rootfs/etc/watchdogd.conf +++ b/board/common/rootfs/etc/watchdogd.conf @@ -90,9 +90,9 @@ reset-reason { # Monitors file descriptor leaks based on /proc/sys/fs/file-nr filenr { -# enabled = true - interval = 300 - logmark = false + enabled = true + interval = 3600 + logmark = true warning = 0.9 critical = 1.0 # script = "/path/to/alt-reboot-action.sh" @@ -102,33 +102,42 @@ filenr { # The script is called with fsmon as the first argument and there # are two environment variables FSMON_NAME, for the monitored path, # and FSMON_TYPE indicating either 'blocks' or 'inodes'. -#fsmon /var { -# enabled = true -# interval = 300 -# logmark = false -# warning = 0.95 -# critical = 1.0 +fsmon /var { + enabled = true + interval = 3600 + logmark = true + warning = 0.95 + critical = 1.0 # script = "/path/to/alt-reboot-action.sh" -#} +} + +fsmon /tmp { + enabled = true + interval = 3600 + logmark = true + warning = 0.95 + critical = 1.0 +# script = "/path/to/alt-reboot-action.sh" +} # Monitors load average based on sysinfo() from /proc/loadavg # The level is composed from the average of the 1 and 5 min marks. -loadavg { +#loadavg { # enabled = true - interval = 300 - logmark = false - warning = 1.0 - critical = 2.0 +# interval = 300 +# logmark = true +# warning = 1.0 +# critical = 2.0 # script = "/path/to/alt-reboot-action.sh" -} +#} # Monitors free RAM based on data from /proc/meminfo meminfo { -# enabled = true - interval = 300 - logmark = false + enabled = true + interval = 3600 + logmark = true warning = 0.9 - critical = 0.95 + critical = 0.97 # script = "/path/to/alt-reboot-action.sh" } diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 9089e43fe..d0ce71253 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -111,6 +111,12 @@ BR2_PACKAGE_RAUC_JSON=y BR2_PACKAGE_SYSKLOGD=y BR2_PACKAGE_SYSKLOGD_LOGGER=y BR2_PACKAGE_WATCHDOGD=y +BR2_PACKAGE_WATCHDOGD_GENERIC=y +BR2_PACKAGE_WATCHDOGD_LOADAVG=y +BR2_PACKAGE_WATCHDOGD_FILENR=y +BR2_PACKAGE_WATCHDOGD_MEMINFO=y +BR2_PACKAGE_WATCHDOGD_FSMON=y +BR2_PACKAGE_WATCHDOGD_TEMPMON BR2_PACKAGE_LESS=y BR2_PACKAGE_MG=y BR2_PACKAGE_NANO=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index 6c7e900f7..b72fc3da6 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -94,6 +94,12 @@ BR2_PACKAGE_RAUC_JSON=y BR2_PACKAGE_SYSKLOGD=y BR2_PACKAGE_SYSKLOGD_LOGGER=y BR2_PACKAGE_WATCHDOGD=y +BR2_PACKAGE_WATCHDOGD_GENERIC=y +BR2_PACKAGE_WATCHDOGD_LOADAVG=y +BR2_PACKAGE_WATCHDOGD_FILENR=y +BR2_PACKAGE_WATCHDOGD_MEMINFO=y +BR2_PACKAGE_WATCHDOGD_FSMON=y +BR2_PACKAGE_WATCHDOGD_TEMPMON BR2_PACKAGE_LESS=y BR2_PACKAGE_MG=y BR2_PACKAGE_NANO=y diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index 8aca79ed1..df2cb5b1f 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -123,6 +123,12 @@ BR2_PACKAGE_RAUC_JSON=y BR2_PACKAGE_SYSKLOGD=y BR2_PACKAGE_SYSKLOGD_LOGGER=y BR2_PACKAGE_WATCHDOGD=y +BR2_PACKAGE_WATCHDOGD_GENERIC=y +BR2_PACKAGE_WATCHDOGD_LOADAVG=y +BR2_PACKAGE_WATCHDOGD_FILENR=y +BR2_PACKAGE_WATCHDOGD_MEMINFO=y +BR2_PACKAGE_WATCHDOGD_FSMON=y +BR2_PACKAGE_WATCHDOGD_TEMPMON BR2_PACKAGE_LESS=y BR2_PACKAGE_MG=y BR2_PACKAGE_NANO=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 8a8b51d2a..9dd7e98e7 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -109,6 +109,12 @@ BR2_PACKAGE_RAUC_JSON=y BR2_PACKAGE_SYSKLOGD=y BR2_PACKAGE_SYSKLOGD_LOGGER=y BR2_PACKAGE_WATCHDOGD=y +BR2_PACKAGE_WATCHDOGD_GENERIC=y +BR2_PACKAGE_WATCHDOGD_LOADAVG=y +BR2_PACKAGE_WATCHDOGD_FILENR=y +BR2_PACKAGE_WATCHDOGD_MEMINFO=y +BR2_PACKAGE_WATCHDOGD_FSMON=y +BR2_PACKAGE_WATCHDOGD_TEMPMON BR2_PACKAGE_LESS=y BR2_PACKAGE_MG=y BR2_PACKAGE_NANO=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 4bcb82895..87ba4a1ab 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -93,6 +93,12 @@ BR2_PACKAGE_RAUC_JSON=y BR2_PACKAGE_SYSKLOGD=y BR2_PACKAGE_SYSKLOGD_LOGGER=y BR2_PACKAGE_WATCHDOGD=y +BR2_PACKAGE_WATCHDOGD_GENERIC=y +BR2_PACKAGE_WATCHDOGD_LOADAVG=y +BR2_PACKAGE_WATCHDOGD_FILENR=y +BR2_PACKAGE_WATCHDOGD_MEMINFO=y +BR2_PACKAGE_WATCHDOGD_FSMON=y +BR2_PACKAGE_WATCHDOGD_TEMPMON=y BR2_PACKAGE_LESS=y BR2_PACKAGE_MG=y BR2_PACKAGE_NANO=y From b04bae1ad64c949b606ed45a9170e432d05a135d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 8 Dec 2025 16:50:10 +0100 Subject: [PATCH 7/9] board/*/linux_defconfig: panic on lockups and hung tasks Turn on OOPS-to-panic, soft/hard lockup panic, hung-task panic, and extra workqueue watchdog reporting. This makes latent stalls visible instead of silently freezing, improving diagnosis of issues like the recent resource-pressure lockup. Signed-off-by: Joachim Wiberg --- board/aarch64/linux_defconfig | 8 +++++++- board/riscv64/linux_defconfig | 9 ++++++++- board/x86_64/linux_defconfig | 7 ++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index f6efbf0d9..5979e344b 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -549,7 +549,13 @@ CONFIG_MAGIC_SYSRQ=y CONFIG_DEBUG_FS=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=20 -CONFIG_DETECT_HUNG_TASK=y +CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y +CONFIG_HARDLOCKUP_DETECTOR=y +CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y +CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y +CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y +CONFIG_WQ_WATCHDOG=y +CONFIG_WQ_CPU_INTENSIVE_REPORT=y # CONFIG_SCHED_DEBUG is not set # CONFIG_RCU_TRACE is not set CONFIG_FUNCTION_TRACER=y diff --git a/board/riscv64/linux_defconfig b/board/riscv64/linux_defconfig index 0933fae33..d2512e279 100644 --- a/board/riscv64/linux_defconfig +++ b/board/riscv64/linux_defconfig @@ -461,8 +461,15 @@ CONFIG_DEBUG_FS=y # CONFIG_SLUB_DEBUG is not set CONFIG_DEBUG_RODATA_TEST=y CONFIG_DEBUG_WX=y -CONFIG_SOFTLOCKUP_DETECTOR=y +CONFIG_PANIC_ON_OOPS=y +CONFIG_PANIC_TIMEOUT=20 +CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y +CONFIG_HARDLOCKUP_DETECTOR=y +CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y +CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y +CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y CONFIG_WQ_WATCHDOG=y +CONFIG_WQ_CPU_INTENSIVE_REPORT=y # CONFIG_SCHED_DEBUG is not set CONFIG_STACKTRACE=y CONFIG_RCU_CPU_STALL_TIMEOUT=60 diff --git a/board/x86_64/linux_defconfig b/board/x86_64/linux_defconfig index e2328779d..3e39d3c77 100644 --- a/board/x86_64/linux_defconfig +++ b/board/x86_64/linux_defconfig @@ -266,7 +266,12 @@ CONFIG_MAGIC_SYSRQ=y CONFIG_DEBUG_FS=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=20 -CONFIG_DETECT_HUNG_TASK=y +CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y +CONFIG_HARDLOCKUP_DETECTOR=y +CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY=y +CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y +CONFIG_WQ_WATCHDOG=y +CONFIG_WQ_CPU_INTENSIVE_REPORT=y CONFIG_FUNCTION_TRACER=y CONFIG_UNWINDER_FRAME_POINTER=y From 6f6bb412f9680fd0643021635c00a052523d5b05 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 12 Dec 2025 12:52:59 +0000 Subject: [PATCH 8/9] aarch64: kernel: Enable SBSA watchdog Usable by all Server Base System Architecture (SBSA) compliant SoCs, e.g., CN9130. --- board/aarch64/linux_defconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index 5979e344b..066a46d23 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -401,6 +401,7 @@ CONFIG_WATCHDOG=y CONFIG_WATCHDOG_SYSFS=y CONFIG_SOFT_WATCHDOG=y CONFIG_GPIO_WATCHDOG=y +CONFIG_ARM_SBSA_WATCHDOG=y CONFIG_ARMADA_37XX_WATCHDOG=y CONFIG_I6300ESB_WDT=y CONFIG_MFD_MAX77620=y From 0a3879f5214178a48e73809872a0434372cc8813 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 12 Dec 2025 14:43:16 +0000 Subject: [PATCH 9/9] kernel: Enable test_lockup module This will be us the ability to test a hardware watchdog's ability to trigger on different kinds of lockups. --- board/aarch64/linux_defconfig | 1 + board/riscv64/linux_defconfig | 1 + board/x86_64/linux_defconfig | 1 + 3 files changed, 3 insertions(+) diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index 066a46d23..f538e98c2 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -557,6 +557,7 @@ CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y CONFIG_WQ_WATCHDOG=y CONFIG_WQ_CPU_INTENSIVE_REPORT=y +CONFIG_TEST_LOCKUP=m # CONFIG_SCHED_DEBUG is not set # CONFIG_RCU_TRACE is not set CONFIG_FUNCTION_TRACER=y diff --git a/board/riscv64/linux_defconfig b/board/riscv64/linux_defconfig index d2512e279..cb3711442 100644 --- a/board/riscv64/linux_defconfig +++ b/board/riscv64/linux_defconfig @@ -470,6 +470,7 @@ CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y CONFIG_WQ_WATCHDOG=y CONFIG_WQ_CPU_INTENSIVE_REPORT=y +CONFIG_TEST_LOCKUP=m # CONFIG_SCHED_DEBUG is not set CONFIG_STACKTRACE=y CONFIG_RCU_CPU_STALL_TIMEOUT=60 diff --git a/board/x86_64/linux_defconfig b/board/x86_64/linux_defconfig index 3e39d3c77..d74cf834d 100644 --- a/board/x86_64/linux_defconfig +++ b/board/x86_64/linux_defconfig @@ -273,5 +273,6 @@ CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y CONFIG_WQ_WATCHDOG=y CONFIG_WQ_CPU_INTENSIVE_REPORT=y +CONFIG_TEST_LOCKUP=m CONFIG_FUNCTION_TRACER=y CONFIG_UNWINDER_FRAME_POINTER=y