From e55e7a5a613712dc9a94d40a1fea1f02d359961f Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Tue, 2 Jul 2024 14:41:31 -0400 Subject: [PATCH] sysupdated: Plumb through optional features This adds APIs to enumerate/inspect/enable/disable optional features. --- man/org.freedesktop.sysupdate1.xml | 97 ++++++- src/sysupdate/org.freedesktop.sysupdate1.conf | 12 + .../org.freedesktop.sysupdate1.policy | 10 + src/sysupdate/sysupdated.c | 248 ++++++++++++++++-- src/sysupdate/updatectl.c | 14 +- 5 files changed, 352 insertions(+), 29 deletions(-) diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml index 79f718f93c4..fca11c607dc 100644 --- a/man/org.freedesktop.sysupdate1.xml +++ b/man/org.freedesktop.sysupdate1.xml @@ -122,9 +122,18 @@ node /org/freedesktop/sysupdate1/target/host { out s new_version, out t job_id, out o job_path); - Vacuum(out u count); + Vacuum(out u instances, + out u disabled_transfers); GetAppStream(out as appstream); GetVersion(out s version); + ListFeatures(in t flags, + out as features); + DescribeFeature(in s feature, + in t flags, + out s json); + SetFeatureEnabled(in s feature, + in i enabled, + in t flags); properties: @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s Class = '...'; @@ -159,6 +168,12 @@ node /org/freedesktop/sysupdate1/target/host { + + + + + + @@ -273,6 +288,68 @@ node /org/freedesktop/sysupdate1/target/host { IMAGE_VERSION in /etc/os-release. If the target has no current version, the function will return an empty string. + ListFeatures() returns a list of this target's optional features, by ID. + The flags argument is added for future extensibility, and must be set to 0. + If the target has no optional features, the method returns an empty array. + + DescribeFeature() returns all known information about a given optional feature. + The feature argument is used to pass the ID of the feature to be described. + The flags argument is added for future extensibility, and must be set to 0. + The returned JSON object contains several known keys. More keys may be added in the future. + The currently known keys are as follows: + + + + name + A string containing the feature's name. + + + + description + An optional string that contains a user-presentable description that identifies + this feature + + + + enabled + A boolean indicating whether this feature is enabled. + + + + documentationUrl + An optional string that contains a user-presentable HTTP/HTTPS URL to documentation + about this feature. + + + + appstreamUrl + An optional string that contains an HTTP/HTTPS URL to an + appstream + catalog XML file containing metadata about this feature. + + + + transfers + An optional array of strings that list which transfer definitions belong to this + feature. + + + + SetFeatureEnabled() writes an appropriate drop-in file to enable or disable + the specified optional feature. + If enable is zero, the feature is disabled. When greater than zero, the feature is + enabled. When less than zero, the feature is reset to the distribution's default. + The flags argument is added for future extensibility, and must be set to 0. + The feature does not have to exist; this allows for graceful handling of masked features, and for + preemptive decisions to be made about features that are planned to appear in future releases of the OS. + The drop-in will have a filename of 50-systemd-sysupdate-enabled.conf. + This method only changes configuration files; to actually apply the changes, clients will need to + call Update(). + Depending on the exact needs of the client, it can choose to update the system to the latest available + version, or it can extend the newest existing installation in-place (by passing in the version returned + by GetVersion()). + For now, this method only works with the host target. + @@ -327,8 +404,13 @@ node /org/freedesktop/sysupdate1/target/host { org.freedesktop.sysupdate1.vacuum. By default, this action requires administrator authentication. - GetAppStream() and GetVersion() are unauthenticated and - may be called by anybody. + SetFeatureEnabled() uses the polkit action + org.freedesktop.sysupdate1.manage-features. By default, this action + requires administrator authentication. + + GetAppStream(), GetVersion(), + ListFeatures(), and DescribeFeature() + are unauthenticated and may be called by anybody. All methods called on this interface expose additional variables to the polkit rules. class contains the class of the Target being acted upon, and name @@ -409,9 +491,9 @@ node /org/freedesktop/sysupdate1/job/_1 { The Id property exposes the numeric job ID of the job object. - The Type property exposes the type of operation (one of: list, - describe, check-new, update, or vacuum). - + The Type property exposes the type of operation (one of: + list, describe, check-new, + update, vacuum, or describe-feature). The Offline property exposes whether the job is permitted to access the network or not. @@ -481,6 +563,9 @@ node /org/freedesktop/sysupdate1/job/_1 { Vacuum(), GetAppStream(), GetVersion(), + ListFeatures(), + DescribeFeature(), + SetFeatureEnabled(), Class, Name, and Path were added in version 257. diff --git a/src/sysupdate/org.freedesktop.sysupdate1.conf b/src/sysupdate/org.freedesktop.sysupdate1.conf index 30cb1eec241..1cb80a6311e 100644 --- a/src/sysupdate/org.freedesktop.sysupdate1.conf +++ b/src/sysupdate/org.freedesktop.sysupdate1.conf @@ -78,6 +78,18 @@ send_interface="org.freedesktop.sysupdate1.Target" send_member="GetVersion"/> + + + + + + diff --git a/src/sysupdate/org.freedesktop.sysupdate1.policy b/src/sysupdate/org.freedesktop.sysupdate1.policy index 047f5d11a45..fd894793a2f 100644 --- a/src/sysupdate/org.freedesktop.sysupdate1.policy +++ b/src/sysupdate/org.freedesktop.sysupdate1.policy @@ -71,4 +71,14 @@ + + Manage optional features + Authentication is required to manage optional features + + auth_admin + auth_admin + auth_admin_keep + + + diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c index 8709c60b78d..a61ad6b1fd2 100644 --- a/src/sysupdate/sysupdated.c +++ b/src/sysupdate/sysupdated.c @@ -13,6 +13,7 @@ #include "bus-util.h" #include "common-signal.h" #include "discover-image.h" +#include "dropin.h" #include "env-util.h" #include "escape.h" #include "event-util.h" @@ -31,6 +32,8 @@ #include "string-table.h" #include "sysupdate-util.h" +#define FEATURES_DROPIN_NAME "systemd-sysupdate-enabled" + typedef struct Manager { sd_event *event; sd_bus *bus; @@ -85,6 +88,7 @@ typedef enum JobType { JOB_CHECK_NEW, JOB_UPDATE, JOB_VACUUM, + JOB_DESCRIBE_FEATURE, _JOB_TYPE_MAX, _JOB_TYPE_INVALID = -EINVAL, } JobType; @@ -104,6 +108,7 @@ struct Job { JobType type; bool offline; char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */ + char *feature; /* Passed into sysupdate for JOB_DESCRIBE_FEATURE */ unsigned progress_percent; @@ -131,11 +136,12 @@ static const char* const target_class_table[_TARGET_CLASS_MAX] = { DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(target_class, TargetClass); static const char* const job_type_table[_JOB_TYPE_MAX] = { - [JOB_LIST] = "list", - [JOB_DESCRIBE] = "describe", - [JOB_CHECK_NEW] = "check-new", - [JOB_UPDATE] = "update", - [JOB_VACUUM] = "vacuum", + [JOB_LIST] = "list", + [JOB_DESCRIBE] = "describe", + [JOB_CHECK_NEW] = "check-new", + [JOB_UPDATE] = "update", + [JOB_VACUUM] = "vacuum", + [JOB_DESCRIBE_FEATURE] = "describe-feature", }; DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(job_type, JobType); @@ -149,6 +155,7 @@ static Job *job_free(Job *j) { free(j->object_path); free(j->version); + free(j->feature); sd_json_variant_unref(j->json); @@ -440,8 +447,8 @@ static int job_start(Job *j) { NULL, /* maybe --verify=no */ NULL, /* maybe --component=, --root=, or --image= */ NULL, /* maybe --offline */ - NULL, /* list, check-new, update, vacuum */ - NULL, /* maybe version (for list, update) */ + NULL, /* list, check-new, update, vacuum, features */ + NULL, /* maybe version (for list, update), maybe feature (features) */ NULL }; size_t k = 2; @@ -493,6 +500,12 @@ static int job_start(Job *j) { cmd[k++] = "vacuum"; break; + case JOB_DESCRIBE_FEATURE: + cmd[k++] = "features"; + assert(!isempty(j->feature)); + cmd[k++] = j->feature; + break; + default: assert_not_reached(); } @@ -573,20 +586,26 @@ static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error * action = "org.freedesktop.sysupdate1.vacuum"; break; + case JOB_DESCRIBE_FEATURE: + action = NULL; + break; + default: assert_not_reached(); } - r = bus_verify_polkit_async( - msg, - action, - /* details= */ NULL, - &j->manager->polkit_registry, - error); - if (r < 0) - return r; - if (r == 0) - return 1; /* Will call us back */ + if (action) { + r = bus_verify_polkit_async( + msg, + action, + /* details= */ NULL, + &j->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + } r = job_cancel(j); if (r < 0) @@ -918,6 +937,8 @@ static int target_method_describe_finish( _cleanup_free_ char *text = NULL; int r; + /* NOTE: This is also reused by target_method_describe_feature */ + assert(json); r = sd_json_variant_format(json, 0, &text); @@ -1132,7 +1153,7 @@ static int target_method_vacuum_finish( sd_bus_error *error) { sd_json_variant *v; - uint64_t instances; + uint64_t instances, disabled; assert(json); @@ -1144,7 +1165,15 @@ static int target_method_vacuum_finish( instances = sd_json_variant_unsigned(v); assert(instances <= UINT32_MAX); - return sd_bus_reply_method_return(msg, "u", (uint32_t) instances); + v = sd_json_variant_by_key(json, "disabledTransfers"); + if (!v) + return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Missing key 'disabledTransfers'"); + if (!sd_json_variant_is_unsigned(v)) + return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Key 'disabledTransfers' should be an unsigned int"); + disabled = sd_json_variant_unsigned(v); + assert(disabled <= UINT32_MAX); + + return sd_bus_reply_method_return(msg, "uu", (uint32_t) instances, (uint32_t) disabled); } static int target_method_vacuum(sd_bus_message *msg, void *userdata, sd_bus_error *error) { @@ -1247,6 +1276,167 @@ static int target_method_get_appstream(sd_bus_message *msg, void *userdata, sd_b return sd_bus_send(NULL, reply, NULL); } +static int target_method_list_features(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + _cleanup_strv_free_ char **features = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Target *t = ASSERT_PTR(userdata); + sd_json_variant *v; + uint64_t flags; + int r; + + assert(msg); + + r = sd_bus_message_read(msg, "t", &flags); + if (r < 0) + return r; + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0"); + + r = sysupdate_run_simple(&json, t, "features", NULL); + if (r < 0) + return r; + + v = sd_json_variant_by_key(json, "features"); + if (!v) + return -EINVAL; + r = sd_json_variant_strv(v, &features); + if (r < 0) + return r; + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append_strv(reply, features); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int target_method_describe_feature(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + const char *feature; + uint64_t flags; + int r; + + assert(msg); + + r = sd_bus_message_read(msg, "st", &feature, &flags); + if (r < 0) + return r; + + if (isempty(feature)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Feature must be specified"); + + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0"); + + r = job_new(JOB_DESCRIBE_FEATURE, t, msg, target_method_describe_finish, &j); + if (r < 0) + return r; + + j->feature = strdup(feature); + if (!j->feature) + return log_oom(); + + r = job_start(j); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to start job: %m"); + TAKE_PTR(j); /* Avoid job from being killed & freed */ + + return 1; +} + +static bool feature_name_is_valid(const char *name) { + if (isempty(name)) + return false; + + if (!ascii_is_valid(name)) + return false; + + if (!filename_is_valid(strjoina(name, ".feature.d"))) + return false; + + return true; +} + +static int target_method_set_feature_enabled(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_free_ char *feature_ext = NULL; + Target *t = ASSERT_PTR(userdata); + const char *feature; + uint64_t flags; + int32_t enabled; + int r; + + assert(msg); + + if (t->class != TARGET_HOST) + return sd_bus_reply_method_errorf(msg, + SD_BUS_ERROR_NOT_SUPPORTED, + "For now, features can only be managed on the host system."); + + r = sd_bus_message_read(msg, "sit", &feature, &enabled, &flags); + if (r < 0) + return r; + if (!feature_name_is_valid(feature)) + return sd_bus_reply_method_errorf(msg, + SD_BUS_ERROR_INVALID_ARGS, + "The specified feature is invalid"); + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0"); + + if (!endswith(feature, ".feature")) { + feature_ext = strjoin(feature, ".feature"); + if (!feature_ext) + return -ENOMEM; + feature = feature_ext; + } + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "feature", feature, + "enabled", enabled >= 0 ? true_false(enabled) : "unset", + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.manage-features", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + /* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */ + if (enabled < 0) { /* Reset -> delete the drop-in file */ + _cleanup_free_ char *path = NULL; + + r = drop_in_file(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME, NULL, &path); + if (r < 0) + return r; + + if (unlink(path) < 0) + return -errno; + } else { /* otherwise, create the drop-in with the right settings */ + r = write_drop_in_format(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME, + "# Generated via org.freedesktop.sysupdate1 D-Bus interface\n\n" + "[Feature]\n" + "Enabled=%s\n", + yes_no(enabled)); + if (r < 0) + return r; + } + + return sd_bus_reply_method_return(msg, NULL); +} + static int target_list_components(Target *t, char ***ret_components, bool *ret_have_default) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; _cleanup_strv_free_ char **components = NULL; @@ -1397,7 +1587,7 @@ static const sd_bus_vtable target_vtable[] = { SD_BUS_METHOD_WITH_ARGS("Vacuum", SD_BUS_NO_ARGS, - SD_BUS_RESULT("u", count), + SD_BUS_RESULT("u", instances, "u", disabled_transfers), target_method_vacuum, SD_BUS_VTABLE_UNPRIVILEGED), @@ -1413,6 +1603,24 @@ static const sd_bus_vtable target_vtable[] = { target_method_get_version, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ListFeatures", + SD_BUS_ARGS("t", flags), + SD_BUS_RESULT("as", features), + target_method_list_features, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("DescribeFeature", + SD_BUS_ARGS("s", feature, "t", flags), + SD_BUS_RESULT("s", json), + target_method_describe_feature, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("SetFeatureEnabled", + SD_BUS_ARGS("s", feature, "i", enabled, "t", flags), + SD_BUS_NO_RESULT, + target_method_set_feature_enabled, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END }; diff --git a/src/sysupdate/updatectl.c b/src/sysupdate/updatectl.c index 216f37f9317..8da35d67d5b 100644 --- a/src/sysupdate/updatectl.c +++ b/src/sysupdate/updatectl.c @@ -1166,17 +1166,25 @@ static int verb_vacuum(int argc, char **argv, void *userdata) { for (size_t i = 0; i < n; i++) { _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - uint32_t count; + uint32_t count, disabled; r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, target_paths[i], SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL); if (r < 0) return log_bus_error(r, &error, targets[i], "call Vacuum"); - r = sd_bus_message_read(reply, "u", &count); + r = sd_bus_message_read(reply, "uu", &count, &disabled); if (r < 0) return bus_log_parse_error(r); - log_info("Deleted %u instance(s) of %s.\n", count, targets[i]); + 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]); } return 0; } -- 2.47.3