[libvirt] [RFC PATCH 0/8] Introduce the virDomainDefineJSONFlags() public API

Back in 2005, when the libvirt project was started, XML was pretty much the only sensible, widely used format one could use to store structured data. A lot has changed since then, and these days you'd be hard pressed to find any API that does not use JSON for encoding information; in fact, we already use JSON heavily inside libvirt, for example to communicate with QEMU processes through QMP or store configuration data for use by the libvirt NSS plugin. The reasons for JSON's success over XML are plain to see: you get the same nicely structured data, but none of the ugly angle brackets or annoying repetition caused by having to both open *and* close elements explicitly; additionally, you get support for extremely useful and common data structures such as lists and dictionary out of the box, and even properly typed strings and numbers. It's just an overall much better data representation language. This RFC series provides the initial, very basic (yet fully functional) implementation of a JSON-based alternative to the existing virDomainDefineXML(); over time, we would introduce similar APIs for defining libvirt objects other than domains, such as for example networks. Once enough time has passed, we might even consider removing the XML-based APIs altogether and drop the XML parsing code along with our libxml2 dependency, making libvirt much leaner in the process. Andrea Bolognani (8): conf: Introduce virDomainDefParseJSONString() tests: Add qemujson2argvtest rpc: Add JSON to the list of fixups in gendispatch Introduce the virDomainDefineJSONFlags() public API remote: Implement virDomainDefineJSONFlags() support qemu: Add virDomainDefineJSONFlags() support virsh: Add JSON support to the 'define' subcommand news: Update for virDomainDefineJSONFlags() docs/news.xml | 11 + include/libvirt/libvirt-domain.h | 3 + include/libvirt/virterror.h | 1 + src/conf/domain_conf.c | 241 ++++ src/conf/domain_conf.h | 5 + src/driver-hypervisor.h | 6 + src/libvirt-domain.c | 45 + src/libvirt_private.syms | 1 + src/libvirt_public.syms | 1 + src/qemu/qemu_driver.c | 84 ++ src/remote/remote_driver.c | 3 +- src/remote/remote_protocol.x | 19 +- src/remote_protocol-structs | 8 + src/rpc/gendispatch.pl | 1 + src/util/virerror.c | 3 + tests/Makefile.am | 15 + tests/qemujson2argvdata/tiny.json | 29 + .../qemujson2argvdata/tiny.x86_64-latest.args | 33 + tests/qemujson2argvtest.c | 1001 +++++++++++++++++ tools/virsh-domain.c | 36 +- 20 files changed, 1540 insertions(+), 6 deletions(-) create mode 100644 tests/qemujson2argvdata/tiny.json create mode 100644 tests/qemujson2argvdata/tiny.x86_64-latest.args create mode 100644 tests/qemujson2argvtest.c -- 2.20.1

We parse just enough JSON to be able to define a domain using the new API at the moment: clearly more work is needed before it can be considered a replacement for the existing XML-based APIs, but you gotta start somewhere :) Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- include/libvirt/virterror.h | 1 + src/conf/domain_conf.c | 241 ++++++++++++++++++++++++++++++++++++ src/conf/domain_conf.h | 5 + src/libvirt_private.syms | 1 + src/util/virerror.c | 3 + 5 files changed, 251 insertions(+) diff --git a/include/libvirt/virterror.h b/include/libvirt/virterror.h index 6dc83a17cc..90406d595a 100644 --- a/include/libvirt/virterror.h +++ b/include/libvirt/virterror.h @@ -326,6 +326,7 @@ typedef enum { VIR_ERR_INVALID_DOMAIN_CHECKPOINT = 102, /* invalid domain checkpoint */ VIR_ERR_NO_DOMAIN_CHECKPOINT = 103, /* domain checkpoint not found */ VIR_ERR_NO_DOMAIN_BACKUP = 104, /* domain backup job id not found */ + VIR_ERR_JSON_ERROR = 105, /* JSON format error */ # ifdef VIR_ENUM_SENTINELS VIR_ERR_NUMBER_LAST diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c index 5f2b1f68b5..896dd75082 100644 --- a/src/conf/domain_conf.c +++ b/src/conf/domain_conf.c @@ -58,6 +58,7 @@ #include "virhostdev.h" #include "virmdev.h" #include "virdomainsnapshotobjlist.h" +#include "virjson.h" #define VIR_FROM_THIS VIR_FROM_DOMAIN @@ -21206,6 +21207,246 @@ virDomainDefParseFile(const char *filename, } +static int +virDomainDefParseJSONDomainOSType(virDomainDefPtr def, + virJSONValuePtr osType) +{ + virJSONValuePtr attributes = virJSONValueObjectGetObject(osType, "attributes"); + const char *type = NULL; + const char *arch = NULL; + const char *machine = NULL; + int ret = -1; + + if (!attributes) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("No attributes for 'type' object")); + goto cleanup; + } + + if (!(type = virJSONValueObjectGetString(osType, "value"))) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("Missing OS type")); + goto cleanup; + } + + if ((def->os.type = virDomainOSTypeFromString(type)) < 0) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("Invalid OS type '%s'"), + type); + return -1; + } + + if ((machine = virJSONValueObjectGetString(attributes, "machine")) && + VIR_STRDUP(def->os.machine, machine) < 0) { + goto cleanup; + } + + if ((arch = virJSONValueObjectGetString(attributes, "arch")) && + !(def->os.arch = virArchFromString(arch))) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("Invalid architecture '%s'"), + arch); + } + + ret = 0; + + cleanup: + return ret; +} + + +static int +virDomainDefParseJSONDomainOS(virDomainDefPtr def, + virJSONValuePtr os) +{ + virJSONValuePtr children = virJSONValueObjectGetObject(os, "children"); + virJSONValuePtr osType = NULL; + int ret = -1; + + if (!children) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("No children for 'os' object")); + goto cleanup; + } + + if (!(osType = virJSONValueObjectGetObject(children, "type"))) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("Missing 'type' children for 'os' object")); + goto cleanup; + } + + if (virDomainDefParseJSONDomainOSType(def, osType) < 0) + goto cleanup; + + ret = 0; + + cleanup: + return ret; +} + + +static int +virDomainDefParseJSONDomain(virDomainDefPtr def, + virJSONValuePtr domain) +{ + virJSONValuePtr attributes = virJSONValueObjectGetObject(domain, "attributes"); + virJSONValuePtr children = virJSONValueObjectGetObject(domain, "children"); + virJSONValuePtr tmp = NULL; + const char *virtType = NULL; + const char *name = NULL; + int ret = -1; + + if (!attributes) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("No attributes for 'domain' object")); + goto cleanup; + } + + if (!(virtType = virJSONValueObjectGetString(attributes, "type"))) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("Missing 'type' attribute for 'domain' object")); + goto cleanup; + } + + if ((def->virtType = virDomainVirtTypeFromString(virtType)) < 0) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("Invalid virtualization type '%s'"), + virtType); + goto cleanup; + } + + if (!children) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("No children for 'domain' object")); + goto cleanup; + } + + if (!(tmp = virJSONValueObjectGetObject(children, "name")) || + !(name = virJSONValueObjectGetString(tmp, "value"))) { + virReportError(VIR_ERR_NO_NAME, NULL); + goto cleanup; + } + + if (!VIR_STRDUP(def->name, name)) + goto cleanup; + + if ((tmp = virJSONValueObjectGetObject(children, "uuid"))) { + + const char *uuid; + + if (!(uuid = virJSONValueObjectGetString(tmp, "value")) || + virUUIDParse(uuid, def->uuid) < 0) { + virReportError(VIR_ERR_JSON_ERROR, + _("Invalid UUID '%s'"), + uuid); + goto cleanup; + } + } else { + if (virUUIDGenerate(def->uuid) < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("Failed to generate UUID")); + goto cleanup; + } + } + + if ((tmp = virJSONValueObjectGetObject(children, "memory"))) { + + unsigned long long max = virMemoryMaxValue(true); + unsigned long long memory; + + if (!virJSONValueObjectHasKey(tmp, "value")) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("Missing 'value' attribute for 'memory' object")); + goto cleanup; + } + + if (virJSONValueObjectGetNumberUlong(tmp, "value", &memory) < 0 || + virScaleInteger(&memory, NULL, 1024, max) < 0) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("Invalid memory size")); + goto cleanup; + } + + /* Yes, we really do use kibibytes for our internal sizing. */ + def->mem.total_memory = VIR_DIV_UP(memory, 1024); + + if (def->mem.total_memory >= VIR_DIV_UP(max, 1024)) { + virReportError(VIR_ERR_OVERFLOW, "%s", + _("Memory size is too large")); + goto cleanup; + } + } + + if (virDomainDefSetVcpusMax(def, 1, NULL) < 0 || + virDomainDefSetVcpus(def, 1) < 0) { + goto cleanup; + } + + if (!(tmp = virJSONValueObjectGetObject(children, "os"))) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("No 'os' object")); + goto cleanup; + } + + if (virDomainDefParseJSONDomainOS(def, tmp) < 0) + goto cleanup; + + ret = 0; + + cleanup: + return ret; +} + + +static virDomainDefPtr +virDomainDefParseJSON(virJSONValuePtr json, + virCapsPtr caps, + virDomainXMLOptionPtr xmlopt, + void *parseOpaque, + unsigned int flags) +{ + virDomainDefPtr def = NULL; + virJSONValuePtr domain = NULL; + + if (!(def = virDomainDefNew())) + goto error; + + if (!(domain = virJSONValueObjectGetObject(json, "domain"))) { + virReportError(VIR_ERR_JSON_ERROR, "%s", + _("No 'domain' object")); + goto error; + } + + if (virDomainDefParseJSONDomain(def, domain) < 0 || + virDomainDefPostParse(def, caps, flags, xmlopt, parseOpaque) < 0 || + virDomainDefValidate(def, caps, flags, xmlopt) < 0) { + goto error; + } + + return def; + + error: + virDomainDefFree(def); + return NULL; +} + + +virDomainDefPtr +virDomainDefParseJSONString(const char *buf, + virCapsPtr caps, + virDomainXMLOptionPtr xmlopt, + void *parseOpaque, + unsigned int flags) +{ + VIR_AUTOPTR(virJSONValue) json = NULL; + + if (!(json = virJSONValueFromString(buf))) + return NULL; + + return virDomainDefParseJSON(json, caps, xmlopt, parseOpaque, flags); +} + + virDomainDefPtr virDomainDefParseNode(xmlDocPtr xml, xmlNodePtr root, diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h index 4a25480662..ebeabd8dbe 100644 --- a/src/conf/domain_conf.h +++ b/src/conf/domain_conf.h @@ -2954,6 +2954,11 @@ virDomainDefPtr virDomainDefParseFile(const char *filename, virDomainXMLOptionPtr xmlopt, void *parseOpaque, unsigned int flags); +virDomainDefPtr virDomainDefParseJSONString(const char *buf, + virCapsPtr caps, + virDomainXMLOptionPtr xmlopt, + void *parseOpaque, + unsigned int flags); virDomainDefPtr virDomainDefParseNode(xmlDocPtr doc, xmlNodePtr root, virCapsPtr caps, diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index 73ef24d66f..8c60a01d8c 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -291,6 +291,7 @@ virDomainDefMaybeAddInput; virDomainDefNeedsPlacementAdvice; virDomainDefNew; virDomainDefParseFile; +virDomainDefParseJSONString; virDomainDefParseNode; virDomainDefParseString; virDomainDefPostParse; diff --git a/src/util/virerror.c b/src/util/virerror.c index 05e535d859..4e843efe99 100644 --- a/src/util/virerror.c +++ b/src/util/virerror.c @@ -1224,6 +1224,9 @@ const virErrorMsgTuple virErrorMsgStrings[VIR_ERR_NUMBER_LAST] = { [VIR_ERR_NO_DOMAIN_BACKUP] = { N_("Domain backup job id not found"), N_("Domain backup job id not found: %s") }, + [VIR_ERR_JSON_ERROR] = { + N_("JSON description is invalid or not well formed"), + N_("JSON error: %s") }, }; -- 2.20.1

This is basically a copy of the existing qemuxml2argvtest, with a couple of obvious changes. We're not using most of the features at the moment, but as the JSON-based APIs evolve and get closer to feature-parity with the XML-based ones we're definitely going to need more and more of them, so we might as well start with the full package. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- tests/Makefile.am | 15 + tests/qemujson2argvdata/tiny.json | 29 + .../qemujson2argvdata/tiny.x86_64-latest.args | 33 + tests/qemujson2argvtest.c | 1001 +++++++++++++++++ 4 files changed, 1078 insertions(+) create mode 100644 tests/qemujson2argvdata/tiny.json create mode 100644 tests/qemujson2argvdata/tiny.x86_64-latest.args create mode 100644 tests/qemujson2argvtest.c diff --git a/tests/Makefile.am b/tests/Makefile.am index d3cdbff8bb..df82c7dc22 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -162,6 +162,7 @@ EXTRA_DIST = \ virstorageutildata \ virfilecachedata \ virresctrldata \ + qemujson2argvdata \ $(NULL) test_helpers = commandhelper ssh @@ -283,6 +284,7 @@ test_programs += qemuxml2argvtest qemuxml2xmltest \ qemumigparamstest \ qemusecuritytest \ qemufirmwaretest \ + qemujson2argvtest \ $(NULL) test_helpers += qemucapsprobe test_libraries += libqemumonitortestutils.la \ @@ -692,6 +694,18 @@ qemufirmwaretest_SOURCES = \ $(NULL) qemufirmwaretest_LDADD = $(qemu_LDADDS) $(LDADDS) +qemujson2argvtest_SOURCES = \ + virfilewrapper.c virfilewrapper.h \ + testutilsqemu.c testutilsqemu.h \ + testutils.c testutils.h \ + qemujson2argvtest.c \ + $(NULL) +qemujson2argvtest_LDADD = \ + libqemutestdriver.la \ + $(LIBXML_LIBS) \ + $(LDADDS) \ + $(NULL) + else ! WITH_QEMU EXTRA_DIST += qemuxml2argvtest.c qemuxml2xmltest.c qemuargv2xmltest.c \ domainsnapshotxml2xmltest.c \ @@ -706,6 +720,7 @@ EXTRA_DIST += qemuxml2argvtest.c qemuxml2xmltest.c qemuargv2xmltest.c \ qemusecuritytest.c qemusecuritytest.h \ qemusecuritymock.c \ qemufirmwaretest.c \ + qemujson2argvtest.c \ $(QEMUMONITORTESTUTILS_SOURCES) endif ! WITH_QEMU diff --git a/tests/qemujson2argvdata/tiny.json b/tests/qemujson2argvdata/tiny.json new file mode 100644 index 0000000000..99071c5ec0 --- /dev/null +++ b/tests/qemujson2argvdata/tiny.json @@ -0,0 +1,29 @@ +{ + "domain": { + "attributes": { + "type": "qemu" + }, + "children": { + "name": { + "value": "guest" + }, + "uuid": { + "value": "4f49aff1-4f74-45c3-87a7-51a6ad052a54" + }, + "memory": { + "value": 4194304 + }, + "os": { + "children": { + "type": { + "attributes": { + "arch": "x86_64", + "machine": "pc" + }, + "value": "hvm" + } + } + } + } + } +} diff --git a/tests/qemujson2argvdata/tiny.x86_64-latest.args b/tests/qemujson2argvdata/tiny.x86_64-latest.args new file mode 100644 index 0000000000..e8e67d7727 --- /dev/null +++ b/tests/qemujson2argvdata/tiny.x86_64-latest.args @@ -0,0 +1,33 @@ +LC_ALL=C \ +PATH=/bin \ +HOME=/tmp/lib/domain--1-guest \ +USER=test \ +LOGNAME=test \ +XDG_DATA_HOME=/tmp/lib/domain--1-guest/.local/share \ +XDG_CACHE_HOME=/tmp/lib/domain--1-guest/.cache \ +XDG_CONFIG_HOME=/tmp/lib/domain--1-guest/.config \ +QEMU_AUDIO_DRV=none \ +/usr/bin/qemu-system-x86_64 \ +-name guest=guest,debug-threads=on \ +-S \ +-object secret,id=masterKey0,format=raw,\ +file=/tmp/lib/domain--1-guest/master-key.aes \ +-machine pc,accel=tcg,usb=off,dump-guest-core=off \ +-m 4096 \ +-realtime mlock=off \ +-smp 1,sockets=1,cores=1,threads=1 \ +-uuid 4f49aff1-4f74-45c3-87a7-51a6ad052a54 \ +-display none \ +-no-user-config \ +-nodefaults \ +-chardev socket,id=charmonitor,fd=1729,server,nowait \ +-mon chardev=charmonitor,id=monitor,mode=control \ +-rtc base=utc \ +-no-reboot \ +-no-acpi \ +-boot strict=on \ +-device piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2 \ +-device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x2 \ +-sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,\ +resourcecontrol=deny \ +-msg timestamp=on diff --git a/tests/qemujson2argvtest.c b/tests/qemujson2argvtest.c new file mode 100644 index 0000000000..e754813646 --- /dev/null +++ b/tests/qemujson2argvtest.c @@ -0,0 +1,1001 @@ +#include <config.h> + +#include <unistd.h> + +#include <sys/types.h> +#include <fcntl.h> + +#include "testutils.h" + +#ifdef WITH_QEMU + +# include "internal.h" +# include "viralloc.h" +# include "qemu/qemu_alias.h" +# include "qemu/qemu_capabilities.h" +# include "qemu/qemu_command.h" +# include "qemu/qemu_domain.h" +# include "qemu/qemu_migration.h" +# include "qemu/qemu_process.h" +# include "datatypes.h" +# include "conf/storage_conf.h" +# include "cpu/cpu_map.h" +# include "virstring.h" +# include "storage/storage_driver.h" +# include "virmock.h" +# include "virfilewrapper.h" +# include "configmake.h" + +# define LIBVIRT_QEMU_CAPSPRIV_H_ALLOW +# include "qemu/qemu_capspriv.h" + +# include "testutilsqemu.h" + +# define VIR_FROM_THIS VIR_FROM_QEMU + +static virQEMUDriver driver; + +static unsigned char * +fakeSecretGetValue(virSecretPtr obj ATTRIBUTE_UNUSED, + size_t *value_size, + unsigned int fakeflags ATTRIBUTE_UNUSED, + unsigned int internalFlags ATTRIBUTE_UNUSED) +{ + char *secret; + if (VIR_STRDUP(secret, "AQCVn5hO6HzFAhAAq0NCv8jtJcIcE+HOBlMQ1A") < 0) + return NULL; + *value_size = strlen(secret); + return (unsigned char *) secret; +} + +static virSecretPtr +fakeSecretLookupByUsage(virConnectPtr conn, + int usageType, + const char *usageID) +{ + unsigned char uuid[VIR_UUID_BUFLEN]; + if (usageType == VIR_SECRET_USAGE_TYPE_VOLUME) { + if (!STRPREFIX(usageID, "/storage/guest_disks/")) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "test provided invalid volume storage prefix '%s'", + usageID); + return NULL; + } + } else if (STRNEQ(usageID, "mycluster_myname")) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "test provided incorrect usage '%s'", usageID); + return NULL; + } + + if (virUUIDGenerate(uuid) < 0) + return NULL; + + return virGetSecret(conn, uuid, usageType, usageID); +} + +static virSecretPtr +fakeSecretLookupByUUID(virConnectPtr conn, + const unsigned char *uuid) +{ + /* NB: This mocked value could be "tls" or "volume" depending on + * which test is being run, we'll leave at NONE (or 0) */ + return virGetSecret(conn, uuid, VIR_SECRET_USAGE_TYPE_NONE, ""); +} + +static virSecretDriver fakeSecretDriver = { + .connectNumOfSecrets = NULL, + .connectListSecrets = NULL, + .secretLookupByUUID = fakeSecretLookupByUUID, + .secretLookupByUsage = fakeSecretLookupByUsage, + .secretDefineXML = NULL, + .secretGetXMLDesc = NULL, + .secretSetValue = NULL, + .secretGetValue = fakeSecretGetValue, + .secretUndefine = NULL, +}; + + +# define STORAGE_POOL_XML_PATH "storagepoolxml2xmlout/" +static const unsigned char fakeUUID[VIR_UUID_BUFLEN] = "fakeuuid"; + +static virStoragePoolPtr +fakeStoragePoolLookupByName(virConnectPtr conn, + const char *name) +{ + char *xmlpath = NULL; + virStoragePoolPtr ret = NULL; + + if (STRNEQ(name, "inactive")) { + if (virAsprintf(&xmlpath, "%s/%s%s.xml", + abs_srcdir, + STORAGE_POOL_XML_PATH, + name) < 0) + return NULL; + + if (!virFileExists(xmlpath)) { + virReportError(VIR_ERR_NO_STORAGE_POOL, + "File '%s' not found", xmlpath); + goto cleanup; + } + } + + ret = virGetStoragePool(conn, name, fakeUUID, NULL, NULL); + + cleanup: + VIR_FREE(xmlpath); + return ret; +} + + +static virStorageVolPtr +fakeStorageVolLookupByName(virStoragePoolPtr pool, + const char *name) +{ + char **volinfo = NULL; + virStorageVolPtr ret = NULL; + + if (STREQ(pool->name, "inactive")) { + virReportError(VIR_ERR_OPERATION_INVALID, + "storage pool '%s' is not active", pool->name); + return NULL; + } + + if (STREQ(name, "nonexistent")) { + virReportError(VIR_ERR_NO_STORAGE_VOL, + "no storage vol with matching name '%s'", name); + return NULL; + } + + if (!strchr(name, '+')) + goto fallback; + + if (!(volinfo = virStringSplit(name, "+", 2))) + return NULL; + + if (!volinfo[1]) + goto fallback; + + ret = virGetStorageVol(pool->conn, pool->name, volinfo[1], volinfo[0], + NULL, NULL); + + cleanup: + virStringListFree(volinfo); + return ret; + + fallback: + ret = virGetStorageVol(pool->conn, pool->name, name, "block", NULL, NULL); + goto cleanup; +} + +static int +fakeStorageVolGetInfo(virStorageVolPtr vol, + virStorageVolInfoPtr info) +{ + memset(info, 0, sizeof(*info)); + + info->type = virStorageVolTypeFromString(vol->key); + + if (info->type < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "Invalid volume type '%s'", vol->key); + return -1; + } + + return 0; +} + + +static char * +fakeStorageVolGetPath(virStorageVolPtr vol) +{ + char *ret = NULL; + + ignore_value(virAsprintf(&ret, "/some/%s/device/%s", vol->key, vol->name)); + + return ret; +} + + +static char * +fakeStoragePoolGetXMLDesc(virStoragePoolPtr pool, + unsigned int flags_unused ATTRIBUTE_UNUSED) +{ + char *xmlpath = NULL; + char *xmlbuf = NULL; + + if (STREQ(pool->name, "inactive")) { + virReportError(VIR_ERR_NO_STORAGE_POOL, NULL); + return NULL; + } + + if (virAsprintf(&xmlpath, "%s/%s%s.xml", + abs_srcdir, + STORAGE_POOL_XML_PATH, + pool->name) < 0) + return NULL; + + if (virTestLoadFile(xmlpath, &xmlbuf) < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "failed to load XML file '%s'", + xmlpath); + goto cleanup; + } + + cleanup: + VIR_FREE(xmlpath); + + return xmlbuf; +} + +static int +fakeStoragePoolIsActive(virStoragePoolPtr pool) +{ + if (STREQ(pool->name, "inactive")) + return 0; + + return 1; +} + +/* Test storage pool implementation + * + * These functions aid testing of storage pool related stuff when creating a + * qemu command line. + * + * There are a few "magic" values to pass to these functions: + * + * 1) "inactive" as a pool name to create an inactive pool. All other names are + * interpreted as file names in storagepoolxml2xmlout/ and are used as the + * definition for the pool. If the file doesn't exist the pool doesn't exist. + * + * 2) "nonexistent" returns an error while looking up a volume. Otherwise + * pattern VOLUME_TYPE+VOLUME_PATH can be used to simulate a volume in a pool. + * This creates a fake path for this volume. If the '+' sign is omitted, block + * type is assumed. + */ +static virStorageDriver fakeStorageDriver = { + .storagePoolLookupByName = fakeStoragePoolLookupByName, + .storageVolLookupByName = fakeStorageVolLookupByName, + .storagePoolGetXMLDesc = fakeStoragePoolGetXMLDesc, + .storageVolGetPath = fakeStorageVolGetPath, + .storageVolGetInfo = fakeStorageVolGetInfo, + .storagePoolIsActive = fakeStoragePoolIsActive, +}; + + +/* virNetDevOpenvswitchGetVhostuserIfname mocks a portdev name - handle that */ +static virNWFilterBindingPtr +fakeNWFilterBindingLookupByPortDev(virConnectPtr conn, + const char *portdev) +{ + if (STREQ(portdev, "vhost-user0")) + return virGetNWFilterBinding(conn, "fake_vnet0", "fakeFilterName"); + + virReportError(VIR_ERR_NO_NWFILTER_BINDING, + "no nwfilter binding for port dev '%s'", portdev); + return NULL; +} + + +static int +fakeNWFilterBindingDelete(virNWFilterBindingPtr binding ATTRIBUTE_UNUSED) +{ + return 0; +} + + +static virNWFilterDriver fakeNWFilterDriver = { + .nwfilterBindingLookupByPortDev = fakeNWFilterBindingLookupByPortDev, + .nwfilterBindingDelete = fakeNWFilterBindingDelete, +}; + +typedef enum { + FLAG_EXPECT_FAILURE = 1 << 0, + FLAG_EXPECT_PARSE_ERROR = 1 << 1, + FLAG_FIPS = 1 << 2, + FLAG_REAL_CAPS = 1 << 3, + FLAG_SKIP_LEGACY_CPUS = 1 << 4, +} virQemuJSON2ArgvTestFlags; + +struct testInfo { + const char *name; + const char *suffix; + virQEMUCapsPtr qemuCaps; + const char *migrateFrom; + int migrateFd; + unsigned int flags; + unsigned int parseFlags; +}; + + +static int +testAddCPUModels(virQEMUCapsPtr caps, bool skipLegacy) +{ + virArch arch = virQEMUCapsGetArch(caps); + const char *x86Models[] = { + "Opteron_G3", "Opteron_G2", "Opteron_G1", + "Nehalem", "Penryn", "Conroe", + "Haswell-noTSX", "Haswell", + }; + const char *x86LegacyModels[] = { + "n270", "athlon", "pentium3", "pentium2", "pentium", + "486", "coreduo", "kvm32", "qemu32", "kvm64", + "core2duo", "phenom", "qemu64", + }; + const char *armModels[] = { + "cortex-a9", "cortex-a8", "cortex-a57", "cortex-a53", + }; + const char *ppc64Models[] = { + "POWER8", "POWER7", + }; + const char *s390xModels[] = { + "z990", "zEC12", "z13", + }; + + if (ARCH_IS_X86(arch)) { + if (virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_KVM, x86Models, + ARRAY_CARDINALITY(x86Models), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0 || + virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_QEMU, x86Models, + ARRAY_CARDINALITY(x86Models), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0) + return -1; + + if (!skipLegacy) { + if (virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_KVM, + x86LegacyModels, + ARRAY_CARDINALITY(x86LegacyModels), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0 || + virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_QEMU, + x86LegacyModels, + ARRAY_CARDINALITY(x86LegacyModels), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0) + return -1; + } + } else if (ARCH_IS_ARM(arch)) { + if (virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_KVM, armModels, + ARRAY_CARDINALITY(armModels), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0 || + virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_QEMU, armModels, + ARRAY_CARDINALITY(armModels), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0) + return -1; + } else if (ARCH_IS_PPC64(arch)) { + if (virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_KVM, ppc64Models, + ARRAY_CARDINALITY(ppc64Models), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0 || + virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_QEMU, ppc64Models, + ARRAY_CARDINALITY(ppc64Models), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0) + return -1; + } else if (ARCH_IS_S390(arch)) { + if (virQEMUCapsAddCPUDefinitions(caps, VIR_DOMAIN_VIRT_KVM, s390xModels, + ARRAY_CARDINALITY(s390xModels), + VIR_DOMCAPS_CPU_USABLE_UNKNOWN) < 0) + return -1; + } + + return 0; +} + + +static int +testUpdateQEMUCaps(const struct testInfo *info, + virDomainObjPtr vm, + virCapsPtr caps) +{ + int ret = -1; + + if (!caps) + goto cleanup; + + virQEMUCapsSetArch(info->qemuCaps, vm->def->os.arch); + + virQEMUCapsInitQMPBasicArch(info->qemuCaps); + + if (testAddCPUModels(info->qemuCaps, + !!(info->flags & FLAG_SKIP_LEGACY_CPUS)) < 0) + goto cleanup; + + virQEMUCapsInitHostCPUModel(info->qemuCaps, caps->host.arch, + VIR_DOMAIN_VIRT_KVM); + virQEMUCapsInitHostCPUModel(info->qemuCaps, caps->host.arch, + VIR_DOMAIN_VIRT_QEMU); + + ret = 0; + + cleanup: + return ret; +} + + +static int +testCheckExclusiveFlags(int flags) +{ + virCheckFlags(FLAG_EXPECT_FAILURE | + FLAG_EXPECT_PARSE_ERROR | + FLAG_FIPS | + FLAG_REAL_CAPS | + FLAG_SKIP_LEGACY_CPUS | + 0, -1); + + VIR_EXCLUSIVE_FLAGS_RET(FLAG_REAL_CAPS, FLAG_SKIP_LEGACY_CPUS, -1); + return 0; +} + + +# define JSON_BUFSIZE (10*1024*1024) + + +static int +testCompareJSONToArgv(const void *data) +{ + struct testInfo *info = (void *) data; + char *json = NULL; + char *args = NULL; + char *migrateURI = NULL; + char *actualargv = NULL; + const char *suffix = info->suffix; + unsigned int flags = info->flags; + unsigned int parseFlags = info->parseFlags; + int ret = -1; + virDomainObjPtr vm = NULL; + virDomainChrSourceDef monitor_chr; + virConnectPtr conn; + char *log = NULL; + virCommandPtr cmd = NULL; + size_t i; + qemuDomainObjPrivatePtr priv = NULL; + VIR_AUTOFREE(char *) buf = NULL; + + memset(&monitor_chr, 0, sizeof(monitor_chr)); + + if (!(conn = virGetConnect())) + goto cleanup; + + if (!suffix) + suffix = ""; + + conn->secretDriver = &fakeSecretDriver; + conn->storageDriver = &fakeStorageDriver; + conn->nwfilterDriver = &fakeNWFilterDriver; + + virSetConnectInterface(conn); + virSetConnectNetwork(conn); + virSetConnectNWFilter(conn); + virSetConnectNodeDev(conn); + virSetConnectSecret(conn); + virSetConnectStorage(conn); + + if (virQEMUCapsGet(info->qemuCaps, QEMU_CAPS_ENABLE_FIPS)) + flags |= FLAG_FIPS; + + if (testCheckExclusiveFlags(info->flags) < 0) + goto cleanup; + + if (qemuTestCapsCacheInsert(driver.qemuCapsCache, info->qemuCaps) < 0) + goto cleanup; + + if (virAsprintf(&json, "%s/qemujson2argvdata/%s.json", + abs_srcdir, info->name) < 0 || + virAsprintf(&args, "%s/qemujson2argvdata/%s%s.args", + abs_srcdir, info->name, suffix) < 0) + goto cleanup; + + if (info->migrateFrom && + !(migrateURI = qemuMigrationDstGetURI(info->migrateFrom, + info->migrateFd))) + goto cleanup; + + if (!(vm = virDomainObjNew(driver.xmlopt))) + goto cleanup; + + if (virFileReadAll(json, JSON_BUFSIZE, &buf) < 0) + goto cleanup; + + parseFlags |= VIR_DOMAIN_DEF_PARSE_INACTIVE; + if (!(vm->def = virDomainDefParseJSONString(buf, driver.caps, driver.xmlopt, + NULL, parseFlags))) { + if (flags & FLAG_EXPECT_PARSE_ERROR) + goto ok; + goto cleanup; + } + if (flags & FLAG_EXPECT_PARSE_ERROR) { + VIR_TEST_DEBUG("passed instead of expected parse error"); + goto cleanup; + } + priv = vm->privateData; + + if (virBitmapParse("0-3", &priv->autoNodeset, 4) < 0) + goto cleanup; + + if (!virDomainDefCheckABIStability(vm->def, vm->def, driver.xmlopt)) { + VIR_TEST_DEBUG("ABI stability check failed on %s", json); + goto cleanup; + } + + vm->def->id = -1; + + if (qemuProcessPrepareMonitorChr(&monitor_chr, priv->libDir) < 0) + goto cleanup; + + if (!(info->flags & FLAG_REAL_CAPS) && + testUpdateQEMUCaps(info, vm, driver.caps) < 0) + goto cleanup; + + log = virTestLogContentAndReset(); + VIR_FREE(log); + virResetLastError(); + + for (i = 0; i < vm->def->nhostdevs; i++) { + virDomainHostdevDefPtr hostdev = vm->def->hostdevs[i]; + + if (hostdev->mode == VIR_DOMAIN_HOSTDEV_MODE_SUBSYS && + hostdev->source.subsys.type == VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_PCI && + hostdev->source.subsys.u.pci.backend == VIR_DOMAIN_HOSTDEV_PCI_BACKEND_DEFAULT) { + hostdev->source.subsys.u.pci.backend = VIR_DOMAIN_HOSTDEV_PCI_BACKEND_KVM; + } + } + + if (vm->def->vsock) { + virDomainVsockDefPtr vsock = vm->def->vsock; + qemuDomainVsockPrivatePtr vsockPriv = + (qemuDomainVsockPrivatePtr)vsock->privateData; + + if (vsock->auto_cid == VIR_TRISTATE_BOOL_YES) + vsock->guest_cid = 42; + + vsockPriv->vhostfd = 6789; + } + + if (vm->def->tpm) { + switch (vm->def->tpm->type) { + case VIR_DOMAIN_TPM_TYPE_EMULATOR: + VIR_FREE(vm->def->tpm->data.emulator.source.data.file.path); + if (VIR_STRDUP(vm->def->tpm->data.emulator.source.data.file.path, + "/dev/test") < 0) + goto cleanup; + vm->def->tpm->data.emulator.source.type = VIR_DOMAIN_CHR_TYPE_FILE; + break; + case VIR_DOMAIN_TPM_TYPE_PASSTHROUGH: + case VIR_DOMAIN_TPM_TYPE_LAST: + break; + } + } + + if (!(cmd = qemuProcessCreatePretendCmd(&driver, vm, migrateURI, + (flags & FLAG_FIPS), false, + VIR_QEMU_PROCESS_START_COLD))) { + if (flags & FLAG_EXPECT_FAILURE) + goto ok; + goto cleanup; + } + if (flags & FLAG_EXPECT_FAILURE) { + VIR_TEST_DEBUG("passed instead of expected failure"); + goto cleanup; + } + + if (!(actualargv = virCommandToString(cmd, false))) + goto cleanup; + + if (virTestCompareToFile(actualargv, args) < 0) + goto cleanup; + + ret = 0; + + ok: + if (ret == 0 && flags & FLAG_EXPECT_FAILURE) { + ret = -1; + VIR_TEST_DEBUG("Error expected but there wasn't any.\n"); + goto cleanup; + } + if (!virTestOOMActive()) { + if (flags & FLAG_EXPECT_FAILURE) { + if ((log = virTestLogContentAndReset())) + VIR_TEST_DEBUG("Got expected error: \n%s", log); + } + virResetLastError(); + ret = 0; + } + + cleanup: + VIR_FREE(log); + VIR_FREE(actualargv); + virDomainChrSourceDefClear(&monitor_chr); + virCommandFree(cmd); + virObjectUnref(vm); + virSetConnectSecret(NULL); + virSetConnectStorage(NULL); + virObjectUnref(conn); + VIR_FREE(migrateURI); + VIR_FREE(json); + VIR_FREE(args); + return ret; +} + +# define TEST_CAPS_PATH abs_srcdir "/qemucapabilitiesdata" + +typedef enum { + ARG_QEMU_CAPS, + ARG_GIC, + ARG_MIGRATE_FROM, + ARG_MIGRATE_FD, + ARG_FLAGS, + ARG_PARSEFLAGS, + ARG_CAPS_ARCH, + ARG_CAPS_VER, + ARG_END, +} testInfoArgName; + +static int +testInfoSetArgs(struct testInfo *info, + virHashTablePtr capslatest, ...) +{ + va_list argptr; + testInfoArgName argname; + virQEMUCapsPtr qemuCaps = NULL; + int gic = GIC_NONE; + char *capsarch = NULL; + char *capsver = NULL; + VIR_AUTOFREE(char *) capsfile = NULL; + int flag; + int ret = -1; + + va_start(argptr, capslatest); + argname = va_arg(argptr, testInfoArgName); + while (argname != ARG_END) { + switch (argname) { + case ARG_QEMU_CAPS: + if (qemuCaps || !(qemuCaps = virQEMUCapsNew())) + goto cleanup; + + while ((flag = va_arg(argptr, int)) < QEMU_CAPS_LAST) + virQEMUCapsSet(qemuCaps, flag); + + /* Some tests are run with NONE capabilities, which is just + * another name for QEMU_CAPS_LAST. If that is the case the + * arguments look like this : + * + * ARG_QEMU_CAPS, NONE, QEMU_CAPS_LAST, ARG_END + * + * Fetch one argument more and if it is QEMU_CAPS_LAST then + * break from the switch() to force getting next argument + * in the line. If it is not QEMU_CAPS_LAST then we've + * fetched real ARG_* and we must process it. + */ + if ((flag = va_arg(argptr, int)) != QEMU_CAPS_LAST) { + argname = flag; + continue; + } + + break; + + case ARG_GIC: + gic = va_arg(argptr, int); + break; + + case ARG_MIGRATE_FROM: + info->migrateFrom = va_arg(argptr, char *); + break; + + case ARG_MIGRATE_FD: + info->migrateFd = va_arg(argptr, int); + break; + + case ARG_FLAGS: + info->flags = va_arg(argptr, int); + break; + + case ARG_PARSEFLAGS: + info->parseFlags = va_arg(argptr, int); + break; + + case ARG_CAPS_ARCH: + capsarch = va_arg(argptr, char *); + break; + + case ARG_CAPS_VER: + capsver = va_arg(argptr, char *); + break; + + case ARG_END: + default: + fprintf(stderr, "Unexpected test info argument"); + goto cleanup; + } + + argname = va_arg(argptr, testInfoArgName); + } + + if (!!capsarch ^ !!capsver) { + fprintf(stderr, "ARG_CAPS_ARCH and ARG_CAPS_VER " + "must be specified together.\n"); + goto cleanup; + } + + if (qemuCaps && (capsarch || capsver)) { + fprintf(stderr, "ARG_QEMU_CAPS can not be combined with ARG_CAPS_ARCH " + "or ARG_CAPS_VER\n"); + goto cleanup; + } + + if (!qemuCaps && capsarch && capsver) { + bool stripmachinealiases = false; + + if (STREQ(capsver, "latest")) { + if (VIR_STRDUP(capsfile, virHashLookup(capslatest, capsarch)) < 0) + goto cleanup; + stripmachinealiases = true; + } else if (virAsprintf(&capsfile, "%s/caps_%s.%s.xml", + TEST_CAPS_PATH, capsver, capsarch) < 0) { + goto cleanup; + } + + if (!(qemuCaps = qemuTestParseCapabilitiesArch(virArchFromString(capsarch), + capsfile))) { + goto cleanup; + } + + if (stripmachinealiases) + virQEMUCapsStripMachineAliases(qemuCaps); + info->flags |= FLAG_REAL_CAPS; + } + + if (!qemuCaps) { + fprintf(stderr, "No qemuCaps generated\n"); + goto cleanup; + } + VIR_STEAL_PTR(info->qemuCaps, qemuCaps); + + if (gic != GIC_NONE && testQemuCapsSetGIC(info->qemuCaps, gic) < 0) + goto cleanup; + + ret = 0; + + cleanup: + virObjectUnref(qemuCaps); + va_end(argptr); + + return ret; +} + +static void +testInfoClear(struct testInfo *info) +{ + virObjectUnref(info->qemuCaps); +} + +# define FAKEROOTDIRTEMPLATE abs_builddir "/fakerootdir-XXXXXX" + +static int +mymain(void) +{ + int ret = 0, i; + char *fakerootdir; + const char *archs[] = { + "aarch64", + "ppc64", + "riscv64", + "s390x", + "x86_64", + }; + virHashTablePtr capslatest = NULL; + + if (VIR_STRDUP_QUIET(fakerootdir, FAKEROOTDIRTEMPLATE) < 0) { + fprintf(stderr, "Out of memory\n"); + abort(); + } + + if (!mkdtemp(fakerootdir)) { + fprintf(stderr, "Cannot create fakerootdir"); + abort(); + } + + setenv("LIBVIRT_FAKE_ROOT_DIR", fakerootdir, 1); + + /* Set the timezone because we are mocking the time() function. + * If we don't do that, then localtime() may return unpredictable + * results. In order to detect things that just work by a blind + * chance, we need to set an virtual timezone that no libvirt + * developer resides in. */ + if (setenv("TZ", "VIR00:30", 1) < 0) { + perror("setenv"); + return EXIT_FAILURE; + } + + if (qemuTestDriverInit(&driver) < 0) + return EXIT_FAILURE; + + driver.privileged = true; + + VIR_FREE(driver.config->defaultTLSx509certdir); + if (VIR_STRDUP_QUIET(driver.config->defaultTLSx509certdir, "/etc/pki/qemu") < 0) + return EXIT_FAILURE; + VIR_FREE(driver.config->vncTLSx509certdir); + if (VIR_STRDUP_QUIET(driver.config->vncTLSx509certdir, "/etc/pki/libvirt-vnc") < 0) + return EXIT_FAILURE; + VIR_FREE(driver.config->spiceTLSx509certdir); + if (VIR_STRDUP_QUIET(driver.config->spiceTLSx509certdir, "/etc/pki/libvirt-spice") < 0) + return EXIT_FAILURE; + VIR_FREE(driver.config->chardevTLSx509certdir); + if (VIR_STRDUP_QUIET(driver.config->chardevTLSx509certdir, "/etc/pki/libvirt-chardev") < 0) + return EXIT_FAILURE; + VIR_FREE(driver.config->vxhsTLSx509certdir); + if (VIR_STRDUP_QUIET(driver.config->vxhsTLSx509certdir, "/etc/pki/libvirt-vxhs/dummy,path") < 0) + return EXIT_FAILURE; + VIR_FREE(driver.config->nbdTLSx509certdir); + if (VIR_STRDUP_QUIET(driver.config->nbdTLSx509certdir, "/etc/pki/libvirt-nbd/dummy,path") < 0) + return EXIT_FAILURE; + + VIR_FREE(driver.config->hugetlbfs); + if (VIR_ALLOC_N(driver.config->hugetlbfs, 2) < 0) + return EXIT_FAILURE; + driver.config->nhugetlbfs = 2; + if (VIR_STRDUP(driver.config->hugetlbfs[0].mnt_dir, "/dev/hugepages2M") < 0 || + VIR_STRDUP(driver.config->hugetlbfs[1].mnt_dir, "/dev/hugepages1G") < 0) + return EXIT_FAILURE; + driver.config->hugetlbfs[0].size = 2048; + driver.config->hugetlbfs[0].deflt = true; + driver.config->hugetlbfs[1].size = 1048576; + driver.config->spiceTLS = 1; + if (VIR_STRDUP_QUIET(driver.config->spicePassword, "123456") < 0) + return EXIT_FAILURE; + VIR_FREE(driver.config->memoryBackingDir); + if (VIR_STRDUP_QUIET(driver.config->memoryBackingDir, "/var/lib/libvirt/qemu/ram") < 0) + return EXIT_FAILURE; + VIR_FREE(driver.config->nvramDir); + if (VIR_STRDUP(driver.config->nvramDir, "/var/lib/libvirt/qemu/nvram") < 0) + return EXIT_FAILURE; + + capslatest = virHashCreate(4, virHashValueFree); + if (!capslatest) + return EXIT_FAILURE; + + VIR_TEST_VERBOSE("\n"); + + for (i = 0; i < ARRAY_CARDINALITY(archs); ++i) { + char *cap = testQemuGetLatestCapsForArch(abs_srcdir "/qemucapabilitiesdata", + archs[i], "xml"); + + if (!cap || virHashAddEntry(capslatest, archs[i], cap) < 0) + return EXIT_FAILURE; + + VIR_TEST_VERBOSE("latest caps for %s: %s\n", archs[i], cap); + } + + VIR_TEST_VERBOSE("\n"); + + virFileWrapperAddPrefix(SYSCONFDIR "/qemu/firmware", + abs_srcdir "/qemufirmwaredata/etc/qemu/firmware"); + virFileWrapperAddPrefix(PREFIX "/share/qemu/firmware", + abs_srcdir "/qemufirmwaredata/usr/share/qemu/firmware"); + virFileWrapperAddPrefix("/home/user/.config/qemu/firmware", + abs_srcdir "/qemufirmwaredata/home/user/.config/qemu/firmware"); + +/** + * The following set of macros allows testing of JSON -> argv conversion with a + * real set of capabilities gathered from a real qemu copy. It is desired to use + * these for positive test cases as it provides combinations of flags which + * can be met in real life. + * + * The capabilities are taken from the real capabilities stored in + * tests/qemucapabilitiesdata. + * + * It is suggested to use the DO_TEST_CAPS_LATEST macro which always takes the + * most recent capability set. In cases when the new code would change behaviour + * the test cases should be forked using DO_TEST_CAPS_VER with the appropriate + * version. + */ +# define DO_TEST_INTERNAL(_name, _suffix, ...) \ + do { \ + static struct testInfo info = { \ + .name = _name, \ + .suffix = _suffix, \ + }; \ + if (testInfoSetArgs(&info, capslatest, \ + __VA_ARGS__, ARG_END) < 0) \ + return EXIT_FAILURE; \ + if (virTestRun("QEMU JSON-2-ARGV " _name _suffix, \ + testCompareJSONToArgv, &info) < 0) \ + ret = -1; \ + testInfoClear(&info); \ + } while (0) + +# define DO_TEST_CAPS_INTERNAL(name, arch, ver, ...) \ + DO_TEST_INTERNAL(name, "." arch "-" ver, \ + ARG_CAPS_ARCH, arch, \ + ARG_CAPS_VER, ver, \ + __VA_ARGS__) + +# define DO_TEST_CAPS_ARCH_VER(name, arch, ver) \ + DO_TEST_CAPS_INTERNAL(name, arch, ver, ARG_END) + +# define DO_TEST_CAPS_VER(name, ver) \ + DO_TEST_CAPS_ARCH_VER(name, "x86_64", ver) + +# define DO_TEST_CAPS_ARCH_LATEST_FULL(name, arch, ...) \ + DO_TEST_CAPS_INTERNAL(name, arch, "latest", __VA_ARGS__) + +# define DO_TEST_CAPS_ARCH_LATEST(name, arch) \ + DO_TEST_CAPS_ARCH_LATEST_FULL(name, arch, ARG_END) + +# define DO_TEST_CAPS_LATEST(name) \ + DO_TEST_CAPS_ARCH_LATEST(name, "x86_64") + +# define DO_TEST_CAPS_LATEST_FAILURE(name) \ + DO_TEST_CAPS_ARCH_LATEST_FULL(name, "x86_64", \ + ARG_FLAGS, FLAG_EXPECT_FAILURE) + +# define DO_TEST_CAPS_LATEST_PARSE_ERROR(name) \ + DO_TEST_CAPS_ARCH_LATEST_FULL(name, "x86_64", \ + ARG_FLAGS, FLAG_EXPECT_PARSE_ERROR) + + +# define DO_TEST_FULL(name, ...) \ + DO_TEST_INTERNAL(name, "", \ + __VA_ARGS__, QEMU_CAPS_LAST) + +/* All the following macros require an explicit QEMU_CAPS_* list + * at the end of the argument list, or the NONE placeholder. + * */ +# define DO_TEST(name, ...) \ + DO_TEST_FULL(name, \ + ARG_QEMU_CAPS, __VA_ARGS__) + +# define DO_TEST_GIC(name, gic, ...) \ + DO_TEST_FULL(name, \ + ARG_GIC, gic, \ + ARG_QEMU_CAPS, __VA_ARGS__) + +# define DO_TEST_FAILURE(name, ...) \ + DO_TEST_FULL(name, \ + ARG_FLAGS, FLAG_EXPECT_FAILURE, \ + ARG_QEMU_CAPS, __VA_ARGS__) + +# define DO_TEST_PARSE_ERROR(name, ...) \ + DO_TEST_FULL(name, \ + ARG_FLAGS, FLAG_EXPECT_PARSE_ERROR | FLAG_EXPECT_FAILURE, \ + ARG_QEMU_CAPS, __VA_ARGS__) + +# define NONE QEMU_CAPS_LAST + + /* Unset or set all envvars here that are copied in qemudBuildCommandLine + * using ADD_ENV_COPY, otherwise these tests may fail due to unexpected + * values for these envvars */ + setenv("PATH", "/bin", 1); + setenv("USER", "test", 1); + setenv("LOGNAME", "test", 1); + setenv("HOME", "/home/test", 1); + unsetenv("TMPDIR"); + unsetenv("LD_PRELOAD"); + unsetenv("LD_LIBRARY_PATH"); + unsetenv("QEMU_AUDIO_DRV"); + unsetenv("SDL_AUDIODRIVER"); + + DO_TEST_CAPS_LATEST("tiny"); + + if (getenv("LIBVIRT_SKIP_CLEANUP") == NULL) + virFileDeleteTree(fakerootdir); + + VIR_FREE(driver.config->nbdTLSx509certdir); + qemuTestDriverFree(&driver); + VIR_FREE(fakerootdir); + virHashFree(capslatest); + virFileWrapperClearPrefixes(); + + return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} + +VIR_TEST_MAIN_PRELOAD(mymain, + abs_builddir "/.libs/qemuxml2argvmock.so", + abs_builddir "/.libs/virrandommock.so", + abs_builddir "/.libs/qemucpumock.so", + abs_builddir "/.libs/virpcimock.so") + +#else + +int main(void) +{ + return EXIT_AM_SKIP; +} + +#endif /* WITH_QEMU */ -- 2.20.1

Without this tweak, the script will generate functions and code that look like virCromulentJsonFrobnicate() instead of the expected virCromulentJSONFrobnicate(), thus making it impossible to integrate the automatically generated functions with the hand-crafted ones. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- src/rpc/gendispatch.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rpc/gendispatch.pl b/src/rpc/gendispatch.pl index ae3a42c4c1..3708d56810 100755 --- a/src/rpc/gendispatch.pl +++ b/src/rpc/gendispatch.pl @@ -74,6 +74,7 @@ sub fixup_name { $name =~ s/Scsi/SCSI/; $name =~ s/Wwn$/WWN/; $name =~ s/Dhcp$/DHCP/; + $name =~ s/Json/JSON/; return $name; } -- 2.20.1

This will be the entry point to JSON-based APIs for all non-internal users. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- include/libvirt/libvirt-domain.h | 3 +++ src/driver-hypervisor.h | 6 +++++ src/libvirt-domain.c | 45 ++++++++++++++++++++++++++++++++ src/libvirt_public.syms | 1 + 4 files changed, 55 insertions(+) diff --git a/include/libvirt/libvirt-domain.h b/include/libvirt/libvirt-domain.h index 7d36820b5a..193c9ef24f 100644 --- a/include/libvirt/libvirt-domain.h +++ b/include/libvirt/libvirt-domain.h @@ -1777,6 +1777,9 @@ virDomainPtr virDomainDefineXML (virConnectPtr conn, virDomainPtr virDomainDefineXMLFlags (virConnectPtr conn, const char *xml, unsigned int flags); +virDomainPtr virDomainDefineJSONFlags (virConnectPtr conn, + const char *json, + unsigned int flags); int virDomainUndefine (virDomainPtr domain); typedef enum { diff --git a/src/driver-hypervisor.h b/src/driver-hypervisor.h index 5315e33dde..4b2a0ca443 100644 --- a/src/driver-hypervisor.h +++ b/src/driver-hypervisor.h @@ -1328,6 +1328,11 @@ typedef int int *nparams, unsigned int flags); +typedef virDomainPtr +(*virDrvDomainDefineJSONFlags)(virConnectPtr conn, + const char *json, + unsigned int flags); + typedef struct _virHypervisorDriver virHypervisorDriver; typedef virHypervisorDriver *virHypervisorDriverPtr; @@ -1580,6 +1585,7 @@ struct _virHypervisorDriver { virDrvConnectBaselineHypervisorCPU connectBaselineHypervisorCPU; virDrvNodeGetSEVInfo nodeGetSEVInfo; virDrvDomainGetLaunchSecurityInfo domainGetLaunchSecurityInfo; + virDrvDomainDefineJSONFlags domainDefineJSONFlags; }; diff --git a/src/libvirt-domain.c b/src/libvirt-domain.c index be5b1f6740..7c2fcd9f09 100644 --- a/src/libvirt-domain.c +++ b/src/libvirt-domain.c @@ -6217,6 +6217,51 @@ virDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flags) } +/** + * virDomainDefineJSONFlags: + * @conn: pointer to the hypervisor connection + * @json: the JSON description for the domain, preferably in UTF-8 + * @flags: bitwise OR of the virDomainDefineFlags constants + * + * Defines a domain, but does not start it. + * This definition is persistent, until explicitly undefined with + * virDomainUndefine(). A previous definition for this domain would be + * overridden if it already exists. + * + * virDomainFree should be used to free the resources after the + * domain object is no longer needed. + * + * Returns NULL in case of error, a pointer to the domain otherwise + */ +virDomainPtr +virDomainDefineJSONFlags(virConnectPtr conn, + const char *json, + unsigned int flags) +{ + VIR_DEBUG("conn=%p, json=%s flags=0x%x", conn, NULLSTR(json), flags); + + virResetLastError(); + + virCheckConnectReturn(conn, NULL); + virCheckReadOnlyGoto(conn->flags, error); + virCheckNonNullArgGoto(json, error); + + if (conn->driver->domainDefineJSONFlags) { + virDomainPtr ret; + ret = conn->driver->domainDefineJSONFlags(conn, json, flags); + if (!ret) + goto error; + return ret; + } + + virReportUnsupportedError(); + + error: + virDispatchError(conn); + return NULL; +} + + /** * virDomainUndefine: * @domain: pointer to a defined domain diff --git a/src/libvirt_public.syms b/src/libvirt_public.syms index dbce3336d5..fe63b51cdb 100644 --- a/src/libvirt_public.syms +++ b/src/libvirt_public.syms @@ -817,6 +817,7 @@ LIBVIRT_4.10.0 { LIBVIRT_5.2.0 { global: virConnectGetStoragePoolCapabilities; + virDomainDefineJSONFlags; } LIBVIRT_4.10.0; # .... define new API here using predicted next version number .... -- 2.20.1

This allows us to use virDomainDefineJSONFlags() when connected to a remote hypervisor. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- src/remote/remote_driver.c | 3 ++- src/remote/remote_protocol.x | 19 ++++++++++++++++++- src/remote_protocol-structs | 8 ++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c index 5c4dd41227..5c22d139d7 100644 --- a/src/remote/remote_driver.c +++ b/src/remote/remote_driver.c @@ -8516,7 +8516,8 @@ static virHypervisorDriver hypervisor_driver = { .connectCompareHypervisorCPU = remoteConnectCompareHypervisorCPU, /* 4.4.0 */ .connectBaselineHypervisorCPU = remoteConnectBaselineHypervisorCPU, /* 4.4.0 */ .nodeGetSEVInfo = remoteNodeGetSEVInfo, /* 4.5.0 */ - .domainGetLaunchSecurityInfo = remoteDomainGetLaunchSecurityInfo /* 4.5.0 */ + .domainGetLaunchSecurityInfo = remoteDomainGetLaunchSecurityInfo, /* 4.5.0 */ + .domainDefineJSONFlags = remoteDomainDefineJSONFlags, /* 5.2.0 */ }; static virNetworkDriver network_driver = { diff --git a/src/remote/remote_protocol.x b/src/remote/remote_protocol.x index 74be4b37d0..93bd916a7d 100644 --- a/src/remote/remote_protocol.x +++ b/src/remote/remote_protocol.x @@ -1106,6 +1106,15 @@ struct remote_domain_define_xml_flags_ret { remote_nonnull_domain dom; }; +struct remote_domain_define_json_flags_args { + remote_nonnull_string json; + unsigned int flags; +}; + +struct remote_domain_define_json_flags_ret { + remote_nonnull_domain dom; +}; + struct remote_domain_undefine_args { remote_nonnull_domain dom; }; @@ -6342,5 +6351,13 @@ enum remote_procedure { * @generate: both * @acl: connect:read */ - REMOTE_PROC_CONNECT_GET_STORAGE_POOL_CAPABILITIES = 403 + REMOTE_PROC_CONNECT_GET_STORAGE_POOL_CAPABILITIES = 403, + + /** + * @priority: high + * @generate: both + * @acl: domain:write + * @acl: domain:save + */ + REMOTE_PROC_DOMAIN_DEFINE_JSON_FLAGS = 404 }; diff --git a/src/remote_protocol-structs b/src/remote_protocol-structs index 768189c573..083e3f1e0d 100644 --- a/src/remote_protocol-structs +++ b/src/remote_protocol-structs @@ -715,6 +715,13 @@ struct remote_domain_define_xml_flags_args { struct remote_domain_define_xml_flags_ret { remote_nonnull_domain dom; }; +struct remote_domain_define_json_flags_args { + remote_nonnull_string json; + u_int flags; +}; +struct remote_domain_define_json_flags_ret { + remote_nonnull_domain dom; +}; struct remote_domain_undefine_args { remote_nonnull_domain dom; }; @@ -3385,4 +3392,5 @@ enum remote_procedure { REMOTE_PROC_CONNECT_LIST_ALL_NWFILTER_BINDINGS = 401, REMOTE_PROC_DOMAIN_SET_IOTHREAD_PARAMS = 402, REMOTE_PROC_CONNECT_GET_STORAGE_POOL_CAPABILITIES = 403, + REMOTE_PROC_DOMAIN_DEFINE_JSON_FLAGS = 404, }; -- 2.20.1

This allows us to use virDomainDefineJSONFlags() when connected to a QEMU-based hypervisor. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- src/qemu/qemu_driver.c | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 62d8d977c5..d973574290 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -7720,6 +7720,89 @@ qemuDomainDefineXML(virConnectPtr conn, const char *xml) return qemuDomainDefineXMLFlags(conn, xml, 0); } +static virDomainPtr +qemuDomainDefineJSONFlags(virConnectPtr conn, + const char *json, + unsigned int flags) +{ + virQEMUDriverPtr driver = conn->privateData; + virDomainDefPtr def = NULL; + virDomainDefPtr oldDef = NULL; + virDomainObjPtr vm = NULL; + virDomainPtr dom = NULL; + virObjectEventPtr event = NULL; + virQEMUDriverConfigPtr cfg; + virCapsPtr caps = NULL; + unsigned int parse_flags = VIR_DOMAIN_DEF_PARSE_INACTIVE | + VIR_DOMAIN_DEF_PARSE_ABI_UPDATE; + + virCheckFlags(VIR_DOMAIN_DEFINE_VALIDATE, NULL); + + if (flags & VIR_DOMAIN_DEFINE_VALIDATE) + parse_flags |= VIR_DOMAIN_DEF_PARSE_VALIDATE_SCHEMA; + + cfg = virQEMUDriverGetConfig(driver); + + if (!(caps = virQEMUDriverGetCapabilities(driver, false))) + goto cleanup; + + if (!(def = virDomainDefParseJSONString(json, caps, driver->xmlopt, + NULL, parse_flags))) + goto cleanup; + + if (virXMLCheckIllegalChars("name", def->name, "\n") < 0) + goto cleanup; + + if (virDomainDefineJSONFlagsEnsureACL(conn, def) < 0) + goto cleanup; + + if (!(vm = virDomainObjListAdd(driver->domains, def, + driver->xmlopt, + 0, &oldDef))) + goto cleanup; + def = NULL; + + vm->persistent = 1; + + if (virDomainSaveConfig(cfg->configDir, driver->caps, + vm->newDef ? vm->newDef : vm->def) < 0) { + if (oldDef) { + /* There is backup so this VM was defined before. + * Just restore the backup. */ + VIR_INFO("Restoring domain '%s' definition", vm->def->name); + if (virDomainObjIsActive(vm)) + vm->newDef = oldDef; + else + vm->def = oldDef; + oldDef = NULL; + } else { + /* Brand new domain. Remove it */ + VIR_INFO("Deleting domain '%s'", vm->def->name); + vm->persistent = 0; + qemuDomainRemoveInactiveJob(driver, vm); + } + goto cleanup; + } + + event = virDomainEventLifecycleNewFromObj(vm, + VIR_DOMAIN_EVENT_DEFINED, + !oldDef ? + VIR_DOMAIN_EVENT_DEFINED_ADDED : + VIR_DOMAIN_EVENT_DEFINED_UPDATED); + + VIR_INFO("Creating domain '%s'", vm->def->name); + dom = virGetDomain(conn, vm->def->name, vm->def->uuid, vm->def->id); + + cleanup: + virDomainDefFree(oldDef); + virDomainDefFree(def); + virDomainObjEndAPI(&vm); + virObjectEventStateQueue(driver->domainEventState, event); + virObjectUnref(caps); + virObjectUnref(cfg); + return dom; +} + static int qemuDomainUndefineFlags(virDomainPtr dom, unsigned int flags) @@ -22559,6 +22642,7 @@ static virHypervisorDriver qemuHypervisorDriver = { .connectBaselineHypervisorCPU = qemuConnectBaselineHypervisorCPU, /* 4.4.0 */ .nodeGetSEVInfo = qemuNodeGetSEVInfo, /* 4.5.0 */ .domainGetLaunchSecurityInfo = qemuDomainGetLaunchSecurityInfo, /* 4.5.0 */ + .domainDefineJSONFlags = qemuDomainDefineJSONFlags, /* 5.2.0 */ }; -- 2.20.1

In order not to break user expectations (and scripts!) we need to keep parsing input files as XML by default, so JSON support is implemented as opt-in through the new --json option; a brand new --xml option is also added for consistency. That said, if the name of the input file ends in .json it's fair to assume the contents are not going to be in XML format: we can use this heuristic to make virsh more user-friendly by not requiring the user to specify --json in this single case. Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- tools/virsh-domain.c | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/tools/virsh-domain.c b/tools/virsh-domain.c index afcd0a5f8d..0ebea3fe13 100644 --- a/tools/virsh-domain.c +++ b/tools/virsh-domain.c @@ -8208,6 +8208,14 @@ static const vshCmdOptDef opts_define[] = { .type = VSH_OT_BOOL, .help = N_("validate the XML against the schema") }, + {.name = "xml", + .type = VSH_OT_BOOL, + .help = N_("Input file is in XML format") + }, + {.name = "json", + .type = VSH_OT_BOOL, + .help = N_("Input file is in JSON format") + }, {.name = NULL} }; @@ -8220,6 +8228,9 @@ cmdDefine(vshControl *ctl, const vshCmd *cmd) char *buffer; unsigned int flags = 0; virshControlPtr priv = ctl->privData; + bool xmlInput; + + VSH_EXCLUSIVE_OPTIONS("xml", "json"); if (vshCommandOptStringReq(ctl, cmd, "file", &from) < 0) return false; @@ -8227,13 +8238,30 @@ cmdDefine(vshControl *ctl, const vshCmd *cmd) if (vshCommandOptBool(cmd, "validate")) flags |= VIR_DOMAIN_DEFINE_VALIDATE; + /* If the input filename ends with .json, it's fair to assume that + * the format is going to be JSON. We default to XML otherwise */ + xmlInput = true; + if (virStringHasCaseSuffix(from, ".json")) + xmlInput = false; + + /* User asked for a specific input format: override both defaults + * and automatic detection */ + if (vshCommandOptBool(cmd, "xml")) + xmlInput = true; + else if (vshCommandOptBool(cmd, "json")) + xmlInput = false; + if (virFileReadAll(from, VSH_MAX_XML_FILE, &buffer) < 0) return false; - if (flags) - dom = virDomainDefineXMLFlags(priv->conn, buffer, flags); - else - dom = virDomainDefineXML(priv->conn, buffer); + if (xmlInput) { + if (flags) + dom = virDomainDefineXMLFlags(priv->conn, buffer, flags); + else + dom = virDomainDefineXML(priv->conn, buffer); + } else { + dom = virDomainDefineJSONFlags(priv->conn, buffer, flags); + } VIR_FREE(buffer); if (dom != NULL) { -- 2.20.1

Signed-off-by: Andrea Bolognani <abologna@redhat.com> --- docs/news.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/news.xml b/docs/news.xml index fcf8520132..76e8c90bf4 100644 --- a/docs/news.xml +++ b/docs/news.xml @@ -116,6 +116,17 @@ unable to saturate the network link. </description> </change> + <change> + <summary> + Introduce the virDomainDefineJSONFlags() public API + </summary> + <description> + This new public API is very similar in scope and use to the existing + <code>virDomainDefineXMLFlags()</code> API, but it expects its input + to be in JSON format rather than XML. Note that support for many + features of the XML-based API is still missing at the moment. + </description> + </change> </section> <section title="Removed features"> <change> -- 2.20.1
participants (1)
-
Andrea Bolognani