summaryrefslogtreecommitdiffstats
path: root/src/sysupdate/sysupdate.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/sysupdate/sysupdate.c1401
1 files changed, 1401 insertions, 0 deletions
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
new file mode 100644
index 0000000..7469f16
--- /dev/null
+++ b/src/sysupdate/sysupdate.c
@@ -0,0 +1,1401 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "chase-symlinks.h"
+#include "conf-files.h"
+#include "def.h"
+#include "dirent-util.h"
+#include "dissect-image.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "glyph-util.h"
+#include "hexdecoct.h"
+#include "login-util.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "os-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "set.h"
+#include "sort-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sysupdate-transfer.h"
+#include "sysupdate-update-set.h"
+#include "sysupdate.h"
+#include "terminal-util.h"
+#include "utf8.h"
+#include "verbs.h"
+
+static char *arg_definitions = NULL;
+bool arg_sync = true;
+uint64_t arg_instances_max = UINT64_MAX;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+char *arg_root = NULL;
+static char *arg_image = NULL;
+static bool arg_reboot = false;
+static char *arg_component = NULL;
+static int arg_verify = -1;
+
+STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
+
+typedef struct Context {
+ Transfer **transfers;
+ size_t n_transfers;
+
+ UpdateSet **update_sets;
+ size_t n_update_sets;
+
+ UpdateSet *newest_installed, *candidate;
+
+ Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */
+} Context;
+
+static Context *context_free(Context *c) {
+ if (!c)
+ return NULL;
+
+ for (size_t i = 0; i < c->n_transfers; i++)
+ transfer_free(c->transfers[i]);
+ free(c->transfers);
+
+ for (size_t i = 0; i < c->n_update_sets; i++)
+ update_set_free(c->update_sets[i]);
+ free(c->update_sets);
+
+ hashmap_free(c->web_cache);
+
+ return mfree(c);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free);
+
+static Context *context_new(void) {
+ /* For now, no fields to initialize non-zero */
+ return new0(Context, 1);
+}
+
+static int context_read_definitions(
+ Context *c,
+ const char *directory,
+ const char *component,
+ const char *root,
+ const char *node) {
+
+ _cleanup_strv_free_ char **files = NULL;
+ int r;
+
+ assert(c);
+
+ if (directory)
+ r = conf_files_list_strv(&files, ".conf", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) STRV_MAKE(directory));
+ else if (component) {
+ _cleanup_strv_free_ char **n = NULL;
+ char **l = CONF_PATHS_STRV("");
+ size_t k = 0;
+
+ n = new0(char*, strv_length(l) + 1);
+ if (!n)
+ return log_oom();
+
+ STRV_FOREACH(i, l) {
+ char *j;
+
+ j = strjoin(*i, "sysupdate.", component, ".d");
+ if (!j)
+ return log_oom();
+
+ n[k++] = j;
+ }
+
+ r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) n);
+ } else
+ r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) CONF_PATHS_STRV("sysupdate.d"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate *.conf files: %m");
+
+ STRV_FOREACH(f, files) {
+ _cleanup_(transfer_freep) Transfer *t = NULL;
+
+ if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1))
+ return log_oom();
+
+ t = transfer_new();
+ if (!t)
+ return log_oom();
+
+ t->definition_path = strdup(*f);
+ if (!t->definition_path)
+ return log_oom();
+
+ r = transfer_read_definition(t, *f);
+ if (r < 0)
+ return r;
+
+ c->transfers[c->n_transfers++] = TAKE_PTR(t);
+ }
+
+ if (c->n_transfers == 0) {
+ if (arg_component)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "No transfer definitions for component '%s' found.", arg_component);
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "No transfer definitions found.");
+ }
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_resolve_paths(c->transfers[i], root, node);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int context_load_installed_instances(Context *c) {
+ int r;
+
+ assert(c);
+
+ log_info("Discovering installed instances%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = resource_load_instances(
+ &c->transfers[i]->target,
+ arg_verify >= 0 ? arg_verify : c->transfers[i]->verify,
+ &c->web_cache);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int context_load_available_instances(Context *c) {
+ int r;
+
+ assert(c);
+
+ log_info("Discovering available instances%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ assert(c->transfers[i]);
+
+ r = resource_load_instances(
+ &c->transfers[i]->source,
+ arg_verify >= 0 ? arg_verify : c->transfers[i]->verify,
+ &c->web_cache);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) {
+ _cleanup_free_ Instance **cursor_instances = NULL;
+ _cleanup_free_ char *boundary = NULL;
+ bool newest_found = false;
+ int r;
+
+ assert(c);
+ assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED));
+
+ for (;;) {
+ bool incomplete = false, exists = false;
+ UpdateSetFlags extra_flags = 0;
+ _cleanup_free_ char *cursor = NULL;
+ UpdateSet *us = NULL;
+
+ for (size_t k = 0; k < c->n_transfers; k++) {
+ Transfer *t = c->transfers[k];
+ bool cursor_found = false;
+ Resource *rr;
+
+ assert(t);
+
+ if (flags == UPDATE_AVAILABLE)
+ rr = &t->source;
+ else {
+ assert(flags == UPDATE_INSTALLED);
+ rr = &t->target;
+ }
+
+ for (size_t j = 0; j < rr->n_instances; j++) {
+ Instance *i = rr->instances[j];
+
+ assert(i);
+
+ /* Is the instance we are looking at equal or newer than the boundary? If so, we
+ * already checked this version, and it wasn't complete, let's ignore it. */
+ if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0)
+ continue;
+
+ if (cursor) {
+ if (strverscmp_improved(i->metadata.version, cursor) != 0)
+ continue;
+ } else {
+ cursor = strdup(i->metadata.version);
+ if (!cursor)
+ return log_oom();
+ }
+
+ cursor_found = true;
+
+ if (!cursor_instances) {
+ cursor_instances = new(Instance*, c->n_transfers);
+ if (!cursor_instances)
+ return -ENOMEM;
+ }
+ cursor_instances[k] = i;
+ break;
+ }
+
+ if (!cursor) /* No suitable instance beyond the boundary found? Then we are done! */
+ break;
+
+ if (!cursor_found) {
+ /* Hmm, we didn't find the version indicated by 'cursor' among the instances
+ * of this transfer, let's skip it. */
+ incomplete = true;
+ break;
+ }
+
+ if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0)
+ extra_flags |= UPDATE_OBSOLETE;
+
+ if (strv_contains(t->protected_versions, cursor))
+ extra_flags |= UPDATE_PROTECTED;
+ }
+
+ if (!cursor) /* EOL */
+ break;
+
+ r = free_and_strdup_warn(&boundary, cursor);
+ if (r < 0)
+ return r;
+
+ if (incomplete) /* One transfer was missing this version, ignore the whole thing */
+ continue;
+
+ /* See if we already have this update set in our table */
+ for (size_t i = 0; i < c->n_update_sets; i++) {
+ if (strverscmp_improved(c->update_sets[i]->version, cursor) != 0)
+ continue;
+
+ /* We only store the instances we found first, but we remember we also found it again */
+ c->update_sets[i]->flags |= flags | extra_flags;
+ exists = true;
+ newest_found = true;
+ break;
+ }
+
+ if (exists)
+ continue;
+
+ /* Doesn't exist yet, let's add it */
+ if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1))
+ return log_oom();
+
+ us = new(UpdateSet, 1);
+ if (!us)
+ return log_oom();
+
+ *us = (UpdateSet) {
+ .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags,
+ .version = TAKE_PTR(cursor),
+ .instances = TAKE_PTR(cursor_instances),
+ .n_instances = c->n_transfers,
+ };
+
+ c->update_sets[c->n_update_sets++] = us;
+
+ newest_found = true;
+
+ /* Remember which one is the newest installed */
+ if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED))
+ c->newest_installed = us;
+
+ /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */
+ if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE))
+ c->candidate = us;
+ }
+
+ /* Newest installed is newer than or equal to candidate? Then suppress the candidate */
+ if (c->newest_installed && c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0)
+ c->candidate = NULL;
+
+ return 0;
+}
+
+static int context_discover_update_sets(Context *c) {
+ int r;
+
+ assert(c);
+
+ log_info("Determining installed update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED);
+ if (r < 0)
+ return r;
+
+ log_info("Determining available update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE);
+ if (r < 0)
+ return r;
+
+ typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp);
+ return 0;
+}
+
+static const char *update_set_flags_to_string(UpdateSetFlags flags) {
+
+ switch ((unsigned) flags) {
+
+ case 0:
+ return "n/a";
+
+ case UPDATE_INSTALLED|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "current";
+
+ case UPDATE_AVAILABLE|UPDATE_NEWEST:
+ case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "candidate";
+
+ case UPDATE_INSTALLED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE:
+ return "installed";
+
+ case UPDATE_INSTALLED|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PROTECTED:
+ return "protected";
+
+ case UPDATE_AVAILABLE:
+ case UPDATE_AVAILABLE|UPDATE_PROTECTED:
+ return "available";
+
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "current+obsolete";
+
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE:
+ return "installed+obsolete";
+
+ case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+ case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+ return "protected+obsolete";
+
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE:
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED:
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST:
+ case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED:
+ return "available+obsolete";
+
+ default:
+ assert_not_reached();
+ }
+}
+
+
+static int context_show_table(Context *c) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ int r;
+
+ assert(c);
+
+ t = table_new("", "version", "installed", "available", "assessment");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50);
+
+ for (size_t i = 0; i < c->n_update_sets; i++) {
+ UpdateSet *us = c->update_sets[i];
+ const char *color;
+
+ color = update_set_flags_to_color(us->flags);
+
+ r = table_add_many(t,
+ TABLE_STRING, update_set_flags_to_glyph(us->flags),
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, us->version,
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)),
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)),
+ TABLE_SET_COLOR, color,
+ TABLE_STRING, update_set_flags_to_string(us->flags),
+ TABLE_SET_COLOR, color);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static UpdateSet *context_update_set_by_version(Context *c, const char *version) {
+ assert(c);
+ assert(version);
+
+ for (size_t i = 0; i < c->n_update_sets; i++)
+ if (streq(c->update_sets[i]->version, version))
+ return c->update_sets[i];
+
+ return NULL;
+}
+
+static int context_show_version(Context *c, const char *version) {
+ bool show_fs_columns = false, show_partition_columns = false,
+ have_fs_attributes = false, have_partition_attributes = false,
+ have_size = false, have_tries = false, have_no_auto = false,
+ have_read_only = false, have_growfs = false, have_sha256 = false;
+ _cleanup_(table_unrefp) Table *t = NULL;
+ UpdateSet *us;
+ int r;
+
+ assert(c);
+ assert(version);
+
+ us = context_update_set_by_version(c, version);
+ if (!us)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
+
+ if (arg_json_format_flags & (JSON_FORMAT_OFF|JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+ (void) pager_open(arg_pager_flags);
+
+ if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF))
+ printf("%s%s%s Version: %s\n"
+ " State: %s%s%s\n"
+ "Installed: %s%s\n"
+ "Available: %s%s\n"
+ "Protected: %s%s%s\n"
+ " Obsolete: %s%s%s\n\n",
+ strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version,
+ strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(),
+ yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "",
+ yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "",
+ FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(),
+ us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal());
+
+
+ t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100);
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100);
+ table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+ /* Determine if the target will make use of partition/fs attributes for any of the transfers */
+ for (size_t n = 0; n < c->n_transfers; n++) {
+ Transfer *tr = c->transfers[n];
+
+ if (tr->target.type == RESOURCE_PARTITION)
+ show_partition_columns = true;
+ if (RESOURCE_IS_FILESYSTEM(tr->target.type))
+ show_fs_columns = true;
+ }
+
+ for (size_t n = 0; n < us->n_instances; n++) {
+ Instance *i = us->instances[n];
+
+ r = table_add_many(t,
+ TABLE_STRING, resource_type_to_string(i->resource->type),
+ TABLE_PATH, i->path);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.partition_uuid_set) {
+ have_partition_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.partition_flags_set) {
+ have_partition_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.mtime != USEC_INFINITY) {
+ have_fs_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.mode != MODE_INVALID) {
+ have_fs_attributes = true;
+ r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.size != UINT64_MAX) {
+ have_size = true;
+ r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.tries_done != UINT64_MAX) {
+ have_tries = true;
+ r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.tries_left != UINT64_MAX) {
+ have_tries = true;
+ r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.no_auto >= 0) {
+ bool b;
+
+ have_no_auto = true;
+ b = i->metadata.no_auto;
+ r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+ if (i->metadata.read_only >= 0) {
+ bool b;
+
+ have_read_only = true;
+ b = i->metadata.read_only;
+ r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.growfs >= 0) {
+ bool b;
+
+ have_growfs = true;
+ b = i->metadata.growfs;
+ r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (i->metadata.sha256sum_set) {
+ _cleanup_free_ char *formatted = NULL;
+
+ have_sha256 = true;
+
+ formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum));
+ if (!formatted)
+ return log_oom();
+
+ r = table_add_cell(t, NULL, TABLE_STRING, formatted);
+ } else
+ r = table_add_cell(t, NULL, TABLE_EMPTY, NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ /* Hide the fs/partition columns if we don't have any data to show there */
+ if (!have_fs_attributes)
+ show_fs_columns = false;
+ if (!have_partition_attributes)
+ show_partition_columns = false;
+
+ if (!show_partition_columns)
+ (void) table_hide_column_from_display(t, 2, 3);
+ if (!show_fs_columns)
+ (void) table_hide_column_from_display(t, 4, 5);
+ if (!have_size)
+ (void) table_hide_column_from_display(t, 6);
+ if (!have_tries)
+ (void) table_hide_column_from_display(t, 7, 8);
+ if (!have_no_auto)
+ (void) table_hide_column_from_display(t, 9);
+ if (!have_read_only)
+ (void) table_hide_column_from_display(t, 10);
+ if (!have_growfs)
+ (void) table_hide_column_from_display(t, 11);
+ if (!have_sha256)
+ (void) table_hide_column_from_display(t, 12);
+
+ return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static int context_vacuum(
+ Context *c,
+ uint64_t space,
+ const char *extra_protected_version) {
+
+ int r, count = 0;
+
+ assert(c);
+
+ if (space == 0)
+ log_info("Making room%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+ else
+ log_info("Making room for %" PRIu64 " updates%s", space,special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_vacuum(c->transfers[i], space, extra_protected_version);
+ if (r < 0)
+ return r;
+
+ count = MAX(count, r);
+ }
+
+ if (count > 0)
+ log_info("Removed %i instances.", count);
+ else
+ log_info("Removed no instances.");
+
+ return 0;
+}
+
+static int context_make_offline(Context **ret, const char *node) {
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(ret);
+
+ /* Allocates a context object and initializes everything we can initialize offline, i.e. without
+ * checking on the update source (i.e. the Internet) what versions are available */
+
+ context = context_new();
+ if (!context)
+ return log_oom();
+
+ r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node);
+ if (r < 0)
+ return r;
+
+ r = context_load_installed_instances(context);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(context);
+ return 0;
+}
+
+static int context_make_online(Context **ret, const char *node) {
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(ret);
+
+ /* Like context_make_offline(), but also communicates with the update source looking for new
+ * versions. */
+
+ r = context_make_offline(&context, node);
+ if (r < 0)
+ return r;
+
+ r = context_load_available_instances(context);
+ if (r < 0)
+ return r;
+
+ r = context_discover_update_sets(context);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(context);
+ return 0;
+}
+
+static int context_apply(
+ Context *c,
+ const char *version,
+ UpdateSet **ret_applied) {
+
+ UpdateSet *us = NULL;
+ int r;
+
+ assert(c);
+
+ if (version) {
+ us = context_update_set_by_version(c, version);
+ if (!us)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version);
+ } else {
+ if (!c->candidate) {
+ log_info("No update needed.");
+
+ if (ret_applied)
+ *ret_applied = NULL;
+
+ return 0;
+ }
+
+ us = c->candidate;
+ }
+
+ if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) {
+ log_info("Selected update '%s' is already installed. Skipping update.", us->version);
+
+ if (ret_applied)
+ *ret_applied = NULL;
+
+ return 0;
+ }
+ if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version);
+ if (FLAGS_SET(us->flags, UPDATE_OBSOLETE))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version);
+
+ assert((us->flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_OBSOLETE)) == UPDATE_AVAILABLE);
+
+ if (!FLAGS_SET(us->flags, UPDATE_NEWEST))
+ log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version);
+ if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0)
+ log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version);
+
+ log_info("Selected update '%s' for install.", us->version);
+
+ (void) sd_notifyf(false,
+ "STATUS=Making room for '%s'.", us->version);
+
+ /* Let's make some room. We make sure for each transfer we have one free space to fill. While
+ * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier
+ * download succeeded already, in which case we shouldn't remove it just to acquire it again */
+ r = context_vacuum(
+ c,
+ /* space = */ 1,
+ /* extra_protected_version = */ us->version);
+ if (r < 0)
+ return r;
+
+ if (arg_sync)
+ sync();
+
+ (void) sd_notifyf(false,
+ "STATUS=Updating to '%s'.\n", us->version);
+
+ /* There should now be one instance picked for each transfer, and the order is the same */
+ assert(us->n_instances == c->n_transfers);
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_acquire_instance(c->transfers[i], us->instances[i]);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_sync)
+ sync();
+
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ r = transfer_install_instance(c->transfers[i], us->instances[i], arg_root);
+ if (r < 0)
+ return r;
+ }
+
+ log_info("%s Successfully installed update '%s'.", special_glyph(SPECIAL_GLYPH_SPARKLES), us->version);
+
+ if (ret_applied)
+ *ret_applied = us;
+
+ return 1;
+}
+
+static int reboot_now(void) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open bus connection: %m");
+
+ r = bus_call_method(bus, bus_login_mgr, "RebootWithFlags", &error, NULL, "t",
+ (uint64_t) SD_LOGIND_ROOT_CHECK_INHIBITORS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to issue reboot request: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int process_image(
+ bool ro,
+ char **ret_mounted_dir,
+ LoopDevice **ret_loop_device) {
+
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ int r;
+
+ assert(ret_mounted_dir);
+ assert(ret_loop_device);
+
+ if (!arg_image)
+ return 0;
+
+ assert(!arg_root);
+
+ r = mount_image_privately_interactively(
+ arg_image,
+ (ro ? DISSECT_IMAGE_READ_ONLY : 0) |
+ DISSECT_IMAGE_FSCK |
+ DISSECT_IMAGE_MKDIR |
+ DISSECT_IMAGE_GROWFS |
+ DISSECT_IMAGE_RELAX_VAR_CHECK |
+ DISSECT_IMAGE_USR_NO_ROOT |
+ DISSECT_IMAGE_GENERIC_ROOT |
+ DISSECT_IMAGE_REQUIRE_ROOT,
+ &mounted_dir,
+ &loop_device);
+ if (r < 0)
+ return r;
+
+ arg_root = strdup(mounted_dir);
+ if (!arg_root)
+ return log_oom();
+
+ *ret_mounted_dir = TAKE_PTR(mounted_dir);
+ *ret_loop_device = TAKE_PTR(loop_device);
+
+ return 0;
+}
+
+static int verb_list(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ const char *version;
+ int r;
+
+ assert(argc <= 2);
+ version = argc >= 2 ? argv[1] : NULL;
+
+ r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
+ if (r < 0)
+ return r;
+
+ r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ if (version)
+ return context_show_version(context, version);
+ else
+ return context_show_table(context);
+}
+
+static int verb_check_new(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(argc <= 1);
+
+ r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
+ if (r < 0)
+ return r;
+
+ r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ if (!context->candidate) {
+ log_debug("No candidate found.");
+ return EXIT_FAILURE;
+ }
+
+ puts(context->candidate->version);
+ return EXIT_SUCCESS;
+}
+
+static int verb_vacuum(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ int r;
+
+ assert(argc <= 1);
+
+ r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
+ if (r < 0)
+ return r;
+
+ r = context_make_offline(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ return context_vacuum(context, 0, NULL);
+}
+
+static int verb_update(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(context_freep) Context* context = NULL;
+ _cleanup_free_ char *booted_version = NULL;
+ UpdateSet *applied = NULL;
+ const char *version;
+ int r;
+
+ assert(argc <= 2);
+ version = argc >= 2 ? argv[1] : NULL;
+
+ if (arg_reboot) {
+ /* If automatic reboot on completion is requested, let's first determine the currently booted image */
+
+ r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse /etc/os-release: %m");
+ if (!booted_version)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field.");
+ }
+
+ r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
+ if (r < 0)
+ return r;
+
+ r = context_make_online(&context, loop_device ? loop_device->node : NULL);
+ if (r < 0)
+ return r;
+
+ r = context_apply(context, version, &applied);
+ if (r < 0)
+ return r;
+
+ if (r > 0 && arg_reboot) {
+ assert(applied);
+ assert(booted_version);
+
+ if (strverscmp_improved(applied->version, booted_version) > 0) {
+ log_notice("Newly installed version is newer than booted version, rebooting.");
+ return reboot_now();
+ }
+
+ log_info("Booted version is newer or identical to newly installed version, not rebooting.");
+ }
+
+ return 0;
+}
+
+static int verb_pending_or_reboot(int argc, char **argv, void *userdata) {
+ _cleanup_(context_freep) Context* context = NULL;
+ _cleanup_free_ char *booted_version = NULL;
+ int r;
+
+ assert(argc == 1);
+
+ if (arg_image || arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "The --root=/--image switches may not be combined with the '%s' operation.", argv[0]);
+
+ r = context_make_offline(&context, NULL);
+ if (r < 0)
+ return r;
+
+ log_info("Determining installed update sets%s", special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED);
+ if (r < 0)
+ return r;
+ if (!context->newest_installed)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions.");
+
+ r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version);
+ if (r < 0) /* yes, arg_root is NULL here, but we have to pass something, and it's a lot more readable
+ * if we see what the first argument is about */
+ return log_error_errno(r, "Failed to parse /etc/os-release: %m");
+ if (!booted_version)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field.");
+
+ r = strverscmp_improved(context->newest_installed->version, booted_version);
+ if (r > 0) {
+ log_notice("Newest installed version '%s' is newer than booted version '%s'.%s",
+ context->newest_installed->version, booted_version,
+ streq(argv[0], "pending") ? " Reboot recommended." : "");
+
+ if (streq(argv[0], "reboot"))
+ return reboot_now();
+
+ return EXIT_SUCCESS;
+ } else if (r == 0)
+ log_info("Newest installed version '%s' matches booted version '%s'.",
+ context->newest_installed->version, booted_version);
+ else
+ log_warning("Newest installed version '%s' is older than booted version '%s'.",
+ context->newest_installed->version, booted_version);
+
+ if (streq(argv[0], "pending")) /* When called as 'pending' tell the caller via failure exit code that there's nothing newer installed */
+ return EXIT_FAILURE;
+
+ return EXIT_SUCCESS;
+}
+
+static int component_name_valid(const char *c) {
+ _cleanup_free_ char *j = NULL;
+
+ /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */
+
+ if (isempty(c))
+ return false;
+
+ if (string_has_cc(c, NULL))
+ return false;
+
+ if (!utf8_is_valid(c))
+ return false;
+
+ j = strjoin("sysupdate.", c, ".d");
+ if (!j)
+ return -ENOMEM;
+
+ return filename_is_valid(j);
+}
+
+static int verb_components(int argc, char **argv, void *userdata) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
+ _cleanup_(set_freep) Set *names = NULL;
+ _cleanup_free_ char **z = NULL; /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */
+ char **l = CONF_PATHS_STRV("");
+ bool has_default_component = false;
+ int r;
+
+ assert(argc <= 1);
+
+ r = process_image(/* ro= */ false, &mounted_dir, &loop_device);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, l) {
+ _cleanup_closedir_ DIR *d = NULL;
+ _cleanup_free_ char *p = NULL;
+
+ r = chase_symlinks_and_opendir(*i, arg_root, CHASE_PREFIX_ROOT, &p, &d);
+ if (r == -ENOENT)
+ continue;
+ if (r < 0)
+ return log_error_errno(r, "Failed to open directory '%s': %m", *i);
+
+ for (;;) {
+ _cleanup_free_ char *n = NULL;
+ struct dirent *de;
+ const char *e, *a;
+
+ de = readdir_ensure_type(d);
+ if (!de) {
+ if (errno != 0)
+ return log_error_errno(errno, "Failed to enumerate directory '%s': %m", p);
+
+ break;
+ }
+
+ if (de->d_type != DT_DIR)
+ continue;
+
+ if (dot_or_dot_dot(de->d_name))
+ continue;
+
+ if (streq(de->d_name, "sysupdate.d")) {
+ has_default_component = true;
+ continue;
+ }
+
+ e = startswith(de->d_name, "sysupdate.");
+ if (!e)
+ continue;
+
+ a = endswith(e, ".d");
+ if (!a)
+ continue;
+
+ n = strndup(e, a - e);
+ if (!n)
+ return log_oom();
+
+ r = component_name_valid(n);
+ if (r < 0)
+ return log_error_errno(r, "Unable to validate component name: %m");
+ if (r == 0)
+ continue;
+
+ r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n));
+ if (r < 0 && r != -EEXIST)
+ return log_error_errno(r, "Failed to add component to set: %m");
+ }
+ }
+
+ if (!has_default_component && set_isempty(names)) {
+ log_info("No components defined.");
+ return 0;
+ }
+
+ z = set_get_strv(names);
+ if (!z)
+ return log_oom();
+
+ strv_sort(z);
+
+ if (has_default_component)
+ printf("%s<default>%s\n",
+ ansi_highlight(), ansi_normal());
+
+ STRV_FOREACH(i, z)
+ puts(*i);
+
+ return 0;
+}
+
+static int verb_help(int argc, char **argv, void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-sysupdate", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] [VERSION]\n"
+ "\n%5$sUpdate OS images.%6$s\n"
+ "\n%3$sCommands:%4$s\n"
+ " list [VERSION] Show installed and available versions\n"
+ " check-new Check if there's a new version available\n"
+ " update [VERSION] Install new version now\n"
+ " vacuum Make room, by deleting old versions\n"
+ " pending Report whether a newer version is installed than\n"
+ " currently booted\n"
+ " reboot Reboot if a newer version is installed than booted\n"
+ " components Show list of components\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ "\n%3$sOptions:%4$s\n"
+ " -C --component=NAME Select component to update\n"
+ " --definitions=DIR Find transfer definitions in specified directory\n"
+ " --root=PATH Operate relative to root path\n"
+ " --image=PATH Operate relative to image file\n"
+ " -m --instances-max=INT How many instances to maintain\n"
+ " --sync=BOOL Controls whether to sync data to disk\n"
+ " --verify=BOOL Force signature verification on or off\n"
+ " --reboot Reboot after updating to newer version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --json=pretty|short|off\n"
+ " Generate JSON output\n"
+ "\nSee the %2$s for details.\n"
+ , program_invocation_short_name
+ , link
+ , ansi_underline(), ansi_normal()
+ , ansi_highlight(), ansi_normal()
+ );
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_SYNC,
+ ARG_DEFINITIONS,
+ ARG_JSON,
+ ARG_ROOT,
+ ARG_IMAGE,
+ ARG_REBOOT,
+ ARG_VERIFY,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "definitions", required_argument, NULL, ARG_DEFINITIONS },
+ { "instances-max", required_argument, NULL, 'm' },
+ { "sync", required_argument, NULL, ARG_SYNC },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "root", required_argument, NULL, ARG_ROOT },
+ { "image", required_argument, NULL, ARG_IMAGE },
+ { "reboot", no_argument, NULL, ARG_REBOOT },
+ { "component", required_argument, NULL, 'C' },
+ { "verify", required_argument, NULL, ARG_VERIFY },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hm:C:", options, NULL)) >= 0) {
+
+ switch (c) {
+
+ case 'h':
+ return verb_help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case 'm':
+ r = safe_atou64(optarg, &arg_instances_max);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", optarg);
+
+ break;
+
+ case ARG_SYNC:
+ r = parse_boolean_argument("--sync=", optarg, &arg_sync);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_DEFINITIONS:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_definitions);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+
+ break;
+
+ case ARG_ROOT:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_IMAGE:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_REBOOT:
+ arg_reboot = true;
+ break;
+
+ case 'C':
+ if (isempty(optarg)) {
+ arg_component = mfree(arg_component);
+ break;
+ }
+
+ r = component_name_valid(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine if component name is valid: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", optarg);
+
+ r = free_and_strdup_warn(&arg_component, optarg);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_VERIFY: {
+ bool b;
+
+ r = parse_boolean_argument("--verify=", optarg, &b);
+ if (r < 0)
+ return r;
+
+ arg_verify = b;
+ break;
+ }
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ if (arg_image && arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
+
+ if ((arg_image || arg_root) && arg_reboot)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
+
+ if (arg_definitions && arg_component)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined.");
+
+ return 1;
+}
+
+static int sysupdate_main(int argc, char *argv[]) {
+
+ static const Verb verbs[] = {
+ { "list", VERB_ANY, 2, VERB_DEFAULT, verb_list },
+ { "components", VERB_ANY, 1, 0, verb_components },
+ { "check-new", VERB_ANY, 1, 0, verb_check_new },
+ { "update", VERB_ANY, 2, 0, verb_update },
+ { "vacuum", VERB_ANY, 1, 0, verb_vacuum },
+ { "reboot", 1, 1, 0, verb_pending_or_reboot },
+ { "pending", 1, 1, 0, verb_pending_or_reboot },
+ { "help", VERB_ANY, 1, 0, verb_help },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return sysupdate_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);