Move automatic login request to CRD.

Also reorganizes constants.
This commit is contained in:
Michael Lipp 2025-03-01 11:02:52 +01:00
parent 3152ff842b
commit 5366e24092
22 changed files with 259 additions and 206 deletions

View file

@ -1430,6 +1430,12 @@ spec:
outputs: outputs:
type: integer type: integer
default: 1 default: 1
loggedInUser:
description: >-
The name of a user that should be automatically
logged in on the display. Note that this requires
support from an agent in the guest OS.
type: string
spice: spice:
type: object type: object
properties: properties:
@ -1485,6 +1491,11 @@ spec:
connection. connection.
type: string type: string
default: "" default: ""
loggedInUser:
description: >-
The name of a user that is currently logged in by the
VM operator agent.
type: string
displayPasswordSerial: displayPasswordSerial:
description: >- description: >-
Counts changes of the display password. Set to -1 Counts changes of the display password. Set to -1

View file

@ -126,8 +126,18 @@ attemptLogout() {
# Log out any user currently using tty1. This is invoked when executing # Log out any user currently using tty1. This is invoked when executing
# the logout command and therefore sends back a 2xx return code. # the logout command and therefore sends back a 2xx return code.
# Also try to restart gdm, if it is not running.
doLogout() { doLogout() {
attemptLogout attemptLogout
systemctl status gdm >/dev/null 2>&1
if [ $? != 0 ]; then
systemctl restart gdm 2>$temperr
if [ $? -eq 0 ]; then
echo >&${con} "102 gdm restarted"
else
echo >&${con} "102 Restarting gdm failed: $(tr '\n' ' ' <${temperr})"
fi
fi
echo >&${con} "202 User logged out" echo >&${con} "202 User logged out"
} }

View file

@ -27,31 +27,47 @@ public class Constants {
/** The Constant APP_NAME. */ /** The Constant APP_NAME. */
public static final String APP_NAME = "vm-runner"; public static final String APP_NAME = "vm-runner";
/** The Constant VM_OP_NAME. */ /**
public static final String VM_OP_NAME = "vm-operator"; * Constants related to the CRD.
*/
@SuppressWarnings("PMD.ShortClassName")
public static class Crd {
/** The Constant VM_OP_GROUP. */ /** The Constant NAME. */
public static final String VM_OP_GROUP = "vmoperator.jdrupes.org"; public static final String NAME = "vm-operator";
/** The Constant VM_OP_KIND_VM. */ /** The Constant GROUP. */
public static final String VM_OP_KIND_VM = "VirtualMachine"; public static final String GROUP = "vmoperator.jdrupes.org";
/** The Constant VM_OP_KIND_VM_POOL. */ /** The Constant KIND_VM. */
public static final String VM_OP_KIND_VM_POOL = "VmPool"; public static final String KIND_VM = "VirtualMachine";
/** The Constant COMP_DISPLAY_SECRETS. */ /** The Constant KIND_VM_POOL. */
public static final String COMP_DISPLAY_SECRET = "display-secret"; public static final String KIND_VM_POOL = "VmPool";
}
/** The Constant DATA_DISPLAY_PASSWORD. */ /**
public static final String DATA_DISPLAY_PASSWORD = "display-password"; * Constants for the display secret.
*/
public static class DisplaySecret {
/** The Constant DATA_PASSWORD_EXPIRY. */ /** The Constant NAME. */
public static final String DATA_PASSWORD_EXPIRY = "password-expiry"; public static final String NAME = "display-secret";
/** The Constant DATA_DISPLAY_USER. */ /** The Constant DISPLAY_PASSWORD. */
public static final String DATA_DISPLAY_USER = "display-user"; public static final String DISPLAY_PASSWORD = "display-password";
/** The Constant DATA_DISPLAY_LOGIN. */ /** The Constant PASSWORD_EXPIRY. */
public static final String DATA_DISPLAY_LOGIN = "login-user"; public static final String PASSWORD_EXPIRY = "password-expiry";
}
/**
* Constants for status fields.
*/
public static class Status {
/** The Constant LOGGED_IN_USER. */
public static final String LOGGED_IN_USER = "loggedInUser";
} }
}

View file

@ -193,7 +193,7 @@ public class K8sGenericStub<O extends KubernetesObject,
} }
/** /**
* Updates the object's status. * Updates the object's status. This method will not retry.
* *
* @param object the current state of the object (passed to `status`) * @param object the current state of the object (passed to `status`)
* @param status function that returns the new status * @param status function that returns the new status
@ -231,7 +231,7 @@ public class K8sGenericStub<O extends KubernetesObject,
} }
/** /**
* Updates the status. * Updates the status. In case of conflict, retries up to 16 times.
* *
* @param status the status * @param status the status
* @return the kubernetes api response * @return the kubernetes api response

View file

@ -201,6 +201,9 @@ data:
<#if spec.vm.display.outputs?? > <#if spec.vm.display.outputs?? >
outputs: ${ spec.vm.display.outputs?c } outputs: ${ spec.vm.display.outputs?c }
</#if> </#if>
<#if spec.vm.display.loggedInUser?? >
loggedInUser: "${ spec.vm.display.loggedInUser }"
</#if>
<#if spec.vm.display.spice??> <#if spec.vm.display.spice??>
spice: spice:
port: ${ spec.vm.display.spice.port?c } port: ${ spec.vm.display.spice.port?c }

View file

@ -33,9 +33,9 @@ import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.Constants.Crd;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.util.DataPath; import org.jdrupes.vmoperator.util.DataPath;
import org.jdrupes.vmoperator.util.GsonPtr; import org.jdrupes.vmoperator.util.GsonPtr;
@ -121,7 +121,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
DynamicKubernetesObject newCm) { DynamicKubernetesObject newCm) {
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector( listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + Crd.NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + "," + "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + newCm.getMetadata() + "app.kubernetes.io/instance=" + newCm.getMetadata()
.getLabels().get("app.kubernetes.io/instance")); .getLabels().get("app.kubernetes.io/instance"));

View file

@ -29,8 +29,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.Constants.Crd;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
@ -194,7 +193,7 @@ public class Controller extends Component {
private void patchVmDef(K8sClient client, String name, String path, private void patchVmDef(K8sClient client, String name, String path,
Object value) throws ApiException, IOException { Object value) throws ApiException, IOException {
var vmStub = K8sDynamicStub.get(client, var vmStub = K8sDynamicStub.get(client,
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), namespace, new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace,
name); name);
// Patch running // Patch running
@ -227,7 +226,7 @@ public class Controller extends Component {
try { try {
var vmDef = channel.vmDefinition(); var vmDef = channel.vmDefinition();
var vmStub = VmDefinitionStub.get(channel.client(), var vmStub = VmDefinitionStub.get(channel.client(),
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
vmDef.namespace(), vmDef.name()); vmDef.namespace(), vmDef.name());
if (vmStub.updateStatus(vmDef, from -> { if (vmStub.updateStatus(vmDef, from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();

View file

@ -28,11 +28,11 @@ import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; import org.jdrupes.vmoperator.common.Constants.Crd;
import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sV1PodStub; import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1SecretStub; import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
import org.jdrupes.vmoperator.manager.events.ChannelDictionary; import org.jdrupes.vmoperator.manager.events.ChannelDictionary;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
@ -61,7 +61,7 @@ public class DisplaySecretMonitor
context(K8sV1SecretStub.CONTEXT); context(K8sV1SecretStub.CONTEXT);
ListOptions options = new ListOptions(); ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); + "app.kubernetes.io/component=" + DisplaySecret.NAME);
options(options); options(options);
} }
@ -95,7 +95,7 @@ public class DisplaySecretMonitor
// Force update for pod // Force update for pod
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector( listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + Crd.NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + "," + "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + change.object.getMetadata() + "app.kubernetes.io/instance=" + change.object.getMetadata()
.getLabels().get("app.kubernetes.io/instance")); .getLabels().get("app.kubernetes.io/instance"));

View file

@ -26,7 +26,6 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret; import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException; import java.io.IOException;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.Instant; import java.time.Instant;
@ -34,20 +33,16 @@ import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Scanner; import java.util.Scanner;
import java.util.logging.Logger; import java.util.logging.Logger;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.COMP_DISPLAY_SECRET; import org.jdrupes.vmoperator.common.Constants.Crd;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import org.jdrupes.vmoperator.common.Constants.Status;
import org.jdrupes.vmoperator.common.K8sV1SecretStub; import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_LOGIN;
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_USER;
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
import org.jdrupes.vmoperator.manager.events.PrepareConsole; import org.jdrupes.vmoperator.manager.events.PrepareConsole;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged;
@ -146,7 +141,7 @@ public class DisplaySecretReconciler extends Component {
var vmDef = event.vmDefinition(); var vmDef = event.vmDefinition();
ListOptions options = new ListOptions(); ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + "," + "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name()); + "app.kubernetes.io/instance=" + vmDef.name());
var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(), var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(),
options); options);
@ -157,9 +152,9 @@ public class DisplaySecretReconciler extends Component {
// Create secret // Create secret
var secret = new V1Secret(); var secret = new V1Secret();
secret.setMetadata(new V1ObjectMeta().namespace(vmDef.namespace()) secret.setMetadata(new V1ObjectMeta().namespace(vmDef.namespace())
.name(vmDef.name() + "-" + COMP_DISPLAY_SECRET) .name(vmDef.name() + "-" + DisplaySecret.NAME)
.putLabelsItem("app.kubernetes.io/name", APP_NAME) .putLabelsItem("app.kubernetes.io/name", APP_NAME)
.putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET) .putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME)
.putLabelsItem("app.kubernetes.io/instance", vmDef.name())); .putLabelsItem("app.kubernetes.io/instance", vmDef.name()));
secret.setType("Opaque"); secret.setType("Opaque");
SecureRandom random = null; SecureRandom random = null;
@ -172,8 +167,8 @@ public class DisplaySecretReconciler extends Component {
byte[] bytes = new byte[16]; byte[] bytes = new byte[16];
random.nextBytes(bytes); random.nextBytes(bytes);
var password = Base64.encode(bytes); var password = Base64.encode(bytes);
secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password, secret.setStringData(Map.of(DisplaySecret.DISPLAY_PASSWORD, password,
DATA_PASSWORD_EXPIRY, "now")); DisplaySecret.PASSWORD_EXPIRY, "now"));
K8sV1SecretStub.create(channel.client(), secret); K8sV1SecretStub.create(channel.client(), secret);
} }
@ -192,49 +187,31 @@ public class DisplaySecretReconciler extends Component {
public void onPrepareConsole(PrepareConsole event, VmChannel channel) public void onPrepareConsole(PrepareConsole event, VmChannel channel)
throws ApiException { throws ApiException {
// Update console user in status // Update console user in status
var vmStub = VmDefinitionStub.get(channel.client(), var vmDef = updateConsoleUser(event, channel);
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), if (vmDef == null) {
event.vmDefinition().namespace(), event.vmDefinition().name());
var optVmDef = vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
status.addProperty("consoleUser", event.user());
return status;
});
if (optVmDef.isEmpty()) {
return; return;
} }
var vmDef = optVmDef.get();
// Check if access is possible // Check if access is possible
if (event.loginUser() if (event.loginUser()
? !vmDef.conditionStatus("Booted").orElse(false) ? !vmDef.<String> fromStatus(Status.LOGGED_IN_USER)
.map(u -> u.equals(event.user())).orElse(false)
: !vmDef.conditionStatus("Running").orElse(false)) { : !vmDef.conditionStatus("Running").orElse(false)) {
return; return;
} }
// Look for secret // Get secret and update password in secret
ListOptions options = new ListOptions(); var stub = getSecretStub(event, channel, vmDef);
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," if (stub == null) {
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
+ "app.kubernetes.io/instance=" + vmDef.name());
var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(),
options);
if (stubs.isEmpty()) {
// No secret means no password for this VM wanted
event.setResult(null);
return; return;
} }
var stub = stubs.iterator().next();
// Get secret and update
var secret = stub.model().get(); var secret = stub.model().get();
var updPw = updatePassword(secret, event); if (!updatePassword(secret, event)) {
var updUsr = updateUser(secret, event);
if (!updPw && !updUsr) {
return; return;
} }
// Register wait for confirmation (by VM status change) // Register wait for confirmation (by VM status change,
// after secret update)
var pending = new PendingPrepare(event, var pending = new PendingPrepare(event,
event.vmDefinition().displayPasswordSerial().orElse(0L) + 1, event.vmDefinition().displayPasswordSerial().orElse(0L) + 1,
new CompletionLock(event, 1500)); new CompletionLock(event, 1500));
@ -247,30 +224,45 @@ public class DisplaySecretReconciler extends Component {
stub.update(secret).getObject(); stub.update(secret).getObject();
} }
private boolean updateUser(V1Secret secret, PrepareConsole event) { private VmDefinition updateConsoleUser(PrepareConsole event,
var curUser = DataPath.<byte[]> get(secret, "data", DATA_DISPLAY_USER) VmChannel channel) throws ApiException {
.map(b -> new String(b, UTF_8)).orElse(null); var vmStub = VmDefinitionStub.get(channel.client(),
var curLogin = DataPath.<byte[]> get(secret, "data", DATA_DISPLAY_LOGIN) new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
.map(b -> new String(b, UTF_8)).map(Boolean::parseBoolean) event.vmDefinition().namespace(), event.vmDefinition().name());
.orElse(null); return vmStub.updateStatus(from -> {
if (Objects.equals(curUser, event.user()) && Objects.equals( JsonObject status = from.statusJson();
curLogin, event.loginUser())) { status.addProperty("consoleUser", event.user());
return false; return status;
}).orElse(null);
} }
secret.getData().put(DATA_DISPLAY_USER, event.user().getBytes(UTF_8));
secret.getData().put(DATA_DISPLAY_LOGIN, private K8sV1SecretStub getSecretStub(PrepareConsole event,
Boolean.toString(event.loginUser()).getBytes(UTF_8)); VmChannel channel, VmDefinition vmDef) throws ApiException {
return true; // Look for secret
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name());
var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(),
options);
if (stubs.isEmpty()) {
// No secret means no password for this VM wanted
event.setResult(null);
return null;
}
return stubs.iterator().next();
} }
private boolean updatePassword(V1Secret secret, PrepareConsole event) { private boolean updatePassword(V1Secret secret, PrepareConsole event) {
var expiry = Optional.ofNullable(secret.getData() var expiry = Optional.ofNullable(secret.getData()
.get(DATA_PASSWORD_EXPIRY)).map(b -> new String(b)).orElse(null); .get(DisplaySecret.PASSWORD_EXPIRY)).map(b -> new String(b))
if (secret.getData().get(DATA_DISPLAY_PASSWORD) != null .orElse(null);
if (secret.getData().get(DisplaySecret.DISPLAY_PASSWORD) != null
&& stillValid(expiry)) { && stillValid(expiry)) {
// Fixed secret, don't touch // Fixed secret, don't touch
event.setResult( event.setResult(
new String(secret.getData().get(DATA_DISPLAY_PASSWORD))); new String(
secret.getData().get(DisplaySecret.DISPLAY_PASSWORD)));
return false; return false;
} }
@ -285,8 +277,8 @@ public class DisplaySecretReconciler extends Component {
byte[] bytes = new byte[16]; byte[] bytes = new byte[16];
random.nextBytes(bytes); random.nextBytes(bytes);
var password = Base64.encode(bytes); var password = Base64.encode(bytes);
secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password, secret.setStringData(Map.of(DisplaySecret.DISPLAY_PASSWORD, password,
DATA_PASSWORD_EXPIRY, DisplaySecret.PASSWORD_EXPIRY,
Long.toString(Instant.now().getEpochSecond() + passwordValidity))); Long.toString(Instant.now().getEpochSecond() + passwordValidity)));
event.setResult(password); event.setResult(password);
return true; return true;

View file

@ -40,7 +40,7 @@ import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option; import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import org.jdrupes.vmoperator.common.Constants.Crd;
import org.jdrupes.vmoperator.manager.events.Exit; import org.jdrupes.vmoperator.manager.events.Exit;
import org.jdrupes.vmoperator.util.FsdUtils; import org.jdrupes.vmoperator.util.FsdUtils;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
@ -108,7 +108,7 @@ public class Manager extends Component {
// Configuration store with file in /etc/opt (default) // Configuration store with file in /etc/opt (default)
File cfgFile = new File(cmdLine.getOptionValue('c', File cfgFile = new File(cmdLine.getOptionValue('c',
"/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml")); "/etc/opt/" + Crd.NAME.replace("-", "") + "/config.yaml"));
logger.config(() -> "Using configuration from: " + cfgFile.getPath()); logger.config(() -> "Using configuration from: " + cfgFile.getPath());
// Don't rely on night config to produce a good exception // Don't rely on night config to produce a good exception
// for this simple case // for this simple case
@ -271,7 +271,7 @@ public class Manager extends Component {
try { try {
// Get logging properties from file and put them in effect // Get logging properties from file and put them in effect
InputStream props; InputStream props;
var path = FsdUtils.findConfigFile(VM_OP_NAME.replace("-", ""), var path = FsdUtils.findConfigFile(Crd.NAME.replace("-", ""),
"logging.properties"); "logging.properties");
if (path.isPresent()) { if (path.isPresent()) {
props = Files.newInputStream(path.get()); props = Files.newInputStream(path.get());

View file

@ -28,8 +28,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.Constants.Crd;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel; import org.jdrupes.vmoperator.common.K8sDynamicModel;
@ -38,7 +37,6 @@ import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM_POOL;
import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmPoolChanged; import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
@ -88,7 +86,7 @@ public class PoolMonitor extends
client(new K8sClient()); client(new K8sClient());
// Get all our API versions // Get all our API versions
var ctx = K8s.context(client(), VM_OP_GROUP, "", VM_OP_KIND_VM_POOL); var ctx = K8s.context(client(), Crd.GROUP, "", Crd.KIND_VM_POOL);
if (ctx.isEmpty()) { if (ctx.isEmpty()) {
logger.severe(() -> "Cannot get CRD context."); logger.severe(() -> "Cannot get CRD context.");
return; return;
@ -184,7 +182,7 @@ public class PoolMonitor extends
return; return;
} }
var vmStub = VmDefinitionStub.get(client(), var vmStub = VmDefinitionStub.get(client(),
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
vmDef.namespace(), vmDef.name()); vmDef.namespace(), vmDef.name());
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
// TODO // TODO

View file

@ -36,7 +36,7 @@ import java.util.Set;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; import org.jdrupes.vmoperator.common.Constants.Crd;
import org.jdrupes.vmoperator.common.K8sV1PvcStub; import org.jdrupes.vmoperator.common.K8sV1PvcStub;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged;
@ -83,7 +83,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
// Existing disks // Existing disks
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector( listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + Crd.NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + "," + "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name()); + "app.kubernetes.io/instance=" + vmDef.name());
var knownDisks = K8sV1PvcStub.list(channel.client(), var knownDisks = K8sV1PvcStub.list(channel.client(),

View file

@ -46,12 +46,12 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import org.jdrupes.vmoperator.common.Convertions; import org.jdrupes.vmoperator.common.Convertions;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sV1SecretStub; import org.jdrupes.vmoperator.common.K8sV1SecretStub;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged;
@ -276,7 +276,7 @@ public class Reconciler extends Component {
// Check if we have a display secret // Check if we have a display secret
ListOptions options = new ListOptions(); ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + "," + "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
+ "app.kubernetes.io/instance=" + vmDef.name()); + "app.kubernetes.io/instance=" + vmDef.name());
var dsStub = K8sV1SecretStub var dsStub = K8sV1SecretStub
.list(client, vmDef.namespace(), options) .list(client, vmDef.namespace(), options)

View file

@ -31,8 +31,7 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.Constants.Crd;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.common.K8sDynamicStub;
@ -46,7 +45,6 @@ import org.jdrupes.vmoperator.common.VmDefinitions;
import org.jdrupes.vmoperator.common.VmExtraData; import org.jdrupes.vmoperator.common.VmExtraData;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.AssignVm; import org.jdrupes.vmoperator.manager.events.AssignVm;
import org.jdrupes.vmoperator.manager.events.ChannelManager; import org.jdrupes.vmoperator.manager.events.ChannelManager;
import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.GetPools;
@ -87,7 +85,7 @@ public class VmMonitor extends
client(new K8sClient()); client(new K8sClient());
// Get all our API versions // Get all our API versions
var ctx = K8s.context(client(), VM_OP_GROUP, "", VM_OP_KIND_VM); var ctx = K8s.context(client(), Crd.GROUP, "", Crd.KIND_VM);
if (ctx.isEmpty()) { if (ctx.isEmpty()) {
logger.severe(() -> "Cannot get CRD context."); logger.severe(() -> "Cannot get CRD context.");
return; return;
@ -105,7 +103,7 @@ public class VmMonitor extends
.stream().map(stub -> stub.name()).collect(Collectors.toSet()); .stream().map(stub -> stub.name()).collect(Collectors.toSet());
ListOptions opts = new ListOptions(); ListOptions opts = new ListOptions();
opts.setLabelSelector( opts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + Crd.NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME); + "app.kubernetes.io/name=" + APP_NAME);
for (var context : Set.of(K8sV1StatefulSetStub.CONTEXT, for (var context : Set.of(K8sV1StatefulSetStub.CONTEXT,
K8sV1ConfigMapStub.CONTEXT)) { K8sV1ConfigMapStub.CONTEXT)) {

View file

@ -13,10 +13,8 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.COMP_DISPLAY_SECRET; import org.jdrupes.vmoperator.common.Constants.Crd;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.common.K8sDynamicStub;
@ -60,7 +58,7 @@ class BasicTests {
waitForManager(); waitForManager();
// Context for working with our CR // Context for working with our CR
var apiRes = K8s.context(client, VM_OP_GROUP, null, VM_OP_KIND_VM); var apiRes = K8s.context(client, Crd.GROUP, null, Crd.KIND_VM);
assertTrue(apiRes.isPresent()); assertTrue(apiRes.isPresent());
vmsContext = apiRes.get(); vmsContext = apiRes.get();
@ -70,7 +68,7 @@ class BasicTests {
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + VM_NAME + "," + "app.kubernetes.io/instance=" + VM_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); + "app.kubernetes.io/component=" + DisplaySecret.NAME);
var secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts); var secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts);
for (var secret : secrets) { for (var secret : secrets) {
secret.delete(); secret.delete();
@ -100,7 +98,7 @@ class BasicTests {
private static void deletePvcs() throws ApiException { private static void deletePvcs() throws ApiException {
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector( listOpts.setLabelSelector(
"app.kubernetes.io/managed-by=" + VM_OP_NAME + "," "app.kubernetes.io/managed-by=" + Crd.NAME + ","
+ "app.kubernetes.io/name=" + APP_NAME + "," + "app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + VM_NAME); + "app.kubernetes.io/instance=" + VM_NAME);
var knownPvcs = K8sV1PvcStub.list(client, "vmop-dev", listOpts); var knownPvcs = K8sV1PvcStub.list(client, "vmop-dev", listOpts);
@ -139,11 +137,11 @@ class BasicTests {
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
List.of("labels", "app.kubernetes.io/managed-by"), List.of("labels", "app.kubernetes.io/managed-by"),
Constants.VM_OP_NAME, Crd.NAME,
List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS, List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS,
List.of("ownerReferences", 0, "apiVersion"), List.of("ownerReferences", 0, "apiVersion"),
vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0), vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0),
List.of("ownerReferences", 0, "kind"), Constants.VM_OP_KIND_VM, List.of("ownerReferences", 0, "kind"), Crd.KIND_VM,
List.of("ownerReferences", 0, "name"), VM_NAME, List.of("ownerReferences", 0, "name"), VM_NAME,
List.of("ownerReferences", 0, "uid"), EXISTS); List.of("ownerReferences", 0, "uid"), EXISTS);
checkProps(config.getMetadata(), toCheck); checkProps(config.getMetadata(), toCheck);
@ -189,7 +187,7 @@ class BasicTests {
ListOptions listOpts = new ListOptions(); ListOptions listOpts = new ListOptions();
listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/instance=" + VM_NAME + "," + "app.kubernetes.io/instance=" + VM_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); + "app.kubernetes.io/component=" + DisplaySecret.NAME);
Collection<K8sV1SecretStub> secrets = null; Collection<K8sV1SecretStub> secrets = null;
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts); secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts);
@ -220,7 +218,7 @@ class BasicTests {
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
List.of("labels", "app.kubernetes.io/managed-by"), List.of("labels", "app.kubernetes.io/managed-by"),
Constants.VM_OP_NAME)); Crd.NAME));
checkProps(pvc.getSpec(), Map.of( checkProps(pvc.getSpec(), Map.of(
List.of("resources", "requests", "storage"), List.of("resources", "requests", "storage"),
Quantity.fromString("1Mi"))); Quantity.fromString("1Mi")));
@ -241,7 +239,7 @@ class BasicTests {
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
List.of("labels", "app.kubernetes.io/managed-by"), List.of("labels", "app.kubernetes.io/managed-by"),
Constants.VM_OP_NAME, Crd.NAME,
List.of("annotations", "use_as"), "system-disk")); List.of("annotations", "use_as"), "system-disk"));
checkProps(pvc.getSpec(), Map.of( checkProps(pvc.getSpec(), Map.of(
List.of("resources", "requests", "storage"), List.of("resources", "requests", "storage"),
@ -263,7 +261,7 @@ class BasicTests {
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME, List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
List.of("labels", "app.kubernetes.io/managed-by"), List.of("labels", "app.kubernetes.io/managed-by"),
Constants.VM_OP_NAME)); Crd.NAME));
checkProps(pvc.getSpec(), Map.of( checkProps(pvc.getSpec(), Map.of(
List.of("resources", "requests", "storage"), List.of("resources", "requests", "storage"),
Quantity.fromString("1Gi"))); Quantity.fromString("1Gi")));
@ -291,12 +289,12 @@ class BasicTests {
List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
List.of("labels", "app.kubernetes.io/component"), APP_NAME, List.of("labels", "app.kubernetes.io/component"), APP_NAME,
List.of("labels", "app.kubernetes.io/managed-by"), List.of("labels", "app.kubernetes.io/managed-by"),
Constants.VM_OP_NAME, Crd.NAME,
List.of("annotations", "vmrunner.jdrupes.org/cmVersion"), EXISTS, List.of("annotations", "vmrunner.jdrupes.org/cmVersion"), EXISTS,
List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS, List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS,
List.of("ownerReferences", 0, "apiVersion"), List.of("ownerReferences", 0, "apiVersion"),
vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0), vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0),
List.of("ownerReferences", 0, "kind"), Constants.VM_OP_KIND_VM, List.of("ownerReferences", 0, "kind"), Crd.KIND_VM,
List.of("ownerReferences", 0, "name"), VM_NAME, List.of("ownerReferences", 0, "name"), VM_NAME,
List.of("ownerReferences", 0, "uid"), EXISTS)); List.of("ownerReferences", 0, "uid"), EXISTS));
checkProps(pod.getSpec(), Map.of( checkProps(pod.getSpec(), Map.of(
@ -319,7 +317,7 @@ class BasicTests {
checkProps(svc.getMetadata(), Map.of( checkProps(svc.getMetadata(), Map.of(
List.of("labels", "app.kubernetes.io/name"), APP_NAME, List.of("labels", "app.kubernetes.io/name"), APP_NAME,
List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME, List.of("labels", "app.kubernetes.io/managed-by"), Crd.NAME,
List.of("labels", "label1"), "label1", List.of("labels", "label1"), "label1",
List.of("labels", "label2"), "replaced", List.of("labels", "label2"), "replaced",
List.of("labels", "label3"), "added", List.of("labels", "label3"), "added",

View file

@ -19,8 +19,8 @@
handlers=java.util.logging.ConsoleHandler handlers=java.util.logging.ConsoleHandler
org.jgrapes.level=FINE #org.jgrapes.level=FINE
org.jgrapes.core.handlerTracking.level=FINER #org.jgrapes.core.handlerTracking.level=FINER
org.jdrupes.vmoperator.runner.qemu.level=FINE org.jdrupes.vmoperator.runner.qemu.level=FINE

View file

@ -248,6 +248,9 @@ public class Configuration implements Dto {
/** The number of outputs. */ /** The number of outputs. */
public int outputs = 1; public int outputs = 1;
/** The logged in user. */
public String loggedInUser;
/** The spice. */ /** The spice. */
public Spice spice; public Spice spice;
} }

View file

@ -25,8 +25,7 @@ import io.kubernetes.client.openapi.models.EventsV1Event;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.Constants.Crd;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
@ -74,7 +73,7 @@ public class ConsoleTracker extends VmDefUpdater {
} }
try { try {
vmStub = VmDefinitionStub.get(apiClient, vmStub = VmDefinitionStub.get(apiClient,
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
namespace, vmName); namespace, vmName);
} catch (ApiException e) { } catch (ApiException e) {
logger.log(Level.SEVERE, e, logger.log(Level.SEVERE, e,
@ -115,7 +114,7 @@ public class ConsoleTracker extends VmDefUpdater {
// Log event // Log event
var evt = new EventsV1Event() var evt = new EventsV1Event()
.reportingController(VM_OP_GROUP + "/" + APP_NAME) .reportingController(Crd.GROUP + "/" + APP_NAME)
.action("ConsoleConnectionUpdate") .action("ConsoleConnectionUpdate")
.reason("Connection from " + event.clientHost()); .reason("Connection from " + event.clientHost());
K8s.createEvent(apiClient, vmStub.model().get(), evt); K8s.createEvent(apiClient, vmStub.model().get(), evt);
@ -150,7 +149,7 @@ public class ConsoleTracker extends VmDefUpdater {
// Log event // Log event
var evt = new EventsV1Event() var evt = new EventsV1Event()
.reportingController(VM_OP_GROUP + "/" + APP_NAME) .reportingController(Crd.GROUP + "/" + APP_NAME)
.action("ConsoleConnectionUpdate") .action("ConsoleConnectionUpdate")
.reason("Disconnected from " + event.clientHost()); .reason("Disconnected from " + event.clientHost());
K8s.createEvent(apiClient, vmStub.model().get(), evt); K8s.createEvent(apiClient, vmStub.model().get(), evt);

View file

@ -1,6 +1,6 @@
/* /*
* VM-Operator * VM-Operator
* Copyright (C) 2023 Michael N. Lipp * Copyright (C) 2023,2025 Michael N. Lipp
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -24,10 +24,7 @@ import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.DATA_DISPLAY_LOGIN; import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import static org.jdrupes.vmoperator.common.Constants.DATA_DISPLAY_PASSWORD;
import static org.jdrupes.vmoperator.common.Constants.DATA_DISPLAY_USER;
import static org.jdrupes.vmoperator.common.Constants.DATA_PASSWORD_EXPIRY;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
@ -35,9 +32,10 @@ import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected;
import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn; import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn;
import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn; import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
import org.jgrapes.core.Event;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.util.events.FileChanged; import org.jgrapes.util.events.FileChanged;
import org.jgrapes.util.events.WatchFile; import org.jgrapes.util.events.WatchFile;
@ -52,6 +50,7 @@ public class DisplayController extends Component {
private String protocol; private String protocol;
private final Path configDir; private final Path configDir;
private boolean vmopAgentConnected; private boolean vmopAgentConnected;
private String loggedInUser;
/** /**
* Instantiates a new Display controller. * Instantiates a new Display controller.
@ -64,17 +63,7 @@ public class DisplayController extends Component {
public DisplayController(Channel componentChannel, Path configDir) { public DisplayController(Channel componentChannel, Path configDir) {
super(componentChannel); super(componentChannel);
this.configDir = configDir; this.configDir = configDir;
fire(new WatchFile(configDir.resolve(DATA_DISPLAY_PASSWORD))); fire(new WatchFile(configDir.resolve(DisplaySecret.DISPLAY_PASSWORD)));
}
/**
* On vmop agent connected.
*
* @param event the event
*/
@Handler
public void onVmopAgentConnected(VmopAgentConnected event) {
vmopAgentConnected = true;
} }
/** /**
@ -89,7 +78,32 @@ public class DisplayController extends Component {
} }
protocol protocol
= event.configuration().vm.display.spice != null ? "spice" : null; = event.configuration().vm.display.spice != null ? "spice" : null;
configureAccess(false); loggedInUser = event.configuration().vm.display.loggedInUser;
configureLogin();
if (event.runState() == RunState.STARTING) {
configurePassword();
}
}
/**
* On vmop agent connected.
*
* @param event the event
*/
@Handler
public void onVmopAgentConnected(VmopAgentConnected event) {
vmopAgentConnected = true;
configureLogin();
}
private void configureLogin() {
if (!vmopAgentConnected) {
return;
}
Event<?> evt = loggedInUser != null
? new VmopAgentLogIn(loggedInUser)
: new VmopAgentLogOut();
fire(evt);
} }
/** /**
@ -100,46 +114,10 @@ public class DisplayController extends Component {
@Handler @Handler
@SuppressWarnings("PMD.EmptyCatchBlock") @SuppressWarnings("PMD.EmptyCatchBlock")
public void onFileChanged(FileChanged event) { public void onFileChanged(FileChanged event) {
if (event.path().equals(configDir.resolve(DATA_DISPLAY_PASSWORD))) { if (event.path()
configureAccess(true); .equals(configDir.resolve(DisplaySecret.DISPLAY_PASSWORD))) {
}
}
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
private void configureAccess(boolean passwordChange) {
var userLoginConfigured = readFromFile(DATA_DISPLAY_LOGIN)
.map(Boolean::parseBoolean).orElse(false);
if (!userLoginConfigured) {
configurePassword(); configurePassword();
return;
} }
// With user login configured, we have to make sure that the
// user is logged in before we set the password and thus allow
// access to the display.
if (!vmopAgentConnected) {
if (passwordChange) {
logger.warning(() -> "Request for user login before "
+ "VM operator agent has connected");
}
return;
}
var user = readFromFile(DATA_DISPLAY_USER);
if (user.isEmpty()) {
logger.warning(() -> "Login requested, but no user configured");
}
fire(new VmopAgentLogIn(user.get()).setAssociated(this, user.get()));
}
/**
* On vmop agent logged in.
*
* @param event the event
*/
@Handler
public void onVmopAgentLoggedIn(VmopAgentLoggedIn event) {
configurePassword();
} }
private void configurePassword() { private void configurePassword() {
@ -152,7 +130,7 @@ public class DisplayController extends Component {
} }
private boolean setDisplayPassword() { private boolean setDisplayPassword() {
return readFromFile(DATA_DISPLAY_PASSWORD).map(password -> { return readFromFile(DisplaySecret.DISPLAY_PASSWORD).map(password -> {
if (Objects.equals(this.currentPassword, password)) { if (Objects.equals(this.currentPassword, password)) {
return true; return true;
} }
@ -165,7 +143,7 @@ public class DisplayController extends Component {
} }
private void setPasswordExpiry() { private void setPasswordExpiry() {
readFromFile(DATA_PASSWORD_EXPIRY).ifPresent(expiry -> { readFromFile(DisplaySecret.PASSWORD_EXPIRY).ifPresent(expiry -> {
logger.fine(() -> "Updating expiry time to " + expiry); logger.fine(() -> "Updating expiry time to " + expiry);
fire( fire(
new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry)));

View file

@ -56,7 +56,7 @@ import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option; import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.DATA_DISPLAY_PASSWORD; import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont; import org.jdrupes.vmoperator.runner.qemu.commands.QmpCont;
import org.jdrupes.vmoperator.runner.qemu.commands.QmpReset; import org.jdrupes.vmoperator.runner.qemu.commands.QmpReset;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
@ -312,7 +312,7 @@ public class Runner extends Component {
// Add some values from other sources to configuration // Add some values from other sources to configuration
newConf.asOf = Instant.ofEpochSecond(configFile.lastModified()); newConf.asOf = Instant.ofEpochSecond(configFile.lastModified());
Path dsPath = configDir.resolve(DATA_DISPLAY_PASSWORD); Path dsPath = configDir.resolve(DisplaySecret.DISPLAY_PASSWORD);
newConf.hasDisplayPassword = dsPath.toFile().canRead(); newConf.hasDisplayPassword = dsPath.toFile().canRead();
// Special actions for initial configuration (startup) // Special actions for initial configuration (startup)

View file

@ -33,8 +33,8 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import org.jdrupes.vmoperator.common.Constants.Crd;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import org.jdrupes.vmoperator.common.Constants.Status;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
@ -48,6 +48,8 @@ import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange;
import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent; import org.jdrupes.vmoperator.runner.qemu.events.ShutdownEvent;
import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected; import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected;
import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedIn;
import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLoggedOut;
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.annotation.Handler; import org.jgrapes.core.annotation.Handler;
@ -110,11 +112,17 @@ public class StatusUpdater extends VmDefUpdater {
} }
try { try {
vmStub = VmDefinitionStub.get(apiClient, vmStub = VmDefinitionStub.get(apiClient,
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM),
namespace, vmName); namespace, vmName);
vmStub.model().ifPresent(model -> { var vmDef = vmStub.updateStatus(from -> {
observedGeneration = model.getMetadata().getGeneration(); JsonObject status = from.statusJson();
}); status.remove(Status.LOGGED_IN_USER);
return status;
}).orElse(null);
if (vmDef == null) {
return;
}
observedGeneration = vmDef.getMetadata().getGeneration();
} catch (ApiException e) { } catch (ApiException e) {
logger.log(Level.SEVERE, e, logger.log(Level.SEVERE, e,
() -> "Cannot access VM object, terminating."); () -> "Cannot access VM object, terminating.");
@ -152,7 +160,7 @@ public class StatusUpdater extends VmDefUpdater {
"displayPasswordSerial").getAsInt() == -1)) { "displayPasswordSerial").getAsInt() == -1)) {
return; return;
} }
vmStub.updateStatus(vmDef.get(), from -> { vmStub.updateStatus(from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();
if (!event.configuration().hasDisplayPassword) { if (!event.configuration().hasDisplayPassword) {
status.addProperty("displayPasswordSerial", -1); status.addProperty("displayPasswordSerial", -1);
@ -173,15 +181,15 @@ public class StatusUpdater extends VmDefUpdater {
* @throws ApiException * @throws ApiException
*/ */
@Handler @Handler
@SuppressWarnings({ "PMD.AssignmentInOperand", @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AvoidLiteralsInIfCondition" }) "PMD.AssignmentInOperand", "PMD.AvoidDuplicateLiterals" })
public void onRunnerStateChanged(RunnerStateChange event) public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException { throws ApiException {
VmDefinition vmDef; VmDefinition vmDef;
if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
return; return;
} }
vmStub.updateStatus(vmDef, from -> { vmStub.updateStatus(from -> {
JsonObject status = from.statusJson(); JsonObject status = from.statusJson();
boolean running = event.runState().vmRunning(); boolean running = event.runState().vmRunning();
updateCondition(vmDef, vmDef.statusJson(), "Running", running, updateCondition(vmDef, vmDef.statusJson(), "Running", running,
@ -196,6 +204,7 @@ public class StatusUpdater extends VmDefUpdater {
} else if (event.runState() == RunState.STOPPED) { } else if (event.runState() == RunState.STOPPED) {
status.addProperty("ram", "0"); status.addProperty("ram", "0");
status.addProperty("cpus", 0); status.addProperty("cpus", 0);
status.remove(Status.LOGGED_IN_USER);
} }
if (!running) { if (!running) {
@ -228,7 +237,7 @@ public class StatusUpdater extends VmDefUpdater {
// Log event // Log event
var evt = new EventsV1Event() var evt = new EventsV1Event()
.reportingController(VM_OP_GROUP + "/" + APP_NAME) .reportingController(Crd.GROUP + "/" + APP_NAME)
.action("StatusUpdate").reason(event.reason()) .action("StatusUpdate").reason(event.reason())
.note(event.message()); .note(event.message());
K8s.createEvent(apiClient, vmDef, evt); K8s.createEvent(apiClient, vmDef, evt);
@ -344,4 +353,35 @@ public class StatusUpdater extends VmDefUpdater {
return status; return status;
}); });
} }
/**
* @param event the event
* @throws ApiException
*/
@Handler
@SuppressWarnings("PMD.AssignmentInOperand")
public void onVmopAgentLoggedIn(VmopAgentLoggedIn event)
throws ApiException {
vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
status.addProperty(Status.LOGGED_IN_USER,
event.triggering().user());
return status;
});
}
/**
* @param event the event
* @throws ApiException
*/
@Handler
@SuppressWarnings("PMD.AssignmentInOperand")
public void onVmopAgentLoggedOut(VmopAgentLoggedOut event)
throws ApiException {
vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
status.remove(Status.LOGGED_IN_USER);
return status;
});
}
} }

View file

@ -12,11 +12,12 @@ layout: vm-operator
### Shared file system ### Shared file system
Mount a shared file system as home file system on all VMs in the pool. Mount a shared file system as home file system on all VMs in the pool.
If you want to use the sample script for logging in a user, the filesystem
must support POSIX file access control lists (ACLs).
### Restrict access ### Restrict access
The only possibility to access the VMs should be via a desktop started by The VMs should only be accessible via a desktop started by the VM-Operator.
the VM-Operator.
* Disable the display manager. * Disable the display manager.
@ -36,6 +37,13 @@ the VM-Operator.
that you can still access your master VM through `ssh`, else you have that you can still access your master VM through `ssh`, else you have
locked yourself out. locked yourself out.
Strictly speaking, it is not necessary to disable these services, because
the sample script includes a `Conflicts=` directive in the systemd service
that starts the desktop for the user. However, this is mainly intended for
development purposes and not for production.
The following should actually be configured for any VM.
* Prevent suspend/hibernate, because it will lock the VM. * Prevent suspend/hibernate, because it will lock the VM.
```console ```console