From 5366e240928a43d5114536727dc9b0da55ba6a24 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Sat, 1 Mar 2025 11:02:52 +0100 Subject: [PATCH] Move automatic login request to CRD. Also reorganizes constants. --- deploy/crds/vms-crd.yaml | 11 ++ dev-example/vmop-agent/vmop-agent | 10 ++ .../jdrupes/vmoperator/common/Constants.java | 52 +++++--- .../vmoperator/common/K8sGenericStub.java | 4 +- .../vmoperator/manager/runnerConfig.ftl.yaml | 3 + .../manager/ConfigMapReconciler.java | 4 +- .../vmoperator/manager/Controller.java | 7 +- .../manager/DisplaySecretMonitor.java | 8 +- .../manager/DisplaySecretReconciler.java | 112 ++++++++---------- .../jdrupes/vmoperator/manager/Manager.java | 6 +- .../vmoperator/manager/PoolMonitor.java | 8 +- .../vmoperator/manager/PvcReconciler.java | 4 +- .../vmoperator/manager/Reconciler.java | 4 +- .../jdrupes/vmoperator/manager/VmMonitor.java | 8 +- .../vmoperator/manager/BasicTests.java | 30 +++-- .../logging.properties | 4 +- .../vmoperator/runner/qemu/Configuration.java | 3 + .../runner/qemu/ConsoleTracker.java | 9 +- .../runner/qemu/DisplayController.java | 94 ++++++--------- .../vmoperator/runner/qemu/Runner.java | 4 +- .../vmoperator/runner/qemu/StatusUpdater.java | 62 ++++++++-- webpages/vm-operator/pools.md | 18 ++- 22 files changed, 259 insertions(+), 206 deletions(-) diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index 749b896..2a14f0c 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -1430,6 +1430,12 @@ spec: outputs: type: integer 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: type: object properties: @@ -1485,6 +1491,11 @@ spec: connection. type: string default: "" + loggedInUser: + description: >- + The name of a user that is currently logged in by the + VM operator agent. + type: string displayPasswordSerial: description: >- Counts changes of the display password. Set to -1 diff --git a/dev-example/vmop-agent/vmop-agent b/dev-example/vmop-agent/vmop-agent index 99aa25d..bb2fb86 100755 --- a/dev-example/vmop-agent/vmop-agent +++ b/dev-example/vmop-agent/vmop-agent @@ -126,8 +126,18 @@ attemptLogout() { # Log out any user currently using tty1. This is invoked when executing # the logout command and therefore sends back a 2xx return code. +# Also try to restart gdm, if it is not running. doLogout() { 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" } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java index 9bfba8d..a7ed780 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java @@ -27,31 +27,47 @@ public class Constants { /** The Constant APP_NAME. */ 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. */ - public static final String VM_OP_GROUP = "vmoperator.jdrupes.org"; + /** The Constant NAME. */ + public static final String NAME = "vm-operator"; - /** The Constant VM_OP_KIND_VM. */ - public static final String VM_OP_KIND_VM = "VirtualMachine"; + /** The Constant GROUP. */ + public static final String GROUP = "vmoperator.jdrupes.org"; - /** The Constant VM_OP_KIND_VM_POOL. */ - public static final String VM_OP_KIND_VM_POOL = "VmPool"; + /** The Constant KIND_VM. */ + public static final String KIND_VM = "VirtualMachine"; - /** The Constant COMP_DISPLAY_SECRETS. */ - public static final String COMP_DISPLAY_SECRET = "display-secret"; + /** The Constant KIND_VM_POOL. */ + 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. */ - public static final String DATA_PASSWORD_EXPIRY = "password-expiry"; + /** The Constant NAME. */ + public static final String NAME = "display-secret"; - /** The Constant DATA_DISPLAY_USER. */ - public static final String DATA_DISPLAY_USER = "display-user"; + /** The Constant DISPLAY_PASSWORD. */ + public static final String DISPLAY_PASSWORD = "display-password"; - /** The Constant DATA_DISPLAY_LOGIN. */ - public static final String DATA_DISPLAY_LOGIN = "login-user"; + /** The Constant PASSWORD_EXPIRY. */ + 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"; + + } } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java index 688f43f..b1db86f 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java @@ -193,7 +193,7 @@ public class K8sGenericStub outputs: ${ spec.vm.display.outputs?c } + <#if spec.vm.display.loggedInUser?? > + loggedInUser: "${ spec.vm.display.loggedInUser }" + <#if spec.vm.display.spice??> spice: port: ${ spec.vm.display.spice.port?c } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java index 9c6dc3e..c5daf73 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java @@ -33,9 +33,9 @@ import java.io.IOException; import java.io.StringWriter; import java.util.Map; import java.util.logging.Logger; +import org.jdrupes.vmoperator.common.Constants.Crd; import org.jdrupes.vmoperator.common.K8s; 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.util.DataPath; import org.jdrupes.vmoperator.util.GsonPtr; @@ -121,7 +121,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; DynamicKubernetesObject newCm) { ListOptions listOpts = new ListOptions(); 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/instance=" + newCm.getMetadata() .getLabels().get("app.kubernetes.io/instance")); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java index 80ff0f7..f3deefa 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java @@ -29,8 +29,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.Constants.Crd; import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.common.VmDefinitionStub; @@ -194,7 +193,7 @@ public class Controller extends Component { private void patchVmDef(K8sClient client, String name, String path, Object value) throws ApiException, IOException { var vmStub = K8sDynamicStub.get(client, - new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), namespace, + new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace, name); // Patch running @@ -227,7 +226,7 @@ public class Controller extends Component { try { var vmDef = channel.vmDefinition(); 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()); if (vmStub.updateStatus(vmDef, from -> { JsonObject status = from.statusJson(); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java index 99c8a11..66cd2f4 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretMonitor.java @@ -28,11 +28,11 @@ import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.util.logging.Level; 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.K8sV1PodStub; 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.VmChannel; import org.jgrapes.core.Channel; @@ -61,7 +61,7 @@ public class DisplaySecretMonitor context(K8sV1SecretStub.CONTEXT); ListOptions options = new ListOptions(); options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); + + "app.kubernetes.io/component=" + DisplaySecret.NAME); options(options); } @@ -95,7 +95,7 @@ public class DisplaySecretMonitor // Force update for pod ListOptions listOpts = new ListOptions(); 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/instance=" + change.object.getMetadata() .getLabels().get("app.kubernetes.io/instance")); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java index e1955b4..24ba978 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretReconciler.java @@ -26,7 +26,6 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Secret; import io.kubernetes.client.util.generic.options.ListOptions; import java.io.IOException; -import static java.nio.charset.StandardCharsets.UTF_8; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.time.Instant; @@ -34,20 +33,16 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Scanner; import java.util.logging.Logger; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.COMP_DISPLAY_SECRET; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.Constants.Crd; +import org.jdrupes.vmoperator.common.Constants.DisplaySecret; +import org.jdrupes.vmoperator.common.Constants.Status; import org.jdrupes.vmoperator.common.K8sV1SecretStub; +import org.jdrupes.vmoperator.common.VmDefinition; 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.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; @@ -146,7 +141,7 @@ public class DisplaySecretReconciler extends Component { var vmDef = event.vmDefinition(); ListOptions options = new ListOptions(); 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()); var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(), options); @@ -157,9 +152,9 @@ public class DisplaySecretReconciler extends Component { // Create secret var secret = new V1Secret(); 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/component", COMP_DISPLAY_SECRET) + .putLabelsItem("app.kubernetes.io/component", DisplaySecret.NAME) .putLabelsItem("app.kubernetes.io/instance", vmDef.name())); secret.setType("Opaque"); SecureRandom random = null; @@ -172,8 +167,8 @@ public class DisplaySecretReconciler extends Component { byte[] bytes = new byte[16]; random.nextBytes(bytes); var password = Base64.encode(bytes); - secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password, - DATA_PASSWORD_EXPIRY, "now")); + secret.setStringData(Map.of(DisplaySecret.DISPLAY_PASSWORD, password, + DisplaySecret.PASSWORD_EXPIRY, "now")); K8sV1SecretStub.create(channel.client(), secret); } @@ -192,49 +187,31 @@ public class DisplaySecretReconciler extends Component { public void onPrepareConsole(PrepareConsole event, VmChannel channel) throws ApiException { // Update console user in status - var vmStub = VmDefinitionStub.get(channel.client(), - new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), - 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()) { + var vmDef = updateConsoleUser(event, channel); + if (vmDef == null) { return; } - var vmDef = optVmDef.get(); // Check if access is possible if (event.loginUser() - ? !vmDef.conditionStatus("Booted").orElse(false) + ? !vmDef. fromStatus(Status.LOGGED_IN_USER) + .map(u -> u.equals(event.user())).orElse(false) : !vmDef.conditionStatus("Running").orElse(false)) { return; } - // Look for secret - ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "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); + // Get secret and update password in secret + var stub = getSecretStub(event, channel, vmDef); + if (stub == null) { return; } - var stub = stubs.iterator().next(); - - // Get secret and update var secret = stub.model().get(); - var updPw = updatePassword(secret, event); - var updUsr = updateUser(secret, event); - if (!updPw && !updUsr) { + if (!updatePassword(secret, event)) { 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, event.vmDefinition().displayPasswordSerial().orElse(0L) + 1, new CompletionLock(event, 1500)); @@ -247,30 +224,45 @@ public class DisplaySecretReconciler extends Component { stub.update(secret).getObject(); } - private boolean updateUser(V1Secret secret, PrepareConsole event) { - var curUser = DataPath. get(secret, "data", DATA_DISPLAY_USER) - .map(b -> new String(b, UTF_8)).orElse(null); - var curLogin = DataPath. get(secret, "data", DATA_DISPLAY_LOGIN) - .map(b -> new String(b, UTF_8)).map(Boolean::parseBoolean) - .orElse(null); - if (Objects.equals(curUser, event.user()) && Objects.equals( - curLogin, event.loginUser())) { - return false; + private VmDefinition updateConsoleUser(PrepareConsole event, + VmChannel channel) throws ApiException { + var vmStub = VmDefinitionStub.get(channel.client(), + new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), + event.vmDefinition().namespace(), event.vmDefinition().name()); + return vmStub.updateStatus(from -> { + JsonObject status = from.statusJson(); + status.addProperty("consoleUser", event.user()); + 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)); - secret.getData().put(DATA_DISPLAY_LOGIN, - Boolean.toString(event.loginUser()).getBytes(UTF_8)); - return true; + return stubs.iterator().next(); } private boolean updatePassword(V1Secret secret, PrepareConsole event) { var expiry = Optional.ofNullable(secret.getData() - .get(DATA_PASSWORD_EXPIRY)).map(b -> new String(b)).orElse(null); - if (secret.getData().get(DATA_DISPLAY_PASSWORD) != null + .get(DisplaySecret.PASSWORD_EXPIRY)).map(b -> new String(b)) + .orElse(null); + if (secret.getData().get(DisplaySecret.DISPLAY_PASSWORD) != null && stillValid(expiry)) { // Fixed secret, don't touch event.setResult( - new String(secret.getData().get(DATA_DISPLAY_PASSWORD))); + new String( + secret.getData().get(DisplaySecret.DISPLAY_PASSWORD))); return false; } @@ -285,8 +277,8 @@ public class DisplaySecretReconciler extends Component { byte[] bytes = new byte[16]; random.nextBytes(bytes); var password = Base64.encode(bytes); - secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password, - DATA_PASSWORD_EXPIRY, + secret.setStringData(Map.of(DisplaySecret.DISPLAY_PASSWORD, password, + DisplaySecret.PASSWORD_EXPIRY, Long.toString(Instant.now().getEpochSecond() + passwordValidity))); event.setResult(password); return true; diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java index 9d291cf..8acddc5 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java @@ -40,7 +40,7 @@ import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; 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.util.FsdUtils; import org.jgrapes.core.Channel; @@ -108,7 +108,7 @@ public class Manager extends Component { // Configuration store with file in /etc/opt (default) 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()); // Don't rely on night config to produce a good exception // for this simple case @@ -271,7 +271,7 @@ public class Manager extends Component { try { // Get logging properties from file and put them in effect InputStream props; - var path = FsdUtils.findConfigFile(VM_OP_NAME.replace("-", ""), + var path = FsdUtils.findConfigFile(Crd.NAME.replace("-", ""), "logging.properties"); if (path.isPresent()) { props = Files.newInputStream(path.get()); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java index 25fb10b..465a9ed 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PoolMonitor.java @@ -28,8 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.Constants.Crd; import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8sClient; 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.VmDefinitionStub; 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.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmPoolChanged; @@ -88,7 +86,7 @@ public class PoolMonitor extends client(new K8sClient()); // 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()) { logger.severe(() -> "Cannot get CRD context."); return; @@ -184,7 +182,7 @@ public class PoolMonitor extends return; } 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()); vmStub.updateStatus(from -> { // TODO diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java index 34085f0..f0a3ede 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/PvcReconciler.java @@ -36,7 +36,7 @@ import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; 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.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; @@ -83,7 +83,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; // Existing disks ListOptions listOpts = new ListOptions(); 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/instance=" + vmDef.name()); var knownDisks = K8sV1PvcStub.list(channel.client(), diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java index 8011e2c..0622a7c 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java @@ -46,12 +46,12 @@ import java.util.List; import java.util.Map; import java.util.Optional; 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.K8sClient; import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sV1SecretStub; 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.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; @@ -276,7 +276,7 @@ public class Reconciler extends Component { // Check if we have a display secret ListOptions options = new ListOptions(); 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()); var dsStub = K8sV1SecretStub .list(client, vmDef.namespace(), options) diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java index 5c1ae77..a253b17 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java @@ -31,8 +31,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.stream.Collectors; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.Constants.Crd; import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8sClient; 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.VmPool; 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.ChannelManager; import org.jdrupes.vmoperator.manager.events.GetPools; @@ -87,7 +85,7 @@ public class VmMonitor extends client(new K8sClient()); // 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()) { logger.severe(() -> "Cannot get CRD context."); return; @@ -105,7 +103,7 @@ public class VmMonitor extends .stream().map(stub -> stub.name()).collect(Collectors.toSet()); ListOptions opts = new ListOptions(); opts.setLabelSelector( - "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," + "app.kubernetes.io/managed-by=" + Crd.NAME + "," + "app.kubernetes.io/name=" + APP_NAME); for (var context : Set.of(K8sV1StatefulSetStub.CONTEXT, K8sV1ConfigMapStub.CONTEXT)) { diff --git a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java index 4f5d7a3..b3b9b1a 100644 --- a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java +++ b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java @@ -13,10 +13,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.COMP_DISPLAY_SECRET; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -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.Constants.Crd; +import org.jdrupes.vmoperator.common.Constants.DisplaySecret; import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sDynamicStub; @@ -60,7 +58,7 @@ class BasicTests { waitForManager(); // 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()); vmsContext = apiRes.get(); @@ -70,7 +68,7 @@ class BasicTests { ListOptions listOpts = new ListOptions(); listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_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); for (var secret : secrets) { secret.delete(); @@ -100,7 +98,7 @@ class BasicTests { private static void deletePvcs() throws ApiException { ListOptions listOpts = new ListOptions(); 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/instance=" + VM_NAME); 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/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/managed-by"), - Constants.VM_OP_NAME, + Crd.NAME, List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS, List.of("ownerReferences", 0, "apiVersion"), 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, "uid"), EXISTS); checkProps(config.getMetadata(), toCheck); @@ -189,7 +187,7 @@ class BasicTests { ListOptions listOpts = new ListOptions(); listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," + "app.kubernetes.io/instance=" + VM_NAME + "," - + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); + + "app.kubernetes.io/component=" + DisplaySecret.NAME); Collection secrets = null; for (int i = 0; i < 10; i++) { 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/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/managed-by"), - Constants.VM_OP_NAME)); + Crd.NAME)); checkProps(pvc.getSpec(), Map.of( List.of("resources", "requests", "storage"), 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/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/managed-by"), - Constants.VM_OP_NAME, + Crd.NAME, List.of("annotations", "use_as"), "system-disk")); checkProps(pvc.getSpec(), Map.of( 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/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/managed-by"), - Constants.VM_OP_NAME)); + Crd.NAME)); checkProps(pvc.getSpec(), Map.of( List.of("resources", "requests", "storage"), Quantity.fromString("1Gi"))); @@ -291,12 +289,12 @@ class BasicTests { List.of("labels", "app.kubernetes.io/instance"), VM_NAME, List.of("labels", "app.kubernetes.io/component"), APP_NAME, 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", "vmoperator.jdrupes.org/version"), EXISTS, List.of("ownerReferences", 0, "apiVersion"), 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, "uid"), EXISTS)); checkProps(pod.getSpec(), Map.of( @@ -319,7 +317,7 @@ class BasicTests { checkProps(svc.getMetadata(), Map.of( List.of("labels", "app.kubernetes.io/name"), APP_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", "label2"), "replaced", List.of("labels", "label3"), "added", diff --git a/org.jdrupes.vmoperator.runner.qemu/logging.properties b/org.jdrupes.vmoperator.runner.qemu/logging.properties index 1cf84fe..6b0542d 100644 --- a/org.jdrupes.vmoperator.runner.qemu/logging.properties +++ b/org.jdrupes.vmoperator.runner.qemu/logging.properties @@ -19,8 +19,8 @@ handlers=java.util.logging.ConsoleHandler -org.jgrapes.level=FINE -org.jgrapes.core.handlerTracking.level=FINER +#org.jgrapes.level=FINE +#org.jgrapes.core.handlerTracking.level=FINER org.jdrupes.vmoperator.runner.qemu.level=FINE diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java index 20d4c66..50635b5 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java @@ -248,6 +248,9 @@ public class Configuration implements Dto { /** The number of outputs. */ public int outputs = 1; + /** The logged in user. */ + public String loggedInUser; + /** The spice. */ public Spice spice; } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java index b91b5df..db29932 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/ConsoleTracker.java @@ -25,8 +25,7 @@ import io.kubernetes.client.openapi.models.EventsV1Event; import java.io.IOException; import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.Constants.Crd; import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.VmDefinitionStub; @@ -74,7 +73,7 @@ public class ConsoleTracker extends VmDefUpdater { } try { vmStub = VmDefinitionStub.get(apiClient, - new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), + new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace, vmName); } catch (ApiException e) { logger.log(Level.SEVERE, e, @@ -115,7 +114,7 @@ public class ConsoleTracker extends VmDefUpdater { // Log event var evt = new EventsV1Event() - .reportingController(VM_OP_GROUP + "/" + APP_NAME) + .reportingController(Crd.GROUP + "/" + APP_NAME) .action("ConsoleConnectionUpdate") .reason("Connection from " + event.clientHost()); K8s.createEvent(apiClient, vmStub.model().get(), evt); @@ -150,7 +149,7 @@ public class ConsoleTracker extends VmDefUpdater { // Log event var evt = new EventsV1Event() - .reportingController(VM_OP_GROUP + "/" + APP_NAME) + .reportingController(Crd.GROUP + "/" + APP_NAME) .action("ConsoleConnectionUpdate") .reason("Disconnected from " + event.clientHost()); K8s.createEvent(apiClient, vmStub.model().get(), evt); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java index 479e2b1..63a3ec1 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java @@ -1,6 +1,6 @@ /* * 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 * 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.Optional; import java.util.logging.Level; -import static org.jdrupes.vmoperator.common.Constants.DATA_DISPLAY_LOGIN; -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.common.Constants.DisplaySecret; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry; 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.VmopAgentConnected; 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.Component; +import org.jgrapes.core.Event; import org.jgrapes.core.annotation.Handler; import org.jgrapes.util.events.FileChanged; import org.jgrapes.util.events.WatchFile; @@ -52,6 +50,7 @@ public class DisplayController extends Component { private String protocol; private final Path configDir; private boolean vmopAgentConnected; + private String loggedInUser; /** * Instantiates a new Display controller. @@ -64,17 +63,7 @@ public class DisplayController extends Component { public DisplayController(Channel componentChannel, Path configDir) { super(componentChannel); this.configDir = configDir; - fire(new WatchFile(configDir.resolve(DATA_DISPLAY_PASSWORD))); - } - - /** - * On vmop agent connected. - * - * @param event the event - */ - @Handler - public void onVmopAgentConnected(VmopAgentConnected event) { - vmopAgentConnected = true; + fire(new WatchFile(configDir.resolve(DisplaySecret.DISPLAY_PASSWORD))); } /** @@ -89,7 +78,32 @@ public class DisplayController extends Component { } protocol = 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 @SuppressWarnings("PMD.EmptyCatchBlock") public void onFileChanged(FileChanged event) { - if (event.path().equals(configDir.resolve(DATA_DISPLAY_PASSWORD))) { - configureAccess(true); - } - } - - @SuppressWarnings("PMD.DataflowAnomalyAnalysis") - private void configureAccess(boolean passwordChange) { - var userLoginConfigured = readFromFile(DATA_DISPLAY_LOGIN) - .map(Boolean::parseBoolean).orElse(false); - if (!userLoginConfigured) { + if (event.path() + .equals(configDir.resolve(DisplaySecret.DISPLAY_PASSWORD))) { 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() { @@ -152,7 +130,7 @@ public class DisplayController extends Component { } private boolean setDisplayPassword() { - return readFromFile(DATA_DISPLAY_PASSWORD).map(password -> { + return readFromFile(DisplaySecret.DISPLAY_PASSWORD).map(password -> { if (Objects.equals(this.currentPassword, password)) { return true; } @@ -165,7 +143,7 @@ public class DisplayController extends Component { } private void setPasswordExpiry() { - readFromFile(DATA_PASSWORD_EXPIRY).ifPresent(expiry -> { + readFromFile(DisplaySecret.PASSWORD_EXPIRY).ifPresent(expiry -> { logger.fine(() -> "Updating expiry time to " + expiry); fire( new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java index f64af2d..0c62a93 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java @@ -56,7 +56,7 @@ import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; 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.QmpReset; 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 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(); // Special actions for initial configuration (startup) diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java index fa0e3ab..9c149d5 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java @@ -33,8 +33,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.Constants.Crd; +import org.jdrupes.vmoperator.common.Constants.Status; import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.VmDefinition; 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.ShutdownEvent; 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.jgrapes.core.Channel; import org.jgrapes.core.annotation.Handler; @@ -110,11 +112,17 @@ public class StatusUpdater extends VmDefUpdater { } try { vmStub = VmDefinitionStub.get(apiClient, - new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), + new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), namespace, vmName); - vmStub.model().ifPresent(model -> { - observedGeneration = model.getMetadata().getGeneration(); - }); + var vmDef = vmStub.updateStatus(from -> { + 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) { logger.log(Level.SEVERE, e, () -> "Cannot access VM object, terminating."); @@ -152,7 +160,7 @@ public class StatusUpdater extends VmDefUpdater { "displayPasswordSerial").getAsInt() == -1)) { return; } - vmStub.updateStatus(vmDef.get(), from -> { + vmStub.updateStatus(from -> { JsonObject status = from.statusJson(); if (!event.configuration().hasDisplayPassword) { status.addProperty("displayPasswordSerial", -1); @@ -173,15 +181,15 @@ public class StatusUpdater extends VmDefUpdater { * @throws ApiException */ @Handler - @SuppressWarnings({ "PMD.AssignmentInOperand", - "PMD.AvoidLiteralsInIfCondition" }) + @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", + "PMD.AssignmentInOperand", "PMD.AvoidDuplicateLiterals" }) public void onRunnerStateChanged(RunnerStateChange event) throws ApiException { VmDefinition vmDef; if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { return; } - vmStub.updateStatus(vmDef, from -> { + vmStub.updateStatus(from -> { JsonObject status = from.statusJson(); boolean running = event.runState().vmRunning(); updateCondition(vmDef, vmDef.statusJson(), "Running", running, @@ -196,6 +204,7 @@ public class StatusUpdater extends VmDefUpdater { } else if (event.runState() == RunState.STOPPED) { status.addProperty("ram", "0"); status.addProperty("cpus", 0); + status.remove(Status.LOGGED_IN_USER); } if (!running) { @@ -228,7 +237,7 @@ public class StatusUpdater extends VmDefUpdater { // Log event var evt = new EventsV1Event() - .reportingController(VM_OP_GROUP + "/" + APP_NAME) + .reportingController(Crd.GROUP + "/" + APP_NAME) .action("StatusUpdate").reason(event.reason()) .note(event.message()); K8s.createEvent(apiClient, vmDef, evt); @@ -344,4 +353,35 @@ public class StatusUpdater extends VmDefUpdater { 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; + }); + } } diff --git a/webpages/vm-operator/pools.md b/webpages/vm-operator/pools.md index 290d0fd..a42293e 100644 --- a/webpages/vm-operator/pools.md +++ b/webpages/vm-operator/pools.md @@ -12,11 +12,12 @@ layout: vm-operator ### Shared file system 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 -The only possibility to access the VMs should be via a desktop started by -the VM-Operator. +The VMs should only be accessible via a desktop started by the VM-Operator. * Disable the display manager. @@ -31,10 +32,17 @@ the VM-Operator. # systemctl mask 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 - that you can still access your master VM through `ssh`, else you have - locked yourself out. +The following should actually be configured for any VM. * Prevent suspend/hibernate, because it will lock the VM.