]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdated: Plumb through optional features
authorAdrian Vovk <adrianvovk@gmail.com>
Tue, 2 Jul 2024 18:41:31 +0000 (14:41 -0400)
committerAdrian Vovk <adrianvovk@gmail.com>
Fri, 18 Oct 2024 22:08:38 +0000 (18:08 -0400)
This adds APIs to enumerate/inspect/enable/disable optional features.

man/org.freedesktop.sysupdate1.xml
src/sysupdate/org.freedesktop.sysupdate1.conf
src/sysupdate/org.freedesktop.sysupdate1.policy
src/sysupdate/sysupdated.c
src/sysupdate/updatectl.c

index 79f718f93c4e2c61d581272b1b497ad839c0a696..fca11c607dc05509a78fff72374bd8ef575f6f6a 100644 (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>
index 30cb1eec241bd61cb44bf86d39964101016a1459..1cb80a6311eef7208cbbc3c5fea5a03937a72d28 100644 (file)
                        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"/>
index 047f5d11a45d38c493f49af65750b7a17a955578..fd894793a2f82215d20525d0e71f81ee176f80a6 100644 (file)
                 </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>
index 8709c60b78d90b4adc03c53186d6079b28e1d79d..a61ad6b1fd2e818cbbef7128de727afda461691e 100644 (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
 };
 
index 216f37f931769be8538c327f97eeb7c63f2d4fee..8da35d67d5b19f12410c39f0c24d91d762d0729c 100644 (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;
 }