diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java
new file mode 100644
index 0000000..7d54235
--- /dev/null
+++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java
@@ -0,0 +1,159 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 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.google.gson.JsonObject;
+import io.kubernetes.client.apimachinery.GroupVersionKind;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.models.EventsV1Event;
+import java.io.IOException;
+import java.util.logging.Level;
+import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
+import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
+import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
+import org.jdrupes.vmoperator.common.K8s;
+import org.jdrupes.vmoperator.common.K8sClient;
+import org.jdrupes.vmoperator.common.VmDefinitionStub;
+import org.jdrupes.vmoperator.runner.qemu.events.Exit;
+import org.jdrupes.vmoperator.runner.qemu.events.SpiceDisconnectedEvent;
+import org.jdrupes.vmoperator.runner.qemu.events.SpiceInitializedEvent;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.core.events.Start;
+
+/**
+ * A (sub)component that updates the console status in the CR status.
+ * Created as child of {@link StatusUpdater}.
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class ConsoleTracker extends VmDefUpdater {
+
+ private final K8sClient apiClient;
+ private VmDefinitionStub vmStub;
+ private String mainChannelClientHost;
+ private long mainChannelClientPort;
+
+ /**
+ * Instantiates a new status updater.
+ *
+ * @param componentChannel the component channel
+ */
+ @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
+ public ConsoleTracker(Channel componentChannel) {
+ super(componentChannel);
+ apiClient = (K8sClient) io.kubernetes.client.openapi.Configuration
+ .getDefaultApiClient();
+ }
+
+ /**
+ * Handle the start event.
+ *
+ * @param event the event
+ * @throws IOException
+ * @throws ApiException
+ */
+ @Handler
+ public void onStart(Start event) {
+ if (namespace == null) {
+ return;
+ }
+ try {
+ vmStub = VmDefinitionStub.get(apiClient,
+ new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
+ namespace, vmName);
+ } catch (ApiException e) {
+ logger.log(Level.SEVERE, e,
+ () -> "Cannot access VM object, terminating.");
+ event.cancel(true);
+ fire(new Exit(1));
+ }
+ }
+
+ /**
+ * On spice connected.
+ *
+ * @param event the event
+ * @throws ApiException the api exception
+ */
+ @Handler
+ @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
+ "PMD.AvoidDuplicateLiterals" })
+ public void onSpiceInitialized(SpiceInitializedEvent event)
+ throws ApiException {
+ if (vmStub == null) {
+ return;
+ }
+
+ // Only process connections using main channel.
+ if (event.channelType() != 1) {
+ return;
+ }
+ mainChannelClientHost = event.clientHost();
+ mainChannelClientPort = event.clientPort();
+ vmStub.updateStatus(from -> {
+ JsonObject status = from.status();
+ status.addProperty("consoleClient", event.clientHost());
+ updateCondition(apiClient, from, status, "ConsoleConnected",
+ true, "Connection from " + event.clientHost(), null);
+ return status;
+ });
+
+ // Log event
+ var evt = new EventsV1Event()
+ .reportingController(VM_OP_GROUP + "/" + APP_NAME)
+ .action("ConsoleConnectionUpdate")
+ .reason("Connection from " + event.clientHost());
+ K8s.createEvent(apiClient, vmStub.model().get(), evt);
+ }
+
+ /**
+ * On spice disconnected.
+ *
+ * @param event the event
+ * @throws ApiException the api exception
+ */
+ @Handler
+ @SuppressWarnings("PMD.AvoidDuplicateLiterals")
+ public void onSpiceDisconnected(SpiceDisconnectedEvent event)
+ throws ApiException {
+ if (vmStub == null) {
+ return;
+ }
+
+ // Only process disconnects from main channel.
+ if (!event.clientHost().equals(mainChannelClientHost)
+ || event.clientPort() != mainChannelClientPort) {
+ return;
+ }
+ vmStub.updateStatus(from -> {
+ JsonObject status = from.status();
+ status.addProperty("consoleClient", "");
+ updateCondition(apiClient, from, status, "ConsoleConnected",
+ false, event.clientHost() + " has disconnected", null);
+ return status;
+ });
+
+ // Log event
+ var evt = new EventsV1Event()
+ .reportingController(VM_OP_GROUP + "/" + APP_NAME)
+ .action("ConsoleConnectionUpdate")
+ .reason("Disconnected from " + event.clientHost());
+ K8s.createEvent(apiClient, vmStub.model().get(), evt);
+ }
+}
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 f6814b3..ca5d46a 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
@@ -27,22 +27,13 @@ import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.EventsV1Event;
import java.io.IOException;
import java.math.BigDecimal;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
-import java.util.stream.Collectors;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient;
-import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
@@ -53,28 +44,21 @@ import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
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;
-import org.jdrupes.vmoperator.runner.qemu.events.SpiceConnectedEvent;
-import org.jdrupes.vmoperator.runner.qemu.events.SpiceDisconnectedEvent;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jgrapes.core.Channel;
-import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
-import org.jgrapes.util.events.ConfigurationUpdate;
-import org.jgrapes.util.events.InitialConfiguration;
/**
* Updates the CR status.
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
-public class StatusUpdater extends Component {
+public class StatusUpdater extends VmDefUpdater {
private static final Set RUNNING_STATES
= Set.of(RunState.RUNNING, RunState.TERMINATING);
- private String namespace;
- private String vmName;
private K8sClient apiClient;
private long observedGeneration;
private boolean guestShutdownStops;
@@ -98,6 +82,7 @@ public class StatusUpdater extends Component {
() -> "Cannot access events API, terminating.");
fire(new Exit(1));
}
+ attach(new ConsoleTracker(componentChannel));
}
/**
@@ -114,43 +99,6 @@ public class StatusUpdater extends Component {
}
}
- /**
- * On configuration update.
- *
- * @param event the event
- */
- @Handler
- @SuppressWarnings("unchecked")
- public void onConfigurationUpdate(ConfigurationUpdate event) {
- event.structured("/Runner").ifPresent(c -> {
- if (event instanceof InitialConfiguration) {
- namespace = (String) c.get("namespace");
- updateNamespace();
- vmName = Optional.ofNullable((Map) c.get("vm"))
- .map(vm -> vm.get("name")).orElse(null);
- }
- });
- }
-
- private void updateNamespace() {
- if (namespace == null) {
- var path = Path
- .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
- if (Files.isReadable(path)) {
- try {
- namespace = Files.lines(path).findFirst().orElse(null);
- } catch (IOException e) {
- logger.log(Level.WARNING, e,
- () -> "Cannot read namespace.");
- }
- }
- }
- if (namespace == null) {
- logger.warning(() -> "Namespace is unknown, some functions"
- + " won't be available.");
- }
- }
-
/**
* Handle the start event.
*
@@ -238,13 +186,9 @@ public class StatusUpdater extends Component {
}
vmStub.updateStatus(vmDef, from -> {
JsonObject status = from.status();
- status.getAsJsonArray("conditions").asList().stream()
- .map(cond -> (JsonObject) cond)
- .forEach(cond -> {
- if ("Running".equals(cond.get("type").getAsString())) {
- updateRunningCondition(event, from, cond);
- }
- });
+ boolean running = RUNNING_STATES.contains(event.runState());
+ updateCondition(apiClient, vmDef, vmDef.status(), "Running",
+ running, event.reason(), event.message());
if (event.runState() == RunState.STARTING) {
status.addProperty("ram", GsonPtr.to(from.data())
.getAsString("spec", "vm", "maximumRam").orElse("0"));
@@ -253,6 +197,13 @@ public class StatusUpdater extends Component {
status.addProperty("ram", "0");
status.addProperty("cpus", 0);
}
+
+ // In case console connection was still present
+ if (!running) {
+ status.addProperty("consoleClient", "");
+ updateCondition(apiClient, from, status, "ConsoleConnected",
+ false, "VM has stopped", null);
+ }
return status;
});
@@ -278,29 +229,6 @@ public class StatusUpdater extends Component {
K8s.createEvent(apiClient, vmDef, evt);
}
- private void updateRunningCondition(RunnerStateChange event,
- K8sDynamicModel from, JsonObject cond) {
- @SuppressWarnings("PMD.AvoidDuplicateLiterals")
- boolean reportedRunning
- = "True".equals(cond.get("status").getAsString());
- if (RUNNING_STATES.contains(event.runState())
- && !reportedRunning) {
- cond.addProperty("status", "True");
- cond.addProperty("lastTransitionTime",
- Instant.now().toString());
- }
- if (!RUNNING_STATES.contains(event.runState())
- && reportedRunning) {
- cond.addProperty("status", "False");
- cond.addProperty("lastTransitionTime",
- Instant.now().toString());
- }
- cond.addProperty("reason", event.reason());
- cond.addProperty("message", event.message());
- cond.addProperty("observedGeneration",
- from.getMetadata().getGeneration());
- }
-
/**
* On ballon change.
*
@@ -369,91 +297,4 @@ public class StatusUpdater extends Component {
public void onShutdown(ShutdownEvent event) throws ApiException {
shutdownByGuest = event.byGuest();
}
-
- /**
- * On spice connected.
- *
- * @param event the event
- * @throws ApiException the api exception
- */
- @Handler
- public void onSpiceConnected(SpiceConnectedEvent event)
- throws ApiException {
- if (vmStub == null) {
- return;
- }
- vmStub.updateStatus(from -> {
- JsonObject status = from.status();
- status.addProperty("consoleClient", event.clientHost());
- updateConsoleConnectedCondition(from, status, true);
- return status;
- });
-
- // Log event
- var evt = new EventsV1Event()
- .reportingController(VM_OP_GROUP + "/" + APP_NAME)
- .action("ConsoleConnectionUpdate")
- .reason("Connection from " + event.clientHost());
- K8s.createEvent(apiClient, vmStub.model().get(), evt);
- }
-
- /**
- * On spice disconnected.
- *
- * @param event the event
- * @throws ApiException the api exception
- */
- @Handler
- public void onSpiceDisconnected(SpiceDisconnectedEvent event)
- throws ApiException {
- if (vmStub == null) {
- return;
- }
- vmStub.updateStatus(from -> {
- JsonObject status = from.status();
- status.addProperty("consoleClient", "");
- updateConsoleConnectedCondition(from, status, false);
- return status;
- });
-
- // Log event
- var evt = new EventsV1Event()
- .reportingController(VM_OP_GROUP + "/" + APP_NAME)
- .action("ConsoleConnectionUpdate")
- .reason("Disconnected from " + event.clientHost());
- K8s.createEvent(apiClient, vmStub.model().get(), evt);
- }
-
- private void updateConsoleConnectedCondition(VmDefinitionModel from,
- JsonObject status, boolean connected) {
- // Optimize, as we can get this several times
- var current = status.getAsJsonArray("conditions").asList().stream()
- .map(cond -> (JsonObject) cond)
- .filter(cond -> "ConsoleConnected"
- .equals(cond.get("type").getAsString()))
- .findFirst()
- .map(cond -> "True".equals(cond.get("status").getAsString()));
- if (current.isPresent() && current.get() == connected) {
- return;
- }
-
- // Do update
- final var condition = Map.of("type", "ConsoleConnected",
- "status", connected ? "True" : "False",
- "observedGeneration", from.getMetadata().getGeneration(),
- "reason", connected ? "Connected" : "Disconnected",
- "lastTransitionTime", Instant.now().toString());
- List