Merge pull request #33398 from AdrianVovk/sysupdate-optional

sysupdate: Add support for optional features
This commit is contained in:
Lennart Poettering
2024-10-21 12:36:43 +02:00
committed by GitHub
21 changed files with 1723 additions and 162 deletions
-3
View File
@@ -1429,9 +1429,6 @@ Features:
- "systemd-sysupdate update --all" support, that iterates through all components
defined on the host, plus all images installed into /var/lib/machines/,
/var/lib/portable/ and so on.
- figure out what to do about system extensions (i.e. they need to imply an
update component, since otherwise sysupdate.d/ files would override the
host's update files.)
- Allow invocation with a single transfer definition, i.e. with
--definitions= pointing to a file rather than a dir.
- add ability to disable implicit decompression of downloaded artifacts,
+91 -6
View File
@@ -122,9 +122,18 @@ node /org/freedesktop/sysupdate1/target/host {
out s new_version,
out t job_id,
out o job_path);
Vacuum(out u count);
Vacuum(out u instances,
out u disabled_transfers);
GetAppStream(out as appstream);
GetVersion(out s version);
ListFeatures(in t flags,
out as features);
DescribeFeature(in s feature,
in t flags,
out s json);
SetFeatureEnabled(in s feature,
in i enabled,
in t flags);
properties:
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s Class = '...';
@@ -159,6 +168,12 @@ node /org/freedesktop/sysupdate1/target/host {
<variablelist class="dbus-method" generated="True" extra-ref="GetVersion()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ListFeatures()"/>
<variablelist class="dbus-method" generated="True" extra-ref="DescribeFeature()"/>
<variablelist class="dbus-method" generated="True" extra-ref="SetFeatureEnabled()"/>
<variablelist class="dbus-property" generated="True" extra-ref="Class"/>
<variablelist class="dbus-property" generated="True" extra-ref="Name"/>
@@ -273,6 +288,68 @@ node /org/freedesktop/sysupdate1/target/host {
<varname>IMAGE_VERSION</varname> in <filename>/etc/os-release</filename>. If the target has no current
version, the function will return an empty string.</para>
<para><function>ListFeatures()</function> returns a list of this target's optional features, by ID.
The <varname>flags</varname> argument is added for future extensibility, and must be set to 0.
If the target has no optional features, the method returns an empty array.</para>
<para><function>DescribeFeature()</function> returns all known information about a given optional feature.
The <varname>feature</varname> argument is used to pass the ID of the feature to be described.
The <varname>flags</varname> argument is added for future extensibility, and must be set to 0.
The returned JSON object contains several known keys. More keys may be added in the future.
The currently known keys are as follows:</para>
<variablelist>
<varlistentry>
<term><literal>name</literal></term>
<listitem><para>A string containing the feature's name.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>description</literal></term>
<listitem><para>An optional string that contains a user-presentable description that identifies
this feature</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>enabled</literal></term>
<listitem><para>A boolean indicating whether this feature is enabled.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>documentationUrl</literal></term>
<listitem><para>An optional string that contains a user-presentable HTTP/HTTPS URL to documentation
about this feature.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>appstreamUrl</literal></term>
<listitem><para>An optional string that contains an HTTP/HTTPS URL to an
<ulink url="https://wwww.freedesktop.org/software/appstream/docs/chap-CatalogData.html">appstream
catalog</ulink> XML file containing metadata about this feature.</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>transfers</literal></term>
<listitem><para>An optional array of strings that list which transfer definitions belong to this
feature.</para></listitem>
</varlistentry>
</variablelist>
<para><function>SetFeatureEnabled()</function> writes an appropriate drop-in file to enable or disable
the specified optional feature.
If <varname>enable</varname> is zero, the feature is disabled. When greater than zero, the feature is
enabled. When less than zero, the feature is reset to the distribution's default.
The <varname>flags</varname> argument is added for future extensibility, and must be set to 0.
The feature does not have to exist; this allows for graceful handling of masked features, and for
preemptive decisions to be made about features that are planned to appear in future releases of the OS.
The drop-in will have a filename of <literal>50-systemd-sysupdate-enabled.conf</literal>.
This method only changes configuration files; to actually apply the changes, clients will need to
call <function>Update()</function>.
Depending on the exact needs of the client, it can choose to update the system to the latest available
version, or it can extend the newest existing installation in-place (by passing in the version returned
by <varname>GetVersion()</varname>).
For now, this method only works with the <literal>host</literal> target.</para>
</refsect2>
<refsect2>
@@ -327,8 +404,13 @@ node /org/freedesktop/sysupdate1/target/host {
<interfacename>org.freedesktop.sysupdate1.vacuum</interfacename>. By default, this action requires
administrator authentication.</para>
<para><function>GetAppStream()</function> and <function>GetVersion()</function> are unauthenticated and
may be called by anybody.</para>
<para><function>SetFeatureEnabled()</function> uses the polkit action
<interfacename>org.freedesktop.sysupdate1.manage-features</interfacename>. By default, this action
requires administrator authentication.</para>
<para><function>GetAppStream()</function>, <function>GetVersion()</function>,
<function>ListFeatures()</function>, and <function>DescribeFeature()</function>
are unauthenticated and may be called by anybody.</para>
<para>All methods called on this interface expose additional variables to the polkit rules.
<literal>class</literal> contains the class of the Target being acted upon, and <literal>name</literal>
@@ -409,9 +491,9 @@ node /org/freedesktop/sysupdate1/job/_1 {
<para>The <varname>Id</varname> property exposes the numeric job ID of the job object.</para>
<para>The <varname>Type</varname> property exposes the type of operation (one of: <literal>list</literal>,
<literal>describe</literal>, <literal>check-new</literal>, <literal>update</literal>, or <literal>vacuum</literal>).
</para>
<para>The <varname>Type</varname> property exposes the type of operation (one of:
<literal>list</literal>, <literal>describe</literal>, <literal>check-new</literal>,
<literal>update</literal>, <literal>vacuum</literal>, or <literal>describe-feature</literal>).</para>
<para>The <varname>Offline</varname> property exposes whether the job is permitted to access
the network or not.</para>
@@ -481,6 +563,9 @@ node /org/freedesktop/sysupdate1/job/_1 {
<function>Vacuum()</function>,
<function>GetAppStream()</function>,
<function>GetVersion()</function>,
<function>ListFeatures()</function>,
<function>DescribeFeature()</function>,
<function>SetFeatureEnabled()</function>,
<varname>Class</varname>,
<varname>Name</varname>, and
<varname>Path</varname> were added in version 257.</para>
+1
View File
@@ -1192,6 +1192,7 @@ manpages = [
['systemd.unit', '5', [], ''],
['systemd.v', '7', [], ''],
['sysupdate.d', '5', [], 'ENABLE_SYSUPDATE'],
['sysupdate.features', '5', [], 'ENABLE_SYSUPDATE'],
['sysusers.d', '5', [], 'ENABLE_SYSUSERS'],
['telinit', '8', [], 'HAVE_SYSV_COMPAT'],
['timedatectl', '1', [], 'ENABLE_TIMEDATECTL'],
+10
View File
@@ -106,6 +106,16 @@
<xi:include href="version-info.xml" xpointer="v251"/></listitem>
</varlistentry>
<varlistentry>
<term><option>features</option> <optional><replaceable>FEATURE</replaceable></optional></term>
<listitem><para>If invoked without an argument, enumerates optional features and shows a summarizing
table, including which features are enabled or disabled. If a feature argument is specified, shows
details about the specific feature, including the transfers that are controlled by the feature.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>check-new</option></term>
+63 -20
View File
@@ -23,21 +23,23 @@
<refsynopsisdiv>
<para><simplelist>
<member><filename>/etc/sysupdate.d/*.conf</filename></member>
<member><filename>/run/sysupdate.d/*.conf</filename></member>
<member><filename>/usr/local/lib/sysupdate.d/*.conf</filename></member>
<member><filename>/usr/lib/sysupdate.d/*.conf</filename></member>
<member><filename>/etc/sysupdate.d/*.transfer</filename></member>
<member><filename>/run/sysupdate.d/*.transfer</filename></member>
<member><filename>/usr/local/lib/sysupdate.d/*.transfer</filename></member>
<member><filename>/usr/lib/sysupdate.d/*.transfer</filename></member>
</simplelist></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><filename>sysupdate.d/*.conf</filename> files describe how specific resources on the local system
shall be updated from a remote source. Each such file defines one such transfer: typically a remote
HTTP/HTTPS resource as source; and a local file, directory or partition as target. This may be used as a
simple, automatic, atomic update mechanism for the OS itself, for containers, portable services or system
extension images — but in fact may be used to update any kind of file from a remote source.</para>
<para>These files describe how specific resources on the local system shall be updated from a remote
source.
Each such file defines one such transfer: typically a remote HTTP/HTTPS resource as source; and a local
file, directory or partition as target.
This may be used as a simple, automatic, atomic update mechanism for the OS itself, for containers,
portable services or system extension images — but in fact may be used to update any kind of file from a
remote source.</para>
<para>The
<citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>
@@ -48,14 +50,22 @@
versions, in order to implement flexible update schemes, e.g. A/B updating (or a superset thereof,
e.g. A/B/C, A/B/C/D, …).</para>
<para>Each <filename>*.conf</filename> file defines one transfer, i.e. describes one resource to
update. Typically, multiple of these files (i.e. multiple of such transfers) are defined together, and
<para>Each <filename>*.transfer</filename> file defines one transfer, i.e. describes one resource to
update.
Typically, multiple of these files (i.e. multiple of such transfers) are defined together, and
are bound together by a common version identifier in order to update multiple resources at once on each
update operation, for example to update a kernel, a root file system and a Verity partition in a single,
combined, synchronized operation, so that only a combined update of all three together constitutes a
complete update.</para>
complete update.
We'll call such a collection of transfers a target.
<command>systemd-sysupdate</command> always operates on a single target.</para>
<para>Each <filename>*.conf</filename> file contains three sections: [Transfer], [Source] and [Target].</para>
<para>Transfers may be grouped together into sets that can be individually enabled or disabled by the
system administrator, called "Optional Features":
<citerefentry><refentrytitle>sysupdate.features</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
</para>
<para>Each <filename>*.transfer</filename> file contains three sections: [Transfer], [Source] and [Target].</para>
</refsect1>
<refsect1>
@@ -106,8 +116,8 @@
<para>An update can only complete if the relevant URLs provide their resources for the same version,
i.e. for the same value of <literal>@v</literal>.</para>
<para>The above may be translated into three <filename>*.conf</filename> files in
<filename>sysupdate.d/</filename>, one for each resource to transfer. The <filename>*.conf</filename>
<para>The above may be translated into three <filename>*.transfer</filename> files in
<filename>sysupdate.d/</filename>, one for each resource to transfer. The <filename>*.transfer</filename>
files configure the type of download, and what place to write the download to (i.e. whether to a
partition or a file in the file system). Most importantly these files contain the URL, partition name and
filename patterns shown above that describe how these resources are called on the source and how they
@@ -130,11 +140,11 @@
lists file names and their SHA256 hashes.</para></listitem>
</itemizedlist>
<para>Transfers are done in the alphabetical order of the <filename>.conf</filename> file names they are
<para>Transfers are done in the alphabetical order of the <filename>.transfer</filename> file names they are
defined in. First, the resource data is downloaded directly into a target file/directory/partition. Once
this is completed for all defined transfers, in a second step the files/directories/partitions are
renamed to their final names as defined by the target <varname>MatchPattern=</varname>, again in the
order the <filename>.conf</filename> transfer file names dictate. This step is not atomic, however it is
order the <filename>.transfer</filename> transfer file names dictate. This step is not atomic, however it is
guaranteed to be executed strictly in order with suitable disk synchronization in place. Typically, when
updating an OS one of the transfers defines the entry point when booting. Thus it is generally a good idea
to order the resources via the transfer configuration file names so that the entry point is written
@@ -519,6 +529,39 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Features=</varname></term>
<listitem><para>A space-separated list of
<citerefentry><refentrytitle>sysupdate.features</refentrytitle><manvolnum>5</manvolnum></citerefentry>
that this transfer belongs to, by name.
This option may be specified more than once, in which case the specified list of features is merged.
If the empty string is assigned to this option, the list is reset and all prior assignments will have
no effect.
For example: <literal>Features=foo bar</literal> specifies that the transfer belongs to
<literal>foo.feature</literal> and <literal>bar.feature</literal>.</para>
<para>If the list of features is empty, then this transfer is always used.
If this transfer belongs to more than one feature, then it will be used if <emphasis>any</emphasis>
one of the listed features is enabled.
A name that does not correspond to a defined feature will resolve to an implicit feature that is
always disabled.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>RequisiteFeatures=</varname></term>
<listitem><para>This is like <varname>Features=</varname>, except that <emphasis>all</emphasis>
features listed here must be enabled for this transfer to be enabled.
If both options are specified, then they both apply: the transfer will be enabled only if all
features specified here are enabled, and at least one feature listed in <varname>Features=</varname>
is enabled.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
@@ -869,7 +912,7 @@
partition, and a unified kernel image to update as one. This example is an extension of the example
discussed earlier in this man page.</para>
<para><programlisting># /usr/lib/sysupdate.d/50-verity.conf
<para><programlisting># /usr/lib/sysupdate.d/50-verity.transfer
[Transfer]
ProtectVersion=%A
@@ -899,7 +942,7 @@ ReadOnly=1</programlisting></para>
<citerefentry><refentrytitle>systemd-gpt-auto-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry>
suggests).</para>
<para><programlisting># /usr/lib/sysupdate.d/60-root.conf
<para><programlisting># /usr/lib/sysupdate.d/60-root.transfer
[Transfer]
ProtectVersion=%A
@@ -918,7 +961,7 @@ ReadOnly=1</programlisting></para>
<para>The above defines a matching transfer definition for the root file system.</para>
<para><programlisting># /usr/lib/sysupdate.d/70-kernel.conf
<para><programlisting># /usr/lib/sysupdate.d/70-kernel.transfer
[Transfer]
ProtectVersion=%A
+338
View File
@@ -0,0 +1,338 @@
<?xml version='1.0'?>
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="sysupdate.features" conditional='ENABLE_SYSUPDATE'
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>sysupdate.features</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>sysupdate.features</refentrytitle>
<manvolnum>5</manvolnum>
</refmeta>
<refnamediv>
<refname>sysupdate.features</refname>
<refpurpose>Definition Files for Optional Features</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><simplelist>
<member><filename>/etc/sysupdate.d/*.feature</filename></member>
<member><filename>/run/sysupdate.d/*.feature</filename></member>
<member><filename>/usr/local/lib/sysupdate.d/*.feature</filename></member>
<member><filename>/usr/lib/sysupdate.d/*.feature</filename></member>
</simplelist></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para>"Optional Features" are functionality provided by
<citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
that allow a distribution to define sets of
<citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>
transfer definitions that are intended to be enabled or disabled by the system administrator.</para>
<para>When a feature is enabled, transfers belonging to it will be considered when checking for and
downloading updates, when vacuuming, and in all other situations.
In effect, transfers belonging to a feature will always be updated in lock-step with the rest of their
target.
This is the primary difference an Optional Feature and a <command>systemd-sysupdate</command> component.
When a feature is disabled, its transfers will not be considered when checking for and downloading updates,
but <command>systemd-sysupdate</command> will still consider them while vacuuming and in other situations
where it needs to determine ownership over previously downloaded system resources.
<command>systemd-sysupdate</command> will clean up all instances of the feature's transfers whenever it
is disabled, effectively uninstalling it.</para>
<para>Optional Features are described by <filename>sysupdate.d/*.feature</filename> files, which are
defined below.
Transfers can declare that they belong to a feature via the <varname>Features=</varname> setting.
Feature definitions support drop-in files, which are most commonly used to override the
<varname>Enabled=</varname> setting).
They can also be masked out to hide the availability of the feature entirely.</para>
<para>Each <filename>*.feature</filename> file contains one section: [Feature].</para>
</refsect1>
<refsect1>
<title>[Feature] Section Options</title>
<para>This section defines general properties of this feature.</para>
<variablelist>
<varlistentry>
<term><varname>Description=</varname></term>
<listitem><para>A short human readable description of this feature.
This may be used as a label for this feature, so the string should meaningfully identify the feature
among the features available in <filename>sysupdate.d/</filename>.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Documentation=</varname></term>
<listitem><para>A user-presentable URL to documentation about this feature.
This setting supports specifier expansion; see below for details on supported specifiers.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>AppStream=</varname></term>
<listitem><para>A URL to an
<ulink url="https://www.freedesktop.org/software/appstream/docs/chap-CatalogData.html">AppStream catalog</ulink>
XML file.
This may be used by software centers (such as GNOME Software or KDE Discover) to present rich
metadata about this feature.
This includes display names, chagnelogs, icons, and more.
This setting supports specifier expansion; see below for details on supported specifiers.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Enabled=</varname></term>
<listitem><para>Whether or not this feature is enabled. If unspecified, the feature is disabled
by default.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Specifiers</title>
<para>Specifiers may be used in the <varname>Documentation=</varname> and <varname>AppStream=</varname>
settings. The following expansions are understood:</para>
<table class='specifiers'>
<title>Specifiers available</title>
<tgroup cols='3' align='left' colsep='1' rowsep='1'>
<colspec colname="spec" />
<colspec colname="mean" />
<colspec colname="detail" />
<thead>
<row>
<entry>Specifier</entry>
<entry>Meaning</entry>
<entry>Details</entry>
</row>
</thead>
<tbody>
<xi:include href="standard-specifiers.xml" xpointer="a"/>
<xi:include href="standard-specifiers.xml" xpointer="A"/>
<xi:include href="standard-specifiers.xml" xpointer="b"/>
<xi:include href="standard-specifiers.xml" xpointer="B"/>
<xi:include href="standard-specifiers.xml" xpointer="H"/>
<xi:include href="standard-specifiers.xml" xpointer="l"/>
<xi:include href="standard-specifiers.xml" xpointer="m"/>
<xi:include href="standard-specifiers.xml" xpointer="M"/>
<xi:include href="standard-specifiers.xml" xpointer="o"/>
<xi:include href="standard-specifiers.xml" xpointer="v"/>
<xi:include href="standard-specifiers.xml" xpointer="w"/>
<xi:include href="standard-specifiers.xml" xpointer="W"/>
<xi:include href="standard-specifiers.xml" xpointer="T"/>
<xi:include href="standard-specifiers.xml" xpointer="V"/>
<xi:include href="standard-specifiers.xml" xpointer="percent"/>
</tbody>
</tgroup>
</table>
</refsect1>
<refsect1>
<title>Examples</title>
<example>
<title>Development Tools for Image-Based OS</title>
<para>We'll use the hypothetical "foobarOS" described in
<citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry> as our
example base OS.
The vast majority of foobarOS's users have no need for a compiler, build system, debugger, and other
such development tools to be part of their OS.
However, the developers of foobarOS itself need this build tooling to be available.
So, foobarOS needs to provide a system extension image (see
<citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry>)
containing these development tools, and this image must be updated in lock-step with the underlying
base OS.
This is a great use case for an optional OS feature, so let's define one:
</para>
<para><programlisting># /usr/lib/sysupdate.d/devel.feature
[Feature]
Description=Development Tools
Documentation=https://developer.example.com/foobarOS/getting-started
Enabled=false
</programlisting></para>
<para>The above defines the <literal>devel</literal> feature, and disables it by default.
Now let's a define a transfer that's associated with this feature:</para>
<para><programlisting># /usr/lib/sysupdate.d/50-devel.transfer
[Transfer]
Features=devel
ProtectVersion=%A
[Source]
Type=url-file
Path=https://download.example.com/
MatchPattern=foobarOS_@v_devel.raw.xz
[Target]
Type=regular-file
Path=/var/lib/extensions
MatchPattern=foobarOS_@v_devel.raw
Mode=0444
InstancesMax=2
</programlisting></para>
<para>With these two files, we have created a feature called <literal>devel</literal> that, when
enabled, will download and decompress the appropriate version of
<literal>https://download.example.com/foobarOS_@v_devel.raw.xz</literal> into
<literal>/var/lib/extensions/foobarOS_@v_devel.raw</literal> during each OS update.</para>
<para>The developers of foobarOS can enable the <literal>devel</literal> feature on their workstations
by creating the following drop-in:</para>
<para><programlisting># /etc/sysupdate.d/devel.feature.d/enable.conf
[Feature]
Enabled=true
</programlisting></para>
</example>
<example>
<title>Proprietary Drivers</title>
<para>Suppose that many of foobarOS's users have a GPU manufactured by the MVISUAL corporation.
Due to lack of documentation and difficulty in reverse-engineering the hardware, the open-source
drivers for MVISUAL GPUs are unable to make proper use of available graphics and compute performance.
MVISUAL provides a redistributable proprietary driver for their cards, and foobarOS's developers
distribute them to address their users' needs.</para>
<para>MVISUAL's driver has a couple different parts that must be installed for it to function: a UKI
addon to configure the kernel command-line, an initrd system extension image to add the MVISUAL kernel
module into the initrd, and a regular system extension image to add the proprietary OpenGL and Vulkan
userspace drivers.
All of these should be version-locked to the core OS.</para>
<para>Let's start by defining an optional feature named <literal>mvisual-driver</literal>:</para>
<para><programlisting># /usr/lib/sysupdate.d/mvisual-driver.feature
[Feature]
Description=MVISUAL Proprietary GPU Driver
Documentation=https://support.example.com/foobarOS/mvisual
AppStream=https://metadata.example.com/mvisual-driver-%A.xml.gz
</programlisting></para>
<para>Note that we define AppStream metadata for this feature, because we want software centers to
present it to end-users.
Next, let's define the corresponding transfers:</para>
<para><programlisting># /usr/lib/sysupdate.d/50-mvisual-userspace.transfer
[Transfer]
Features=mvisual-driver
ProtectVersion=%A
[Source]
Type=url-file
Path=https://download.example.com/
MatchPattern=foobarOS_@v_mvisual_userspace.raw.xz
[Target]
Type=regular-file
Path=/var/lib/extensions
MatchPattern=foobarOS_@v_mvisual.raw
Mode=0444
InstancesMax=2
</programlisting></para>
<para><programlisting># /usr/lib/sysupdate.d/70-mvisual-initrd.transfer
[Transfer]
Features=mvisual-driver
ProtectVersion=%A
[Source]
Type=url-file
Path=https://download.example.com/
MatchPattern=foobarOS_@v_mvisual_initrd.raw.xz
[Target]
Type=regular-file
Path=/EFI/Linux
PathRelativeTo=boot
MatchPattern=foobarOS_@v.efi.extra.d/foobarOS_mvisual.raw
Mode=0444
InstancesMax=2
</programlisting></para>
<para><programlisting># /usr/lib/sysupdate.d/90-mvisual-addon.transfer
[Transfer]
Features=mvisual-driver
ProtectVersion=%A
[Source]
Type=url-file
Path=https://download.example.com/
MatchPattern=foobarOS_@v_mvisual_addon.efi.xz
[Target]
Type=regular-file
Path=/EFI/Linux
PathRelativeTo=boot
MatchPattern=foobarOS_@v.efi.extra.d/foobarOS_mvisual.addon.efi
Mode=0444
InstancesMax=2
</programlisting></para>
</example>
<example>
<title>Intersecting Features</title>
<para>Suppose that MVISUAL releases special tooling to help a distribution developer troubleshoot
crashes in their proprietary driver.
Let's define a transfer:</para>
<para><programlisting># /usr/lib/sysupdate.d/50-mvisual-debugger.transfer
[Transfer]
RequisiteFeatures=devel mvisual-driver
ProtectVersion=%A
[Source]
Type=url-file
Path=https://download.example.com/
MatchPattern=foobarOS_@v_devel.raw.xz
[Target]
Type=regular-file
Path=/var/lib/extensions
MatchPattern=foobarOS_@v_devel.raw
Mode=0444
InstancesMax=2
</programlisting></para>
<para>This transfer will be used only if both the <literal>devel</literal> and
<literal>mvisual-driver</literal> features are enabled.</para>
</example>
</refsect1>
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+42
View File
@@ -90,6 +90,35 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><command>features</command> [<replaceable>FEATURE</replaceable>]</term>
<listitem><para>When no <replaceable>FEATURE</replaceable> is specified, this command lists all
optional features.
When a <replaceable>FEATURE</replaceable> is specified, this command lists all known information
about that feature.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><command>enable</command> <replaceable>FEATURE</replaceable>…</term>
<term><command>disable</command> <replaceable>FEATURE</replaceable>…</term>
<listitem><para>These commands enable or disable optional features.
See <citerefentry><refentrytitle>sysupdate.features</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
These commands always operate on the host system.</para>
<para>By default, these commands will only change the system's configuration by creating or deleting
drop-in files; they will not immediately download the enabled features, or clean up after the
disabled ones.
Enabled features will be downloaded and installed the next time the target is updated, and disabled
transfers will be cleaned up the next time the target is updated or vacuumed.
Pass <option>--now</option> to immediately apply these changes.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
@@ -107,6 +136,9 @@
<listitem><para>When used with the <command>update</command> command, reboots the system
after updates finish applying. If any update fails, the system will not reboot.</para>
<para>When used with the <command>enable</command> or <command>disable</command> commands and the
<option>--now</option> flag, reboots the system after download or clean-up finish applying.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
@@ -121,6 +153,16 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--now</option></term>
<listitem><para>When used with the <command>enable</command> command, downloads and installs the
enabled features. When used with the <command>disable</command> command, deletes all resources
downloaded by the disabled features.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<xi:include href="user-system-options.xml" xpointer="host" />
<xi:include href="standard-options.xml" xpointer="no-pager" />
+3 -3
View File
@@ -416,7 +416,7 @@ int touch_file(const char *path, bool parents, usec_t stamp, uid_t uid, gid_t gi
return RET_GATHER(ret, r);
}
int symlink_idempotent(const char *from, const char *to, bool make_relative) {
int symlinkat_idempotent(const char *from, int atfd, const char *to, bool make_relative) {
_cleanup_free_ char *relpath = NULL;
int r;
@@ -431,13 +431,13 @@ int symlink_idempotent(const char *from, const char *to, bool make_relative) {
from = relpath;
}
if (symlink(from, to) < 0) {
if (symlinkat(from, atfd, to) < 0) {
_cleanup_free_ char *p = NULL;
if (errno != EEXIST)
return -errno;
r = readlink_malloc(to, &p);
r = readlinkat_malloc(atfd, to, &p);
if (r == -EINVAL) /* Not a symlink? In that case return the original error we encountered: -EEXIST */
return -EEXIST;
if (r < 0) /* Any other error? In that case propagate it as is */
+4 -1
View File
@@ -60,7 +60,10 @@ static inline int touch(const char *path) {
return touch_file(path, false, USEC_INFINITY, UID_INVALID, GID_INVALID, MODE_INVALID);
}
int symlink_idempotent(const char *from, const char *to, bool make_relative);
int symlinkat_idempotent(const char *from, int atfd, const char *to, bool make_relative);
static inline int symlink_idempotent(const char *from, const char *to, bool make_relative) {
return symlinkat_idempotent(from, AT_FDCWD, to, make_relative);
}
int symlinkat_atomic_full(const char *from, int atfd, const char *to, bool make_relative);
static inline int symlink_atomic(const char *from, const char *to) {
+1
View File
@@ -2,6 +2,7 @@
systemd_sysupdate_sources = files(
'sysupdate-cache.c',
'sysupdate-feature.c',
'sysupdate-instance.c',
'sysupdate-partition.c',
'sysupdate-pattern.c',
@@ -78,6 +78,18 @@
send_interface="org.freedesktop.sysupdate1.Target"
send_member="GetVersion"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Target"
send_member="ListFeatures"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Target"
send_member="DescribeFeature"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Target"
send_member="SetFeatureEnabled"/>
<allow send_destination="org.freedesktop.sysupdate1"
send_interface="org.freedesktop.sysupdate1.Job"
send_member="Cancel"/>
@@ -71,4 +71,14 @@
</defaults>
</action>
<action id="org.freedesktop.sysupdate1.manage-features">
<description gettext-domain="systemd">Manage optional features</description>
<message gettext-domain="systemd">Authentication is required to manage optional features</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
+121
View File
@@ -0,0 +1,121 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "conf-parser.h"
#include "hash-funcs.h"
#include "path-util.h"
#include "sysupdate-feature.h"
#include "sysupdate.h"
#include "web-util.h"
static Feature *feature_free(Feature *f) {
if (!f)
return NULL;
free(f->id);
free(f->description);
free(f->documentation);
free(f->appstream);
return mfree(f);
}
Feature *feature_new(void) {
Feature *f;
f = new(Feature, 1);
if (!f)
return NULL;
*f = (Feature) {
.n_ref = 1,
};
return f;
}
DEFINE_TRIVIAL_REF_UNREF_FUNC(Feature, feature, feature_free);
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(feature_hash_ops,
char, string_hash_func, string_compare_func,
Feature, feature_unref);
static int config_parse_url_specifiers(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
char **s = ASSERT_PTR(data);
_cleanup_free_ char *resolved = NULL;
int r;
assert(rvalue);
if (isempty(rvalue)) {
*s = mfree(*s);
return 0;
}
r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
return 0;
}
if (!http_url_is_valid(resolved)) {
log_syntax(unit, LOG_WARNING, filename, line, 0,
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
return 0;
}
return free_and_replace(*s, resolved);
}
int feature_read_definition(Feature *f, const char *path, const char *const *dirs) {
assert(f);
ConfigTableItem table[] = {
{ "Feature", "Description", config_parse_string, 0, &f->description },
{ "Feature", "Documentation", config_parse_url_specifiers, 0, &f->documentation },
{ "Feature", "AppStream", config_parse_url_specifiers, 0, &f->appstream },
{ "Feature", "Enabled", config_parse_bool, 0, &f->enabled },
{}
};
_cleanup_free_ char *filename = NULL;
int r;
assert(path);
assert(dirs);
r = path_extract_filename(path, &filename);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", path);
r = config_parse_many(
STRV_MAKE_CONST(path),
dirs,
strjoina(filename, ".d"),
arg_root,
"Feature\0",
config_item_table_lookup, table,
CONFIG_PARSE_WARN,
/* userdata= */ NULL,
/* stats_by_path= */ NULL,
/* drop_in_files= */ NULL);
if (r < 0)
return r;
*ASSERT_PTR(endswith(filename, ".feature")) = 0; /* Remove the file extension */
f->id = TAKE_PTR(filename);
return 0;
}
+27
View File
@@ -0,0 +1,27 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "hash-funcs.h"
#include "macro.h"
#include "sysupdate-transfer.h"
typedef struct Feature {
unsigned n_ref;
char *id;
char *description;
char *documentation;
char *appstream;
bool enabled;
} Feature;
Feature *feature_new(void);
Feature *feature_ref(Feature *f);
Feature *feature_unref(Feature *f);
DEFINE_TRIVIAL_CLEANUP_FUNC(Feature*, feature_unref);
extern const struct hash_ops feature_hash_ops;
int feature_read_definition(Feature *f, const char *path, const char *const *conf_file_dirs);
+71 -20
View File
@@ -28,6 +28,7 @@
#include "stdio-util.h"
#include "strv.h"
#include "sync-util.h"
#include "sysupdate-feature.h"
#include "sysupdate-pattern.h"
#include "sysupdate-resource.h"
#include "sysupdate-transfer.h"
@@ -44,12 +45,16 @@ Transfer* transfer_free(Transfer *t) {
t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
free(t->definition_path);
free(t->id);
free(t->min_version);
strv_free(t->protected_versions);
free(t->current_symlink);
free(t->final_path);
strv_free(t->features);
strv_free(t->requisite_features);
strv_free(t->changelog);
strv_free(t->appstream);
@@ -93,12 +98,6 @@ Transfer* transfer_new(Context *ctx) {
return t;
}
static const Specifier specifier_table[] = {
COMMON_SYSTEM_SPECIFIERS,
COMMON_TMP_SPECIFIERS,
{}
};
static int config_parse_protect_version(
const char *unit,
const char *filename,
@@ -466,11 +465,33 @@ static int config_parse_partition_flags(
return 0;
}
int transfer_read_definition(Transfer *t, const char *path) {
int r;
static bool transfer_decide_if_enabled(Transfer *t, Hashmap *known_features) {
assert(t);
/* Requisite feature disabled -> transfer disabled */
STRV_FOREACH(id, t->requisite_features) {
Feature *f = hashmap_get(known_features, *id);
if (!f || !f->enabled) /* missing features are implicitly disabled */
return false;
}
/* No features defined -> transfer implicitly enabled */
if (strv_isempty(t->features))
return true;
/* At least one feature enabled -> transfer enabled */
STRV_FOREACH(id, t->features) {
Feature *f = hashmap_get(known_features, *id);
if (f && f->enabled)
return true;
}
/* All listed features disabled -> transfer disabled */
return false;
}
int transfer_read_definition(Transfer *t, const char *path, const char **dirs, Hashmap *known_features) {
assert(t);
assert(path);
ConfigTableItem table[] = {
{ "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version },
@@ -478,6 +499,8 @@ int transfer_read_definition(Transfer *t, const char *path) {
{ "Transfer", "Verify", config_parse_bool, 0, &t->verify },
{ "Transfer", "ChangeLog", config_parse_url_specifiers, 0, &t->changelog },
{ "Transfer", "AppStream", config_parse_url_specifiers, 0, &t->appstream },
{ "Transfer", "Features", config_parse_strv, 0, &t->features },
{ "Transfer", "RequisiteFeatures", config_parse_strv, 0, &t->requisite_features },
{ "Source", "Type", config_parse_resource_type, 0, &t->source.type },
{ "Source", "Path", config_parse_resource_path, 0, &t->source },
{ "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to },
@@ -501,17 +524,39 @@ int transfer_read_definition(Transfer *t, const char *path) {
{}
};
r = config_parse(NULL, path, NULL,
"Transfer\0"
"Source\0"
"Target\0",
config_item_table_lookup, table,
CONFIG_PARSE_WARN,
t,
NULL);
_cleanup_free_ char *filename = NULL;
char *e;
int r;
assert(path);
assert(dirs);
r = path_extract_filename(path, &filename);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", path);
r = config_parse_many(
STRV_MAKE_CONST(path),
dirs,
strjoina(filename, ".d"),
arg_root,
"Transfer\0"
"Source\0"
"Target\0",
config_item_table_lookup, table,
CONFIG_PARSE_WARN,
/* userdata= */ NULL,
/* stats_by_path= */ NULL,
/* drop_in_files= */ NULL);
if (r < 0)
return r;
e = ASSERT_PTR(endswith(filename, ".transfer") ?: endswith(filename, ".conf"));
*e = 0; /* Remove the file extension */
t->id = TAKE_PTR(filename);
t->enabled = transfer_decide_if_enabled(t, known_features);
if (!RESOURCE_IS_SOURCE(t->source.type))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Source Type= must be one of url-file, url-tar, tar, regular-file, directory, subvolume.");
@@ -704,6 +749,8 @@ int transfer_vacuum(
assert(instances_max >= 1);
if (instances_max == UINT64_MAX) /* Keep infinite instances? */
limit = UINT64_MAX;
else if (space == UINT64_MAX) /* forcibly delete all instances? */
limit = 0;
else if (space > instances_max)
return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
"Asked to delete more instances than total maximum allowed number of instances, refusing.");
@@ -713,7 +760,7 @@ int transfer_vacuum(
else
limit = instances_max - space;
if (t->target.type == RESOURCE_PARTITION) {
if (t->target.type == RESOURCE_PARTITION && space != UINT64_MAX) {
uint64_t rm, remain;
/* If we are looking at a partition table, we also have to take into account how many
@@ -767,7 +814,11 @@ int transfer_vacuum(
assert(oldest->resource);
log_info("%s Removing old '%s' (%s).", special_glyph(SPECIAL_GLYPH_RECYCLING), oldest->path, resource_type_to_string(oldest->resource->type));
log_info("%s Removing %s '%s' (%s).",
special_glyph(SPECIAL_GLYPH_RECYCLING),
space == UINT64_MAX ? "disabled" : "old",
oldest->path,
resource_type_to_string(oldest->resource->type));
switch (t->target.type) {
+7 -3
View File
@@ -15,12 +15,17 @@ typedef struct Transfer Transfer;
#include "sysupdate.h"
struct Transfer {
char *definition_path;
char *id;
char *min_version;
char **protected_versions;
char *current_symlink;
bool verify;
char **features;
char **requisite_features;
bool enabled;
Resource source, target;
uint64_t instances_max;
@@ -55,11 +60,10 @@ struct Transfer {
typedef int (*TransferProgress)(const Transfer *t, const Instance *inst, unsigned percentage);
Transfer* transfer_new(Context *ctx);
Transfer* transfer_free(Transfer *t);
DEFINE_TRIVIAL_CLEANUP_FUNC(Transfer*, transfer_free);
int transfer_read_definition(Transfer *t, const char *path);
int transfer_read_definition(Transfer *t, const char *path, const char **dirs, Hashmap *features);
int transfer_resolve_paths(Transfer *t, const char *root, const char *node);
+296 -54
View File
@@ -25,9 +25,11 @@
#include "set.h"
#include "signal-util.h"
#include "sort-util.h"
#include "specifier.h"
#include "string-util.h"
#include "strv.h"
#include "sysupdate.h"
#include "sysupdate-feature.h"
#include "sysupdate-transfer.h"
#include "sysupdate-update-set.h"
#include "sysupdate-util.h"
@@ -57,10 +59,21 @@ STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
const Specifier specifier_table[] = {
COMMON_SYSTEM_SPECIFIERS,
COMMON_TMP_SPECIFIERS,
{}
};
typedef struct Context {
Transfer **transfers;
size_t n_transfers;
Transfer **disabled_transfers;
size_t n_disabled_transfers;
Hashmap *features; /* Defined features, keyed by ID */
UpdateSet **update_sets;
size_t n_update_sets;
@@ -77,6 +90,12 @@ static Context* context_free(Context *c) {
transfer_free(*tr);
free(c->transfers);
FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers)
transfer_free(*tr);
free(c->disabled_transfers);
hashmap_free(c->features);
FOREACH_ARRAY(us, c->update_sets, c->n_update_sets)
update_set_free(*us);
free(c->update_sets);
@@ -93,83 +112,145 @@ static Context* context_new(void) {
return new0(Context, 1);
}
static int context_read_definitions(
static void free_transfers(Transfer **array, size_t n) {
FOREACH_ARRAY(t, array, n)
transfer_free(*t);
free(array);
}
static int read_definitions(
Context *c,
const char *directory,
const char *component,
const char *root,
const char **dirs,
const char *suffix,
const char *node) {
_cleanup_strv_free_ char **files = NULL;
Transfer **transfers = NULL, **disabled = NULL;
size_t n_transfers = 0, n_disabled = 0;
int r;
CLEANUP_ARRAY(transfers, n_transfers, free_transfers);
CLEANUP_ARRAY(disabled, n_disabled, free_transfers);
assert(c);
assert(dirs);
assert(suffix);
if (directory)
r = conf_files_list_strv(&files, ".conf", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) STRV_MAKE(directory));
else if (component) {
_cleanup_strv_free_ char **n = NULL;
char **l = CONF_PATHS_STRV("");
size_t k = 0;
n = new0(char*, strv_length(l) + 1);
if (!n)
return log_oom();
STRV_FOREACH(i, l) {
char *j;
j = strjoin(*i, "sysupdate.", component, ".d");
if (!j)
return log_oom();
n[k++] = j;
}
r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) n);
} else
r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) CONF_PATHS_STRV("sysupdate.d"));
r = conf_files_list_strv(&files, suffix, arg_root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, dirs);
if (r < 0)
return log_error_errno(r, "Failed to enumerate *.conf files: %m");
return log_error_errno(r, "Failed to enumerate sysupdate.d/*%s definitions: %m", suffix);
STRV_FOREACH(f, files) {
STRV_FOREACH(p, files) {
_cleanup_(transfer_freep) Transfer *t = NULL;
if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1))
return log_oom();
Transfer **appended;
t = transfer_new(c);
if (!t)
return log_oom();
t->definition_path = strdup(*f);
if (!t->definition_path)
return log_oom();
r = transfer_read_definition(t, *f);
r = transfer_read_definition(t, *p, dirs, c->features);
if (r < 0)
return r;
c->transfers[c->n_transfers++] = TAKE_PTR(t);
r = transfer_resolve_paths(t, arg_root, node);
if (r < 0)
return r;
if (t->enabled)
appended = GREEDY_REALLOC_APPEND(transfers, n_transfers, &t, 1);
else
appended = GREEDY_REALLOC_APPEND(disabled, n_disabled, &t, 1);
if (!appended)
return log_oom();
TAKE_PTR(t);
}
c->transfers = TAKE_PTR(transfers);
c->n_transfers = n_transfers;
c->disabled_transfers = TAKE_PTR(disabled);
c->n_disabled_transfers = n_disabled;
return 0;
}
static int context_read_definitions(Context *c, const char* node) {
_cleanup_strv_free_ char **dirs = NULL, **files = NULL;
int r;
assert(c);
if (arg_definitions)
dirs = strv_new(arg_definitions);
else if (arg_component) {
char **l = CONF_PATHS_STRV("");
size_t i = 0;
dirs = new0(char*, strv_length(l) + 1);
if (!dirs)
return log_oom();
STRV_FOREACH(dir, l) {
char *j;
j = strjoin(*dir, "sysupdate.", arg_component, ".d");
if (!j)
return log_oom();
dirs[i++] = j;
}
} else
dirs = strv_new(CONF_PATHS("sysupdate.d"));
if (!dirs)
return log_oom();
r = conf_files_list_strv(&files,
".feature",
arg_root,
CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED,
(const char**) dirs);
if (r < 0)
return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
STRV_FOREACH(p, files) {
_cleanup_(feature_unrefp) Feature *f = NULL;
f = feature_new();
if (!f)
return log_oom();
r = feature_read_definition(f, *p, (const char**) dirs);
if (r < 0)
return r;
r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
if (r < 0)
return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
TAKE_PTR(f);
}
r = read_definitions(c, (const char**) dirs, ".transfer", node);
if (r < 0)
return r;
if (c->n_transfers + c->n_disabled_transfers == 0) {
/* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */
r = read_definitions(c, (const char**) dirs, ".conf", node);
if (r < 0)
return r;
if (c->n_transfers + c->n_disabled_transfers > 0)
log_warning("As of v257, transfer definitions should have the '.transfer' extension.");
}
if (c->n_transfers == 0) {
if (arg_component)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
"No transfer definitions for component '%s' found.", arg_component);
"No transfer definitions for component '%s' found.",
arg_component);
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
"No transfer definitions found.");
}
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Transfer *t = *tr;
r = transfer_resolve_paths(t, root, node);
if (r < 0)
return r;
}
return 0;
}
@@ -191,6 +272,17 @@ static int context_load_installed_instances(Context *c) {
return r;
}
FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
Transfer *t = *tr;
r = resource_load_instances(
&t->target,
arg_verify >= 0 ? arg_verify : t->verify,
&c->web_cache);
if (r < 0)
return r;
}
return 0;
}
@@ -203,7 +295,6 @@ static int context_load_available_instances(Context *c) {
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
Transfer *t = *tr;
assert(t);
r = resource_load_instances(
&t->source,
@@ -750,6 +841,7 @@ static int context_vacuum(
uint64_t space,
const char *extra_protected_version) {
size_t disabled_count = 0;
int r, count = 0;
assert(c);
@@ -773,15 +865,29 @@ static int context_vacuum(
count = MAX(count, r);
}
FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
r = transfer_vacuum(*tr, UINT64_MAX /* wipe all instances */, NULL);
if (r < 0)
return r;
if (r > 0)
disabled_count++;
}
if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (count > 0)
if (count > 0 && disabled_count > 0)
log_info("Removed %i instances, and %zu disabled transfers.", count, disabled_count);
else if (count > 0)
log_info("Removed %i instances.", count);
else if (disabled_count > 0)
log_info("Removed %zu disabled transfers.", disabled_count);
else
log_info("Removed no instances.");
log_info("Found nothing to remove.");
} else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_INTEGER("removed", count));
r = sd_json_buildo(&json,
SD_JSON_BUILD_PAIR_INTEGER("removed", count),
SD_JSON_BUILD_PAIR_UNSIGNED("disabledTransfers", disabled_count));
if (r < 0)
return log_error_errno(r, "Failed to create JSON: %m");
@@ -806,7 +912,7 @@ static int context_make_offline(Context **ret, const char *node) {
if (!context)
return log_oom();
r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node);
r = context_read_definitions(context, node);
if (r < 0)
return r;
@@ -1095,6 +1201,140 @@ static int verb_list(int argc, char **argv, void *userdata) {
}
}
static int verb_features(int argc, char **argv, void *userdata) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
_cleanup_(context_freep) Context* context = NULL;
_cleanup_(table_unrefp) Table *table = NULL;
const char *feature_id;
Feature *f;
int r;
assert(argc <= 2);
feature_id = argc >= 2 ? argv[1] : NULL;
r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
if (r < 0)
return r;
r = context_make_offline(&context, loop_device ? loop_device->node : NULL);
if (r < 0)
return r;
if (feature_id) {
_cleanup_strv_free_ char **transfers = NULL;
f = hashmap_get(context->features, feature_id);
if (!f)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
"Optional feature not found: %s",
feature_id);
table = table_new_vertical();
if (!table)
return log_oom();
FOREACH_ARRAY(tr, context->transfers, context->n_transfers) {
Transfer *t = *tr;
if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
continue;
r = strv_extend(&transfers, t->id);
if (r < 0)
return log_oom();
}
FOREACH_ARRAY(tr, context->disabled_transfers, context->n_disabled_transfers) {
Transfer *t = *tr;
if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
continue;
r = strv_extend(&transfers, t->id);
if (r < 0)
return log_oom();
}
r = table_add_many(table,
TABLE_FIELD, "Name",
TABLE_STRING, f->id,
TABLE_FIELD, "Enabled",
TABLE_BOOLEAN, f->enabled);
if (r < 0)
return table_log_add_error(r);
if (f->description) {
r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description);
if (r < 0)
return table_log_add_error(r);
}
if (f->documentation) {
r = table_add_many(table,
TABLE_FIELD, "Documentation",
TABLE_STRING, f->documentation,
TABLE_SET_URL, f->documentation);
if (r < 0)
return table_log_add_error(r);
}
if (f->appstream) {
r = table_add_many(table,
TABLE_FIELD, "AppStream",
TABLE_STRING, f->appstream,
TABLE_SET_URL, f->appstream);
if (r < 0)
return table_log_add_error(r);
}
if (!strv_isempty(transfers)) {
r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, transfers);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
} else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
table = table_new("", "feature", "description", "documentation");
if (!table)
return log_oom();
HASHMAP_FOREACH(f, context->features) {
r = table_add_many(table,
TABLE_BOOLEAN_CHECKMARK, f->enabled,
TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled),
TABLE_STRING, f->id,
TABLE_STRING, f->description,
TABLE_STRING, f->documentation,
TABLE_SET_URL, f->documentation);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
} else {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **features = NULL;
HASHMAP_FOREACH(f, context->features) {
r = strv_extend(&features, f->id);
if (r < 0)
return log_oom();
}
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRV("features", features));
if (r < 0)
return log_error_errno(r, "Failed to create JSON: %m");
r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
if (r < 0)
return log_error_errno(r, "Failed to print JSON: %m");
}
return 0;
}
static int verb_check_new(int argc, char **argv, void *userdata) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
@@ -1411,6 +1651,7 @@ static int verb_help(int argc, char **argv, void *userdata) {
"\n%5$sUpdate OS images.%6$s\n"
"\n%3$sCommands:%4$s\n"
" list [VERSION] Show installed and available versions\n"
" features [FEATURE] Show optional features\n"
" check-new Check if there's a new version available\n"
" update [VERSION] Install new version now\n"
" vacuum Make room, by deleting old versions\n"
@@ -1623,8 +1864,9 @@ static int sysupdate_main(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "list", VERB_ANY, 2, VERB_DEFAULT, verb_list },
{ "components", VERB_ANY, 1, 0, verb_components },
{ "features", VERB_ANY, 2, 0, verb_features },
{ "check-new", VERB_ANY, 1, 0, verb_check_new },
{ "update", VERB_ANY, 2, 0, verb_update },
{ "update", VERB_ANY, 2, 0, verb_update },
{ "vacuum", VERB_ANY, 1, 0, verb_vacuum },
{ "reboot", 1, 1, 0, verb_pending_or_reboot },
{ "pending", 1, 1, 0, verb_pending_or_reboot },
+4
View File
@@ -4,6 +4,8 @@
#include <inttypes.h>
#include <stdbool.h>
#include "specifier.h"
/* Forward declare this type so that Transfers can point at it */
typedef struct Context Context;
@@ -11,3 +13,5 @@ extern bool arg_sync;
extern uint64_t arg_instances_max;
extern char *arg_root;
extern char *arg_transfer_source;
extern const Specifier specifier_table[];
+228 -20
View File
@@ -13,6 +13,7 @@
#include "bus-util.h"
#include "common-signal.h"
#include "discover-image.h"
#include "dropin.h"
#include "env-util.h"
#include "escape.h"
#include "event-util.h"
@@ -31,6 +32,8 @@
#include "string-table.h"
#include "sysupdate-util.h"
#define FEATURES_DROPIN_NAME "systemd-sysupdate-enabled"
typedef struct Manager {
sd_event *event;
sd_bus *bus;
@@ -85,6 +88,7 @@ typedef enum JobType {
JOB_CHECK_NEW,
JOB_UPDATE,
JOB_VACUUM,
JOB_DESCRIBE_FEATURE,
_JOB_TYPE_MAX,
_JOB_TYPE_INVALID = -EINVAL,
} JobType;
@@ -104,6 +108,7 @@ struct Job {
JobType type;
bool offline;
char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */
char *feature; /* Passed into sysupdate for JOB_DESCRIBE_FEATURE */
unsigned progress_percent;
@@ -131,11 +136,12 @@ static const char* const target_class_table[_TARGET_CLASS_MAX] = {
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(target_class, TargetClass);
static const char* const job_type_table[_JOB_TYPE_MAX] = {
[JOB_LIST] = "list",
[JOB_DESCRIBE] = "describe",
[JOB_CHECK_NEW] = "check-new",
[JOB_UPDATE] = "update",
[JOB_VACUUM] = "vacuum",
[JOB_LIST] = "list",
[JOB_DESCRIBE] = "describe",
[JOB_CHECK_NEW] = "check-new",
[JOB_UPDATE] = "update",
[JOB_VACUUM] = "vacuum",
[JOB_DESCRIBE_FEATURE] = "describe-feature",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(job_type, JobType);
@@ -149,6 +155,7 @@ static Job *job_free(Job *j) {
free(j->object_path);
free(j->version);
free(j->feature);
sd_json_variant_unref(j->json);
@@ -440,8 +447,8 @@ static int job_start(Job *j) {
NULL, /* maybe --verify=no */
NULL, /* maybe --component=, --root=, or --image= */
NULL, /* maybe --offline */
NULL, /* list, check-new, update, vacuum */
NULL, /* maybe version (for list, update) */
NULL, /* list, check-new, update, vacuum, features */
NULL, /* maybe version (for list, update), maybe feature (features) */
NULL
};
size_t k = 2;
@@ -493,6 +500,12 @@ static int job_start(Job *j) {
cmd[k++] = "vacuum";
break;
case JOB_DESCRIBE_FEATURE:
cmd[k++] = "features";
assert(!isempty(j->feature));
cmd[k++] = j->feature;
break;
default:
assert_not_reached();
}
@@ -573,20 +586,26 @@ static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *
action = "org.freedesktop.sysupdate1.vacuum";
break;
case JOB_DESCRIBE_FEATURE:
action = NULL;
break;
default:
assert_not_reached();
}
r = bus_verify_polkit_async(
msg,
action,
/* details= */ NULL,
&j->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
if (action) {
r = bus_verify_polkit_async(
msg,
action,
/* details= */ NULL,
&j->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
}
r = job_cancel(j);
if (r < 0)
@@ -918,6 +937,8 @@ static int target_method_describe_finish(
_cleanup_free_ char *text = NULL;
int r;
/* NOTE: This is also reused by target_method_describe_feature */
assert(json);
r = sd_json_variant_format(json, 0, &text);
@@ -1132,7 +1153,7 @@ static int target_method_vacuum_finish(
sd_bus_error *error) {
sd_json_variant *v;
uint64_t instances;
uint64_t instances, disabled;
assert(json);
@@ -1144,7 +1165,15 @@ static int target_method_vacuum_finish(
instances = sd_json_variant_unsigned(v);
assert(instances <= UINT32_MAX);
return sd_bus_reply_method_return(msg, "u", (uint32_t) instances);
v = sd_json_variant_by_key(json, "disabledTransfers");
if (!v)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Missing key 'disabledTransfers'");
if (!sd_json_variant_is_unsigned(v))
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Key 'disabledTransfers' should be an unsigned int");
disabled = sd_json_variant_unsigned(v);
assert(disabled <= UINT32_MAX);
return sd_bus_reply_method_return(msg, "uu", (uint32_t) instances, (uint32_t) disabled);
}
static int target_method_vacuum(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
@@ -1247,6 +1276,167 @@ static int target_method_get_appstream(sd_bus_message *msg, void *userdata, sd_b
return sd_bus_send(NULL, reply, NULL);
}
static int target_method_list_features(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **features = NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Target *t = ASSERT_PTR(userdata);
sd_json_variant *v;
uint64_t flags;
int r;
assert(msg);
r = sd_bus_message_read(msg, "t", &flags);
if (r < 0)
return r;
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
r = sysupdate_run_simple(&json, t, "features", NULL);
if (r < 0)
return r;
v = sd_json_variant_by_key(json, "features");
if (!v)
return -EINVAL;
r = sd_json_variant_strv(v, &features);
if (r < 0)
return r;
r = sd_bus_message_new_method_return(msg, &reply);
if (r < 0)
return r;
r = sd_bus_message_append_strv(reply, features);
if (r < 0)
return r;
return sd_bus_send(NULL, reply, NULL);
}
static int target_method_describe_feature(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(job_freep) Job *j = NULL;
const char *feature;
uint64_t flags;
int r;
assert(msg);
r = sd_bus_message_read(msg, "st", &feature, &flags);
if (r < 0)
return r;
if (isempty(feature))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Feature must be specified");
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
r = job_new(JOB_DESCRIBE_FEATURE, t, msg, target_method_describe_finish, &j);
if (r < 0)
return r;
j->feature = strdup(feature);
if (!j->feature)
return log_oom();
r = job_start(j);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
TAKE_PTR(j); /* Avoid job from being killed & freed */
return 1;
}
static bool feature_name_is_valid(const char *name) {
if (isempty(name))
return false;
if (!ascii_is_valid(name))
return false;
if (!filename_is_valid(strjoina(name, ".feature.d")))
return false;
return true;
}
static int target_method_set_feature_enabled(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
_cleanup_free_ char *feature_ext = NULL;
Target *t = ASSERT_PTR(userdata);
const char *feature;
uint64_t flags;
int32_t enabled;
int r;
assert(msg);
if (t->class != TARGET_HOST)
return sd_bus_reply_method_errorf(msg,
SD_BUS_ERROR_NOT_SUPPORTED,
"For now, features can only be managed on the host system.");
r = sd_bus_message_read(msg, "sit", &feature, &enabled, &flags);
if (r < 0)
return r;
if (!feature_name_is_valid(feature))
return sd_bus_reply_method_errorf(msg,
SD_BUS_ERROR_INVALID_ARGS,
"The specified feature is invalid");
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
if (!endswith(feature, ".feature")) {
feature_ext = strjoin(feature, ".feature");
if (!feature_ext)
return -ENOMEM;
feature = feature_ext;
}
const char *details[] = {
"class", target_class_to_string(t->class),
"name", t->name,
"feature", feature,
"enabled", enabled >= 0 ? true_false(enabled) : "unset",
NULL
};
r = bus_verify_polkit_async(
msg,
"org.freedesktop.sysupdate1.manage-features",
details,
&t->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
/* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */
if (enabled < 0) { /* Reset -> delete the drop-in file */
_cleanup_free_ char *path = NULL;
r = drop_in_file(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME, NULL, &path);
if (r < 0)
return r;
if (unlink(path) < 0)
return -errno;
} else { /* otherwise, create the drop-in with the right settings */
r = write_drop_in_format(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME,
"# Generated via org.freedesktop.sysupdate1 D-Bus interface\n\n"
"[Feature]\n"
"Enabled=%s\n",
yes_no(enabled));
if (r < 0)
return r;
}
return sd_bus_reply_method_return(msg, NULL);
}
static int target_list_components(Target *t, char ***ret_components, bool *ret_have_default) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **components = NULL;
@@ -1397,7 +1587,7 @@ static const sd_bus_vtable target_vtable[] = {
SD_BUS_METHOD_WITH_ARGS("Vacuum",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("u", count),
SD_BUS_RESULT("u", instances, "u", disabled_transfers),
target_method_vacuum,
SD_BUS_VTABLE_UNPRIVILEGED),
@@ -1413,6 +1603,24 @@ static const sd_bus_vtable target_vtable[] = {
target_method_get_version,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("ListFeatures",
SD_BUS_ARGS("t", flags),
SD_BUS_RESULT("as", features),
target_method_list_features,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("DescribeFeature",
SD_BUS_ARGS("s", feature, "t", flags),
SD_BUS_RESULT("s", json),
target_method_describe_feature,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("SetFeatureEnabled",
SD_BUS_ARGS("s", feature, "i", enabled, "t", flags),
SD_BUS_NO_RESULT,
target_method_set_feature_enabled,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};
+330 -24
View File
@@ -12,12 +12,18 @@
#include "bus-locator.h"
#include "bus-map-properties.h"
#include "bus-util.h"
#include "conf-files.h"
#include "conf-parser.h"
#include "errno-list.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-table.h"
#include "fs-util.h"
#include "json-util.h"
#include "main-func.h"
#include "os-util.h"
#include "pager.h"
#include "path-util.h"
#include "pretty-print.h"
#include "strv.h"
#include "sysupdate-update-set-flags.h"
@@ -29,9 +35,11 @@ static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_reboot = false;
static bool arg_offline = false;
static bool arg_now = false;
static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
static char *arg_host = NULL;
#define SYSUPDATE_HOST_PATH "/org/freedesktop/sysupdate1/target/host"
#define SYSUPDATE_TARGET_INTERFACE "org.freedesktop.sysupdate1.Target"
typedef struct Version {
@@ -1044,21 +1052,19 @@ static int update_started(sd_bus_message *reply, void *userdata, sd_bus_error *r
return 0;
}
static int verb_update(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
static int do_update(sd_bus *bus, char **targets) {
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(sd_event_source_unrefp) sd_event_source *render_exit = NULL;
_cleanup_ordered_hashmap_free_ OrderedHashmap *map = NULL;
_cleanup_strv_free_ char **targets = NULL, **versions = NULL, **target_paths = NULL;
_cleanup_strv_free_ char **versions = NULL, **target_paths = NULL;
size_t n;
unsigned remaining = 0;
void *p;
bool did_anything = false;
int r;
r = ensure_targets(bus, argv + 1, &targets);
if (r < 0)
return log_error_errno(r, "Could not find targets: %m");
assert(bus);
assert(targets);
r = parse_targets(targets, &n, &target_paths, &versions);
if (r < 0)
@@ -1140,18 +1146,64 @@ static int verb_update(int argc, char **argv, void *userdata) {
did_anything = true;
}
if (arg_reboot) {
if (did_anything)
return reboot_now();
log_info("Nothing was updated... skipping reboot.");
}
return did_anything ? 1 : 0;
}
static int verb_update(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
_cleanup_strv_free_ char **targets = NULL;
bool did_anything = false;
int r;
r = ensure_targets(bus, argv + 1, &targets);
if (r < 0)
return log_error_errno(r, "Could not find targets: %m");
r = do_update(bus, targets);
if (r < 0)
return r;
if (r > 0)
did_anything = true;
if (!arg_reboot)
return 0;
if (did_anything)
return reboot_now();
log_info("Nothing was updated... skipping reboot.");
return 0;
}
static int do_vacuum(sd_bus *bus, const char *target, const char *path) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
uint32_t count, disabled;
int r;
r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, path, SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
if (r < 0)
return log_bus_error(r, &error, target, "call Vacuum");
r = sd_bus_message_read(reply, "uu", &count, &disabled);
if (r < 0)
return bus_log_parse_error(r);
if (count > 0 && disabled > 0)
log_info("Deleted %u instance(s) and %u disabled transfer(s) of %s.",
count, disabled, target);
else if (count > 0)
log_info("Deleted %u instance(s) of %s.", count, target);
else if (disabled > 0)
log_info("Deleted %u disabled transfer(s) of %s.", disabled, target);
else
log_info("Found nothing to delete for %s.", target);
return count + disabled > 0 ? 1 : 0;
}
static int verb_vacuum(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
size_t n;
int r;
@@ -1165,19 +1217,260 @@ static int verb_vacuum(int argc, char **argv, void *userdata) {
return log_error_errno(r, "Failed to parse targets: %m");
for (size_t i = 0; i < n; i++) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
uint32_t count;
r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, target_paths[i], SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
r = do_vacuum(bus, targets[i], target_paths[i]);
if (r < 0)
return log_bus_error(r, &error, targets[i], "call Vacuum");
return r;
}
return 0;
}
r = sd_bus_message_read(reply, "u", &count);
typedef struct Feature {
char *name;
char *description;
bool enabled;
char *documentation;
char **transfers;
} Feature;
static void feature_done(Feature *f) {
assert(f);
f->name = mfree(f->name);
f->description = mfree(f->description);
f->documentation = mfree(f->documentation);
f->transfers = strv_free(f->transfers);
}
static int describe_feature(sd_bus *bus, const char *feature, Feature *ret) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_(feature_done) Feature f = {};
char *json;
int r;
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, name), SD_JSON_MANDATORY },
{ "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, description), 0 },
{ "enabled", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Feature, enabled), SD_JSON_MANDATORY },
{ "documentationUrl", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, documentation), 0 },
{ "transfers", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Feature, transfers), 0 },
{}
};
assert(bus);
assert(feature);
assert(ret);
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"DescribeFeature",
&error,
&reply,
"st",
feature,
UINT64_C(0));
if (r < 0)
return log_bus_error(r, &error, "host", "lookup feature");
r = sd_bus_message_read_basic(reply, 's', &json);
if (r < 0)
return bus_log_parse_error(r);
r = sd_json_parse(json, 0, &v, NULL, NULL);
if (r < 0)
return log_error_errno(r, "Failed to parse JSON: %m");
r = sd_json_dispatch(v, dispatch_table, 0, &f);
if (r < 0)
return log_error_errno(r, "Failed to dispatch JSON: %m");
*ret = TAKE_STRUCT(f);
return 0;
}
static int list_features(sd_bus *bus) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_strv_free_ char **features = NULL;
_cleanup_(table_unrefp) Table *table = NULL;
int r;
assert(bus);
table = table_new("", "feature", "description");
if (!table)
return log_oom();
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"ListFeatures",
&error,
&reply,
"t",
UINT64_C(0));
if (r < 0)
return log_bus_error(r, &error, "host", "lookup feature");
r = sd_bus_message_read_strv(reply, &features);
if (r < 0)
return bus_log_parse_error(r);
STRV_FOREACH(feature, features) {
_cleanup_(feature_done) Feature f = {};
_cleanup_free_ char *name_link = NULL;
r = describe_feature(bus, *feature, &f);
if (r < 0)
return r;
if (urlify_enabled() && f.documentation) {
name_link = strjoin(f.name, special_glyph(SPECIAL_GLYPH_EXTERNAL_LINK));
if (!name_link)
return log_oom();
}
r = table_add_many(table,
TABLE_BOOLEAN_CHECKMARK, f.enabled,
TABLE_SET_COLOR, ansi_highlight_green_red(f.enabled),
TABLE_STRING, name_link ?: f.name,
TABLE_SET_URL, f.documentation,
TABLE_STRING, f.description);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
}
static int verb_features(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
_cleanup_(table_unrefp) Table *table = NULL;
_cleanup_(feature_done) Feature f = {};
int r;
if (argc == 1)
return list_features(bus);
table = table_new_vertical();
if (!table)
return log_oom();
r = describe_feature(bus, argv[1], &f);
if (r < 0)
return r;
r = table_add_many(table,
TABLE_FIELD, "Name",
TABLE_STRING, f.name,
TABLE_FIELD, "Enabled",
TABLE_BOOLEAN, f.enabled);
if (r < 0)
return table_log_add_error(r);
if (f.description) {
r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f.description);
if (r < 0)
return table_log_add_error(r);
}
if (f.documentation) {
r = table_add_many(table,
TABLE_FIELD, "Documentation",
TABLE_STRING, f.documentation,
TABLE_SET_URL, f.documentation);
if (r < 0)
return table_log_add_error(r);
}
if (!strv_isempty(f.transfers)) {
r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, f.transfers);
if (r < 0)
return table_log_add_error(r);
}
return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, false);
}
static int verb_enable(int argc, char **argv, void *userdata) {
sd_bus *bus = ASSERT_PTR(userdata);
bool did_anything = false, enable;
char **features;
int r;
enable = streq(argv[0], "enable");
features = strv_skip(argv, 1);
STRV_FOREACH(feature, features) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"SetFeatureEnabled",
&error,
/* reply= */ NULL,
"sbt",
*feature,
(int) enable,
UINT64_C(0));
if (r < 0)
return log_bus_error(r, &error, "host", "call SetFeatureEnabled");
}
if (!arg_now) /* We weren't asked to apply the changes, so we're done! */
return 0;
if (enable) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_free_ char *target = NULL;
char *version = NULL;
/* We're downloading the new feature into the "current" version, which is either going to be
* the currently booted version or it's going to be a pending update that has already been
* installed and is just waiting for us to reboot into it. */
r = sd_bus_call_method(bus,
bus_sysupdate_mgr->destination,
SYSUPDATE_HOST_PATH,
SYSUPDATE_TARGET_INTERFACE,
"GetVersion",
&error,
&reply,
NULL);
if (r < 0)
return log_bus_error(r, &error, "host", "get current version");
r = sd_bus_message_read_basic(reply, 's', &version);
if (r < 0)
return bus_log_parse_error(r);
log_info("Deleted %u instance(s) of %s.\n", count, targets[i]);
}
target = strjoin("host@", version);
if (!target)
return log_oom();
r = do_update(bus, STRV_MAKE(target));
} else
r = do_vacuum(bus, "host", SYSUPDATE_HOST_PATH);
if (r < 0)
return r;
if (r > 0)
did_anything = true;
if (arg_reboot && did_anything)
return reboot_now();
else if (did_anything)
log_info("Feature(s) %s.", enable ? "downloaded" : "deleted");
else
log_info("Nothing %s%s.",
enable ? "downloaded" : "deleted",
arg_reboot ? ", skipping reboot" :"");
return 0;
}
@@ -1196,11 +1489,15 @@ static int help(void) {
" check [TARGET...] Check for updates\n"
" update [TARGET[@VERSION]...] Install updates\n"
" vacuum [TARGET...] Clean up old updates\n"
" features [FEATURE] List and inspect optional features on host OS\n"
" enable FEATURE... Enable optional feature on host OS\n"
" disable FEATURE... Disable optional feature on host OS\n"
" -h --help Show this help\n"
" --version Show package version\n"
"\n%3$sOptions:%4$s\n"
" --reboot Reboot after updating to newer version\n"
" --offline Do not fetch metadata from the network\n"
" --now Download/delete resources immediately\n"
" -H --host=[USER@]HOST Operate on remote host\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Do not show the headers and footers\n"
@@ -1222,6 +1519,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_NO_LEGEND,
ARG_REBOOT,
ARG_OFFLINE,
ARG_NOW,
};
static const struct option options[] = {
@@ -1232,6 +1530,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "host", required_argument, NULL, 'H' },
{ "reboot", no_argument, NULL, ARG_REBOOT },
{ "offline", no_argument, NULL, ARG_OFFLINE },
{ "now", no_argument, NULL, ARG_NOW },
{}
};
@@ -1270,6 +1569,10 @@ static int parse_argv(int argc, char *argv[]) {
arg_offline = true;
break;
case ARG_NOW:
arg_now = true;
break;
case '?':
return -EINVAL;
@@ -1286,10 +1589,13 @@ static int run(int argc, char *argv[]) {
int r;
static const Verb verbs[] = {
{ "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list },
{ "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check },
{ "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update },
{ "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum },
{ "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list },
{ "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check },
{ "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update },
{ "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum },
{ "features", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_features },
{ "enable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable },
{ "disable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable },
{}
};
+64 -8
View File
@@ -72,6 +72,9 @@ new_version() {
# Create a random extra payload
echo $RANDOM >"$WORKDIR/source/uki-extra-$version.efi"
# Create a random optional payload
echo $RANDOM >"$WORKDIR/source/optional-$version.efi"
# Create tarball of a directory
mkdir -p "$WORKDIR/source/dir-$version"
echo $RANDOM >"$WORKDIR/source/dir-$version/foo.txt"
@@ -155,7 +158,7 @@ EOF
mkdir -p "$d"
done
cat >"$CONFIGDIR/01-first.conf" <<EOF
cat >"$CONFIGDIR/01-first.transfer" <<EOF
[Source]
Type=regular-file
Path=$WORKDIR/source
@@ -168,7 +171,7 @@ MatchPattern=part1-@v
MatchPartitionType=root-x86-64
EOF
cat >"$CONFIGDIR/02-second.conf" <<EOF
cat >"$CONFIGDIR/02-second.transfer" <<EOF
[Source]
Type=regular-file
Path=$WORKDIR/source
@@ -181,7 +184,7 @@ MatchPattern=part2-@v
MatchPartitionType=root-x86-64-verity
EOF
cat >"$CONFIGDIR/03-third.conf" <<EOF
cat >"$CONFIGDIR/03-third.transfer" <<EOF
[Source]
Type=directory
Path=$WORKDIR/source
@@ -195,7 +198,7 @@ MatchPattern=dir-@v
InstancesMax=3
EOF
cat >"$CONFIGDIR/04-fourth.conf" <<EOF
cat >"$CONFIGDIR/04-fourth.transfer" <<EOF
[Source]
Type=regular-file
Path=$WORKDIR/source
@@ -214,7 +217,7 @@ TriesDone=0
InstancesMax=2
EOF
cat >"$CONFIGDIR/05-fifth.conf" <<EOF
cat >"$CONFIGDIR/05-fifth.transfer" <<EOF
[Source]
Type=regular-file
Path=$WORKDIR/source
@@ -227,6 +230,29 @@ PathRelativeTo=boot
MatchPattern=uki_@v.efi.extra.d/extra.addon.efi
Mode=0444
InstancesMax=2
EOF
cat >"$CONFIGDIR/optional.feature" <<EOF
[Feature]
Description=Optional Feature
EOF
cat >"$CONFIGDIR/99-optional.transfer" <<EOF
[Transfer]
Features=optional
[Source]
Type=regular-file
Path=$WORKDIR/source
MatchPattern=optional-@v.efi
[Target]
Type=regular-file
Path=/EFI/Linux
PathRelativeTo=boot
MatchPattern=uki_@v.efi.extra.d/optional.efi
Mode=0444
InstancesMax=2
EOF
rm -rf "${WORKDIR:?}"/{esp,xbootldr,source}
@@ -276,6 +302,28 @@ EOF
verify_version "$blockdev" "$sector_size" v3 1
verify_version_current "$blockdev" "$sector_size" v5 2
# Now let's try enabling an optional feature
"$SYSUPDATE" features | grep "optional"
"$SYSUPDATE" features optional | grep "99-optional"
test ! -f "$WORKDIR/xbootldr/EFI/Linux/uki_v5.efi.extra.d/optional.efi"
mkdir "$CONFIGDIR/optional.feature.d"
echo -e "[Feature]\nEnabled=true" > "$CONFIGDIR/optional.feature.d/enable.conf"
"$SYSUPDATE" --offline list v5 | grep -q "incomplete"
update_now
"$SYSUPDATE" --offline list v5 | grep -qv "incomplete"
verify_version "$blockdev" "$sector_size" v3 1
verify_version_current "$blockdev" "$sector_size" v5 2
test -f "$WORKDIR/xbootldr/EFI/Linux/uki_v5.efi.extra.d/optional.efi"
# And now let's disable it and make sure it gets cleaned up
rm -r "$CONFIGDIR/optional.feature.d"
(! "$SYSUPDATE" --verify=no check-new)
"$SYSUPDATE" vacuum
"$SYSUPDATE" --offline list v5 | grep -qv "incomplete"
verify_version "$blockdev" "$sector_size" v3 1
verify_version_current "$blockdev" "$sector_size" v5 2
test ! -f "$WORKDIR/xbootldr/EFI/Linux/uki_v5.efi.extra.d/optional.efi"
# Create sixth version, update using updatectl and verify it replaced the
# correct version
new_version "$sector_size" v6
@@ -299,7 +347,7 @@ EOF
# component so that updatectl has multiple targets to list.
if [[ -x "$SYSUPDATED" ]] && command -v updatectl; then
mkdir -p /run/sysupdate.test.d/
cp "$CONFIGDIR/01-first.conf" /run/sysupdate.test.d/01-first.conf
cp "$CONFIGDIR/01-first.transfer" /run/sysupdate.test.d/01-first.transfer
updatectl list
updatectl list host
updatectl list host@v6
@@ -315,7 +363,7 @@ EOF
# see above)
new_version "$sector_size" v7
cat >"$CONFIGDIR/02-second.conf" <<EOF
cat >"$CONFIGDIR/02-second.transfer" <<EOF
[Source]
Type=url-file
Path=file://$WORKDIR/source
@@ -328,7 +376,7 @@ MatchPattern=part2-@v
MatchPartitionType=root-x86-64-verity
EOF
cat >"$CONFIGDIR/03-third.conf" <<EOF
cat >"$CONFIGDIR/03-third.transfer" <<EOF
[Source]
Type=url-tar
Path=file://$WORKDIR/source
@@ -346,6 +394,14 @@ EOF
verify_version "$blockdev" "$sector_size" v6 1
verify_version_current "$blockdev" "$sector_size" v7 2
# Let's make sure that we don't break our backwards-compat for .conf files
# (what .transfer files were called before v257)
for i in "$CONFIGDIR/"*.conf; do echo mv "$i" "${i%.conf}.transfer"; done
new_version "$sector_size" v8
update_now
verify_version_current "$blockdev" "$sector_size" v8 1
verify_version "$blockdev" "$sector_size" v7 2
# Cleanup
[[ -b "$blockdev" ]] && losetup --detach "$blockdev"
rm "$BACKING_FILE"