/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include "sd-bus.h" #include "sd-daemon.h" #include "sd-json.h" #include "sd-varlink.h" #include "build.h" #include "bus-polkit.h" #include "conf-files.h" #include "constants.h" #include "discover-image.h" #include "dissect-image.h" #include "env-util.h" #include "errno-util.h" #include "fd-util.h" #include "format-table.h" #include "glyph-util.h" #include "hashmap.h" #include "help-util.h" #include "hexdecoct.h" #include "image-policy.h" #include "json-util.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 "pretty-print.h" #include "runtime-scope.h" #include "sort-util.h" #include "specifier.h" #include "string-util.h" #include "strv.h" #include "sysupdate.h" #include "sysupdate-cleanup.h" #include "sysupdate-feature.h" #include "sysupdate-instance.h" #include "sysupdate-target.h" #include "sysupdate-transfer.h" #include "sysupdate-update-set.h" #include "sysupdate-util.h" #include "varlink-io.systemd.SysUpdate.h" #include "varlink-util.h" #include "verbs.h" static char *arg_definitions = NULL; static bool arg_sync = true; static 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; static char *arg_root = NULL; static char *arg_image = NULL; static bool arg_reboot = false; static int arg_cleanup = -1; static char *arg_component = NULL; static bool arg_component_all = false; static int arg_verify = -1; static ImagePolicy *arg_image_policy = NULL; static bool arg_offline = false; static char *arg_transfer_source = NULL; static bool arg_varlink = false; 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, {} }; #define CONTEXT_NULL \ (Context) { \ .sync = true, \ .instances_max = UINT64_MAX, \ .verify = -1, \ .cleanup = -1, \ .installdb_fd = -EBADF, \ .target_identifier.class = _TARGET_CLASS_INVALID, \ } void context_done(Context *c) { assert(c); c->mounted_dir = umount_and_rmdir_and_free(c->mounted_dir); c->loop_device = loop_device_unref(c->loop_device); FOREACH_ARRAY(tr, c->transfers, c->n_transfers) transfer_free(*tr); c->transfers = mfree(c->transfers); c->n_transfers = 0; FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) transfer_free(*tr); c->disabled_transfers = mfree(c->disabled_transfers); c->n_disabled_transfers = 0; c->features = hashmap_free(c->features); FOREACH_ARRAY(us, c->update_sets, c->n_update_sets) update_set_free(*us); c->update_sets = mfree(c->update_sets); c->n_update_sets = 0; c->web_cache = hashmap_free(c->web_cache); c->installdb_fd = safe_close(c->installdb_fd); c->definitions = mfree(c->definitions); c->root = mfree(c->root); c->image = mfree(c->image); c->component = mfree(c->component); c->image_policy = image_policy_free(c->image_policy); c->transfer_source = mfree(c->transfer_source); target_identifier_done(&c->target_identifier); } static int context_from_cmdline(Context *ret) { assert(ret); _cleanup_(context_done) Context context = CONTEXT_NULL; context.instances_max = arg_instances_max; context.sync = arg_sync; context.reboot = arg_reboot; context.verify = arg_verify; context.offline = arg_offline; context.cleanup = arg_cleanup; context.component_all = arg_component_all; if (strdup_to(&context.definitions, arg_definitions) < 0) return log_oom(); if (strdup_to(&context.root, arg_root) < 0) return log_oom(); if (strdup_to(&context.image, arg_image) < 0) return log_oom(); if (strdup_to(&context.component, arg_component) < 0) return log_oom(); if (strdup_to(&context.transfer_source, arg_transfer_source) < 0) return log_oom(); if (arg_image_policy) { context.image_policy = image_policy_copy(arg_image_policy); if (!context.image_policy) return log_oom(); } *ret = TAKE_GENERIC(context, Context, CONTEXT_NULL); return 0; } /* Stores any long-running server state which needs to persist between varlink calls, such as state for * pending polkit requests */ typedef struct Server { sd_bus *system_bus; Hashmap *polkit_registry; } Server; #define SERVER_NULL \ (Server) { \ /* all fields fine with being initialised to NULL */ \ } static void server_done(Server *s) { assert(s); s->polkit_registry = hashmap_free(s->polkit_registry); s->system_bus = sd_bus_flush_close_unref(s->system_bus); } 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, c->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, c->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 (c->definitions) dirs = strv_new(c->definitions); else if (c->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.", c->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", c->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, c->root, 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 (c->component) return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No transfer definitions for component '%s' found.", c->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, c->verify >= 0 ? c->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, c->verify >= 0 ? c->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, c->verify >= 0 ? c->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 (!c->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; } typedef enum ProcessImageFlags { PROCESS_IMAGE_READ_ONLY = 1 << 0, } ProcessImageFlags; static int process_image( Context *c, ProcessImageFlags flags) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; int r; assert(c); if (!c->image) return 0; assert(!c->root); assert(!c->mounted_dir); assert(!c->loop_device); r = mount_image_privately_interactively( c->image, c->image_policy, (FLAGS_SET(flags, PROCESS_IMAGE_READ_ONLY) ? 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; c->root = strdup(mounted_dir); if (!c->root) return log_oom(); c->mounted_dir = TAKE_PTR(mounted_dir); c->loop_device = TAKE_PTR(loop_device); return 0; } static int context_list_components(Context *context, char ***component_names, bool *has_default_component); static int context_load_offline( Context *context, ProcessImageFlags process_image_flags, ReadDefinitionsFlags read_definitions_flags) { int r; assert(context); /* Sets up 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 */ r = process_image(context, process_image_flags); if (r < 0) return r; r = context_read_definitions(context, context->loop_device ? context->loop_device->node : NULL, read_definitions_flags); if (r < 0) return r; r = context_load_installed_instances(context); if (r < 0) return r; return 0; } static int context_load_online( Context *context, ProcessImageFlags process_image_flags) { int r; assert(context); /* Like context_load_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_load_offline( context, process_image_flags, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS); if (r < 0) return r; if (!context->offline) { r = context_load_available_instances(context); if (r < 0) return r; } r = context_discover_update_sets(context); if (r < 0) return r; return 0; } static bool image_type_can_sysupdate(ImageType image_type) { switch (image_type) { case IMAGE_DIRECTORY: case IMAGE_SUBVOLUME: case IMAGE_RAW: case IMAGE_BLOCK: return true; /* systemd-sysupdate doesn't support mstack images yet */ case IMAGE_MSTACK: default: return false; } } static int context_load_paths_from_image(Context *context, Image *image) { assert(context); assert(image); assert(!context->root); assert(!context->image); switch (image->type) { case IMAGE_DIRECTORY: case IMAGE_SUBVOLUME: context->root = strdup(image->path); if (!context->root) return log_oom(); return 0; case IMAGE_RAW: case IMAGE_BLOCK: context->image = strdup(image->path); if (!context->image) return log_oom(); return 0; default: assert_not_reached(); } } /* Load a Context to point to the target given by the TargetIdentifier. The TargetIdentifier will have been * syntactically validated by dispatch_target_identifier(), but might still point to components which don’t * exist, images which the user isn’t privileged to access, etc. This function validates the TargetIdentifier * against an enumerated list of known targets, which are safe to update without additional permissions. */ static int context_load_online_from_target(Context *context, ProcessImageFlags process_image_flags) { int r; assert(context); assert(context->target_identifier.class != _TARGET_CLASS_INVALID); /* These shouldn’t have been set up some other way first */ assert(!context->component); assert(!context->root); assert(!context->image); switch (context->target_identifier.class) { case TARGET_MACHINE: case TARGET_PORTABLE: case TARGET_SYSEXT: case TARGET_CONFEXT: { _cleanup_hashmap_free_ Hashmap *images = NULL; Image *image, *selected_image = NULL; /* These are all image-based target classes, so first find the corresponding image. */ r = image_discover(RUNTIME_SCOPE_SYSTEM, (ImageClass) context->target_identifier.class, NULL, &images); if (r < 0) return r; HASHMAP_FOREACH(image, images) { bool have = false; _cleanup_(context_done) Context image_context = CONTEXT_NULL; if (image_is_host(image)) continue; /* We already enroll the host ourselves */ if (!image_type_can_sysupdate(image->type)) continue; if (!streq(image->name, context->target_identifier.name)) continue; r = context_load_paths_from_image(&image_context, image); if (r < 0) return r; /* Load the components in a separate Context specific to the given Image before * committing to loading that state to the main Context. */ r = context_load_offline(&image_context, 0, 0); if (r < 0) return r; r = context_list_components(&image_context, /* component_names= */ NULL, &have); if (r < 0) return r; if (!have) { log_debug("Skipping %s because it has no default component", image->path); continue; } /* This is the match we were looking for */ selected_image = image; break; } if (!selected_image) return -ENOENT; r = context_load_paths_from_image(context, selected_image); if (r < 0) return r; break; } case TARGET_HOST: /* No additional setup needed */ break; case TARGET_COMPONENT: { _cleanup_strv_free_ char **component_names = NULL; r = context_list_components(context, &component_names, /* has_default_component= */ NULL); if (r < 0) return r; if (!strv_contains(component_names, context->target_identifier.name)) return -ENOENT; context->component = strdup(context->target_identifier.name); if (!context->component) return log_oom(); break; } default: assert_not_reached(); } return context_load_online(context, process_image_flags); } 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 (c->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 (c->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 notify_subscribers_reply( sd_varlink *link, sd_json_variant *reply, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) { assert(link); if (error_id) log_warning("Notification subscriber '%s' returned error, ignoring: %s", strna(sd_varlink_get_description(link)), error_id); return 0; } static int context_notify_subscribers(Context *c, UpdateSet *us) { int r; assert(c); /* 'us' is NULL when we are forced to notify even though no update was applied (via * SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1). In that case we send neither a version nor a resource list. */ _cleanup_(sd_json_variant_unrefp) sd_json_variant *resources = NULL; if (us) 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; /* Report where the resource was installed *to* (not the source it came from): the * final on-disk path for filesystem targets, the partition device node for partition * targets. */ const char *target_path = RESOURCE_IS_FILESYSTEM(t->target.type) ? t->final_path : t->target.type == RESOURCE_PARTITION ? t->partition_info.device : NULL; r = sd_json_variant_append_arraybo( &resources, SD_JSON_BUILD_PAIR_STRING("transfer", t->id), SD_JSON_BUILD_PAIR_CONDITION(!!target_path, "path", SD_JSON_BUILD_STRING(target_path))); if (r < 0) return log_warning_errno(r, "Failed to build sysupdate notify resources list, skipping notification: %m"); } _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; r = sd_json_buildo( ¶ms, SD_JSON_BUILD_PAIR_CONDITION(!!c->component, "component", SD_JSON_BUILD_STRING(c->component)), SD_JSON_BUILD_PAIR_CONDITION(!!us, "version", SD_JSON_BUILD_STRING(us ? us->version : NULL)), SD_JSON_BUILD_PAIR_CONDITION(!!resources, "resources", SD_JSON_BUILD_VARIANT(resources))); if (r < 0) return log_warning_errno(r, "Failed to build sysupdate notify parameters, skipping notification: %m"); ssize_t n = varlink_execute_directory( VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK, "io.systemd.SysUpdate.Notify.OnCompletedUpdate", params, /* more= */ false, /* timeout_usec= */ 5 * USEC_PER_MINUTE, notify_subscribers_reply, /* userdata= */ NULL); if (n < 0) log_debug_errno(n, "Failed to dispatch sysupdate notification to %s, ignoring: %m", VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK); else if (n > 0) log_debug("Dispatched sysupdate notification to %zi subscribers in %s.", n, VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK); return 0; } 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, c->root); if (r < 0) return r; } log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version); if (!c->root) (void) context_notify_subscribers(c, us); (void) sd_notifyf(/* unset_environment= */ false, "STATUS=Installed '%s'.", us->version); if (ret_applied) *ret_applied = us; return 1; } static JSON_DISPATCH_ENUM_DEFINE(dispatch_target_class, TargetClass, target_class_from_string); static int dispatch_target_identifier(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { TargetIdentifier *t = ASSERT_PTR(userdata); static const sd_json_dispatch_field dispatch[] = { { "class", SD_JSON_VARIANT_STRING, dispatch_target_class, voffsetof(*t, class), SD_JSON_MANDATORY }, { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(*t, name), SD_JSON_NULLABLE }, {} }; int r; r = sd_json_dispatch(variant, dispatch, flags, t); if (r < 0) return r; /* Name is mandatory unless class is `host` */ if ((t->class == TARGET_HOST) != (!t->name)) return json_log(variant, flags, SYNTHETIC_ERRNO(ENXIO), "Target name does not match class."); if (t->class == TARGET_COMPONENT && !component_name_valid(t->name)) return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", t->name); return 0; } static int verify_polkit(Context *context, sd_varlink *link, const char *action, const char **details) { int r; Server *s = ASSERT_PTR(sd_varlink_get_userdata(ASSERT_PTR(link))); assert(context); if (!s->system_bus) { r = sd_bus_open_system_with_description(&s->system_bus, "sysupdate-system"); if (r < 0) return log_error_errno(r, "Failed to get system bus connection: %m"); r = sd_bus_attach_event(s->system_bus, sd_varlink_get_event(link), SD_EVENT_PRIORITY_NORMAL); if (r < 0) return log_error_errno(r, "Failed to attach system bus to event loop: %m"); } return varlink_verify_polkit_async(link, s->system_bus, action, details, &s->polkit_registry); } 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_(context_done) Context 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 = context_from_cmdline(&context); if (r < 0) return r; if (context.component_all) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); r = context_load_online(&context, PROCESS_IMAGE_READ_ONLY); 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_(context_done) Context 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 = context_from_cmdline(&context); if (r < 0) return r; if (context.component_all) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); r = context_load_offline( &context, PROCESS_IMAGE_READ_ONLY, 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_(context_done) Context context = CONTEXT_NULL; int r; assert(argc <= 1); r = context_from_cmdline(&context); if (r < 0) return r; if (context.component_all) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); r = context_load_online( &context, PROCESS_IMAGE_READ_ONLY); 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; } static int vl_method_check_new(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { _cleanup_(context_done) Context context = CONTEXT_NULL; int r; assert(link); static const sd_json_dispatch_field dispatch_table[] = { { "target", SD_JSON_VARIANT_OBJECT, dispatch_target_identifier, voffsetof(context, target_identifier), SD_JSON_MANDATORY }, VARLINK_DISPATCH_POLKIT_FIELD, {}, }; r = sd_varlink_dispatch(link, parameters, dispatch_table, &context); if (r != 0) return r; r = verify_polkit(&context, link, "org.freedesktop.sysupdate1.check", (const char**) STRV_MAKE( "class", target_class_to_string(context.target_identifier.class), "offline", "0", context.target_identifier.name ? "name" : NULL, context.target_identifier.name)); if (r <= 0) return r; if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0) context.verify = 0; /* CheckNew is always online */ context.offline = false; r = context_load_online_from_target(&context, PROCESS_IMAGE_READ_ONLY); if (r == -ENOENT) return sd_varlink_error(link, "io.systemd.SysUpdate.NoSuchTarget", NULL); else if (r < 0) return r; if (context.candidate) r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("available", context.candidate->version)); else r = sd_varlink_error(link, "io.systemd.SysUpdate.NoUpdateNeeded", NULL); if (r < 0) return r; return 0; } 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_(context_done) Context 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; r = context_from_cmdline(&context); if (r < 0) return r; if (context.component_all) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); if (context.instances_max < 2) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --instances-max argument must be >= 2 while updating"); if (context.reboot) { /* If automatic reboot on completion is requested, let's first determine the currently booted image */ r = parse_os_release(context.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."); } bool installed = false; int ret = 0; r = context_load_online( &context, /* process_image_flags= */ 0); if (r < 0) { if (r != -ENOENT) return r; /* No transfer files found. In that case, still do the installdb cleanup below */ RET_GATHER(ret, r); } else { 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; if (FLAGS_SET(action_flags, UPDATE_ACTION_INSTALL) && r > 0) { /* installation of update indicated */ r = context_install(&context, version, &applied); if (r < 0) return r; installed = r > 0; } /* context_install() returns > 0 (and emits a notification) only if it actually applied an update. If * nothing was applied but SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1 is set, still notify subscribers (without a * resource list), so e.g. a kernel/policy refresh can be triggered unconditionally. */ if ((action_flags & UPDATE_ACTION_INSTALL) && !installed) { int f = secure_getenv_bool("SYSTEMD_SYSUPDATE_FORCE_NOTIFY"); if (f < 0 && f != -ENXIO) log_debug_errno(f, "Failed to parse $SYSTEMD_SYSUPDATE_FORCE_NOTIFY, ignoring: %m"); if (f > 0) (void) context_notify_subscribers(&context, /* us= */ NULL); } } if (context.cleanup > 0) RET_GATHER(ret, installdb_cleanup_component(&context)); if (installed) { /* We installed something, yay */ if (context.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."); RET_GATHER(ret, reboot_now()); } else 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."); RET_GATHER(ret, reboot_now()); } else log_info("Booted version is newer or identical to newly installed version, not rebooting."); } } return ret; } 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_(context_done) Context context = CONTEXT_NULL; int r; assert(argc <= 1); r = context_from_cmdline(&context); if (r < 0) return r; if (context.component_all) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); if (context.instances_max < 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --instances-max argument must be >= 1 while vacuuming"); r = context_load_offline( &context, /* process_image_flags= */ 0, 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_done) Context context = CONTEXT_NULL; _cleanup_free_ char *booted_version = NULL; int r; assert(argc == 1); r = context_from_cmdline(&context); if (r < 0) return r; if (context.image || context.root) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]); if (context.component || context.component_all) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --component= and --component-all switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); r = context_load_offline( &context, /* process_image_flags= */ 0, 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(context.root, "IMAGE_VERSION", &booted_version); if (r < 0) /* yes, context.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 context_list_components(Context *context, char ***component_names, bool *has_default_component) { int r; assert(context); _cleanup_strv_free_ char **z = NULL; r = get_component_list(context->root, &z); if (r < 0) return r; if (component_names) *component_names = TAKE_PTR(z); /* 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 */ if (has_default_component) *has_default_component = (!context->definitions && !context->component && !context->root && !context->image && context->n_transfers > 0); return 0; } VERB_NOARG(verb_components, "components", "Show list of components"); static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(context_done) Context context = CONTEXT_NULL; _cleanup_strv_free_ char **component_names = NULL; bool has_default_component = false; int r; assert(argc <= 1); r = context_from_cmdline(&context); if (r < 0) return r; if (context.component_all) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); r = context_load_offline( &context, /* process_image_flags= */ 0, /* read_definitions_flags= */ 0); if (r < 0) return r; r = context_list_components(&context, &component_names, &has_default_component); if (r < 0) return log_error_errno(r, "Failed to enumerate components: %m"); if (!sd_json_format_enabled(arg_json_format_flags)) { if (!has_default_component && strv_isempty(component_names)) { log_info("No components defined."); return 0; } if (has_default_component) printf("%s%s\n", ansi_highlight(), ansi_normal()); STRV_FOREACH(i, component_names) 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", component_names)); 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_cleanup, "cleanup", "Clean up orphaned files"); static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(context_done) Context context = CONTEXT_NULL; int r; assert(argc <= 1); r = context_from_cmdline(&context); if (r < 0) return r; if (context.cleanup == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invocation of 'cleanup' with --cleanup=no is contradictory, refusing."); r = context_load_offline( &context, /* process_image_flags= */ 0, /* read_definitions_flags= */ 0); if (r < 0) return r; int ret = 0; RET_GATHER(ret, installdb_cleanup_component(&context)); if (context.component_all) { _cleanup_strv_free_ char **z = NULL; r = installdb_list_components(&context, &z); if (r < 0) return log_error_errno(r, "Failed to enumerate components: %m"); STRV_FOREACH(i, z) { _cleanup_(context_done) Context component_context = CONTEXT_NULL; r = context_from_cmdline(&component_context); if (r < 0) return r; /* Override the component with our iter. This needs to be done in a fresh Context * as the installdb_fd and other state are specific to the component. */ r = free_and_strdup_warn(&component_context.component, *i); if (r < 0) return r; r = context_load_offline( &component_context, /* process_image_flags= */ 0, /* read_definitions_flags= */ 0); if (r < 0) return r; RET_GATHER(ret, installdb_cleanup_component(&component_context)); } } return ret; } static int help(void) { _cleanup_(table_unrefp) Table *common_options = NULL, *options = NULL, *verbs = NULL; int r; 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); help_cmdline("[OPTIONS…] [VERSION]"); help_abstract("Update OS images."); help_section("Commands"); r = table_print_or_warn(verbs); if (r < 0) return r; r = table_print_or_warn(common_options); if (r < 0) return r; help_section("Options"); r = table_print_or_warn(options); if (r < 0) return r; help_man_page_reference("systemd-sysupdate", "8"); 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); arg_component_all = false; break; } if (!component_name_valid(opts.arg)) 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; arg_component_all = false; break; OPTION('A', "component-all", NULL, "Process all components"): arg_component = mfree(arg_component); arg_component_all = true; 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_LONG("cleanup", "BOOL", "Clean up orphaned files after completing update"): { bool b; r = parse_boolean_argument("--cleanup=", opts.arg, &b); if (r < 0) return r; arg_cleanup = b; 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_reboot && arg_component) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=, as automatic reboots only apply to the booted OS version."); if (arg_definitions && arg_component) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined."); r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); if (r < 0) return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); if (r > 0) arg_varlink = true; *remaining_args = option_parser_get_args(&opts); return 1; } static int vl_server(void) { _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; _cleanup_(server_done) Server server = SERVER_NULL; int r; r = varlink_server_new(&varlink_server, SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA, &server); if (r < 0) return log_error_errno(r, "Failed to allocate Varlink server: %m"); r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_SysUpdate); if (r < 0) return log_error_errno(r, "Failed to add Varlink interface: %m"); r = sd_varlink_server_bind_method_many( varlink_server, "io.systemd.SysUpdate.CheckNew", vl_method_check_new); if (r < 0) return log_error_errno(r, "Failed to bind Varlink method: %m"); r = sd_varlink_server_loop_auto(varlink_server); if (r < 0) return log_error_errno(r, "Failed to run Varlink event loop: %m"); return 0; } 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; if (arg_varlink) return vl_server(); /* Invocation as Varlink service */ return dispatch_verb(args, NULL); } DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);