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);
}
private K8sV1SecretStub getSecretStub(PrepareConsole event,
VmChannel channel, VmDefinition vmDef) throws ApiException {
// 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;
} }
secret.getData().put(DATA_DISPLAY_USER, event.user().getBytes(UTF_8)); return stubs.iterator().next();
secret.getData().put(DATA_DISPLAY_LOGIN,
Boolean.toString(event.loginUser()).getBytes(UTF_8));
return true;
} }
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.
@ -31,10 +32,17 @@ the VM-Operator.
# systemctl mask getty@tty1 # systemctl mask getty@tty1
# systemctl stop getty@tty1 # systemctl stop getty@tty1
``` ```
You can, of course, disable `getty` completely. If you do this, make sure
that you can still access your master VM through `ssh`, else you have
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.
You can, of course, disable `getty` completely. If you do this, make sure The following should actually be configured for any VM.
that you can still access your master VM through `ssh`, else you have
locked yourself out.
* Prevent suspend/hibernate, because it will lock the VM. * Prevent suspend/hibernate, because it will lock the VM.