/
niceSOFT
/
systemd
Обзор
Документация
Войти
/
niceSOFT
/
systemd
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
v261
src/sysupdate/sysupdate.c
2 038 строк
81 KB
Philip Withnall
sysupdate: List default component only if transfer definition exists
22 май 2026, 13:46
Не верифицирован
22 май 2026, 13:46
8de5a7b
Код
Авторство
О чём код?
/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include <unistd.h> #include "sd-daemon.h" #include "build.h" #include "conf-files.h" #include "constants.h" #include "dissect-image.h" #include "format-table.h" #include "glyph-util.h" #include "hexdecoct.h" #include "image-policy.h" #include "loop-util.h" #include "main-func.h" #include "mount-util.h" #include "options.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 "specifier.h" #include "string-util.h" #include "strv.h" #include "sysupdate.h" #include "sysupdate-feature.h" #include "sysupdate-instance.h" #include "sysupdate-transfer.h" #include "sysupdate-update-set.h" #include "sysupdate-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); const Specifier specifier_table[] = { COMMON_SYSTEM_SPECIFIERS, COMMON_TMP_SPECIFIERS, {} }; typedef struct Context { Transfer **transfers; size_t n_transfers; Transfer **disabled_transfers; size_t n_disabled_transfers; Hashmap *features; /* Defined features, keyed by ID */ 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(tr, c->disabled_transfers, c->n_disabled_transfers) transfer_free(*tr); free(c->disabled_transfers); hashmap_free(c->features); 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 DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free); static int read_definitions( Context *c, const char **dirs, const char *suffix, const char *node) { ConfFile **files = NULL; Transfer **transfers = NULL, **disabled = NULL; size_t n_files = 0, n_transfers = 0, n_disabled = 0; int r; CLEANUP_ARRAY(files, n_files, conf_file_free_array); CLEANUP_ARRAY(transfers, n_transfers, transfer_free_array); CLEANUP_ARRAY(disabled, n_disabled, transfer_free_array); assert(c); assert(dirs); assert(suffix); r = conf_files_list_strv_full(suffix, arg_root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN, dirs, &files, &n_files); if (r < 0) return log_error_errno(r, "Failed to enumerate sysupdate.d/*%s definitions: %m", suffix); FOREACH_ARRAY(i, files, n_files) { _cleanup_(transfer_freep) Transfer *t = NULL; Transfer **appended; ConfFile *e = *i; t = transfer_new(c); if (!t) return log_oom(); r = transfer_read_definition(t, e->result, dirs, c->features); if (r < 0) return r; r = transfer_resolve_paths(t, arg_root, node); if (r < 0) return r; if (t->enabled) appended = GREEDY_REALLOC_APPEND(transfers, n_transfers, &t, 1); else appended = GREEDY_REALLOC_APPEND(disabled, n_disabled, &t, 1); if (!appended) return log_oom(); TAKE_PTR(t); } c->transfers = TAKE_PTR(transfers); c->n_transfers = n_transfers; c->disabled_transfers = TAKE_PTR(disabled); c->n_disabled_transfers = n_disabled; return 0; } typedef enum ReadDefinitionsFlags { READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS = 1 << 1, } ReadDefinitionsFlags; static int context_read_definitions(Context *c, const char* node, ReadDefinitionsFlags flags) { _cleanup_strv_free_ char **dirs = NULL; int r; assert(c); if (arg_definitions) dirs = strv_new(arg_definitions); else if (arg_component) { char **l = CONF_PATHS_STRV(""); size_t i = 0; dirs = new0(char*, strv_length(l) + 1); if (!dirs) return log_oom(); STRV_FOREACH(dir, l) { char *j; j = strjoin(*dir, "sysupdate.", arg_component, ".d"); if (!j) return log_oom(); dirs[i++] = j; } } else dirs = strv_new(CONF_PATHS("sysupdate.d")); if (!dirs) return log_oom(); ConfFile **files = NULL; size_t n_files = 0; CLEANUP_ARRAY(files, n_files, conf_file_free_array); r = conf_files_list_strv_full(".feature", arg_root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN, (const char**) dirs, &files, &n_files); if (r < 0) return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m"); FOREACH_ARRAY(i, files, n_files) { _cleanup_(feature_unrefp) Feature *f = NULL; ConfFile *e = *i; f = feature_new(); if (!f) return log_oom(); r = feature_read_definition(f, e->result, (const char**) dirs); if (r < 0) return r; r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f); if (r < 0) return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id); TAKE_PTR(f); } r = read_definitions(c, (const char**) dirs, ".transfer", node); if (r < 0) return r; if (c->n_transfers + c->n_disabled_transfers == 0) { /* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */ r = read_definitions(c, (const char**) dirs, ".conf", node); if (r < 0) return r; if (c->n_transfers + c->n_disabled_transfers > 0) log_warning("As of v257, transfer definitions should have the '.transfer' extension."); } if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS) && c->n_transfers + (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS) ? 0 : c->n_disabled_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."); } return 0; } static int context_load_installed_instances(Context *c) { int r; assert(c); log_info("Discovering installed instances%s", glyph(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; } FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_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", glyph(GLYPH_ELLIPSIS)); FOREACH_ARRAY(tr, c->transfers, c->n_transfers) { Transfer *t = *tr; 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 && !(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. * 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; /* Partial or pending updates by definition are not incomplete, they’re * partial/pending instead. While an individual Instance cannot be both partial and * pending, an UpdateSet as a whole can contain both partial and pending instances. */ assert(!match || !(match->is_partial && match->is_pending)); 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); 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) || 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 * 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. 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; 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". * It may be partial or pending. */ 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; /* Newest installed is still pending or partial and no candidate is set? Then it becomes the candidate. */ if (c->newest_installed && (c->newest_installed->flags & (UPDATE_PENDING|UPDATE_PARTIAL)) && !c->candidate) c->candidate = c->newest_installed; return 0; } static int context_discover_update_sets(Context *c) { int r; assert(c); log_info("Determining installed update sets%s", glyph(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", glyph(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, glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)), TABLE_SET_COLOR, color, TABLE_STRING, 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)) pager_open(arg_pager_flags); 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\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()); 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(us->flags & (UPDATE_INCOMPLETE|UPDATE_PARTIAL|UPDATE_PENDING)); 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 (!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%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(), 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()); 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("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)), SD_JSON_BUILD_PAIR_STRV("changelogUrls", 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) { size_t disabled_count = 0; int r, count = 0; assert(c); if (space == 0) log_info("Making room%s", glyph(GLYPH_ELLIPSIS)); else log_info("Making room for %" PRIu64 " updates%s", space, glyph(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); } FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) { r = transfer_vacuum(*tr, UINT64_MAX /* wipe all instances */, NULL); if (r < 0) return r; if (r > 0) disabled_count++; } if (!sd_json_format_enabled(arg_json_format_flags)) { if (count > 0 && disabled_count > 0) log_info("Removed %i instances, and %zu disabled transfers.", count, disabled_count); else if (count > 0) log_info("Removed %i instances.", count); else if (disabled_count > 0) log_info("Removed %zu disabled transfers.", disabled_count); else log_info("Found nothing to remove."); } else { _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_INTEGER("removed", count), SD_JSON_BUILD_PAIR_UNSIGNED("disabledTransfers", disabled_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, ReadDefinitionsFlags read_definitions_flags) { _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, node, read_definitions_flags); 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, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS | READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); 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_environment= */ 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_process_partial_and_pending(Context *c, const char *version); static int context_acquire( 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 (FLAGS_SET(us->flags, UPDATE_PARTIAL)) { return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Selected update '%s' is already acquired and partially installed. Vacuum it to try installing again.", us->version); } 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; } 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); _cleanup_free_ InstanceMetadata *metadata = new0(InstanceMetadata, c->n_transfers); if (!metadata) return log_oom(); /* Compute up the temporary paths before vacuuming so we don't vacuum anything if we fail to compute * any paths because of failed validations (e.g. exceeding the gpt partition label size). */ for (size_t i = 0; i < c->n_transfers; i++) { Instance *inst = us->instances[i]; Transfer *t = c->transfers[i]; assert(inst); r = transfer_compute_temporary_paths(t, inst, metadata + i); if (r < 0) return r; } (void) sd_notifyf(/* unset_environment= */ 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_environment= */ 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, metadata + i, context_on_acquire_progress, c); if (r < 0) return r; } if (arg_sync) sync(); 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, "READY=1\n" "X_SYSUPDATE_VERSION=%s\n" "STATUS=Installing '%s'.", us->version, 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 && !inst->is_pending) continue; r = transfer_install_instance(t, inst, arg_root); if (r < 0) return r; } log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version); (void) sd_notifyf(/* unset_environment= */ false, "STATUS=Installed '%s'.", 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; } VERB(verb_list, "list", "[VERSION]", VERB_ANY, 2, VERB_DEFAULT, "Show installed and available versions"); static int verb_list(int argc, char *argv[], uintptr_t _data, 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 (!sd_json_format_enabled(arg_json_format_flags)) 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; 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)) { current = us->version; current_is_pending = FLAGS_SET(us->flags, UPDATE_PENDING); } 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_is_pending ? "current+pending" : "current", current), SD_JSON_BUILD_PAIR_STRV("all", versions), SD_JSON_BUILD_PAIR_STRV("appstreamUrls", 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; } } VERB(verb_features, "features", "[FEATURE]", VERB_ANY, 2, 0, "Show optional features"); static int verb_features(int argc, char *argv[], uintptr_t _data, 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_(table_unrefp) Table *table = NULL; const char *feature_id; Feature *f; int r; assert(argc <= 2); feature_id = argc >= 2 ? argv[1] : NULL; r = process_image(/* ro= */ true, &mounted_dir, &loop_device); if (r < 0) return r; r = context_make_offline(&context, loop_device ? loop_device->node : NULL, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; if (feature_id) { _cleanup_strv_free_ char **transfers = NULL; f = hashmap_get(context->features, feature_id); if (!f) return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Optional feature not found: %s", feature_id); table = table_new_vertical(); if (!table) return log_oom(); FOREACH_ARRAY(tr, context->transfers, context->n_transfers) { Transfer *t = *tr; if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id)) continue; r = strv_extend(&transfers, t->id); if (r < 0) return log_oom(); } FOREACH_ARRAY(tr, context->disabled_transfers, context->n_disabled_transfers) { Transfer *t = *tr; if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id)) continue; r = strv_extend(&transfers, t->id); if (r < 0) return log_oom(); } r = table_add_many(table, TABLE_FIELD, "Name", TABLE_STRING, f->id, TABLE_FIELD, "Enabled", TABLE_BOOLEAN, f->enabled); if (r < 0) return table_log_add_error(r); if (f->description) { r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description); if (r < 0) return table_log_add_error(r); } if (f->documentation) { r = table_add_many(table, TABLE_FIELD, "Documentation", TABLE_STRING, f->documentation, TABLE_SET_URL, f->documentation); if (r < 0) return table_log_add_error(r); } if (f->appstream) { r = table_add_many(table, TABLE_FIELD, "AppStream", TABLE_STRING, f->appstream, TABLE_SET_URL, f->appstream); if (r < 0) return table_log_add_error(r); } if (!strv_isempty(transfers)) { r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, transfers); if (r < 0) return table_log_add_error(r); } return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); } else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) { table = table_new("", "feature", "description", "documentation"); if (!table) return log_oom(); HASHMAP_FOREACH(f, context->features) { r = table_add_many(table, TABLE_BOOLEAN_CHECKMARK, f->enabled, TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled), TABLE_STRING, f->id, TABLE_STRING, f->description, TABLE_STRING, f->documentation, TABLE_SET_URL, f->documentation); if (r < 0) return table_log_add_error(r); } return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); } else { _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; _cleanup_strv_free_ char **features = NULL; HASHMAP_FOREACH(f, context->features) { r = strv_extend(&features, f->id); if (r < 0) return log_oom(); } r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRV("features", features)); 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; } VERB_NOARG(verb_check_new, "check-new", "Check if there's a new version available"); static int verb_check_new(int argc, char *argv[], uintptr_t _data, 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 (!sd_json_format_enabled(arg_json_format_flags)) { 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; } 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; _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; 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; 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; } VERB(verb_update, "update", "[VERSION]", VERB_ANY, 2, 0, "Install new version now"); static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) { UpdateActionFlags flags = UPDATE_ACTION_INSTALL; if (!arg_offline) flags |= UPDATE_ACTION_ACQUIRE; return verb_update_impl(argc, argv, flags); } VERB(verb_acquire, "acquire", "[VERSION]", VERB_ANY, 2, 0, "Acquire (download) new version now"); static int verb_acquire(int argc, char *argv[], uintptr_t _data, void *userdata) { return verb_update_impl(argc, argv, UPDATE_ACTION_ACQUIRE); } VERB_NOARG(verb_vacuum, "vacuum", "Make room, by deleting old versions"); static int verb_vacuum(int argc, char *argv[], uintptr_t _data, 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, READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; return context_vacuum(context, 0, NULL); } VERB(verb_pending_or_reboot, "pending", NULL, 1, 1, 0, "Report whether a newer version is installed than currently booted"); VERB(verb_pending_or_reboot, "reboot", NULL, 1, 1, 0, "Reboot if a newer version is installed than booted"); static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, 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, /* node= */ NULL, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS | READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; log_info("Determining installed update sets%s", glyph(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); } VERB_NOARG(verb_components, "components", "Show list of components"); static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(context_freep) Context* context = NULL; _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; _cleanup_set_free_ Set *names = NULL; bool has_default_component = false; 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, 0); if (r < 0) return r; ConfFile **directories = NULL; size_t n_directories = 0; CLEANUP_ARRAY(directories, n_directories, conf_file_free_array); r = conf_files_list_strv_full(".d", arg_root, CONF_FILES_DIRECTORY|CONF_FILES_WARN, (const char * const *) CONF_PATHS_STRV(""), &directories, &n_directories); if (r < 0) return log_error_errno(r, "Failed to enumerate directories: %m"); FOREACH_ARRAY(i, directories, n_directories) { ConfFile *e = *i; if (streq(e->filename, "sysupdate.d")) { continue; } const char *s = startswith(e->filename, "sysupdate."); if (!s) continue; const char *a = endswith(s, ".d"); if (!a) continue; _cleanup_free_ char *n = strndup(s, a - s); if (!n) return log_oom(); r = component_name_valid(n); if (r < 0) return log_error_errno(r, "Unable to validate component name '%s': %m", n); if (r == 0) continue; r = set_ensure_put(&names, &string_hash_ops_free, n); if (r < 0 && r != -EEXIST) return log_error_errno(r, "Failed to add component '%s' to set: %m", n); TAKE_PTR(n); } /* Does the system have at least one transfer file in /etc/sysupdate.d, which can be considered a * TARGET_HOST? See target_get_argument() in sysupdated.c */ has_default_component = (!arg_definitions && !arg_component && !arg_root && !arg_image && context->n_transfers > 0); /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */ _cleanup_free_ char **z = set_get_strv(names); if (!z) return log_oom(); strv_sort(z); if (!sd_json_format_enabled(arg_json_format_flags)) { 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 help(void) { _cleanup_free_ char *link = NULL; _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL; int r; r = terminal_urlify_man("systemd-sysupdate", "8", &link); if (r < 0) return log_oom(); r = verbs_get_help_table(&verbs); if (r < 0) return r; r = option_parser_get_help_table(&common_options); if (r < 0) return r; r = option_parser_get_help_table_group("Options", &options); if (r < 0) return r; (void) table_sync_column_widths(0, verbs, common_options, options); printf("%s [OPTIONS...] [VERSION]\n" "\n%sUpdate OS images.%s\n" "\n%sCommands:%s\n", program_invocation_short_name, ansi_highlight(), ansi_normal(), ansi_underline(), ansi_normal()); r = table_print_or_warn(verbs); if (r < 0) return r; r = table_print_or_warn(common_options); if (r < 0) return r; printf("\n%sOptions:%s\n", ansi_underline(), ansi_normal()); r = table_print_or_warn(options); if (r < 0) return r; printf("\nSee the %s for details.\n", link); return 0; } VERB_COMMON_HELP_HIDDEN(help); static int parse_argv(int argc, char *argv[], char ***remaining_args) { assert(argc >= 0); assert(argv); assert(remaining_args); OptionParser opts = { argc, argv }; int r; FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: return help(); OPTION_COMMON_VERSION: return version(); OPTION_GROUP("Options"): break; OPTION('C', "component", "NAME", "Select component to update"): if (isempty(opts.arg)) { arg_component = mfree(arg_component); break; } r = component_name_valid(opts.arg); 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", opts.arg); r = free_and_strdup_warn(&arg_component, opts.arg); if (r < 0) return r; break; OPTION_LONG("definitions", "DIR", "Find transfer definitions in specified directory"): r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_definitions); if (r < 0) return r; break; OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"): r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_root); if (r < 0) return r; break; OPTION_LONG("image", "PATH", "Operate on disk image as filesystem root"): r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_image); if (r < 0) return r; break; OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"): r = parse_image_policy_argument(opts.arg, &arg_image_policy); if (r < 0) return r; break; OPTION_LONG("transfer-source", "PATH", "Specify the directory to transfer sources from"): r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_transfer_source); if (r < 0) return r; break; OPTION('m', "instances-max", "INT", "How many instances to maintain"): r = safe_atou64(opts.arg, &arg_instances_max); if (r < 0) return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", opts.arg); break; OPTION_LONG("sync", "BOOL", "Controls whether to sync data to disk"): r = parse_boolean_argument("--sync=", opts.arg, &arg_sync); if (r < 0) return r; break; OPTION_LONG("verify", "BOOL", "Force signature verification on or off"): { bool b; r = parse_boolean_argument("--verify=", opts.arg, &b); if (r < 0) return r; arg_verify = b; break; } OPTION_LONG("reboot", NULL, "Reboot after updating to newer version"): arg_reboot = true; break; OPTION_LONG("offline", NULL, "Do not fetch metadata from the network"): arg_offline = true; break; OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; OPTION_COMMON_NO_LEGEND: arg_legend = false; break; OPTION_COMMON_JSON: r = parse_json_argument(opts.arg, &arg_json_format_flags); if (r <= 0) return r; break; } 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."); *remaining_args = option_parser_get_args(&opts); return 1; } static int run(int argc, char *argv[]) { int r; log_setup(); char **args = NULL; r = parse_argv(argc, argv, &args); if (r <= 0) return r; return dispatch_verb(args, NULL); } DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);