libvirt/tests/testutilsqemuschema.c
Peter Krempa b17fd211e2 testQEMUSchemaValidateCommand: Add possibility for partial QMP validation
The QMP schema for 'device_add' is not complete yet. Allow validation of
incomplete schema so that we can enable at least some validation. Once
there's more schema in the future all present members are still
validated.

Signed-off-by: Peter Krempa <pkrempa@redhat.com>
Reviewed-by: Ján Tomko <jtomko@redhat.com>
2021-10-18 14:00:58 +02:00

817 lines
25 KiB
C

/*
* testutilsqemuschema.c: helper functions for QEMU QAPI schema testing
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see
* <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include "testutils.h"
#include "testutilsqemu.h"
#include "testutilsqemuschema.h"
#include "qemu/qemu_qapi.h"
struct testQEMUSchemaValidateCtxt {
GHashTable *schema;
virBuffer *debug;
bool allowDeprecated;
bool allowIncomplete; /* allow members not (yet) covered by the schema */
};
static int
testQEMUSchemaValidateRecurse(virJSONValue *obj,
virJSONValue *root,
struct testQEMUSchemaValidateCtxt *ctxt);
static int
testQEMUSchemaValidateBuiltin(virJSONValue *obj,
virJSONValue *root,
struct testQEMUSchemaValidateCtxt *ctxt)
{
const char *t = virJSONValueObjectGetString(root, "json-type");
const char *s = NULL;
bool b = false;
int ret = -1;
if (STREQ_NULLABLE(t, "value")) {
s = "{any}";
ret = 0;
goto cleanup;
}
switch (virJSONValueGetType(obj)) {
case VIR_JSON_TYPE_STRING:
if (STRNEQ_NULLABLE(t, "string"))
goto cleanup;
s = virJSONValueGetString(obj);
break;
case VIR_JSON_TYPE_NUMBER:
if (STRNEQ_NULLABLE(t, "int") &&
STRNEQ_NULLABLE(t, "number"))
goto cleanup;
s = "{number}";
break;
case VIR_JSON_TYPE_BOOLEAN:
if (STRNEQ_NULLABLE(t, "boolean"))
goto cleanup;
virJSONValueGetBoolean(obj, &b);
if (b)
s = "true";
else
s = "false";
break;
case VIR_JSON_TYPE_NULL:
if (STRNEQ_NULLABLE(t, "null"))
goto cleanup;
break;
case VIR_JSON_TYPE_OBJECT:
case VIR_JSON_TYPE_ARRAY:
goto cleanup;
}
ret = 0;
cleanup:
if (ret == 0)
virBufferAsprintf(ctxt->debug, "'%s': OK", s);
else
virBufferAsprintf(ctxt->debug, "ERROR: expected type '%s', actual type %d",
t, virJSONValueGetType(obj));
return ret;
}
struct testQEMUSchemaValidateObjectMemberData {
virJSONValue *rootmembers;
struct testQEMUSchemaValidateCtxt *ctxt;
bool missingMandatory;
};
static virJSONValue *
testQEMUSchemaStealObjectMemberByName(const char *name,
virJSONValue *members)
{
virJSONValue *member;
virJSONValue *ret = NULL;
size_t i;
for (i = 0; i < virJSONValueArraySize(members); i++) {
member = virJSONValueArrayGet(members, i);
if (STREQ_NULLABLE(name, virJSONValueObjectGetString(member, "name"))) {
ret = virJSONValueArraySteal(members, i);
break;
}
}
return ret;
}
static int
testQEMUSchemaValidateObjectMember(const char *key,
virJSONValue *value,
void *opaque)
{
struct testQEMUSchemaValidateObjectMemberData *data = opaque;
g_autoptr(virJSONValue) keymember = NULL;
const char *keytype;
virJSONValue *keyschema = NULL;
int rc;
virBufferStrcat(data->ctxt->debug, key, ": ", NULL);
/* lookup 'member' entry for key */
if (!(keymember = testQEMUSchemaStealObjectMemberByName(key, data->rootmembers))) {
if (data->ctxt->allowIncomplete) {
virBufferAddLit(data->ctxt->debug, " schema missing - OK(waived)\n");
return 0;
}
virBufferAddLit(data->ctxt->debug, "ERROR: attribute not in schema\n");
return -1;
}
/* lookup schema entry for keytype */
if (!(keytype = virJSONValueObjectGetString(keymember, "type")) ||
!(keyschema = virHashLookup(data->ctxt->schema, keytype))) {
virBufferAsprintf(data->ctxt->debug, "ERROR: can't find schema for type '%s'\n",
NULLSTR(keytype));
return -2;
}
/* recurse */
rc = testQEMUSchemaValidateRecurse(value, keyschema, data->ctxt);
virBufferAddLit(data->ctxt->debug, "\n");
return rc;
}
static int
testQEMUSchemaValidateObjectMergeVariantMember(size_t pos G_GNUC_UNUSED,
virJSONValue *item,
void *opaque)
{
virJSONValue *array = opaque;
g_autoptr(virJSONValue) copy = NULL;
if (!(copy = virJSONValueCopy(item)))
return -1;
if (virJSONValueArrayAppend(array, &copy) < 0)
return -1;
return 1;
}
/**
* testQEMUSchemaValidateObjectMergeVariant:
*
* Merges schema of variant @variantname in @root into @root and removes the
* 'variants' array from @root.
*/
static int
testQEMUSchemaValidateObjectMergeVariant(virJSONValue *root,
const char *variantfield,
const char *variantname,
struct testQEMUSchemaValidateCtxt *ctxt)
{
size_t i;
g_autoptr(virJSONValue) variants = NULL;
virJSONValue *variant;
virJSONValue *variantschema;
virJSONValue *variantschemamembers;
virJSONValue *rootmembers;
const char *varianttype = NULL;
if (!(variants = virJSONValueObjectStealArray(root, "variants"))) {
virBufferAddLit(ctxt->debug, "ERROR: missing 'variants' in schema\n");
return -2;
}
for (i = 0; i < virJSONValueArraySize(variants); i++) {
variant = virJSONValueArrayGet(variants, i);
if (STREQ_NULLABLE(variantname,
virJSONValueObjectGetString(variant, "case"))) {
varianttype = virJSONValueObjectGetString(variant, "type");
break;
}
}
if (!varianttype) {
virBufferAsprintf(ctxt->debug, "ERROR: variant '%s' for discriminator '%s' not found\n",
variantname, variantfield);
return -1;
}
if (!(variantschema = virHashLookup(ctxt->schema, varianttype)) ||
!(variantschemamembers = virJSONValueObjectGetArray(variantschema, "members"))) {
virBufferAsprintf(ctxt->debug,
"ERROR: missing schema or schema members for variant '%s'(%s)\n",
variantname, varianttype);
return -2;
}
rootmembers = virJSONValueObjectGetArray(root, "members");
if (virJSONValueArrayForeachSteal(variantschemamembers,
testQEMUSchemaValidateObjectMergeVariantMember,
rootmembers) < 0) {
return -2;
}
return 0;
}
static int
testQEMUSchemaValidateObjectMandatoryMember(size_t pos G_GNUC_UNUSED,
virJSONValue *item,
void *opaque G_GNUC_UNUSED)
{
struct testQEMUSchemaValidateObjectMemberData *data = opaque;
if (virJSONValueObjectHasKey(item, "default") != 1) {
virBufferAsprintf(data->ctxt->debug, "ERROR: missing mandatory attribute '%s'\n",
NULLSTR(virJSONValueObjectGetString(item, "name")));
data->missingMandatory = true;
}
return 1;
}
static int
testQEMUSchemaValidateObject(virJSONValue *obj,
virJSONValue *root,
struct testQEMUSchemaValidateCtxt *ctxt)
{
struct testQEMUSchemaValidateObjectMemberData data = { NULL, ctxt, false };
g_autoptr(virJSONValue) localroot = NULL;
const char *variantfield;
const char *variantname;
if (virJSONValueGetType(obj) != VIR_JSON_TYPE_OBJECT) {
virBufferAddLit(ctxt->debug, "ERROR: not an object");
return -1;
}
virBufferAddLit(ctxt->debug, "{\n");
virBufferAdjustIndent(ctxt->debug, 3);
/* copy schema */
if (!(localroot = virJSONValueCopy(root)))
return -2;
/* remove variant */
if ((variantfield = virJSONValueObjectGetString(localroot, "tag"))) {
if (!(variantname = virJSONValueObjectGetString(obj, variantfield))) {
virBufferAsprintf(ctxt->debug, "ERROR: missing variant discriminator attribute '%s'\n",
variantfield);
return -1;
}
if (testQEMUSchemaValidateObjectMergeVariant(localroot, variantfield,
variantname, ctxt) < 0)
return -1;
}
/* validate members */
data.rootmembers = virJSONValueObjectGetArray(localroot, "members");
if (virJSONValueObjectForeachKeyValue(obj,
testQEMUSchemaValidateObjectMember,
&data) < 0)
return -1;
/* check missing mandatory values */
if (virJSONValueArrayForeachSteal(data.rootmembers,
testQEMUSchemaValidateObjectMandatoryMember,
&data) < 0) {
return -2;
}
if (data.missingMandatory)
return -1;
virBufferAdjustIndent(ctxt->debug, -3);
virBufferAddLit(ctxt->debug, "} OK");
return 0;
}
static int
testQEMUSchemaValidateEnum(virJSONValue *obj,
virJSONValue *root,
struct testQEMUSchemaValidateCtxt *ctxt)
{
const char *objstr;
virJSONValue *values = NULL;
virJSONValue *value;
size_t i;
if (virJSONValueGetType(obj) != VIR_JSON_TYPE_STRING) {
virBufferAddLit(ctxt->debug, "ERROR: not a string");
return -1;
}
objstr = virJSONValueGetString(obj);
if (!(values = virJSONValueObjectGetArray(root, "values"))) {
virBufferAsprintf(ctxt->debug, "ERROR: missing enum values in schema '%s'",
NULLSTR(virJSONValueObjectGetString(root, "name")));
return -2;
}
for (i = 0; i < virJSONValueArraySize(values); i++) {
value = virJSONValueArrayGet(values, i);
if (STREQ_NULLABLE(objstr, virJSONValueGetString(value))) {
virBufferAsprintf(ctxt->debug, "'%s' OK", NULLSTR(objstr));
return 0;
}
}
virBufferAsprintf(ctxt->debug, "ERROR: enum value '%s' is not in schema",
NULLSTR(objstr));
return -1;
}
static int
testQEMUSchemaValidateArray(virJSONValue *objs,
virJSONValue *root,
struct testQEMUSchemaValidateCtxt *ctxt)
{
const char *elemtypename = virJSONValueObjectGetString(root, "element-type");
virJSONValue *elementschema;
virJSONValue *obj;
size_t i;
if (virJSONValueGetType(objs) != VIR_JSON_TYPE_ARRAY) {
virBufferAddLit(ctxt->debug, "ERROR: not an array\n");
return -1;
}
if (!elemtypename ||
!(elementschema = virHashLookup(ctxt->schema, elemtypename))) {
virBufferAsprintf(ctxt->debug, "ERROR: missing schema for array element type '%s'",
NULLSTR(elemtypename));
return -2;
}
virBufferAddLit(ctxt->debug, "[\n");
virBufferAdjustIndent(ctxt->debug, 3);
for (i = 0; i < virJSONValueArraySize(objs); i++) {
obj = virJSONValueArrayGet(objs, i);
if (testQEMUSchemaValidateRecurse(obj, elementschema, ctxt) < 0)
return -1;
virBufferAddLit(ctxt->debug, ",\n");
}
virBufferAddLit(ctxt->debug, "] OK");
virBufferAdjustIndent(ctxt->debug, -3);
return 0;
}
static int
testQEMUSchemaValidateAlternate(virJSONValue *obj,
virJSONValue *root,
struct testQEMUSchemaValidateCtxt *ctxt)
{
virJSONValue *members;
virJSONValue *member;
size_t i;
size_t n;
const char *membertype;
virJSONValue *memberschema;
int indent;
int rc;
if (!(members = virJSONValueObjectGetArray(root, "members"))) {
virBufferAddLit(ctxt->debug, "ERROR: missing 'members' for alternate schema");
return -2;
}
virBufferAddLit(ctxt->debug, "(\n");
virBufferAdjustIndent(ctxt->debug, 3);
indent = virBufferGetIndent(ctxt->debug);
n = virJSONValueArraySize(members);
for (i = 0; i < n; i++) {
membertype = NULL;
/* P != NP */
virBufferAsprintf(ctxt->debug, "(alternate %zu/%zu)\n", i + 1, n);
virBufferAdjustIndent(ctxt->debug, 3);
if (!(member = virJSONValueArrayGet(members, i)) ||
!(membertype = virJSONValueObjectGetString(member, "type")) ||
!(memberschema = virHashLookup(ctxt->schema, membertype))) {
virBufferAsprintf(ctxt->debug, "ERROR: missing schema for alternate type '%s'",
NULLSTR(membertype));
return -2;
}
rc = testQEMUSchemaValidateRecurse(obj, memberschema, ctxt);
virBufferAddLit(ctxt->debug, "\n");
virBufferSetIndent(ctxt->debug, indent);
virBufferAsprintf(ctxt->debug, "(/alternate %zu/%zu)\n", i + 1, n);
if (rc == 0) {
virBufferAdjustIndent(ctxt->debug, -3);
virBufferAddLit(ctxt->debug, ") OK");
return 0;
}
}
virBufferAddLit(ctxt->debug, "ERROR: no alternate type was matched");
return -1;
}
static int
testQEMUSchemaValidateDeprecated(virJSONValue *root,
const char *name,
struct testQEMUSchemaValidateCtxt *ctxt)
{
virJSONValue *features = virJSONValueObjectGetArray(root, "features");
size_t nfeatures;
size_t i;
if (!features)
return 0;
nfeatures = virJSONValueArraySize(features);
for (i = 0; i < nfeatures; i++) {
virJSONValue *cur = virJSONValueArrayGet(features, i);
const char *curstr;
if (!cur ||
!(curstr = virJSONValueGetString(cur))) {
virBufferAsprintf(ctxt->debug, "ERROR: features of '%s' are malformed", name);
return -2;
}
if (STREQ(curstr, "deprecated")) {
if (ctxt->allowDeprecated) {
virBufferAsprintf(ctxt->debug, "WARNING: '%s' is deprecated", name);
if (virTestGetVerbose())
g_fprintf(stderr, "\nWARNING: '%s' is deprecated\n", name);
return 0;
} else {
virBufferAsprintf(ctxt->debug, "ERROR: '%s' is deprecated", name);
return -1;
}
}
}
return 0;
}
static int
testQEMUSchemaValidateRecurse(virJSONValue *obj,
virJSONValue *root,
struct testQEMUSchemaValidateCtxt *ctxt)
{
const char *n = virJSONValueObjectGetString(root, "name");
const char *t = virJSONValueObjectGetString(root, "meta-type");
int rc;
if ((rc = testQEMUSchemaValidateDeprecated(root, n, ctxt)) < 0)
return rc;
if (STREQ_NULLABLE(t, "builtin")) {
return testQEMUSchemaValidateBuiltin(obj, root, ctxt);
} else if (STREQ_NULLABLE(t, "object")) {
return testQEMUSchemaValidateObject(obj, root, ctxt);
} else if (STREQ_NULLABLE(t, "enum")) {
return testQEMUSchemaValidateEnum(obj, root, ctxt);
} else if (STREQ_NULLABLE(t, "array")) {
return testQEMUSchemaValidateArray(obj, root, ctxt);
} else if (STREQ_NULLABLE(t, "alternate")) {
return testQEMUSchemaValidateAlternate(obj, root, ctxt);
}
virBufferAsprintf(ctxt->debug,
"qapi schema meta-type '%s' of type '%s' not handled\n",
NULLSTR(t), NULLSTR(n));
return -2;
}
/**
* testQEMUSchemaValidate:
* @obj: object to validate
* @root: schema entry to start from
* @schema: hash table containing schema entries
* @debug: a virBuffer which will be filled with debug information if provided
*
* Validates whether @obj conforms to the QAPI schema passed in via @schema,
* starting from the node @root. Returns 0, if @obj matches @schema, -1 if it
* does not and -2 if there is a problem with the schema or with internals.
*
* @debug is filled with information regarding the validation process
*/
int
testQEMUSchemaValidate(virJSONValue *obj,
virJSONValue *root,
GHashTable *schema,
bool allowDeprecated,
virBuffer *debug)
{
struct testQEMUSchemaValidateCtxt ctxt = { .schema = schema,
.debug = debug,
.allowDeprecated = allowDeprecated };
return testQEMUSchemaValidateRecurse(obj, root, &ctxt);
}
/**
* testQEMUSchemaValidateCommand:
* @command: command to validate
* @arguments: arguments of @command to validate
* @schema: hash table containing schema entries
* @allowDeprecated: don't fails schema validation if @command or one of @arguments
* is deprecated
* @allowRemoved: skip validation fully if @command was not found
* @allowIncomplete: don't fail validation if members not covered by schema are present
* (for waiving commands with incomplete schema)
* @debug: a virBuffer which will be filled with debug information if provided
*
* Validates whether @command and its @arguments conform to the QAPI schema
* passed in via @schema. Returns 0, if the command and args match @schema,
* -1 if it does not and -2 if there is a problem with the schema or with
* internals.
*
* @allowRemoved should generally be used only if it's certain that there's a
* replacement of @command in place.
*
* @debug is filled with information regarding the validation process
*/
int
testQEMUSchemaValidateCommand(const char *command,
virJSONValue *arguments,
GHashTable *schema,
bool allowDeprecated,
bool allowRemoved,
bool allowIncomplete,
virBuffer *debug)
{
struct testQEMUSchemaValidateCtxt ctxt = { .schema = schema,
.debug = debug,
.allowDeprecated = allowDeprecated,
.allowIncomplete = allowIncomplete };
g_autofree char *schemapatharguments = g_strdup_printf("%s/arg-type", command);
g_autoptr(virJSONValue) emptyargs = NULL;
virJSONValue *schemarootcommand;
virJSONValue *schemarootarguments;
int rc;
if (virQEMUQAPISchemaPathGet(command, schema, &schemarootcommand) < 0 ||
!schemarootcommand) {
if (allowRemoved)
return 0;
virBufferAsprintf(debug, "ERROR: command '%s' not found in the schema", command);
return -1;
}
if ((rc = testQEMUSchemaValidateDeprecated(schemarootcommand, command, &ctxt)) < 0)
return rc;
if (!arguments)
arguments = emptyargs = virJSONValueNewObject();
if (virQEMUQAPISchemaPathGet(schemapatharguments, schema, &schemarootarguments) < 0 ||
!schemarootarguments) {
virBufferAsprintf(debug, "ERROR: failed to look up 'arg-type' of '%s'", command);
return -1;
}
return testQEMUSchemaValidateRecurse(arguments, schemarootarguments, &ctxt);
}
/**
* testQEMUSchemaEntryMatchTemplate:
*
* @schemaentry: a JSON object representing a 'object' node in the QAPI schema
* ...: a NULL terminated list of strings representing the template of properties
* which the QMP object needs to have.
*
* The strings have following format:
*
* "type:name"
* "?type:name"
*
* "type" corresponds to the 'type' property of the member to check (str, bool, any ...)
* "name" corresponds to the name of the member to check
*
* If the query string starts with an '?' and member 'name' may be missing.
*
* This function matches that @schemaentry has all expected members and the
* members have expected types. @schemaentry also must not have any unknown
* members.
*/
int
testQEMUSchemaEntryMatchTemplate(virJSONValue *schemaentry,
...)
{
g_autoptr(virJSONValue) members = NULL;
va_list ap;
const char *next;
int ret = -1;
if (STRNEQ_NULLABLE(virJSONValueObjectGetString(schemaentry, "meta-type"), "object")) {
VIR_TEST_VERBOSE("schemaentry is not an object");
return -1;
}
if (!(members = virJSONValueCopy(virJSONValueObjectGetArray(schemaentry, "members")))) {
VIR_TEST_VERBOSE("failed to copy 'members'");
return -1;
}
va_start(ap, schemaentry);
/* pass 1 */
while ((next = va_arg(ap, const char *))) {
char modifier = *next;
g_autofree char *type = NULL;
char *name;
size_t i;
bool found = false;
bool optional = false;
if (!g_ascii_isalpha(modifier))
next++;
if (modifier == '?')
optional = true;
type = g_strdup(next);
if ((name = strchr(type, ':'))) {
*(name++) = '\0';
} else {
VIR_TEST_VERBOSE("malformed template string '%s'", next);
goto cleanup;
}
for (i = 0; i < virJSONValueArraySize(members); i++) {
virJSONValue *member = virJSONValueArrayGet(members, i);
const char *membername = virJSONValueObjectGetString(member, "name");
const char *membertype = virJSONValueObjectGetString(member, "type");
if (STRNEQ_NULLABLE(name, membername))
continue;
if (STRNEQ_NULLABLE(membertype, type)) {
VIR_TEST_VERBOSE("member '%s' is of unexpected type '%s' (expected '%s')",
NULLSTR(membername), NULLSTR(membertype), type);
goto cleanup;
}
found = true;
break;
}
if (found) {
virJSONValueFree(virJSONValueArraySteal(members, i));
} else {
if (!optional) {
VIR_TEST_VERBOSE("mandatory member '%s' not found", name);
goto cleanup;
}
}
}
/* pass 2 - check any unexpected members */
if (virJSONValueArraySize(members) > 0) {
size_t i;
for (i = 0; i < virJSONValueArraySize(members); i++) {
VIR_TEST_VERBOSE("unexpected member '%s'",
NULLSTR(virJSONValueObjectGetString(virJSONValueArrayGet(members, i), "name")));
}
goto cleanup;
}
ret = 0;
cleanup:
va_end(ap);
return ret;
}
static virJSONValue *
testQEMUSchemaLoadReplies(const char *filename)
{
g_autofree char *caps = NULL;
char *schemaReply;
char *end;
g_autoptr(virJSONValue) reply = NULL;
virJSONValue *schema = NULL;
if (virTestLoadFile(filename, &caps) < 0)
return NULL;
if (!(schemaReply = strstr(caps, "\"execute\": \"query-qmp-schema\"")) ||
!(schemaReply = strstr(schemaReply, "\n\n")) ||
!(end = strstr(schemaReply + 2, "\n\n"))) {
VIR_TEST_VERBOSE("failed to find reply to 'query-qmp-schema' in '%s'",
filename);
return NULL;
}
schemaReply += 2;
*end = '\0';
if (!(reply = virJSONValueFromString(schemaReply))) {
VIR_TEST_VERBOSE("failed to parse 'query-qmp-schema' reply from '%s'",
filename);
return NULL;
}
if (!(schema = virJSONValueObjectStealArray(reply, "return"))) {
VIR_TEST_VERBOSE("missing qapi schema data in reply in '%s'",
filename);
return NULL;
}
return schema;
}
/**
* testQEMUSchemaGetLatest:
*
* Returns the schema data as the qemu monitor would reply from the latest
* replies file used for qemucapabilitiestest for the x86_64 architecture.
*/
virJSONValue *
testQEMUSchemaGetLatest(const char *arch)
{
g_autofree char *capsLatestFile = NULL;
if (!(capsLatestFile = testQemuGetLatestCapsForArch(arch, "replies"))) {
VIR_TEST_VERBOSE("failed to find latest caps replies");
return NULL;
}
VIR_TEST_DEBUG("replies file: '%s'", capsLatestFile);
return testQEMUSchemaLoadReplies(capsLatestFile);
}
GHashTable *
testQEMUSchemaLoadLatest(const char *arch)
{
virJSONValue *schema;
if (!(schema = testQEMUSchemaGetLatest(arch)))
return NULL;
return virQEMUQAPISchemaConvert(schema);
}
GHashTable *
testQEMUSchemaLoad(const char *filename)
{
virJSONValue *schema;
if (!(schema = testQEMUSchemaLoadReplies(filename)))
return NULL;
return virQEMUQAPISchemaConvert(schema);
}