mirror of https://gitee.com/openkylin/libvirt.git
1234 lines
33 KiB
C
1234 lines
33 KiB
C
/*
|
|
* Copyright (C) 2011-2013 Red Hat, Inc.
|
|
*
|
|
* 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 <time.h>
|
|
|
|
#include "testutils.h"
|
|
#include "testutilsqemuschema.h"
|
|
#include "qemumonitortestutils.h"
|
|
|
|
#include "virthread.h"
|
|
#define LIBVIRT_QEMU_PROCESSPRIV_H_ALLOW
|
|
#include "qemu/qemu_processpriv.h"
|
|
#include "qemu/qemu_monitor.h"
|
|
#include "qemu/qemu_agent.h"
|
|
#include "rpc/virnetsocket.h"
|
|
#include "viralloc.h"
|
|
#include "virlog.h"
|
|
#include "virerror.h"
|
|
#include "vireventthread.h"
|
|
|
|
#define VIR_FROM_THIS VIR_FROM_NONE
|
|
|
|
VIR_LOG_INIT("tests.qemumonitortestutils");
|
|
|
|
struct _qemuMonitorTestItem {
|
|
char *identifier;
|
|
qemuMonitorTestResponseCallback cb;
|
|
void *opaque;
|
|
virFreeCallback freecb;
|
|
};
|
|
|
|
struct _qemuMonitorTest {
|
|
virMutex lock;
|
|
virThread thread;
|
|
|
|
bool quit;
|
|
bool running;
|
|
bool started;
|
|
|
|
bool allowUnusedCommands;
|
|
|
|
bool skipValidationDeprecated;
|
|
bool skipValidationRemoved;
|
|
|
|
char *incoming;
|
|
size_t incomingLength;
|
|
size_t incomingCapacity;
|
|
|
|
char *outgoing;
|
|
size_t outgoingLength;
|
|
size_t outgoingCapacity;
|
|
|
|
virNetSocket *server;
|
|
virNetSocket *client;
|
|
|
|
virEventThread *eventThread;
|
|
|
|
qemuMonitor *mon;
|
|
qemuAgent *agent;
|
|
|
|
char *tmpdir;
|
|
|
|
size_t nitems;
|
|
qemuMonitorTestItem **items;
|
|
|
|
virDomainObj *vm;
|
|
GHashTable *qapischema;
|
|
};
|
|
|
|
|
|
static void
|
|
qemuMonitorTestItemFree(qemuMonitorTestItem *item)
|
|
{
|
|
if (!item)
|
|
return;
|
|
|
|
g_free(item->identifier);
|
|
|
|
if (item->freecb)
|
|
(item->freecb)(item->opaque);
|
|
|
|
g_free(item);
|
|
}
|
|
|
|
|
|
/*
|
|
* Appends data for a reply to the outgoing buffer
|
|
*/
|
|
int
|
|
qemuMonitorTestAddResponse(qemuMonitorTest *test,
|
|
const char *response)
|
|
{
|
|
size_t want = strlen(response) + 2;
|
|
size_t have = test->outgoingCapacity - test->outgoingLength;
|
|
|
|
VIR_DEBUG("Adding response to monitor command: '%s", response);
|
|
|
|
if (have < want) {
|
|
size_t need = want - have;
|
|
VIR_EXPAND_N(test->outgoing, test->outgoingCapacity, need);
|
|
}
|
|
|
|
want -= 2;
|
|
memcpy(test->outgoing + test->outgoingLength, response, want);
|
|
memcpy(test->outgoing + test->outgoingLength + want, "\r\n", 2);
|
|
test->outgoingLength += want + 2;
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
qemuMonitorTestAddErrorResponseInternal(qemuMonitorTest *test,
|
|
const char *usermsg)
|
|
{
|
|
g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
|
|
g_autofree char *escapemsg = NULL;
|
|
g_autofree char *jsonmsg = NULL;
|
|
char *tmp;
|
|
|
|
if (!usermsg)
|
|
usermsg = "unexpected command";
|
|
|
|
virBufferEscape(&buf, '\\', "\"", "%s", usermsg);
|
|
escapemsg = virBufferContentAndReset(&buf);
|
|
|
|
/* replace newline/carriage return with space */
|
|
tmp = escapemsg;
|
|
while (*tmp) {
|
|
if (*tmp == '\r' || *tmp == '\n')
|
|
*tmp = ' ';
|
|
|
|
tmp++;
|
|
}
|
|
|
|
/* format the JSON error message */
|
|
jsonmsg = g_strdup_printf("{ \"error\": " " { \"desc\": \"%s\", "
|
|
" \"class\": \"UnexpectedCommand\" } }", escapemsg);
|
|
|
|
return qemuMonitorTestAddResponse(test, jsonmsg);
|
|
}
|
|
|
|
|
|
int
|
|
qemuMonitorTestAddInvalidCommandResponse(qemuMonitorTest *test,
|
|
const char *expectedcommand,
|
|
const char *actualcommand)
|
|
{
|
|
g_autofree char *msg = NULL;
|
|
|
|
msg = g_strdup_printf("expected command '%s' got '%s'", expectedcommand,
|
|
actualcommand);
|
|
|
|
return qemuMonitorTestAddErrorResponseInternal(test, msg);
|
|
}
|
|
|
|
|
|
int G_GNUC_PRINTF(2, 3)
|
|
qemuMonitorTestAddErrorResponse(qemuMonitorTest *test, const char *errmsg, ...)
|
|
{
|
|
va_list msgargs;
|
|
g_autofree char *msg = NULL;
|
|
g_autofree char *jsonmsg = NULL;
|
|
int ret = -1;
|
|
|
|
va_start(msgargs, errmsg);
|
|
|
|
msg = g_strdup_vprintf(errmsg, msgargs);
|
|
|
|
jsonmsg = g_strdup_printf("{ \"error\": " " { \"desc\": \"%s\", "
|
|
" \"class\": \"UnexpectedCommand\" } }", msg);
|
|
|
|
ret = qemuMonitorTestAddResponse(test, jsonmsg);
|
|
|
|
va_end(msgargs);
|
|
return ret;
|
|
}
|
|
|
|
|
|
static void G_GNUC_NORETURN G_GNUC_PRINTF(1, 2)
|
|
qemuMonitorTestError(const char *errmsg,
|
|
...)
|
|
{
|
|
va_list msgargs;
|
|
|
|
va_start(msgargs, errmsg);
|
|
|
|
g_fprintf(stderr, "\n");
|
|
g_vfprintf(stderr, errmsg, msgargs);
|
|
g_fprintf(stderr, "\n");
|
|
exit(EXIT_FAILURE); /* exempt from syntax-check */
|
|
}
|
|
|
|
|
|
static void G_GNUC_NORETURN
|
|
qemuMonitorTestErrorInvalidCommand(const char *expectedcommand,
|
|
const char *actualcommand)
|
|
{
|
|
qemuMonitorTestError("expected command '%s' got '%s'",
|
|
expectedcommand, actualcommand);
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
qemuMonitorTestProcessCommand(qemuMonitorTest *test,
|
|
const char *cmdstr)
|
|
{
|
|
int ret;
|
|
|
|
VIR_DEBUG("Processing string from monitor handler: '%s", cmdstr);
|
|
|
|
if (test->nitems == 0) {
|
|
qemuMonitorTestError("unexpected command: '%s'", cmdstr);
|
|
} else {
|
|
qemuMonitorTestItem *item = test->items[0];
|
|
ret = (item->cb)(test, item, cmdstr);
|
|
qemuMonitorTestItemFree(item);
|
|
if (VIR_DELETE_ELEMENT(test->items, 0, test->nitems) < 0)
|
|
return -1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/*
|
|
* Handles read/write of monitor data on the monitor server side
|
|
*/
|
|
static void
|
|
qemuMonitorTestIO(virNetSocket *sock,
|
|
int events,
|
|
void *opaque)
|
|
{
|
|
qemuMonitorTest *test = opaque;
|
|
bool err = false;
|
|
VIR_LOCK_GUARD lock = virLockGuardLock(&test->lock);
|
|
|
|
if (test->quit)
|
|
return;
|
|
|
|
if (events & VIR_EVENT_HANDLE_WRITABLE) {
|
|
ssize_t ret;
|
|
if ((ret = virNetSocketWrite(sock,
|
|
test->outgoing,
|
|
test->outgoingLength)) < 0) {
|
|
err = true;
|
|
goto cleanup;
|
|
}
|
|
|
|
memmove(test->outgoing,
|
|
test->outgoing + ret,
|
|
test->outgoingLength - ret);
|
|
test->outgoingLength -= ret;
|
|
|
|
if ((test->outgoingCapacity - test->outgoingLength) > 1024)
|
|
VIR_SHRINK_N(test->outgoing, test->outgoingCapacity, 1024);
|
|
}
|
|
|
|
if (events & VIR_EVENT_HANDLE_READABLE) {
|
|
ssize_t ret, used;
|
|
char *t1, *t2;
|
|
|
|
if ((test->incomingCapacity - test->incomingLength) < 1024) {
|
|
VIR_EXPAND_N(test->incoming, test->incomingCapacity, 1024);
|
|
}
|
|
|
|
if ((ret = virNetSocketRead(sock,
|
|
test->incoming + test->incomingLength,
|
|
(test->incomingCapacity - test->incomingLength) - 1)) < 0) {
|
|
err = true;
|
|
goto cleanup;
|
|
}
|
|
test->incomingLength += ret;
|
|
test->incoming[test->incomingLength] = '\0';
|
|
|
|
/* Look to see if we've got a complete line, and
|
|
* if so, handle that command
|
|
*/
|
|
t1 = test->incoming;
|
|
while ((t2 = strstr(t1, "\n")) ||
|
|
(test->agent && (t2 = strstr(t1, "\r")))) {
|
|
*t2 = '\0';
|
|
|
|
if (qemuMonitorTestProcessCommand(test, t1) < 0) {
|
|
err = true;
|
|
goto cleanup;
|
|
}
|
|
|
|
t1 = t2 + 1;
|
|
}
|
|
used = t1 - test->incoming;
|
|
memmove(test->incoming, t1, test->incomingLength - used);
|
|
test->incomingLength -= used;
|
|
if ((test->incomingCapacity - test->incomingLength) > 1024) {
|
|
VIR_SHRINK_N(test->incoming,
|
|
test->incomingCapacity,
|
|
1024);
|
|
}
|
|
}
|
|
|
|
if (events & (VIR_EVENT_HANDLE_HANGUP |
|
|
VIR_EVENT_HANDLE_ERROR))
|
|
err = true;
|
|
|
|
cleanup:
|
|
if (err) {
|
|
virNetSocketRemoveIOCallback(sock);
|
|
virNetSocketClose(sock);
|
|
g_clear_pointer(&test->client, virObjectUnref);
|
|
} else {
|
|
events = VIR_EVENT_HANDLE_READABLE;
|
|
|
|
if (test->outgoingLength)
|
|
events |= VIR_EVENT_HANDLE_WRITABLE;
|
|
|
|
virNetSocketUpdateIOCallback(sock, events);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
qemuMonitorTestWorker(void *opaque)
|
|
{
|
|
qemuMonitorTest *test = opaque;
|
|
|
|
while (true) {
|
|
VIR_WITH_MUTEX_LOCK_GUARD(&test->lock) {
|
|
if (test->quit) {
|
|
test->running = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (virEventRunDefaultImpl() < 0) {
|
|
VIR_WITH_MUTEX_LOCK_GUARD(&test->lock) {
|
|
test->quit = true;
|
|
test->running = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
qemuMonitorTestFreeTimer(int timer G_GNUC_UNUSED,
|
|
void *opaque G_GNUC_UNUSED)
|
|
{
|
|
/* nothing to be done here */
|
|
}
|
|
|
|
|
|
void
|
|
qemuMonitorTestFree(qemuMonitorTest *test)
|
|
{
|
|
size_t i;
|
|
int timer = -1;
|
|
|
|
if (!test)
|
|
return;
|
|
|
|
VIR_WITH_MUTEX_LOCK_GUARD(&test->lock) {
|
|
if (test->running) {
|
|
test->quit = true;
|
|
/* HACK: Add a dummy timeout to break event loop */
|
|
timer = virEventAddTimeout(0, qemuMonitorTestFreeTimer, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
if (test->client) {
|
|
virNetSocketRemoveIOCallback(test->client);
|
|
virNetSocketClose(test->client);
|
|
virObjectUnref(test->client);
|
|
}
|
|
|
|
virObjectUnref(test->server);
|
|
if (test->mon) {
|
|
virObjectUnlock(test->mon);
|
|
qemuMonitorClose(test->mon);
|
|
}
|
|
|
|
if (test->agent) {
|
|
virObjectUnlock(test->agent);
|
|
qemuAgentClose(test->agent);
|
|
}
|
|
|
|
g_object_unref(test->eventThread);
|
|
|
|
virObjectUnref(test->vm);
|
|
|
|
if (test->started)
|
|
virThreadJoin(&test->thread);
|
|
|
|
if (timer != -1)
|
|
virEventRemoveTimeout(timer);
|
|
|
|
g_free(test->incoming);
|
|
g_free(test->outgoing);
|
|
|
|
for (i = 0; i < test->nitems; i++) {
|
|
if (!test->allowUnusedCommands) {
|
|
g_fprintf(stderr,
|
|
"\nunused test monitor item '%s'",
|
|
NULLSTR(test->items[i]->identifier));
|
|
}
|
|
|
|
qemuMonitorTestItemFree(test->items[i]);
|
|
}
|
|
g_free(test->items);
|
|
|
|
if (test->tmpdir && rmdir(test->tmpdir) < 0)
|
|
VIR_WARN("Failed to remove tempdir: %s", g_strerror(errno));
|
|
|
|
g_free(test->tmpdir);
|
|
|
|
if (!test->allowUnusedCommands &&
|
|
test->nitems != 0) {
|
|
qemuMonitorTestError("unused test monitor items are not allowed for this test\n");
|
|
}
|
|
|
|
virMutexDestroy(&test->lock);
|
|
g_free(test);
|
|
}
|
|
|
|
|
|
void
|
|
qemuMonitorTestAddHandler(qemuMonitorTest *test,
|
|
const char *identifier,
|
|
qemuMonitorTestResponseCallback cb,
|
|
void *opaque,
|
|
virFreeCallback freecb)
|
|
{
|
|
qemuMonitorTestItem *item;
|
|
|
|
item = g_new0(qemuMonitorTestItem, 1);
|
|
|
|
item->identifier = g_strdup(identifier);
|
|
item->cb = cb;
|
|
item->freecb = freecb;
|
|
item->opaque = opaque;
|
|
|
|
VIR_WITH_MUTEX_LOCK_GUARD(&test->lock) {
|
|
VIR_APPEND_ELEMENT(test->items, test->nitems, item);
|
|
}
|
|
}
|
|
|
|
void *
|
|
qemuMonitorTestItemGetPrivateData(qemuMonitorTestItem *item)
|
|
{
|
|
return item ? item->opaque : NULL;
|
|
}
|
|
|
|
|
|
struct qemuMonitorTestHandlerData {
|
|
char *command_name;
|
|
char *cmderr;
|
|
char *response;
|
|
char *expectArgs;
|
|
};
|
|
|
|
static void
|
|
qemuMonitorTestHandlerDataFree(void *opaque)
|
|
{
|
|
struct qemuMonitorTestHandlerData *data = opaque;
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
g_free(data->command_name);
|
|
g_free(data->cmderr);
|
|
g_free(data->response);
|
|
g_free(data->expectArgs);
|
|
g_free(data);
|
|
}
|
|
|
|
|
|
/* Returns -1 on error, 0 if validation was successful/not necessary */
|
|
static int
|
|
qemuMonitorTestProcessCommandDefaultValidate(qemuMonitorTest *test,
|
|
const char *cmdname,
|
|
virJSONValue *args)
|
|
{
|
|
g_auto(virBuffer) debug = VIR_BUFFER_INITIALIZER;
|
|
bool allowIncomplete = false;
|
|
|
|
if (!test->qapischema)
|
|
return 0;
|
|
|
|
if (test->agent) {
|
|
virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
|
|
"Command validation testing is not "
|
|
"implemented for the guest agent");
|
|
return -1;
|
|
}
|
|
|
|
/* The schema of 'device_add' is incomplete so we relax the validator */
|
|
if (STREQ(cmdname, "device_add"))
|
|
allowIncomplete = true;
|
|
|
|
if (testQEMUSchemaValidateCommand(cmdname, args, test->qapischema,
|
|
test->skipValidationDeprecated,
|
|
test->skipValidationRemoved,
|
|
allowIncomplete,
|
|
&debug) < 0) {
|
|
if (virTestGetDebug() == 2) {
|
|
g_autofree char *argstr = NULL;
|
|
|
|
if (args)
|
|
argstr = virJSONValueToString(args, true);
|
|
|
|
fprintf(stderr,
|
|
"\nfailed to validate arguments of '%s' against QAPI schema\n"
|
|
"args:\n%s\nvalidator output:\n %s\n",
|
|
cmdname, NULLSTR(argstr), virBufferCurrentContent(&debug));
|
|
}
|
|
|
|
qemuMonitorTestError("failed to validate arguments of '%s' "
|
|
"against QAPI schema "
|
|
"(to see debug output use VIR_TEST_DEBUG=2)",
|
|
cmdname);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
qemuMonitorTestProcessCommandDefault(qemuMonitorTest *test,
|
|
qemuMonitorTestItem *item,
|
|
const char *cmdstr)
|
|
{
|
|
struct qemuMonitorTestHandlerData *data = item->opaque;
|
|
g_autoptr(virJSONValue) val = NULL;
|
|
virJSONValue *cmdargs = NULL;
|
|
const char *cmdname;
|
|
|
|
if (!(val = virJSONValueFromString(cmdstr)))
|
|
return -1;
|
|
|
|
if (!(cmdname = virJSONValueObjectGetString(val, "execute"))) {
|
|
qemuMonitorTestError("Missing command name in %s", cmdstr);
|
|
return -1;
|
|
}
|
|
|
|
cmdargs = virJSONValueObjectGet(val, "arguments");
|
|
if (qemuMonitorTestProcessCommandDefaultValidate(test, cmdname, cmdargs) < 0)
|
|
return -1;
|
|
|
|
if (data->command_name && STRNEQ(data->command_name, cmdname)) {
|
|
qemuMonitorTestErrorInvalidCommand(data->command_name, cmdname);
|
|
return -1;
|
|
} else {
|
|
return qemuMonitorTestAddResponse(test, data->response);
|
|
}
|
|
}
|
|
|
|
|
|
int
|
|
qemuMonitorTestAddItem(qemuMonitorTest *test,
|
|
const char *command_name,
|
|
const char *response)
|
|
{
|
|
struct qemuMonitorTestHandlerData *data;
|
|
|
|
data = g_new0(struct qemuMonitorTestHandlerData, 1);
|
|
|
|
data->command_name = g_strdup(command_name);
|
|
data->response = g_strdup(response);
|
|
|
|
qemuMonitorTestAddHandler(test,
|
|
command_name,
|
|
qemuMonitorTestProcessCommandDefault,
|
|
data, qemuMonitorTestHandlerDataFree);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
qemuMonitorTestProcessCommandVerbatim(qemuMonitorTest *test,
|
|
qemuMonitorTestItem *item,
|
|
const char *cmdstr)
|
|
{
|
|
struct qemuMonitorTestHandlerData *data = item->opaque;
|
|
g_autofree char *reformatted = NULL;
|
|
g_autoptr(virJSONValue) json = NULL;
|
|
virJSONValue *cmdargs;
|
|
const char *cmdname;
|
|
int ret = -1;
|
|
|
|
/* JSON strings will be reformatted to simplify checking */
|
|
if (!(json = virJSONValueFromString(cmdstr)) ||
|
|
!(reformatted = virJSONValueToString(json, false)))
|
|
return -1;
|
|
|
|
cmdstr = reformatted;
|
|
|
|
/* in this case we do a best-effort schema check if we can find the command */
|
|
if ((cmdname = virJSONValueObjectGetString(json, "execute"))) {
|
|
cmdargs = virJSONValueObjectGet(json, "arguments");
|
|
if (qemuMonitorTestProcessCommandDefaultValidate(test, cmdname, cmdargs) < 0)
|
|
return -1;
|
|
}
|
|
|
|
if (STREQ(data->command_name, cmdstr)) {
|
|
ret = qemuMonitorTestAddResponse(test, data->response);
|
|
} else {
|
|
if (data->cmderr) {
|
|
qemuMonitorTestError("%s: %s expected %s",
|
|
data->cmderr, cmdstr, data->command_name);
|
|
} else {
|
|
qemuMonitorTestErrorInvalidCommand(data->command_name, cmdstr);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* qemuMonitorTestAddItemVerbatim:
|
|
* @test: monitor test object
|
|
* @command: full expected command syntax
|
|
* @cmderr: possible explanation of expected command (may be NULL)
|
|
* @response: full reply of @command
|
|
*
|
|
* Adds a test command for the simulated monitor. The full syntax is checked
|
|
* as specified in @command. For JSON monitor tests formatting/whitespace is
|
|
* ignored. If the command on the monitor is not as expected an error containing
|
|
* @cmderr is returned. Otherwise @response is put as-is on the monitor.
|
|
*
|
|
* Returns 0 when command was successfully added, -1 on error.
|
|
*/
|
|
int
|
|
qemuMonitorTestAddItemVerbatim(qemuMonitorTest *test,
|
|
const char *command,
|
|
const char *cmderr,
|
|
const char *response)
|
|
{
|
|
struct qemuMonitorTestHandlerData *data;
|
|
char *reformatted = NULL;
|
|
|
|
if (!(reformatted = virJSONStringReformat(command, false)))
|
|
return -1;
|
|
|
|
data = g_new0(struct qemuMonitorTestHandlerData, 1);
|
|
|
|
data->response = g_strdup(response);
|
|
data->cmderr = g_strdup(cmderr);
|
|
data->command_name = g_steal_pointer(&reformatted);
|
|
|
|
qemuMonitorTestAddHandler(test,
|
|
command,
|
|
qemuMonitorTestProcessCommandVerbatim,
|
|
data, qemuMonitorTestHandlerDataFree);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
qemuMonitorTestProcessGuestAgentSync(qemuMonitorTest *test,
|
|
qemuMonitorTestItem *item G_GNUC_UNUSED,
|
|
const char *cmdstr)
|
|
{
|
|
g_autoptr(virJSONValue) val = NULL;
|
|
virJSONValue *args;
|
|
unsigned long long id;
|
|
const char *cmdname;
|
|
g_autofree char *retmsg = NULL;
|
|
|
|
if (!(val = virJSONValueFromString(cmdstr)))
|
|
return -1;
|
|
|
|
if (!(cmdname = virJSONValueObjectGetString(val, "execute")))
|
|
return qemuMonitorTestAddErrorResponse(test, "Missing guest-sync command name");
|
|
|
|
if (STRNEQ(cmdname, "guest-sync"))
|
|
return qemuMonitorTestAddInvalidCommandResponse(test, "guest-sync", cmdname);
|
|
|
|
if (!(args = virJSONValueObjectGet(val, "arguments")))
|
|
return qemuMonitorTestAddErrorResponse(test, "Missing arguments for guest-sync");
|
|
|
|
if (virJSONValueObjectGetNumberUlong(args, "id", &id))
|
|
return qemuMonitorTestAddErrorResponse(test, "Missing id for guest sync");
|
|
|
|
retmsg = g_strdup_printf("{\"return\":%llu}", id);
|
|
|
|
return qemuMonitorTestAddResponse(test, retmsg);
|
|
}
|
|
|
|
|
|
int
|
|
qemuMonitorTestAddAgentSyncResponse(qemuMonitorTest *test)
|
|
{
|
|
if (!test->agent) {
|
|
virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
|
|
"This test is not an agent test");
|
|
return -1;
|
|
}
|
|
|
|
qemuMonitorTestAddHandler(test,
|
|
"agent-sync",
|
|
qemuMonitorTestProcessGuestAgentSync,
|
|
NULL, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void
|
|
qemuMonitorTestEOFNotify(qemuMonitor *mon G_GNUC_UNUSED,
|
|
virDomainObj *vm G_GNUC_UNUSED)
|
|
{
|
|
}
|
|
|
|
|
|
static void
|
|
qemuMonitorTestErrorNotify(qemuMonitor *mon G_GNUC_UNUSED,
|
|
virDomainObj *vm G_GNUC_UNUSED)
|
|
{
|
|
}
|
|
|
|
|
|
static qemuMonitorCallbacks qemuMonitorTestCallbacks = {
|
|
.eofNotify = qemuMonitorTestEOFNotify,
|
|
.errorNotify = qemuMonitorTestErrorNotify,
|
|
.domainDeviceDeleted = qemuProcessHandleDeviceDeleted,
|
|
};
|
|
|
|
|
|
static void
|
|
qemuMonitorTestAgentNotify(qemuAgent *agent G_GNUC_UNUSED,
|
|
virDomainObj *vm G_GNUC_UNUSED)
|
|
{
|
|
}
|
|
|
|
|
|
static qemuAgentCallbacks qemuMonitorTestAgentCallbacks = {
|
|
.eofNotify = qemuMonitorTestAgentNotify,
|
|
.errorNotify = qemuMonitorTestAgentNotify,
|
|
};
|
|
|
|
|
|
static qemuMonitorTest *
|
|
qemuMonitorCommonTestNew(virDomainXMLOption *xmlopt,
|
|
virDomainObj *vm,
|
|
virDomainChrSourceDef *src)
|
|
{
|
|
g_autoptr(qemuMonitorTest) test = NULL;
|
|
g_autofree char *path = NULL;
|
|
g_autofree char *tmpdir_template = NULL;
|
|
|
|
test = g_new0(qemuMonitorTest, 1);
|
|
|
|
if (virMutexInit(&test->lock) < 0) {
|
|
virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
|
|
"Cannot initialize mutex");
|
|
VIR_FREE(test);
|
|
return NULL;
|
|
}
|
|
|
|
tmpdir_template = g_strdup("/tmp/libvirt_XXXXXX");
|
|
|
|
if (!(test->tmpdir = g_mkdtemp(tmpdir_template))) {
|
|
virReportSystemError(errno, "%s",
|
|
"Failed to create temporary directory");
|
|
return NULL;
|
|
}
|
|
|
|
tmpdir_template = NULL;
|
|
|
|
path = g_strdup_printf("%s/qemumonitorjsontest.sock", test->tmpdir);
|
|
|
|
if (vm) {
|
|
test->vm = virObjectRef(vm);
|
|
} else {
|
|
test->vm = virDomainObjNew(xmlopt);
|
|
if (!test->vm)
|
|
return NULL;
|
|
if (!(test->vm->def = virDomainDefNew(xmlopt)))
|
|
return NULL;
|
|
}
|
|
|
|
if (virNetSocketNewListenUNIX(path, 0700, geteuid(), getegid(),
|
|
&test->server) < 0)
|
|
return NULL;
|
|
|
|
memset(src, 0, sizeof(*src));
|
|
src->type = VIR_DOMAIN_CHR_TYPE_UNIX;
|
|
src->data.nix.path = (char *)path;
|
|
src->data.nix.listen = false;
|
|
path = NULL;
|
|
|
|
if (virNetSocketListen(test->server, 1) < 0)
|
|
return NULL;
|
|
|
|
return g_steal_pointer(&test);
|
|
}
|
|
|
|
|
|
static int
|
|
qemuMonitorCommonTestInit(qemuMonitorTest *test)
|
|
{
|
|
int events = VIR_EVENT_HANDLE_READABLE;
|
|
|
|
if (!test)
|
|
return -1;
|
|
|
|
if (virNetSocketAccept(test->server, &test->client) < 0)
|
|
return -1;
|
|
|
|
if (!test->client)
|
|
return -1;
|
|
|
|
if (test->outgoingLength > 0)
|
|
events = VIR_EVENT_HANDLE_WRITABLE;
|
|
|
|
if (virNetSocketAddIOCallback(test->client,
|
|
events,
|
|
qemuMonitorTestIO,
|
|
test,
|
|
NULL) < 0)
|
|
return -1;
|
|
|
|
VIR_WITH_MUTEX_LOCK_GUARD(&test->lock) {
|
|
if (virThreadCreate(&test->thread, true, qemuMonitorTestWorker, test) < 0)
|
|
return -1;
|
|
test->started = test->running = true;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
#define QEMU_JSON_GREETING "{\"QMP\":"\
|
|
" {\"version\":"\
|
|
" {\"qemu\":"\
|
|
" {\"micro\": 1,"\
|
|
" \"minor\": 0,"\
|
|
" \"major\": 1"\
|
|
" },"\
|
|
" \"package\": \"(qemu-kvm-1.0.1)"\
|
|
" \"},"\
|
|
" \"capabilities\": []"\
|
|
" }"\
|
|
"}"
|
|
/* We skip the normal handshake reply of "{\"execute\":\"qmp_capabilities\"}" */
|
|
|
|
|
|
qemuMonitorTest *
|
|
qemuMonitorTestNew(virDomainXMLOption *xmlopt,
|
|
virDomainObj *vm,
|
|
const char *greeting,
|
|
GHashTable *schema)
|
|
{
|
|
g_autoptr(qemuMonitorTest) test = NULL;
|
|
virDomainChrSourceDef src = { 0 };
|
|
|
|
if (!(test = qemuMonitorCommonTestNew(xmlopt, vm, &src)))
|
|
goto error;
|
|
|
|
if (!(test->eventThread = virEventThreadNew("mon-test")))
|
|
goto error;
|
|
|
|
test->qapischema = schema;
|
|
if (!(test->mon = qemuMonitorOpen(test->vm,
|
|
&src,
|
|
virEventThreadGetContext(test->eventThread),
|
|
&qemuMonitorTestCallbacks)))
|
|
goto error;
|
|
|
|
virObjectLock(test->mon);
|
|
|
|
if (!greeting)
|
|
greeting = QEMU_JSON_GREETING;
|
|
|
|
if (qemuMonitorTestAddResponse(test, greeting) < 0)
|
|
goto error;
|
|
|
|
if (qemuMonitorCommonTestInit(test) < 0)
|
|
goto error;
|
|
|
|
virDomainChrSourceDefClear(&src);
|
|
|
|
return g_steal_pointer(&test);
|
|
|
|
error:
|
|
virDomainChrSourceDefClear(&src);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* qemuMonitorTestNewFromFile:
|
|
* @fileName: File name to load monitor replies from
|
|
* @xmlopt: XML parser configuration object
|
|
* @simple: see below
|
|
*
|
|
* Create a JSON test monitor simulator object and fill it with replies
|
|
* specified in @fileName. The file contains JSON reply objects separated by
|
|
* empty lines. If @simple is true a generic QMP greeting is automatically
|
|
* added as the first reply, otherwise the first entry in the file is used.
|
|
*
|
|
* Returns the monitor object on success; NULL on error.
|
|
*/
|
|
qemuMonitorTest *
|
|
qemuMonitorTestNewFromFile(const char *fileName,
|
|
virDomainXMLOption *xmlopt,
|
|
bool simple)
|
|
{
|
|
g_autoptr(qemuMonitorTest) test = NULL;
|
|
g_autofree char *json = NULL;
|
|
char *tmp;
|
|
char *singleReply;
|
|
|
|
if (virTestLoadFile(fileName, &json) < 0)
|
|
return NULL;
|
|
|
|
if (simple && !(test = qemuMonitorTestNewSimple(xmlopt)))
|
|
return NULL;
|
|
|
|
/* Our JSON parser expects replies to be separated by a newline character.
|
|
* Hence we must preprocess the file a bit. */
|
|
tmp = singleReply = json;
|
|
while ((tmp = strchr(tmp, '\n'))) {
|
|
/* It is safe to touch tmp[1] since all strings ends with '\0'. */
|
|
bool eof = !tmp[1];
|
|
|
|
if (*(tmp + 1) != '\n') {
|
|
*tmp = ' ';
|
|
tmp++;
|
|
} else {
|
|
/* Cut off a single reply. */
|
|
*(tmp + 1) = '\0';
|
|
|
|
if (test) {
|
|
if (qemuMonitorTestAddItem(test, NULL, singleReply) < 0)
|
|
return NULL;
|
|
} else {
|
|
/* Create new mocked monitor with our greeting */
|
|
if (!(test = qemuMonitorTestNew(xmlopt, NULL,
|
|
singleReply, NULL)))
|
|
return NULL;
|
|
}
|
|
|
|
if (!eof) {
|
|
/* Move the @tmp and @singleReply. */
|
|
tmp += 2;
|
|
singleReply = tmp;
|
|
}
|
|
}
|
|
|
|
if (eof)
|
|
break;
|
|
}
|
|
|
|
if (test && qemuMonitorTestAddItem(test, NULL, singleReply) < 0)
|
|
return NULL;
|
|
|
|
return g_steal_pointer(&test);
|
|
}
|
|
|
|
|
|
/**
|
|
* qemuMonitorTestAllowUnusedCommands:
|
|
* @test: test monitor object
|
|
*
|
|
* By default all test items/commands must be used by the test. This function
|
|
* allows to override the requirement for individual tests e.g. if it's necessary
|
|
* to test some negative scenarios which would not use all commands.
|
|
*/
|
|
void
|
|
qemuMonitorTestAllowUnusedCommands(qemuMonitorTest *test)
|
|
{
|
|
test->allowUnusedCommands = true;
|
|
}
|
|
|
|
|
|
/**
|
|
* qemuMonitorTestSkipDeprecatedValidation:
|
|
* @test: test monitor object
|
|
* @allowRemoved: don't produce errors if command was removed from QMP schema
|
|
*
|
|
* By default if the QMP schema is provided all test items/commands are
|
|
* validated against the schema. This function allows to override the validation
|
|
* and additionally if @allowRemoved is true and if such a command is no longer
|
|
* present in the QMP, only a warning is printed.
|
|
*
|
|
* '@allowRemoved' must be used only if a suitable replacement is already in
|
|
* use and the code tests legacy interactions.
|
|
*
|
|
* Note that currently '@allowRemoved' influences only removed commands. If an
|
|
* argument is removed it will still fail validation.
|
|
*/
|
|
void
|
|
qemuMonitorTestSkipDeprecatedValidation(qemuMonitorTest *test,
|
|
bool allowRemoved)
|
|
{
|
|
test->skipValidationDeprecated = true;
|
|
test->skipValidationRemoved = allowRemoved;
|
|
}
|
|
|
|
|
|
static int
|
|
qemuMonitorTestFullAddItem(qemuMonitorTest *test,
|
|
const char *filename,
|
|
const char *command,
|
|
const char *response,
|
|
size_t line)
|
|
{
|
|
g_autofree char *cmderr = NULL;
|
|
|
|
cmderr = g_strdup_printf("wrong expected command in %s:%zu: ", filename, line);
|
|
|
|
return qemuMonitorTestAddItemVerbatim(test, command, cmderr, response);
|
|
}
|
|
|
|
|
|
/**
|
|
* qemuMonitorTestProcessFileEntries:
|
|
* @inputstr: input file contents (modified)
|
|
* @fileName: File name of @inputstr (for error reporting)
|
|
* @items: filled with command, reply tuples
|
|
* @nitems: Count of elements in @items.
|
|
*
|
|
* Process a monitor interaction file.
|
|
*
|
|
* The file contains a sequence of JSON commands and reply objects separated by
|
|
* empty lines. A command is followed by a reply.
|
|
*/
|
|
int
|
|
qemuMonitorTestProcessFileEntries(char *inputstr,
|
|
const char *fileName,
|
|
struct qemuMonitorTestCommandReplyTuple **items,
|
|
size_t *nitems)
|
|
{
|
|
size_t nalloc = 0;
|
|
char *tmp = inputstr;
|
|
size_t line = 0;
|
|
char *command = inputstr;
|
|
char *response = NULL;
|
|
size_t commandln = 0;
|
|
|
|
*items = NULL;
|
|
*nitems = 0;
|
|
|
|
while ((tmp = strchr(tmp, '\n'))) {
|
|
line++;
|
|
|
|
/* eof */
|
|
if (!tmp[1])
|
|
break;
|
|
|
|
/* concatenate block which was broken up for readability */
|
|
if (*(tmp + 1) != '\n') {
|
|
*tmp = ' ';
|
|
tmp++;
|
|
continue;
|
|
}
|
|
|
|
/* We've seen a new line, increment the counter */
|
|
line++;
|
|
|
|
/* Cut off a single reply. */
|
|
*(tmp + 1) = '\0';
|
|
|
|
if (response) {
|
|
struct qemuMonitorTestCommandReplyTuple *item;
|
|
|
|
VIR_RESIZE_N(*items, nalloc, *nitems, 1);
|
|
|
|
item = *items + *nitems;
|
|
|
|
item->command = g_steal_pointer(&command);
|
|
item->reply = g_steal_pointer(&response);
|
|
item->line = commandln;
|
|
(*nitems)++;
|
|
}
|
|
|
|
/* Move the @tmp and @singleReply. */
|
|
tmp += 2;
|
|
|
|
if (!command) {
|
|
commandln = line;
|
|
command = tmp;
|
|
} else {
|
|
response = tmp;
|
|
}
|
|
}
|
|
|
|
if (command) {
|
|
struct qemuMonitorTestCommandReplyTuple *item;
|
|
|
|
if (!response) {
|
|
virReportError(VIR_ERR_INTERNAL_ERROR, "missing response for command "
|
|
"on line '%zu' in '%s'", commandln, fileName);
|
|
return -1;
|
|
}
|
|
|
|
VIR_RESIZE_N(*items, nalloc, *nitems, 1);
|
|
|
|
item = *items + *nitems;
|
|
|
|
item->command = g_steal_pointer(&command);
|
|
item->reply = g_steal_pointer(&response);
|
|
item->line = commandln;
|
|
(*nitems)++;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* qemuMonitorTestNewFromFileFull:
|
|
* @fileName: File name to load monitor replies from
|
|
* @driver: qemu driver object
|
|
* @vm: domain object (may be null if it's not needed by the test)
|
|
* @qmpschema: QMP schema data hash table if QMP checking is required
|
|
*
|
|
* Create a JSON test monitor simulator object and fill it with expected command
|
|
* sequence and replies specified in @fileName.
|
|
*
|
|
* The file contains a sequence of JSON commands and reply objects separated by
|
|
* empty lines. A command is followed by a reply. The QMP greeting is added
|
|
* automatically.
|
|
*
|
|
* Returns the monitor object on success; NULL on error.
|
|
*/
|
|
qemuMonitorTest *
|
|
qemuMonitorTestNewFromFileFull(const char *fileName,
|
|
virQEMUDriver *driver,
|
|
virDomainObj *vm,
|
|
GHashTable *qmpschema)
|
|
{
|
|
g_autoptr(qemuMonitorTest) ret = NULL;
|
|
g_autofree char *jsonstr = NULL;
|
|
g_autofree struct qemuMonitorTestCommandReplyTuple *items = NULL;
|
|
size_t nitems = 0;
|
|
size_t i;
|
|
|
|
if (virTestLoadFile(fileName, &jsonstr) < 0)
|
|
return NULL;
|
|
|
|
if (!(ret = qemuMonitorTestNew(driver->xmlopt, vm, NULL, qmpschema)))
|
|
return NULL;
|
|
|
|
if (qemuMonitorTestProcessFileEntries(jsonstr, fileName, &items, &nitems) < 0)
|
|
return NULL;
|
|
|
|
for (i = 0; i < nitems; i++) {
|
|
struct qemuMonitorTestCommandReplyTuple *item = items + i;
|
|
|
|
if (qemuMonitorTestFullAddItem(ret, fileName, item->command, item->reply,
|
|
item->line) < 0)
|
|
return NULL;
|
|
}
|
|
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
|
|
qemuMonitorTest *
|
|
qemuMonitorTestNewAgent(virDomainXMLOption *xmlopt)
|
|
{
|
|
g_autoptr(qemuMonitorTest) test = NULL;
|
|
virDomainChrSourceDef src = { 0 };
|
|
|
|
if (!(test = qemuMonitorCommonTestNew(xmlopt, NULL, &src)))
|
|
goto error;
|
|
|
|
if (!(test->eventThread = virEventThreadNew("agent-test")))
|
|
goto error;
|
|
|
|
if (!(test->agent = qemuAgentOpen(test->vm,
|
|
&src,
|
|
virEventThreadGetContext(test->eventThread),
|
|
&qemuMonitorTestAgentCallbacks)))
|
|
goto error;
|
|
|
|
virObjectLock(test->agent);
|
|
|
|
if (qemuMonitorCommonTestInit(test) < 0)
|
|
goto error;
|
|
|
|
virDomainChrSourceDefClear(&src);
|
|
|
|
return g_steal_pointer(&test);
|
|
|
|
error:
|
|
virDomainChrSourceDefClear(&src);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
qemuMonitor *
|
|
qemuMonitorTestGetMonitor(qemuMonitorTest *test)
|
|
{
|
|
return test->mon;
|
|
}
|
|
|
|
|
|
qemuAgent *
|
|
qemuMonitorTestGetAgent(qemuMonitorTest *test)
|
|
{
|
|
return test->agent;
|
|
}
|
|
|
|
|
|
virDomainObj *
|
|
qemuMonitorTestGetDomainObj(qemuMonitorTest *test)
|
|
{
|
|
return test->vm;
|
|
}
|