storage: add 'storagectl' command-line tool

CLI for inspecting and using storage providers. Scans
/run/systemd/io.systemd.StorageProvider/ (or the user-mode equivalent)
for AF_UNIX sockets and talks to each one over Varlink. Verbs:
"volumes" lists volumes across all providers, "templates" lists
supported creation templates, "providers" lists the endpoints
themselves.

Also installed as a mount.storage helper, so
'mount -t storage PROVIDER:VOLUME /mnt' (or 'mount -t storage.<fstype>'
to put a fresh filesystem on a block volume) acquires the volume and
mounts it. Ships with bash/zsh completions and a man page.
This commit is contained in:
Lennart Poettering
2026-04-29 12:25:47 +02:00
parent 9825195c25
commit 4002a7e8be
8 changed files with 1215 additions and 0 deletions
+1
View File
@@ -972,6 +972,7 @@ manpages = [
['sd_watchdog_enabled', '3', [], ''],
['shutdown', '8', [], ''],
['smbios-type-11', '7', [], ''],
['storagectl', '1', ['mount.storage'], ''],
['sysctl.d', '5', [], ''],
['sysext.conf',
'5',
+281
View File
@@ -0,0 +1,281 @@
<?xml version='1.0'?>
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="storagectl"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>storagectl</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>storagectl</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>storagectl</refname>
<refname>mount.storage</refname>
<refpurpose>Enumerate and mount storage volumes provided by storage providers</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>storagectl</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="req">COMMAND</arg>
<arg choice="opt" rep="repeat">NAME</arg>
</cmdsynopsis>
<cmdsynopsis>
<command>mount</command>
<arg choice="plain">-t</arg>
<arg choice="plain">storage</arg>
<arg choice="plain"><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></arg>
<arg choice="plain"><replaceable>DIRECTORY</replaceable></arg>
</cmdsynopsis>
<cmdsynopsis>
<command>mount</command>
<arg choice="plain">-t</arg>
<arg choice="plain">storage.<replaceable>FSTYPE</replaceable></arg>
<arg choice="plain"><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></arg>
<arg choice="plain"><replaceable>DIRECTORY</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>storagectl</command> may be used to inspect storage providers and the storage
volumes they expose. A storage provider is a service implementing the
<constant>io.systemd.StorageProvider</constant> <ulink url="https://varlink.org/">Varlink</ulink>
interface, registered as an AF_UNIX socket below the well-known socket directory
<filename>/run/systemd/io.systemd.StorageProvider/</filename> (in system mode) or
<varname>$XDG_RUNTIME_DIR</varname><filename>/systemd/io.systemd.StorageProvider/</filename> (in user mode). The two
storage providers shipped with systemd are
<citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
which exposes the system's block devices, and
<citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
which exposes regular files and directories from a backing file system.</para>
<para>The tool also provides a <citerefentry
project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry> helper
for the file system type <literal>storage</literal>, which permits mounting storage volumes to arbitrary
places. See "Use as a mount helper" below for details.</para>
</refsect1>
<refsect1>
<title>Commands</title>
<para>The following commands are understood:</para>
<variablelist>
<varlistentry>
<term><command>volumes</command> <optional><replaceable>GLOB</replaceable></optional></term>
<listitem><para>List storage volumes provided by all storage providers running on the
system (or, with <option>--user</option>, in the user runtime). The optional
<replaceable>GLOB</replaceable> argument is a shell-style pattern (see
<citerefentry project='man-pages'><refentrytitle>fnmatch</refentrytitle><manvolnum>3</manvolnum></citerefentry>)
that filters the result by volume name. The output is a table containing the providing
service, the volume name, its type (<literal>blk</literal>, <literal>reg</literal> or
<literal>dir</literal>), whether it is read-only, and — if known — its size and the number
of bytes used.</para>
<para>This is the default command if none is specified.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>templates</command> <optional><replaceable>GLOB</replaceable></optional></term>
<listitem><para>List volume templates supported by the running storage providers. Templates
encapsulate a configuration to use when creating volumes on-the-fly, when they are acquired. Template
support is an optional feature for providers, and only applies to providers that allow creation
of volumes on-the-fly. See the respective provider documentation for details, for example
<citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>. The
optional <replaceable>GLOB</replaceable> argument filters by template name. Storage providers that do
not implement template-based volume creation (such as the block-device provider) do not contribute to
this output.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>providers</command></term>
<listitem><para>List the storage providers known to the system. This is determined by scanning the
well-known socket directory for <constant>AF_UNIX</constant> sockets that look like
<constant>io.systemd.StorageProvider</constant> endpoints. For each provider it is also reported
whether the socket can currently be connected to.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<varlistentry>
<term><option>--system</option></term>
<listitem><para>Operate on system-wide storage providers. Sockets are looked for in
<filename>/run/systemd/io.systemd.StorageProvider/</filename>. This is the default.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--user</option></term>
<listitem><para>Operate on per-user storage providers. Sockets are looked for in
<filename>$XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/</filename>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="json" />
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="no-legend" />
<xi:include href="standard-options.xml" xpointer="no-ask-password" />
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Use as a mount helper</title>
<para>The tool provides the <command>/sbin/mount.storage</command> alias, implementing the
<citerefentry project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry>
"external helper" interface, allowing storage volumes to be mounted with the regular
<command>mount</command> command. The volume to mount is encoded as the source of the mount,
in the form
<literal><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></literal>, where
<replaceable>PROVIDER</replaceable> is the name of a storage provider (as listed by
<command>storagectl providers</command>) and <replaceable>VOLUME</replaceable> is the volume
name. Two file system type spellings are recognized:</para>
<variablelist>
<varlistentry>
<term><literal>storage</literal></term>
<listitem><para>Acquires a directory volume and bind-mounts its directory tree onto the
target.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><literal>storage.<replaceable>FSTYPE</replaceable></literal></term>
<listitem><para>Acquires a regular file or block device volume and mounts it as a file system of type
<replaceable>FSTYPE</replaceable> (for example <literal>storage.ext4</literal>,
<literal>storage.btrfs</literal>, …).</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
<para>The standard <option>-o</option> mount options are forwarded to
<command>mount</command>. In addition, the following <literal>storage.</literal>-prefixed
options are interpreted by <command>mount.storage</command> itself and stripped from the
forwarded list:</para>
<variablelist>
<varlistentry>
<term><option>storage.create=</option><replaceable>MODE</replaceable></term>
<listitem><para>Takes one of <literal>any</literal> (open if it exists, otherwise create — the
default), <literal>open</literal> (fail if the volume does not yet exist) or <literal>new</literal>
(fail if the volume already exists).</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>storage.template=</option><replaceable>NAME</replaceable></term>
<listitem><para>The template to use when creating a new volume, if it is missing and the provider
supports on-the-fly creation of volumes.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>storage.create-size=</option><replaceable>BYTES</replaceable></term>
<listitem><para>When creating a new volume on-the-fly, the size in bytes to allocate. Accepts the
usual <literal>K</literal>/<literal>M</literal>/<literal>G</literal>/<literal>T</literal> suffixes
(base 1024). Required when creating a regular file volume.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Examples</title>
<example>
<title>Enumerate available storage providers, volumes and templates</title>
<programlisting>$ storagectl providers
$ storagectl volumes
$ storagectl volumes '*foo*'
$ storagectl templates</programlisting>
</example>
<example>
<title>Mount a directory volume from the file system provider</title>
<programlisting># mount -t storage fs:myvol /mnt/myvol</programlisting>
<para>If the volume <literal>myvol</literal> does not yet exist, it will be created using
the default <literal>subvolume</literal> template.</para>
</example>
<example>
<title>Create and mount an ext4 file system from a regular file.</title>
<programlisting># mount -t storage.ext4 fs:scratch /mnt/scratch -o loop</programlisting>
</example>
<example>
<title>Mount a block device volume read-only</title>
<programlisting># mount -t storage.ext4 -o ro block:/dev/disk/by-id/usb-foo /mnt/foo</programlisting>
</example>
</refsect1>
<refsect1>
<title>Exit status</title>
<para>On success, 0 is returned, a non-zero failure code otherwise.</para>
</refsect1>
<xi:include href="common-variables.xml" />
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>varlinkctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+1
View File
@@ -36,6 +36,7 @@ foreach item : [
['portablectl', 'ENABLE_PORTABLED'],
['resolvectl', 'ENABLE_RESOLVE'],
['run0', ''],
['storagectl', ''],
['systemd-analyze', ''],
['systemd-cat', ''],
['systemd-cgls', ''],
+74
View File
@@ -0,0 +1,74 @@
# shellcheck shell=bash
# storagectl(1) completion -*- shell-script -*-
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# systemd is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with systemd; If not, see <https://www.gnu.org/licenses/>.
__contains_word () {
local w word=$1; shift
for w in "$@"; do
[[ $w = "$word" ]] && return
done
}
_storagectl() {
local i verb comps
local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
local -A OPTS=(
[STANDALONE]='-h --help --version --no-pager --no-legend --no-ask-password
--system --user'
[ARG]='--json'
)
if __contains_word "$prev" ${OPTS[ARG]}; then
case $prev in
--json)
comps=$( storagectl --json=help 2>/dev/null )
;;
esac
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
fi
if [[ "$cur" = -* ]]; then
COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") )
return 0
fi
local -A VERBS=(
[STANDALONE]='volumes templates providers help'
)
for ((i=0; i < COMP_CWORD; i++)); do
if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} &&
! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then
verb=${COMP_WORDS[i]}
break
fi
done
if [[ -z ${verb-} ]]; then
comps=${VERBS[*]}
elif __contains_word "$verb" ${VERBS[STANDALONE]}; then
comps=''
fi
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
}
complete -F _storagectl storagectl
+35
View File
@@ -0,0 +1,35 @@
#compdef storagectl
# SPDX-License-Identifier: LGPL-2.1-or-later
(( $+functions[_storagectl_commands] )) || _storagectl_commands()
{
local -a _storagectl_cmds
_storagectl_cmds=(
"volumes:List storage volumes"
"templates:List storage volume templates"
"providers:List storage providers"
"help:Prints a short help text and exits"
)
if (( CURRENT == 1 )); then
_describe -t commands 'storagectl command' _storagectl_cmds
else
local curcontext="$curcontext"
cmd="${${_storagectl_cmds[(r)$words[1]:*]%%:*}}"
if (( $+functions[_storagectl_$cmd] )); then
_storagectl_$cmd
else
_message "no more options"
fi
fi
}
_arguments \
'(- *)'{-h,--help}'[Prints a short help text and exits.]' \
'(- *)--version[Prints a short version string and exits.]' \
'--no-pager[Do not pipe output into a pager]' \
'--no-legend[Do not show the headers and footers]' \
'--no-ask-password[Do not query the user for authentication]' \
'--json=[Show output as JSON]:mode:(pretty short off help)' \
'--system[Operate in system mode]' \
'--user[Operate in user mode]' \
'*::storagectl command:_storagectl_commands'
+1
View File
@@ -33,6 +33,7 @@ foreach item : [
['_sd_machines', 'ENABLE_MACHINED'],
['_sd_outputmodes', ''],
['_sd_unit_files', ''],
['_storagectl', ''],
['_systemd', ''],
['_systemd-analyze', ''],
['_systemd-delta', ''],
+10
View File
@@ -11,7 +11,17 @@ executables += [
'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)
+812
View File
@@ -0,0 +1,812 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-varlink.h"
#include <getopt.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#include "alloc-util.h"
#include "ansi-color.h"
#include "argv-util.h"
#include "build.h"
#include "bus-util.h"
#include "errno-list.h"
#include "escape.h"
#include "extract-word.h"
#include "fd-util.h"
#include "format-table.h"
#include "format-util.h"
#include "help-util.h"
#include "json-util.h"
#include "main-func.h"
#include "mount-util.h"
#include "namespace-util.h"
#include "options.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "path-lookup.h"
#include "path-util.h"
#include "polkit-agent.h"
#include "recurse-dir.h"
#include "runtime-scope.h"
#include "socket-util.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "storage-util.h"
#include "string-util.h"
#include "strv.h"
#include "user-util.h"
#include "varlink-util.h"
#include "verbs.h"
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_ask_password = true;
static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
static int help(void) {
int r;
help_cmdline("[OPTIONS...] COMMAND");
help_abstract("Enumerate storage volumes and providers.");
_cleanup_(table_unrefp) Table *verbs = NULL;
r = verbs_get_help_table(&verbs);
if (r < 0)
return r;
_cleanup_(table_unrefp) Table *options = NULL;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
(void) table_sync_column_widths(0, verbs, options);
help_section("Commands:");
r = table_print_or_warn(verbs);
if (r < 0)
return r;
help_section("Options:");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("storagectl", "1");
return 0;
}
VERB_COMMON_HELP_HIDDEN(help);
static const char *ro_color(int ro) {
if (ro > 0)
return ansi_highlight_red();
if (ro == 0)
return ansi_highlight_green();
return NULL;
}
static int on_list_reply(
sd_varlink *link,
sd_json_variant *parameters,
const char *error_id,
sd_varlink_reply_flags_t flags,
void* userdata) {
Table *t = ASSERT_PTR(userdata);
int r;
assert(link);
const char *d = ASSERT_PTR(sd_varlink_get_description(link));
if (error_id) {
log_debug("%s: Received error '%s', ignoring.", d, error_id);
return 0;
}
_cleanup_free_ char *provider = NULL;
r = path_extract_filename(d, &provider);
if (r < 0)
return log_error_errno(r, "Failed to extract provider name from socket path: %m");
struct {
const char *name;
const char *type;
int read_only;
uint64_t size_bytes;
uint64_t used_bytes;
} p = {
.read_only = -1,
.size_bytes = UINT64_MAX,
.used_bytes = UINT64_MAX,
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), 0 },
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), 0 },
{ "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
{ "sizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, size_bytes), 0 },
{ "usedBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, used_bytes), 0 },
{}
};
r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
if (r < 0)
return log_error_errno(r, "Failed to decode List() reply: %m");
r = table_add_many(
t,
TABLE_STRING, provider,
TABLE_STRING, p.name,
TABLE_STRING, p.type,
TABLE_TRISTATE, p.read_only,
TABLE_SET_COLOR, ro_color(p.read_only));
if (r < 0)
return table_log_add_error(r);
if (p.size_bytes == UINT64_MAX)
r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
else
r = table_add_many(t, TABLE_SIZE, p.size_bytes, TABLE_SET_ALIGN_PERCENT, 100);
if (r < 0)
return table_log_add_error(r);
if (p.used_bytes == UINT64_MAX)
r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
else
r = table_add_many(t, TABLE_SIZE, p.used_bytes, TABLE_SET_ALIGN_PERCENT, 100);
if (r < 0)
return table_log_add_error(r);
return 0;
}
VERB(verb_list_volumes, "volumes", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, VERB_DEFAULT, "List storage volumes");
static int verb_list_volumes(int argc, char *argv[], uintptr_t data, void *userdata) {
int r;
assert(argc <= 2);
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
_cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type", "ro", "size", "used");
if (!t)
return log_oom();
(void) table_set_sort(t, (size_t) 0, (size_t) 1);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
if (argc >= 2) {
r = sd_json_buildo(
&v,
SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
if (r < 0)
return log_oom();
}
ssize_t n = varlink_execute_directory(
socket_path,
"io.systemd.StorageProvider.ListVolumes",
v,
/* more= */ true,
/* timeout_usec= */ 0, /* 0 means default */
on_list_reply,
t);
if (n < 0 && n != -ENOENT)
return log_error_errno(n, "Failed to enumerate storage volumes: %m");
if (!table_isempty(t)) {
r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
}
if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (table_isempty(t))
printf("No storage volumes.\n");
else
printf("\n%zu storage volumes listed.\n", table_get_rows(t) - 1);
}
return 0;
}
static int on_list_templates_reply(
sd_varlink *link,
sd_json_variant *parameters,
const char *error_id,
sd_varlink_reply_flags_t flags,
void* userdata) {
Table *t = ASSERT_PTR(userdata);
int r;
assert(link);
const char *d = ASSERT_PTR(sd_varlink_get_description(link));
if (error_id) {
log_debug("%s: Received error '%s', ignoring.", d, error_id);
return 0;
}
_cleanup_free_ char *provider = NULL;
r = path_extract_filename(d, &provider);
if (r < 0)
return log_error_errno(r, "Failed to extract provider name from socket path: %m");
struct {
const char *name;
const char *type;
} p = {
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), 0 },
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), 0 },
{}
};
r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
if (r < 0)
return log_error_errno(r, "Failed to decode ListTemplates() reply: %m");
r = table_add_many(
t,
TABLE_STRING, provider,
TABLE_STRING, p.name,
TABLE_STRING, p.type);
if (r < 0)
return table_log_add_error(r);
return 0;
}
VERB(verb_templates, "templates", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, /* flags= */ 0, "List storage volume templates");
static int verb_templates(int argc, char *argv[], uintptr_t data, void *userdata) {
int r;
assert(argc <= 2);
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
_cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type");
if (!t)
return log_oom();
(void) table_set_sort(t, (size_t) 0, (size_t) 1);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
if (argc >= 2) {
r = sd_json_buildo(
&v,
SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
if (r < 0)
return log_oom();
}
ssize_t n = varlink_execute_directory(
socket_path,
"io.systemd.StorageProvider.ListTemplates",
v,
/* more= */ true,
/* timeout_usec= */ 0, /* 0 means default */
on_list_templates_reply,
t);
if (n < 0 && n != -ENOENT)
return log_error_errno(n, "Failed to enumerate storage volume templates: %m");
if (!table_isempty(t)) {
r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
}
if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (table_isempty(t))
printf("No templates.\n");
else
printf("\n%zu templates listed.\n", table_get_rows(t) - 1);
}
return 0;
}
VERB_NOARG(verb_providers, "providers", "List storage providers");
static int verb_providers(int argc, char *argv[], uintptr_t data, void *userdata) {
int r;
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
_cleanup_(table_unrefp) Table *t = table_new("provider", "listening");
if (!t)
return log_oom();
(void) table_set_sort(t, (size_t) 0);
table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
_cleanup_close_ int fd = open(socket_path, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
if (fd < 0) {
if (errno != ENOENT)
return log_error_errno(errno, "Failed to open '%s': %m", socket_path);
} else {
_cleanup_free_ DirectoryEntries *dentries = NULL;
r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries);
if (r < 0)
return log_error_errno(r, "Failed to enumerate '%s': %m", socket_path);
FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
struct dirent *de = *dp;
if (de->d_type != DT_SOCK)
continue;
if (!storage_provider_name_is_valid(de->d_name))
continue;
_cleanup_close_ int socket_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if (socket_fd < 0)
return log_error_errno(errno, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
_cleanup_free_ char *no = NULL;
r = connect_unix_path(socket_fd, fd, de->d_name);
if (r < 0) {
no = strjoin("no (", ERRNO_NAME(r), ")");
if (!no)
return log_oom();
}
r = table_add_many(t,
TABLE_STRING, de->d_name,
TABLE_STRING, no ?: "yes",
TABLE_SET_COLOR, ansi_highlight_green_red(!no));
if (r < 0)
return table_log_add_error(r);
}
}
if (!table_isempty(t)) {
r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
}
if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
if (table_isempty(t))
printf("No providers.\n");
else
printf("\n%zu providers listed.\n", table_get_rows(t) - 1);
}
return 0;
}
static int parse_argv(int argc, char *argv[], char ***args) {
int r;
assert(argc >= 0);
assert(argv);
OptionParser opts = { argc, argv };
FOREACH_OPTION(c, &opts, /* on_error= */ return c)
switch (c) {
OPTION_COMMON_HELP:
return help();
OPTION_COMMON_VERSION:
return version();
OPTION_COMMON_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
OPTION_COMMON_NO_LEGEND:
arg_legend = false;
break;
OPTION_COMMON_JSON:
r = parse_json_argument(opts.arg, &arg_json_format_flags);
if (r <= 0)
return r;
break;
OPTION_COMMON_NO_ASK_PASSWORD:
arg_ask_password = false;
break;
OPTION_LONG("system", NULL, "Operate in system mode"):
arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
break;
OPTION_LONG("user", NULL, "Operate in user mode"):
arg_runtime_scope = RUNTIME_SCOPE_USER;
break;
}
*args = option_parser_get_args(&opts);
return 1;
}
static int run_as_mount_helper(int argc, char *argv[]) {
int c, r;
/* Implements util-linux "external helper" command line interface, as per mount(8) man page.
*
* Usage:
*
* mount -t storage fs:mydirvolume /some/place # Directory volumes
* mount -t storage.ext4 fs:myblkvolume /some/place # Block volumes
*/
const char *fstype = NULL, *options = NULL;
bool fake = false;
while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) {
switch (c) {
case 'f':
fake = true;
break;
case 'o':
options = optarg;
break;
case 't':
fstype = startswith(optarg, "storage.");
if (fstype) {
/* Paranoia: don't allow "storage.storage.storage.…" chains... */
if (startswith(fstype, "storage.") || streq(fstype, "storage"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing nested storage volumes.");
} else if (!streq(optarg, "storage"))
log_warning("Unexpected file system type '%s', ignoring.", optarg);
break;
case 's': /* sloppy mount options */
case 'n': /* aka --no-mtab */
case 'v': /* aka --verbose */
log_debug("Ignoring option -%c, not implemented.", c);
break;
case 'N': /* aka --namespace= */
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c);
case '?':
return -EINVAL;
}
}
if (optind + 2 != argc)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Expected a storage volume specification and target directory as only arguments.");
const char *colon = strchr(argv[optind], ':');
if (!colon)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume specification, refusing: %s", argv[optind]);
_cleanup_free_ char *provider = strndup(argv[optind], colon - argv[optind]);
if (!provider)
return log_oom();
if (!storage_provider_name_is_valid(provider))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage provider name: %s", provider);
_cleanup_free_ char *name = strdup(colon + 1);
if (!name)
return log_oom();
if (!storage_volume_name_is_valid(name))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume name: %s", name);
_cleanup_free_ char *path = NULL;
r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &path);
if (r < 0)
return r;
_cleanup_free_ char *filtered = NULL, *template = NULL;
CreateMode create_mode = _CREATE_MODE_INVALID;
uint64_t create_size = UINT64_MAX;
int read_only = -1;
for (const char *p = options;;) {
_cleanup_free_ char *word = NULL;
r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE|EXTRACT_UNESCAPE_SEPARATORS);
if (r < 0)
return log_error_errno(r, "Failed to extract mount option: %m");
if (r == 0)
break;
const char *t = startswith(word, "storage.");
if (t) {
const char *v;
if ((v = startswith(t, "create="))) {
create_mode = create_mode_from_string(v);
if (create_mode < 0)
return log_error_errno(create_mode, "Failed to parse storage.create= parameter: %s", v);
} else if ((v = startswith(t, "create-size="))) {
r = parse_size(v, /* base= */ 1024, &create_size);
if (r < 0)
return log_error_errno(r, "Failed to parse storage.create-size= parameter: %s", v);
} else if ((v = startswith(t, "template="))) {
if (!storage_template_name_is_valid(v))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid template name, refusing: %s", v);
r = free_and_strdup(&template, v);
if (r < 0)
return log_oom();
} else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown mount option '%s', refusing.", word);
} else if (streq(word, "ro"))
read_only = true;
else if (streq(word, "rw"))
read_only = false;
else if (!strextend_with_separator(&filtered, ",", word))
return log_oom();
}
if (fake)
return 0;
_cleanup_free_ char *socket_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
if (r < 0)
return log_error_errno(r, "Failed to determine socket directory: %m");
if (!path_extend(&socket_path, provider))
return log_oom();
_cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
r = sd_varlink_connect_address(&link, socket_path);
if (r < 0)
return log_error_errno(r, "Failed to connect to '%s': %m", socket_path);
r = sd_varlink_set_allow_fd_passing_input(link, true);
if (r < 0)
return log_error_errno(r, "Failed to enable file descriptor passing: %m");
(void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
sd_json_variant *mreply = NULL;
const char *merror_id = NULL, *vtype = fstype ? "reg" : "dir";
r = sd_varlink_callbo(
link,
"io.systemd.StorageProvider.Acquire",
&mreply,
&merror_id,
SD_JSON_BUILD_PAIR_STRING("name", name),
SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
SD_JSON_BUILD_PAIR_STRING("requestAs", vtype),
SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
if (r < 0)
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
_cleanup_(sd_json_variant_unrefp) sd_json_variant *reply = sd_json_variant_ref(mreply);
if (merror_id) {
/* Copy out the error ID, as the follow-up call will invalidate it */
_cleanup_free_ char *error_id = strdup(merror_id);
if (!error_id)
return log_oom();
/* Hmm, the type might not have been right for the backend or the volume? then try
* again, and switch from "reg" to "blk", maybe it works then. (We keep the original
* reply referenced, since we prefer generating an error for the first error.) */
if (streq(vtype, "reg") && STR_IN_SET(error_id,
"io.systemd.StorageProvider.TypeNotSupported",
"io.systemd.StorageProvider.WrongType")) {
sd_json_variant *freply = NULL;
const char *ferror_id = NULL;
r = sd_varlink_callbo(
link,
"io.systemd.StorageProvider.Acquire",
&freply,
&ferror_id,
SD_JSON_BUILD_PAIR_STRING("name", name),
SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
SD_JSON_BUILD_PAIR_STRING("requestAs", "blk"),
SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
if (r < 0)
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
if (!ferror_id) {
/* The 2nd call worked? then let's forget about the first failure */
sd_json_variant_unref(reply);
reply = sd_json_variant_ref(freply);
error_id = mfree(error_id);
}
/* NB: if both fail we show the Varlink error of the first call here, i.e. of the preferred type */
}
if (error_id) {
if (streq(error_id, "io.systemd.StorageProvider.NoSuchVolume"))
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Volume '%s' not known.", name);
if (streq(error_id, "io.systemd.StorageProvider.NoSuchTemplate"))
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Template '%s' not known.", template);
if (streq(error_id, "io.systemd.StorageProvider.VolumeExists"))
return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Volume '%s' exists already.", name);
if (streq(error_id, "io.systemd.StorageProvider.TypeNotSupported"))
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support the specified volume type '%s'.", vtype);
if (streq(error_id, "io.systemd.StorageProvider.WrongType"))
return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Volume '%s' is not of type '%s'.", name, vtype);
if (streq(error_id, "io.systemd.StorageProvider.CreateNotSupported"))
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support creating volumes.");
if (streq(error_id, "io.systemd.StorageProvider.CreateSizeRequired"))
return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Storage provider requires a create size to be provided when creating volumes on-the-fly. Use 'storage.create-size=' mount option.");
if (streq(error_id, "io.systemd.StorageProvider.ReadOnlyVolume"))
return log_error_errno(SYNTHETIC_ERRNO(EROFS), "Volume '%s' is read-only.", name);
if (streq(error_id, "io.systemd.StorageProvider.BadTemplate"))
return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Template does not apply to this volume type.");
r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */
if (r != -EBADR)
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %s", error_id);
}
}
struct {
unsigned fd_idx;
int read_only;
const char *type;
uid_t base_uid;
gid_t base_gid;
} p = {
.fd_idx = UINT_MAX,
.read_only = -1,
.base_uid = UID_INVALID,
.base_gid = GID_INVALID,
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "fileDescriptorIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, fd_idx), SD_JSON_MANDATORY },
{ "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), SD_JSON_MANDATORY },
{ "baseUID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(p, base_uid), 0 },
{ "baseGID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(p, base_gid), 0 },
{}
};
r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
if (r < 0)
return log_error_errno(r, "Failed to decode Acquire() reply: %m");
_cleanup_close_ int fd = sd_varlink_take_fd(link, p.fd_idx);
if (fd < 0)
return log_error_errno(fd, "Failed to acquire fd from Varlink connection: %m");
struct stat st;
if (fstat(fd, &st) < 0)
return log_error_errno(errno, "Failed to stat returned file descriptor: %m");
_cleanup_strv_free_ char **cmdline = strv_new("mount", "-c");
if (!cmdline)
return log_oom();
if (fstype) {
if (!STR_IN_SET(p.type, "reg", "blk"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mounting as file system type '%s' requested, but volume is not a block device or regular file.", fstype);
r = stat_verify_regular_or_block(&st);
if (r < 0)
return log_error_errno(r, "File descriptor for block/regular volume is not a block or regular inode: %m");
if (strv_extend_strv(&cmdline, STRV_MAKE("-t", fstype), /* filter_duplicates= */ false) < 0)
return log_oom();
} else {
if (!streq(p.type, "dir"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mount as directory requested, but volume is not a directory.");
if (!uid_is_valid(p.base_uid) || !gid_is_valid(p.base_gid))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provider did not report base UID/GID, cannot mount.");
if (p.base_uid > UINT32_MAX - 0x10000U ||
p.base_gid > UINT32_MAX - 0x10000U)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Returned base UID/GID out of range.");
r = stat_verify_directory(&st);
if (r < 0)
return log_error_errno(r, "File descriptor for directory volume is not a directory inode: %m");
if (st.st_uid < p.base_uid || st.st_uid >= p.base_uid + 0x10000 ||
st.st_gid < p.base_gid || st.st_gid >= p.base_gid + 0x10000)
return log_error_errno(SYNTHETIC_ERRNO(EPERM), "File descriptor for directory volume is not owned by base UID/GID range, refusing.");
/* Now move the mount into our own UID/GID range */
_cleanup_free_ char *uid_line = asprintf_safe(
UID_FMT " " UID_FMT " " UID_FMT "\n",
p.base_uid, (uid_t) 0, (uid_t) 0x10000);
_cleanup_free_ char *gid_line = asprintf_safe(
GID_FMT " " GID_FMT " " GID_FMT "\n",
p.base_gid, (gid_t) 0, (gid_t) 0x10000);
if (!uid_line || !gid_line)
return log_oom();
_cleanup_close_ int userns_fd = userns_acquire(uid_line, gid_line, /* setgroups_deny= */ true);
if (userns_fd < 0)
return log_error_errno(userns_fd, "Failed to acquire new user namespace: %m");
_cleanup_close_ int remapped_fd = open_tree_attr_with_fallback(
fd,
/* path= */ NULL,
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC,
&(struct mount_attr) {
.attr_set = MOUNT_ATTR_IDMAP,
.userns_fd = userns_fd,
});
if (remapped_fd < 0)
return log_error_errno(remapped_fd, "Failed to set ID mapping on returned mount: %m");
close_and_replace(fd, remapped_fd);
if (strv_extend(&cmdline, "--bind") < 0)
return log_oom();
}
if (p.read_only > 0)
read_only = true;
if (!strextend_with_separator(&filtered, ",", read_only > 0 ? "ro" : "rw"))
return log_oom();
if (strv_extend_strv(&cmdline, STRV_MAKE("-o", filtered), /* filter_duplicates= */ false) < 0)
return log_oom();
if (strv_extend_strv(&cmdline, STRV_MAKE(FORMAT_PROC_FD_PATH(fd), path), /* filter_duplicates= */ false) < 0)
return log_oom();
r = fd_cloexec(fd, false);
if (r < 0)
return log_error_errno(r, "Failed to disable O_CLOEXEC for mount fd: %m");
if (DEBUG_LOGGING) {
_cleanup_free_ char *q = quote_command_line(cmdline, SHELL_ESCAPE_EMPTY);
log_debug("Chain-loading: %s", strna(q));
}
/* NB: we do not honour $PATH here, since as plugin to /bin/mount we might be called in a setuid()
* context, and hence don't want to chain to programs potentially under user control. */
execv("/bin/mount", cmdline);
return log_error_errno(errno, "Failed to execute mount tool: %m");
}
static int run(int argc, char *argv[]) {
int r;
log_setup();
if (invoked_as(argv, "mount.storage"))
return run_as_mount_helper(argc, argv);
char **args = NULL;
r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
return dispatch_verb_with_args(args, /* userdata= */ NULL);
}
DEFINE_MAIN_FUNCTION(run);