1
0
mirror of https://github.com/systemd/systemd.git synced 2024-12-22 17:35:35 +03:00

updatectl: Introduce optional feature verbs

This introduces a nice UX for listing, inspecting, enabling, and
disabling optional features from the command line.
This commit is contained in:
Adrian Vovk 2024-07-04 20:03:09 -04:00
parent e55e7a5a61
commit 5803efff44
No known key found for this signature in database
GPG Key ID: 90A7B546533E15FB
2 changed files with 372 additions and 32 deletions

View File

@ -90,6 +90,35 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><command>features</command> [<replaceable>FEATURE</replaceable>]</term>
<listitem><para>When no <replaceable>FEATURE</replaceable> is specified, this command lists all
optional features.
When a <replaceable>FEATURE</replaceable> is specified, this command lists all known information
about that feature.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><command>enable</command> <replaceable>FEATURE</replaceable></term>
<term><command>disable</command> <replaceable>FEATURE</replaceable></term>
<listitem><para>These commands enable or disable optional features.
See <citerefentry><refentrytitle>sysupdate.features</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
These commands always operate on the host system.</para>
<para>By default, these commands will only change the system's configuration by creating or deleting
drop-in files; they will not immediately download the enabled features, or clean up after the
disabled ones.
Enabled features will be downloaded and installed the next time the target is updated, and disabled
transfers will be cleaned up the next time the target is updated or vacuumed.
Pass <option>--now</option> to immediately apply these changes.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
@ -107,6 +136,9 @@
<listitem><para>When used with the <command>update</command> command, reboots the system
after updates finish applying. If any update fails, the system will not reboot.</para>
<para>When used with the <command>enable</command> or <command>disable</command> commands and the
<option>--now</option> flag, reboots the system after download or clean-up finish applying.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
@ -121,6 +153,16 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--now</option></term>
<listitem><para>When used with the <command>enable</command> command, downloads and installs the
enabled features. When used with the <command>disable</command> command, deletes all resources
downloaded by the disabled features.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<xi:include href="user-system-options.xml" xpointer="host" />
<xi:include href="standard-options.xml" xpointer="no-pager" />

View File

@ -12,12 +12,18 @@
#include "bus-locator.h"
#include "bus-map-properties.h"
#include "bus-util.h"
#include "conf-files.h"
#include "conf-parser.h"
#include "errno-list.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-table.h"
#include "fs-util.h"
#include "json-util.h"
#include "main-func.h"
#include "os-util.h"
#include "pager.h"
#include "path-util.h"
#include "pretty-print.h"
#include "strv.h"
#include "sysupdate-update-set-flags.h"
@ -29,9 +35,11 @@ static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_reboot = false;
static bool arg_offline = false;
static bool arg_now = false;
static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
static char *arg_host = NULL;
#define SYSUPDATE_HOST_PATH "/org/freedesktop/sysupdate1/target/host"
#define SYSUPDATE_TARGET_INTERFACE "org.freedesktop.sysupdate1.Target"
typedef struct Version {
@ -1044,21 +1052,19 @@ static int update_started(sd_bus_message *reply, void *userdata, sd_bus_error *r
return 0;
}
static int verb_update(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
static int do_update(sd_bus *bus, char **targets) {
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *render_exit = NULL;
_cleanup_ordered_hashmap_free_ OrderedHashmap *map = NULL;
_cleanup_strv_free_ char **targets = NULL, **versions = NULL, **target_paths = NULL;
_cleanup_strv_free_ char **versions = NULL, **target_paths = NULL;
size_t n;
unsigned remaining = 0;
void *p;
bool did_anything = false;
int r;
r = ensure_targets(bus, argv + 1, &targets);
if (r < 0)
return log_error_errno(r, "Could not find targets: %m");
assert(bus);
assert(targets);
r = parse_targets(targets, &n, &target_paths, &versions);
if (r < 0)
@ -1140,18 +1146,64 @@ static int verb_update(int argc, char **argv, void *userdata) {
did_anything = true;
}
if (arg_reboot) {
if (did_anything)
return reboot_now();
log_info("Nothing was updated... skipping reboot.");
}
return did_anything ? 1 : 0;
}
static int verb_update(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
_cleanup_strv_free_ char **targets = NULL;
bool did_anything = false;
int r;
r = ensure_targets(bus, argv + 1, &targets);
if (r < 0)
return log_error_errno(r, "Could not find targets: %m");
r = do_update(bus, targets);
if (r < 0)
return r;
if (r > 0)
did_anything = true;
if (!arg_reboot)
return 0;
if (did_anything)
return reboot_now();
log_info("Nothing was updated... skipping reboot.");
return 0;
}
static int do_vacuum(sd_bus *bus, const char *target, const char *path) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
uint32_t count, disabled;
int r;
r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, path, SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
if (r < 0)
return log_bus_error(r, &error, target, "call Vacuum");
r = sd_bus_message_read(reply, "uu", &count, &disabled);
if (r < 0)
return bus_log_parse_error(r);
if (count > 0 && disabled > 0)
log_info("Deleted %u instance(s) and %u disabled transfer(s) of %s.",
count, disabled, target);
else if (count > 0)
log_info("Deleted %u instance(s) of %s.", count, target);
else if (disabled > 0)
log_info("Deleted %u disabled transfer(s) of %s.", disabled, target);
else
log_info("Found nothing to delete for %s.", target);
return count + disabled > 0 ? 1 : 0;
}
static int verb_vacuum(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
size_t n;
int r;
@ -1165,27 +1217,260 @@ static int verb_vacuum(int argc, char **argv, void *userdata) {
return log_error_errno(r, "Failed to parse targets: %m");
for (size_t i = 0; i < n; i++) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
uint32_t count, disabled;
r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, target_paths[i], SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
r = do_vacuum(bus, targets[i], target_paths[i]);
if (r < 0)
return log_bus_error(r, &error, targets[i], "call Vacuum");
return r;
}
return 0;
}
r = sd_bus_message_read(reply, "uu", &count, &disabled);
typedef struct Feature {
char *name;
char *description;
bool enabled;
char *documentation;
char **transfers;
} Feature;
static void feature_done(Feature *f) {
assert(f);
f->name = mfree(f->name);
f->description = mfree(f->description);
f->documentation = mfree(f->documentation);
f->transfers = strv_free(f->transfers);
}
static int describe_feature(sd_bus *bus, const char *feature, Feature *ret) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_(feature_done) Feature f = {};
char *json;
int r;
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, name), SD_JSON_MANDATORY },
{ "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, description), 0 },
{ "enabled", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Feature, enabled), SD_JSON_MANDATORY },
{ "documentationUrl", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, documentation), 0 },
{ "transfers", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Feature, transfers), 0 },
{}
};
assert(bus);
assert(feature);
assert(ret);
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"DescribeFeature",
&error,
&reply,
"st",
feature,
UINT64_C(0));
if (r < 0)
return log_bus_error(r, &error, "host", "lookup feature");
r = sd_bus_message_read_basic(reply, 's', &json);
if (r < 0)
return bus_log_parse_error(r);
r = sd_json_parse(json, 0, &v, NULL, NULL);
if (r < 0)
return log_error_errno(r, "Failed to parse JSON: %m");
r = sd_json_dispatch(v, dispatch_table, 0, &f);
if (r < 0)
return log_error_errno(r, "Failed to dispatch JSON: %m");
*ret = TAKE_STRUCT(f);
return 0;
}
static int list_features(sd_bus *bus) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_strv_free_ char **features = NULL;
_cleanup_(table_unrefp) Table *table = NULL;
int r;
assert(bus);
table = table_new("", "feature", "description");
if (!table)
return log_oom();
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"ListFeatures",
&error,
&reply,
"t",
UINT64_C(0));
if (r < 0)
return log_bus_error(r, &error, "host", "lookup feature");
r = sd_bus_message_read_strv(reply, &features);
if (r < 0)
return bus_log_parse_error(r);
STRV_FOREACH(feature, features) {
_cleanup_(feature_done) Feature f = {};
_cleanup_free_ char *name_link = NULL;
r = describe_feature(bus, *feature, &f);
if (r < 0)
return r;
if (urlify_enabled() && f.documentation) {
name_link = strjoin(f.name, special_glyph(SPECIAL_GLYPH_EXTERNAL_LINK));
if (!name_link)
return log_oom();
}
r = table_add_many(table,
TABLE_BOOLEAN_CHECKMARK, f.enabled,
TABLE_SET_COLOR, ansi_highlight_green_red(f.enabled),
TABLE_STRING, name_link ?: f.name,
TABLE_SET_URL, f.documentation,
TABLE_STRING, f.description);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
}
static int verb_features(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
_cleanup_(table_unrefp) Table *table = NULL;
_cleanup_(feature_done) Feature f = {};
int r;
if (argc == 1)
return list_features(bus);
table = table_new_vertical();
if (!table)
return log_oom();
r = describe_feature(bus, argv[1], &f);
if (r < 0)
return r;
r = table_add_many(table,
TABLE_FIELD, "Name",
TABLE_STRING, f.name,
TABLE_FIELD, "Enabled",
TABLE_BOOLEAN, f.enabled);
if (r < 0)
return table_log_add_error(r);
if (f.description) {
r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f.description);
if (r < 0)
return table_log_add_error(r);
}
if (f.documentation) {
r = table_add_many(table,
TABLE_FIELD, "Documentation",
TABLE_STRING, f.documentation,
TABLE_SET_URL, f.documentation);
if (r < 0)
return table_log_add_error(r);
}
if (!strv_isempty(f.transfers)) {
r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, f.transfers);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, false);
}
static int verb_enable(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
bool did_anything = false, enable;
char **features;
int r;
enable = streq(argv[0], "enable");
features = strv_skip(argv, 1);
STRV_FOREACH(feature, features) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"SetFeatureEnabled",
&error,
/* reply= */ NULL,
"sbt",
*feature,
(int) enable,
UINT64_C(0));
if (r < 0)
return log_bus_error(r, &error, "host", "call SetFeatureEnabled");
}
if (!arg_now) /* We weren't asked to apply the changes, so we're done! */
return 0;
if (enable) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_free_ char *target = NULL;
char *version = NULL;
/* We're downloading the new feature into the "current" version, which is either going to be
* the currently booted version or it's going to be a pending update that has already been
* installed and is just waiting for us to reboot into it. */
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"GetVersion",
&error,
&reply,
NULL);
if (r < 0)
return log_bus_error(r, &error, "host", "get current version");
r = sd_bus_message_read_basic(reply, 's', &version);
if (r < 0)
return bus_log_parse_error(r);
if (count > 0 && disabled > 0)
log_info("Deleted %u instance(s) and %u disabled transfer(s) of %s.",
count, disabled, targets[i]);
else if (count > 0)
log_info("Deleted %u instance(s) of %s.", count, targets[i]);
else if (disabled > 0)
log_info("Deleted %u disabled transfer(s) of %s.", disabled, targets[i]);
else
log_info("Found nothing to delete for %s.", targets[i]);
}
target = strjoin("host@", version);
if (!target)
return log_oom();
r = do_update(bus, STRV_MAKE(target));
} else
r = do_vacuum(bus, "host", SYSUPDATE_HOST_PATH);
if (r < 0)
return r;
if (r > 0)
did_anything = true;
if (arg_reboot && did_anything)
return reboot_now();
else if (did_anything)
log_info("Feature(s) %s.", enable ? "downloaded" : "deleted");
else
log_info("Nothing %s%s.",
enable ? "downloaded" : "deleted",
arg_reboot ? ", skipping reboot" :"");
return 0;
}
@ -1204,11 +1489,15 @@ static int help(void) {
" check [TARGET...] Check for updates\n"
" update [TARGET[@VERSION]...] Install updates\n"
" vacuum [TARGET...] Clean up old updates\n"
" features [FEATURE] List and inspect optional features on host OS\n"
" enable FEATURE... Enable optional feature on host OS\n"
" disable FEATURE... Disable optional feature on host OS\n"
" -h --help Show this help\n"
" --version Show package version\n"
"\n%3$sOptions:%4$s\n"
" --reboot Reboot after updating to newer version\n"
" --offline Do not fetch metadata from the network\n"
" --now Download/delete resources immediately\n"
" -H --host=[USER@]HOST Operate on remote host\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Do not show the headers and footers\n"
@ -1230,6 +1519,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_NO_LEGEND,
ARG_REBOOT,
ARG_OFFLINE,
ARG_NOW,
};
static const struct option options[] = {
@ -1240,6 +1530,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "host", required_argument, NULL, 'H' },
{ "reboot", no_argument, NULL, ARG_REBOOT },
{ "offline", no_argument, NULL, ARG_OFFLINE },
{ "now", no_argument, NULL, ARG_NOW },
{}
};
@ -1278,6 +1569,10 @@ static int parse_argv(int argc, char *argv[]) {
arg_offline = true;
break;
case ARG_NOW:
arg_now = true;
break;
case '?':
return -EINVAL;
@ -1294,10 +1589,13 @@ static int run(int argc, char *argv[]) {
int r;
static const Verb verbs[] = {
{ "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list },
{ "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check },
{ "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update },
{ "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum },
{ "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list },
{ "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check },
{ "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update },
{ "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum },
{ "features", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_features },
{ "enable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable },
{ "disable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable },
{}
};