diff --git a/man/updatectl.xml b/man/updatectl.xml
index 3228c808d03..9fece4f779d 100644
--- a/man/updatectl.xml
+++ b/man/updatectl.xml
@@ -90,6 +90,35 @@
+
+ features [FEATURE]
+
+ When no FEATURE is specified, this command lists all
+ optional features.
+ When a FEATURE is specified, this command lists all known information
+ about that feature.
+
+
+
+
+
+ enable FEATURE…
+ disable FEATURE…
+
+ These commands enable or disable optional features.
+ See sysupdate.features5.
+ These commands always operate on the host system.
+
+ 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 to immediately apply these changes.
+
+
+
+
@@ -107,6 +136,9 @@
When used with the update command, reboots the system
after updates finish applying. If any update fails, the system will not reboot.
+ When used with the enable or disable commands and the
+ flag, reboots the system after download or clean-up finish applying.
+
@@ -121,6 +153,16 @@
+
+
+
+ When used with the enable command, downloads and installs the
+ enabled features. When used with the disable command, deletes all resources
+ downloaded by the disabled features.
+
+
+
+
diff --git a/src/sysupdate/updatectl.c b/src/sysupdate/updatectl.c
index 8da35d67d5b..1cfdccbe6d1 100644
--- a/src/sysupdate/updatectl.c
+++ b/src/sysupdate/updatectl.c
@@ -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 },
{}
};