]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared: add BindVolume parser in machine-util
authorChristian Brauner <brauner@kernel.org>
Fri, 1 May 2026 11:32:27 +0000 (13:32 +0200)
committerChristian Brauner <brauner@kernel.org>
Wed, 6 May 2026 08:30:16 +0000 (10:30 +0200)
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>
src/shared/machine-util.c
src/shared/machine-util.h
src/test/meson.build
src/test/test-machine-util.c [new file with mode: 0644]

index fa5e46ace1e53be252a36af72a4ddc1270b792cf..43a4fdfdd81b9ad12ea2f5b0768bca1336e1649e 100644 (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;
+}
index 3937ce170377e659159907dee29a0a5f6a389cc3..a992e244480762c9d30cf50d7a3f6ed01174c4e7 100644 (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);
index 09c367d3074f3484ad90fe0853246628dec74cb4..f4288119f94ba874e884f7bd027a32003da89fd3 100644 (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',
diff --git a/src/test/test-machine-util.c b/src/test/test-machine-util.c
new file mode 100644 (file)
index 0000000..8774be2
--- /dev/null
@@ -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);