Files
systemd/src/sysupdate/sysupdate.c
T
Daan De Meyer c1852f9d5f sysupdate: Add --transfer-source=
In mkosi, I want to add a sysupdate verb to wrap systemd-sysupdate.
The definitions will be picked up from mkosi.sysupdate/ and passed
to systemd-sysupdate. I want users to be able to write transfer
definitions that are independent of the output directory used by
mkosi. To make this possible, it should be possible to specify the
directory that transfer sources should be looked up in on the sysupdate
command line. Let's allow this via a new --transfer-source= option.

Additionally, transfer sources that want to take advantage of this
feature should specify PathRelativeTo=directory to indicate the configured
Path= is interpreted relative to the tranfer source directory specified
on the CLI.

This allows for the following transfer definition to be put in
mkosi.sysupdate:

"""
[Transfer]
ProtectVersion=%A

[Source]
Type=regular-file
Path=/
PathRelativeTo=directory
MatchPattern=ParticleOS_@v.usr-%a.@u.raw

[Target]
Type=partition
Path=auto
MatchPattern=ParticleOS_@v
MatchPartitionType=usr
PartitionFlags=0
ReadOnly=1
"""
2024-09-09 16:41:52 +02:00

1656 lines
66 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <getopt.h>
#include <unistd.h>
#include "build.h"
#include "chase.h"
#include "conf-files.h"
#include "constants.h"
#include "dirent-util.h"
#include "dissect-image.h"
#include "fd-util.h"
#include "format-table.h"
#include "glyph-util.h"
#include "hexdecoct.h"
#include "json-util.h"
#include "main-func.h"
#include "mount-util.h"
#include "os-util.h"
#include "pager.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "path-util.h"
#include "pretty-print.h"
#include "set.h"
#include "signal-util.h"
#include "sort-util.h"
#include "string-util.h"
#include "strv.h"
#include "sysupdate.h"
#include "sysupdate-transfer.h"
#include "sysupdate-update-set.h"
#include "sysupdate-util.h"
#include "terminal-util.h"
#include "utf8.h"
#include "verbs.h"
static char *arg_definitions = NULL;
bool arg_sync = true;
uint64_t arg_instances_max = UINT64_MAX;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
char *arg_root = NULL;
static char *arg_image = NULL;
static bool arg_reboot = false;
static char *arg_component = NULL;
static int arg_verify = -1;
static ImagePolicy *arg_image_policy = NULL;
static bool arg_offline = false;
char *arg_transfer_source = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
typedef struct Context {
Transfer **transfers;
size_t n_transfers;
UpdateSet **update_sets;
size_t n_update_sets;
UpdateSet *newest_installed, *candidate;
Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */
} Context;
static Context* context_free(Context *c) {
if (!c)
return NULL;
FOREACH_ARRAY(tr, c->transfers, c->n_transfers)
transfer_free(*tr);
free(c->transfers);
FOREACH_ARRAY(us, c->update_sets, c->n_update_sets)
update_set_free(*us);
free(c->update_sets);
hashmap_free(c->web_cache);
return mfree(c);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free);
static Context* context_new(void) {
/* For now, no fields to initialize non-zero */
return new0(Context, 1);
}
static int context_read_definitions(
Context *c,
const char *directory,
const char *component,
const char *root,
const char *node) {
_cleanup_strv_free_ char **files = NULL;
int r;
assert(c);
if (directory)
r = conf_files_list_strv(&files, ".conf", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) STRV_MAKE(directory));
else if (component) {
_cleanup_strv_free_ char **n = NULL;
char **l = CONF_PATHS_STRV("");
size_t k = 0;
n = new0(char*, strv_length(l) + 1);
if (!n)
return log_oom();
STRV_FOREACH(i, l) {
char *j;
j = strjoin(*i, "sysupdate.", component, ".d");
if (!j)
return log_oom();
n[k++] = j;
}
r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) n);
} else
r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) CONF_PATHS_STRV("sysupdate.d"));
if (r < 0)
return log_error_errno(r, "Failed to enumerate *.conf files: %m");
STRV_FOREACH(f, files) {
_cleanup_(transfer_freep) Transfer *t = NULL;
if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1))
return log_oom();
t = transfer_new(c);
if (!t)
return log_oom();
t->definition_path = strdup(*f);
if (!t->definition_path)
return log_oom();
r = transfer_read_definition(t, *f);
if (r < 0)
return r;
c->transfers[c->n_transfers++] = TAKE_PTR(t);
}
if (c->n_transfers == 0) {
if (arg_component)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
"No transfer definitions for component '%s' found.", arg_component);
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
"No transfer definitions found.");
}
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Transfer *t = *tr;
r = transfer_resolve_paths(t, root, node);
if (r < 0)
return r;
}
return 0;
}
static int context_load_installed_instances(Context *c) {
int r;
assert(c);
log_info("Discovering installed instances%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Transfer *t = *tr;
r = resource_load_instances(
&t->target,
arg_verify >= 0 ? arg_verify : t->verify,
&c->web_cache);
if (r < 0)
return r;
}
return 0;
}
static int context_load_available_instances(Context *c) {
int r;
assert(c);
log_info("Discovering available instances%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Transfer *t = *tr;
assert(t);
r = resource_load_instances(
&t->source,
arg_verify >= 0 ? arg_verify : t->verify,
&c->web_cache);
if (r < 0)
return r;
}
return 0;
}
static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
_cleanup_free_ char *boundary = NULL;
bool newest_found = false;
int r;
assert(c);
assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
for (;;) {
_cleanup_free_ Instance **cursor_instances = NULL;
bool skip = false;
UpdateSetFlags extra_flags = 0;
_cleanup_free_ char *cursor = NULL;
UpdateSet *us = NULL;
/* First, let's find the newest version that's older than the boundary. */
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Resource *rr;
assert(*tr);
if (flags == UPDATE_AVAILABLE)
rr = &(*tr)->source;
else {
assert(flags == UPDATE_INSTALLED);
rr = &(*tr)->target;
}
FOREACH_ARRAY(inst, rr->instances, rr->n_instances) {
Instance *i = *inst; /* Sorted newest-to-oldest */
assert(i);
if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
continue; /* Not older than the boundary */
if (cursor && strverscmp(i->metadata.version, cursor) <= 0)
break; /* Not newer than the cursor. The same will be true for all
* subsequent instances (due to sorting) so let's skip to the
* next transfer. */
if (free_and_strdup(&cursor, i->metadata.version) < 0)
return log_oom();
break; /* All subsequent instances will be older than this one */
}
if (flags == UPDATE_AVAILABLE && !cursor)
break; /* This transfer didn't have a version older than the boundary,
* so any older-than-boundary version that might exist in a different
* transfer must always be incomplete. For reasons described below,
* we don't include incomplete versions for AVAILABLE updates. So we
* are completely done looking. */
}
if (!cursor) /* We didn't find anything older than the boundary, so we're done. */
break;
cursor_instances = new0(Instance*, c->n_transfers);
if (!cursor_instances)
return log_oom();
/* Now let's find all the instances that match the version of the cursor, if we have them */
for (size_t k = 0; k < c->n_transfers; k++) {
Transfer *t = c->transfers[k];
Instance *match = NULL;
assert(t);
if (flags == UPDATE_AVAILABLE) {
match = resource_find_instance(&t->source, cursor);
if (!match) {
/* When we're looking for updates to download, we don't offer
* incomplete versions at all. The server wants to send us an update
* with parts of the OS missing. For robustness sake, let's not do
* that. */
skip = true;
break;
}
} else {
assert(flags == UPDATE_INSTALLED);
match = resource_find_instance(&t->target, cursor);
if (!match) {
/* 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.
* See https://github.com/systemd/systemd/issues/33339 */
extra_flags |= UPDATE_INCOMPLETE;
}
}
cursor_instances[k] = match;
if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
extra_flags |= UPDATE_OBSOLETE;
if (strv_contains(t->protected_versions, cursor))
extra_flags |= UPDATE_PROTECTED;
}
r = free_and_strdup_warn(&boundary, cursor);
if (r < 0)
return r;
if (skip)
continue;
/* See if we already have this update set in our table */
FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
UpdateSet *u = *update_set;
if (strverscmp_improved(u->version, cursor) != 0)
continue;
/* Merge in what we've learned and continue onto the next version */
if (FLAGS_SET(u->flags, UPDATE_INCOMPLETE)) {
assert(u->n_instances == c->n_transfers);
/* Incomplete updates will have picked NULL instances for the transfers that
* are missing. Now we have more information, so let's try to fill them in. */
for (size_t j = 0; j < u->n_instances; j++) {
if (!u->instances[j])
u->instances[j] = cursor_instances[j];
/* Make sure that the list is full if the update is AVAILABLE */
assert(flags != UPDATE_AVAILABLE || u->instances[j]);
}
}
u->flags |= flags | extra_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. */
if (FLAGS_SET(u->flags, UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_INCOMPLETE|UPDATE_AVAILABLE) &&
!c->candidate && !FLAGS_SET(u->flags, UPDATE_OBSOLETE))
c->candidate = u;
skip = true;
newest_found = true;
break;
}
if (skip)
continue;
/* Doesn't exist yet, let's add it */
if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
return log_oom();
us = new(UpdateSet, 1);
if (!us)
return log_oom();
*us = (UpdateSet) {
.flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
.version = TAKE_PTR(cursor),
.instances = TAKE_PTR(cursor_instances),
.n_instances = c->n_transfers,
};
c->update_sets[c->n_update_sets++] = us;
newest_found = true;
/* Remember which one is the newest installed */
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" */
if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
c->candidate = us;
}
/* Newest installed is newer than or equal to candidate? Then suppress the candidate */
if (c->newest_installed && !FLAGS_SET(c->newest_installed->flags, UPDATE_INCOMPLETE) &&
c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
c->candidate = NULL;
return 0;
}
static int context_discover_update_sets(Context *c) {
int r;
assert(c);
log_info("Determining installed update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
if (r < 0)
return r;
if (!arg_offline) {
log_info("Determining available update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
if (r < 0)
return r;
}
typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
return 0;
}
static int context_show_table(Context *c) {
_cleanup_(table_unrefp) Table *t = NULL;
int r;
assert(c);
t = table_new("", "version", "installed", "available", "assessment");
if (!t)
return log_oom();
(void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
(void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
(void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets) {
UpdateSet *us = *update_set;
const char *color;
color = update_set_flags_to_color(us->flags);
r = table_add_many(t,
TABLE_STRING, update_set_flags_to_glyph(us->flags),
TABLE_SET_COLOR, color,
TABLE_STRING, us->version,
TABLE_SET_COLOR, color,
TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)),
TABLE_SET_COLOR, color,
TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
TABLE_SET_COLOR, color,
TABLE_STRING, update_set_flags_to_string(us->flags),
TABLE_SET_COLOR, color);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
}
static UpdateSet* context_update_set_by_version(Context *c, const char *version) {
assert(c);
assert(version);
FOREACH_ARRAY(update_set, c->update_sets, c->n_update_sets)
if (streq((*update_set)->version, version))
return *update_set;
return NULL;
}
static int context_show_version(Context *c, const char *version) {
bool show_fs_columns = false, show_partition_columns = false,
have_fs_attributes = false, have_partition_attributes = false,
have_size = false, have_tries = false, have_no_auto = false,
have_read_only = false, have_growfs = false, have_sha256 = false;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_(table_unrefp) Table *t = NULL;
_cleanup_strv_free_ char **changelog_urls = NULL;
UpdateSet *us;
int r;
assert(c);
assert(version);
us = context_update_set_by_version(c, version);
if (!us)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
if (arg_json_format_flags & (SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_PRETTY_AUTO))
(void) pager_open(arg_pager_flags);
if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF))
printf("%s%s%s Version: %s\n"
" State: %s%s%s\n"
"Installed: %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)" : "",
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());
t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
if (!t)
return log_oom();
(void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
(void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
(void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
(void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
(void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
(void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
/* Starting in v257, these fields would be automatically formatted with underscores. This would have
* been a breaking change, so to avoid that let's hard-code their original names. */
(void) table_set_json_field_name(t, 7, "tries-done");
(void) table_set_json_field_name(t, 8, "tries-left");
/* Determine if the target will make use of partition/fs attributes for any of the transfers */
FOREACH_ARRAY(transfer, c->transfers, c->n_transfers) {
Transfer *tr = *transfer;
if (tr->target.type == RESOURCE_PARTITION)
show_partition_columns = true;
if (RESOURCE_IS_FILESYSTEM(tr->target.type))
show_fs_columns = true;
STRV_FOREACH(changelog, tr->changelog) {
assert(*changelog);
_cleanup_free_ char *changelog_url = strreplace(*changelog, "@v", version);
if (!changelog_url)
return log_oom();
/* Avoid duplicates */
if (strv_contains(changelog_urls, changelog_url))
continue;
/* changelog_urls takes ownership of expanded changelog_url */
r = strv_consume(&changelog_urls, TAKE_PTR(changelog_url));
if (r < 0)
return log_oom();
}
}
FOREACH_ARRAY(inst, us->instances, us->n_instances) {
Instance *i = *inst;
if (!i) {
assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE));
continue;
}
r = table_add_many(t,
TABLE_STRING, resource_type_to_string(i->resource->type),
TABLE_PATH, i->path);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.partition_uuid_set) {
have_partition_attributes = true;
r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.partition_flags_set) {
have_partition_attributes = true;
r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.mtime != USEC_INFINITY) {
have_fs_attributes = true;
r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.mode != MODE_INVALID) {
have_fs_attributes = true;
r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.size != UINT64_MAX) {
have_size = true;
r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.tries_done != UINT64_MAX) {
have_tries = true;
r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.tries_left != UINT64_MAX) {
have_tries = true;
r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.no_auto >= 0) {
bool b;
have_no_auto = true;
b = i->metadata.no_auto;
r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.read_only >= 0) {
bool b;
have_read_only = true;
b = i->metadata.read_only;
r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.growfs >= 0) {
bool b;
have_growfs = true;
b = i->metadata.growfs;
r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
if (i->metadata.sha256sum_set) {
_cleanup_free_ char *formatted = NULL;
have_sha256 = true;
formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
if (!formatted)
return log_oom();
r = table_add_cell(t, NULL, TABLE_STRING, formatted);
} else
r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
if (r < 0)
return table_log_add_error(r);
}
/* Hide the fs/partition columns if we don't have any data to show there */
if (!have_fs_attributes)
show_fs_columns = false;
if (!have_partition_attributes)
show_partition_columns = false;
if (!show_partition_columns)
(void) table_hide_column_from_display(t, 2, 3);
if (!show_fs_columns)
(void) table_hide_column_from_display(t, 4, 5);
if (!have_size)
(void) table_hide_column_from_display(t, 6);
if (!have_tries)
(void) table_hide_column_from_display(t, 7, 8);
if (!have_no_auto)
(void) table_hide_column_from_display(t, 9);
if (!have_read_only)
(void) table_hide_column_from_display(t, 10);
if (!have_growfs)
(void) table_hide_column_from_display(t, 11);
if (!have_sha256)
(void) table_hide_column_from_display(t, 12);
if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
printf("%s%s%s Version: %s\n"
" State: %s%s%s\n"
"Installed: %s%s%s%s%s\n"
"Available: %s%s\n"
"Protected: %s%s%s\n"
" Obsolete: %s%s%s\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_INCOMPLETE) ? ansi_highlight_yellow() : "", FLAGS_SET(us->flags, UPDATE_INCOMPLETE) ? " (incomplete)" : "", ansi_normal(),
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());
STRV_FOREACH(url, changelog_urls) {
_cleanup_free_ char *changelog_link = NULL;
r = terminal_urlify(*url, NULL, &changelog_link);
if (r < 0)
return log_oom();
printf("ChangeLog: %s\n", changelog_link);
}
printf("\n");
return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
} else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *t_json = NULL;
r = table_to_json(t, &t_json);
if (r < 0)
return log_error_errno(r, "failed to convert table to JSON: %m");
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("version", us->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("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)),
SD_JSON_BUILD_PAIR_STRV("changelog_urls", changelog_urls),
SD_JSON_BUILD_PAIR_VARIANT("contents", t_json));
if (r < 0)
return log_error_errno(r, "Failed to create JSON: %m");
r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
if (r < 0)
return log_error_errno(r, "Failed to print JSON: %m");
return 0;
}
}
static int context_vacuum(
Context *c,
uint64_t space,
const char *extra_protected_version) {
int r, count = 0;
assert(c);
if (space == 0)
log_info("Making room%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
else
log_info("Making room for %" PRIu64 " updates%s", space, special_glyph(SPECIAL_GLYPH_ELLIPSIS));
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Transfer *t = *tr;
/* Don't bother clearing out space if we're not going to be downloading anything */
if (extra_protected_version && resource_find_instance(&t->target, extra_protected_version))
continue;
r = transfer_vacuum(t, space, extra_protected_version);
if (r < 0)
return r;
count = MAX(count, r);
}
if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (count > 0)
log_info("Removed %i instances.", count);
else
log_info("Removed no instances.");
} else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_INTEGER("removed", count));
if (r < 0)
return log_error_errno(r, "Failed to create JSON: %m");
r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
if (r < 0)
return log_error_errno(r, "Failed to print JSON: %m");
}
return 0;
}
static int context_make_offline(Context **ret, const char *node) {
_cleanup_(context_freep) Context* context = NULL;
int r;
assert(ret);
/* Allocates a context object and initializes everything we can initialize offline, i.e. without
* checking on the update source (i.e. the Internet) what versions are available */
context = context_new();
if (!context)
return log_oom();
r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node);
if (r < 0)
return r;
r = context_load_installed_instances(context);
if (r < 0)
return r;
*ret = TAKE_PTR(context);
return 0;
}
static int context_make_online(Context **ret, const char *node) {
_cleanup_(context_freep) Context* context = NULL;
int r;
assert(ret);
/* Like context_make_offline(), but also communicates with the update source looking for new
* versions (as long as --offline is not specified on the command line). */
r = context_make_offline(&context, node);
if (r < 0)
return r;
if (!arg_offline) {
r = context_load_available_instances(context);
if (r < 0)
return r;
}
r = context_discover_update_sets(context);
if (r < 0)
return r;
*ret = TAKE_PTR(context);
return 0;
}
static int context_on_acquire_progress(const Transfer *t, const Instance *inst, unsigned percentage) {
const Context *c = ASSERT_PTR(t->context);
size_t i, n = c->n_transfers;
uint64_t base, scaled;
unsigned overall;
for (i = 0; i < n; i++)
if (c->transfers[i] == t)
break;
assert(i < n); /* We should have found the index */
base = (100 * 100 * i) / n;
scaled = (100 * percentage) / n;
overall = (unsigned) ((base + scaled) / 100);
assert(overall <= 100);
log_debug("Transfer %zu/%zu is %u%% complete (%u%% overall).", i+1, n, percentage, overall);
return sd_notifyf(/* unset= */ false, "X_SYSUPDATE_PROGRESS=%u\n"
"X_SYSUPDATE_TRANSFERS_LEFT=%zu\n"
"X_SYSUPDATE_TRANSFERS_DONE=%zu\n"
"STATUS=Updating to '%s' (%u%% complete).",
overall, n - i, i, inst->metadata.version, overall);
}
static int context_apply(
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.");
if (ret_applied)
*ret_applied = NULL;
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 (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
log_info("Selected update '%s' is already installed. Skipping update.", us->version);
if (ret_applied)
*ret_applied = NULL;
return 0;
}
if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, 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);
(void) sd_notifyf(/* unset= */ false,
"READY=1\n"
"X_SYSUPDATE_VERSION=%s\n"
"STATUS=Making room for '%s'.", us->version, us->version);
/* Let's make some room. We make sure for each transfer we have one free space to fill. While
* removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
* download succeeded already, in which case we shouldn't remove it just to acquire it again */
r = context_vacuum(
c,
/* space = */ 1,
/* extra_protected_version = */ us->version);
if (r < 0)
return r;
if (arg_sync)
sync();
(void) sd_notifyf(/* unset= */ false,
"STATUS=Updating to '%s'.", 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); /* ditto */
if (inst->resource == &t->target) { /* a present transfer in an incomplete installation */
assert(FLAGS_SET(us->flags, UPDATE_INCOMPLETE));
continue;
}
r = transfer_acquire_instance(t, inst, context_on_acquire_progress, c);
if (r < 0)
return r;
}
if (arg_sync)
sync();
(void) sd_notifyf(/* unset= */ 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)
continue;
r = transfer_install_instance(t, inst, arg_root);
if (r < 0)
return r;
}
log_info("%s Successfully installed update '%s'.", special_glyph(SPECIAL_GLYPH_SPARKLES), us->version);
if (ret_applied)
*ret_applied = us;
return 1;
}
static int process_image(
bool ro,
char **ret_mounted_dir,
LoopDevice **ret_loop_device) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_freep) char *mounted_dir = NULL;
int r;
assert(ret_mounted_dir);
assert(ret_loop_device);
if (!arg_image)
return 0;
assert(!arg_root);
r = mount_image_privately_interactively(
arg_image,
arg_image_policy,
(ro ? DISSECT_IMAGE_READ_ONLY : 0) |
DISSECT_IMAGE_FSCK |
DISSECT_IMAGE_MKDIR |
DISSECT_IMAGE_GROWFS |
DISSECT_IMAGE_RELAX_VAR_CHECK |
DISSECT_IMAGE_USR_NO_ROOT |
DISSECT_IMAGE_GENERIC_ROOT |
DISSECT_IMAGE_REQUIRE_ROOT |
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
&mounted_dir,
/* ret_dir_fd= */ NULL,
&loop_device);
if (r < 0)
return r;
arg_root = strdup(mounted_dir);
if (!arg_root)
return log_oom();
*ret_mounted_dir = TAKE_PTR(mounted_dir);
*ret_loop_device = TAKE_PTR(loop_device);
return 0;
}
static int verb_list(int argc, char **argv, void *userdata) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_(context_freep) Context* context = NULL;
_cleanup_strv_free_ char **appstream_urls = NULL;
const char *version;
int r;
assert(argc <= 2);
version = argc >= 2 ? argv[1] : NULL;
r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
if (r < 0)
return r;
r = context_make_online(&context, loop_device ? loop_device->node : NULL);
if (r < 0)
return r;
if (version)
return context_show_version(context, version);
else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF))
return context_show_table(context);
else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **versions = NULL;
const char *current = NULL;
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))
current = us->version;
r = strv_extend(&versions, us->version);
if (r < 0)
return log_oom();
}
FOREACH_ARRAY(tr, context->transfers, context->n_transfers)
STRV_FOREACH(appstream_url, (*tr)->appstream) {
/* Avoid duplicates */
if (strv_contains(appstream_urls, *appstream_url))
continue;
r = strv_extend(&appstream_urls, *appstream_url);
if (r < 0)
return log_oom();
}
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("current", current),
SD_JSON_BUILD_PAIR_STRV("all", versions),
SD_JSON_BUILD_PAIR_STRV("appstream_urls", appstream_urls));
if (r < 0)
return log_error_errno(r, "Failed to create JSON: %m");
r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
if (r < 0)
return log_error_errno(r, "Failed to print JSON: %m");
return 0;
}
}
static int verb_check_new(int argc, char **argv, void *userdata) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_(context_freep) Context* context = NULL;
int r;
assert(argc <= 1);
r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
if (r < 0)
return r;
r = context_make_online(&context, loop_device ? loop_device->node : NULL);
if (r < 0)
return r;
if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (!context->candidate) {
log_debug("No candidate found.");
return EXIT_FAILURE;
}
puts(context->candidate->version);
} else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
if (context->candidate)
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRING("available", context->candidate->version));
else
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_NULL("available"));
if (r < 0)
return log_error_errno(r, "Failed to create JSON: %m");
r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
if (r < 0)
return log_error_errno(r, "Failed to print JSON: %m");
}
return EXIT_SUCCESS;
}
static int verb_vacuum(int argc, char **argv, void *userdata) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_(context_freep) Context* context = NULL;
int r;
assert(argc <= 1);
if (arg_instances_max < 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"The --instances-max argument must be >= 1 while vacuuming");
r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
if (r < 0)
return r;
r = context_make_offline(&context, loop_device ? loop_device->node : NULL);
if (r < 0)
return r;
return context_vacuum(context, 0, NULL);
}
static int verb_update(int argc, char **argv, void *userdata) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_(context_freep) Context* context = NULL;
_cleanup_free_ char *booted_version = NULL;
UpdateSet *applied = NULL;
const char *version;
int r;
assert(argc <= 2);
version = argc >= 2 ? argv[1] : NULL;
if (arg_instances_max < 2)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"The --instances-max argument must be >= 2 while updating");
if (arg_reboot) {
/* If automatic reboot on completion is requested, let's first determine the currently booted image */
r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
if (r < 0)
return log_error_errno(r, "Failed to parse /etc/os-release: %m");
if (!booted_version)
return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
}
r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
if (r < 0)
return r;
r = context_make_online(&context, loop_device ? loop_device->node : NULL);
if (r < 0)
return r;
r = context_apply(context, version, &applied);
if (r < 0)
return r;
if (r > 0 && arg_reboot) {
assert(applied);
assert(booted_version);
if (strverscmp_improved(applied->version, booted_version) > 0) {
log_notice("Newly installed version is newer than booted version, rebooting.");
return reboot_now();
}
if (strverscmp_improved(applied->version, booted_version) == 0 &&
FLAGS_SET(applied->flags, UPDATE_INCOMPLETE)) {
log_notice("Currently booted version was incomplete and has been repaired, rebooting.");
return reboot_now();
}
log_info("Booted version is newer or identical to newly installed version, not rebooting.");
}
return 0;
}
static int verb_pending_or_reboot(int argc, char **argv, void *userdata) {
_cleanup_(context_freep) Context* context = NULL;
_cleanup_free_ char *booted_version = NULL;
int r;
assert(argc == 1);
if (arg_image || arg_root)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]);
r = context_make_offline(&context, NULL);
if (r < 0)
return r;
log_info("Determining installed update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED);
if (r < 0)
return r;
if (!context->newest_installed)
return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
if (r < 0) /* yes, arg_root is NULL here, but we have to pass something, and it's a lot more readable
* if we see what the first argument is about */
return log_error_errno(r, "Failed to parse /etc/os-release: %m");
if (!booted_version)
return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field.");
r = strverscmp_improved(context->newest_installed->version, booted_version);
if (r > 0) {
log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
context->newest_installed->version, booted_version,
streq(argv[0], "pending") ? " Reboot recommended." : "");
if (streq(argv[0], "reboot"))
return reboot_now();
return EXIT_SUCCESS;
} else if (r == 0)
log_info("Newest installed version '%s' matches booted version '%s'.",
context->newest_installed->version, booted_version);
else
log_warning("Newest installed version '%s' is older than booted version '%s'.",
context->newest_installed->version, booted_version);
if (streq(argv[0], "pending")) /* When called as 'pending' tell the caller via failure exit code that there's nothing newer installed */
return EXIT_FAILURE;
return EXIT_SUCCESS;
}
static int component_name_valid(const char *c) {
_cleanup_free_ char *j = NULL;
/* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */
if (isempty(c))
return false;
if (string_has_cc(c, NULL))
return false;
if (!utf8_is_valid(c))
return false;
j = strjoin("sysupdate.", c, ".d");
if (!j)
return -ENOMEM;
return filename_is_valid(j);
}
static int verb_components(int argc, char **argv, void *userdata) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_set_free_ Set *names = NULL;
_cleanup_free_ char **z = NULL; /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */
char **l = CONF_PATHS_STRV("");
bool has_default_component = false;
int r;
assert(argc <= 1);
r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
if (r < 0)
return r;
STRV_FOREACH(i, l) {
_cleanup_closedir_ DIR *d = NULL;
_cleanup_free_ char *p = NULL;
r = chase_and_opendir(*i, arg_root, CHASE_PREFIX_ROOT, &p, &d);
if (r == -ENOENT)
continue;
if (r < 0)
return log_error_errno(r, "Failed to open directory '%s': %m", *i);
for (;;) {
_cleanup_free_ char *n = NULL;
struct dirent *de;
const char *e, *a;
de = readdir_ensure_type(d);
if (!de) {
if (errno != 0)
return log_error_errno(errno, "Failed to enumerate directory '%s': %m", p);
break;
}
if (de->d_type != DT_DIR)
continue;
if (dot_or_dot_dot(de->d_name))
continue;
if (streq(de->d_name, "sysupdate.d")) {
has_default_component = true;
continue;
}
e = startswith(de->d_name, "sysupdate.");
if (!e)
continue;
a = endswith(e, ".d");
if (!a)
continue;
n = strndup(e, a - e);
if (!n)
return log_oom();
r = component_name_valid(n);
if (r < 0)
return log_error_errno(r, "Unable to validate component name: %m");
if (r == 0)
continue;
r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n));
if (r < 0 && r != -EEXIST)
return log_error_errno(r, "Failed to add component to set: %m");
}
}
z = set_get_strv(names);
if (!z)
return log_oom();
strv_sort(z);
if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (!has_default_component && set_isempty(names)) {
log_info("No components defined.");
return 0;
}
if (has_default_component)
printf("%s<default>%s\n",
ansi_highlight(), ansi_normal());
STRV_FOREACH(i, z)
puts(*i);
} else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_component),
SD_JSON_BUILD_PAIR_STRV("components", z));
if (r < 0)
return log_error_errno(r, "Failed to create JSON: %m");
r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
if (r < 0)
return log_error_errno(r, "Failed to print JSON: %m");
}
return 0;
}
static int verb_help(int argc, char **argv, void *userdata) {
_cleanup_free_ char *link = NULL;
int r;
r = terminal_urlify_man("systemd-sysupdate", "8", &link);
if (r < 0)
return log_oom();
printf("%1$s [OPTIONS...] [VERSION]\n"
"\n%5$sUpdate OS images.%6$s\n"
"\n%3$sCommands:%4$s\n"
" list [VERSION] Show installed and available versions\n"
" check-new Check if there's a new version available\n"
" update [VERSION] Install 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"
" reboot Reboot if a newer version is installed than booted\n"
" components Show list of components\n"
" -h --help Show this help\n"
" --version Show package version\n"
"\n%3$sOptions:%4$s\n"
" -C --component=NAME Select component to update\n"
" --definitions=DIR Find transfer definitions in specified directory\n"
" --root=PATH Operate on an alternate filesystem root\n"
" --image=PATH Operate on disk image as filesystem root\n"
" --image-policy=POLICY\n"
" Specify disk image dissection policy\n"
" -m --instances-max=INT How many instances to maintain\n"
" --sync=BOOL Controls whether to sync data to disk\n"
" --verify=BOOL Force signature verification on or off\n"
" --reboot Reboot after updating to newer version\n"
" --offline Do not fetch metadata from the network\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Do not show the headers and footers\n"
" --json=pretty|short|off\n"
" Generate JSON output\n"
" --transfer-source=PATH\n"
" Specify the directory to transfer sources from\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
ansi_underline(),
ansi_normal(),
ansi_highlight(),
ansi_normal());
return 0;
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
ARG_NO_PAGER,
ARG_NO_LEGEND,
ARG_SYNC,
ARG_DEFINITIONS,
ARG_JSON,
ARG_ROOT,
ARG_IMAGE,
ARG_IMAGE_POLICY,
ARG_REBOOT,
ARG_VERIFY,
ARG_OFFLINE,
ARG_TRANSFER_SOURCE,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
{ "no-legend", no_argument, NULL, ARG_NO_LEGEND },
{ "definitions", required_argument, NULL, ARG_DEFINITIONS },
{ "instances-max", required_argument, NULL, 'm' },
{ "sync", required_argument, NULL, ARG_SYNC },
{ "json", required_argument, NULL, ARG_JSON },
{ "root", required_argument, NULL, ARG_ROOT },
{ "image", required_argument, NULL, ARG_IMAGE },
{ "image-policy", required_argument, NULL, ARG_IMAGE_POLICY },
{ "reboot", no_argument, NULL, ARG_REBOOT },
{ "component", required_argument, NULL, 'C' },
{ "verify", required_argument, NULL, ARG_VERIFY },
{ "offline", no_argument, NULL, ARG_OFFLINE },
{ "transfer-source", required_argument, NULL, ARG_TRANSFER_SOURCE },
{}
};
int c, r;
assert(argc >= 0);
assert(argv);
while ((c = getopt_long(argc, argv, "hm:C:", options, NULL)) >= 0) {
switch (c) {
case 'h':
return verb_help(0, NULL, NULL);
case ARG_VERSION:
return version();
case ARG_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
case ARG_NO_LEGEND:
arg_legend = false;
break;
case 'm':
r = safe_atou64(optarg, &arg_instances_max);
if (r < 0)
return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", optarg);
break;
case ARG_SYNC:
r = parse_boolean_argument("--sync=", optarg, &arg_sync);
if (r < 0)
return r;
break;
case ARG_DEFINITIONS:
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_definitions);
if (r < 0)
return r;
break;
case ARG_JSON:
r = parse_json_argument(optarg, &arg_json_format_flags);
if (r <= 0)
return r;
break;
case ARG_ROOT:
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root);
if (r < 0)
return r;
break;
case ARG_IMAGE:
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
if (r < 0)
return r;
break;
case ARG_IMAGE_POLICY:
r = parse_image_policy_argument(optarg, &arg_image_policy);
if (r < 0)
return r;
break;
case ARG_REBOOT:
arg_reboot = true;
break;
case 'C':
if (isempty(optarg)) {
arg_component = mfree(arg_component);
break;
}
r = component_name_valid(optarg);
if (r < 0)
return log_error_errno(r, "Failed to determine if component name is valid: %m");
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", optarg);
r = free_and_strdup_warn(&arg_component, optarg);
if (r < 0)
return r;
break;
case ARG_VERIFY: {
bool b;
r = parse_boolean_argument("--verify=", optarg, &b);
if (r < 0)
return r;
arg_verify = b;
break;
}
case ARG_OFFLINE:
arg_offline = true;
break;
case ARG_TRANSFER_SOURCE:
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_transfer_source);
if (r < 0)
return r;
break;
case '?':
return -EINVAL;
default:
assert_not_reached();
}
}
if (arg_image && arg_root)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
if ((arg_image || arg_root) && arg_reboot)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
if (arg_definitions && arg_component)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined.");
return 1;
}
static int sysupdate_main(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "list", VERB_ANY, 2, VERB_DEFAULT, verb_list },
{ "components", VERB_ANY, 1, 0, verb_components },
{ "check-new", VERB_ANY, 1, 0, verb_check_new },
{ "update", VERB_ANY, 2, 0, verb_update },
{ "vacuum", VERB_ANY, 1, 0, verb_vacuum },
{ "reboot", 1, 1, 0, verb_pending_or_reboot },
{ "pending", 1, 1, 0, verb_pending_or_reboot },
{ "help", VERB_ANY, 1, 0, verb_help },
{}
};
return dispatch_verb(argc, argv, verbs, NULL);
}
static int run(int argc, char *argv[]) {
int r;
log_setup();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
/* SIGCHLD signal must be blocked for sd_event_add_child to work */
assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0);
return sysupdate_main(argc, argv);
}
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);