From 0dc380aed9f15e6c8ffa9ff3a8cb215a9b11c2d7 Mon Sep 17 00:00:00 2001 From: Ejub Sabic Date: Mon, 22 Jun 2026 15:00:31 +0200 Subject: [PATCH 1/4] Add support for unattended updates This commit introduces unattended-update cron consumer. Unattended updates are configurable to perform update autonomously or to wait for admin to reboot. Unattended upadate reuses UPDATE URL from check-update and is configurable under system/software yang. Cron consumer downloads release tar and extracts .pkg(used by rauc) under /var/lib/misc/unattended-update. Resolves: #1301 Signed-off-by: Ejub Sabic --- board/common/post-build.sh | 3 + .../rootfs/usr/libexec/infix/update-common | 74 ++++++++++++++++ board/common/rootfs/usr/sbin/check-update | 41 ++------- .../common/rootfs/usr/sbin/unattended-update | 85 +++++++++++++++++++ board/common/xattrs | 5 ++ doc/ChangeLog.md | 8 ++ src/confd/src/core.c | 40 +++++++++ src/confd/src/core.h | 1 + src/confd/src/schedule.c | 7 ++ src/confd/src/system-software.c | 9 ++ src/confd/yang/confd.inc | 2 +- .../yang/confd/infix-system-software.yang | 74 ++++++++++++++-- ... => infix-system-software@2026-07-02.yang} | 0 src/confd/yang/confd/infix-system.yang | 5 ++ ...6-17.yang => infix-system@2026-07-02.yang} | 0 15 files changed, 311 insertions(+), 43 deletions(-) create mode 100644 board/common/rootfs/usr/libexec/infix/update-common create mode 100755 board/common/rootfs/usr/sbin/unattended-update rename src/confd/yang/confd/{infix-system-software@2026-06-17.yang => infix-system-software@2026-07-02.yang} (100%) rename src/confd/yang/confd/{infix-system@2026-06-17.yang => infix-system@2026-07-02.yang} (100%) diff --git a/board/common/post-build.sh b/board/common/post-build.sh index 305f77ad9..35dbc2276 100755 --- a/board/common/post-build.sh +++ b/board/common/post-build.sh @@ -102,6 +102,9 @@ fi # Drop Buildroot default pam_lastlog.so from login chain sed -i '/^[^#]*pam_lastlog.so/s/^/# /' "$TARGET_DIR/etc/pam.d/login" +# Scratch dir for unattended-update's scheduled job (run as 'admin') to stage large downloads +mkdir -p "$TARGET_DIR/var/lib/misc/unattended-update" + # Allow bash to be login shells, it is added automatically when selected # in menuyconfig, but not when BusyBox provides a symlink (for ash). # The /bin/{true,false} are old UNIX beart means of disabling a user. diff --git a/board/common/rootfs/usr/libexec/infix/update-common b/board/common/rootfs/usr/libexec/infix/update-common new file mode 100644 index 000000000..74d209081 --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/update-common @@ -0,0 +1,74 @@ +# Shared helpers for check-update and unattended-update. Sourced, not run; +# the caller sets TAG first. + +# Read the shared update-url from running-config, fall back to upstream. +update_read_url() { + url=$(copy running-config \ + -x '/ietf-system:system/infix-system:software/update-url' \ + 2>/dev/null \ + | jq -r '.. | objects | ."update-url"? // empty') + [ -n "$url" ] && printf '%s' "$url" || printf 'https://github.com/kernelkit/infix' +} + +# Is $1 strictly newer than $2? +newer() { + [ "$1" = "$2" ] && return 1 + [ "$(printf '%s\n%s' "$1" "$2" | sort -V | tail -1)" = "$1" ] +} + +# Gather running and latest version info. +# Returns: 0 ok, 1 fatal (no os-release), 2 release API unreachable. +update_probe() { + if [ ! -f /etc/os-release ]; then + logger -t "$TAG" "ERROR: /etc/os-release not found" + return 1 + fi + . /etc/os-release + + # Dev/dirty builds have no comparable semver -- always treat as upgradable. + IS_RELEASE=true + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then + IS_RELEASE=false + fi + + UPDATE_URL=$(update_read_url) + + # Derive the GitHub release API URL from the configured update URL. + # https://github.com/org/repo -> https://api.github.com/repos/org/repo + REPO=$(echo "$UPDATE_URL" | sed 's|https://github.com/||; s|/*$||') + API_URL="https://api.github.com/repos/${REPO}/releases/latest" + + RELEASE_JSON=$(curl -sSL --max-time 10 "$API_URL" 2>/dev/null) + LATEST_TAG=$(printf '%s' "$RELEASE_JSON" | jq -r '.tag_name // empty') + if [ -z "$LATEST_TAG" ]; then + return 2 + fi + LATEST=${LATEST_TAG#v} + return 0 +} + +# Should the latest release be applied over the running version? +# Requires update_probe() to have run. Returns 0 if an update is available. +update_available() { + [ "$IS_RELEASE" = false ] && return 0 + newer "$LATEST" "$VERSION" +} + +# Print the download URL of this platform's release tarball, or nothing if it +# is missing. Releases ship no standalone bundle; the RAUC .pkg is packed in +# a per-platform tarball named "-.tar.gz", e.g. +# "infix-x86_64-26.06.0.tar.gz". +update_tarball_url() { + name="${IMAGE_ID}-${LATEST}.tar.gz" + printf '%s' "$RELEASE_JSON" \ + | jq -r --arg n "$name" \ + '.assets[]? | select(.name == $n) | .browser_download_url' \ + | head -1 +} + +# Print the path of the RAUC bundle inside that tarball. The archive unpacks +# to a "-/" directory holding "-v.pkg" +# -- note the 'v' on the bundle name but not on the directory. +update_tarball_member() { + printf '%s-%s/%s-v%s.pkg' "$IMAGE_ID" "$LATEST" "$IMAGE_ID" "$LATEST" +} diff --git a/board/common/rootfs/usr/sbin/check-update b/board/common/rootfs/usr/sbin/check-update index f380bf392..ba25e1211 100755 --- a/board/common/rootfs/usr/sbin/check-update +++ b/board/common/rootfs/usr/sbin/check-update @@ -5,46 +5,19 @@ NOTIFY_FILE=/run/os-update TAG=os-update -# Source os-release for VERSION and IMAGE_ID -if [ ! -f /etc/os-release ]; then - logger -t "$TAG" "ERROR: /etc/os-release not found" - exit 1 -fi -. /etc/os-release +. /usr/libexec/infix/update-common -# Dev/dirty builds have no comparable semver — always show the latest release -IS_RELEASE=true -if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then - IS_RELEASE=false +update_probe +rc=$? +if [ $rc -eq 1 ]; then + exit 1 fi - -# Read configured update-url from running config, fall back to upstream -UPDATE_URL=$(copy running-config \ - -x '/ietf-system:system/infix-system:software/check-update/update-url' \ - 2>/dev/null \ - | jq -r '.. | objects | ."update-url"? // empty') -UPDATE_URL=${UPDATE_URL:-"https://github.com/kernelkit/infix"} - -# Derive API URL from the configured update URL. -# Default (github.com): https://github.com/org/repo → https://api.github.com/repos/org/repo -REPO=$(echo "$UPDATE_URL" | sed 's|https://github.com/||; s|/*$||') -API_URL="https://api.github.com/repos/${REPO}/releases/latest" - -LATEST_TAG=$(curl -sSL --max-time 10 "$API_URL" 2>/dev/null \ - | jq -r '.tag_name // empty') -if [ -z "$LATEST_TAG" ]; then +if [ $rc -eq 2 ]; then logger -p daemon.info -t "$TAG" "Update check skipped: could not reach ${API_URL}" exit 0 fi -LATEST=${LATEST_TAG#v} - -# Compare: is $1 strictly newer than $2? -newer() { - [ "$1" = "$2" ] && return 1 - [ "$(printf '%s\n%s' "$1" "$2" | sort -V | tail -1)" = "$1" ] -} -if [ "$IS_RELEASE" = false ] || newer "$LATEST" "$VERSION"; then +if update_available; then RELEASE_URL="${UPDATE_URL}/releases/${LATEST_TAG}" MSG="Software update available: ${LATEST_TAG}, running ${VERSION} (see ${RELEASE_URL})" logger -t "$TAG" "$MSG" diff --git a/board/common/rootfs/usr/sbin/unattended-update b/board/common/rootfs/usr/sbin/unattended-update new file mode 100755 index 000000000..42701604b --- /dev/null +++ b/board/common/rootfs/usr/sbin/unattended-update @@ -0,0 +1,85 @@ +#!/bin/sh +# Download and install a newer release, unattended. Called by the scheduler. +# +# Installs to the inactive slot like a manual 'upgrade': RAUC flips the +# boot-order to activate on next reboot, leaving the old slot as fallback. +# The 'reboot' config policy decides whether that reboot is automatic. + +TAG=unattended-update +# Scratch dir is used for the lock and the download bundle +SCRATCH=/var/lib/misc/unattended-update +LOCKFILE=$SCRATCH/lock + +. /usr/libexec/infix/update-common + +# Read the reboot policy (manual|immediate) from running-config; default manual. +read_reboot_policy() { + policy=$(copy running-config \ + -x '/ietf-system:system/infix-system:software/unattended-update/reboot' \ + 2>/dev/null \ + | jq -r '.. | objects | .reboot? // empty') + [ -n "$policy" ] && printf '%s' "$policy" || printf 'manual' +} + +# Single-instance guard -- also avoids racing a manual 'upgrade' or an +# overlapping tick if a previous run is still installing. +exec 9>"$LOCKFILE" +if ! flock -n 9; then + logger -t "$TAG" "An update is already in progress, skipping" + exit 0 +fi + +update_probe +rc=$? +if [ $rc -eq 1 ]; then + exit 1 +fi +if [ $rc -eq 2 ]; then + logger -p daemon.info -t "$TAG" "Skipped: could not reach ${API_URL}" + exit 0 +fi + +if ! update_available; then + logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST)" + exit 0 +fi + +TARBALL_URL=$(update_tarball_url) +if [ -z "$TARBALL_URL" ]; then + logger -t "$TAG" "Update ${LATEST_TAG} found, but no tarball '${IMAGE_ID}-${LATEST}.tar.gz' in release; skipping" + exit 1 +fi +MEMBER=$(update_tarball_member) + +# Stage the bundle next to the lock. Clean up on any exit. +PKG=$SCRATCH/bundle.pkg +trap 'rm -f "$PKG"' EXIT + +# The release ships the .pkg inside a tarball, so stream it through tar and +# write out only the bundle member -- we never store the full archive. +logger -t "$TAG" "Downloading ${LATEST_TAG} bundle from ${TARBALL_URL}" +set -o pipefail +if ! curl -sSfL --max-time 1800 "$TARBALL_URL" | tar -xzOf - "$MEMBER" > "$PKG"; then + logger -t "$TAG" "ERROR: failed to download or extract '${MEMBER}' from ${TARBALL_URL}" + exit 1 +fi +if [ ! -s "$PKG" ]; then + logger -t "$TAG" "ERROR: extracted bundle is empty; '${MEMBER}' not found in tarball?" + exit 1 +fi + +logger -t "$TAG" "Installing ${LATEST_TAG} (running ${VERSION})" +if ! rauc install "$PKG"; then + logger -t "$TAG" "ERROR: installation of ${LATEST_TAG} failed" + exit 1 +fi +rm -f "$PKG" + +POLICY=$(read_reboot_policy) +if [ "$POLICY" = immediate ]; then + logger -t "$TAG" "Installed ${LATEST_TAG}; reboot policy 'immediate', rebooting to activate" + sleep 2 + /usr/sbin/reboot +else + logger -t "$TAG" "Installed ${LATEST_TAG}; reboot to activate the new image" +fi diff --git a/board/common/xattrs b/board/common/xattrs index 89db8732e..ec36d37bc 100644 --- a/board/common/xattrs +++ b/board/common/xattrs @@ -3,3 +3,8 @@ /sbin/factory f 4750 root wheel - - - - - /var/lib/avahi-autoipd d 0755 avahi avahi - - - - - + +# Scratch area for scheduled jobs (run as 'admin', a wheel member) to stage +# downloads, e.g. unattended-update's install bundle. setgid so staged files +# inherit the wheel group; no access for other. +/var/lib/misc/unattended-update d 2770 root wheel - - - - - diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 1611a361c..36adf55dd 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -2,6 +2,14 @@ Change Log ========== All notable changes to the project are documented in this file. +[v26.08.0][UNRELEASED] - +------------------------- +### Changes +- Add support for unattended software upgrades, letting a unit fetch a newer release on a + schedule and install it to the inactive partition on its own, then either + reboot to activate it or leave it staged for the next reboot + +### Fixes [v26.06.0][] - 2026-07-01 ------------------------- diff --git a/src/confd/src/core.c b/src/confd/src/core.c index a0569b688..f3585659b 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -505,6 +505,42 @@ static confd_dependency_t dep_radio_components(struct lyd_node **diff, struct ly return result; } +static confd_dependency_t dep_schedule_consumers(struct lyd_node **diff, struct lyd_node *config) +{ + confd_dependency_t result = CONFD_DEP_DONE; + const struct cron_consumer **consumers; + size_t i, count; + + consumers = schedule_consumers(&count); + for (i = 0; i < count; i++) { + const struct cron_consumer *c = consumers[i]; + struct lyd_node *dnode, *cnode; + const char *name; + char xpath[256]; + + dnode = lydx_get_xpathf(*diff, "%s", c->path); + if (!dnode) + continue; + + cnode = lydx_get_xpathf(config, "%s", c->path); + name = cnode ? lydx_get_cattr(cnode, c->sched_leaf) : NULL; + if (!name) + name = lydx_get_cattr(dnode, c->sched_leaf); + if (!name) + continue; + + snprintf(xpath, sizeof(xpath), + "/ietf-system:system/infix-schedule:schedules/schedule[name='%s']", name); + result = add_dependencies(diff, xpath, name); + if (result == CONFD_DEP_ERROR) { + ERROR("Failed to add schedule '%s' to diff for consumer %s", name, c->path); + return result; + } + } + + return result; +} + static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd_node *config) { confd_dependency_t result; @@ -529,6 +565,10 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd if (result == CONFD_DEP_ERROR) return result; + result = dep_schedule_consumers(diff, config); + if (result == CONFD_DEP_ERROR) + return result; + return result; } diff --git a/src/confd/src/core.h b/src/confd/src/core.h index 9d0273e83..dac062997 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -224,6 +224,7 @@ struct cron_consumer { const char *command; /* what crond runs on each occurrence */ }; int schedule_consumer_register(const struct cron_consumer *consumer); +const struct cron_consumer **schedule_consumers(size_t *count); int schedule_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* containers.c */ diff --git a/src/confd/src/schedule.c b/src/confd/src/schedule.c index 8efd8ecb0..323ddf312 100644 --- a/src/confd/src/schedule.c +++ b/src/confd/src/schedule.c @@ -36,6 +36,13 @@ int schedule_consumer_register(const struct cron_consumer *consumer) return 0; } +/* Read-only view of the registered consumers, for the dependency tracker. */ +const struct cron_consumer **schedule_consumers(size_t *count) +{ + *count = consumer_count; + return consumers; +} + /* * Convert ietf-schedule recurrence to a 5-field cron expression. * diff --git a/src/confd/src/system-software.c b/src/confd/src/system-software.c index 50a265491..2c813b821 100644 --- a/src/confd/src/system-software.c +++ b/src/confd/src/system-software.c @@ -97,6 +97,14 @@ static const struct cron_consumer check_update_consumer = { .command = "/usr/sbin/check-update", }; +/* Scheduler consumer for unattended-update. */ +static const struct cron_consumer unattended_update_consumer = { + .path = "/ietf-system:system/infix-system:software/unattended-update", + .sched_leaf = "schedule", + .enabled_leaf = "enabled", + .command = "/usr/sbin/unattended-update", +}; + int system_sw_rpc_init(struct confd *confd) { int rc = 0; @@ -107,6 +115,7 @@ int system_sw_rpc_init(struct confd *confd) infix_system_sw_set_boot_order, NULL, &confd->sub); schedule_consumer_register(&check_update_consumer); + schedule_consumer_register(&unattended_update_consumer); fail: return rc; diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index e740c6cd4..d89bd8e59 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -43,7 +43,7 @@ MODULES=( "infix-firewall-icmp-types@2025-04-26.yang" "infix-meta@2025-12-10.yang" "infix-services@2026-06-17.yang" - "infix-system@2026-06-17.yang" + "infix-system@2026-07-02.yang" "ieee802-ethernet-interface@2025-09-10.yang" "ieee802-ethernet-phy-type@2025-09-10.yang" "infix-ethernet-interface@2026-05-21.yang" diff --git a/src/confd/yang/confd/infix-system-software.yang b/src/confd/yang/confd/infix-system-software.yang index 29afb29f7..101ab856e 100644 --- a/src/confd/yang/confd/infix-system-software.yang +++ b/src/confd/yang/confd/infix-system-software.yang @@ -24,6 +24,13 @@ submodule infix-system-software { contact "kernelkit@googlegroups.com"; description "Software status and upgrade."; + revision 2026-07-02 { + description "Add unattended-update config, triggered from a referenced + schedule, and lift update-url to the shared software + container so check-update and unattended-update use one + update source."; + reference "Internal"; + } revision 2026-06-17 { description "Add check-update config, triggered from a referenced schedule"; reference "Internal"; @@ -93,13 +100,24 @@ submodule infix-system-software { description "Software management configuration."; + leaf update-url { + type string; + default "https://github.com/kernelkit/infix"; + description + "Base URL of the update source, shared by check-update and + unattended-update. The latest release tag is resolved from + /releases/latest, and the per-platform bundle asset + is fetched from that release. Override for customer-specific + channels."; + } + container check-update { description "Policy for automatic software update checks. When 'enabled' and 'schedule' references a schedule, the system - checks the configured URL for a newer release on each occurrence - and logs a notification if one is found."; + checks the configured update-url for a newer release on each + occurrence and logs a notification if one is found."; leaf enabled { type boolean; @@ -114,14 +132,54 @@ submodule infix-system-software { "The schedule whose occurrences trigger an update check. Without a referenced schedule no checks are performed."; } + } - leaf update-url { - type string; - default "https://github.com/kernelkit/infix"; + container unattended-update { + description + "Policy for automatic, unattended software upgrades. + + When 'enabled' and 'schedule' references a schedule, the system + checks the configured update-url for a newer release on each + occurrence and, if one is found, downloads and installs the + per-platform bundle to the inactive slot exactly as a manual + 'upgrade' would: the boot-order is flipped to activate the new + image on the next reboot, and the previously running slot is + left intact as a fallback. + + The 'reboot' leaf governs whether that reboot happens + automatically or is left to the operator."; + + leaf enabled { + type boolean; + default false; + description + "Enable automatic unattended upgrades."; + } + + leaf schedule { + type infix-schedule:schedule-ref; + description + "The schedule whose occurrences trigger an unattended upgrade. + Without a referenced schedule no upgrades are performed."; + } + + leaf reboot { + type enumeration { + enum manual { + description + "Install and flip the boot-order, but do not reboot. The + new image activates the next time the operator reboots."; + } + enum immediate { + description + "Reboot automatically after a successful install to activate + the new image at once."; + } + } + default manual; description - "Base URL of the update source. The check script appends - /releases/latest and follows the redirect to determine the - latest release tag. Override for customer-specific channels."; + "What to do once a bundle has been installed to the inactive + slot."; } } } diff --git a/src/confd/yang/confd/infix-system-software@2026-06-17.yang b/src/confd/yang/confd/infix-system-software@2026-07-02.yang similarity index 100% rename from src/confd/yang/confd/infix-system-software@2026-06-17.yang rename to src/confd/yang/confd/infix-system-software@2026-07-02.yang diff --git a/src/confd/yang/confd/infix-system.yang b/src/confd/yang/confd/infix-system.yang index b79a2fcad..64663074d 100644 --- a/src/confd/yang/confd/infix-system.yang +++ b/src/confd/yang/confd/infix-system.yang @@ -32,6 +32,11 @@ module infix-system { contact "kernelkit@googlegroups.com"; description "Infix augments and deviations to ietf-system."; + revision 2026-07-02 { + description "Add unattended-update and shared software/update-url (see the + infix-system-software submodule)."; + reference "internal"; + } revision 2026-06-17 { description "Add scheduled-reboot, triggered from a referenced schedule."; reference "internal"; diff --git a/src/confd/yang/confd/infix-system@2026-06-17.yang b/src/confd/yang/confd/infix-system@2026-07-02.yang similarity index 100% rename from src/confd/yang/confd/infix-system@2026-06-17.yang rename to src/confd/yang/confd/infix-system@2026-07-02.yang From 24cdbbcffeb79f72c5ce6242749bbe09210e00eb Mon Sep 17 00:00:00 2001 From: Ejub Sabic Date: Wed, 15 Jul 2026 09:15:27 +0200 Subject: [PATCH 2/4] fix: #1562 comment fixes Signed-off-by: Ejub Sabic --- board/common/rootfs/usr/sbin/unattended-update | 10 ++++++++-- src/confd/configure.ac | 3 ++- .../migrate/1.10/10-software-update-url.sh | 18 ++++++++++++++++++ src/confd/share/migrate/1.10/Makefile.am | 2 ++ src/confd/share/migrate/Makefile.am | 2 +- src/confd/src/core.c | 3 +++ src/confd/yang/confd.inc | 2 +- src/confd/yang/confd/infix-schedule.yang | 16 ++++++++++++++-- ...-17.yang => infix-schedule@2026-07-15.yang} | 0 .../yang/confd/infix-system-software.yang | 13 ++++++++----- 10 files changed, 57 insertions(+), 12 deletions(-) create mode 100755 src/confd/share/migrate/1.10/10-software-update-url.sh create mode 100644 src/confd/share/migrate/1.10/Makefile.am rename src/confd/yang/confd/{infix-schedule@2026-06-17.yang => infix-schedule@2026-07-15.yang} (100%) diff --git a/board/common/rootfs/usr/sbin/unattended-update b/board/common/rootfs/usr/sbin/unattended-update index 42701604b..d35ccdad7 100755 --- a/board/common/rootfs/usr/sbin/unattended-update +++ b/board/common/rootfs/usr/sbin/unattended-update @@ -22,10 +22,16 @@ read_reboot_policy() { } # Single-instance guard -- also avoids racing a manual 'upgrade' or an -# overlapping tick if a previous run is still installing. +# overlapping tick if a previous run is still installing. Open the lock +# explicitly first: a missing or unwritable scratch dir must fail loudly here, +# not slip through to flock and get misreported as "already in progress". +if ! { true >> "$LOCKFILE"; } 2>/dev/null; then + logger -t "$TAG" "ERROR: cannot open lock $LOCKFILE; is $SCRATCH present and writable?" + exit 1 +fi exec 9>"$LOCKFILE" if ! flock -n 9; then - logger -t "$TAG" "An update is already in progress, skipping" + logger -t "$TAG" "Another update is already in progress, skipping" exit 0 fi diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 2306d5864..8999a27a9 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.61) # confd version is same as system YANG model version, step on breaking changes -AC_INIT([confd], [1.9], [https://github.com/kernelkit/infix/issues]) +AC_INIT([confd], [1.10], [https://github.com/kernelkit/infix/issues]) AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) AM_SILENT_RULES(yes) @@ -23,6 +23,7 @@ AC_CONFIG_FILES([ share/migrate/1.7/Makefile share/migrate/1.8/Makefile share/migrate/1.9/Makefile + share/migrate/1.10/Makefile yang/Makefile yang/confd/Makefile yang/test-mode/Makefile diff --git a/src/confd/share/migrate/1.10/10-software-update-url.sh b/src/confd/share/migrate/1.10/10-software-update-url.sh new file mode 100755 index 000000000..07f87f370 --- /dev/null +++ b/src/confd/share/migrate/1.10/10-software-update-url.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Move software/check-update/update-url to the shared software/update-url. +# +# The update-source URL was lifted out of the check-update container so that +# check-update and unattended-update share a single setting. Relocate any +# configured value to the new location and drop the old leaf; configs that +# never set it are left untouched. + +file=$1 +temp=${file}.tmp + +jq ' + ["ietf-system:system", "infix-system:software", "check-update", "update-url"] as $old + | ["ietf-system:system", "infix-system:software", "update-url"] as $new + | if getpath($old) != null + then setpath($new; getpath($old)) | delpaths([$old]) + else . end +' "$file" > "$temp" && mv "$temp" "$file" diff --git a/src/confd/share/migrate/1.10/Makefile.am b/src/confd/share/migrate/1.10/Makefile.am new file mode 100644 index 000000000..07782eedc --- /dev/null +++ b/src/confd/share/migrate/1.10/Makefile.am @@ -0,0 +1,2 @@ +migratedir = $(pkgdatadir)/migrate/1.10 +dist_migrate_DATA = 10-software-update-url.sh diff --git a/src/confd/share/migrate/Makefile.am b/src/confd/share/migrate/Makefile.am index 2abea24e0..755ac16a4 100644 --- a/src/confd/share/migrate/Makefile.am +++ b/src/confd/share/migrate/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 +SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 migratedir = $(pkgdatadir)/migrate diff --git a/src/confd/src/core.c b/src/confd/src/core.c index f3585659b..f25c698e2 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -529,6 +529,9 @@ static confd_dependency_t dep_schedule_consumers(struct lyd_node **diff, struct if (!name) continue; + /* Safe to interpolate: infix-schedule constrains the schedule + * name to a bounded identifier (no quotes, length 1..64), so it + * neither breaks the XPath literal nor overflows xpath[]. */ snprintf(xpath, sizeof(xpath), "/ietf-system:system/infix-schedule:schedules/schedule[name='%s']", name); result = add_dependencies(diff, xpath, name); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index d89bd8e59..f84d62e0a 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -58,5 +58,5 @@ MODULES=( "ieee802-dot1as-gptp@2025-12-10.yang" "infix-ptp@2026-04-07.yang" "ietf-schedule@2026-03-10.yang -e icalendar-recurrence" - "infix-schedule@2026-06-17.yang" + "infix-schedule@2026-07-15.yang" ) diff --git a/src/confd/yang/confd/infix-schedule.yang b/src/confd/yang/confd/infix-schedule.yang index 0eecfbd13..1478f34fb 100644 --- a/src/confd/yang/confd/infix-schedule.yang +++ b/src/confd/yang/confd/infix-schedule.yang @@ -14,6 +14,12 @@ module infix-schedule { contact "kernelkit@googlegroups.com"; description "Infix deviations and augments to ietf-schedule"; + revision 2026-07-15 { + description + "Constrain schedule name to a bounded identifier so features can + reference it verbatim (e.g. in a resolved XPath)."; + reference "internal"; + } revision 2026-06-17 { description "Initial revision - system scheduling. @@ -131,9 +137,15 @@ module infix-schedule { action of their own; features trigger off a schedule by pointing a schedule-ref leaf at its name."; leaf name { - type string; + type string { + length "1..64"; + pattern '[a-zA-Z0-9][a-zA-Z0-9_.-]*'; + } description - "Unique name identifying this schedule."; + "Unique name identifying this schedule. Restricted to a bounded + identifier (letters, digits, '_', '.', '-') so features can use + it verbatim, e.g. in the XPath the scheduler builds to resolve a + schedule-ref."; } leaf enabled { type boolean; diff --git a/src/confd/yang/confd/infix-schedule@2026-06-17.yang b/src/confd/yang/confd/infix-schedule@2026-07-15.yang similarity index 100% rename from src/confd/yang/confd/infix-schedule@2026-06-17.yang rename to src/confd/yang/confd/infix-schedule@2026-07-15.yang diff --git a/src/confd/yang/confd/infix-system-software.yang b/src/confd/yang/confd/infix-system-software.yang index 101ab856e..5c7826385 100644 --- a/src/confd/yang/confd/infix-system-software.yang +++ b/src/confd/yang/confd/infix-system-software.yang @@ -104,11 +104,14 @@ submodule infix-system-software { type string; default "https://github.com/kernelkit/infix"; description - "Base URL of the update source, shared by check-update and - unattended-update. The latest release tag is resolved from - /releases/latest, and the per-platform bundle asset - is fetched from that release. Override for customer-specific - channels."; + "GitHub repository whose releases provide software updates, shared + by check-update and unattended-update. Must be of the form + 'https://github.com//': the latest release is queried + via the GitHub REST API (derived as + api.github.com/repos///releases/latest) and the + per-platform bundle is fetched from that release's assets. Only + GitHub-hosted repositories are supported; override for a fork or a + customer-specific channel."; } container check-update { From 7040d2ecc046e5932e718f79cd92dfdb9db867cc Mon Sep 17 00:00:00 2001 From: Ejub Sabic Date: Wed, 15 Jul 2026 10:32:32 +0200 Subject: [PATCH 3/4] fix: #1562 comment fixes 2 Signed-off-by: Ejub Sabic --- board/common/rootfs/usr/libexec/infix/update-common | 4 ++-- board/common/rootfs/usr/sbin/check-update | 2 +- board/common/rootfs/usr/sbin/unattended-update | 3 +-- doc/ChangeLog.md | 4 ++-- src/confd/src/schedule.c | 3 ++- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/board/common/rootfs/usr/libexec/infix/update-common b/board/common/rootfs/usr/libexec/infix/update-common index 74d209081..9e30aa227 100644 --- a/board/common/rootfs/usr/libexec/infix/update-common +++ b/board/common/rootfs/usr/libexec/infix/update-common @@ -17,7 +17,7 @@ newer() { } # Gather running and latest version info. -# Returns: 0 ok, 1 fatal (no os-release), 2 release API unreachable. +# Returns: 0 ok, 1 fatal (no os-release), 2 failed to query latest release tag. update_probe() { if [ ! -f /etc/os-release ]; then logger -t "$TAG" "ERROR: /etc/os-release not found" @@ -38,7 +38,7 @@ update_probe() { REPO=$(echo "$UPDATE_URL" | sed 's|https://github.com/||; s|/*$||') API_URL="https://api.github.com/repos/${REPO}/releases/latest" - RELEASE_JSON=$(curl -sSL --max-time 10 "$API_URL" 2>/dev/null) + RELEASE_JSON=$(curl -sSfL --max-time 10 "$API_URL" 2>/dev/null) || return 2 LATEST_TAG=$(printf '%s' "$RELEASE_JSON" | jq -r '.tag_name // empty') if [ -z "$LATEST_TAG" ]; then return 2 diff --git a/board/common/rootfs/usr/sbin/check-update b/board/common/rootfs/usr/sbin/check-update index ba25e1211..11485aa34 100755 --- a/board/common/rootfs/usr/sbin/check-update +++ b/board/common/rootfs/usr/sbin/check-update @@ -13,7 +13,7 @@ if [ $rc -eq 1 ]; then exit 1 fi if [ $rc -eq 2 ]; then - logger -p daemon.info -t "$TAG" "Update check skipped: could not reach ${API_URL}" + logger -p daemon.info -t "$TAG" "Update check skipped: failed to query latest release from ${API_URL}" exit 0 fi diff --git a/board/common/rootfs/usr/sbin/unattended-update b/board/common/rootfs/usr/sbin/unattended-update index d35ccdad7..cdb473bb4 100755 --- a/board/common/rootfs/usr/sbin/unattended-update +++ b/board/common/rootfs/usr/sbin/unattended-update @@ -41,7 +41,7 @@ if [ $rc -eq 1 ]; then exit 1 fi if [ $rc -eq 2 ]; then - logger -p daemon.info -t "$TAG" "Skipped: could not reach ${API_URL}" + logger -p daemon.info -t "$TAG" "Skipped: failed to query latest release from ${API_URL}" exit 0 fi @@ -64,7 +64,6 @@ trap 'rm -f "$PKG"' EXIT # The release ships the .pkg inside a tarball, so stream it through tar and # write out only the bundle member -- we never store the full archive. logger -t "$TAG" "Downloading ${LATEST_TAG} bundle from ${TARBALL_URL}" -set -o pipefail if ! curl -sSfL --max-time 1800 "$TARBALL_URL" | tar -xzOf - "$MEMBER" > "$PKG"; then logger -t "$TAG" "ERROR: failed to download or extract '${MEMBER}' from ${TARBALL_URL}" exit 1 diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 36adf55dd..42793db02 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -2,12 +2,12 @@ Change Log ========== All notable changes to the project are documented in this file. -[v26.08.0][UNRELEASED] - +[v26.08.0][UNRELEASED] ------------------------- ### Changes - Add support for unattended software upgrades, letting a unit fetch a newer release on a schedule and install it to the inactive partition on its own, then either - reboot to activate it or leave it staged for the next reboot + reboot to activate it or leave it staged for the next reboot. ### Fixes diff --git a/src/confd/src/schedule.c b/src/confd/src/schedule.c index 323ddf312..f02605f51 100644 --- a/src/confd/src/schedule.c +++ b/src/confd/src/schedule.c @@ -39,7 +39,8 @@ int schedule_consumer_register(const struct cron_consumer *consumer) /* Read-only view of the registered consumers, for the dependency tracker. */ const struct cron_consumer **schedule_consumers(size_t *count) { - *count = consumer_count; + if (count) + *count = consumer_count; return consumers; } From 14112960518170d26be0d4c9cb0e7ef06dd818a1 Mon Sep 17 00:00:00 2001 From: Ejub Sabic Date: Wed, 15 Jul 2026 15:28:45 +0200 Subject: [PATCH 4/4] fix: #1562 comment fixes 3 Signed-off-by: Ejub Sabic --- doc/ChangeLog.md | 2 +- src/confd/src/core.c | 2 +- src/confd/src/core.h | 2 +- src/confd/src/schedule.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 42793db02..70992dfab 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -7,7 +7,7 @@ All notable changes to the project are documented in this file. ### Changes - Add support for unattended software upgrades, letting a unit fetch a newer release on a schedule and install it to the inactive partition on its own, then either - reboot to activate it or leave it staged for the next reboot. + reboot to activate it or leave it staged for the next reboot ### Fixes diff --git a/src/confd/src/core.c b/src/confd/src/core.c index f25c698e2..fe0dd105b 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -508,7 +508,7 @@ static confd_dependency_t dep_radio_components(struct lyd_node **diff, struct ly static confd_dependency_t dep_schedule_consumers(struct lyd_node **diff, struct lyd_node *config) { confd_dependency_t result = CONFD_DEP_DONE; - const struct cron_consumer **consumers; + const struct cron_consumer *const *consumers; size_t i, count; consumers = schedule_consumers(&count); diff --git a/src/confd/src/core.h b/src/confd/src/core.h index dac062997..9d40061fb 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -224,7 +224,7 @@ struct cron_consumer { const char *command; /* what crond runs on each occurrence */ }; int schedule_consumer_register(const struct cron_consumer *consumer); -const struct cron_consumer **schedule_consumers(size_t *count); +const struct cron_consumer *const *schedule_consumers(size_t *count); int schedule_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* containers.c */ diff --git a/src/confd/src/schedule.c b/src/confd/src/schedule.c index f02605f51..92dc65dc7 100644 --- a/src/confd/src/schedule.c +++ b/src/confd/src/schedule.c @@ -37,7 +37,7 @@ int schedule_consumer_register(const struct cron_consumer *consumer) } /* Read-only view of the registered consumers, for the dependency tracker. */ -const struct cron_consumer **schedule_consumers(size_t *count) +const struct cron_consumer *const *schedule_consumers(size_t *count) { if (count) *count = consumer_count;