1
0
mirror of https://github.com/systemd/systemd synced 2026-03-13 00:24:48 +01:00

Compare commits

...

19 Commits

Author SHA1 Message Date
Christopher Head
8ae4aa26c1 Fix order of class/type in resolve man page
For each RR `ResolveRecord` returns, it returns the class before the
type, not after.
2026-02-10 13:40:13 +00:00
Luca Boccassi
6cf5581af8
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
2026-02-10 13:11:52 +00:00
Luca Boccassi
b6defb8f84
bootctl: install tweaks (#40622) 2026-02-10 13:04:48 +00:00
Kai Lüke
eefb46c83b repart: Discard only once
The indirect discard in mkfs.btrfs on the loop device mapped to the
region on disk can hang and fail the first-boot creation of the rootfs.
Since there already is a discard done we anyway don't need to do it
twice. This might help for most cases to avoid the failure in
mkfs.btrfs.
Keep track if the direct discard worked and then skip the mkfs.btrfs
discard if it did. This still leaves the case where mkfs.btrfs can hang
when the direct discard couldn't succeed and mkfs.btrfs tries again but
since the conditions are rather the same it might be that this case is
not easy to trigger. If the problem still shows up and the kernel won't
be fixed soon we can still disable the mkfs discard for at least btrfs.
2026-02-10 13:09:33 +01:00
Lennart Poettering
7081618cd9 update TODO 2026-02-10 12:39:23 +01:00
Lennart Poettering
51480b56c8 bootctl: rearrange if branches in vl_method_install() 2026-02-10 11:56:26 +01:00
Lennart Poettering
10268261a8 bootctl: toughen fd validation in Install() handler
Let's also check the fd flags. Just in case.
2026-02-10 11:56:26 +01:00
Lennart Poettering
7f204857e1 bootctl: fix varlink IDL for rootDirectory field 2026-02-10 11:56:26 +01:00
Philip Withnall
6015c6cd6f
test: Expand sysupdate test to cover split acquire/install updates
This essentially means the sysupdate tests are now run twice: once with
a monolithic update (`sysupdate update`) and once with a split update
(`sysupdate acquire; sysupdate install`).

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
2026-02-09 12:05:20 +00:00
Philip Withnall
e6e8efcffb
sysupdate: Add some more debug output
Signed-off-by: Philip Withnall <pwithnall@gnome.org>
2026-02-09 12:05:15 +00:00
Philip Withnall
3bc9208435
sysupdate: Add acquire and install verbs
These expose the two parts of ‘update’, so that update sets can be
acquired (downloaded) and installed (applied) in separate actions at
different times. For example, this could allow a load of update sets to
be acquired when online, and later applied when offline.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 12:05:10 +00:00
Philip Withnall
cf7cccf2f7
sysupdate: Split the update verb into two parts internally
An ‘acquire’ (download) part, and an ‘install’ (apply) part.

Following commits will expose these as separate verbs and D-Bus methods,
but this commit is the one which rearranges the internals.

If doing an ‘install’, a mirror version of the ‘acquire’ has to happen
first to make sure the transfer’s internal state is correct.

‘Acquire’ can require an internet connection, but ‘install’ will always
work with `--offline` specified.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 12:05:05 +00:00
Philip Withnall
594d0345fa
sysupdate: Add partial/pending flags to UpdateSet
This commit adds the flags and some basic formatting/printing of them.
Following commits will integrate them into the update/acquire/install
logic.

`UPDATE_PARTIAL` is set if any of the instances in the `UpdateSet` are
partial, i.e. have been partially downloaded.

`UPDATE_PENDING` is set if any of the instances in the `UpdateSet` are
pending, i.e. have been acquired (downloaded) but not yet installed.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 12:05:01 +00:00
Philip Withnall
ba9687adef
sysupdate: Factor out temporary path computation for transfers
This helper function will be reused in a following commit.

This introduces no functional changes.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 12:04:52 +00:00
Philip Withnall
adcc03a0c0
sysupdate: Vacuum partial/pending instances first
Modify the vacuum implementation to preferentially vacuum partial or
pending transfers first (unless protected) as they are meant to be
fairly transitory, and ones which are hanging around have probably been
forgotten about and/or are out of date.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 12:03:59 +00:00
Philip Withnall
48fff6a27a
sysupdate: Implement acquire and install steps for transfers
Instead of using a random temporary path for file transfers, use a
predictable one which indicates whether the transfer is partially
complete or pending installation. Similarly for partitions.

This is another step towards being able to split the ‘update’ step into
‘acquire’ and ‘install’.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 12:03:47 +00:00
Philip Withnall
edf7679faf
sysupdate: Factor out a vacuum helper function
This will be reused in an upcoming commit.

This commit introduces no functional changes.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 11:54:45 +00:00
Philip Withnall
fdc661116d
sysupdate: Allow instances to be partial or pending
If we allow target instances to be partial or pending, we can build on
top of this to allow updates to be split into two phases: ‘acquire’ (which
takes an available source instance and copies it (temporarily partial) to
a pending target instance; and ‘install’ (which takes a pending target
instance and installs it as an installed target instance).

This commit introduces a file/directory and partition prefix naming
scheme to identify partial and pending instances.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 11:54:40 +00:00
Philip Withnall
ec10efa1a1
sysupdate: Split context_apply() into acquire and install steps
This introduces no functional changes at the moment, but will be used in
upcoming commits.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/34814
2026-02-09 11:54:25 +00:00
16 changed files with 639 additions and 129 deletions

6
TODO
View File

@ -128,6 +128,12 @@ Features:
- metrics from pid1: suppress metrics form units that are inactive and have nothing to report
- how to plug facts into this? i.e. hostname, ssh keys, and so on
- switch to daan's suggested hierarchy?
- enforce naming rules: a backend can only report metrics with the prefix of its service name
- use that to optimize sorted report generation
- figure out report vs. metrics
- systemd-report: add prefix matching of metrics
- add "hint-suppress-zero" flag (which suppresses all metrics which are zero)
- add "hint-object" parameter (which only queries info about certain object)
* implement a varlink registry service, similar to the one of the reference
implementation, backed by /run/varlink/registry/. Then, also implement

View File

@ -292,7 +292,7 @@ node /org/freedesktop/resolve1 {
(no IDNA conversion is applied), followed by the 16-bit class and type fields (which may be
ANY). Finally, a <varname>flags</varname> field may be passed in to alter behaviour of the look-up (see
below). On completion, an array of RR items is returned. Each array entry consists of the network interface
index the RR was discovered on, the type and class field of the RR found, and a byte array of the raw
index the RR was discovered on, the class and type field of the RR found, and a byte array of the raw
RR discovered. The raw RR data starts with the RR's domain name, in the original casing, followed
by the RR type, class, TTL and RDATA, in the binary format documented in
<ulink url="https://www.ietf.org/rfc/rfc1035.txt">RFC 1035</ulink>. For RRs that support name

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>

View File

@ -2035,6 +2035,10 @@ int vl_method_install(
if (p.context.root_fd < 0)
return log_debug_errno(p.context.root_fd, "Failed to acquire root fd from Varlink: %m");
r = fd_verify_safe_flags_full(p.context.root_fd, O_DIRECTORY);
if (r < 0)
return sd_varlink_error_invalid_parameter_name(link, "rootFileDescriptor");
r = fd_verify_directory(p.context.root_fd);
if (r < 0)
return log_debug_errno(r, "Specified file descriptor does not refer to a directory: %m");
@ -2047,15 +2051,11 @@ int vl_method_install(
if (empty_or_root(p.context.root))
p.context.root = mfree(p.context.root);
}
}
if (p.context.root_fd < 0 && p.context.root) {
} else if (p.context.root) {
p.context.root_fd = open(p.context.root, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
if (p.context.root_fd < 0)
return log_debug_errno(errno, "Failed to open '%s': %m", p.context.root);
}
if (p.context.root_fd < 0)
} else
p.context.root_fd = XAT_FDROOT;
if (p.context.entry_token_type < 0)

View File

@ -422,6 +422,7 @@ typedef struct Partition {
bool dropped;
bool factory_reset;
bool discarded;
int32_t priority;
uint32_t weight, padding_weight;
@ -2748,7 +2749,7 @@ static bool partition_needs_populate(const Partition *p) {
static MakeFileSystemFlags partition_mkfs_flags(const Partition *p) {
MakeFileSystemFlags flags = 0;
if (arg_discard)
if (arg_discard && !p->discarded)
flags |= MKFS_DISCARD;
if (streq(p->format, "erofs") && !DEBUG_LOGGING && !isatty_safe(STDERR_FILENO))
@ -4668,6 +4669,7 @@ static int context_discard_partition(Context *context, Partition *p) {
return log_error_errno(r, "Failed to discard data for future partition %" PRIu64 ".", p->partno);
log_info("Successfully discarded data from future partition %" PRIu64 ".", p->partno);
p->discarded = true;
return 1;
}

View File

@ -128,7 +128,7 @@ static SD_VARLINK_DEFINE_METHOD(
SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors passed along with this message, pointing to file descriptor to root file system to operate on"),
SD_VARLINK_DEFINE_INPUT(rootFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Root directory to operate relative to. If both this and rootFileDescriptor is specified, this is purely informational. If only this is specified, it is what will be used."),
SD_VARLINK_DEFINE_INPUT(rootDirectory, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_DEFINE_INPUT(rootDirectory, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Selects how to identify boot entries"),
SD_VARLINK_DEFINE_INPUT_BY_TYPE(bootEntryTokenType, BootEntryTokenType, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("If true the boot loader will be registered in an EFI boot entry via EFI variables, otherwise this is omitted"),

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);

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,

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);

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
* users patterns still apply. But dont 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 users existing patterns
* match regardless of the instances 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;
}
}

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);
r = transfer_instance_vacuum(t, oldest);
if (r < 0)
return r;
t->target.n_empty++;
break;
}
default:
assert_not_reached();
}
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 its
* being acquired and after its been acquired (but before its moved to the final_path
* when its 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 its 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 isnt 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 dont 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,

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);

View File

@ -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";

View File

@ -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);

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, theyre
* 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 },

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
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