-
Notifications
You must be signed in to change notification settings - Fork 20
Add support for unattended updates #1562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # 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. | ||
| 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/releases.atom' | ||
| } | ||
|
|
||
| # 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. | ||
| 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) | ||
| 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 | ||
| LATEST=${LATEST_TAG#v} | ||
|
|
||
| RELEASE_BASE=$(printf '%s\n' "$hrefs" | grep -E "/${LATEST_TAG}\$" | head -1 \ | ||
| | sed "s|/releases/tag/${LATEST_TAG}\$||") | ||
|
|
||
| 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 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" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| #!/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 ! update_available; then | ||
| logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST)" | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| # Scheduling | ||
|
|
||
| Some Infix features run on a recurring calendar instead of on demand. | ||
| The recurrence itself lives in one place: a *named schedule*, which any | ||
| number of features can point at. | ||
|
|
||
| A schedule has no action of its own. It only says when something should | ||
| happen, and the feature referencing it decides what happens. Two features | ||
| can share the same schedule. | ||
|
|
||
| YANG support is defined in [infix-schedule][1], which augments | ||
| `ietf-system` with a `schedules` container and builds on the iCalendar | ||
| recurrence grouping from [ietf-schedule][2] (RFC 9922). | ||
|
|
||
|
|
||
| ## Creating a Schedule | ||
|
|
||
| A schedule needs a name and a recurrence rule. The example below fires | ||
| every night at 03:30. | ||
|
|
||
| <pre class="cli"><code>admin@example:/> <b>configure</b> | ||
| admin@example:/config/> <b>edit system schedule nightly</b> | ||
| admin@example:/config/system/schedule/nightly/> <b>set description "Nightly maintenance window"</b> | ||
| admin@example:/config/system/schedule/nightly/> <b>set recurrence frequency daily</b> | ||
| admin@example:/config/system/schedule/nightly/> <b>set recurrence byhour 3</b> | ||
| admin@example:/config/system/schedule/nightly/> <b>set recurrence byminute 30</b> | ||
| admin@example:/config/system/schedule/nightly/> <b>leave</b> | ||
| </code></pre> | ||
|
|
||
| **Schedule parameters:** | ||
|
|
||
| - `name`: Unique identifier, 1-64 characters, starting with a letter or | ||
| digit and otherwise limited to letters, digits, `_`, `.` and `-`. The | ||
| name is used verbatim by features referencing it | ||
| - `enabled`: Turn the schedule on or off (default: `true`). When off, | ||
| everything that uses it stops running, but the schedule is kept | ||
| - `description`: Optional human-readable note on the schedule's purpose | ||
| - `recurrence`: The recurrence rule. A schedule without one is rejected | ||
| at commit time | ||
|
|
||
|
|
||
| ## Recurrence Rules | ||
|
|
||
| Schedules run in the system's local time. | ||
|
|
||
| `frequency` is mandatory and selects the base period: | ||
|
|
||
| | Frequency | Fires | | ||
| |------------|------------------------------------| | ||
| | `minutely` | Every minute | | ||
| | `hourly` | Every hour, on the hour | | ||
| | `daily` | Every day at midnight | | ||
| | `weekly` | Every week | | ||
| | `monthly` | The 1st of every month at midnight | | ||
| | `yearly` | January 1st at midnight | | ||
|
|
||
| `interval` (default `1`) stretches the base period: `frequency hourly` | ||
| with `interval 6` fires every six hours. | ||
|
|
||
| The remaining fields refine that period by pinning one field to specific | ||
| values: | ||
|
|
||
| - `byminute`: Minutes within the hour, 0-59 | ||
| - `byhour`: Hours of the day, 0-23 | ||
| - `byday`: Days of the week, by `weekday` name (`monday` … `sunday`) | ||
| - `bymonthday`: Days of the month, 1-31 | ||
| - `byyearmonth`: Months of the year, 1-12 | ||
|
|
||
| Each accepts a list, so `byhour 8` plus `byhour 20` fires twice a day. | ||
|
|
||
| > [!TIP] | ||
| > Set `frequency` to the coarsest period you want, then refine it with the | ||
| > `by*` fields. A weekly window on Sunday mornings is `frequency weekly` | ||
| > with `byday sunday` and `byhour 4`. Writing the same window as | ||
| > `frequency daily` would fire every morning. | ||
|
|
||
|
|
||
| ## Limitations | ||
|
|
||
| Infix turns each schedule into a five-field cron expression, and the YANG | ||
| model is pruned to the subset cron can express. Everything below is | ||
| rejected at commit time, so a schedule never fires on the wrong days: | ||
|
|
||
| - **`secondly` frequency.** Cron has no seconds field; the finest | ||
| supported resolution is `minutely` | ||
| - **Combining `bymonthday` and `byday`.** Cron fires on the *union* of | ||
| day-of-month and day-of-week, where RFC 5545 specifies their | ||
| intersection, so the combination is refused | ||
| - **Negative values.** "The last Monday of the month" (`byday` with a | ||
| direction) and "the last day of the month" (`bymonthday -1`) have no | ||
| cron equivalent | ||
| - **Start and end bounds.** There is no start anchor, no `until` date and | ||
| no occurrence count. A schedule recurs until it is disabled | ||
| - **Per-schedule time zones**, day-of-year, week-of-year and set-position | ||
|
|
||
| `frequency yearly` with an `interval` above 1 ("every other year") is also | ||
| not expressible; the interval is ignored in that case. | ||
|
|
||
|
|
||
| ## Using a Schedule | ||
|
|
||
| Features reference a schedule through a leaf of type `schedule-ref`. The | ||
| reference is validated, so a schedule cannot be deleted while something | ||
| still uses it, and a typo shows up at commit time instead of at the next | ||
| occurrence. | ||
|
|
||
| These features consume schedules today: | ||
|
|
||
| | Feature | Configuration path | | ||
| |----------------------------|-------------------------------------| | ||
| | Reboot on a schedule | `system scheduled-reboot` | | ||
| | Update checks | `system software check-update` | | ||
| | [Unattended updates][3] | `system software unattended-update` | | ||
|
|
||
| The example below reboots the system on the `nightly` schedule created | ||
| above. Note that `scheduled-reboot` has no `enabled` leaf. It is active | ||
| as soon as it references a schedule; remove the reference or disable the | ||
| schedule to stop it. | ||
|
|
||
| <pre class="cli"><code>admin@example:/> <b>configure</b> | ||
| admin@example:/config/> <b>set system scheduled-reboot schedule nightly</b> | ||
| admin@example:/config/> <b>leave</b> | ||
| </code></pre> | ||
|
|
||
|
|
||
| ## Verifying | ||
|
|
||
| Active schedules become cron jobs owned by the `admin` user. Infix starts | ||
| the cron daemon when at least one job is active and stops it when none | ||
| are. To confirm a schedule took effect, look at the generated crontab | ||
| from the shell: | ||
|
|
||
| ```sh | ||
| admin@example:~$ crontab -l | ||
| # Managed by infix-schedule | ||
| 30 3 * * * /usr/sbin/reboot | ||
| ``` | ||
|
|
||
| An empty crontab means nothing is scheduled. Check that the consuming | ||
| feature is enabled, that it names the schedule correctly, and that the | ||
| schedule itself is enabled. | ||
|
|
||
| > [!NOTE] | ||
| > The crontab is generated and must not be edited by hand. It is | ||
| > rewritten from the configuration on every change. | ||
|
|
||
| [1]: https://github.com/kernelkit/infix/blob/main/src/confd/yang/confd/infix-schedule.yang | ||
| [2]: https://www.rfc-editor.org/rfc/rfc9922 | ||
| [3]: upgrade.md#unattended-updates |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.