diff options
Diffstat (limited to '')
-rw-r--r-- | src/sysupdate/sysupdate.c | 1401 |
1 files changed, 1401 insertions, 0 deletions
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c new file mode 100644 index 0000000..7469f16 --- /dev/null +++ b/src/sysupdate/sysupdate.c @@ -0,0 +1,1401 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <unistd.h> + +#include "bus-error.h" +#include "bus-locator.h" +#include "chase-symlinks.h" +#include "conf-files.h" +#include "def.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 "login-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 "sort-util.h" +#include "string-util.h" +#include "strv.h" +#include "sysupdate-transfer.h" +#include "sysupdate-update-set.h" +#include "sysupdate.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 JsonFormatFlags arg_json_format_flags = 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_DESTRUCTOR_REGISTER(arg_definitions, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_component, 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; + + for (size_t i = 0; i < c->n_transfers; i++) + transfer_free(c->transfers[i]); + free(c->transfers); + + for (size_t i = 0; i < c->n_update_sets; i++) + update_set_free(c->update_sets[i]); + 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(); + 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."); + } + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_resolve_paths(c->transfers[i], 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)); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = resource_load_instances( + &c->transfers[i]->target, + arg_verify >= 0 ? arg_verify : c->transfers[i]->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)); + + for (size_t i = 0; i < c->n_transfers; i++) { + assert(c->transfers[i]); + + r = resource_load_instances( + &c->transfers[i]->source, + arg_verify >= 0 ? arg_verify : c->transfers[i]->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_ Instance **cursor_instances = NULL; + _cleanup_free_ char *boundary = NULL; + bool newest_found = false; + int r; + + assert(c); + assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED)); + + for (;;) { + bool incomplete = false, exists = false; + UpdateSetFlags extra_flags = 0; + _cleanup_free_ char *cursor = NULL; + UpdateSet *us = NULL; + + for (size_t k = 0; k < c->n_transfers; k++) { + Transfer *t = c->transfers[k]; + bool cursor_found = false; + Resource *rr; + + assert(t); + + if (flags == UPDATE_AVAILABLE) + rr = &t->source; + else { + assert(flags == UPDATE_INSTALLED); + rr = &t->target; + } + + for (size_t j = 0; j < rr->n_instances; j++) { + Instance *i = rr->instances[j]; + + assert(i); + + /* Is the instance we are looking at equal or newer than the boundary? If so, we + * already checked this version, and it wasn't complete, let's ignore it. */ + if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0) + continue; + + if (cursor) { + if (strverscmp_improved(i->metadata.version, cursor) != 0) + continue; + } else { + cursor = strdup(i->metadata.version); + if (!cursor) + return log_oom(); + } + + cursor_found = true; + + if (!cursor_instances) { + cursor_instances = new(Instance*, c->n_transfers); + if (!cursor_instances) + return -ENOMEM; + } + cursor_instances[k] = i; + break; + } + + if (!cursor) /* No suitable instance beyond the boundary found? Then we are done! */ + break; + + if (!cursor_found) { + /* Hmm, we didn't find the version indicated by 'cursor' among the instances + * of this transfer, let's skip it. */ + incomplete = true; + break; + } + + 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; + } + + if (!cursor) /* EOL */ + break; + + r = free_and_strdup_warn(&boundary, cursor); + if (r < 0) + return r; + + if (incomplete) /* One transfer was missing this version, ignore the whole thing */ + continue; + + /* See if we already have this update set in our table */ + for (size_t i = 0; i < c->n_update_sets; i++) { + if (strverscmp_improved(c->update_sets[i]->version, cursor) != 0) + continue; + + /* We only store the instances we found first, but we remember we also found it again */ + c->update_sets[i]->flags |= flags | extra_flags; + exists = true; + newest_found = true; + break; + } + + if (exists) + 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 && 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; + + 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 const char *update_set_flags_to_string(UpdateSetFlags flags) { + + switch ((unsigned) flags) { + + case 0: + return "n/a"; + + case UPDATE_INSTALLED|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current"; + + case UPDATE_AVAILABLE|UPDATE_NEWEST: + case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "candidate"; + + case UPDATE_INSTALLED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE: + return "installed"; + + case UPDATE_INSTALLED|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PROTECTED: + return "protected"; + + case UPDATE_AVAILABLE: + case UPDATE_AVAILABLE|UPDATE_PROTECTED: + return "available"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current+obsolete"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE: + return "installed+obsolete"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED: + return "protected+obsolete"; + + case UPDATE_AVAILABLE|UPDATE_OBSOLETE: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "available+obsolete"; + + default: + assert_not_reached(); + } +} + + +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); + + for (size_t i = 0; i < c->n_update_sets; i++) { + UpdateSet *us = c->update_sets[i]; + 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); + + for (size_t i = 0; i < c->n_update_sets; i++) + if (streq(c->update_sets[i]->version, version)) + return c->update_sets[i]; + + 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_(table_unrefp) Table *t = 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 & (JSON_FORMAT_OFF|JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO)) + (void) pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, 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); + + /* Determine if the target will make use of partition/fs attributes for any of the transfers */ + for (size_t n = 0; n < c->n_transfers; n++) { + Transfer *tr = c->transfers[n]; + + if (tr->target.type == RESOURCE_PARTITION) + show_partition_columns = true; + if (RESOURCE_IS_FILESYSTEM(tr->target.type)) + show_fs_columns = true; + } + + for (size_t n = 0; n < us->n_instances; n++) { + Instance *i = us->instances[n]; + + 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); + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +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)); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_vacuum(c->transfers[i], space, extra_protected_version); + if (r < 0) + return r; + + count = MAX(count, r); + } + + if (count > 0) + log_info("Removed %i instances.", count); + else + log_info("Removed no instances."); + + 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. */ + + r = context_make_offline(&context, node); + if (r < 0) + return r; + + 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_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_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); + + assert((us->flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_OBSOLETE)) == UPDATE_AVAILABLE); + + 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(false, + "STATUS=Making room for '%s'.", 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(false, + "STATUS=Updating to '%s'.\n", 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++) { + r = transfer_acquire_instance(c->transfers[i], us->instances[i]); + if (r < 0) + return r; + } + + if (arg_sync) + sync(); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_install_instance(c->transfers[i], us->instances[i], 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 reboot_now(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL; + int r; + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "Failed to open bus connection: %m"); + + r = bus_call_method(bus, bus_login_mgr, "RebootWithFlags", &error, NULL, "t", + (uint64_t) SD_LOGIND_ROOT_CHECK_INHIBITORS); + if (r < 0) + return log_error_errno(r, "Failed to issue reboot request: %s", bus_error_message(&error, r)); + + return 0; +} + +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_rmdir_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, + (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, + &mounted_dir, + &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; + 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 + return context_show_table(context); +} + +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 (!context->candidate) { + log_debug("No candidate found."); + return EXIT_FAILURE; + } + + puts(context->candidate->version); + 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); + + 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_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(); + } + + 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_freep) 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_symlinks_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"); + } + } + + if (!has_default_component && set_isempty(names)) { + log_info("No components defined."); + return 0; + } + + z = set_get_strv(names); + if (!z) + return log_oom(); + + strv_sort(z); + + if (has_default_component) + printf("%s<default>%s\n", + ansi_highlight(), ansi_normal()); + + STRV_FOREACH(i, z) + puts(*i); + + 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 relative to root path\n" + " --image=PATH Operate relative to image file\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" + " --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" + "\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_REBOOT, + ARG_VERIFY, + }; + + 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 }, + { "reboot", no_argument, NULL, ARG_REBOOT }, + { "component", required_argument, NULL, 'C' }, + { "verify", required_argument, NULL, ARG_VERIFY }, + {} + }; + + 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_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 '?': + 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; + + return sysupdate_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); |