Allow guest to (finally) shutdown the VM.

This commit is contained in:
Michael Lipp 2024-02-25 13:48:44 +01:00
parent 1a608df411
commit aa7fdbee08
6 changed files with 136 additions and 18 deletions

View file

@ -12,6 +12,7 @@ rules:
verbs:
- list
- get
- patch
- apiGroups:
- vmoperator.jdrupes.org
resources:

View file

@ -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());

View file

@ -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();
}
}

View file

@ -28,11 +28,13 @@ import org.jgrapes.core.Event;
*/
public class MonitorEvent extends Event<Void> {
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<Void> {
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
public static Optional<MonitorEvent> 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();

View file

@ -25,6 +25,7 @@ import org.jgrapes.core.Event;
/**
* The Class RunnerStateChange.
*/
@SuppressWarnings("PMD.DataClass")
public class RunnerStateChange extends Event<Void> {
/**
@ -37,17 +38,36 @@ public class RunnerStateChange extends Event<Void> {
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<Void> {
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()));

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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();
}
}