diff options
39 files changed, 6445 insertions, 21 deletions
@@ -12493,7 +12493,7 @@ CHANGES WITH 197: based on a calendar time specification such as "Thu,Fri 2013-*-1,5 11:12:13" which refers to 11:12:13 of the first or fifth day of any month of the year 2013, given that it is - a thursday or friday. This brings timer event support + a Thursday or a Friday. This brings timer event support considerably closer to cron's capabilities. For details on the supported calendar time specification language see systemd.time(7). @@ -251,11 +251,26 @@ Features: that images cannot be misused. * New udev block device symlink names: - /dev/disk/by-parttypelabel/<pttype>/<ptlabel>. Use case: if pt label is used + /dev/disk/by-parttypelabel/<pttype>-<ptlabel>. Use case: if pt label is used as partition image version string, this is a safe way to reference a specific version of a specific partition type, in particular where related partitions are processed (e.g. verity + rootfs both named "LennartOS_0.7"). +* sysupdate: + - add fuzzing to the pattern parser + - support casync as download mechanism + - direct TPM2 PCR change handling, possible renrolling LUKS2 media if needed. + - "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 system extenion' 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, + i.e. a Compress=no option in the transfer definitions + * in sd-id128: also parse UUIDs in RFC4122 URN syntax (i.e. chop off urn:uuid: prefix) * DynamicUser= + StateDirectory= → use uid mapping mounts, too, in order to diff --git a/man/loader.conf.xml b/man/loader.conf.xml index e5453c7dcd..1186d3e2b4 100644 --- a/man/loader.conf.xml +++ b/man/loader.conf.xml @@ -30,10 +30,11 @@ <title>Description</title> <para> - <citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry> - will read <filename><replaceable>ESP</replaceable>/loader/loader.conf</filename> and any files with the + <citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry> will + read <filename><replaceable>ESP</replaceable>/loader/loader.conf</filename>, and any files with the <literal>.conf</literal> extension under - <filename><replaceable>ESP</replaceable>/loader/entries/</filename> on the EFI system partition (ESP). + <filename><replaceable>ESP</replaceable>/loader/entries/</filename> on the EFI system partition (ESP) as + defined by <ulink url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink>. </para> <para>Each configuration file must consist of an option name, followed by diff --git a/man/rules/meson.build b/man/rules/meson.build index 2e334ff331..aa969344ab 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -987,6 +987,13 @@ manpages = [ '5', ['system.conf.d', 'systemd-user.conf', 'user.conf.d'], ''], + ['systemd-sysupdate', + '8', + ['systemd-sysupdate-reboot.service', + 'systemd-sysupdate-reboot.timer', + 'systemd-sysupdate.service', + 'systemd-sysupdate.timer'], + 'ENABLE_SYSUPDATE'], ['systemd-sysusers', '8', ['systemd-sysusers.service'], ''], ['systemd-sysv-generator', '8', [], 'HAVE_SYSV_COMPAT'], ['systemd-time-wait-sync.service', @@ -1058,6 +1065,7 @@ manpages = [ ['systemd.time', '7', [], ''], ['systemd.timer', '5', [], ''], ['systemd.unit', '5', [], ''], + ['sysupdate.d', '5', [], 'ENABLE_SYSUPDATE'], ['sysusers.d', '5', [], 'ENABLE_SYSUSERS'], ['telinit', '8', [], 'HAVE_SYSV_COMPAT'], ['timedatectl', '1', [], 'ENABLE_TIMEDATECTL'], diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml index ceea368ef2..6029330dd1 100644 --- a/man/systemd-boot.xml +++ b/man/systemd-boot.xml @@ -39,23 +39,22 @@ <itemizedlist> <listitem><para>Boot entries defined with <ulink - url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink> description files - located in <filename>/loader/entries/</filename> on the ESP and the Extended Boot Loader - Partition. These usually describe Linux kernel images with associated initrd images, but alternatively - may also describe arbitrary other EFI executables.</para></listitem> - - <listitem><para>Unified kernel images following the <ulink - url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink>, as executable EFI - binaries in <filename>/EFI/Linux/</filename> on the ESP and the Extended Boot Loader Partition. - </para></listitem> + url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader Specification</ulink> Type #1 + description files located in <filename>/loader/entries/</filename> on the ESP and the Extended Boot + Loader Partition. These usually describe Linux kernel images with associated initrd images, but + alternatively may also describe other arbitrary EFI executables.</para></listitem> + + <listitem><para>Unified kernel images, <ulink url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot + Loader Specification</ulink> Type #2, which are executable EFI binaries in + <filename>/EFI/Linux/</filename> on the ESP and the Extended Boot Loader Partition.</para></listitem> - <listitem><para>The Microsoft Windows EFI boot manager, if installed</para></listitem> + <listitem><para>The Microsoft Windows EFI boot manager, if installed.</para></listitem> - <listitem><para>The Apple macOS boot manager, if installed</para></listitem> + <listitem><para>The Apple macOS boot manager, if installed.</para></listitem> - <listitem><para>The EFI Shell binary, if installed</para></listitem> + <listitem><para>The EFI Shell binary, if installed.</para></listitem> - <listitem><para>A reboot into the UEFI firmware setup option, if supported by the firmware</para></listitem> + <listitem><para>A reboot into the UEFI firmware setup option, if supported by the firmware.</para></listitem> </itemizedlist> <para><command>systemd-boot</command> supports the following features:</para> diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml new file mode 100644 index 0000000000..81f57e8b52 --- /dev/null +++ b/man/systemd-sysupdate.xml @@ -0,0 +1,287 @@ +<?xml version='1.0'?> <!--*-nxml-*--> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"> +<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> + +<refentry id="systemd-sysupdate" conditional='ENABLE_SYSUPDATE' + xmlns:xi="http://www.w3.org/2001/XInclude"> + + <refentryinfo> + <title>systemd-sysupdate</title> + <productname>systemd</productname> + </refentryinfo> + + <refmeta> + <refentrytitle>systemd-sysupdate</refentrytitle> + <manvolnum>8</manvolnum> + </refmeta> + + <refnamediv> + <refname>systemd-sysupdate</refname> + <refname>systemd-sysupdate.service</refname> + <refname>systemd-sysupdate.timer</refname> + <refname>systemd-sysupdate-reboot.service</refname> + <refname>systemd-sysupdate-reboot.timer</refname> + <refpurpose>Automatically Update OS or Other Resources</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <cmdsynopsis> + <command>systemd-sysupdate</command> + <arg choice="opt" rep="repeat">OPTIONS</arg> + </cmdsynopsis> + + <para><filename>systemd-sysupdate.service</filename></para> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para><command>systemd-sysupdate</command> atomically updates the host OS, container images, portable + service images or other sources, based on the transfer configuration files described in + <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para> + + <para>This tool implements file, directory, or partition based update schemes, supporting multiple + parallel installed versions of specific resources in an A/B (or even: A/B/C, A/B/C/D/, …) style. A/B + updating means that when one version of a resource is currently being used, the next version can be + downloaded, unpacked, and prepared in an entirely separate location, indepdently of the first, and — once + complete — be activated, swapping the roles so that it becomes the used one and the previously used one + becomes the the one that is replaced by the next update, and so on. The resources to update are defined + in transfer files, one for each resource to be updated. For example, resources that may be updated with + this tool could be: a root file system partition, a matching Verity partition plus one kernel image. The + combination of the three would be considered a complete OS update.</para> + + <para>The tool updates partitions, files or directory trees always in whole, and operates with at least + two versions of each of these resources: the <emphasis>current</emphasis> version, plus the + <emphasis>next</emphasis> version: the one that is being updated to, and which is initially incomplete as + the downloaded data is written to it; plus optionally more versions. Once the download of a newer version + is complete it becomes the current version, releasing the version previously considered current for + deletion/replacement/updating.</para> + + <para>When installing new versions the tool will directly download, decompress, unpack and write the new + version into the destination. This is done in a robust fashion so that an incomplete download can be + recognized on next invocation, and flushed out before a new attempt is initiated.</para> + + <para>Note that when writing updates to a partition, the partition has to exist already, as + <command>systemd-sysupdate</command> will not automatically create new partitions. Use a tool such as + <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry> to + automatically create additional partitions to be used with <command>systemd-sysupdate</command> on + boot.</para> + + <para>The tool can both be used on the running OS, to update the OS in "online" state from within itself, + and on "offline" disk images, to update them from the outside based on transfer files + embedded in the disk images. For the latter, see <option>--image=</option> below. The latter is + particularly interesting to update container images or portable service images.</para> + + <para>The <filename>systemd-sysupdate.service</filename> system service will automatically update the + host OS based on the installed transfer files. It is triggered in regular intervals via + <filename>systemd-sysupdate.timer</filename>. The <filename>systemd-sysupdate-reboot.service</filename> + will automatically reboot the system after a new version is installed. It is triggered via + <filename>systemd-sysupdate-reboot.timer</filename>. The two services are separate from each other as it + is typically advisable to download updates regularly while the system is up, but delay reboots until the + appropriate time (i.e. typically at night). The two sets of service/timer units may be enabled + separately.</para> + + <para>For details about transfer files and examples see + <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para> + </refsect1> + + <refsect1> + <title>Command</title> + + <para>The following commands are understood:</para> + + <variablelist> + <varlistentry> + <term><option>list</option> <optional><replaceable>VERSION</replaceable></optional></term> + + <listitem><para>If invoked without an argument, enumerates downloadable and installed versions, and + shows a summarizing table with the discovered versions and their properties, including whether + there's a newer candidate version to update to. If a version argument is specified, shows details + about the specific version, including the individual files that need to be transferred to acquire the + version.</para> + + <para>If no command is explicitly specified this command is implied.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>check-new</option></term> + + <listitem><para>Checks if there's a new version available. This internally enumerates downloadable and + installed versions and returns exit status 0 if there's a new version to update to, non-zero + otherwise. If there is a new version to update to, its version identifier is written to standard + output.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>update</option> <optional><replaceable>VERSION</replaceable></optional></term> + + <listitem><para>Installs (updates to) the specified version, or if none is specified to the newest + version available. If the version is already installed or no newer version available, no operation is + executed.</para> + + <para>If a new version to install/update to is found, old installed versions are deleted until at + least one new version can be installed, as configured via <varname>InstanceMax=</varname> in + <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>, or + via the available partition slots of the right type. This implicit operation can also be invoked + explicitly via the <command>vacuum</command> command described below.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>vacuum</option></term> + + <listitem><para>Deletes old installed versions until the limits configured via + <varname>InstanceMax=</varname> in + <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry> are + met again. Normally, it should not be necessary to invoke this command explicitly, since it is + implicitly invoked whenever a new update is initiated.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>pending</option></term> + + <listitem><para>Checks whether a newer version of the OS is installed than the one currently + running. Returns zero if so, non-zero otherwise. This compares the newest installed version's + identifier with the OS image version as reported by the <varname>IMAGE_VERSION=</varname> field in + <filename>/etc/os-release</filename>. If the former is newer than the latter, an update was + apparently completed but not activated (i.e. rebooted into) yet.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>reboot</option></term> + + <listitem><para>Similar to the <option>pending</option> command but immediately reboots in case a + newer version of the OS has been installed than the one currently running. This operation can be done + implicitly together with the <command>update</command> command, after a completed update via the + <option>--reboot</option> switch, see below. This command will execute no operation (and return + success) if no update has been installed, and thus the system was not rebooted.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>components</option></term> + + <listitem><para>Lists components that can be updated. This enumerates the + <filename>/etc/sysupdate.*.d/</filename>, <filename>/run/sysupdate.*.d/</filename> and + <filename>/usr/lib/sysupdate.*.d/</filename> directories that contain transfer files. This command is + useful to list possible parameters for <option>--component=</option> (see below).</para></listitem> + </varlistentry> + + <xi:include href="standard-options.xml" xpointer="help" /> + <xi:include href="standard-options.xml" xpointer="version" /> + </variablelist> + </refsect1> + + <refsect1> + <title>Options</title> + + <para>The following options are understood:</para> + + <variablelist> + + <varlistentry> + <term><option>--component=</option></term> + <term><option>-C</option></term> + + <listitem><para>Selects the component to update. Takes a component name as argument. This has the + effect of slightly altering the search logic for transfer files. If this switch is not used, the + transfer files are loaded from <filename>/etc/sysupdate.d/*.conf</filename>, + <filename>/run/sysupdate.d/*.conf</filename> and <filename>/usr/lib/sysupdate.d/*.conf</filename>. If + this switch is used, the specified component name is used to alter the directories to look in to be + <filename>/etc/sysupdate.<replaceable>component</replaceable>.d/*.conf</filename>, + <filename>/run/sysupdate.<replaceable>component</replaceable>.d/*.conf</filename> and + <filename>/usr/lib/sysupdate.<replaceable>component</replaceable>.d/*.conf</filename>, each time with + the <filename><replaceable>component</replaceable></filename> string replaced with the specified + component name.</para> + + <para>Use the <command>components</command> command to list available components to update. This enumerates + the directories matching this naming rule.</para> + + <para>Components may be used to define a separate set of transfer files for different components of + the OS that shall be updated separately. Do not use this concept for resources that shall always be + updated together in a synchronous fashion. Simply define multiple transfer files within the same + <filename>sysupdate.d/</filename> directory for these cases.</para> + + <para>This option may not be combined with <option>--definitions=</option>.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--definitions=</option></term> + + <listitem><para>A path to a directory. If specified, the transfer <filename>*.conf</filename> files + are read from this directory instead of <filename>/usr/lib/sysupdate.d/*.conf</filename>, + <filename>/etc/sysupdate.d/*.conf</filename>, and <filename>/run/sysupdate.d/*.conf</filename>.</para> + + <para>This option may not be combined with <option>--component=</option>.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--root=</option></term> + + <listitem><para>Takes a path to a directory to use as root file system when searching for + <filename>sysupdate.d/*.conf</filename> files.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--image=</option></term> + + <listitem><para>Takes a path to a disk image file or device to mount and use in a similar fashion to + <option>--root=</option>, see above. If this is used and partition resources are updated this is done + inside the specified disk image.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--instances-max=</option></term> + <term><option>-m</option></term> + + <listitem><para>Takes a decimal integer greater than or equal to 2. Controls how many versions to + keep at any time. This option may also be configured inside the transfer files, via the + <varname>InstancesMax=</varname> setting, see + <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry> for + details.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--sync=</option></term> + + <listitem><para>Takes a boolean argument, defaults to yes. This may be used to specify whether the + newly updated resource versions shall be synchronized to disk when appropriate (i.e. after the + download is complete, before it is finalized, and again after finalization). This should not be + turned off, except to improve runtime performance in testing environments.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--verify=</option></term> + + <listitem><para>Takes a boolean argument, defaults to yes. Controls whether to cryptographically + verify downloads. Do not turn this off, except in testing environments.</para></listitem> + </varlistentry> + + <varlistentry> + <term><option>--reboot</option></term> + + <listitem><para>When used in combination with the <command>update</command> command and a new version is + installed, automatically reboots the system immediately afterwards.</para></listitem> + </varlistentry> + + <xi:include href="standard-options.xml" xpointer="no-pager" /> + <xi:include href="standard-options.xml" xpointer="no-legend" /> + <xi:include href="standard-options.xml" xpointer="json" /> + </variablelist> + </refsect1> + + <refsect1> + <title>Exit status</title> + + <para>On success, 0 is returned, a non-zero failure code otherwise.</para> + </refsect1> + + <refsect1> + <title>See Also</title> + <para> + <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>, + <citerefentry><refentrytitle>sysupdate.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>, + <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry> + </para> + </refsect1> + +</refentry> diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml new file mode 100644 index 0000000000..03d27b9fbc --- /dev/null +++ b/man/sysupdate.d.xml @@ -0,0 +1,885 @@ +<?xml version='1.0'?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"> +<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> + +<refentry id="sysupdate.d" conditional='ENABLE_SYSUPDATE' + xmlns:xi="http://www.w3.org/2001/XInclude"> + + <refentryinfo> + <title>sysupdate.d</title> + <productname>systemd</productname> + </refentryinfo> + + <refmeta> + <refentrytitle>sysupdate.d</refentrytitle> + <manvolnum>5</manvolnum> + </refmeta> + + <refnamediv> + <refname>sysupdate.d</refname> + <refpurpose>Transfer Definition Files for Automatic Updates</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <para><literallayout><filename>/etc/sysupdate.d/*.conf</filename> +<filename>/run/sysupdate.d/*.conf</filename> +<filename>/usr/lib/sysupdate.d/*.conf</filename> + </literallayout></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>The + <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry> + command reads these files and uses them to determine which local resources should be updated, and then + executes the update.</para> + + <para>Both the remote HTTP/HTTPS source and the local target typically exist in multiple, concurrent + 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 + 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> + + <para>Each <filename>*.conf</filename> file contains three sections: [Transfer], [Source] and [Target].</para> + </refsect1> + + <refsect1> + <title>Basic Mode of Operation</title> + + <para>Disk-image based OS updates typically consist of multiple different resources that need to be + updated together, for example a secure OS update might consist of a root file system image to drop into a + partition, a matching Verity integrity data partition image, and a kernel image prepared to boot into the + combination of the two partitions. The first two resources are files that are downloaded and placed in a + disk partition, the latter is a file that is downloaded and placed in a regular file in the boot file + system (e.g. EFI system partition). Hence, during an update of a hypothetical operating system "foobarOS" + to a hypothetical version 47 the following operations should take place:</para> + + <orderedlist> + <listitem><para>A file <literal>https://download.example.com/foobarOS_47.root.xz</literal> should be + downloaded, decompressed and written to a previously unused partition with GPT partition type UUID + 4f68bce3-e8cd-4db1-96e7-fbcaf984b709 for x86-64, as per <ulink + url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions + Specification</ulink>.</para></listitem> + + <listitem><para>Similarly, a file <literal>https://download.example.com/foobarOS_47.verity.xz</literal> + should be downloaded, decompressed and written to a previously empty partition with GPT partition type + UUID of 2c7357ed-ebd2-46d9-aec1-23d437ec2bf5 (i.e the partition type for Verity integrity information + for x86-64 root file systems).</para></listitem> + + <listitem><para>Finally, a file <literal>https://download.example.com/foobarOS_47.efi.xz</literal> (a + unified kernel, as per <ulink url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot Loader + Specification</ulink> Type #2) should be downloaded, decompressed and written to the ESP file system, + i.e. to <filename>EFI/Linux/foobarOS_47.efi</filename> in the ESP.</para></listitem> + </orderedlist> + + <para>The version-independent generalization of this would be (using the special marker + <literal>@v</literal> as wildcard for the version identifier):</para> + + <orderedlist> + <listitem><para>A transfer of a file <literal>https://download.example.com/foobarOS_@v.root.xz</literal> + → a local, previously empty GPT partition of type 4f68bce3-e8cd-4db1-96e7-fbcaf984b709, with the label to + be set to <literal>foobarOS_@v</literal>.</para></listitem> + + <listitem><para>A transfer of a file <literal>https://download.example.com/foobarOS_@v.verity.xz</literal> + → a local, previously empty GPT partition of type 2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, with the label to be + set to <literal>foobarOS_@v_verity</literal>.</para></listitem> + + <listitem><para>A transfer of a file <literal>https://download.example.com/foobarOS_@v.efi.xz</literal> + → a local file <filename>/efi/EFI/Linux/foobarOS_@v.efi</filename>.</para></listitem> + </orderedlist> + + <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> + 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 + shall be called on the target.</para> + + <para>In order to enumerate available versions and figuring out candidates to update to, a mechanism is + necessary to list suitable files:</para> + + <itemizedlist> + <listitem><para>For partitions: the surrounding GPT partition table contains a list of defined + partitions, including a partition type UUID and a partition label (in this scheme the partition label + plays a role for the partition similar to the filename for a regular file)</para></listitem> + + <listitem><para>For regular files: the directory listing of the directory the files are contained in + provides a list of existing files in a straightforward way.</para></listitem> + + <listitem><para>For HTTP/HTTPS sources a simple scheme is used: a manifest file + <filename>SHA256SUMS</filename>, following the format defined by <citerefentry + project='man-pages'><refentrytitle>sha256sum</refentrytitle><manvolnum>1</manvolnum></citerefentry>, + 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 + 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 + 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 + last, ensuring that any abnormal termination does not leave an entry point around whose backing is not + established yet. In the example above it would hence make sense to establish the EFI kernel image last + and thus give its transfer configuration file the alphabetically last name.</para> + + <para>See below for an extended, more specific example based on the above.</para> + </refsect1> + + <refsect1> + <title>Resource Types</title> + + <para>Each transfer file defines one source resource to transfer to one target resource. The following + resource types are supported:</para> + + <orderedlist> + + <listitem><para>Resources of type <literal>url-file</literal> encapsulate a file on a web server, + referenced via a HTTP or HTTPS URL. When an update takes place, the file is downloaded and decompressed + and then written to the target file or partition. This resource type is only available for sources, not + for targets. The list of available versions of resources of this type is encoded in + <filename>SHA256SUMS</filename> manifest files, accompanied by + <filename>SHA256SUMS.gpg</filename> detached signatures.</para></listitem> + + <listitem><para>The <literal>url-tar</literal> resource type is similar, but the file must be a + <filename>.tar</filename> archive. When an update takes place, the file is decompressed and unpacked + into a directory or btrfs subvolume. This resource type is only available for sources, not for + targets. Just like <literal>url-file</literal>, <literal>url-tar</literal> version enumeration makes + use of <filename>SHA256SUMS</filename> files, authenticated via + <filename>SHA256SUMS.gpg</filename>.</para></listitem> + + <listitem><para>The <literal>regular-file</literal> resource type encapsulates a local regular file on + disk. During updates the file is uncompressed and written to the target file or partition. This + resource type is available both as source and as target. When updating no integrity or authentication + verification is done for resources of this type.</para></listitem> + + <listitem><para>The <literal>partition</literal> resource type is similar to + <literal>regular-file</literal>, and encapsulates a GPT partition on disk. When updating, the partition + must exist already, and have the correct GPT partition type. A partition whose GPT partition label is + set to <literal>_empty</literal> is considered empty, and a candidate to place a newly downloaded + resource in. The GPT partition label is used to store version information, once a partition is + updated. This resource type is only available for target resources.</para></listitem> + + <listitem><para>The <literal>tar</literal> resource type encapsulates local <filename>.tar</filename> + archive files. When an update takes place, the files are uncompressed and unpacked into a target + directory or btrfs subvolume. Behaviour of <literal>tar</literal> and <literal>url-tar</literal> is + generally similar, but the latter downloads from remote sources, and does integrity and authentication + checks while the former does not. The <literal>tar</literal> resource type is only available for source + resources.</para></listitem> + + <listitem><para>The <literal>directory</literal> resource type encapsulates local directory trees. This + type is available both for source and target resources. If an update takes place on a source resource + of this type, a recursive copy of the directory is done.</para></listitem> + + <listitem><para>The <literal>subvolume</literal> resource type is identical to + <literal>directory</literal>, except when used as the target, in which case the file tree is placed in + a btrfs subvolume instead of a plain directory, if the backing file system supports it (i.e. is + btrfs).</para></listitem> + </orderedlist> + + <para>As already indicated, only a subset of source and target resource type combinations are + supported:</para> + + <table> + <title>Resource Types</title> + + <tgroup cols='3' align='left' colsep='1' rowsep='1'> + <colspec colname="name" /> + <colspec colname="explanation" /> + + <thead> + <row> + <entry>Identifier</entry> + <entry>Description</entry> + <entry>Usable as Source</entry> + <entry>When Used as Source: Compatible Targets</entry> + <entry>When Used as Source: Integrity + Authentication</entry> + <entry>When Used as Source: Decompression</entry> + <entry>Usable as Target</entry> + <entry>When Used as Target: Compatible Sources</entry> + </row> + </thead> + + <tbody> + <row> + <entry><constant>url-file</constant></entry> + <entry>HTTP/HTTPS files</entry> + <entry>yes</entry> + <entry><constant>regular-file</constant>, <constant>partition</constant></entry> + <entry>yes</entry> + <entry>yes</entry> + <entry>no</entry> + <entry>-</entry> + </row> + + <row> + <entry><constant>url-tar</constant></entry> + <entry>HTTP/HTTPS <filename>.tar</filename> archives</entry> + <entry>yes</entry> + <entry><constant>directory</constant>, <constant>subvolume</constant></entry> + <entry>yes</entry> + <entry>yes</entry> + <entry>no</entry> + <entry>-</entry> + </row> + + <row> + <entry><constant>regular-file</constant></entry> + <entry>Local files</entry> + <entry>yes</entry> + <entry><constant>regular-file</constant>, <constant>partition</constant></entry> + <entry>no</entry> + <entry>yes</entry> + <entry>yes</entry> + <entry><constant>url-file</constant>, <constant>regular-file</constant></entry> + </row> + + <row> + <entry><constant>partition</constant></entry> + <entry>Local GPT partitions</entry> + <entry>no</entry> + <entry>-</entry> + <entry>-</entry> + <entry>-</entry> + <entry>yes</entry> + <entry><constant>url-file</constant>, <constant>regular-file</constant></entry> + </row> + + <row> + <entry><constant>tar</constant></entry> + <entry>Local <filename>.tar</filename> archives</entry> + <entry>yes</entry> + <entry><constant>directory</constant>, <constant>subvolume</constant></entry> + <entry>no</entry> + <entry>yes</entry> + <entry>no</entry> + <entry>-</entry> + </row> + + <row> + <entry><constant>directory</constant></entry> + <entry>Local directories</entry> + <entry>yes</entry> + <entry><constant>directory</constant>, <constant>subvolume</constant></entry> + <entry>no</entry> + <entry>no</entry> + <entry>yes</entry> + <entry><constant>url-tar</constant>, <constant>tar</constant>, <constant>directory</constant>, <constant>subvolume</constant></entry> + </row> + + <row> + <entry><constant>subvolume</constant></entry> + <entry>Local btrfs subvolumes</entry> + <entry>yes</entry> + <entry><constant>directory</constant>, <constant>subvolume</constant></entry> + <entry>no</entry> + <entry>no</entry> + <entry>yes</entry> + <entry><constant>url-tar</constant>, <constant>tar</constant>, <constant>directory</constant>, <constant>subvolume</constant></entry> + </row> + + </tbody> + </tgroup> + </table> + </refsect1> + + <refsect1> + <title>Match Patterns</title> + + <para>Both the source and target resources typically exist in multiple versions concurrently. An update + operation is done whenever the newest of the source versions is newer than the newest of the target + versions. To determine the newest version of the resources a directory listing, partition listing or + manifest listing is used, a subset of qualifying entries selected from that, and the version identifier + extracted from the file names or partition labels of these selected entries. Subset selection and + extraction of the version identifier (plus potentially other metadata) is done via match patterns, + configured in <varname>MatchPattern=</varname> in the [Source] and [Target] sections. These patterns are + strings that describe how files or partitions are named, with named wildcards for specific fields such as + the version identifier. The following wildcards are defined:</para> + + <table> + <title>Match Pattern Wildcards</title> + + <tgroup cols='2' align='left' colsep='1' rowsep='1'> + <colspec colname="name" /> + <colspec colname="explanation" /> + + <thead> + <row> + <entry>Wildcard</entry> + <entry>Description</entry> + <entry>Format</entry> + <entry>Notes</entry> + </row> + </thead> + + <tbody> + <row> + <entry><literal>@v</literal></entry> + <entry>Version identifier</entry> + <entry>Valid version string</entry> + <entry>Mandatory</entry> + </row> + + <row> + <entry><literal>@u</literal></entry> + <entry>GPT partition UUID</entry> + <entry>Valid 128-Bit UUID string</entry> + <entry>Only relevant if target resource type chosen as <constant>partition</constant></entry> + </row> + + <row> + <entry><literal>@f</literal></entry> + <entry>GPT partition flags</entry> + <entry>Formatted hexadecimal integer</entry> + <entry>Only relevant if target resource type chosen as <constant>partition</constant></entry> + </row> + + <row> + <entry><literal>@a</literal></entry> + <entry>GPT partition flag NoAuto</entry> + <entry>Either <literal>0</literal> or <literal>1</literal></entry> + <entry>Controls NoAuto bit of the GPT partition flags, as per <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink>; only relevant if target resource type chosen as <constant>partition</constant></entry> + </row> + + <row> + <entry><literal>@g</literal></entry> + <entry>GPT partition flag GrowFileSystem</entry> + <entry>Either <literal>0</literal> or <literal>1</literal></entry> + <entry>Controls GrowFileSystem bit of the GPT partition flags, as per <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink>; only relevant if target resource type chosen as <constant>partition</constant></entry> + </row> + + <row> + <entry><literal>@r</literal></entry> + <entry>Read-only flag</entry> + <entry>Either <literal>0</literal> or <literal>1</literal></entry> + <entry>Controls ReadOnly bit of the GPT partition flags, as per <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions Specification</ulink> and other output read-only flags, see <varname>ReadOnly=</varname> below.</entry> + </row> + + <row> + <entry><literal>@t</literal></entry> + <entry>File modification time</entry> + <entry>Formatted decimal integer, µs since UNIX epoch Jan 1st 1970</entry> + <entry>Only relevant if target resource type chosen as <constant>regular-file</constant></entry> + </row> + + <row> + <entry><literal>@m</literal></entry> + <entry>File access mode</entry> + <entry>Formatted octal integer, in UNIX fashion</entry> + <entry>Only relevant if target resource type chosen as <constant>regular-file</constant></entry> + </row> + + <row> + <entry><literal>@s</literal></entry> + <entry>File size after decompression</entry> + <entry>Formatted decimal integer</entry> + <entry>Useful for measuring progress and to improve partition allocation logic</entry> + </row> + + <row> + <entry><literal>@d</literal></entry> + <entry>Tries done</entry> + <entry>Formatted decimal integer</entry> + <entry>Useful when operating with kernel image files, as per <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot Assessment</ulink></entry> + </row> + + <row> + <entry><literal>@l</literal></entry> + <entry>Tries left</entry> + <entry>Formatted decimal integer</entry> + <entry>Useful when operating with kernel images, as per <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot Assessment</ulink></entry> + </row> + + <row> + <entry><literal>@h</literal></entry> + <entry>SHA256 hash of compressed file</entry> + <entry>64 hexadecimal characters</entry> + <entry>The SHA256 hash of the compressed file; not useful for <constant>url-file</constant> or <constant>url-tar</constant> where the SHA256 hash is already included in the manifest file anyway.</entry> + </row> + </tbody> + </tgroup> + </table> + + <para>Of these wildcards only <literal>@v</literal> must be present in a valid pattern, all other + wildcards are optional. Each wildcard may be used at most once in each pattern. A typical wildcard + matching a file system source image could be <literal>MatchPattern=foobar_@v.raw.xz</literal>, i.e. any file + whose name begins with <literal>foobar_</literal>, followed by a version ID and suffixed by + <literal>.raw.xz</literal>.</para> + + <para>Do not confuse the <literal>@</literal> pattern matching wildcard prefix with the + <literal>%</literal> specifier expansion prefix. The former encapsulate a variable part of a match + pattern string, the latter are simple shortcuts that are expanded while the drop-in files are + parsed. For details about specifiers, see below.</para> + </refsect1> + + <refsect1> + <title>[Transfer] Section Options</title> + + <para>This section defines general properties of this transfer.</para> + + <variablelist> + <varlistentry> + <term><varname>MinVersion=</varname></term> + + <listitem><para>Specifies the minimum version to require for this transfer to take place. If the + source or target patterns in this transfer definition match files older than this version they will + be considered obsolete, and never be considered for the update operation.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>ProtectVersion=</varname></term> + + <listitem><para>Takes one or more version strings to mark as "protected". Protected versions are + never removed while making room for new, updated versions. This is useful to ensure that the + currently booted OS version (or auxiliary resources associated with it) is not replaced/overwritten + during updates, in order to avoid runtime file system corruptions.</para> + + <para>Like many of the settings in these configuration files this setting supports specifier + expansion. It's particularly useful to set this setting to one of the <literal>%A</literal>, + <literal>%B</literal> or <literal>%w</literal> specifiers to automatically refer to the current OS + version of the running system. See below for details on supported specifiers.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>Verify=</varname></term> + + <listitem><para>Takes a boolean, defaults to yes. Controls whether to cryptographically verify + downloaded resources (specifically: validate the GPG signatures for downloaded + <filename>SHA256SUMS</filename> manifest files, via their detached signature files + <filename>SHA256SUMS.gpg</filename> in combination with the system keyring + <filename>/usr/lib/systemd/import-pubring.gpg</filename> or + <filename>/etc/systemd/import-pubring.gpg</filename>).</para> + + <para>This option is essential to provide integrity guarantees for downloaded resources and thus + should be left enabled, outside of test environments.</para> + + <para>Note that the downloaded payload files are unconditionally checked against the SHA256 hashes + listed in the manifest. This option only controls whether the signatures of these manifests are + verified.</para> + + <para>This option only has an effect if the source resource type is selected as + <constant>url-file</constant> or <constant>url-tar</constant>, as integrity and authentication + checking is only available for transfers from remote sources.</para></listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>[Source] Section Options</title> + + <para>This section defines properties of the transfer source:</para> + + <variablelist> + <varlistentry> + <term><varname>Type=</varname></term> + + <listitem><para>Specifies the resource type of the source for the transfer. Takes one of + <constant>url-file</constant>, <constant>url-tar</constant>, <constant>tar</constant>, + <constant>regular-file</constant>, <constant>directory</constant> or + <constant>subvolume</constant>. For details about the resource types, see above. This option is + mandatory.</para> + + <para>Note that only some combinations of source and target resource types are supported, see + above.</para></listitem> + </varlistentry> + </variablelist> + + <variablelist> + <varlistentry> + <term><varname>Path=</varname></term> + + <listitem><para>Specifies where to find source versions of this resource.</para> + + <para>If the source type is selected as <constant>url-file</constant> or + <constant>url-tar</constant> this must be a HTTP/HTTPS URL. The URL is suffixed with + <filename>/SHA256SUMS</filename> to acquire the manifest file, with + <filename>/SHA256SUMS.gpg</filename> to acquire the detached signature file for it, and with the file + names listed in the manifest file in case an update is executed and a resource shall be + downloaded.</para> + + <para>For all other source resource types this must be a local path in the file system, referring to + a local directory to find the versions of this resource in.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>MatchPattern=</varname></term> + + <listitem><para>Specifies one or more file name match patterns that select the subset of files that + are update candidates as source for this transfer. See above for details on match patterns.</para> + + <para>This option is mandatory. Any pattern listed must contain at least the <literal>@v</literal> + wildcard, so that a version identifier may be extracted from the filename. All other wildcards are + optional.</para></listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>[Target] Section Options</title> + + <para>This section defines properties of the transfer target:</para> + + <variablelist> + <varlistentry> + <term><varname>Type=</varname></term> + + <listitem><para>Specifies the resource type of the target for the transfer. Takes one of + <constant>partition</constant>, <constant>regular-file</constant>, <constant>directory</constant> or + <constant>subvolume</constant>. For details about the resource types, see above. This option is + mandatory.</para> + + <para>Note that only some combinations of source and target resource types are supported, see above.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>Path=</varname></term> + + <listitem><para>Specifies a file system path where to look for already installed versions or place + newly downloaded versions of this configured resource. If <varname>Type=</varname> is set to + <constant>partition</constant>, expects a path to a (whole) block device node, or the special string + <literal>auto</literal> in which case the block device the root file system of the currently booted + system is automatically determined and used. If <varname>Type=</varname> is set to + <constant>regular-file</constant>, <constant>directory</constant> or <constant>subvolume</constant>, + must refer to a path in the local file system referencing the directory to find or place the version + files or directories under.</para> + + <para>Note that this mechanism cannot be used to create or remove partitions, in case + <varname>Type=</varname> is set to <constant>partition</constant>. Partitions must exist already, and + a special partition label <literal>_empty</literal> is used to indicate empty partitions. To + automatically generate suitable partitions on first boot, use a tool such as + <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>MatchPattern=</varname></term> + + <listitem><para>Specifies one or more file name or partition label match patterns that select the + subset of files or partitions that are update candidates as targets for this transfer. See above for + details on match patterns.</para> + + <para>This option is mandatory. Any pattern listed must contain at least the <literal>@v</literal> + wildcard, so that a version identifier may be extracted from the filename. All other wildcards are + optional.</para> + + <para>This pattern is both used for matching existing installed versions and for determining the name + of new versions to install. If multiple patterns are specified, the first specified is used for + naming newly installed versions.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>MatchPartitionType=</varname></term> + + <listitem><para>When the target <varname>Type=</varname> is chosen as <constant>partition</constant>, + specifies the GPT partition type to look for. Only partitions of this type are considered, all other + partitions are ignored. If not specified, the GPT partition type <constant>linux-generic</constant> + is used. Accepts either a literal type UUID or a symbolic type identifier. For a list of supported + type identifiers, see the <varname>Type=</varname> setting in + <citerefentry><refentrytitle>repart.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>PartitionUUID=</varname></term> + <term><varname>PartitionFlags=</varname></term> + <term><varname>PartitionNoAuto=</varname></term> + <term><varname>PartitionGrowFileSystem=</varname></term> + + <listitem><para>When the target <varname>Type=</varname> is picked as <constant>partition</constant>, + selects the GPT partition UUID and partition flags to use for the updated partition. Expects a valid + UUID string, a hexadecimal integer, or booleans, respectively. If not set, but the source match + pattern includes wildcards for these fields (i.e. <literal>@u</literal>, <literal>@f</literal>, + <literal>@a</literal>, or <literal>@g</literal>), the values from the patterns are used. If neither + configured with wildcards or these explicit settings, the values are left untouched. If both the + overall <varname>PartitionFlags=</varname> flags setting and the individual flag settings + <varname>PartitionNoAuto=</varname> and <varname>PartitionGrowFileSystem=</varname> are used (or the + wildcards for them), then the latter override the former, i.e. the individual flag bit overrides the + overall flags value. See <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable + Partitions Specification</ulink> for details about these flags.</para> + + <para>Note that these settings are not used for matching, they only have effect on newly written + partitions in case a transfer takes place.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>ReadOnly=</varname></term> + + <listitem><para>Controls whether to mark the resulting file, subvolume or partition read-only. If the + target type is <constant>partition</constant> this controls the ReadOnly partition flag, as per + <ulink url="https://systemd.io/DISCOVERABLE_PARTITIONS">Discoverable Partitions + Specification</ulink>, similar to the <varname>PartitionNoAuto=</varname> and + <varname>PartitionGrowFileSystem=</varname> flags described above. If the target type is + <constant>regular-file</constant>, the writable bit is removed from the access mode. If the the + target type is <constant>subvolume</constant>, the subvolume will be marked read-only as a + whole. Finally, if the target <varname>Type=</varname> is selected as <constant>directory</constant>, + the "immutable" file attribute is set, see <citerefentry + project='man-pages'><refentrytitle>chattr</refentrytitle><manvolnum>1</manvolnum></citerefentry> for + details.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>Mode=</varname></term> + + <listitem><para>The UNIX file access mode to use for newly created files in case the target resource + type is picked as <constant>regular-file</constant>. Expects an octal integer, in typical UNIX + fashion. If not set, but the source match pattern includes a wildcard for this field + (i.e. <literal>@t</literal>), the value from the pattern is used.</para> + + <para>Note that this setting is not used for matching, it only has an effect on newly written + files when a transfer takes place.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>TriesDone=</varname></term> + <term><varname>TriesLeft=</varname></term> + + <listitem><para>These options take positive, decimal integers, and control the number of attempts + done and left for this file. These settings are useful for managing kernel images, following the + scheme defined in <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot + Assessment</ulink>, and only have an effect if the target pattern includes the <literal>@d</literal> + or <literal>@l</literal> wildcards.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>InstancesMax=</varname></term> + + <listitem><para>Takes a decimal integer equal to or greater than 2. This configures how many concurrent + versions of the resource to keep. Whenever a new update is initiated it is made sure that no more + than the number of versions specified here minus one exist in the target. Any excess versions are + deleted (in case the target <varname>Type=</varname> of <constant>regular-file</constant>, + <constant>directory</constant>, <constant>subvolume</constant> is used) or emptied (in case the + target <varname>Type=</varname> of <constant>partition</constant> is used; emptying in this case + simply means to set the partition label to the special string <literal>_empty</literal>; note that no + partitions are actually removed). After an update is completed the number of concurrent versions of + the target resources is equal to or below the number specified here.</para> + + <para>Note that this setting may be set differently for each transfer. However, it generally is + advisable to keep this setting the same for all transfers, since otherwise incomplete combinations of + files or partitions will be left installed.</para> + + <para>If the target <varname>Type=</varname> is selected as <constant>partition</constant>, the number + of concurrent versions to keep is additionally restricted by the number of partition slots of the + right type in the partition table. i.e. if there are only 2 partition slots for the selected + partition type, setting this value larger than 2 is without effect, since no more than 2 concurrent + versions could be stored in the image anyway.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>RemoveTemporary=</varname></term> + + <listitem><para>Takes a boolean argument. If this option is enabled (which is the default) before + initiating an update, all left-over, incomplete updates from a previous attempt are removed from the + target directory. This only has an effect if the target resource <varname>Type=</varname> is selected + as <constant>regular-file</constant>, <constant>directory</constant> or + <constant>subvolume</constant>.</para></listitem> + </varlistentry> + + <varlistentry> + <term><varname>CurrentSymlink=</varname></term> + + <listitem><para>Takes a symlink name as argument. If this option is used, as the last step of the + update a symlink under the specified name is created/updated pointing to the completed update. This + is useful in to provide a stable name always pointing to the newest version of the resource. This is + only supported if the target resource <varname>Type=</varname> is selected as + <constant>regular-file</constant>, <constant>directory</constant> or + <constant>subvolume</constant>.</para></listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Specifiers</title> + + <para>Specifiers may be used in the <varname>MinVersion=</varname>, <varname>ProtectVersion=</varname>, + <varname>Path=</varname>, <varname>MatchPattern=</varname> and <varname>CurrentSymlink=</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> + + <para>Do not confuse the <literal>%</literal> specifier expansion prefix with the <literal>@</literal> + pattern matching wildcard prefix. The former are simple shortcuts that are expanded while the drop-in + files are parsed, the latter encapsulate a variable part of a match pattern string. For details about + pattern matching wildcards, see above.</para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <example> + <title>Updates for a Verity Enabled Secure OS</title> + + <para>With the following three files we define a root file system partition, a matching Verity + 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 +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=https://download.example.com/ +MatchPattern=foobarOS_@v_@u.verity.xz + +[Target] +Type=partition +Path=auto +MatchPattern=foobarOS_@v_verity +MatchPartitionType=root-verity +PartitionFlags=0 +PartitionReadOnly=1</programlisting></para> + + <para>The above defines the update mechanism for the Verity partition of the root file system. Verity + partition images are downloaded from + <literal>https://download.example.com/foobarOS_@v_@u.verity.xz</literal> and written to a suitable + local partition, which is marked read-only. Under the assumption this update is run from the image + itself the current image version (i.e. the <literal>%A</literal> specifier) is marked as protected, to + ensure it is not corrupted while booted. Note that the partition UUID for the target partition is + encoded in the source file name. Fixating the partition UUID can be useful to ensure that + <literal>roothash=</literal> on the kernel command line is sufficient to pinpoint both the Verity and + root file system partition, and also encode the Verity root level hash (under the assumption the UUID + in the file names match their top-level hash, the way + <citerefentry><refentrytitle>systemd-gpt-auto-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry> + suggests).</para> + + <para><programlisting># /usr/lib/sysupdate.d/60-root.conf +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=https://download.example.com/ +MatchPattern=foobarOS_@v_@u.root.xz + +[Target] +Type=partition +Path=auto +MatchPattern=foobarOS_@v +MatchPartitionType=root +PartitionFlags=0 +PartitionReadOnly=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 +[Transfer] +ProtectVersion=%A + +[Source] +Type=url-file +Path=https://download.example.com/ +MatchPattern=foobarOS_@v.efi.xz + +[Target] +Type=file +Path=/efi/EFI/Linux +MatchPattern=foobarOS_@v+@l-@d.efi \ + foobarOS_@v+@l.efi \ + foobarOS_@v.efi +Mode=0444 +TriesLeft=3 +TriesDone=0 +InstancesMax=2</programlisting></para> + + <para>The above installs a unified kernel image into the ESP (which is mounted to + <filename>/efi/</filename>), as per <ulink url="https://systemd.io/BOOT_LOADER_SPECIFICATION">Boot + Loader Specification</ulink> Type #2. This defines three possible patterns for the names of the + kernel images images, as per <ulink url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot + Assessment</ulink>, and ensures when installing new kernels, they are set up with 3 tries left. No + more than two parallel kernels are kept.</para> + + <para>With this setup the web server would serve the following files, for a hypothetical version 7 of + the OS:</para> + + <itemizedlist> + <listitem><para><filename>SHA256SUMS</filename> – The manifest file, containing available files and their SHA256 hashes</para></listitem> + <listitem><para><filename>SHA256SUMS.gpg</filename> – The detached cryptographic signature for the manifest file</para></listitem> + <listitem><para><filename>foobarOS_7_8b8186b1-2b4e-4eb6-ad39-8d4d18d2a8fb.verity.xz</filename> – The Verity image for version 7</para></listitem> + <listitem><para><filename>foobarOS_7_f4d1234f-3ebf-47c4-b31d-4052982f9a2f.root.xz</filename> – The root file system image for version 7</para></listitem> + <listitem><para><filename>foobarOS_7_efi.xz</filename> – The unified kernel image for version 7</para></listitem> + </itemizedlist> + + <para>For each new OS release a new set of the latter three files would be added, each time with an + updated version. The <filename>SHA256SUMS</filename> manifest should then be updated accordingly, + listing all files for all versions that shall be offered for download.</para> + </example> + + <example> + <title>Updates for Plain Directory Container Image</title> + + <para><programlisting> +[Source] +Type=url-tar +Path=https://download.example.com/ +MatchPattern=myContainer_@v.tar.gz + +[Target] +Type=subvolume +Path=/var/lib/machines +MatchPattern=myContainer_@v +CurrentSymlink=myContainer</programlisting></para> + + <para>On updates this downloads <literal>https://download.example.com/myContainer_@v.tar.gz</literal> + and decompresses/unpacks it to <filename>/var/lib/machines/myContainer_@v</filename>. After each update + a symlink <filename>/var/lib/machines/myContainer</filename> is created/updated always pointing to the + most recent update.</para> + </example> + </refsect1> + + <refsect1> + <title>See Also</title> + <para> + <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>, + <citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>, + <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry> + </para> + </refsect1> + +</refentry> diff --git a/meson.build b/meson.build index 107192b211..05dcc79cfa 100644 --- a/meson.build +++ b/meson.build @@ -1644,6 +1644,18 @@ conf.set('DEFAULT_DNSSEC_MODE', 'DNSSEC_' + default_dnssec.underscorify().to_upper()) conf.set_quoted('DEFAULT_DNSSEC_MODE_STR', default_dnssec) +want_sysupdate = get_option('sysupdate') +if want_sysupdate != 'false' + have = (conf.get('HAVE_OPENSSL') == 1 and + conf.get('HAVE_LIBFDISK') == 1) + if want_sysupdate == 'true' and not have + error('sysupdate support was requested, but dependencies are not available') + endif +else + have = false +endif +conf.set10('ENABLE_SYSUPDATE', have) + want_importd = get_option('importd') if want_importd != 'false' have = (conf.get('HAVE_LIBCURL') == 1 and @@ -2006,6 +2018,7 @@ subdir('src/rpm') subdir('src/shutdown') subdir('src/sysext') subdir('src/systemctl') +subdir('src/sysupdate') subdir('src/timedate') subdir('src/timesync') subdir('src/tmpfiles') @@ -3074,6 +3087,22 @@ if conf.get('ENABLE_REPART') == 1 endif endif +if conf.get('ENABLE_SYSUPDATE') == 1 + exe = executable( + 'systemd-sysupdate', + systemd_sysupdate_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libblkid, + libfdisk, + libopenssl], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) + public_programs += exe +endif + if conf.get('ENABLE_VCONSOLE') == 1 executable( 'systemd-vconsole-setup', @@ -4117,6 +4146,7 @@ foreach tuple : [ ['rfkill'], ['sysext'], ['systemd-analyze', conf.get('ENABLE_ANALYZE') == 1], + ['sysupdate'], ['sysusers'], ['timedated'], ['timesyncd'], diff --git a/meson_options.txt b/meson_options.txt index 284109cadf..27cfa9b697 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -100,6 +100,8 @@ option('binfmt', type : 'boolean', description : 'support for custom binary formats') option('repart', type : 'combo', choices : ['auto', 'true', 'false'], description : 'install the systemd-repart tool') +option('sysupdate', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'install the systemd-sysupdate tool') option('coredump', type : 'boolean', description : 'install the coredump handler') option('pstore', type : 'boolean', diff --git a/src/basic/strv.h b/src/basic/strv.h index 985499272f..bc76a2861c 100644 --- a/src/basic/strv.h +++ b/src/basic/strv.h @@ -133,8 +133,8 @@ bool strv_overlap(char * const *a, char * const *b) _pure_; size_t _len = strv_length(h); \ _len > 0 ? h + _len - 1 : NULL; \ }); \ - i && (s = i) >= h; \ - i--) + (s = i); \ + i > h ? i-- : (i = NULL)) #define STRV_FOREACH_BACKWARDS(s, l) \ _STRV_FOREACH_BACKWARDS(s, l, UNIQ_T(h, UNIQ), UNIQ_T(i, UNIQ)) diff --git a/src/sysupdate/meson.build b/src/sysupdate/meson.build new file mode 100644 index 0000000000..2b1a256026 --- /dev/null +++ b/src/sysupdate/meson.build @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_sysupdate_sources = files(''' + sysupdate-instance.c + sysupdate-instance.h + sysupdate-partition.c + sysupdate-partition.h + sysupdate-pattern.c + sysupdate-pattern.h + sysupdate-resource.c + sysupdate-resource.h + sysupdate-transfer.c + sysupdate-transfer.h + sysupdate-update-set.c + sysupdate-update-set.h + sysupdate-util.c + sysupdate-util.h + sysupdate-cache.c + sysupdate-cache.h + sysupdate.c + sysupdate.h +'''.split()) diff --git a/src/sysupdate/sysupdate-cache.c b/src/sysupdate/sysupdate-cache.c new file mode 100644 index 0000000000..8dad3ee479 --- /dev/null +++ b/src/sysupdate/sysupdate-cache.c @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "memory-util.h" +#include "sysupdate-cache.h" + +#define WEB_CACHE_ENTRIES_MAX 64U +#define WEB_CACHE_ITEM_SIZE_MAX (64U*1024U*1024U) + +static WebCacheItem* web_cache_item_free(WebCacheItem *i) { + if (!i) + return NULL; + + free(i->url); + return mfree(i); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(WebCacheItem*, web_cache_item_free); + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(web_cache_hash_ops, char, string_hash_func, string_compare_func, WebCacheItem, web_cache_item_free); + +int web_cache_add_item( + Hashmap **web_cache, + const char *url, + bool verified, + const void *data, + size_t size) { + + _cleanup_(web_cache_item_freep) WebCacheItem *item = NULL; + _cleanup_free_ char *u = NULL; + int r; + + assert(web_cache); + assert(url); + assert(data || size == 0); + + if (size > WEB_CACHE_ITEM_SIZE_MAX) + return -E2BIG; + + item = web_cache_get_item(*web_cache, url, verified); + if (item && memcmp_nn(item->data, item->size, data, size) == 0) + return 0; + + if (hashmap_size(*web_cache) >= (size_t) (WEB_CACHE_ENTRIES_MAX + !!hashmap_get(*web_cache, url))) + return -ENOSPC; + + r = hashmap_ensure_allocated(web_cache, &web_cache_hash_ops); + if (r < 0) + return r; + + u = strdup(url); + if (!u) + return -ENOMEM; + + item = malloc(offsetof(WebCacheItem, data) + size + 1); + if (!item) + return -ENOMEM; + + *item = (WebCacheItem) { + .url = TAKE_PTR(u), + .size = size, + .verified = verified, + }; + + /* Just to be extra paranoid, let's NUL terminate the downloaded buffer */ + *(uint8_t*) mempcpy(item->data, data, size) = 0; + + web_cache_item_free(hashmap_remove(*web_cache, url)); + + r = hashmap_put(*web_cache, item->url, item); + if (r < 0) + return r; + + TAKE_PTR(item); + return 1; +} + +WebCacheItem* web_cache_get_item(Hashmap *web_cache, const char *url, bool verified) { + WebCacheItem *i; + + i = hashmap_get(web_cache, url); + if (!i) + return NULL; + + if (i->verified != verified) + return NULL; + + return i; +} diff --git a/src/sysupdate/sysupdate-cache.h b/src/sysupdate/sysupdate-cache.h new file mode 100644 index 0000000000..d6a7897399 --- /dev/null +++ b/src/sysupdate/sysupdate-cache.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "hashmap.h" + +typedef struct WebCacheItem { + char *url; + bool verified; + size_t size; + uint8_t data[]; +} WebCacheItem; + +/* A simple in-memory cache for downloaded manifests. Very likely multiple transfers will use the same + * manifest URLs, hence let's make sure we only download them once within each sysupdate invocation. */ + +int web_cache_add_item(Hashmap **cache, const char *url, bool verified, const void *data, size_t size); + +WebCacheItem* web_cache_get_item(Hashmap *cache, const char *url, bool verified); diff --git a/src/sysupdate/sysupdate-instance.c b/src/sysupdate/sysupdate-instance.c new file mode 100644 index 0000000000..16bfab912f --- /dev/null +++ b/src/sysupdate/sysupdate-instance.c @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <sys/stat.h> + +#include "sysupdate-instance.h" + +void instance_metadata_destroy(InstanceMetadata *m) { + assert(m); + free(m->version); +} + +int instance_new( + Resource *rr, + const char *path, + const InstanceMetadata *f, + Instance **ret) { + + _cleanup_(instance_freep) Instance *i = NULL; + _cleanup_free_ char *p = NULL, *v = NULL; + + assert(rr); + assert(path); + assert(f); + assert(f->version); + assert(ret); + + p = strdup(path); + if (!p) + return log_oom(); + + v = strdup(f->version); + if (!v) + return log_oom(); + + i = new(Instance, 1); + if (!i) + return log_oom(); + + *i = (Instance) { + .resource = rr, + .metadata = *f, + .path = TAKE_PTR(p), + .partition_info = PARTITION_INFO_NULL, + }; + + i->metadata.version = TAKE_PTR(v); + + *ret = TAKE_PTR(i); + return 0; +} + +Instance *instance_free(Instance *i) { + if (!i) + return NULL; + + instance_metadata_destroy(&i->metadata); + + free(i->path); + partition_info_destroy(&i->partition_info); + + return mfree(i); +} diff --git a/src/sysupdate/sysupdate-instance.h b/src/sysupdate/sysupdate-instance.h new file mode 100644 index 0000000000..2860d295da --- /dev/null +++ b/src/sysupdate/sysupdate-instance.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-id128.h" + +#include "fs-util.h" +#include "time-util.h" + +typedef struct InstanceMetadata InstanceMetadata; +typedef struct Instance Instance; + +#include "sysupdate-resource.h" +#include "sysupdate-partition.h" + +struct InstanceMetadata { + /* Various bits of metadata for each instance, that is either derived from the filename/GPT label or + * from metadata of the file/partition itself */ + char *version; + sd_id128_t partition_uuid; + bool partition_uuid_set; + uint64_t partition_flags; /* GPT partition flags */ + bool partition_flags_set; + usec_t mtime; + mode_t mode; + uint64_t size; /* uncompressed size of the file */ + uint64_t tries_done, tries_left; /* for boot assessment counters */ + int no_auto; + int read_only; + int growfs; + uint8_t sha256sum[32]; /* SHA256 sum of the download (i.e. compressed) file */ + bool sha256sum_set; +}; + +#define INSTANCE_METADATA_NULL \ + { \ + .mtime = USEC_INFINITY, \ + .mode = MODE_INVALID, \ + .size = UINT64_MAX, \ + .tries_done = UINT64_MAX, \ + .tries_left = UINT64_MAX, \ + .no_auto = -1, \ + .read_only = -1, \ + .growfs = -1, \ + } + +struct Instance { + /* A pointer back to the resource this belongs to */ + Resource *resource; + + /* Metadata of this version */ + InstanceMetadata metadata; + + /* Where we found the instance */ + char *path; + PartitionInfo partition_info; +}; + +void instance_metadata_destroy(InstanceMetadata *m); + +int instance_new(Resource *rr, const char *path, const InstanceMetadata *f, Instance **ret); +Instance *instance_free(Instance *i); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Instance*, instance_free); diff --git a/src/sysupdate/sysupdate-partition.c b/src/sysupdate/sysupdate-partition.c new file mode 100644 index 0000000000..f3e21001e4 --- /dev/null +++ b/src/sysupdate/sysupdate-partition.c @@ -0,0 +1,379 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/file.h> + +#include "alloc-util.h" +#include "extract-word.h" +#include "gpt.h" +#include "id128-util.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "sysupdate-partition.h" +#include "util.h" + +void partition_info_destroy(PartitionInfo *p) { + assert(p); + + p->label = mfree(p->label); + p->device = mfree(p->device); +} + +static int fdisk_partition_get_attrs_as_uint64( + struct fdisk_partition *pa, + uint64_t *ret) { + + uint64_t flags = 0; + const char *a; + int r; + + assert(pa); + assert(ret); + + /* Retrieve current flags as uint64_t mask */ + + a = fdisk_partition_get_attrs(pa); + if (!a) { + *ret = 0; + return 0; + } + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&a, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + break; + + if (streq(word, "RequiredPartition")) + flags |= GPT_FLAG_REQUIRED_PARTITION; + else if (streq(word, "NoBlockIOProtocol")) + flags |= GPT_FLAG_NO_BLOCK_IO_PROTOCOL; + else if (streq(word, "LegacyBIOSBootable")) + flags |= GPT_FLAG_LEGACY_BIOS_BOOTABLE; + else { + const char *e; + unsigned u; + + /* Drop "GUID" prefix if specified */ + e = startswith(word, "GUID:") ?: word; + + if (safe_atou(e, &u) < 0) { + log_debug("Unknown partition flag '%s', ignoring.", word); + continue; + } + + if (u >= sizeof(flags)*8) { /* partition flags on GPT are 64bit. Let's ignore any further + bits should libfdisk report them */ + log_debug("Partition flag above bit 63 (%s), ignoring.", word); + continue; + } + + flags |= UINT64_C(1) << u; + } + } + + *ret = flags; + return 0; +} + +static int fdisk_partition_set_attrs_as_uint64( + struct fdisk_partition *pa, + uint64_t flags) { + + _cleanup_free_ char *attrs = NULL; + int r; + + assert(pa); + + for (unsigned i = 0; i < sizeof(flags) * 8; i++) { + if (!FLAGS_SET(flags, UINT64_C(1) << i)) + continue; + + r = strextendf_with_separator(&attrs, ",", "%u", i); + if (r < 0) + return r; + } + + return fdisk_partition_set_attrs(pa, strempty(attrs)); +} + +int read_partition_info( + struct fdisk_context *c, + struct fdisk_table *t, + size_t i, + PartitionInfo *ret) { + + _cleanup_free_ char *label_copy = NULL, *device = NULL; + const char *pts, *ids, *label; + struct fdisk_partition *p; + struct fdisk_parttype *pt; + uint64_t start, size, flags; + sd_id128_t ptid, id; + size_t partno; + int r; + + assert(c); + assert(t); + assert(ret); + + p = fdisk_table_get_partition(t, i); + if (!p) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m"); + + if (fdisk_partition_is_used(p) <= 0) { + *ret = (PartitionInfo) PARTITION_INFO_NULL; + return 0; /* not found! */ + } + + if (fdisk_partition_has_partno(p) <= 0 || + fdisk_partition_has_start(p) <= 0 || + fdisk_partition_has_size(p) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a number, position or size."); + + partno = fdisk_partition_get_partno(p); + + start = fdisk_partition_get_start(p); + assert(start <= UINT64_MAX / 512U); + start *= 512U; + + size = fdisk_partition_get_size(p); + assert(size <= UINT64_MAX / 512U); + size *= 512U; + + label = fdisk_partition_get_name(p); + if (!label) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a label."); + + pt = fdisk_partition_get_type(p); + if (!pt) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire type of partition: %m"); + + pts = fdisk_parttype_get_string(pt); + if (!pts) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire type of partition as string: %m"); + + r = sd_id128_from_string(pts, &ptid); + if (r < 0) + return log_error_errno(r, "Failed to parse partition type UUID %s: %m", pts); + + ids = fdisk_partition_get_uuid(p); + if (!ids) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found a partition without a UUID."); + + r = sd_id128_from_string(ids, &id); + if (r < 0) + return log_error_errno(r, "Failed to parse partition UUID %s: %m", ids); + + r = fdisk_partition_get_attrs_as_uint64(p, &flags); + if (r < 0) + return log_error_errno(r, "Failed to get partition flags: %m"); + + r = fdisk_partition_to_string(p, c, FDISK_FIELD_DEVICE, &device); + if (r != 0) + return log_error_errno(r, "Failed to get partition device name: %m"); + + label_copy = strdup(label); + if (!label_copy) + return log_oom(); + + *ret = (PartitionInfo) { + .partno = partno, + .start = start, + .size = size, + .flags = flags, + .type = ptid, + .uuid = id, + .label = TAKE_PTR(label_copy), + .device = TAKE_PTR(device), + .no_auto = FLAGS_SET(flags, GPT_FLAG_NO_AUTO) && gpt_partition_type_knows_no_auto(ptid), + .read_only = FLAGS_SET(flags, GPT_FLAG_READ_ONLY) && gpt_partition_type_knows_read_only(ptid), + .growfs = FLAGS_SET(flags, GPT_FLAG_GROWFS) && gpt_partition_type_knows_growfs(ptid), + }; + + return 1; /* found! */ +} + +int find_suitable_partition( + const char *device, + uint64_t space, + sd_id128_t *partition_type, + PartitionInfo *ret) { + + _cleanup_(partition_info_destroy) PartitionInfo smallest = PARTITION_INFO_NULL; + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; + size_t n_partitions; + int r; + + assert(device); + assert(ret); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + r = fdisk_assign_device(c, device, /* readonly= */ true); + if (r < 0) + return log_error_errno(r, "Failed to open device '%s': %m", device); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device); + + r = fdisk_get_partitions(c, &t); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition table: %m"); + + n_partitions = fdisk_table_get_nents(t); + for (size_t i = 0; i < n_partitions; i++) { + _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL; + + r = read_partition_info(c, t, i, &pinfo); + if (r < 0) + return r; + if (r == 0) /* not assigned */ + continue; + + /* Filter out non-matching partition types */ + if (partition_type && !sd_id128_equal(pinfo.type, *partition_type)) + continue; + + if (!streq_ptr(pinfo.label, "_empty")) /* used */ + continue; + + if (space != UINT64_MAX && pinfo.size < space) /* too small */ + continue; + + if (smallest.partno != SIZE_MAX && smallest.size <= pinfo.size) /* already found smaller */ + continue; + + smallest = pinfo; + pinfo = (PartitionInfo) PARTITION_INFO_NULL; + } + + if (smallest.partno == SIZE_MAX) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), "No available partition of a suitable size found."); + + *ret = smallest; + smallest = (PartitionInfo) PARTITION_INFO_NULL; + + return 0; +} + +int patch_partition( + const char *device, + const PartitionInfo *info, + PartitionChange change) { + + _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *pa = NULL; + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + bool tweak_no_auto, tweak_read_only, tweak_growfs; + int r, fd; + + assert(device); + assert(info); + assert(change <= _PARTITION_CHANGE_MAX); + + if (change == 0) /* Nothing to do */ + return 0; + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + r = fdisk_assign_device(c, device, /* readonly= */ false); + if (r < 0) + return log_error_errno(r, "Failed to open device '%s': %m", device); + + assert_se((fd = fdisk_get_devfd(c)) >= 0); + + /* Make sure udev doesn't read the device while we make changes (this lock is released automatically + * by the kernel when the fd is closed, i.e. when the fdisk context is freed, hence no explicit + * unlock by us here anywhere.) */ + if (flock(fd, LOCK_EX) < 0) + return log_error_errno(errno, "Failed to lock block device '%s': %m", device); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", device); + + r = fdisk_get_partition(c, info->partno, &pa); + if (r < 0) + return log_error_errno(r, "Failed to read partition %zu of GPT label of '%s': %m", info->partno, device); + + if (change & PARTITION_LABEL) { + r = fdisk_partition_set_name(pa, info->label); + if (r < 0) + return log_error_errno(r, "Failed to update partition label: %m"); + } + + if (change & PARTITION_UUID) { + r = fdisk_partition_set_uuid(pa, SD_ID128_TO_UUID_STRING(info->uuid)); + if (r < 0) + return log_error_errno(r, "Failed to update partition UUID: %m"); + } + + /* Tweak the read-only flag, but only if supported by the partition type */ + tweak_no_auto = + FLAGS_SET(change, PARTITION_NO_AUTO) && + gpt_partition_type_knows_no_auto(info->type); + tweak_read_only = + FLAGS_SET(change, PARTITION_READ_ONLY) && + gpt_partition_type_knows_read_only(info->type); + tweak_growfs = + FLAGS_SET(change, PARTITION_GROWFS) && + gpt_partition_type_knows_growfs(info->type); + + if (change & PARTITION_FLAGS) { + uint64_t flags; + + /* Update the full flags parameter, and import the read-only flag into it */ + + flags = info->flags; + if (tweak_no_auto) + SET_FLAG(flags, GPT_FLAG_NO_AUTO, info->no_auto); + if (tweak_read_only) + SET_FLAG(flags, GPT_FLAG_READ_ONLY, info->read_only); + if (tweak_growfs) + SET_FLAG(flags, GPT_FLAG_GROWFS, info->growfs); + + r = fdisk_partition_set_attrs_as_uint64(pa, flags); + if (r < 0) + return log_error_errno(r, "Failed to update partition flags: %m"); + + } else if (tweak_no_auto || tweak_read_only || tweak_growfs) { + uint64_t old_flags, new_flags; + + /* So we aren't supposed to update the full flags parameter, but we are supposed to update + * the RO flag of it. */ + + r = fdisk_partition_get_attrs_as_uint64(pa, &old_flags); + if (r < 0) + return log_error_errno(r, "Failed to get old partition flags: %m"); + + new_flags = old_flags; + if (tweak_no_auto) + SET_FLAG(new_flags, GPT_FLAG_NO_AUTO, info->no_auto); + if (tweak_read_only) + SET_FLAG(new_flags, GPT_FLAG_READ_ONLY, info->read_only); + if (tweak_growfs) + SET_FLAG(new_flags, GPT_FLAG_GROWFS, info->growfs); + + if (new_flags != old_flags) { + r = fdisk_partition_set_attrs_as_uint64(pa, new_flags); + if (r < 0) + return log_error_errno(r, "Failed to update partition flags: %m"); + } + } + + r = fdisk_set_partition(c, info->partno, pa); + if (r < 0) + return log_error_errno(r, "Failed to update partition: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write updated partition table: %m"); + + return 0; +} diff --git a/src/sysupdate/sysupdate-partition.h b/src/sysupdate/sysupdate-partition.h new file mode 100644 index 0000000000..672eb93e90 --- /dev/null +++ b/src/sysupdate/sysupdate-partition.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <sys/types.h> + +#include "sd-id128.h" + +#include "fdisk-util.h" +#include "macro.h" + +typedef struct PartitionInfo PartitionInfo; + +typedef enum PartitionChange { + PARTITION_FLAGS = 1 << 0, + PARTITION_NO_AUTO = 1 << 1, + PARTITION_READ_ONLY = 1 << 2, + PARTITION_GROWFS = 1 << 3, + PARTITION_UUID = 1 << 4, + PARTITION_LABEL = 1 << 5, + _PARTITION_CHANGE_MAX = (1 << 6) - 1, /* all of the above */ + _PARTITION_CHANGE_INVALID = -EINVAL, +} PartitionChange; + +struct PartitionInfo { + size_t partno; + uint64_t start, size; + uint64_t flags; + sd_id128_t type, uuid; + char *label; + char *device; /* Note that this might point to some non-existing path in case we operate on a loopback file */ + bool no_auto:1; + bool read_only:1; + bool growfs:1; +}; + +#define PARTITION_INFO_NULL \ + { \ + .partno = SIZE_MAX, \ + .start = UINT64_MAX, \ + .size = UINT64_MAX, \ + } + +void partition_info_destroy(PartitionInfo *p); + +int read_partition_info(struct fdisk_context *c, struct fdisk_table *t, size_t i, PartitionInfo *ret); + +int find_suitable_partition(const char *device, uint64_t space, sd_id128_t *partition_type, PartitionInfo *ret); +int patch_partition(const char *device, const PartitionInfo *info, PartitionChange change); diff --git a/src/sysupdate/sysupdate-pattern.c b/src/sysupdate/sysupdate-pattern.c new file mode 100644 index 0000000000..c9228687f7 --- /dev/null +++ b/src/sysupdate/sysupdate-pattern.c @@ -0,0 +1,602 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "hexdecoct.h" +#include "list.h" +#include "parse-util.h" +#include "path-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "sysupdate-pattern.h" +#include "sysupdate-util.h" + +typedef enum PatternElementType { + PATTERN_LITERAL, + PATTERN_VERSION, + PATTERN_PARTITION_UUID, + PATTERN_PARTITION_FLAGS, + PATTERN_MTIME, + PATTERN_MODE, + PATTERN_SIZE, + PATTERN_TRIES_DONE, + PATTERN_TRIES_LEFT, + PATTERN_NO_AUTO, + PATTERN_READ_ONLY, + PATTERN_GROWFS, + PATTERN_SHA256SUM, + _PATTERN_ELEMENT_TYPE_MAX, + _PATTERN_ELEMENT_TYPE_INVALID = -EINVAL, +} PatternElementType; + +typedef struct PatternElement PatternElement; + +struct PatternElement { + PatternElementType type; + LIST_FIELDS(PatternElement, elements); + char literal[]; +}; + +static PatternElement *pattern_element_free_all(PatternElement *e) { + PatternElement *p; + + while ((p = LIST_POP(elements, e))) + free(p); + + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(PatternElement*, pattern_element_free_all); + +static PatternElementType pattern_element_type_from_char(char c) { + switch (c) { + case 'v': + return PATTERN_VERSION; + case 'u': + return PATTERN_PARTITION_UUID; + case 'f': + return PATTERN_PARTITION_FLAGS; + case 't': + return PATTERN_MTIME; + case 'm': + return PATTERN_MODE; + case 's': + return PATTERN_SIZE; + case 'd': + return PATTERN_TRIES_DONE; + case 'l': + return PATTERN_TRIES_LEFT; + case 'a': + return PATTERN_NO_AUTO; + case 'r': + return PATTERN_READ_ONLY; + case 'g': + return PATTERN_GROWFS; + case 'h': + return PATTERN_SHA256SUM; + default: + return _PATTERN_ELEMENT_TYPE_INVALID; + } +} + +static bool valid_char(char x) { + + /* Let's refuse control characters here, and let's reserve some characters typically used in pattern + * languages so that we can use them later, possibly. */ + + if ((unsigned) x < ' ' || x >= 127) + return false; + + return !IN_SET(x, '$', '*', '?', '[', ']', '!', '\\', '/', '|'); +} + +static int pattern_split( + const char *pattern, + PatternElement **ret) { + + _cleanup_(pattern_element_free_allp) PatternElement *first = NULL; + bool at = false, last_literal = true; + PatternElement *last = NULL; + uint64_t mask_found = 0; + size_t l, k = 0; + + assert(pattern); + + l = strlen(pattern); + + for (const char *e = pattern; *e != 0; e++) { + if (*e == '@') { + if (!at) { + at = true; + continue; + } + + /* Two at signs in a sequence, write out one */ + at = false; + + } else if (at) { + PatternElementType t; + uint64_t bit; + + t = pattern_element_type_from_char(*e); + if (t < 0) + return log_debug_errno(t, "Unknown pattern field marker '@%c'.", *e); + + bit = UINT64_C(1) << t; + if (mask_found & bit) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Pattern field marker '@%c' appears twice in pattern.", *e); + + /* We insist that two pattern field markers are separated by some literal string that + * we can use to separate the fields when parsing. */ + if (!last_literal) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Found two pattern field markers without separating literal."); + + if (ret) { + PatternElement *z; + + z = malloc(offsetof(PatternElement, literal)); + if (!z) + return -ENOMEM; + + z->type = t; + LIST_INSERT_AFTER(elements, first, last, z); + last = z; + } + + mask_found |= bit; + last_literal = at = false; + continue; + } + + if (!valid_char(*e)) + return log_debug_errno(SYNTHETIC_ERRNO(EBADRQC), "Invalid character 0x%0x in pattern, refusing.", *e); + + last_literal = true; + + if (!ret) + continue; + + if (!last || last->type != PATTERN_LITERAL) { + PatternElement *z; + + z = malloc0(offsetof(PatternElement, literal) + l + 1); /* l is an upper bound to all literal elements */ + if (!z) + return -ENOMEM; + + z->type = PATTERN_LITERAL; + k = 0; + + LIST_INSERT_AFTER(elements, first, last, z); + last = z; + } + + assert(last); + assert(last->type == PATTERN_LITERAL); + + last->literal[k++] = *e; + } + + if (at) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Trailing @ character found, refusing."); + if (!(mask_found & (UINT64_C(1) << PATTERN_VERSION))) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Version field marker '@v' not specified in pattern, refusing."); + + if (ret) + *ret = TAKE_PTR(first); + + return 0; +} + +int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret) { + _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL; + _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL; + const char *p; + int r; + + assert(pattern); + assert(s); + + r = pattern_split(pattern, &elements); + if (r < 0) + return r; + + p = s; + LIST_FOREACH(elements, e, elements) { + _cleanup_free_ char *t = NULL; + const char *n; + + if (e->type == PATTERN_LITERAL) { + const char *k; + + /* Skip literal fields */ + k = startswith(p, e->literal); + if (!k) + goto nope; + + p = k; + continue; + } + + if (e->elements_next) { + /* The next element must be literal, as we use it to determine where to split */ + assert(e->elements_next->type == PATTERN_LITERAL); + + n = strstr(p, e->elements_next->literal); + if (!n) + goto nope; + + } else + /* End of the string */ + assert_se(n = strchr(p, 0)); + t = strndup(p, n - p); + if (!t) + return -ENOMEM; + + switch (e->type) { + + case PATTERN_VERSION: + if (!version_is_valid(t)) { + log_debug("Version string is not valid, refusing: %s", t); + goto nope; + } + + assert(!found.version); + found.version = TAKE_PTR(t); + break; + + case PATTERN_PARTITION_UUID: { + sd_id128_t id; + + if (sd_id128_from_string(t, &id) < 0) + goto nope; + + assert(!found.partition_uuid_set); + found.partition_uuid = id; + found.partition_uuid_set = true; + break; + } + + case PATTERN_PARTITION_FLAGS: { + uint64_t f; + + if (safe_atoux64(t, &f) < 0) + goto nope; + + if (found.partition_flags_set && found.partition_flags != f) + goto nope; + + assert(!found.partition_flags_set); + found.partition_flags = f; + found.partition_flags_set = true; + break; + } + + case PATTERN_MTIME: { + uint64_t v; + + if (safe_atou64(t, &v) < 0) + goto nope; + if (v == USEC_INFINITY) /* Don't permit our internal special infinity value */ + goto nope; + if (v / 1000000U > TIME_T_MAX) /* Make sure this fits in a timespec structure */ + goto nope; + + assert(found.mtime == USEC_INFINITY); + found.mtime = v; + break; + } + + case PATTERN_MODE: { + mode_t m; + + r = parse_mode(t, &m); + if (r < 0) + goto nope; + if (m & ~0775) /* Don't allow world-writable files or suid files to be generated this way */ + goto nope; + + assert(found.mode == MODE_INVALID); + found.mode = m; + break; + } + + case PATTERN_SIZE: { + uint64_t u; + + r = safe_atou64(t, &u); + if (r < 0) + goto nope; + if (u == UINT64_MAX) + goto nope; + + assert(found.size == UINT64_MAX); + found.size = u; + break; + } + + case PATTERN_TRIES_DONE: { + uint64_t u; + + r = safe_atou64(t, &u); + if (r < 0) + goto nope; + if (u == UINT64_MAX) + goto nope; + + assert(found.tries_done == UINT64_MAX); + found.tries_done = u; + break; + } + + case PATTERN_TRIES_LEFT: { + uint64_t u; + + r = safe_atou64(t, &u); + if (r < 0) + goto nope; + if (u == UINT64_MAX) + goto nope; + + assert(found.tries_left == UINT64_MAX); + found.tries_left = u; + break; + } + + case PATTERN_NO_AUTO: + r = parse_boolean(t); + if (r < 0) + goto nope; + + assert(found.no_auto < 0); + found.no_auto = r; + break; + + case PATTERN_READ_ONLY: + r = parse_boolean(t); + if (r < 0) + goto nope; + + assert(found.read_only < 0); + found.read_only = r; + break; + + case PATTERN_GROWFS: + r = parse_boolean(t); + if (r < 0) + goto nope; + + assert(found.growfs < 0); + found.growfs = r; + break; + + case PATTERN_SHA256SUM: { + _cleanup_free_ void *d = NULL; + size_t l; + + if (strlen(t) != sizeof(found.sha256sum) * 2) + goto nope; + + r = unhexmem(t, sizeof(found.sha256sum) * 2, &d, &l); + if (r == -ENOMEM) + return r; + if (r < 0) + goto nope; + + assert(!found.sha256sum_set); + assert(l == sizeof(found.sha256sum)); + memcpy(found.sha256sum, d, l); + found.sha256sum_set = true; + break; + } + + default: + assert_se("unexpected pattern element"); + } + + p = n; + } + + if (ret) { + *ret = found; + found = (InstanceMetadata) INSTANCE_METADATA_NULL; + } + + return true; + +nope: + if (ret) + *ret = (InstanceMetadata) INSTANCE_METADATA_NULL; + + return false; +} + +int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret) { + _cleanup_(instance_metadata_destroy) InstanceMetadata found = INSTANCE_METADATA_NULL; + int r; + + STRV_FOREACH(p, patterns) { + r = pattern_match(*p, s, &found); + if (r < 0) + return r; + if (r > 0) { + if (ret) { + *ret = found; + found = (InstanceMetadata) INSTANCE_METADATA_NULL; + } + + return true; + } + } + + if (ret) + *ret = (InstanceMetadata) INSTANCE_METADATA_NULL; + + return false; +} + +int pattern_valid(const char *pattern) { + int r; + + r = pattern_split(pattern, NULL); + if (r == -EINVAL) + return false; + if (r < 0) + return r; + + return true; +} + +int pattern_format( + const char *pattern, + const InstanceMetadata *fields, + char **ret) { + + _cleanup_(pattern_element_free_allp) PatternElement *elements = NULL; + _cleanup_free_ char *j = NULL; + int r; + + assert(pattern); + assert(fields); + assert(ret); + + r = pattern_split(pattern, &elements); + if (r < 0) + return r; + + LIST_FOREACH(elements, e, elements) { + + switch (e->type) { + + case PATTERN_LITERAL: + if (!strextend(&j, e->literal)) + return -ENOMEM; + + break; + + case PATTERN_VERSION: + if (!fields->version) + return -ENXIO; + + if (!strextend(&j, fields->version)) + return -ENOMEM; + break; + + case PATTERN_PARTITION_UUID: { + char formatted[SD_ID128_STRING_MAX]; + + if (!fields->partition_uuid_set) + return -ENXIO; + + if (!strextend(&j, sd_id128_to_string(fields->partition_uuid, formatted))) + return -ENOMEM; + + break; + } + + case PATTERN_PARTITION_FLAGS: + if (!fields->partition_flags_set) + return -ENXIO; + + r = strextendf(&j, "%" PRIx64, fields->partition_flags); + if (r < 0) + return r; + + break; + + case PATTERN_MTIME: + if (fields->mtime == USEC_INFINITY) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->mtime); + if (r < 0) + return r; + + break; + + case PATTERN_MODE: + if (fields->mode == MODE_INVALID) + return -ENXIO; + + r = strextendf(&j, "%03o", fields->mode); + if (r < 0) + return r; + + break; + + case PATTERN_SIZE: + if (fields->size == UINT64_MAX) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->size); + if (r < 0) + return r; + break; + + case PATTERN_TRIES_DONE: + if (fields->tries_done == UINT64_MAX) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->tries_done); + if (r < 0) + return r; + break; + + case PATTERN_TRIES_LEFT: + if (fields->tries_left == UINT64_MAX) + return -ENXIO; + + r = strextendf(&j, "%" PRIu64, fields->tries_left); + if (r < 0) + return r; + break; + + case PATTERN_NO_AUTO: + if (fields->no_auto < 0) + return -ENXIO; + + if (!strextend(&j, one_zero(fields->no_auto))) + return -ENOMEM; + + break; + + case PATTERN_READ_ONLY: + if (fields->read_only < 0) + return -ENXIO; + + if (!strextend(&j, one_zero(fields->read_only))) + return -ENOMEM; + + break; + + case PATTERN_GROWFS: + if (fields->growfs < 0) + return -ENXIO; + + if (!strextend(&j, one_zero(fields->growfs))) + return -ENOMEM; + + break; + + case PATTERN_SHA256SUM: { + _cleanup_free_ char *h = NULL; + + if (!fields->sha256sum_set) + return -ENXIO; + + h = hexmem(fields->sha256sum, sizeof(fields->sha256sum)); + if (!h) + return -ENOMEM; + + if (!strextend(&j, h)) + return -ENOMEM; + + break; + } + + default: + assert_not_reached(); + } + } + + *ret = TAKE_PTR(j); + return 0; +} diff --git a/src/sysupdate/sysupdate-pattern.h b/src/sysupdate/sysupdate-pattern.h new file mode 100644 index 0000000000..1c60fa0250 --- /dev/null +++ b/src/sysupdate/sysupdate-pattern.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sysupdate-instance.h" +#include "time-util.h" + +int pattern_match(const char *pattern, const char *s, InstanceMetadata *ret); +int pattern_match_many(char **patterns, const char *s, InstanceMetadata *ret); +int pattern_valid(const char *pattern); +int pattern_format(const char *pattern, const InstanceMetadata *fields, char **ret); diff --git a/src/sysupdate/sysupdate-resource.c b/src/sysupdate/sysupdate-resource.c new file mode 100644 index 0000000000..97d8973f71 --- /dev/null +++ b/src/sysupdate/sysupdate-resource.c @@ -0,0 +1,633 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "blockdev-util.h" +#include "chase-symlinks.h" +#include "dirent-util.h" +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "glyph-util.h" +#include "gpt.h" +#include "hexdecoct.h" +#include "import-util.h" +#include "macro.h" +#include "process-util.h" +#include "sort-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "sysupdate-cache.h" +#include "sysupdate-instance.h" +#include "sysupdate-pattern.h" +#include "sysupdate-resource.h" +#include "sysupdate.h" +#include "utf8.h" + +void resource_destroy(Resource *rr) { + assert(rr); + + free(rr->path); + strv_free(rr->patterns); + + for (size_t i = 0; i < rr->n_instances; i++) + instance_free(rr->instances[i]); + free(rr->instances); +} + +static int resource_add_instance( + Resource *rr, + const char *path, + const InstanceMetadata *f, + Instance **ret) { + + Instance *i; + int r; + + assert(rr); + assert(path); + assert(f); + assert(f->version); + + if (!GREEDY_REALLOC(rr->instances, rr->n_instances + 1)) + return log_oom(); + + r = instance_new(rr, path, f, &i); + if (r < 0) + return r; + + rr->instances[rr->n_instances++] = i; + + if (ret) + *ret = i; + + return 0; +} + +static int resource_load_from_directory( + Resource *rr, + mode_t m) { + + _cleanup_(closedirp) DIR *d = NULL; + int r; + + assert(rr); + assert(IN_SET(rr->type, RESOURCE_TAR, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + assert(IN_SET(m, S_IFREG, S_IFDIR)); + + d = opendir(rr->path); + if (!d) { + if (errno == ENOENT) { + log_debug("Directory %s does not exist, not loading any resources.", rr->path); + return 0; + } + + return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path); + } + + for (;;) { + _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; + _cleanup_free_ char *joined = NULL; + Instance *instance; + struct dirent *de; + struct stat st; + + errno = 0; + de = readdir_no_dot(d); + if (!de) { + if (errno != 0) + return log_error_errno(errno, "Failed to read directory '%s': %m", rr->path); + break; + } + + switch (de->d_type) { + + case DT_UNKNOWN: + break; + + case DT_DIR: + if (m != S_IFDIR) + continue; + + break; + + case DT_REG: + if (m != S_IFREG) + continue; + break; + + default: + continue; + } + + if (fstatat(dirfd(d), de->d_name, &st, AT_NO_AUTOMOUNT) < 0) { + if (errno == ENOENT) /* Gone by now? */ + continue; + + return log_error_errno(errno, "Failed to stat %s/%s: %m", rr->path, de->d_name); + } + + if ((st.st_mode & S_IFMT) != m) + continue; + + r = pattern_match_many(rr->patterns, de->d_name, &extracted_fields); + if (r < 0) + return log_error_errno(r, "Failed to match pattern: %m"); + if (r == 0) + continue; + + joined = path_join(rr->path, de->d_name); + if (!joined) + return log_oom(); + + r = resource_add_instance(rr, joined, &extracted_fields, &instance); + if (r < 0) + return r; + + /* Inherit these from the source, if not explicitly overwritten */ + if (instance->metadata.mtime == USEC_INFINITY) + instance->metadata.mtime = timespec_load(&st.st_mtim) ?: USEC_INFINITY; + + if (instance->metadata.mode == MODE_INVALID) + instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */ + } + + return 0; +} + +static int resource_load_from_blockdev(Resource *rr) { + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; + size_t n_partitions; + int r; + + assert(rr); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + r = fdisk_assign_device(c, rr->path, /* readonly= */ true); + if (r < 0) + return log_error_errno(r, "Failed to open device '%s': %m", rr->path); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", rr->path); + + r = fdisk_get_partitions(c, &t); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition table: %m"); + + n_partitions = fdisk_table_get_nents(t); + for (size_t i = 0; i < n_partitions; i++) { + _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; + _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL; + Instance *instance; + + r = read_partition_info(c, t, i, &pinfo); + if (r < 0) + return r; + if (r == 0) /* not assigned */ + continue; + + /* Check if partition type matches */ + if (rr->partition_type_set && !sd_id128_equal(pinfo.type, rr->partition_type)) + continue; + + /* A label of "_empty" means "not used so far" for us */ + if (streq_ptr(pinfo.label, "_empty")) { + rr->n_empty++; + continue; + } + + r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields); + if (r < 0) + return log_error_errno(r, "Failed to match pattern: %m"); + if (r == 0) + continue; + + r = resource_add_instance(rr, pinfo.device, &extracted_fields, &instance); + if (r < 0) + return r; + + instance->partition_info = pinfo; + pinfo = (PartitionInfo) PARTITION_INFO_NULL; + + /* Inherit data from source if not configured explicitly */ + if (!instance->metadata.partition_uuid_set) { + instance->metadata.partition_uuid = instance->partition_info.uuid; + instance->metadata.partition_uuid_set = true; + } + + if (!instance->metadata.partition_flags_set) { + instance->metadata.partition_flags = instance->partition_info.flags; + instance->metadata.partition_flags_set = true; + } + + if (instance->metadata.read_only < 0) + instance->metadata.read_only = instance->partition_info.read_only; + } + + return 0; +} + +static int download_manifest( + const char *url, + bool verify_signature, + char **ret_buffer, + size_t *ret_size) { + + _cleanup_free_ char *buffer = NULL, *suffixed_url = NULL; + _cleanup_(close_pairp) int pfd[2] = { -1, -1 }; + _cleanup_fclose_ FILE *manifest = NULL; + size_t size = 0; + pid_t pid; + int r; + + assert(url); + assert(ret_buffer); + assert(ret_size); + + /* Download a SHA256SUMS file as manifest */ + + r = import_url_append_component(url, "SHA256SUMS", &suffixed_url); + if (r < 0) + return log_error_errno(r, "Failed to append SHA256SUMS to URL: %m"); + + if (pipe2(pfd, O_CLOEXEC) < 0) + return log_error_errno(errno, "Failed to allocate pipe: %m"); + + log_info("%s Acquiring manifest file %s…", special_glyph(SPECIAL_GLYPH_DOWNLOAD), suffixed_url); + + r = safe_fork("(sd-pull)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + const char *cmdline[] = { + "systemd-pull", + "raw", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", verify_signature ? "signature" : "no", /* verify the manifest file */ + suffixed_url, + "-", /* write to stdout */ + NULL + }; + + pfd[0] = safe_close(pfd[0]); + + r = rearrange_stdio(-1, pfd[1], STDERR_FILENO); + if (r < 0) { + log_error_errno(r, "Failed to rearrange stdin/stdout: %m"); + _exit(EXIT_FAILURE); + } + + (void) unsetenv("NOTIFY_SOCKET"); + execv(pull_binary_path(), (char *const*) cmdline); + log_error_errno(errno, "Failed to execute %s tool: %m", pull_binary_path()); + _exit(EXIT_FAILURE); + }; + + pfd[1] = safe_close(pfd[1]); + + /* We'll first load the entire manifest into memory before parsing it. That's because the + * systemd-pull tool can validate the download only after its completion, but still pass the data to + * us as it runs. We thus need to check the return value of the process *before* parsing, to be + * reasonably safe. */ + + manifest = fdopen(pfd[0], "r"); + if (!manifest) + return log_error_errno(errno, "Failed allocate FILE object for manifest file: %m"); + + TAKE_FD(pfd[0]); + + r = read_full_stream(manifest, &buffer, &size); + if (r < 0) + return log_error_errno(r, "Failed to read manifest file from child: %m"); + + manifest = safe_fclose(manifest); + + r = wait_for_terminate_and_check("(sd-pull)", pid, WAIT_LOG); + if (r < 0) + return r; + if (r != 0) + return -EPROTO; + + *ret_buffer = TAKE_PTR(buffer); + *ret_size = size; + + return 0; +} + +static int resource_load_from_web( + Resource *rr, + bool verify, + Hashmap **web_cache) { + + size_t manifest_size = 0, left = 0; + _cleanup_free_ char *buf = NULL; + const char *manifest, *p; + size_t line_nr = 1; + WebCacheItem *ci; + int r; + + assert(rr); + + ci = web_cache ? web_cache_get_item(*web_cache, rr->path, verify) : NULL; + if (ci) { + log_debug("Manifest web cache hit for %s.", rr->path); + + manifest = (char*) ci->data; + manifest_size = ci->size; + } else { + log_debug("Manifest web cache miss for %s.", rr->path); + + r = download_manifest(rr->path, verify, &buf, &manifest_size); + if (r < 0) + return r; + + manifest = buf; + } + + if (memchr(manifest, 0, manifest_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file has embedded NUL byte, refusing."); + if (!utf8_is_valid_n(manifest, manifest_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file is not valid UTF-8, refusing."); + + p = manifest; + left = manifest_size; + + while (left > 0) { + _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL; + _cleanup_free_ char *fn = NULL; + _cleanup_free_ void *h = NULL; + Instance *instance; + const char *e; + size_t hlen; + + /* 64 character hash + separator + filename + newline */ + if (left < 67) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Corrupt manifest at line %zu, refusing.", line_nr); + + if (p[0] == '\\') + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "File names with escapes not supported in manifest at line %zu, refusing.", line_nr); + + r = unhexmem(p, 64, &h, &hlen); + if (r < 0) + return log_error_errno(r, "Failed to parse digest at manifest line %zu, refusing.", line_nr); + + p += 64, left -= 64; + + if (*p != ' ') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing space separator at manifest line %zu, refusing.", line_nr); + p++, left--; + + if (!IN_SET(*p, '*', ' ')) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing binary/text input marker at manifest line %zu, refusing.", line_nr); + p++, left--; + + e = memchr(p, '\n', left); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Truncated manifest file at line %zu, refusing.", line_nr); + if (e == p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty filename specified at manifest line %zu, refusing.", line_nr); + + fn = strndup(p, e - p); + if (!fn) + return log_oom(); + + if (!filename_is_valid(fn)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid filename specified at manifest line %zu, refusing.", line_nr); + if (string_has_cc(fn, NULL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Filename contains control characters at manifest line %zu, refusing.", line_nr); + + r = pattern_match_many(rr->patterns, fn, &extracted_fields); + if (r < 0) + return log_error_errno(r, "Failed to match pattern: %m"); + if (r > 0) { + _cleanup_free_ char *path = NULL; + + r = import_url_append_component(rr->path, fn, &path); + if (r < 0) + return log_error_errno(r, "Failed to build instance URL: %m"); + + r = resource_add_instance(rr, path, &extracted_fields, &instance); + if (r < 0) + return r; + + assert(hlen == sizeof(instance->metadata.sha256sum)); + + if (instance->metadata.sha256sum_set) { + if (memcmp(instance->metadata.sha256sum, h, hlen) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 sum parsed from filename and manifest don't match at line %zu, refusing.", line_nr); + } else { + memcpy(instance->metadata.sha256sum, h, hlen); + instance->metadata.sha256sum_set = true; + } + } + + left -= (e - p) + 1; + p = e + 1; + + line_nr++; + } + + if (!ci && web_cache) { + r = web_cache_add_item(web_cache, rr->path, verify, manifest, manifest_size); + if (r < 0) + log_debug_errno(r, "Failed to add manifest '%s' to cache, ignoring: %m", rr->path); + else + log_debug("Added manifest '%s' to cache.", rr->path); + } + + return 0; +} + +static int instance_cmp(Instance *const*a, Instance *const*b) { + int r; + + assert(a); + assert(b); + assert(*a); + assert(*b); + assert((*a)->metadata.version); + assert((*b)->metadata.version); + + /* Newest version at the beginning */ + r = strverscmp_improved((*a)->metadata.version, (*b)->metadata.version); + if (r != 0) + return -r; + + /* Instances don't have to be uniquely named (uniqueness on partition tables is not enforced at all, + * and since we allow multiple matching patterns not even in directories they are unique). Hence + * let's order by path as secondary ordering key. */ + return path_compare((*a)->path, (*b)->path); +} + +int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) { + int r; + + assert(rr); + + switch (rr->type) { + + case RESOURCE_TAR: + case RESOURCE_REGULAR_FILE: + r = resource_load_from_directory(rr, S_IFREG); + break; + + case RESOURCE_DIRECTORY: + case RESOURCE_SUBVOLUME: + r = resource_load_from_directory(rr, S_IFDIR); + break; + + case RESOURCE_PARTITION: + r = resource_load_from_blockdev(rr); + break; + + case RESOURCE_URL_FILE: + case RESOURCE_URL_TAR: + r = resource_load_from_web(rr, verify, web_cache); + break; + + default: + assert_not_reached(); + } + if (r < 0) + return r; + + typesafe_qsort(rr->instances, rr->n_instances, instance_cmp); + return 0; +} + +Instance* resource_find_instance(Resource *rr, const char *version) { + Instance key = { + .metadata.version = (char*) version, + }, *k = &key; + + return typesafe_bsearch(&k, rr->instances, rr->n_instances, instance_cmp); +} + +int resource_resolve_path( + Resource *rr, + const char *root, + const char *node) { + + _cleanup_free_ char *p = NULL; + dev_t d; + int r; + + assert(rr); + + if (rr->path_auto) { + + /* NB: we don't actually check the backing device of the root fs "/", but of "/usr", in order + * to support environments where the root fs is a tmpfs, and the OS itself placed exclusively + * in /usr/. */ + + if (rr->type != RESOURCE_PARTITION) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Automatic root path discovery only supported for partition resources."); + + if (node) { /* If --image= is specified, directly use the loopback device */ + r = free_and_strdup_warn(&rr->path, node); + if (r < 0) + return r; + + return 0; + } + + if (root) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Block device is not allowed when using --root= mode."); + + r = get_block_device_harder("/usr/", &d); + + } else if (rr->type == RESOURCE_PARTITION) { + _cleanup_close_ int fd = -1, real_fd = -1; + _cleanup_free_ char *resolved = NULL; + struct stat st; + + r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, &fd); + if (r < 0) + return log_error_errno(r, "Failed to resolve '%s': %m", rr->path); + + if (fstat(fd, &st) < 0) + return log_error_errno(r, "Failed to stat '%s': %m", resolved); + + if (S_ISBLK(st.st_mode) && root) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "When using --root= or --image= access to device nodes is prohibited."); + + if (S_ISREG(st.st_mode) || S_ISBLK(st.st_mode)) { + /* Not a directory, hence no need to find backing block device for the path */ + free_and_replace(rr->path, resolved); + return 0; + } + + if (!S_ISDIR(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Target path '%s' does not refer to regular file, directory or block device, refusing.", rr->path); + + if (node) { /* If --image= is specified all file systems are backed by the same loopback device, hence shortcut things. */ + r = free_and_strdup_warn(&rr->path, node); + if (r < 0) + return r; + + return 0; + } + + real_fd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (real_fd < 0) + return log_error_errno(real_fd, "Failed to convert O_PATH file descriptor for %s to regular file descriptor: %m", rr->path); + + r = get_block_device_harder_fd(fd, &d); + + } else if (RESOURCE_IS_FILESYSTEM(rr->type) && root) { + _cleanup_free_ char *resolved = NULL; + + r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, NULL); + if (r < 0) + return log_error_errno(r, "Failed to resolve '%s': %m", rr->path); + + free_and_replace(rr->path, resolved); + return 0; + } else + return 0; /* Otherwise assume there's nothing to resolve */ + + if (r < 0) + return log_error_errno(r, "Failed to determine block device of file system: %m"); + + r = block_get_whole_disk(d, &d); + if (r < 0) + return log_error_errno(r, "Failed to find whole disk device for partition backing file system: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "File system is not placed on a partition block device, cannot determine whole block device backing root file system."); + + r = device_path_make_canonical(S_IFBLK, d, &p); + if (r < 0) + return r; + + if (rr->path) + log_info("Automatically discovered block device '%s' from '%s'.", p, rr->path); + else + log_info("Automatically discovered root block device '%s'.", p); + + free_and_replace(rr->path, p); + return 1; +} + +static const char *resource_type_table[_RESOURCE_TYPE_MAX] = { + [RESOURCE_URL_FILE] = "url-file", + [RESOURCE_URL_TAR] = "url-tar", + [RESOURCE_TAR] = "tar", + [RESOURCE_PARTITION] = "partition", + [RESOURCE_REGULAR_FILE] = "regular-file", + [RESOURCE_DIRECTORY] = "directory", + [RESOURCE_SUBVOLUME] = "subvolume", +}; + +DEFINE_STRING_TABLE_LOOKUP(resource_type, ResourceType); diff --git a/src/sysupdate/sysupdate-resource.h b/src/sysupdate/sysupdate-resource.h new file mode 100644 index 0000000000..86be0d3389 --- /dev/null +++ b/src/sysupdate/sysupdate-resource.h @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-id128.h" + +#include "hashmap.h" +#include "macro.h" + +/* Forward declare this type so that the headers below can use it */ +typedef struct Resource Resource; + +#include "sysupdate-instance.h" + +typedef enum ResourceType { + RESOURCE_URL_FILE, + RESOURCE_URL_TAR, + RESOURCE_TAR, + RESOURCE_PARTITION, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME, + _RESOURCE_TYPE_MAX, + _RESOURCE_TYPE_INVALID = -EINVAL, +} ResourceType; + +static inline bool RESOURCE_IS_SOURCE(ResourceType t) { + return IN_SET(t, + RESOURCE_URL_FILE, + RESOURCE_URL_TAR, + RESOURCE_TAR, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME); +} + +static inline bool RESOURCE_IS_TARGET(ResourceType t) { + return IN_SET(t, + RESOURCE_PARTITION, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME); +} + +/* Returns true for all resources that deal with file system objects, i.e. where we operate on top of the + * file system layer, instead of below. */ +static inline bool RESOURCE_IS_FILESYSTEM(ResourceType t) { + return IN_SET(t, + RESOURCE_TAR, + RESOURCE_REGULAR_FILE, + RESOURCE_DIRECTORY, + RESOURCE_SUBVOLUME); +} + +static inline bool RESOURCE_IS_TAR(ResourceType t) { + return IN_SET(t, + RESOURCE_TAR, + RESOURCE_URL_TAR); +} + +static inline bool RESOURCE_IS_URL(ResourceType t) { + return IN_SET(t, + RESOURCE_URL_TAR, + RESOURCE_URL_FILE); +} + +struct Resource { + ResourceType type; + + /* Where to look for instances, and what to match precisely */ + char *path; + bool path_auto; /* automatically find root path (only available if target resource, not source resource) */ + char **patterns; + sd_id128_t partition_type; + bool partition_type_set; + + /* All instances of this resource we found */ + Instance **instances; + size_t n_instances; + + /* If this is a partition resource (RESOURCE_PARTITION), then how many partition slots are currently unassigned, that we can use */ + size_t n_empty; +}; + +void resource_destroy(Resource *rr); + +int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache); + +Instance* resource_find_instance(Resource *rr, const char *version); + +int resource_resolve_path(Resource *rr, const char *root, const char *node); + +ResourceType resource_type_from_string(const char *s) _pure_; +const char *resource_type_to_string(ResourceType t) _const_; diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c new file mode 100644 index 0000000000..a9fceed601 --- /dev/null +++ b/src/sysupdate/sysupdate-transfer.c @@ -0,0 +1,1247 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-id128.h" + +#include "alloc-util.h" +#include "blockdev-util.h" +#include "chase-symlinks.h" +#include "conf-parser.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "glyph-util.h" +#include "gpt.h" +#include "hexdecoct.h" +#include "install-file.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "specifier.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "strv.h" +#include "sync-util.h" +#include "sysupdate-pattern.h" +#include "sysupdate-resource.h" +#include "sysupdate-transfer.h" +#include "sysupdate-util.h" +#include "sysupdate.h" +#include "tmpfile-util.h" +#include "web-util.h" + +/* Default value for InstancesMax= for fs object targets */ +#define DEFAULT_FILE_INSTANCES_MAX 3 + +Transfer *transfer_free(Transfer *t) { + if (!t) + return NULL; + + t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path); + + free(t->definition_path); + free(t->min_version); + strv_free(t->protected_versions); + free(t->current_symlink); + free(t->final_path); + + partition_info_destroy(&t->partition_info); + + resource_destroy(&t->source); + resource_destroy(&t->target); + + return mfree(t); +} + +Transfer *transfer_new(void) { + Transfer *t; + + t = new(Transfer, 1); + if (!t) + return NULL; + + *t = (Transfer) { + .source.type = _RESOURCE_TYPE_INVALID, + .target.type = _RESOURCE_TYPE_INVALID, + .remove_temporary = true, + .mode = MODE_INVALID, + .tries_left = UINT64_MAX, + .tries_done = UINT64_MAX, + .verify = true, + + /* the three flags, as configured by the user */ + .no_auto = -1, + .read_only = -1, + .growfs = -1, + + /* the read only flag, as ultimately determined */ + .install_read_only = -1, + + .partition_info = PARTITION_INFO_NULL, + }; + + 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, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ char *resolved = NULL; + char ***protected_versions = data; + int r; + + assert(rvalue); + assert(data); + + 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 ProtectVersion=, ignoring: %s", rvalue); + return 0; + } + + if (!version_is_valid(resolved)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "ProtectVersion= string is not valid, ignoring: %s", resolved); + return 0; + } + + r = strv_extend(protected_versions, resolved); + if (r < 0) + return log_oom(); + + return 0; +} + +static int config_parse_min_version( + 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) { + + _cleanup_free_ char *resolved = NULL; + char **version = data; + int r; + + assert(rvalue); + assert(data); + + 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 MinVersion=, ignoring: %s", rvalue); + return 0; + } + + if (!version_is_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "MinVersion= string is not valid, ignoring: %s", resolved); + return 0; + } + + return free_and_replace(*version, resolved); +} + +static int config_parse_current_symlink( + 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) { + + _cleanup_free_ char *resolved = NULL; + char **current_symlink = data; + int r; + + assert(rvalue); + assert(data); + + 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 CurrentSymlink=, ignoring: %s", rvalue); + return 0; + } + + r = path_simplify_and_warn(resolved, 0, unit, filename, line, lvalue); + if (r < 0) + return 0; + + return free_and_replace(*current_symlink, resolved); +} + +static int config_parse_instances_max( + 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) { + + uint64_t *instances_max = data, i; + int r; + + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + *instances_max = 0; /* Revert to default logic, see transfer_read_definition() */ + return 0; + } + + r = safe_atou64(rvalue, &i); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse InstancesMax= value, ignoring: %s", rvalue); + return 0; + } + + if (i < 2) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "InstancesMax= value must be at least 2, bumping: %s", rvalue); + *instances_max = 2; + } else + *instances_max = i; + + return 0; +} + +static int config_parse_resource_pattern( + 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 ***patterns = data; + int r; + + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + *patterns = strv_free(*patterns); + return 0; + } + + for (;;) { + _cleanup_free_ char *word = NULL, *resolved = NULL; + + r = extract_first_word(&rvalue, &word, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to extract first pattern from MatchPattern=, ignoring: %s", rvalue); + return 0; + } + if (r == 0) + break; + + r = specifier_printf(word, NAME_MAX, specifier_table, arg_root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in MatchPattern=, ignoring: %s", rvalue); + return 0; + } + + if (!pattern_valid(resolved)) + return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL), + "MatchPattern= string is not valid, refusing: %s", resolved); + + r = strv_consume(patterns, TAKE_PTR(resolved)); + if (r < 0) + return log_oom(); + } + + strv_uniq(*patterns); + return 0; +} + +static int config_parse_resource_path( + 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) { + + _cleanup_free_ char *resolved = NULL; + Resource *rr = data; + int r; + + assert(rvalue); + assert(data); + + if (streq(rvalue, "auto")) { + rr->path_auto = true; + rr->path = mfree(rr->path); + return 0; + } + + r = specifier_printf(rvalue, PATH_MAX-1, specifier_table, arg_root, NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in Path=, ignoring: %s", rvalue); + return 0; + } + + /* Note that we don't validate the path as being absolute or normalized. We'll do that in + * transfer_read_definition() as we might not know yet whether Path refers to an URL or a file system + * path. */ + + rr->path_auto = false; + return free_and_replace(rr->path, resolved); +} + +static DEFINE_CONFIG_PARSE_ENUM(config_parse_resource_type, resource_type, ResourceType, "Invalid resource type"); + +static int config_parse_resource_ptype( + 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) { + + Resource *rr = data; + int r; + + assert(rvalue); + assert(data); + + r = gpt_partition_type_uuid_from_string(rvalue, &rr->partition_type); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed parse partition type, ignoring: %s", rvalue); + return 0; + } + + rr->partition_type_set = true; + return 0; +} + +static int config_parse_partition_uuid( + 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) { + + Transfer *t = data; + int r; + + assert(rvalue); + assert(data); + + r = sd_id128_from_string(rvalue, &t->partition_uuid); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed parse partition UUID, ignoring: %s", rvalue); + return 0; + } + + t->partition_uuid_set = true; + return 0; +} + +static int config_parse_partition_flags( + 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) { + + Transfer *t = data; + int r; + + assert(rvalue); + assert(data); + + r = safe_atou64(rvalue, &t->partition_flags); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed parse partition flags, ignoring: %s", rvalue); + return 0; + } + + t->partition_flags_set = true; + return 0; +} + +int transfer_read_definition(Transfer *t, const char *path) { + int r; + + assert(t); + assert(path); + + ConfigTableItem table[] = { + { "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version }, + { "Transfer", "ProtectVersion", config_parse_protect_version, 0, &t->protected_versions }, + { "Transfer", "Verify", config_parse_bool, 0, &t->verify }, + { "Source", "Type", config_parse_resource_type, 0, &t->source.type }, + { "Source", "Path", config_parse_resource_path, 0, &t->source }, + { "Source", "MatchPattern", config_parse_resource_pattern, 0, &t->source.patterns }, + { "Target", "Type", config_parse_resource_type, 0, &t->target.type }, + { "Target", "Path", config_parse_resource_path, 0, &t->target }, + { "Target", "MatchPattern", config_parse_resource_pattern, 0, &t->target.patterns }, + { "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target }, + { "Target", "PartitionUUID", config_parse_partition_uuid, 0, t }, + { "Target", "PartitionFlags", config_parse_partition_flags, 0, t }, + { "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto }, + { "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs }, + { "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only }, + { "Target", "Mode", config_parse_mode, 0, &t->mode }, + { "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left }, + { "Target", "TriesDone", config_parse_uint64, 0, &t->tries_done }, + { "Target", "InstancesMax", config_parse_instances_max, 0, &t->instances_max }, + { "Target", "RemoveTemporary", config_parse_bool, 0, &t->remove_temporary }, + { "Target", "CurrentSymlink", config_parse_current_symlink, 0, &t->current_symlink }, + {} + }; + + r = config_parse(NULL, path, NULL, + "Transfer\0" + "Source\0" + "Target\0", + config_item_table_lookup, table, + CONFIG_PARSE_WARN, + t, + NULL); + if (r < 0) + return r; + + 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."); + + if (t->target.type < 0) { + switch (t->source.type) { + + case RESOURCE_URL_FILE: + case RESOURCE_REGULAR_FILE: + t->target.type = + t->target.path && path_startswith(t->target.path, "/dev/") ? + RESOURCE_PARTITION : RESOURCE_REGULAR_FILE; + break; + + case RESOURCE_URL_TAR: + case RESOURCE_TAR: + case RESOURCE_DIRECTORY: + t->target.type = RESOURCE_DIRECTORY; + break; + + case RESOURCE_SUBVOLUME: + t->target.type = RESOURCE_SUBVOLUME; + break; + + default: + assert_not_reached(); + } + } + + if (!RESOURCE_IS_TARGET(t->target.type)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target Type= must be one of partition, regular-file, directory, subvolume."); + + if ((IN_SET(t->source.type, RESOURCE_URL_FILE, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE) && + !IN_SET(t->target.type, RESOURCE_PARTITION, RESOURCE_REGULAR_FILE)) || + (IN_SET(t->source.type, RESOURCE_URL_TAR, RESOURCE_TAR, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME) && + !IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME))) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target type '%s' is incompatible with source type '%s', refusing.", + resource_type_to_string(t->source.type), resource_type_to_string(t->target.type)); + + if (!t->source.path && !t->source.path_auto) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source specification lacks Path=."); + + if (t->source.path) { + if (RESOURCE_IS_FILESYSTEM(t->source.type) || t->source.type == RESOURCE_PARTITION) + if (!path_is_absolute(t->source.path) || !path_is_normalized(t->source.path)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source path is not a normalized, absolute path: %s", t->source.path); + + /* We unofficially support file:// in addition to http:// and https:// for url + * sources. That's mostly for testing, since it relieves us from having to set up a HTTP + * server, and CURL abstracts this away from us thankfully. */ + if (RESOURCE_IS_URL(t->source.type)) + if (!http_url_is_valid(t->source.path) && !file_url_is_valid(t->source.path)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source path is not a valid HTTP or HTTPS URL: %s", t->source.path); + } + + if (strv_isempty(t->source.patterns)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Source specification lacks MatchPattern=."); + + if (!t->target.path && !t->target.path_auto) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target specification lacks Path= field."); + + if (t->target.path && + (!path_is_absolute(t->target.path) || !path_is_normalized(t->target.path))) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Target path is not a normalized, absolute path: %s", t->target.path); + + if (strv_isempty(t->target.patterns)) { + strv_free(t->target.patterns); + t->target.patterns = strv_copy(t->source.patterns); + if (!t->target.patterns) + return log_oom(); + } + + if (t->current_symlink && !RESOURCE_IS_FILESYSTEM(t->target.type) && !path_is_absolute(t->current_symlink)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Current symlink must be absolute path if target is partition: %s", t->current_symlink); + + /* When no instance limit is set, use all available partition slots in case of partitions, or 3 in case of fs objects */ + if (t->instances_max == 0) + t->instances_max = t->target.type == RESOURCE_PARTITION ? UINT64_MAX : DEFAULT_FILE_INSTANCES_MAX; + + return 0; +} + +int transfer_resolve_paths( + Transfer *t, + const char *root, + const char *node) { + + int r; + + /* If Path=auto is used in [Source] or [Target] sections, let's automatically detect the path of the + * block device to use. Moreover, if this path points to a directory but we need a block device, + * automatically determine the backing block device, so that users can reference block devices by + * mount point. */ + + assert(t); + + r = resource_resolve_path(&t->source, root, node); + if (r < 0) + return r; + + r = resource_resolve_path(&t->target, root, node); + if (r < 0) + return r; + + return 0; +} + +static void transfer_remove_temporary(Transfer *t) { + _cleanup_(closedirp) DIR *d = NULL; + int r; + + assert(t); + + if (!t->remove_temporary) + return; + + if (!IN_SET(t->target.type, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)) + return; + + /* Removes all temporary files/dirs from previous runs in the target directory, i.e. all those starting with '.#' */ + + d = opendir(t->target.path); + if (!d) { + if (errno == ENOENT) + return; + + log_debug_errno(errno, "Failed to open target directory '%s', ignoring: %m", t->target.path); + return; + } + + for (;;) { + struct dirent *de; + + errno = 0; + de = readdir_no_dot(d); + if (!de) { + if (errno != 0) + log_debug_errno(errno, "Failed to read target directory '%s', ignoring: %m", t->target.path); + break; + } + + if (!startswith(de->d_name, ".#")) + continue; + + r = rm_rf_child(dirfd(d), de->d_name, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to remove temporary resource instance '%s/%s', ignoring: %m", t->target.path, de->d_name); + continue; + } + + log_debug("Removed temporary resource instance '%s/%s'.", t->target.path, de->d_name); + } +} + +int transfer_vacuum( + Transfer *t, + uint64_t space, + const char *extra_protected_version) { + + uint64_t instances_max, limit; + int r, count = 0; + + assert(t); + + transfer_remove_temporary(t); + + /* First, calculate how many instances to keep, based on the instance limit — but keep at least one */ + + instances_max = arg_instances_max != UINT64_MAX ? arg_instances_max : t->instances_max; + assert(instances_max >= 1); + if (instances_max == UINT64_MAX) /* Keep infinite instances? */ + limit = UINT64_MAX; + 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."); + else if (space == instances_max) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Asked to delete all possible instances, can't allow that. One instance must always remain."); + else + limit = instances_max - space; + + if (t->target.type == RESOURCE_PARTITION) { + uint64_t rm, remain; + + /* If we are looking at a partition table, we also have to take into account how many + * partition slots of the right type are available */ + + if (t->target.n_empty + t->target.n_instances < 2) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Partition table has less than two partition slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), refusing.", + SD_ID128_FORMAT_VAL(t->target.partition_type), + gpt_partition_type_uuid_to_string(t->target.partition_type)); + if (space > t->target.n_empty + t->target.n_instances) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Partition table does not have enough partition slots of right type " SD_ID128_UUID_FORMAT_STR " (%s) for operation.", + SD_ID128_FORMAT_VAL(t->target.partition_type), + gpt_partition_type_uuid_to_string(t->target.partition_type)); + if (space == t->target.n_empty + t->target.n_instances) + return log_error_errno(SYNTHETIC_ERRNO(ENOSPC), + "Asked to empty all partition table slots of the right type " SD_ID128_UUID_FORMAT_STR " (%s), can't allow that. One instance must always remain.", + SD_ID128_FORMAT_VAL(t->target.partition_type), + gpt_partition_type_uuid_to_string(t->target.partition_type)); + + rm = LESS_BY(space, t->target.n_empty); + remain = LESS_BY(t->target.n_instances, rm); + limit = MIN(limit, remain); + } + + while (t->target.n_instances > limit) { + Instance *oldest; + size_t p = t->target.n_instances - 1; + + for (;;) { + oldest = t->target.instances[p]; + assert(oldest); + + /* If this is listed among the protected versions, then let's not remove it */ + if (!strv_contains(t->protected_versions, oldest->metadata.version) && + (!extra_protected_version || !streq(extra_protected_version, oldest->metadata.version))) + break; + + log_debug("Version '%s' is protected, not removing.", oldest->metadata.version); + if (p == 0) { + oldest = NULL; + break; + } + + p--; + } + + if (!oldest) /* Nothing more to remove */ + break; + + assert(oldest->resource); + + log_info("%s Removing old '%s' (%s).", special_glyph(SPECIAL_GLYPH_RECYCLING), oldest->path, resource_type_to_string(oldest->resource->type)); + + switch (t->target.type) { + + case RESOURCE_REGULAR_FILE: + case RESOURCE_DIRECTORY: + case RESOURCE_SUBVOLUME: + r = rm_rf(oldest->path, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_MISSING_OK|REMOVE_CHMOD); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to make room, deleting '%s' failed: %m", oldest->path); + + break; + + case RESOURCE_PARTITION: { + PartitionInfo pinfo = oldest->partition_info; + + /* label "_empty" means "no contents" for our purposes */ + pinfo.label = (char*) "_empty"; + + r = patch_partition(t->target.path, &pinfo, PARTITION_LABEL); + if (r < 0) + return r; + + t->target.n_empty++; + break; + } + + default: + assert_not_reached(); + break; + } + + instance_free(oldest); + memmove(t->target.instances + p, t->target.instances + p + 1, (t->target.n_instances - p - 1) * sizeof(Instance*)); + t->target.n_instances--; + + count++; + } + + return count; +} + +static void compile_pattern_fields( + const Transfer *t, + const Instance *i, + InstanceMetadata *ret) { + + assert(t); + assert(i); + assert(ret); + + *ret = (InstanceMetadata) { + .version = i->metadata.version, + + /* We generally prefer explicitly configured values for the transfer over those automatically + * derived from the source instance. Also, if the source is a tar archive, then let's not + * patch mtime/mode and use the one embedded in the tar file */ + .partition_uuid = t->partition_uuid_set ? t->partition_uuid : i->metadata.partition_uuid, + .partition_uuid_set = t->partition_uuid_set || i->metadata.partition_uuid_set, + .partition_flags = t->partition_flags_set ? t->partition_flags : i->metadata.partition_flags, + .partition_flags_set = t->partition_flags_set || i->metadata.partition_flags_set, + .mtime = RESOURCE_IS_TAR(i->resource->type) ? USEC_INFINITY : i->metadata.mtime, + .mode = t->mode != MODE_INVALID ? t->mode : (RESOURCE_IS_TAR(i->resource->type) ? MODE_INVALID : i->metadata.mode), + .size = i->metadata.size, + .tries_done = t->tries_done != UINT64_MAX ? t->tries_done : + i->metadata.tries_done != UINT64_MAX ? i->metadata.tries_done : 0, + .tries_left = t->tries_left != UINT64_MAX ? t->tries_left : + i->metadata.tries_left != UINT64_MAX ? i->metadata.tries_left : 3, + .no_auto = t->no_auto >= 0 ? t->no_auto : i->metadata.no_auto, + .read_only = t->read_only >= 0 ? t->read_only : i->metadata.read_only, + .growfs = t->growfs >= 0 ? t->growfs : i->metadata.growfs, + .sha256sum_set = i->metadata.sha256sum_set, + }; + + memcpy(ret->sha256sum, i->metadata.sha256sum, sizeof(ret->sha256sum)); +} + +static int run_helper( + const char *name, + const char *path, + const char * const cmdline[]) { + + int r; + + assert(name); + assert(path); + assert(cmdline); + + r = safe_fork(name, FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG|FORK_WAIT, NULL); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + (void) unsetenv("NOTIFY_SOCKET"); + execv(path, (char *const*) cmdline); + log_error_errno(errno, "Failed to execute %s tool: %m", path); + _exit(EXIT_FAILURE); + } + + return 0; +} + +int transfer_acquire_instance(Transfer *t, Instance *i) { + _cleanup_free_ char *formatted_pattern = NULL, *digest = NULL; + char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1]; + const char *where = NULL; + InstanceMetadata f; + Instance *existing; + int r; + + assert(t); + assert(i); + assert(i->resource); + assert(t == container_of(i->resource, Transfer, source)); + + /* Does this instance already exist in the target? Then we don't need to acquire anything */ + existing = resource_find_instance(&t->target, i->metadata.version); + if (existing) { + log_info("No need to acquire '%s', already installed.", i->path); + return 0; + } + + assert(!t->final_path); + assert(!t->temporary_path); + assert(!strv_isempty(t->target.patterns)); + + /* Format the target name using the first pattern specified */ + compile_pattern_fields(t, i, &f); + r = pattern_format(t->target.patterns[0], &f, &formatted_pattern); + if (r < 0) + return log_error_errno(r, "Failed to format target pattern: %m"); + + if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + + if (!filename_is_valid(formatted_pattern)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as file name, refusing: %s", formatted_pattern); + + t->final_path = path_join(t->target.path, formatted_pattern); + if (!t->final_path) + return log_oom(); + + r = tempfn_random(t->final_path, "sysupdate", &t->temporary_path); + if (r < 0) + return log_error_errno(r, "Failed to generate temporary target path: %m"); + + where = t->final_path; + } + + if (t->target.type == RESOURCE_PARTITION) { + r = gpt_partition_label_valid(formatted_pattern); + if (r < 0) + return log_error_errno(r, "Failed to determine if formatted pattern is suitable as GPT partition label: %s", formatted_pattern); + if (!r) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Formatted pattern is not suitable as GPT partition label, refusing: %s", formatted_pattern); + + r = find_suitable_partition( + t->target.path, + i->metadata.size, + t->target.partition_type_set ? &t->target.partition_type : NULL, + &t->partition_info); + if (r < 0) + return r; + + xsprintf(offset, "%" PRIu64, t->partition_info.start); + xsprintf(max_size, "%" PRIu64, t->partition_info.size); + + where = t->partition_info.device; + } + + assert(where); + + log_info("%s Acquiring %s %s %s...", special_glyph(SPECIAL_GLYPH_DOWNLOAD), i->path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), where); + + if (RESOURCE_IS_URL(i->resource->type)) { + /* For URL sources we require the SHA256 sum to be known so that we can validate the + * download. */ + + if (!i->metadata.sha256sum_set) + return log_error_errno(r, "SHA256 checksum not known for download '%s', refusing.", i->path); + + digest = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum)); + if (!digest) + return log_oom(); + } + + switch (i->resource->type) { /* Source */ + + case RESOURCE_REGULAR_FILE: + + switch (t->target.type) { /* Target */ + + case RESOURCE_REGULAR_FILE: + + /* regular file → regular file (why fork off systemd-import for such a simple file + * copy case? implicit decompression mostly, and thus also sandboxing. Also, the + * importer has some tricks up its sleeve, such as sparse file generation, which we + * want to take benefit of, too.) */ + + r = run_helper("(sd-import-raw)", + import_binary_path(), + (const char* const[]) { + "systemd-import", + "raw", + "--direct", /* just copy/unpack the specified file, don't do anything else */ + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_PARTITION: + + /* regular file → partition */ + + r = run_helper("(sd-import-raw)", + import_binary_path(), + (const char* const[]) { + "systemd-import", + "raw", + "--direct", /* just copy/unpack the specified file, don't do anything else */ + "--offset", offset, + "--size-max", max_size, + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->target.path, + NULL + }); + break; + + default: + assert_not_reached(); + } + + break; + + case RESOURCE_DIRECTORY: + case RESOURCE_SUBVOLUME: + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + + /* directory/subvolume → directory/subvolume */ + + r = run_helper("(sd-import-fs)", + import_fs_binary_path(), + (const char* const[]) { + "systemd-import-fs", + "run", + "--direct", /* just untar the specified file, don't do anything else */ + arg_sync ? "--sync=yes" : "--sync=no", + t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_TAR: + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + + /* tar → directory/subvolume */ + + r = run_helper("(sd-import-tar)", + import_binary_path(), + (const char* const[]) { + "systemd-import", + "tar", + "--direct", /* just untar the specified file, don't do anything else */ + arg_sync ? "--sync=yes" : "--sync=no", + t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_URL_FILE: + + switch (t->target.type) { + + case RESOURCE_REGULAR_FILE: + + /* url file → regular file */ + + r = run_helper("(sd-pull-raw)", + pull_binary_path(), + (const char* const[]) { + "systemd-pull", + "raw", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", digest, /* validate by explicit SHA256 sum */ + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->temporary_path, + NULL + }); + break; + + case RESOURCE_PARTITION: + + /* url file → partition */ + + r = run_helper("(sd-pull-raw)", + pull_binary_path(), + (const char* const[]) { + "systemd-pull", + "raw", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", digest, /* validate by explicit SHA256 sum */ + "--offset", offset, + "--size-max", max_size, + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->target.path, + NULL + }); + break; + + default: + assert_not_reached(); + } + + break; + + case RESOURCE_URL_TAR: + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + + r = run_helper("(sd-pull-tar)", + pull_binary_path(), + (const char*const[]) { + "systemd-pull", + "tar", + "--direct", /* just download the specified URL, don't download anything else */ + "--verify", digest, /* validate by explicit SHA256 sum */ + t->target.type == RESOURCE_SUBVOLUME ? "--btrfs-subvol=yes" : "--btrfs-subvol=no", + arg_sync ? "--sync=yes" : "--sync=no", + i->path, + t->temporary_path, + NULL + }); + break; + + default: + assert_not_reached(); + } + if (r < 0) + return r; + + if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + bool need_sync = false; + assert(t->temporary_path); + + /* Apply file attributes if set */ + if (f.mtime != USEC_INFINITY) { + struct timespec ts; + + timespec_store(&ts, f.mtime); + + if (utimensat(AT_FDCWD, t->temporary_path, (struct timespec[2]) { ts, ts }, AT_SYMLINK_NOFOLLOW) < 0) + return log_error_errno(errno, "Failed to adjust mtime of '%s': %m", t->temporary_path); + + need_sync = true; + } + + if (f.mode != MODE_INVALID) { + /* Try with AT_SYMLINK_NOFOLLOW first, because it's the safe thing to do. Older + * kernels don't support that however, in that case we fall back to chmod(). Not as + * safe, but shouldn't be a problem, given that we don't create symlinks here. */ + if (fchmodat(AT_FDCWD, t->temporary_path, f.mode, AT_SYMLINK_NOFOLLOW) < 0 && + (!ERRNO_IS_NOT_SUPPORTED(errno) || chmod(t->temporary_path, f.mode) < 0)) + return log_error_errno(errno, "Failed to adjust mode of '%s': %m", t->temporary_path); + + need_sync = true; + } + + /* Synchronize */ + if (arg_sync && need_sync) { + if (t->target.type == RESOURCE_REGULAR_FILE) + r = fsync_path_and_parent_at(AT_FDCWD, t->temporary_path); + else { + assert(IN_SET(t->target.type, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME)); + r = syncfs_path(AT_FDCWD, t->temporary_path); + } + if (r < 0) + return log_error_errno(r, "Failed to synchronize file system backing '%s': %m", t->temporary_path); + } + + t->install_read_only = f.read_only; + } + + if (t->target.type == RESOURCE_PARTITION) { + free_and_replace(t->partition_info.label, formatted_pattern); + t->partition_change = PARTITION_LABEL; + + if (f.partition_uuid_set) { + t->partition_info.uuid = f.partition_uuid; + t->partition_change |= PARTITION_UUID; + } + + if (f.partition_flags_set) { + t->partition_info.flags = f.partition_flags; + t->partition_change |= PARTITION_FLAGS; + } + + if (f.no_auto >= 0) { + t->partition_info.no_auto = f.no_auto; + t->partition_change |= PARTITION_NO_AUTO; + } + + if (f.read_only >= 0) { + t->partition_info.read_only = f.read_only; + t->partition_change |= PARTITION_READ_ONLY; + } + + if (f.growfs >= 0) { + t->partition_info.growfs = f.growfs; + t->partition_change |= PARTITION_GROWFS; + } + } + + /* For regular file cases the only step left is to install the file in place, which install_file() + * will do via rename(). For partition cases the only step left is to update the partition table, + * which is done at the same place. */ + + log_info("Successfully acquired '%s'.", i->path); + return 0; +} + +int transfer_install_instance( + Transfer *t, + Instance *i, + const char *root) { + + int r; + + assert(t); + assert(i); + assert(i->resource); + assert(t == container_of(i->resource, Transfer, source)); + + if (t->temporary_path) { + assert(RESOURCE_IS_FILESYSTEM(t->target.type)); + assert(t->final_path); + + r = install_file(AT_FDCWD, t->temporary_path, + AT_FDCWD, t->final_path, + INSTALL_REPLACE| + (t->install_read_only > 0 ? INSTALL_READ_ONLY : 0)| + (t->target.type == RESOURCE_REGULAR_FILE ? INSTALL_FSYNC_FULL : INSTALL_SYNCFS)); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", t->final_path); + + log_info("Successfully installed '%s' (%s) as '%s' (%s).", + i->path, + resource_type_to_string(i->resource->type), + t->final_path, + resource_type_to_string(t->target.type)); + + t->temporary_path = mfree(t->temporary_path); + } + + if (t->partition_change != 0) { + assert(t->target.type == RESOURCE_PARTITION); + + r = patch_partition( + t->target.path, + &t->partition_info, + t->partition_change); + if (r < 0) + return r; + + log_info("Successfully installed '%s' (%s) as '%s' (%s).", + i->path, + resource_type_to_string(i->resource->type), + t->partition_info.device, + resource_type_to_string(t->target.type)); + } + + if (t->current_symlink) { + _cleanup_free_ char *buf = NULL, *parent = NULL, *relative = NULL, *resolved = NULL; + const char *link_path, *link_target; + bool resolve_link_path = false; + + if (RESOURCE_IS_FILESYSTEM(t->target.type)) { + + assert(t->target.path); + + if (path_is_absolute(t->current_symlink)) { + link_path = t->current_symlink; + resolve_link_path = true; + } else { + buf = path_make_absolute(t->current_symlink, t->target.path); + if (!buf) + return log_oom(); + + link_path = buf; + } + + link_target = t->final_path; + + } else if (t->target.type == RESOURCE_PARTITION) { + + assert(path_is_absolute(t->current_symlink)); + + link_path = t->current_symlink; + link_target = t->partition_info.device; + + resolve_link_path = true; + } else + assert_not_reached(); + + if (resolve_link_path && root) { + r = chase_symlinks(link_path, root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL); + if (r < 0) + return log_error_errno(r, "Failed to resolve current symlink path '%s': %m", link_path); + + link_path = resolved; + } + + if (link_target) { + r = path_extract_directory(link_path, &parent); + if (r < 0) + return log_error_errno(r, "Failed to extract directory of target path '%s': %m", link_path); + + r = path_make_relative(parent, link_target, &relative); + if (r < 0) + return log_error_errno(r, "Failed to make symlink path '%s' relative to '%s': %m", link_target, parent); + + r = symlink_atomic(relative, link_path); + if (r < 0) + return log_error_errno(r, "Failed to update current symlink '%s' → '%s': %m", link_path, relative); + + log_info("Updated symlink '%s' → '%s'.", link_path, relative); + } + } + + return 0; +} diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h new file mode 100644 index 0000000000..b0c2a6e455 --- /dev/null +++ b/src/sysupdate/sysupdate-transfer.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-id128.h" + +/* Forward declare this type so that the headers below can use it */ +typedef struct Transfer Transfer; + +#include "sysupdate-partition.h" +#include "sysupdate-resource.h" + +struct Transfer { + char *definition_path; + char *min_version; + char **protected_versions; + char *current_symlink; + bool verify; + + Resource source, target; + + uint64_t instances_max; + bool remove_temporary; + + /* When creating a new partition/file, optionally override these attributes explicitly */ + sd_id128_t partition_uuid; + bool partition_uuid_set; + uint64_t partition_flags; + bool partition_flags_set; + mode_t mode; + uint64_t tries_left, tries_done; + int no_auto; + int read_only; + int growfs; + + /* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */ + char *temporary_path; + char *final_path; + int install_read_only; + + /* If we write to a partition in a partition table, the metrics of it */ + PartitionInfo partition_info; + PartitionChange partition_change; +}; + +Transfer *transfer_new(void); + +Transfer *transfer_free(Transfer *t); +DEFINE_TRIVIAL_CLEANUP_FUNC(Transfer*, transfer_free); + +int transfer_read_definition(Transfer *t, const char *path); + +int transfer_resolve_paths(Transfer *t, const char *root, const char *node); + +int transfer_vacuum(Transfer *t, uint64_t space, const char *extra_protected_version); + +int transfer_acquire_instance(Transfer *t, Instance *i); + +int transfer_install_instance(Transfer *t, Instance *i, const char *root); diff --git a/src/sysupdate/sysupdate-update-set.c b/src/sysupdate/sysupdate-update-set.c new file mode 100644 index 0000000000..6d6051d15a --- /dev/null +++ b/src/sysupdate/sysupdate-update-set.c @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "glyph-util.h" +#include "string-util.h" +#include "sysupdate-update-set.h" +#include "terminal-util.h" + +UpdateSet *update_set_free(UpdateSet *us) { + if (!us) + return NULL; + + free(us->version); + free(us->instances); /* The objects referenced by this array are freed via resource_free(), not us */ + + return mfree(us); +} + +int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b) { + assert(a); + assert(b); + assert(*a); + assert(*b); + assert((*a)->version); + assert((*b)->version); + + /* Newest version at the beginning */ + return -strverscmp_improved((*a)->version, (*b)->version); +} + +const char *update_set_flags_to_color(UpdateSetFlags flags) { + + if (flags == 0 || (flags & UPDATE_OBSOLETE)) + return (flags & UPDATE_NEWEST) ? ansi_highlight_grey() : ansi_grey(); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST)) + return ansi_highlight(); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED)) + return ansi_highlight_magenta(); + + if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST)) + return ansi_highlight_green(); + + return NULL; +} + +const char *update_set_flags_to_glyph(UpdateSetFlags flags) { + + if (flags == 0 || (flags & UPDATE_OBSOLETE)) + return special_glyph(SPECIAL_GLYPH_MULTIPLICATION_SIGN); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_NEWEST)) + return special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE); + + if (FLAGS_SET(flags, UPDATE_INSTALLED|UPDATE_PROTECTED)) + return special_glyph(SPECIAL_GLYPH_WHITE_CIRCLE); + + if ((flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_OBSOLETE)) == (UPDATE_AVAILABLE|UPDATE_NEWEST)) + return special_glyph(SPECIAL_GLYPH_CIRCLE_ARROW); + + return " "; +} diff --git a/src/sysupdate/sysupdate-update-set.h b/src/sysupdate/sysupdate-update-set.h new file mode 100644 index 0000000000..5dd94bce41 --- /dev/null +++ b/src/sysupdate/sysupdate-update-set.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> +#include <sys/types.h> + +typedef struct UpdateSet UpdateSet; + +#include "sysupdate-instance.h" + +typedef enum UpdateSetFlags { + UPDATE_NEWEST = 1 << 0, + UPDATE_AVAILABLE = 1 << 1, + UPDATE_INSTALLED = 1 << 2, + UPDATE_OBSOLETE = 1 << 3, + UPDATE_PROTECTED = 1 << 4, +} UpdateSetFlags; + +struct UpdateSet { + UpdateSetFlags flags; + char *version; + Instance **instances; + size_t n_instances; +}; + +UpdateSet *update_set_free(UpdateSet *us); + +int update_set_cmp(UpdateSet *const*a, UpdateSet *const*b); + +const char *update_set_flags_to_color(UpdateSetFlags flags); +const char *update_set_flags_to_glyph(UpdateSetFlags flags); diff --git a/src/sysupdate/sysupdate-util.c b/src/sysupdate/sysupdate-util.c new file mode 100644 index 0000000000..c7a23015ce --- /dev/null +++ b/src/sysupdate/sysupdate-util.c @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "path-util.h" +#include "sysupdate-util.h" + +bool version_is_valid(const char *s) { + if (isempty(s)) + return false; + + if (!filename_is_valid(s)) + return false; + + if (!in_charset(s, ALPHANUMERICAL ".,_-+")) + return false; + + return true; +} diff --git a/src/sysupdate/sysupdate-util.h b/src/sysupdate/sysupdate-util.h new file mode 100644 index 0000000000..afa3a9d498 --- /dev/null +++ b/src/sysupdate/sysupdate-util.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> + +bool version_is_valid(const char *s); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c new file mode 100644 index 0000000000..76c5bae051 --- /dev/null +++ b/src/sysupdate/sysupdate.c @@ -0,0 +1,1411 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <unistd.h> + +#include "bus-error.h" +#include "bus-locator.h" +#include "chase-symlinks.h" +#include "conf-files.h" +#include "def.h" +#include "dirent-util.h" +#include "dissect-image.h" +#include "fd-util.h" +#include "format-table.h" +#include "glyph-util.h" +#include "hexdecoct.h" +#include "login-util.h" +#include "main-func.h" +#include "mount-util.h" +#include "os-util.h" +#include "pager.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "set.h" +#include "sort-util.h" +#include "string-util.h" +#include "strv.h" +#include "sysupdate-transfer.h" +#include "sysupdate-update-set.h" +#include "sysupdate.h" +#include "terminal-util.h" +#include "utf8.h" +#include "verbs.h" + +static char *arg_definitions = NULL; +bool arg_sync = true; +uint64_t arg_instances_max = UINT64_MAX; +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +char *arg_root = NULL; +static char *arg_image = NULL; +static bool arg_reboot = false; +static char *arg_component = NULL; +static int arg_verify = -1; + +STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_component, freep); + +typedef struct Context { + Transfer **transfers; + size_t n_transfers; + + UpdateSet **update_sets; + size_t n_update_sets; + + UpdateSet *newest_installed, *candidate; + + Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */ +} Context; + +static Context *context_free(Context *c) { + if (!c) + return NULL; + + for (size_t i = 0; i < c->n_transfers; i++) + transfer_free(c->transfers[i]); + free(c->transfers); + + for (size_t i = 0; i < c->n_update_sets; i++) + update_set_free(c->update_sets[i]); + free(c->update_sets); + + hashmap_free(c->web_cache); + + return mfree(c); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free); + +static Context *context_new(void) { + /* For now, no fields to initialize non-zero */ + return new0(Context, 1); +} + +static int context_read_definitions( + Context *c, + const char *directory, + const char *component, + const char *root, + const char *node) { + + _cleanup_strv_free_ char **files = NULL; + int r; + + assert(c); + + 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")); + if (r < 0) + return log_error_errno(r, "Failed to enumerate *.conf files: %m"); + + STRV_FOREACH(f, files) { + _cleanup_(transfer_freep) Transfer *t = NULL; + + if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1)) + return log_oom(); + + t = transfer_new(); + if (!t) + return log_oom(); + + t->definition_path = strdup(*f); + if (!t->definition_path) + return log_oom(); + + r = transfer_read_definition(t, *f); + if (r < 0) + return r; + + c->transfers[c->n_transfers++] = TAKE_PTR(t); + } + + if (c->n_transfers == 0) { + if (arg_component) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "No transfer definitions for component '%s' found.", arg_component); + + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "No transfer definitions found."); + } + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_resolve_paths(c->transfers[i], root, node); + if (r < 0) + return r; + } + + return 0; +} + +static int context_load_installed_instances(Context *c) { + int r; + + assert(c); + + log_info("Discovering installed instances…"); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = resource_load_instances( + &c->transfers[i]->target, + arg_verify >= 0 ? arg_verify : c->transfers[i]->verify, + &c->web_cache); + if (r < 0) + return r; + } + + return 0; +} + +static int context_load_available_instances(Context *c) { + int r; + + assert(c); + + log_info("Discovering available instances…"); + + for (size_t i = 0; i < c->n_transfers; i++) { + assert(c->transfers[i]); + + r = resource_load_instances( + &c->transfers[i]->source, + arg_verify >= 0 ? arg_verify : c->transfers[i]->verify, + &c->web_cache); + if (r < 0) + return r; + } + + return 0; +} + +static int context_discover_update_sets_by_flag(Context *c, UpdateSetFlags flags) { + _cleanup_free_ Instance **cursor_instances = NULL; + _cleanup_free_ char *boundary = NULL; + bool newest_found = false; + int r; + + assert(c); + assert(IN_SET(flags, UPDATE_AVAILABLE, UPDATE_INSTALLED)); + + for (;;) { + bool incomplete = false, exists = false; + UpdateSetFlags extra_flags = 0; + _cleanup_free_ char *cursor = NULL; + UpdateSet *us = NULL; + + for (size_t k = 0; k < c->n_transfers; k++) { + Transfer *t = c->transfers[k]; + bool cursor_found = false; + Resource *rr; + + assert(t); + + if (flags == UPDATE_AVAILABLE) + rr = &t->source; + else { + assert(flags == UPDATE_INSTALLED); + rr = &t->target; + } + + for (size_t j = 0; j < rr->n_instances; j++) { + Instance *i = rr->instances[j]; + + assert(i); + + /* Is the instance we are looking at equal or newer than the boundary? If so, we + * already checked this version, and it wasn't complete, let's ignore it. */ + if (boundary && strverscmp_improved(i->metadata.version, boundary) >= 0) + continue; + + if (cursor) { + if (strverscmp_improved(i->metadata.version, cursor) != 0) + continue; + } else { + cursor = strdup(i->metadata.version); + if (!cursor) + return log_oom(); + } + + cursor_found = true; + + if (!cursor_instances) { + cursor_instances = new(Instance*, c->n_transfers); + if (!cursor_instances) + return -ENOMEM; + } + cursor_instances[k] = i; + break; + } + + if (!cursor) /* No suitable instance beyond the boundary found? Then we are done! */ + break; + + if (!cursor_found) { + /* Hmm, we didn't find the version indicated by 'cursor' among the instances + * of this transfer, let's skip it. */ + incomplete = true; + break; + } + + if (t->min_version && strverscmp_improved(t->min_version, cursor) > 0) + extra_flags |= UPDATE_OBSOLETE; + + if (strv_contains(t->protected_versions, cursor)) + extra_flags |= UPDATE_PROTECTED; + } + + if (!cursor) /* EOL */ + break; + + r = free_and_strdup_warn(&boundary, cursor); + if (r < 0) + return r; + + if (incomplete) /* One transfer was missing this version, ignore the whole thing */ + continue; + + /* See if we already have this update set in our table */ + for (size_t i = 0; i < c->n_update_sets; i++) { + if (strverscmp_improved(c->update_sets[i]->version, cursor) != 0) + continue; + + /* We only store the instances we found first, but we remember we also found it again */ + c->update_sets[i]->flags |= flags | extra_flags; + exists = true; + newest_found = true; + break; + } + + if (exists) + continue; + + /* Doesn't exist yet, let's add it */ + if (!GREEDY_REALLOC(c->update_sets, c->n_update_sets + 1)) + return log_oom(); + + us = new(UpdateSet, 1); + if (!us) + return log_oom(); + + *us = (UpdateSet) { + .flags = flags | (newest_found ? 0 : UPDATE_NEWEST) | extra_flags, + .version = TAKE_PTR(cursor), + .instances = TAKE_PTR(cursor_instances), + .n_instances = c->n_transfers, + }; + + c->update_sets[c->n_update_sets++] = us; + + newest_found = true; + + /* Remember which one is the newest installed */ + if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED)) == (UPDATE_NEWEST|UPDATE_INSTALLED)) + c->newest_installed = us; + + /* Remember which is the newest non-obsolete, available (and not installed) version, which we declare the "candidate" */ + if ((us->flags & (UPDATE_NEWEST|UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE)) == (UPDATE_NEWEST|UPDATE_AVAILABLE)) + c->candidate = us; + } + + /* Newest installed is newer than or equal to candidate? Then suppress the candidate */ + if (c->newest_installed && c->candidate && strverscmp_improved(c->newest_installed->version, c->candidate->version) >= 0) + c->candidate = NULL; + + return 0; +} + +static int context_discover_update_sets(Context *c) { + int r; + + assert(c); + + log_info("Determining installed update sets…"); + + r = context_discover_update_sets_by_flag(c, UPDATE_INSTALLED); + if (r < 0) + return r; + + log_info("Determining available update sets…"); + + r = context_discover_update_sets_by_flag(c, UPDATE_AVAILABLE); + if (r < 0) + return r; + + typesafe_qsort(c->update_sets, c->n_update_sets, update_set_cmp); + return 0; +} + +static const char *update_set_flags_to_string(UpdateSetFlags flags) { + + switch ((unsigned) flags) { + + case 0: + return "n/a"; + + case UPDATE_INSTALLED|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current"; + + case UPDATE_AVAILABLE|UPDATE_NEWEST: + case UPDATE_AVAILABLE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "candidate"; + + case UPDATE_INSTALLED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE: + return "installed"; + + case UPDATE_INSTALLED|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_PROTECTED: + return "protected"; + + case UPDATE_AVAILABLE: + case UPDATE_AVAILABLE|UPDATE_PROTECTED: + return "available"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "current+obsolete"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE: + return "installed+obsolete"; + + case UPDATE_INSTALLED|UPDATE_OBSOLETE|UPDATE_PROTECTED: + case UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED: + return "protected+obsolete"; + + case UPDATE_AVAILABLE|UPDATE_OBSOLETE: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_PROTECTED: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST: + case UPDATE_AVAILABLE|UPDATE_OBSOLETE|UPDATE_NEWEST|UPDATE_PROTECTED: + return "available+obsolete"; + + default: + assert_not_reached(); + } +} + + +static int context_show_table(Context *c) { + _cleanup_(table_unrefp) Table *t = NULL; + int r; + + assert(c); + + t = table_new("", "version", "installed", "available", "assessment"); + if (!t) + return log_oom(); + + (void) table_set_align_percent(t, table_get_cell(t, 0, 0), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 50); + (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 50); + + for (size_t i = 0; i < c->n_update_sets; i++) { + UpdateSet *us = c->update_sets[i]; + const char *color; + + color = update_set_flags_to_color(us->flags); + + r = table_add_many(t, + TABLE_STRING, update_set_flags_to_glyph(us->flags), + TABLE_SET_COLOR, color, + TABLE_STRING, us->version, + TABLE_SET_COLOR, color, + TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_INSTALLED)), + TABLE_SET_COLOR, color, + TABLE_STRING, special_glyph_check_mark_space(FLAGS_SET(us->flags, UPDATE_AVAILABLE)), + TABLE_SET_COLOR, color, + TABLE_STRING, update_set_flags_to_string(us->flags), + TABLE_SET_COLOR, color); + if (r < 0) + return table_log_add_error(r); + } + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +static UpdateSet *context_update_set_by_version(Context *c, const char *version) { + assert(c); + assert(version); + + for (size_t i = 0; i < c->n_update_sets; i++) + if (streq(c->update_sets[i]->version, version)) + return c->update_sets[i]; + + return NULL; +} + +static int context_show_version(Context *c, const char *version) { + bool show_fs_columns = false, show_partition_columns = false, + have_fs_attributes = false, have_partition_attributes = false, + have_size = false, have_tries = false, have_no_auto = false, + have_read_only = false, have_growfs = false, have_sha256 = false; + _cleanup_(table_unrefp) Table *t = NULL; + UpdateSet *us; + int r; + + assert(c); + assert(version); + + us = context_update_set_by_version(c, version); + if (!us) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version); + + if (arg_json_format_flags & (JSON_FORMAT_OFF|JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO)) + (void) pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + printf("%s%s%s Version: %s\n" + " State: %s%s%s\n" + "Installed: %s%s\n" + "Available: %s%s\n" + "Protected: %s%s%s\n" + " Obsolete: %s%s%s\n\n", + strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_glyph(us->flags), ansi_normal(), us->version, + strempty(update_set_flags_to_color(us->flags)), update_set_flags_to_string(us->flags), ansi_normal(), + yes_no(us->flags & UPDATE_INSTALLED), FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_NEWEST) ? " (newest)" : "", + yes_no(us->flags & UPDATE_AVAILABLE), (us->flags & (UPDATE_INSTALLED|UPDATE_AVAILABLE|UPDATE_NEWEST)) == (UPDATE_AVAILABLE|UPDATE_NEWEST) ? " (newest)" : "", + FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED) ? ansi_highlight() : "", yes_no(FLAGS_SET(us->flags, UPDATE_INSTALLED|UPDATE_PROTECTED)), ansi_normal(), + us->flags & UPDATE_OBSOLETE ? ansi_highlight_red() : "", yes_no(us->flags & UPDATE_OBSOLETE), ansi_normal()); + + + t = table_new("type", "path", "ptuuid", "ptflags", "mtime", "mode", "size", "tries-done", "tries-left", "noauto", "ro", "growfs", "sha256"); + if (!t) + return log_oom(); + + (void) table_set_align_percent(t, table_get_cell(t, 0, 3), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 4), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 6), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 7), 100); + (void) table_set_align_percent(t, table_get_cell(t, 0, 8), 100); + (void) table_set_empty_string(t, "-"); + + /* Determine if the target will make use of partition/fs attributes for any of the transfers */ + for (size_t n = 0; n < c->n_transfers; n++) { + Transfer *tr = c->transfers[n]; + + if (tr->target.type == RESOURCE_PARTITION) + show_partition_columns = true; + if (RESOURCE_IS_FILESYSTEM(tr->target.type)) + show_fs_columns = true; + } + + for (size_t n = 0; n < us->n_instances; n++) { + Instance *i = us->instances[n]; + + r = table_add_many(t, + TABLE_STRING, resource_type_to_string(i->resource->type), + TABLE_PATH, i->path); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.partition_uuid_set) { + have_partition_attributes = true; + r = table_add_cell(t, NULL, TABLE_UUID, &i->metadata.partition_uuid); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.partition_flags_set) { + have_partition_attributes = true; + r = table_add_cell(t, NULL, TABLE_UINT64_HEX, &i->metadata.partition_flags); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.mtime != USEC_INFINITY) { + have_fs_attributes = true; + r = table_add_cell(t, NULL, TABLE_TIMESTAMP, &i->metadata.mtime); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.mode != MODE_INVALID) { + have_fs_attributes = true; + r = table_add_cell(t, NULL, TABLE_MODE, &i->metadata.mode); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.size != UINT64_MAX) { + have_size = true; + r = table_add_cell(t, NULL, TABLE_SIZE, &i->metadata.size); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.tries_done != UINT64_MAX) { + have_tries = true; + r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_done); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.tries_left != UINT64_MAX) { + have_tries = true; + r = table_add_cell(t, NULL, TABLE_UINT64, &i->metadata.tries_left); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.no_auto >= 0) { + bool b; + + have_no_auto = true; + b = i->metadata.no_auto; + r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + if (i->metadata.read_only >= 0) { + bool b; + + have_read_only = true; + b = i->metadata.read_only; + r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.growfs >= 0) { + bool b; + + have_growfs = true; + b = i->metadata.growfs; + r = table_add_cell(t, NULL, TABLE_BOOLEAN, &b); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (i->metadata.sha256sum_set) { + _cleanup_free_ char *formatted = NULL; + + have_sha256 = true; + + formatted = hexmem(i->metadata.sha256sum, sizeof(i->metadata.sha256sum)); + if (!formatted) + return log_oom(); + + r = table_add_cell(t, NULL, TABLE_STRING, formatted); + } else + r = table_add_cell(t, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + } + + /* Hide the fs/partition columns if we don't have any data to show there */ + if (!have_fs_attributes) + show_fs_columns = false; + if (!have_partition_attributes) + show_partition_columns = false; + + if (!show_partition_columns) + (void) table_hide_column_from_display(t, 2, 3); + if (!show_fs_columns) + (void) table_hide_column_from_display(t, 4, 5); + if (!have_size) + (void) table_hide_column_from_display(t, 6); + if (!have_tries) + (void) table_hide_column_from_display(t, 7, 8); + if (!have_no_auto) + (void) table_hide_column_from_display(t, 9); + if (!have_read_only) + (void) table_hide_column_from_display(t, 10); + if (!have_growfs) + (void) table_hide_column_from_display(t, 11); + if (!have_sha256) + (void) table_hide_column_from_display(t, 12); + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +static int context_vacuum( + Context *c, + uint64_t space, + const char *extra_protected_version) { + + int r, count = 0; + + assert(c); + + if (space == 0) + log_info("Making room…"); + else + log_info("Making room for %" PRIu64 " updates…", space); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_vacuum(c->transfers[i], space, extra_protected_version); + if (r < 0) + return r; + + count = MAX(count, r); + } + + if (count > 0) + log_info("Removed %i instances.", count); + else + log_info("Removed no instances."); + + return 0; +} + +static int context_make_offline(Context **ret, const char *node) { + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(ret); + + /* Allocates a context object and initializes everything we can initialize offline, i.e. without + * checking on the update source (i.e. the Internet) what versions are available */ + + context = context_new(); + if (!context) + return log_oom(); + + r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node); + if (r < 0) + return r; + + r = context_load_installed_instances(context); + if (r < 0) + return r; + + *ret = TAKE_PTR(context); + return 0; +} + +static int context_make_online(Context **ret, const char *node) { + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(ret); + + /* Like context_make_offline(), but also communicates with the update source looking for new + * versions. */ + + r = context_make_offline(&context, node); + if (r < 0) + return r; + + r = context_load_available_instances(context); + if (r < 0) + return r; + + r = context_discover_update_sets(context); + if (r < 0) + return r; + + *ret = TAKE_PTR(context); + return 0; +} + +static int context_apply( + Context *c, + const char *version, + UpdateSet **ret_applied) { + + UpdateSet *us = NULL; + int r; + + assert(c); + + if (version) { + us = context_update_set_by_version(c, version); + if (!us) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Update '%s' not found.", version); + } else { + if (!c->candidate) { + log_info("No update needed."); + + if (ret_applied) + *ret_applied = NULL; + + return 0; + } + + us = c->candidate; + } + + if (FLAGS_SET(us->flags, UPDATE_INSTALLED)) { + log_info("Selected update '%s' is already installed. Skipping update.", us->version); + + if (ret_applied) + *ret_applied = NULL; + + return 0; + } + if (!FLAGS_SET(us->flags, UPDATE_AVAILABLE)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is not available, refusing.", us->version); + if (FLAGS_SET(us->flags, UPDATE_OBSOLETE)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected update '%s' is obsolete, refusing.", us->version); + + assert((us->flags & (UPDATE_AVAILABLE|UPDATE_INSTALLED|UPDATE_OBSOLETE)) == UPDATE_AVAILABLE); + + if (!FLAGS_SET(us->flags, UPDATE_NEWEST)) + log_notice("Selected update '%s' is not the newest, proceeding anyway.", us->version); + if (c->newest_installed && strverscmp_improved(c->newest_installed->version, us->version) > 0) + log_notice("Selected update '%s' is older than newest installed version, proceeding anyway.", us->version); + + log_info("Selected update '%s' for install.", us->version); + + (void) sd_notifyf(false, + "STATUS=Making room for '%s'.", us->version); + + /* Let's make some room. We make sure for each transfer we have one free space to fill. While + * removing stuff we'll protect the version we are trying to acquire. Why that? Maybe an earlier + * download succeeded already, in which case we shouldn't remove it just to acquire it again */ + r = context_vacuum( + c, + /* space = */ 1, + /* extra_protected_version = */ us->version); + if (r < 0) + return r; + + if (arg_sync) + sync(); + + (void) sd_notifyf(false, + "STATUS=Updating to '%s'.\n", us->version); + + /* There should now be one instance picked for each transfer, and the order is the same */ + assert(us->n_instances == c->n_transfers); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_acquire_instance(c->transfers[i], us->instances[i]); + if (r < 0) + return r; + } + + if (arg_sync) + sync(); + + for (size_t i = 0; i < c->n_transfers; i++) { + r = transfer_install_instance(c->transfers[i], us->instances[i], arg_root); + if (r < 0) + return r; + } + + log_info("%s Successfully installed update '%s'.", special_glyph(SPECIAL_GLYPH_SPARKLES), us->version); + + if (ret_applied) + *ret_applied = us; + + return 1; +} + +static int reboot_now(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL; + int r; + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "Failed to open bus connection: %m"); + + r = bus_call_method(bus, bus_login_mgr, "RebootWithFlags", &error, NULL, "t", + (uint64_t) SD_LOGIND_ROOT_CHECK_INHIBITORS); + if (r < 0) + return log_error_errno(r, "Failed to issue reboot request: %s", bus_error_message(&error, r)); + + return 0; +} + +static int process_image( + bool ro, + char **ret_mounted_dir, + LoopDevice **ret_loop_device, + DecryptedImage **ret_decrypted_image) { + + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + int r; + + assert(ret_mounted_dir); + assert(ret_loop_device); + assert(ret_decrypted_image); + + if (!arg_image) + return 0; + + assert(!arg_root); + + r = mount_image_privately_interactively( + arg_image, + (ro ? DISSECT_IMAGE_READ_ONLY : 0) | + DISSECT_IMAGE_FSCK | + DISSECT_IMAGE_MKDIR | + DISSECT_IMAGE_GROWFS | + DISSECT_IMAGE_RELAX_VAR_CHECK | + DISSECT_IMAGE_USR_NO_ROOT | + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_REQUIRE_ROOT, + &mounted_dir, + &loop_device, + &decrypted_image); + if (r < 0) + return r; + + arg_root = strdup(mounted_dir); + if (!arg_root) + return log_oom(); + + *ret_mounted_dir = TAKE_PTR(mounted_dir); + *ret_loop_device = TAKE_PTR(loop_device); + *ret_decrypted_image = TAKE_PTR(decrypted_image); + + return 0; +} + +static int verb_list(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + const char *version; + int r; + + assert(argc <= 2); + version = argc >= 2 ? argv[1] : NULL; + + r = process_image(/* ro= */ true, &mounted_dir, &loop_device, &decrypted_image); + if (r < 0) + return r; + + r = context_make_online(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + if (version) + return context_show_version(context, version); + else + return context_show_table(context); +} + +static int verb_check_new(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(argc <= 1); + + r = process_image(/* ro= */ true, &mounted_dir, &loop_device, &decrypted_image); + if (r < 0) + return r; + + r = context_make_online(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + if (!context->candidate) { + log_debug("No candidate found."); + return EXIT_FAILURE; + } + + puts(context->candidate->version); + return EXIT_SUCCESS; +} + +static int verb_vacuum(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + int r; + + assert(argc <= 1); + + r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image); + if (r < 0) + return r; + + r = context_make_offline(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + return context_vacuum(context, 0, NULL); +} + +static int verb_update(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(context_freep) Context* context = NULL; + _cleanup_free_ char *booted_version = NULL; + UpdateSet *applied = NULL; + const char *version; + int r; + + assert(argc <= 2); + version = argc >= 2 ? argv[1] : NULL; + + if (arg_reboot) { + /* If automatic reboot on completion is requested, let's first determine the currently booted image */ + + r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version); + if (r < 0) + return log_error_errno(r, "Failed to parse /etc/os-release: %m"); + if (!booted_version) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field."); + } + + r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image); + if (r < 0) + return r; + + r = context_make_online(&context, loop_device ? loop_device->node : NULL); + if (r < 0) + return r; + + r = context_apply(context, version, &applied); + if (r < 0) + return r; + + if (r > 0 && arg_reboot) { + assert(applied); + assert(booted_version); + + if (strverscmp_improved(applied->version, booted_version) > 0) { + log_notice("Newly installed version is newer than booted version, rebooting."); + return reboot_now(); + } + + log_info("Booted version is newer or identical to newly installed version, not rebooting."); + } + + return 0; +} + +static int verb_pending_or_reboot(int argc, char **argv, void *userdata) { + _cleanup_(context_freep) Context* context = NULL; + _cleanup_free_ char *booted_version = NULL; + int r; + + assert(argc == 1); + + if (arg_image || arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --root=/--image switches may not be combined with the '%s' operation.", argv[0]); + + r = context_make_offline(&context, NULL); + if (r < 0) + return r; + + log_info("Determining installed update sets…"); + + r = context_discover_update_sets_by_flag(context, UPDATE_INSTALLED); + if (r < 0) + return r; + if (!context->newest_installed) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Couldn't find any suitable installed versions."); + + r = parse_os_release(arg_root, "IMAGE_VERSION", &booted_version); + if (r < 0) /* yes, arg_root is NULL here, but we have to pass something, and it's a lot more readable + * if we see what the first argument is about */ + return log_error_errno(r, "Failed to parse /etc/os-release: %m"); + if (!booted_version) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION= field."); + + r = strverscmp_improved(context->newest_installed->version, booted_version); + if (r > 0) { + log_notice("Newest installed version '%s' is newer than booted version '%s'.%s", + context->newest_installed->version, booted_version, + streq(argv[0], "pending") ? " Reboot recommended." : ""); + + if (streq(argv[0], "reboot")) + return reboot_now(); + + return EXIT_SUCCESS; + } else if (r == 0) + log_info("Newest installed version '%s' matches booted version '%s'.", + context->newest_installed->version, booted_version); + else + log_warning("Newest installed version '%s' is older than booted version '%s'.", + context->newest_installed->version, booted_version); + + if (streq(argv[0], "pending")) /* When called as 'pending' tell the caller via failure exit code that there's nothing newer installed */ + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} + +static int component_name_valid(const char *c) { + _cleanup_free_ char *j = NULL; + + /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */ + + if (isempty(c)) + return false; + + if (string_has_cc(c, NULL)) + return false; + + if (!utf8_is_valid(c)) + return false; + + j = strjoin("sysupdate.", c, ".d"); + if (!j) + return -ENOMEM; + + return filename_is_valid(j); +} + +static int verb_components(int argc, char **argv, void *userdata) { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; + _cleanup_(set_freep) Set *names = NULL; + _cleanup_free_ char **z = NULL; /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */ + char **l = CONF_PATHS_STRV(""); + bool has_default_component = false; + int r; + + assert(argc <= 1); + + r = process_image(/* ro= */ false, &mounted_dir, &loop_device, &decrypted_image); + if (r < 0) + return r; + + STRV_FOREACH(i, l) { + _cleanup_closedir_ DIR *d = NULL; + _cleanup_free_ char *p = NULL; + + r = chase_symlinks_and_opendir(*i, arg_root, CHASE_PREFIX_ROOT, &p, &d); + if (r == -ENOENT) + continue; + if (r < 0) + return log_error_errno(r, "Failed to open directory '%s': %m", *i); + + for (;;) { + _cleanup_free_ char *n = NULL; + struct dirent *de; + const char *e, *a; + + de = readdir_ensure_type(d); + if (!de) { + if (errno != 0) + return log_error_errno(errno, "Failed to enumerate directory '%s': %m", p); + + break; + } + + if (de->d_type != DT_DIR) + continue; + + if (dot_or_dot_dot(de->d_name)) + continue; + + if (streq(de->d_name, "sysupdate.d")) { + has_default_component = true; + continue; + } + + e = startswith(de->d_name, "sysupdate."); + if (!e) + continue; + + a = endswith(e, ".d"); + if (!a) + continue; + + n = strndup(e, a - e); + if (!n) + return log_oom(); + + r = component_name_valid(n); + if (r < 0) + return log_error_errno(r, "Unable to validate component name: %m"); + if (r == 0) + continue; + + r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n)); + if (r < 0 && r != -EEXIST) + return log_error_errno(r, "Failed to add component to set: %m"); + } + } + + if (!has_default_component && set_isempty(names)) { + log_info("No components defined."); + return 0; + } + + z = set_get_strv(names); + if (!z) + return log_oom(); + + strv_sort(z); + + if (has_default_component) + printf("%s<default>%s\n", + ansi_highlight(), ansi_normal()); + + STRV_FOREACH(i, z) + puts(*i); + + return 0; +} + +static int verb_help(int argc, char **argv, void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-sysupdate", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] [VERSION]\n" + "\n%5$sUpdate OS images.%6$s\n" + "\n%3$sCommands:%4$s\n" + " list [VERSION] Show installed and available versions\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" + " pending Report whether a newer version is installed than\n" + " currently booted\n" + " reboot Reboot if a newer version is installed than booted\n" + " components Show list of components\n" + " -h --help Show this help\n" + " --version Show package version\n" + "\n%3$sOptions:%4$s\n" + " -C --component=NAME Select component to update\n" + " --definitions=DIR Find transfer definitions in specified directory\n" + " --root=PATH Operate relative to root path\n" + " --image=PATH Operate relative to image file\n" + " -m --instances-max=INT How many instances to maintain\n" + " --sync=BOOL Controls whether to sync data to disk\n" + " --verify=BOOL Force signature verification on or off\n" + " --reboot Reboot after updating to newer version\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" + " --json=pretty|short|off\n" + " Generate JSON output\n" + "\nSee the %2$s for details.\n" + , program_invocation_short_name + , link + , ansi_underline(), ansi_normal() + , ansi_highlight(), ansi_normal() + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_SYNC, + ARG_DEFINITIONS, + ARG_JSON, + ARG_ROOT, + ARG_IMAGE, + ARG_REBOOT, + ARG_VERIFY, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "definitions", required_argument, NULL, ARG_DEFINITIONS }, + { "instances-max", required_argument, NULL, 'm' }, + { "sync", required_argument, NULL, ARG_SYNC }, + { "json", required_argument, NULL, ARG_JSON }, + { "root", required_argument, NULL, ARG_ROOT }, + { "image", required_argument, NULL, ARG_IMAGE }, + { "reboot", no_argument, NULL, ARG_REBOOT }, + { "component", required_argument, NULL, 'C' }, + { "verify", required_argument, NULL, ARG_VERIFY }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hm:C:", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return verb_help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case 'm': + r = safe_atou64(optarg, &arg_instances_max); + if (r < 0) + return log_error_errno(r, "Failed to parse --instances-max= parameter: %s", optarg); + + break; + + case ARG_SYNC: + r = parse_boolean_argument("--sync=", optarg, &arg_sync); + if (r < 0) + return r; + break; + + case ARG_DEFINITIONS: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_definitions); + if (r < 0) + return r; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case ARG_ROOT: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root); + if (r < 0) + return r; + break; + + case ARG_IMAGE: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); + if (r < 0) + return r; + break; + + case ARG_REBOOT: + arg_reboot = true; + break; + + case 'C': + if (isempty(optarg)) { + arg_component = mfree(arg_component); + break; + } + + r = component_name_valid(optarg); + if (r < 0) + return log_error_errno(r, "Failed to determine if component name is valid: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", optarg); + + r = free_and_strdup_warn(&arg_component, optarg); + if (r < 0) + return r; + + break; + + case ARG_VERIFY: { + bool b; + + r = parse_boolean_argument("--verify=", optarg, &b); + if (r < 0) + return r; + + arg_verify = b; + break; + } + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + if (arg_image && arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported."); + + if ((arg_image || arg_root) && arg_reboot) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --root= or --image=."); + + if (arg_definitions && arg_component) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined."); + + return 1; +} + +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 }, + { "check-new", VERB_ANY, 1, 0, verb_check_new }, + { "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 }, + { "help", VERB_ANY, 1, 0, verb_help }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return sysupdate_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h new file mode 100644 index 0000000000..6d387b7a5d --- /dev/null +++ b/src/sysupdate/sysupdate.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> +#include <stdbool.h> + +extern bool arg_sync; +extern uint64_t arg_instances_max; +extern char *arg_root; + +static inline const char* import_binary_path(void) { + return secure_getenv("SYSTEMD_IMPORT_PATH") ?: SYSTEMD_IMPORT_PATH; +} + +static inline const char* import_fs_binary_path(void) { + return secure_getenv("SYSTEMD_IMPORT_FS_PATH") ?: SYSTEMD_IMPORT_FS_PATH; +} + +static inline const char *pull_binary_path(void) { + return secure_getenv("SYSTEMD_PULL_PATH") ?: SYSTEMD_PULL_PATH; +} diff --git a/src/test/test-strv.c b/src/test/test-strv.c index ae3e2798c2..edb782eb0d 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -639,8 +639,13 @@ TEST(strv_foreach_backwards) { STRV_FOREACH_BACKWARDS(check, (char**) NULL) assert_not_reached(); - STRV_FOREACH_BACKWARDS(check, (char**) { NULL }) + STRV_FOREACH_BACKWARDS(check, STRV_MAKE_EMPTY) assert_not_reached(); + + unsigned count = 0; + STRV_FOREACH_BACKWARDS(check, STRV_MAKE("ONE")) + count++; + assert_se(count == 1); } TEST(strv_foreach_pair) { diff --git a/test/TEST-72-SYSUPDATE/Makefile b/test/TEST-72-SYSUPDATE/Makefile new file mode 120000 index 0000000000..e9f93b1104 --- /dev/null +++ b/test/TEST-72-SYSUPDATE/Makefile @@ -0,0 +1 @@ +../TEST-01-BASIC/Makefile
\ No newline at end of file diff --git a/test/TEST-72-SYSUPDATE/test.sh b/test/TEST-72-SYSUPDATE/test.sh new file mode 100755 index 0000000000..471b02e9a3 --- /dev/null +++ b/test/TEST-72-SYSUPDATE/test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh +set -e + +TEST_DESCRIPTION="test sysupdate" + +# shellcheck source=test/test-functions +. "${TEST_BASE_DIR:?}/test-functions" + +test_append_files() { + inst_binary sha256sum +} + +do_test "$@" diff --git a/test/units/testsuite-72.service b/test/units/testsuite-72.service new file mode 100644 index 0000000000..1640350578 --- /dev/null +++ b/test/units/testsuite-72.service @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-72-SYSUPDATE + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-72.sh b/test/units/testsuite-72.sh new file mode 100755 index 0000000000..9effc982ba --- /dev/null +++ b/test/units/testsuite-72.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh +set -eux +set -o pipefail + +SYSUPDATE=/lib/systemd/systemd-sysupdate + +if ! test -x "$SYSUPDATE"; then + echo "no systemd-sysupdate" >/skipped + exit 0 +fi + +export SYSTEMD_PAGER=cat +export SYSTEMD_LOG_LEVEL=debug + +rm -f /var/tmp/72-joined.raw +truncate -s 10M /var/tmp/72-joined.raw + +sfdisk /var/tmp/72-joined.raw <<EOF +label: gpt +unit: sectors +sector-size: 512 + +size=2048, type=4f68bce3-e8cd-4db1-96e7-fbcaf984b709, name=_empty +size=2048, type=4f68bce3-e8cd-4db1-96e7-fbcaf984b709, name=_empty +size=2048, type=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, name=_empty +size=2048, type=2c7357ed-ebd2-46d9-aec1-23d437ec2bf5, name=_empty +EOF + +rm -rf /var/tmp/72-dirs + +rm -rf /var/tmp/72-defs +mkdir -p /var/tmp/72-defs + +cat >/var/tmp/72-defs/01-first.conf <<"EOF" +[Source] +Type=regular-file +Path=/var/tmp/72-source +MatchPattern=part1-@v.raw + +[Target] +Type=partition +Path=/var/tmp/72-joined.raw +MatchPattern=part1-@v +MatchPartitionType=root-x86-64 +EOF + +cat >/var/tmp/72-defs/02-second.conf <<"EOF" +[Source] +Type=regular-file +Path=/var/tmp/72-source +MatchPattern=part2-@v.raw.gz + +[Target] +Type=partition +Path=/var/tmp/72-joined.raw +MatchPattern=part2-@v +MatchPartitionType=root-x86-64-verity +EOF + +cat >/var/tmp/72-defs/03-third.conf <<"EOF" +[Source] +Type=directory +Path=/var/tmp/72-source +MatchPattern=dir-@v + +[Target] +Type=directory +Path=/var/tmp/72-dirs +CurrentSymlink=/var/tmp/72-dirs/current +MatchPattern=dir-@v +InstancesMax=3 +EOF + +rm -rf /var/tmp/72-source +mkdir -p /var/tmp/72-source + +new_version() { + # Create a pair of random partition payloads, and compress one + dd if=/dev/urandom of="/var/tmp/72-source/part1-$1.raw" bs=1024 count=1024 + dd if=/dev/urandom of="/var/tmp/72-source/part2-$1.raw" bs=1024 count=1024 + gzip -k -f "/var/tmp/72-source/part2-$1.raw" + + mkdir -p "/var/tmp/72-source/dir-$1" + echo $RANDOM >"/var/tmp/72-source/dir-$1/foo.txt" + echo $RANDOM >"/var/tmp/72-source/dir-$1/bar.txt" + + tar --numeric-owner -C "/var/tmp/72-source/dir-$1/" -czf "/var/tmp/72-source/dir-$1.tar.gz" . + + ( cd /var/tmp/72-source/ && sha256sum part* dir-*.tar.gz >SHA256SUMS ) +} + +update_now() { + # Update to newest version. First there should be an update ready, then we + # do the update, and then there should not be any ready anymore + + "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no check-new + "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no update + ( ! "$SYSUPDATE" --definitions=/var/tmp/72-defs --verify=no check-new ) +} + +verify_version() { + # Expects: version ID + sector offset of both partitions to compare + dd if=/var/tmp/72-joined.raw bs=1024 skip="$2" count=1024 | cmp "/var/tmp/72-source/part1-$1.raw" + dd if=/var/tmp/72-joined.raw bs=1024 skip="$3" count=1024 | cmp "/var/tmp/72-source/part2-$1.raw" + cmp "/var/tmp/72-source/dir-$1/foo.txt" /var/tmp/72-dirs/current/foo.txt + cmp "/var/tmp/72-source/dir-$1/bar.txt" /var/tmp/72-dirs/current/bar.txt +} + +# Install initial version and verify +new_version v1 +update_now +verify_version v1 1024 3072 + +# Create second version, update and verify that it is added +new_version v2 +update_now +verify_version v2 2048 4096 + +# Create third version, update and verify it replaced the first version +new_version v3 +update_now +verify_version v3 1024 3072 + +# Create fourth version, and update through a file:// URL. This should be +# almost as good as testing HTTP, but is simpler for us to set up. file:// is +# abstracted in curl for us, and since our main goal is to test our own code +# (and not curl) this test should be quite good even if not comprehensive. This +# will test the SHA256SUMS logic at least (we turn off GPG validation though, +# see above) +new_version v4 + +cat >/var/tmp/72-defs/02-second.conf <<"EOF" +[Source] +Type=url-file +Path=file:///var/tmp/72-source +MatchPattern=part2-@v.raw.gz + +[Target] +Type=partition +Path=/var/tmp/72-joined.raw +MatchPattern=part2-@v +MatchPartitionType=root-x86-64-verity +EOF + +cat >/var/tmp/72-defs/03-third.conf <<"EOF" +[Source] +Type=url-tar +Path=file:///var/tmp/72-source +MatchPattern=dir-@v.tar.gz + +[Target] +Type=directory +Path=/var/tmp/72-dirs +CurrentSymlink=/var/tmp/72-dirs/current +MatchPattern=dir-@v +InstancesMax=3 +EOF + +update_now +verify_version v4 2048 4096 + +rm /var/tmp/72-joined.raw +rm -r /var/tmp/72-dirs /var/tmp/72-defs /var/tmp/72-source + +echo OK >/testok + +exit 0 diff --git a/units/meson.build b/units/meson.build index 2bb0a8e845..8a3bd0da51 100644 --- a/units/meson.build +++ b/units/meson.build @@ -140,6 +140,8 @@ units = [ ['systemd-reboot.service', ''], ['systemd-rfkill.socket', 'ENABLE_RFKILL'], ['systemd-sysext.service', 'ENABLE_SYSEXT'], + ['systemd-sysupdate.timer', 'ENABLE_SYSUPDATE'], + ['systemd-sysupdate-reboot.timer', 'ENABLE_SYSUPDATE'], ['systemd-sysusers.service', 'ENABLE_SYSUSERS', 'sysinit.target.wants/'], ['systemd-tmpfiles-clean.service', 'ENABLE_TMPFILES'], @@ -236,6 +238,8 @@ in_units = [ ['systemd-suspend.service', ''], ['systemd-sysctl.service', '', 'sysinit.target.wants/'], + ['systemd-sysupdate.service', 'ENABLE_SYSUPDATE'], + ['systemd-sysupdate-reboot.service', 'ENABLE_SYSUPDATE'], ['systemd-timedated.service', 'ENABLE_TIMEDATED', 'dbus-org.freedesktop.timedate1.service'], ['systemd-timesyncd.service', 'ENABLE_TIMESYNCD'], diff --git a/units/systemd-sysupdate-reboot.service.in b/units/systemd-sysupdate-reboot.service.in new file mode 100644 index 0000000000..9d7b7d1657 --- /dev/null +++ b/units/systemd-sysupdate-reboot.service.in @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Reboot Automatically After System Update +Documentation=man:systemd-sysupdate-reboot.service(8) +ConditionVirtualization=!container + +[Service] +Type=oneshot +ExecStart={{ROOTLIBEXECDIR}}/systemd-sysupdate reboot + +[Install] +Also=systemd-sysupdate-reboot.timer diff --git a/units/systemd-sysupdate-reboot.timer b/units/systemd-sysupdate-reboot.timer new file mode 100644 index 0000000000..95a44bfea7 --- /dev/null +++ b/units/systemd-sysupdate-reboot.timer @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Reboot Automatically After System Update +Documentation=man:systemd-sysupdate-reboot.service(8) +ConditionVirtualization=!container + +[Timer] +OnCalendar=4:10 +RandomizedDelaySec=30min + +[Install] +WantedBy=timers.target diff --git a/units/systemd-sysupdate.service.in b/units/systemd-sysupdate.service.in new file mode 100644 index 0000000000..085a9c4a22 --- /dev/null +++ b/units/systemd-sysupdate.service.in @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Automatic System Update +Documentation=man:systemd-sysupdate.service(8) +Wants=network-online.target +After=network-online.target +ConditionVirtualization=!container + +[Service] +Type=simple +NotifyAccess=main +ExecStart={{ROOTLIBEXECDIR}}/systemd-sysupdate update +CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD CAP_SETFCAP CAP_SYS_ADMIN CAP_SETPCAP CAP_DAC_OVERRIDE CAP_LINUX_IMMUTABLE +NoNewPrivileges=yes +MemoryDenyWriteExecute=yes +ProtectHostname=yes +RestrictRealtime=yes +RestrictNamespaces=net +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +SystemCallFilter=@system-service @mount +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +LockPersonality=yes + +[Install] +Also=systemd-sysupdate.timer diff --git a/units/systemd-sysupdate.timer b/units/systemd-sysupdate.timer new file mode 100644 index 0000000000..6ecd98d13e --- /dev/null +++ b/units/systemd-sysupdate.timer @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Automatic System Update +Documentation=man:systemd-sysupdate.service(8) + +# For containers we assume that the manager will handle updates. And we likely +# can't even access our backing block device anyway. +ConditionVirtualization=!container + +[Timer] +# Trigger the update 15min after boot, and then – on average – every 6h, but +# randomly distributed in a 2h…6h interval. In addition trigger things +# persistently once on each Saturday, to ensure that even on systems that are +# never booted up for long we have a chance to to do the update. +OnBootSec=15min +OnUnitActiveSec=2h +OnCalendar=Sat +RandomizedDelaySec=4h +Persistent=yes + +[Install] +WantedBy=timers.target |