volume: add an "io.systemd.StorageProvider" IPC API that is supposed to be used by vmspawn/nspawn/pid1 to provide storage volumes in a generic fashion (#41776)

BindPath= in unit files, and --bind= in nspawn/vmspawn doesn't really
cut it to connect arbitrary storage infra to it. Let's do something
about it, and implement a simple, light-weight API for acquiring an fd
to a storage volume. Benefits:

1. the interface can be implemented by anyone, connecting anything to
vmspawn/nspawn/service management
2. very lose coupling: just bind a socket into a well-known dir, done
3. mounting can happen on-demand
This commit is contained in:
Lennart Poettering
2026-04-29 15:51:52 +02:00
committed by GitHub
30 changed files with 3381 additions and 2 deletions
+8 -2
View File
@@ -128,6 +128,11 @@ SPDX-License-Identifier: LGPL-2.1-or-later
## Features
- StorageProvider interface + storagectl
- hook-up in systemd-nspawn
- hook-up in systemd-vmspawn
- hook-up in service manager (BindVolume=)
- a small tool that can do basic btrfs raid policy mgmt. i.e. gets started as
part of the initial transaction for some btrfs raid fs, waits for some time,
then puts message on screen (plymouth, console) that some devices apparently
@@ -2545,8 +2550,9 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- systemd-tpm2-support: add a some logic that detects if system is in DA
lockout mode, and queries the user for TPM recovery PIN then.
- systemd: add storage API via varlink, where everyone can drop a socket in a
dir, similar, do the same thing for networking
- add a networking provider API, inspired by the StorageProvider. Make networkd
a provider that exposes interfaces for adding tap, tun, veth via the api,
base this on .netdev logic somehow.
- $SYSTEMD_EXECPID that the service manager sets should
be augmented with $SYSTEMD_EXECPIDFD (and similar for
+9
View File
@@ -972,6 +972,7 @@ manpages = [
['sd_watchdog_enabled', '3', [], ''],
['shutdown', '8', [], ''],
['smbios-type-11', '7', [], ''],
['storagectl', '1', ['mount.storage'], ''],
['sysctl.d', '5', [], ''],
['sysext.conf',
'5',
@@ -1186,6 +1187,14 @@ manpages = [
['systemd-ssh-issue', '1', [], ''],
['systemd-ssh-proxy', '1', [], ''],
['systemd-stdio-bridge', '1', [], ''],
['systemd-storage-block@.service',
'8',
['systemd-storage-block', 'systemd-storage-block.socket'],
''],
['systemd-storage-fs@.service',
'8',
['systemd-storage-fs', 'systemd-storage-fs.socket'],
''],
['systemd-storagetm.service', '8', ['systemd-storagetm'], 'ENABLE_STORAGETM'],
['systemd-stub',
'7',
+281
View File
@@ -0,0 +1,281 @@
<?xml version='1.0'?>
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="storagectl"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>storagectl</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>storagectl</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>storagectl</refname>
<refname>mount.storage</refname>
<refpurpose>Enumerate and mount storage volumes provided by storage providers</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>storagectl</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="req">COMMAND</arg>
<arg choice="opt" rep="repeat">NAME</arg>
</cmdsynopsis>
<cmdsynopsis>
<command>mount</command>
<arg choice="plain">-t</arg>
<arg choice="plain">storage</arg>
<arg choice="plain"><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></arg>
<arg choice="plain"><replaceable>DIRECTORY</replaceable></arg>
</cmdsynopsis>
<cmdsynopsis>
<command>mount</command>
<arg choice="plain">-t</arg>
<arg choice="plain">storage.<replaceable>FSTYPE</replaceable></arg>
<arg choice="plain"><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></arg>
<arg choice="plain"><replaceable>DIRECTORY</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>storagectl</command> may be used to inspect storage providers and the storage
volumes they expose. A storage provider is a service implementing the
<constant>io.systemd.StorageProvider</constant> <ulink url="https://varlink.org/">Varlink</ulink>
interface, registered as an AF_UNIX socket below the well-known socket directory
<filename>/run/systemd/io.systemd.StorageProvider/</filename> (in system mode) or
<varname>$XDG_RUNTIME_DIR</varname><filename>/systemd/io.systemd.StorageProvider/</filename> (in user mode). The two
storage providers shipped with systemd are
<citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
which exposes the system's block devices, and
<citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
which exposes regular files and directories from a backing file system.</para>
<para>The tool also provides a <citerefentry
project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry> helper
for the file system type <literal>storage</literal>, which permits mounting storage volumes to arbitrary
places. See "Use as a mount helper" below for details.</para>
</refsect1>
<refsect1>
<title>Commands</title>
<para>The following commands are understood:</para>
<variablelist>
<varlistentry>
<term><command>volumes</command> <optional><replaceable>GLOB</replaceable></optional></term>
<listitem><para>List storage volumes provided by all storage providers running on the
system (or, with <option>--user</option>, in the user runtime). The optional
<replaceable>GLOB</replaceable> argument is a shell-style pattern (see
<citerefentry project='man-pages'><refentrytitle>fnmatch</refentrytitle><manvolnum>3</manvolnum></citerefentry>)
that filters the result by volume name. The output is a table containing the providing
service, the volume name, its type (<literal>blk</literal>, <literal>reg</literal> or
<literal>dir</literal>), whether it is read-only, and — if known — its size and the number
of bytes used.</para>
<para>This is the default command if none is specified.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>templates</command> <optional><replaceable>GLOB</replaceable></optional></term>
<listitem><para>List volume templates supported by the running storage providers. Templates
encapsulate a configuration to use when creating volumes on-the-fly, when they are acquired. Template
support is an optional feature for providers, and only applies to providers that allow creation
of volumes on-the-fly. See the respective provider documentation for details, for example
<citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>. The
optional <replaceable>GLOB</replaceable> argument filters by template name. Storage providers that do
not implement template-based volume creation (such as the block-device provider) do not contribute to
this output.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>providers</command></term>
<listitem><para>List the storage providers known to the system. This is determined by scanning the
well-known socket directory for <constant>AF_UNIX</constant> sockets that look like
<constant>io.systemd.StorageProvider</constant> endpoints. For each provider it is also reported
whether the socket can currently be connected to.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<varlistentry>
<term><option>--system</option></term>
<listitem><para>Operate on system-wide storage providers. Sockets are looked for in
<filename>/run/systemd/io.systemd.StorageProvider/</filename>. This is the default.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--user</option></term>
<listitem><para>Operate on per-user storage providers. Sockets are looked for in
<filename>$XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/</filename>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="json" />
<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="no-ask-password" />
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Use as a mount helper</title>
<para>The tool provides the <command>/sbin/mount.storage</command> alias, implementing the
<citerefentry project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry>
"external helper" interface, allowing storage volumes to be mounted with the regular
<command>mount</command> command. The volume to mount is encoded as the source of the mount,
in the form
<literal><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></literal>, where
<replaceable>PROVIDER</replaceable> is the name of a storage provider (as listed by
<command>storagectl providers</command>) and <replaceable>VOLUME</replaceable> is the volume
name. Two file system type spellings are recognized:</para>
<variablelist>
<varlistentry>
<term><literal>storage</literal></term>
<listitem><para>Acquires a directory volume and bind-mounts its directory tree onto the
target.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><literal>storage.<replaceable>FSTYPE</replaceable></literal></term>
<listitem><para>Acquires a regular file or block device volume and mounts it as a file system of type
<replaceable>FSTYPE</replaceable> (for example <literal>storage.ext4</literal>,
<literal>storage.btrfs</literal>, …).</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
<para>The standard <option>-o</option> mount options are forwarded to
<command>mount</command>. In addition, the following <literal>storage.</literal>-prefixed
options are interpreted by <command>mount.storage</command> itself and stripped from the
forwarded list:</para>
<variablelist>
<varlistentry>
<term><option>storage.create=</option><replaceable>MODE</replaceable></term>
<listitem><para>Takes one of <literal>any</literal> (open if it exists, otherwise create — the
default), <literal>open</literal> (fail if the volume does not yet exist) or <literal>new</literal>
(fail if the volume already exists).</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>storage.template=</option><replaceable>NAME</replaceable></term>
<listitem><para>The template to use when creating a new volume, if it is missing and the provider
supports on-the-fly creation of volumes.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>storage.create-size=</option><replaceable>BYTES</replaceable></term>
<listitem><para>When creating a new volume on-the-fly, the size in bytes to allocate. Accepts the
usual <literal>K</literal>/<literal>M</literal>/<literal>G</literal>/<literal>T</literal> suffixes
(base 1024). Required when creating a regular file volume.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Examples</title>
<example>
<title>Enumerate available storage providers, volumes and templates</title>
<programlisting>$ storagectl providers
$ storagectl volumes
$ storagectl volumes '*foo*'
$ storagectl templates</programlisting>
</example>
<example>
<title>Mount a directory volume from the file system provider</title>
<programlisting># mount -t storage fs:myvol /mnt/myvol</programlisting>
<para>If the volume <literal>myvol</literal> does not yet exist, it will be created using
the default <literal>subvolume</literal> template.</para>
</example>
<example>
<title>Create and mount an ext4 file system from a regular file.</title>
<programlisting># mount -t storage.ext4 fs:scratch /mnt/scratch -o loop</programlisting>
</example>
<example>
<title>Mount a block device volume read-only</title>
<programlisting># mount -t storage.ext4 -o ro block:/dev/disk/by-id/usb-foo /mnt/foo</programlisting>
</example>
</refsect1>
<refsect1>
<title>Exit status</title>
<para>On success, 0 is returned, a non-zero failure code otherwise.</para>
</refsect1>
<xi:include href="common-variables.xml" />
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>varlinkctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+97
View File
@@ -0,0 +1,97 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-storage-block_.service"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-storage-block@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-storage-block@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-storage-block@.service</refname>
<refname>systemd-storage-block.socket</refname>
<refname>systemd-storage-block</refname>
<refpurpose>Storage provider exposing local block devices as storage volumes</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-storage-block@.service</filename></para>
<para><filename>systemd-storage-block.socket</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><filename>systemd-storage-block@.service</filename> is a system service that implements the
<constant>io.systemd.StorageProvider</constant> <ulink url="https://varlink.org/">Varlink</ulink>
interface, exposing the system's block devices (such as disks, partitions, and device-mapper
nodes) as storage volumes that may be acquired by other programs as file descriptors.</para>
<para>The service is socket-activated via <filename>systemd-storage-block.socket</filename>, which
listens on the AF_UNIX socket <filename>/run/systemd/io.systemd.StorageProvider/block</filename>. The
socket directory <filename>/run/systemd/io.systemd.StorageProvider/</filename> is the well-known location
where storage providers register, see
<citerefentry><refentrytitle>storagectl</refentrytitle><manvolnum>1</manvolnum></citerefentry> for an
enumeration tool.</para>
<para>See also
<citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
for a complementary implementation that exposes regular files and directories from a backing file
system.</para>
</refsect1>
<refsect1>
<title>Volumes</title>
<para>The volumes exposed via the provider are identified by an absolute path (which must begin with
<filename>/dev/</filename>), i.e. as a kernel block device node such as <filename>/dev/sda</filename> or
<filename>/dev/disk/by-id/…</filename>. Volume names that are not normalized or that do not begin with
<filename>/dev/</filename> are not accepted.</para>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Files</title>
<variablelist>
<varlistentry>
<term><filename>/run/systemd/io.systemd.StorageProvider/block</filename></term>
<listitem><para>AF_UNIX socket the service listens on. This is the canonical location
for the <literal>block</literal> storage provider, and is enumerated by
<command>storagectl providers</command>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>storagectl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+199
View File
@@ -0,0 +1,199 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-storage-fs_.service"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-storage-fs@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-storage-fs@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-storage-fs@.service</refname>
<refname>systemd-storage-fs.socket</refname>
<refname>systemd-storage-fs</refname>
<refpurpose>Storage provider exposing regular files and directories as storage volumes</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-storage-fs@.service</filename></para>
<para><filename>systemd-storage-fs.socket</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><filename>systemd-storage-fs@.service</filename> is a service that implements the
<constant>io.systemd.StorageProvider</constant> <ulink url="https://varlink.org/">Varlink</ulink>
interface, exposing regular files and directories in <filename>/var/lib/storage/*.volume</filename> (if
used in system mode) or <filename>$XDG_STATE_HOME/storage</filename> (when used in user mode) as storage
volumes. Acquired volumes are returned to the caller as file descriptors. Unlike
<citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
this implementation also supports creating new volumes on demand from a small set of built-in
templates.</para>
<para>The service is socket-activated via <filename>systemd-storage-fs.socket</filename>. In system mode
it listens on the AF_UNIX socket <filename>/run/systemd/io.systemd.StorageProvider/fs</filename>, in user
mode on <filename>$XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/fs</filename>. See
<citerefentry><refentrytitle>storagectl</refentrytitle><manvolnum>1</manvolnum></citerefentry> for an
enumeration tool.</para>
<para>See also
<citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
for a complementary implementation that exposes local block devices as storage volumes.</para>
</refsect1>
<refsect1>
<title>Volumes</title>
<para>Volumes are stored below the storage directory:</para>
<itemizedlist>
<listitem><para><filename>/var/lib/storage/</filename> when run in system mode.</para></listitem>
<listitem><para><filename>$XDG_STATE_HOME/storage/</filename> (typically
<filename>~/.local/state/storage/</filename>) when run in user mode.</para></listitem>
</itemizedlist>
<para>Each volume on disk is stored as a directory entry with a <filename>.volume</filename> suffix in
the storage directory. Entries which are regular files are exposed as volumes of type
<constant>reg</constant>; entries which are directories are exposed as volumes of type
<constant>dir</constant>. Moreover, block device nodes may be symlinked (or bind mounted) into the
directory, which are then exposed as volumes of type <constant>blk</constant>.</para>
<para>For directory volumes, the root of the file system passed to clients is placed in a subdirectory
<filename>root/</filename> of the <filename><replaceable>NAME</replaceable>.volume</filename> directory. The former (and all inodes
below it) must be owned by the foreign UID range, the latter by the host's root.</para>
<para>When acquiring a volume, symlinks are followed.</para>
<para>An administrator is permitted to freely manipulate the volume hierarchy directly as long as the
rules described above are followed. In particular, it's permitted to copy, mount or symlink arbitrary
external resources (regardless if directory, regular file or block) into the volume directory, so that
they are exposed as additional volumes.</para>
</refsect1>
<refsect1>
<title>Templates</title>
<para>The provider supports creating new volumes automatically when they are acquired. The caller may
select a <emphasis>template</emphasis> that determines configuration details of the volume to create. The
following built-in templates are available:</para>
<variablelist>
<varlistentry>
<term><constant>sparse-file</constant></term>
<listitem><para>Creates a volume backed by a sparsely populated regular file. This is the default
template when creating a regular file volume. (Volume type is <literal>reg</literal>.)</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><constant>allocated-file</constant></term>
<listitem><para>Creates a volume backed by a fully allocated regular file. (Volume type is
<literal>reg</literal>.)</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><constant>directory</constant></term>
<listitem><para>Creates a volume backed by a regular directory. (Volume type is
<literal>dir</literal>.)</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><constant>subvolume</constant></term>
<listitem><para>Creates a btrfs subvolume as backing inode (falling back to a regular directory if
the storage directory is not on btrfs). This is the default template when creating a directory
volume. (Volume type is <literal>dir</literal>.)</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following command-line options are understood:</para>
<variablelist>
<varlistentry>
<term><option>--system</option></term>
<listitem><para>Operate in system mode. Volumes are stored below
<filename>/var/lib/storage/</filename>. This is the default when invoked from
<filename>systemd-storage-fs@.service</filename> in the system manager.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--user</option></term>
<listitem><para>Operate in user mode. Volumes are stored below
<filename>$XDG_STATE_HOME/storage/</filename>. This is the default when invoked from
<filename>systemd-storage-fs@.service</filename> in the user manager.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Files</title>
<variablelist>
<varlistentry>
<term><filename>/var/lib/storage/</filename></term>
<term><filename>$XDG_STATE_HOME/storage/</filename></term>
<listitem><para>The storage directory used to back the system mode and user mode service
instances respectively. Each volume is stored as an entry with a
<filename>.volume</filename> suffix below this directory.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><filename>/run/systemd/io.systemd.StorageProvider/fs</filename></term>
<term><filename>$XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/fs</filename></term>
<listitem><para>AF_UNIX sockets the service listens on, in system and user mode
respectively. These are the canonical locations for the <literal>fs</literal> storage
provider, and are enumerated by <command>storagectl providers</command>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>storagectl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+1
View File
@@ -2139,6 +2139,7 @@ subdir('src/socket-activate')
subdir('src/socket-proxy')
subdir('src/ssh-generator')
subdir('src/stdio-bridge')
subdir('src/storage')
subdir('src/storagetm')
subdir('src/sulogin-shell')
subdir('src/sysctl')
+1
View File
@@ -36,6 +36,7 @@ foreach item : [
['portablectl', 'ENABLE_PORTABLED'],
['resolvectl', 'ENABLE_RESOLVE'],
['run0', ''],
['storagectl', ''],
['systemd-analyze', ''],
['systemd-cat', ''],
['systemd-cgls', ''],
+74
View File
@@ -0,0 +1,74 @@
# shellcheck shell=bash
# storagectl(1) completion -*- shell-script -*-
# 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.
#
# systemd is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with systemd; If not, see <https://www.gnu.org/licenses/>.
__contains_word () {
local w word=$1; shift
for w in "$@"; do
[[ $w = "$word" ]] && return
done
}
_storagectl() {
local i verb comps
local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
local -A OPTS=(
[STANDALONE]='-h --help --version --no-pager --no-legend --no-ask-password
--system --user'
[ARG]='--json'
)
if __contains_word "$prev" ${OPTS[ARG]}; then
case $prev in
--json)
comps=$( storagectl --json=help 2>/dev/null )
;;
esac
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
fi
if [[ "$cur" = -* ]]; then
COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") )
return 0
fi
local -A VERBS=(
[STANDALONE]='volumes templates providers help'
)
for ((i=0; i < COMP_CWORD; i++)); do
if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} &&
! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then
verb=${COMP_WORDS[i]}
break
fi
done
if [[ -z ${verb-} ]]; then
comps=${VERBS[*]}
elif __contains_word "$verb" ${VERBS[STANDALONE]}; then
comps=''
fi
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
}
complete -F _storagectl storagectl
+35
View File
@@ -0,0 +1,35 @@
#compdef storagectl
# SPDX-License-Identifier: LGPL-2.1-or-later
(( $+functions[_storagectl_commands] )) || _storagectl_commands()
{
local -a _storagectl_cmds
_storagectl_cmds=(
"volumes:List storage volumes"
"templates:List storage volume templates"
"providers:List storage providers"
"help:Prints a short help text and exits"
)
if (( CURRENT == 1 )); then
_describe -t commands 'storagectl command' _storagectl_cmds
else
local curcontext="$curcontext"
cmd="${${_storagectl_cmds[(r)$words[1]:*]%%:*}}"
if (( $+functions[_storagectl_$cmd] )); then
_storagectl_$cmd
else
_message "no more options"
fi
fi
}
_arguments \
'(- *)'{-h,--help}'[Prints a short help text and exits.]' \
'(- *)--version[Prints a short version string and exits.]' \
'--no-pager[Do not pipe output into a pager]' \
'--no-legend[Do not show the headers and footers]' \
'--no-ask-password[Do not query the user for authentication]' \
'--json=[Show output as JSON]:mode:(pretty short off help)' \
'--system[Operate in system mode]' \
'--user[Operate in user mode]' \
'*::storagectl command:_storagectl_commands'
+1
View File
@@ -33,6 +33,7 @@ foreach item : [
['_sd_machines', 'ENABLE_MACHINED'],
['_sd_outputmodes', ''],
['_sd_unit_files', ''],
['_storagectl', ''],
['_systemd', ''],
['_systemd-analyze', ''],
['_systemd-delta', ''],
@@ -44,6 +44,7 @@
#include "varlink-io.systemd.Resolve.h"
#include "varlink-io.systemd.Resolve.Hook.h"
#include "varlink-io.systemd.Resolve.Monitor.h"
#include "varlink-io.systemd.StorageProvider.h"
#include "varlink-io.systemd.Udev.h"
#include "varlink-io.systemd.Unit.h"
#include "varlink-io.systemd.UserDatabase.h"
@@ -212,6 +213,7 @@ TEST(parse_format) {
&vl_interface_io_systemd_Resolve,
&vl_interface_io_systemd_Resolve_Hook,
&vl_interface_io_systemd_Resolve_Monitor,
&vl_interface_io_systemd_StorageProvider,
&vl_interface_io_systemd_Udev,
&vl_interface_io_systemd_Unit,
&vl_interface_io_systemd_UserDatabase,
+1
View File
@@ -245,6 +245,7 @@ shared_sources = files(
'varlink-io.systemd.Resolve.Hook.c',
'varlink-io.systemd.Resolve.Monitor.c',
'varlink-io.systemd.Shutdown.c',
'varlink-io.systemd.StorageProvider.c',
'varlink-io.systemd.Udev.c',
'varlink-io.systemd.Unit.c',
'varlink-io.systemd.UserDatabase.c',
@@ -0,0 +1,119 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "bus-polkit.h"
#include "varlink-io.systemd.StorageProvider.h"
static SD_VARLINK_DEFINE_ENUM_TYPE(
VolumeType,
SD_VARLINK_FIELD_COMMENT("Block device storage volumes, block-addressable"),
SD_VARLINK_DEFINE_ENUM_VALUE(blk),
SD_VARLINK_FIELD_COMMENT("Regular file storage volumes, byte-addressable"),
SD_VARLINK_DEFINE_ENUM_VALUE(reg),
SD_VARLINK_FIELD_COMMENT("POSIX file system storage volumes, path/offset-addressable"),
SD_VARLINK_DEFINE_ENUM_VALUE(dir));
static SD_VARLINK_DEFINE_ENUM_TYPE(
CreateMode,
SD_VARLINK_FIELD_COMMENT("Open if exists already, create if missing"),
SD_VARLINK_DEFINE_ENUM_VALUE(any),
SD_VARLINK_FIELD_COMMENT("Create if missing, fail if exists already"),
SD_VARLINK_DEFINE_ENUM_VALUE(new),
SD_VARLINK_FIELD_COMMENT("Open if exists already, fail if missing"),
SD_VARLINK_DEFINE_ENUM_VALUE(open));
static SD_VARLINK_DEFINE_METHOD(
Acquire,
SD_VARLINK_FIELD_COMMENT("The name of the storage volume to acquire"),
SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("Determines whether to open or create a storage volume"),
SD_VARLINK_DEFINE_INPUT_BY_TYPE(createMode, CreateMode, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The template to use when creating a new storage volume"),
SD_VARLINK_DEFINE_INPUT(template, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Controls read/write access to the storage volume. If false and the storage volume cannot be opened in writable mode the call will fail. If null, storage volume will be acquired in writable mode if possible, read-only otherwise. If true, storage volume will be opened in read-only mode (and fail if that's not possible)."),
SD_VARLINK_DEFINE_INPUT(readOnly, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Dictates what kind of storage volume to request. Some storage volumes can be acquired either as regular file or as block device. In all other cases if this value doesn't match the volume type, the request will fail."),
SD_VARLINK_DEFINE_INPUT_BY_TYPE(requestAs, VolumeType, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The size of the storage volume, if one is created. Has no effect if no storage volume is created."),
SD_VARLINK_DEFINE_INPUT(createSizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
VARLINK_DEFINE_POLKIT_INPUT,
SD_VARLINK_FIELD_COMMENT("Returns an index into the array of file descriptors associated with this reply. This may be used to get the file descriptor of the volume. The file descriptor must be properly opened, i.e. not an O_PATH file descriptor."),
SD_VARLINK_DEFINE_OUTPUT(fileDescriptorIndex, SD_VARLINK_INT, 0),
SD_VARLINK_FIELD_COMMENT("The storage volume type, i.e. ultimately the inode type of the returned file descriptor"),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, VolumeType, 0),
SD_VARLINK_FIELD_COMMENT("Whether storage volume has been opened in read-only mode"),
SD_VARLINK_DEFINE_OUTPUT(readOnly, SD_VARLINK_BOOL, 0),
SD_VARLINK_FIELD_COMMENT("Base UID for the returned file descriptor (if directory). If not specified shall default to 0."),
SD_VARLINK_DEFINE_OUTPUT(baseUID, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Base GID for the returned file descriptor (if directory). If not specified shall default to 0."),
SD_VARLINK_DEFINE_OUTPUT(baseGID, SD_VARLINK_INT, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_METHOD_FULL(
ListVolumes,
SD_VARLINK_REQUIRES_MORE,
SD_VARLINK_FIELD_COMMENT("Specifies a shell glob to filter enumeration by"),
SD_VARLINK_DEFINE_INPUT(matchName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The storage volume's primary name"),
SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("Additional names"),
SD_VARLINK_DEFINE_OUTPUT(aliases, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
SD_VARLINK_FIELD_COMMENT("The type of the storage volume"),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, VolumeType, 0),
SD_VARLINK_FIELD_COMMENT("Whether the storage volume is read-only."),
SD_VARLINK_DEFINE_OUTPUT(readOnly, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Size in bytes, if known"),
SD_VARLINK_DEFINE_OUTPUT(sizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Used bytes, if known"),
SD_VARLINK_DEFINE_OUTPUT(usedBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_METHOD_FULL(
ListTemplates,
SD_VARLINK_REQUIRES_MORE,
SD_VARLINK_FIELD_COMMENT("Specifies a shell glob to filter enumeration by"),
SD_VARLINK_DEFINE_INPUT(matchName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The template's name"),
SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("The type of the storage volumes defined by this template"),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, VolumeType, 0));
static SD_VARLINK_DEFINE_ERROR(NoSuchVolume);
static SD_VARLINK_DEFINE_ERROR(VolumeExists);
static SD_VARLINK_DEFINE_ERROR(NoSuchTemplate);
static SD_VARLINK_DEFINE_ERROR(TypeNotSupported);
static SD_VARLINK_DEFINE_ERROR(WrongType);
static SD_VARLINK_DEFINE_ERROR(CreateNotSupported);
static SD_VARLINK_DEFINE_ERROR(CreateSizeRequired);
static SD_VARLINK_DEFINE_ERROR(ReadOnlyVolume);
static SD_VARLINK_DEFINE_ERROR(BadTemplate);
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_StorageProvider,
"io.systemd.StorageProvider",
SD_VARLINK_INTERFACE_COMMENT("Storage Provider API, a generic interface for acquiring access to storage volumes"),
SD_VARLINK_SYMBOL_COMMENT("Encodes three classes of storage volumes. This follows the kernel's nomenclature for inode types, i.e. reg, dir, blk."),
&vl_type_VolumeType,
SD_VARLINK_SYMBOL_COMMENT("Determines whether to open existing or create a new storage volume."),
&vl_type_CreateMode,
SD_VARLINK_SYMBOL_COMMENT("Acquires a file descriptor for a storage volume."),
&vl_method_Acquire,
SD_VARLINK_SYMBOL_COMMENT("Lists available storage volumes."),
&vl_method_ListVolumes,
SD_VARLINK_SYMBOL_COMMENT("Lists available templates."),
&vl_method_ListTemplates,
SD_VARLINK_SYMBOL_COMMENT("No storage volume under the specified name exists."),
&vl_error_NoSuchVolume,
SD_VARLINK_SYMBOL_COMMENT("A storage volume under the specified name already exists."),
&vl_error_VolumeExists,
SD_VARLINK_SYMBOL_COMMENT("No template under the specified name exists."),
&vl_error_NoSuchTemplate,
SD_VARLINK_SYMBOL_COMMENT("The specified volume type is not supported by this backend or system."),
&vl_error_TypeNotSupported,
SD_VARLINK_SYMBOL_COMMENT("The volume's type does not match the requested volume type."),
&vl_error_WrongType,
SD_VARLINK_SYMBOL_COMMENT("This backend does not support storage volume creation of the requested type."),
&vl_error_CreateNotSupported,
SD_VARLINK_SYMBOL_COMMENT("This backend or selected volume type requires a storage volume size to be specified if the storage volume does not exist yet and needs to be created."),
&vl_error_CreateSizeRequired,
SD_VARLINK_SYMBOL_COMMENT("A storage volume was to be acquired in writable mode, but only read-only access is permitted."),
&vl_error_ReadOnlyVolume,
SD_VARLINK_SYMBOL_COMMENT("Template not suitable for this storage volume type."),
&vl_error_BadTemplate);
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_StorageProvider;
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<!--
SPDX-License-Identifier: LGPL-2.1-or-later
This file is part of systemd.
systemd is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
-->
<policyconfig>
<vendor>The systemd Project</vendor>
<vendor_url>https://systemd.io</vendor_url>
<action id="io.systemd.storage.block.acquire">
<description gettext-domain="systemd">Allow access to block storage volumes</description>
<message gettext-domain="systemd">Authentication is required for an application to gain access to block storage volume '$(name)'.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
<action id="io.systemd.storage.fs.acquire">
<description gettext-domain="systemd">Allow access to file system storage volumes</description>
<message gettext-domain="systemd">Authentication is required for an application to gain access to file system storage volume '$(name)'.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
+27
View File
@@ -0,0 +1,27 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
executables += [
libexec_template + {
'name' : 'systemd-storage-block',
'sources' : files('storage-block.c'),
'extract' : files('storage-util.c')
},
libexec_template + {
'name' : 'systemd-storage-fs',
'sources' : files('storage-fs.c'),
'objects' : ['systemd-storage-block'],
},
executable_template + {
'name' : 'storagectl',
'public' : true,
'sources' : files('storagectl.c'),
'objects' : ['systemd-storage-block'],
},
]
install_symlink('mount.storage',
pointing_to : sbin_to_bin + 'storagectl',
install_dir : sbindir)
install_data('io.systemd.storage.policy',
install_dir : polkitpolicydir)
+439
View File
@@ -0,0 +1,439 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fnmatch.h>
#include "sd-device.h"
#include "sd-json.h"
#include "sd-varlink.h"
#include "blockdev-list.h"
#include "build.h"
#include "bus-polkit.h"
#include "device-private.h"
#include "device-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "format-table.h"
#include "hashmap.h"
#include "help-util.h"
#include "json-util.h"
#include "log.h"
#include "main-func.h"
#include "options.h"
#include "path-util.h"
#include "storage-util.h"
#include "strv.h"
#include "varlink-io.systemd.StorageProvider.h"
#include "varlink-util.h"
static int block_device_pick_name(
const BlockDevice *d,
const char **ret_name,
char ***ret_aliases) {
int r;
assert(d);
assert(d->node);
assert(ret_name);
assert(ret_aliases);
static const char *const prefixes[] = {
/* The list of preferred prefixes, in order of preference. Note: for security reasons we only
* use identifiers that do not depend on the *contents* of the device, i.e. we restrict
* ourselves to IDs whose fields are either chosen by whoever created the kernel device or are
* hardware properties, but not names generated from superblock metainformation or similar. */
"/dev/mapper",
"/dev/disk/by-loop-ref",
"/dev/disk/by-id",
"/dev/disk/by-path",
};
const char* found[ELEMENTSOF(prefixes)] = {};
_cleanup_strv_free_ char **aliases = NULL;
size_t best = SIZE_MAX;
STRV_FOREACH(sl, d->symlinks) {
bool matched = false;
for (size_t i = 0; i < ELEMENTSOF(prefixes); i++) {
if (!path_startswith(*sl, prefixes[i]))
continue;
if (found[i]) {
/* Two symlinks with the same prefix? Then keep the lower one. */
if (path_compare(*sl, found[i]) > 0)
continue;
r = strv_extend(&aliases, found[i]);
if (r < 0)
return r;
}
found[i] = *sl;
if (i < best)
best = i;
matched = true;
}
if (!matched) {
r = strv_extend(&aliases, *sl);
if (r < 0)
return r;
}
}
if (best == SIZE_MAX) /* No preferred prefix found, use the kernel device name */
*ret_name = d->node;
else {
/* We found a preferred prefix, add the kernel device name to the aliases then. */
r = strv_extend(&aliases, d->node);
if (r < 0)
return r;
/* If there are any less preferred prefixes also add them to the aliases array */
for (size_t i = best + 1; i < ELEMENTSOF(prefixes); i++) {
if (!found[i])
continue;
r = strv_extend(&aliases, found[i]);
if (r < 0)
return r;
}
*ret_name = found[best];
}
strv_sort(aliases);
*ret_aliases = TAKE_PTR(aliases);
return 0;
}
static bool block_device_match(const BlockDevice *d, const char *match) {
assert(d);
assert(d->node);
if (!match)
return true;
if (fnmatch(match, d->node, FNM_NOESCAPE) == 0)
return true;
STRV_FOREACH(sl, d->symlinks)
if (fnmatch(match, *sl, FNM_NOESCAPE) == 0)
return true;
return false;
}
static int vl_method_list_volumes(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
int r;
assert(link);
assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
struct {
const char *match_name;
} p = {};
static const sd_json_dispatch_field dispatch_table[] = {
{ "matchName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, match_name), 0 },
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
BlockDevice *l = NULL;
size_t n = 0;
CLEANUP_ARRAY(l, n, block_device_array_free);
r = blockdev_list(
BLOCKDEV_LIST_SHOW_SYMLINKS|
BLOCKDEV_LIST_IGNORE_ROOT|
BLOCKDEV_LIST_IGNORE_EMPTY|
BLOCKDEV_LIST_METADATA,
&l,
&n);
if (r < 0)
return r;
r = sd_varlink_set_sentinel(link, "io.systemd.StorageProvider.NoSuchVolume");
if (r < 0)
return r;
FOREACH_ARRAY(d, l, n) {
const char *name = NULL;
_cleanup_strv_free_ char **aliases = NULL;
if (!block_device_match(d, p.match_name))
continue;
r = block_device_pick_name(d, &name, &aliases);
if (r < 0)
return r;
r = sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_STRING("name", name),
JSON_BUILD_PAIR_STRV_NON_EMPTY("aliases", aliases),
SD_JSON_BUILD_PAIR_STRING("type", "blk"),
SD_JSON_BUILD_PAIR_CONDITION(d->read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(d->read_only)),
JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("sizeBytes", d->size, UINT64_MAX));
if (r < 0)
return r;
}
return 0;
}
static int vl_method_list_templates(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
/* This storage provider does not support templates */
assert(link);
assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
}
static int device_open_disk_auto_rw(sd_device *d, int *read_only) {
assert(d);
assert(read_only);
int fd = sd_device_open(d, *read_only > 0 ? O_RDONLY : O_RDWR);
if (fd < 0) {
if (!ERRNO_IS_NEG_FS_WRITE_REFUSED(fd) || *read_only >= 0)
return log_device_debug_errno(d, fd, "Failed to open device in %s mode: %m", *read_only > 0 ? "read-only" : "read-write");
/* Try again in read-only mode */
fd = sd_device_open(d, O_RDONLY);
if (fd < 0)
return log_device_debug_errno(d, fd, "Failed to open device in read-only mode, too: %m");
*read_only = true;
} else
*read_only = *read_only > 0;
return fd;
}
static int vl_method_acquire(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
Hashmap **polkit_registry = ASSERT_PTR(userdata);
int r;
assert(link);
struct {
const char *name;
CreateMode create_mode;
const char *template;
int read_only;
VolumeType request_as;
uint64_t create_size;
} p = {
.create_mode = CREATE_ANY,
.read_only = -1,
.request_as = _VOLUME_TYPE_INVALID,
.create_size = UINT64_MAX, /* never actually used here, just validated; we don't allow creation of block devices here */
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
{ "createMode", SD_JSON_VARIANT_STRING, json_dispatch_create_mode, voffsetof(p, create_mode), 0 },
{ "template", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, template), 0 },
{ "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
{ "requestAs", SD_JSON_VARIANT_STRING, json_dispatch_volume_type, voffsetof(p, request_as), 0 },
{ "createSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, create_size), 0 },
VARLINK_DISPATCH_POLKIT_FIELD,
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
if (!storage_volume_name_is_valid(p.name))
return sd_varlink_error_invalid_parameter_name(link, "name");
if (!path_startswith(p.name, "/dev") || !path_is_normalized(p.name))
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
if (!IN_SET(p.create_mode, CREATE_ANY, CREATE_OPEN))
return sd_varlink_error(link, "io.systemd.StorageProvider.CreateNotSupported", NULL);
/* off_t is signed, hence refuse overly long requests */
if (p.create_size != UINT64_MAX && p.create_size > INT64_MAX)
return sd_varlink_error_invalid_parameter_name(link, "createSizeBytes");
if (!isempty(p.template)) {
if (!storage_template_name_is_valid(p.template))
return sd_varlink_error_invalid_parameter_name(link, "template");
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
}
if (p.request_as >= 0 && p.request_as != VOLUME_BLK)
return sd_varlink_error(link, "io.systemd.StorageProvider.TypeNotSupported", NULL);
const char *details[] = {
"name", p.name,
NULL
};
r = varlink_verify_polkit_async(
link,
/* bus= */ NULL,
"io.systemd.storage.block.acquire",
details,
polkit_registry);
if (r <= 0)
return r;
_cleanup_(sd_device_unrefp) sd_device *d = NULL;
r = sd_device_new_from_devname(&d, p.name);
if (ERRNO_IS_NEG_DEVICE_ABSENT(r))
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
if (r < 0)
return r;
if (!device_in_subsystem(d, "block"))
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
/* The error returns are sometimes a bit inconclusive (i.e. read-only media might appear as
* inaccessible due to a permission issue), hence let's do an explicit check first, to give good
* answers */
if (p.read_only <= 0) {
r = device_get_sysattr_bool(d, "ro");
if (r < 0)
log_device_debug_errno(d, r, "Failed to acquire read-only flag of device '%s', ignoring: %m", p.name);
else if (r > 0) {
if (p.read_only == 0)
return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
p.read_only = true;
}
}
_cleanup_close_ int fd = device_open_disk_auto_rw(d, &p.read_only);
if (ERRNO_IS_NEG_FS_WRITE_REFUSED(fd))
return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
if (fd < 0)
return fd;
assert(p.read_only >= 0); /* flag is now definitely initialized to either true or false, not negative anymore */
int idx = sd_varlink_push_fd(link, fd);
if (idx < 0)
return idx;
TAKE_FD(fd);
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", idx),
SD_JSON_BUILD_PAIR_STRING("type", "blk"),
SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", p.read_only));
}
static int vl_server(void) {
int r;
_cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL;
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
r = varlink_server_new(
&varlink_server,
SD_VARLINK_SERVER_HANDLE_SIGINT|
SD_VARLINK_SERVER_HANDLE_SIGTERM|
SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT|
SD_VARLINK_SERVER_INHERIT_USERDATA,
&polkit_registry);
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_StorageProvider);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
varlink_server,
"io.systemd.StorageProvider.Acquire", vl_method_acquire,
"io.systemd.StorageProvider.ListVolumes", vl_method_list_volumes,
"io.systemd.StorageProvider.ListTemplates", vl_method_list_templates);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = sd_varlink_server_loop_auto(varlink_server);
if (r < 0)
return log_error_errno(r, "Failed to run Varlink event loop: %m");
return 0;
}
static int help(void) {
int r;
help_cmdline("[OPTIONS...]");
help_abstract("Simple block device backed storage provider");
_cleanup_(table_unrefp) Table *options = NULL;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
help_section("Options:");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("systemd-storage-block", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
assert(argc >= 0);
assert(argv);
OptionParser opts = { argc, argv };
FOREACH_OPTION(c, &opts, /* on_error= */ return c)
switch (c) {
OPTION_COMMON_HELP:
return help();
OPTION_COMMON_VERSION:
return version();
}
if (option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
return 1;
}
static int run(int argc, char* argv[]) {
int r;
log_setup();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
return vl_server();
}
DEFINE_MAIN_FUNCTION(run);
+807
View File
@@ -0,0 +1,807 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <fnmatch.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#include "sd-device.h"
#include "sd-json.h"
#include "alloc-util.h"
#include "build.h"
#include "bus-polkit.h"
#include "chase.h"
#include "chattr-util.h"
#include "device-private.h"
#include "device-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "format-table.h"
#include "fs-util.h"
#include "hashmap.h"
#include "help-util.h"
#include "log.h"
#include "main-func.h"
#include "mount-util.h"
#include "options.h"
#include "path-lookup.h"
#include "path-util.h"
#include "recurse-dir.h"
#include "runtime-scope.h"
#include "stat-util.h"
#include "storage-util.h"
#include "string-table.h"
#include "tmpfile-util.h"
#include "uid-classification.h"
#include "varlink-io.systemd.StorageProvider.h"
#include "varlink-util.h"
static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
/* For now we maintain a simple, compiled-in list of templates. One of those days we might want to move these
* into configurable drop-in files on disk. */
typedef enum Template {
TEMPLATE_SPARSE_FILE,
TEMPLATE_ALLOCATED_FILE,
TEMPLATE_DIRECTORY,
TEMPLATE_SUBVOLUME,
_TEMPLATE_MAX,
_TEMPLATE_INVALID = -EINVAL,
} Template;
static const char *template_table[_TEMPLATE_MAX] = {
[TEMPLATE_SPARSE_FILE] = "sparse-file",
[TEMPLATE_ALLOCATED_FILE] = "allocated-file",
[TEMPLATE_DIRECTORY] = "directory",
[TEMPLATE_SUBVOLUME] = "subvolume",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP(template, Template);
static VolumeType volume_type_from_template(Template t) {
switch (t) {
case TEMPLATE_SPARSE_FILE:
case TEMPLATE_ALLOCATED_FILE:
return VOLUME_REG;
case TEMPLATE_DIRECTORY:
case TEMPLATE_SUBVOLUME:
return VOLUME_DIR;
default:
return _VOLUME_TYPE_INVALID;
}
}
static int open_storage_dir(void) {
int r;
_cleanup_free_ char *state_dir = NULL;
r = state_directory_generic(arg_runtime_scope, /* suffix= */ NULL, &state_dir);
if (r < 0)
return log_error_errno(r, "Failed to get state directory path: %m");
_cleanup_close_ int state_fd = chase_and_open(state_dir, /* root= */ NULL, CHASE_TRIGGER_AUTOFS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, O_CLOEXEC|O_CREAT|O_DIRECTORY, /* ret_path= */ NULL);
if (state_fd < 0)
return log_error_errno(state_fd, "Failed to open '%s': %m", state_dir);
/* First we try to open the storage directory. If it exists this will work and we are happy. If we
* get ENOENT we'll try to create it. If that works, great. If we get EEXIST we'll try to reopen it
* again, to deal with other instances of ourselves racing with us. We only do this exactly once
* though, under the assumption that the dir is never removed, only created during runtime. */
_cleanup_close_ int storage_fd = chase_and_openat(XAT_FDROOT, state_fd, "storage", CHASE_TRIGGER_AUTOFS|CHASE_MUST_BE_DIRECTORY, O_CLOEXEC|O_DIRECTORY, /* ret_path= */ NULL);
if (storage_fd == -ENOENT) {
storage_fd = xopenat_full(state_fd, "storage", O_EXCL|O_CREAT|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW, XO_LABEL|XO_SUBVOLUME, 0700);
if (storage_fd == -EEXIST)
storage_fd = chase_and_openat(XAT_FDROOT, state_fd, "storage", CHASE_TRIGGER_AUTOFS|CHASE_MUST_BE_DIRECTORY, O_CLOEXEC|O_DIRECTORY, /* ret_path= */ NULL);
}
if (storage_fd < 0)
return log_error_errno(storage_fd, "Failed to open '%s/storage/': %m", state_dir);
return TAKE_FD(storage_fd);
}
static int vl_method_list_volumes(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
int r;
assert(link);
assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
struct {
const char *match_name;
} p = {};
static const sd_json_dispatch_field dispatch_table[] = {
{ "matchName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, match_name), 0 },
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
_cleanup_close_ int fd = open_storage_dir();
if (fd < 0)
return fd;
_cleanup_free_ DirectoryEntries *dentries = NULL;
r = readdir_all(fd, RECURSE_DIR_SORT, &dentries);
if (r < 0)
return r;
r = sd_varlink_set_sentinel(link, "io.systemd.StorageProvider.NoSuchVolume");
if (r < 0)
return r;
FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
struct dirent *d = *dp;
if (!IN_SET(d->d_type, DT_REG, DT_DIR, DT_LNK, DT_BLK, DT_UNKNOWN))
continue;
const char *e = endswith(d->d_name, ".volume");
if (!e)
continue;
_cleanup_free_ char *n = strndup(d->d_name, e - d->d_name);
if (!n)
return log_oom_debug();
if (!storage_volume_name_is_valid(n))
continue;
if (p.match_name && fnmatch(p.match_name, n, FNM_NOESCAPE) != 0)
continue;
_cleanup_close_ int pin_fd = -EBADF;
r = chaseat(XAT_FDROOT, fd, d->d_name, CHASE_TRIGGER_AUTOFS, /* ret_path= */ NULL, &pin_fd);
if (r < 0) {
log_debug_errno(r, "Failed to stat() '%s' in storage directory, ignoring: %m", d->d_name);
continue;
}
struct stat st;
if (fstat(pin_fd, &st) < 0)
return log_debug_errno(errno, "Failed to stat() '%s' in storage directory: %m", d->d_name);
uint64_t size = UINT64_MAX, used = UINT64_MAX;
bool ro = false;
switch (st.st_mode & S_IFMT) {
case S_IFREG:
ro = (st.st_mode & 0222) == 0;
size = st.st_size;
used = (uint64_t) st.st_blocks * UINT64_C(512);
break;
case S_IFDIR:
r = fd_is_read_only_fs(pin_fd);
if (r < 0)
log_debug_errno(r, "Failed to determine if '%s' is read-only, ignoring", d->d_name);
else
ro = r > 0;
break;
case S_IFBLK: {
_cleanup_(sd_device_unrefp) sd_device *dev = NULL;
r = sd_device_new_from_stat_rdev(&dev, &st);
if (r < 0)
log_debug_errno(r, "Failed to acquire device for '%s', ignoring: %m", d->d_name);
else {
r = device_get_sysattr_bool(dev, "ro");
if (r < 0)
log_device_debug_errno(dev, r, "Failed to get read/only state of '%s', ignoring: %m", d->d_name);
else
ro = r > 0;
r = device_get_sysattr_u64(dev, "size", &size);
if (r < 0)
log_device_debug_errno(dev, r, "Failed to acquire size of device '%s', ignoring: %m", d->d_name);
else
/* the 'size' sysattr is always in multiples of 512, even on 4K sector block devices! */
assert_se(MUL_ASSIGN_SAFE(&size, 512)); /* Overflow check for coverity */
}
break;
}
default:
log_debug("Volume of unexpected inode type, ignoring: %s", d->d_name);
continue;
}
r = sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_STRING("name", n),
SD_JSON_BUILD_PAIR_STRING("type", inode_type_to_string(st.st_mode)),
SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", ro),
SD_JSON_BUILD_PAIR_CONDITION(size != UINT64_MAX, "sizeBytes", SD_JSON_BUILD_UNSIGNED(size)),
SD_JSON_BUILD_PAIR_CONDITION(used != UINT64_MAX, "usedBytes", SD_JSON_BUILD_UNSIGNED(used)));
if (r < 0)
return r;
}
return 0;
}
static int vl_method_list_templates(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
int r;
assert(link);
assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
struct {
const char *match_name;
} p = {};
static const sd_json_dispatch_field dispatch_table[] = {
{ "matchName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, match_name), 0 },
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
r = sd_varlink_set_sentinel(link, "io.systemd.StorageProvider.NoSuchTemplate");
if (r < 0)
return r;
for (Template t = 0; t < _TEMPLATE_MAX; t++) {
const char *n = template_to_string(t);
if (p.match_name && fnmatch(p.match_name, n, FNM_NOESCAPE) != 0)
continue;
r = sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_STRING("name", n),
SD_JSON_BUILD_PAIR_STRING("type", volume_type_to_string(volume_type_from_template(t))));
if (r < 0)
return r;
}
return 0;
}
static int create_volume_dir(
int storage_fd,
const char *filename,
Template t) {
int r;
assert(storage_fd >= 0);
assert(filename);
XOpenFlags xopen_flags;
switch (t) {
case TEMPLATE_DIRECTORY:
xopen_flags = 0;
break;
case TEMPLATE_SUBVOLUME:
xopen_flags = XO_SUBVOLUME;
break;
default:
return -ENOMEDIUM; /* Recognizable error for: template doesn't apply here */
}
_cleanup_free_ char *tf = NULL;
r = tempfn_random(filename, /* extra= */ NULL, &tf);
if (r < 0)
return r;
_cleanup_close_ int volume_fd = xopenat_full(storage_fd, tf, O_CREAT|O_EXCL|O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW, xopen_flags, 0700);
if (volume_fd < 0)
return volume_fd;
_cleanup_close_ int root_fd = xopenat_full(volume_fd, "root", O_CREAT|O_EXCL|O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW, xopen_flags, 0755);
if (root_fd < 0) {
r = root_fd;
goto fail;
}
r = RET_NERRNO(fchown(root_fd, FOREIGN_UID_MIN, FOREIGN_UID_MIN));
if (r < 0)
goto fail;
r = rename_noreplace(storage_fd, tf, storage_fd, filename);
if (r < 0)
goto fail;
return TAKE_FD(root_fd);
fail:
if (root_fd >= 0) {
assert(volume_fd >= 0);
root_fd = safe_close(root_fd);
(void) unlinkat(volume_fd, "root", AT_REMOVEDIR);
}
if (volume_fd >= 0) {
volume_fd = safe_close(volume_fd);
(void) unlinkat(storage_fd, tf, AT_REMOVEDIR);
}
return r;
}
static int create_volume_reg(
int storage_fd,
const char *filename,
Template t,
uint64_t create_size) {
int r;
assert(storage_fd >= 0);
assert(filename);
bool sparse;
switch (t) {
case TEMPLATE_SPARSE_FILE:
sparse = true;
break;
case TEMPLATE_ALLOCATED_FILE:
sparse = false;
break;
default:
return -ENOMEDIUM; /* Recognizable error for: template doesn't apply here */
}
_cleanup_free_ char *tf = NULL;
_cleanup_close_ int fd = open_tmpfile_linkable_at(storage_fd, filename, O_RDWR|O_CLOEXEC, &tf);
if (fd < 0)
return fd;
CLEANUP_TMPFILE_AT(storage_fd, tf);
r = chattr_fd(fd, FS_NOCOW_FL, FS_NOCOW_FL);
if (r < 0 && !ERRNO_IS_IOCTL_NOT_SUPPORTED(r))
return r;
if (create_size > 0) {
if (sparse)
r = RET_NERRNO(ftruncate(fd, create_size));
else
r = RET_NERRNO(fallocate(fd, /* mode= */ 0, /* offset= */ 0, create_size));
if (r < 0)
return r;
}
r = RET_NERRNO(fchmod(fd, 0600));
if (r < 0)
return r;
r = link_tmpfile_at(fd, storage_fd, tf, filename, /* flags= */ 0);
if (r < 0)
return r;
tf = mfree(tf); /* disarm clean-up */
return TAKE_FD(fd);
}
static int vl_method_acquire(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
Hashmap **polkit_registry = ASSERT_PTR(userdata);
int r;
assert(link);
struct {
const char *name;
CreateMode create_mode;
const char *template;
int read_only;
VolumeType request_as;
uint64_t create_size;
} p = {
.create_mode = CREATE_ANY,
.read_only = -1,
.request_as = _VOLUME_TYPE_INVALID,
.create_size = UINT64_MAX,
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
{ "createMode", SD_JSON_VARIANT_STRING, json_dispatch_create_mode, voffsetof(p, create_mode), 0 },
{ "template", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, template), 0 },
{ "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
{ "requestAs", SD_JSON_VARIANT_STRING, json_dispatch_volume_type, voffsetof(p, request_as), 0 },
{ "createSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, create_size), 0 },
VARLINK_DISPATCH_POLKIT_FIELD,
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
if (!storage_volume_name_is_valid(p.name))
return sd_varlink_error_invalid_parameter_name(link, "name");
if (!IN_SET(p.create_mode, CREATE_ANY, CREATE_OPEN, CREATE_NEW))
return sd_varlink_error(link, "io.systemd.StorageProvider.CreateNotSupported", NULL);
/* off_t is signed, hence refuse overly long requests */
if (p.create_size != UINT64_MAX && p.create_size > INT64_MAX)
return sd_varlink_error_invalid_parameter_name(link, "createSizeBytes");
Template t = _TEMPLATE_INVALID;
if (!isempty(p.template)) {
if (!storage_template_name_is_valid(p.template))
return sd_varlink_error_invalid_parameter_name(link, "template");
t = template_from_string(p.template);
if (t < 0)
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
}
if (p.read_only > 0) {
if (p.create_mode == CREATE_NEW)
return sd_varlink_error_invalid_parameter_name(link, "readOnly");
p.create_mode = CREATE_OPEN;
}
/* Add a suffix so that we are never attempted to open a temporary file assuming it was a valid
* volume. */
_cleanup_free_ char *filename = strjoin(p.name, ".volume");
if (!filename)
return log_oom_debug();
if (!filename_is_valid(filename))
return sd_varlink_error_invalid_parameter_name(link, "name");
if (arg_runtime_scope != RUNTIME_SCOPE_USER) {
const char *details[] = {
"name", p.name,
NULL
};
r = varlink_verify_polkit_async(
link,
/* bus= */ NULL,
"io.systemd.storage.fs.acquire",
details,
polkit_registry);
if (r <= 0)
return r;
}
_cleanup_close_ int storage_fd = open_storage_dir();
if (storage_fd < 0)
return storage_fd;
_cleanup_close_ int pin_fd = -EBADF, real_fd = -EBADF;
r = chaseat(XAT_FDROOT, storage_fd, filename, CHASE_TRIGGER_AUTOFS, /* ret_path= */ NULL, &pin_fd);
if (r < 0) {
if (r != -ENOENT)
return r;
if (p.create_mode == CREATE_OPEN || p.read_only > 0)
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
/* Doesn't exist yet: create it now */
if (p.request_as < 0) /* Make a choice: pick default type */
p.request_as = t < 0 ? VOLUME_DIR : volume_type_from_template(t);
/* Try to create the volume */
switch (p.request_as) {
case VOLUME_DIR: {
if (t < 0) /* Make a choice: pick default template */
t = TEMPLATE_SUBVOLUME;
real_fd = create_volume_dir(storage_fd, filename, t);
break;
}
case VOLUME_REG: {
if (p.create_size == UINT64_MAX)
return sd_varlink_error(link, "io.systemd.StorageProvider.CreateSizeRequired", NULL);
if (t < 0) /* Make a choice: pick default template */
t = TEMPLATE_SPARSE_FILE;
real_fd = create_volume_reg(storage_fd, filename, t, p.create_size);
break;
}
case VOLUME_BLK:
/* We don't support creating block devices, we only support if they are symlinked
* into the storage directory. */
return sd_varlink_error(link, "io.systemd.StorageProvider.CreateNotSupported", NULL);
default:
assert_not_reached();
}
if (real_fd == -ENOMEDIUM)
return sd_varlink_error(link, "io.systemd.StorageProvider.BadTemplate", NULL);
if (real_fd == -EEXIST) {
if (p.create_mode == CREATE_NEW)
return sd_varlink_error(link, "io.systemd.StorageProvider.VolumeExists", NULL);
/* If we failed to open the volume and reached this point, then the volume already
* exists by now (i.e. we ran into a race). In that case, try to pin it a second time
* (but only once, let's never loop around this). */
r = chaseat(XAT_FDROOT, storage_fd, filename, CHASE_TRIGGER_AUTOFS, /* ret_path= */ NULL, &pin_fd);
if (r < 0)
return r;
} else if (real_fd < 0)
return real_fd;
} else if (p.create_mode == CREATE_NEW)
return sd_varlink_error(link, "io.systemd.StorageProvider.VolumeExists", NULL);
/* At this point, we either already opened the real fd, or we managed to pin it (but not both) */
assert((real_fd >= 0) != (pin_fd >= 0));
/* Let's first settle the volume type */
struct stat st;
if (fstat(real_fd >= 0 ? real_fd : pin_fd, &st) < 0)
return -errno;
if (p.request_as == VOLUME_REG) {
/* First, check for the other supported types and generate a nice error */
if (IN_SET(st.st_mode & S_IFMT, S_IFDIR, S_IFBLK))
return sd_varlink_error(link, "io.systemd.StorageProvider.WrongType", NULL);
/* Second verify cover all other types */
r = stat_verify_regular(&st);
if (r < 0)
return r;
} else if (p.request_as == VOLUME_DIR) {
if (IN_SET(st.st_mode & S_IFMT, S_IFREG, S_IFBLK))
return sd_varlink_error(link, "io.systemd.StorageProvider.WrongType", NULL);
r = stat_verify_directory(&st);
if (r < 0)
return r;
} else if (p.request_as == VOLUME_BLK) {
if (IN_SET(st.st_mode & S_IFMT, S_IFREG, S_IFDIR))
return sd_varlink_error(link, "io.systemd.StorageProvider.WrongType", NULL);
r = stat_verify_block(&st);
if (r < 0)
return r;
} else if (S_ISREG(st.st_mode))
p.request_as = VOLUME_REG;
else if (S_ISDIR(st.st_mode))
p.request_as = VOLUME_DIR;
else if (S_ISBLK(st.st_mode))
p.request_as = VOLUME_BLK;
else
return log_debug_errno(SYNTHETIC_ERRNO(EBADF), "Unexpected inode type, refusing.");
/* Let's now acquire a real fd for the pinned fd, if we still need to */
if (real_fd < 0) {
assert(pin_fd >= 0);
XOpenFlags xopen_flags =
(p.read_only < 0 && !S_ISDIR(st.st_mode) ? XO_AUTO_RW_RO : 0);
int open_flags =
(p.read_only < 0 ? 0 : (p.read_only > 0 || S_ISDIR(st.st_mode) ? O_RDONLY : O_RDWR));
const char *subdir = NULL;
if (p.request_as == VOLUME_DIR) {
/* We place the root of the directory tree one level down, to separate ownership of
* the inode: the upper inode is owned by the host, the lower one by the volume. This
* matters so that the host one can be owned by the host's root, and the volume one
* by the foreign UID range. */
subdir = "root";
open_flags |= O_DIRECTORY|O_NOFOLLOW;
}
real_fd = xopenat_full(pin_fd, subdir, open_flags|O_CLOEXEC, xopen_flags, /* mode= */ MODE_INVALID);
if (real_fd < 0)
return log_debug_errno(real_fd, "Failed to reopen volume fd for '%s': %m", filename);
/* In directory mode we might be looking at a different inode node, refresh the stat data */
if (p.request_as == VOLUME_DIR && fstat(real_fd, &st) < 0)
return -errno;
}
assert(real_fd >= 0);
bool ro;
switch (p.request_as) {
case VOLUME_REG:
case VOLUME_BLK: {
assert(IN_SET(st.st_mode & S_IFMT, S_IFREG, S_IFBLK));
int open_flags = fcntl(real_fd, F_GETFL, 0);
if (open_flags < 0)
return -errno;
ro = (open_flags & O_ACCMODE_STRICT) == O_RDONLY;
break;
}
case VOLUME_DIR: {
assert(S_ISDIR(st.st_mode));
if (!uid_is_foreign(st.st_uid) ||
!gid_is_foreign(st.st_gid))
return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Storage directory not owned by foreign UID/GID range.");
/* Let's now generate a new mount for the directory tree, where propagation is disabled, and the
* flags are all set to good defaults */
_cleanup_close_ int mount_fd = open_tree_attr_with_fallback(
real_fd,
/* path= */ NULL,
OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW,
&(struct mount_attr) {
.attr_set = (p.read_only > 0 ? MOUNT_ATTR_RDONLY : 0),
.attr_clr = MOUNT_ATTR_NOSUID|MOUNT_ATTR_NOEXEC|MOUNT_ATTR_NODEV,
.propagation = MS_PRIVATE,
});
if (mount_fd < 0)
return log_debug_errno(mount_fd, "Failed to generate per-volume mount: %m");
/* Let's turn on propagation again now that it is disconnected, simply because MS_SHARED is
* generally the default for everything we return. */
if (mount_setattr(mount_fd, "", AT_EMPTY_PATH|AT_SYMLINK_NOFOLLOW,
&(struct mount_attr) {
.propagation = MS_SHARED,
}, MOUNT_ATTR_SIZE_VER0) < 0)
return log_debug_errno(errno, "Failed to enable propagation on per-volume mount: %m");
close_and_replace(real_fd, mount_fd);
r = fd_is_read_only_fs(real_fd);
if (r < 0)
return r;
ro = r > 0;
break;
}
default:
assert_not_reached();
}
if (p.read_only == 0 && ro)
return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
int idx = sd_varlink_push_fd(link, real_fd);
if (idx < 0)
return idx;
TAKE_FD(real_fd);
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", idx),
SD_JSON_BUILD_PAIR_STRING("type", inode_type_to_string(st.st_mode)),
SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", ro),
SD_JSON_BUILD_PAIR_CONDITION(p.request_as == VOLUME_DIR, "baseUID", SD_JSON_BUILD_INTEGER(FOREIGN_UID_BASE)),
SD_JSON_BUILD_PAIR_CONDITION(p.request_as == VOLUME_DIR, "baseGID", SD_JSON_BUILD_INTEGER(FOREIGN_UID_BASE)));
}
static int vl_server(void) {
int r;
_cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL;
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
r = varlink_server_new(
&varlink_server,
SD_VARLINK_SERVER_HANDLE_SIGINT|
SD_VARLINK_SERVER_HANDLE_SIGTERM|
SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT|
SD_VARLINK_SERVER_INHERIT_USERDATA,
&polkit_registry);
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_StorageProvider);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
varlink_server,
"io.systemd.StorageProvider.Acquire", vl_method_acquire,
"io.systemd.StorageProvider.ListVolumes", vl_method_list_volumes,
"io.systemd.StorageProvider.ListTemplates", vl_method_list_templates);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = sd_varlink_server_loop_auto(varlink_server);
if (r < 0)
return log_error_errno(r, "Failed to run Varlink event loop: %m");
return 0;
}
static int help(void) {
int r;
help_cmdline("[OPTIONS...]");
help_abstract("Simple file system backed storage provider");
_cleanup_(table_unrefp) Table *options = NULL;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
help_section("Options:");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("systemd-storage-fs", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
assert(argc >= 0);
assert(argv);
OptionParser opts = { argc, argv };
FOREACH_OPTION(c, &opts, /* on_error= */ return c)
switch (c) {
OPTION_COMMON_HELP:
return help();
OPTION_COMMON_VERSION:
return version();
OPTION_LONG("system", NULL, "Operate in system mode"):
arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
break;
OPTION_LONG("user", NULL, "Operate in user mode"):
arg_runtime_scope = RUNTIME_SCOPE_USER;
break;
}
if (option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
return 1;
}
static int run(int argc, char* argv[]) {
int r;
log_setup();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
return vl_server();
}
DEFINE_MAIN_FUNCTION(run);
+23
View File
@@ -0,0 +1,23 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "json-util.h"
#include "string-table.h"
#include "storage-util.h"
static const char *volume_type_table[_VOLUME_TYPE_MAX] = {
[VOLUME_BLK] = "blk",
[VOLUME_REG] = "reg",
[VOLUME_DIR] = "dir",
};
static const char *create_mode_table[_CREATE_MODE_MAX] = {
[CREATE_ANY] = "any",
[CREATE_NEW] = "new",
[CREATE_OPEN] = "open",
};
DEFINE_STRING_TABLE_LOOKUP(volume_type, VolumeType);
DEFINE_STRING_TABLE_LOOKUP(create_mode, CreateMode);
JSON_DISPATCH_ENUM_DEFINE(json_dispatch_volume_type, VolumeType, volume_type_from_string);
JSON_DISPATCH_ENUM_DEFINE(json_dispatch_create_mode, CreateMode, create_mode_from_string);
+43
View File
@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-json.h"
#include "string-table-fundamental.h"
#include "string-util.h"
/* This closely follows the kernel's inode type naming, i.e. is supposed to be a subset of what
* inode_type_from_string() parses. */
typedef enum VolumeType {
VOLUME_BLK,
VOLUME_REG,
VOLUME_DIR,
_VOLUME_TYPE_MAX,
_VOLUME_TYPE_INVALID = -EINVAL,
} VolumeType;
typedef enum CreateMode {
CREATE_ANY,
CREATE_NEW,
CREATE_OPEN,
_CREATE_MODE_MAX,
_CREATE_MODE_INVALID = -EINVAL,
} CreateMode;
DECLARE_STRING_TABLE_LOOKUP(volume_type, VolumeType);
DECLARE_STRING_TABLE_LOOKUP(create_mode, CreateMode);
int json_dispatch_volume_type(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_create_mode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
static inline bool storage_volume_name_is_valid(const char *n) {
return string_is_safe(n, /* flags= */ 0);
}
static inline bool storage_template_name_is_valid(const char *n) {
return string_is_safe(n, /* flags= */ 0);
}
static inline bool storage_provider_name_is_valid(const char *n) {
return string_is_safe(n, STRING_FILENAME);
}
+812
View File
@@ -0,0 +1,812 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-varlink.h"
#include <getopt.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#include "alloc-util.h"
#include "ansi-color.h"
#include "argv-util.h"
#include "build.h"
#include "bus-util.h"
#include "errno-list.h"
#include "escape.h"
#include "extract-word.h"
#include "fd-util.h"
#include "format-table.h"
#include "format-util.h"
#include "help-util.h"
#include "json-util.h"
#include "main-func.h"
#include "mount-util.h"
#include "namespace-util.h"
#include "options.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "path-lookup.h"
#include "path-util.h"
#include "polkit-agent.h"
#include "recurse-dir.h"
#include "runtime-scope.h"
#include "socket-util.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "storage-util.h"
#include "string-util.h"
#include "strv.h"
#include "user-util.h"
#include "varlink-util.h"
#include "verbs.h"
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_ask_password = true;
static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
static int help(void) {
int r;
help_cmdline("[OPTIONS...] COMMAND");
help_abstract("Enumerate storage volumes and providers.");
_cleanup_(table_unrefp) Table *verbs = NULL;
r = verbs_get_help_table(&verbs);
if (r < 0)
return r;
_cleanup_(table_unrefp) Table *options = NULL;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
(void) table_sync_column_widths(0, verbs, options);
help_section("Commands:");
r = table_print_or_warn(verbs);
if (r < 0)
return r;
help_section("Options:");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("storagectl", "1");
return 0;
}
VERB_COMMON_HELP_HIDDEN(help);
static const char *ro_color(int ro) {
if (ro > 0)
return ansi_highlight_red();
if (ro == 0)
return ansi_highlight_green();
return NULL;
}
static int on_list_reply(
sd_varlink *link,
sd_json_variant *parameters,
const char *error_id,
sd_varlink_reply_flags_t flags,
void* userdata) {
Table *t = ASSERT_PTR(userdata);
int r;
assert(link);
const char *d = ASSERT_PTR(sd_varlink_get_description(link));
if (error_id) {
log_debug("%s: Received error '%s', ignoring.", d, error_id);
return 0;
}
_cleanup_free_ char *provider = NULL;
r = path_extract_filename(d, &provider);
if (r < 0)
return log_error_errno(r, "Failed to extract provider name from socket path: %m");
struct {
const char *name;
const char *type;
int read_only;
uint64_t size_bytes;
uint64_t used_bytes;
} p = {
.read_only = -1,
.size_bytes = UINT64_MAX,
.used_bytes = UINT64_MAX,
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), 0 },
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), 0 },
{ "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
{ "sizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, size_bytes), 0 },
{ "usedBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, used_bytes), 0 },
{}
};
r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
if (r < 0)
return log_error_errno(r, "Failed to decode List() reply: %m");
r = table_add_many(
t,
TABLE_STRING, provider,
TABLE_STRING, p.name,
TABLE_STRING, p.type,
TABLE_TRISTATE, p.read_only,
TABLE_SET_COLOR, ro_color(p.read_only));
if (r < 0)
return table_log_add_error(r);
if (p.size_bytes == UINT64_MAX)
r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
else
r = table_add_many(t, TABLE_SIZE, p.size_bytes, TABLE_SET_ALIGN_PERCENT, 100);
if (r < 0)
return table_log_add_error(r);
if (p.used_bytes == UINT64_MAX)
r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
else
r = table_add_many(t, TABLE_SIZE, p.used_bytes, TABLE_SET_ALIGN_PERCENT, 100);
if (r < 0)
return table_log_add_error(r);
return 0;
}
VERB(verb_list_volumes, "volumes", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, VERB_DEFAULT, "List storage volumes");
static int verb_list_volumes(int argc, char *argv[], uintptr_t data, void *userdata) {
int r;
assert(argc <= 2);
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
_cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type", "ro", "size", "used");
if (!t)
return log_oom();
(void) table_set_sort(t, (size_t) 0, (size_t) 1);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
if (argc >= 2) {
r = sd_json_buildo(
&v,
SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
if (r < 0)
return log_oom();
}
ssize_t n = varlink_execute_directory(
socket_path,
"io.systemd.StorageProvider.ListVolumes",
v,
/* more= */ true,
/* timeout_usec= */ 0, /* 0 means default */
on_list_reply,
t);
if (n < 0 && n != -ENOENT)
return log_error_errno(n, "Failed to enumerate storage volumes: %m");
if (!table_isempty(t)) {
r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
}
if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (table_isempty(t))
printf("No storage volumes.\n");
else
printf("\n%zu storage volumes listed.\n", table_get_rows(t) - 1);
}
return 0;
}
static int on_list_templates_reply(
sd_varlink *link,
sd_json_variant *parameters,
const char *error_id,
sd_varlink_reply_flags_t flags,
void* userdata) {
Table *t = ASSERT_PTR(userdata);
int r;
assert(link);
const char *d = ASSERT_PTR(sd_varlink_get_description(link));
if (error_id) {
log_debug("%s: Received error '%s', ignoring.", d, error_id);
return 0;
}
_cleanup_free_ char *provider = NULL;
r = path_extract_filename(d, &provider);
if (r < 0)
return log_error_errno(r, "Failed to extract provider name from socket path: %m");
struct {
const char *name;
const char *type;
} p = {
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), 0 },
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), 0 },
{}
};
r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
if (r < 0)
return log_error_errno(r, "Failed to decode ListTemplates() reply: %m");
r = table_add_many(
t,
TABLE_STRING, provider,
TABLE_STRING, p.name,
TABLE_STRING, p.type);
if (r < 0)
return table_log_add_error(r);
return 0;
}
VERB(verb_templates, "templates", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, /* flags= */ 0, "List storage volume templates");
static int verb_templates(int argc, char *argv[], uintptr_t data, void *userdata) {
int r;
assert(argc <= 2);
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
_cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type");
if (!t)
return log_oom();
(void) table_set_sort(t, (size_t) 0, (size_t) 1);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
if (argc >= 2) {
r = sd_json_buildo(
&v,
SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
if (r < 0)
return log_oom();
}
ssize_t n = varlink_execute_directory(
socket_path,
"io.systemd.StorageProvider.ListTemplates",
v,
/* more= */ true,
/* timeout_usec= */ 0, /* 0 means default */
on_list_templates_reply,
t);
if (n < 0 && n != -ENOENT)
return log_error_errno(n, "Failed to enumerate storage volume templates: %m");
if (!table_isempty(t)) {
r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
}
if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (table_isempty(t))
printf("No templates.\n");
else
printf("\n%zu templates listed.\n", table_get_rows(t) - 1);
}
return 0;
}
VERB_NOARG(verb_providers, "providers", "List storage providers");
static int verb_providers(int argc, char *argv[], uintptr_t data, void *userdata) {
int r;
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
_cleanup_(table_unrefp) Table *t = table_new("provider", "listening");
if (!t)
return log_oom();
(void) table_set_sort(t, (size_t) 0);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
_cleanup_close_ int fd = open(socket_path, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
if (fd < 0) {
if (errno != ENOENT)
return log_error_errno(errno, "Failed to open '%s': %m", socket_path);
} else {
_cleanup_free_ DirectoryEntries *dentries = NULL;
r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries);
if (r < 0)
return log_error_errno(r, "Failed to enumerate '%s': %m", socket_path);
FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
struct dirent *de = *dp;
if (de->d_type != DT_SOCK)
continue;
if (!storage_provider_name_is_valid(de->d_name))
continue;
_cleanup_close_ int socket_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if (socket_fd < 0)
return log_error_errno(errno, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
_cleanup_free_ char *no = NULL;
r = connect_unix_path(socket_fd, fd, de->d_name);
if (r < 0) {
no = strjoin("no (", ERRNO_NAME(r), ")");
if (!no)
return log_oom();
}
r = table_add_many(t,
TABLE_STRING, de->d_name,
TABLE_STRING, no ?: "yes",
TABLE_SET_COLOR, ansi_highlight_green_red(!no));
if (r < 0)
return table_log_add_error(r);
}
}
if (!table_isempty(t)) {
r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
}
if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (table_isempty(t))
printf("No providers.\n");
else
printf("\n%zu providers listed.\n", table_get_rows(t) - 1);
}
return 0;
}
static int parse_argv(int argc, char *argv[], char ***args) {
int r;
assert(argc >= 0);
assert(argv);
OptionParser opts = { argc, argv };
FOREACH_OPTION(c, &opts, /* on_error= */ return c)
switch (c) {
OPTION_COMMON_HELP:
return help();
OPTION_COMMON_VERSION:
return version();
OPTION_COMMON_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
OPTION_COMMON_NO_LEGEND:
arg_legend = false;
break;
OPTION_COMMON_JSON:
r = parse_json_argument(opts.arg, &arg_json_format_flags);
if (r <= 0)
return r;
break;
OPTION_COMMON_NO_ASK_PASSWORD:
arg_ask_password = false;
break;
OPTION_LONG("system", NULL, "Operate in system mode"):
arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
break;
OPTION_LONG("user", NULL, "Operate in user mode"):
arg_runtime_scope = RUNTIME_SCOPE_USER;
break;
}
*args = option_parser_get_args(&opts);
return 1;
}
static int run_as_mount_helper(int argc, char *argv[]) {
int c, r;
/* Implements util-linux "external helper" command line interface, as per mount(8) man page.
*
* Usage:
*
* mount -t storage fs:mydirvolume /some/place # Directory volumes
* mount -t storage.ext4 fs:myblkvolume /some/place # Block volumes
*/
const char *fstype = NULL, *options = NULL;
bool fake = false;
while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) {
switch (c) {
case 'f':
fake = true;
break;
case 'o':
options = optarg;
break;
case 't':
fstype = startswith(optarg, "storage.");
if (fstype) {
/* Paranoia: don't allow "storage.storage.storage.…" chains... */
if (startswith(fstype, "storage.") || streq(fstype, "storage"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing nested storage volumes.");
} else if (!streq(optarg, "storage"))
log_warning("Unexpected file system type '%s', ignoring.", optarg);
break;
case 's': /* sloppy mount options */
case 'n': /* aka --no-mtab */
case 'v': /* aka --verbose */
log_debug("Ignoring option -%c, not implemented.", c);
break;
case 'N': /* aka --namespace= */
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c);
case '?':
return -EINVAL;
}
}
if (optind + 2 != argc)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Expected a storage volume specification and target directory as only arguments.");
const char *colon = strchr(argv[optind], ':');
if (!colon)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume specification, refusing: %s", argv[optind]);
_cleanup_free_ char *provider = strndup(argv[optind], colon - argv[optind]);
if (!provider)
return log_oom();
if (!storage_provider_name_is_valid(provider))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage provider name: %s", provider);
_cleanup_free_ char *name = strdup(colon + 1);
if (!name)
return log_oom();
if (!storage_volume_name_is_valid(name))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume name: %s", name);
_cleanup_free_ char *path = NULL;
r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &path);
if (r < 0)
return r;
_cleanup_free_ char *filtered = NULL, *template = NULL;
CreateMode create_mode = _CREATE_MODE_INVALID;
uint64_t create_size = UINT64_MAX;
int read_only = -1;
for (const char *p = options;;) {
_cleanup_free_ char *word = NULL;
r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE|EXTRACT_UNESCAPE_SEPARATORS);
if (r < 0)
return log_error_errno(r, "Failed to extract mount option: %m");
if (r == 0)
break;
const char *t = startswith(word, "storage.");
if (t) {
const char *v;
if ((v = startswith(t, "create="))) {
create_mode = create_mode_from_string(v);
if (create_mode < 0)
return log_error_errno(create_mode, "Failed to parse storage.create= parameter: %s", v);
} else if ((v = startswith(t, "create-size="))) {
r = parse_size(v, /* base= */ 1024, &create_size);
if (r < 0)
return log_error_errno(r, "Failed to parse storage.create-size= parameter: %s", v);
} else if ((v = startswith(t, "template="))) {
if (!storage_template_name_is_valid(v))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid template name, refusing: %s", v);
r = free_and_strdup(&template, v);
if (r < 0)
return log_oom();
} else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown mount option '%s', refusing.", word);
} else if (streq(word, "ro"))
read_only = true;
else if (streq(word, "rw"))
read_only = false;
else if (!strextend_with_separator(&filtered, ",", word))
return log_oom();
}
if (fake)
return 0;
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
if (!path_extend(&socket_path, provider))
return log_oom();
_cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
r = sd_varlink_connect_address(&link, socket_path);
if (r < 0)
return log_error_errno(r, "Failed to connect to '%s': %m", socket_path);
r = sd_varlink_set_allow_fd_passing_input(link, true);
if (r < 0)
return log_error_errno(r, "Failed to enable file descriptor passing: %m");
(void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
sd_json_variant *mreply = NULL;
const char *merror_id = NULL, *vtype = fstype ? "reg" : "dir";
r = sd_varlink_callbo(
link,
"io.systemd.StorageProvider.Acquire",
&mreply,
&merror_id,
SD_JSON_BUILD_PAIR_STRING("name", name),
SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
SD_JSON_BUILD_PAIR_STRING("requestAs", vtype),
SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
if (r < 0)
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
_cleanup_(sd_json_variant_unrefp) sd_json_variant *reply = sd_json_variant_ref(mreply);
if (merror_id) {
/* Copy out the error ID, as the follow-up call will invalidate it */
_cleanup_free_ char *error_id = strdup(merror_id);
if (!error_id)
return log_oom();
/* Hmm, the type might not have been right for the backend or the volume? then try
* again, and switch from "reg" to "blk", maybe it works then. (We keep the original
* reply referenced, since we prefer generating an error for the first error.) */
if (streq(vtype, "reg") && STR_IN_SET(error_id,
"io.systemd.StorageProvider.TypeNotSupported",
"io.systemd.StorageProvider.WrongType")) {
sd_json_variant *freply = NULL;
const char *ferror_id = NULL;
r = sd_varlink_callbo(
link,
"io.systemd.StorageProvider.Acquire",
&freply,
&ferror_id,
SD_JSON_BUILD_PAIR_STRING("name", name),
SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
SD_JSON_BUILD_PAIR_STRING("requestAs", "blk"),
SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
if (r < 0)
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
if (!ferror_id) {
/* The 2nd call worked? then let's forget about the first failure */
sd_json_variant_unref(reply);
reply = sd_json_variant_ref(freply);
error_id = mfree(error_id);
}
/* NB: if both fail we show the Varlink error of the first call here, i.e. of the preferred type */
}
if (error_id) {
if (streq(error_id, "io.systemd.StorageProvider.NoSuchVolume"))
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Volume '%s' not known.", name);
if (streq(error_id, "io.systemd.StorageProvider.NoSuchTemplate"))
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Template '%s' not known.", template);
if (streq(error_id, "io.systemd.StorageProvider.VolumeExists"))
return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Volume '%s' exists already.", name);
if (streq(error_id, "io.systemd.StorageProvider.TypeNotSupported"))
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support the specified volume type '%s'.", vtype);
if (streq(error_id, "io.systemd.StorageProvider.WrongType"))
return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Volume '%s' is not of type '%s'.", name, vtype);
if (streq(error_id, "io.systemd.StorageProvider.CreateNotSupported"))
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support creating volumes.");
if (streq(error_id, "io.systemd.StorageProvider.CreateSizeRequired"))
return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Storage provider requires a create size to be provided when creating volumes on-the-fly. Use 'storage.create-size=' mount option.");
if (streq(error_id, "io.systemd.StorageProvider.ReadOnlyVolume"))
return log_error_errno(SYNTHETIC_ERRNO(EROFS), "Volume '%s' is read-only.", name);
if (streq(error_id, "io.systemd.StorageProvider.BadTemplate"))
return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Template does not apply to this volume type.");
r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */
if (r != -EBADR)
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %s", error_id);
}
}
struct {
unsigned fd_idx;
int read_only;
const char *type;
uid_t base_uid;
gid_t base_gid;
} p = {
.fd_idx = UINT_MAX,
.read_only = -1,
.base_uid = UID_INVALID,
.base_gid = GID_INVALID,
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "fileDescriptorIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, fd_idx), SD_JSON_MANDATORY },
{ "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), SD_JSON_MANDATORY },
{ "baseUID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(p, base_uid), 0 },
{ "baseGID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(p, base_gid), 0 },
{}
};
r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
if (r < 0)
return log_error_errno(r, "Failed to decode Acquire() reply: %m");
_cleanup_close_ int fd = sd_varlink_take_fd(link, p.fd_idx);
if (fd < 0)
return log_error_errno(fd, "Failed to acquire fd from Varlink connection: %m");
struct stat st;
if (fstat(fd, &st) < 0)
return log_error_errno(errno, "Failed to stat returned file descriptor: %m");
_cleanup_strv_free_ char **cmdline = strv_new("mount", "-c");
if (!cmdline)
return log_oom();
if (fstype) {
if (!STR_IN_SET(p.type, "reg", "blk"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mounting as file system type '%s' requested, but volume is not a block device or regular file.", fstype);
r = stat_verify_regular_or_block(&st);
if (r < 0)
return log_error_errno(r, "File descriptor for block/regular volume is not a block or regular inode: %m");
if (strv_extend_strv(&cmdline, STRV_MAKE("-t", fstype), /* filter_duplicates= */ false) < 0)
return log_oom();
} else {
if (!streq(p.type, "dir"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mount as directory requested, but volume is not a directory.");
if (!uid_is_valid(p.base_uid) || !gid_is_valid(p.base_gid))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provider did not report base UID/GID, cannot mount.");
if (p.base_uid > UINT32_MAX - 0x10000U ||
p.base_gid > UINT32_MAX - 0x10000U)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Returned base UID/GID out of range.");
r = stat_verify_directory(&st);
if (r < 0)
return log_error_errno(r, "File descriptor for directory volume is not a directory inode: %m");
if (st.st_uid < p.base_uid || st.st_uid >= p.base_uid + 0x10000 ||
st.st_gid < p.base_gid || st.st_gid >= p.base_gid + 0x10000)
return log_error_errno(SYNTHETIC_ERRNO(EPERM), "File descriptor for directory volume is not owned by base UID/GID range, refusing.");
/* Now move the mount into our own UID/GID range */
_cleanup_free_ char *uid_line = asprintf_safe(
UID_FMT " " UID_FMT " " UID_FMT "\n",
p.base_uid, (uid_t) 0, (uid_t) 0x10000);
_cleanup_free_ char *gid_line = asprintf_safe(
GID_FMT " " GID_FMT " " GID_FMT "\n",
p.base_gid, (gid_t) 0, (gid_t) 0x10000);
if (!uid_line || !gid_line)
return log_oom();
_cleanup_close_ int userns_fd = userns_acquire(uid_line, gid_line, /* setgroups_deny= */ true);
if (userns_fd < 0)
return log_error_errno(userns_fd, "Failed to acquire new user namespace: %m");
_cleanup_close_ int remapped_fd = open_tree_attr_with_fallback(
fd,
/* path= */ NULL,
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC,
&(struct mount_attr) {
.attr_set = MOUNT_ATTR_IDMAP,
.userns_fd = userns_fd,
});
if (remapped_fd < 0)
return log_error_errno(remapped_fd, "Failed to set ID mapping on returned mount: %m");
close_and_replace(fd, remapped_fd);
if (strv_extend(&cmdline, "--bind") < 0)
return log_oom();
}
if (p.read_only > 0)
read_only = true;
if (!strextend_with_separator(&filtered, ",", read_only > 0 ? "ro" : "rw"))
return log_oom();
if (strv_extend_strv(&cmdline, STRV_MAKE("-o", filtered), /* filter_duplicates= */ false) < 0)
return log_oom();
if (strv_extend_strv(&cmdline, STRV_MAKE(FORMAT_PROC_FD_PATH(fd), path), /* filter_duplicates= */ false) < 0)
return log_oom();
r = fd_cloexec(fd, false);
if (r < 0)
return log_error_errno(r, "Failed to disable O_CLOEXEC for mount fd: %m");
if (DEBUG_LOGGING) {
_cleanup_free_ char *q = quote_command_line(cmdline, SHELL_ESCAPE_EMPTY);
log_debug("Chain-loading: %s", strna(q));
}
/* NB: we do not honour $PATH here, since as plugin to /bin/mount we might be called in a setuid()
* context, and hence don't want to chain to programs potentially under user control. */
execv("/bin/mount", cmdline);
return log_error_errno(errno, "Failed to execute mount tool: %m");
}
static int run(int argc, char *argv[]) {
int r;
log_setup();
if (invoked_as(argv, "mount.storage"))
return run_as_mount_helper(argc, argv);
char **args = NULL;
r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
return dispatch_verb_with_args(args, /* userdata= */ NULL);
}
DEFINE_MAIN_FUNCTION(run);
+211
View File
@@ -0,0 +1,211 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
if ! command -v storagectl >/dev/null; then
echo "storagectl not found, skipping."
exit 77
fi
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
# Unset $PAGER so we don't have to use --no-pager everywhere
export PAGER=
# storagectl runs in a VM-only test
if systemd-detect-virt -cq ; then
echo "can't run in a container, skipping."
exit 77
fi
at_exit() {
set +e
if [[ -n "${MOUNT_DIR:-}" ]] && mountpoint -q "$MOUNT_DIR"; then
umount "$MOUNT_DIR"
fi
if [[ -n "${LOOP:-}" ]]; then
systemd-dissect --detach "$LOOP"
fi
if [[ -n "${WORK_DIR:-}" ]]; then
rm -fr "$WORK_DIR"
fi
rm -fr /var/lib/storage/test-87-storage-*.volume
}
trap at_exit EXIT
# The storage providers are socket-activated by sockets.target, so the listening
# AF_UNIX sockets should already exist.
test -S /run/systemd/io.systemd.StorageProvider/block
test -S /run/systemd/io.systemd.StorageProvider/fs
WORK_DIR="$(mktemp -d /tmp/test-storagectl.XXXXXXXXXX)"
MOUNT_DIR="$WORK_DIR/mnt"
mkdir -p "$MOUNT_DIR"
# --- storagectl basic ---
storagectl --help
storagectl --version
storagectl help
# Unknown verb / option
(! storagectl this-verb-does-not-exist)
(! storagectl --no-such-option providers)
# --- storagectl providers ---
storagectl providers
storagectl providers --no-legend
storagectl providers --no-pager
storagectl providers --json=pretty | jq .
storagectl providers --json=short | jq .
providers_output="$(storagectl providers --no-legend)"
assert_in 'block' "$providers_output"
assert_in 'fs' "$providers_output"
assert_in 'yes' "$providers_output"
# --- storagectl volumes ---
# 'volumes' is the default verb
storagectl
storagectl volumes
storagectl volumes --no-legend
storagectl volumes --no-pager
storagectl volumes --json=pretty | jq .
storagectl volumes --json=short | jq .
# Glob filter that matches nothing should not error
storagectl volumes 'no-such-volume-*'
# --- storagectl templates ---
storagectl templates
storagectl templates --no-legend --no-pager
storagectl templates --json=pretty | jq .
storagectl templates --json=short | jq --seq .
templates_output="$(storagectl templates --no-legend)"
assert_in 'sparse-file' "$templates_output"
assert_in 'allocated-file' "$templates_output"
assert_in 'directory' "$templates_output"
assert_in 'subvolume' "$templates_output"
# Glob filter
storagectl templates 'sparse-*' --no-legend | grep sparse-file >/dev/null
(! storagectl templates 'sparse-*' --no-legend | grep allocated-file >/dev/null)
storagectl templates 'no-such-template-*'
# --- direct varlink calls ---
varlinkctl introspect /run/systemd/io.systemd.StorageProvider/block io.systemd.StorageProvider
varlinkctl introspect /run/systemd/io.systemd.StorageProvider/fs io.systemd.StorageProvider
# Block provider does not expose templates
varlinkctl call --more /run/systemd/io.systemd.StorageProvider/block \
io.systemd.StorageProvider.ListTemplates '{}' \
--graceful=io.systemd.StorageProvider.NoSuchTemplate
# fs provider lists the four built-in templates
varlinkctl call --more --json=short /run/systemd/io.systemd.StorageProvider/fs \
io.systemd.StorageProvider.ListTemplates '{}' | grep '"name":"sparse-file"' >/dev/null
# Block provider rejects names not under /dev/
varlinkctl call /run/systemd/io.systemd.StorageProvider/block \
io.systemd.StorageProvider.Acquire '{"name":"/tmp/no-such-dev"}' \
--graceful=io.systemd.StorageProvider.NoSuchVolume
# fs provider rejects bad volume names (contain '/' → not a valid filename)
varlinkctl call /run/systemd/io.systemd.StorageProvider/fs \
io.systemd.StorageProvider.Acquire '{"name":"bad/name"}' \
--graceful=org.varlink.service.InvalidParameter
# --- mount.storage: regular file via fs provider ---
TESTVOL_REG="test-87-storage-reg-$RANDOM"
truncate -s 32M "/var/lib/storage/$TESTVOL_REG.volume"
mkfs.ext4 "/var/lib/storage/$TESTVOL_REG.volume"
mount -t storage.ext4 "fs:$TESTVOL_REG" "$MOUNT_DIR"
mountpoint -q "$MOUNT_DIR"
echo "hello reg" >"$MOUNT_DIR/hello"
umount "$MOUNT_DIR"
# Volume now appears in 'storagectl volumes'
volumes_after_create="$(storagectl volumes "$TESTVOL_REG" --no-legend)"
assert_in "$TESTVOL_REG" "$volumes_after_create"
assert_in 'reg' "$volumes_after_create"
# Re-mount existing (default storage.create=any)
mount -t storage.ext4 "fs:$TESTVOL_REG" "$MOUNT_DIR"
test -f "$MOUNT_DIR/hello"
umount "$MOUNT_DIR"
# storage.create=open succeeds for existing volume
mount -t storage.ext4 -o "storage.create=open" "fs:$TESTVOL_REG" "$MOUNT_DIR"
umount "$MOUNT_DIR"
# storage.create=new on existing volume must fail
(! mount -t storage.ext4 -o "storage.create=new,storage.create-size=16M" "fs:$TESTVOL_REG" "$MOUNT_DIR")
# Read-only mount
mount -t storage.ext4 -o ro "fs:$TESTVOL_REG" "$MOUNT_DIR"
findmnt -n -o options "$MOUNT_DIR" | grep -E '(^|,)ro(,|$)' >/dev/null
(! touch "$MOUNT_DIR/readonly-test")
umount "$MOUNT_DIR"
rm -f "/var/lib/storage/$TESTVOL_REG.volume"
# storage.create=open on missing volume must fail
(! mount -t storage.ext4 -o "storage.create=open" "fs:test-87-storage-missing-$RANDOM" "$MOUNT_DIR")
# --- mount.storage: directory volume via fs provider (requires idmapped mounts) ---
TESTVOL_DIR="test-87-storage-dir-$RANDOM"
if mount -t storage "fs:$TESTVOL_DIR" "$MOUNT_DIR"; then
mountpoint -q "$MOUNT_DIR"
test -d "/var/lib/storage/$TESTVOL_DIR.volume/root"
echo "dir test" >"$MOUNT_DIR/hello"
test -f "/var/lib/storage/$TESTVOL_DIR.volume/root/hello"
umount "$MOUNT_DIR"
rm -fr "/var/lib/storage/$TESTVOL_DIR.volume"
else
echo "Directory volume mounting failed (idmapped mounts unsupported?), skipping."
rm -fr "/var/lib/storage/$TESTVOL_DIR.volume"
fi
# --- mount.storage: block device via block provider ---
truncate -s 32M "$WORK_DIR/block.img"
mkfs.ext4 -L sd-storage-blk "$WORK_DIR/block.img"
LOOP="$(systemd-dissect --attach --loop-ref=test-storagectl "$WORK_DIR/block.img")"
mount -t storage.ext4 "block:$LOOP" "$MOUNT_DIR"
mountpoint -q "$MOUNT_DIR"
echo "hello blk" >"$MOUNT_DIR/hello"
umount "$MOUNT_DIR"
# Read-only mount of the block volume
mount -t storage.ext4 -o ro "block:$LOOP" "$MOUNT_DIR"
findmnt -n -o options "$MOUNT_DIR" | grep -E '(^|,)ro(,|$)' >/dev/null
test -f "$MOUNT_DIR/hello"
umount "$MOUNT_DIR"
# Block volume is enumerable; matchName globs over device node and aliases
varlinkctl call --more --json=short /run/systemd/io.systemd.StorageProvider/block \
io.systemd.StorageProvider.ListVolumes "{\"matchName\":\"$LOOP\"}" |
grep '"type":"blk"' >/dev/null
systemd-dissect --detach "$LOOP"
unset LOOP
# --- error cases ---
# Bad provider name (no such socket)
(! mount -t storage.ext4 "no-such-provider:foo" "$MOUNT_DIR")
# Bad volume specification (no colon)
(! mount -t storage.ext4 "no-colon-here" "$MOUNT_DIR")
# Refuse nested storage volumes (FS type "storage.storage")
(! mount -t storage.storage "fs:something" "$MOUNT_DIR")
+14
View File
@@ -804,6 +804,20 @@ units = [
'conditions' : ['ENABLE_SYSUSERS'],
'symlinks' : ['sysinit.target.wants/'],
},
{
'file' : 'systemd-storage-block.socket',
'symlinks' : ['sockets.target.wants/']
},
{
'file' : 'systemd-storage-block@.service.in',
},
{
'file' : 'systemd-storage-fs.socket',
'symlinks' : ['sockets.target.wants/']
},
{
'file' : 'systemd-storage-fs@.service.in',
},
{
'file' : 'systemd-storagetm.service.in',
'conditions' : ['ENABLE_STORAGETM'],
+24
View File
@@ -0,0 +1,24 @@
# 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=Simple Block Device Backed Storage Provider
Documentation=man:systemd-storage-block@..service(8)
DefaultDependencies=no
Before=sockets.target
[Socket]
ListenStream=/run/systemd/io.systemd.StorageProvider/block
FileDescriptorName=varlink
SocketMode=0666
Accept=yes
MaxConnectionsPerSource=16
[Install]
WantedBy=sockets.target
+18
View File
@@ -0,0 +1,18 @@
# 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=Simple Block Device Backed Storage Provider
Documentation=man:systemd-storage-block@.service(8)
DefaultDependencies=no
Conflicts=shutdown.target initrd-switch-root.target
Before=shutdown.target initrd-switch-root.target
[Service]
ExecStart=-{{LIBEXECDIR}}/systemd-storage-block
+25
View File
@@ -0,0 +1,25 @@
# 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=Simple File System Backed Storage Provider
Documentation=man:systemd-storage-fs@.service(8)
DefaultDependencies=no
RequiresMountsFor=/var/lib/storage
Before=sockets.target
[Socket]
ListenStream=/run/systemd/io.systemd.StorageProvider/fs
FileDescriptorName=varlink
SocketMode=0666
Accept=yes
MaxConnectionsPerSource=16
[Install]
WantedBy=sockets.target
+19
View File
@@ -0,0 +1,19 @@
# 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=Simple File System Backed Storage Provider
Documentation=man:systemd-storage-fs@.service(8)
DefaultDependencies=no
RequiresMountsFor=/var/lib/storage
Conflicts=shutdown.target initrd-switch-root.target
Before=shutdown.target initrd-switch-root.target
[Service]
ExecStart=-{{LIBEXECDIR}}/systemd-storage-fs
+7
View File
@@ -61,6 +61,13 @@ units = [
'file' : 'systemd-journalctl.socket',
'symlinks' : ['sockets.target.wants/'],
},
{
'file' : 'systemd-storage-fs.socket',
'symlinks' : ['sockets.target.wants/']
},
{
'file' : 'systemd-storage-fs@.service.in',
},
{ 'file' : 'systemd-tmpfiles-clean.service' },
{ 'file' : 'systemd-tmpfiles-clean.timer' },
{ 'file' : 'systemd-tmpfiles-setup.service' },
+23
View File
@@ -0,0 +1,23 @@
# 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=Simple File System Backed Storage Provider
Documentation=man:systemd-storage-fs.service(8)
Before=sockets.target
[Socket]
ListenStream=%t/systemd/io.systemd.StorageProvider/fs
FileDescriptorName=varlink
SocketMode=0600
Accept=yes
MaxConnectionsPerSource=16
[Install]
WantedBy=sockets.target
+15
View File
@@ -0,0 +1,15 @@
# 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=Simple File System Backed Storage Provider
Documentation=man:systemd-storage-fs.service(8)
[Service]
ExecStart=-{{LIBEXECDIR}}/systemd-storage-fs --user