diff options
author | Adrian Vovk <adrianvovk@gmail.com> | 2023-06-30 04:58:41 +0200 |
---|---|---|
committer | Tom Coldrick <thomas.coldrick@codethink.co.uk> | 2024-08-21 10:31:41 +0200 |
commit | bf2c741fd772af6f04b4fa234ada2d364f9a5d6c (patch) | |
tree | 5a8a98cf56241ea6d3fac1c57d7ac2cc79b716c2 | |
parent | Merge pull request #34018 from yuwata/network-address-label (diff) | |
download | systemd-bf2c741fd772af6f04b4fa234ada2d364f9a5d6c.tar.xz systemd-bf2c741fd772af6f04b4fa234ada2d364f9a5d6c.zip |
sysupdate: Implement systemd-sysupdated dbus service
Co-authored-by: Tom Coldrick <thomas.coldrick@codethink.co.uk>
Co-authored-by: Abderrahim Kitouni <abderrahim.kitouni@codethink.co.uk>
-rw-r--r-- | man/org.freedesktop.sysupdate1.xml | 487 | ||||
-rw-r--r-- | man/rules/meson.build | 5 | ||||
-rw-r--r-- | man/systemd-sysupdate.xml | 1 | ||||
-rw-r--r-- | man/systemd-sysupdated.service.xml | 54 | ||||
-rw-r--r-- | meson.build | 1 | ||||
-rw-r--r-- | po/POTFILES.in | 1 | ||||
-rw-r--r-- | src/shared/bus-locator.c | 6 | ||||
-rw-r--r-- | src/shared/bus-locator.h | 1 | ||||
-rw-r--r-- | src/sysupdate/meson.build | 16 | ||||
-rw-r--r-- | src/sysupdate/org.freedesktop.sysupdate1.conf | 88 | ||||
-rw-r--r-- | src/sysupdate/org.freedesktop.sysupdate1.policy | 74 | ||||
-rw-r--r-- | src/sysupdate/org.freedesktop.sysupdate1.service | 14 | ||||
-rw-r--r-- | src/sysupdate/sysupdate-util.h | 2 | ||||
-rw-r--r-- | src/sysupdate/sysupdate.c | 2 | ||||
-rw-r--r-- | src/sysupdate/sysupdated.c | 1911 | ||||
-rw-r--r-- | units/meson.build | 5 | ||||
-rw-r--r-- | units/systemd-sysupdated.service.in | 30 |
17 files changed, 2697 insertions, 1 deletions
diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml new file mode 100644 index 0000000000..ac0e9152a1 --- /dev/null +++ b/man/org.freedesktop.sysupdate1.xml @@ -0,0 +1,487 @@ +<?xml version='1.0'?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" > +<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> + +<refentry id="org.freedesktop.sysupdate1" conditional='ENABLE_SYSUPDATE' + xmlns:xi="http://www.w3.org/2001/XInclude"> + <refentryinfo> + <title>org.freedesktop.sysupdate1</title> + <productname>systemd</productname> + </refentryinfo> + + <refmeta> + <refentrytitle>org.freedesktop.sysupdate1</refentrytitle> + <manvolnum>5</manvolnum> + </refmeta> + + <refnamediv> + <refname>org.freedesktop.sysupdate1</refname> + <refpurpose>The D-Bus interface of systemd-sysupdated</refpurpose> + </refnamediv> + + <refsect1> + <title>Introduction</title> + + <para> + <citerefentry><refentrytitle>systemd-sysupdated.service</refentrytitle><manvolnum>8</manvolnum></citerefentry> + is a system service that allows unprivileged clients to update the system. This page describes the D-Bus + interface.</para> + </refsect1> + + <refsect1> + <title>The Manager Object</title> + + <para>The service exposes the following interfaces on the Manager object on the bus:</para> + + <programlisting executable="systemd-sysupdated" node="/org/freedesktop/sysupdate1" interface="org.freedesktop.sysupdate1.Manager"> +node /org/freedesktop/sysupdate1 { + interface org.freedesktop.sysupdate1.Manager { + methods: + ListTargets(out a(sso) targets); + ListJobs(out a(tsuo) jobs); + ListAppStream(out as urls); + signals: + JobRemoved(t id, + o path, + i status); + }; + interface org.freedesktop.DBus.Peer { ... }; + interface org.freedesktop.DBus.Introspectable { ... }; + interface org.freedesktop.DBus.Properties { ... }; +}; + </programlisting> + + <!--Autogenerated cross-references for systemd.directives, do not edit--> + + <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Manager"/> + + <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Manager"/> + + <variablelist class="dbus-method" generated="True" extra-ref="ListTargets()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="ListJobs()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="ListAppStream()"/> + + <variablelist class="dbus-signal" generated="True" extra-ref="JobRemoved()"/> + + <!--End of Autogenerated section--> + + <refsect2> + <title>Methods</title> + + <para><function>ListTargets()</function> returns a list all known update targets. It returns + an array of structures which consist of a string indicating the target's class (see Target's + <varname>Class</varname> property below for an explanation of the possible values), a string + with the name of the target, and the target object path.</para> + + <para><function>ListJobs()</function> returns a list all ongoing jobs. It returns + an array of structures which consist of a numeric job ID, a string indicating the job type (see Job's + <varname>Type</varname> property below for an explanation of the possible values), the job's progress, + and the job's object path.</para> + + <para><function>ListAppStream()</function> returns an array of all the appstream catalog URLs that this + service knows about. See Target's <varname>GetAppStream()</varname> method below for more + details.</para> + </refsect2> + + <refsect2> + <title>Signals</title> + + <para>The <function>JobRemoved()</function> signal is sent each time a job finishes, + is canceled or fails. It also carries the job ID and object path, followed by a numeric status + code. If the status is zero, the job has succeed. A positive status should be treated as an + exit code (i.e. <literal>EXIT_FAILURE</literal>), and a negative status should be treated as a + negative errno-style error code (i.e. <literal>-EINVAL</literal>).</para> + </refsect2> + </refsect1> + + <refsect1> + <title>The Target Object</title> + + <para>A target is a component of the system (i.e. the host itself, a sysext, a confext, etc.) that + can be updated by + <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>. + </para> + + <para>The service exposes the following interfaces on Target objects on the bus:</para> + + <programlisting executable="systemd-sysupdated" node="/org/freedesktop/sysupdate1/target/host" interface="org.freedesktop.sysupdate1.Target"> +node /org/freedesktop/sysupdate1/target/host { + interface org.freedesktop.sysupdate1.Target { + methods: + List(in t flags, + out as versions); + Describe(in s version, + in t flags, + out s json); + CheckNew(out s new_version); + Update(in s new_version, + in t flags, + out s new_version, + out t job_id, + out o job_path); + Vacuum(out u count); + GetAppStream(out as appstream); + GetVersion(out s version); + properties: + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Class = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Name = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Path = '...'; + }; + interface org.freedesktop.DBus.Peer { ... }; + interface org.freedesktop.DBus.Introspectable { ... }; + interface org.freedesktop.DBus.Properties { ... }; +}; + </programlisting> + + <!--Autogenerated cross-references for systemd.directives, do not edit--> + + <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Target"/> + + <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Target"/> + + <variablelist class="dbus-method" generated="True" extra-ref="List()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="Describe()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="CheckNew()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="Update()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="Vacuum()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="GetAppStream()"/> + + <variablelist class="dbus-method" generated="True" extra-ref="GetVersion()"/> + + <variablelist class="dbus-property" generated="True" extra-ref="Class"/> + + <variablelist class="dbus-property" generated="True" extra-ref="Name"/> + + <variablelist class="dbus-property" generated="True" extra-ref="Path"/> + + <!--End of Autogenerated section--> + + <refsect2> + <title>Methods</title> + + <para><function>List()</function> returns a list of versions available for this target. The + <varname>flags</varname> argument can be used to pass additional options, with bit 0 mapping to + <option>offline</option>. When <option>offline</option> is true, this method returns only the versions + installed locally. Otherwise, this method pulls metadata from the network and returns all versions + available for this target. Use <function>Describe()</function> to query more information about each + version returned by this method.</para> + + <para><function>Describe()</function> returns all known information about a given version as a JSON + object. The <varname>version</varname> argument is used to pass the version to be described. Additional + options may be passed through the <varname>flags</varname> argument. The only supported value currently + is <varname>SD_SYSTEMD_SYSUPDATE_OFFLINE</varname>, which prevents the call from accessing the network + and restricts results to locally installed versions. This flag is defined as follows:</para> + + <programlisting> +#define SD_SYSTEMD_SYSUPDATE_OFFLINE (UINT64_C(1) << 0) + </programlisting> + + <para>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>version</term> + <listitem><para>A string containing the version number.</para></listitem> + </varlistentry> + + <varlistentry> + <term>newest</term> + <listitem><para>A boolean indicating whether this version is the latest available for the target.</para></listitem> + </varlistentry> + + <varlistentry> + <term>available</term> + <listitem><para>A boolean indicating whether this version is available for download.</para></listitem> + </varlistentry> + + <varlistentry> + <term>installed</term> + <listitem><para>A boolean indicating whether this version is installed locally.</para></listitem> + </varlistentry> + + <varlistentry> + <term>obsolete</term> + <listitem><para>A boolean indicating whether this version is considered obsolete by the service, + and is therefore disallowed from being installed.</para></listitem> + </varlistentry> + + <varlistentry> + <term>protected</term> + <listitem><para>A boolean indicating whether this version is exempt from deletion by a + <function>Vacuum()</function> operation.</para></listitem> + </varlistentry> + + <varlistentry> + <term>changelog_urls</term> + <listitem><para>A list of strings that contain user-presentable URLs to ChangeLogs associated with + this version.</para></listitem> + </varlistentry> + </variablelist> + + <para><function>CheckNew()</function> checks if a newer version is available for this target. This + method pulls metadata from the network. If a newer version is found, this method returns the + version number. If no newer version is found, this method returns an empty string. Use + <function>Describe()</function> to query more information about the version returned by this method. + </para> + + <para><function>Update()</function> installs an update for this target. If a + <varname>new_version</varname> is specified, that is the version that gets installed. Otherwise, the + latest version is installed. The <varname>flags</varname> argument is added for future + extensibility. No flags are currently defined, and the argument is required to be set to + <literal>0</literal>. Unlike all the other methods in this interface, <function>Update()</function> + does not wait for its job to complete. Instead, it returns the job's numeric ID and object path as soon + as the job begins, so that the caller can listen for progress updates or cancel the operation. This + method also returns the version the target will be updated to, for cases where no version was specified + by the caller. This method pulls both metadata and payload data from the network. Listen for the + Manager's <function>JobRemoved()</function> signal to detect when the job is complete.</para> + + <para><function>Vacuum()</function> deletes old installed versions of this target to free up space. + It returns the number of instances that have been deleted.</para> + + <para><function>GetAppStream()</function> returns a list of HTTP/HTTPS URLs to this target's + <ulink url="https://wwww.freedesktop.org/software/appstream/docs/chap-CatalogData.html">appstream catalog</ulink> + XML files. If this target has no appstream catalogs, the method will return an empty list. These + catalog files can be used by software centers (such as GNOME Software or KDE Discover) to present rich + metadata about the target, including a display name, changelog, icon, and more. The returned catalogs + will include <ulink url="https://systemd.io/APPSTREAM_BUNDLE">special metadata</ulink> to allow the + software center to correctly associate the catalogs with this target.</para> + + <para><function>GetVersion()</function> returns the current version of this target, if any. The current + version is the newest version that is installed. Note that this isn't necessarily the same thing as the + booted or currently-in-use version of the target. For example, on the host system the booted version + is the current version most of the time, but if an update is installed and pending a reboot it will + become the current version instead. You can query the booted version of the host system via + <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> + + </refsect2> + + <refsect2> + <title>Properties</title> + + <para>The <varname>Class</varname> property exposes the class of this target, which describes + where it was enumerated. Possible values include: <literal>machine</literal> for containers and + virtual machines managed by + <citerefentry><refentrytitle>systemd-machined.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>, + <literal>portable</literal> for <ulink url="https://systemd.io/PORTABLE_SERVICES">portable services</ulink>, + <literal>sysext</literal> for system extensions managed by + <citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry>, + <literal>confext</literal> for configuration extensions managed by + <citerefentry><refentrytitle>systemd-confext</refentrytitle><manvolnum>8</manvolnum></citerefentry>, + <literal>component</literal> for components accepted by the <option>--component=</option> option of + <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>, + and <literal>host</literal> for the host system itself. At most one target will have a class of + <literal>host</literal>.</para> + + <para>The <varname>Path</varname> property exposes more detail about where this target was found. + For <literal>machine</literal>, <literal>portable</literal>, <literal>extension</literal>, and + <literal>confext</literal> targets, this is the file path to the image. For <literal>component</literal> + and <literal>host</literal> targets, this is the name of a + <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry> + directory.</para> + + <para>The <varname>Name</varname> property exposes the name of this target. Note that the name is + unique within a class but is not necessarily unique between classes. For instance, it is possible + to have both a <literal>portable</literal> target named <literal>foobar</literal> and an + <literal>extension</literal> target named <literal>foobar</literal>, but it is not possible to have + two <literal>portable</literal> targets named <literal>foobar</literal>.</para> + + </refsect2> + + <refsect2> + <title>Security</title> + + <para>Method calls on this service are authenticated via + <ulink url="https://www.freedesktop.org/software/polkit/docs/latest/">polkit</ulink>.</para> + + <para><function>List()</function>, <function>Describe()</function>, and <function>CheckNew()</function> + use the polkit action <interfacename>org.freedesktop.sysupdate1.check</interfacename>. + By default, this action is permitted without administrator authentication.</para> + + <para><function>Update()</function> uses the polkit action + <interfacename>org.freedesktop.sysupdate1.update</interfacename> when no version is specified. + By default, this action is permitted without administrator authentication. When a version is + specified, <interfacename>org.freedesktop.sysupdate1.update-to-version</interfacename> is + used instead. By default, this alternate action requires administrator authentication.</para> + + <para><function>Vacuum()</function> uses the polkit action + <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>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> + contains the name of the same Target. Additionally, each method exposes its arguments to the + rule. Arguments containing flags are unwrapped into a variable-per-flag; for example, the + <literal>SD_SYSTEMD_SYSUPDATE_OFFLINE</literal> flag is exposed as a variable named + <literal>offline</literal>.</para> + </refsect2> + </refsect1> + + <refsect1> + <title>The Job Object</title> + + <para>A job is an ongoing operation, started by one of the methods on a Target object.</para> + + <para>The service exposes the following interfaces on Job objects on the bus:</para> + + <programlisting executable="systemd-sysupdated" node="/org/freedesktop/sysupdate1/job/_1" interface="org.freedesktop.sysupdate1.Job"> +node /org/freedesktop/sysupdate1/job/_1 { + interface org.freedesktop.sysupdate1.Job { + methods: + Cancel(); + properties: + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly t Id = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s Type = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b Offline = ...; + readonly u Progress = ...; + }; + interface org.freedesktop.DBus.Peer { ... }; + interface org.freedesktop.DBus.Introspectable { ... }; + interface org.freedesktop.DBus.Properties { ... }; +}; + </programlisting> + + + <!--Autogenerated cross-references for systemd.directives, do not edit--> + + + <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Job"/> + + + <variablelist class="dbus-interface" generated="True" extra-ref="org.freedesktop.sysupdate1.Job"/> + + + <variablelist class="dbus-method" generated="True" extra-ref="Cancel()"/> + + + <variablelist class="dbus-property" generated="True" extra-ref="Id"/> + + + <variablelist class="dbus-property" generated="True" extra-ref="Type"/> + + + <variablelist class="dbus-property" generated="True" extra-ref="Offline"/> + + + <variablelist class="dbus-property" generated="True" extra-ref="Progress"/> + + + <!--End of Autogenerated section--> + + + <refsect2> + <title>Methods</title> + + <para>The <function>Cancel()</function> method may be used to cancel the job. It takes no + parameters.</para> + </refsect2> + + <refsect2> + <title>Properties</title> + + <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>Offline</varname> property exposes whether the job is permitted to access + the network or not.</para> + + <para>The <varname>Progress</varname> property exposes the current progress of the job as a value + between 0 and 100. It is only available for <literal>update</literal> jobs; for all other jobs + it is always 0.</para> + </refsect2> + + <refsect2> + <title>Security</title> + + <para><function>Cancel()</function> uses the polkit action that corresponds to the method + that started this job. For instance, trying to cancel a <literal>list</literal> job will + require polkit to permit the <interfacename>org.freedesktop.sysupdate1.check</interfacename> + action.</para> + </refsect2> + </refsect1> + + <refsect1> + <title>Examples</title> + + <example> + <title>Introspect <interfacename>org.freedesktop.sysupdate1.Manager</interfacename> on the bus</title> + + <programlisting>$ gdbus introspect --system \ + --dest org.freedesktop.sysupdate1 \ + --object-path /org/freedesktop/sysupdate1 + </programlisting> + </example> + + <example> + <title>Introspect <interfacename>org.freedesktop.sysupdate1.Target</interfacename> on the bus</title> + + <programlisting>$ gdbus introspect --system \ + --dest org.freedesktop.sysupdate1 \ + --object-path /org/freedesktop/sysupdate1/target/host + </programlisting> + </example> + + <example> + <title>Introspect <interfacename>org.freedesktop.sysupdate1.Job</interfacename> on the bus</title> + + <programlisting>$ gdbus introspect --system \ + --dest org.freedesktop.sysupdate1 \ + --object-path /org/freedesktop/sysupdate1/job/_1 + </programlisting> + </example> + </refsect1> + + <xi:include href="org.freedesktop.locale1.xml" xpointer="versioning"/> + <refsect1> + <title>History</title> + <refsect2> + <title>The Manager Object</title> + <para><function>ListTargets()</function>, + <function>ListJobs()</function>, + <function>ListAppStream()</function>, and + <function>JobRemoved()</function> were added in version 257.</para> + </refsect2> + <refsect2> + <title>The Target Object</title> + <para><function>List()</function>, + <function>Describe()</function>, + <function>CheckNew()</function>, + <function>Update()</function>, + <function>Vacuum()</function>, + <function>GetAppStream()</function>, + <function>GetVersion()</function>, + <varname>Class</varname>, + <varname>Name</varname>, and + <varname>Path</varname> were added in version 257.</para> + </refsect2> + <refsect2> + <title>The Job Object</title> + <para><function>Cancel()</function>, + <varname>Id</varname>, + <varname>Type</varname>, + <varname>Offline</varname>, and + <varname>Progress</varname> were added in version 257.</para> + </refsect2> + </refsect1> +</refentry> diff --git a/man/rules/meson.build b/man/rules/meson.build index fda14d55bd..abe2b1e92f 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -65,6 +65,7 @@ manpages = [ ['org.freedesktop.portable1', '5', [], 'ENABLE_PORTABLED'], ['org.freedesktop.resolve1', '5', [], 'ENABLE_RESOLVE'], ['org.freedesktop.systemd1', '5', [], ''], + ['org.freedesktop.sysupdate1', '5', [], 'ENABLE_SYSUPDATE'], ['org.freedesktop.timedate1', '5', [], 'ENABLE_TIMEDATED'], ['os-release', '5', ['extension-release', 'initrd-release'], ''], ['pam_systemd', '8', [], 'HAVE_PAM'], @@ -1100,6 +1101,10 @@ manpages = [ 'systemd-sysupdate.service', 'systemd-sysupdate.timer'], 'ENABLE_SYSUPDATE'], + ['systemd-sysupdated.service', + '8', + ['systemd-sysupdated'], + 'ENABLE_SYSUPDATE'], ['systemd-sysusers', '8', ['systemd-sysusers.service'], ''], ['systemd-sysv-generator', '8', [], 'HAVE_SYSV_COMPAT'], ['systemd-time-wait-sync.service', diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml index f77bd3d0d9..dffe835c04 100644 --- a/man/systemd-sysupdate.xml +++ b/man/systemd-sysupdate.xml @@ -322,6 +322,7 @@ <para><simplelist type="inline"> <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member> <member><citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry></member> + <member><citerefentry><refentrytitle>systemd-sysupdated.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member> <member><citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry></member> </simplelist></para> </refsect1> diff --git a/man/systemd-sysupdated.service.xml b/man/systemd-sysupdated.service.xml new file mode 100644 index 0000000000..b7a4f3942a --- /dev/null +++ b/man/systemd-sysupdated.service.xml @@ -0,0 +1,54 @@ +<?xml version='1.0'?> <!--*-nxml-*--> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"> +<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> + +<refentry id="systemd-sysupdated.service" conditional='ENABLE_SYSUPDATE'> + + <refentryinfo> + <title>systemd-sysupdated.service</title> + <productname>systemd</productname> + </refentryinfo> + + <refmeta> + <refentrytitle>systemd-sysupdated.service</refentrytitle> + <manvolnum>8</manvolnum> + </refmeta> + + <refnamediv> + <refname>systemd-sysupdated.service</refname> + <refname>systemd-sysupdated</refname> + <refpurpose>System Update Service</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <para><filename>systemd-sysupdated.service</filename></para> + <para><filename>/usr/lib/systemd/systemd-sysupdated</filename></para> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para><command>systemd-sysupdated</command> is a system service that allows unprivileged + clients to update the system. It works by scanning the system for updateable "targets" (i.e. + portable services, sysexts, sysupdate components, etc.) and exposing them on the bus. Each + target then has methods that translate directly into invocations of + <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>. + </para> + + <para>See + <citerefentry><refentrytitle>org.freedesktop.sysupdate1</refentrytitle><manvolnum>5</manvolnum></citerefentry> + and + <citerefentry><refentrytitle>org.freedesktop.LogControl1</refentrytitle><manvolnum>5</manvolnum></citerefentry> + for a description of the D-Bus API.</para> + </refsect1> + + <refsect1> + <title>See Also</title> + <para> + <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>, + <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry> + </para> + </refsect1> + +</refentry> diff --git a/meson.build b/meson.build index cef6ab9cb8..5e0b666c64 100644 --- a/meson.build +++ b/meson.build @@ -277,6 +277,7 @@ conf.set_quoted('SYSTEMD_LANGUAGE_FALLBACK_MAP', pkgdatadir / 'lang conf.set_quoted('SYSTEMD_MAKEFS_PATH', libexecdir / 'systemd-makefs') conf.set_quoted('SYSTEMD_PULL_PATH', libexecdir / 'systemd-pull') conf.set_quoted('SYSTEMD_SHUTDOWN_BINARY_PATH', libexecdir / 'systemd-shutdown') +conf.set_quoted('SYSTEMD_SYSUPDATE_PATH', libexecdir / 'systemd-sysupdate') conf.set_quoted('SYSTEMD_TEST_DATA', testdata_dir) conf.set_quoted('SYSTEMD_TTY_ASK_PASSWORD_AGENT_BINARY_PATH', bindir / 'systemd-tty-ask-password-agent') conf.set_quoted('SYSTEMD_UPDATE_HELPER_PATH', libexecdir / 'systemd-update-helper') diff --git a/po/POTFILES.in b/po/POTFILES.in index 16899fd5f9..d9c602cf20 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -11,5 +11,6 @@ src/machine/org.freedesktop.machine1.policy src/network/org.freedesktop.network1.policy src/portable/org.freedesktop.portable1.policy src/resolve/org.freedesktop.resolve1.policy +src/sysupdate/org.freedesktop.sysupdate1.policy src/timedate/org.freedesktop.timedate1.policy src/core/dbus-unit.c diff --git a/src/shared/bus-locator.c b/src/shared/bus-locator.c index ff7a872bdb..80d2b5371c 100644 --- a/src/shared/bus-locator.c +++ b/src/shared/bus-locator.c @@ -63,6 +63,12 @@ const BusLocator* const bus_systemd_mgr = &(BusLocator){ .interface = "org.freedesktop.systemd1.Manager" }; +const BusLocator* const bus_sysupdate_mgr = &(BusLocator){ + .destination = "org.freedesktop.sysupdate1", + .path = "/org/freedesktop/sysupdate1", + .interface = "org.freedesktop.sysupdate1.Manager" +}; + const BusLocator* const bus_timedate = &(BusLocator){ .destination = "org.freedesktop.timedate1", .path = "/org/freedesktop/timedate1", diff --git a/src/shared/bus-locator.h b/src/shared/bus-locator.h index 4f50a9727f..8116aa27c0 100644 --- a/src/shared/bus-locator.h +++ b/src/shared/bus-locator.h @@ -20,6 +20,7 @@ extern const BusLocator* const bus_oom_mgr; extern const BusLocator* const bus_portable_mgr; extern const BusLocator* const bus_resolve_mgr; extern const BusLocator* const bus_systemd_mgr; +extern const BusLocator* const bus_sysupdate_mgr; extern const BusLocator* const bus_timedate; extern const BusLocator* const bus_timesync_mgr; diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build index b1b1204a2a..8bd422fc43 100644 --- a/src/sysupdate/meson.build +++ b/src/sysupdate/meson.build @@ -30,4 +30,20 @@ executables += [ threads, ], }, + libexec_template + { + 'name' : 'systemd-sysupdated', + 'dbus' : true, + 'conditions' : ['ENABLE_SYSUPDATE'], + 'sources' : files('sysupdated.c'), + 'dependencies' : threads, + }, ] + +if conf.get('ENABLE_SYSUPDATE') == 1 + install_data('org.freedesktop.sysupdate1.conf', + install_dir : dbuspolicydir) + install_data('org.freedesktop.sysupdate1.service', + install_dir : dbussystemservicedir) + install_data('org.freedesktop.sysupdate1.policy', + install_dir : polkitpolicydir) +endif diff --git a/src/sysupdate/org.freedesktop.sysupdate1.conf b/src/sysupdate/org.freedesktop.sysupdate1.conf new file mode 100644 index 0000000000..30cb1eec24 --- /dev/null +++ b/src/sysupdate/org.freedesktop.sysupdate1.conf @@ -0,0 +1,88 @@ +<?xml version="1.0"?> <!--*-nxml-*--> +<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> + +<!-- + SPDX-License-Identifier: LGPL-2.1-or-later + + This file is part of systemd. + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. +--> + +<busconfig> + + <policy user="root"> + <allow own="org.freedesktop.sysupdate1"/> + <allow send_destination="org.freedesktop.sysupdate1"/> + <allow receive_sender="org.freedesktop.sysupdate1"/> + </policy> + + <policy context="default"> + <deny send_destination="org.freedesktop.sysupdate1"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.DBus.Introspectable"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.DBus.Peer"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.DBus.Properties" + send_member="Get"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.DBus.Properties" + send_member="GetAll"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Manager" + send_member="ListTargets"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Manager" + send_member="ListJobs"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Manager" + send_member="ListAppStream"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Target" + send_member="List"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Target" + send_member="Describe"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Target" + send_member="CheckNew"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Target" + send_member="Update"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Target" + send_member="Vacuum"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Target" + send_member="GetAppstream"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Target" + send_member="GetVersion"/> + + <allow send_destination="org.freedesktop.sysupdate1" + send_interface="org.freedesktop.sysupdate1.Job" + send_member="Cancel"/> + + <allow receive_sender="org.freedesktop.sysupdate1"/> + </policy> + +</busconfig> diff --git a/src/sysupdate/org.freedesktop.sysupdate1.policy b/src/sysupdate/org.freedesktop.sysupdate1.policy new file mode 100644 index 0000000000..7c1b94333c --- /dev/null +++ b/src/sysupdate/org.freedesktop.sysupdate1.policy @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*--> +<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> + +<!-- + SPDX-License-Identifier: LGPL-2.1-or-later + + This file is part of systemd. + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. +--> + +<policyconfig> + + <vendor>The systemd Project</vendor> + <vendor_url>https://systemd.io</vendor_url> + + <!-- + SECURITY: the default policy allows any user with an active session on the local console to check + for updates and update the system to the latest version without extra authentication. + Depending on the use case it might make sense to request authentication here, or add a polkit + rule to only allow access to these actions for members of a given group. + + The default policy matches prior art in distributions and system update managers. To update a + system, for example: packagekit requires only a user with an active session, eos-updater needs + a user at the console, and rpm-ostree (generally) needs an "administrative user" at the computer. + Without this default, distributions hoping to use sysupdate as an update mechanism will have to + set the policy to it anyhow. + --> + + <action id="org.freedesktop.sysupdate1.check"> + <description gettext-domain="systemd">Check for system updates</description> + <message gettext-domain="systemd">Authentication is required to check for system updates</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>yes</allow_active> + </defaults> + </action> + + <action id="org.freedesktop.sysupdate1.update"> + <description gettext-domain="systemd">Install system updates</description> + <message gettext-domain="systemd">Authentication is required to install system updates</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>yes</allow_active> + </defaults> + </action> + + <action id="org.freedesktop.sysupdate1.update-to-version"> + <description gettext-domain="systemd">Install specific system version</description> + <message gettext-domain="systemd">Authentication is required to update the system to a specific (possibly old) version</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + </action> + + <action id="org.freedesktop.sysupdate1.vacuum"> + <description gettext-domain="systemd">Cleanup old system updates</description> + <message gettext-domain="systemd">Authentication is required to cleanup old system updates</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + </action> + +</policyconfig> diff --git a/src/sysupdate/org.freedesktop.sysupdate1.service b/src/sysupdate/org.freedesktop.sysupdate1.service new file mode 100644 index 0000000000..67e1a29078 --- /dev/null +++ b/src/sysupdate/org.freedesktop.sysupdate1.service @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[D-BUS Service] +Name=org.freedesktop.sysupdate1 +Exec=/bin/false +User=root +SystemdService=dbus-org.freedesktop.sysupdate1.service diff --git a/src/sysupdate/sysupdate-util.h b/src/sysupdate/sysupdate-util.h index fdd6c8318e..56339a87b1 100644 --- a/src/sysupdate/sysupdate-util.h +++ b/src/sysupdate/sysupdate-util.h @@ -3,3 +3,5 @@ #pragma once int reboot_now(void); + +#define SD_SYSTEMD_SYSUPDATE_OFFLINE (UINT64_C(1) << 0) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index aded7dde0b..dee8348bdb 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -1543,7 +1543,7 @@ static int run(int argc, char *argv[]) { return r; /* SIGCHLD signal must be blocked for sd_event_add_child to work */ - BLOCK_SIGNALS(SIGCHLD); + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); return sysupdate_main(argc, argv); } diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c new file mode 100644 index 0000000000..e2c3d7e102 --- /dev/null +++ b/src/sysupdate/sysupdated.c @@ -0,0 +1,1911 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-bus.h" +#include "sd-json.h" + +#include "build-path.h" +#include "bus-error.h" +#include "bus-get-properties.h" +#include "bus-label.h" +#include "bus-log-control-api.h" +#include "bus-polkit.h" +#include "bus-util.h" +#include "common-signal.h" +#include "discover-image.h" +#include "env-util.h" +#include "escape.h" +#include "event-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "hashmap.h" +#include "log.h" +#include "main-func.h" +#include "memfd-util.h" +#include "mkdir-label.h" +#include "os-util.h" +#include "process-util.h" +#include "service-util.h" +#include "signal-util.h" +#include "socket-util.h" +#include "string-table.h" +#include "sysupdate-util.h" + +typedef struct Manager { + sd_event *event; + sd_bus *bus; + + Hashmap *targets; + + uint64_t last_job_id; + Hashmap *jobs; + + Hashmap *polkit_registry; + + sd_event_source *notify_event; +} Manager; + +/* Forward declare so that jobs can call it on exit */ +static void manager_check_idle(Manager *m); + +typedef enum TargetClass { + /* These should try to match ImageClass from src/basic/os-util.h */ + TARGET_MACHINE = IMAGE_MACHINE, + TARGET_PORTABLE = IMAGE_PORTABLE, + TARGET_SYSEXT = IMAGE_SYSEXT, + TARGET_CONFEXT = IMAGE_CONFEXT, + _TARGET_CLASS_IS_IMAGE_CLASS_MAX, + + /* sysupdate-specific classes */ + TARGET_HOST = _TARGET_CLASS_IS_IMAGE_CLASS_MAX, + TARGET_COMPONENT, + + _TARGET_CLASS_MAX, + _TARGET_CLASS_INVALID = -EINVAL, +} TargetClass; + +/* Let's ensure when the number of classes is updated things are updated here too */ +assert_cc((int) _IMAGE_CLASS_MAX == (int) _TARGET_CLASS_IS_IMAGE_CLASS_MAX); + +typedef struct Target { + Manager *manager; + + TargetClass class; + char *name; + char *path; + + char *id; + ImageType image_type; + bool busy; +} Target; + +typedef enum JobType { + JOB_LIST, + JOB_DESCRIBE, + JOB_CHECK_NEW, + JOB_UPDATE, + JOB_VACUUM, + _JOB_TYPE_MAX, + _JOB_TYPE_INVALID = -EINVAL, +} JobType; + +typedef struct Job Job; + +typedef int (*JobReady)(sd_bus_message *msg, const Job *job); +typedef int (*JobComplete)(sd_bus_message *msg, const Job *job, sd_json_variant *response, sd_bus_error *error); + +struct Job { + Manager *manager; + Target *target; + + uint64_t id; + char *object_path; + + JobType type; + bool offline; + char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */ + + unsigned progress_percent; + + sd_event_source *child; + int stdout_fd; + int status_errno; + unsigned n_cancelled; + + sd_json_variant *json; + + JobComplete complete_cb; /* Callback called on job exit */ + sd_bus_message *dbus_msg; + JobReady detach_cb; /* Callback called when job has started. Detaches the job to run in the background */ +}; + +static const char* const target_class_table[_TARGET_CLASS_MAX] = { + [TARGET_MACHINE] = "machine", + [TARGET_PORTABLE] = "portable", + [TARGET_SYSEXT] = "sysext", + [TARGET_CONFEXT] = "confext", + [TARGET_COMPONENT] = "component", + [TARGET_HOST] = "host", +}; + +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", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(job_type, JobType); + +static Job *job_free(Job *j) { + if (!j) + return NULL; + + if (j->manager) + assert_se(hashmap_remove(j->manager->jobs, &j->id) == j); + + free(j->object_path); + free(j->version); + + sd_json_variant_unref(j->json); + + sd_bus_message_unref(j->dbus_msg); + + sd_event_source_disable_unref(j->child); + safe_close(j->stdout_fd); + + return mfree(j); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Job*, job_free); +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(job_hash_ops, uint64_t, uint64_hash_func, uint64_compare_func, + Job, job_free); + +static int job_new(JobType type, Target *t, sd_bus_message *msg, JobComplete complete_cb, Job **ret) { + _cleanup_(job_freep) Job *j = NULL; + int r; + + assert(t); + assert(ret); + + j = new(Job, 1); + if (!j) + return -ENOMEM; + + *j = (Job) { + .type = type, + .target = t, + .id = t->manager->last_job_id + 1, + .stdout_fd = -EBADF, + .complete_cb = complete_cb, + .dbus_msg = sd_bus_message_ref(msg), + }; + + if (asprintf(&j->object_path, "/org/freedesktop/sysupdate1/job/_%" PRIu64, j->id) < 0) + return -ENOMEM; + + r = hashmap_ensure_put(&t->manager->jobs, &job_hash_ops, &j->id, j); + if (r < 0) + return r; + + j->manager = t->manager; + + t->manager->last_job_id = j->id; + + *ret = TAKE_PTR(j); + return 0; +} + +static int job_parse_child_output(int _fd, sd_json_variant **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + /* Take ownership of the passed fd */ + _cleanup_close_ int fd = _fd; + _cleanup_fclose_ FILE *f = NULL; + struct stat st; + int r; + + assert(ret); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat stdout fd: %m"); + + assert(S_ISREG(st.st_mode)); + + if (st.st_size == 0) { + log_warning("No output from child job, ignoring"); + return 0; + } + + if (lseek(fd, SEEK_SET, 0) == (off_t) -1) + return log_error_errno(errno, "Failed to seek to beginning of memfd: %m"); + + f = take_fdopen(&fd, "r"); + if (!f) + return log_error_errno(errno, "Failed to reopen memfd: %m"); + + r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON: %m"); + + *ret = TAKE_PTR(v); + return 0; +} + +static void job_on_ready(Job *j) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *msg = NULL; + int r; + + assert(j); + + /* Some jobs run in the background as we return the job ID to the dbus caller (i.e. for the Update + * method). However, the worker will perform some sanity-checks on startup which would be valuable + * as dbus errors. So, we wait for the worker to signal via READY=1 that it has completed its sanity + * checks and we should continue the job in the background. */ + + if (!j->detach_cb) + return; + + assert(j->dbus_msg); + msg = TAKE_PTR(j->dbus_msg); + + j->complete_cb = NULL; + + r = j->detach_cb(msg, j); + if (r < 0) + log_warning_errno(r, "Failed to run callback on job ready event, ignoring: %m"); +} + +static void job_on_errno(Job *j, char *b) { + /* Take ownership of donated buffer */ + _cleanup_free_ char *buf = TAKE_PTR(b); + int r; + + assert(j); + assert_se(buf); + + r = parse_errno(buf); + if (r < 0) { + log_warning_errno(r, "Got invalid errno value, ignoring: %m"); + return; + } + + j->status_errno = r; + + log_debug_errno(r, "Got errno from job %" PRIu64 ": %i (%m)", j->id, r); +} + +static void job_on_progress(Job *j, char *b) { + /* Take ownership of donated buffer */ + _cleanup_free_ char *buf = TAKE_PTR(b); + unsigned progress; + int r; + + assert(j); + assert_se(buf); + + r = safe_atou(buf, &progress); + if (r < 0 || progress > 100) { + log_warning("Got invalid percent value, ignoring."); + return; + } + + j->progress_percent = progress; + (void) sd_bus_emit_properties_changed(j->manager->bus, j->object_path, + "org.freedesktop.sysupdate1.Job", + "Progress", NULL); + + log_debug("Got percentage from job %" PRIu64 ": %u%%", j->id, j->progress_percent); +} + +static void job_on_version(Job *j, char *version) { + assert(j); + assert_se(version); + + /* Take ownership of donated memory */ + free_and_replace(j->version, version); + + log_debug("Got version from job %" PRIu64 ": %s ", j->id, j->version); +} + +static int job_on_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + Job *j = ASSERT_PTR(userdata); + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + Manager *manager = j->manager; + int r; + + assert(j); + assert(s); + assert(si); + + if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM)) { + assert(j->target->busy); + j->target->busy = false; + } + + if (si->si_code != CLD_EXITED) { + assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED)); + sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, + "Job terminated abnormally with signal %s.", + signal_to_string(si->si_status)); + } else if (si->si_status != EXIT_SUCCESS) + if (j->status_errno != 0) + sd_bus_error_set_errno(&error, j->status_errno); + else + sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, + "Job failed with exit code %i.", si->si_status); + else { + r = job_parse_child_output(TAKE_FD(j->stdout_fd), &json); + if (r < 0) + sd_bus_error_set_errnof(&error, r, "Failed to parse JSON: %m"); + } + + /* Only send notification of exit if the job was actually detached */ + if (j->detach_cb) { + r = sd_bus_emit_signal( + j->manager->bus, + "/org/freedesktop/sysupdate1", + "org.freedesktop.sysupdate1.Manager", + "JobRemoved", + "toi", + j->id, + j->object_path, + j->status_errno != 0 ? -j->status_errno : si->si_status); + if (r < 0) + log_warning_errno(r, "Cannot emit JobRemoved message, ignoring: %m"); + } + + if (j->dbus_msg && j->complete_cb) { + if (sd_bus_error_is_set(&error)) { + log_warning("Bus error occurred, ignoring callback for job: %s", error.message); + sd_bus_reply_method_error(j->dbus_msg, &error); + } else { + r = j->complete_cb(j->dbus_msg, j, json, &error); + if (r < 0) { + log_warning_errno(r, "Error during execution of job callback: %s", bus_error_message(&error, r)); + sd_bus_reply_method_errno(j->dbus_msg, r, &error); + } + } + } + + job_free(j); + + if (manager) + manager_check_idle(manager); + + return 0; +} + +static inline const char* sysupdate_binary_path(void) { + return secure_getenv("SYSTEMD_SYSUPDATE_PATH") ?: SYSTEMD_SYSUPDATE_PATH; +} + +static int target_get_argument(Target *t, char **ret) { + _cleanup_free_ char *target_arg = NULL; + + assert(t); + assert(ret); + + if (t->class != TARGET_HOST) { + if (t->class == TARGET_COMPONENT) + target_arg = strjoin("--component=", t->name); + else if (IN_SET(t->image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME)) + target_arg = strjoin("--root=", t->path); + else if (IN_SET(t->image_type, IMAGE_RAW, IMAGE_BLOCK)) + target_arg = strjoin("--image=", t->path); + else + assert_not_reached(); + if (!target_arg) + return -ENOMEM; + } + + *ret = TAKE_PTR(target_arg); + return 0; +} + +static int job_start(Job *j) { + _cleanup_close_ int stdout_fd = -EBADF; + _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL; + int r; + + assert(j); + + if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM) && j->target->busy) + return log_notice_errno(SYNTHETIC_ERRNO(EBUSY), "Target %s busy, ignoring job.", j->target->name); + + stdout_fd = memfd_new("sysupdate-stdout"); + if (stdout_fd < 0) + return log_error_errno(stdout_fd, "Failed to create memfd: %m"); + + r = pidref_safe_fork_full("(sd-sysupdate)", + (int[]) { -EBADF, stdout_fd, STDERR_FILENO }, NULL, 0, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM| + FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid); + if (r < 0) + return r; /* FORK_LOG means pidref_safe_fork_full will handle the logging */ + if (r == 0) { + /* Child */ + + _cleanup_free_ char *target_arg = NULL; + const char *cmd[] = { + "systemd-sysupdate", + "--json=short", + 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 + }; + size_t k = 2; + + if (setenv("NOTIFY_SOCKET", "/run/systemd/sysupdate/notify", /* overwrite= */ 1) < 0) { + log_error_errno(errno, "setenv() failed: %m"); + _exit(EXIT_FAILURE); + } + + if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0) + cmd[k++] = "--verify=no"; /* For testing */ + + r = setenv_systemd_exec_pid(true); + if (r < 0) + log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m"); + + r = target_get_argument(j->target, &target_arg); + if (r < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + if (target_arg) + cmd[k++] = target_arg; + + if (j->offline) + cmd[k++] = "--offline"; + + switch (j->type) { + case JOB_LIST: + cmd[k++] = "list"; + break; + + case JOB_DESCRIBE: + cmd[k++] = "list"; + assert(!isempty(j->version)); + cmd[k++] = j->version; + break; + + case JOB_CHECK_NEW: + cmd[k++] = "check-new"; + break; + + case JOB_UPDATE: + cmd[k++] = "update"; + cmd[k++] = empty_to_null(j->version); + break; + + case JOB_VACUUM: + cmd[k++] = "vacuum"; + break; + + default: + assert_not_reached(); + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *s = NULL; + + s = quote_command_line((char**) cmd, SHELL_ESCAPE_EMPTY); + if (!s) { + log_oom(); + _exit(EXIT_FAILURE); + } + + log_debug("Spawning worker for job %" PRIu64 ": %s", j->id, s); + } + + r = invoke_callout_binary(sysupdate_binary_path(), (char *const *) cmd); + log_error_errno(r, "Failed to execute systemd-sysupdate: %m"); + _exit(EXIT_FAILURE); + } + + r = event_add_child_pidref(j->manager->event, &j->child, &pid, WEXITED, job_on_exit, j); + if (r < 0) + return log_error_errno(r, "Failed to add child process to event loop: %m"); + + r = sd_event_source_set_child_process_own(j->child, true); + if (r < 0) + return log_error_errno(r, "Event loop failed to take ownership of child process: %m"); + TAKE_PIDREF(pid); + + j->stdout_fd = TAKE_FD(stdout_fd); + + if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM)) + j->target->busy = true; + + return 0; +} + +static int job_cancel(Job *j) { + int r; + + assert(j); + + r = sd_event_source_send_child_signal(j->child, j->n_cancelled < 3 ? SIGTERM : SIGKILL, + NULL, 0); + if (r < 0) + return r; + + j->n_cancelled++; + return 0; +} + +static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Job *j = ASSERT_PTR(userdata); + const char *action; + int r; + + assert(msg); + + switch (j->type) { + case JOB_LIST: + case JOB_DESCRIBE: + case JOB_CHECK_NEW: + action = "org.freedesktop.sysupdate1.check"; + break; + + case JOB_UPDATE: + if (j->version) + action = "org.freedesktop.sysupdate1.update-to-version"; + else + action = "org.freedesktop.sysupdate1.update"; + break; + + case JOB_VACUUM: + action = "org.freedesktop.sysupdate1.vacuum"; + 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 */ + + r = job_cancel(j); + if (r < 0) + return r; + + return sd_bus_reply_method_return(msg, NULL); +} + +static BUS_DEFINE_PROPERTY_GET_ENUM(job_property_get_type, job_type, JobType); + +static int job_object_find( + sd_bus *bus, + const char *path, + const char *iface, + void *userdata, + void **ret, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + Job *j; + const char *p; + uint64_t id; + int r; + + assert(bus); + assert(path); + assert(ret); + + p = startswith(path, "/org/freedesktop/sysupdate1/job/_"); + if (!p) + return 0; + + r = safe_atou64(p, &id); + if (r < 0 || id == 0) + return 0; + + j = hashmap_get(m->jobs, &id); + if (!j) + return 0; + + *ret = j; + return 1; +} + +static int job_node_enumerator( + sd_bus *bus, + const char *path, + void *userdata, + char ***nodes, + sd_bus_error *error) { + + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + Job *j; + unsigned k = 0; + + l = new0(char*, hashmap_size(m->jobs) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(j, m->jobs) { + l[k] = strdup(j->object_path); + if (!l[k]) + return -ENOMEM; + k++; + } + + *nodes = TAKE_PTR(l); + return 1; +} + +static const sd_bus_vtable job_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("Id", "t", NULL, offsetof(Job, id), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Type", "s", job_property_get_type, offsetof(Job, type), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Offline", "b", bus_property_get_bool, offsetof(Job, offline), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Progress", "u", bus_property_get_unsigned, offsetof(Job, progress_percent), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_METHOD("Cancel", NULL, NULL, job_method_cancel, SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END +}; + +static const BusObjectImplementation job_object = { + "/org/freedesktop/sysupdate1/job", + "org.freedesktop.sysupdate1.Job", + .fallback_vtables = BUS_FALLBACK_VTABLES({job_vtable, job_object_find}), + .node_enumerator = job_node_enumerator, +}; + +static Target *target_free(Target *t) { + if (!t) + return NULL; + + free(t->name); + free(t->path); + free(t->id); + + return mfree(t); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Target*, target_free); +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(target_hash_ops, char, string_hash_func, string_compare_func, + Target, target_free); + +static int target_new(Manager *m, TargetClass class, const char *name, const char *path, Target **ret) { + _cleanup_(target_freep) Target *t = NULL; + int r; + + assert(m); + assert(ret); + + t = new(Target, 1); + if (!t) + return -ENOMEM; + + *t = (Target) { + .manager = m, + .class = class, + .image_type = _IMAGE_TYPE_INVALID, + }; + + t->name = strdup(name); + if (!t->name) + return -ENOMEM; + + t->path = strdup(path); + if (!t->path) + return -ENOMEM; + + if (class == TARGET_HOST) + t->id = strdup("host"); /* This is what appears in the object path */ + else + t->id = strjoin(target_class_to_string(class), ":", name); + if (!t->id) + return -ENOMEM; + + r = hashmap_ensure_put(&m->targets, &target_hash_ops, t->id, t); + if (r < 0) + return r; + + *ret = TAKE_PTR(t); + return 0; +} + +static int sysupdate_run_simple(sd_json_variant **ret, ...) { + _cleanup_close_pair_ int pipe[2] = EBADF_PAIR; + _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + r = pipe2(pipe, O_CLOEXEC); + if (r < 0) + return -errno; + + r = pidref_safe_fork_full("(sd-sysupdate)", + (int[]) { -EBADF, pipe[1], STDERR_FILENO }, + NULL, 0, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM| + FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, + &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + va_list ap; + char *arg; + _cleanup_strv_free_ char **args = NULL; + + if (strv_extend(&args, "systemd-sysupdate") < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + + if (strv_extend(&args, "--json=short") < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + + va_start(ap, ret); + while ((arg = va_arg(ap, char*))) { + r = strv_extend(&args, arg); + if (r < 0) + break; + } + va_end(ap); + if (r < 0) { + log_oom(); + _exit(EXIT_FAILURE); + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *s = NULL; + + s = quote_command_line((char**) args, SHELL_ESCAPE_EMPTY); + if (!s) { + log_oom(); + _exit(EXIT_FAILURE); + } + + log_debug("Spawning sysupdate: %s", s); + } + + r = invoke_callout_binary(sysupdate_binary_path(), args); + log_error_errno(r, "Failed to execute systemd-sysupdate: %m"); + _exit(EXIT_FAILURE); + } + + pipe[1] = safe_close(pipe[1]); + f = take_fdopen(&pipe[0], "r"); + if (!f) + return -errno; + + r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON: %m"); + + *ret = TAKE_PTR(v); + return 0; +} + +static BUS_DEFINE_PROPERTY_GET_ENUM(target_property_get_class, target_class, TargetClass); + +#define log_sysupdate_bad_json(verb, msg) \ + log_debug("Invalid JSON response from 'systemd-sysupdate %s': %s", verb, msg) + +static int target_method_list_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + + sd_json_variant *v; + _cleanup_strv_free_ char **versions = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r; + + assert(json); + + v = sd_json_variant_by_key(json, "all"); + if (!v) { + log_sysupdate_bad_json("list", "Missing key 'all'"); + return -EINVAL; + } + + r = sd_json_variant_strv(v, &versions); + 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, versions); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int target_method_list(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + int r; + uint64_t flags; + + assert(msg); + + r = sd_bus_message_read(msg, "t", &flags); + if (r < 0) + return r; + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "offline", one_zero(FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE)), + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.check", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_LIST, t, msg, target_method_list_finish, &j); + if (r < 0) + return r; + + j->offline = FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE); + + 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 int target_method_describe_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + _cleanup_free_ char *text = NULL; + int r; + + assert(json); + + r = sd_json_variant_format(json, 0, &text); + if (r < 0) + return r; + + return sd_bus_reply_method_return(msg, "s", text); +} + +static int target_method_describe(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + const char *version; + int r; + uint64_t flags; + + assert(msg); + + r = sd_bus_message_read(msg, "st", &version, &flags); + if (r < 0) + return r; + + if (isempty(version)) + return -EINVAL; + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "version", version, + "offline", one_zero(FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE)), + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.check", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_DESCRIBE, t, msg, target_method_describe_finish, &j); + if (r < 0) + return r; + + j->version = strdup(version); + if (!j->version) + return log_oom(); + + j->offline = FLAGS_SET(flags, SD_SYSTEMD_SYSUPDATE_OFFLINE); + + 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 int target_method_check_new_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + const char *reply; + + assert(json); + + sd_json_variant *v = sd_json_variant_by_key(json, "available"); + if (!v) { + log_sysupdate_bad_json("check-new", "Missing key 'available'"); + return -EINVAL; + } + + if (sd_json_variant_is_null(v)) + reply = ""; + else + reply = sd_json_variant_string(v); + if (!reply) + return -EINVAL; + + return sd_bus_reply_method_return(msg, "s", reply); +} + +static int target_method_check_new(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + int r; + + assert(msg); + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "offline", "0", + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.check", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_CHECK_NEW, t, msg, target_method_check_new_finish, &j); + if (r < 0) + return r; + + 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 int target_method_update_finished_early( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + + /* Called when job finishes w/ a successful exit code, but before any work begins. + * This happens when there is no candidate (i.e. we're already up-to-date), or + * specified update is already installed. */ + return sd_bus_error_setf(error, "org.freedesktop.sysupdate1.NoCandidate", + "Job exited successfully with no work to do, assume already updated"); +} + +static int target_method_update_detach(sd_bus_message *msg, const Job *j) { + int r; + + assert(msg); + assert(j); + + r = sd_bus_reply_method_return(msg, "sto", j->version, j->id, j->object_path); + if (r < 0) + return bus_log_parse_error(r); + + return 0; +} + +static int target_method_update(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + const char *version, *action; + uint64_t flags; + int r; + + assert(msg); + + r = sd_bus_message_read(msg, "st", &version, &flags); + if (r < 0) + return r; + + if (flags != 0) + return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(EINVAL), "Flags argument must be 0: %m"); + + if (isempty(version)) + action = "org.freedesktop.sysupdate1.update"; + else + action = "org.freedesktop.sysupdate1.update-to-version"; + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + "version", version, + NULL + }; + + r = bus_verify_polkit_async( + msg, + action, + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_UPDATE, t, msg, target_method_update_finished_early, &j); + if (r < 0) + return r; + j->detach_cb = target_method_update_detach; + + j->version = strdup(version); + if (!j->version) + return -ENOMEM; + + r = job_start(j); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to start job: %m"); + TAKE_PTR(j); + + return 1; +} + +static int target_method_vacuum_finish( + sd_bus_message *msg, + const Job *j, + sd_json_variant *json, + sd_bus_error *error) { + + uint64_t instances; + + assert(json); + + instances = sd_json_variant_unsigned(sd_json_variant_by_key(json, "removed")); + + return sd_bus_reply_method_return(msg, "u", instances); +} + +static int target_method_vacuum(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(job_freep) Job *j = NULL; + int r; + + assert(msg); + + const char *details[] = { + "class", target_class_to_string(t->class), + "name", t->name, + NULL + }; + + r = bus_verify_polkit_async( + msg, + "org.freedesktop.sysupdate1.vacuum", + details, + &t->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = job_new(JOB_VACUUM, t, msg, target_method_vacuum_finish, &j); + if (r < 0) + return r; + + 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 int target_method_get_version(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_free_ char *target_arg = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + sd_json_variant *version_json; + int r; + + r = target_get_argument(t, &target_arg); + if (r < 0) + return r; + + r = sysupdate_run_simple(&v, "--offline", "list", target_arg, NULL); + if (r < 0) + return r; + + version_json = sd_json_variant_by_key(v, "current"); + if (!version_json) { + log_sysupdate_bad_json("list", "Missing key 'current'"); + return -EINVAL; + } + + if (sd_json_variant_is_null(version_json)) + return sd_bus_reply_method_return(msg, "s", ""); + + if (!sd_json_variant_is_string(version_json)) { + log_sysupdate_bad_json("list", "Expected string value for key 'current'"); + return -EINVAL; + } + + return sd_bus_reply_method_return(msg, "s", sd_json_variant_string(version_json)); +} + +static int target_get_appstream(Target *t, char ***ret) { + _cleanup_free_ char *target_arg = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + sd_json_variant *appstream_url_json; + int r; + + r = target_get_argument(t, &target_arg); + if (r < 0) + return r; + + r = sysupdate_run_simple(&v, "--offline", "list", target_arg, NULL); + if (r < 0) + return r; + + appstream_url_json = sd_json_variant_by_key(v, "appstream_urls"); + if (!appstream_url_json) { + log_sysupdate_bad_json("list", "Missing key 'appstream_urls'"); + return -EINVAL; + } + + r = sd_json_variant_strv(appstream_url_json, ret); + if (r < 0) { + log_sysupdate_bad_json("list", "Expected array of strings for key 'appstream_urls'"); + return r; + } + + return 0; +} + +static int target_method_get_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + Target *t = ASSERT_PTR(userdata); + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_strv_free_ char **appstream_urls = NULL; + int r; + + r = target_get_appstream(t, &appstream_urls); + 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, appstream_urls); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, 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; + _cleanup_free_ char *target_arg = NULL; + sd_json_variant *v; + bool have_default; + int r; + + if (t) { + r = target_get_argument(t, &target_arg); + if (r < 0) + return r; + } + + r = sysupdate_run_simple(&json, "components", target_arg, NULL); + if (r < 0) + return r; + + v = sd_json_variant_by_key(json, "default"); + if (!v) + return -EINVAL; + have_default = sd_json_variant_boolean(v); + + v = sd_json_variant_by_key(json, "components"); + if (!v) + return -EINVAL; + r = sd_json_variant_strv(v, &components); + if (r < 0) + return r; + + if (ret_components) + *ret_components = TAKE_PTR(components); + if (ret_have_default) + *ret_have_default = have_default; + return 0; +} + +static int manager_ensure_targets(Manager *m); + +static int target_object_find( + sd_bus *bus, + const char *path, + const char *iface, + void *userdata, + void **found, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + Target *t; + _cleanup_free_ char *e = NULL; + const char *p; + int r; + + assert(bus); + assert(path); + assert(found); + + p = startswith(path, "/org/freedesktop/sysupdate1/target/"); + if (!p) + return 0; + + e = bus_label_unescape(p); + if (!e) + return -ENOMEM; + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + t = hashmap_get(m->targets, e); + if (!t) + return 0; + + *found = t; + return 1; +} + +static char *target_bus_path(Target *t) { + _cleanup_free_ char *e = NULL; + + assert(t); + + e = bus_label_escape(t->id); + if (!e) + return NULL; + + return strjoin("/org/freedesktop/sysupdate1/target/", e); +} + +static int target_node_enumerator( + sd_bus *bus, + const char *path, + void *userdata, + char ***nodes, + sd_bus_error *error) { + + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + Target *t; + unsigned k = 0; + int r; + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + l = new0(char*, hashmap_size(m->targets) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(t, m->targets) { + l[k] = target_bus_path(t); + if (!l[k]) + return -ENOMEM; + k++; + } + + *nodes = TAKE_PTR(l); + return 1; +} + +static const sd_bus_vtable target_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("Class", "s", target_property_get_class, + offsetof(Target, class), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Name", "s", NULL, offsetof(Target, name), + SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Path", "s", NULL, offsetof(Target, path), + SD_BUS_VTABLE_PROPERTY_CONST), + + SD_BUS_METHOD_WITH_ARGS("List", + SD_BUS_ARGS("t", flags), + SD_BUS_RESULT("as", versions), + target_method_list, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("Describe", + SD_BUS_ARGS("s", version, "t", flags), + SD_BUS_RESULT("s", json), + target_method_describe, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("CheckNew", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("s", new_version), + target_method_check_new, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("Update", + SD_BUS_ARGS("s", new_version, "t", flags), + SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path), + target_method_update, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("Vacuum", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("u", count), + target_method_vacuum, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("GetAppStream", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("as", appstream), + target_method_get_appstream, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("GetVersion", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("s", version), + target_method_get_version, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END +}; + +static const BusObjectImplementation target_object = { + "/org/freedesktop/sysupdate1/target", + "org.freedesktop.sysupdate1.Target", + .fallback_vtables = BUS_FALLBACK_VTABLES({target_vtable, target_object_find}), + .node_enumerator = target_node_enumerator, +}; + +static Manager *manager_free(Manager *m) { + if (!m) + return NULL; + + hashmap_free(m->targets); + hashmap_free(m->jobs); + + m->bus = sd_bus_flush_close_unref(m->bus); + sd_event_source_unref(m->notify_event); + sd_event_unref(m->event); + + return mfree(m); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager *, manager_free); + +static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + char buf[NOTIFY_BUFFER_MAX+1]; + struct iovec iovec = { + .iov_base = buf, + .iov_len = sizeof(buf)-1, + }; + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control; + struct msghdr msghdr = { + .msg_iov = &iovec, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct ucred *ucred; + Manager *m = ASSERT_PTR(userdata); + Job *j; + ssize_t n; + char *p; + + n = recvmsg_safe(fd, &msghdr, MSG_DONTWAIT|MSG_CMSG_CLOEXEC); + if (n < 0) { + if (ERRNO_IS_TRANSIENT(n)) + return 0; + return (int) n; + } + + cmsg_close_all(&msghdr); + + if (msghdr.msg_flags & MSG_TRUNC) { + log_warning("Got overly long notification datagram, ignoring."); + return 0; + } + + ucred = CMSG_FIND_DATA(&msghdr, SOL_SOCKET, SCM_CREDENTIALS, struct ucred); + if (!ucred || ucred->pid <= 0) { + log_warning("Got notification datagram lacking credential information, ignoring."); + return 0; + } + + HASHMAP_FOREACH(j, m->jobs) { + pid_t pid; + assert_se(sd_event_source_get_child_pid(j->child, &pid) >= 0); + + if (ucred->pid == pid) + break; + } + + if (!j) { + log_warning("Got notification datagram from unexpected peer, ignoring."); + return 0; + } + + buf[n] = 0; + + p = find_line_startswith(buf, "X_SYSUPDATE_VERSION="); + if (p) { + p = strdupcspn(p, "\n"); + if (p) + job_on_version(j, p); + } + + p = find_line_startswith(buf, "ERRNO="); + if (p) { + p = strdupcspn(p, "\n"); + if (p) + job_on_errno(j, p); + } + + p = find_line_startswith(buf, "X_SYSUPDATE_PROGRESS="); + if (p) { + p = strdupcspn(p, "\n"); + if (p) + job_on_progress(j, p); + } + + /* Should come last, since this might actually detach the job */ + if (find_line_startswith(buf, "READY=1")) + job_on_ready(j); + + return 0; +} + +static int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + _cleanup_close_ int notify_fd = -EBADF; + static const union sockaddr_union sa = { + .un.sun_family = AF_UNIX, + .un.sun_path = "/run/systemd/sysupdate/notify", + }; + int r; + + assert(ret); + + m = new0(Manager, 1); + if (!m) + return -ENOMEM; + + r = sd_event_default(&m->event); + if (r < 0) + return r; + + (void) sd_event_set_watchdog(m->event, true); + + r = sd_event_set_signal_exit(m->event, true); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18) | SD_EVENT_SIGNAL_PROCMASK, + sigrtmin18_handler, NULL); + if (r < 0) + return r; + + r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL); + if (r < 0) + log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m"); + + r = sd_bus_default_system(&m->bus); + if (r < 0) + return r; + + notify_fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (notify_fd < 0) + return -errno; + + (void) mkdir_parents_label(sa.un.sun_path, 0755); + (void) sockaddr_un_unlink(&sa.un); + + if (bind(notify_fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0) + return -errno; + + r = setsockopt_int(notify_fd, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + return r; + + r = sd_event_add_io(m->event, &m->notify_event, notify_fd, EPOLLIN, manager_on_notify, m); + if (r < 0) + return r; + + (void) sd_event_source_set_description(m->notify_event, "notify-socket"); + + r = sd_event_source_set_io_fd_own(m->notify_event, true); + if (r < 0) + return r; + TAKE_FD(notify_fd); + + *ret = TAKE_PTR(m); + return 0; +} + +static int manager_enumerate_image_class(Manager *m, TargetClass class) { + _cleanup_hashmap_free_ Hashmap *images = NULL; + Image *image; + int r; + + images = hashmap_new(&image_hash_ops); + if (!images) + return -ENOMEM; + + r = image_discover((ImageClass) class, NULL, images); + if (r < 0) + return r; + + HASHMAP_FOREACH(image, images) { + Target *t = NULL; + bool have = false; + + if (IMAGE_IS_HOST(image)) + continue; /* We already enroll the host ourselves */ + + r = target_new(m, class, image->name, image->path, &t); + if (r < 0) + return r; + t->image_type = image->type; + + r = target_list_components(t, NULL, &have); + if (r < 0) + return r; + if (!have) { + log_debug("Skipping %s because it has no default component", image->path); + continue; + } + } + + return 0; +} + +static int manager_enumerate_components(Manager *m) { + _cleanup_strv_free_ char **components = NULL; + bool have_default; + Target *t; + int r; + + r = target_list_components(NULL, &components, &have_default); + if (r < 0) + return r; + + if (have_default) { + r = target_new(m, TARGET_HOST, "host", "sysupdate.d", &t); + if (r < 0) + return r; + } + + STRV_FOREACH(component, components) { + _cleanup_free_ char *path = NULL; + + path = strjoin("sysupdate.", *component, ".d"); + if (!path) + return -ENOMEM; + + r = target_new(m, TARGET_COMPONENT, *component, path, &t); + if (r < 0) + return r; + } + + return 0; +} + +static int manager_enumerate_targets(Manager *m) { + static const TargetClass discoverable_classes[] = { + TARGET_MACHINE, + TARGET_PORTABLE, + TARGET_SYSEXT, + TARGET_CONFEXT, + }; + int r; + + assert(m); + + FOREACH_ARRAY(class, discoverable_classes, ELEMENTSOF(discoverable_classes)) { + r = manager_enumerate_image_class(m, *class); + if (r < 0) + log_warning_errno(r, "Failed to enumerate %ss, ignoring: %m", + target_class_to_string(*class)); + } + + return manager_enumerate_components(m); +} + +static int manager_ensure_targets(Manager *m) { + assert(m); + + if (!hashmap_isempty(m->targets)) + return 0; + + return manager_enumerate_targets(m); +} + +static int method_list_targets(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = ASSERT_PTR(userdata); + Target *t; + int r; + + assert(msg); + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(sso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(t, m->targets) { + _cleanup_free_ char *bus_path = NULL; + + bus_path = target_bus_path(t); + if (!bus_path) + return -ENOMEM; + + r = sd_bus_message_append(reply, "(sso)", + target_class_to_string(t->class), + t->name, + bus_path); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_list_jobs(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = ASSERT_PTR(userdata); + Job *j; + int r; + + assert(msg); + + r = sd_bus_message_new_method_return(msg, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(tsuo)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(j, m->jobs) { + r = sd_bus_message_append(reply, "(tsuo)", + j->id, + job_type_to_string(j->type), + j->progress_percent, + j->object_path); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_list_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) { + _cleanup_strv_free_ char **urls = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = ASSERT_PTR(userdata); + Target *t; + int r; + + assert(msg); + + r = manager_ensure_targets(m); + if (r < 0) + return r; + + HASHMAP_FOREACH(t, m->targets) { + _cleanup_strv_free_ char **target_appstream = NULL; + r = target_get_appstream(t, &target_appstream); + if (r < 0) + return r; + + r = strv_extend_strv(&urls, target_appstream, true); + 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, urls); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static const sd_bus_vtable manager_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_METHOD_WITH_ARGS("ListTargets", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("a(sso)", targets), + method_list_targets, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("ListJobs", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("a(tsuo)", jobs), + method_list_jobs, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_METHOD_WITH_ARGS("ListAppStream", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("as", urls), + method_list_appstream, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_SIGNAL_WITH_ARGS("JobRemoved", + SD_BUS_ARGS("t", id, "o", path, "i", status), + 0), + + SD_BUS_VTABLE_END +}; + +static const BusObjectImplementation manager_object = { + "/org/freedesktop/sysupdate1", + "org.freedesktop.sysupdate1.Manager", + .vtables = BUS_VTABLES(manager_vtable), + .children = BUS_IMPLEMENTATIONS(&job_object, &target_object), +}; + +static int manager_add_bus_objects(Manager *m) { + int r; + + assert(m); + + r = bus_add_implementation(m->bus, &manager_object, m); + if (r < 0) + return r; + + r = bus_log_control_api_register(m->bus); + if (r < 0) + return r; + + r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.sysupdate1", 0, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request name: %m"); + + r = sd_bus_attach_event(m->bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + return 0; +} + +static bool manager_is_idle(void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + return hashmap_isempty(m->jobs); +} + +static void manager_check_idle(Manager *m) { + assert(m); + + if (!hashmap_isempty(m->jobs)) + return; + + hashmap_clear(m->targets); + log_debug("Cleared target cache"); +} + +static int manager_run(Manager *m) { + assert(m); + + return bus_event_loop_with_idle(m->event, + m->bus, + "org.freedesktop.sysupdate1", + DEFAULT_EXIT_USEC, + manager_is_idle, + m); +} + +static int run(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup(); + + r = service_parse_argv("systemd-sysupdated.service", + "System update management service.", + BUS_IMPLEMENTATIONS(&manager_object, + &log_control_object), + argc, argv); + if (r <= 0) + return r; + + umask(0022); + + /* SIGCHLD signal must be blocked for sd_event_add_child to work */ + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Failed to allocate manager object: %m"); + + r = manager_add_bus_objects(m); + if (r < 0) + return log_error_errno(r, "Failed to add bus objects: %m"); + + r = manager_run(m); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/units/meson.build b/units/meson.build index bdc34e6f2c..004c5f92d4 100644 --- a/units/meson.build +++ b/units/meson.build @@ -641,6 +641,11 @@ units = [ 'conditions' : ['ENABLE_SYSUPDATE'], }, { + 'file' : 'systemd-sysupdated.service.in', + 'conditions' : ['ENABLE_SYSUPDATE'], + 'symlinks' : ['dbus-org.freedesktop.sysupdate1.service'], + }, + { 'file' : 'systemd-sysupdate.timer', 'conditions' : ['ENABLE_SYSUPDATE'], }, diff --git a/units/systemd-sysupdated.service.in b/units/systemd-sysupdated.service.in new file mode 100644 index 0000000000..28671fbc54 --- /dev/null +++ b/units/systemd-sysupdated.service.in @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=System Update Service +Documentation=man:systemd-sysupdated.service(8) +Documentation=man:org.freedesktop.sysupdate1(5) + +[Service] +ExecStart={{LIBEXECDIR}}/systemd-sysupdated +BusName=org.freedesktop.sysupdate1 +KillMode=mixed +CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD CAP_SETFCAP CAP_SYS_ADMIN CAP_SETPCAP CAP_DAC_OVERRIDE CAP_LINUX_IMMUTABLE +NoNewPrivileges=yes +MemoryDenyWriteExecute=yes +ProtectHostname=yes +RestrictRealtime=yes +RestrictNamespaces=net +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +SystemCallFilter=@system-service @mount +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +LockPersonality=yes +{{SERVICE_WATCHDOG}} |