This will be used in the following commit to add a varlink interface for it. This introduces no functional changes.
3224 lines
131 KiB
C
3224 lines
131 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||
|
||
#include <unistd.h>
|
||
|
||
#include "sd-bus.h"
|
||
#include "sd-daemon.h"
|
||
#include "sd-json.h"
|
||
#include "sd-varlink.h"
|
||
|
||
#include "build.h"
|
||
#include "bus-polkit.h"
|
||
#include "condition.h"
|
||
#include "conf-files.h"
|
||
#include "conf-parser.h"
|
||
#include "constants.h"
|
||
#include "discover-image.h"
|
||
#include "dissect-image.h"
|
||
#include "dlopen-note.h"
|
||
#include "dropin.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 "path-util.h"
|
||
#include "pretty-print.h"
|
||
#include "runtime-scope.h"
|
||
#include "sort-util.h"
|
||
#include "stdio-util.h"
|
||
#include "string-util.h"
|
||
#include "strv.h"
|
||
#include "sysupdate.h"
|
||
#include "sysupdate-cleanup.h"
|
||
#include "sysupdate-config.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 SelectMode arg_feature_select = SELECT_EXPLICIT;
|
||
static char *arg_component = NULL;
|
||
static SelectMode arg_component_select = SELECT_EXPLICIT;
|
||
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);
|
||
|
||
#define CONTEXT_NULL \
|
||
(Context) { \
|
||
.component_enabled = true, \
|
||
.sync = true, \
|
||
.instances_max = UINT64_MAX, \
|
||
.verify = -1, \
|
||
.cleanup = -1, \
|
||
.installdb_fd = -EBADF, \
|
||
.target_identifier.class = _TARGET_CLASS_INVALID, \
|
||
.component_suggest = -1, \
|
||
}
|
||
|
||
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->component_description = mfree(c->component_description);
|
||
c->component_documentation = strv_free(c->component_documentation);
|
||
c->image_policy = image_policy_free(c->image_policy);
|
||
c->transfer_source = mfree(c->transfer_source);
|
||
|
||
target_identifier_done(&c->target_identifier);
|
||
condition_free_list(c->component_suggest_on);
|
||
}
|
||
|
||
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_select = arg_component_select;
|
||
context.feature_select = arg_feature_select;
|
||
|
||
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;
|
||
}
|
||
|
||
static int context_from_base_with_component(const Context *base, const char *component, Context *ret) {
|
||
assert(base);
|
||
assert(component);
|
||
assert(ret);
|
||
|
||
/* Copies the specified context, but changes the component to the specified one */
|
||
|
||
_cleanup_(context_done) Context context = CONTEXT_NULL;
|
||
|
||
context.instances_max = base->instances_max;
|
||
context.sync = base->sync;
|
||
context.reboot = base->reboot;
|
||
context.verify = base->verify;
|
||
context.offline = base->offline;
|
||
context.cleanup = base->cleanup;
|
||
context.feature_select = base->feature_select;
|
||
|
||
if (strdup_to(&context.root, base->root) < 0)
|
||
return log_oom();
|
||
|
||
if (strdup_to(&context.image, base->image) < 0)
|
||
return log_oom();
|
||
|
||
if (strdup_to(&context.transfer_source, base->transfer_source) < 0)
|
||
return log_oom();
|
||
|
||
if (base->image_policy) {
|
||
context.image_policy = image_policy_copy(base->image_policy);
|
||
if (!context.image_policy)
|
||
return log_oom();
|
||
}
|
||
|
||
if (strdup_to(&context.component, component) < 0)
|
||
return log_oom();
|
||
|
||
/* NB: we do not copy .loop_device/.mounted_dir here, since that's for lifetime tracking only, and
|
||
* not needed for access (we only need to copy .root/.image) for that. Under the assumption that the
|
||
* contexts initialized via context_from_base_with_component() have a shorter lifetime than the
|
||
* contexts they are copied from we don't bother with lifetime tracking of the loopback device/mount
|
||
* point here. */
|
||
|
||
*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_features(
|
||
Context *c,
|
||
const char **dirs) {
|
||
|
||
int r;
|
||
|
||
assert(c);
|
||
|
||
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,
|
||
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) {
|
||
ConfFile *e = *i;
|
||
|
||
_cleanup_(feature_unrefp) Feature *f = feature_new();
|
||
if (!f)
|
||
return log_oom();
|
||
|
||
r = feature_read_definition(f, c->root, e->result, 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);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
static int read_transfers(
|
||
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;
|
||
}
|
||
|
||
static int read_component(Context *c) {
|
||
int r;
|
||
|
||
assert(c);
|
||
|
||
/* Read a component description file, but only if we actually operate on a component */
|
||
if (c->definitions || !c->component)
|
||
return 0;
|
||
|
||
_cleanup_free_ char *j = strjoin("sysupdate.", c->component, ".component");
|
||
if (!j)
|
||
return log_oom();
|
||
|
||
ConfigTableItem table[] = {
|
||
{ "Component", "Description", config_parse_string, 0, &c->component_description },
|
||
{ "Component", "Documentation", config_parse_url_specifiers_many, 0, &c->component_documentation },
|
||
{ "Component", "Enabled", config_parse_bool, 0, &c->component_enabled },
|
||
{ "Component", "Suggest", config_parse_tristate, 0, &c->component_suggest },
|
||
{ "Component", "SuggestOnArchitecture", config_parse_condition, CONDITION_ARCHITECTURE, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnFirmware", config_parse_condition, CONDITION_FIRMWARE, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnVirtualization", config_parse_condition, CONDITION_VIRTUALIZATION, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnHost", config_parse_condition, CONDITION_HOST, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnFraction", config_parse_condition, CONDITION_FRACTION, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnKernelCommandLine", config_parse_condition, CONDITION_KERNEL_COMMAND_LINE, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnVersion", config_parse_condition, CONDITION_VERSION, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnCredential", config_parse_condition, CONDITION_CREDENTIAL, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnSecurity", config_parse_condition, CONDITION_SECURITY, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnOSRelease", config_parse_condition, CONDITION_OS_RELEASE, &c->component_suggest_on },
|
||
{ "Component", "SuggestOnMachineTag", config_parse_condition, CONDITION_MACHINE_TAG, &c->component_suggest_on },
|
||
{}
|
||
};
|
||
|
||
r = config_parse_standard_file_with_dropins_full(
|
||
c->root,
|
||
/* root_fd= */ -EBADF,
|
||
j,
|
||
"Component\0",
|
||
config_item_table_lookup, table,
|
||
CONFIG_PARSE_WARN,
|
||
/* userdata= */ (void *) c->root,
|
||
/* ret_stats_by_path= */ NULL,
|
||
/* ret_dropin_files= */ NULL);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
return 0;
|
||
}
|
||
|
||
typedef enum ReadDefinitionsFlags {
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS = 1 << 0, /* fail unless there's at least one enabled transfer */
|
||
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS = 1 << 1, /* fail unless there's at least one transfer */
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT = 1 << 2, /* fail if component is disabled */
|
||
} 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();
|
||
|
||
r = read_component(c);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
r = read_features(c, (const char**) dirs);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
r = read_transfers(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_transfers(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.");
|
||
}
|
||
|
||
if (FLAGS_SET(flags, READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT) && !c->component_enabled)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Component is disabled.");
|
||
|
||
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 context_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->root || !c->image) /* Idempotent or nothing to do */
|
||
return 0;
|
||
|
||
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 ***ret_component_names, bool *ret_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 = context_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,
|
||
ReadDefinitionsFlags read_definitions_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_flags);
|
||
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) {
|
||
/* systemd-sysupdate doesn't support mstack images yet */
|
||
return IN_SET(image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME, IMAGE_RAW, IMAGE_BLOCK);
|
||
}
|
||
|
||
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,
|
||
ReadDefinitionsFlags read_definitions_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, /* ret_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, /* ret_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, read_definitions_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 int context_get_transfers_for_feature(Context *context, const char *feature_id, char ***ret_transfers) {
|
||
int r;
|
||
|
||
assert(context);
|
||
assert(feature_id);
|
||
|
||
FOREACH_ARRAY(tr, context->transfers, context->n_transfers) {
|
||
Transfer *t = *tr;
|
||
|
||
if (!strv_contains(t->features, feature_id) && !strv_contains(t->requisite_features, feature_id))
|
||
continue;
|
||
|
||
r = strv_extend(ret_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, feature_id) && !strv_contains(t->requisite_features, feature_id))
|
||
continue;
|
||
|
||
r = strv_extend(ret_transfers, t->id);
|
||
if (r < 0)
|
||
return log_oom();
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
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.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
|
||
|
||
r = context_load_online(
|
||
&context,
|
||
PROCESS_IMAGE_READ_ONLY,
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
|
||
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
|
||
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.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested 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();
|
||
|
||
r = context_get_transfers_for_feature(&context, f->id, &transfers);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
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);
|
||
|
||
r = feature_is_suggested(f);
|
||
if (r < 0) {
|
||
errno = -r; /* Let's make %m below show this error */
|
||
_cleanup_free_ char *k = asprintf_safe("error (%m)");
|
||
r = table_add_many(table,
|
||
TABLE_FIELD, "Suggested",
|
||
TABLE_STRING, k);
|
||
} else
|
||
r = table_add_many(table,
|
||
TABLE_FIELD, "Suggested",
|
||
TABLE_BOOLEAN, r);
|
||
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("enabled", "suggested", "feature", "description", "documentation");
|
||
if (!table)
|
||
return log_oom();
|
||
|
||
table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
|
||
|
||
HASHMAP_FOREACH(f, context.features) {
|
||
r = table_add_many(table,
|
||
TABLE_BOOLEAN_CHECKMARK, f->enabled,
|
||
TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled));
|
||
if (r < 0)
|
||
return table_log_add_error(r);
|
||
|
||
r = feature_is_suggested(f);
|
||
if (r < 0)
|
||
r = table_add_many(table, TABLE_EMPTY);
|
||
else
|
||
r = table_add_many(table, TABLE_BOOLEAN_CHECKMARK, r);
|
||
if (r < 0)
|
||
return table_log_add_error(r);
|
||
|
||
r = table_add_many(table,
|
||
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);
|
||
}
|
||
|
||
r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (arg_legend) {
|
||
if (table_isempty(table))
|
||
log_info("No features.");
|
||
else
|
||
printf("\n%zu features listed.\n", table_get_rows(table) - 1);
|
||
}
|
||
} 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;
|
||
}
|
||
|
||
static int make_dropin_dir(Context *c, char **ret) {
|
||
_cleanup_free_ char *dir = NULL;
|
||
|
||
assert(c);
|
||
assert(ret);
|
||
|
||
/* Returns the (writable) directory where feature definitions live, so that we can drop our
|
||
* 'Enabled=' override right next to them. This mirrors the directory logic in
|
||
* context_read_definitions(), but settles on a single writable location below /etc. */
|
||
|
||
if (c->definitions)
|
||
dir = strdup(c->definitions); /* --root= is not supported for this for now */
|
||
else if (c->component) {
|
||
_cleanup_free_ char *n = strjoin("sysupdate.", c->component, ".d");
|
||
if (!n)
|
||
return log_oom();
|
||
|
||
dir = path_join(c->root, SYSCONF_DIR, n);
|
||
} else
|
||
dir = path_join(c->root, SYSCONF_DIR "/sysupdate.d");
|
||
if (!dir)
|
||
return log_oom();
|
||
|
||
*ret = TAKE_PTR(dir);
|
||
return 0;
|
||
}
|
||
|
||
static const char *context_component_display(const Context *c) {
|
||
assert(c);
|
||
|
||
return c->component ?: "<default>";
|
||
}
|
||
|
||
static int context_enable_feature(
|
||
Context *c,
|
||
const char *argv0,
|
||
bool enable,
|
||
char **features) {
|
||
|
||
int r;
|
||
|
||
assert(c);
|
||
assert(argv0);
|
||
|
||
_cleanup_free_ char **l = NULL;
|
||
switch (c->feature_select) {
|
||
|
||
case SELECT_EXPLICIT:
|
||
break;
|
||
|
||
case SELECT_ALL: {
|
||
assert(!features);
|
||
|
||
Feature *f;
|
||
HASHMAP_FOREACH(f, c->features)
|
||
if (strv_push(&l, f->id) < 0)
|
||
return log_oom();
|
||
|
||
features = l;
|
||
break;
|
||
}
|
||
|
||
case SELECT_SUGGESTED: {
|
||
assert(!features);
|
||
|
||
Feature *f;
|
||
HASHMAP_FOREACH(f, c->features) {
|
||
r = feature_is_suggested(f);
|
||
if (r < 0)
|
||
return log_error_errno(r, "Failed to determine if feature '%s' of component '%s' shall be enabled: %m", f->id, context_component_display(c));
|
||
if (!!r != !!enable) {
|
||
log_debug("Skipping feature '%s' of component '%s'.", f->id, context_component_display(c));
|
||
continue;
|
||
}
|
||
|
||
log_info("%s feature '%s' of component '%s'.", enable ? "Enabling" : "Disabling", f->id, context_component_display(c));
|
||
|
||
if (strv_push(&l, f->id) < 0)
|
||
return log_oom();
|
||
}
|
||
|
||
features = l;
|
||
break;
|
||
}
|
||
|
||
default:
|
||
assert_not_reached();
|
||
}
|
||
|
||
if (strv_isempty(features)) {
|
||
log_debug("No features selected.");
|
||
return 0;
|
||
}
|
||
|
||
_cleanup_free_ char *dropin_dir = NULL;
|
||
r = make_dropin_dir(c, &dropin_dir);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
int ret = 0;
|
||
STRV_FOREACH(name, features) {
|
||
if (!hashmap_contains(c->features, *name)) {
|
||
RET_GATHER(ret, log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Optional feature not found in component '%s': %s", context_component_display(c), *name));
|
||
continue;
|
||
}
|
||
|
||
_cleanup_free_ char *fname = strjoin(*name, ".feature");
|
||
if (!fname)
|
||
return log_oom();
|
||
|
||
/* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */
|
||
r = write_drop_in_format(
|
||
dropin_dir,
|
||
fname,
|
||
50,
|
||
"systemd-sysupdate-enabled",
|
||
"# Generated via 'systemd-sysupdate %s'\n\n"
|
||
"[Feature]\n"
|
||
"Enabled=%s\n",
|
||
argv0,
|
||
yes_no(enable));
|
||
if (r < 0) {
|
||
RET_GATHER(ret, log_error_errno(r, "Failed to write drop-in for feature '%s': %m", *name));
|
||
continue;
|
||
}
|
||
|
||
log_info("Feature '%s' %s.", *name, enabled_disabled(enable));
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
VERB(verb_enable_feature, "enable-feature", "FEATURE…", 1, VERB_ANY, 0,
|
||
"Enable optional feature");
|
||
VERB(verb_enable_feature, "disable-feature", "FEATURE…", 1, VERB_ANY, 0,
|
||
"Disable optional feature");
|
||
static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||
bool enable = streq(argv[0], "enable-feature");
|
||
int r;
|
||
|
||
_cleanup_(context_done) Context context = CONTEXT_NULL;
|
||
r = context_from_cmdline(&context);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL))
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--component-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.definitions)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||
"The --definitions= switch may not be combined with '%s'.", argv[0]);
|
||
|
||
char **features;
|
||
if (argc > 1 && context.feature_select == SELECT_EXPLICIT) {
|
||
features = strv_skip(argv, 1);
|
||
|
||
STRV_FOREACH(name, features)
|
||
if (!feature_name_valid(*name))
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature name invalid: %s", *name);
|
||
|
||
} else if (argc <= 1 && context.feature_select != SELECT_EXPLICIT)
|
||
features = NULL;
|
||
else
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify features as positional parameter or via --feature-all/--feature-suggested, not both.");
|
||
|
||
r = context_load_offline(
|
||
&context,
|
||
/* process_image_flags= */ 0,
|
||
/* read_definitions_flags= */ 0);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
switch (context.component_select) {
|
||
|
||
case SELECT_EXPLICIT:
|
||
return context_enable_feature(&context, argv[0], enable, features);
|
||
|
||
case SELECT_ALL: {
|
||
_cleanup_strv_free_ char **component_names = NULL;
|
||
bool has_default_component;
|
||
r = context_list_components(&context, &component_names, &has_default_component);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (strv_isempty(component_names) && !has_default_component) {
|
||
log_debug("No components selected.");
|
||
return 0;
|
||
}
|
||
|
||
int ret = 0;
|
||
if (has_default_component)
|
||
RET_GATHER(ret, context_enable_feature(&context, argv[0], enable, features));
|
||
|
||
STRV_FOREACH(name, component_names) {
|
||
_cleanup_(context_done) Context cc = CONTEXT_NULL;
|
||
|
||
r = context_from_base_with_component(&context, *name, &cc);
|
||
if (r < 0) {
|
||
RET_GATHER(ret, r);
|
||
continue;
|
||
}
|
||
|
||
r = context_load_offline(
|
||
&cc,
|
||
/* process_image_flags= */ 0,
|
||
/* read_definitions_flags= */ 0);
|
||
if (r < 0) {
|
||
RET_GATHER(ret, r);
|
||
continue;
|
||
}
|
||
|
||
RET_GATHER(ret, context_enable_feature(&cc, argv[0], enable, features));
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
default:
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
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) {
|
||
int r;
|
||
|
||
assert(argc <= 1);
|
||
|
||
_cleanup_(context_done) Context context = CONTEXT_NULL;
|
||
r = context_from_cmdline(&context);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (context.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
|
||
|
||
r = context_load_online(
|
||
&context,
|
||
PROCESS_IMAGE_READ_ONLY,
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
|
||
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
|
||
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,
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
|
||
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS);
|
||
if (r == -ENOENT)
|
||
return sd_varlink_error(link, "io.systemd.SysUpdate.NoSuchTarget", NULL);
|
||
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_free_ char *booted_version = NULL;
|
||
UpdateSet *applied = NULL;
|
||
const char *version;
|
||
int r;
|
||
|
||
assert(argc <= 2);
|
||
version = argc >= 2 ? argv[1] : NULL;
|
||
|
||
_cleanup_(context_done) Context context = CONTEXT_NULL;
|
||
r = context_from_cmdline(&context);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (context.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested 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,
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS|
|
||
READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS|
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT);
|
||
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.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested 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_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.");
|
||
|
||
if (context.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL))
|
||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-suggested 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;
|
||
|
||
int ret = 0;
|
||
RET_GATHER(ret, installdb_cleanup_component(&context));
|
||
|
||
if (context.component_select == SELECT_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;
|
||
}
|
||
|
||
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.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.component || context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||
"The --component=, --component-all and --component-suggested 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|
|
||
READ_DEFINITIONS_REQUIRES_ENABLED_COMPONENT);
|
||
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 ***ret_component_names, bool *ret_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 (ret_component_names)
|
||
*ret_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 (ret_has_default_component)
|
||
*ret_has_default_component =
|
||
!context->definitions &&
|
||
!context->component &&
|
||
!context->root &&
|
||
!context->image &&
|
||
context->n_transfers > 0;
|
||
|
||
return 0;
|
||
}
|
||
|
||
static int context_component_is_suggested(Context *c) {
|
||
assert(c);
|
||
|
||
/* Only applies to components, not to the main system */
|
||
if (!c->component)
|
||
return -ENOTTY;
|
||
|
||
if (c->component_suggest >= 0)
|
||
return c->component_suggest;
|
||
|
||
if (!c->component_suggest_on) /* no condition → false */
|
||
return false;
|
||
|
||
return condition_test_list(c->component_suggest_on, environ, suggest_on_type_to_string, /* logger= */ NULL, /* userdata= */ NULL);
|
||
}
|
||
|
||
VERB_NOARG(verb_components, "components",
|
||
"Show list of components");
|
||
static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||
int r;
|
||
|
||
assert(argc <= 1);
|
||
|
||
_cleanup_(context_done) Context context = CONTEXT_NULL;
|
||
r = context_from_cmdline(&context);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (context.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]);
|
||
if (context.definitions)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||
"The --definitions= switch may not be combined with '%s'.", argv[0]);
|
||
|
||
r = context_load_offline(
|
||
&context,
|
||
/* process_image_flags= */ 0,
|
||
/* read_definitions_flags= */ 0);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
_cleanup_strv_free_ char **component_names = NULL;
|
||
bool has_default_component = false;
|
||
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;
|
||
}
|
||
|
||
_cleanup_(table_unrefp) Table *t = table_new("enabled", "suggested", "component", "description", "documentation");
|
||
if (!t)
|
||
return log_oom();
|
||
|
||
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
|
||
|
||
if (has_default_component) {
|
||
r = table_add_many(
|
||
t,
|
||
TABLE_EMPTY,
|
||
TABLE_EMPTY,
|
||
TABLE_STRING, "<default>",
|
||
TABLE_SET_COLOR, ansi_highlight(),
|
||
TABLE_STRING, "Default Component",
|
||
TABLE_EMPTY);
|
||
if (r < 0)
|
||
return table_log_add_error(r);
|
||
}
|
||
|
||
STRV_FOREACH(i, component_names) {
|
||
_cleanup_(context_done) Context cc = CONTEXT_NULL;
|
||
|
||
r = context_from_base_with_component(
|
||
&context,
|
||
*i,
|
||
&cc);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
r = context_load_offline(
|
||
&cc,
|
||
/* process_image_flags= */ 0,
|
||
/* read_definitions_flags= */ 0);
|
||
if (r == -ENOMEM)
|
||
return r;
|
||
if (r < 0)
|
||
continue;
|
||
|
||
r = table_add_many(
|
||
t,
|
||
TABLE_BOOLEAN_CHECKMARK, cc.component_enabled,
|
||
TABLE_SET_COLOR, ansi_highlight_green_red(cc.component_enabled));
|
||
if (r < 0)
|
||
return table_log_add_error(r);
|
||
|
||
r = context_component_is_suggested(&cc);
|
||
if (r < 0)
|
||
r = table_add_many(t, TABLE_EMPTY);
|
||
else
|
||
r = table_add_many(t, TABLE_BOOLEAN_CHECKMARK, r);
|
||
if (r < 0)
|
||
return table_log_add_error(r);
|
||
|
||
const char *doc = cc.component_documentation ? cc.component_documentation[0] : NULL;
|
||
|
||
r = table_add_many(
|
||
t,
|
||
TABLE_STRING, *i,
|
||
TABLE_STRING, cc.component_description,
|
||
TABLE_STRING, doc,
|
||
TABLE_SET_URL, doc);
|
||
if (r < 0)
|
||
return table_log_add_error(r);
|
||
}
|
||
|
||
return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
|
||
} 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(verb_enable_component, "enable-component", "COMPONENT…", 1, VERB_ANY, 0,
|
||
"Enable component");
|
||
VERB(verb_enable_component, "disable-component", "COMPONENT…", 1, VERB_ANY, 0,
|
||
"Disable component");
|
||
static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||
bool enable = streq(argv[0], "enable-component");
|
||
int r;
|
||
|
||
_cleanup_(context_done) Context context = CONTEXT_NULL;
|
||
r = context_from_cmdline(&context);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (context.feature_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]);
|
||
if (context.definitions)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||
"The --definitions= switch may not be combined with '%s'.", argv[0]);
|
||
|
||
char **arguments, *array[2];
|
||
if (argc > 1) {
|
||
if (context.component || context.component_select != SELECT_EXPLICIT)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify component names as positional parameter or via --component=/--component-all/--component-suggested, not both.");
|
||
|
||
arguments = strv_skip(argv, 1);
|
||
|
||
} else if (context.component_select == SELECT_EXPLICIT) {
|
||
if (!context.component)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No component specified.");
|
||
|
||
array[0] = context.component;
|
||
array[1] = NULL;
|
||
arguments = array;
|
||
} else {
|
||
assert(!context.component);
|
||
arguments = NULL;
|
||
}
|
||
|
||
STRV_FOREACH(name, arguments)
|
||
if (!component_name_valid(*name))
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", *name);
|
||
|
||
r = context_load_offline(
|
||
&context,
|
||
/* process_image_flags= */ 0,
|
||
/* read_definitions_flags= */ 0);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
_cleanup_strv_free_ char **component_names = NULL;
|
||
r = context_list_components(&context, &component_names, /* ret_has_default_component= */ NULL);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
_cleanup_strv_free_ char **suggested = NULL;
|
||
|
||
switch (context.component_select) {
|
||
|
||
case SELECT_EXPLICIT:
|
||
STRV_FOREACH(name, arguments)
|
||
if (!strv_contains(component_names, *name))
|
||
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Component not found: %s", *name);
|
||
break;
|
||
|
||
case SELECT_ALL:
|
||
assert(!arguments);
|
||
arguments = component_names;
|
||
break;
|
||
|
||
case SELECT_SUGGESTED:
|
||
assert(!arguments);
|
||
STRV_FOREACH(name, component_names) {
|
||
_cleanup_(context_done) Context cc = CONTEXT_NULL;
|
||
|
||
r = context_from_base_with_component(&context, *name, &cc);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
r = context_load_offline(
|
||
&cc,
|
||
/* process_image_flags= */ 0,
|
||
/* read_definitions_flags= */ 0);
|
||
if (r < 0)
|
||
continue;
|
||
|
||
r = context_component_is_suggested(&cc);
|
||
if (r < 0) {
|
||
log_warning_errno(r, "Failed to determine whether '%s' shall be enabled, skipping: %m", *name);
|
||
continue;
|
||
}
|
||
|
||
/* This reconciles the system with the suggestions: on 'enable-component' we act on
|
||
* the components that are suggested, on 'disable-component' we act on the ones that
|
||
* are not. Hence pick the components whose suggestion state matches the operation. */
|
||
if (!!r != !!enable) {
|
||
log_debug("Skipping '%s'.", *name);
|
||
continue;
|
||
}
|
||
|
||
log_info("%s '%s'.", enable ? "Enabling" : "Disabling", *name);
|
||
|
||
if (strv_extend(&suggested, *name) < 0)
|
||
return log_oom();
|
||
}
|
||
|
||
arguments = suggested;
|
||
break;
|
||
|
||
default:
|
||
assert_not_reached();
|
||
}
|
||
|
||
if (strv_isempty(arguments)) {
|
||
log_info("No components selected.");
|
||
return 0;
|
||
}
|
||
|
||
/* Component definition files live directly below the configuration directories, hence the drop-in
|
||
* goes right next to them below /etc. */
|
||
_cleanup_free_ char *dropin_dir = path_join(context.root, SYSCONF_DIR);
|
||
if (!dropin_dir)
|
||
return log_oom();
|
||
|
||
STRV_FOREACH(name, arguments) {
|
||
_cleanup_free_ char *fname = strjoin("sysupdate.", *name, ".component");
|
||
if (!fname)
|
||
return log_oom();
|
||
|
||
/* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */
|
||
r = write_drop_in_format(
|
||
dropin_dir,
|
||
fname,
|
||
50, "systemd-sysupdate-enabled",
|
||
"# Generated via 'systemd-sysupdate %s'\n\n"
|
||
"[Component]\n"
|
||
"Enabled=%s\n",
|
||
argv[0],
|
||
yes_no(enable));
|
||
if (r < 0)
|
||
return log_error_errno(r, "Failed to write drop-in for component '%s': %m", *name);
|
||
|
||
log_info("Component '%s' %s.", *name, enable ? "enabled" : "disabled");
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
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_select = SELECT_EXPLICIT;
|
||
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_select = SELECT_EXPLICIT;
|
||
break;
|
||
|
||
OPTION('A', "component-all", NULL, "Select all components"):
|
||
arg_component = mfree(arg_component);
|
||
arg_component_select = SELECT_ALL;
|
||
break;
|
||
|
||
OPTION('S', "component-suggested", NULL, "Select all suggested components"):
|
||
arg_component = mfree(arg_component);
|
||
arg_component_select = SELECT_SUGGESTED;
|
||
break;
|
||
|
||
OPTION('a', "feature-all", NULL, "Select all features"):
|
||
arg_feature_select = SELECT_ALL;
|
||
break;
|
||
|
||
OPTION('s', "feature-suggested", NULL, "Select all suggested features"):
|
||
arg_feature_select = SELECT_SUGGESTED;
|
||
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) {
|
||
if (arg_reboot)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=.");
|
||
|
||
if (arg_definitions)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= switch may not be combined with --root= or --image=.");
|
||
}
|
||
|
||
if ((arg_component || arg_component_select != SELECT_EXPLICIT)) {
|
||
if (arg_reboot)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=/--component-all/--component-suggested, as automatic reboots only apply to the booted OS version.");
|
||
|
||
if (arg_definitions)
|
||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component=/--component-all/--component-suggested 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;
|
||
|
||
LIBAPPARMOR_NOTE(recommended);
|
||
LIBAUDIT_NOTE(recommended);
|
||
LIBBLKID_NOTE(recommended);
|
||
LIBCRYPTO_NOTE(suggested);
|
||
LIBCRYPTSETUP_NOTE(suggested);
|
||
LIBMOUNT_NOTE(recommended);
|
||
LIBSELINUX_NOTE(recommended);
|
||
LIBTSS2_ESYS_NOTE(suggested);
|
||
LIBTSS2_MU_NOTE(suggested);
|
||
LIBTSS2_RC_NOTE(suggested);
|
||
|
||
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);
|