Basically working.
This commit is contained in:
parent
d060a9334a
commit
6d5ba8829c
12 changed files with 493 additions and 148 deletions
|
|
@ -44,7 +44,7 @@ spec:
|
|||
- reset
|
||||
- accessConsole
|
||||
- "*"
|
||||
default: []
|
||||
default: ["accessConsole"]
|
||||
required:
|
||||
- permissions
|
||||
# either Namespaced or Cluster
|
||||
|
|
|
|||
|
|
@ -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: >-
|
||||
|
|
|
|||
|
|
@ -8,3 +8,8 @@ spec:
|
|||
- user: admin
|
||||
may:
|
||||
- accessConsole
|
||||
- user: test
|
||||
may:
|
||||
- accessConsole
|
||||
- start
|
||||
- stop
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ spec:
|
|||
- role: admin
|
||||
may:
|
||||
- "*"
|
||||
- user: test
|
||||
may:
|
||||
- accessConsole
|
||||
|
||||
guestShutdownStops: true
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ spec:
|
|||
- user: admin
|
||||
may:
|
||||
- "*"
|
||||
- user: test
|
||||
may:
|
||||
- "accessConsole"
|
||||
|
||||
resources:
|
||||
requests:
|
||||
|
|
|
|||
|
|
@ -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<Permission> 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<String> pools() {
|
||||
return this.<List<String>> 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<String> assignedFrom() {
|
||||
return fromStatus("assignment", "pool");
|
||||
}
|
||||
|
||||
/**
|
||||
* The user that the VM was assigned to.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> assignedTo() {
|
||||
return fromStatus("assignment", "user");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a condition's status.
|
||||
*
|
||||
|
|
@ -298,6 +336,7 @@ public class VmDefinition {
|
|||
/**
|
||||
* Return extra data.
|
||||
*
|
||||
* @param <T> 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) {
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<VmData> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,8 @@ public class GetVms extends Event<List<GetVms.VmData>> {
|
|||
private String name;
|
||||
private String user;
|
||||
private List<String> 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<List<GetVms.VmData>> {
|
|||
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<List<GetVms.VmData>> {
|
|||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pool filter criterion, if set.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> fromPool() {
|
||||
return Optional.ofNullable(fromPool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user filter criterion, if set.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> toUser() {
|
||||
return Optional.ofNullable(toUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return tuple.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<VmAccess.ResourceModel> {
|
||||
|
||||
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<VmAccess.ResourceModel> {
|
|||
private void addMissingConlets(ConsoleConfigured event,
|
||||
ConsoleConnection connection, final Set<ResourceModel> 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<VmAccess.ResourceModel> {
|
|||
protected Optional<ResourceModel> 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<VmAccess.ResourceModel> {
|
|||
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> 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<VmAccess.ResourceModel> {
|
|||
.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<VmAccess.ResourceModel> {
|
|||
if (!Strings.isNullOrEmpty(model.name())) {
|
||||
Optional.ofNullable(channel.session().get(RENDERED))
|
||||
.ifPresent(s -> ((Set<ResourceModel>) s).add(model));
|
||||
updateConfig(channel, model);
|
||||
updatePreview(channel, model, vmDef);
|
||||
}
|
||||
return EnumSet.of(RenderMode.Preview);
|
||||
}
|
||||
|
||||
private Set<Permission> vmPermissions(VmDefinition vmDef,
|
||||
Session session) {
|
||||
private Optional<VmData> 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<Permission> 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<VmAccess.ResourceModel> {
|
|||
return vmDef.permissionsFor(user, roles);
|
||||
}
|
||||
|
||||
private Set<Permission> poolPermissions(VmPool pool,
|
||||
Session session) {
|
||||
private Set<Permission> 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<VmAccess.ResourceModel> {
|
|||
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<Permission> 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<Permission> 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<String, Object> 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<VmAccess.ResourceModel> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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<VmAccess.ResourceModel> {
|
|||
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<VmAccess.ResourceModel> {
|
|||
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<VmAccess.ResourceModel> {
|
|||
"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> 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<VmAccess.ResourceModel> {
|
|||
.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<VmAccess.ResourceModel> {
|
|||
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.<Number> 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<VmAccess.ResourceModel> {
|
|||
|
||||
private Mode mode;
|
||||
private String name;
|
||||
private String assignedVm;
|
||||
|
||||
/**
|
||||
* Instantiates a new resource model.
|
||||
|
|
@ -809,6 +913,25 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
|
|||
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<VmAccess.ResourceModel> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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<VmAccess.ResourceModel> {
|
|||
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<VmAccess.ResourceModel> {
|
|||
.append(", name=").append(name).append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: `
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="2" style="position: relative"><span
|
||||
style="position: absolute;"
|
||||
:class="{ busy: configured && !startable && !stoppable }"
|
||||
style="position: absolute;" :class="{ busy: busy }"
|
||||
><img role=button :aria-disabled="!running
|
||||
|| !permissions.includes('accessConsole')"
|
||||
v-on:click="vmAction('openConsole')"
|
||||
|
|
@ -110,9 +117,12 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
:title="localize('Open console')"></span><span
|
||||
style="visibility: hidden;"><img
|
||||
:src="resourceBase + 'computer.svg'"></span></td>
|
||||
<td v-if="!poolName" style="padding: 0;"></td>
|
||||
<td v-else>{{ vmName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="jdrupes-vmoperator-vmaccess-preview-action-list">
|
||||
<span role="button"
|
||||
:aria-disabled="!startable || !permissions.includes('start')"
|
||||
<span role="button" :aria-disabled="!startable"
|
||||
tabindex="0" class="fa fa-play" :title="localize('Start VM')"
|
||||
v-on:click="vmAction('start')"></span>
|
||||
<span role="button"
|
||||
|
|
@ -130,9 +140,6 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
});
|
||||
|
|
@ -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<Api>(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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,12 @@
|
|||
|
||||
.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-preview {
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 3em;
|
||||
padding: 0.25rem;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue