From aa7fdbee080d7ffb626b18f68057c74d4082c2b2 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sun, 25 Feb 2024 13:48:44 +0100 Subject: [PATCH] Allow guest to (finally) shutdown the VM. --- deploy/vmrunner-role.yaml | 1 + .../vmoperator/runner/qemu/Runner.java | 26 +++++----- .../vmoperator/runner/qemu/StatusUpdater.java | 31 ++++++++++++ .../runner/qemu/events/MonitorEvent.java | 17 ++++--- .../runner/qemu/events/RunnerStateChange.java | 32 +++++++++++++ .../runner/qemu/events/ShutdownEvent.java | 47 +++++++++++++++++++ 6 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java diff --git a/deploy/vmrunner-role.yaml b/deploy/vmrunner-role.yaml index 8aea4e2..c6df666 100644 --- a/deploy/vmrunner-role.yaml +++ b/deploy/vmrunner-role.yaml @@ -12,6 +12,7 @@ rules: verbs: - list - get + - patch - apiGroups: - vmoperator.jdrupes.org resources: 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 d615ad6..922f2af 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 @@ -640,13 +640,25 @@ public class Runner extends Component { return; } if (procDef.equals(qemuDefinition) && state == State.RUNNING) { - rep.fire(new Stop()); + rep.fire(new Exit(event.exitValue())); } logger.info(() -> "Process " + procDef.name + " has exited with value " + event.exitValue()); }); } + /** + * On exit. + * + * @param event the event + */ + @Handler(priority = 10_001) + public void onExit(Exit event) { + if (exitStatus == 0) { + exitStatus = event.exitStatus(); + } + } + /** * On stop. * @@ -656,7 +668,7 @@ public class Runner extends Component { public void onStopFirst(Stop event) { state = State.TERMINATING; rep.fire(new RunnerStateChange(state, "VmTerminating", - "The VM is being shut down")); + "The VM is being shut down", exitStatus != 0)); } /** @@ -671,16 +683,6 @@ public class Runner extends Component { "The VM has been shut down")); } - /** - * On exit. - * - * @param event the event - */ - @Handler - public void onExit(Exit event) { - exitStatus = event.exitStatus(); - } - private void shutdown() { if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) { fire(new Stop()); 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 542fa06..5f8cf13 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 @@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.runner.qemu; import com.google.gson.JsonObject; 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.apis.ApisApi; import io.kubernetes.client.openapi.apis.CustomObjectsApi; @@ -32,6 +33,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.Config; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.math.BigDecimal; import java.nio.file.Files; @@ -52,6 +54,7 @@ import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus; import org.jdrupes.vmoperator.runner.qemu.events.RunnerConfigurationUpdate; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; +import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent; import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -75,6 +78,7 @@ public class StatusUpdater extends Component { private DynamicKubernetesApi vmCrApi; private EventsV1Api evtsApi; private long observedGeneration; + private boolean shutdownByGuest; /** * Instantiates a new status updater. @@ -268,6 +272,22 @@ public class StatusUpdater extends Component { return status; }).throwsApiException(); + // Maybe stop VM + if (event.state() == State.TERMINATING && !event.failed() + && shutdownByGuest) { + PatchOptions patchOpts = new PatchOptions(); + patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); + var res = vmCrApi.patch(namespace, vmName, + V1Patch.PATCH_FORMAT_JSON_PATCH, + new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state" + + "\", \"value\": \"Stopped\"}]"), + patchOpts); + if (!res.isSuccess()) { + logger.warning( + () -> "Cannot patch pod annotations: " + res.getStatus()); + } + } + // Log event var evt = new EventsV1Event().kind("Event") .metadata(new V1ObjectMeta().namespace(namespace) @@ -344,4 +364,15 @@ public class StatusUpdater extends Component { return status; }).throwsApiException(); } + + /** + * On shutdown. + * + * @param event the event + * @throws ApiException the api exception + */ + @Handler + public void onShutdown(ShutdownEvent event) throws ApiException { + shutdownByGuest = event.byGuest(); + } } 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 72647a1..ba04a26 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 @@ -28,11 +28,13 @@ import org.jgrapes.core.Event; */ public class MonitorEvent extends Event { + private static final String EVENT_DATA = "data"; + /** * The kind of monitor event. */ public enum Kind { - READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE + READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN } private final Kind kind; @@ -47,20 +49,23 @@ public class MonitorEvent extends Event { @SuppressWarnings("PMD.TooFewBranchesForASwitchStatement") public static Optional from(JsonNode response) { try { - var kind - = MonitorEvent.Kind.valueOf(response.get("event").asText()); + var kind = MonitorEvent.Kind + .valueOf(response.get("event").asText()); switch (kind) { case POWERDOWN: return Optional.of(new PowerdownEvent(kind, null)); case DEVICE_TRAY_MOVED: return Optional - .of(new TrayMovedEvent(kind, response.get("data"))); + .of(new TrayMovedEvent(kind, response.get(EVENT_DATA))); case BALLOON_CHANGE: + return Optional.of( + new BalloonChangeEvent(kind, response.get(EVENT_DATA))); + case SHUTDOWN: return Optional - .of(new BalloonChangeEvent(kind, response.get("data"))); + .of(new ShutdownEvent(kind, response.get(EVENT_DATA))); default: return Optional - .of(new MonitorEvent(kind, response.get("data"))); + .of(new MonitorEvent(kind, response.get(EVENT_DATA))); } } catch (IllegalArgumentException e) { return Optional.empty(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java index 46fa1f8..5d5bffd 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java @@ -25,6 +25,7 @@ import org.jgrapes.core.Event; /** * The Class RunnerStateChange. */ +@SuppressWarnings("PMD.DataClass") public class RunnerStateChange extends Event { /** @@ -37,17 +38,36 @@ public class RunnerStateChange extends Event { private final State state; private final String reason; private final String message; + private final boolean failed; /** * Instantiates a new runner state change. * + * @param state the state + * @param reason the reason + * @param message the message * @param channels the channels */ public RunnerStateChange(State state, String reason, String message, Channel... channels) { + this(state, reason, message, false, channels); + } + + /** + * Instantiates a new runner state change. + * + * @param state the state + * @param reason the reason + * @param message the message + * @param failed the failed + * @param channels the channels + */ + public RunnerStateChange(State state, String reason, String message, + boolean failed, Channel... channels) { super(channels); this.state = state; this.reason = reason; + this.failed = failed; this.message = message; } @@ -78,11 +98,23 @@ public class RunnerStateChange extends Event { return message; } + /** + * Checks if is failed. + * + * @return the failed + */ + public boolean failed() { + return failed; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(Components.objectName(this)) .append(" [").append(state).append(": ").append(reason); + if (failed) { + builder.append(" (failed)"); + } if (channels() != null) { builder.append(", channels="); builder.append(Channel.toString(channels())); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java new file mode 100644 index 0000000..e46bbd3 --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/ShutdownEvent.java @@ -0,0 +1,47 @@ +/* + * 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.events; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Signals the processing of the {@link QmpShutdown} event. + */ +public class ShutdownEvent extends MonitorEvent { + + /** + * Instantiates a new shutdown event. + * + * @param kind the kind + * @param data the data + */ + public ShutdownEvent(Kind kind, JsonNode data) { + super(kind, data); + } + + /** + * returns if this is initiated by the guest. + * + * @return the value + */ + public boolean byGuest() { + return data().get("guest").asBoolean(); + } + +}