sysupdate: Allow vacuuming partially downloaded updates and improve error reporting in that case (#41775)

See the commit messages for details.

Fixes: https://github.com/systemd/systemd/issues/41502
This commit is contained in:
Lennart Poettering
2026-05-11 17:27:05 +02:00
committed by GitHub
6 changed files with 80 additions and 14 deletions
+8
View File
@@ -139,9 +139,11 @@ static int resource_load_from_directory_recursive(
if ((stripped = startswith(de->d_name, ".sysupdate.partial."))) {
de_d_name_stripped = stripped;
is_partial = true;
is_pending = false;
} else if ((stripped = startswith(de->d_name, ".sysupdate.pending."))) {
de_d_name_stripped = stripped;
is_pending = true;
is_partial = false;
} else
de_d_name_stripped = de->d_name;
@@ -192,6 +194,9 @@ static int resource_load_from_directory_recursive(
if (instance->metadata.mode == MODE_INVALID)
instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */
/* Can’t be both partial and pending. */
assert(!(is_partial && is_pending));
instance->is_partial = is_partial;
instance->is_pending = is_pending;
}
@@ -313,6 +318,9 @@ static int resource_load_from_blockdev(Resource *rr) {
if (instance->metadata.read_only < 0)
instance->metadata.read_only = instance->partition_info.read_only;
/* Can’t be both partial and pending. */
assert(!(is_partial && is_pending));
instance->is_partial = is_partial;
instance->is_pending = is_pending;
}
+7 -4
View File
@@ -816,10 +816,13 @@ int transfer_vacuum(
continue;
}
/* If this is listed among the protected versions, then let's not remove it */
if (strv_contains(t->protected_versions, instance->metadata.version) ||
(extra_protected_version && streq(extra_protected_version, instance->metadata.version))) {
log_debug("Version '%s' is pending/partial but protected, not removing.", instance->metadata.version);
/* If this is pending and listed among the protected versions, then let's not remove it.
* In future, we will also want to keep partial protected versions, but that’s only useful
* once we support resuming downloads. */
if (instance->is_pending &&
(strv_contains(t->protected_versions, instance->metadata.version) ||
(extra_protected_version && streq(extra_protected_version, instance->metadata.version)))) {
log_debug("Version '%s' is pending but protected, not removing.", instance->metadata.version);
i++;
continue;
}
@@ -70,6 +70,11 @@ const char* update_set_flags_to_string(UpdateSetFlags flags) {
case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_NEWEST|UPDATE_PROTECTED:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_NEWEST|UPDATE_PROTECTED:
/* can also contain pending instances: */
case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED:
return "current+partial";
case UPDATE_AVAILABLE|UPDATE_NEWEST:
+9 -7
View File
@@ -410,7 +410,10 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
extra_flags |= UPDATE_PROTECTED;
/* Partial or pending updates by definition are not incomplete, they’re
* partial/pending instead */
* partial/pending instead. While an individual Instance cannot be both partial and
* pending, an UpdateSet as a whole can contain both partial and pending instances. */
assert(!match || !(match->is_partial && match->is_pending));
if (match && match->is_partial)
extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE;
@@ -502,8 +505,9 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
c->candidate = NULL;
/* Newest installed is still pending and no candidate is set? Then it becomes the candidate. */
if (c->newest_installed && FLAGS_SET(c->newest_installed->flags, UPDATE_PENDING) &&
/* Newest installed is still pending or partial and no candidate is set? Then it becomes the candidate. */
if (c->newest_installed &&
(c->newest_installed->flags & (UPDATE_PENDING|UPDATE_PARTIAL)) &&
!c->candidate)
c->candidate = c->newest_installed;
@@ -666,7 +670,7 @@ static int context_show_version(Context *c, const char *version) {
Instance *i = *inst;
if (!i) {
assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE));
assert(us->flags & (UPDATE_INCOMPLETE|UPDATE_PARTIAL|UPDATE_PENDING));
continue;
}
@@ -1025,9 +1029,7 @@ static int context_acquire(
if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
else if (FLAGS_SET(us->flags, UPDATE_PARTIAL)) {
log_info("Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version);
return 0;
return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version);
} else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
log_info("Selected update '%s' is already acquired and pending installation.", us->version);
+4
View File
@@ -867,6 +867,10 @@ static int update_render_progress(sd_event_source *source, void *userdata) {
clear_progress_bar_unbuffered(target);
fprintf(stderr, "%s: %s Already up-to-date\n", target, GREEN_CHECK_MARK());
n--; /* Don't consider this target in the total */
} else if (progress == -EUCLEAN) {
clear_progress_bar_unbuffered(target);
fprintf(stderr, "%s: %s Update is already acquired and partially installed. Vacuum it to try installing again.\n", target, RED_CROSS_MARK());
total += 100;
} else if (progress < 0) {
clear_progress_bar_unbuffered(target);
fprintf(stderr, "%s: %s %s\n", target, RED_CROSS_MARK(), STRERROR(progress));
+47 -3
View File
@@ -66,6 +66,7 @@ update_checksums_with_best_before() {
new_version() {
local sector_size="${1:?}"
local version="${2:?}"
local corrupt="${3:-}"
# Create a pair of random partition payloads, and compress one.
# To make not the initial bytes of part1-xxx.raw accidentally match one of the compression header,
@@ -90,11 +91,26 @@ new_version() {
echo $RANDOM >"$WORKDIR/source/dir-$version/bar.txt"
tar --numeric-owner -C "$WORKDIR/source/dir-$version/" -czf "$WORKDIR/source/dir-$version.tar.gz" .
update_checksums
if [[ "$corrupt" == "corrupt-checksum" ]]; then
# As requested, add a deliberately corrupt checksum for this file. This
# will get overwritten next time update_checksums() is called, but the
# integration test will probably have moved on to other things by then.
{
echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea part1-$version.raw"
echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea part2-$version.raw"
echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea part2-$version.raw.gz"
echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea uki-$version.efi"
echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea uki-extra-$version.efi"
echo "abad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1deaabad1dea dir-$version.tar.gz"
} >> "$WORKDIR/source/SHA256SUMS"
else
update_checksums
fi
}
update_now() {
local update_type="${1:?}"
local checks="${2:-}"
# Update to newest version. First there should be an update ready, then we
# do the update, and then there should not be any ready anymore
@@ -105,7 +121,10 @@ update_now() {
# modes. Some updates in the test suite need to be monolithic (e.g. when
# repairing an installation), so that can be overridden via the local.
"$SYSUPDATE" --verify=no check-new
if [[ "$checks" != "no-checks" ]]; then
"$SYSUPDATE" --verify=no check-new
fi
if [[ "$update_type" == "monolithic" ]]; then
"$SYSUPDATE" --verify=no update
elif [[ "$update_type" == "split-offline" ]]; then
@@ -125,7 +144,10 @@ update_now() {
else
exit 1
fi
(! "$SYSUPDATE" --verify=no check-new)
if [[ "$checks" != "no-checks" ]]; then
(! "$SYSUPDATE" --verify=no check-new)
fi
}
verify_version() {
@@ -462,6 +484,28 @@ EOF
verify_version_current "$blockdev" "$sector_size" v8 1
verify_version "$blockdev" "$sector_size" v7 2
# Create a 9th version but corrupt the checksum in SHA256SUMS so pulling it
# fails when verifying the checksum, in order to create a current+partial
# state. Try to update again and verify that this results in an error.
# Vacuum the partial version, regenerate it on the server, try updating
# again and it should succeed.
new_version "$sector_size" v9 "corrupt-checksum"
(! update_now "$update_type")
"$SYSUPDATE" --offline list v9 | grep "partial" >/dev/null
verify_version_current "$blockdev" "$sector_size" v8 1
# don’t verify the other part of the block device as it’s in an indeterminate state
(! update_now "$update_type" "no-checks") |& tee "$WORKDIR"/update_now-9
cat "$WORKDIR"/update_now-9
grep "is already acquired and partially installed. Vacuum it to try installing again." "$WORKDIR"/update_now-9
"$SYSUPDATE" --offline vacuum |& grep "Removing old partial" >/dev/null
verify_version_current "$blockdev" "$sector_size" v8 1
# don’t verify the other part of the block device as it’s in an indeterminate state
"$SYSUPDATE" --verify=no list v9 | grep "candidate" >/dev/null
new_version "$sector_size" v9
update_now "$update_type"
verify_version "$blockdev" "$sector_size" v8 1
verify_version_current "$blockdev" "$sector_size" v9 2
# Cleanup
[[ -b "$blockdev" ]] && losetup --detach "$blockdev"
rm "$BACKING_FILE"