sysupdate: Split update into acquire and install verbs (#40236)

Using roughly the approach described in
https://gitlab.gnome.org/GNOME/gnome-software/-/merge_requests/2004#note_2145880.
Basically, copying in-progress downloads to a file/partition with a
predictable prefix, and then moving to a predictable ‘pending’ prefix
when ready to install.

Helps: https://github.com/systemd/systemd/issues/34814
This commit is contained in:
Luca Boccassi
2026-02-10 13:11:52 +00:00
committed by GitHub
11 changed files with 622 additions and 120 deletions
+20 -3
View File
@@ -128,19 +128,33 @@
</varlistentry>
<varlistentry>
<term><option>update</option> <optional><replaceable>VERSION</replaceable></optional></term>
<term><option>update</option> <optional>--offline</optional> <optional><replaceable>VERSION</replaceable></optional></term>
<listitem><para>Installs (updates to) the specified version, or if none is specified to the newest
version available. If the version is already installed or no newer version available, no operation is
executed.</para>
<para>If <option>--offline</option> is specified, the update must already have been acquired using
<command>acquire</command> and, if so, this pre-acquired version is the one which will be updated
to.</para>
<xi:include href="version-info.xml" xpointer="v251"/></listitem>
</varlistentry>
<varlistentry>
<term><option>acquire</option> <optional><replaceable>VERSION</replaceable></optional></term>
<listitem><para>Acquires (downloads) the specified version, ready to install it. If no version is
specified, the newest version available is acquired. If the version is already installed or no newer
version is available, no operation is executed.</para>
<para>If a new version to install/update to is found, old installed versions are deleted until at
least one new version can be installed, as configured via <varname>InstanceMax=</varname> in
<citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>, or
via the available partition slots of the right type. This implicit operation can also be invoked
explicitly via the <command>vacuum</command> command described below.</para>
<xi:include href="version-info.xml" xpointer="v251"/></listitem>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
<varlistentry>
@@ -299,7 +313,7 @@
<varlistentry>
<term><option>--reboot</option></term>
<listitem><para>When used in combination with the <command>update</command> command and a new version is
<listitem><para>When used in combination with the <command>update</command> commands and a new version is
installed, automatically reboots the system immediately afterwards.</para>
<xi:include href="version-info.xml" xpointer="v251"/></listitem>
@@ -312,6 +326,9 @@
This is most useful when used in combination with the <command>list</command> command, to query
locally installed versions.</para>
<para>If used in combination with the <command>update</command> command, it allows updates to be
downloaded in advance (using <command>acquire</command>) and installed later.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
+4 -1
View File
@@ -45,8 +45,11 @@ struct Instance {
InstanceMetadata metadata;
/* Where we found the instance */
char *path;
char *path; /* includes the `.sysupdate.partial.` (etc.) prefix, if applicable */
PartitionInfo partition_info;
bool is_partial;
bool is_pending;
};
void instance_metadata_destroy(InstanceMetadata *m);
+27
View File
@@ -16,6 +16,33 @@ void partition_info_destroy(PartitionInfo *p) {
p->device = mfree(p->device);
}
int partition_info_copy(PartitionInfo *dest, const PartitionInfo *src) {
int r;
assert(dest);
assert(src);
r = free_and_strdup_warn(&dest->label, src->label);
if (r < 0)
return r;
r = free_and_strdup_warn(&dest->device, src->device);
if (r < 0)
return r;
dest->partno = src->partno;
dest->start = src->start;
dest->size = src->size;
dest->flags = src->flags;
dest->type = src->type;
dest->uuid = src->uuid;
dest->no_auto = src->no_auto;
dest->read_only = src->read_only;
dest->growfs = src->growfs;
return 0;
}
int read_partition_info(
struct fdisk_context *c,
struct fdisk_table *t,
+1
View File
@@ -36,6 +36,7 @@ typedef struct PartitionInfo {
}
void partition_info_destroy(PartitionInfo *p);
int partition_info_copy(PartitionInfo *dest, const PartitionInfo *src);
int read_partition_info(struct fdisk_context *c, struct fdisk_table *t, size_t i, PartitionInfo *ret);
+52 -5
View File
@@ -80,15 +80,22 @@ static int resource_load_from_directory_recursive(
Resource *rr,
DIR* d,
const char* relpath,
mode_t m) {
const char* relpath_for_matching,
mode_t m,
bool ancestor_is_partial,
bool ancestor_is_pending) {
int r;
for (;;) {
_cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
_cleanup_free_ char *joined = NULL, *rel_joined = NULL;
_cleanup_free_ char *rel_joined_for_matching = NULL;
Instance *instance;
struct dirent *de;
const char *de_d_name_stripped;
struct stat st;
bool is_partial = ancestor_is_partial, is_pending = ancestor_is_pending;
const char *stripped;
errno = 0;
de = readdir_no_dot(d);
@@ -128,11 +135,26 @@ static int resource_load_from_directory_recursive(
if (!(S_ISDIR(st.st_mode) && S_ISREG(m)) && ((st.st_mode & S_IFMT) != m))
continue;
if ((stripped = startswith(de->d_name, ".sysupdate.partial."))) {
de_d_name_stripped = stripped;
is_partial = true;
} else if ((stripped = startswith(de->d_name, ".sysupdate.pending."))) {
de_d_name_stripped = stripped;
is_pending = true;
} else
de_d_name_stripped = de->d_name;
rel_joined = path_join(relpath, de->d_name);
if (!rel_joined)
return log_oom();
r = pattern_match_many(rr->patterns, rel_joined, &extracted_fields);
/* Match against the filename with any `.sysupdate.partial.` (etc.) prefix stripped, so the
* user’s patterns still apply. But don’t use the stripped version in any paths or recursion. */
rel_joined_for_matching = path_join(relpath_for_matching, de_d_name_stripped);
if (!rel_joined_for_matching)
return log_oom();
r = pattern_match_many(rr->patterns, rel_joined_for_matching, &extracted_fields);
if (r == PATTERN_MATCH_RETRY) {
_cleanup_closedir_ DIR *subdir = NULL;
@@ -140,7 +162,7 @@ static int resource_load_from_directory_recursive(
if (!subdir)
continue;
r = resource_load_from_directory_recursive(rr, subdir, rel_joined, m);
r = resource_load_from_directory_recursive(rr, subdir, rel_joined, rel_joined_for_matching, m, is_partial, is_pending);
if (r < 0)
return r;
if (r == 0)
@@ -168,6 +190,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 */
instance->is_partial = is_partial;
instance->is_pending = is_pending;
}
return 0;
@@ -192,7 +217,7 @@ static int resource_load_from_directory(
return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path);
}
return resource_load_from_directory_recursive(rr, d, NULL, m);
return resource_load_from_directory_recursive(rr, d, NULL, NULL, m, false, false);
}
static int resource_load_from_blockdev(Resource *rr) {
@@ -219,6 +244,9 @@ static int resource_load_from_blockdev(Resource *rr) {
_cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
_cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL;
Instance *instance;
const char *pinfo_label_stripped;
bool is_partial = false, is_pending = false;
const char *stripped;
r = read_partition_info(c, t, i, &pinfo);
if (r < 0)
@@ -236,7 +264,18 @@ static int resource_load_from_blockdev(Resource *rr) {
continue;
}
r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields);
/* Match the label with any partial/pending prefix removed so the user’s existing patterns
* match regardless of the instance’s state. */
if ((stripped = startswith(pinfo.label, "PRT#"))) {
pinfo_label_stripped = stripped;
is_partial = true;
} else if ((stripped = startswith(pinfo.label, "PND#"))) {
pinfo_label_stripped = stripped;
is_pending = true;
} else
pinfo_label_stripped = pinfo.label;
r = pattern_match_many(rr->patterns, pinfo_label_stripped, &extracted_fields);
if (r < 0)
return log_error_errno(r, "Failed to match pattern: %m");
if (IN_SET(r, PATTERN_MATCH_NO, PATTERN_MATCH_RETRY))
@@ -262,6 +301,9 @@ static int resource_load_from_blockdev(Resource *rr) {
if (instance->metadata.read_only < 0)
instance->metadata.read_only = instance->partition_info.read_only;
instance->is_partial = is_partial;
instance->is_pending = is_pending;
}
return 0;
@@ -539,6 +581,11 @@ static int resource_load_from_web(
memcpy(instance->metadata.sha256sum, h.iov_base, h.iov_len);
instance->metadata.sha256sum_set = true;
}
/* Web resources can only be a source, not a target, so
* can never be partial or pending. */
instance->is_partial = false;
instance->is_pending = false;
}
}
+295 -78
View File
@@ -41,7 +41,6 @@
#include "sysupdate-resource.h"
#include "sysupdate-transfer.h"
#include "time-util.h"
#include "tmpfile-util.h"
#include "web-util.h"
/* Default value for InstancesMax= for fs object targets */
@@ -51,7 +50,8 @@ Transfer* transfer_free(Transfer *t) {
if (!t)
return NULL;
t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
free(t->temporary_partial_path);
free(t->temporary_pending_path);
free(t->id);
@@ -67,6 +67,9 @@ Transfer* transfer_free(Transfer *t) {
strv_free(t->appstream);
partition_info_destroy(&t->partition_info);
free(t->temporary_partial_partition_label);
free(t->temporary_pending_partition_label);
free(t->final_partition_label);
resource_destroy(&t->source);
resource_destroy(&t->target);
@@ -740,6 +743,49 @@ static void transfer_remove_temporary(Transfer *t) {
}
}
static int transfer_instance_vacuum(
Transfer *t,
Instance *instance) {
int r;
assert(t);
assert(instance);
switch (t->target.type) {
case RESOURCE_REGULAR_FILE:
case RESOURCE_DIRECTORY:
case RESOURCE_SUBVOLUME:
r = rm_rf(instance->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", instance->path);
(void) rmdir_parents(instance->path, t->target.path);
break;
case RESOURCE_PARTITION: {
PartitionInfo pinfo = instance->partition_info;
/* label "_empty" means "no contents" for our purposes */
pinfo.label = (char*) "_empty";
log_debug("Relabelling partition '%s' to '%s'.", pinfo.device, pinfo.label);
r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
if (r < 0)
return r;
t->target.n_empty++;
break;
}
default:
assert_not_reached();
}
return 0;
}
int transfer_vacuum(
Transfer *t,
uint64_t space,
@@ -752,7 +798,45 @@ int transfer_vacuum(
transfer_remove_temporary(t);
/* First, calculate how many instances to keep, based on the instance limit — but keep at least one */
/* First, remove any partial or pending instances (unless protected) */
for (size_t i = 0; i < t->target.n_instances;) {
Instance *instance = t->target.instances[i];
assert(instance);
if (!instance->is_pending && !instance->is_partial) {
i++;
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);
i++;
continue;
}
assert(instance->resource);
log_info("%s Removing old %s '%s' (%s).",
glyph(GLYPH_RECYCLING),
instance->is_partial ? "partial" : "pending",
instance->path,
resource_type_to_string(instance->resource->type));
r = transfer_instance_vacuum(t, instance);
if (r < 0)
return 0;
instance_free(instance);
memmove(t->target.instances + i, t->target.instances + i + 1, (t->target.n_instances - i - 1) * sizeof(Instance*));
t->target.n_instances--;
count++;
}
/* Second, calculate how many instances to keep, based on the instance limit — but keep at least one */
instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max;
assert(instances_max >= 1);
@@ -840,36 +924,9 @@ int transfer_vacuum(
oldest->path,
resource_type_to_string(oldest->resource->type));
switch (t->target.type) {
case RESOURCE_REGULAR_FILE:
case RESOURCE_DIRECTORY:
case RESOURCE_SUBVOLUME:
r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD);
if (r < 0 && r != -ENOENT)
return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path);
(void) rmdir_parents(oldest->path, t->target.path);
break;
case RESOURCE_PARTITION: {
PartitionInfo pinfo = oldest->partition_info;
/* label "_empty" means "no contents" for our purposes */
pinfo.label = (char*) "_empty";
r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL);
if (r < 0)
return r;
t->target.n_empty++;
break;
}
default:
assert_not_reached();
}
r = transfer_instance_vacuum(t, oldest);
if (r < 0)
return 0;
instance_free(oldest);
memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*));
@@ -1111,8 +1168,100 @@ static int run_callout(
return sd_event_loop(event);
}
/* Build the filenames and paths which is normally done by transfer_acquire_instance(), but for partial
* and pending instances which are about to be installed (in which case, transfer_acquire_instance() is
* skipped). */
static int transfer_compute_temporary_paths(Transfer *t, Instance *i, InstanceMetadata *f) {
_cleanup_free_ char *formatted_pattern = NULL, *formatted_partial_pattern = NULL, *formatted_pending_pattern = NULL;
int r;
assert(t);
assert(i);
assert(!t->final_path);
assert(!t->temporary_partial_path);
assert(!t->temporary_pending_path);
assert(!t->final_partition_label);
assert(!t->temporary_partial_partition_label);
assert(!t->temporary_pending_partition_label);
assert(!strv_isempty(t->target.patterns));
/* Format the target name using the first pattern specified */
compile_pattern_fields(t, i, f);
r = pattern_format(t->target.patterns[0], f, &formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to format target pattern: %m");
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
_cleanup_free_ char *final_dir = NULL, *final_filename = NULL, *partial_filename = NULL, *pending_filename = NULL;
if (!path_is_safe(formatted_pattern))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
t->final_path = path_join(t->target.path, formatted_pattern);
if (!t->final_path)
return log_oom();
/* Build the paths for the partial and pending files, which hold the resource while it’s
* being acquired and after it’s been acquired (but before it’s moved to the final_path
* when it’s installed).
*
* Split the filename off the `final_path`, then add a prefix to it for each of partial and
* pending, then join them back on to the same directory. */
r = path_split_prefix_filename(t->final_path, &final_dir, &final_filename);
if (r < 0)
return log_error_errno(r, "Failed to parse path: %m");
if (!strprepend(&partial_filename, ".sysupdate.partial.", final_filename))
return log_oom();
if (!strprepend(&pending_filename, ".sysupdate.pending.", final_filename))
return log_oom();
t->temporary_partial_path = path_join(final_dir, partial_filename);
if (!t->temporary_partial_path)
return log_oom();
t->temporary_pending_path = path_join(final_dir, pending_filename);
if (!t->temporary_pending_path)
return log_oom();
}
if (t->target.type == RESOURCE_PARTITION) {
r = gpt_partition_label_valid(formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
if (!strprepend(&formatted_partial_pattern, "PRT#", formatted_pattern))
return log_oom();
r = gpt_partition_label_valid(formatted_partial_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_partial_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_partial_pattern);
free_and_replace(t->temporary_partial_partition_label, formatted_partial_pattern);
if (!strprepend(&formatted_pending_pattern, "PND#", formatted_pattern))
return log_oom();
r = gpt_partition_label_valid(formatted_pending_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pending_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pending_pattern);
free_and_replace(t->temporary_pending_partition_label, formatted_pending_pattern);
t->final_partition_label = TAKE_PTR(formatted_pattern);
}
return 0;
}
int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata) {
_cleanup_free_ char *formatted_pattern = NULL, *digest = NULL;
_cleanup_free_ char *digest = NULL;
char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
const char *where = NULL;
InstanceMetadata f;
@@ -1126,48 +1275,35 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
/* Does this instance already exist in the target? Then we don't need to acquire anything */
existing = resource_find_instance(&t->target, i->metadata.version);
if (existing && (existing->is_partial || existing->is_pending))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to acquire '%s', instance is already partial or pending in the target.", i->path);
if (existing) {
log_info("No need to acquire '%s', already installed.", i->path);
return 0;
}
assert(!t->final_path);
assert(!t->temporary_path);
assert(!strv_isempty(t->target.patterns));
/* Format the target name using the first pattern specified */
compile_pattern_fields(t, i, &f);
r = pattern_format(t->target.patterns[0], &f, &formatted_pattern);
/* Compute up the temporary paths */
r = transfer_compute_temporary_paths(t, i, &f);
if (r < 0)
return log_error_errno(r, "Failed to format target pattern: %m");
return r;
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
r = mkdir_parents(t->temporary_partial_path, 0755);
if (r < 0)
return log_error_errno(r, "Cannot create target directory: %m");
if (!path_is_safe(formatted_pattern))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern);
t->final_path = path_join(t->target.path, formatted_pattern);
if (!t->final_path)
return log_oom();
r = mkdir_parents(t->temporary_pending_path, 0755);
if (r < 0)
return log_error_errno(r, "Cannot create target directory: %m");
r = mkdir_parents(t->final_path, 0755);
if (r < 0)
return log_error_errno(r, "Cannot create target directory: %m");
r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path);
if (r < 0)
return log_error_errno(r, "Failed to generate temporary target path: %m");
where = t->final_path;
}
if (t->target.type == RESOURCE_PARTITION) {
r = gpt_partition_label_valid(formatted_pattern);
if (r < 0)
return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern);
if (!r)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern);
r = find_suitable_partition(
t->target.path,
i->metadata.size,
@@ -1180,6 +1316,20 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
xsprintf(max_size, "%" PRIu64, t->partition_info.size);
where = t->partition_info.device;
/* Rename the partition to `PRT#<VERSION>` to indicate that a transfer to it is in progress. */
r = free_and_strdup_warn(&t->partition_info.label, t->temporary_partial_partition_label);
if (r < 0)
return r;
t->partition_change = PARTITION_LABEL;
log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label);
r = patch_partition(
t->target.path,
&t->partition_info,
t->partition_change);
if (r < 0)
return r;
}
assert(where);
@@ -1218,7 +1368,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
"--direct", /* just copy/unpack the specified file, don't do anything else */
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1259,7 +1409,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
arg_sync ? "--sync=yes" : "--sync=no",
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1276,7 +1426,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
arg_sync ? "--sync=yes" : "--sync=no",
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1296,7 +1446,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
"--verify", digest, /* validate by explicit SHA256 sum */
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1336,7 +1486,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no",
arg_sync ? "--sync=yes" : "--sync=no",
i->path,
t->temporary_path),
t->temporary_partial_path),
t, i, cb, userdata);
break;
@@ -1348,7 +1498,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
if (RESOURCE_IS_FILESYSTEM(t->target.type)) {
bool need_sync = false;
assert(t->temporary_path);
assert(t->temporary_partial_path);
assert(t->temporary_pending_path);
/* Apply file attributes if set */
if (f.mtime != USEC_INFINITY) {
@@ -1356,8 +1507,8 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
timespec_store(&ts, f.mtime);
if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path);
if (utimensat(AT_FDCWD, t->temporary_partial_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_partial_path);
need_sync = true;
}
@@ -1366,9 +1517,9 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
/* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older
* kernels don't support that however, in that case we fall back to chmod(). Not as
* safe, but shouldn't be a problem, given that we don't create symlinks here. */
if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
(!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0))
return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path);
if (fchmodat(AT_FDCWD, t->temporary_partial_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 &&
(!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_partial_path, f.mode) < 0))
return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_partial_path);
need_sync = true;
}
@@ -1376,20 +1527,34 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
/* Synchronize */
if (arg_sync && need_sync) {
if (t->target.type == RESOURCE_REGULAR_FILE)
r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path);
r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_partial_path);
else {
assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
r = syncfs_path(AT_FDCWD, t->temporary_path);
r = syncfs_path(AT_FDCWD, t->temporary_partial_path);
}
if (r < 0)
return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path);
return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_partial_path);
}
t->install_read_only = f.read_only;
/* Rename the file from `.sysupdate.partial.<VERSION>` to `.sysupdate.pending.<VERSION>` to indicate it’s ready to install. */
log_debug("Renaming resource instance '%s' to '%s'.", t->temporary_partial_path, t->temporary_pending_path);
r = install_file(AT_FDCWD, t->temporary_partial_path,
AT_FDCWD, t->temporary_pending_path,
INSTALL_REPLACE|
(t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
(t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS));
if (r < 0)
return log_error_errno(r, "Failed to move '%s' into pending place: %m", t->temporary_pending_path);
}
if (t->target.type == RESOURCE_PARTITION) {
free_and_replace(t->partition_info.label, formatted_pattern);
/* Now rename the partition again to `PND#<VERSION>` to indicate that the acquire is complete
* and the partition is ready for install. */
r = free_and_strdup_warn(&t->partition_info.label, t->temporary_pending_partition_label);
if (r < 0)
return r;
t->partition_change = PARTITION_LABEL;
if (f.partition_uuid_set) {
@@ -1416,6 +1581,14 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
t->partition_info.growfs = f.growfs;
t->partition_change |= PARTITION_GROWFS;
}
log_debug("Relabelling partition '%s' to '%s'.", t->partition_info.device, t->partition_info.label);
r = patch_partition(
t->target.path,
&t->partition_info,
t->partition_change);
if (r < 0)
return r;
}
/* For regular file cases the only step left is to install the file in place, which install_file()
@@ -1426,6 +1599,42 @@ int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, voi
return 0;
}
int transfer_process_partial_and_pending_instance(Transfer *t, Instance *i) {
InstanceMetadata f;
Instance *existing;
int r;
assert(t);
assert(i);
log_debug("transfer_process_partial_and_pending_instance %s", i->path);
/* Does this instance already exist in the target but isn’t pending? */
existing = resource_find_instance(&t->target, i->metadata.version);
if (existing && !existing->is_pending)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to acquire '%s', instance is already in the target but is not pending.", i->path);
/* All we need to do is compute the temporary paths. We don’t need to do any of the other work in
* transfer_acquire_instance(). */
r = transfer_compute_temporary_paths(t, i, &f);
if (r < 0)
return r;
/* This is the analogue of find_suitable_partition(), but since finding the suitable partition has
* already happened in the acquire phase, the target should already have that information and it
* should already have been claimed as `PND#`. */
if (t->target.type == RESOURCE_PARTITION) {
assert(i->resource == &t->target);
assert(i->is_pending);
r = partition_info_copy(&t->partition_info, &i->partition_info);
if (r < 0)
return r;
}
return 0;
}
int transfer_install_instance(
Transfer *t,
Instance *i,
@@ -1436,13 +1645,15 @@ int transfer_install_instance(
assert(t);
assert(i);
assert(i->resource);
assert(t == container_of(i->resource, Transfer, source));
assert(i->is_pending || t == container_of(i->resource, Transfer, source));
if (t->temporary_path) {
log_debug("transfer_install_instance %s %s %s %d", i->path, t->temporary_pending_path, t->final_partition_label, t->partition_change);
if (t->temporary_pending_path) {
assert(RESOURCE_IS_FILESYSTEM(t->target.type));
assert(t->final_path);
r = install_file(AT_FDCWD, t->temporary_path,
r = install_file(AT_FDCWD, t->temporary_pending_path,
AT_FDCWD, t->final_path,
INSTALL_REPLACE|
(t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)|
@@ -1456,11 +1667,17 @@ int transfer_install_instance(
t->final_path,
resource_type_to_string(t->target.type));
t->temporary_path = mfree(t->temporary_path);
t->temporary_pending_path = mfree(t->temporary_pending_path);
}
if (t->partition_change != 0) {
if (t->temporary_pending_partition_label) {
assert(t->target.type == RESOURCE_PARTITION);
assert(t->final_partition_label);
r = free_and_strdup_warn(&t->partition_info.label, t->final_partition_label);
if (r < 0)
return r;
t->partition_change = PARTITION_LABEL;
r = patch_partition(
t->target.path,
+6 -1
View File
@@ -39,13 +39,17 @@ typedef struct Transfer {
int growfs;
/* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */
char *temporary_path;
char *temporary_partial_path;
char *temporary_pending_path;
char *final_path;
int install_read_only;
/* If we write to a partition in a partition table, the metrics of it */
PartitionInfo partition_info;
PartitionChange partition_change;
char *final_partition_label;
char *temporary_partial_partition_label;
char *temporary_pending_partition_label;
Context *context;
} Transfer;
@@ -63,5 +67,6 @@ int transfer_resolve_paths(Transfer *t, const char *root, const char *node);
int transfer_vacuum(Transfer *t, uint64_t space, const char *extra_protected_version);
int transfer_acquire_instance(Transfer *t, Instance *i, TransferProgress cb, void *userdata);
int transfer_process_partial_and_pending_instance(Transfer *t, Instance *i);
int transfer_install_instance(Transfer *t, Instance *i, const char *root);
@@ -9,6 +9,9 @@ const char* update_set_flags_to_color(UpdateSetFlags flags) {
if (flags == 0 || (flags & UPDATE_OBSOLETE))
return (flags & UPDATE_NEWEST) ? ansi_highlight_grey() : ansi_grey();
if (flags & (UPDATE_PARTIAL|UPDATE_PENDING))
return ansi_highlight_cyan();
if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_INCOMPLETE))
return ansi_highlight_yellow();
@@ -29,6 +32,9 @@ const char* update_set_flags_to_glyph(UpdateSetFlags flags) {
if (flags == 0 || (flags & UPDATE_OBSOLETE))
return glyph(GLYPH_MULTIPLICATION_SIGN);
if (flags & (UPDATE_PARTIAL|UPDATE_PENDING))
return glyph(GLYPH_DOWNLOAD);
if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST))
return glyph(GLYPH_BLACK_CIRCLE);
@@ -54,6 +60,18 @@ const char* update_set_flags_to_string(UpdateSetFlags flags) {
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
return "current";
case UPDATE_INSTALLED|UPDATE_PENDING|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PENDING|UPDATE_NEWEST:
case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PENDING|UPDATE_NEWEST|UPDATE_PROTECTED:
return "current+pending";
case UPDATE_INSTALLED|UPDATE_PARTIAL|UPDATE_NEWEST:
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:
return "current+partial";
case UPDATE_AVAILABLE|UPDATE_NEWEST:
case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
return "candidate";
@@ -10,6 +10,8 @@ typedef enum UpdateSetFlags {
UPDATE_OBSOLETE = 1 << 3,
UPDATE_PROTECTED = 1 << 4,
UPDATE_INCOMPLETE = 1 << 5,
UPDATE_PARTIAL = 1 << 6,
UPDATE_PENDING = 1 << 7,
} UpdateSetFlags;
const char* update_set_flags_to_color(UpdateSetFlags flags);
+168 -22
View File
@@ -397,7 +397,7 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
assert(flags == UPDATE_INSTALLED);
match = resource_find_instance(&t->target, cursor);
if (!match) {
if (!match && !(extra_flags & (UPDATE_PARTIAL|UPDATE_PENDING))) {
/* When we're looking for installed versions, let's be robust and treat
* an incomplete installation as an installation. Otherwise, there are
* situations that can lead to sysupdate wiping the currently booted OS.
@@ -413,6 +413,14 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
if (strv_contains(t->protected_versions, cursor))
extra_flags |= UPDATE_PROTECTED;
/* Partial or pending updates by definition are not incomplete, they’re
* partial/pending instead */
if (match && match->is_partial)
extra_flags = (extra_flags | UPDATE_PARTIAL) & ~UPDATE_INCOMPLETE;
if (match && match->is_pending)
extra_flags = (extra_flags | UPDATE_PENDING) & ~UPDATE_INCOMPLETE;
}
r = free_and_strdup_warn(&boundary, cursor);
@@ -431,7 +439,9 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
/* Merge in what we've learned and continue onto the next version */
if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE)) {
if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE) ||
FLAGS_SET(u->flags, UPDATE_PARTIAL) ||
FLAGS_SET(u->flags, UPDATE_PENDING)) {
assert(u->n_instances == c->n_transfers);
/* Incomplete updates will have picked NULL instances for the transfers that
@@ -450,7 +460,7 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
/* If this is the newest installed version, that is incomplete and just became marked
* as available, and if there is no other candidate available, we promote this to be
* the candidate. */
* the candidate. Ignore partial or pending status on the update set. */
if (FLAGS_SET(u->flags, UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_INCOMPLETE|UPDATE_AVAILABLE) &&
!c->candidate && !FLAGS_SET(u->flags, UPDATE_OBSOLETE))
c->candidate = u;
@@ -486,7 +496,8 @@ static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags
if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
c->newest_installed = us;
/* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */
/* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate".
* It may be partial or pending. */
if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
c->candidate = us;
}
@@ -496,6 +507,11 @@ 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) &&
!c->candidate)
c->candidate = c->newest_installed;
return 0;
}
@@ -595,13 +611,14 @@ static int context_show_version(Context *c, const char *version) {
if (!sd_json_format_enabled(arg_json_format_flags))
printf("%s%s%s Version: %s\n"
" State: %s%s%s\n"
"Installed: %s%s\n"
"Installed: %s%s%s%s\n"
"Available: %s%s\n"
"Protected: %s%s%s\n"
" Obsolete: %s%s%s\n\n",
strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
@@ -794,7 +811,7 @@ static int context_show_version(Context *c, const char *version) {
if (!sd_json_format_enabled(arg_json_format_flags)) {
printf("%s%s%s Version: %s\n"
" State: %s%s%s\n"
"Installed: %s%s%s%s%s\n"
"Installed: %s%s%s%s%s%s%s\n"
"Available: %s%s\n"
"Protected: %s%s%s\n"
" Obsolete: %s%s%s\n",
@@ -802,6 +819,7 @@ static int context_show_version(Context *c, const char *version) {
strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? ansi_highlight_yellow() : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? " (incomplete)" : "", ansi_normal(),
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PENDING) ? " (pending)" : "", FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PARTIAL) ? " (partial)" : "",
yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
@@ -827,6 +845,8 @@ static int context_show_version(Context *c, const char *version) {
SD_JSON_BUILD_PAIR_BOOLEAN("newest", FLAGS_SET(us->flags, UPDATE_NEWEST)),
SD_JSON_BUILD_PAIR_BOOLEAN("available", FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
SD_JSON_BUILD_PAIR_BOOLEAN("installed", FLAGS_SET(us->flags, UPDATE_INSTALLED)),
SD_JSON_BUILD_PAIR_BOOLEAN("partial", FLAGS_SET(us->flags, UPDATE_PARTIAL)),
SD_JSON_BUILD_PAIR_BOOLEAN("pending", FLAGS_SET(us->flags, UPDATE_PENDING)),
SD_JSON_BUILD_PAIR_BOOLEAN("obsolete", FLAGS_SET(us->flags, UPDATE_OBSOLETE)),
SD_JSON_BUILD_PAIR_BOOLEAN("protected", FLAGS_SET(us->flags, UPDATE_PROTECTED)),
SD_JSON_BUILD_PAIR_BOOLEAN("incomplete", FLAGS_SET(us->flags, UPDATE_INCOMPLETE)),
@@ -982,10 +1002,11 @@ static int context_on_acquire_progress(const Transfer *t, const Instance *inst,
overall, n - i, i, inst->metadata.version, overall);
}
static int context_apply(
static int context_process_partial_and_pending(Context *c, const char *version);
static int context_acquire(
Context *c,
const char *version,
UpdateSet **ret_applied) {
const char *version) {
UpdateSet *us = NULL;
int r;
@@ -1000,9 +1021,6 @@ static int context_apply(
if (!c->candidate) {
log_info("No update needed.");
if (ret_applied)
*ret_applied = NULL;
return 0;
}
@@ -1011,11 +1029,16 @@ static int context_apply(
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_INSTALLED)) {
log_info("Selected update '%s' is already installed. Skipping update.", 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);
if (ret_applied)
*ret_applied = NULL;
return 0;
} else if (FLAGS_SET(us->flags, UPDATE_PENDING)) {
log_info("Selected update '%s' is already acquired and pending installation.", us->version);
return context_process_partial_and_pending(c, version);
} else if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
log_info("Selected update '%s' is already installed. Skipping update.", us->version);
return 0;
}
@@ -1075,14 +1098,106 @@ static int context_apply(
if (arg_sync)
sync();
(void) sd_notifyf(/* unset_environment= */ false,
return 1;
}
/* Check to see if we have an update set acquired and pending installation. */
static int context_process_partial_and_pending(
Context *c,
const char *version) {
UpdateSet *us = NULL;
int r;
assert(c);
if (version) {
us = context_update_set_by_version(c, version);
if (!us)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
} else {
if (!c->candidate) {
log_info("No update needed.");
return 0;
}
us = c->candidate;
}
if (FLAGS_SET(us->flags, UPDATE_INCOMPLETE))
log_info("Selected update '%s' is already installed, but incomplete. Repairing.", us->version);
else if ((us->flags & (UPDATE_PARTIAL|UPDATE_PENDING|UPDATE_INSTALLED)) == UPDATE_INSTALLED) {
log_info("Selected update '%s' is already installed. Skipping update.", us->version);
return 0;
}
if (FLAGS_SET(us->flags, UPDATE_PARTIAL))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is only partially downloaded, refusing.", us->version);
if (!FLAGS_SET(us->flags, UPDATE_PENDING))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not pending installation, refusing.", us->version);
if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
log_info("Selected update '%s' for install.", us->version);
/* There should now be one instance picked for each transfer, and the order is the same */
assert(us->n_instances == c->n_transfers);
for (size_t i = 0; i < c->n_transfers; i++) {
Instance *inst = us->instances[i];
Transfer *t = c->transfers[i];
assert(inst);
r = transfer_process_partial_and_pending_instance(t, inst);
if (r < 0)
return r;
}
return 1;
}
static int context_install(
Context *c,
const char *version,
UpdateSet **ret_applied) {
UpdateSet *us = NULL;
int r;
assert(c);
if (version) {
us = context_update_set_by_version(c, version);
if (!us)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
} else {
if (!c->candidate) {
log_info("No update needed.");
return 0;
}
us = c->candidate;
}
(void) sd_notifyf(/* unset_environment=*/ false,
"STATUS=Installing '%s'.", us->version);
for (size_t i = 0; i < c->n_transfers; i++) {
Instance *inst = us->instances[i];
Transfer *t = c->transfers[i];
if (inst->resource == &t->target)
if (inst->resource == &t->target &&
!inst->is_pending)
continue;
r = transfer_install_instance(t, inst, arg_root);
@@ -1173,13 +1288,16 @@ static int verb_list(int argc, char **argv, void *userdata) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **versions = NULL;
const char *current = NULL;
bool current_is_pending = false;
FOREACH_ARRAY(update_set, context->update_sets, context->n_update_sets) {
UpdateSet *us = *update_set;
if (FLAGS_SET(us->flags, UPDATE_INSTALLED) &&
FLAGS_SET(us->flags, UPDATE_NEWEST))
FLAGS_SET(us->flags, UPDATE_NEWEST)) {
current = us->version;
current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING);
}
r = strv_extend(&versions, us->version);
if (r < 0)
@@ -1197,7 +1315,7 @@ static int verb_list(int argc, char **argv, void *userdata) {
return log_oom();
}
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("current", current),
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING(current_is_pending ? "current+pending" : "current", current),
SD_JSON_BUILD_PAIR_STRV("all", versions),
SD_JSON_BUILD_PAIR_STRV("appstreamUrls", appstream_urls));
if (r < 0)
@@ -1409,7 +1527,12 @@ static int verb_vacuum(int argc, char **argv, void *userdata) {
return context_vacuum(context, 0, NULL);
}
static int verb_update(int argc, char **argv, void *userdata) {
typedef enum {
UPDATE_ACTION_ACQUIRE = 1 << 0,
UPDATE_ACTION_INSTALL = 1 << 1,
} UpdateActionFlags;
static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_(context_freep) Context* context = NULL;
@@ -1443,7 +1566,15 @@ static int verb_update(int argc, char **argv, void *userdata) {
if (r < 0)
return r;
r = context_apply(context, version, &applied);
if (action_flags & UPDATE_ACTION_ACQUIRE)
r = context_acquire(context, version);
else
r = context_process_partial_and_pending(context, version);
if (r < 0)
return r; /* error */
if (action_flags & UPDATE_ACTION_INSTALL && r > 0) /* update needed */
r = context_install(context, version, &applied);
if (r < 0)
return r;
@@ -1468,6 +1599,19 @@ static int verb_update(int argc, char **argv, void *userdata) {
return 0;
}
static int verb_update(int argc, char **argv, void *userdata) {
UpdateActionFlags flags = UPDATE_ACTION_INSTALL;
if (!arg_offline)
flags |= UPDATE_ACTION_ACQUIRE;
return verb_update_impl(argc, argv, flags);
}
static int verb_acquire(int argc, char **argv, void *userdata) {
return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE);
}
static int verb_pending_or_reboot(int argc, char **argv, void *userdata) {
_cleanup_(context_freep) Context* context = NULL;
_cleanup_free_ char *booted_version = NULL;
@@ -1647,6 +1791,7 @@ static int verb_help(int argc, char **argv, void *userdata) {
" features [FEATURE] Show optional features\n"
" check-new Check if there's a new version available\n"
" update [VERSION] Install new version now\n"
" acquire [VERSION] Acquire (download) new version now\n"
" vacuum Make room, by deleting old versions\n"
" pending Report whether a newer version is installed than\n"
" currently booted\n"
@@ -1860,6 +2005,7 @@ static int sysupdate_main(int argc, char *argv[]) {
{ "features", VERB_ANY, 2, 0, verb_features },
{ "check-new", VERB_ANY, 1, 0, verb_check_new },
{ "update", VERB_ANY, 2, 0, verb_update },
{ "acquire", VERB_ANY, 2, 0, verb_acquire },
{ "vacuum", VERB_ANY, 1, 0, verb_vacuum },
{ "reboot", 1, 1, 0, verb_pending_or_reboot },
{ "pending", 1, 1, 0, verb_pending_or_reboot },
+29 -10
View File
@@ -92,11 +92,27 @@ new_version() {
}
update_now() {
local update_type="${1:?}"
# 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
#
# The update can either be done monolithically (by calling the `update`
# verb) or split (`acquire` then `update`). Both options are allowed for
# most updates in the test suite, so the test suite can be run to test both
# 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
"$SYSUPDATE" --verify=no update
if [[ "$update_type" == "monolithic" ]]; then
"$SYSUPDATE" --verify=no update
elif [[ "$update_type" == "split-offline" ]]; then
"$SYSUPDATE" --verify=no acquire
"$SYSUPDATE" --verify=no update --offline
elif [[ "$update_type" == "split" ]]; then
"$SYSUPDATE" --verify=no acquire
"$SYSUPDATE" --verify=no update
fi
(! "$SYSUPDATE" --verify=no check-new)
}
@@ -135,6 +151,7 @@ verify_version_current() {
}
for sector_size in "${SECTOR_SIZES[@]}"; do
for update_type in monolithic split-offline split; do
# Disk size of:
# - 1MB for GPT
# - 4 partitions of 2048 sectors each
@@ -267,18 +284,18 @@ EOF
# Install initial version and verify
new_version "$sector_size" v1
update_now
update_now "$update_type"
verify_version_current "$blockdev" "$sector_size" v1 1
# Create second version, update and verify that it is added
new_version "$sector_size" v2
update_now
update_now "$update_type"
verify_version "$blockdev" "$sector_size" v1 1
verify_version_current "$blockdev" "$sector_size" v2 2
# Create third version, update and verify it replaced the first version
new_version "$sector_size" v3
update_now
update_now "$update_type"
verify_version_current "$blockdev" "$sector_size" v3 1
verify_version "$blockdev" "$sector_size" v2 2
test ! -f "$WORKDIR/xbootldr/EFI/Linux/uki_v1+3-0.efi"
@@ -295,16 +312,17 @@ EOF
# Create a fifth version, that's complete on the server side. We should
# completely skip the incomplete v4 and install v5 instead.
new_version "$sector_size" v5
update_now
update_now "$update_type"
verify_version "$blockdev" "$sector_size" v3 1
verify_version_current "$blockdev" "$sector_size" v5 2
# Make the local installation of v5 incomplete by deleting a file, then make
# sure that sysupdate still recognizes the installation and can complete it
# in place
# Always do this as a monolithic update for the repair to work.
rm -r "$WORKDIR/xbootldr/EFI/Linux/uki_v5.efi.extra.d"
"$SYSUPDATE" --offline list v5 | grep "incomplete" >/dev/null
update_now
update_now "monolithic"
"$SYSUPDATE" --offline list v5 | grep -v "incomplete" >/dev/null
verify_version "$blockdev" "$sector_size" v3 1
verify_version_current "$blockdev" "$sector_size" v5 2
@@ -316,7 +334,7 @@ EOF
mkdir "$CONFIGDIR/optional.feature.d"
echo -e "[Feature]\nEnabled=true" > "$CONFIGDIR/optional.feature.d/enable.conf"
"$SYSUPDATE" --offline list v5 | grep "incomplete" >/dev/null
update_now
update_now "monolithic"
"$SYSUPDATE" --offline list v5 | grep -v "incomplete" >/dev/null
verify_version "$blockdev" "$sector_size" v3 1
verify_version_current "$blockdev" "$sector_size" v5 2
@@ -340,7 +358,7 @@ EOF
updatectl update
else
# If no updatectl, gracefully fall back to systemd-sysupdate
update_now
update_now "$update_type"
fi
# User-facing updatectl returns 0 if there's no updates, so use the low-level
# utility to make sure we did upgrade
@@ -397,7 +415,7 @@ MatchPattern=dir-@v
InstancesMax=3
EOF
update_now
update_now "$update_type"
verify_version "$blockdev" "$sector_size" v6 1
verify_version_current "$blockdev" "$sector_size" v7 2
@@ -420,7 +438,7 @@ EOF
# (what .transfer files were called before v257)
for i in "$CONFIGDIR/"*.conf; do echo mv "$i" "${i%.conf}.transfer"; done
new_version "$sector_size" v8
update_now
update_now "$update_type"
verify_version_current "$blockdev" "$sector_size" v8 1
verify_version "$blockdev" "$sector_size" v7 2
@@ -428,5 +446,6 @@ EOF
[[ -b "$blockdev" ]] && losetup --detach "$blockdev"
rm "$BACKING_FILE"
done
done
touch /testok