From 6d5ba8829c71697411710d1499f92a365882eed9 Mon Sep 17 00:00:00 2001 From: "Michael N. Lipp" Date: Thu, 23 Jan 2025 13:41:53 +0100 Subject: [PATCH] Basically working. --- deploy/crds/vmpools-crd.yaml | 2 +- deploy/crds/vms-crd.yaml | 5 +- dev-example/test-pool.yaml | 5 + dev-example/test-vm.tpl.yaml | 3 - dev-example/test-vm.yaml | 3 - .../vmoperator/common/VmDefinition.java | 52 ++- .../vmoperator/manager/events/AssignVm.java | 61 ++++ .../vmoperator/manager/events/GetVms.java | 42 +++ .../jdrupes/vmoperator/manager/VmMonitor.java | 60 +++- .../jdrupes/vmoperator/vmaccess/VmAccess.java | 332 ++++++++++++------ .../vmaccess/browser/VmAccess-functions.ts | 71 ++-- .../vmaccess/browser/VmAccess-style.scss | 5 + 12 files changed, 493 insertions(+), 148 deletions(-) create mode 100644 org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java diff --git a/deploy/crds/vmpools-crd.yaml b/deploy/crds/vmpools-crd.yaml index 5f8414f..193d547 100644 --- a/deploy/crds/vmpools-crd.yaml +++ b/deploy/crds/vmpools-crd.yaml @@ -44,7 +44,7 @@ spec: - reset - accessConsole - "*" - default: [] + default: ["accessConsole"] required: - permissions # either Namespaced or Cluster diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index 1e79e27..9d7bbb8 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -1022,7 +1022,7 @@ spec: pools: type: array description: >- - List of pools to which this VM belongs. + List of pools this VM belongs to. items: type: string default: [] @@ -1495,17 +1495,14 @@ spec: description: >- The pool this VM is taken from. type: string - default: "" user: description: >- The user this VM is assigned to. type: string - default: "" lastUsed: description: >- The last time this VM was used by the user. type: string - default: "1970-01-01T00:00:00Z" default: {} conditions: description: >- diff --git a/dev-example/test-pool.yaml b/dev-example/test-pool.yaml index 73bd6ab..2ff8d06 100644 --- a/dev-example/test-pool.yaml +++ b/dev-example/test-pool.yaml @@ -8,3 +8,8 @@ spec: - user: admin may: - accessConsole + - user: test + may: + - accessConsole + - start + - stop diff --git a/dev-example/test-vm.tpl.yaml b/dev-example/test-vm.tpl.yaml index 1ce8d95..50031bb 100644 --- a/dev-example/test-vm.tpl.yaml +++ b/dev-example/test-vm.tpl.yaml @@ -21,9 +21,6 @@ spec: - role: admin may: - "*" - - user: test - may: - - accessConsole guestShutdownStops: true diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml index 6f71b6a..7ee2793 100644 --- a/dev-example/test-vm.yaml +++ b/dev-example/test-vm.yaml @@ -14,9 +14,6 @@ spec: - user: admin may: - "*" - - user: test - may: - - "accessConsole" resources: requests: 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 b807e8c..375612b 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 @@ -89,6 +89,11 @@ public class VmDefinition { return Set.of(reprs.get(value)); } + /** + * To string. + * + * @return the string + */ @Override public String toString() { return repr; @@ -104,6 +109,11 @@ public class VmDefinition { */ public record Grant(String user, String role, Set may) { + /** + * To string. + * + * @return the string + */ @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -180,6 +190,16 @@ public class VmDefinition { this.metadata = metadata; } + /** + * The pools that this VM belongs to. + * + * @return the list + */ + public List pools() { + return this.> fromSpec("pools") + .orElse(Collections.emptyList()); + } + /** * Gets the spec. * @@ -268,6 +288,24 @@ public class VmDefinition { this.status = status; } + /** + * The pool that the VM was taken from. + * + * @return the optional + */ + public Optional assignedFrom() { + return fromStatus("assignment", "pool"); + } + + /** + * The user that the VM was assigned to. + * + * @return the optional + */ + public Optional assignedTo() { + return fromStatus("assignment", "user"); + } + /** * Return a condition's status. * @@ -298,6 +336,7 @@ public class VmDefinition { /** * Return extra data. * + * @param the generic type * @param property the property * @return the object */ @@ -325,7 +364,7 @@ public class VmDefinition { } /** - * Return the requested VM state + * Return the requested VM state. * * @return the string */ @@ -367,11 +406,22 @@ public class VmDefinition { .map(Number::longValue); } + /** + * Hash code. + * + * @return the int + */ @Override public int hashCode() { return Objects.hash(metadata.getNamespace(), metadata.getName()); } + /** + * Equals. + * + * @param obj the obj + * @return true, if successful + */ @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java new file mode 100644 index 0000000..21e6031 --- /dev/null +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/AssignVm.java @@ -0,0 +1,61 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.manager.events; + +import org.jdrupes.vmoperator.manager.events.GetVms.VmData; +import org.jgrapes.core.Event; + +/** + * Assign a VM from a pool to a user. + */ +@SuppressWarnings("PMD.DataClass") +public class AssignVm extends Event { + + private final String fromPool; + private final String toUser; + + /** + * Instantiates a new event. + * + * @param fromPool the from pool + * @param toUser the to user + */ + public AssignVm(String fromPool, String toUser) { + this.fromPool = fromPool; + this.toUser = toUser; + } + + /** + * Gets the pool to assign from. + * + * @return the pool + */ + public String fromPool() { + return fromPool; + } + + /** + * Gets the user to assign to. + * + * @return the to user + */ + public String toUser() { + return toUser; + } +} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java index ebb19cc..8b00698 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetVms.java @@ -33,6 +33,8 @@ public class GetVms extends Event> { private String name; private String user; private List roles = Collections.emptyList(); + private String fromPool; + private String toUser; /** * Return only the VMs with the given name. @@ -59,6 +61,28 @@ public class GetVms extends Event> { return this; } + /** + * Return only {@link VmDefinition}s that are assigned from the given pool. + * + * @param pool the pool + * @return the returns the vms + */ + public GetVms assignedFrom(String pool) { + this.fromPool = pool; + return this; + } + + /** + * Return only {@link VmDefinition}s that are assigned to the given user. + * + * @param user the user + * @return the returns the vms + */ + public GetVms assignedTo(String user) { + this.toUser = user; + return this; + } + /** * Returns the name filter criterion, if set. * @@ -86,6 +110,24 @@ public class GetVms extends Event> { return roles; } + /** + * Returns the pool filter criterion, if set. + * + * @return the optional + */ + public Optional fromPool() { + return Optional.ofNullable(fromPool); + } + + /** + * Returns the user filter criterion, if set. + * + * @return the optional + */ + public Optional toUser() { + return Optional.ofNullable(toUser); + } + /** * Return tuple. * 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 b5268a2..b8ba467 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 @@ -18,6 +18,8 @@ package org.jdrupes.vmoperator.manager; +import com.google.gson.JsonObject; +import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.Watch; @@ -29,6 +31,7 @@ import java.util.Set; 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.K8s; import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sDynamicStub; @@ -41,13 +44,15 @@ import org.jdrupes.vmoperator.common.VmDefinitionModel; import org.jdrupes.vmoperator.common.VmDefinitionModels; import org.jdrupes.vmoperator.common.VmDefinitionStub; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; -import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM; 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.GetVms; import org.jdrupes.vmoperator.manager.events.GetVms.VmData; +import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; +import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; import org.jgrapes.core.Event; import org.jgrapes.core.annotation.Handler; @@ -225,7 +230,60 @@ public class VmMonitor extends .filter(c -> event.user().isEmpty() && event.roles().isEmpty() || !c.vmDefinition().permissionsFor(event.user().orElse(null), event.roles()).isEmpty()) + .filter(c -> event.fromPool().isEmpty() + || c.vmDefinition().assignedFrom() + .map(p -> p.equals(event.fromPool().get())).orElse(false)) + .filter(c -> event.toUser().isEmpty() + || c.vmDefinition().assignedTo() + .map(u -> u.equals(event.toUser().get())).orElse(false)) .map(c -> new VmData(c.vmDefinition(), c)) .toList()); } + + /** + * Assign a VM if not already assigned. + * + * @param event the event + * @throws ApiException the api exception + */ + @Handler + public void onAssignVm(AssignVm event) throws ApiException { + // Search for existing assignment. + var assignedVm = channelManager.channels().stream() + .filter(c -> c.vmDefinition().assignedFrom() + .map(p -> p.equals(event.fromPool())).orElse(false)) + .filter(c -> c.vmDefinition().assignedTo() + .map(u -> u.equals(event.toUser())).orElse(false)) + .findFirst(); + if (assignedVm.isPresent()) { + event.setResult(new VmData(assignedVm.get().vmDefinition(), + assignedVm.get())); + return; + } + + // Find available VM. + assignedVm = channelManager.channels().stream() + .filter(c -> c.vmDefinition().pools().contains(event.fromPool())) + .filter(c -> c.vmDefinition().assignedTo().isEmpty()) + .findFirst(); + if (assignedVm.isPresent()) { + var vmDef = assignedVm.get().vmDefinition(); + var vmStub = VmDefinitionStub.get(client(), + new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), + vmDef.namespace(), vmDef.name()); + vmStub.updateStatus(from -> { + JsonObject status = from.status(); + var assignment = GsonPtr.to(status).to("assignment"); + assignment.set("pool", event.fromPool()); + assignment.set("user", event.toUser()); + return status; + }); + + // Always start a newly assigned VM. + fire(new ModifyVm(vmDef.name(), "state", "Running", + assignedVm.get())); + event.setResult(new VmData(vmDef, assignedVm.get())); + } + } + } 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 c48488a..d38d379 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 @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023,2024 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 @@ -51,6 +51,7 @@ import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.VmDefinition; 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.GetDisplayPassword; import org.jdrupes.vmoperator.manager.events.GetPools; import org.jdrupes.vmoperator.manager.events.GetVms; @@ -117,6 +118,7 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; public class VmAccess extends FreeMarkerConlet { private static final String VM_NAME_PROPERTY = "vmName"; + private static final String POOL_NAME_PROPERTY = "poolName"; private static final String RENDERED = VmAccess.class.getName() + ".rendered"; private static final String PENDING @@ -281,33 +283,56 @@ public class VmAccess extends FreeMarkerConlet { private void addMissingConlets(ConsoleConfigured event, ConsoleConnection connection, final Set rendered) throws InterruptedException { - boolean foundMissing = false; var session = connection.session(); - for (var vmName : appPipeline.fire(new GetVms().accessibleFor( + + // Evaluate missing VMs + var missingVms = appPipeline.fire(new GetVms().accessibleFor( WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null), WebConsoleUtils.rolesFromSession(session).stream() .map(ConsoleRole::getName).toList())) - .get().stream().map(d -> d.definition().name()).toList()) { - if (rendered.stream() - .anyMatch(r -> r.mode() == ResourceModel.Mode.VM - && r.name().equals(vmName))) { - continue; - } - if (!foundMissing) { - // Suspending to allow rendering of conlets to be noticed - var failSafe = Components.schedule(t -> event.resumeHandling(), - Duration.ofSeconds(1)); - event.suspendHandling(failSafe::cancel); - connection.setAssociated(PENDING, event); - foundMissing = true; - } + .get().stream().map(d -> d.definition().name()) + .collect(Collectors.toCollection(HashSet::new)); + missingVms.removeAll(rendered.stream() + .filter(r -> r.mode() == ResourceModel.Mode.VM) + .map(ResourceModel::name).toList()); + + // Evaluate missing pools + var missingPools = appPipeline.fire(new GetPools().accessibleFor( + WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null), + WebConsoleUtils.rolesFromSession(session).stream() + .map(ConsoleRole::getName).toList())) + .get().stream().map(VmPool::name) + .collect(Collectors.toCollection(HashSet::new)); + missingPools.removeAll(rendered.stream() + .filter(r -> r.mode() == ResourceModel.Mode.POOL) + .map(ResourceModel::name).toList()); + + // Nothing to do + if (missingVms.isEmpty() && missingPools.isEmpty()) { + return; + } + + // Suspending to allow rendering of conlets to be noticed + var failSafe = Components.schedule(t -> event.resumeHandling(), + Duration.ofSeconds(1)); + event.suspendHandling(failSafe::cancel); + connection.setAssociated(PENDING, event); + + // Create conlets for VMs and pools that haven't been rendered + for (var vmName : missingVms) { fire(new AddConletRequest(event.event().event().renderSupport(), - VmAccess.class.getName(), - RenderMode.asSet(RenderMode.Preview)) + VmAccess.class.getName(), RenderMode.asSet(RenderMode.Preview)) .addProperty(VM_NAME_PROPERTY, vmName), connection); } + for (var poolName : missingPools) { + fire(new AddConletRequest(event.event().event().renderSupport(), + VmAccess.class.getName(), RenderMode.asSet(RenderMode.Preview)) + .addProperty(POOL_NAME_PROPERTY, poolName), + connection); + } } /** @@ -334,9 +359,14 @@ public class VmAccess extends FreeMarkerConlet { protected Optional createNewState(AddConletRequest event, ConsoleConnection connection, String conletId) throws Exception { var model = new ResourceModel(conletId); - model.setMode(ResourceModel.Mode.VM); - model - .setName((String) event.properties().get(VM_NAME_PROPERTY)); + var poolName = (String) event.properties().get(POOL_NAME_PROPERTY); + if (poolName != null) { + model.setMode(ResourceModel.Mode.POOL); + model.setName(poolName); + } else { + model.setMode(ResourceModel.Mode.VM); + model.setName((String) event.properties().get(VM_NAME_PROPERTY)); + } String jsonState = objectMapper.writeValueAsString(model); connection.respond(new KeyValueStoreUpdate().update( storagePath(connection.session(), model.getConletId()), jsonState)); @@ -428,18 +458,13 @@ public class VmAccess extends FreeMarkerConlet { channel.setAssociated(PENDING, null); }); - var session = channel.session(); + VmDefinition vmDef = null; if (model.mode() == ResourceModel.Mode.VM && model.name() != null) { // Remove conlet if VM definition has been removed // or user has not at least one permission - Optional vmData = appPipeline.fire(new GetVms() - .withName(model.name()).accessibleFor( - WebConsoleUtils.userFromSession(session) - .map(ConsoleUser::getName).orElse(null), - WebConsoleUtils.rolesFromSession(session).stream() - .map(ConsoleRole::getName).toList())) - .get().stream().findFirst(); - if (vmData.isEmpty()) { + vmDef = getVmData(model, channel).map(VmData::definition) + .orElse(null); + if (vmDef == null) { channel.respond( new DeleteConlet(conletId, Collections.emptySet())); return Collections.emptySet(); @@ -453,11 +478,13 @@ public class VmAccess extends FreeMarkerConlet { .fire(new GetPools().withName(model.name())).get() .stream().findFirst().orElse(null); if (pool == null - || poolPermissions(pool, channel.session()).isEmpty()) { + || permissions(pool, channel.session()).isEmpty()) { channel.respond( new DeleteConlet(conletId, Collections.emptySet())); return Collections.emptySet(); } + vmDef = getVmData(model, channel).map(VmData::definition) + .orElse(null); } // Render @@ -474,13 +501,32 @@ public class VmAccess extends FreeMarkerConlet { if (!Strings.isNullOrEmpty(model.name())) { Optional.ofNullable(channel.session().get(RENDERED)) .ifPresent(s -> ((Set) s).add(model)); - updateConfig(channel, model); + updatePreview(channel, model, vmDef); } return EnumSet.of(RenderMode.Preview); } - private Set vmPermissions(VmDefinition vmDef, - Session session) { + private Optional getVmData(ResourceModel model, + ConsoleConnection channel) throws InterruptedException { + if (model.mode() == ResourceModel.Mode.VM) { + // Get the VM data by name. + var session = channel.session(); + return appPipeline.fire(new GetVms().withName(model.name()) + .accessibleFor(WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null), + WebConsoleUtils.rolesFromSession(session).stream() + .map(ConsoleRole::getName).toList())) + .get().stream().findFirst(); + } + + // Look for an (already) assigned VM + var user = WebConsoleUtils.userFromSession(channel.session()) + .map(ConsoleUser::getName).orElse(null); + return appPipeline.fire(new GetVms().assignedFrom(model.name()) + .assignedTo(user)).get().stream().findFirst(); + } + + private Set permissions(VmDefinition vmDef, Session session) { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); var roles = WebConsoleUtils.rolesFromSession(session) @@ -488,8 +534,7 @@ public class VmAccess extends FreeMarkerConlet { return vmDef.permissionsFor(user, roles); } - private Set poolPermissions(VmPool pool, - Session session) { + private Set permissions(VmPool pool, Session session) { var user = WebConsoleUtils.userFromSession(session) .map(ConsoleUser::getName).orElse(null); var roles = WebConsoleUtils.rolesFromSession(session) @@ -497,36 +542,60 @@ public class VmAccess extends FreeMarkerConlet { return pool.permissionsFor(user, roles); } - private void updateConfig(ConsoleConnection channel, ResourceModel model) - throws InterruptedException { - channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateConfig", model.mode(), model.name())); - updateVmDef(channel, model); + private Set permissions(ResourceModel model, Session session, + VmPool pool, VmDefinition vmDef) throws InterruptedException { + var user = WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse(null); + var roles = WebConsoleUtils.rolesFromSession(session) + .stream().map(ConsoleRole::getName).toList(); + Set result = new HashSet<>(); + if (model.mode() == ResourceModel.Mode.POOL) { + if (pool == null) { + pool = appPipeline.fire(new GetPools() + .withName(model.name())).get().stream().findFirst() + .orElse(null); + } + if (pool != null) { + result.addAll(pool.permissionsFor(user, roles)); + } + } + if (vmDef != null) { + result.addAll(vmDef.permissionsFor(user, roles)); + } + return result; } - private void updateVmDef(ConsoleConnection channel, ResourceModel model) - throws InterruptedException { - if (Strings.isNullOrEmpty(model.name())) { - return; + private void updatePreview(ConsoleConnection channel, ResourceModel model, + VmDefinition vmDef) throws InterruptedException { + channel.respond(new NotifyConletView(type(), + model.getConletId(), "updateConfig", model.mode(), model.name())); + updateVmDef(channel, model, vmDef); + } + + private void updateVmDef(ConsoleConnection channel, ResourceModel model, + VmDefinition vmDef) throws InterruptedException { + Map data = null; + if (vmDef != null) { + model.setAssignedVm(vmDef.name()); + try { + data = Map.of("metadata", + Map.of("namespace", vmDef.namespace(), + "name", vmDef.name()), + "spec", vmDef.spec(), + "status", vmDef.getStatus(), + "userPermissions", + permissions(model, channel.session(), null, vmDef).stream() + .map(VmDefinition.Permission::toString).toList()); + } catch (JsonSyntaxException e) { + logger.log(Level.SEVERE, e, + () -> "Failed to serialize VM definition"); + return; + } + } else { + model.setAssignedVm(null); } - appPipeline.fire(new GetVms().withName(model.name())).get().stream() - .findFirst().map(d -> d.definition()).ifPresent(vmDef -> { - try { - var data = Map.of("metadata", - Map.of("namespace", vmDef.namespace(), - "name", vmDef.name()), - "spec", vmDef.spec(), - "status", vmDef.getStatus(), - "userPermissions", - vmPermissions(vmDef, channel.session()).stream() - .map(VmDefinition.Permission::toString).toList()); - channel.respond(new NotifyConletView(type(), - model.getConletId(), "updateVmDefinition", data)); - } catch (JsonSyntaxException e) { - logger.log(Level.SEVERE, e, - () -> "Failed to serialize VM definition"); - } - }); + channel.respond(new NotifyConletView(type(), + model.getConletId(), "updateVmDefinition", data)); } @Override @@ -541,7 +610,7 @@ public class VmAccess extends FreeMarkerConlet { } /** - * Track the VM definitions. + * Track the VM definitions and update conlets. * * @param event the event * @param channel the channel @@ -562,17 +631,43 @@ public class VmAccess extends FreeMarkerConlet { for (var conletId : entry.getValue()) { var model = stateFromSession(connection.session(), conletId); if (model.isEmpty() - || model.get().mode() != ResourceModel.Mode.VM - || !Objects.areEqual(model.get().name(), vmDef.name())) { + || Strings.isNullOrEmpty(model.get().name())) { continue; } - if (event.type() == K8sObserver.ResponseType.DELETED - || vmPermissions(vmDef, connection.session()).isEmpty()) { - connection.respond( - new DeleteConlet(conletId, Collections.emptySet())); + if (model.get().mode() == ResourceModel.Mode.VM) { + // Check if this VM is used by conlet + if (!Objects.areEqual(model.get().name(), vmDef.name())) { + continue; + } + if (event.type() == K8sObserver.ResponseType.DELETED + || permissions(vmDef, connection.session()).isEmpty()) { + connection.respond( + new DeleteConlet(conletId, Collections.emptySet())); + continue; + } } else { - updateVmDef(connection, model.get()); + // 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() + .map(p -> p.equals(model.get().name())).orElse(false) + && vmDef.assignedTo().map(u -> u.equals(user)) + .orElse(false); + if (!Objects.areEqual(model.get().assignedVm(), + vmDef.name()) && !toBeUsedByConlet) { + continue; + } + + // Now unassigned if VM is deleted or no longer to be used + if (event.type() == K8sObserver.ResponseType.DELETED + || !toBeUsedByConlet) { + updateVmDef(connection, model.get(), null); + continue; + } } + updateVmDef(connection, model.get(), vmDef); } } } @@ -598,7 +693,7 @@ public class VmAccess extends FreeMarkerConlet { continue; } if (event.deleted() - || poolPermissions(event.vmPool(), connection.session()) + || permissions(event.vmPool(), connection.session()) .isEmpty()) { connection.respond( new DeleteConlet(conletId, Collections.emptySet())); @@ -612,24 +707,36 @@ public class VmAccess extends FreeMarkerConlet { "PMD.ConfusingArgumentToVarargsMethod", "PMD.NcssCount", "PMD.AvoidLiteralsInIfCondition" }) protected void doUpdateConletState(NotifyConletModel event, - ConsoleConnection channel, ResourceModel model) - throws Exception { + ConsoleConnection channel, ResourceModel model) throws Exception { event.stop(); if ("selectedResource".equals(event.method())) { selectResource(event, channel, model); return; } - // Handle command for selected VM - var vmData = appPipeline.fire(new GetVms().withName(model.name())).get() - .stream().findFirst(); + Optional vmData = getVmData(model, channel); if (vmData.isEmpty()) { - return; + if (model.mode() == ResourceModel.Mode.VM) { + return; + } + if ("start".equals(event.method())) { + // Assign a VM. + var user = WebConsoleUtils.userFromSession(channel.session()) + .map(ConsoleUser::getName).orElse(null); + vmData = Optional.ofNullable(appPipeline + .fire(new AssignVm(model.name(), user)).get()); + if (vmData.isEmpty()) { + // TODO message + return; + } + } } + + // Handle command for selected VM var vmChannel = vmData.get().channel(); var vmDef = vmData.get().definition(); var vmName = vmDef.metadata().getName(); - var perms = vmPermissions(vmDef, channel.session()); + var perms = permissions(model, channel.session(), null, vmDef); var resourceBundle = resourceBundle(channel.locale()); switch (event.method()) { case "start": @@ -658,7 +765,7 @@ public class VmAccess extends FreeMarkerConlet { .map(ConsoleUser::getName).orElse(""); var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user), - e -> openConsole(vmName, channel, model, + e -> openConsole(vmDef, channel, model, e.password().orElse(null))); fire(pwQuery, vmChannel); } @@ -680,33 +787,29 @@ public class VmAccess extends FreeMarkerConlet { String jsonState = objectMapper.writeValueAsString(model); channel.respond(new KeyValueStoreUpdate().update(storagePath( channel.session(), model.getConletId()), jsonState)); - updateConfig(channel, model); + updatePreview(channel, model, + getVmData(model, channel).map(VmData::definition).orElse(null)); } catch (IllegalArgumentException e) { logger.warning(() -> "Invalid resource type: " + e.getMessage()); } } - private void openConsole(String vmName, ConsoleConnection connection, + private void openConsole(VmDefinition vmDef, ConsoleConnection connection, ResourceModel model, String password) { - VmDefinition vmDef; - try { - vmDef = appPipeline.fire(new GetVms().withName(model.name())).get() - .stream().findFirst().map(VmData::definition).orElse(null); - } catch (InterruptedException e) { - return; - } if (vmDef == null) { return; } var addr = displayIp(vmDef); if (addr.isEmpty()) { - logger.severe(() -> "Failed to find display IP for " + vmName); + logger + .severe(() -> "Failed to find display IP for " + vmDef.name()); return; } var port = vmDef. fromVm("display", "spice", "port") .map(Number::longValue); if (port.isEmpty()) { - logger.severe(() -> "No port defined for display of " + vmName); + logger + .severe(() -> "No port defined for display of " + vmDef.name()); return; } StringBuffer data = new StringBuffer(100) @@ -799,6 +902,7 @@ public class VmAccess extends FreeMarkerConlet { private Mode mode; private String name; + private String assignedVm; /** * Instantiates a new resource model. @@ -809,6 +913,25 @@ public class VmAccess extends FreeMarkerConlet { super(conletId); } + /** + * Returns the mode. + * + * @return the resourceType + */ + @JsonGetter("mode") + public Mode mode() { + return mode; + } + + /** + * Sets the mode. + * + * @param mode the resource mode to set + */ + public void setMode(Mode mode) { + this.mode = mode; + } + /** * Gets the resource name. * @@ -829,29 +952,29 @@ public class VmAccess extends FreeMarkerConlet { } /** - * Returns the mode. + * Gets the assigned vm. * - * @return the resourceType + * @return the string */ - @JsonGetter("mode") - public Mode mode() { - return mode; + @JsonGetter("assignedVm") + public String assignedVm() { + return assignedVm; } /** - * Sets the mode. + * Sets the assigned vm. * - * @param mode the resource mode to set + * @param name the assigned vm */ - public void setMode(Mode mode) { - this.mode = mode; + public void setAssignedVm(String name) { + this.assignedVm = name; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); - result = prime * result + java.util.Objects.hash(name, mode); + result = prime * result + java.util.Objects.hash(mode, name); return result; } @@ -867,8 +990,8 @@ public class VmAccess extends FreeMarkerConlet { return false; } ResourceModel other = (ResourceModel) obj; - return java.util.Objects.equals(name, other.name) - && mode == other.mode; + return mode == other.mode + && java.util.Objects.equals(name, other.name); } @Override @@ -878,6 +1001,5 @@ public class VmAccess extends FreeMarkerConlet { .append(", name=").append(name).append(']'); return builder.toString(); } - } } 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 c5f3da0..f00d876 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 @@ -44,7 +44,7 @@ interface Api { /* eslint-disable @typescript-eslint/no-explicit-any */ vmName: string; vmDefinition: any; - poolName: string; + poolName: string | null; } const localize = (key: string) => { @@ -64,12 +64,21 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, const previewApi: Api = reactive({ vmName: "", vmDefinition: {}, - poolName: "" + poolName: null }); + const poolName = computed(() => previewApi.poolName); + const vmName = computed(() => previewApi.vmDefinition.name); const configured = computed(() => previewApi.vmDefinition.spec); - const startable = computed(() => previewApi.vmDefinition.spec && - previewApi.vmDefinition.spec.vm.state !== 'Running' - && !previewApi.vmDefinition.running); + const busy = computed(() => previewApi.vmDefinition.spec + && (previewApi.vmDefinition.spec.vm.state === 'Running' + && !previewApi.vmDefinition.running + || previewApi.vmDefinition.spec.vm.state === 'Stopped' + && previewApi.vmDefinition.running)); + const startable = computed(() => previewApi.vmDefinition.spec + && previewApi.vmDefinition.spec.vm.state !== 'Running' + && !previewApi.vmDefinition.running + && previewApi.vmDefinition.userPermissions.includes('start') + || previewApi.poolName !== null && !previewApi.vmDefinition.name); const stoppable = computed(() => previewApi.vmDefinition.spec && previewApi.vmDefinition.spec.vm.state !== 'Stopped' && previewApi.vmDefinition.running); @@ -79,10 +88,8 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, ? previewApi.vmDefinition.userPermissions : []); watch(previewApi, (api: Api) => { - const name = api.vmName || api.poolName; - if (name !== "") { - JGConsole.instance.updateConletTitle(conletId, name); - } + JGConsole.instance.updateConletTitle(conletId, + api.poolName || api.vmDefinition.name || ""); }); provideApi(previewDom, previewApi); @@ -91,16 +98,16 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement, JGConsole.notifyConletModel(conletId, action); }; - return { localize, resourceBase, vmAction, configured, - startable, stoppable, running, inUse, permissions }; + return { localize, resourceBase, vmAction, poolName, vmName, + configured, busy, startable, stoppable, running, inUse, + permissions }; }, template: ` + + + + - - -
{{ vmName }}
-
` }); @@ -160,25 +167,29 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", }); JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess", - "updateVmDefinition", function(conletId: string, vmDefinition: any) { + "updateVmDefinition", function(conletId: string, vmDefinition: any | null) { const conlet = JGConsole.findConletPreview(conletId); if (!conlet) { return; } const api = getApi(conlet.element().querySelector( ":scope .jdrupes-vmoperator-vmaccess-preview"))!; - // Add some short-cuts for rendering - vmDefinition.name = vmDefinition.metadata.name; - vmDefinition.currentCpus = vmDefinition.status.cpus; - vmDefinition.currentRam = Number(vmDefinition.status.ram); - vmDefinition.usedBy = vmDefinition.status.consoleClient || ""; - for (const condition of vmDefinition.status.conditions) { - if (condition.type === "Running") { - vmDefinition.running = condition.status === "True"; - vmDefinition.runningConditionSince - = new Date(condition.lastTransitionTime); - break; + if (vmDefinition) { + // Add some short-cuts for rendering + vmDefinition.name = vmDefinition.metadata.name; + vmDefinition.currentCpus = vmDefinition.status.cpus; + vmDefinition.currentRam = Number(vmDefinition.status.ram); + vmDefinition.usedBy = vmDefinition.status.consoleClient || ""; + for (const condition of vmDefinition.status.conditions) { + if (condition.type === "Running") { + vmDefinition.running = condition.status === "True"; + vmDefinition.runningConditionSince + = new Date(condition.lastTransitionTime); + break; + } } + } else { + vmDefinition = {}; } api.vmDefinition = vmDefinition; }); 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 cf0fb56..86b4014 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 @@ -49,7 +49,12 @@ .jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-preview { + table { + border-spacing: 0; + } + img { + display: block; height: 3em; padding: 0.25rem;