po/POTFILES | 1 + src/bhyve/bhyve_domain.c | 32 + src/bhyve/bhyve_domain.h | 14 + src/bhyve/bhyve_driver.c | 179 ++++- src/bhyve/bhyve_process.c | 117 ++++ src/bhyve/bhyve_process.h | 4 + src/bhyve/bhyve_qemu_agent.c | 1259 ++++++++++++++++++++++++++++++++++ src/bhyve/bhyve_qemu_agent.h | 197 ++++++ src/bhyve/meson.build | 5 +- src/remote/qemu_protocol.x | 18 - src/remote/remote_protocol.x | 20 +- 11 files changed, 1822 insertions(+), 24 deletions(-) create mode 100644 src/bhyve/bhyve_qemu_agent.c create mode 100644 src/bhyve/bhyve_qemu_agent.h
Implement QEMU Guest Agent support for bhyve. In bhyve it can configured
using the virtio-console device.
This change covers only two APIs using the agent:
- DomainQemuAgentCommand -- the most generic one.
- DomainGetHostname -- extended to support not only DHCP lease source,
but an agent as well.
There are two things that I'm not sure about this patch.
- The protocol files were updated to make DomainQemuAgentCommand generic
instead of Qemu specific. QEMU_PROC_DOMAIN_AGENT_COMMAND was removed in
favor of REMOTE_PROC_DOMAIN_QEMU_AGENT_COMMAND.
Even though the protocol is documented as private, I'm not sure how
desired this change is.
- src/qemu/bhyve_qemu_agent.c and src/qemu/qemu_agent.c should
share the same implementation. Ideally this should live
somewhere in src/util/virqemuagent.c, but as it is using things
from conf/, such as virDomainObj, it cannot be moved as is.
I considered extracting a simpler data structure that will not
use the conf/ types, but it didn't work very well for me and
I didn't like the way it looks in the driver code.
Maybe I'm missing some better approach?
Signed-off-by: Roman Bogorodskiy <bogorodskiy@gmail.com>
---
po/POTFILES | 1 +
src/bhyve/bhyve_domain.c | 32 +
src/bhyve/bhyve_domain.h | 14 +
src/bhyve/bhyve_driver.c | 179 ++++-
src/bhyve/bhyve_process.c | 117 ++++
src/bhyve/bhyve_process.h | 4 +
src/bhyve/bhyve_qemu_agent.c | 1259 ++++++++++++++++++++++++++++++++++
src/bhyve/bhyve_qemu_agent.h | 197 ++++++
src/bhyve/meson.build | 5 +-
src/remote/qemu_protocol.x | 18 -
src/remote/remote_protocol.x | 20 +-
11 files changed, 1822 insertions(+), 24 deletions(-)
create mode 100644 src/bhyve/bhyve_qemu_agent.c
create mode 100644 src/bhyve/bhyve_qemu_agent.h
diff --git a/po/POTFILES b/po/POTFILES
index a5f8705eb8..1d80d9a57a 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -19,6 +19,7 @@ src/bhyve/bhyve_firmware.c
src/bhyve/bhyve_monitor.c
src/bhyve/bhyve_parse_command.c
src/bhyve/bhyve_process.c
+src/bhyve/bhyve_qemu_agent.c
src/ch/ch_alias.c
src/ch/ch_conf.c
src/ch/ch_domain.c
diff --git a/src/bhyve/bhyve_domain.c b/src/bhyve/bhyve_domain.c
index 832a9b58d1..6367985efc 100644
--- a/src/bhyve/bhyve_domain.c
+++ b/src/bhyve/bhyve_domain.c
@@ -41,6 +41,7 @@ bhyveDomainObjPrivateAlloc(void *opaque)
{
bhyveDomainObjPrivate *priv = g_new0(bhyveDomainObjPrivate, 1);
+ priv->agentTimeout = 30;
priv->driver = opaque;
return priv;
@@ -663,3 +664,34 @@ virXMLNamespace virBhyveDriverDomainXMLNamespace = {
.uri = "http://libvirt.org/schemas/domain/bhyve/1.0",
};
+
+
+int
+virBhyveDomainObjStartWorker(virDomainObj *dom)
+{
+ bhyveDomainObjPrivate *priv = dom->privateData;
+
+ if (!priv->eventThread) {
+ g_autofree char *threadName = g_strdup_printf("vm-%s", dom->def->name);
+ if (!(priv->eventThread = virEventThreadNew(threadName)))
+ return -1;
+ }
+
+ return 0;
+}
+
+
+void
+virBhyveDomainObjStopWorker(virDomainObj *dom)
+{
+ bhyveDomainObjPrivate *priv = dom->privateData;
+ virEventThread *eventThread;
+
+ if (!priv->eventThread)
+ return;
+
+ eventThread = g_steal_pointer(&priv->eventThread);
+ virObjectUnlock(dom);
+ g_object_unref(eventThread);
+ virObjectLock(dom);
+}
diff --git a/src/bhyve/bhyve_domain.h b/src/bhyve/bhyve_domain.h
index 5a539bc4c0..8e3663f4c0 100644
--- a/src/bhyve/bhyve_domain.h
+++ b/src/bhyve/bhyve_domain.h
@@ -22,8 +22,10 @@
#include "domain_addr.h"
#include "domain_conf.h"
+#include "vireventthread.h"
#include "bhyve_monitor.h"
+#include "bhyve_qemu_agent.h"
typedef struct _bhyveDomainObjPrivate bhyveDomainObjPrivate;
struct _bhyveDomainObjPrivate {
@@ -33,10 +35,22 @@ struct _bhyveDomainObjPrivate {
bool persistentAddrs;
bhyveMonitor *mon;
+
+ qemuAgent *agent;
+ bool agentError;
+ int agentTimeout;
+
+ virEventThread *eventThread;
};
+#define BHYVE_DOMAIN_PRIVATE(vm) \
+ ((bhyveDomainObjPrivate *) (vm)->privateData)
+
virDomainXMLOption *virBhyveDriverCreateXMLConf(struct _bhyveConn *);
extern virDomainXMLPrivateDataCallbacks virBhyveDriverPrivateDataCallbacks;
extern virDomainDefParserConfig virBhyveDriverDomainDefParserConfig;
extern virXMLNamespace virBhyveDriverDomainXMLNamespace;
+
+int virBhyveDomainObjStartWorker(virDomainObj *dom);
+void virBhyveDomainObjStopWorker(virDomainObj *dom);
diff --git a/src/bhyve/bhyve_driver.c b/src/bhyve/bhyve_driver.c
index c8dd1a728a..8bffbbec43 100644
--- a/src/bhyve/bhyve_driver.c
+++ b/src/bhyve/bhyve_driver.c
@@ -1895,6 +1895,165 @@ bhyveDomainInterfaceAddresses(virDomainPtr domain,
}
+static qemuAgent *
+bhyveDomainObjEnterAgent(virDomainObj *obj)
+{
+ bhyveDomainObjPrivate *priv = obj->privateData;
+ qemuAgent *agent = priv->agent;
+
+ VIR_DEBUG("Entering agent (agent=%p vm=%p name=%s)",
+ priv->agent, obj, obj->def->name);
+
+ virObjectLock(agent);
+ virObjectRef(agent);
+ virObjectUnlock(obj);
+
+ return agent;
+}
+
+
+static void
+bhyveDomainObjExitAgent(virDomainObj *obj, qemuAgent *agent)
+{
+ virObjectUnlock(agent);
+ virObjectUnref(agent);
+ virObjectLock(obj);
+
+ VIR_DEBUG("Exited agent (agent=%p vm=%p name=%s)",
+ agent, obj, obj->def->name);
+}
+
+
+static bool
+bhyveDomainAgentAvailable(virDomainObj *vm,
+ bool reportError)
+{
+ bhyveDomainObjPrivate *priv = vm->privateData;
+
+ if (virDomainObjGetState(vm, NULL) != VIR_DOMAIN_RUNNING) {
+ if (reportError) {
+ virReportError(VIR_ERR_OPERATION_INVALID, "%s",
+ _("domain is not running"));
+ }
+ return false;
+ }
+
+ if (!priv->agent) {
+ if (bhyveFindAgentConfig(vm->def)) {
+ if (reportError) {
+ virReportError(VIR_ERR_AGENT_UNRESPONSIVE, "%s",
+ _("QEMU guest agent is not connected"));
+ }
+ return false;
+ } else {
+ if (reportError) {
+ virReportError(VIR_ERR_ARGUMENT_UNSUPPORTED, "%s",
+ _("QEMU guest agent is not configured"));
+ }
+ return false;
+ }
+ }
+ return true;
+}
+
+
+static int
+bhyveDomainEnsureAgent(virDomainObj *vm,
+ bool reportError)
+{
+ bhyveDomainObjPrivate *priv = vm->privateData;
+
+ if (virDomainObjGetState(vm, NULL) != VIR_DOMAIN_RUNNING) {
+ if (reportError) {
+ virReportError(VIR_ERR_OPERATION_INVALID, "%s",
+ _("domain is not running"));
+ }
+ return -1;
+ }
+
+ if (priv->agent)
+ return 0;
+
+ if (!priv->eventThread &&
+ virBhyveDomainObjStartWorker(vm) < 0)
+ return -1;
+
+ if (bhyveConnectAgent(NULL, vm) < 0)
+ return -1;
+
+ return 0;
+}
+
+
+static int
+bhyveDomainGetHostnameAgent(virDomainObj *vm,
+ char **hostname)
+{
+ qemuAgent *agent;
+ int ret = -1;
+
+ if (virDomainObjBeginAgentJob(vm, VIR_AGENT_JOB_QUERY) < 0)
+ return -1;
+
+ if (virDomainObjCheckActive(vm) < 0)
+ goto endjob;
+
+ if (bhyveDomainEnsureAgent(vm, true) < 0)
+ goto endjob;
+
+ agent = bhyveDomainObjEnterAgent(vm);
+ ret = qemuAgentGetHostname(agent, hostname, true);
+ bhyveDomainObjExitAgent(vm, agent);
+
+ endjob:
+ virDomainObjEndAgentJob(vm);
+ return ret;
+}
+
+
+static char *
+bhyveDomainQemuAgentCommand(virDomainPtr domain,
+ const char *cmd,
+ int timeout,
+ unsigned int flags)
+{
+ virDomainObj *vm;
+ int ret = -1;
+ char *result = NULL;
+ qemuAgent *agent;
+
+ virCheckFlags(0, NULL);
+
+ if (!(vm = bhyveDomObjFromDomain(domain)))
+ goto cleanup;
+
+ if (virDomainQemuAgentCommandEnsureACL(domain->conn, vm->def) < 0)
+ goto cleanup;
+
+ if (virDomainObjBeginAgentJob(vm, VIR_AGENT_JOB_MODIFY) < 0)
+ goto cleanup;
+
+ if (virDomainObjCheckActive(vm) < 0)
+ goto endjob;
+
+ if (!bhyveDomainAgentAvailable(vm, true))
+ goto endjob;
+
+ agent = bhyveDomainObjEnterAgent(vm);
+ ret = qemuAgentArbitraryCommand(agent, cmd, &result, timeout);
+ bhyveDomainObjExitAgent(vm, agent);
+ if (ret < 0)
+ VIR_FREE(result);
+
+ endjob:
+ virDomainObjEndAgentJob(vm);
+
+ cleanup:
+ virDomainObjEndAPI(&vm);
+ return result;
+}
+
+
static int
bhyveDomainGetHostnameLease(virDomainObj *vm,
char **hostname)
@@ -1961,7 +2120,15 @@ bhyveDomainGetHostname(virDomainPtr domain,
virDomainObj *vm = NULL;
char *hostname = NULL;
- virCheckFlags(VIR_DOMAIN_GET_HOSTNAME_LEASE, NULL);
+ virCheckFlags(VIR_DOMAIN_GET_HOSTNAME_LEASE |
+ VIR_DOMAIN_GET_HOSTNAME_AGENT, NULL);
+
+ VIR_EXCLUSIVE_FLAGS_RET(VIR_DOMAIN_GET_HOSTNAME_LEASE,
+ VIR_DOMAIN_GET_HOSTNAME_AGENT,
+ NULL);
+
+ if (!(flags & VIR_DOMAIN_GET_HOSTNAME_AGENT))
+ flags |= VIR_DOMAIN_GET_HOSTNAME_LEASE;
if (!(vm = bhyveDomObjFromDomain(domain)))
return NULL;
@@ -1969,8 +2136,13 @@ bhyveDomainGetHostname(virDomainPtr domain,
if (virDomainGetHostnameEnsureACL(domain->conn, vm->def) < 0)
goto cleanup;
- if (bhyveDomainGetHostnameLease(vm, &hostname) < 0)
- goto cleanup;
+ if (flags & VIR_DOMAIN_GET_HOSTNAME_LEASE) {
+ if (bhyveDomainGetHostnameLease(vm, &hostname) < 0)
+ goto cleanup;
+ } else if (flags & VIR_DOMAIN_GET_HOSTNAME_AGENT) {
+ if (bhyveDomainGetHostnameAgent(vm, &hostname) < 0)
+ goto cleanup;
+ }
if (!hostname) {
virReportError(VIR_ERR_NO_HOSTNAME,
@@ -2052,6 +2224,7 @@ static virHypervisorDriver bhyveHypervisorDriver = {
.domainGetVcpuPinInfo = bhyveDomainGetVcpuPinInfo, /* 12.1.0 */
.domainInterfaceAddresses = bhyveDomainInterfaceAddresses, /* 12.3.0 */
.domainGetHostname = bhyveDomainGetHostname, /* 12.3.0 */
+ .domainQemuAgentCommand = bhyveDomainQemuAgentCommand, /* 12.4.0 */
};
diff --git a/src/bhyve/bhyve_process.c b/src/bhyve/bhyve_process.c
index 6078d995cd..fc97731510 100644
--- a/src/bhyve/bhyve_process.c
+++ b/src/bhyve/bhyve_process.c
@@ -171,6 +171,117 @@ bhyveSetResourceLimits(struct _bhyveConn *driver, virDomainObj *vm)
return 0;
}
+virDomainChrDef *
+bhyveFindAgentConfig(virDomainDef *def)
+{
+ size_t i;
+
+ for (i = 0; i < def->nchannels; i++) {
+ virDomainChrDef *channel = def->channels[i];
+
+ if (channel->targetType != VIR_DOMAIN_CHR_CHANNEL_TARGET_TYPE_VIRTIO)
+ continue;
+
+
+ if (STREQ_NULLABLE(channel->target.name, "org.qemu.guest_agent.0")) {
+ return channel;
+ }
+ }
+
+ return NULL;
+}
+
+static void
+qemuProcessHandleAgentEOF(qemuAgent *agent,
+ virDomainObj *vm)
+{
+ bhyveDomainObjPrivate *priv;
+
+ virObjectLock(vm);
+ VIR_INFO("Received EOF from agent on %p '%s'", vm, vm->def->name);
+
+ priv = vm->privateData;
+
+ if (!priv->agent) {
+ VIR_DEBUG("Agent freed already");
+ goto unlock;
+ }
+
+ qemuAgentClose(agent);
+ priv->agent = NULL;
+ priv->agentError = false;
+
+ virObjectUnlock(vm);
+ return;
+
+ unlock:
+ virObjectUnlock(vm);
+ return;
+}
+
+/*
+ * 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(qemuAgent *agent G_GNUC_UNUSED,
+ virDomainObj *vm)
+{
+ bhyveDomainObjPrivate *priv;
+
+ virObjectLock(vm);
+ VIR_INFO("Received error from agent on %p '%s'", vm, vm->def->name);
+
+ priv = vm->privateData;
+
+ priv->agentError = true;
+
+ virObjectUnlock(vm);
+}
+
+static qemuAgentCallbacks agentCallbacks = {
+ .eofNotify = qemuProcessHandleAgentEOF,
+ .errorNotify = qemuProcessHandleAgentError,
+};
+
+int
+bhyveConnectAgent(struct _bhyveConn *driver G_GNUC_UNUSED, virDomainObj *vm)
+{
+ bhyveDomainObjPrivate *priv = vm->privateData;
+ qemuAgent *agent = NULL;
+ virDomainChrDef *config = bhyveFindAgentConfig(vm->def);
+
+ if (!config)
+ return 0;
+
+ if (priv->agent)
+ return 0;
+
+ agent = qemuAgentOpen(vm,
+ config->source,
+ virEventThreadGetContext(priv->eventThread),
+ &agentCallbacks);
+
+ if (!virDomainObjIsActive(vm)) {
+ qemuAgentClose(agent);
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("guest crashed while connecting to the guest agent"));
+ return -1;
+ }
+
+ priv->agent = agent;
+ if (!priv->agent) {
+ VIR_WARN("Cannot connect to QEMU guest agent for %s", vm->def->name);
+ priv->agentError = true;
+ virResetLastError();
+ }
+
+ return 0;
+}
+
+
static int
virBhyveProcessStartImpl(struct _bhyveConn *driver,
virDomainObj *vm,
@@ -293,6 +404,9 @@ virBhyveProcessStartImpl(struct _bhyveConn *driver,
virDomainObjSetState(vm, VIR_DOMAIN_RUNNING, reason);
priv->mon = bhyveMonitorOpen(vm, driver);
+ if (virBhyveDomainObjStartWorker(vm) < 0)
+ goto cleanup;
+
if (virDomainObjSave(vm, driver->xmlopt,
BHYVE_STATE_DIR) < 0)
goto cleanup;
@@ -714,6 +828,9 @@ virBhyveProcessReconnect(virDomainObj *vm,
virDomainNetNotifyActualDevice(conn, vm->def, net);
}
+ if (virBhyveDomainObjStartWorker(vm) < 0)
+ goto cleanup;
+
cleanup:
if (ret < 0) {
/* If VM is reported to be in active state, but we cannot find
diff --git a/src/bhyve/bhyve_process.h b/src/bhyve/bhyve_process.h
index 5e0acc810c..bf82f748a6 100644
--- a/src/bhyve/bhyve_process.h
+++ b/src/bhyve/bhyve_process.h
@@ -56,6 +56,10 @@ int virBhyveGetDomainTotalCpuStats(virDomainObj *vm,
void virBhyveProcessReconnectAll(struct _bhyveConn *driver);
+int bhyveConnectAgent(struct _bhyveConn *driver, virDomainObj *vm);
+
+virDomainChrDef *bhyveFindAgentConfig(virDomainDef *def);
+
typedef enum {
VIR_BHYVE_PROCESS_START_AUTODESTROY = 1 << 0,
} bhyveProcessStartFlags;
diff --git a/src/bhyve/bhyve_qemu_agent.c b/src/bhyve/bhyve_qemu_agent.c
new file mode 100644
index 0000000000..619d8baf57
--- /dev/null
+++ b/src/bhyve/bhyve_qemu_agent.c
@@ -0,0 +1,1259 @@
+/*
+ * bhyve_qemu_agent.c: interaction with QEMU guest agent
+ *
+ * Copyright (C) 2006-2014 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, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include <poll.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/time.h>
+#include <gio/gio.h>
+
+#include "bhyve_qemu_agent.h"
+#include "bhyve_domain.h"
+#include "viralloc.h"
+#include "virlog.h"
+#include "virerror.h"
+#include "virjson.h"
+#include "virfile.h"
+#include "virtime.h"
+#include "virobject.h"
+#include "virstring.h"
+#include "virenum.h"
+#include "virutil.h"
+
+#define VIR_FROM_THIS VIR_FROM_QEMU
+
+VIR_LOG_INIT("bhyve.bhyve_qemu_agent");
+
+#define LINE_ENDING "\n"
+
+/* We read from QEMU until seeing a \r\n pair to indicate a
+ * completed reply or event. To avoid memory denial-of-service
+ * though, we must have a size limit on amount of data we
+ * buffer. 10 MB is large enough that it ought to cope with
+ * normal QEMU replies, and small enough that we're not
+ * consuming unreasonable mem.
+ */
+#define QEMU_AGENT_MAX_RESPONSE (10 * 1024 * 1024)
+
+typedef struct _qemuAgentMessage qemuAgentMessage;
+struct _qemuAgentMessage {
+ char *txBuffer;
+ int txOffset;
+ int txLength;
+
+ /* Used by the JSON agent to hold reply / error */
+ char *rxBuffer;
+ int rxLength;
+ void *rxObject;
+
+ /* True if rxBuffer / rxObject are ready, or a
+ * fatal error occurred on the agent channel
+ */
+ bool finished;
+ /* true for sync command */
+ bool sync;
+ /* id of the issued sync command */
+ unsigned long long id;
+ bool first;
+};
+
+
+struct _qemuAgent {
+ virObjectLockable parent;
+
+ virCond notify;
+
+ int fd;
+
+ GMainContext *context;
+ GSocket *socket;
+ GSource *watch;
+
+ bool running;
+ bool inSync;
+
+ virDomainObj *vm;
+
+ qemuAgentCallbacks *cb;
+
+ /* If there's a command being processed this will be
+ * non-NULL */
+ qemuAgentMessage *msg;
+
+ /* Buffer incoming data ready for agent
+ * 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 agent msg */
+ virError lastError;
+
+ /* Some guest agent commands don't return anything
+ * but fire up an event on qemu agent instead.
+ * Take that as indication of successful completion */
+ qemuAgentEvent await_event;
+ int timeout;
+};
+
+static virClass *qemuAgentClass;
+static void qemuAgentDispose(void *obj);
+
+static int qemuAgentOnceInit(void)
+{
+ if (!VIR_CLASS_NEW(qemuAgent, virClassForObjectLockable()))
+ return -1;
+
+ return 0;
+}
+
+VIR_ONCE_GLOBAL_INIT(qemuAgent);
+
+
+
+static void qemuAgentDispose(void *obj)
+{
+ qemuAgent *agent = obj;
+
+ VIR_DEBUG("agent=%p", agent);
+
+ if (agent->vm)
+ virObjectUnref(agent->vm);
+ virCondDestroy(&agent->notify);
+ g_free(agent->buffer);
+ g_main_context_unref(agent->context);
+ virResetError(&agent->lastError);
+}
+
+static int
+qemuAgentOpenUnix(const char *socketpath)
+{
+ struct sockaddr_un addr = { 0 };
+ int agentfd;
+
+ if ((agentfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
+ virReportSystemError(errno,
+ "%s", _("failed to create socket"));
+ return -1;
+ }
+
+ if (virSetCloseExec(agentfd) < 0) {
+ virReportSystemError(errno, "%s",
+ _("Unable to set agent close-on-exec flag"));
+ goto error;
+ }
+
+ addr.sun_family = AF_UNIX;
+ if (virStrcpyStatic(addr.sun_path, socketpath) < 0) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Socket path %1$s too big for destination"), socketpath);
+ goto error;
+ }
+
+ if (connect(agentfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ virReportSystemError(errno, "%s",
+ _("failed to connect to agent socket"));
+ goto error;
+ }
+
+ return agentfd;
+
+ error:
+ VIR_FORCE_CLOSE(agentfd);
+ return -1;
+}
+
+
+static int
+qemuAgentIOProcessEvent(qemuAgent *agent,
+ virJSONValue *obj)
+{
+ const char *type;
+ VIR_DEBUG("agent=%p obj=%p", agent, obj);
+
+ type = virJSONValueObjectGetString(obj, "event");
+ if (!type) {
+ VIR_WARN("missing event type in message");
+ errno = EINVAL;
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+qemuAgentIOProcessLine(qemuAgent *agent,
+ const char *line,
+ qemuAgentMessage *msg)
+{
+ g_autoptr(virJSONValue) obj = NULL;
+
+ VIR_DEBUG("Line [%s]", line);
+
+ if (!(obj = virJSONValueFromString(line))) {
+ /* receiving garbage on first sync is regular situation */
+ if (msg && msg->sync && msg->first) {
+ VIR_DEBUG("Received garbage on sync");
+ msg->finished = true;
+ return 0;
+ }
+
+ return -1;
+ }
+
+ if (virJSONValueGetType(obj) != VIR_JSON_TYPE_OBJECT) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Parsed JSON reply '%1$s' isn't an object"), line);
+ return -1;
+ }
+
+ if (virJSONValueObjectHasKey(obj, "QMP")) {
+ return 0;
+ } else if (virJSONValueObjectHasKey(obj, "event")) {
+ return qemuAgentIOProcessEvent(agent, obj);
+ } else if (virJSONValueObjectHasKey(obj, "error") ||
+ virJSONValueObjectHasKey(obj, "return")) {
+ if (msg) {
+ if (msg->sync) {
+ unsigned long long id;
+
+ if (virJSONValueObjectGetNumberUlong(obj, "return", &id) < 0) {
+ VIR_DEBUG("Ignoring delayed reply on sync");
+ return 0;
+ }
+
+ VIR_DEBUG("Guest returned ID: %llu", id);
+
+ if (msg->id != id) {
+ VIR_DEBUG("Guest agent returned ID: %llu instead of %llu",
+ id, msg->id);
+ return 0;
+ }
+ }
+ msg->rxObject = g_steal_pointer(&obj);
+ msg->finished = true;
+ } else {
+ /* we are out of sync */
+ VIR_DEBUG("Ignoring delayed reply");
+ }
+
+ return 0;
+ }
+
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Unknown JSON reply '%1$s'"), line);
+ return -1;
+}
+
+static int qemuAgentIOProcessData(qemuAgent *agent,
+ char *data,
+ size_t len,
+ qemuAgentMessage *msg)
+{
+ int used = 0;
+ size_t i = 0;
+
+ 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(agent, 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 agent. Looking for async events and
+ * replies/errors.
+ */
+static int
+qemuAgentIOProcess(qemuAgent *agent)
+{
+ int len;
+ qemuAgentMessage *msg = NULL;
+
+ /* See if there's a message ready for reply; that is,
+ * one that has completed writing all its data.
+ */
+ if (agent->msg && agent->msg->txOffset == agent->msg->txLength)
+ msg = agent->msg;
+
+ len = qemuAgentIOProcessData(agent,
+ agent->buffer, agent->bufferOffset,
+ msg);
+
+ if (len < 0)
+ return -1;
+
+ if (len < agent->bufferOffset) {
+ memmove(agent->buffer, agent->buffer + len, agent->bufferOffset - len);
+ agent->bufferOffset -= len;
+ } else {
+ VIR_FREE(agent->buffer);
+ agent->bufferOffset = agent->bufferLength = 0;
+ }
+ if (msg && msg->finished)
+ virCondBroadcast(&agent->notify);
+ return len;
+}
+
+
+/*
+ * Called when the agent is able to write data
+ * Call this function while holding the agent lock.
+ */
+static int
+qemuAgentIOWrite(qemuAgent *agent)
+{
+ int done;
+
+ /* If no active message, or fully transmitted, then no-op */
+ if (!agent->msg || agent->msg->txOffset == agent->msg->txLength)
+ return 0;
+
+ done = safewrite(agent->fd,
+ agent->msg->txBuffer + agent->msg->txOffset,
+ agent->msg->txLength - agent->msg->txOffset);
+
+ if (done < 0) {
+ if (errno == EAGAIN)
+ return 0;
+
+ virReportSystemError(errno, "%s",
+ _("Unable to write to agent"));
+ return -1;
+ }
+ agent->msg->txOffset += done;
+ return done;
+}
+
+/*
+ * Called when the agent has incoming data to read
+ * Call this function while holding the agent lock.
+ *
+ * Returns -1 on error, or number of bytes read
+ */
+static int
+qemuAgentIORead(qemuAgent *agent)
+{
+ size_t avail = agent->bufferLength - agent->bufferOffset;
+ int ret = 0;
+
+ if (avail < 1024) {
+ if (agent->bufferLength >= QEMU_AGENT_MAX_RESPONSE) {
+ virReportSystemError(ERANGE,
+ _("No complete agent response found in %1$d bytes"),
+ QEMU_AGENT_MAX_RESPONSE);
+ return -1;
+ }
+ VIR_REALLOC_N(agent->buffer, agent->bufferLength + 1024);
+ agent->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(agent->fd,
+ agent->buffer + agent->bufferOffset,
+ avail - 1);
+ if (got < 0) {
+ if (errno == EAGAIN)
+ break;
+ virReportSystemError(errno, "%s",
+ _("Unable to read from agent"));
+ ret = -1;
+ break;
+ }
+ if (got == 0)
+ break;
+
+ ret += got;
+ avail -= got;
+ agent->bufferOffset += got;
+ agent->buffer[agent->bufferOffset] = '\0';
+ }
+
+ return ret;
+}
+
+
+static gboolean
+qemuAgentIO(GSocket *socket,
+ GIOCondition cond,
+ gpointer opaque);
+
+
+static void
+qemuAgentRegister(qemuAgent *agent)
+{
+ GIOCondition cond = 0;
+
+ if (agent->lastError.code == VIR_ERR_OK) {
+ cond |= G_IO_IN;
+
+ if (agent->msg && agent->msg->txOffset < agent->msg->txLength)
+ cond |= G_IO_OUT;
+ }
+
+ agent->watch = g_socket_create_source(agent->socket,
+ cond,
+ NULL);
+
+ virObjectRef(agent);
+ g_source_set_callback(agent->watch,
+ (GSourceFunc)qemuAgentIO,
+ agent,
+ (GDestroyNotify)virObjectUnref);
+
+ g_source_attach(agent->watch,
+ agent->context);
+}
+
+
+static void
+qemuAgentUnregister(qemuAgent *agent)
+{
+ if (agent->watch) {
+ g_source_destroy(agent->watch);
+ g_source_unref(agent->watch);
+ agent->watch = NULL;
+ }
+}
+
+
+static void qemuAgentUpdateWatch(qemuAgent *agent)
+{
+ qemuAgentUnregister(agent);
+ if (agent->socket)
+ qemuAgentRegister(agent);
+}
+
+
+static gboolean
+qemuAgentIO(GSocket *socket G_GNUC_UNUSED,
+ GIOCondition cond,
+ gpointer opaque)
+{
+ qemuAgent *agent = opaque;
+ bool error = false;
+ bool eof = false;
+
+ virObjectRef(agent);
+ /* lock access to the agent and protect fd */
+ virObjectLock(agent);
+
+ if (agent->fd == -1 || !agent->watch) {
+ virObjectUnlock(agent);
+ virObjectUnref(agent);
+ return G_SOURCE_REMOVE;
+ }
+
+ if (agent->lastError.code != VIR_ERR_OK) {
+ if (cond & (G_IO_HUP | G_IO_ERR))
+ eof = true;
+ error = true;
+ } else {
+ if (cond & G_IO_OUT) {
+ if (qemuAgentIOWrite(agent) < 0)
+ error = true;
+ }
+
+ if (!error &&
+ cond & G_IO_IN) {
+ int got = qemuAgentIORead(agent);
+ if (got < 0) {
+ error = true;
+ } else if (got == 0) {
+ eof = true;
+ } else {
+ /* Ignore hangup/error cond if we read some data, to
+ * give time for that data to be consumed */
+ cond = 0;
+
+ if (qemuAgentIOProcess(agent) < 0)
+ error = true;
+ }
+ }
+
+ if (!error &&
+ cond & G_IO_HUP) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("End of file from agent socket"));
+ eof = true;
+ }
+
+ if (!error && !eof &&
+ cond & G_IO_ERR) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("Invalid file descriptor while waiting for agent"));
+ eof = true;
+ }
+ }
+
+ if (error || eof) {
+ if (agent->lastError.code != VIR_ERR_OK) {
+ /* Already have an error, so clear any new error */
+ virResetLastError();
+ } else {
+ if (virGetLastErrorCode() == VIR_ERR_OK)
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("Error while processing agent IO"));
+ virCopyLastError(&agent->lastError);
+ virResetLastError();
+ }
+
+ VIR_DEBUG("Error on agent %s", NULLSTR(agent->lastError.message));
+ /* If IO process resulted in an error & we have a message,
+ * then wakeup that waiter */
+ if (agent->msg && !agent->msg->finished) {
+ agent->msg->finished = true;
+ virCondSignal(&agent->notify);
+ }
+ }
+
+ qemuAgentUpdateWatch(agent);
+
+ /* 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 virDomainObj *mutex next */
+ if (eof) {
+ void (*eofNotify)(qemuAgent *, virDomainObj *)
+ = agent->cb->eofNotify;
+ virDomainObj *vm = agent->vm;
+
+ /* Make sure anyone waiting wakes up now */
+ virCondSignal(&agent->notify);
+ virObjectUnlock(agent);
+ virObjectUnref(agent);
+ VIR_DEBUG("Triggering EOF callback");
+ (eofNotify)(agent, vm);
+ } else if (error) {
+ void (*errorNotify)(qemuAgent *, virDomainObj *)
+ = agent->cb->errorNotify;
+ virDomainObj *vm = agent->vm;
+
+ /* Make sure anyone waiting wakes up now */
+ virCondSignal(&agent->notify);
+ virObjectUnlock(agent);
+ virObjectUnref(agent);
+ VIR_DEBUG("Triggering error callback");
+ (errorNotify)(agent, vm);
+ } else {
+ virObjectUnlock(agent);
+ virObjectUnref(agent);
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+
+qemuAgent *
+qemuAgentOpen(virDomainObj *vm,
+ const virDomainChrSourceDef *config,
+ GMainContext *context,
+ qemuAgentCallbacks *cb)
+{
+ qemuAgent *agent;
+ g_autoptr(GError) gerr = NULL;
+
+ if (!cb || !cb->eofNotify) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("EOF notify callback must be supplied"));
+ return NULL;
+ }
+
+ if (qemuAgentInitialize() < 0)
+ return NULL;
+
+ if (!(agent = virObjectLockableNew(qemuAgentClass)))
+ return NULL;
+
+ agent->timeout = BHYVE_DOMAIN_PRIVATE(vm)->agentTimeout;
+ agent->fd = -1;
+ if (virCondInit(&agent->notify) < 0) {
+ virReportSystemError(errno, "%s",
+ _("cannot initialize agent condition"));
+ virObjectUnref(agent);
+ return NULL;
+ }
+ agent->vm = virObjectRef(vm);
+ agent->cb = cb;
+
+ if (config->type != VIR_DOMAIN_CHR_TYPE_UNIX) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("unable to handle agent type: %1$s"),
+ virDomainChrTypeToString(config->type));
+ goto cleanup;
+ }
+
+ virObjectUnlock(vm);
+ agent->fd = qemuAgentOpenUnix(config->data.nix.path);
+ virObjectLock(vm);
+
+ if (agent->fd == -1)
+ goto cleanup;
+
+ agent->context = g_main_context_ref(context);
+
+ agent->socket = g_socket_new_from_fd(agent->fd, &gerr);
+ if (!agent->socket) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Unable to create socket object: %1$s"),
+ gerr->message);
+ goto cleanup;
+ }
+
+ qemuAgentRegister(agent);
+
+ agent->running = true;
+ VIR_DEBUG("New agent %p fd=%d", agent, agent->fd);
+
+ return agent;
+
+ cleanup:
+ qemuAgentClose(agent);
+ return NULL;
+}
+
+
+static void
+qemuAgentNotifyCloseLocked(qemuAgent *agent)
+{
+ if (agent) {
+ agent->running = false;
+
+ /* If there is somebody waiting for a message
+ * wake him up. No message will arrive anyway. */
+ if (agent->msg && !agent->msg->finished) {
+ agent->msg->finished = true;
+ virCondSignal(&agent->notify);
+ }
+ }
+}
+
+
+void
+qemuAgentNotifyClose(qemuAgent *agent)
+{
+ if (!agent)
+ return;
+
+ VIR_DEBUG("agent=%p", agent);
+
+ VIR_WITH_OBJECT_LOCK_GUARD(agent) {
+ qemuAgentNotifyCloseLocked(agent);
+ }
+}
+
+
+void qemuAgentClose(qemuAgent *agent)
+{
+ if (!agent)
+ return;
+
+ VIR_DEBUG("agent=%p", agent);
+
+ VIR_WITH_OBJECT_LOCK_GUARD(agent) {
+ if (agent->socket) {
+ qemuAgentUnregister(agent);
+ g_clear_pointer(&agent->socket, g_object_unref);
+ agent->fd = -1;
+ }
+
+ qemuAgentNotifyCloseLocked(agent);
+ }
+
+ virObjectUnref(agent);
+}
+
+#define QEMU_AGENT_WAIT_TIME 5
+
+/**
+ * qemuAgentSend:
+ * @agent: agent object
+ * @msg: Message
+ * @seconds: number of seconds to wait for the result, it can be either
+ * -2, -1, 0 or positive.
+ * @report_sync: On timeout; report synchronization error instead of the normal error
+ *
+ * Send @msg to agent @agent. If @seconds is equal to
+ * VIR_DOMAIN_QEMU_AGENT_COMMAND_BLOCK(-2), this function will block forever
+ * waiting for the result. The value of
+ * VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT(-1) means use default timeout value
+ * and VIR_DOMAIN_QEMU_AGENT_COMMAND_NOWAIT(0) makes this function return
+ * immediately without waiting. Any positive value means the number of seconds
+ * to wait for the result.
+ *
+ * Returns: 0 on success,
+ * -2 on timeout,
+ * -1 otherwise
+ */
+static int
+qemuAgentSend(qemuAgent *agent,
+ qemuAgentMessage *msg,
+ int seconds,
+ bool report_sync)
+{
+ int ret = -1;
+ unsigned long long then = 0;
+
+ VIR_INFO("qemuAgentSend: seconds=%d", seconds);
+
+ /* Check whether qemu quit unexpectedly */
+ if (agent->lastError.code != VIR_ERR_OK) {
+ VIR_DEBUG("Attempt to send command while error is set %s",
+ NULLSTR(agent->lastError.message));
+ virSetError(&agent->lastError);
+ return -1;
+ }
+
+ if (seconds > VIR_DOMAIN_QEMU_AGENT_COMMAND_BLOCK) {
+ unsigned long long now;
+ if (virTimeMillisNow(&now) < 0)
+ return -1;
+ if (seconds == VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT)
+ seconds = QEMU_AGENT_WAIT_TIME;
+ then = now + seconds * 1000ull;
+ }
+
+ agent->msg = msg;
+ qemuAgentUpdateWatch(agent);
+
+ while (!agent->msg->finished) {
+ if ((then && virCondWaitUntil(&agent->notify, &agent->parent.lock, then) < 0) ||
+ (!then && virCondWait(&agent->notify, &agent->parent.lock) < 0)) {
+ if (errno == ETIMEDOUT) {
+ if (report_sync) {
+ virReportError(VIR_ERR_AGENT_UNRESPONSIVE,
+ _("guest agent didn't respond to synchronization within '%1$d' seconds"),
+ seconds);
+ } else {
+ virReportError(VIR_ERR_AGENT_COMMAND_TIMEOUT,
+ _("guest agent didn't respond to command within '%1$d' seconds"),
+ seconds);
+ }
+ ret = -2;
+ } else {
+ virReportSystemError(errno, "%s",
+ _("Unable to wait on agent socket condition"));
+ }
+ agent->inSync = false;
+ goto cleanup;
+ }
+ }
+
+ if (agent->lastError.code != VIR_ERR_OK) {
+ VIR_DEBUG("Send command resulted in error %s",
+ NULLSTR(agent->lastError.message));
+ virSetError(&agent->lastError);
+ goto cleanup;
+ }
+
+ ret = 0;
+
+ cleanup:
+ agent->msg = NULL;
+ qemuAgentUpdateWatch(agent);
+
+ return ret;
+}
+
+
+/**
+ * qemuAgentGuestSyncSend:
+ * @agent: agent object
+ * @timeout: timeout for the command
+ * @first: true when this is the first invocation to drain possible leftovers
+ * from the pipe
+ *
+ * Sends a sync request to the guest agent.
+ * Returns: -1 on error
+ * 0 on successful send, but when no reply was received
+ * 1 when a reply was received
+ */
+static int
+qemuAgentGuestSyncSend(qemuAgent *agent,
+ int timeout,
+ bool first)
+{
+ g_autofree char *txMsg = NULL;
+ g_autoptr(virJSONValue) rxObj = NULL;
+ unsigned long long id;
+ qemuAgentMessage sync_msg = { 0 };
+ int rc;
+
+ if (virTimeMillisNow(&id) < 0)
+ return -1;
+
+ txMsg = g_strdup_printf("{\"execute\":\"guest-sync\", "
+ "\"arguments\":{\"id\":%llu}}\n", id);
+
+ sync_msg.txBuffer = txMsg;
+ sync_msg.txLength = strlen(txMsg);
+ sync_msg.sync = true;
+ sync_msg.id = id;
+ sync_msg.first = first;
+
+ VIR_DEBUG("Sending guest-sync command with ID: %llu", id);
+
+ rc = qemuAgentSend(agent, &sync_msg, timeout, true);
+ rxObj = g_steal_pointer(&sync_msg.rxObject);
+
+ VIR_DEBUG("qemuAgentSend returned: %d", rc);
+
+ if (rc < 0)
+ return -1;
+
+ if (rxObj)
+ return 1;
+
+ return 0;
+}
+
+
+/**
+ * qemuAgentGuestSync:
+ * @agent: agent object
+ * @seconds: qemu agent command timeout value
+ *
+ * Send guest-sync with unique ID
+ * and wait for reply. If we get one, check if
+ * received ID is equal to given.
+ *
+ * Returns: 0 on success,
+ * -1 otherwise
+ */
+static int
+qemuAgentGuestSync(qemuAgent *agent,
+ int seconds)
+{
+ int timeout = QEMU_AGENT_WAIT_TIME;
+ int rc;
+
+ if (agent->inSync)
+ return 0;
+
+ /* if user specified a custom agent timeout that is lower than the
+ * default timeout, use the shorter timeout instead */
+ if ((agent->timeout >= 0) && (agent->timeout < timeout))
+ timeout = agent->timeout;
+
+ /* If user specified a timeout parameter smaller than both default
+ * value and agent->timeout in qga APIs(such as qemu-agent-command),
+ * use the parameter timeout value */
+ if ((seconds >= 0) && (seconds < timeout))
+ timeout = seconds;
+
+ if ((rc = qemuAgentGuestSyncSend(agent, timeout, true)) < 0)
+ return -1;
+
+ /* successfully sync'd */
+ if (rc == 1) {
+ agent->inSync = true;
+ return 0;
+ }
+
+ /* send another sync */
+ if ((rc = qemuAgentGuestSyncSend(agent, timeout, false)) < 0)
+ return -1;
+
+ /* successfully sync'd */
+ if (rc == 1) {
+ agent->inSync = true;
+ return 0;
+ }
+
+ if (agent->running)
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("Missing agent reply object"));
+ else
+ virReportError(VIR_ERR_AGENT_UNRESPONSIVE, "%s",
+ _("Guest agent disappeared while executing command"));
+
+ return -1;
+}
+
+static const char *
+qemuAgentStringifyErrorClass(const char *klass)
+{
+ if (STREQ_NULLABLE(klass, "BufferOverrun"))
+ return "Buffer overrun";
+ else if (STREQ_NULLABLE(klass, "CommandDisabled"))
+ return "The command has been disabled for this instance";
+ else if (STREQ_NULLABLE(klass, "CommandNotFound"))
+ return "The command has not been found";
+ else if (STREQ_NULLABLE(klass, "FdNotFound"))
+ return "File descriptor not found";
+ else if (STREQ_NULLABLE(klass, "InvalidParameter"))
+ return "Invalid parameter";
+ else if (STREQ_NULLABLE(klass, "InvalidParameterType"))
+ return "Invalid parameter type";
+ else if (STREQ_NULLABLE(klass, "InvalidParameterValue"))
+ return "Invalid parameter value";
+ else if (STREQ_NULLABLE(klass, "OpenFileFailed"))
+ return "Cannot open file";
+ else if (STREQ_NULLABLE(klass, "QgaCommandFailed"))
+ return "Guest agent command failed";
+ else if (STREQ_NULLABLE(klass, "QMPBadInputObjectMember"))
+ return "Bad QMP input object member";
+ else if (STREQ_NULLABLE(klass, "QMPExtraInputObjectMember"))
+ return "Unexpected extra object member";
+ else if (STREQ_NULLABLE(klass, "UndefinedError"))
+ return "An undefined error has occurred";
+ else if (STREQ_NULLABLE(klass, "Unsupported"))
+ return "this feature or command is not currently supported";
+ else if (klass)
+ return klass;
+ else
+ return "unknown QEMU command error";
+}
+
+/* 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(virJSONValue *error)
+{
+ const char *klass = virJSONValueObjectGetString(error, "class");
+ const char *detail = virJSONValueObjectGetString(error, "desc");
+
+ /* The QMP 'desc' field is usually sufficient for our generic
+ * error reporting needs. However, if not present, translate
+ * the class into something readable.
+ */
+ if (!detail)
+ detail = qemuAgentStringifyErrorClass(klass);
+
+ return detail;
+}
+
+static const char *
+qemuAgentCommandName(virJSONValue *cmd)
+{
+ const char *name = virJSONValueObjectGetString(cmd, "execute");
+ if (name)
+ return name;
+ return "<unknown>";
+}
+
+static int
+qemuAgentCheckError(virJSONValue *cmd,
+ virJSONValue *reply,
+ bool report_unsupported)
+{
+ if (virJSONValueObjectHasKey(reply, "error")) {
+ virJSONValue *error = virJSONValueObjectGet(reply, "error");
+ g_autofree char *cmdstr = virJSONValueToString(cmd, false);
+ g_autofree char *replystr = virJSONValueToString(reply, false);
+
+ /* Log the full JSON formatted command & error */
+ VIR_DEBUG("unable to execute QEMU agent command %s: %s",
+ NULLSTR(cmdstr), NULLSTR(replystr));
+
+ /* Only send the user the command name + friendly error */
+ if (!error) {
+ virReportError(VIR_ERR_AGENT_COMMAND_FAILED,
+ _("unable to execute QEMU agent command '%1$s'"),
+ qemuAgentCommandName(cmd));
+ return -1;
+ }
+
+ if (!report_unsupported) {
+ const char *klass = virJSONValueObjectGetString(error, "class");
+
+ if (STREQ_NULLABLE(klass, "CommandNotFound") ||
+ STREQ_NULLABLE(klass, "CommandDisabled"))
+ return -2;
+ }
+
+ virReportError(VIR_ERR_AGENT_COMMAND_FAILED,
+ _("unable to execute QEMU agent command '%1$s': %2$s"),
+ qemuAgentCommandName(cmd),
+ qemuAgentStringifyError(error));
+
+ return -1;
+ }
+ if (!virJSONValueObjectHasKey(reply, "return")) {
+ g_autofree char *cmdstr = virJSONValueToString(cmd, false);
+ g_autofree char *replystr = virJSONValueToString(reply, false);
+
+ VIR_DEBUG("Neither 'return' nor 'error' is set in the JSON reply %s: %s",
+ NULLSTR(cmdstr), NULLSTR(replystr));
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("QEMU agent command '%1$s' returned neither error nor success"),
+ qemuAgentCommandName(cmd));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+qemuAgentCommandFull(qemuAgent *agent,
+ virJSONValue *cmd,
+ virJSONValue **reply,
+ int seconds,
+ bool report_unsupported)
+{
+ int ret = -1;
+ qemuAgentMessage msg = { 0 };
+ g_autofree char *cmdstr = NULL;
+ int await_event = agent->await_event;
+
+ *reply = NULL;
+
+ if (!agent->running) {
+ virReportError(VIR_ERR_AGENT_UNRESPONSIVE, "%s",
+ _("Guest agent disappeared while executing command"));
+ goto cleanup;
+ }
+
+ if (qemuAgentGuestSync(agent, seconds) < 0)
+ goto cleanup;
+
+ if (!(cmdstr = virJSONValueToString(cmd, false)))
+ goto cleanup;
+ msg.txBuffer = g_strdup_printf("%s" LINE_ENDING, cmdstr);
+ msg.txLength = strlen(msg.txBuffer);
+
+ VIR_DEBUG("Send command '%s' for write, seconds = %d", cmdstr, seconds);
+
+ ret = qemuAgentSend(agent, &msg, seconds, false);
+
+ VIR_DEBUG("Receive command reply ret=%d rxObject=%p",
+ ret, msg.rxObject);
+
+ if (ret < 0)
+ goto cleanup;
+
+ /* If we haven't obtained any reply but we wait for an
+ * event, then don't report this as error */
+ if (!msg.rxObject) {
+ if (!await_event) {
+ if (agent->running) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("Missing agent reply object"));
+ } else {
+ virReportError(VIR_ERR_AGENT_COMMAND_TIMEOUT, "%s",
+ _("Guest agent disappeared while executing command"));
+ }
+ ret = -1;
+ }
+ goto cleanup;
+ }
+
+ *reply = msg.rxObject;
+ ret = qemuAgentCheckError(cmd, *reply, report_unsupported);
+
+ cleanup:
+ VIR_FREE(msg.txBuffer);
+ agent->await_event = QEMU_AGENT_EVENT_NONE;
+
+ return ret;
+}
+
+static int
+qemuAgentCommand(qemuAgent *agent,
+ virJSONValue *cmd,
+ virJSONValue **reply,
+ int seconds)
+{
+ return qemuAgentCommandFull(agent, cmd, reply, seconds, true);
+}
+
+static virJSONValue *G_GNUC_NULL_TERMINATED
+qemuAgentMakeCommand(const char *cmdname,
+ ...)
+{
+ g_autoptr(virJSONValue) obj = NULL;
+ g_autoptr(virJSONValue) jargs = NULL;
+ va_list args;
+
+ va_start(args, cmdname);
+
+ if (virJSONValueObjectAddVArgs(&jargs, args) < 0) {
+ va_end(args);
+ return NULL;
+ }
+
+ va_end(args);
+
+ if (virJSONValueObjectAdd(&obj,
+ "s:execute", cmdname,
+ "A:arguments", &jargs,
+ NULL) < 0)
+ return NULL;
+
+ return g_steal_pointer(&obj);
+}
+
+static virJSONValue *
+qemuAgentMakeStringsArray(const char **strings, unsigned int len)
+{
+ size_t i;
+ g_autoptr(virJSONValue) ret = virJSONValueNewArray();
+
+ for (i = 0; i < len; i++) {
+ if (virJSONValueArrayAppendString(ret, strings[i]) < 0)
+ return NULL;
+ }
+
+ return g_steal_pointer(&ret);
+}
+
+void qemuAgentNotifyEvent(qemuAgent *agent,
+ qemuAgentEvent event)
+{
+ VIR_LOCK_GUARD lock = virObjectLockGuard(agent);
+
+ VIR_DEBUG("agent=%p event=%d await_event=%d", agent, event, agent->await_event);
+ if (agent->await_event == event) {
+ agent->await_event = QEMU_AGENT_EVENT_NONE;
+ /* somebody waiting for this event, wake him up. */
+ if (agent->msg && !agent->msg->finished) {
+ agent->msg->finished = true;
+ virCondSignal(&agent->notify);
+ }
+ }
+}
+
+
+int
+qemuAgentArbitraryCommand(qemuAgent *agent,
+ const char *cmd_str,
+ char **result,
+ int timeout)
+{
+ int rc;
+ g_autoptr(virJSONValue) cmd = NULL;
+ g_autoptr(virJSONValue) reply = NULL;
+
+ *result = NULL;
+ if (timeout < VIR_DOMAIN_QEMU_AGENT_COMMAND_MIN) {
+ virReportError(VIR_ERR_INVALID_ARG,
+ _("guest agent timeout '%1$d' is less than the minimum '%2$d'"),
+ timeout, VIR_DOMAIN_QEMU_AGENT_COMMAND_MIN);
+ return -1;
+ }
+
+ if (!(cmd = virJSONValueFromString(cmd_str)))
+ return -1;
+
+ if ((rc = qemuAgentCommand(agent, cmd, &reply, timeout)) < 0)
+ return rc;
+
+ if (!(*result = virJSONValueToString(reply, false)))
+ return -1;
+
+ return rc;
+}
+
+
+/**
+ * qemuAgentGetHostname:
+ *
+ * Gets the guest hostname using the guest agent.
+ *
+ * Returns 0 on success and fills @hostname. On error -1 is returned with an
+ * error reported and if '@report_unsupported' is false -2 is returned if the
+ * guest agent does not support the command without reporting an error
+ */
+int
+qemuAgentGetHostname(qemuAgent *agent,
+ char **hostname,
+ bool report_unsupported)
+{
+ g_autoptr(virJSONValue) cmd = qemuAgentMakeCommand("guest-get-host-name", NULL);
+ g_autoptr(virJSONValue) reply = NULL;
+ virJSONValue *data = NULL;
+ const char *result = NULL;
+ int rc;
+
+ if (!cmd)
+ return -1;
+
+ if ((rc = qemuAgentCommandFull(agent, cmd, &reply, agent->timeout,
+ report_unsupported)) < 0)
+ return rc;
+
+ if (!(data = virJSONValueObjectGet(reply, "return"))) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("malformed return value"));
+ return -1;
+ }
+
+ if (!(result = virJSONValueObjectGetString(data, "host-name"))) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("'host-name' missing in guest-get-host-name reply"));
+ return -1;
+ }
+
+ *hostname = g_strdup(result);
+
+ return 0;
+}
+
+
+int
+qemuAgentGetTime(qemuAgent *agent,
+ long long *seconds,
+ unsigned int *nseconds)
+{
+ unsigned long long json_time;
+ g_autoptr(virJSONValue) cmd = NULL;
+ g_autoptr(virJSONValue) reply = NULL;
+
+ cmd = qemuAgentMakeCommand("guest-get-time",
+ NULL);
+ if (!cmd)
+ return -1;
+
+ if (qemuAgentCommand(agent, cmd, &reply, agent->timeout) < 0)
+ return -1;
+
+ if (virJSONValueObjectGetNumberUlong(reply, "return", &json_time) < 0) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("malformed return value"));
+ return -1;
+ }
+
+ /* guest agent returns time in nanoseconds,
+ * we need it in seconds here */
+ *seconds = json_time / 1000000000LL;
+ *nseconds = json_time % 1000000000LL;
+ return 0;
+}
diff --git a/src/bhyve/bhyve_qemu_agent.h b/src/bhyve/bhyve_qemu_agent.h
new file mode 100644
index 0000000000..0e1752a2cb
--- /dev/null
+++ b/src/bhyve/bhyve_qemu_agent.h
@@ -0,0 +1,197 @@
+/*
+ * bhyve_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, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "internal.h"
+#include "domain_conf.h"
+
+typedef struct _qemuAgent qemuAgent;
+
+typedef struct _qemuAgentCallbacks qemuAgentCallbacks;
+struct _qemuAgentCallbacks {
+ void (*eofNotify)(qemuAgent *mon,
+ virDomainObj *vm);
+ void (*errorNotify)(qemuAgent *mon,
+ virDomainObj *vm);
+};
+
+
+qemuAgent *qemuAgentOpen(virDomainObj *vm,
+ const virDomainChrSourceDef *config,
+ GMainContext *context,
+ qemuAgentCallbacks *cb);
+
+void qemuAgentClose(qemuAgent *mon);
+
+void qemuAgentNotifyClose(qemuAgent *mon);
+
+typedef enum {
+ QEMU_AGENT_EVENT_NONE = 0,
+ QEMU_AGENT_EVENT_SHUTDOWN,
+ QEMU_AGENT_EVENT_SUSPEND,
+ QEMU_AGENT_EVENT_RESET,
+} qemuAgentEvent;
+
+void qemuAgentNotifyEvent(qemuAgent *mon,
+ qemuAgentEvent event);
+
+typedef enum {
+ QEMU_AGENT_SHUTDOWN_POWERDOWN,
+ QEMU_AGENT_SHUTDOWN_REBOOT,
+ QEMU_AGENT_SHUTDOWN_HALT,
+
+ QEMU_AGENT_SHUTDOWN_LAST,
+} qemuAgentShutdownMode;
+
+typedef struct _qemuAgentDiskAddress qemuAgentDiskAddress;
+struct _qemuAgentDiskAddress {
+ char *serial;
+ virPCIDeviceAddress pci_controller;
+ char *bus_type;
+ unsigned int bus;
+ unsigned int target;
+ unsigned int unit;
+ char *devnode;
+ virCCWDeviceAddress *ccw_addr;
+};
+void qemuAgentDiskAddressFree(qemuAgentDiskAddress *addr);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuAgentDiskAddress, qemuAgentDiskAddressFree);
+
+typedef struct _qemuAgentDiskInfo qemuAgentDiskInfo;
+struct _qemuAgentDiskInfo {
+ char *name;
+ bool partition;
+ char **dependencies;
+ qemuAgentDiskAddress *address;
+ char *alias;
+};
+void qemuAgentDiskInfoFree(qemuAgentDiskInfo *info);
+
+typedef struct _qemuAgentFSInfo qemuAgentFSInfo;
+struct _qemuAgentFSInfo {
+ char *mountpoint; /* path to mount point */
+ char *name; /* device name in the guest (e.g. "sda1") */
+ char *fstype; /* filesystem type */
+ long long total_bytes;
+ long long used_bytes;
+ size_t ndisks;
+ qemuAgentDiskAddress **disks;
+};
+void qemuAgentFSInfoFree(qemuAgentFSInfo *info);
+
+int qemuAgentShutdown(qemuAgent *mon,
+ qemuAgentShutdownMode mode);
+
+int qemuAgentFSFreeze(qemuAgent *mon,
+ const char **mountpoints, unsigned int nmountpoints);
+int qemuAgentFSThaw(qemuAgent *mon);
+int qemuAgentGetFSInfo(qemuAgent *mon,
+ qemuAgentFSInfo ***info,
+ bool report_unsupported);
+
+int qemuAgentSuspend(qemuAgent *mon,
+ unsigned int target);
+
+int qemuAgentArbitraryCommand(qemuAgent *mon,
+ const char *cmd,
+ char **result,
+ int timeout);
+int qemuAgentFSTrim(qemuAgent *mon,
+ unsigned long long minimum);
+
+
+typedef struct _qemuAgentCPUInfo qemuAgentCPUInfo;
+struct _qemuAgentCPUInfo {
+ unsigned int id; /* logical cpu ID */
+ bool online; /* true if the CPU is activated */
+ bool offlinable; /* true if the CPU can be offlined */
+
+ bool modified; /* set to true if the vcpu state needs to be changed */
+};
+
+int qemuAgentGetVCPUs(qemuAgent *mon, qemuAgentCPUInfo **info);
+int qemuAgentSetVCPUs(qemuAgent *mon, qemuAgentCPUInfo *cpus, size_t ncpus);
+int qemuAgentUpdateCPUInfo(unsigned int nvcpus,
+ qemuAgentCPUInfo *cpuinfo,
+ int ncpuinfo);
+
+int
+qemuAgentGetHostname(qemuAgent *mon,
+ char **hostname,
+ bool report_unsupported);
+
+int qemuAgentGetTime(qemuAgent *mon,
+ long long *seconds,
+ unsigned int *nseconds);
+int qemuAgentSetTime(qemuAgent *mon,
+ long long seconds,
+ unsigned int nseconds,
+ bool sync);
+
+int qemuAgentGetInterfaces(qemuAgent *mon,
+ virDomainInterfacePtr **ifaces,
+ bool report_unsupported);
+
+int qemuAgentSetUserPassword(qemuAgent *mon,
+ const char *user,
+ const char *password,
+ bool crypted);
+
+int qemuAgentGetUsers(qemuAgent *mon,
+ virTypedParamList *list,
+ bool report_unsupported);
+
+int qemuAgentGetOSInfo(qemuAgent *mon,
+ virTypedParamList *list,
+ bool report_unsupported);
+
+int qemuAgentGetTimezone(qemuAgent *mon,
+ virTypedParamList *list,
+ bool report_unsupported);
+
+void qemuAgentSetResponseTimeout(qemuAgent *mon,
+ int timeout);
+
+int qemuAgentSSHGetAuthorizedKeys(qemuAgent *agent,
+ const char *user,
+ char ***keys);
+
+int qemuAgentSSHAddAuthorizedKeys(qemuAgent *agent,
+ const char *user,
+ const char **keys,
+ size_t nkeys,
+ bool reset);
+
+int qemuAgentSSHRemoveAuthorizedKeys(qemuAgent *agent,
+ const char *user,
+ const char **keys,
+ size_t nkeys);
+
+int qemuAgentGetDisks(qemuAgent *mon,
+ qemuAgentDiskInfo ***disks,
+ bool report_unsupported);
+
+int qemuAgentGetLoadAvg(qemuAgent *agent,
+ double *load1m,
+ double *load5m,
+ double *load15m,
+ bool report_unsupported);
diff --git a/src/bhyve/meson.build b/src/bhyve/meson.build
index 11920d9c3e..a1f0aa63b2 100644
--- a/src/bhyve/meson.build
+++ b/src/bhyve/meson.build
@@ -2,13 +2,14 @@ bhyve_sources = files(
'bhyve_capabilities.c',
'bhyve_command.c',
'bhyve_conf.c',
- 'bhyve_firmware.c',
- 'bhyve_parse_command.c',
'bhyve_device.c',
'bhyve_domain.c',
'bhyve_driver.c',
+ 'bhyve_firmware.c',
'bhyve_monitor.c',
+ 'bhyve_parse_command.c',
'bhyve_process.c',
+ 'bhyve_qemu_agent.c',
)
driver_source_files += bhyve_sources
diff --git a/src/remote/qemu_protocol.x b/src/remote/qemu_protocol.x
index c7f3abfcbf..757e54efcc 100644
--- a/src/remote/qemu_protocol.x
+++ b/src/remote/qemu_protocol.x
@@ -44,17 +44,6 @@ struct qemu_domain_attach_ret {
remote_nonnull_domain dom;
};
-struct qemu_domain_agent_command_args {
- remote_nonnull_domain dom;
- remote_nonnull_string cmd;
- int timeout;
- unsigned int flags;
-};
-
-struct qemu_domain_agent_command_ret {
- remote_string result;
-};
-
struct qemu_connect_domain_monitor_event_register_args {
remote_domain dom;
@@ -135,13 +124,6 @@ enum qemu_procedure {
*/
QEMU_PROC_DOMAIN_ATTACH = 2,
- /**
- * @generate: both
- * @priority: low
- * @acl: domain:write
- */
- QEMU_PROC_DOMAIN_AGENT_COMMAND = 3,
-
/**
* @generate: none
* @priority: high
diff --git a/src/remote/remote_protocol.x b/src/remote/remote_protocol.x
index 38a83c64ea..a3a0920061 100644
--- a/src/remote/remote_protocol.x
+++ b/src/remote/remote_protocol.x
@@ -4009,6 +4009,17 @@ struct remote_domain_event_nic_mac_change_msg {
remote_nonnull_string newMAC;
};
+struct remote_domain_qemu_agent_command_args {
+ remote_nonnull_domain dom;
+ remote_nonnull_string cmd;
+ int timeout;
+ unsigned int flags;
+};
+
+struct remote_domain_qemu_agent_command_ret {
+ remote_string result;
+};
+
/*----- Protocol. -----*/
/* Define the program number, protocol version and procedure numbers here. */
@@ -7120,5 +7131,12 @@ enum remote_procedure {
* @generate: both
* @acl: none
*/
- REMOTE_PROC_DOMAIN_EVENT_NIC_MAC_CHANGE = 453
+ REMOTE_PROC_DOMAIN_EVENT_NIC_MAC_CHANGE = 453,
+
+ /**
+ * @generate: both
+ * @priority: low
+ * @acl: domain:write
+ */
+ REMOTE_PROC_DOMAIN_QEMU_AGENT_COMMAND = 454
};
--
2.52.0
On Mon, May 11, 2026 at 07:22:15PM +0200, Roman Bogorodskiy wrote: > Implement QEMU Guest Agent support for bhyve. In bhyve it can configured > using the virtio-console device. > > This change covers only two APIs using the agent: > > - DomainQemuAgentCommand -- the most generic one. > - DomainGetHostname -- extended to support not only DHCP lease source, > but an agent as well. > > There are two things that I'm not sure about this patch. > > - The protocol files were updated to make DomainQemuAgentCommand generic > instead of Qemu specific. QEMU_PROC_DOMAIN_AGENT_COMMAND was removed in > favor of REMOTE_PROC_DOMAIN_QEMU_AGENT_COMMAND. > > Even though the protocol is documented as private, I'm not sure how > desired this change is. While we declare the extra protocols "Subject to change", we really shouldn't change them without compelling reason, and I don't think we have one. Either just add the new API in parallel with the old one, or we could declare that the bhyve driver uses the libvirt-qemu.so library for QEMU guest agent interaction and not change the protocol at all ? Despite the name libvirt-qemu.so doesn't have a tight coupling with thue QEMU driver. > - src/qemu/bhyve_qemu_agent.c and src/qemu/qemu_agent.c should > share the same implementation. Ideally this should live > somewhere in src/util/virqemuagent.c, but as it is using things > from conf/, such as virDomainObj, it cannot be moved as is. > > I considered extracting a simpler data structure that will not > use the conf/ types, but it didn't work very well for me and > I didn't like the way it looks in the driver code. > > Maybe I'm missing some better approach? We have "src/hypervisor/" which can be used in these scenarios. With regards, Daniel -- |: https://berrange.com ~~ https://hachyderm.io/@berrange :| |: https://libvirt.org ~~ https://entangle-photo.org :| |: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
Daniel P. Berrangé wrote: > On Mon, May 11, 2026 at 07:22:15PM +0200, Roman Bogorodskiy wrote: > > Implement QEMU Guest Agent support for bhyve. In bhyve it can configured > > using the virtio-console device. > > > > This change covers only two APIs using the agent: > > > > - DomainQemuAgentCommand -- the most generic one. > > - DomainGetHostname -- extended to support not only DHCP lease source, > > but an agent as well. > > > > There are two things that I'm not sure about this patch. > > > > - The protocol files were updated to make DomainQemuAgentCommand generic > > instead of Qemu specific. QEMU_PROC_DOMAIN_AGENT_COMMAND was removed in > > favor of REMOTE_PROC_DOMAIN_QEMU_AGENT_COMMAND. > > > > Even though the protocol is documented as private, I'm not sure how > > desired this change is. > > While we declare the extra protocols "Subject to change", we really > shouldn't change them without compelling reason, and I don't think > we have one. Either just add the new API in parallel with the old one, > or we could declare that the bhyve driver uses the libvirt-qemu.so > library for QEMU guest agent interaction and not change the protocol > at all ? Despite the name libvirt-qemu.so doesn't have a tight coupling > with thue QEMU driver. It looks libvirt-qemu does not solve the issue. The main trigger for extending the protocol was the lack of ACL check in my bhyveDomainQemuAgentCommand() implementation. And as virDomainQemuAgentCommandEnsureACL() is available only in the generated access/viraccessapicheckqemu.h file, it does not seem there is a way I can use it. This leaves me with the new API option, I guess. > > - src/qemu/bhyve_qemu_agent.c and src/qemu/qemu_agent.c should > > share the same implementation. Ideally this should live > > somewhere in src/util/virqemuagent.c, but as it is using things > > from conf/, such as virDomainObj, it cannot be moved as is. > > > > I considered extracting a simpler data structure that will not > > use the conf/ types, but it didn't work very well for me and > > I didn't like the way it looks in the driver code. > > > > Maybe I'm missing some better approach? > > We have "src/hypervisor/" which can be used in these scenarios. > Thanks, I will take a look! > With regards, > Daniel > -- > |: https://berrange.com ~~ https://hachyderm.io/@berrange :| > |: https://libvirt.org ~~ https://entangle-photo.org :| > |: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :| >
On Tue, May 12, 2026 at 07:08:19PM +0200, Roman Bogorodskiy wrote: > Daniel P. Berrangé wrote: > > > On Mon, May 11, 2026 at 07:22:15PM +0200, Roman Bogorodskiy wrote: > > > Implement QEMU Guest Agent support for bhyve. In bhyve it can configured > > > using the virtio-console device. > > > > > > This change covers only two APIs using the agent: > > > > > > - DomainQemuAgentCommand -- the most generic one. > > > - DomainGetHostname -- extended to support not only DHCP lease source, > > > but an agent as well. > > > > > > There are two things that I'm not sure about this patch. > > > > > > - The protocol files were updated to make DomainQemuAgentCommand generic > > > instead of Qemu specific. QEMU_PROC_DOMAIN_AGENT_COMMAND was removed in > > > favor of REMOTE_PROC_DOMAIN_QEMU_AGENT_COMMAND. > > > > > > Even though the protocol is documented as private, I'm not sure how > > > desired this change is. > > > > While we declare the extra protocols "Subject to change", we really > > shouldn't change them without compelling reason, and I don't think > > we have one. Either just add the new API in parallel with the old one, > > or we could declare that the bhyve driver uses the libvirt-qemu.so > > library for QEMU guest agent interaction and not change the protocol > > at all ? Despite the name libvirt-qemu.so doesn't have a tight coupling > > with thue QEMU driver. > > It looks libvirt-qemu does not solve the issue. The main trigger for > extending the protocol was the lack of ACL check in my > bhyveDomainQemuAgentCommand() implementation. And as > virDomainQemuAgentCommandEnsureACL() is available only > in the generated access/viraccessapicheckqemu.h file, it does not seem > there is a way I can use it. Can you not #include "viraccessapicheckqemu.h" it ? AFAIR it is tied to the QEMU driver in name only, no functional dep. With regards, Daniel -- |: https://berrange.com ~~ https://hachyderm.io/@berrange :| |: https://libvirt.org ~~ https://entangle-photo.org :| |: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
© 2016 - 2026 Red Hat, Inc.