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>
280 lines
8.3 KiB
C
280 lines
8.3 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include "alloc-util.h"
|
|
#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",
|
|
[IMAGE_FORMAT_QCOW2] = "qcow2",
|
|
};
|
|
|
|
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",
|
|
[DISK_TYPE_NVME] = "nvme",
|
|
[DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd",
|
|
};
|
|
|
|
DEFINE_STRING_TABLE_LOOKUP(disk_type, DiskType);
|
|
|
|
/* Wire value for the io.systemd.VirtualMachineInstance.BlockDriver IDL enum. */
|
|
static const char *const block_driver_table[_DISK_TYPE_MAX] = {
|
|
[DISK_TYPE_VIRTIO_BLK] = "virtio_blk",
|
|
[DISK_TYPE_VIRTIO_SCSI] = "scsi_hd",
|
|
[DISK_TYPE_NVME] = "nvme",
|
|
[DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi_cd",
|
|
};
|
|
|
|
DEFINE_STRING_TABLE_LOOKUP(block_driver, DiskType);
|
|
|
|
/* QEMU -device driver name (e.g. "virtio-blk-pci"). */
|
|
static const char *const qemu_device_driver_table[_DISK_TYPE_MAX] = {
|
|
[DISK_TYPE_VIRTIO_BLK] = "virtio-blk-pci",
|
|
[DISK_TYPE_VIRTIO_SCSI] = "scsi-hd",
|
|
[DISK_TYPE_NVME] = "nvme",
|
|
[DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd",
|
|
};
|
|
|
|
DEFINE_STRING_TABLE_LOOKUP(qemu_device_driver, DiskType);
|
|
|
|
int parse_disk_spec(
|
|
const char *arg,
|
|
ImageFormat *format,
|
|
DiskType *disk_type,
|
|
char **ret_path) {
|
|
|
|
int r;
|
|
|
|
assert(arg);
|
|
assert(format);
|
|
assert(disk_type);
|
|
assert(ret_path);
|
|
|
|
ImageFormat parsed_format = *format;
|
|
DiskType parsed_disk_type = *disk_type;
|
|
const char *dp = arg;
|
|
|
|
/* Format and disk-type vocabularies don't overlap, so prefixes may appear in any order. */
|
|
for (;;) {
|
|
_cleanup_free_ char *word = NULL;
|
|
const char *save = dp;
|
|
|
|
r = extract_first_word(&dp, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
|
|
if (r < 0)
|
|
return r;
|
|
if (r == 0 || !dp) {
|
|
/* No ':' remained after this word — rest is the path. */
|
|
dp = save;
|
|
break;
|
|
}
|
|
|
|
ImageFormat f = image_format_from_string(word);
|
|
if (f >= 0) {
|
|
parsed_format = f;
|
|
continue;
|
|
}
|
|
|
|
DiskType dt = disk_type_from_string(word);
|
|
if (dt >= 0) {
|
|
parsed_disk_type = dt;
|
|
continue;
|
|
}
|
|
|
|
/* Unknown prefix — rewind, remainder is the path. */
|
|
dp = save;
|
|
break;
|
|
}
|
|
|
|
_cleanup_free_ char *path = NULL;
|
|
r = parse_path_argument(dp, /* suppress_root= */ false, &path);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
*format = parsed_format;
|
|
*disk_type = parsed_disk_type;
|
|
*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;
|
|
}
|