shared: add BindVolume parser in machine-util

Add a universal parser for the colon-separated grammar
'PROVIDER:VOLUME[:CONFIG][:K=V,K=V,…]' that backs --bind-volume on
systemd-vmspawn (next), machinectl bind-volume, and the future nspawn
+ service-manager BindVolume= integrations.

The 'config' field is opaque to shared code and interpreted per
backend (vmspawn: a DiskType name, future nspawn: a mount path). The
trailing key=value list is parsed into the io.systemd.StorageProvider
.Acquire() parameters (template, create, read-only/ro, size/create-size
and request-as), with values validated against the existing
storage-util enums and validators. Provider/volume names are checked
with storage_provider_name_is_valid() and storage_volume_name_is_valid();
the combined "<provider>:<volume>" string is also validated as
string_is_safe so it is safe to use as a QEMU device id.

Add a test-machine-util unit test covering the happy paths plus a
handful of malformed inputs.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-05-06 10:30:16 +02:00
parent efb0a0fa7a
commit 32a80416b6
4 changed files with 376 additions and 0 deletions
+177
View File
@@ -4,7 +4,11 @@
#include "extract-word.h"
#include "machine-util.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "storage-util.h"
#include "string-table.h"
#include "string-util.h"
#include "strv.h"
static const char *const image_format_table[_IMAGE_FORMAT_MAX] = {
[IMAGE_FORMAT_RAW] = "raw",
@@ -13,6 +17,14 @@ static const char *const image_format_table[_IMAGE_FORMAT_MAX] = {
DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat);
static const char *const read_only_mode_table[_READ_ONLY_MAX] = {
[READ_ONLY_NO] = "no",
[READ_ONLY_YES] = "yes",
[READ_ONLY_AUTO] = "auto",
};
DEFINE_STRING_TABLE_LOOKUP(read_only_mode, ReadOnlyMode);
static const char *const disk_type_table[_DISK_TYPE_MAX] = {
[DISK_TYPE_VIRTIO_BLK] = "virtio-blk",
[DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi",
@@ -100,3 +112,168 @@ int parse_disk_spec(
*ret_path = TAKE_PTR(path);
return 0;
}
BindVolume* bind_volume_free(BindVolume *v) {
if (!v)
return NULL;
free(v->provider);
free(v->volume);
free(v->config);
free(v->template);
return mfree(v);
}
static int bind_volume_apply_extra(BindVolume *v, const char *key, const char *value) {
int r;
assert(v);
assert(key);
assert(value);
if (streq(key, "template")) {
if (v->template)
return -EINVAL;
if (!storage_template_name_is_valid(value))
return -EINVAL;
r = free_and_strdup(&v->template, value);
if (r < 0)
return r;
return 0;
}
if (streq(key, "create")) {
if (v->create_mode >= 0)
return -EINVAL;
CreateMode m = create_mode_from_string(value);
if (m < 0)
return m;
v->create_mode = m;
return 0;
}
if (STR_IN_SET(key, "read-only", "ro")) {
if (v->read_only >= 0)
return -EINVAL;
ReadOnlyMode m = read_only_mode_from_string(value);
if (m < 0) {
r = parse_boolean(value);
if (r < 0)
return r;
m = r ? READ_ONLY_YES : READ_ONLY_NO;
}
v->read_only = m;
return 0;
}
if (STR_IN_SET(key, "size", "create-size")) {
if (v->create_size_bytes != UINT64_MAX)
return -EINVAL;
uint64_t sz;
r = parse_size(value, 1024, &sz);
if (r < 0)
return r;
if (sz == 0)
return -EINVAL;
v->create_size_bytes = sz;
return 0;
}
if (streq(key, "request-as")) {
if (v->request_as >= 0)
return -EINVAL;
VolumeType t = volume_type_from_string(value);
if (t < 0)
return t;
v->request_as = t;
return 0;
}
return -EINVAL;
}
int bind_volume_parse(const char *arg, BindVolume **ret) {
_cleanup_(bind_volume_freep) BindVolume *v = NULL;
int r;
assert(arg);
assert(ret);
v = new(BindVolume, 1);
if (!v)
return -ENOMEM;
*v = BIND_VOLUME_INIT;
const char *p = arg;
_cleanup_free_ char *provider = NULL, *volume = NULL, *config = NULL;
r = extract_first_word(&p, &provider, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r < 0)
return r;
if (r == 0 || isempty(provider) || !storage_provider_name_is_valid(provider))
return -EINVAL;
r = extract_first_word(&p, &volume, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r < 0)
return r;
if (r == 0 || isempty(volume) || !storage_volume_name_is_valid(volume))
return -EINVAL;
r = extract_first_word(&p, &config, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r < 0)
return r;
v->provider = TAKE_PTR(provider);
v->volume = TAKE_PTR(volume);
if (!isempty(config)) {
if (!string_is_safe(config, /* flags= */ 0))
return -EINVAL;
v->config = TAKE_PTR(config);
}
for (;;) {
_cleanup_free_ char *kv = NULL, *key = NULL, *value = NULL;
r = extract_first_word(&p, &kv, ",", 0);
if (r < 0)
return r;
if (r == 0)
break;
r = split_pair(kv, "=", &key, &value);
if (r < 0)
return r;
if (isempty(key))
return -EINVAL;
r = bind_volume_apply_extra(v, key, value);
if (r < 0)
return r;
}
*ret = TAKE_PTR(v);
return 0;
}
int machine_storage_name_split(const char *s, char **ret_provider, char **ret_volume) {
_cleanup_free_ char *p = NULL, *v = NULL;
int r;
if (isempty(s))
return -EINVAL;
r = split_pair(s, ":", &p, &v);
if (r < 0)
return r;
if (!storage_provider_name_is_valid(p) || !storage_volume_name_is_valid(v))
return -EINVAL;
if (ret_provider)
*ret_provider = TAKE_PTR(p);
if (ret_volume)
*ret_volume = TAKE_PTR(v);
return 0;
}
+54
View File
@@ -2,6 +2,7 @@
#pragma once
#include "shared-forward.h"
#include "storage-util.h"
typedef enum ImageFormat {
IMAGE_FORMAT_RAW,
@@ -30,3 +31,56 @@ int parse_disk_spec(
ImageFormat *format,
DiskType *disk_type,
char **ret_path);
typedef enum ReadOnlyMode {
READ_ONLY_NO,
READ_ONLY_YES,
READ_ONLY_AUTO,
_READ_ONLY_MAX,
_READ_ONLY_INVALID = -EINVAL,
} ReadOnlyMode;
DECLARE_STRING_TABLE_LOOKUP(read_only_mode, ReadOnlyMode);
/* Map ReadOnlyMode onto the Acquire() wire tristate (-1 unset/auto, 0 no, 1 yes). */
static inline int read_only_mode_to_tristate(ReadOnlyMode m) {
switch (m) {
case READ_ONLY_NO: return 0;
case READ_ONLY_YES: return 1;
default: return -1;
}
}
/* Parsed "PROVIDER:VOLUME[:CONFIG][:K=V,K=V,...]" used by --bind-volume,
* machinectl bind-volume, and (future) the BindVolume= unit setting. The 'config'
* field is opaque here and interpreted per-backend (vmspawn: a DiskType name;
* nspawn: a mount path). */
typedef struct BindVolume {
char *provider;
char *volume;
char *config;
/* Acquire() parameters parsed from the trailing key=value list. */
char *template;
CreateMode create_mode;
ReadOnlyMode read_only;
uint64_t create_size_bytes;
VolumeType request_as;
} BindVolume;
#define BIND_VOLUME_INIT \
(BindVolume) { \
.create_mode = _CREATE_MODE_INVALID, \
.read_only = _READ_ONLY_INVALID, \
.create_size_bytes = UINT64_MAX, \
.request_as = _VOLUME_TYPE_INVALID, \
}
BindVolume* bind_volume_free(BindVolume *v);
DEFINE_TRIVIAL_CLEANUP_FUNC(BindVolume*, bind_volume_free);
int bind_volume_parse(const char *arg, BindVolume **ret);
/* Validate a "<provider>:<volume>" binding name as used by AddStorage/RemoveStorage.
* ret_provider/ret_volume may each be NULL when the caller only wants validation. */
int machine_storage_name_split(const char *s, char **ret_provider, char **ret_volume);
+1
View File
@@ -141,6 +141,7 @@ simple_tests += files(
'test-log.c',
'test-logarithm.c',
'test-login-util.c',
'test-machine-util.c',
'test-macro.c',
'test-memfd-util.c',
'test-memory-util.c',
+144
View File
@@ -0,0 +1,144 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "machine-util.h"
#include "tests.h"
TEST(bind_volume_parse_minimal) {
_cleanup_(bind_volume_freep) BindVolume *v = NULL;
ASSERT_OK(bind_volume_parse("block:/dev/sda", &v));
ASSERT_STREQ(v->provider, "block");
ASSERT_STREQ(v->volume, "/dev/sda");
ASSERT_NULL(v->config);
ASSERT_NULL(v->template);
ASSERT_EQ(v->create_mode, _CREATE_MODE_INVALID);
ASSERT_EQ(v->request_as, _VOLUME_TYPE_INVALID);
ASSERT_EQ(v->read_only, _READ_ONLY_INVALID);
ASSERT_EQ(v->create_size_bytes, UINT64_MAX);
}
TEST(bind_volume_parse_with_config) {
_cleanup_(bind_volume_freep) BindVolume *v = NULL;
ASSERT_OK(bind_volume_parse("block:/dev/sda:virtio-scsi", &v));
ASSERT_STREQ(v->provider, "block");
ASSERT_STREQ(v->volume, "/dev/sda");
ASSERT_STREQ(v->config, "virtio-scsi");
}
TEST(bind_volume_parse_empty_config) {
_cleanup_(bind_volume_freep) BindVolume *v = NULL;
ASSERT_OK(bind_volume_parse("fs:vol-1::create=new,size=64M,template=sparse-file", &v));
ASSERT_STREQ(v->provider, "fs");
ASSERT_STREQ(v->volume, "vol-1");
ASSERT_NULL(v->config);
ASSERT_EQ(v->create_mode, CREATE_NEW);
ASSERT_STREQ(v->template, "sparse-file");
ASSERT_EQ(v->create_size_bytes, UINT64_C(64) * 1024 * 1024);
}
TEST(bind_volume_parse_full) {
_cleanup_(bind_volume_freep) BindVolume *v = NULL;
ASSERT_OK(bind_volume_parse(
"fs:vol-2:nvme:create=any,template=allocated-file,size=128M,ro=auto,request-as=blk",
&v));
ASSERT_STREQ(v->provider, "fs");
ASSERT_STREQ(v->volume, "vol-2");
ASSERT_STREQ(v->config, "nvme");
ASSERT_EQ(v->create_mode, CREATE_ANY);
ASSERT_STREQ(v->template, "allocated-file");
ASSERT_EQ(v->request_as, VOLUME_BLK);
ASSERT_EQ(v->create_size_bytes, UINT64_C(128) * 1024 * 1024);
ASSERT_EQ(v->read_only, READ_ONLY_AUTO);
}
TEST(bind_volume_parse_read_only) {
_cleanup_(bind_volume_freep) BindVolume *v = NULL;
ASSERT_OK(bind_volume_parse("block:/dev/sdb:scsi-cd:read-only=yes", &v));
ASSERT_EQ(v->read_only, READ_ONLY_YES);
v = bind_volume_free(v);
ASSERT_OK(bind_volume_parse("block:/dev/sdb:scsi-cd:ro=no", &v));
ASSERT_EQ(v->read_only, READ_ONLY_NO);
}
TEST(bind_volume_parse_invalid) {
BindVolume *v = NULL;
/* Missing provider */
ASSERT_ERROR(bind_volume_parse(":vol", &v), EINVAL);
ASSERT_NULL(v);
/* Missing volume */
ASSERT_ERROR(bind_volume_parse("block:", &v), EINVAL);
ASSERT_NULL(v);
/* Provider with control char */
ASSERT_ERROR(bind_volume_parse("bl\x01ock:vol", &v), EINVAL);
ASSERT_NULL(v);
/* Config with control char */
ASSERT_ERROR(bind_volume_parse("block:vol:nv\x01me", &v), EINVAL);
ASSERT_NULL(v);
/* Unknown extras key */
ASSERT_ERROR(bind_volume_parse("block:vol::bogus=foo", &v), EINVAL);
ASSERT_NULL(v);
/* Bogus create mode */
ASSERT_ERROR(bind_volume_parse("block:vol::create=bogus", &v), EINVAL);
ASSERT_NULL(v);
/* Bogus request-as */
ASSERT_ERROR(bind_volume_parse("block:vol::request-as=bogus", &v), EINVAL);
ASSERT_NULL(v);
/* Extras entry without '=' */
ASSERT_ERROR(bind_volume_parse("block:vol::nokey", &v), EINVAL);
ASSERT_NULL(v);
/* Empty key (=value with no key) */
ASSERT_ERROR(bind_volume_parse("block:vol::=value", &v), EINVAL);
ASSERT_NULL(v);
/* Duplicate key */
ASSERT_ERROR(bind_volume_parse("block:vol::create=new,create=any", &v), EINVAL);
ASSERT_NULL(v);
/* Aliased duplicate (size / create-size) */
ASSERT_ERROR(bind_volume_parse("block:vol::size=64M,create-size=128M", &v), EINVAL);
ASSERT_NULL(v);
/* Zero-byte size */
ASSERT_ERROR(bind_volume_parse("block:vol::size=0", &v), EINVAL);
ASSERT_NULL(v);
/* Duplicate read-only with explicit yes/no values */
ASSERT_ERROR(bind_volume_parse("block:vol::read-only=yes,read-only=no", &v), EINVAL);
ASSERT_NULL(v);
ASSERT_ERROR(bind_volume_parse("block:vol::read-only=yes,ro=auto", &v), EINVAL);
ASSERT_NULL(v);
}
TEST(machine_storage_name_split) {
_cleanup_free_ char *p = NULL, *v = NULL;
ASSERT_OK(machine_storage_name_split("block:/dev/sda", &p, &v));
ASSERT_STREQ(p, "block");
ASSERT_STREQ(v, "/dev/sda");
/* NULL outputs — validate-only mode */
ASSERT_OK(machine_storage_name_split("fs:vol-1", NULL, NULL));
ASSERT_ERROR(machine_storage_name_split(NULL, NULL, NULL), EINVAL);
ASSERT_ERROR(machine_storage_name_split("", NULL, NULL), EINVAL);
ASSERT_ERROR(machine_storage_name_split("no-colon", NULL, NULL), EINVAL);
ASSERT_ERROR(machine_storage_name_split(":vol", NULL, NULL), EINVAL);
ASSERT_ERROR(machine_storage_name_split("block:", NULL, NULL), EINVAL);
ASSERT_ERROR(machine_storage_name_split("bl\x01ock:vol", NULL, NULL), EINVAL);
}
DEFINE_TEST_MAIN(LOG_INFO);