Allow guest to (finally) shutdown the VM.
This commit is contained in:
parent
1a608df411
commit
aa7fdbee08
6 changed files with 136 additions and 18 deletions
|
|
@ -12,6 +12,7 @@ rules:
|
||||||
verbs:
|
verbs:
|
||||||
- list
|
- list
|
||||||
- get
|
- get
|
||||||
|
- patch
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- vmoperator.jdrupes.org
|
- vmoperator.jdrupes.org
|
||||||
resources:
|
resources:
|
||||||
|
|
|
||||||
|
|
@ -640,13 +640,25 @@ public class Runner extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (procDef.equals(qemuDefinition) && state == State.RUNNING) {
|
if (procDef.equals(qemuDefinition) && state == State.RUNNING) {
|
||||||
rep.fire(new Stop());
|
rep.fire(new Exit(event.exitValue()));
|
||||||
}
|
}
|
||||||
logger.info(() -> "Process " + procDef.name
|
logger.info(() -> "Process " + procDef.name
|
||||||
+ " has exited with value " + event.exitValue());
|
+ " 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.
|
* On stop.
|
||||||
*
|
*
|
||||||
|
|
@ -656,7 +668,7 @@ public class Runner extends Component {
|
||||||
public void onStopFirst(Stop event) {
|
public void onStopFirst(Stop event) {
|
||||||
state = State.TERMINATING;
|
state = State.TERMINATING;
|
||||||
rep.fire(new RunnerStateChange(state, "VmTerminating",
|
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"));
|
"The VM has been shut down"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On exit.
|
|
||||||
*
|
|
||||||
* @param event the event
|
|
||||||
*/
|
|
||||||
@Handler
|
|
||||||
public void onExit(Exit event) {
|
|
||||||
exitStatus = event.exitStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void shutdown() {
|
private void shutdown() {
|
||||||
if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) {
|
if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) {
|
||||||
fire(new Stop());
|
fire(new Stop());
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.runner.qemu;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import io.kubernetes.client.custom.Quantity;
|
import io.kubernetes.client.custom.Quantity;
|
||||||
import io.kubernetes.client.custom.Quantity.Format;
|
import io.kubernetes.client.custom.Quantity.Format;
|
||||||
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.openapi.apis.ApisApi;
|
import io.kubernetes.client.openapi.apis.ApisApi;
|
||||||
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
|
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.Config;
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
|
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
||||||
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Files;
|
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.RunnerConfigurationUpdate;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
|
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
|
||||||
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State;
|
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.jdrupes.vmoperator.util.GsonPtr;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
|
|
@ -75,6 +78,7 @@ public class StatusUpdater extends Component {
|
||||||
private DynamicKubernetesApi vmCrApi;
|
private DynamicKubernetesApi vmCrApi;
|
||||||
private EventsV1Api evtsApi;
|
private EventsV1Api evtsApi;
|
||||||
private long observedGeneration;
|
private long observedGeneration;
|
||||||
|
private boolean shutdownByGuest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new status updater.
|
* Instantiates a new status updater.
|
||||||
|
|
@ -268,6 +272,22 @@ public class StatusUpdater extends Component {
|
||||||
return status;
|
return status;
|
||||||
}).throwsApiException();
|
}).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
|
// Log event
|
||||||
var evt = new EventsV1Event().kind("Event")
|
var evt = new EventsV1Event().kind("Event")
|
||||||
.metadata(new V1ObjectMeta().namespace(namespace)
|
.metadata(new V1ObjectMeta().namespace(namespace)
|
||||||
|
|
@ -344,4 +364,15 @@ public class StatusUpdater extends Component {
|
||||||
return status;
|
return status;
|
||||||
}).throwsApiException();
|
}).throwsApiException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On shutdown.
|
||||||
|
*
|
||||||
|
* @param event the event
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
@Handler
|
||||||
|
public void onShutdown(ShutdownEvent event) throws ApiException {
|
||||||
|
shutdownByGuest = event.byGuest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,13 @@ import org.jgrapes.core.Event;
|
||||||
*/
|
*/
|
||||||
public class MonitorEvent extends Event<Void> {
|
public class MonitorEvent extends Event<Void> {
|
||||||
|
|
||||||
|
private static final String EVENT_DATA = "data";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The kind of monitor event.
|
* The kind of monitor event.
|
||||||
*/
|
*/
|
||||||
public enum Kind {
|
public enum Kind {
|
||||||
READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE
|
READY, POWERDOWN, DEVICE_TRAY_MOVED, BALLOON_CHANGE, SHUTDOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Kind kind;
|
private final Kind kind;
|
||||||
|
|
@ -47,20 +49,23 @@ public class MonitorEvent extends Event<Void> {
|
||||||
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
|
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
|
||||||
public static Optional<MonitorEvent> from(JsonNode response) {
|
public static Optional<MonitorEvent> from(JsonNode response) {
|
||||||
try {
|
try {
|
||||||
var kind
|
var kind = MonitorEvent.Kind
|
||||||
= MonitorEvent.Kind.valueOf(response.get("event").asText());
|
.valueOf(response.get("event").asText());
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case POWERDOWN:
|
case POWERDOWN:
|
||||||
return Optional.of(new PowerdownEvent(kind, null));
|
return Optional.of(new PowerdownEvent(kind, null));
|
||||||
case DEVICE_TRAY_MOVED:
|
case DEVICE_TRAY_MOVED:
|
||||||
return Optional
|
return Optional
|
||||||
.of(new TrayMovedEvent(kind, response.get("data")));
|
.of(new TrayMovedEvent(kind, response.get(EVENT_DATA)));
|
||||||
case BALLOON_CHANGE:
|
case BALLOON_CHANGE:
|
||||||
|
return Optional.of(
|
||||||
|
new BalloonChangeEvent(kind, response.get(EVENT_DATA)));
|
||||||
|
case SHUTDOWN:
|
||||||
return Optional
|
return Optional
|
||||||
.of(new BalloonChangeEvent(kind, response.get("data")));
|
.of(new ShutdownEvent(kind, response.get(EVENT_DATA)));
|
||||||
default:
|
default:
|
||||||
return Optional
|
return Optional
|
||||||
.of(new MonitorEvent(kind, response.get("data")));
|
.of(new MonitorEvent(kind, response.get(EVENT_DATA)));
|
||||||
}
|
}
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import org.jgrapes.core.Event;
|
||||||
/**
|
/**
|
||||||
* The Class RunnerStateChange.
|
* The Class RunnerStateChange.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class RunnerStateChange extends Event<Void> {
|
public class RunnerStateChange extends Event<Void> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,17 +38,36 @@ public class RunnerStateChange extends Event<Void> {
|
||||||
private final State state;
|
private final State state;
|
||||||
private final String reason;
|
private final String reason;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
private final boolean failed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new runner state change.
|
* Instantiates a new runner state change.
|
||||||
*
|
*
|
||||||
|
* @param state the state
|
||||||
|
* @param reason the reason
|
||||||
|
* @param message the message
|
||||||
* @param channels the channels
|
* @param channels the channels
|
||||||
*/
|
*/
|
||||||
public RunnerStateChange(State state, String reason, String message,
|
public RunnerStateChange(State state, String reason, String message,
|
||||||
Channel... channels) {
|
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);
|
super(channels);
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
|
this.failed = failed;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,11 +98,23 @@ public class RunnerStateChange extends Event<Void> {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if is failed.
|
||||||
|
*
|
||||||
|
* @return the failed
|
||||||
|
*/
|
||||||
|
public boolean failed() {
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append(Components.objectName(this))
|
builder.append(Components.objectName(this))
|
||||||
.append(" [").append(state).append(": ").append(reason);
|
.append(" [").append(state).append(": ").append(reason);
|
||||||
|
if (failed) {
|
||||||
|
builder.append(" (failed)");
|
||||||
|
}
|
||||||
if (channels() != null) {
|
if (channels() != null) {
|
||||||
builder.append(", channels=");
|
builder.append(", channels=");
|
||||||
builder.append(Channel.toString(channels()));
|
builder.append(Channel.toString(channels()));
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue