Merge branch 'feature/pools' into 'main'
Add VM pools See merge request org/jdrupes/vm-operator!11
This commit is contained in:
commit
85be5b9cbf
34 changed files with 1566 additions and 453 deletions
|
|
@ -16,6 +16,15 @@ spec:
|
|||
spec:
|
||||
type: object
|
||||
properties:
|
||||
retention:
|
||||
description: >-
|
||||
Defines the timeout for assignments. The time may be
|
||||
specified as ISO 8601 time or duration. When specifying
|
||||
a duration, it will be added to the last time the VM's
|
||||
console was used to obtain the timeout.
|
||||
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"
|
||||
permissions:
|
||||
type: array
|
||||
description: >-
|
||||
|
|
@ -44,7 +53,7 @@ spec:
|
|||
- reset
|
||||
- accessConsole
|
||||
- "*"
|
||||
default: []
|
||||
default: ["accessConsole"]
|
||||
required:
|
||||
- permissions
|
||||
# either Namespaced or Cluster
|
||||
|
|
|
|||
|
|
@ -994,6 +994,10 @@ spec:
|
|||
type: array
|
||||
description: >-
|
||||
Defines permissions for accessing and manipulating the VM.
|
||||
The meaning of most permissions should be obvious. The
|
||||
difference between "accessConsole" and "takeConsole" is
|
||||
that "takeConsole" allows the user to take control of
|
||||
the console even if it is already in use by another user.
|
||||
items:
|
||||
type: object
|
||||
description: >-
|
||||
|
|
@ -1017,12 +1021,13 @@ spec:
|
|||
- stop
|
||||
- reset
|
||||
- accessConsole
|
||||
- takeConsole
|
||||
- "*"
|
||||
default: []
|
||||
pools:
|
||||
type: array
|
||||
description: >-
|
||||
List of pools to which this VM belongs.
|
||||
List of pools this VM belongs to.
|
||||
items:
|
||||
type: string
|
||||
default: []
|
||||
|
|
@ -1486,6 +1491,24 @@ spec:
|
|||
by the runner if password protection is not enabled.
|
||||
type: integer
|
||||
default: 0
|
||||
assignment:
|
||||
description: >-
|
||||
The assignment of this VM to a a particular user.
|
||||
type: object
|
||||
properties:
|
||||
pool:
|
||||
description: >-
|
||||
The pool this VM is taken from.
|
||||
type: string
|
||||
user:
|
||||
description: >-
|
||||
The user this VM is assigned to.
|
||||
type: string
|
||||
lastUsed:
|
||||
description: >-
|
||||
The last time this VM was used by the user.
|
||||
type: string
|
||||
default: {}
|
||||
conditions:
|
||||
description: >-
|
||||
List of component conditions observed
|
||||
|
|
|
|||
|
|
@ -40,15 +40,30 @@
|
|||
- name: admin
|
||||
fullName: Administrator
|
||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||
- name: test
|
||||
fullName: Test Account
|
||||
- name: operator
|
||||
fullName: Operator
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
- name: test1
|
||||
fullName: Test Account 1
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
- name: test2
|
||||
fullName: Test Account 2
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
- name: test3
|
||||
fullName: Test Account 3
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
"/RoleConfigurator":
|
||||
rolesByUser:
|
||||
# User admin has role admin
|
||||
admin:
|
||||
- admin
|
||||
test:
|
||||
operator:
|
||||
- operator
|
||||
test1:
|
||||
- user
|
||||
test2:
|
||||
- user
|
||||
test3:
|
||||
- user
|
||||
# All users have role other
|
||||
"*":
|
||||
|
|
@ -59,8 +74,10 @@
|
|||
# Admins can use all conlets
|
||||
admin:
|
||||
- "*"
|
||||
operator:
|
||||
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||
user:
|
||||
- org.jdrupes.vmoperator.vmviewer.VmViewer
|
||||
- org.jdrupes.vmoperator.vmaccess.VmAccess
|
||||
# Others cannot use any conlet (except login conlet to log out)
|
||||
other:
|
||||
- org.jgrapes.webconlet.oidclogin.LoginConlet
|
||||
|
|
|
|||
|
|
@ -54,7 +54,13 @@ patches:
|
|||
- name: admin
|
||||
fullName: Administrator
|
||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||
- name: test
|
||||
- name: test1
|
||||
fullName: Test Account
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
- name: test2
|
||||
fullName: Test Account
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
- name: test3
|
||||
fullName: Test Account
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
"/RoleConfigurator":
|
||||
|
|
@ -62,7 +68,11 @@ patches:
|
|||
# User admin has role admin
|
||||
admin:
|
||||
- admin
|
||||
test:
|
||||
test1:
|
||||
- user
|
||||
test2:
|
||||
- user
|
||||
test3:
|
||||
- user
|
||||
# All users have role other
|
||||
"*":
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ metadata:
|
|||
namespace: vmop-dev
|
||||
name: test-vms
|
||||
spec:
|
||||
retention: "PT1m"
|
||||
permissions:
|
||||
- user: admin
|
||||
may:
|
||||
- accessConsole
|
||||
- role: user
|
||||
may:
|
||||
- accessConsole
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@ dependencies {
|
|||
api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)'
|
||||
api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
|
||||
api 'org.yaml:snakeyaml'
|
||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ import java.util.function.Function;
|
|||
* @param <O> the generic type
|
||||
* @param <L> the generic type
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
||||
"PMD.CouplingBetweenObjects" })
|
||||
public class K8sClusterGenericStub<O extends KubernetesObject,
|
||||
L extends KubernetesListObject> {
|
||||
protected final K8sClient client;
|
||||
|
|
|
|||
|
|
@ -18,13 +18,18 @@
|
|||
|
||||
package org.jdrupes.vmoperator.common;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import io.kubernetes.client.openapi.models.V1Condition;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
|
@ -35,9 +40,12 @@ import org.jdrupes.vmoperator.util.DataPath;
|
|||
/**
|
||||
* Represents a VM definition.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataClass" })
|
||||
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
|
||||
public class VmDefinition {
|
||||
|
||||
private static ObjectMapper objectMapper
|
||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
private String kind;
|
||||
private String apiVersion;
|
||||
private V1ObjectMeta metadata;
|
||||
|
|
@ -57,7 +65,7 @@ public class VmDefinition {
|
|||
*/
|
||||
public enum Permission {
|
||||
START("start"), STOP("stop"), RESET("reset"),
|
||||
ACCESS_CONSOLE("accessConsole");
|
||||
ACCESS_CONSOLE("accessConsole"), TAKE_CONSOLE("takeConsole");
|
||||
|
||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||
private static Map<String, Permission> reprs = new HashMap<>();
|
||||
|
|
@ -88,12 +96,44 @@ public class VmDefinition {
|
|||
return Set.of(reprs.get(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* To string.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return repr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions granted to a user or role.
|
||||
*
|
||||
* @param user the user
|
||||
* @param role the role
|
||||
* @param may the may
|
||||
*/
|
||||
public record Grant(String user, String role, Set<Permission> may) {
|
||||
|
||||
/**
|
||||
* To string.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (user != null) {
|
||||
builder.append("User ").append(user);
|
||||
} else {
|
||||
builder.append("Role ").append(role);
|
||||
}
|
||||
builder.append(" may=").append(may).append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the kind.
|
||||
*
|
||||
|
|
@ -157,6 +197,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.
|
||||
*
|
||||
|
|
@ -245,6 +295,82 @@ 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Last usage of assigned VM.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<Instant> assignmentLastUsed() {
|
||||
return this.<String> fromStatus("assignment", "lastUsed")
|
||||
.map(Instant::parse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a condition from the status.
|
||||
*
|
||||
* @param name the condition's name
|
||||
* @return the status, if the condition is defined
|
||||
*/
|
||||
public Optional<V1Condition> condition(String name) {
|
||||
return this.<List<Map<String, Object>>> fromStatus("conditions")
|
||||
.orElse(Collections.emptyList()).stream()
|
||||
.filter(cond -> DataPath.get(cond, "type")
|
||||
.map(name::equals).orElse(false))
|
||||
.findFirst()
|
||||
.map(cond -> objectMapper.convertValue(cond, V1Condition.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a condition's status.
|
||||
*
|
||||
* @param name the condition's name
|
||||
* @return the status, if the condition is defined
|
||||
*/
|
||||
public Optional<Boolean> conditionStatus(String name) {
|
||||
return this.<List<Map<String, Object>>> fromStatus("conditions")
|
||||
.orElse(Collections.emptyList()).stream()
|
||||
.filter(cond -> DataPath.get(cond, "type")
|
||||
.map(name::equals).orElse(false))
|
||||
.findFirst().map(cond -> DataPath.get(cond, "status")
|
||||
.map("True"::equals).orElse(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the console is in use.
|
||||
*
|
||||
* @return true, if successful
|
||||
*/
|
||||
public boolean consoleConnected() {
|
||||
return conditionStatus("ConsoleConnected").orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last known console user.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> consoleUser() {
|
||||
return this.<String> fromStatus("consoleUser");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set extra data (locally used, unknown to kubernetes).
|
||||
*
|
||||
|
|
@ -260,6 +386,7 @@ public class VmDefinition {
|
|||
/**
|
||||
* Return extra data.
|
||||
*
|
||||
* @param <T> the generic type
|
||||
* @param property the property
|
||||
* @return the object
|
||||
*/
|
||||
|
|
@ -287,12 +414,11 @@ public class VmDefinition {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the requested VM state
|
||||
* Return the requested VM state.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
public RequestedVmState vmState() {
|
||||
// TODO
|
||||
return fromVm("state")
|
||||
.map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
|
||||
: RequestedVmState.STOPPED)
|
||||
|
|
@ -329,4 +455,38 @@ public class VmDefinition {
|
|||
return this.<Number> fromStatus("displayPasswordSerial")
|
||||
.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) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
VmDefinition other = (VmDefinition) obj;
|
||||
return Objects.equals(metadata.getNamespace(),
|
||||
other.metadata.getNamespace())
|
||||
&& Objects.equals(metadata.getName(), other.metadata.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,16 +18,17 @@
|
|||
|
||||
package org.jdrupes.vmoperator.common;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jdrupes.vmoperator.common.VmDefinition.Grant;
|
||||
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
|
||||
import org.jdrupes.vmoperator.util.DataPath;
|
||||
|
||||
/**
|
||||
|
|
@ -37,10 +38,21 @@ import org.jdrupes.vmoperator.util.DataPath;
|
|||
public class VmPool {
|
||||
|
||||
private String name;
|
||||
private String retention;
|
||||
private boolean defined;
|
||||
private List<Grant> permissions = Collections.emptyList();
|
||||
private final Set<String> vms
|
||||
= Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
/**
|
||||
* Instantiates a new vm pool.
|
||||
*
|
||||
* @param name the name
|
||||
*/
|
||||
public VmPool(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name.
|
||||
*
|
||||
|
|
@ -60,6 +72,44 @@ public class VmPool {
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if is defined.
|
||||
*
|
||||
* @return the result
|
||||
*/
|
||||
public boolean isDefined() {
|
||||
return defined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if is.
|
||||
*
|
||||
* @param defined the defined to set
|
||||
*/
|
||||
public void setDefined(boolean defined) {
|
||||
this.defined = defined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the retention.
|
||||
*
|
||||
* @return the retention
|
||||
*/
|
||||
public String retention() {
|
||||
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.
|
||||
*
|
||||
* @return the permissions
|
||||
*/
|
||||
public List<Grant> permissions() {
|
||||
|
|
@ -84,6 +134,11 @@ public class VmPool {
|
|||
return vms;
|
||||
}
|
||||
|
||||
/**
|
||||
* To string.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||
public String toString() {
|
||||
|
|
@ -93,9 +148,8 @@ public class VmPool {
|
|||
if (vms.size() <= 3) {
|
||||
builder.append(vms);
|
||||
} else {
|
||||
builder.append('[');
|
||||
vms.stream().limit(3).map(s -> s + ",").forEach(builder::append);
|
||||
builder.append("...]");
|
||||
builder.append('[').append(vms.stream().limit(3).map(s -> s + ",")
|
||||
.collect(Collectors.joining())).append("...]");
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
|
|
@ -120,67 +174,15 @@ public class VmPool {
|
|||
}
|
||||
|
||||
/**
|
||||
* A permission grant to a user or role.
|
||||
* Return the instant until which an assignment should be retained.
|
||||
*
|
||||
* @param user the user
|
||||
* @param role the role
|
||||
* @param may the may
|
||||
* @param lastUsed the last used
|
||||
* @return the instant
|
||||
*/
|
||||
public record Grant(String user, String role, Set<Permission> may) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (user != null) {
|
||||
builder.append("User ").append(user);
|
||||
} else {
|
||||
builder.append("Role ").append(role);
|
||||
public Instant retainUntil(Instant lastUsed) {
|
||||
if (retention.startsWith("P")) {
|
||||
return lastUsed.plus(Duration.parse(retention));
|
||||
}
|
||||
builder.append(" may=").append(may).append(']');
|
||||
return builder.toString();
|
||||
return Instant.parse(retention);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions for accessing and manipulating the pool.
|
||||
*/
|
||||
public enum Permission {
|
||||
START("start"), STOP("stop"), RESET("reset"),
|
||||
ACCESS_CONSOLE("accessConsole");
|
||||
|
||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||
private static Map<String, Permission> reprs = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (var value : EnumSet.allOf(Permission.class)) {
|
||||
reprs.put(value.repr, value);
|
||||
}
|
||||
}
|
||||
|
||||
private final String repr;
|
||||
|
||||
Permission(String repr) {
|
||||
this.repr = repr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create permission from representation in CRD.
|
||||
*
|
||||
* @param value the value
|
||||
* @return the permission
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||
public static Set<Permission> parse(String value) {
|
||||
if ("*".equals(value)) {
|
||||
return EnumSet.allOf(Permission.class);
|
||||
}
|
||||
return Set.of(reprs.get(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return repr;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,5 +10,4 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
api project(':org.jdrupes.vmoperator.common')
|
||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jdrupes.vmoperator.common.VmPool;
|
||||
import org.jgrapes.core.Event;
|
||||
|
||||
/**
|
||||
* Gets the known pools' definitions.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
public class GetPools extends Event<List<VmPool>> {
|
||||
|
||||
private String name;
|
||||
private String user;
|
||||
private List<String> roles = Collections.emptyList();
|
||||
|
||||
/**
|
||||
* Return only the pool with the given name.
|
||||
*
|
||||
* @param name the name
|
||||
* @return the returns the vms
|
||||
*/
|
||||
public GetPools withName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only {@link VmPool}s that are accessible by
|
||||
* the given user or roles.
|
||||
*
|
||||
* @param user the user
|
||||
* @param roles the roles
|
||||
* @return the event
|
||||
*/
|
||||
public GetPools accessibleFor(String user, List<String> roles) {
|
||||
this.user = user;
|
||||
this.roles = roles;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name filter criterion, if set.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> name() {
|
||||
return Optional.ofNullable(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user filter criterion, if set.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> forUser() {
|
||||
return Optional.ofNullable(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the roles criterion.
|
||||
*
|
||||
* @return the list
|
||||
*/
|
||||
public List<String> forRoles() {
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||
import org.jgrapes.core.Event;
|
||||
|
||||
/**
|
||||
* Gets the known VMs' definitions and channels.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
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.
|
||||
*
|
||||
* @param name the name
|
||||
* @return the returns the vms
|
||||
*/
|
||||
public GetVms withName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only {@link VmDefinition}s that are accessible by
|
||||
* the given user or roles.
|
||||
*
|
||||
* @param user the user
|
||||
* @param roles the roles
|
||||
* @return the event
|
||||
*/
|
||||
public GetVms accessibleFor(String user, List<String> roles) {
|
||||
this.user = user;
|
||||
this.roles = roles;
|
||||
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.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> name() {
|
||||
return Optional.ofNullable(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user filter criterion, if set.
|
||||
*
|
||||
* @return the optional
|
||||
*/
|
||||
public Optional<String> user() {
|
||||
return Optional.ofNullable(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the roles criterion.
|
||||
*
|
||||
* @return the list
|
||||
*/
|
||||
public List<String> roles() {
|
||||
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.
|
||||
*
|
||||
* @param definition the definition
|
||||
* @param channel the channel
|
||||
*/
|
||||
public record VmData(VmDefinition definition, VmChannel channel) {
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ dependencies {
|
|||
implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)'
|
||||
implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)'
|
||||
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.2.0,3)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.8.0,2)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.4.0,2)'
|
||||
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.7.0,2)'
|
||||
|
|
|
|||
|
|
@ -86,14 +86,14 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
|||
// Maybe override logging.properties from reconciler configuration.
|
||||
DataPath.<String> get(model, "reconciler", "loggingProperties")
|
||||
.ifPresent(props -> {
|
||||
GsonPtr.to(mapDef.getRaw()).get(JsonObject.class, "data")
|
||||
GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data")
|
||||
.get().addProperty("logging.properties", props);
|
||||
});
|
||||
|
||||
// Maybe override logging.properties from VM definition.
|
||||
DataPath.<String> get(model, "cr", "spec", "loggingProperties")
|
||||
.ifPresent(props -> {
|
||||
GsonPtr.to(mapDef.getRaw()).get(JsonObject.class, "data")
|
||||
GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data")
|
||||
.get().addProperty("logging.properties", props);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ public class Controller extends Component {
|
|||
// to access the VM's console. Might change in the future.
|
||||
// attach(new ServiceMonitor(channel()).channelManager(chanMgr));
|
||||
attach(new Reconciler(channel()));
|
||||
attach(new PoolManager(channel()));
|
||||
attach(new PoolMonitor(channel()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,27 +18,28 @@
|
|||
|
||||
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;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
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.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.VmDefinitionStub;
|
||||
import org.jdrupes.vmoperator.common.VmPool;
|
||||
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM_POOL;
|
||||
import org.jdrupes.vmoperator.manager.events.GetPools;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
|
||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||
|
|
@ -53,11 +54,9 @@ import org.jgrapes.core.events.Attached;
|
|||
* avoid concurrent change informations.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
|
||||
public class PoolManager extends
|
||||
public class PoolMonitor extends
|
||||
AbstractMonitor<K8sDynamicModel, K8sDynamicModels, Channel> {
|
||||
|
||||
private final ReentrantLock pendingLock = new ReentrantLock();
|
||||
private final Map<String, Set<String>> pending = new ConcurrentHashMap<>();
|
||||
private final Map<String, VmPool> pools = new ConcurrentHashMap<>();
|
||||
private EventPipeline poolPipeline;
|
||||
|
||||
|
|
@ -67,7 +66,7 @@ public class PoolManager extends
|
|||
* @param componentChannel the component channel
|
||||
* @param channelManager the channel manager
|
||||
*/
|
||||
public PoolManager(Channel componentChannel) {
|
||||
public PoolMonitor(Channel componentChannel) {
|
||||
super(componentChannel, K8sDynamicModel.class,
|
||||
K8sDynamicModels.class);
|
||||
}
|
||||
|
|
@ -107,18 +106,13 @@ public class PoolManager extends
|
|||
|
||||
// When pool is deleted, save VMs in pending
|
||||
if (type == ResponseType.DELETED) {
|
||||
try {
|
||||
pendingLock.lock();
|
||||
Optional.ofNullable(pools.get(poolName)).ifPresent(
|
||||
p -> {
|
||||
pending.computeIfAbsent(poolName, k -> Collections
|
||||
.synchronizedSet(new HashSet<>())).addAll(p.vms());
|
||||
Optional.ofNullable(pools.get(poolName)).ifPresent(pool -> {
|
||||
pool.setDefined(false);
|
||||
if (pool.vms().isEmpty()) {
|
||||
pools.remove(poolName);
|
||||
poolPipeline.fire(new VmPoolChanged(p, true));
|
||||
});
|
||||
} finally {
|
||||
pendingLock.unlock();
|
||||
}
|
||||
poolPipeline.fire(new VmPoolChanged(pool, true));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -135,75 +129,85 @@ public class PoolManager extends
|
|||
}
|
||||
}
|
||||
|
||||
// Convert to VM pool
|
||||
var vmPool = client().getJSON().getGson().fromJson(
|
||||
GsonPtr.to(poolModel.data()).to("spec").get(),
|
||||
VmPool.class);
|
||||
V1ObjectMeta metadata = response.object.getMetadata();
|
||||
vmPool.setName(metadata.getName());
|
||||
|
||||
// If modified, merge changes
|
||||
if (type == ResponseType.MODIFIED && pools.containsKey(poolName)) {
|
||||
pools.get(poolName).setPermissions(vmPool.permissions());
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new pool
|
||||
try {
|
||||
pendingLock.lock();
|
||||
Optional.ofNullable(pending.get(poolName)).ifPresent(s -> {
|
||||
vmPool.vms().addAll(s);
|
||||
});
|
||||
pending.remove(poolName);
|
||||
pools.put(poolName, vmPool);
|
||||
// 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);
|
||||
poolPipeline.fire(new VmPoolChanged(vmPool));
|
||||
} finally {
|
||||
pendingLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track VM definition changes.
|
||||
*
|
||||
* @param event the event
|
||||
* @throws ApiException
|
||||
*/
|
||||
@Handler
|
||||
public void onVmDefChanged(VmDefChanged event) {
|
||||
String vmName = event.vmDefinition().name();
|
||||
public void onVmDefChanged(VmDefChanged event) throws ApiException {
|
||||
final var vmDef = event.vmDefinition();
|
||||
final String vmName = vmDef.name();
|
||||
switch (event.type()) {
|
||||
case ADDED:
|
||||
try {
|
||||
pendingLock.lock();
|
||||
event.vmDefinition().<List<String>> fromSpec("pools")
|
||||
vmDef.<List<String>> fromSpec("pools")
|
||||
.orElse(Collections.emptyList()).stream().forEach(p -> {
|
||||
if (pools.containsKey(p)) {
|
||||
pools.get(p).vms().add(vmName);
|
||||
} else {
|
||||
pending.computeIfAbsent(p, k -> Collections
|
||||
.synchronizedSet(new HashSet<>())).add(vmName);
|
||||
}
|
||||
pools.computeIfAbsent(p, k -> new VmPool(p))
|
||||
.vms().add(vmName);
|
||||
poolPipeline.fire(new VmPoolChanged(pools.get(p)));
|
||||
});
|
||||
} finally {
|
||||
pendingLock.unlock();
|
||||
}
|
||||
break;
|
||||
case DELETED:
|
||||
try {
|
||||
pendingLock.lock();
|
||||
pools.values().stream().forEach(p -> {
|
||||
if (p.vms().remove(vmName)) {
|
||||
poolPipeline.fire(new VmPoolChanged(p));
|
||||
}
|
||||
});
|
||||
// Should not be necessary, but just in case
|
||||
pending.values().stream().forEach(s -> s.remove(vmName));
|
||||
} finally {
|
||||
pendingLock.unlock();
|
||||
}
|
||||
break;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Sync last usage to console state change if user matches
|
||||
if (vmDef.assignedTo()
|
||||
.map(at -> at.equals(vmDef.consoleUser().orElse(null)))
|
||||
.orElse(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ccChange = vmDef.condition("ConsoleConnected")
|
||||
.map(cc -> cc.getLastTransitionTime().toInstant());
|
||||
if (ccChange
|
||||
.map(tt -> vmDef.assignmentLastUsed().map(alu -> alu.isAfter(tt))
|
||||
.orElse(true))
|
||||
.orElse(true)) {
|
||||
return;
|
||||
}
|
||||
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("lastUsed", ccChange.get().toString());
|
||||
return status;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the requested pools.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
@Handler
|
||||
public void onGetPools(GetPools event) {
|
||||
event.setResult(pools.values().stream().filter(VmPool::isDefined)
|
||||
.filter(p -> event.name().isEmpty()
|
||||
|| p.name().equals(event.name().get()))
|
||||
.filter(p -> event.forUser().isEmpty() && event.forRoles().isEmpty()
|
||||
|| !p.permissionsFor(event.forUser().orElse(null),
|
||||
event.forRoles()).isEmpty())
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ import org.jdrupes.vmoperator.common.K8sV1PvcStub;
|
|||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jdrupes.vmoperator.util.DataPath;
|
||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||
|
|
@ -179,9 +180,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
|||
var pvcDef = Dynamics.newFromYaml(
|
||||
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||
|
||||
// Do apply changes
|
||||
// Apply changes
|
||||
var pvcStub
|
||||
= K8sV1PvcStub.get(channel.client(), vmDef.namespace(), pvcName);
|
||||
var pvc = pvcStub.model();
|
||||
if (pvc.isEmpty()
|
||||
|| !"Bound".equals(pvc.get().getStatus().getPhase())) {
|
||||
// Does not exist or isn't bound, use apply
|
||||
PatchOptions opts = new PatchOptions();
|
||||
opts.setForce(true);
|
||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||
|
|
@ -191,5 +196,20 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
|||
logger.warning(
|
||||
() -> "Could not patch pvc for " + pvcStub.name());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If bound, use json merge, omitting immutable fields
|
||||
var spec = GsonPtr.to(pvcDef.getRaw()).to("spec");
|
||||
spec.removeExcept("volumeAttributesClassName", "resources");
|
||||
spec.get("resources").ifPresent(p -> p.removeExcept("requests"));
|
||||
PatchOptions opts = new PatchOptions();
|
||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||
if (pvcStub.patch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH,
|
||||
new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts)
|
||||
.isEmpty()) {
|
||||
logger.warning(
|
||||
() -> "Could not patch pvc for " + pvcStub.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -18,20 +18,22 @@
|
|||
|
||||
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;
|
||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
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;
|
||||
|
|
@ -43,15 +45,21 @@ import org.jdrupes.vmoperator.common.VmDefinition;
|
|||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||
import org.jdrupes.vmoperator.common.VmDefinitionModels;
|
||||
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||
import org.jdrupes.vmoperator.common.VmPool;
|
||||
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.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.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jdrupes.vmoperator.util.DataPath;
|
||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.Event;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
|
||||
/**
|
||||
* Watches for changes of VM definitions.
|
||||
|
|
@ -119,11 +127,6 @@ public class VmMonitor extends
|
|||
V1ObjectMeta metadata = response.object.getMetadata();
|
||||
VmChannel channel = channelManager.channelGet(metadata.getName());
|
||||
|
||||
// Remove from channel manager if deleted
|
||||
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
|
||||
channelManager.remove(metadata.getName());
|
||||
}
|
||||
|
||||
// Get full definition and associate with channel as backup
|
||||
var vmModel = response.object;
|
||||
if (vmModel.data() == null) {
|
||||
|
|
@ -151,17 +154,16 @@ public class VmMonitor extends
|
|||
|
||||
// Create and fire changed event. Remove channel from channel
|
||||
// manager on completion.
|
||||
channel.pipeline()
|
||||
.fire(Event.onCompletion(
|
||||
new VmDefChanged(ResponseType.valueOf(response.type),
|
||||
VmDefChanged chgEvt
|
||||
= new VmDefChanged(ResponseType.valueOf(response.type),
|
||||
channel.setGeneration(response.object.getMetadata()
|
||||
.getGeneration()),
|
||||
vmDef),
|
||||
e -> {
|
||||
if (e.type() == ResponseType.DELETED) {
|
||||
channelManager.remove(e.vmDefinition().name());
|
||||
vmDef);
|
||||
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
|
||||
chgEvt = Event.onCompletion(chgEvt,
|
||||
e -> channelManager.remove(e.vmDefinition().name()));
|
||||
}
|
||||
}), channel);
|
||||
channel.pipeline().fire(chgEvt, channel);
|
||||
}
|
||||
|
||||
private VmDefinitionModel getModel(K8sClient client,
|
||||
|
|
@ -190,16 +192,7 @@ public class VmMonitor extends
|
|||
// VM definition status changes before the pod terminates.
|
||||
// This results in pod information being shown for a stopped
|
||||
// VM which is irritating. So check condition first.
|
||||
@SuppressWarnings("PMD.LambdaCanBeMethodReference")
|
||||
var isRunning
|
||||
= vmDef.<List<Map<String, Object>>> fromStatus("conditions")
|
||||
.orElse(Collections.emptyList()).stream()
|
||||
.filter(cond -> DataPath.get(cond, "type")
|
||||
.map(t -> "Running".equals(t)).orElse(false))
|
||||
.findFirst().map(cond -> DataPath.get(cond, "status")
|
||||
.map(s -> "True".equals(s)).orElse(false))
|
||||
.orElse(false);
|
||||
if (!isRunning) {
|
||||
if (!vmDef.conditionStatus("Running").orElse(false)) {
|
||||
return;
|
||||
}
|
||||
var podSearch = new ListOptions();
|
||||
|
|
@ -227,4 +220,131 @@ public class VmMonitor extends
|
|||
() -> "Cannot access node information: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the VM data.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
@Handler
|
||||
public void onGetVms(GetVms event) {
|
||||
event.setResult(channelManager.channels().stream()
|
||||
.filter(c -> event.name().isEmpty()
|
||||
|| c.vmDefinition().name().equals(event.name().get()))
|
||||
.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
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
@Handler
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
public void onAssignVm(AssignVm event)
|
||||
throws ApiException, InterruptedException {
|
||||
VmPool vmPool = null;
|
||||
while (true) {
|
||||
// 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()) {
|
||||
var vmDef = assignedVm.get().vmDefinition();
|
||||
event.setResult(new VmData(vmDef, assignedVm.get()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the pool definition for retention time calculations
|
||||
if (vmPool == null) {
|
||||
vmPool = newEventPipeline().fire(new GetPools()
|
||||
.withName(event.fromPool())).get().stream().findFirst()
|
||||
.orElse(null);
|
||||
if (vmPool == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find available VM.
|
||||
var pool = vmPool;
|
||||
assignedVm = channelManager.channels().stream()
|
||||
.filter(c -> isAssignable(pool, c.vmDefinition()))
|
||||
.sorted(Comparator.comparing(c -> c.vmDefinition()
|
||||
.assignmentLastUsed().orElse(Instant.ofEpochSecond(0))))
|
||||
.findFirst();
|
||||
|
||||
// None found
|
||||
if (assignedVm.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign to user
|
||||
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());
|
||||
assignment.set("lastUsed", Instant.now().toString());
|
||||
return status;
|
||||
});
|
||||
|
||||
// Make sure that a newly assigned VM is running.
|
||||
fire(new ModifyVm(vmDef.name(), "state", "Running",
|
||||
assignedVm.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.SimplifyBooleanReturns")
|
||||
private boolean isAssignable(VmPool pool, VmDefinition vmDef) {
|
||||
// Check if the VM is in the pool
|
||||
if (!vmDef.pools().contains(pool.name())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the VM is not in use
|
||||
if (vmDef.consoleConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If not assigned, it's usable
|
||||
if (vmDef.assignedTo().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it is to be retained
|
||||
if (vmDef.assignmentLastUsed()
|
||||
.map(lu -> pool.retainUntil(lu))
|
||||
.map(ru -> Instant.now().isBefore(ru)).orElse(false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional check in case lastUsed has not been updated
|
||||
// by PoolMonitor#onVmDefChanged() yet ("race condition")
|
||||
if (vmDef.condition("ConsoleConnected")
|
||||
.map(cc -> cc.getLastTransitionTime().toInstant())
|
||||
.map(t -> pool.retainUntil(t))
|
||||
.map(ru -> Instant.now().isBefore(ru)).orElse(false)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.google.gson.JsonElement;
|
|||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
|
@ -62,7 +63,8 @@ public class GsonPtr {
|
|||
* @param selectors the selectors
|
||||
* @return the Gson pointer
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" })
|
||||
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace",
|
||||
"PMD.AvoidDuplicateLiterals" })
|
||||
public GsonPtr to(Object... selectors) {
|
||||
JsonElement element = position;
|
||||
for (Object sel : selectors) {
|
||||
|
|
@ -91,6 +93,42 @@ public class GsonPtr {
|
|||
return new GsonPtr(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance pointing to the {@link JsonElement}
|
||||
* selected by the given selectors. If a selector of type
|
||||
* {@link String} denotes a non-existant member of a
|
||||
* {@link JsonObject} the result is empty.
|
||||
*
|
||||
* @param selectors the selectors
|
||||
* @return the Gson pointer
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" })
|
||||
public Optional<GsonPtr> get(Object... selectors) {
|
||||
JsonElement element = position;
|
||||
for (Object sel : selectors) {
|
||||
if (element instanceof JsonObject obj
|
||||
&& sel instanceof String member) {
|
||||
element = obj.get(member);
|
||||
if (element == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (element instanceof JsonArray arr
|
||||
&& sel instanceof Integer index) {
|
||||
try {
|
||||
element = arr.get(index);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
throw new IllegalStateException("Selected array index"
|
||||
+ " may not be empty.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw new IllegalStateException("Invalid selection");
|
||||
}
|
||||
return Optional.of(new GsonPtr(element));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link JsonElement} that the pointer points to.
|
||||
*
|
||||
|
|
@ -109,7 +147,7 @@ public class GsonPtr {
|
|||
* @return the result
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
||||
public <T extends JsonElement> T get(Class<T> cls) {
|
||||
public <T extends JsonElement> T getAs(Class<T> cls) {
|
||||
if (cls.isAssignableFrom(position.getClass())) {
|
||||
return cls.cast(position);
|
||||
}
|
||||
|
|
@ -128,7 +166,7 @@ public class GsonPtr {
|
|||
*/
|
||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
||||
public <T extends JsonElement> Optional<T>
|
||||
get(Class<T> cls, Object... selectors) {
|
||||
getAs(Class<T> cls, Object... selectors) {
|
||||
JsonElement element = position;
|
||||
for (Object sel : selectors) {
|
||||
if (element instanceof JsonObject obj
|
||||
|
|
@ -163,7 +201,7 @@ public class GsonPtr {
|
|||
* @return the as string
|
||||
*/
|
||||
public Optional<String> getAsString(Object... selectors) {
|
||||
return get(JsonPrimitive.class, selectors)
|
||||
return getAs(JsonPrimitive.class, selectors)
|
||||
.map(JsonPrimitive::getAsString);
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +212,7 @@ public class GsonPtr {
|
|||
* @return the as string
|
||||
*/
|
||||
public Optional<Integer> getAsInt(Object... selectors) {
|
||||
return get(JsonPrimitive.class, selectors)
|
||||
return getAs(JsonPrimitive.class, selectors)
|
||||
.map(JsonPrimitive::getAsInt);
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +223,7 @@ public class GsonPtr {
|
|||
* @return the as string
|
||||
*/
|
||||
public Optional<BigInteger> getAsBigInteger(Object... selectors) {
|
||||
return get(JsonPrimitive.class, selectors)
|
||||
return getAs(JsonPrimitive.class, selectors)
|
||||
.map(JsonPrimitive::getAsBigInteger);
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +234,7 @@ public class GsonPtr {
|
|||
* @return the as string
|
||||
*/
|
||||
public Optional<Long> getAsLong(Object... selectors) {
|
||||
return get(JsonPrimitive.class, selectors)
|
||||
return getAs(JsonPrimitive.class, selectors)
|
||||
.map(JsonPrimitive::getAsLong);
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +245,7 @@ public class GsonPtr {
|
|||
* @return the boolean
|
||||
*/
|
||||
public Optional<Boolean> getAsBoolean(Object... selectors) {
|
||||
return get(JsonPrimitive.class, selectors)
|
||||
return getAs(JsonPrimitive.class, selectors)
|
||||
.map(JsonPrimitive::getAsBoolean);
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +260,7 @@ public class GsonPtr {
|
|||
@SuppressWarnings("unchecked")
|
||||
public <T extends JsonElement> List<T> getAsListOf(Class<T> cls,
|
||||
Object... selectors) {
|
||||
return get(JsonArray.class, selectors).map(a -> (List<T>) a.asList())
|
||||
return getAs(JsonArray.class, selectors).map(a -> (List<T>) a.asList())
|
||||
.orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
|
|
@ -336,4 +374,22 @@ public class GsonPtr {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all properties except the specified ones.
|
||||
*
|
||||
* @param properties the properties
|
||||
*/
|
||||
public void removeExcept(String... properties) {
|
||||
if (!position.isJsonObject()) {
|
||||
return;
|
||||
}
|
||||
for (var itr = ((JsonObject) position).entrySet().iterator();
|
||||
itr.hasNext();) {
|
||||
var entry = itr.next();
|
||||
if (Arrays.asList(properties).contains(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,35 @@
|
|||
data-jgwc-on-unload="JGConsole.jgwc.unmountVueApps">
|
||||
<form :id="formId" ref="formDom" onsubmit="return false;">
|
||||
<section>
|
||||
<span>{{ localize("Select VM") }}</span>
|
||||
<p>
|
||||
<fieldset>
|
||||
<legend>{{ localize("Select VM or pool") }}</legend>
|
||||
<ul>
|
||||
<li>
|
||||
<label>
|
||||
<input v-model="resource" type="radio" value="vm"
|
||||
<#if vmNames?size == 0>:disabled="true"</#if>label>
|
||||
<span>{{ localize("VM") }}</span>
|
||||
<select v-model="vmNameInput">
|
||||
<select v-model="vmNameInput" :disabled="resource !== 'vm'">
|
||||
<#list vmNames as name>
|
||||
<option value="${name}">${name}</option>
|
||||
</#list>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
<input v-model="resource" type="radio" value="pool"
|
||||
<#if poolNames?size == 0>:disabled="true"</#if>>
|
||||
<span>{{ localize("Pool") }}</span>
|
||||
<select v-model="poolNameInput" :disabled="resource !== 'pool'">
|
||||
<#list poolNames as name>
|
||||
<option value="${name}">${name}</option>
|
||||
</#list>
|
||||
</select>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,3 +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.
|
||||
poolEmptyNotification = No VM available. Please consult your administrator.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
conletName = VM-Zugriff
|
||||
|
||||
okayLabel = Anwenden und Schließen
|
||||
Select\ VM = VM auswählen
|
||||
Select\ VM\ or\ pool = VM oder Pool auswählen
|
||||
|
||||
Start\ VM = VM starten
|
||||
Stop\ VM = VM anhalten
|
||||
|
|
@ -11,3 +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.
|
||||
poolEmptyNotification = Keine VM verfügbar. Wenden Sie sich bitte an den \
|
||||
Systemadministrator.
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -50,17 +50,24 @@ import org.bouncycastle.util.Objects;
|
|||
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
|
||||
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
||||
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;
|
||||
import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
|
||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||
import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
|
||||
import org.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.Components;
|
||||
import org.jgrapes.core.Event;
|
||||
import org.jgrapes.core.EventPipeline;
|
||||
import org.jgrapes.core.Manager;
|
||||
import org.jgrapes.core.annotation.Handler;
|
||||
import org.jgrapes.core.events.Start;
|
||||
import org.jgrapes.http.Session;
|
||||
import org.jgrapes.util.events.ConfigurationUpdate;
|
||||
import org.jgrapes.util.events.KeyValueStoreQuery;
|
||||
|
|
@ -79,6 +86,7 @@ import org.jgrapes.webconsole.base.events.ConsoleConfigured;
|
|||
import org.jgrapes.webconsole.base.events.ConsolePrepared;
|
||||
import org.jgrapes.webconsole.base.events.ConsoleReady;
|
||||
import org.jgrapes.webconsole.base.events.DeleteConlet;
|
||||
import org.jgrapes.webconsole.base.events.DisplayNotification;
|
||||
import org.jgrapes.webconsole.base.events.NotifyConletModel;
|
||||
import org.jgrapes.webconsole.base.events.NotifyConletView;
|
||||
import org.jgrapes.webconsole.base.events.OpenModalDialog;
|
||||
|
|
@ -106,10 +114,12 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
|||
*
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
|
||||
"PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods" })
|
||||
public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
||||
"PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods",
|
||||
"PMD.CyclomaticComplexity" })
|
||||
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
|
||||
|
|
@ -118,8 +128,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
RenderMode.Preview, RenderMode.Edit);
|
||||
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
||||
RenderMode.Preview, RenderMode.StickyPreview);
|
||||
private final ChannelTracker<String, VmChannel,
|
||||
VmDefinition> channelTracker = new ChannelTracker<>();
|
||||
private EventPipeline appPipeline;
|
||||
private static ObjectMapper objectMapper
|
||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||
|
|
@ -144,6 +153,16 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
super(componentChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* On start.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
@Handler
|
||||
public void onStart(Start event) {
|
||||
appPipeline = event.processedBy().get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the component.
|
||||
*
|
||||
|
|
@ -247,36 +266,74 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
* @throws InterruptedException the interrupted exception
|
||||
*/
|
||||
@Handler
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
public void onConsoleConfigured(ConsoleConfigured event,
|
||||
ConsoleConnection connection) throws InterruptedException,
|
||||
IOException {
|
||||
@SuppressWarnings("unchecked")
|
||||
final var rendered = (Set<String>) connection.session().get(RENDERED);
|
||||
final var rendered
|
||||
= (Set<ResourceModel>) connection.session().get(RENDERED);
|
||||
connection.session().remove(RENDERED);
|
||||
if (!syncPreviews(connection.session())) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean foundMissing = false;
|
||||
for (var vmName : accessibleVms(connection)) {
|
||||
if (rendered.contains(vmName)) {
|
||||
continue;
|
||||
addMissingConlets(event, connection, rendered);
|
||||
}
|
||||
if (!foundMissing) {
|
||||
|
||||
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
|
||||
"PMD.AvoidDuplicateLiterals" })
|
||||
private void addMissingConlets(ConsoleConfigured event,
|
||||
ConsoleConnection connection, final Set<ResourceModel> rendered)
|
||||
throws InterruptedException {
|
||||
var session = connection.session();
|
||||
|
||||
// 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())
|
||||
.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);
|
||||
foundMissing = true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -300,12 +357,16 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Optional<ViewerModel> createNewState(AddConletRequest event,
|
||||
protected Optional<ResourceModel> createNewState(AddConletRequest event,
|
||||
ConsoleConnection connection, String conletId) throws Exception {
|
||||
var model = new ViewerModel(conletId);
|
||||
model.vmName = (String) event.properties().get(VM_NAME_PROPERTY);
|
||||
if (model.vmName != null) {
|
||||
model.setGenerated(true);
|
||||
var model = new ResourceModel(conletId);
|
||||
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(
|
||||
|
|
@ -314,9 +375,9 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Optional<ViewerModel> createStateRepresentation(Event<?> event,
|
||||
protected Optional<ResourceModel> createStateRepresentation(Event<?> event,
|
||||
ConsoleConnection connection, String conletId) throws Exception {
|
||||
var model = new ViewerModel(conletId);
|
||||
var model = new ResourceModel(conletId);
|
||||
String jsonState = objectMapper.writeValueAsString(model);
|
||||
connection.respond(new KeyValueStoreUpdate().update(
|
||||
storagePath(connection.session(), model.getConletId()), jsonState));
|
||||
|
|
@ -325,7 +386,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
|
||||
@Override
|
||||
@SuppressWarnings("PMD.EmptyCatchBlock")
|
||||
protected Optional<ViewerModel> recreateState(Event<?> event,
|
||||
protected Optional<ResourceModel> recreateState(Event<?> event,
|
||||
ConsoleConnection channel, String conletId) throws Exception {
|
||||
KeyValueStoreQuery query = new KeyValueStoreQuery(
|
||||
storagePath(channel.session(), conletId), channel);
|
||||
|
|
@ -334,8 +395,8 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
if (!query.results().isEmpty()) {
|
||||
var json = query.results().get(0).values().stream().findFirst()
|
||||
.get();
|
||||
ViewerModel model
|
||||
= objectMapper.readValue(json, ViewerModel.class);
|
||||
ResourceModel model
|
||||
= objectMapper.readValue(json, ResourceModel.class);
|
||||
return Optional.of(model);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
|
|
@ -347,34 +408,85 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "unchecked" })
|
||||
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops" })
|
||||
protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
|
||||
ConsoleConnection channel, String conletId, ViewerModel model)
|
||||
ConsoleConnection channel, String conletId, ResourceModel model)
|
||||
throws Exception {
|
||||
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||
return renderPreview(event, channel, conletId, model);
|
||||
}
|
||||
|
||||
// Render edit
|
||||
ResourceBundle resourceBundle = resourceBundle(channel.locale());
|
||||
Set<RenderMode> renderedAs = EnumSet.noneOf(RenderMode.class);
|
||||
if (event.renderAs().contains(RenderMode.Preview)) {
|
||||
if (event.renderAs().contains(RenderMode.Edit)) {
|
||||
var session = channel.session();
|
||||
var vmNames = 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()).sorted()
|
||||
.toList();
|
||||
var poolNames = 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).sorted().toList();
|
||||
Template tpl
|
||||
= freemarkerConfig().getTemplate("VmAccess-edit.ftl.html");
|
||||
var fmModel = fmModel(event, channel, conletId, model);
|
||||
fmModel.put("vmNames", vmNames);
|
||||
fmModel.put("poolNames", poolNames);
|
||||
channel.respond(new OpenModalDialog(type(), conletId,
|
||||
processTemplate(event, tpl, fmModel))
|
||||
.addOption("cancelable", true)
|
||||
.addOption("okayLabel",
|
||||
resourceBundle.getString("okayLabel")));
|
||||
}
|
||||
return renderedAs;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<RenderMode> renderPreview(RenderConletRequestBase<?> event,
|
||||
ConsoleConnection channel, String conletId, ResourceModel model)
|
||||
throws TemplateNotFoundException, MalformedTemplateNameException,
|
||||
ParseException, IOException, InterruptedException {
|
||||
channel.associated(PENDING, Event.class)
|
||||
.ifPresent(e -> {
|
||||
e.resumeHandling();
|
||||
channel.setAssociated(PENDING, null);
|
||||
});
|
||||
|
||||
// Remove conlet if definition has been removed
|
||||
if (model.vmName() != null
|
||||
&& !channelTracker.associated(model.vmName()).isPresent()) {
|
||||
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
|
||||
vmDef = getVmData(model, channel).map(VmData::definition)
|
||||
.orElse(null);
|
||||
if (vmDef == null) {
|
||||
channel.respond(
|
||||
new DeleteConlet(conletId, Collections.emptySet()));
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
// Don't render if user has not at least one permission
|
||||
if (model.vmName() != null
|
||||
&& channelTracker.associated(model.vmName())
|
||||
.map(d -> permissions(d, channel.session()).isEmpty())
|
||||
.orElse(true)) {
|
||||
if (model.mode() == ResourceModel.Mode.POOL && model.name() != null) {
|
||||
// Remove conlet if pool definition has been removed
|
||||
// or user has not at least one permission
|
||||
VmPool pool = appPipeline
|
||||
.fire(new GetPools().withName(model.name())).get()
|
||||
.stream().findFirst().orElse(null);
|
||||
if (pool == null
|
||||
|| 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
|
||||
Template tpl
|
||||
|
|
@ -387,31 +499,32 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
.setSupportedModes(syncPreviews(channel.session())
|
||||
? MODES_FOR_GENERATED
|
||||
: MODES));
|
||||
renderedAs.add(RenderMode.Preview);
|
||||
if (!Strings.isNullOrEmpty(model.vmName())) {
|
||||
if (!Strings.isNullOrEmpty(model.name())) {
|
||||
Optional.ofNullable(channel.session().get(RENDERED))
|
||||
.ifPresent(s -> ((Set<String>) s).add(model.vmName()));
|
||||
updateConfig(channel, model);
|
||||
.ifPresent(s -> ((Set<ResourceModel>) s).add(model));
|
||||
updatePreview(channel, model, vmDef);
|
||||
}
|
||||
}
|
||||
if (event.renderAs().contains(RenderMode.Edit)) {
|
||||
Template tpl = freemarkerConfig()
|
||||
.getTemplate("VmAccess-edit.ftl.html");
|
||||
var fmModel = fmModel(event, channel, conletId, model);
|
||||
fmModel.put("vmNames", accessibleVms(channel));
|
||||
channel.respond(new OpenModalDialog(type(), conletId,
|
||||
processTemplate(event, tpl, fmModel))
|
||||
.addOption("cancelable", true)
|
||||
.addOption("okayLabel",
|
||||
resourceBundle.getString("okayLabel")));
|
||||
}
|
||||
return renderedAs;
|
||||
return EnumSet.of(RenderMode.Preview);
|
||||
}
|
||||
|
||||
private List<String> accessibleVms(ConsoleConnection channel) {
|
||||
return channelTracker.associated().stream()
|
||||
.filter(d -> !permissions(d, channel.session()).isEmpty())
|
||||
.map(d -> d.getMetadata().getName()).sorted().toList();
|
||||
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) {
|
||||
|
|
@ -422,39 +535,83 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
return vmDef.permissionsFor(user, roles);
|
||||
}
|
||||
|
||||
private void updateConfig(ConsoleConnection channel, ViewerModel model) {
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "updateConfig", model.vmName()));
|
||||
updateVmDef(channel, model);
|
||||
private Set<Permission> permissions(VmPool pool, Session session) {
|
||||
var user = WebConsoleUtils.userFromSession(session)
|
||||
.map(ConsoleUser::getName).orElse(null);
|
||||
var roles = WebConsoleUtils.rolesFromSession(session)
|
||||
.stream().map(ConsoleRole::getName).toList();
|
||||
return pool.permissionsFor(user, roles);
|
||||
}
|
||||
|
||||
private void updateVmDef(ConsoleConnection channel, ViewerModel model) {
|
||||
if (Strings.isNullOrEmpty(model.vmName())) {
|
||||
return;
|
||||
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);
|
||||
}
|
||||
channelTracker.value(model.vmName()).ifPresent(item -> {
|
||||
if (pool != null) {
|
||||
result.addAll(pool.permissionsFor(user, roles));
|
||||
}
|
||||
}
|
||||
if (vmDef == null) {
|
||||
vmDef = appPipeline.fire(new GetVms().assignedFrom(model.name())
|
||||
.assignedTo(user)).get().stream().map(VmData::definition)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
if (vmDef != null) {
|
||||
result.addAll(vmDef.permissionsFor(user, roles));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void updatePreview(ConsoleConnection channel, ResourceModel model,
|
||||
VmDefinition vmDef) throws InterruptedException {
|
||||
updateConfig(channel, model, vmDef);
|
||||
updateVmDef(channel, model, vmDef);
|
||||
}
|
||||
|
||||
private void updateConfig(ConsoleConnection channel, ResourceModel model,
|
||||
VmDefinition vmDef) throws InterruptedException {
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "updateConfig", model.mode(), model.name(),
|
||||
permissions(model, channel.session(), null, vmDef).stream()
|
||||
.map(VmDefinition.Permission::toString).toList()));
|
||||
}
|
||||
|
||||
private void updateVmDef(ConsoleConnection channel, ResourceModel model,
|
||||
VmDefinition vmDef) throws InterruptedException {
|
||||
Map<String, Object> data = null;
|
||||
if (vmDef == null) {
|
||||
model.setAssignedVm(null);
|
||||
} else {
|
||||
model.setAssignedVm(vmDef.name());
|
||||
try {
|
||||
var vmDef = item.associated();
|
||||
var data = Map.of("metadata",
|
||||
data = Map.of("metadata",
|
||||
Map.of("namespace", vmDef.namespace(),
|
||||
"name", vmDef.name()),
|
||||
"spec", vmDef.spec(),
|
||||
"status", vmDef.getStatus(),
|
||||
"userPermissions",
|
||||
permissions(vmDef, channel.session()).stream()
|
||||
.map(Permission::toString).toList());
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "updateVmDefinition", data));
|
||||
"status", vmDef.getStatus());
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.log(Level.SEVERE, e,
|
||||
() -> "Failed to serialize VM definition");
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
channel.respond(new NotifyConletView(type(),
|
||||
model.getConletId(), "updateVmDefinition", data));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doConletDeleted(ConletDeleted event,
|
||||
ConsoleConnection channel, String conletId, ViewerModel conletState)
|
||||
ConsoleConnection channel, String conletId,
|
||||
ResourceModel conletState)
|
||||
throws Exception {
|
||||
if (event.renderModes().isEmpty()) {
|
||||
channel.respond(new KeyValueStoreUpdate().delete(
|
||||
|
|
@ -463,95 +620,181 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Track the VM definitions.
|
||||
* Track the VM definitions and update conlets.
|
||||
*
|
||||
* @param event the event
|
||||
* @param channel the channel
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
@Handler(namedChannels = "manager")
|
||||
@SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity",
|
||||
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals",
|
||||
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||
throws IOException {
|
||||
throws IOException, InterruptedException {
|
||||
var vmDef = event.vmDefinition();
|
||||
var vmName = vmDef.name();
|
||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||
channelTracker.remove(vmName);
|
||||
} else {
|
||||
channelTracker.put(vmName, channel, vmDef);
|
||||
}
|
||||
|
||||
// Update known conlets
|
||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||
var connection = entry.getKey();
|
||||
for (var conletId : entry.getValue()) {
|
||||
var model = stateFromSession(connection.session(), conletId);
|
||||
if (model.isEmpty()
|
||||
|| !Objects.areEqual(model.get().vmName(), vmName)) {
|
||||
|| Strings.isNullOrEmpty(model.get().name())) {
|
||||
continue;
|
||||
}
|
||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||
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()));
|
||||
} else {
|
||||
updateVmDef(connection, model.get());
|
||||
continue;
|
||||
}
|
||||
} 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()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
// Full update because permissions may have changed
|
||||
updatePreview(connection, model.get(), vmDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "PMD.AvoidDecimalLiteralsInBigDecimalConstructor",
|
||||
"PMD.ConfusingArgumentToVarargsMethod", "PMD.NcssCount",
|
||||
/**
|
||||
* On vm pool changed.
|
||||
*
|
||||
* @param event the event
|
||||
* @param channel the channel
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
@Handler(namedChannels = "manager")
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
public void onVmPoolChanged(VmPoolChanged event)
|
||||
throws InterruptedException {
|
||||
var poolName = event.vmPool().name();
|
||||
// Update known conlets
|
||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||
var connection = entry.getKey();
|
||||
for (var conletId : entry.getValue()) {
|
||||
var model = stateFromSession(connection.session(), conletId);
|
||||
if (model.isEmpty()
|
||||
|| model.get().mode() != ResourceModel.Mode.POOL
|
||||
|| !Objects.areEqual(model.get().name(), poolName)) {
|
||||
continue;
|
||||
}
|
||||
if (event.deleted()
|
||||
|| permissions(event.vmPool(), connection.session())
|
||||
.isEmpty()) {
|
||||
connection.respond(
|
||||
new DeleteConlet(conletId, Collections.emptySet()));
|
||||
continue;
|
||||
}
|
||||
updateConfig(connection, model.get(), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "PMD.NcssCount", "PMD.CognitiveComplexity",
|
||||
"PMD.AvoidLiteralsInIfCondition" })
|
||||
@Override
|
||||
protected void doUpdateConletState(NotifyConletModel event,
|
||||
ConsoleConnection channel, ViewerModel model)
|
||||
throws Exception {
|
||||
ConsoleConnection channel, ResourceModel model) throws Exception {
|
||||
event.stop();
|
||||
if ("selectedVm".equals(event.method())) {
|
||||
selectVm(event, channel, model);
|
||||
if ("selectedResource".equals(event.method())) {
|
||||
selectResource(event, channel, model);
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<VmData> vmData = getVmData(model, channel);
|
||||
if (vmData.isEmpty()) {
|
||||
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()) {
|
||||
ResourceBundle resourceBundle
|
||||
= resourceBundle(channel.locale());
|
||||
channel.respond(new DisplayNotification(
|
||||
resourceBundle.getString("poolEmptyNotification"),
|
||||
Map.of("autoClose", 10_000, "type", "Error")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle command for selected VM
|
||||
var both = Optional.ofNullable(model.vmName())
|
||||
.flatMap(vm -> channelTracker.value(vm));
|
||||
if (both.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var vmChannel = both.get().channel();
|
||||
var vmDef = both.get().associated();
|
||||
var vmChannel = vmData.get().channel();
|
||||
var vmDef = vmData.get().definition();
|
||||
var vmName = vmDef.metadata().getName();
|
||||
var perms = permissions(vmDef, channel.session());
|
||||
var perms = permissions(model, channel.session(), null, vmDef);
|
||||
var resourceBundle = resourceBundle(channel.locale());
|
||||
switch (event.method()) {
|
||||
case "start":
|
||||
if (perms.contains(Permission.START)) {
|
||||
if (perms.contains(VmDefinition.Permission.START)) {
|
||||
fire(new ModifyVm(vmName, "state", "Running", vmChannel));
|
||||
}
|
||||
break;
|
||||
case "stop":
|
||||
if (perms.contains(Permission.STOP)) {
|
||||
if (perms.contains(VmDefinition.Permission.STOP)) {
|
||||
fire(new ModifyVm(vmName, "state", "Stopped", vmChannel));
|
||||
}
|
||||
break;
|
||||
case "reset":
|
||||
if (perms.contains(Permission.RESET)) {
|
||||
if (perms.contains(VmDefinition.Permission.RESET)) {
|
||||
confirmReset(event, channel, model, resourceBundle);
|
||||
}
|
||||
break;
|
||||
case "resetConfirmed":
|
||||
if (perms.contains(Permission.RESET)) {
|
||||
if (perms.contains(VmDefinition.Permission.RESET)) {
|
||||
fire(new ResetVm(vmName), vmChannel);
|
||||
}
|
||||
break;
|
||||
case "openConsole":
|
||||
if (perms.contains(Permission.ACCESS_CONSOLE)) {
|
||||
var user = WebConsoleUtils.userFromSession(channel.session())
|
||||
.map(ConsoleUser::getName).orElse("");
|
||||
if (vmDef.conditionStatus("ConsoleConnected").orElse(false)
|
||||
&& vmDef.consoleUser().map(cu -> !cu.equals(user)
|
||||
&& !perms.contains(VmDefinition.Permission.TAKE_CONSOLE))
|
||||
.orElse(false)) {
|
||||
channel.respond(new DisplayNotification(
|
||||
resourceBundle.getString("consoleTakenNotification"),
|
||||
Map.of("autoClose", 5_000, "type", "Warning")));
|
||||
return;
|
||||
}
|
||||
if (perms.contains(VmDefinition.Permission.ACCESS_CONSOLE)
|
||||
|| perms.contains(VmDefinition.Permission.TAKE_CONSOLE)) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -561,30 +804,41 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
}
|
||||
}
|
||||
|
||||
private void selectVm(NotifyConletModel event, ConsoleConnection channel,
|
||||
ViewerModel model) throws JsonProcessingException {
|
||||
model.setVmName(event.param(0));
|
||||
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||
"PMD.UseLocaleWithCaseConversions" })
|
||||
private void selectResource(NotifyConletModel event,
|
||||
ConsoleConnection channel, ResourceModel model)
|
||||
throws JsonProcessingException, InterruptedException {
|
||||
try {
|
||||
model.setMode(ResourceModel.Mode
|
||||
.valueOf(event.<String> param(0).toUpperCase()));
|
||||
model.setName(event.param(1));
|
||||
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,
|
||||
ViewerModel model, String password) {
|
||||
var vmDef = channelTracker.associated(vmName).orElse(null);
|
||||
private void openConsole(VmDefinition vmDef, ConsoleConnection connection,
|
||||
ResourceModel model, String password) {
|
||||
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)
|
||||
|
|
@ -642,7 +896,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
}
|
||||
|
||||
private void confirmReset(NotifyConletModel event,
|
||||
ConsoleConnection channel, ViewerModel model,
|
||||
ConsoleConnection channel, ResourceModel model,
|
||||
ResourceBundle resourceBundle) throws TemplateNotFoundException,
|
||||
MalformedTemplateNameException, ParseException, IOException {
|
||||
Template tpl = freemarkerConfig()
|
||||
|
|
@ -662,59 +916,119 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ViewerModel> {
|
|||
}
|
||||
|
||||
/**
|
||||
* The Class VmsModel.
|
||||
* The Class AccessModel.
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataClass")
|
||||
public static class ViewerModel extends ConletBaseModel {
|
||||
|
||||
private String vmName;
|
||||
private boolean generated;
|
||||
public static class ResourceModel extends ConletBaseModel {
|
||||
|
||||
/**
|
||||
* Instantiates a new vms model.
|
||||
* The Enum ResourceType.
|
||||
*/
|
||||
@SuppressWarnings("PMD.ShortVariable")
|
||||
public enum Mode {
|
||||
VM, POOL
|
||||
}
|
||||
|
||||
private Mode mode;
|
||||
private String name;
|
||||
private String assignedVm;
|
||||
|
||||
/**
|
||||
* Instantiates a new resource model.
|
||||
*
|
||||
* @param conletId the conlet id
|
||||
*/
|
||||
public ViewerModel(@JsonProperty("conletId") String conletId) {
|
||||
public ResourceModel(@JsonProperty("conletId") String conletId) {
|
||||
super(conletId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vm name.
|
||||
* Returns the mode.
|
||||
*
|
||||
* @return the vmName
|
||||
* @return the resourceType
|
||||
*/
|
||||
@JsonGetter("vmName")
|
||||
public String vmName() {
|
||||
return vmName;
|
||||
@JsonGetter("mode")
|
||||
public Mode mode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vm name.
|
||||
* Sets the mode.
|
||||
*
|
||||
* @param vmName the vmName to set
|
||||
* @param mode the resource mode to set
|
||||
*/
|
||||
public void setVmName(String vmName) {
|
||||
this.vmName = vmName;
|
||||
public void setMode(Mode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if is generated.
|
||||
* Gets the resource name.
|
||||
*
|
||||
* @return the generated
|
||||
* @return the string
|
||||
*/
|
||||
public boolean isGenerated() {
|
||||
return generated;
|
||||
@JsonGetter("name")
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the generated.
|
||||
* Sets the name.
|
||||
*
|
||||
* @param generated the generated to set
|
||||
* @param name the resource name to set
|
||||
*/
|
||||
public void setGenerated(boolean generated) {
|
||||
this.generated = generated;
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the assigned vm.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
@JsonGetter("assignedVm")
|
||||
public String assignedVm() {
|
||||
return assignedVm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the assigned vm.
|
||||
*
|
||||
* @param name the assigned vm
|
||||
*/
|
||||
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(mode, name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(obj)) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ResourceModel other = (ResourceModel) obj;
|
||||
return mode == other.mode
|
||||
&& java.util.Objects.equals(name, other.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder(50);
|
||||
builder.append("AccessModel [mode=").append(mode)
|
||||
.append(", name=").append(name).append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ interface Api {
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
vmName: string;
|
||||
vmDefinition: any;
|
||||
poolName: string | null;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const localize = (key: string) => {
|
||||
|
|
@ -62,24 +64,33 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
|
||||
const previewApi: Api = reactive({
|
||||
vmName: "",
|
||||
vmDefinition: {}
|
||||
vmDefinition: {},
|
||||
poolName: null,
|
||||
permissions: []
|
||||
});
|
||||
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.permissions.includes('start')
|
||||
|| previewApi.poolName !== null && !previewApi.vmDefinition.name);
|
||||
const stoppable = computed(() => previewApi.vmDefinition.spec &&
|
||||
previewApi.vmDefinition.spec.vm.state !== 'Stopped'
|
||||
&& previewApi.vmDefinition.running);
|
||||
const running = computed(() => previewApi.vmDefinition.running);
|
||||
const inUse = computed(() => previewApi.vmDefinition.usedBy != '');
|
||||
const permissions = computed(() => previewApi.vmDefinition.spec
|
||||
? previewApi.vmDefinition.userPermissions : []);
|
||||
const permissions = computed(() => previewApi.permissions);
|
||||
|
||||
watch(() => previewApi.vmName, (name: string) => {
|
||||
if (name !== "") {
|
||||
JGConsole.instance.updateConletTitle(conletId, name);
|
||||
}
|
||||
watch(previewApi, (api: Api) => {
|
||||
JGConsole.instance.updateConletTitle(conletId,
|
||||
api.poolName || api.vmDefinition.name || "");
|
||||
});
|
||||
|
||||
provideApi(previewDom, previewApi);
|
||||
|
|
@ -88,16 +99,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')"
|
||||
|
|
@ -107,9 +118,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"
|
||||
|
|
@ -127,9 +141,6 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
});
|
||||
|
|
@ -139,24 +150,34 @@ window.orgJDrupesVmOperatorVmAccess.initPreview = (previewDom: HTMLElement,
|
|||
};
|
||||
|
||||
JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
||||
"updateConfig", function(conletId: string, vmName: string) {
|
||||
"updateConfig",
|
||||
function(conletId: string, type: string, resource: string,
|
||||
permissions: []) {
|
||||
const conlet = JGConsole.findConletPreview(conletId);
|
||||
if (!conlet) {
|
||||
return;
|
||||
}
|
||||
const api = getApi<Api>(conlet.element().querySelector(
|
||||
":scope .jdrupes-vmoperator-vmaccess-preview"))!;
|
||||
api.vmName = vmName;
|
||||
if (type === "VM") {
|
||||
api.vmName = resource;
|
||||
api.poolName = "";
|
||||
} else {
|
||||
api.poolName = resource;
|
||||
api.vmName = "";
|
||||
}
|
||||
api.permissions = permissions;
|
||||
});
|
||||
|
||||
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"))!;
|
||||
if (vmDefinition) {
|
||||
// Add some short-cuts for rendering
|
||||
vmDefinition.name = vmDefinition.metadata.name;
|
||||
vmDefinition.currentCpus = vmDefinition.status.cpus;
|
||||
|
|
@ -170,6 +191,9 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmaccess.VmAccess",
|
|||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vmDefinition = {};
|
||||
}
|
||||
api.vmDefinition = vmDefinition;
|
||||
});
|
||||
|
||||
|
|
@ -203,19 +227,36 @@ window.orgJDrupesVmOperatorVmAccess.initEdit = (dialogDom: HTMLElement,
|
|||
l10nBundles, JGWC.lang()!, key);
|
||||
};
|
||||
|
||||
const resource = ref<string>("vm");
|
||||
const vmNameInput = ref<string>("");
|
||||
const poolNameInput = ref<string>("");
|
||||
|
||||
watch(resource, (resource: string) => {
|
||||
if (resource === "vm") {
|
||||
poolNameInput.value = "";
|
||||
}
|
||||
if (resource === "pool")
|
||||
vmNameInput.value = "";
|
||||
});
|
||||
|
||||
const conletId = (<HTMLElement>dialogDom.closest(
|
||||
"[data-conlet-id]")!).dataset["conletId"]!;
|
||||
const conlet = JGConsole.findConletPreview(conletId);
|
||||
if (conlet) {
|
||||
const api = getApi<Api>(conlet.element().querySelector(
|
||||
":scope .jdrupes-vmoperator-vmaccess-preview"))!;
|
||||
if (api.poolName) {
|
||||
resource.value = "pool";
|
||||
}
|
||||
vmNameInput.value = api.vmName;
|
||||
poolNameInput.value = api.poolName;
|
||||
}
|
||||
|
||||
provideApi(dialogDom, vmNameInput);
|
||||
provideApi(dialogDom, { resource: () => resource.value,
|
||||
name: () => resource.value === "vm"
|
||||
? vmNameInput.value : poolNameInput.value });
|
||||
|
||||
return { formId, localize, vmNameInput };
|
||||
return { formId, localize, resource, vmNameInput, poolNameInput };
|
||||
}
|
||||
});
|
||||
app.use(JgwcPlugin);
|
||||
|
|
@ -229,8 +270,9 @@ window.orgJDrupesVmOperatorVmAccess.applyEdit =
|
|||
}
|
||||
const conletId = (<HTMLElement>dialogDom.closest("[data-conlet-id]")!)
|
||||
.dataset["conletId"]!;
|
||||
const vmName = getApi<ref<string>>(dialogDom!)!.value;
|
||||
JGConsole.notifyConletModel(conletId, "selectedVm", vmName);
|
||||
const editApi = getApi<ref<string>>(dialogDom!)!;
|
||||
JGConsole.notifyConletModel(conletId, "selectedResource", editApi.resource(),
|
||||
editApi.name());
|
||||
}
|
||||
|
||||
window.orgJDrupesVmOperatorVmAccess.confirmReset =
|
||||
|
|
|
|||
|
|
@ -49,7 +49,12 @@
|
|||
|
||||
.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-preview {
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 3em;
|
||||
padding: 0.25rem;
|
||||
|
||||
|
|
@ -77,6 +82,11 @@
|
|||
}
|
||||
|
||||
.jdrupes-vmoperator-vmaccess.jdrupes-vmoperator-vmaccess-edit {
|
||||
|
||||
fieldset ul li {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 15em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
:aria-expanded="(entry.name in detailsByName) ? 'true' : 'false'">
|
||||
<td v-for="key in controller.keys"
|
||||
v-bind:class="'column-' + key"
|
||||
v-bind:title="key == 'name' ? entry['name']: false"
|
||||
v-bind:title="key == 'name' ? entry['name'] : null"
|
||||
v-bind:rowspan="(key == 'name') && $aash.isDisclosed(scopedId(rowIndex)) ? 2 : false">
|
||||
<aash-disclosure-button v-if="key === 'name'" :type="'div'"
|
||||
:id-ref="scopedId(rowIndex)">
|
||||
|
|
@ -48,6 +48,11 @@
|
|||
>{{ shortDateTime(entry[key].toString()) }}</span>
|
||||
<span v-else-if="key === 'currentRam'"
|
||||
>{{ formatMemory(entry[key]) }}</span>
|
||||
<span v-else-if="key === 'usedBy'"
|
||||
:class="{ 'console-conection-closed' : !entry.usedFrom }"
|
||||
:title="entry.usedFrom ? localize('usedFrom')
|
||||
+ ' ' + entry.usedFrom : localize('notInUse')"
|
||||
v-html="controller.breakBeforeDots(entry[key])"></span>
|
||||
<span v-else
|
||||
v-html="controller.breakBeforeDots(entry[key])"></span>
|
||||
</td>
|
||||
|
|
@ -103,6 +108,12 @@
|
|||
><span>{{ cic.error }}</span></form></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="table--basic table--basic--autoStriped">
|
||||
<tr>
|
||||
<td>{{ localize("usedFrom") }}</td>
|
||||
<td>{{ entry.usedFrom }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ conletName = VM Management
|
|||
|
||||
VMsSummary = VMs (running/total)
|
||||
|
||||
since = Since
|
||||
assignedTo = Assigned to
|
||||
currentCpus = Current CPUs
|
||||
currentRam = Current RAM
|
||||
maximumCpus = Maximum CPUs
|
||||
maximumRam = Maximum RAM
|
||||
notInUse = Currently closed
|
||||
nodeName = Node
|
||||
requestedCpus = Requested CPUs
|
||||
requestedRam = Requested RAM
|
||||
running = Running
|
||||
since = Since
|
||||
usedBy = Used by
|
||||
usedFrom = Used from
|
||||
vmActions = Actions
|
||||
|
|
|
|||
|
|
@ -6,15 +6,17 @@ Period = Zeitraum
|
|||
Last\ hour = Letzte Stunde
|
||||
Last\ day = Letzter Tag
|
||||
|
||||
running = Gestartet
|
||||
since = Seit
|
||||
assignedTo = Zugewiesen an
|
||||
currentCpus = Aktuelle CPUs
|
||||
currentRam = Akuelles RAM
|
||||
maximumCpus = Maximale CPUs
|
||||
maximumRam = Maximales RAM
|
||||
nodeName = Knoten
|
||||
notInUse = Derzeit geschlossen
|
||||
requestedCpus = Angeforderte CPUs
|
||||
requestedRam = Angefordertes RAM
|
||||
running = Gestartet
|
||||
since = Seit
|
||||
usedBy = Benutzt durch
|
||||
usedFrom = Benutzt von
|
||||
vmActions = Aktionen
|
||||
|
|
|
|||
|
|
@ -29,9 +29,7 @@ import java.math.BigDecimal;
|
|||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
|
@ -328,14 +326,9 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
|
|||
.add(vmDef.<String> fromStatus("ram")
|
||||
.map(r -> Quantity.fromString(r).getNumber().toBigInteger())
|
||||
.orElse(BigInteger.ZERO));
|
||||
summary.runningVms
|
||||
+= vmDef.<List<Map<String, Object>>> fromStatus("conditions")
|
||||
.orElse(Collections.emptyList()).stream()
|
||||
.filter(cond -> DataPath.get(cond, "type")
|
||||
.map(t -> "Running".equals(t)).orElse(false)
|
||||
&& DataPath.get(cond, "status")
|
||||
.map(s -> "True".equals(s)).orElse(false))
|
||||
.count();
|
||||
if (vmDef.conditionStatus("Running").orElse(false)) {
|
||||
summary.runningVms += 1;
|
||||
}
|
||||
}
|
||||
cachedSummary = summary;
|
||||
return summary;
|
||||
|
|
|
|||
|
|
@ -75,17 +75,17 @@ window.orgJDrupesVmOperatorVmMgmt.initPreview = (previewDom: HTMLElement,
|
|||
chart = new CpuRamChart(canvas, chartData);
|
||||
})
|
||||
|
||||
watch(chartDateUpdate, (_) => {
|
||||
watch(chartDateUpdate, (_: never) => {
|
||||
chart?.update();
|
||||
})
|
||||
|
||||
watch(JGWC.langRef(), (_) => {
|
||||
watch(JGWC.langRef(), (_: never) => {
|
||||
chart?.localizeChart();
|
||||
})
|
||||
|
||||
const period: Ref<string> = ref<string>("day");
|
||||
|
||||
watch(period, (_) => {
|
||||
watch(period, (_: never) => {
|
||||
const hours = (period.value === "day") ? 24 : 1;
|
||||
chart?.setPeriod(hours * 3600 * 1000);
|
||||
});
|
||||
|
|
@ -112,8 +112,8 @@ window.orgJDrupesVmOperatorVmMgmt.initView = (viewDom: HTMLElement,
|
|||
["currentCpus", "currentCpus"],
|
||||
["currentRam", "currentRam"],
|
||||
["nodeName", "nodeName"],
|
||||
["usedFrom", "usedFrom"],
|
||||
["usedBy", "usedBy"]
|
||||
["usedBy", "usedBy"],
|
||||
["assignedTo", "assignedTo"]
|
||||
], {
|
||||
sortKey: "name",
|
||||
sortOrder: "up"
|
||||
|
|
@ -183,6 +183,7 @@ JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmmgmt.VmMgmt",
|
|||
vmDefinition.currentRam = Number(vmDefinition.status.ram);
|
||||
vmDefinition.usedFrom = vmDefinition.status.consoleClient || "";
|
||||
vmDefinition.usedBy = vmDefinition.status.consoleUser || "";
|
||||
vmDefinition.assignedTo = vmDefinition.status.assignment?.user || "";
|
||||
for (const condition of vmDefinition.status.conditions) {
|
||||
if (condition.type === "Running") {
|
||||
vmDefinition.running = condition.status === "True";
|
||||
|
|
|
|||
|
|
@ -72,12 +72,18 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.console-conection-closed {
|
||||
color: var(--disabled);
|
||||
}
|
||||
}
|
||||
|
||||
td.details {
|
||||
padding-left: 1em;
|
||||
padding-left: 0;
|
||||
|
||||
table {
|
||||
display: inline;
|
||||
|
||||
td:nth-child(2) {
|
||||
min-width: 7em;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue