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..9e30aa227 --- /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 failed to query latest release tag. +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 -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 + 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..11485aa34 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 - logger -p daemon.info -t "$TAG" "Update check skipped: could not reach ${API_URL}" +if [ $rc -eq 2 ]; then + logger -p daemon.info -t "$TAG" "Update check skipped: failed to query latest release from ${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..cdb473bb4 --- /dev/null +++ b/board/common/rootfs/usr/sbin/unattended-update @@ -0,0 +1,90 @@ +#!/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. 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" "Another 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: failed to query latest release from ${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}" +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..70992dfab 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/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 a0569b688..fe0dd105b 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -505,6 +505,45 @@ 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 *const *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; + + /* 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); + 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 +568,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..9d40061fb 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 *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 8efd8ecb0..92dc65dc7 100644 --- a/src/confd/src/schedule.c +++ b/src/confd/src/schedule.c @@ -36,6 +36,14 @@ 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 *const *schedule_consumers(size_t *count) +{ + if (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..f84d62e0a 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" @@ -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 29afb29f7..5c7826385 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,27 @@ submodule infix-system-software { description "Software management configuration."; + leaf update-url { + type string; + default "https://github.com/kernelkit/infix"; + description + "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 { 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 +135,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