1
0
mirror of https://github.com/systemd/systemd.git synced 2024-10-26 17:27:41 +03:00

sysupdated: Plumb through optional features

This adds APIs to enumerate/inspect/enable/disable optional features.
This commit is contained in:
Adrian Vovk 2024-07-02 14:41:31 -04:00
parent 0cd1a58921
commit e55e7a5a61
No known key found for this signature in database
GPG Key ID: 90A7B546533E15FB
5 changed files with 352 additions and 29 deletions

View File

@ -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 {
<variablelist class="dbus-method" generated="True" extra-ref="GetVersion()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ListFeatures()"/>
<variablelist class="dbus-method" generated="True" extra-ref="DescribeFeature()"/>
<variablelist class="dbus-method" generated="True" extra-ref="SetFeatureEnabled()"/>
<variablelist class="dbus-property" generated="True" extra-ref="Class"/>
<variablelist class="dbus-property" generated="True" extra-ref="Name"/>
@ -273,6 +288,68 @@ node /org/freedesktop/sysupdate1/target/host {
<varname>IMAGE_VERSION</varname> in <filename>/etc/os-release</filename>. If the target has no current
version, the function will return an empty string.</para>
<para><function>ListFeatures()</function> returns a list of this target's optional features, by ID.
The <varname>flags</varname> 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.</para>
<para><function>DescribeFeature()</function> returns all known information about a given optional feature.
The <varname>feature</varname> argument is used to pass the ID of the feature to be described.
The <varname>flags</varname> 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:</para>
<variablelist>
<varlistentry>
<term><literal>name</literal></term>
<listitem><para>A string containing the feature's name.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>description</literal></term>
<listitem><para>An optional string that contains a user-presentable description that identifies
this feature</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>enabled</literal></term>
<listitem><para>A boolean indicating whether this feature is enabled.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>documentationUrl</literal></term>
<listitem><para>An optional string that contains a user-presentable HTTP/HTTPS URL to documentation
about this feature.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>appstreamUrl</literal></term>
<listitem><para>An optional string that contains an HTTP/HTTPS URL to an
<ulink url="https://wwww.freedesktop.org/software/appstream/docs/chap-CatalogData.html">appstream
catalog</ulink> XML file containing metadata about this feature.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>transfers</literal></term>
<listitem><para>An optional array of strings that list which transfer definitions belong to this
feature.</para></listitem>
</varlistentry>
</variablelist>
<para><function>SetFeatureEnabled()</function> writes an appropriate drop-in file to enable or disable
the specified optional feature.
If <varname>enable</varname> 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 <varname>flags</varname> 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 <literal>50-systemd-sysupdate-enabled.conf</literal>.
This method only changes configuration files; to actually apply the changes, clients will need to
call <function>Update()</function>.
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 <varname>GetVersion()</varname>).
For now, this method only works with the <literal>host</literal> target.</para>
</refsect2>
<refsect2>
@ -327,8 +404,13 @@ node /org/freedesktop/sysupdate1/target/host {
<interfacename>org.freedesktop.sysupdate1.vacuum</interfacename>. By default, this action requires
administrator authentication.</para>
<para><function>GetAppStream()</function> and <function>GetVersion()</function> are unauthenticated and
may be called by anybody.</para>
<para><function>SetFeatureEnabled()</function> uses the polkit action
<interfacename>org.freedesktop.sysupdate1.manage-features</interfacename>. By default, this action
requires administrator authentication.</para>
<para><function>GetAppStream()</function>, <function>GetVersion()</function>,
<function>ListFeatures()</function>, and <function>DescribeFeature()</function>
are unauthenticated and may be called by anybody.</para>
<para>All methods called on this interface expose additional variables to the polkit rules.
<literal>class</literal> contains the class of the Target being acted upon, and <literal>name</literal>
@ -409,9 +491,9 @@ node /org/freedesktop/sysupdate1/job/_1 {
<para>The <varname>Id</varname> property exposes the numeric job ID of the job object.</para>
<para>The <varname>Type</varname> property exposes the type of operation (one of: <literal>list</literal>,
<literal>describe</literal>, <literal>check-new</literal>, <literal>update</literal>, or <literal>vacuum</literal>).
</para>
<para>The <varname>Type</varname> property exposes the type of operation (one of:
<literal>list</literal>, <literal>describe</literal>, <literal>check-new</literal>,
<literal>update</literal>, <literal>vacuum</literal>, or <literal>describe-feature</literal>).</para>
<para>The <varname>Offline</varname> property exposes whether the job is permitted to access
the network or not.</para>
@ -481,6 +563,9 @@ node /org/freedesktop/sysupdate1/job/_1 {
<function>Vacuum()</function>,
<function>GetAppStream()</function>,
<function>GetVersion()</function>,
<function>ListFeatures()</function>,
<function>DescribeFeature()</function>,
<function>SetFeatureEnabled()</function>,
<varname>Class</varname>,
<varname>Name</varname>, and
<varname>Path</varname> were added in version 257.</para>

View File

@ -78,6 +78,18 @@
send_interface="org.freedesktop.sysupdate1.Target"
send_member="GetVersion"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Target"
send_member="ListFeatures"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Target"
send_member="DescribeFeature"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Target"
send_member="SetFeatureEnabled"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Job"
send_member="Cancel"/>

View File

@ -71,4 +71,14 @@
</defaults>
</action>
<action id="org.freedesktop.sysupdate1.manage-features">
<description gettext-domain="systemd">Manage optional features</description>
<message gettext-domain="systemd">Authentication is required to manage optional features</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>

View File

@ -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
};

View File

@ -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;
}