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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user