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:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
|
||||
@@ -36,6 +36,7 @@ foreach item : [
|
||||
['portablectl', 'ENABLE_PORTABLED'],
|
||||
['resolvectl', 'ENABLE_RESOLVE'],
|
||||
['run0', ''],
|
||||
['storagectl', ''],
|
||||
['systemd-analyze', ''],
|
||||
['systemd-cat', ''],
|
||||
['systemd-cgls', ''],
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user