diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml
index 7b46dc7..749b896 100644
--- a/deploy/crds/vms-crd.yaml
+++ b/deploy/crds/vms-crd.yaml
@@ -1491,6 +1491,10 @@ spec:
by the runner if password protection is not enabled.
type: integer
default: 0
+ osinfo:
+ description: Copy of the OS info provided by the guest agent.
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
assignment:
description: >-
The assignment of this VM to a a particular user.
diff --git a/dev-example/pool-action.sh b/dev-example/pool-action.sh
index b605479..bc8fbce 100755
--- a/dev-example/pool-action.sh
+++ b/dev-example/pool-action.sh
@@ -7,6 +7,10 @@ Applys action to all VMs in the pool.
--context Context to be passed to kubectl (required)
-n, --namespace Namespace to be passed to kubectl
+
+Action is one of "start", "stop", "delete" or "delete-disks"
+
+Defaults for context and namespace are read from .vm-operator-cmd.rc.
EOF
exit 1
}
diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml
index 7ee2793..aa75bc3 100644
--- a/dev-example/test-vm.yaml
+++ b/dev-example/test-vm.yaml
@@ -5,9 +5,7 @@ metadata:
name: test-vm
spec:
image:
- repository: docker-registry.lan.mnl.de
- path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine
- version: latest
+ source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing
pullPolicy: Always
permissions:
@@ -34,8 +32,9 @@ spec:
currentCpus: 4
networks:
- - tap:
- mac: "02:16:3e:33:58:10"
+ # No bridge on test cluster
+ - user: {}
+
disks:
- volumeClaimTemplate:
metadata:
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java
index 0665d32..dcae3a3 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java
@@ -41,13 +41,16 @@ import org.jose4j.base64url.Base64;
/**
* Delegee for reconciling the display secret
*/
-@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" })
/* default */ class DisplaySecretReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName());
/**
- * Reconcile.
+ * Reconcile. If the configuration prevents generating a secret
+ * or the secret already exists, do nothing. Else generate a new
+ * secret with a random password and immediate expiration, thus
+ * preventing access to the display.
*
* @param event the event
* @param model the model
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
index dd45147..086f085 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java
@@ -67,6 +67,9 @@ public class Configuration implements Dto {
/** The monitor socket. */
public Path monitorSocket;
+ /** The guest agent socket socket. */
+ public Path guestAgentSocket;
+
/** The firmware rom. */
public Path firmwareRom;
@@ -341,6 +344,7 @@ public class Configuration implements Dto {
runtimeDir.toFile().mkdir();
swtpmSocket = runtimeDir.resolve("swtpm-sock");
monitorSocket = runtimeDir.resolve("monitor.sock");
+ guestAgentSocket = runtimeDir.resolve("org.qemu.guest_agent.0");
}
if (!Files.isDirectory(runtimeDir) || !Files.isWritable(runtimeDir)) {
logger.severe(() -> String.format(
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java
new file mode 100644
index 0000000..4d1c764
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/GuestAgentClient.java
@@ -0,0 +1,254 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2025 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.net.UnixDomainSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.logging.Level;
+import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
+import org.jdrupes.vmoperator.runner.qemu.commands.QmpGuestGetOsinfo;
+import org.jdrupes.vmoperator.runner.qemu.events.GuestAgentCommand;
+import org.jdrupes.vmoperator.runner.qemu.events.MonitorReady;
+import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent;
+import org.jdrupes.vmoperator.runner.qemu.events.VserportChangeEvent;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.Component;
+import org.jgrapes.core.EventPipeline;
+import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.core.events.Start;
+import org.jgrapes.core.events.Stop;
+import org.jgrapes.io.events.Closed;
+import org.jgrapes.io.events.ConnectError;
+import org.jgrapes.io.events.Input;
+import org.jgrapes.io.events.OpenSocketConnection;
+import org.jgrapes.io.util.ByteBufferWriter;
+import org.jgrapes.io.util.LineCollector;
+import org.jgrapes.net.SocketIOChannel;
+import org.jgrapes.net.events.ClientConnected;
+import org.jgrapes.util.events.ConfigurationUpdate;
+
+/**
+ * A component that handles the communication over the guest agent
+ * socket.
+ *
+ * If the log level for this class is set to fine, the messages
+ * exchanged on the monitor socket are logged.
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class GuestAgentClient extends Component {
+
+ private static ObjectMapper mapper = new ObjectMapper();
+
+ private EventPipeline rep;
+ private Path socketPath;
+ private SocketIOChannel gaChannel;
+ private final Queue executing = new LinkedList<>();
+
+ /**
+ * Instantiates a new guest agent client.
+ *
+ * @param componentChannel the component channel
+ * @throws IOException Signals that an I/O exception has occurred.
+ */
+ @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
+ "PMD.ConstructorCallsOverridableMethod" })
+ public GuestAgentClient(Channel componentChannel) throws IOException {
+ super(componentChannel);
+ }
+
+ /**
+ * As the initial configuration of this component depends on the
+ * configuration of the {@link Runner}, it doesn't have a handler
+ * for the {@link ConfigurationUpdate} event. The values are
+ * forwarded from the {@link Runner} instead.
+ *
+ * @param socketPath the socket path
+ * @param powerdownTimeout
+ */
+ /* default */ void configure(Path socketPath) {
+ this.socketPath = socketPath;
+ }
+
+ /**
+ * Handle the start event.
+ *
+ * @param event the event
+ * @throws IOException Signals that an I/O exception has occurred.
+ */
+ @Handler
+ public void onStart(Start event) throws IOException {
+ rep = event.associated(EventPipeline.class).get();
+ if (socketPath == null) {
+ return;
+ }
+ Files.deleteIfExists(socketPath);
+ }
+
+ /**
+ * When the virtual serial port "channel0" has been opened,
+ * establish the connection by opening the socket.
+ *
+ * @param event the event
+ */
+ @Handler
+ public void onVserportChanged(VserportChangeEvent event) {
+ if ("channel0".equals(event.id()) && event.isOpen()) {
+ fire(new OpenSocketConnection(
+ UnixDomainSocketAddress.of(socketPath))
+ .setAssociated(GuestAgentClient.class, this));
+ }
+ }
+
+ /**
+ * Check if this is from opening the monitor socket and if true,
+ * save the socket in the context and associate the channel with
+ * the context. Then send the initial message to the socket.
+ *
+ * @param event the event
+ * @param channel the channel
+ */
+ @SuppressWarnings("resource")
+ @Handler
+ public void onClientConnected(ClientConnected event,
+ SocketIOChannel channel) {
+ event.openEvent().associated(GuestAgentClient.class).ifPresent(qm -> {
+ gaChannel = channel;
+ channel.setAssociated(GuestAgentClient.class, this);
+ channel.setAssociated(Writer.class, new ByteBufferWriter(
+ channel).nativeCharset());
+ channel.setAssociated(LineCollector.class,
+ new LineCollector()
+ .consumer(line -> {
+ try {
+ processGuestAgentInput(line);
+ } catch (IOException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ }));
+ fire(new GuestAgentCommand(new QmpGuestGetOsinfo()));
+ });
+ }
+
+ /**
+ * Called when a connection attempt fails.
+ *
+ * @param event the event
+ * @param channel the channel
+ */
+ @Handler
+ public void onConnectError(ConnectError event, SocketIOChannel channel) {
+ event.event().associated(GuestAgentClient.class).ifPresent(qm -> {
+ rep.fire(new Stop());
+ });
+ }
+
+ /**
+ * Handle data from qemu monitor connection.
+ *
+ * @param event the event
+ * @param channel the channel
+ */
+ @Handler
+ public void onInput(Input> event, SocketIOChannel channel) {
+ if (channel.associated(GuestAgentClient.class).isEmpty()) {
+ return;
+ }
+ channel.associated(LineCollector.class).ifPresent(collector -> {
+ collector.feed(event);
+ });
+ }
+
+ private void processGuestAgentInput(String line)
+ throws IOException {
+ logger.fine(() -> "guest agent(in): " + line);
+ try {
+ var response = mapper.readValue(line, ObjectNode.class);
+ if (response.has("QMP")) {
+ rep.fire(new MonitorReady());
+ return;
+ }
+ if (response.has("return") || response.has("error")) {
+ QmpCommand executed = executing.poll();
+ logger.fine(
+ () -> String.format("(Previous \"guest agent(in)\" is "
+ + "result from executing %s)", executed));
+ if (executed instanceof QmpGuestGetOsinfo) {
+ rep.fire(new OsinfoEvent(response.get("return")));
+ }
+ }
+ } catch (JsonProcessingException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * On closed.
+ *
+ * @param event the event
+ */
+ @Handler
+ @SuppressWarnings({ "PMD.AvoidSynchronizedStatement",
+ "PMD.AvoidDuplicateLiterals" })
+ public void onClosed(Closed> event, SocketIOChannel channel) {
+ channel.associated(QemuMonitor.class).ifPresent(qm -> {
+ gaChannel = null;
+ });
+ }
+
+ /**
+ * On guest agent command.
+ *
+ * @param event the event
+ */
+ @Handler
+ @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
+ "PMD.AvoidSynchronizedStatement" })
+ public void onGuestAgentCommand(GuestAgentCommand event) {
+ var command = event.command();
+ logger.fine(() -> "guest agent(out): " + command.toString());
+ String asText;
+ try {
+ asText = command.asText();
+ } catch (JsonProcessingException e) {
+ logger.log(Level.SEVERE, e,
+ () -> "Cannot serialize Json: " + e.getMessage());
+ return;
+ }
+ synchronized (executing) {
+ gaChannel.associated(Writer.class).ifPresent(writer -> {
+ try {
+ executing.add(command);
+ writer.append(asText).append('\n').flush();
+ } catch (IOException e) {
+ // Cannot happen, but...
+ logger.log(Level.WARNING, e, e::getMessage);
+ }
+ });
+ }
+ }
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
index 52db0ce..c8b9f44 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java
@@ -218,6 +218,7 @@ public class Runner extends Component {
private CommandDefinition cloudInitImgDefinition;
private CommandDefinition qemuDefinition;
private final QemuMonitor qemuMonitor;
+ private final GuestAgentClient guestAgentClient;
private Integer resetCounter;
private RunState state = RunState.INITIALIZING;
@@ -275,6 +276,7 @@ public class Runner extends Component {
attach(new ProcessManager(channel()));
attach(new SocketConnector(channel()));
attach(qemuMonitor = new QemuMonitor(channel(), configDir));
+ attach(guestAgentClient = new GuestAgentClient(channel()));
attach(new StatusUpdater(channel()));
attach(new YamlConfigurationStore(channel(), configFile, false));
fire(new WatchFile(configFile.toPath()));
@@ -349,6 +351,7 @@ public class Runner extends Component {
// Forward some values to child components
qemuMonitor.configure(config.monitorSocket,
config.vm.powerdownTimeout);
+ guestAgentClient.configure(config.guestAgentSocket);
} catch (IllegalArgumentException | IOException | TemplateException e) {
logger.log(Level.SEVERE, e, () -> "Invalid configuration: "
+ e.getMessage());
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
index d33358b..d4548bf 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java
@@ -18,12 +18,16 @@
package org.jdrupes.vmoperator.runner.qemu;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.custom.Quantity.Format;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.EventsV1Event;
import java.io.IOException;
import java.math.BigDecimal;
@@ -40,6 +44,7 @@ import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
import org.jdrupes.vmoperator.runner.qemu.events.DisplayPasswordChanged;
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
+import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
@@ -55,6 +60,12 @@ import org.jgrapes.core.events.Start;
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class StatusUpdater extends VmDefUpdater {
+ @SuppressWarnings("PMD.FieldNamingConventions")
+ private static final Gson gson = new JSON().getGson();
+ @SuppressWarnings("PMD.FieldNamingConventions")
+ private static final ObjectMapper objectMapper
+ = new ObjectMapper().registerModule(new JavaTimeModule());
+
private static final Set RUNNING_STATES
= Set.of(RunState.RUNNING, RunState.TERMINATING);
@@ -286,4 +297,26 @@ public class StatusUpdater extends VmDefUpdater {
public void onShutdown(ShutdownEvent event) throws ApiException {
shutdownByGuest = event.byGuest();
}
+
+ /**
+ * On osinfo.
+ *
+ * @param event the event
+ * @throws ApiException
+ */
+ @Handler
+ public void onOsinfo(OsinfoEvent event) throws ApiException {
+ if (vmStub == null) {
+ return;
+ }
+ var asGson = gson.toJsonTree(
+ objectMapper.convertValue(event.osinfo(), Object.class));
+
+ vmStub.updateStatus(from -> {
+ JsonObject status = from.statusJson();
+ status.add("osinfo", asGson);
+ return status;
+ });
+
+ }
}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java
new file mode 100644
index 0000000..cf4ba72
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestGetOsinfo.java
@@ -0,0 +1,41 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2025 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * A {@link QmpCommand} that pings the guest agent.
+ */
+public class QmpGuestGetOsinfo extends QmpCommand {
+
+ @Override
+ public JsonNode toJson() {
+ ObjectNode cmd = mapper.createObjectNode();
+ cmd.put("execute", "guest-get-osinfo");
+ return cmd;
+ }
+
+ @Override
+ public String toString() {
+ return "QmpGuestGetOsinfo()";
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java
new file mode 100644
index 0000000..75fdf73
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestInfo.java
@@ -0,0 +1,41 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2025 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * A {@link QmpCommand} that requests the guest info.
+ */
+public class QmpGuestInfo extends QmpCommand {
+
+ @Override
+ public JsonNode toJson() {
+ ObjectNode cmd = mapper.createObjectNode();
+ cmd.put("execute", "guest-info");
+ return cmd;
+ }
+
+ @Override
+ public String toString() {
+ return "QmpGuestInfo()";
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java
new file mode 100644
index 0000000..257c838
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpGuestPing.java
@@ -0,0 +1,41 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2023 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * A {@link QmpCommand} that pings the guest agent.
+ */
+public class QmpGuestPing extends QmpCommand {
+
+ @Override
+ public JsonNode toJson() {
+ ObjectNode cmd = mapper.createObjectNode();
+ cmd.put("execute", "guest-ping");
+ return cmd;
+ }
+
+ @Override
+ public String toString() {
+ return "QmpGuestPing()";
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java
new file mode 100644
index 0000000..a1b585d
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/GuestAgentCommand.java
@@ -0,0 +1,63 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2025 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu.events;
+
+import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.Components;
+import org.jgrapes.core.Event;
+
+/**
+ * An {@link Event} that causes some component to send a QMP
+ * command to the guest agent process.
+ */
+public class GuestAgentCommand extends Event {
+
+ private final QmpCommand command;
+
+ /**
+ * Instantiates a new exec qmp command.
+ *
+ * @param command the command
+ */
+ public GuestAgentCommand(QmpCommand command) {
+ this.command = command;
+ }
+
+ /**
+ * Gets the command.
+ *
+ * @return the command
+ */
+ public QmpCommand command() {
+ return command;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(Components.objectName(this))
+ .append(" [").append(command);
+ if (channels() != null) {
+ builder.append(", channels=").append(Channel.toString(channels()));
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
index df981c8..e35a172 100644
--- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorEvent.java
@@ -35,7 +35,7 @@ public class MonitorEvent extends Event {
*/
public enum Kind {
READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN,
- SPICE_CONNECTED, SPICE_INITIALIZED, SPICE_DISCONNECTED
+ SPICE_CONNECTED, SPICE_INITIALIZED, SPICE_DISCONNECTED, VSERPORT_CHANGE
}
private final Kind kind;
@@ -72,6 +72,9 @@ public class MonitorEvent extends Event {
case SPICE_DISCONNECTED:
return Optional.of(new SpiceDisconnectedEvent(kind,
response.get(EVENT_DATA)));
+ case VSERPORT_CHANGE:
+ return Optional.of(new VserportChangeEvent(kind,
+ response.get(EVENT_DATA)));
default:
return Optional
.of(new MonitorEvent(kind, response.get(EVENT_DATA)));
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java
new file mode 100644
index 0000000..294ac7b
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/OsinfoEvent.java
@@ -0,0 +1,43 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2025 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu.events;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.jgrapes.core.Event;
+
+/**
+ * Signals information about the guest OS.
+ */
+public class OsinfoEvent extends Event {
+
+ private final JsonNode osinfo;
+
+ /**
+ * Instantiates a new osinfo event.
+ *
+ * @param data the data
+ */
+ public OsinfoEvent(JsonNode data) {
+ osinfo = data;
+ }
+
+ public JsonNode osinfo() {
+ return osinfo;
+ }
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java
new file mode 100644
index 0000000..b590cd3
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/VserportChangeEvent.java
@@ -0,0 +1,56 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2023 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.runner.qemu.events;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Signals a virtual serial port's open state change.
+ */
+public class VserportChangeEvent extends MonitorEvent {
+
+ /**
+ * Initializes a new instance.
+ *
+ * @param kind the kind
+ * @param data the data
+ */
+ public VserportChangeEvent(Kind kind, JsonNode data) {
+ super(kind, data);
+ }
+
+ /**
+ * Return the channel's id.
+ *
+ * @return the string
+ */
+ @SuppressWarnings("PMD.ShortMethodName")
+ public String id() {
+ return data().get("id").asText();
+ }
+
+ /**
+ * Returns the open state of the port.
+ *
+ * @return true, if is open
+ */
+ public boolean isOpen() {
+ return Boolean.parseBoolean(data().get("open").asText());
+ }
+}
diff --git a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
index 5d10b54..e2610ba 100644
--- a/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
+++ b/org.jdrupes.vmoperator.runner.qemu/templates/Standard-VM-latest.ftl.yaml
@@ -122,7 +122,7 @@
# Best explanation found:
# https://fedoraproject.org/wiki/Features/VirtioSerial
- [ "-device", "virtio-serial-pci,id=virtio-serial0" ]
- # - Guest agent serial connection
+ # - Guest agent serial connection. MUST have id "channel0"!
- [ "-device", "virtserialport,id=channel0,name=org.qemu.guest_agent.0,\
chardev=guest-agent-socket" ]
- [ "-chardev","socket,id=guest-agent-socket,\