Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@
"bridge": "br0"
}
}

]
},
"ietf-keystore:keystore": {
Expand Down Expand Up @@ -376,6 +375,9 @@
},
"ietf-system:system": {
"hostname": "acer-connect-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"server": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@
},
"ietf-system:system": {
"hostname": "bpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"server": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@
},
"ietf-system:system": {
"hostname": "bpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"server": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@
},
"ietf-system:system": {
"hostname": "bpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"server": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@
},
"ietf-system:system": {
"hostname": "bpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"server": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@
},
"ietf-system:system": {
"hostname": "bpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"server": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@
},
"ietf-system:system": {
"hostname": "r2s-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"server": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@
},
"ietf-system:system": {
"hostname": "rpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"enabled": true,
"server": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@
},
"ietf-system:system": {
"hostname": "rpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"enabled": true,
"server": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@
},
"ietf-system:system": {
"hostname": "rpi-%m",
"infix-system:software": {
"update-url": "https://github.com/kernelkit/infix/releases.atom"
},
"ntp": {
"enabled": true,
"server": [
Expand Down
1 change: 1 addition & 0 deletions board/common/rootfs/etc/tmpfiles.d/os-schedule.conf
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
f /run/os-update 0666 admin admin
f /run/unattended-update.lock 0666 admin admin
109 changes: 109 additions & 0 deletions board/common/rootfs/usr/libexec/infix/update-common
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Shared helpers for check-update and unattended-update. Sourced, not run;
# the caller sets TAG first.

# Read the shared update-url (an RSS/Atom release feed) from running-config.
# The leaf is mandatory and factory-config supplies it, so an empty result
# means the software container is absent -- no update source is configured.
update_read_url() {
copy running-config \
-x '/ietf-system:system/infix-system:software/update-url' \
2>/dev/null \
| jq -r '.. | objects | ."update-url"? // empty'
}

# Read whether pre-releases may be installed; default false.
update_read_prerelease() {
val=$(copy running-config \
-x '/ietf-system:system/infix-system:software/allow-prerelease' \
2>/dev/null \
| jq -r '.. | objects | ."allow-prerelease"? // empty')
[ "$val" = true ] && printf 'true' || printf 'false'
}

# 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,
# 3 no update-url configured.
update_probe() {
if [ ! -f /etc/os-release ]; then
logger -t "$TAG" "ERROR: /etc/os-release not found"
return 1
fi
. /etc/os-release

UPDATE_URL=$(update_read_url)
[ -z "$UPDATE_URL" ] && return 3
ALLOW_PRERELEASE=$(update_read_prerelease)

FEED=$(curl -sSfL --max-time 10 "$UPDATE_URL" 2>/dev/null) || return 2

# Every feed entry links to its release page, ".../releases/tag/<tag>",
# which is the only place the tag appears in machine-readable form.
hrefs=$(printf '%s' "$FEED" \
| xmllint --xpath "//*[local-name()='entry']/*[local-name()='link']/@href" - 2>/dev/null \
| tr ' ' '\n' | sed -n 's|^href="\(.*\)"$|\1|p')

# RSS 2.0 has no href attribute, the URL is the <link> element text.
[ -n "$hrefs" ] || hrefs=$(printf '%s' "$FEED" \
| xmllint --xpath "//*[local-name()='item']/*[local-name()='link']/text()" - 2>/dev/null \
| tr -d ' \t' | grep -v '^$')

[ -n "$hrefs" ] || return 2


if [ "$ALLOW_PRERELEASE" = true ]; then
LATEST_TAG=$(printf '%s\n' "$hrefs" | sed 's|.*/||' | head -1)
else
LATEST_TAG=$(printf '%s\n' "$hrefs" | sed 's|.*/||' \
| grep -vE -- '-(rc|alpha|beta)' | head -1)
fi
[ -n "$LATEST_TAG" ] || return 2

RELEASE_BASE=$(printf '%s\n' "$hrefs" | grep -E "/${LATEST_TAG}\$" | head -1 \
| sed "s|/releases/tag/${LATEST_TAG}\$||")

return 0
}

# Version that will be running after the next reboot: the bundle installed in
# the slot RAUC boots from. This is the running version until something has
# been staged into the inactive slot, after which it is the staged one -- so a
# release installed but not yet rebooted into is not offered again.
#
# 'rauc status' needs --detailed, without it the slot_status object holding the
# version is left out entirely. Note that .booted is a bootname ("primary"),
# only .boot_primary is a slot name ("rootfs.0") that keys into .slots.
update_pending_version() {
rauc status --detailed --output-format=json 2>/dev/null \
| jq -r '(.slots | add) as $s
| $s[.boot_primary].slot_status.bundle.version // empty'
}

# Should the latest release be applied? Requires update_probe() to have run.
# Returns 0 if an update is available.
update_available() {
pending=$(update_pending_version)
# A slot never written by RAUC has no recorded version; fall back to the
# running one rather than skipping the upgrade.
[ -n "$pending" ] || pending=$VERSION

newer "$LATEST_TAG" "$pending"
}

# Print the release page URL of the latest release, for operator-facing logs.
update_release_url() {
[ -n "$RELEASE_BASE" ] || return 0
printf '%s/releases/tag/%s' "$RELEASE_BASE" "$LATEST_TAG"
}

# Print the download URL of this platform's RAUC bundle.
update_bundle_url() {
[ -n "$RELEASE_BASE" ] || return 0
printf '%s/releases/download/%s/%s-%s.pkg' \
"$RELEASE_BASE" "$LATEST_TAG" "$IMAGE_ID" "$LATEST_TAG"
}
49 changes: 13 additions & 36 deletions board/common/rootfs/usr/sbin/check-update
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,28 @@
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"
. /usr/libexec/infix/update-common

update_probe
rc=$?
if [ $rc -eq 1 ]; then
exit 1
fi
. /etc/os-release

# 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
if [ $rc -eq 2 ]; then
logger -p daemon.info -t "$TAG" "Update check skipped: failed to query latest release from ${UPDATE_URL}"
exit 0
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 3 ]; then
logger -p daemon.info -t "$TAG" "Update check skipped: no update-url configured"
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
RELEASE_URL="${UPDATE_URL}/releases/${LATEST_TAG}"
if update_available; then
RELEASE_URL=$(update_release_url)
MSG="Software update available: ${LATEST_TAG}, running ${VERSION} (see ${RELEASE_URL})"
logger -t "$TAG" "$MSG"
printf '%s\n' "$MSG" > "$NOTIFY_FILE"
else
logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST)"
logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST_TAG)"
printf '' > "$NOTIFY_FILE"
fi
78 changes: 78 additions & 0 deletions board/common/rootfs/usr/sbin/unattended-update
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/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
# Pre-created by tmpfiles.d, scheduled jobs run as 'admin' and cannot create
# files in /run themselves.
LOCKFILE=/run/unattended-update.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: an unwritable lock 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"
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 ${UPDATE_URL}"
exit 0
fi
if [ $rc -eq 3 ]; then
logger -p daemon.info -t "$TAG" "Skipped: no update-url configured"
exit 0
fi

if ! update_available; then
logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST_TAG)"
exit 0
fi

BUNDLE_URL=$(update_bundle_url)
if [ -z "$BUNDLE_URL" ]; then
logger -t "$TAG" "Update ${LATEST_TAG} found, but no bundle URL could be resolved; skipping"
exit 1
fi

# RAUC streams the bundle from the URL, nothing is staged locally.
logger -t "$TAG" "Installing ${LATEST_TAG} from ${BUNDLE_URL} (running ${VERSION})"
if ! rauc install "$BUNDLE_URL"; then
logger -t "$TAG" "ERROR: installation of ${LATEST_TAG} failed"
exit 1
fi

POLICY=$(read_reboot_policy)
if [ "$POLICY" = immediate ]; then
logger -t "$TAG" "Installed ${LATEST_TAG}; reboot policy 'immediate', rebooting to activate"
sync
sleep 2
/usr/sbin/reboot
else
logger -t "$TAG" "Installed ${LATEST_TAG}; reboot to activate the new image"
fi
4 changes: 4 additions & 0 deletions doc/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ All notable changes to the project are documented in this file.
editor, show mesh peers on the WiFi and interface status pages, and add
an editor section for access point roaming (802.11k/r/v, band steering,
OKC).
- Add support for unattended software upgrades, letting a unit track an RSS/Atom
release feed on a schedule and install a newer release to the inactive
partition on its own, then either reboot to activate it or leave it staged for
the next reboot

### Fixes

Expand Down
Loading