diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml
index 778ff2e793..1dea457e13 100644
--- a/man/systemd-sysupdate.xml
+++ b/man/systemd-sysupdate.xml
@@ -128,19 +128,33 @@
- VERSION
+ --offline VERSION
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.
+ If is specified, the update must already have been acquired using
+ acquire and, if so, this pre-acquired version is the one which will be updated
+ to.
+
+
+
+
+
+ VERSION
+
+ 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.
+
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 InstanceMax= in
sysupdate.d5, or
via the available partition slots of the right type. This implicit operation can also be invoked
explicitly via the vacuum command described below.
-
+
@@ -299,7 +313,7 @@
- When used in combination with the update command and a new version is
+ When used in combination with the update commands and a new version is
installed, automatically reboots the system immediately afterwards.
@@ -312,6 +326,9 @@
This is most useful when used in combination with the list command, to query
locally installed versions.
+ If used in combination with the update command, it allows updates to be
+ downloaded in advance (using acquire) and installed later.
+
diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h
index 42ba1f1ba9..05b42ac610 100644
--- a/src/sysupdate/sysupdate-instance.h
+++ b/src/sysupdate/sysupdate-instance.h
@@ -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);
diff --git a/src/sysupdate/sysupdate-partition.c b/src/sysupdate/sysupdate-partition.c
index cd85db5236..237c157831 100644
--- a/src/sysupdate/sysupdate-partition.c
+++ b/src/sysupdate/sysupdate-partition.c
@@ -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,
diff --git a/src/sysupdate/sysupdate-partition.h b/src/sysupdate/sysupdate-partition.h
index 21277962e4..fbda14de6f 100644
--- a/src/sysupdate/sysupdate-partition.h
+++ b/src/sysupdate/sysupdate-partition.h
@@ -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);
diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c
index b268841899..821854cda8 100644
--- a/src/sysupdate/sysupdate-resource.c
+++ b/src/sysupdate/sysupdate-resource.c
@@ -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;
}
}
diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c
index 51ae49f407..04b1fd09cd 100644
--- a/src/sysupdate/sysupdate-transfer.c
+++ b/src/sysupdate/sysupdate-transfer.c
@@ -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#` 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.` to `.sysupdate.pending.` 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#` 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,
diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h
index 1a6cba804f..f5976f0477 100644
--- a/src/sysupdate/sysupdate-transfer.h
+++ b/src/sysupdate/sysupdate-transfer.h
@@ -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);
diff --git a/src/sysupdate/sysupdate-update-set-flags.c b/src/sysupdate/sysupdate-update-set-flags.c
index 394e390791..36801938f6 100644
--- a/src/sysupdate/sysupdate-update-set-flags.c
+++ b/src/sysupdate/sysupdate-update-set-flags.c
@@ -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";
diff --git a/src/sysupdate/sysupdate-update-set-flags.h b/src/sysupdate/sysupdate-update-set-flags.h
index 27cfab8c7c..fd4e77e117 100644
--- a/src/sysupdate/sysupdate-update-set-flags.h
+++ b/src/sysupdate/sysupdate-update-set-flags.h
@@ -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);
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
index 05163754af..9af09b497d 100644
--- a/src/sysupdate/sysupdate.c
+++ b/src/sysupdate/sysupdate.c
@@ -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 },
diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh
index a3e0e834bb..f3b712a6e6 100755
--- a/test/units/TEST-72-SYSUPDATE.sh
+++ b/test/units/TEST-72-SYSUPDATE.sh
@@ -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