diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..3501bd3 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,38 @@ +# See https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml + +# Default state for all rules +default: true + +# MD007/ul-indent : Unordered list indentation : +# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md007.md +MD007: + # Spaces for indent + indent: 2 + # Whether to indent the first level of the list + start_indented: true + # Spaces for first level indent (when start_indented is set) + start_indent: 2 + +# MD025/single-title/single-h1 : Multiple top-level headings in the same document : +# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md025.md +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter (disable) + front_matter_title: "" + +# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : +# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md036.md +MD036: false + +# MD043/required-headings : Required heading structure : +# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md043.md +MD043: + # List of headings + headings: [ + "# Head", + "## Item", + "### Detail" + ] + # Match case of headings + match_case: false diff --git a/deploy/crds/vmpools-crd.yaml b/deploy/crds/vmpools-crd.yaml index b34d096..2144940 100644 --- a/deploy/crds/vmpools-crd.yaml +++ b/deploy/crds/vmpools-crd.yaml @@ -25,6 +25,12 @@ spec: type: string pattern: '^(?:\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3])(?:|:?[0-5]\d))|P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+[Hh])?(?:\d+[Mm])?(?:\d+(?:\.\d{1,9})?[Ss])?)?)$' default: "PT1h" + loginOnAssignment: + description: >- + If set to true, the user will be automatically logged in + to the VM's console when the VM is assigned to him. + type: boolean + default: false permissions: type: array description: >- diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index 2a14f0c..101784f 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -1534,6 +1534,24 @@ spec: lastTransitionTime: "1970-01-01T00:00:00Z" reason: Creation message: "Creation of CR" + - type: Booted + status: "False" + observedGeneration: 1 + lastTransitionTime: "1970-01-01T00:00:00Z" + reason: Creation + message: "Creation of CR" + - type: VmopAgentConnected + status: "False" + observedGeneration: 1 + lastTransitionTime: "1970-01-01T00:00:00Z" + reason: Creation + message: "Creation of CR" + - type: UserLoggedIn + status: "False" + observedGeneration: 1 + lastTransitionTime: "1970-01-01T00:00:00Z" + reason: Creation + message: "Creation of CR" - type: ConsoleConnected status: "False" observedGeneration: 1 diff --git a/dev-example/test-pool.yaml b/dev-example/test-pool.yaml index a72a623..497aaf7 100644 --- a/dev-example/test-pool.yaml +++ b/dev-example/test-pool.yaml @@ -5,6 +5,7 @@ metadata: name: test-vms spec: retention: "PT1m" + loginOnAssignment: true permissions: - user: admin may: diff --git a/dev-example/test-vm.tpl.yaml b/dev-example/test-vm.tpl.yaml index 50031bb..9ac06ce 100644 --- a/dev-example/test-vm.tpl.yaml +++ b/dev-example/test-vm.tpl.yaml @@ -8,12 +8,11 @@ metadata: spec: image: -# repository: docker-registry.lan.mnl.de -# path: vmoperator/org.jdrupes.vmoperator.runner.qemu-arch # pullPolicy: Always # repository: ghcr.io # path: mnlipp/org.jdrupes.vmoperator.runner.qemu-alpine # version: "3.0.0" +# source: docker-registry.lan.mnl.de/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:feature-auto-login source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing pullPolicy: Always 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 71c8cf3..83b261e 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 @@ -74,6 +74,37 @@ public class Constants { /** The Constant ASSIGNMENT. */ public static final String ASSIGNMENT = "assignment"; + + /** + * Conditions used in Status. + */ + public static class Condition { + /** The Constant COND_RUNNING. */ + public static final String RUNNING = "Running"; + + /** The Constant COND_BOOTED. */ + public static final String BOOTED = "Booted"; + + /** The Constant COND_VMOP_AGENT. */ + public static final String VMOP_AGENT = "VmopAgentConnected"; + + /** The Constant COND_USER_LOGGED_IN. */ + public static final String USER_LOGGED_IN = "UserLoggedIn"; + + /** The Constant COND_CONSOLE. */ + public static final String CONSOLE_CONNECTED = "ConsoleConnected"; + + /** + * Reasons used in conditions. + */ + public static class Reason { + /** The Constant NOT_REQUESTED. */ + public static final String NOT_REQUESTED = "NotRequested"; + + /** The Constant USER_LOGGED_IN. */ + public static final String LOGGED_IN = "LoggedIn"; + } + } } /** diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java index a42d2d4..f763c47 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmDefinition.java @@ -39,6 +39,8 @@ import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; import org.jdrupes.vmoperator.common.Constants.Status; +import org.jdrupes.vmoperator.common.Constants.Status.Condition; +import org.jdrupes.vmoperator.common.Constants.Status.Condition.Reason; import org.jdrupes.vmoperator.util.DataPath; /** @@ -141,6 +143,16 @@ public class VmDefinition extends K8sDynamicModel { } } + /** + * The assignment information. + * + * @param pool the pool + * @param user the user + * @param lastUsed the last used + */ + public record Assignment(String pool, String user, Instant lastUsed) { + } + /** * Instantiates a new vm definition. * @@ -215,31 +227,15 @@ public class VmDefinition extends K8sDynamicModel { } /** - * The pool that the VM was taken from. + * The assignment information. * * @return the optional */ - public Optional assignedFrom() { - return fromStatus(Status.ASSIGNMENT, "pool"); - } - - /** - * The user that the VM was assigned to. - * - * @return the optional - */ - public Optional assignedTo() { - return fromStatus(Status.ASSIGNMENT, "user"); - } - - /** - * Last usage of assigned VM. - * - * @return the optional - */ - public Optional assignmentLastUsed() { - return this. fromStatus(Status.ASSIGNMENT, "lastUsed") - .map(Instant::parse); + public Optional assignment() { + return this.> fromStatus(Status.ASSIGNMENT) + .filter(m -> !m.isEmpty()).map(a -> new Assignment( + a.get("pool").toString(), a.get("user").toString(), + Instant.parse(a.get("lastUsed").toString()))); } /** @@ -369,18 +365,47 @@ public class VmDefinition extends K8sDynamicModel { } /** - * Check if the console is accessible. Returns true if the console is - * currently unused, used by the given user or if the permissions - * allow taking over the console. + * Check if the console is accessible. Always returns `true` if + * the VM is running and the permissions allow taking over the + * console. Else, returns `true` if + * + * * the permissions allow access to the console and + * + * * the VM is running and + * + * * the console is currently unused or used by the given user and + * + * * if user login is requested, the given user is logged in. * * @param user the user * @param permissions the permissions * @return true, if successful */ + @SuppressWarnings("PMD.SimplifyBooleanReturns") public boolean consoleAccessible(String user, Set permissions) { - return !conditionStatus("ConsoleConnected").orElse(true) - || consoleUser().map(cu -> cu.equals(user)).orElse(true) - || permissions.contains(VmDefinition.Permission.TAKE_CONSOLE); + // Basic checks + if (!conditionStatus(Condition.RUNNING).orElse(false)) { + return false; + } + if (permissions.contains(Permission.TAKE_CONSOLE)) { + return true; + } + if (!permissions.contains(Permission.ACCESS_CONSOLE)) { + return false; + } + + // If the console is in use by another user, deny access + if (conditionStatus(Condition.CONSOLE_CONNECTED).orElse(false) + && !consoleUser().map(cu -> cu.equals(user)).orElse(false)) { + return false; + } + + // If no login is requested, allow access, else check if user matches + if (condition(Condition.USER_LOGGED_IN).map(V1Condition::getReason) + .map(r -> Reason.NOT_REQUESTED.equals(r)).orElse(false)) { + return true; + } + return user.equals(status().get(Status.LOGGED_IN_USER)); } /** diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java index e0817d5..7c13ddb 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/VmPool.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import org.jdrupes.vmoperator.common.VmDefinition.Assignment; import org.jdrupes.vmoperator.common.VmDefinition.Grant; import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.util.DataPath; @@ -37,8 +38,9 @@ import org.jdrupes.vmoperator.util.DataPath; @SuppressWarnings({ "PMD.DataClass" }) public class VmPool { - private String name; + private final String name; private String retention; + private boolean loginOnAssignment; private boolean defined; private List permissions = Collections.emptyList(); private final Set vms @@ -53,6 +55,19 @@ public class VmPool { this.name = name; } + /** + * Fill the properties of a provisionally created pool from + * the definition. + * + * @param definition the definition + */ + public void defineFrom(VmPool definition) { + retention = definition.retention(); + permissions = definition.permissions(); + loginOnAssignment = definition.loginOnAssignment(); + defined = true; + } + /** * Returns the name. * @@ -63,12 +78,12 @@ public class VmPool { } /** - * Sets the name. + * Checks if is login on assignment. * - * @param name the name to set + * @return the loginOnAssignment */ - public void setName(String name) { - this.name = name; + public boolean loginOnAssignment() { + return loginOnAssignment; } /** @@ -81,12 +96,10 @@ public class VmPool { } /** - * Sets if is. - * - * @param defined the defined to set + * Marks the pool as undefined. */ - public void setDefined(boolean defined) { - this.defined = defined; + public void setUndefined() { + defined = false; } /** @@ -98,15 +111,6 @@ public class VmPool { return retention; } - /** - * Sets the retention. - * - * @param retention the retention to set - */ - public void setRetention(String retention) { - this.retention = retention; - } - /** * Permissions granted for a VM from the pool. * @@ -116,15 +120,6 @@ public class VmPool { return permissions; } - /** - * Sets the permissions. - * - * @param permissions the permissions to set - */ - public void setPermissions(List permissions) { - this.permissions = permissions; - } - /** * Returns the VM names. * @@ -171,13 +166,12 @@ public class VmPool { } // If not assigned, it's usable - if (vmDef.assignedTo().isEmpty()) { + if (vmDef.assignment().isEmpty()) { return true; } // Check if it is to be retained - if (vmDef.assignmentLastUsed() - .map(this::retainUntil) + if (vmDef.assignment().map(Assignment::lastUsed).map(this::retainUntil) .map(ru -> Instant.now().isBefore(ru)).orElse(false)) { return false; } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PrepareConsole.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java similarity index 64% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PrepareConsole.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java index ad8f9ce..2f7dbd6 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/PrepareConsole.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplaySecret.java @@ -25,46 +25,29 @@ import org.jgrapes.core.Event; * Gets the current display secret and optionally updates it. */ @SuppressWarnings("PMD.DataClass") -public class PrepareConsole extends Event { +public class GetDisplaySecret extends Event { private final VmDefinition vmDef; private final String user; - private final boolean loginUser; /** * Instantiates a new request for the display secret. * After handling the event, a result of `null` means that - * no password is needed. No result means that the console - * is not accessible. - * - * @param vmDef the vm name - * @param user the requesting user - * @param loginUser login the user - */ - public PrepareConsole(VmDefinition vmDef, String user, - boolean loginUser) { - this.vmDef = vmDef; - this.user = user; - this.loginUser = loginUser; - } - - /** - * Instantiates a new request for the display secret. - * After handling the event, a result of `null` means that - * no password is needed. No result means that the console + * no secret is needed. No result means that the console * is not accessible. * * @param vmDef the vm name * @param user the requesting user */ - public PrepareConsole(VmDefinition vmDef, String user) { - this(vmDef, user, false); + public GetDisplaySecret(VmDefinition vmDef, String user) { + this.vmDef = vmDef; + this.user = user; } /** - * Gets the vm definition. + * Gets the VM definition. * - * @return the vm definition + * @return the VM definition */ public VmDefinition vmDefinition() { return vmDef; @@ -79,24 +62,15 @@ public class PrepareConsole extends Event { return user; } - /** - * Checks if the user should be logged in before allowing access. - * - * @return the loginUser - */ - public boolean loginUser() { - return loginUser; - } - /** * Returns `true` if a password is available. May only be called * when the event is completed. Note that the password returned - * by {@link #password()} may be `null`, indicating that no password + * by {@link #secret()} may be `null`, indicating that no password * is needed. * * @return true, if successful */ - public boolean passwordAvailable() { + public boolean secretAvailable() { if (!isDone()) { throw new IllegalStateException("Event is not done."); } @@ -104,13 +78,13 @@ public class PrepareConsole extends Event { } /** - * Return the password. May only be called when the event has been - * completed with a valid result (see {@link #passwordAvailable()}). + * Return the secret. May only be called when the event has been + * completed with a valid result (see {@link #secretAvailable()}). * * @return the password. A value of `null` means that no password * is required. */ - public String password() { + public String secret() { if (!isDone() || currentResults().isEmpty()) { throw new IllegalStateException("Event is not done."); } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java index 676af3d..af9dde0 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/UpdateAssignment.java @@ -18,6 +18,7 @@ package org.jdrupes.vmoperator.manager.events; +import org.jdrupes.vmoperator.common.VmPool; import org.jgrapes.core.Event; /** @@ -26,31 +27,31 @@ import org.jgrapes.core.Event; @SuppressWarnings("PMD.DataClass") public class UpdateAssignment extends Event { - private final String usedPool; + private final VmPool fromPool; private final String toUser; /** * Instantiates a new event. * - * @param usedPool the used pool + * @param fromPool the pool from which the VM was assigned * @param toUser the to user */ - public UpdateAssignment(String usedPool, String toUser) { - this.usedPool = usedPool; + public UpdateAssignment(VmPool fromPool, String toUser) { + this.fromPool = fromPool; this.toUser = toUser; } /** - * Gets the pool to assign from. + * Gets the pool from which the VM was assigned. * * @return the pool */ - public String usedPool() { - return usedPool; + public VmPool fromPool() { + return fromPool; } /** - * Gets the user to assign to. + * Gets the user to whom the VM was assigned. * * @return the to user */ diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index 2fbeb94..2a59a2c 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml @@ -201,8 +201,8 @@ data: <#if spec.vm.display.outputs?? > outputs: ${ spec.vm.display.outputs?c } - <#if spec.vm.display.loggedInUser?? > - loggedInUser: "${ spec.vm.display.loggedInUser }" + <#if loginRequestedFor?? > + loggedInUser: "${ loginRequestedFor }" <#if spec.vm.display.spice??> spice: 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 b61b26a..5d5c592 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 @@ -232,7 +232,7 @@ public class Controller extends Component { if (vmStub.updateStatus(vmDef, from -> { JsonObject status = from.statusJson(); var assignment = GsonPtr.to(status).to(Status.ASSIGNMENT); - assignment.set("pool", event.usedPool()); + assignment.set("pool", event.fromPool().name()); assignment.set("user", event.toUser()); assignment.set("lastUsed", Instant.now().toString()); return status; 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 7c5c3ad..dabffb6 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 @@ -43,7 +43,7 @@ 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 org.jdrupes.vmoperator.manager.events.PrepareConsole; +import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.util.DataPath; @@ -71,7 +71,7 @@ public class DisplaySecretReconciler extends Component { protected final Logger logger = Logger.getLogger(getClass().getName()); private int passwordValidity = 10; - private final List pendingPrepares + private final List pendingPrepares = Collections.synchronizedList(new LinkedList<>()); /** @@ -184,21 +184,23 @@ public class DisplaySecretReconciler extends Component { */ @Handler @SuppressWarnings("PMD.StringInstantiation") - public void onPrepareConsole(PrepareConsole event, VmChannel channel) + public void onGetDisplaySecret(GetDisplaySecret event, VmChannel channel) throws ApiException { - // Update console user in status - var vmDef = updateConsoleUser(event, channel); - if (vmDef == null) { + // Get VM definition and check if running + var vmStub = VmDefinitionStub.get(channel.client(), + new GroupVersionKind(Crd.GROUP, "", Crd.KIND_VM), + event.vmDefinition().namespace(), event.vmDefinition().name()); + var vmDef = vmStub.model().orElse(null); + if (vmDef == null || !vmDef.conditionStatus("Running").orElse(false)) { return; } - // Check if access is possible - if (event.loginUser() - ? !vmDef. fromStatus(Status.LOGGED_IN_USER) - .map(u -> u.equals(event.user())).orElse(false) - : !vmDef.conditionStatus("Running").orElse(false)) { - return; - } + // Update console user in status + vmDef = vmStub.updateStatus(from -> { + JsonObject status = from.statusJson(); + status.addProperty(Status.CONSOLE_USER, event.user()); + return status; + }).get(); // Get secret and update password in secret var stub = getSecretStub(event, channel, vmDef); @@ -212,7 +214,7 @@ public class DisplaySecretReconciler extends Component { // Register wait for confirmation (by VM status change, // after secret update) - var pending = new PendingPrepare(event, + var pending = new PendingRequest(event, event.vmDefinition().displayPasswordSerial().orElse(0L) + 1, new CompletionLock(event, 1500)); pendingPrepares.add(pending); @@ -224,19 +226,7 @@ public class DisplaySecretReconciler extends Component { stub.update(secret).getObject(); } - 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(Status.CONSOLE_USER, event.user()); - return status; - }).orElse(null); - } - - private K8sV1SecretStub getSecretStub(PrepareConsole event, + private K8sV1SecretStub getSecretStub(GetDisplaySecret event, VmChannel channel, VmDefinition vmDef) throws ApiException { // Look for secret ListOptions options = new ListOptions(); @@ -253,7 +243,7 @@ public class DisplaySecretReconciler extends Component { return stubs.iterator().next(); } - private boolean updatePassword(V1Secret secret, PrepareConsole event) { + private boolean updatePassword(V1Secret secret, GetDisplaySecret event) { var expiry = Optional.ofNullable(secret.getData() .get(DisplaySecret.EXPIRY)).map(b -> new String(b)).orElse(null); if (secret.getData().get(DisplaySecret.PASSWORD) != null @@ -323,8 +313,8 @@ public class DisplaySecretReconciler extends Component { * The Class PendingGet. */ @SuppressWarnings("PMD.DataClass") - private static class PendingPrepare { - public final PrepareConsole event; + private static class PendingRequest { + public final GetDisplaySecret event; public final long expectedSerial; public final CompletionLock lock; @@ -334,7 +324,7 @@ public class DisplaySecretReconciler extends Component { * @param event the event * @param expectedSerial the expected serial */ - public PendingPrepare(PrepareConsole event, long expectedSerial, + public PendingRequest(GetDisplaySecret event, long expectedSerial, CompletionLock lock) { super(); this.event = event; 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 1ea15e1..5d85280 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 @@ -36,6 +36,7 @@ import org.jdrupes.vmoperator.common.K8sDynamicModel; import org.jdrupes.vmoperator.common.K8sDynamicModels; import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; +import org.jdrupes.vmoperator.common.VmDefinition.Assignment; import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.manager.events.GetPools; @@ -105,7 +106,7 @@ public class PoolMonitor extends // When pool is deleted, save VMs in pending if (type == ResponseType.DELETED) { Optional.ofNullable(pools.get(poolName)).ifPresent(pool -> { - pool.setDefined(false); + pool.setUndefined(); if (pool.vms().isEmpty()) { pools.remove(poolName); } @@ -129,11 +130,8 @@ public class PoolMonitor extends // Get pool and merge changes var vmPool = pools.computeIfAbsent(poolName, k -> new VmPool(poolName)); - var newData = client().getJSON().getGson().fromJson( - GsonPtr.to(poolModel.data()).to("spec").get(), VmPool.class); - vmPool.setRetention(newData.retention()); - vmPool.setPermissions(newData.permissions()); - vmPool.setDefined(true); + vmPool.defineFrom(client().getJSON().getGson().fromJson( + GsonPtr.to(poolModel.data()).to("spec").get(), VmPool.class)); poolPipeline.fire(new VmPoolChanged(vmPool)); } @@ -168,7 +166,7 @@ public class PoolMonitor extends } // Sync last usage to console state change if user matches - if (vmDef.assignedTo() + if (vmDef.assignment().map(Assignment::user) .map(at -> at.equals(vmDef.consoleUser().orElse(null))) .orElse(true)) { return; @@ -177,8 +175,8 @@ public class PoolMonitor extends var ccChange = vmDef.condition("ConsoleConnected") .map(cc -> cc.getLastTransitionTime().toInstant()); if (ccChange - .map(tt -> vmDef.assignmentLastUsed().map(alu -> alu.isAfter(tt)) - .orElse(true)) + .map(tt -> vmDef.assignment().map(Assignment::lastUsed) + .map(alu -> alu.isAfter(tt)).orElse(true)) .orElse(true)) { return; } 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 e5bfaec..8df5c88 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 @@ -45,6 +45,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import org.jdrupes.vmoperator.common.Constants.DisplaySecret; import org.jdrupes.vmoperator.common.Convertions; @@ -52,6 +53,9 @@ 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 org.jdrupes.vmoperator.common.VmDefinition.Assignment; +import org.jdrupes.vmoperator.common.VmPool; +import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; @@ -212,11 +216,6 @@ public class Reconciler extends Component { @SuppressWarnings("PMD.ConfusingTernary") public void onVmDefChanged(VmDefChanged event, VmChannel channel) throws ApiException, TemplateException, IOException { - // We're only interested in "spec" changes. - if (!event.specChanged()) { - return; - } - // Ownership relationships takes care of deletions if (event.type() == K8sObserver.ResponseType.DELETED) { logger.fine( @@ -228,6 +227,11 @@ public class Reconciler extends Component { Map model = prepareModel(channel.client(), event.vmDefinition()); var configMap = cmReconciler.reconcile(model, channel); + + // The remaining reconcilers depend only on changes of the spec part. + if (!event.specChanged()) { + return; + } model.put("cm", configMap); dsReconciler.reconcile(event, model, channel); // Manage (eventual) removal of stateful set. @@ -266,24 +270,10 @@ public class Reconciler extends Component { Optional.ofNullable(Reconciler.class.getPackage() .getImplementationVersion()).orElse("(Unknown)")); model.put("cr", vmDef); - // Freemarker's static models don't handle nested classes. - model.put("constants", constantsMap(Constants.class)); model.put("reconciler", config); - - // Check if we have a display 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 dsStub = K8sV1SecretStub - .list(client, vmDef.namespace(), options) - .stream() - .findFirst(); - if (dsStub.isPresent()) { - dsStub.get().model().ifPresent(m -> { - model.put("displaySecret", m.getMetadata().getName()); - }); - } + model.put("constants", constantsMap(Constants.class)); + addLoginRequestedFor(model, vmDef); + addDisplaySecret(client, model, vmDef); // Methods model.put("parseQuantity", parseQuantityModel); @@ -294,6 +284,13 @@ public class Reconciler extends Component { return model; } + /** + * Creates a map with constants. Needed because freemarker doesn't support + * nested classes with its static models. + * + * @param clazz the clazz + * @return the map + */ @SuppressWarnings("PMD.EmptyCatchBlock") private Map constantsMap(Class clazz) { @SuppressWarnings("PMD.UseConcurrentHashMap") @@ -318,6 +315,38 @@ public class Reconciler extends Component { return result; } + private void addLoginRequestedFor(Map model, + VmDefinition vmDef) { + vmDef.assignment().filter(a -> { + try { + return newEventPipeline() + .fire(new GetPools().withName(a.pool())).get() + .stream().findFirst().map(VmPool::loginOnAssignment) + .orElse(false); + } catch (InterruptedException e) { + logger.log(Level.WARNING, e, e::getMessage); + } + return false; + }).map(Assignment::user) + .or(() -> vmDef.fromSpec("vm", "display", "loggedInUser")) + .ifPresent(u -> model.put("loginRequestedFor", u)); + } + + private void addDisplaySecret(K8sClient client, Map model, + VmDefinition vmDef) throws ApiException { + 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 dsStub = K8sV1SecretStub + .list(client, vmDef.namespace(), options).stream().findFirst(); + if (dsStub.isPresent()) { + dsStub.get().model().ifPresent(m -> { + model.put("displaySecret", m.getMetadata().getName()); + }); + } + } + private final TemplateMethodModelEx parseQuantityModel = new TemplateMethodModelEx() { @Override 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 4f8ac77..1a559b3 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 @@ -40,6 +40,7 @@ import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; import org.jdrupes.vmoperator.common.K8sV1PodStub; import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.VmDefinition.Assignment; import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitions; import org.jdrupes.vmoperator.common.VmExtraData; @@ -234,10 +235,10 @@ public class VmMonitor extends || !c.vmDefinition().permissionsFor(event.user().orElse(null), event.roles()).isEmpty()) .filter(c -> event.fromPool().isEmpty() - || c.vmDefinition().assignedFrom() + || c.vmDefinition().assignment().map(Assignment::pool) .map(p -> p.equals(event.fromPool().get())).orElse(false)) .filter(c -> event.toUser().isEmpty() - || c.vmDefinition().assignedTo() + || c.vmDefinition().assignment().map(Assignment::user) .map(u -> u.equals(event.toUser().get())).orElse(false)) .map(c -> new VmData(c.vmDefinition(), c)) .toList()); @@ -257,9 +258,9 @@ public class VmMonitor extends while (true) { // Search for existing assignment. var vmQuery = channelManager.channels().stream() - .filter(c -> c.vmDefinition().assignedFrom() + .filter(c -> c.vmDefinition().assignment().map(Assignment::pool) .map(p -> p.equals(event.fromPool())).orElse(false)) - .filter(c -> c.vmDefinition().assignedTo() + .filter(c -> c.vmDefinition().assignment().map(Assignment::user) .map(u -> u.equals(event.toUser())).orElse(false)) .findFirst(); if (vmQuery.isPresent()) { @@ -280,7 +281,8 @@ public class VmMonitor extends vmQuery = channelManager.channels().stream() .filter(c -> vmPool.isAssignable(c.vmDefinition())) .sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition() - .assignmentLastUsed().orElse(Instant.ofEpochSecond(0))) + .assignment().map(Assignment::lastUsed) + .orElse(Instant.ofEpochSecond(0))) .thenComparing(preferRunning)) .findFirst(); @@ -293,7 +295,7 @@ public class VmMonitor extends var chosenVm = vmQuery.get(); var vmPipeline = chosenVm.pipeline(); if (Optional.ofNullable(vmPipeline.fire(new UpdateAssignment( - vmPool.name(), event.toUser()), chosenVm).get()) + vmPool, event.toUser()), chosenVm).get()) .orElse(false)) { var vmDef = chosenVm.vmDefinition(); event.setResult(new VmData(vmDef, chosenVm)); 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 36a63c1..b1580ae 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 @@ -35,6 +35,8 @@ import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import org.jdrupes.vmoperator.common.Constants.Crd; import org.jdrupes.vmoperator.common.Constants.Status; +import org.jdrupes.vmoperator.common.Constants.Status.Condition; +import org.jdrupes.vmoperator.common.Constants.Status.Condition.Reason; import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinitionStub; @@ -72,6 +74,7 @@ public class StatusUpdater extends VmDefUpdater { private boolean guestShutdownStops; private boolean shutdownByGuest; private VmDefinitionStub vmStub; + private String loggedInUser; /** * Instantiates a new status updater. @@ -143,6 +146,7 @@ public class StatusUpdater extends VmDefUpdater { public void onConfigureQemu(ConfigureQemu event) throws ApiException { guestShutdownStops = event.configuration().guestShutdownStops; + loggedInUser = event.configuration().vm.display.loggedInUser; // Remainder applies only if we have a connection to k8s. if (vmStub == null) { @@ -169,10 +173,12 @@ public class StatusUpdater extends VmDefUpdater { status.addProperty(Status.DISPLAY_PASSWORD_SERIAL, -1); } status.getAsJsonArray("conditions").asList().stream() - .map(cond -> (JsonObject) cond).filter(cond -> "Running" + .map(cond -> (JsonObject) cond) + .filter(cond -> Condition.RUNNING .equals(cond.get("type").getAsString())) .forEach(cond -> cond.addProperty("observedGeneration", from.getMetadata().getGeneration())); + updateUserLoggedIn(from); return status; }, vmDef); } @@ -194,9 +200,9 @@ public class StatusUpdater extends VmDefUpdater { } vmStub.updateStatus(from -> { boolean running = event.runState().vmRunning(); - updateCondition(vmDef, "Running", running, event.reason(), + updateCondition(vmDef, Condition.RUNNING, running, event.reason(), event.message()); - JsonObject status = updateCondition(vmDef, "Booted", + JsonObject status = updateCondition(vmDef, Condition.BOOTED, event.runState() == RunState.BOOTED, event.reason(), event.message()); if (event.runState() == RunState.STARTING) { @@ -212,10 +218,13 @@ public class StatusUpdater extends VmDefUpdater { if (!running) { // In case console connection was still present status.addProperty(Status.CONSOLE_CLIENT, ""); - updateCondition(from, "ConsoleConnected", false, "VmStopped", + updateCondition(from, Condition.CONSOLE_CONNECTED, false, + "VmStopped", "The VM is not running"); // In case we had an irregular shutdown + updateCondition(from, Condition.USER_LOGGED_IN, false, + "VmStopped", "The VM is not running"); status.remove(Status.OSINFO); updateCondition(vmDef, "VmopAgentConnected", false, "VmStopped", "The VM is not running"); @@ -245,6 +254,26 @@ public class StatusUpdater extends VmDefUpdater { K8s.createEvent(apiClient, vmDef, evt); } + private void updateUserLoggedIn(VmDefinition from) { + if (loggedInUser == null) { + updateCondition(from, Condition.USER_LOGGED_IN, false, + Reason.NOT_REQUESTED, "No user to be logged in"); + return; + } + if (!from.conditionStatus(Condition.VMOP_AGENT).orElse(false)) { + updateCondition(from, Condition.USER_LOGGED_IN, false, + "VmopAgentDisconnected", "Waiting for VMOP agent to connect"); + return; + } + if (!from.fromStatus(Status.LOGGED_IN_USER).map(loggedInUser::equals) + .orElse(false)) { + updateCondition(from, Condition.USER_LOGGED_IN, false, + "Processing", "Waiting for user to be logged in"); + } + updateCondition(from, Condition.USER_LOGGED_IN, true, + Reason.LOGGED_IN, "User is logged in"); + } + /** * On ballon change. * @@ -348,8 +377,10 @@ public class StatusUpdater extends VmDefUpdater { return; } vmStub.updateStatus(from -> { - return updateCondition(vmDef, "VmopAgentConnected", + var status = updateCondition(vmDef, "VmopAgentConnected", true, "VmopAgentStarted", "The VM operator agent is running"); + updateUserLoggedIn(from); + return status; }, vmDef); } @@ -365,6 +396,7 @@ public class StatusUpdater extends VmDefUpdater { JsonObject status = from.statusJson(); status.addProperty(Status.LOGGED_IN_USER, event.triggering().user()); + updateUserLoggedIn(from); return status; }); } @@ -380,6 +412,7 @@ public class StatusUpdater extends VmDefUpdater { vmStub.updateStatus(from -> { JsonObject status = from.statusJson(); status.remove(Status.LOGGED_IN_USER); + updateUserLoggedIn(from); return status; }); } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java index 50017c1..4c64ff1 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/VmDefUpdater.java @@ -129,9 +129,12 @@ public class VmDefUpdater extends Component { var current = status.getAsJsonArray("conditions").asList().stream() .map(cond -> (JsonObject) cond) .filter(cond -> type.equals(cond.get("type").getAsString())) - .findFirst() - .map(cond -> "True".equals(cond.get("status").getAsString())); - if (current.isPresent() && current.get() == state) { + .findFirst(); + if (current.isPresent() + && current.map(c -> c.get("status").getAsString()) + .map("True"::equals).map(s -> s == state).orElse(false) + && current.map(c -> c.get("reason").getAsString()) + .map(reason::equals).orElse(false)) { return status; } diff --git a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java index 36c3444..f78374c 100644 --- a/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java +++ b/org.jdrupes.vmoperator.util/src/org/jdrupes/vmoperator/util/GsonPtr.java @@ -327,6 +327,18 @@ public class GsonPtr { return set(selector, new JsonPrimitive(value)); } + /** + * Short for `set(selector, new JsonPrimitive(value))`. + * + * @param selector the selector + * @param value the value + * @return the gson ptr + * @see #set(Object, JsonElement) + */ + public GsonPtr set(Object selector, Boolean value) { + return set(selector, new JsonPrimitive(value)); + } + /** * Same as {@link #set(Object, JsonElement)}, but sets the value * only if it doesn't exist yet, else returns the existing value. diff --git a/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd b/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd index 5d69caa..60d7780 100644 --- a/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd +++ b/org.jdrupes.vmoperator.vmaccess/.eclipse-pmd @@ -4,4 +4,4 @@ - + \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties index 8f4051e..6ec24aa 100644 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties +++ b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n.properties @@ -5,5 +5,5 @@ okayLabel = Apply and Close confirmResetTitle = Confirm reset confirmResetMsg = Resetting the VM may cause loss of data. \ Please confirm to continue. -consoleTakenNotification = Console access is locked by another user. +consoleInaccessibleNotification = Console is not ready or in use. poolEmptyNotification = No VM available. Please consult your administrator. diff --git a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties index e51eb5e..28c01f0 100644 --- a/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties +++ b/org.jdrupes.vmoperator.vmaccess/resources/org/jdrupes/vmoperator/vmaccess/l10n_de.properties @@ -11,7 +11,7 @@ Open\ console = Konsole anzeigen confirmResetTitle = Zurücksetzen bestätigen confirmResetMsg = Zurücksetzen der VM kann zu Datenverlust führen. \ Bitte bestätigen um fortzufahren. -consoleTakenNotification = Die Konsole wird von einem anderen Benutzer verwendet. +consoleInaccessibleNotification = Die Konsole ist nicht bereit oder belegt. poolEmptyNotification = Keine VM verfügbar. Wenden Sie sich bitte an den \ Systemadministrator. \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java index 3b28d1c..91642f1 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/VmAccess.java @@ -46,14 +46,15 @@ import java.util.stream.Collectors; import org.bouncycastle.util.Objects; import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.VmDefinition; +import org.jdrupes.vmoperator.common.VmDefinition.Assignment; import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.manager.events.AssignVm; +import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.GetVms; import org.jdrupes.vmoperator.manager.events.GetVms.VmData; import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.PrepareConsole; import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; @@ -265,7 +266,7 @@ public class VmAccess extends FreeMarkerConlet { public void onConsoleConfigured(ConsoleConfigured event, ConsoleConnection connection) throws InterruptedException, IOException { - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "PMD.PrematureDeclaration" }) final var rendered = (Set) connection.session().get(RENDERED); connection.session().remove(RENDERED); @@ -523,6 +524,13 @@ public class VmAccess extends FreeMarkerConlet { .assignedTo(user)).get().stream().findFirst(); } + /** + * Returns the permissions from the VM definition. + * + * @param vmDef the VM definition + * @param session the session + * @return the sets the + */ private Set permissions(VmDefinition vmDef, Session session) { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); @@ -531,6 +539,13 @@ public class VmAccess extends FreeMarkerConlet { return vmDef.permissionsFor(user, roles); } + /** + * Returns the permissions from the pool. + * + * @param pool the pool + * @param session the session + * @return the sets the + */ private Set permissions(VmPool pool, Session session) { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); @@ -539,23 +554,33 @@ public class VmAccess extends FreeMarkerConlet { return pool.permissionsFor(user, roles); } - private Set permissions(ResourceModel model, Session session, - VmPool pool, VmDefinition vmDef) throws InterruptedException { + /** + * Returns the permissions from the VM definition or the pool depending + * on the state of the model. + * + * @param session the session + * @param model the model + * @param vmDef the vm def + * @return the sets the + * @throws InterruptedException the interrupted exception + */ + private Set permissions(Session session, ResourceModel model, + VmDefinition vmDef) throws InterruptedException { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); var roles = WebConsoleUtils.rolesFromSession(session) .stream().map(ConsoleRole::getName).toList(); if (model.mode() == ResourceModel.Mode.POOL) { - if (pool == null) { - pool = appPipeline.fire(new GetPools() - .withName(model.name())).get().stream().findFirst() - .orElse(null); - } + // Use permissions from pool + var pool = appPipeline.fire(new GetPools().withName(model.name())) + .get().stream().findFirst().orElse(null); if (pool == null) { return Collections.emptySet(); } return pool.permissionsFor(user, roles); } + + // Use permissions from VM if (vmDef == null) { vmDef = appPipeline.fire(new GetVms().assignedFrom(model.name()) .assignedTo(user)).get().stream().map(VmData::definition) @@ -577,7 +602,7 @@ public class VmAccess extends FreeMarkerConlet { VmDefinition vmDef) throws InterruptedException { channel.respond(new NotifyConletView(type(), model.getConletId(), "updateConfig", model.mode(), model.name(), - permissions(model, channel.session(), null, vmDef).stream() + permissions(channel.session(), model, vmDef).stream() .map(VmDefinition.Permission::toString).toList())); } @@ -588,12 +613,17 @@ public class VmAccess extends FreeMarkerConlet { model.setAssignedVm(null); } else { model.setAssignedVm(vmDef.name()); + var session = channel.session(); + var user = WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null); + var perms = permissions(session, model, vmDef); try { - data = Map.of("metadata", - Map.of("namespace", vmDef.namespace(), + data = Map.of( + "metadata", Map.of("namespace", vmDef.namespace(), "name", vmDef.name()), "spec", vmDef.spec(), - "status", vmDef.status()); + "status", vmDef.status(), + "consoleAccessible", vmDef.consoleAccessible(user, perms)); } catch (JsonSyntaxException e) { logger.log(Level.SEVERE, e, () -> "Failed to serialize VM definition"); @@ -634,6 +664,8 @@ public class VmAccess extends FreeMarkerConlet { // Update known conlets for (var entry : conletIdsByConsoleConnection().entrySet()) { var connection = entry.getKey(); + var user = WebConsoleUtils.userFromSession(connection.session()) + .map(ConsoleUser::getName).orElse(null); for (var conletId : entry.getValue()) { var model = stateFromSession(connection.session(), conletId); if (model.isEmpty() @@ -654,13 +686,11 @@ public class VmAccess extends FreeMarkerConlet { } else { // Check if VM is used by pool conlet or to be assigned to // it - var user - = WebConsoleUtils.userFromSession(connection.session()) - .map(ConsoleUser::getName).orElse(null); - var toBeUsedByConlet = vmDef.assignedFrom() + var toBeUsedByConlet = vmDef.assignment() + .map(Assignment::pool) .map(p -> p.equals(model.get().name())).orElse(false) - && vmDef.assignedTo().map(u -> u.equals(user)) - .orElse(false); + && vmDef.assignment().map(Assignment::user) + .map(u -> u.equals(user)).orElse(false); if (!Objects.areEqual(model.get().assignedVm(), vmDef.name()) && !toBeUsedByConlet) { continue; @@ -750,7 +780,7 @@ public class VmAccess extends FreeMarkerConlet { var vmChannel = vmData.get().channel(); var vmDef = vmData.get().definition(); var vmName = vmDef.metadata().getName(); - var perms = permissions(model, channel.session(), null, vmDef); + var perms = permissions(channel.session(), model, vmDef); var resourceBundle = resourceBundle(channel.locale()); switch (event.method()) { case "start": @@ -774,9 +804,7 @@ public class VmAccess extends FreeMarkerConlet { } break; case "openConsole": - if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)) { - openConsole(channel, model, vmChannel, vmDef, perms); - } + openConsole(channel, model, vmChannel, vmDef, perms); break; default:// ignore break; @@ -804,22 +832,21 @@ public class VmAccess extends FreeMarkerConlet { .map(ConsoleUser::getName).orElse(""); if (!vmDef.consoleAccessible(user, perms)) { channel.respond(new DisplayNotification( - resourceBundle.getString("consoleTakenNotification"), + resourceBundle.getString("consoleInaccessibleNotification"), Map.of("autoClose", 5_000, "type", "Warning"))); return; } - var pwQuery = Event.onCompletion(new PrepareConsole(vmDef, user, - model.mode() == ResourceModel.Mode.POOL), + var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user), e -> gotPassword(channel, model, vmDef, e)); fire(pwQuery, vmChannel); } private void gotPassword(ConsoleConnection channel, ResourceModel model, - VmDefinition vmDef, PrepareConsole event) { - if (!event.passwordAvailable()) { + VmDefinition vmDef, GetDisplaySecret event) { + if (!event.secretAvailable()) { return; } - vmDef.extra().map(xtra -> xtra.connectionFile(event.password(), + vmDef.extra().map(xtra -> xtra.connectionFile(event.secret(), preferredIpVersion, deleteConnectionFile)) .ifPresent(cf -> channel.respond(new NotifyConletView(type(), model.getConletId(), "openConsole", cf))); diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts index f0ef3c8..47e6e11 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-functions.ts @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2024 Michael N. Lipp + * Copyright (C) 2024,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 @@ -71,11 +71,10 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, const poolName = computed(() => previewApi.poolName); const vmName = computed(() => previewApi.vmDefinition.name); const configured = computed(() => previewApi.vmDefinition.spec); + const accessible = computed(() => previewApi.vmDefinition.consoleAccessible); const busy = computed(() => previewApi.vmDefinition.spec && (previewApi.vmDefinition.spec.vm.state === 'Running' - && (previewApi.poolName - ? !previewApi.vmDefinition.vmopAgent - : !previewApi.vmDefinition.running) + && (!previewApi.vmDefinition.consoleAccessible) || previewApi.vmDefinition.spec.vm.state === 'Stopped' && previewApi.vmDefinition.running)); const startable = computed(() => previewApi.vmDefinition.spec @@ -87,7 +86,6 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, previewApi.vmDefinition.spec.vm.state !== 'Stopped' && previewApi.vmDefinition.running); const running = computed(() => previewApi.vmDefinition.running); - const vmopAgent = computed(() => previewApi.vmDefinition.vmopAgent); const inUse = computed(() => previewApi.vmDefinition.usedBy != ''); const permissions = computed(() => previewApi.permissions); const osicon = computed(() => { @@ -123,7 +121,7 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, }; return { localize, resourceBase, vmAction, poolName, vmName, - configured, busy, startable, stoppable, running, vmopAgent, + configured, accessible, busy, startable, stoppable, running, inUse, permissions, osicon }; }, template: ` @@ -132,9 +130,7 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, { if (condition.type === "Running") { vmDefinition.running = condition.status === "True"; vmDefinition.runningConditionSince = new Date(condition.lastTransitionTime); - } else if (condition.type === "VmopAgentConnected") { - vmDefinition.vmopAgent = condition.status === "True"; - vmDefinition.vmopAgentConditionSince - = new Date(condition.lastTransitionTime); } }) } else { diff --git a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss index 63ae299..3a291dd 100644 --- a/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss +++ b/org.jdrupes.vmoperator.vmaccess/src/org/jdrupes/vmoperator/vmaccess/browser/VmAccess-style.scss @@ -24,6 +24,7 @@ span[role="button"].svg-icon { display: inline-block; line-height: 1; + /* Align with forkawesome */ font-size: 14px; fill: var(--primary); diff --git a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html index 6ec6ce3..533b2f4 100644 --- a/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html +++ b/org.jdrupes.vmoperator.vmmgmt/resources/org/jdrupes/vmoperator/vmmgmt/VmMgmt-view.ftl.html @@ -60,21 +60,21 @@ @@ -86,8 +86,7 @@ ? 'computer-off.svg' : (entry.usedFrom ? 'computer-in-use.svg' : 'computer.svg'))" :title="localize('Open console')" - :aria-disabled="!entry['running'] - || !(entry.permissions.includes('accessConsole'))" + :aria-disabled="!entry.consoleAccessible" v-on:click="vmAction(entry.name, 'openConsole')"> diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java index 6d3891d..00df484 100644 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java +++ b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/VmMgmt.java @@ -44,8 +44,8 @@ import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.common.VmExtraData; import org.jdrupes.vmoperator.manager.events.ChannelTracker; +import org.jdrupes.vmoperator.manager.events.GetDisplaySecret; import org.jdrupes.vmoperator.manager.events.ModifyVm; -import org.jdrupes.vmoperator.manager.events.PrepareConsole; import org.jdrupes.vmoperator.manager.events.ResetVm; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; @@ -249,14 +249,15 @@ public class VmMgmt extends FreeMarkerConlet { .toBigInteger()); // Build result + var perms = vmDef.permissionsFor(user, roles); return Map.of("metadata", Map.of("namespace", vmDef.namespace(), "name", vmDef.name()), "spec", spec, "status", status, "nodeName", vmDef.extra().map(VmExtraData::nodeName).orElse(""), - "permissions", vmDef.permissionsFor(user, roles).stream() - .map(VmDefinition.Permission::toString).toList()); + "consoleAccessible", vmDef.consoleAccessible(user, perms), + "permissions", perms); } /** @@ -438,9 +439,7 @@ public class VmMgmt extends FreeMarkerConlet { } break; case "openConsole": - if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)) { - openConsole(channel, model, vmChannel, vmDef, user, perms); - } + openConsole(channel, model, vmChannel, vmDef, user, perms); break; case "cpus": fire(new ModifyVm(vmName, "currentCpus", @@ -484,17 +483,17 @@ public class VmMgmt extends FreeMarkerConlet { Map.of("autoClose", 5_000, "type", "Warning"))); return; } - var pwQuery = Event.onCompletion(new PrepareConsole(vmDef, user), + var pwQuery = Event.onCompletion(new GetDisplaySecret(vmDef, user), e -> gotPassword(channel, model, vmDef, e)); fire(pwQuery, vmChannel); } private void gotPassword(ConsoleConnection channel, VmsModel model, - VmDefinition vmDef, PrepareConsole event) { - if (!event.passwordAvailable()) { + VmDefinition vmDef, GetDisplaySecret event) { + if (!event.secretAvailable()) { return; } - vmDef.extra().map(xtra -> xtra.connectionFile(event.password(), + vmDef.extra().map(xtra -> xtra.connectionFile(event.secret(), preferredIpVersion, deleteConnectionFile)).ifPresent( cf -> channel.respond(new NotifyConletView(type(), model.getConletId(), "openConsole", cf))); diff --git a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-style.scss b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-style.scss index 4c11b65..248df56 100644 --- a/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-style.scss +++ b/org.jdrupes.vmoperator.vmmgmt/src/org/jdrupes/vmoperator/vmmgmt/browser/VmMgmt-style.scss @@ -118,6 +118,7 @@ span[role="button"].svg-icon { display: inline-block; line-height: 1; + /* Align with forkawesome */ font-size: 14px; fill: var(--primary); diff --git a/webpages/pools.md b/webpages/pools.md index a42293e..6b1e75f 100644 --- a/webpages/pools.md +++ b/webpages/pools.md @@ -19,19 +19,19 @@ must support POSIX file access control lists (ACLs). The VMs should only be accessible via a desktop started by the VM-Operator. - * Disable the display manager. - - ```console - # systemctl disable gdm - # systemctl stop gdm - ``` - - * Disable `getty` on tty1. - - ```console - # systemctl mask getty@tty1 - # systemctl stop getty@tty1 - ``` + * Disable the display manager. + + ```console + # systemctl disable gdm + # systemctl stop gdm + ``` + + * Disable `getty` on tty1. + + ```console + # 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 @@ -44,18 +44,18 @@ development purposes and not for production. The following should actually be configured for any VM. - * Prevent suspend/hibernate, because it will lock the VM. + * Prevent suspend/hibernate, because it will lock the VM. - ```console - # systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target - ``` + ```console + # systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target + ``` ### Install the VM-Operator agent The VM-Operator agent runs as a systemd service. Sample configuration files can be found [here](https://github.com/mnlipp/VM-Operator/tree/main/dev-example/vmop-agent). -Copy +Copy * `99-vmop-agent.rules` to `/usr/local/lib/udev/rules.d/99-vmop-agent.rules`, * `vmop-agent` to `/usr/local/libexec/vmop-agent` and diff --git a/webpages/upgrading.md b/webpages/upgrading.md index 6fdbc44..c1331e5 100644 --- a/webpages/upgrading.md +++ b/webpages/upgrading.md @@ -9,31 +9,31 @@ layout: vm-operator ## To version 4.0.0 - * The VmViewer conlet has been renamed to VmAccess. This affects the - [configuration](https://jdrupes.org/vm-operator/user-gui.html). Configuration - information using the old path - `/Manager/GuiHttpServer/ConsoleWeblet/WebConsole/ComponentCollector/VmViewer` - is still accepted for backward compatibility until the next major version, - but should be updated. + * The VmViewer conlet has been renamed to VmAccess. This affects the + [configuration](https://jdrupes.org/vm-operator/user-gui.html). + Configuration information using the old path + `/Manager/GuiHttpServer/ConsoleWeblet/WebConsole/ComponentCollector/VmViewer` + is still accepted for backward compatibility until the next major version, + but should be updated. - The change of name also causes conlets added to the overview page by - users to "disappear" from the GUI. They have to be re-added. + The change of name also causes conlets added to the overview page by + users to "disappear" from the GUI. They have to be re-added. - The latter behavior also applies to the VmConlet conlet which has been - renamed to VmMgmt. - - * The configuration property `passwordValidity` has been moved from component - `/Manager/Controller/DisplaySecretMonitor` to - `/Manager/Controller/Reconciler/DisplaySecretReconciler`. The old path is - still accepted for backward compatibility until the next major version, - but should be updated. + The latter behavior also applies to the VmConlet conlet which has been + renamed to VmMgmt. - * The standard [template](./runner.html#stand-alone-configuration) used - to generate the QEMU command has been updated. Unless you have enabled - automatic updates of the template in the VM definition, you have to - update the template manually. If you're using your own template, you - have to add a virtual serial port (see the git history of the standard - template for the required addition). + * The configuration property `passwordValidity` has been moved from component + `/Manager/Controller/DisplaySecretMonitor` to + `/Manager/Controller/Reconciler/DisplaySecretReconciler`. The old path is + still accepted for backward compatibility until the next major version, + but should be updated. + + * The standard [template](./runner.html#stand-alone-configuration) used + to generate the QEMU command has been updated. Unless you have enabled + automatic updates of the template in the VM definition, you have to + update the template manually. If you're using your own template, you + have to add a virtual serial port (see the git history of the standard + template for the required addition). ## To version 3.4.0