Merge pull request #33398 from AdrianVovk/sysupdate-optional
sysupdate: Add support for optional features
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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
@@ -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 },
|
||||
{}
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user