diff --git a/docs/formatdomain.html.in b/docs/formatdomain.html.in index de9b480ac2d9a4273d09ac5da99b6ac64b5f6a4e..6667bed6b5d1eb466434d9669d9fe37f9f8a6bcd 100644 --- a/docs/formatdomain.html.in +++ b/docs/formatdomain.html.in @@ -3017,6 +3017,10 @@ qemu-kvm -net nic,model=? /dev/null <channel type='pty'> <target type='virtio' name='arbitrary.virtio.serial.port.name'/> </channel> + <channel type='unix'> + <source mode='bind' path='/var/lib/libvirt/qemu/f16x86_64.agent'/> + <target type='virtio' name='org.qemu.guest_agent.0'/> + </channel> <channel type='spicevmc'> <target type='virtio' name='com.redhat.spice.0'/> </channel> @@ -3045,7 +3049,11 @@ qemu-kvm -net nic,model=? /dev/null optional element address can tie the channel to a particular type='virtio-serial' controller, documented above. - Since 0.7.7 + With qemu, if name is "org.qemu.guest_agent.0", + then libvirt can interact with a guest agent installed in the + guest, for actions such as guest shutdown or file system quiescing. + Since 0.7.7, guest agent interaction + since 0.9.10
spicevmc
Paravirtualized SPICE channel. The domain must also have a diff --git a/po/POTFILES.in b/po/POTFILES.in index ca1db706884314647de1d3042e8555eb94c34e9f..0126320ec4666e4a9ac727672db57a3e2e1c61fe 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -57,6 +57,7 @@ src/nwfilter/nwfilter_learnipaddr.c src/openvz/openvz_conf.c src/openvz/openvz_driver.c src/phyp/phyp_driver.c +src/qemu/qemu_agent.c src/qemu/qemu_bridge_filter.c src/qemu/qemu_capabilities.c src/qemu/qemu_cgroup.c diff --git a/src/Makefile.am b/src/Makefile.am index c459f2dbba7d5b20f0432e813f77eb2682eb1102..a44446f4d47240ef2829be0375ceb8386bae1c49 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -364,6 +364,7 @@ VBOX_DRIVER_EXTRA_DIST = \ vbox/vbox_XPCOMCGlue.c vbox/vbox_XPCOMCGlue.h QEMU_DRIVER_SOURCES = \ + qemu/qemu_agent.c qemu/qemu_agent.h \ qemu/qemu_capabilities.c qemu/qemu_capabilities.h\ qemu/qemu_command.c qemu/qemu_command.h \ qemu/qemu_domain.c qemu/qemu_domain.h \ diff --git a/src/qemu/qemu_agent.c b/src/qemu/qemu_agent.c new file mode 100644 index 0000000000000000000000000000000000000000..6a7c7b34ad20df1cbb1feba6ba97211ba50dc7cd --- /dev/null +++ b/src/qemu/qemu_agent.c @@ -0,0 +1,1112 @@ +/* + * qemu_agent.h: interaction with QEMU guest agent + * + * Copyright (C) 2006-2012 Red Hat, Inc. + * Copyright (C) 2006 Daniel P. Berrange + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author: Daniel P. Berrange + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qemu_agent.h" +#include "qemu_command.h" +#include "memory.h" +#include "logging.h" +#include "virterror_internal.h" +#include "json.h" +#include "virfile.h" + +#define VIR_FROM_THIS VIR_FROM_QEMU + +#define LINE_ENDING "\n" + +#define DEBUG_IO 0 +#define DEBUG_RAW_IO 0 + +/* When you are the first to uncomment this, + * don't forget to uncomment the corresponding + * part in qemuAgentIOProcessEvent as well. + * +static struct { + const char *type; + void (*handler)(qemuAgentPtr mon, virJSONValuePtr data); +} eventHandlers[] = { +}; +*/ + +typedef struct _qemuAgentMessage qemuAgentMessage; +typedef qemuAgentMessage *qemuAgentMessagePtr; + +struct _qemuAgentMessage { + char *txBuffer; + int txOffset; + int txLength; + + /* Used by the JSON monitor to hold reply / error */ + char *rxBuffer; + int rxLength; + void *rxObject; + + /* True if rxBuffer / rxObject are ready, or a + * fatal error occurred on the monitor channel + */ + bool finished; +}; + + +struct _qemuAgent { + virMutex lock; /* also used to protect fd */ + virCond notify; + + int refs; + + int fd; + int watch; + + bool connectPending; + + virDomainObjPtr vm; + + qemuAgentCallbacksPtr cb; + + /* If there's a command being processed this will be + * non-NULL */ + qemuAgentMessagePtr msg; + + /* Buffer incoming data ready for Agent monitor + * code to process & find message boundaries */ + size_t bufferOffset; + size_t bufferLength; + char *buffer; + + /* If anything went wrong, this will be fed back + * the next monitor msg */ + virError lastError; +}; + +#if DEBUG_RAW_IO +# include +static char * +qemuAgentEscapeNonPrintable(const char *text) +{ + int i; + virBuffer buf = VIR_BUFFER_INITIALIZER; + for (i = 0 ; text[i] != '\0' ; i++) { + if (text[i] == '\\') + virBufferAddLit(&buf, "\\\\"); + else if (c_isprint(text[i]) || text[i] == '\n' || + (text[i] == '\r' && text[i+1] == '\n')) + virBufferAddChar(&buf, text[i]); + else + virBufferAsprintf(&buf, "\\x%02x", text[i]); + } + return virBufferContentAndReset(&buf); +} +#endif + +void qemuAgentLock(qemuAgentPtr mon) +{ + virMutexLock(&mon->lock); +} + + +void qemuAgentUnlock(qemuAgentPtr mon) +{ + virMutexUnlock(&mon->lock); +} + + +static void qemuAgentFree(qemuAgentPtr mon) +{ + VIR_DEBUG("mon=%p", mon); + if (mon->cb && mon->cb->destroy) + (mon->cb->destroy)(mon, mon->vm); + ignore_value(virCondDestroy(&mon->notify)); + virMutexDestroy(&mon->lock); + VIR_FREE(mon->buffer); + VIR_FREE(mon); +} + +int qemuAgentRef(qemuAgentPtr mon) +{ + mon->refs++; + return mon->refs; +} + +int qemuAgentUnref(qemuAgentPtr mon) +{ + mon->refs--; + + if (mon->refs == 0) { + qemuAgentUnlock(mon); + qemuAgentFree(mon); + return 0; + } + + return mon->refs; +} + +static void +qemuAgentUnwatch(void *monitor) +{ + qemuAgentPtr mon = monitor; + + qemuAgentLock(mon); + if (qemuAgentUnref(mon) > 0) + qemuAgentUnlock(mon); +} + +static int +qemuAgentOpenUnix(const char *monitor, pid_t cpid, bool *inProgress) +{ + struct sockaddr_un addr; + int monfd; + int timeout = 3; /* In seconds */ + int ret, i = 0; + + *inProgress = false; + + if ((monfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { + virReportSystemError(errno, + "%s", _("failed to create socket")); + return -1; + } + + if (virSetNonBlock(monfd) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + "%s", _("Unable to put monitor into non-blocking mode")); + goto error; + } + + if (virSetCloseExec(monfd) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + "%s", _("Unable to set monitor close-on-exec flag")); + goto error; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + if (virStrcpyStatic(addr.sun_path, monitor) == NULL) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Agent path %s too big for destination"), monitor); + goto error; + } + + do { + ret = connect(monfd, (struct sockaddr *) &addr, sizeof(addr)); + + if (ret == 0) + break; + + if ((errno == ENOENT || errno == ECONNREFUSED) && + virKillProcess(cpid, 0) == 0) { + /* ENOENT : Socket may not have shown up yet + * ECONNREFUSED : Leftover socket hasn't been removed yet */ + continue; + } + + if ((errno == EINPROGRESS) || + (errno == EAGAIN)) { + VIR_DEBUG("Connection attempt continuing in background"); + *inProgress = true; + ret = 0; + break; + } + + virReportSystemError(errno, "%s", + _("failed to connect to monitor socket")); + goto error; + + } while ((++i <= timeout*5) && (usleep(.2 * 1000000) <= 0)); + + if (ret != 0) { + virReportSystemError(errno, "%s", + _("monitor socket did not show up.")); + goto error; + } + + return monfd; + +error: + VIR_FORCE_CLOSE(monfd); + return -1; +} + +static int +qemuAgentOpenPty(const char *monitor) +{ + int monfd; + + if ((monfd = open(monitor, O_RDWR | O_NONBLOCK)) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Unable to open monitor path %s"), monitor); + return -1; + } + + if (virSetCloseExec(monfd) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + "%s", _("Unable to set monitor close-on-exec flag")); + goto error; + } + + return monfd; + +error: + VIR_FORCE_CLOSE(monfd); + return -1; +} + + +static int +qemuAgentIOProcessEvent(qemuAgentPtr mon, + virJSONValuePtr obj) +{ + const char *type; + VIR_DEBUG("mon=%p obj=%p", mon, obj); + + type = virJSONValueObjectGetString(obj, "event"); + if (!type) { + VIR_WARN("missing event type in message"); + errno = EINVAL; + return -1; + } + +/* + for (i = 0 ; i < ARRAY_CARDINALITY(eventHandlers) ; i++) { + if (STREQ(eventHandlers[i].type, type)) { + virJSONValuePtr data = virJSONValueObjectGet(obj, "data"); + VIR_DEBUG("handle %s handler=%p data=%p", type, + eventHandlers[i].handler, data); + (eventHandlers[i].handler)(mon, data); + break; + } + } +*/ + return 0; +} + +static int +qemuAgentIOProcessLine(qemuAgentPtr mon, + const char *line, + qemuAgentMessagePtr msg) +{ + virJSONValuePtr obj = NULL; + int ret = -1; + + VIR_DEBUG("Line [%s]", line); + + if (!(obj = virJSONValueFromString(line))) + goto cleanup; + + if (obj->type != VIR_JSON_TYPE_OBJECT) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Parsed JSON reply '%s' isn't an object"), line); + goto cleanup; + } + + if (virJSONValueObjectHasKey(obj, "QMP") == 1) { + ret = 0; + } else if (virJSONValueObjectHasKey(obj, "event") == 1) { + ret = qemuAgentIOProcessEvent(mon, obj); + } else if (virJSONValueObjectHasKey(obj, "error") == 1 || + virJSONValueObjectHasKey(obj, "return") == 1) { + if (msg) { + msg->rxObject = obj; + msg->finished = 1; + obj = NULL; + ret = 0; + } else { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Unexpected JSON reply '%s'"), line); + } + } else { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Unknown JSON reply '%s'"), line); + } + +cleanup: + virJSONValueFree(obj); + return ret; +} + +static int qemuAgentIOProcessData(qemuAgentPtr mon, + char *data, + size_t len, + qemuAgentMessagePtr msg) +{ + int used = 0; + int i = 0; +#if DEBUG_IO +# if DEBUG_RAW_IO + char *str1 = qemuAgentEscapeNonPrintable(data); + VIR_ERROR("[%s]", str1); + VIR_FREE(str1); +# else + VIR_DEBUG("Data %zu bytes [%s]", len, data); +# endif +#endif + + while (used < len) { + char *nl = strstr(data + used, LINE_ENDING); + + if (nl) { + int got = nl - (data + used); + for (i = 0; i < strlen(LINE_ENDING); i++) + data[used + got + i] = '\0'; + if (qemuAgentIOProcessLine(mon, data + used, msg) < 0) { + return -1; + } + used += got + strlen(LINE_ENDING); + } else { + break; + } + } + + VIR_DEBUG("Total used %d bytes out of %zd available in buffer", used, len); + return used; +} + +/* This method processes data that has been received + * from the monitor. Looking for async events and + * replies/errors. + */ +static int +qemuAgentIOProcess(qemuAgentPtr mon) +{ + int len; + qemuAgentMessagePtr msg = NULL; + + /* See if there's a message ready for reply; that is, + * one that has completed writing all its data. + */ + if (mon->msg && mon->msg->txOffset == mon->msg->txLength) + msg = mon->msg; + +#if DEBUG_IO +# if DEBUG_RAW_IO + char *str1 = qemuAgentEscapeNonPrintable(msg ? msg->txBuffer : ""); + char *str2 = qemuAgentEscapeNonPrintable(mon->buffer); + VIR_ERROR(_("Process %zu %p %p [[[%s]]][[[%s]]]"), + mon->bufferOffset, mon->msg, msg, str1, str2); + VIR_FREE(str1); + VIR_FREE(str2); +# else + VIR_DEBUG("Process %zu", mon->bufferOffset); +# endif +#endif + + len = qemuAgentIOProcessData(mon, + mon->buffer, mon->bufferOffset, + msg); + + if (len < 0) + return -1; + + if (len < mon->bufferOffset) { + memmove(mon->buffer, mon->buffer + len, mon->bufferOffset - len); + mon->bufferOffset -= len; + } else { + VIR_FREE(mon->buffer); + mon->bufferOffset = mon->bufferLength = 0; + } +#if DEBUG_IO + VIR_DEBUG("Process done %zu used %d", mon->bufferOffset, len); +#endif + if (msg && msg->finished) + virCondBroadcast(&mon->notify); + return len; +} + + +static int +qemuAgentIOConnect(qemuAgentPtr mon) +{ + int optval; + socklen_t optlen; + + VIR_DEBUG("Checking on background connection status"); + + mon->connectPending = false; + + optlen = sizeof(optval); + + if (getsockopt(mon->fd, SOL_SOCKET, SO_ERROR, + &optval, &optlen) < 0) { + virReportSystemError(errno, "%s", + _("Cannot check socket connection status")); + return -1; + } + + if (optval != 0) { + virReportSystemError(optval, "%s", + _("Cannot connect to agent socket")); + return -1; + } + + VIR_DEBUG("Agent is now connected"); + return 0; +} + +/* + * Called when the monitor is able to write data + * Call this function while holding the monitor lock. + */ +static int +qemuAgentIOWrite(qemuAgentPtr mon) +{ + int done; + + /* If no active message, or fully transmitted, then no-op */ + if (!mon->msg || mon->msg->txOffset == mon->msg->txLength) + return 0; + + done = safewrite(mon->fd, + mon->msg->txBuffer + mon->msg->txOffset, + mon->msg->txLength - mon->msg->txOffset); + + if (done < 0) { + if (errno == EAGAIN) + return 0; + + virReportSystemError(errno, "%s", + _("Unable to write to monitor")); + return -1; + } + mon->msg->txOffset += done; + return done; +} + +/* + * Called when the monitor has incoming data to read + * Call this function while holding the monitor lock. + * + * Returns -1 on error, or number of bytes read + */ +static int +qemuAgentIORead(qemuAgentPtr mon) +{ + size_t avail = mon->bufferLength - mon->bufferOffset; + int ret = 0; + + if (avail < 1024) { + if (VIR_REALLOC_N(mon->buffer, + mon->bufferLength + 1024) < 0) { + virReportOOMError(); + return -1; + } + mon->bufferLength += 1024; + avail += 1024; + } + + /* Read as much as we can get into our buffer, + until we block on EAGAIN, or hit EOF */ + while (avail > 1) { + int got; + got = read(mon->fd, + mon->buffer + mon->bufferOffset, + avail - 1); + if (got < 0) { + if (errno == EAGAIN) + break; + virReportSystemError(errno, "%s", + _("Unable to read from monitor")); + ret = -1; + break; + } + if (got == 0) + break; + + ret += got; + avail -= got; + mon->bufferOffset += got; + mon->buffer[mon->bufferOffset] = '\0'; + } + +#if DEBUG_IO + VIR_DEBUG("Now read %zu bytes of data", mon->bufferOffset); +#endif + + return ret; +} + + +static void qemuAgentUpdateWatch(qemuAgentPtr mon) +{ + int events = + VIR_EVENT_HANDLE_HANGUP | + VIR_EVENT_HANDLE_ERROR; + + if (mon->lastError.code == VIR_ERR_OK) { + events |= VIR_EVENT_HANDLE_READABLE; + + if (mon->msg && mon->msg->txOffset < mon->msg->txLength) + events |= VIR_EVENT_HANDLE_WRITABLE; + } + + virEventUpdateHandle(mon->watch, events); +} + + +static void +qemuAgentIO(int watch, int fd, int events, void *opaque) { + qemuAgentPtr mon = opaque; + bool error = false; + bool eof = false; + + /* lock access to the monitor and protect fd */ + qemuAgentLock(mon); + qemuAgentRef(mon); +#if DEBUG_IO + VIR_DEBUG("Agent %p I/O on watch %d fd %d events %d", mon, watch, fd, events); +#endif + + if (mon->fd != fd || mon->watch != watch) { + if (events & (VIR_EVENT_HANDLE_HANGUP | VIR_EVENT_HANDLE_ERROR)) + eof = true; + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("event from unexpected fd %d!=%d / watch %d!=%d"), + mon->fd, fd, mon->watch, watch); + error = true; + } else if (mon->lastError.code != VIR_ERR_OK) { + if (events & (VIR_EVENT_HANDLE_HANGUP | VIR_EVENT_HANDLE_ERROR)) + eof = true; + error = true; + } else { + if (events & VIR_EVENT_HANDLE_WRITABLE) { + if (mon->connectPending) { + if (qemuAgentIOConnect(mon) < 0) + error = true; + } else { + if (qemuAgentIOWrite(mon) < 0) + error = true; + } + events &= ~VIR_EVENT_HANDLE_WRITABLE; + } + + if (!error && + events & VIR_EVENT_HANDLE_READABLE) { + int got = qemuAgentIORead(mon); + events &= ~VIR_EVENT_HANDLE_READABLE; + if (got < 0) { + error = true; + } else if (got == 0) { + eof = true; + } else { + /* Ignore hangup/error events if we read some data, to + * give time for that data to be consumed */ + events = 0; + + if (qemuAgentIOProcess(mon) < 0) + error = true; + } + } + + if (!error && + events & VIR_EVENT_HANDLE_HANGUP) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("End of file from monitor")); + eof = 1; + events &= ~VIR_EVENT_HANDLE_HANGUP; + } + + if (!error && !eof && + events & VIR_EVENT_HANDLE_ERROR) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Invalid file descriptor while waiting for monitor")); + eof = 1; + events &= ~VIR_EVENT_HANDLE_ERROR; + } + if (!error && events) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Unhandled event %d for monitor fd %d"), + events, mon->fd); + error = 1; + } + } + + if (error || eof) { + if (mon->lastError.code != VIR_ERR_OK) { + /* Already have an error, so clear any new error */ + virResetLastError(); + } else { + virErrorPtr err = virGetLastError(); + if (!err) + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("Error while processing monitor IO")); + virCopyLastError(&mon->lastError); + virResetLastError(); + } + + VIR_DEBUG("Error on monitor %s", NULLSTR(mon->lastError.message)); + /* If IO process resulted in an error & we have a message, + * then wakeup that waiter */ + if (mon->msg && !mon->msg->finished) { + mon->msg->finished = 1; + virCondSignal(&mon->notify); + } + } + + qemuAgentUpdateWatch(mon); + + /* We have to unlock to avoid deadlock against command thread, + * but is this safe ? I think it is, because the callback + * will try to acquire the virDomainObjPtr mutex next */ + if (eof) { + void (*eofNotify)(qemuAgentPtr, virDomainObjPtr) + = mon->cb->eofNotify; + virDomainObjPtr vm = mon->vm; + + /* Make sure anyone waiting wakes up now */ + virCondSignal(&mon->notify); + if (qemuAgentUnref(mon) > 0) + qemuAgentUnlock(mon); + VIR_DEBUG("Triggering EOF callback"); + (eofNotify)(mon, vm); + } else if (error) { + void (*errorNotify)(qemuAgentPtr, virDomainObjPtr) + = mon->cb->errorNotify; + virDomainObjPtr vm = mon->vm; + + /* Make sure anyone waiting wakes up now */ + virCondSignal(&mon->notify); + if (qemuAgentUnref(mon) > 0) + qemuAgentUnlock(mon); + VIR_DEBUG("Triggering error callback"); + (errorNotify)(mon, vm); + } else { + if (qemuAgentUnref(mon) > 0) + qemuAgentUnlock(mon); + } +} + + +qemuAgentPtr +qemuAgentOpen(virDomainObjPtr vm, + virDomainChrSourceDefPtr config, + qemuAgentCallbacksPtr cb) +{ + qemuAgentPtr mon; + + if (!cb || !cb->eofNotify) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("EOF notify callback must be supplied")); + return NULL; + } + + if (VIR_ALLOC(mon) < 0) { + virReportOOMError(); + return NULL; + } + + if (virMutexInit(&mon->lock) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("cannot initialize monitor mutex")); + VIR_FREE(mon); + return NULL; + } + if (virCondInit(&mon->notify) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("cannot initialize monitor condition")); + virMutexDestroy(&mon->lock); + VIR_FREE(mon); + return NULL; + } + mon->fd = -1; + mon->refs = 1; + mon->vm = vm; + mon->cb = cb; + qemuAgentLock(mon); + + switch (config->type) { + case VIR_DOMAIN_CHR_TYPE_UNIX: + mon->fd = qemuAgentOpenUnix(config->data.nix.path, vm->pid, + &mon->connectPending); + break; + + case VIR_DOMAIN_CHR_TYPE_PTY: + mon->fd = qemuAgentOpenPty(config->data.file.path); + break; + + default: + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("unable to handle monitor type: %s"), + virDomainChrTypeToString(config->type)); + goto cleanup; + } + + if (mon->fd == -1) + goto cleanup; + + if ((mon->watch = virEventAddHandle(mon->fd, + VIR_EVENT_HANDLE_HANGUP | + VIR_EVENT_HANDLE_ERROR | + VIR_EVENT_HANDLE_READABLE | + (mon->connectPending ? + VIR_EVENT_HANDLE_WRITABLE : + 0), + qemuAgentIO, + mon, qemuAgentUnwatch)) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("unable to register monitor events")); + goto cleanup; + } + qemuAgentRef(mon); + + VIR_DEBUG("New mon %p fd =%d watch=%d", mon, mon->fd, mon->watch); + qemuAgentUnlock(mon); + + return mon; + +cleanup: + /* We don't want the 'destroy' callback invoked during + * cleanup from construction failure, because that can + * give a double-unref on virDomainObjPtr in the caller, + * so kill the callbacks now. + */ + mon->cb = NULL; + qemuAgentUnlock(mon); + qemuAgentClose(mon); + return NULL; +} + + +void qemuAgentClose(qemuAgentPtr mon) +{ + if (!mon) + return; + + VIR_DEBUG("mon=%p", mon); + + qemuAgentLock(mon); + + if (mon->fd >= 0) { + if (mon->watch) + virEventRemoveHandle(mon->watch); + VIR_FORCE_CLOSE(mon->fd); + } + + if (qemuAgentUnref(mon) > 0) + qemuAgentUnlock(mon); +} + + +static int qemuAgentSend(qemuAgentPtr mon, + qemuAgentMessagePtr msg) +{ + int ret = -1; + + /* Check whether qemu quit unexpectedly */ + if (mon->lastError.code != VIR_ERR_OK) { + VIR_DEBUG("Attempt to send command while error is set %s", + NULLSTR(mon->lastError.message)); + virSetError(&mon->lastError); + return -1; + } + + mon->msg = msg; + qemuAgentUpdateWatch(mon); + + while (!mon->msg->finished) { + if (virCondWait(&mon->notify, &mon->lock) < 0) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("Unable to wait on monitor condition")); + goto cleanup; + } + } + + if (mon->lastError.code != VIR_ERR_OK) { + VIR_DEBUG("Send command resulted in error %s", + NULLSTR(mon->lastError.message)); + virSetError(&mon->lastError); + goto cleanup; + } + + ret = 0; + +cleanup: + mon->msg = NULL; + qemuAgentUpdateWatch(mon); + + return ret; +} + + +static int +qemuAgentCommand(qemuAgentPtr mon, + virJSONValuePtr cmd, + virJSONValuePtr *reply) +{ + int ret = -1; + qemuAgentMessage msg; + char *cmdstr = NULL; + + *reply = NULL; + + memset(&msg, 0, sizeof msg); + + if (!(cmdstr = virJSONValueToString(cmd))) { + virReportOOMError(); + goto cleanup; + } + if (virAsprintf(&msg.txBuffer, "%s" LINE_ENDING, cmdstr) < 0) { + virReportOOMError(); + goto cleanup; + } + msg.txLength = strlen(msg.txBuffer); + + VIR_DEBUG("Send command '%s' for write", cmdstr); + + ret = qemuAgentSend(mon, &msg); + + VIR_DEBUG("Receive command reply ret=%d rxObject=%p", + ret, msg.rxObject); + + + if (ret == 0) { + if (!msg.rxObject) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("Missing monitor reply object")); + ret = -1; + } else { + *reply = msg.rxObject; + } + } + +cleanup: + VIR_FREE(cmdstr); + VIR_FREE(msg.txBuffer); + + return ret; +} + + +/* Ignoring OOM in this method, since we're already reporting + * a more important error + * + * XXX see qerror.h for different klasses & fill out useful params + */ +static const char * +qemuAgentStringifyError(virJSONValuePtr error) +{ + const char *klass = virJSONValueObjectGetString(error, "class"); + const char *detail = NULL; + + /* The QMP 'desc' field is usually sufficient for our generic + * error reporting needs. + */ + if (klass) + detail = virJSONValueObjectGetString(error, "desc"); + + + if (!detail) + detail = "unknown QEMU command error"; + + return detail; +} + +static const char * +qemuAgentCommandName(virJSONValuePtr cmd) +{ + const char *name = virJSONValueObjectGetString(cmd, "execute"); + if (name) + return name; + else + return ""; +} + +static int +qemuAgentCheckError(virJSONValuePtr cmd, + virJSONValuePtr reply) +{ + if (virJSONValueObjectHasKey(reply, "error")) { + virJSONValuePtr error = virJSONValueObjectGet(reply, "error"); + char *cmdstr = virJSONValueToString(cmd); + char *replystr = virJSONValueToString(reply); + + /* Log the full JSON formatted command & error */ + VIR_DEBUG("unable to execute QEMU command %s: %s", + cmdstr, replystr); + + /* Only send the user the command name + friendly error */ + if (!error) + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("unable to execute QEMU command '%s'"), + qemuAgentCommandName(cmd)); + else + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("unable to execute QEMU command '%s': %s"), + qemuAgentCommandName(cmd), + qemuAgentStringifyError(error)); + + VIR_FREE(cmdstr); + VIR_FREE(replystr); + return -1; + } else if (!virJSONValueObjectHasKey(reply, "return")) { + char *cmdstr = virJSONValueToString(cmd); + char *replystr = virJSONValueToString(reply); + + VIR_DEBUG("Neither 'return' nor 'error' is set in the JSON reply %s: %s", + cmdstr, replystr); + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("unable to execute QEMU command '%s'"), + qemuAgentCommandName(cmd)); + VIR_FREE(cmdstr); + VIR_FREE(replystr); + return -1; + } + return 0; +} + +static virJSONValuePtr ATTRIBUTE_SENTINEL +qemuAgentMakeCommand(const char *cmdname, + ...) +{ + virJSONValuePtr obj; + virJSONValuePtr jargs = NULL; + va_list args; + char *key; + + va_start(args, cmdname); + + if (!(obj = virJSONValueNewObject())) + goto no_memory; + + if (virJSONValueObjectAppendString(obj, "execute", cmdname) < 0) + goto no_memory; + + while ((key = va_arg(args, char *)) != NULL) { + int ret; + char type; + + if (strlen(key) < 3) { + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("argument key '%s' is too short, missing type prefix"), + key); + goto error; + } + + /* Keys look like s:name the first letter is a type code */ + type = key[0]; + key += 2; + + if (!jargs && + !(jargs = virJSONValueNewObject())) + goto no_memory; + + /* This doesn't support maps/arrays. This hasn't + * proved to be a problem..... yet :-) */ + switch (type) { + case 's': { + char *val = va_arg(args, char *); + ret = virJSONValueObjectAppendString(jargs, key, val); + } break; + case 'i': { + int val = va_arg(args, int); + ret = virJSONValueObjectAppendNumberInt(jargs, key, val); + } break; + case 'u': { + unsigned int val = va_arg(args, unsigned int); + ret = virJSONValueObjectAppendNumberUint(jargs, key, val); + } break; + case 'I': { + long long val = va_arg(args, long long); + ret = virJSONValueObjectAppendNumberLong(jargs, key, val); + } break; + case 'U': { + /* qemu silently truncates numbers larger than LLONG_MAX, + * so passing the full range of unsigned 64 bit integers + * is not safe here. Pass them as signed 64 bit integers + * instead. + */ + long long val = va_arg(args, long long); + ret = virJSONValueObjectAppendNumberLong(jargs, key, val); + } break; + case 'd': { + double val = va_arg(args, double); + ret = virJSONValueObjectAppendNumberDouble(jargs, key, val); + } break; + case 'b': { + int val = va_arg(args, int); + ret = virJSONValueObjectAppendBoolean(jargs, key, val); + } break; + case 'n': { + ret = virJSONValueObjectAppendNull(jargs, key); + } break; + default: + qemuReportError(VIR_ERR_INTERNAL_ERROR, + _("unsupported data type '%c' for arg '%s'"), type, key - 2); + goto error; + } + if (ret < 0) + goto no_memory; + } + + if (jargs && + virJSONValueObjectAppend(obj, "arguments", jargs) < 0) + goto no_memory; + + va_end(args); + + return obj; + +no_memory: + virReportOOMError(); +error: + virJSONValueFree(obj); + virJSONValueFree(jargs); + va_end(args); + return NULL; +} + +VIR_ENUM_DECL(qemuAgentShutdownMode); + +VIR_ENUM_IMPL(qemuAgentShutdownMode, + QEMU_AGENT_SHUTDOWN_LAST, + "powerdown", "reboot", "halt"); + +int qemuAgentShutdown(qemuAgentPtr mon, + qemuAgentShutdownMode mode) +{ + int ret = -1; + virJSONValuePtr cmd; + virJSONValuePtr reply = NULL; + + cmd = qemuAgentMakeCommand("guest-shutdown", + "s:mode", qemuAgentShutdownModeTypeToString(mode), + NULL); + if (!cmd) + return -1; + + ret = qemuAgentCommand(mon, cmd, &reply); + + if (ret == 0) + ret = qemuAgentCheckError(cmd, reply); + + virJSONValueFree(cmd); + virJSONValueFree(reply); + return ret; +} diff --git a/src/qemu/qemu_agent.h b/src/qemu/qemu_agent.h new file mode 100644 index 0000000000000000000000000000000000000000..93c2ae7e2f0c7afae8ddd9ca76a2ac16c3d1c6be --- /dev/null +++ b/src/qemu/qemu_agent.h @@ -0,0 +1,69 @@ +/* + * qemu_agent.h: interaction with QEMU guest agent + * + * Copyright (C) 2006-2012 Red Hat, Inc. + * Copyright (C) 2006 Daniel P. Berrange + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author: Daniel P. Berrange + */ + + +#ifndef __QEMU_AGENT_H__ +# define __QEMU_AGENT_H__ + +# include "internal.h" +# include "domain_conf.h" + +typedef struct _qemuAgent qemuAgent; +typedef qemuAgent *qemuAgentPtr; + +typedef struct _qemuAgentCallbacks qemuAgentCallbacks; +typedef qemuAgentCallbacks *qemuAgentCallbacksPtr; +struct _qemuAgentCallbacks { + void (*destroy)(qemuAgentPtr mon, + virDomainObjPtr vm); + void (*eofNotify)(qemuAgentPtr mon, + virDomainObjPtr vm); + void (*errorNotify)(qemuAgentPtr mon, + virDomainObjPtr vm); +}; + + +qemuAgentPtr qemuAgentOpen(virDomainObjPtr vm, + virDomainChrSourceDefPtr config, + qemuAgentCallbacksPtr cb); + +void qemuAgentLock(qemuAgentPtr mon); +void qemuAgentUnlock(qemuAgentPtr mon); + +int qemuAgentRef(qemuAgentPtr mon); +int qemuAgentUnref(qemuAgentPtr mon) ATTRIBUTE_RETURN_CHECK; + +void qemuAgentClose(qemuAgentPtr mon); + +typedef enum { + QEMU_AGENT_SHUTDOWN_POWERDOWN, + QEMU_AGENT_SHUTDOWN_REBOOT, + QEMU_AGENT_SHUTDOWN_HALT, + + QEMU_AGENT_SHUTDOWN_LAST, +} qemuAgentShutdownMode; + +int qemuAgentShutdown(qemuAgentPtr mon, + qemuAgentShutdownMode mode); + +#endif /* __QEMU_AGENT_H__ */ diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c index cfe0ece449ff3233e5fe3e729bcb3d55deac52f9..d56e6176811873ed984ffe04b08008dc3ac6019f 100644 --- a/src/qemu/qemu_domain.c +++ b/src/qemu/qemu_domain.c @@ -219,6 +219,10 @@ static void qemuDomainObjPrivateFree(void *data) VIR_ERROR(_("Unexpected QEMU monitor still active during domain deletion")); qemuMonitorClose(priv->mon); } + if (priv->agent) { + VIR_ERROR(_("Unexpected QEMU agent still active during domain deletion")); + qemuAgentClose(priv->agent); + } VIR_FREE(priv); } @@ -1025,6 +1029,99 @@ void qemuDomainObjExitMonitorWithDriver(struct qemud_driver *driver, qemuDomainObjExitMonitorInternal(driver, true, obj); } + + +static int +qemuDomainObjEnterAgentInternal(struct qemud_driver *driver, + bool driver_locked, + virDomainObjPtr obj) +{ + qemuDomainObjPrivatePtr priv = obj->privateData; + + qemuAgentLock(priv->agent); + qemuAgentRef(priv->agent); + ignore_value(virTimeMillisNow(&priv->agentStart)); + virDomainObjUnlock(obj); + if (driver_locked) + qemuDriverUnlock(driver); + + return 0; +} + +static void ATTRIBUTE_NONNULL(1) +qemuDomainObjExitAgentInternal(struct qemud_driver *driver, + bool driver_locked, + virDomainObjPtr obj) +{ + qemuDomainObjPrivatePtr priv = obj->privateData; + int refs; + + refs = qemuAgentUnref(priv->agent); + + if (refs > 0) + qemuAgentUnlock(priv->agent); + + if (driver_locked) + qemuDriverLock(driver); + virDomainObjLock(obj); + + priv->agentStart = 0; + if (refs == 0) { + priv->agent = NULL; + } +} + +/* + * obj must be locked before calling, qemud_driver must be unlocked + * + * To be called immediately before any QEMU agent API call + * Must have already either called qemuDomainObjBeginJob() and checked + * that the VM is still active; + * + * To be followed with qemuDomainObjExitAgent() once complete + */ +void qemuDomainObjEnterAgent(struct qemud_driver *driver, + virDomainObjPtr obj) +{ + ignore_value(qemuDomainObjEnterAgentInternal(driver, false, obj)); +} + +/* obj must NOT be locked before calling, qemud_driver must be unlocked + * + * Should be paired with an earlier qemuDomainObjEnterAgent() call + */ +void qemuDomainObjExitAgent(struct qemud_driver *driver, + virDomainObjPtr obj) +{ + qemuDomainObjExitAgentInternal(driver, false, obj); +} + +/* + * obj must be locked before calling, qemud_driver must be locked + * + * To be called immediately before any QEMU agent API call + * Must have already either called qemuDomainObjBeginJobWithDriver() and + * checked that the VM is still active; may not be used for nested async jobs. + * + * To be followed with qemuDomainObjExitAgentWithDriver() once complete + */ +void qemuDomainObjEnterAgentWithDriver(struct qemud_driver *driver, + virDomainObjPtr obj) +{ + ignore_value(qemuDomainObjEnterAgentInternal(driver, true, obj)); +} + +/* obj must NOT be locked before calling, qemud_driver must be unlocked, + * and will be locked after returning + * + * Should be paired with an earlier qemuDomainObjEnterAgentWithDriver() call + */ +void qemuDomainObjExitAgentWithDriver(struct qemud_driver *driver, + virDomainObjPtr obj) +{ + qemuDomainObjExitAgentInternal(driver, true, obj); +} + void qemuDomainObjEnterRemoteWithDriver(struct qemud_driver *driver, virDomainObjPtr obj) { diff --git a/src/qemu/qemu_domain.h b/src/qemu/qemu_domain.h index f40fa0923f19cc939e67a72fed66e1e4fc37d225..222d80e400cd2a8621650444631dfd51e0d957a9 100644 --- a/src/qemu/qemu_domain.h +++ b/src/qemu/qemu_domain.h @@ -27,6 +27,7 @@ # include "threads.h" # include "domain_conf.h" # include "qemu_monitor.h" +# include "qemu_agent.h" # include "qemu_conf.h" # include "bitmap.h" @@ -102,6 +103,11 @@ struct _qemuDomainObjPrivate { int monJSON; bool monError; unsigned long long monStart; + + qemuAgentPtr agent; + bool agentError; + unsigned long long agentStart; + bool gotShutdown; bool beingDestroyed; char *pidfile; @@ -192,6 +198,22 @@ int qemuDomainObjEnterMonitorAsync(struct qemud_driver *driver, void qemuDomainObjExitMonitorWithDriver(struct qemud_driver *driver, virDomainObjPtr obj) ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); + + +void qemuDomainObjEnterAgent(struct qemud_driver *driver, + virDomainObjPtr obj) + ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); +void qemuDomainObjExitAgent(struct qemud_driver *driver, + virDomainObjPtr obj) + ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); +void qemuDomainObjEnterAgentWithDriver(struct qemud_driver *driver, + virDomainObjPtr obj) + ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); +void qemuDomainObjExitAgentWithDriver(struct qemud_driver *driver, + virDomainObjPtr obj) + ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); + + void qemuDomainObjEnterRemoteWithDriver(struct qemud_driver *driver, virDomainObjPtr obj) ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); diff --git a/src/qemu/qemu_monitor_json.c b/src/qemu/qemu_monitor_json.c index 4a76fc03322d275a618defe0f086be9ee07fbac5..11fdc0d792c78b13f5fb4d7c8bb4898820ef034c 100644 --- a/src/qemu/qemu_monitor_json.c +++ b/src/qemu/qemu_monitor_json.c @@ -59,7 +59,7 @@ static void qemuMonitorJSONHandleVNCInitialize(qemuMonitorPtr mon, virJSONValueP static void qemuMonitorJSONHandleVNCDisconnect(qemuMonitorPtr mon, virJSONValuePtr data); static void qemuMonitorJSONHandleBlockJob(qemuMonitorPtr mon, virJSONValuePtr data); -struct { +static struct { const char *type; void (*handler)(qemuMonitorPtr mon, virJSONValuePtr data); } eventHandlers[] = { diff --git a/src/qemu/qemu_process.c b/src/qemu/qemu_process.c index e16ca07d4d47e02adb4033fac12b74955bdac6f1..d22020bd2f1da58220ba0db9569982e1be18f229 100644 --- a/src/qemu/qemu_process.c +++ b/src/qemu/qemu_process.c @@ -106,6 +106,164 @@ qemuProcessRemoveDomainStatus(struct qemud_driver *driver, /* XXX figure out how to remove this */ extern struct qemud_driver *qemu_driver; +/* + * This is a callback registered with a qemuAgentPtr instance, + * and to be invoked when the agent console hits an end of file + * condition, or error, thus indicating VM shutdown should be + * performed + */ +static void +qemuProcessHandleAgentEOF(qemuAgentPtr agent ATTRIBUTE_UNUSED, + virDomainObjPtr vm) +{ + struct qemud_driver *driver = qemu_driver; + qemuDomainObjPrivatePtr priv; + + VIR_DEBUG("Received EOF from agent on %p '%s'", vm, vm->def->name); + + qemuDriverLock(driver); + virDomainObjLock(vm); + + priv = vm->privateData; + + qemuAgentClose(agent); + priv->agent = NULL; + + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); +} + + +/* + * This is invoked when there is some kind of error + * parsing data to/from the agent. The VM can continue + * to run, but no further agent commands will be + * allowed + */ +static void +qemuProcessHandleAgentError(qemuAgentPtr agent ATTRIBUTE_UNUSED, + virDomainObjPtr vm) +{ + struct qemud_driver *driver = qemu_driver; + qemuDomainObjPrivatePtr priv; + + VIR_DEBUG("Received error from agent on %p '%s'", vm, vm->def->name); + + qemuDriverLock(driver); + virDomainObjLock(vm); + + priv = vm->privateData; + + priv->agentError = true; + + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); +} + +static void qemuProcessHandleAgentDestroy(qemuAgentPtr agent, + virDomainObjPtr vm) +{ + qemuDomainObjPrivatePtr priv; + + virDomainObjLock(vm); + priv = vm->privateData; + if (priv->agent == agent) + priv->agent = NULL; + if (virDomainObjUnref(vm) > 0) + virDomainObjUnlock(vm); +} + + +static qemuAgentCallbacks agentCallbacks = { + .destroy = qemuProcessHandleAgentDestroy, + .eofNotify = qemuProcessHandleAgentEOF, + .errorNotify = qemuProcessHandleAgentError, +}; + +static virDomainChrSourceDefPtr +qemuFindAgentConfig(virDomainDefPtr def) +{ + virDomainChrSourceDefPtr config = NULL; + int i; + + for (i = 0 ; i < def->nchannels ; i++) { + virDomainChrDefPtr channel = def->channels[i]; + + if (channel->targetType != VIR_DOMAIN_CHR_CHANNEL_TARGET_TYPE_VIRTIO) + continue; + + if (STREQ(channel->target.name, "org.qemu.guest_agent.0")) { + config = &channel->source; + break; + } + } + + return config; +} + +static int +qemuConnectAgent(struct qemud_driver *driver, virDomainObjPtr vm) +{ + qemuDomainObjPrivatePtr priv = vm->privateData; + int ret = -1; + qemuAgentPtr agent = NULL; + virDomainChrSourceDefPtr config = qemuFindAgentConfig(vm->def); + + if (!config) + return 0; + + if (virSecurityManagerSetDaemonSocketLabel(driver->securityManager, + vm->def) < 0) { + VIR_ERROR(_("Failed to set security context for agent for %s"), + vm->def->name); + goto cleanup; + } + + /* Hold an extra reference because we can't allow 'vm' to be + * deleted while the agent is active */ + virDomainObjRef(vm); + + ignore_value(virTimeMillisNow(&priv->agentStart)); + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + + agent = qemuAgentOpen(vm, + config, + &agentCallbacks); + + qemuDriverLock(driver); + virDomainObjLock(vm); + priv->agentStart = 0; + + if (virSecurityManagerClearSocketLabel(driver->securityManager, + vm->def) < 0) { + VIR_ERROR(_("Failed to clear security context for agent for %s"), + vm->def->name); + goto cleanup; + } + + /* Safe to ignore value since ref count was incremented above */ + if (agent == NULL) + ignore_value(virDomainObjUnref(vm)); + + if (!virDomainObjIsActive(vm)) { + qemuAgentClose(agent); + goto cleanup; + } + priv->agent = agent; + + if (priv->agent == NULL) { + VIR_INFO("Failed to connect agent for %s", vm->def->name); + goto cleanup; + } + + ret = 0; + +cleanup: + return ret; +} + + /* * This is a callback registered with a qemuMonitorPtr instance, * and to be invoked when the monitor console hits an end of file @@ -2691,6 +2849,14 @@ qemuProcessReconnect(void *opaque) if (qemuConnectMonitor(driver, obj) < 0) goto error; + /* Failure to connect to agent shouldn't be fatal */ + if (qemuConnectAgent(driver, obj) < 0) { + VIR_WARN("Cannot connect to QEMU guest agent for %s", + obj->def->name); + virResetLastError(); + priv->agentError = true; + } + if (qemuUpdateActivePciHostdevs(driver, obj->def) < 0) { goto error; } @@ -3258,6 +3424,14 @@ int qemuProcessStart(virConnectPtr conn, if (qemuProcessWaitForMonitor(driver, vm, priv->qemuCaps, pos) < 0) goto cleanup; + /* Failure to connect to agent shouldn't be fatal */ + if (qemuConnectAgent(driver, vm) < 0) { + VIR_WARN("Cannot connect to QEMU guest agent for %s", + vm->def->name); + virResetLastError(); + priv->agentError = true; + } + VIR_DEBUG("Detecting VCPU PIDs"); if (qemuProcessDetectVcpuPIDs(driver, vm) < 0) goto cleanup; @@ -3460,6 +3634,12 @@ void qemuProcessStop(struct qemud_driver *driver, } } + if (priv->agent) { + qemuAgentClose(priv->agent); + priv->agent = NULL; + priv->agentError = false; + } + if (priv->mon) qemuMonitorClose(priv->mon); @@ -3713,6 +3893,14 @@ int qemuProcessAttach(virConnectPtr conn ATTRIBUTE_UNUSED, if (qemuProcessWaitForMonitor(driver, vm, priv->qemuCaps, -1) < 0) goto cleanup; + /* Failure to connect to agent shouldn't be fatal */ + if (qemuConnectAgent(driver, vm) < 0) { + VIR_WARN("Cannot connect to QEMU guest agent for %s", + vm->def->name); + virResetLastError(); + priv->agentError = true; + } + VIR_DEBUG("Detecting VCPU PIDs"); if (qemuProcessDetectVcpuPIDs(driver, vm) < 0) goto cleanup;